Merge pull request #7442 from HJianBo/fmt-gw
chore(gw): format gateway application
This commit is contained in:
commit
4a359124f5
|
|
@ -16,8 +16,12 @@
|
|||
-module(emqx_bpapi).
|
||||
|
||||
%% API:
|
||||
-export([start/0, announce/1, supported_version/1, supported_version/2,
|
||||
versions_file/1]).
|
||||
-export([
|
||||
start/0,
|
||||
announce/1,
|
||||
supported_version/1, supported_version/2,
|
||||
versions_file/1
|
||||
]).
|
||||
|
||||
-export_type([api/0, api_version/0, var_name/0, call/0, rpc/0, bpapi_meta/0]).
|
||||
|
||||
|
|
@ -31,10 +35,11 @@
|
|||
-type rpc() :: {_From :: call(), _To :: call()}.
|
||||
|
||||
-type bpapi_meta() ::
|
||||
#{ api := api()
|
||||
, version := api_version()
|
||||
, calls := [rpc()]
|
||||
, casts := [rpc()]
|
||||
#{
|
||||
api := api(),
|
||||
version := api_version(),
|
||||
calls := [rpc()],
|
||||
casts := [rpc()]
|
||||
}.
|
||||
|
||||
-include("emqx_bpapi.hrl").
|
||||
|
|
@ -49,10 +54,11 @@
|
|||
|
||||
-spec start() -> ok.
|
||||
start() ->
|
||||
ok = mria:create_table(?TAB, [ {type, set}
|
||||
, {storage, ram_copies}
|
||||
, {attributes, record_info(fields, ?TAB)}
|
||||
, {rlog_shard, ?COMMON_SHARD}
|
||||
ok = mria:create_table(?TAB, [
|
||||
{type, set},
|
||||
{storage, ram_copies},
|
||||
{attributes, record_info(fields, ?TAB)},
|
||||
{rlog_shard, ?COMMON_SHARD}
|
||||
]),
|
||||
ok = mria:wait_for_tables([?TAB]),
|
||||
announce(emqx).
|
||||
|
|
@ -89,21 +95,30 @@ announce_fun(Data) ->
|
|||
{node(), API}
|
||||
end),
|
||||
OldKeys = mnesia:select(?TAB, MS, write),
|
||||
_ = [mnesia:delete({?TAB, Key})
|
||||
|| Key <- OldKeys],
|
||||
_ = [
|
||||
mnesia:delete({?TAB, Key})
|
||||
|| Key <- OldKeys
|
||||
],
|
||||
%% Insert new records:
|
||||
_ = [mnesia:write(#?TAB{key = {node(), API}, version = Version})
|
||||
|| {API, Version} <- Data],
|
||||
_ = [
|
||||
mnesia:write(#?TAB{key = {node(), API}, version = Version})
|
||||
|| {API, Version} <- Data
|
||||
],
|
||||
%% Update maximum supported version:
|
||||
[update_minimum(API) || {API, _} <- Data],
|
||||
ok.
|
||||
|
||||
-spec update_minimum(api()) -> ok.
|
||||
update_minimum(API) ->
|
||||
MS = ets:fun2ms(fun(#?TAB{ key = {N, A}
|
||||
, version = Value
|
||||
}) when N =/= ?multicall,
|
||||
A =:= API ->
|
||||
MS = ets:fun2ms(fun(
|
||||
#?TAB{
|
||||
key = {N, A},
|
||||
version = Value
|
||||
}
|
||||
) when
|
||||
N =/= ?multicall,
|
||||
A =:= API
|
||||
->
|
||||
Value
|
||||
end),
|
||||
MinVersion = lists:min(mnesia:select(?TAB, MS)),
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@
|
|||
|
||||
-define(multicall, multicall).
|
||||
|
||||
-record(?TAB,
|
||||
{ key :: {node() | ?multicall, emqx_bpapi:api()}
|
||||
, version :: emqx_bpapi:api_version()
|
||||
}).
|
||||
-record(?TAB, {
|
||||
key :: {node() | ?multicall, emqx_bpapi:api()},
|
||||
version :: emqx_bpapi:api_version()
|
||||
}).
|
||||
|
||||
-endif.
|
||||
|
|
|
|||
|
|
@ -26,22 +26,24 @@
|
|||
|
||||
-type semantics() :: call | cast.
|
||||
|
||||
-record(s,
|
||||
{ api :: emqx_bpapi:api()
|
||||
, module :: module()
|
||||
, version :: emqx_bpapi:api_version() | undefined
|
||||
, targets = [] :: [{semantics(), emqx_bpapi:call(), emqx_bpapi:call()}]
|
||||
, errors = [] :: list()
|
||||
, file
|
||||
}).
|
||||
-record(s, {
|
||||
api :: emqx_bpapi:api(),
|
||||
module :: module(),
|
||||
version :: emqx_bpapi:api_version() | undefined,
|
||||
targets = [] :: [{semantics(), emqx_bpapi:call(), emqx_bpapi:call()}],
|
||||
errors = [] :: list(),
|
||||
file
|
||||
}).
|
||||
|
||||
format_error(invalid_name) ->
|
||||
"BPAPI module name should follow <API>_proto_v<number> pattern";
|
||||
format_error({invalid_fun, Name, Arity}) ->
|
||||
io_lib:format("malformed function ~p/~p. "
|
||||
io_lib:format(
|
||||
"malformed function ~p/~p. "
|
||||
"BPAPI functions should have exactly one clause "
|
||||
"and call (emqx_|e)rpc at the top level",
|
||||
[Name, Arity]).
|
||||
[Name, Arity]
|
||||
).
|
||||
|
||||
parse_transform(Forms, _Options) ->
|
||||
log("Original:~n~p", [Forms]),
|
||||
|
|
@ -79,20 +81,19 @@ finalize(Forms, S) ->
|
|||
{Attrs, Funcs} = lists:splitwith(fun is_attribute/1, Forms),
|
||||
AST = mk_meta_fun(S),
|
||||
log("Meta fun:~n~p", [AST]),
|
||||
Attrs ++ [mk_export()] ++ [AST|Funcs].
|
||||
Attrs ++ [mk_export()] ++ [AST | Funcs].
|
||||
|
||||
mk_meta_fun(#s{api = API, version = Vsn, targets = Targets}) ->
|
||||
Line = 0,
|
||||
Calls = [{From, To} || {call, From, To} <- Targets],
|
||||
Casts = [{From, To} || {cast, From, To} <- Targets],
|
||||
Ret = typerefl_quote:const(Line, #{ api => API
|
||||
, version => Vsn
|
||||
, calls => Calls
|
||||
, casts => Casts
|
||||
Ret = typerefl_quote:const(Line, #{
|
||||
api => API,
|
||||
version => Vsn,
|
||||
calls => Calls,
|
||||
casts => Casts
|
||||
}),
|
||||
{function, Line, ?META_FUN, _Arity = 0,
|
||||
[{clause, Line, _Args = [], _Guards = [],
|
||||
[Ret]}]}.
|
||||
{function, Line, ?META_FUN, _Arity = 0, [{clause, Line, _Args = [], _Guards = [], [Ret]}]}.
|
||||
|
||||
mk_export() ->
|
||||
{attribute, 0, export, [{?META_FUN, 0}]}.
|
||||
|
|
@ -122,14 +123,17 @@ analyze_exprs(Line, Name, Arity, Head, Exprs, S) ->
|
|||
|
||||
-spec extract_outer_args([erl_parse:abstract_form()]) -> [atom()].
|
||||
extract_outer_args(Abs) ->
|
||||
lists:map(fun({var, _, Var}) ->
|
||||
lists:map(
|
||||
fun
|
||||
({var, _, Var}) ->
|
||||
Var;
|
||||
({match, _, {var, _, Var}, _}) ->
|
||||
Var;
|
||||
({match, _, _, {var, _, Var}}) ->
|
||||
Var
|
||||
end,
|
||||
Abs).
|
||||
Abs
|
||||
).
|
||||
|
||||
-spec extract_target_call(_AST, [_AST]) -> {semantics(), emqx_bpapi:call()}.
|
||||
extract_target_call(RPCBackend, OuterArgs) ->
|
||||
|
|
@ -172,7 +176,7 @@ call_or_cast(multicall) -> call;
|
|||
call_or_cast(call) -> call.
|
||||
|
||||
list_to_args({cons, _, {var, _, A}, T}) ->
|
||||
[A|list_to_args(T)];
|
||||
[A | list_to_args(T)];
|
||||
list_to_args({nil, _}) ->
|
||||
[].
|
||||
|
||||
|
|
@ -180,10 +184,10 @@ invalid_fun(Line, Name, Arity, S) ->
|
|||
push_err(Line, {invalid_fun, Name, Arity}, S).
|
||||
|
||||
push_err(Line, Err, S = #s{errors = Errs}) ->
|
||||
S#s{errors = [{Line, Err}|Errs]}.
|
||||
S#s{errors = [{Line, Err} | Errs]}.
|
||||
|
||||
push_target(Target, S = #s{targets = Targets}) ->
|
||||
S#s{targets = [Target|Targets]}.
|
||||
S#s{targets = [Target | Targets]}.
|
||||
|
||||
-spec api_and_version(module()) -> {ok, emqx_bpapi:api(), emqx_bpapi:version()} | error.
|
||||
api_and_version(Module) ->
|
||||
|
|
|
|||
|
|
@ -44,10 +44,16 @@ post_config_update(_ConfPath, _Req, _NewConf, _OldConf, _AppEnvs) ->
|
|||
|
||||
update_log_handlers(NewHandlers) ->
|
||||
OldHandlers = application:get_env(kernel, logger, []),
|
||||
lists:foreach(fun({handler, HandlerId, _Mod, _Conf}) ->
|
||||
lists:foreach(
|
||||
fun({handler, HandlerId, _Mod, _Conf}) ->
|
||||
logger:remove_handler(HandlerId)
|
||||
end, OldHandlers -- NewHandlers),
|
||||
lists:foreach(fun({handler, HandlerId, Mod, Conf}) ->
|
||||
end,
|
||||
OldHandlers -- NewHandlers
|
||||
),
|
||||
lists:foreach(
|
||||
fun({handler, HandlerId, Mod, Conf}) ->
|
||||
logger:add_handler(HandlerId, Mod, Conf)
|
||||
end, NewHandlers -- OldHandlers),
|
||||
end,
|
||||
NewHandlers -- OldHandlers
|
||||
),
|
||||
application:set_env(kernel, logger, NewHandlers).
|
||||
|
|
|
|||
|
|
@ -21,17 +21,20 @@
|
|||
%% API
|
||||
-export([new_create_options/2, create/1, delete/1, consume/2]).
|
||||
|
||||
-type create_options() :: #{ module := ?MODULE
|
||||
, type := emqx_limiter_schema:limiter_type()
|
||||
, bucket := emqx_limiter_schema:bucket_name()
|
||||
}.
|
||||
-type create_options() :: #{
|
||||
module := ?MODULE,
|
||||
type := emqx_limiter_schema:limiter_type(),
|
||||
bucket := emqx_limiter_schema:bucket_name()
|
||||
}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec new_create_options(emqx_limiter_schema:limiter_type(),
|
||||
emqx_limiter_schema:bucket_name()) -> create_options().
|
||||
-spec new_create_options(
|
||||
emqx_limiter_schema:limiter_type(),
|
||||
emqx_limiter_schema:bucket_name()
|
||||
) -> create_options().
|
||||
new_create_options(Type, BucketName) ->
|
||||
#{module => ?MODULE, type => Type, bucket => BucketName}.
|
||||
|
||||
|
|
|
|||
|
|
@ -21,52 +21,71 @@
|
|||
%% @end
|
||||
|
||||
%% API
|
||||
-export([ make_token_bucket_limiter/2, make_ref_limiter/2, check/2
|
||||
, consume/2, set_retry/2, retry/1, make_infinity_limiter/0
|
||||
, make_future/1, available/1
|
||||
]).
|
||||
-export([
|
||||
make_token_bucket_limiter/2,
|
||||
make_ref_limiter/2,
|
||||
check/2,
|
||||
consume/2,
|
||||
set_retry/2,
|
||||
retry/1,
|
||||
make_infinity_limiter/0,
|
||||
make_future/1,
|
||||
available/1
|
||||
]).
|
||||
-export_type([token_bucket_limiter/0]).
|
||||
|
||||
%% a token bucket limiter with a limiter server's bucket reference
|
||||
-type token_bucket_limiter() :: #{ %% the number of tokens currently available
|
||||
tokens := non_neg_integer()
|
||||
, rate := decimal()
|
||||
, capacity := decimal()
|
||||
, lasttime := millisecond()
|
||||
|
||||
%% the number of tokens currently available
|
||||
-type token_bucket_limiter() :: #{
|
||||
tokens := non_neg_integer(),
|
||||
rate := decimal(),
|
||||
capacity := decimal(),
|
||||
lasttime := millisecond(),
|
||||
%% @see emqx_limiter_schema
|
||||
, max_retry_time := non_neg_integer()
|
||||
max_retry_time := non_neg_integer(),
|
||||
%% @see emqx_limiter_schema
|
||||
, failure_strategy := failure_strategy()
|
||||
, divisible := boolean() %% @see emqx_limiter_schema
|
||||
, low_water_mark := non_neg_integer() %% @see emqx_limiter_schema
|
||||
, bucket := bucket() %% the limiter server's bucket
|
||||
failure_strategy := failure_strategy(),
|
||||
%% @see emqx_limiter_schema
|
||||
divisible := boolean(),
|
||||
%% @see emqx_limiter_schema
|
||||
low_water_mark := non_neg_integer(),
|
||||
%% the limiter server's bucket
|
||||
bucket := bucket(),
|
||||
|
||||
%% retry contenxt
|
||||
%% undefined meaning no retry context or no need to retry
|
||||
, retry_ctx => undefined
|
||||
| retry_context(token_bucket_limiter()) %% the retry context
|
||||
, atom => any() %% allow to add other keys
|
||||
}.
|
||||
retry_ctx =>
|
||||
undefined
|
||||
%% the retry context
|
||||
| retry_context(token_bucket_limiter()),
|
||||
%% allow to add other keys
|
||||
atom => any()
|
||||
}.
|
||||
|
||||
%% a limiter server's bucket reference
|
||||
-type ref_limiter() :: #{ max_retry_time := non_neg_integer()
|
||||
, failure_strategy := failure_strategy()
|
||||
, divisible := boolean()
|
||||
, low_water_mark := non_neg_integer()
|
||||
, bucket := bucket()
|
||||
-type ref_limiter() :: #{
|
||||
max_retry_time := non_neg_integer(),
|
||||
failure_strategy := failure_strategy(),
|
||||
divisible := boolean(),
|
||||
low_water_mark := non_neg_integer(),
|
||||
bucket := bucket(),
|
||||
|
||||
, retry_ctx => undefined | retry_context(ref_limiter())
|
||||
, atom => any() %% allow to add other keys
|
||||
}.
|
||||
retry_ctx => undefined | retry_context(ref_limiter()),
|
||||
%% allow to add other keys
|
||||
atom => any()
|
||||
}.
|
||||
|
||||
-type retry_fun(Limiter) :: fun((pos_integer(), Limiter) -> inner_check_result(Limiter)).
|
||||
-type acquire_type(Limiter) :: integer() | retry_context(Limiter).
|
||||
-type retry_context(Limiter) :: #{ continuation := undefined | retry_fun(Limiter)
|
||||
, diff := non_neg_integer() %% how many tokens are left to obtain
|
||||
-type retry_context(Limiter) :: #{
|
||||
continuation := undefined | retry_fun(Limiter),
|
||||
%% how many tokens are left to obtain
|
||||
diff := non_neg_integer(),
|
||||
|
||||
, need => pos_integer()
|
||||
, start => millisecond()
|
||||
}.
|
||||
need => pos_integer(),
|
||||
start => millisecond()
|
||||
}.
|
||||
|
||||
-type bucket() :: emqx_limiter_bucket_ref:bucket_ref().
|
||||
-type limiter() :: token_bucket_limiter() | ref_limiter() | infinity.
|
||||
|
|
@ -77,27 +96,31 @@
|
|||
-type check_result_pause(Limiter) :: {pause_type(), millisecond(), retry_context(Limiter), Limiter}.
|
||||
-type result_drop(Limiter) :: {drop, Limiter}.
|
||||
|
||||
-type check_result(Limiter) :: check_result_ok(Limiter)
|
||||
-type check_result(Limiter) ::
|
||||
check_result_ok(Limiter)
|
||||
| check_result_pause(Limiter)
|
||||
| result_drop(Limiter).
|
||||
|
||||
-type inner_check_result(Limiter) :: check_result_ok(Limiter)
|
||||
-type inner_check_result(Limiter) ::
|
||||
check_result_ok(Limiter)
|
||||
| check_result_pause(Limiter).
|
||||
|
||||
-type consume_result(Limiter) :: check_result_ok(Limiter)
|
||||
-type consume_result(Limiter) ::
|
||||
check_result_ok(Limiter)
|
||||
| result_drop(Limiter).
|
||||
|
||||
-type decimal() :: emqx_limiter_decimal:decimal().
|
||||
-type failure_strategy() :: emqx_limiter_schema:failure_strategy().
|
||||
|
||||
-type limiter_bucket_cfg() :: #{ rate := decimal()
|
||||
, initial := non_neg_integer()
|
||||
, low_water_mark := non_neg_integer()
|
||||
, capacity := decimal()
|
||||
, divisible := boolean()
|
||||
, max_retry_time := non_neg_integer()
|
||||
, failure_strategy := failure_strategy()
|
||||
}.
|
||||
-type limiter_bucket_cfg() :: #{
|
||||
rate := decimal(),
|
||||
initial := non_neg_integer(),
|
||||
low_water_mark := non_neg_integer(),
|
||||
capacity := decimal(),
|
||||
divisible := boolean(),
|
||||
max_retry_time := non_neg_integer(),
|
||||
failure_strategy := failure_strategy()
|
||||
}.
|
||||
|
||||
-type future() :: pos_integer().
|
||||
|
||||
|
|
@ -113,9 +136,10 @@
|
|||
%%@doc create a limiter
|
||||
-spec make_token_bucket_limiter(limiter_bucket_cfg(), bucket()) -> _.
|
||||
make_token_bucket_limiter(Cfg, Bucket) ->
|
||||
Cfg#{ tokens => emqx_limiter_server:get_initial_val(Cfg)
|
||||
, lasttime => ?NOW
|
||||
, bucket => Bucket
|
||||
Cfg#{
|
||||
tokens => emqx_limiter_server:get_initial_val(Cfg),
|
||||
lasttime => ?NOW,
|
||||
bucket => Bucket
|
||||
}.
|
||||
|
||||
%%@doc create a limiter server's reference
|
||||
|
|
@ -130,38 +154,39 @@ make_infinity_limiter() ->
|
|||
%% @doc request some tokens
|
||||
%% it will automatically retry when failed until the maximum retry time is reached
|
||||
%% @end
|
||||
-spec consume(integer(), Limiter) -> consume_result(Limiter)
|
||||
when Limiter :: limiter().
|
||||
-spec consume(integer(), Limiter) -> consume_result(Limiter) when
|
||||
Limiter :: limiter().
|
||||
consume(Need, #{max_retry_time := RetryTime} = Limiter) when Need > 0 ->
|
||||
try_consume(RetryTime, Need, Limiter);
|
||||
|
||||
consume(_, Limiter) ->
|
||||
{ok, Limiter}.
|
||||
|
||||
%% @doc try to request the token and return the result without automatically retrying
|
||||
-spec check(acquire_type(Limiter), Limiter) -> check_result(Limiter)
|
||||
when Limiter :: limiter().
|
||||
-spec check(acquire_type(Limiter), Limiter) -> check_result(Limiter) when
|
||||
Limiter :: limiter().
|
||||
check(_, infinity) ->
|
||||
{ok, infinity};
|
||||
|
||||
check(Need, Limiter) when is_integer(Need), Need > 0 ->
|
||||
case do_check(Need, Limiter) of
|
||||
{ok, _} = Done ->
|
||||
Done;
|
||||
{PauseType, Pause, Ctx, Limiter2} ->
|
||||
{PauseType,
|
||||
Pause,
|
||||
Ctx#{start => ?NOW, need => Need}, Limiter2}
|
||||
{PauseType, Pause, Ctx#{start => ?NOW, need => Need}, Limiter2}
|
||||
end;
|
||||
|
||||
%% check with retry context.
|
||||
%% when continuation = undefined, the diff will be 0
|
||||
%% so there is no need to check continuation here
|
||||
check(#{continuation := Cont,
|
||||
check(
|
||||
#{
|
||||
continuation := Cont,
|
||||
diff := Diff,
|
||||
start := Start} = Retry,
|
||||
#{failure_strategy := Failure,
|
||||
max_retry_time := RetryTime} = Limiter) when Diff > 0 ->
|
||||
start := Start
|
||||
} = Retry,
|
||||
#{
|
||||
failure_strategy := Failure,
|
||||
max_retry_time := RetryTime
|
||||
} = Limiter
|
||||
) when Diff > 0 ->
|
||||
case Cont(Diff, Limiter) of
|
||||
{ok, _} = Done ->
|
||||
Done;
|
||||
|
|
@ -175,13 +200,12 @@ check(#{continuation := Cont,
|
|||
on_failure(Failure, try_restore(Retry2, Limiter2))
|
||||
end
|
||||
end;
|
||||
|
||||
check(_, Limiter) ->
|
||||
{ok, Limiter}.
|
||||
|
||||
%% @doc pack the retry context into the limiter data
|
||||
-spec set_retry(retry_context(Limiter), Limiter) -> Limiter
|
||||
when Limiter :: limiter().
|
||||
-spec set_retry(retry_context(Limiter), Limiter) -> Limiter when
|
||||
Limiter :: limiter().
|
||||
set_retry(Retry, Limiter) ->
|
||||
Limiter#{retry_ctx => Retry}.
|
||||
|
||||
|
|
@ -189,7 +213,6 @@ set_retry(Retry, Limiter) ->
|
|||
-spec retry(Limiter) -> check_result(Limiter) when Limiter :: limiter().
|
||||
retry(#{retry_ctx := Retry} = Limiter) when is_map(Retry) ->
|
||||
check(Retry, Limiter#{retry_ctx := undefined});
|
||||
|
||||
retry(Limiter) ->
|
||||
{ok, Limiter}.
|
||||
|
||||
|
|
@ -202,30 +225,32 @@ make_future(Need) ->
|
|||
|
||||
%% @doc get the number of tokens currently available
|
||||
-spec available(limiter()) -> decimal().
|
||||
available(#{tokens := Tokens,
|
||||
available(#{
|
||||
tokens := Tokens,
|
||||
rate := Rate,
|
||||
lasttime := LastTime,
|
||||
capacity := Capacity,
|
||||
bucket := Bucket}) ->
|
||||
bucket := Bucket
|
||||
}) ->
|
||||
Tokens2 = apply_elapsed_time(Rate, ?NOW - LastTime, Tokens, Capacity),
|
||||
erlang:min(Tokens2, emqx_limiter_bucket_ref:available(Bucket));
|
||||
|
||||
available(#{bucket := Bucket}) ->
|
||||
emqx_limiter_bucket_ref:available(Bucket);
|
||||
|
||||
available(infinity) ->
|
||||
infinity.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
-spec try_consume(millisecond(),
|
||||
-spec try_consume(
|
||||
millisecond(),
|
||||
acquire_type(Limiter),
|
||||
Limiter) -> consume_result(Limiter) when Limiter :: limiter().
|
||||
try_consume(LeftTime, Retry, #{failure_strategy := Failure} = Limiter)
|
||||
when LeftTime =< 0, is_map(Retry) ->
|
||||
Limiter
|
||||
) -> consume_result(Limiter) when Limiter :: limiter().
|
||||
try_consume(LeftTime, Retry, #{failure_strategy := Failure} = Limiter) when
|
||||
LeftTime =< 0, is_map(Retry)
|
||||
->
|
||||
on_failure(Failure, try_restore(Retry, Limiter));
|
||||
|
||||
try_consume(LeftTime, Need, Limiter) when is_integer(Need) ->
|
||||
case do_check(Need, Limiter) of
|
||||
{ok, _} = Done ->
|
||||
|
|
@ -234,10 +259,14 @@ try_consume(LeftTime, Need, Limiter) when is_integer(Need) ->
|
|||
timer:sleep(erlang:min(LeftTime, Pause)),
|
||||
try_consume(LeftTime - Pause, Ctx#{need => Need}, Limiter2)
|
||||
end;
|
||||
|
||||
try_consume(LeftTime,
|
||||
#{continuation := Cont,
|
||||
diff := Diff} = Retry, Limiter) when Diff > 0 ->
|
||||
try_consume(
|
||||
LeftTime,
|
||||
#{
|
||||
continuation := Cont,
|
||||
diff := Diff
|
||||
} = Retry,
|
||||
Limiter
|
||||
) when Diff > 0 ->
|
||||
case Cont(Diff, Limiter) of
|
||||
{ok, _} = Done ->
|
||||
Done;
|
||||
|
|
@ -245,64 +274,78 @@ try_consume(LeftTime,
|
|||
timer:sleep(erlang:min(LeftTime, Pause)),
|
||||
try_consume(LeftTime - Pause, maps:merge(Retry, Ctx), Limiter2)
|
||||
end;
|
||||
|
||||
try_consume(_, _, Limiter) ->
|
||||
{ok, Limiter}.
|
||||
|
||||
-spec do_check(acquire_type(Limiter), Limiter) -> inner_check_result(Limiter)
|
||||
when Limiter :: limiter().
|
||||
-spec do_check(acquire_type(Limiter), Limiter) -> inner_check_result(Limiter) when
|
||||
Limiter :: limiter().
|
||||
do_check(Need, #{tokens := Tokens} = Limiter) when Need =< Tokens ->
|
||||
do_check_with_parent_limiter(Need, Limiter);
|
||||
|
||||
do_check(Need, #{tokens := _} = Limiter) ->
|
||||
do_reset(Need, Limiter);
|
||||
|
||||
do_check(Need, #{divisible := Divisible,
|
||||
bucket := Bucket} = Ref) ->
|
||||
do_check(
|
||||
Need,
|
||||
#{
|
||||
divisible := Divisible,
|
||||
bucket := Bucket
|
||||
} = Ref
|
||||
) ->
|
||||
case emqx_limiter_bucket_ref:check(Need, Bucket, Divisible) of
|
||||
{ok, Tokens} ->
|
||||
may_return_or_pause(Tokens, Ref);
|
||||
{PauseType, Rate, Obtained} ->
|
||||
return_pause(Rate,
|
||||
return_pause(
|
||||
Rate,
|
||||
PauseType,
|
||||
fun ?FUNCTION_NAME/2, Need - Obtained, Ref)
|
||||
fun ?FUNCTION_NAME/2,
|
||||
Need - Obtained,
|
||||
Ref
|
||||
)
|
||||
end.
|
||||
|
||||
on_failure(force, Limiter) ->
|
||||
{ok, Limiter};
|
||||
|
||||
on_failure(drop, Limiter) ->
|
||||
{drop, Limiter};
|
||||
|
||||
on_failure(throw, Limiter) ->
|
||||
Message = io_lib:format("limiter consume failed, limiter:~p~n", [Limiter]),
|
||||
erlang:throw({rate_check_fail, Message}).
|
||||
|
||||
-spec do_check_with_parent_limiter(pos_integer(), token_bucket_limiter()) ->
|
||||
inner_check_result(token_bucket_limiter()).
|
||||
do_check_with_parent_limiter(Need,
|
||||
#{tokens := Tokens,
|
||||
do_check_with_parent_limiter(
|
||||
Need,
|
||||
#{
|
||||
tokens := Tokens,
|
||||
divisible := Divisible,
|
||||
bucket := Bucket} = Limiter) ->
|
||||
bucket := Bucket
|
||||
} = Limiter
|
||||
) ->
|
||||
case emqx_limiter_bucket_ref:check(Need, Bucket, Divisible) of
|
||||
{ok, RefLeft} ->
|
||||
Left = sub(Tokens, Need),
|
||||
may_return_or_pause(erlang:min(RefLeft, Left), Limiter#{tokens := Left});
|
||||
{PauseType, Rate, Obtained} ->
|
||||
return_pause(Rate,
|
||||
return_pause(
|
||||
Rate,
|
||||
PauseType,
|
||||
fun ?FUNCTION_NAME/2,
|
||||
Need - Obtained,
|
||||
Limiter#{tokens := sub(Tokens, Obtained)})
|
||||
Limiter#{tokens := sub(Tokens, Obtained)}
|
||||
)
|
||||
end.
|
||||
|
||||
-spec do_reset(pos_integer(), token_bucket_limiter()) -> inner_check_result(token_bucket_limiter()).
|
||||
do_reset(Need,
|
||||
#{tokens := Tokens,
|
||||
do_reset(
|
||||
Need,
|
||||
#{
|
||||
tokens := Tokens,
|
||||
rate := Rate,
|
||||
lasttime := LastTime,
|
||||
divisible := Divisible,
|
||||
capacity := Capacity} = Limiter) ->
|
||||
capacity := Capacity
|
||||
} = Limiter
|
||||
) ->
|
||||
Now = ?NOW,
|
||||
Tokens2 = apply_elapsed_time(Rate, Now - LastTime, Tokens, Capacity),
|
||||
|
||||
|
|
@ -312,49 +355,54 @@ do_reset(Need,
|
|||
do_check_with_parent_limiter(Need, Limiter2);
|
||||
Available when Divisible andalso Available > 0 ->
|
||||
%% must be allocated here, because may be Need > Capacity
|
||||
return_pause(Rate,
|
||||
return_pause(
|
||||
Rate,
|
||||
partial,
|
||||
fun do_reset/2,
|
||||
Need - Available,
|
||||
Limiter#{tokens := 0, lasttime := Now});
|
||||
Limiter#{tokens := 0, lasttime := Now}
|
||||
);
|
||||
_ ->
|
||||
return_pause(Rate, pause, fun do_reset/2, Need, Limiter)
|
||||
end.
|
||||
|
||||
-spec return_pause(decimal(), pause_type(), retry_fun(Limiter), pos_integer(), Limiter)
|
||||
-> check_result_pause(Limiter) when Limiter :: limiter().
|
||||
-spec return_pause(decimal(), pause_type(), retry_fun(Limiter), pos_integer(), Limiter) ->
|
||||
check_result_pause(Limiter)
|
||||
when
|
||||
Limiter :: limiter().
|
||||
return_pause(infinity, PauseType, Fun, Diff, Limiter) ->
|
||||
%% workaround when emqx_limiter_server's rate is infinity
|
||||
{PauseType, ?MINIMUM_PAUSE, make_retry_context(Fun, Diff), Limiter};
|
||||
|
||||
return_pause(Rate, PauseType, Fun, Diff, Limiter) ->
|
||||
Val = erlang:round(Diff * emqx_limiter_schema:minimum_period() / Rate),
|
||||
Pause = emqx_misc:clamp(Val, ?MINIMUM_PAUSE, ?MAXIMUM_PAUSE),
|
||||
{PauseType, Pause, make_retry_context(Fun, Diff), Limiter}.
|
||||
|
||||
-spec make_retry_context(undefined | retry_fun(Limiter), non_neg_integer()) ->
|
||||
retry_context(Limiter) when Limiter :: limiter().
|
||||
retry_context(Limiter)
|
||||
when
|
||||
Limiter :: limiter().
|
||||
make_retry_context(Fun, Diff) ->
|
||||
#{continuation => Fun, diff => Diff}.
|
||||
|
||||
-spec try_restore(retry_context(Limiter), Limiter) -> Limiter
|
||||
when Limiter :: limiter().
|
||||
try_restore(#{need := Need, diff := Diff},
|
||||
#{tokens := Tokens, capacity := Capacity, bucket := Bucket} = Limiter) ->
|
||||
-spec try_restore(retry_context(Limiter), Limiter) -> Limiter when
|
||||
Limiter :: limiter().
|
||||
try_restore(
|
||||
#{need := Need, diff := Diff},
|
||||
#{tokens := Tokens, capacity := Capacity, bucket := Bucket} = Limiter
|
||||
) ->
|
||||
Back = Need - Diff,
|
||||
Tokens2 = erlang:min(Capacity, Back + Tokens),
|
||||
emqx_limiter_bucket_ref:try_restore(Back, Bucket),
|
||||
Limiter#{tokens := Tokens2};
|
||||
|
||||
try_restore(#{need := Need, diff := Diff}, #{bucket := Bucket} = Limiter) ->
|
||||
emqx_limiter_bucket_ref:try_restore(Need - Diff, Bucket),
|
||||
Limiter.
|
||||
|
||||
-spec may_return_or_pause(non_neg_integer(), Limiter) -> check_result(Limiter)
|
||||
when Limiter :: limiter().
|
||||
-spec may_return_or_pause(non_neg_integer(), Limiter) -> check_result(Limiter) when
|
||||
Limiter :: limiter().
|
||||
may_return_or_pause(Left, #{low_water_mark := Mark} = Limiter) when Left >= Mark ->
|
||||
{ok, Limiter};
|
||||
|
||||
may_return_or_pause(_, Limiter) ->
|
||||
{pause, ?MINIMUM_PAUSE, make_retry_context(undefined, 0), Limiter}.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{application, emqx_limiter,
|
||||
[{description, "EMQX Hierarchical Limiter"},
|
||||
{vsn, "1.0.0"}, % strict semver, bump manually!
|
||||
{application, emqx_limiter, [
|
||||
{description, "EMQX Hierarchical Limiter"},
|
||||
% strict semver, bump manually!
|
||||
{vsn, "1.0.0"},
|
||||
{modules, []},
|
||||
{registered, [emqx_limiter_sup]},
|
||||
{applications, [kernel,stdlib,emqx]},
|
||||
{mod, {emqx_limiter_app,[]}},
|
||||
{applications, [kernel, stdlib, emqx]},
|
||||
{mod, {emqx_limiter_app, []}},
|
||||
{env, []},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||
{links, []}
|
||||
]}.
|
||||
]}.
|
||||
|
|
|
|||
|
|
@ -31,13 +31,16 @@
|
|||
%% top supervisor of the tree.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec start(StartType :: normal |
|
||||
{takeover, Node :: node()} |
|
||||
{failover, Node :: node()},
|
||||
StartArgs :: term()) ->
|
||||
{ok, Pid :: pid()} |
|
||||
{ok, Pid :: pid(), State :: term()} |
|
||||
{error, Reason :: term()}.
|
||||
-spec start(
|
||||
StartType ::
|
||||
normal
|
||||
| {takeover, Node :: node()}
|
||||
| {failover, Node :: node()},
|
||||
StartArgs :: term()
|
||||
) ->
|
||||
{ok, Pid :: pid()}
|
||||
| {ok, Pid :: pid(), State :: term()}
|
||||
| {error, Reason :: term()}.
|
||||
start(_StartType, _StartArgs) ->
|
||||
{ok, _} = emqx_limiter_sup:start_link().
|
||||
|
||||
|
|
|
|||
|
|
@ -21,51 +21,68 @@
|
|||
%% @end
|
||||
|
||||
%% API
|
||||
-export([ new/3, check/3, try_restore/2
|
||||
, available/1]).
|
||||
-export([
|
||||
new/3,
|
||||
check/3,
|
||||
try_restore/2,
|
||||
available/1
|
||||
]).
|
||||
|
||||
-export_type([bucket_ref/0]).
|
||||
|
||||
-type infinity_bucket_ref() :: infinity.
|
||||
-type finite_bucket_ref() :: #{ counter := counters:counters_ref()
|
||||
, index := index()
|
||||
, rate := rate()}.
|
||||
-type finite_bucket_ref() :: #{
|
||||
counter := counters:counters_ref(),
|
||||
index := index(),
|
||||
rate := rate()
|
||||
}.
|
||||
|
||||
-type bucket_ref() :: infinity_bucket_ref()
|
||||
-type bucket_ref() ::
|
||||
infinity_bucket_ref()
|
||||
| finite_bucket_ref().
|
||||
|
||||
-type index() :: emqx_limiter_server:index().
|
||||
-type rate() :: emqx_limiter_decimal:decimal().
|
||||
-type check_failure_type() :: partial | pause.
|
||||
|
||||
-elvis([{elvis_style, no_if_expression, disable}]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
-spec new(undefined | counters:countres_ref(),
|
||||
-spec new(
|
||||
undefined | counters:countres_ref(),
|
||||
undefined | index(),
|
||||
rate()) -> bucket_ref().
|
||||
rate()
|
||||
) -> bucket_ref().
|
||||
new(undefined, _, _) ->
|
||||
infinity;
|
||||
|
||||
new(Counter, Index, Rate) ->
|
||||
#{counter => Counter,
|
||||
#{
|
||||
counter => Counter,
|
||||
index => Index,
|
||||
rate => Rate}.
|
||||
rate => Rate
|
||||
}.
|
||||
|
||||
%% @doc check tokens
|
||||
-spec check(pos_integer(), bucket_ref(), Disivisble :: boolean()) ->
|
||||
HasToken :: {ok, emqx_limiter_decimal:decimal()}
|
||||
HasToken ::
|
||||
{ok, emqx_limiter_decimal:decimal()}
|
||||
| {check_failure_type(), rate(), pos_integer()}.
|
||||
check(_, infinity, _) ->
|
||||
{ok, infinity};
|
||||
|
||||
check(Need,
|
||||
#{counter := Counter,
|
||||
check(
|
||||
Need,
|
||||
#{
|
||||
counter := Counter,
|
||||
index := Index,
|
||||
rate := Rate},
|
||||
Divisible)->
|
||||
rate := Rate
|
||||
},
|
||||
Divisible
|
||||
) ->
|
||||
RefToken = counters:get(Counter, Index),
|
||||
if RefToken >= Need ->
|
||||
if
|
||||
RefToken >= Need ->
|
||||
counters:sub(Counter, Index, Need),
|
||||
{ok, RefToken - Need};
|
||||
Divisible andalso RefToken > 0 ->
|
||||
|
|
@ -93,7 +110,6 @@ try_restore(Inc, #{counter := Counter, index := Index}) ->
|
|||
-spec available(bucket_ref()) -> emqx_limiter_decimal:decimal().
|
||||
available(#{counter := Counter, index := Index}) ->
|
||||
counters:get(Counter, Index);
|
||||
|
||||
available(infinity) ->
|
||||
infinity.
|
||||
|
||||
|
|
|
|||
|
|
@ -21,21 +21,31 @@
|
|||
%% @end
|
||||
|
||||
%% API
|
||||
-export([ new/0, new/1, new/2, get_limiter_by_names/2
|
||||
, add_new/3, update_by_name/3, set_retry_context/2
|
||||
, check/3, retry/2, get_retry_context/1
|
||||
, check_list/2, retry_list/2
|
||||
]).
|
||||
-export([
|
||||
new/0, new/1, new/2,
|
||||
get_limiter_by_names/2,
|
||||
add_new/3,
|
||||
update_by_name/3,
|
||||
set_retry_context/2,
|
||||
check/3,
|
||||
retry/2,
|
||||
get_retry_context/1,
|
||||
check_list/2,
|
||||
retry_list/2
|
||||
]).
|
||||
|
||||
-export_type([container/0, check_result/0]).
|
||||
|
||||
-type container() :: #{ limiter_type() => undefined | limiter()
|
||||
-type container() :: #{
|
||||
limiter_type() => undefined | limiter(),
|
||||
%% the retry context of the limiter
|
||||
, retry_key() => undefined
|
||||
retry_key() =>
|
||||
undefined
|
||||
| retry_context()
|
||||
| future()
|
||||
, retry_ctx := undefined | any() %% the retry context of the container
|
||||
}.
|
||||
| future(),
|
||||
%% the retry context of the container
|
||||
retry_ctx := undefined | any()
|
||||
}.
|
||||
|
||||
-type future() :: pos_integer().
|
||||
-type limiter_type() :: emqx_limiter_schema:limiter_type().
|
||||
|
|
@ -43,7 +53,8 @@
|
|||
-type retry_context() :: emqx_htb_limiter:retry_context().
|
||||
-type bucket_name() :: emqx_limiter_schema:bucket_name().
|
||||
-type millisecond() :: non_neg_integer().
|
||||
-type check_result() :: {ok, container()}
|
||||
-type check_result() ::
|
||||
{ok, container()}
|
||||
| {drop, container()}
|
||||
| {pause, millisecond(), container()}.
|
||||
|
||||
|
|
@ -62,16 +73,20 @@ new() ->
|
|||
new(Types) ->
|
||||
new(Types, #{}).
|
||||
|
||||
-spec new(list(limiter_type()),
|
||||
#{limiter_type() => emqx_limiter_schema:bucket_name()}) -> container().
|
||||
-spec new(
|
||||
list(limiter_type()),
|
||||
#{limiter_type() => emqx_limiter_schema:bucket_name()}
|
||||
) -> container().
|
||||
new(Types, Names) ->
|
||||
get_limiter_by_names(Types, Names).
|
||||
|
||||
%% @doc generate a container
|
||||
%% according to the type of limiter and the bucket name configuration of the limiter
|
||||
%% @end
|
||||
-spec get_limiter_by_names(list(limiter_type()),
|
||||
#{limiter_type() => emqx_limiter_schema:bucket_name()}) -> container().
|
||||
-spec get_limiter_by_names(
|
||||
list(limiter_type()),
|
||||
#{limiter_type() => emqx_limiter_schema:bucket_name()}
|
||||
) -> container().
|
||||
get_limiter_by_names(Types, BucketNames) ->
|
||||
Init = fun(Type, Acc) ->
|
||||
Limiter = emqx_limiter_server:connect(Type, BucketNames),
|
||||
|
|
@ -80,17 +95,20 @@ get_limiter_by_names(Types, BucketNames) ->
|
|||
lists:foldl(Init, #{retry_ctx => undefined}, Types).
|
||||
|
||||
%% @doc add the specified type of limiter to the container
|
||||
-spec update_by_name(limiter_type(),
|
||||
-spec update_by_name(
|
||||
limiter_type(),
|
||||
bucket_name() | #{limiter_type() => bucket_name()},
|
||||
container()) -> container().
|
||||
container()
|
||||
) -> container().
|
||||
update_by_name(Type, Buckets, Container) ->
|
||||
Limiter = emqx_limiter_server:connect(Type, Buckets),
|
||||
add_new(Type, Limiter, Container).
|
||||
|
||||
-spec add_new(limiter_type(), limiter(), container()) -> container().
|
||||
add_new(Type, Limiter, Container) ->
|
||||
Container#{ Type => Limiter
|
||||
, ?RETRY_KEY(Type) => undefined
|
||||
Container#{
|
||||
Type => Limiter,
|
||||
?RETRY_KEY(Type) => undefined
|
||||
}.
|
||||
|
||||
%% @doc check the specified limiter
|
||||
|
|
@ -110,15 +128,18 @@ check_list([{Need, Type} | T], Container) ->
|
|||
Future = emqx_htb_limiter:make_future(FN),
|
||||
Acc#{?RETRY_KEY(FT) := Future}
|
||||
end,
|
||||
C2 = lists:foldl(Fun,
|
||||
Container#{Type := Limiter2,
|
||||
?RETRY_KEY(Type) := Ctx},
|
||||
T),
|
||||
C2 = lists:foldl(
|
||||
Fun,
|
||||
Container#{
|
||||
Type := Limiter2,
|
||||
?RETRY_KEY(Type) := Ctx
|
||||
},
|
||||
T
|
||||
),
|
||||
{pause, PauseMs, C2};
|
||||
{drop, Limiter2} ->
|
||||
{drop, Container#{Type := Limiter2}}
|
||||
end;
|
||||
|
||||
check_list([], Container) ->
|
||||
{ok, Container}.
|
||||
|
||||
|
|
@ -132,24 +153,23 @@ retry(Type, Container) ->
|
|||
retry_list([Type | T], Container) ->
|
||||
Key = ?RETRY_KEY(Type),
|
||||
case Container of
|
||||
#{Type := Limiter,
|
||||
Key := Retry} when Retry =/= undefined ->
|
||||
#{
|
||||
Type := Limiter,
|
||||
Key := Retry
|
||||
} when Retry =/= undefined ->
|
||||
case emqx_htb_limiter:check(Retry, Limiter) of
|
||||
{ok, Limiter2} ->
|
||||
%% undefined meaning there is no retry context or there is no need to retry
|
||||
%% when a limiter has a undefined retry context, the check will always success
|
||||
retry_list(T, Container#{Type := Limiter2, Key := undefined});
|
||||
{_, PauseMs, Ctx, Limiter2} ->
|
||||
{pause,
|
||||
PauseMs,
|
||||
Container#{Type := Limiter2, Key := Ctx}};
|
||||
{pause, PauseMs, Container#{Type := Limiter2, Key := Ctx}};
|
||||
{drop, Limiter2} ->
|
||||
{drop, Container#{Type := Limiter2}}
|
||||
end;
|
||||
_ ->
|
||||
retry_list(T, Container)
|
||||
end;
|
||||
|
||||
retry_list([], Container) ->
|
||||
{ok, Container}.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@
|
|||
-module(emqx_limiter_correction).
|
||||
|
||||
%% API
|
||||
-export([ add/2 ]).
|
||||
-export([add/2]).
|
||||
|
||||
-type correction_value() :: #{ correction := emqx_limiter_decimal:zero_or_float()
|
||||
, any() => any()
|
||||
}.
|
||||
-type correction_value() :: #{
|
||||
correction := emqx_limiter_decimal:zero_or_float(),
|
||||
any() => any()
|
||||
}.
|
||||
|
||||
-export_type([correction_value/0]).
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,13 @@
|
|||
-module(emqx_limiter_decimal).
|
||||
|
||||
%% API
|
||||
-export([ add/2, sub/2, mul/2
|
||||
, put_to_counter/3, floor_div/2]).
|
||||
-export([
|
||||
add/2,
|
||||
sub/2,
|
||||
mul/2,
|
||||
put_to_counter/3,
|
||||
floor_div/2
|
||||
]).
|
||||
-export_type([decimal/0, zero_or_float/0]).
|
||||
|
||||
-type decimal() :: infinity | number().
|
||||
|
|
@ -30,33 +35,35 @@
|
|||
%%% API
|
||||
%%--------------------------------------------------------------------
|
||||
-spec add(decimal(), decimal()) -> decimal().
|
||||
add(A, B) when A =:= infinity
|
||||
orelse B =:= infinity ->
|
||||
add(A, B) when
|
||||
A =:= infinity orelse
|
||||
B =:= infinity
|
||||
->
|
||||
infinity;
|
||||
|
||||
add(A, B) ->
|
||||
A + B.
|
||||
|
||||
-spec sub(decimal(), decimal()) -> decimal().
|
||||
sub(A, B) when A =:= infinity
|
||||
orelse B =:= infinity ->
|
||||
sub(A, B) when
|
||||
A =:= infinity orelse
|
||||
B =:= infinity
|
||||
->
|
||||
infinity;
|
||||
|
||||
sub(A, B) ->
|
||||
A - B.
|
||||
|
||||
-spec mul(decimal(), decimal()) -> decimal().
|
||||
mul(A, B) when A =:= infinity
|
||||
orelse B =:= infinity ->
|
||||
mul(A, B) when
|
||||
A =:= infinity orelse
|
||||
B =:= infinity
|
||||
->
|
||||
infinity;
|
||||
|
||||
mul(A, B) ->
|
||||
A * B.
|
||||
|
||||
-spec floor_div(decimal(), number()) -> decimal().
|
||||
floor_div(infinity, _) ->
|
||||
infinity;
|
||||
|
||||
floor_div(A, B) ->
|
||||
erlang:floor(A / B).
|
||||
|
||||
|
|
|
|||
|
|
@ -22,14 +22,26 @@
|
|||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
|
||||
%% API
|
||||
-export([ start_link/0, start_server/1, find_bucket/1
|
||||
, find_bucket/2, insert_bucket/2, insert_bucket/3
|
||||
, make_path/2, restart_server/1
|
||||
]).
|
||||
-export([
|
||||
start_link/0,
|
||||
start_server/1,
|
||||
find_bucket/1,
|
||||
find_bucket/2,
|
||||
insert_bucket/2, insert_bucket/3,
|
||||
make_path/2,
|
||||
restart_server/1
|
||||
]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3, format_status/2]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3,
|
||||
format_status/2
|
||||
]).
|
||||
|
||||
-export_type([path/0]).
|
||||
|
||||
|
|
@ -38,9 +50,10 @@
|
|||
-type bucket_name() :: emqx_limiter_schema:bucket_name().
|
||||
|
||||
%% counter record in ets table
|
||||
-record(bucket, { path :: path()
|
||||
, bucket :: bucket_ref()
|
||||
}).
|
||||
-record(bucket, {
|
||||
path :: path(),
|
||||
bucket :: bucket_ref()
|
||||
}).
|
||||
|
||||
-type bucket_ref() :: emqx_limiter_bucket_ref:bucket_ref().
|
||||
|
||||
|
|
@ -71,13 +84,14 @@ find_bucket(Path) ->
|
|||
undefined
|
||||
end.
|
||||
|
||||
-spec insert_bucket(limiter_type(),
|
||||
-spec insert_bucket(
|
||||
limiter_type(),
|
||||
bucket_name(),
|
||||
bucket_ref()) -> boolean().
|
||||
bucket_ref()
|
||||
) -> boolean().
|
||||
insert_bucket(Type, BucketName, Bucket) ->
|
||||
inner_insert_bucket(make_path(Type, BucketName), Bucket).
|
||||
|
||||
|
||||
-spec insert_bucket(path(), bucket_ref()) -> true.
|
||||
insert_bucket(Path, Bucket) ->
|
||||
inner_insert_bucket(Path, Bucket).
|
||||
|
|
@ -91,10 +105,11 @@ make_path(Type, BucketName) ->
|
|||
%% Starts the server
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec start_link() -> {ok, Pid :: pid()} |
|
||||
{error, Error :: {already_started, pid()}} |
|
||||
{error, Error :: term()} |
|
||||
ignore.
|
||||
-spec start_link() ->
|
||||
{ok, Pid :: pid()}
|
||||
| {error, Error :: {already_started, pid()}}
|
||||
| {error, Error :: term()}
|
||||
| ignore.
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
|
|
@ -108,15 +123,21 @@ start_link() ->
|
|||
%% Initializes the server
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec init(Args :: term()) -> {ok, State :: term()} |
|
||||
{ok, State :: term(), Timeout :: timeout()} |
|
||||
{ok, State :: term(), hibernate} |
|
||||
{stop, Reason :: term()} |
|
||||
ignore.
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, State :: term()}
|
||||
| {ok, State :: term(), Timeout :: timeout()}
|
||||
| {ok, State :: term(), hibernate}
|
||||
| {stop, Reason :: term()}
|
||||
| ignore.
|
||||
init([]) ->
|
||||
_ = ets:new(?TAB, [ set, public, named_table, {keypos, #bucket.path}
|
||||
, {write_concurrency, true}, {read_concurrency, true}
|
||||
, {heir, erlang:whereis(emqx_limiter_sup), none}
|
||||
_ = ets:new(?TAB, [
|
||||
set,
|
||||
public,
|
||||
named_table,
|
||||
{keypos, #bucket.path},
|
||||
{write_concurrency, true},
|
||||
{read_concurrency, true},
|
||||
{heir, erlang:whereis(emqx_limiter_sup), none}
|
||||
]),
|
||||
{ok, #{}}.
|
||||
|
||||
|
|
@ -127,14 +148,14 @@ init([]) ->
|
|||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
|
||||
{reply, Reply :: term(), NewState :: term()} |
|
||||
{reply, Reply :: term(), NewState :: term(), Timeout :: timeout()} |
|
||||
{reply, Reply :: term(), NewState :: term(), hibernate} |
|
||||
{noreply, NewState :: term()} |
|
||||
{noreply, NewState :: term(), Timeout :: timeout()} |
|
||||
{noreply, NewState :: term(), hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: term()} |
|
||||
{stop, Reason :: term(), NewState :: term()}.
|
||||
{reply, Reply :: term(), NewState :: term()}
|
||||
| {reply, Reply :: term(), NewState :: term(), Timeout :: timeout()}
|
||||
| {reply, Reply :: term(), NewState :: term(), hibernate}
|
||||
| {noreply, NewState :: term()}
|
||||
| {noreply, NewState :: term(), Timeout :: timeout()}
|
||||
| {noreply, NewState :: term(), hibernate}
|
||||
| {stop, Reason :: term(), Reply :: term(), NewState :: term()}
|
||||
| {stop, Reason :: term(), NewState :: term()}.
|
||||
handle_call(Req, _From, State) ->
|
||||
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||
{reply, ignore, State}.
|
||||
|
|
@ -146,10 +167,10 @@ handle_call(Req, _From, State) ->
|
|||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_cast(Request :: term(), State :: term()) ->
|
||||
{noreply, NewState :: term()} |
|
||||
{noreply, NewState :: term(), Timeout :: timeout()} |
|
||||
{noreply, NewState :: term(), hibernate} |
|
||||
{stop, Reason :: term(), NewState :: term()}.
|
||||
{noreply, NewState :: term()}
|
||||
| {noreply, NewState :: term(), Timeout :: timeout()}
|
||||
| {noreply, NewState :: term(), hibernate}
|
||||
| {stop, Reason :: term(), NewState :: term()}.
|
||||
handle_cast(Req, State) ->
|
||||
?SLOG(error, #{msg => "unexpected_cast", cast => Req}),
|
||||
{noreply, State}.
|
||||
|
|
@ -161,10 +182,10 @@ handle_cast(Req, State) ->
|
|||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_info(Info :: timeout() | term(), State :: term()) ->
|
||||
{noreply, NewState :: term()} |
|
||||
{noreply, NewState :: term(), Timeout :: timeout()} |
|
||||
{noreply, NewState :: term(), hibernate} |
|
||||
{stop, Reason :: normal | term(), NewState :: term()}.
|
||||
{noreply, NewState :: term()}
|
||||
| {noreply, NewState :: term(), Timeout :: timeout()}
|
||||
| {noreply, NewState :: term(), hibernate}
|
||||
| {stop, Reason :: normal | term(), NewState :: term()}.
|
||||
handle_info(Info, State) ->
|
||||
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||
{noreply, State}.
|
||||
|
|
@ -178,8 +199,10 @@ handle_info(Info, State) ->
|
|||
%% with Reason. The return value is ignored.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(),
|
||||
State :: term()) -> any().
|
||||
-spec terminate(
|
||||
Reason :: normal | shutdown | {shutdown, term()} | term(),
|
||||
State :: term()
|
||||
) -> any().
|
||||
terminate(_Reason, _State) ->
|
||||
ok.
|
||||
|
||||
|
|
@ -189,10 +212,13 @@ terminate(_Reason, _State) ->
|
|||
%% Convert process state when code is changed
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec code_change(OldVsn :: term() | {down, term()},
|
||||
-spec code_change(
|
||||
OldVsn :: term() | {down, term()},
|
||||
State :: term(),
|
||||
Extra :: term()) -> {ok, NewState :: term()} |
|
||||
{error, Reason :: term()}.
|
||||
Extra :: term()
|
||||
) ->
|
||||
{ok, NewState :: term()}
|
||||
| {error, Reason :: term()}.
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
|
|
@ -204,8 +230,10 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
%% or when it appears in termination error logs.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec format_status(Opt :: normal | terminate,
|
||||
Status :: list()) -> Status :: term().
|
||||
-spec format_status(
|
||||
Opt :: normal | terminate,
|
||||
Status :: list()
|
||||
) -> Status :: term().
|
||||
format_status(_Opt, Status) ->
|
||||
Status.
|
||||
|
||||
|
|
@ -214,5 +242,7 @@ format_status(_Opt, Status) ->
|
|||
%%--------------------------------------------------------------------
|
||||
-spec inner_insert_bucket(path(), bucket_ref()) -> true.
|
||||
inner_insert_bucket(Path, Bucket) ->
|
||||
ets:insert(?TAB,
|
||||
#bucket{path = Path, bucket = Bucket}).
|
||||
ets:insert(
|
||||
?TAB,
|
||||
#bucket{path = Path, bucket = Bucket}
|
||||
).
|
||||
|
|
|
|||
|
|
@ -18,14 +18,23 @@
|
|||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-export([ roots/0, fields/1, to_rate/1, to_capacity/1
|
||||
, minimum_period/0, to_burst_rate/1, to_initial/1
|
||||
, namespace/0, get_bucket_cfg_path/2, desc/1
|
||||
]).
|
||||
-export([
|
||||
roots/0,
|
||||
fields/1,
|
||||
to_rate/1,
|
||||
to_capacity/1,
|
||||
minimum_period/0,
|
||||
to_burst_rate/1,
|
||||
to_initial/1,
|
||||
namespace/0,
|
||||
get_bucket_cfg_path/2,
|
||||
desc/1
|
||||
]).
|
||||
|
||||
-define(KILOBYTE, 1024).
|
||||
|
||||
-type limiter_type() :: bytes_in
|
||||
-type limiter_type() ::
|
||||
bytes_in
|
||||
| message_in
|
||||
| connection
|
||||
| message_routing
|
||||
|
|
@ -34,27 +43,35 @@
|
|||
-type bucket_name() :: atom().
|
||||
-type rate() :: infinity | float().
|
||||
-type burst_rate() :: 0 | float().
|
||||
-type capacity() :: infinity | number(). %% the capacity of the token bucket
|
||||
-type initial() :: non_neg_integer(). %% initial capacity of the token bucket
|
||||
%% the capacity of the token bucket
|
||||
-type capacity() :: infinity | number().
|
||||
%% initial capacity of the token bucket
|
||||
-type initial() :: non_neg_integer().
|
||||
-type bucket_path() :: list(atom()).
|
||||
|
||||
%% the processing strategy after the failure of the token request
|
||||
-type failure_strategy() :: force %% Forced to pass
|
||||
| drop %% discard the current request
|
||||
| throw. %% throw an exception
|
||||
|
||||
%% Forced to pass
|
||||
-type failure_strategy() ::
|
||||
force
|
||||
%% discard the current request
|
||||
| drop
|
||||
%% throw an exception
|
||||
| throw.
|
||||
|
||||
-typerefl_from_string({rate/0, ?MODULE, to_rate}).
|
||||
-typerefl_from_string({burst_rate/0, ?MODULE, to_burst_rate}).
|
||||
-typerefl_from_string({capacity/0, ?MODULE, to_capacity}).
|
||||
-typerefl_from_string({initial/0, ?MODULE, to_initial}).
|
||||
|
||||
-reflect_type([ rate/0
|
||||
, burst_rate/0
|
||||
, capacity/0
|
||||
, initial/0
|
||||
, failure_strategy/0
|
||||
, bucket_name/0
|
||||
]).
|
||||
-reflect_type([
|
||||
rate/0,
|
||||
burst_rate/0,
|
||||
capacity/0,
|
||||
initial/0,
|
||||
failure_strategy/0,
|
||||
bucket_name/0
|
||||
]).
|
||||
|
||||
-export_type([limiter_type/0, bucket_path/0]).
|
||||
|
||||
|
|
@ -66,87 +83,154 @@ namespace() -> limiter.
|
|||
roots() -> [limiter].
|
||||
|
||||
fields(limiter) ->
|
||||
[ {bytes_in, sc(ref(limiter_opts),
|
||||
#{description =>
|
||||
<<"The bytes_in limiter.<br>"
|
||||
[
|
||||
{bytes_in,
|
||||
sc(
|
||||
ref(limiter_opts),
|
||||
#{
|
||||
description =>
|
||||
<<
|
||||
"The bytes_in limiter.<br>"
|
||||
"It is used to limit the inbound bytes rate for this EMQX node."
|
||||
"If the this limiter limit is reached,"
|
||||
"the restricted client will be slow down even be hung for a while.">>
|
||||
})}
|
||||
, {message_in, sc(ref(limiter_opts),
|
||||
#{description =>
|
||||
<<"The message_in limiter.<br>"
|
||||
"the restricted client will be slow down even be hung for a while."
|
||||
>>
|
||||
}
|
||||
)},
|
||||
{message_in,
|
||||
sc(
|
||||
ref(limiter_opts),
|
||||
#{
|
||||
description =>
|
||||
<<
|
||||
"The message_in limiter.<br>"
|
||||
"This is used to limit the inbound message numbers for this EMQX node"
|
||||
"If the this limiter limit is reached,"
|
||||
"the restricted client will be slow down even be hung for a while.">>
|
||||
})}
|
||||
, {connection, sc(ref(limiter_opts),
|
||||
#{description =>
|
||||
<<"The connection limiter.<br>"
|
||||
"the restricted client will be slow down even be hung for a while."
|
||||
>>
|
||||
}
|
||||
)},
|
||||
{connection,
|
||||
sc(
|
||||
ref(limiter_opts),
|
||||
#{
|
||||
description =>
|
||||
<<
|
||||
"The connection limiter.<br>"
|
||||
"This is used to limit the connection rate for this EMQX node"
|
||||
"If the this limiter limit is reached,"
|
||||
"New connections will be refused"
|
||||
>>})}
|
||||
, {message_routing, sc(ref(limiter_opts),
|
||||
#{description =>
|
||||
<<"The message_routing limiter.<br>"
|
||||
>>
|
||||
}
|
||||
)},
|
||||
{message_routing,
|
||||
sc(
|
||||
ref(limiter_opts),
|
||||
#{
|
||||
description =>
|
||||
<<
|
||||
"The message_routing limiter.<br>"
|
||||
"This is used to limite the deliver rate for this EMQX node"
|
||||
"If the this limiter limit is reached,"
|
||||
"New publish will be refused"
|
||||
>>
|
||||
})}
|
||||
, {batch, sc(ref(limiter_opts),
|
||||
#{description => <<"The batch limiter.<br>"
|
||||
}
|
||||
)},
|
||||
{batch,
|
||||
sc(
|
||||
ref(limiter_opts),
|
||||
#{
|
||||
description => <<
|
||||
"The batch limiter.<br>"
|
||||
"This is used for EMQX internal batch operation"
|
||||
"e.g. limite the retainer's deliver rate"
|
||||
>>
|
||||
})}
|
||||
}
|
||||
)}
|
||||
];
|
||||
|
||||
fields(limiter_opts) ->
|
||||
[ {rate, sc(rate(), #{default => "infinity", desc => "The rate"})}
|
||||
, {burst, sc(burst_rate(),
|
||||
#{default => "0/0s",
|
||||
desc => "The burst, This value is based on rate.<br/>
|
||||
This value + rate = the maximum limit that can be achieved when limiter burst."
|
||||
})}
|
||||
, {bucket, sc(map("bucket name", ref(bucket_opts)), #{desc => "Buckets config"})}
|
||||
[
|
||||
{rate, sc(rate(), #{default => "infinity", desc => "The rate"})},
|
||||
{burst,
|
||||
sc(
|
||||
burst_rate(),
|
||||
#{
|
||||
default => "0/0s",
|
||||
desc =>
|
||||
"The burst, This value is based on rate.<br/>\n"
|
||||
" This value + rate = the maximum limit that can be achieved when limiter burst."
|
||||
}
|
||||
)},
|
||||
{bucket, sc(map("bucket name", ref(bucket_opts)), #{desc => "Buckets config"})}
|
||||
];
|
||||
|
||||
fields(bucket_opts) ->
|
||||
[ {rate, sc(rate(), #{desc => "Rate for this bucket."})}
|
||||
, {capacity, sc(capacity(), #{desc => "The maximum number of tokens for this bucket."})}
|
||||
, {initial, sc(initial(), #{default => "0",
|
||||
desc => "The initial number of tokens for this bucket."})}
|
||||
, {per_client, sc(ref(client_bucket),
|
||||
#{default => #{},
|
||||
desc => "The rate limit for each user of the bucket,"
|
||||
[
|
||||
{rate, sc(rate(), #{desc => "Rate for this bucket."})},
|
||||
{capacity, sc(capacity(), #{desc => "The maximum number of tokens for this bucket."})},
|
||||
{initial,
|
||||
sc(initial(), #{
|
||||
default => "0",
|
||||
desc => "The initial number of tokens for this bucket."
|
||||
})},
|
||||
{per_client,
|
||||
sc(
|
||||
ref(client_bucket),
|
||||
#{
|
||||
default => #{},
|
||||
desc =>
|
||||
"The rate limit for each user of the bucket,"
|
||||
" this field is not required"
|
||||
})}
|
||||
}
|
||||
)}
|
||||
];
|
||||
|
||||
fields(client_bucket) ->
|
||||
[ {rate, sc(rate(), #{default => "infinity", desc => "Rate for this bucket."})}
|
||||
, {initial, sc(initial(), #{default => "0", desc => "The initial number of tokens for this bucket."})}
|
||||
[
|
||||
{rate, sc(rate(), #{default => "infinity", desc => "Rate for this bucket."})},
|
||||
{initial,
|
||||
sc(initial(), #{default => "0", desc => "The initial number of tokens for this bucket."})},
|
||||
%% low_water_mark add for emqx_channel and emqx_session
|
||||
%% both modules consume first and then check
|
||||
%% so we need to use this value to prevent excessive consumption
|
||||
%% (e.g, consumption from an empty bucket)
|
||||
, {low_water_mark, sc(initial(),
|
||||
#{desc => "If the remaining tokens are lower than this value,
|
||||
the check/consume will succeed, but it will be forced to wait for a short period of time.",
|
||||
default => "0"})}
|
||||
, {capacity, sc(capacity(), #{desc => "The capacity of the token bucket.",
|
||||
default => "infinity"})}
|
||||
, {divisible, sc(boolean(),
|
||||
#{desc => "Is it possible to split the number of requested tokens?",
|
||||
default => false})}
|
||||
, {max_retry_time, sc(emqx_schema:duration(),
|
||||
#{ desc => "The maximum retry time when acquire failed."
|
||||
, default => "10s"})}
|
||||
, {failure_strategy, sc(failure_strategy(),
|
||||
#{ desc => "The strategy when all the retries failed."
|
||||
, default => force})}
|
||||
{low_water_mark,
|
||||
sc(
|
||||
initial(),
|
||||
#{
|
||||
desc =>
|
||||
"If the remaining tokens are lower than this value,\n"
|
||||
"the check/consume will succeed, but it will be forced to wait for a short period of time.",
|
||||
default => "0"
|
||||
}
|
||||
)},
|
||||
{capacity,
|
||||
sc(capacity(), #{
|
||||
desc => "The capacity of the token bucket.",
|
||||
default => "infinity"
|
||||
})},
|
||||
{divisible,
|
||||
sc(
|
||||
boolean(),
|
||||
#{
|
||||
desc => "Is it possible to split the number of requested tokens?",
|
||||
default => false
|
||||
}
|
||||
)},
|
||||
{max_retry_time,
|
||||
sc(
|
||||
emqx_schema:duration(),
|
||||
#{
|
||||
desc => "The maximum retry time when acquire failed.",
|
||||
default => "10s"
|
||||
}
|
||||
)},
|
||||
{failure_strategy,
|
||||
sc(
|
||||
failure_strategy(),
|
||||
#{
|
||||
desc => "The strategy when all the retries failed.",
|
||||
default => force
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
desc(limiter) ->
|
||||
|
|
@ -184,15 +268,23 @@ to_rate(Str, CanInfinity, CanZero) ->
|
|||
case Tokens of
|
||||
["infinity"] when CanInfinity ->
|
||||
{ok, infinity};
|
||||
[QuotaStr] -> %% if time unit is 1s, it can be omitted
|
||||
%% if time unit is 1s, it can be omitted
|
||||
[QuotaStr] ->
|
||||
{ok, Val} = to_capacity(QuotaStr),
|
||||
check_capacity(Str, Val, CanZero,
|
||||
check_capacity(
|
||||
Str,
|
||||
Val,
|
||||
CanZero,
|
||||
fun(Quota) ->
|
||||
{ok, Quota * minimum_period() / ?UNIT_TIME_IN_MS}
|
||||
end);
|
||||
end
|
||||
);
|
||||
[QuotaStr, Interval] ->
|
||||
{ok, Val} = to_capacity(QuotaStr),
|
||||
check_capacity(Str, Val, CanZero,
|
||||
check_capacity(
|
||||
Str,
|
||||
Val,
|
||||
CanZero,
|
||||
fun(Quota) ->
|
||||
case emqx_schema:to_duration_ms(Interval) of
|
||||
{ok, Ms} when Ms > 0 ->
|
||||
|
|
@ -200,17 +292,16 @@ to_rate(Str, CanInfinity, CanZero) ->
|
|||
_ ->
|
||||
{error, Str}
|
||||
end
|
||||
end);
|
||||
end
|
||||
);
|
||||
_ ->
|
||||
{error, Str}
|
||||
end.
|
||||
|
||||
check_capacity(_Str, 0, true, _Cont) ->
|
||||
{ok, 0};
|
||||
|
||||
check_capacity(Str, 0, false, _Cont) ->
|
||||
{error, Str};
|
||||
|
||||
check_capacity(_Str, Quota, _CanZero, Cont) ->
|
||||
Cont(Quota).
|
||||
|
||||
|
|
|
|||
|
|
@ -30,34 +30,53 @@
|
|||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3, format_status/2]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3,
|
||||
format_status/2
|
||||
]).
|
||||
|
||||
-export([ start_link/1, connect/2, info/1
|
||||
, name/1, get_initial_val/1, update_config/1
|
||||
]).
|
||||
-export([
|
||||
start_link/1,
|
||||
connect/2,
|
||||
info/1,
|
||||
name/1,
|
||||
get_initial_val/1,
|
||||
update_config/1
|
||||
]).
|
||||
|
||||
-type root() :: #{ rate := rate() %% number of tokens generated per period
|
||||
, burst := rate()
|
||||
, period := pos_integer() %% token generation interval(second)
|
||||
, consumed := non_neg_integer()
|
||||
}.
|
||||
%% number of tokens generated per period
|
||||
-type root() :: #{
|
||||
rate := rate(),
|
||||
burst := rate(),
|
||||
%% token generation interval(second)
|
||||
period := pos_integer(),
|
||||
consumed := non_neg_integer()
|
||||
}.
|
||||
|
||||
-type bucket() :: #{ name := bucket_name()
|
||||
, rate := rate()
|
||||
, obtained := non_neg_integer()
|
||||
, correction := emqx_limiter_decimal:zero_or_float() %% token correction value
|
||||
, capacity := capacity()
|
||||
, counter := undefined | counters:counters_ref()
|
||||
, index := undefined | index()
|
||||
}.
|
||||
-type bucket() :: #{
|
||||
name := bucket_name(),
|
||||
rate := rate(),
|
||||
obtained := non_neg_integer(),
|
||||
%% token correction value
|
||||
correction := emqx_limiter_decimal:zero_or_float(),
|
||||
capacity := capacity(),
|
||||
counter := undefined | counters:counters_ref(),
|
||||
index := undefined | index()
|
||||
}.
|
||||
|
||||
-type state() :: #{ type := limiter_type()
|
||||
, root := undefined | root()
|
||||
, buckets := buckets()
|
||||
, counter := undefined | counters:counters_ref() %% current counter to alloc
|
||||
, index := index()
|
||||
}.
|
||||
-type state() :: #{
|
||||
type := limiter_type(),
|
||||
root := undefined | root(),
|
||||
buckets := buckets(),
|
||||
%% current counter to alloc
|
||||
counter := undefined | counters:counters_ref(),
|
||||
index := index()
|
||||
}.
|
||||
|
||||
-type buckets() :: #{bucket_name() => bucket()}.
|
||||
-type limiter_type() :: emqx_limiter_schema:limiter_type().
|
||||
|
|
@ -69,7 +88,8 @@
|
|||
-type index() :: pos_integer().
|
||||
|
||||
-define(CALL(Type), gen_server:call(name(Type), ?FUNCTION_NAME)).
|
||||
-define(OVERLOAD_MIN_ALLOC, 0.3). %% minimum coefficient for overloaded limiter
|
||||
%% minimum coefficient for overloaded limiter
|
||||
-define(OVERLOAD_MIN_ALLOC, 0.3).
|
||||
-define(CURRYING(X, F2), fun(Y) -> F2(X, Y) end).
|
||||
|
||||
-export_type([index/0]).
|
||||
|
|
@ -80,25 +100,29 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
-spec connect(limiter_type(),
|
||||
bucket_name() | #{limiter_type() => bucket_name() | undefined}) ->
|
||||
-spec connect(
|
||||
limiter_type(),
|
||||
bucket_name() | #{limiter_type() => bucket_name() | undefined}
|
||||
) ->
|
||||
emqx_htb_limiter:limiter().
|
||||
%% If no bucket path is set in config, there will be no limit
|
||||
connect(_Type, undefined) ->
|
||||
emqx_htb_limiter:make_infinity_limiter();
|
||||
|
||||
connect(Type, BucketName) when is_atom(BucketName) ->
|
||||
CfgPath = emqx_limiter_schema:get_bucket_cfg_path(Type, BucketName),
|
||||
case emqx:get_config(CfgPath, undefined) of
|
||||
undefined ->
|
||||
?SLOG(error, #{msg => "bucket_config_not_found", path => CfgPath}),
|
||||
throw("bucket's config not found");
|
||||
#{rate := AggrRate,
|
||||
#{
|
||||
rate := AggrRate,
|
||||
capacity := AggrSize,
|
||||
per_client := #{rate := CliRate, capacity := CliSize} = Cfg} ->
|
||||
per_client := #{rate := CliRate, capacity := CliSize} = Cfg
|
||||
} ->
|
||||
case emqx_limiter_manager:find_bucket(Type, BucketName) of
|
||||
{ok, Bucket} ->
|
||||
if CliRate < AggrRate orelse CliSize < AggrSize ->
|
||||
if
|
||||
CliRate < AggrRate orelse CliSize < AggrSize ->
|
||||
emqx_htb_limiter:make_token_bucket_limiter(Cfg, Bucket);
|
||||
Bucket =:= infinity ->
|
||||
emqx_htb_limiter:make_infinity_limiter();
|
||||
|
|
@ -110,7 +134,6 @@ connect(Type, BucketName) when is_atom(BucketName) ->
|
|||
throw("invalid bucket")
|
||||
end
|
||||
end;
|
||||
|
||||
connect(Type, Paths) ->
|
||||
connect(Type, maps:get(Type, Paths, undefined)).
|
||||
|
||||
|
|
@ -135,7 +158,6 @@ update_config(Type) ->
|
|||
start_link(Type) ->
|
||||
gen_server:start_link({local, name(Type)}, ?MODULE, [Type], []).
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%% gen_server callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
@ -146,11 +168,12 @@ start_link(Type) ->
|
|||
%% Initializes the server
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec init(Args :: term()) -> {ok, State :: term()} |
|
||||
{ok, State :: term(), Timeout :: timeout()} |
|
||||
{ok, State :: term(), hibernate} |
|
||||
{stop, Reason :: term()} |
|
||||
ignore.
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, State :: term()}
|
||||
| {ok, State :: term(), Timeout :: timeout()}
|
||||
| {ok, State :: term(), hibernate}
|
||||
| {stop, Reason :: term()}
|
||||
| ignore.
|
||||
init([Type]) ->
|
||||
State = init_tree(Type),
|
||||
#{root := #{period := Perido}} = State,
|
||||
|
|
@ -164,21 +187,19 @@ init([Type]) ->
|
|||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
|
||||
{reply, Reply :: term(), NewState :: term()} |
|
||||
{reply, Reply :: term(), NewState :: term(), Timeout :: timeout()} |
|
||||
{reply, Reply :: term(), NewState :: term(), hibernate} |
|
||||
{noreply, NewState :: term()} |
|
||||
{noreply, NewState :: term(), Timeout :: timeout()} |
|
||||
{noreply, NewState :: term(), hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: term()} |
|
||||
{stop, Reason :: term(), NewState :: term()}.
|
||||
{reply, Reply :: term(), NewState :: term()}
|
||||
| {reply, Reply :: term(), NewState :: term(), Timeout :: timeout()}
|
||||
| {reply, Reply :: term(), NewState :: term(), hibernate}
|
||||
| {noreply, NewState :: term()}
|
||||
| {noreply, NewState :: term(), Timeout :: timeout()}
|
||||
| {noreply, NewState :: term(), hibernate}
|
||||
| {stop, Reason :: term(), Reply :: term(), NewState :: term()}
|
||||
| {stop, Reason :: term(), NewState :: term()}.
|
||||
handle_call(info, _From, State) ->
|
||||
{reply, State, State};
|
||||
|
||||
handle_call(update_config, _From, #{type := Type}) ->
|
||||
NewState = init_tree(Type),
|
||||
{reply, ok, NewState};
|
||||
|
||||
handle_call(Req, _From, State) ->
|
||||
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||
{reply, ignored, State}.
|
||||
|
|
@ -190,10 +211,10 @@ handle_call(Req, _From, State) ->
|
|||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_cast(Request :: term(), State :: term()) ->
|
||||
{noreply, NewState :: term()} |
|
||||
{noreply, NewState :: term(), Timeout :: timeout()} |
|
||||
{noreply, NewState :: term(), hibernate} |
|
||||
{stop, Reason :: term(), NewState :: term()}.
|
||||
{noreply, NewState :: term()}
|
||||
| {noreply, NewState :: term(), Timeout :: timeout()}
|
||||
| {noreply, NewState :: term(), hibernate}
|
||||
| {stop, Reason :: term(), NewState :: term()}.
|
||||
handle_cast(Req, State) ->
|
||||
?SLOG(error, #{msg => "unexpected_cast", cast => Req}),
|
||||
{noreply, State}.
|
||||
|
|
@ -205,13 +226,12 @@ handle_cast(Req, State) ->
|
|||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_info(Info :: timeout() | term(), State :: term()) ->
|
||||
{noreply, NewState :: term()} |
|
||||
{noreply, NewState :: term(), Timeout :: timeout()} |
|
||||
{noreply, NewState :: term(), hibernate} |
|
||||
{stop, Reason :: normal | term(), NewState :: term()}.
|
||||
{noreply, NewState :: term()}
|
||||
| {noreply, NewState :: term(), Timeout :: timeout()}
|
||||
| {noreply, NewState :: term(), hibernate}
|
||||
| {stop, Reason :: normal | term(), NewState :: term()}.
|
||||
handle_info(oscillate, State) ->
|
||||
{noreply, oscillation(State)};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||
{noreply, State}.
|
||||
|
|
@ -225,8 +245,10 @@ handle_info(Info, State) ->
|
|||
%% with Reason. The return value is ignored.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(),
|
||||
State :: term()) -> any().
|
||||
-spec terminate(
|
||||
Reason :: normal | shutdown | {shutdown, term()} | term(),
|
||||
State :: term()
|
||||
) -> any().
|
||||
terminate(_Reason, _State) ->
|
||||
ok.
|
||||
|
||||
|
|
@ -236,10 +258,13 @@ terminate(_Reason, _State) ->
|
|||
%% Convert process state when code is changed
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec code_change(OldVsn :: term() | {down, term()},
|
||||
-spec code_change(
|
||||
OldVsn :: term() | {down, term()},
|
||||
State :: term(),
|
||||
Extra :: term()) -> {ok, NewState :: term()} |
|
||||
{error, Reason :: term()}.
|
||||
Extra :: term()
|
||||
) ->
|
||||
{ok, NewState :: term()}
|
||||
| {error, Reason :: term()}.
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
|
|
@ -251,8 +276,10 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
%% or when it appears in termination error logs.
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec format_status(Opt :: normal | terminate,
|
||||
Status :: list()) -> Status :: term().
|
||||
-spec format_status(
|
||||
Opt :: normal | terminate,
|
||||
Status :: list()
|
||||
) -> Status :: term().
|
||||
format_status(_Opt, Status) ->
|
||||
Status.
|
||||
|
||||
|
|
@ -264,40 +291,54 @@ oscillate(Interval) ->
|
|||
|
||||
%% @doc generate tokens, and then spread to leaf nodes
|
||||
-spec oscillation(state()) -> state().
|
||||
oscillation(#{root := #{rate := Flow,
|
||||
oscillation(
|
||||
#{
|
||||
root := #{
|
||||
rate := Flow,
|
||||
period := Interval,
|
||||
consumed := Consumed} = Root,
|
||||
buckets := Buckets} = State) ->
|
||||
consumed := Consumed
|
||||
} = Root,
|
||||
buckets := Buckets
|
||||
} = State
|
||||
) ->
|
||||
oscillate(Interval),
|
||||
Ordereds = get_ordered_buckets(Buckets),
|
||||
{Alloced, Buckets2} = transverse(Ordereds, Flow, 0, Buckets),
|
||||
maybe_burst(State#{buckets := Buckets2,
|
||||
root := Root#{consumed := Consumed + Alloced}}).
|
||||
maybe_burst(State#{
|
||||
buckets := Buckets2,
|
||||
root := Root#{consumed := Consumed + Alloced}
|
||||
}).
|
||||
|
||||
%% @doc horizontal spread
|
||||
-spec transverse(list(bucket()),
|
||||
-spec transverse(
|
||||
list(bucket()),
|
||||
flow(),
|
||||
non_neg_integer(),
|
||||
buckets()) -> {non_neg_integer(), buckets()}.
|
||||
buckets()
|
||||
) -> {non_neg_integer(), buckets()}.
|
||||
transverse([H | T], InFlow, Alloced, Buckets) when InFlow > 0 ->
|
||||
{BucketAlloced, Buckets2} = longitudinal(H, InFlow, Buckets),
|
||||
InFlow2 = sub(InFlow, BucketAlloced),
|
||||
Alloced2 = Alloced + BucketAlloced,
|
||||
transverse(T, InFlow2, Alloced2, Buckets2);
|
||||
|
||||
transverse(_, _, Alloced, Buckets) ->
|
||||
{Alloced, Buckets}.
|
||||
|
||||
%% @doc vertical spread
|
||||
-spec longitudinal(bucket(), flow(), buckets()) ->
|
||||
{non_neg_integer(), buckets()}.
|
||||
longitudinal(#{name := Name,
|
||||
longitudinal(
|
||||
#{
|
||||
name := Name,
|
||||
rate := Rate,
|
||||
capacity := Capacity,
|
||||
counter := Counter,
|
||||
index := Index,
|
||||
obtained := Obtained} = Bucket,
|
||||
InFlow, Buckets) when Counter =/= undefined ->
|
||||
obtained := Obtained
|
||||
} = Bucket,
|
||||
InFlow,
|
||||
Buckets
|
||||
) when Counter =/= undefined ->
|
||||
Flow = erlang:min(InFlow, Rate),
|
||||
|
||||
ShouldAlloc =
|
||||
|
|
@ -305,8 +346,10 @@ longitudinal(#{name := Name,
|
|||
Tokens when Tokens < 0 ->
|
||||
%% toknes's value mayb be a negative value(stolen from the future)
|
||||
%% because ∃ x. add(Capacity, x) < 0, so here we must compare with minimum value
|
||||
erlang:max(add(Capacity, Tokens),
|
||||
mul(Capacity, ?OVERLOAD_MIN_ALLOC));
|
||||
erlang:max(
|
||||
add(Capacity, Tokens),
|
||||
mul(Capacity, ?OVERLOAD_MIN_ALLOC)
|
||||
);
|
||||
Tokens ->
|
||||
%% is it possible that Tokens > Capacity ???
|
||||
erlang:max(sub(Capacity, Tokens), 0)
|
||||
|
|
@ -320,12 +363,10 @@ longitudinal(#{name := Name,
|
|||
{Inc, Bucket2} = emqx_limiter_correction:add(Available, Bucket),
|
||||
counters:add(Counter, Index, Inc),
|
||||
|
||||
{Inc,
|
||||
Buckets#{Name := Bucket2#{obtained := Obtained + Inc}}};
|
||||
{Inc, Buckets#{Name := Bucket2#{obtained := Obtained + Inc}}};
|
||||
_ ->
|
||||
{0, Buckets}
|
||||
end;
|
||||
|
||||
longitudinal(_, _, Buckets) ->
|
||||
{0, Buckets}.
|
||||
|
||||
|
|
@ -333,18 +374,24 @@ longitudinal(_, _, Buckets) ->
|
|||
get_ordered_buckets(Buckets) when is_map(Buckets) ->
|
||||
BucketList = maps:values(Buckets),
|
||||
get_ordered_buckets(BucketList);
|
||||
|
||||
get_ordered_buckets(Buckets) ->
|
||||
%% sort by obtained, avoid node goes hungry
|
||||
lists:sort(fun(#{obtained := A}, #{obtained := B}) ->
|
||||
lists:sort(
|
||||
fun(#{obtained := A}, #{obtained := B}) ->
|
||||
A < B
|
||||
end,
|
||||
Buckets).
|
||||
Buckets
|
||||
).
|
||||
|
||||
-spec maybe_burst(state()) -> state().
|
||||
maybe_burst(#{buckets := Buckets,
|
||||
root := #{burst := Burst}} = State) when Burst > 0 ->
|
||||
Fold = fun(_Name, #{counter := Cnt, index := Idx} = Bucket, Acc) when Cnt =/= undefined ->
|
||||
maybe_burst(
|
||||
#{
|
||||
buckets := Buckets,
|
||||
root := #{burst := Burst}
|
||||
} = State
|
||||
) when Burst > 0 ->
|
||||
Fold = fun
|
||||
(_Name, #{counter := Cnt, index := Idx} = Bucket, Acc) when Cnt =/= undefined ->
|
||||
case counters:get(Cnt, Idx) > 0 of
|
||||
true ->
|
||||
Acc;
|
||||
|
|
@ -357,52 +404,59 @@ maybe_burst(#{buckets := Buckets,
|
|||
|
||||
Empties = maps:fold(Fold, [], Buckets),
|
||||
dispatch_burst(Empties, Burst, State);
|
||||
|
||||
maybe_burst(State) ->
|
||||
State.
|
||||
|
||||
-spec dispatch_burst(list(bucket()), non_neg_integer(), state()) -> state().
|
||||
dispatch_burst([], _, State) ->
|
||||
State;
|
||||
|
||||
dispatch_burst(Empties, InFlow,
|
||||
#{root := #{consumed := Consumed} = Root, buckets := Buckets} = State) ->
|
||||
dispatch_burst(
|
||||
Empties,
|
||||
InFlow,
|
||||
#{root := #{consumed := Consumed} = Root, buckets := Buckets} = State
|
||||
) ->
|
||||
EachFlow = InFlow / erlang:length(Empties),
|
||||
{Alloced, Buckets2} = dispatch_burst_to_buckets(Empties, EachFlow, 0, Buckets),
|
||||
State#{root := Root#{consumed := Consumed + Alloced}, buckets := Buckets2}.
|
||||
|
||||
-spec dispatch_burst_to_buckets(list(bucket()),
|
||||
-spec dispatch_burst_to_buckets(
|
||||
list(bucket()),
|
||||
float(),
|
||||
non_neg_integer(), buckets()) -> {non_neg_integer(), buckets()}.
|
||||
non_neg_integer(),
|
||||
buckets()
|
||||
) -> {non_neg_integer(), buckets()}.
|
||||
dispatch_burst_to_buckets([Bucket | T], InFlow, Alloced, Buckets) ->
|
||||
#{name := Name,
|
||||
#{
|
||||
name := Name,
|
||||
counter := Counter,
|
||||
index := Index,
|
||||
obtained := Obtained} = Bucket,
|
||||
obtained := Obtained
|
||||
} = Bucket,
|
||||
{Inc, Bucket2} = emqx_limiter_correction:add(InFlow, Bucket),
|
||||
|
||||
counters:add(Counter, Index, Inc),
|
||||
|
||||
Buckets2 = Buckets#{Name := Bucket2#{obtained := Obtained + Inc}},
|
||||
dispatch_burst_to_buckets(T, InFlow, Alloced + Inc, Buckets2);
|
||||
|
||||
dispatch_burst_to_buckets([], _, Alloced, Buckets) ->
|
||||
{Alloced, Buckets}.
|
||||
|
||||
-spec init_tree(emqx_limiter_schema:limiter_type()) -> state().
|
||||
init_tree(Type) ->
|
||||
State = #{ type => Type
|
||||
, root => undefined
|
||||
, counter => undefined
|
||||
, index => 1
|
||||
, buckets => #{}
|
||||
State = #{
|
||||
type => Type,
|
||||
root => undefined,
|
||||
counter => undefined,
|
||||
index => 1,
|
||||
buckets => #{}
|
||||
},
|
||||
|
||||
#{bucket := Buckets} = Cfg = emqx:get_config([limiter, Type]),
|
||||
{Factor, Root} = make_root(Cfg),
|
||||
{CounterNum, DelayBuckets} = make_bucket(maps:to_list(Buckets), Type, Cfg, Factor, 1, []),
|
||||
|
||||
State2 = State#{root := Root,
|
||||
State2 = State#{
|
||||
root := Root,
|
||||
counter := counters:new(CounterNum, [write_concurrency])
|
||||
},
|
||||
|
||||
|
|
@ -410,18 +464,21 @@ init_tree(Type) ->
|
|||
|
||||
-spec make_root(hocons:confg()) -> {number(), root()}.
|
||||
make_root(#{rate := Rate, burst := Burst}) when Rate >= 1 ->
|
||||
{1, #{rate => Rate,
|
||||
{1, #{
|
||||
rate => Rate,
|
||||
burst => Burst,
|
||||
period => emqx_limiter_schema:minimum_period(),
|
||||
consumed => 0}};
|
||||
|
||||
consumed => 0
|
||||
}};
|
||||
make_root(#{rate := Rate, burst := Burst}) ->
|
||||
MiniPeriod = emqx_limiter_schema:minimum_period(),
|
||||
Factor = 1 / Rate,
|
||||
{Factor, #{rate => 1,
|
||||
{Factor, #{
|
||||
rate => 1,
|
||||
burst => Burst * Factor,
|
||||
period => erlang:floor(Factor * MiniPeriod),
|
||||
consumed => 0}}.
|
||||
consumed => 0
|
||||
}}.
|
||||
|
||||
make_bucket([{Name, Conf} | T], Type, GlobalCfg, Factor, CounterNum, DelayBuckets) ->
|
||||
Path = emqx_limiter_manager:make_path(Type, Name),
|
||||
|
|
@ -447,34 +504,52 @@ make_bucket([{Name, Conf} | T], Type, GlobalCfg, Factor, CounterNum, DelayBucket
|
|||
end
|
||||
end,
|
||||
|
||||
Bucket = #{ name => Name
|
||||
, rate => Rate
|
||||
, obtained => 0
|
||||
, correction => 0
|
||||
, capacity => Capacity
|
||||
, counter => undefined
|
||||
, index => undefined},
|
||||
Bucket = #{
|
||||
name => Name,
|
||||
rate => Rate,
|
||||
obtained => 0,
|
||||
correction => 0,
|
||||
capacity => Capacity,
|
||||
counter => undefined,
|
||||
index => undefined
|
||||
},
|
||||
|
||||
DelayInit = ?CURRYING(Bucket, InitFun),
|
||||
|
||||
make_bucket(T,
|
||||
Type, GlobalCfg, Factor, CounterNum2, [DelayInit | DelayBuckets]);
|
||||
|
||||
make_bucket(
|
||||
T,
|
||||
Type,
|
||||
GlobalCfg,
|
||||
Factor,
|
||||
CounterNum2,
|
||||
[DelayInit | DelayBuckets]
|
||||
);
|
||||
make_bucket([], _Type, _Global, _Factor, CounterNum, DelayBuckets) ->
|
||||
{CounterNum, DelayBuckets}.
|
||||
|
||||
-spec alloc_counter(emqx_limiter_manager:path(), rate(), capacity(), state()) ->
|
||||
{counters:counters_ref(), pos_integer(), state()}.
|
||||
alloc_counter(Path, Rate, Initial,
|
||||
#{counter := Counter, index := Index} = State) ->
|
||||
|
||||
alloc_counter(
|
||||
Path,
|
||||
Rate,
|
||||
Initial,
|
||||
#{counter := Counter, index := Index} = State
|
||||
) ->
|
||||
case emqx_limiter_manager:find_bucket(Path) of
|
||||
{ok, #{counter := ECounter,
|
||||
index := EIndex}} when ECounter =/= undefined ->
|
||||
{ok, #{
|
||||
counter := ECounter,
|
||||
index := EIndex
|
||||
}} when ECounter =/= undefined ->
|
||||
init_counter(Path, ECounter, EIndex, Rate, Initial, State);
|
||||
_ ->
|
||||
init_counter(Path, Counter, Index,
|
||||
Rate, Initial, State#{index := Index + 1})
|
||||
init_counter(
|
||||
Path,
|
||||
Counter,
|
||||
Index,
|
||||
Rate,
|
||||
Initial,
|
||||
State#{index := Index + 1}
|
||||
)
|
||||
end.
|
||||
|
||||
init_counter(Path, Counter, Index, Rate, Initial, State) ->
|
||||
|
|
@ -483,21 +558,24 @@ init_counter(Path, Counter, Index, Rate, Initial, State) ->
|
|||
emqx_limiter_manager:insert_bucket(Path, Ref),
|
||||
{Counter, Index, State}.
|
||||
|
||||
|
||||
%% @doc find first limited node
|
||||
get_counter_rate(#{rate := Rate, capacity := Capacity}, _GlobalCfg)
|
||||
when Rate =/= infinity orelse Capacity =/= infinity -> %% TODO maybe no need to check capacity
|
||||
get_counter_rate(#{rate := Rate, capacity := Capacity}, _GlobalCfg) when
|
||||
%% TODO maybe no need to check capacity
|
||||
Rate =/= infinity orelse Capacity =/= infinity
|
||||
->
|
||||
Rate;
|
||||
|
||||
get_counter_rate(_Cfg, #{rate := Rate}) ->
|
||||
Rate.
|
||||
|
||||
-spec get_initial_val(hocons:config()) -> decimal().
|
||||
get_initial_val(#{initial := Initial,
|
||||
get_initial_val(#{
|
||||
initial := Initial,
|
||||
rate := Rate,
|
||||
capacity := Capacity}) ->
|
||||
capacity := Capacity
|
||||
}) ->
|
||||
%% initial will nevner be infinity(see the emqx_limiter_schema)
|
||||
if Initial > 0 ->
|
||||
if
|
||||
Initial > 0 ->
|
||||
Initial;
|
||||
Rate =/= infinity ->
|
||||
erlang:min(Rate, Capacity);
|
||||
|
|
|
|||
|
|
@ -33,11 +33,12 @@
|
|||
%% Starts the supervisor
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec start_link() -> {ok, Pid :: pid()} |
|
||||
{error, {already_started, Pid :: pid()}} |
|
||||
{error, {shutdown, term()}} |
|
||||
{error, term()} |
|
||||
ignore.
|
||||
-spec start_link() ->
|
||||
{ok, Pid :: pid()}
|
||||
| {error, {already_started, Pid :: pid()}}
|
||||
| {error, {shutdown, term()}}
|
||||
| {error, term()}
|
||||
| ignore.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
|
|
@ -67,13 +68,14 @@ restart(Type) ->
|
|||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(),
|
||||
[ChildSpec :: supervisor:child_spec()]}} |
|
||||
ignore.
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore.
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one,
|
||||
SupFlags = #{
|
||||
strategy => one_for_one,
|
||||
intensity => 10,
|
||||
period => 3600},
|
||||
period => 3600
|
||||
},
|
||||
|
||||
{ok, {SupFlags, childs()}}.
|
||||
|
||||
|
|
@ -82,12 +84,14 @@ init([]) ->
|
|||
%%--==================================================================
|
||||
make_child(Type) ->
|
||||
Id = emqx_limiter_server:name(Type),
|
||||
#{id => Id,
|
||||
#{
|
||||
id => Id,
|
||||
start => {emqx_limiter_server, start_link, [Type]},
|
||||
restart => transient,
|
||||
shutdown => 5000,
|
||||
type => worker,
|
||||
modules => [emqx_limiter_server]}.
|
||||
modules => [emqx_limiter_server]
|
||||
}.
|
||||
|
||||
childs() ->
|
||||
Conf = emqx:get_config([limiter]),
|
||||
|
|
|
|||
|
|
@ -33,11 +33,12 @@
|
|||
%% Starts the supervisor
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec start_link() -> {ok, Pid :: pid()} |
|
||||
{error, {already_started, Pid :: pid()}} |
|
||||
{error, {shutdown, term()}} |
|
||||
{error, term()} |
|
||||
ignore.
|
||||
-spec start_link() ->
|
||||
{ok, Pid :: pid()}
|
||||
| {error, {already_started, Pid :: pid()}}
|
||||
| {error, {shutdown, term()}}
|
||||
| {error, term()}
|
||||
| ignore.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
|
|
@ -55,22 +56,27 @@ start_link() ->
|
|||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(),
|
||||
[ChildSpec :: supervisor:child_spec()]}} |
|
||||
ignore.
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore.
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one,
|
||||
SupFlags = #{
|
||||
strategy => one_for_one,
|
||||
intensity => 10,
|
||||
period => 3600},
|
||||
period => 3600
|
||||
},
|
||||
|
||||
Childs = [ make_child(emqx_limiter_manager, worker)
|
||||
, make_child(emqx_limiter_server_sup, supervisor)],
|
||||
Childs = [
|
||||
make_child(emqx_limiter_manager, worker),
|
||||
make_child(emqx_limiter_server_sup, supervisor)
|
||||
],
|
||||
|
||||
{ok, {SupFlags, Childs}}.
|
||||
|
||||
make_child(Mod, Type) ->
|
||||
#{id => Mod,
|
||||
#{
|
||||
id => Mod,
|
||||
start => {Mod, start_link, []},
|
||||
restart => transient,
|
||||
type => Type,
|
||||
modules => [Mod]}.
|
||||
modules => [Mod]
|
||||
}.
|
||||
|
|
|
|||
|
|
@ -24,30 +24,33 @@
|
|||
-boot_mnesia({mnesia, [boot]}).
|
||||
-export([mnesia/1]).
|
||||
|
||||
-export([ publish/1
|
||||
, subscribe/3
|
||||
, unsubscribe/2
|
||||
, log/3
|
||||
]).
|
||||
-export([
|
||||
publish/1,
|
||||
subscribe/3,
|
||||
unsubscribe/2,
|
||||
log/3
|
||||
]).
|
||||
|
||||
-export([ start_link/0
|
||||
, list/0
|
||||
, list/1
|
||||
, get_trace_filename/1
|
||||
, create/1
|
||||
, delete/1
|
||||
, clear/0
|
||||
, update/2
|
||||
, check/0
|
||||
]).
|
||||
-export([
|
||||
start_link/0,
|
||||
list/0,
|
||||
list/1,
|
||||
get_trace_filename/1,
|
||||
create/1,
|
||||
delete/1,
|
||||
clear/0,
|
||||
update/2,
|
||||
check/0
|
||||
]).
|
||||
|
||||
-export([ format/1
|
||||
, zip_dir/0
|
||||
, filename/2
|
||||
, trace_dir/0
|
||||
, trace_file/1
|
||||
, delete_files_after_send/2
|
||||
]).
|
||||
-export([
|
||||
format/1,
|
||||
zip_dir/0,
|
||||
filename/2,
|
||||
trace_dir/0,
|
||||
trace_file/1,
|
||||
delete_files_after_send/2
|
||||
]).
|
||||
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
|
|
@ -56,22 +59,23 @@
|
|||
-define(OWN_KEYS, [level, filters, filter_default, handlers]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-export([ log_file/2
|
||||
, find_closest_time/2
|
||||
]).
|
||||
-export([
|
||||
log_file/2,
|
||||
find_closest_time/2
|
||||
]).
|
||||
-endif.
|
||||
|
||||
-export_type([ip_address/0]).
|
||||
-type ip_address() :: string().
|
||||
|
||||
-record(?TRACE, {
|
||||
name :: binary() | undefined | '_'
|
||||
, type :: clientid | topic | ip_address | undefined | '_'
|
||||
, filter :: emqx_types:topic() | emqx_types:clientid() | ip_address() | undefined | '_'
|
||||
, enable = true :: boolean() | '_'
|
||||
, start_at :: integer() | undefined | '_'
|
||||
, end_at :: integer() | undefined | '_'
|
||||
}).
|
||||
name :: binary() | undefined | '_',
|
||||
type :: clientid | topic | ip_address | undefined | '_',
|
||||
filter :: emqx_types:topic() | emqx_types:clientid() | ip_address() | undefined | '_',
|
||||
enable = true :: boolean() | '_',
|
||||
start_at :: integer() | undefined | '_',
|
||||
end_at :: integer() | undefined | '_'
|
||||
}).
|
||||
|
||||
mnesia(boot) ->
|
||||
ok = mria:create_table(?TRACE, [
|
||||
|
|
@ -79,18 +83,23 @@ mnesia(boot) ->
|
|||
{rlog_shard, ?COMMON_SHARD},
|
||||
{storage, disc_copies},
|
||||
{record_name, ?TRACE},
|
||||
{attributes, record_info(fields, ?TRACE)}]).
|
||||
{attributes, record_info(fields, ?TRACE)}
|
||||
]).
|
||||
|
||||
publish(#message{topic = <<"$SYS/", _/binary>>}) -> ignore;
|
||||
publish(#message{topic = <<"$SYS/", _/binary>>}) ->
|
||||
ignore;
|
||||
publish(#message{from = From, topic = Topic, payload = Payload}) when
|
||||
is_binary(From); is_atom(From) ->
|
||||
is_binary(From); is_atom(From)
|
||||
->
|
||||
?TRACE("PUBLISH", "publish_to", #{topic => Topic, payload => Payload}).
|
||||
|
||||
subscribe(<<"$SYS/", _/binary>>, _SubId, _SubOpts) -> ignore;
|
||||
subscribe(<<"$SYS/", _/binary>>, _SubId, _SubOpts) ->
|
||||
ignore;
|
||||
subscribe(Topic, SubId, SubOpts) ->
|
||||
?TRACE("SUBSCRIBE", "subscribe", #{topic => Topic, sub_opts => SubOpts, sub_id => SubId}).
|
||||
|
||||
unsubscribe(<<"$SYS/", _/binary>>, _SubOpts) -> ignore;
|
||||
unsubscribe(<<"$SYS/", _/binary>>, _SubOpts) ->
|
||||
ignore;
|
||||
unsubscribe(Topic, SubOpts) ->
|
||||
?TRACE("UNSUBSCRIBE", "unsubscribe", #{topic => Topic, sub_opts => SubOpts}).
|
||||
|
||||
|
|
@ -103,36 +112,47 @@ log(List, Msg, Meta0) ->
|
|||
Log = #{level => debug, meta => Meta, msg => Msg},
|
||||
log_filter(List, Log).
|
||||
|
||||
log_filter([], _Log) -> ok;
|
||||
log_filter([], _Log) ->
|
||||
ok;
|
||||
log_filter([{Id, FilterFun, Filter, Name} | Rest], Log0) ->
|
||||
case FilterFun(Log0, {Filter, Name}) of
|
||||
stop -> stop;
|
||||
ignore -> ignore;
|
||||
stop ->
|
||||
stop;
|
||||
ignore ->
|
||||
ignore;
|
||||
Log ->
|
||||
case logger_config:get(ets:whereis(logger), Id) of
|
||||
{ok, #{module := Module} = HandlerConfig0} ->
|
||||
HandlerConfig = maps:without(?OWN_KEYS, HandlerConfig0),
|
||||
try Module:log(Log, HandlerConfig)
|
||||
catch C:R:S ->
|
||||
try
|
||||
Module:log(Log, HandlerConfig)
|
||||
catch
|
||||
C:R:S ->
|
||||
case logger:remove_handler(Id) of
|
||||
ok ->
|
||||
logger:internal_log(error, {removed_failing_handler, Id, C, R, S});
|
||||
{error,{not_found,_}} ->
|
||||
logger:internal_log(
|
||||
error, {removed_failing_handler, Id, C, R, S}
|
||||
);
|
||||
{error, {not_found, _}} ->
|
||||
%% Probably already removed by other client
|
||||
%% Don't report again
|
||||
ok;
|
||||
{error,Reason} ->
|
||||
logger:internal_log(error,
|
||||
{removed_handler_failed, Id, Reason, C, R, S})
|
||||
{error, Reason} ->
|
||||
logger:internal_log(
|
||||
error,
|
||||
{removed_handler_failed, Id, Reason, C, R, S}
|
||||
)
|
||||
end
|
||||
end;
|
||||
{error, {not_found, Id}} -> ok;
|
||||
{error, Reason} -> logger:internal_log(error, {find_handle_id_failed, Id, Reason})
|
||||
{error, {not_found, Id}} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
logger:internal_log(error, {find_handle_id_failed, Id, Reason})
|
||||
end
|
||||
end,
|
||||
log_filter(Rest, Log0).
|
||||
|
||||
-spec(start_link() -> emqx_types:startlink_ret()).
|
||||
-spec start_link() -> emqx_types:startlink_ret().
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
|
|
@ -145,7 +165,8 @@ list(Enable) ->
|
|||
ets:match_object(?TRACE, #?TRACE{enable = Enable, _ = '_'}).
|
||||
|
||||
-spec create([{Key :: binary(), Value :: binary()}] | #{atom() => binary()}) ->
|
||||
{ok, #?TRACE{}} | {error, {duplicate_condition, iodata()} | {already_existed, iodata()} | iodata()}.
|
||||
{ok, #?TRACE{}}
|
||||
| {error, {duplicate_condition, iodata()} | {already_existed, iodata()} | iodata()}.
|
||||
create(Trace) ->
|
||||
case mnesia:table_info(?TRACE, size) < ?MAX_SIZE of
|
||||
true ->
|
||||
|
|
@ -154,7 +175,8 @@ create(Trace) ->
|
|||
{error, Reason} -> {error, Reason}
|
||||
end;
|
||||
false ->
|
||||
{error, "The number of traces created has reache the maximum"
|
||||
{error,
|
||||
"The number of traces created has reache the maximum"
|
||||
" please delete the useless ones first"}
|
||||
end.
|
||||
|
||||
|
|
@ -180,8 +202,10 @@ clear() ->
|
|||
update(Name, Enable) ->
|
||||
Tran = fun() ->
|
||||
case mnesia:read(?TRACE, Name) of
|
||||
[] -> mnesia:abort(not_found);
|
||||
[#?TRACE{enable = Enable}] -> ok;
|
||||
[] ->
|
||||
mnesia:abort(not_found);
|
||||
[#?TRACE{enable = Enable}] ->
|
||||
ok;
|
||||
[Rec] ->
|
||||
case erlang:system_time(second) >= Rec#?TRACE.end_at of
|
||||
false -> mnesia:write(?TRACE, Rec#?TRACE{enable = Enable}, write);
|
||||
|
|
@ -201,12 +225,13 @@ get_trace_filename(Name) ->
|
|||
case mnesia:read(?TRACE, Name, read) of
|
||||
[] -> mnesia:abort(not_found);
|
||||
[#?TRACE{start_at = Start}] -> {ok, filename(Name, Start)}
|
||||
end end,
|
||||
end
|
||||
end,
|
||||
transaction(Tran).
|
||||
|
||||
-spec trace_file(File :: file:filename_all()) ->
|
||||
{ok, Node :: list(), Binary :: binary()} |
|
||||
{error, Node :: list(), Reason :: term()}.
|
||||
{ok, Node :: list(), Binary :: binary()}
|
||||
| {error, Node :: list(), Reason :: term()}.
|
||||
trace_file(File) ->
|
||||
FileName = filename:join(trace_dir(), File),
|
||||
Node = atom_to_list(node()),
|
||||
|
|
@ -221,10 +246,13 @@ delete_files_after_send(TraceLog, Zips) ->
|
|||
-spec format(list(#?TRACE{})) -> list(map()).
|
||||
format(Traces) ->
|
||||
Fields = record_info(fields, ?TRACE),
|
||||
lists:map(fun(Trace0 = #?TRACE{}) ->
|
||||
lists:map(
|
||||
fun(Trace0 = #?TRACE{}) ->
|
||||
[_ | Values] = tuple_to_list(Trace0),
|
||||
maps:from_list(lists:zip(Fields, Values))
|
||||
end, Traces).
|
||||
end,
|
||||
Traces
|
||||
).
|
||||
|
||||
init([]) ->
|
||||
ok = mria:wait_for_tables([?TRACE]),
|
||||
|
|
@ -253,7 +281,8 @@ handle_cast(Msg, State) ->
|
|||
|
||||
handle_info({'DOWN', _Ref, process, Pid, _Reason}, State = #{monitors := Monitors}) ->
|
||||
case maps:take(Pid, Monitors) of
|
||||
error -> {noreply, State};
|
||||
error ->
|
||||
{noreply, State};
|
||||
{Files, NewMonitors} ->
|
||||
lists:foreach(fun file:delete/1, Files),
|
||||
{noreply, State#{monitors => NewMonitors}}
|
||||
|
|
@ -264,11 +293,9 @@ handle_info({timeout, TRef, update_trace}, #{timer := TRef} = State) ->
|
|||
update_trace_handler(),
|
||||
?tp(update_trace_done, #{}),
|
||||
{noreply, State#{timer => NextTRef}};
|
||||
|
||||
handle_info({mnesia_table_event, _Events}, State = #{timer := TRef}) ->
|
||||
emqx_misc:cancel_timer(TRef),
|
||||
handle_info({timeout, TRef, update_trace}, State);
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?SLOG(error, #{unexpected_info => Info}),
|
||||
{noreply, State}.
|
||||
|
|
@ -294,9 +321,11 @@ insert_new_trace(Trace) ->
|
|||
[] ->
|
||||
ok = mnesia:write(?TRACE, Trace, write),
|
||||
{ok, Trace};
|
||||
[#?TRACE{name = Name}] -> mnesia:abort({duplicate_condition, Name})
|
||||
[#?TRACE{name = Name}] ->
|
||||
mnesia:abort({duplicate_condition, Name})
|
||||
end;
|
||||
[#?TRACE{name = Name}] -> mnesia:abort({already_existed, Name})
|
||||
[#?TRACE{name = Name}] ->
|
||||
mnesia:abort({already_existed, Name})
|
||||
end
|
||||
end,
|
||||
transaction(Tran).
|
||||
|
|
@ -314,8 +343,10 @@ update_trace(Traces) ->
|
|||
emqx_misc:start_timer(NextTime, update_trace).
|
||||
|
||||
stop_all_trace_handler() ->
|
||||
lists:foreach(fun(#{id := Id}) -> emqx_trace_handler:uninstall(Id) end,
|
||||
emqx_trace_handler:running()).
|
||||
lists:foreach(
|
||||
fun(#{id := Id}) -> emqx_trace_handler:uninstall(Id) end,
|
||||
emqx_trace_handler:running()
|
||||
).
|
||||
get_enable_trace() ->
|
||||
transaction(fun() ->
|
||||
mnesia:match_object(?TRACE, #?TRACE{enable = true, _ = '_'}, read)
|
||||
|
|
@ -324,57 +355,79 @@ get_enable_trace() ->
|
|||
find_closest_time(Traces, Now) ->
|
||||
Sec =
|
||||
lists:foldl(
|
||||
fun(#?TRACE{start_at = Start, end_at = End, enable = true}, Closest) ->
|
||||
fun
|
||||
(#?TRACE{start_at = Start, end_at = End, enable = true}, Closest) ->
|
||||
min(closest(End, Now, Closest), closest(Start, Now, Closest));
|
||||
(_, Closest) -> Closest
|
||||
end, 60 * 15, Traces),
|
||||
(_, Closest) ->
|
||||
Closest
|
||||
end,
|
||||
60 * 15,
|
||||
Traces
|
||||
),
|
||||
timer:seconds(Sec).
|
||||
|
||||
closest(Time, Now, Closest) when Now >= Time -> Closest;
|
||||
closest(Time, Now, Closest) -> min(Time - Now, Closest).
|
||||
|
||||
disable_finished([]) -> ok;
|
||||
disable_finished([]) ->
|
||||
ok;
|
||||
disable_finished(Traces) ->
|
||||
transaction(fun() ->
|
||||
lists:map(fun(#?TRACE{name = Name}) ->
|
||||
lists:map(
|
||||
fun(#?TRACE{name = Name}) ->
|
||||
case mnesia:read(?TRACE, Name, write) of
|
||||
[] -> ok;
|
||||
[Trace] -> mnesia:write(?TRACE, Trace#?TRACE{enable = false}, write)
|
||||
end end, Traces)
|
||||
end
|
||||
end,
|
||||
Traces
|
||||
)
|
||||
end).
|
||||
|
||||
start_trace(Traces, Started0) ->
|
||||
Started = lists:map(fun(#{name := Name}) -> Name end, Started0),
|
||||
lists:foldl(fun(#?TRACE{name = Name} = Trace,
|
||||
{Running, StartedAcc}) ->
|
||||
lists:foldl(
|
||||
fun(
|
||||
#?TRACE{name = Name} = Trace,
|
||||
{Running, StartedAcc}
|
||||
) ->
|
||||
case lists:member(Name, StartedAcc) of
|
||||
true -> {[Name | Running], StartedAcc};
|
||||
true ->
|
||||
{[Name | Running], StartedAcc};
|
||||
false ->
|
||||
case start_trace(Trace) of
|
||||
ok -> {[Name | Running], [Name | StartedAcc]};
|
||||
{error, _Reason} -> {[Name | Running], StartedAcc}
|
||||
end
|
||||
end
|
||||
end, {[], Started}, Traces).
|
||||
end,
|
||||
{[], Started},
|
||||
Traces
|
||||
).
|
||||
|
||||
start_trace(Trace) ->
|
||||
#?TRACE{name = Name
|
||||
, type = Type
|
||||
, filter = Filter
|
||||
, start_at = Start
|
||||
#?TRACE{
|
||||
name = Name,
|
||||
type = Type,
|
||||
filter = Filter,
|
||||
start_at = Start
|
||||
} = Trace,
|
||||
Who = #{name => Name, type => Type, filter => Filter},
|
||||
emqx_trace_handler:install(Who, debug, log_file(Name, Start)).
|
||||
|
||||
stop_trace(Finished, Started) ->
|
||||
lists:foreach(fun(#{name := Name, type := Type, filter := Filter}) ->
|
||||
lists:foreach(
|
||||
fun(#{name := Name, type := Type, filter := Filter}) ->
|
||||
case lists:member(Name, Finished) of
|
||||
true ->
|
||||
?TRACE("API", "trace_stopping", #{Type => Filter}),
|
||||
emqx_trace_handler:uninstall(Type, Name);
|
||||
false -> ok
|
||||
false ->
|
||||
ok
|
||||
end
|
||||
end, Started).
|
||||
end,
|
||||
Started
|
||||
).
|
||||
|
||||
clean_stale_trace_files() ->
|
||||
TraceDir = trace_dir(),
|
||||
|
|
@ -383,30 +436,44 @@ clean_stale_trace_files() ->
|
|||
FileFun = fun(#?TRACE{name = Name, start_at = StartAt}) -> filename(Name, StartAt) end,
|
||||
KeepFiles = lists:map(FileFun, list()),
|
||||
case AllFiles -- ["zip" | KeepFiles] of
|
||||
[] -> ok;
|
||||
[] ->
|
||||
ok;
|
||||
DeleteFiles ->
|
||||
DelFun = fun(F) -> file:delete(filename:join(TraceDir, F)) end,
|
||||
lists:foreach(DelFun, DeleteFiles)
|
||||
end;
|
||||
_ -> ok
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
classify_by_time(Traces, Now) ->
|
||||
classify_by_time(Traces, Now, [], [], []).
|
||||
|
||||
classify_by_time([], _Now, Wait, Run, Finish) -> {Wait, Run, Finish};
|
||||
classify_by_time([Trace = #?TRACE{start_at = Start} | Traces],
|
||||
Now, Wait, Run, Finish) when Start > Now ->
|
||||
classify_by_time([], _Now, Wait, Run, Finish) ->
|
||||
{Wait, Run, Finish};
|
||||
classify_by_time(
|
||||
[Trace = #?TRACE{start_at = Start} | Traces],
|
||||
Now,
|
||||
Wait,
|
||||
Run,
|
||||
Finish
|
||||
) when Start > Now ->
|
||||
classify_by_time(Traces, Now, [Trace | Wait], Run, Finish);
|
||||
classify_by_time([Trace = #?TRACE{end_at = End} | Traces],
|
||||
Now, Wait, Run, Finish) when End =< Now ->
|
||||
classify_by_time(
|
||||
[Trace = #?TRACE{end_at = End} | Traces],
|
||||
Now,
|
||||
Wait,
|
||||
Run,
|
||||
Finish
|
||||
) when End =< Now ->
|
||||
classify_by_time(Traces, Now, Wait, Run, [Trace | Finish]);
|
||||
classify_by_time([Trace | Traces], Now, Wait, Run, Finish) ->
|
||||
classify_by_time(Traces, Now, Wait, [Trace | Run], Finish).
|
||||
|
||||
to_trace(TraceParam) ->
|
||||
case to_trace(ensure_map(TraceParam), #?TRACE{}) of
|
||||
{error, Reason} -> {error, Reason};
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
{ok, #?TRACE{name = undefined}} ->
|
||||
{error, "name required"};
|
||||
{ok, #?TRACE{type = undefined}} ->
|
||||
|
|
@ -421,21 +488,31 @@ to_trace(TraceParam) ->
|
|||
end.
|
||||
|
||||
ensure_map(#{} = Trace) ->
|
||||
maps:fold(fun(K, V, Acc) when is_binary(K) -> Acc#{binary_to_existing_atom(K) => V};
|
||||
maps:fold(
|
||||
fun
|
||||
(K, V, Acc) when is_binary(K) -> Acc#{binary_to_existing_atom(K) => V};
|
||||
(K, V, Acc) when is_atom(K) -> Acc#{K => V}
|
||||
end, #{}, Trace);
|
||||
end,
|
||||
#{},
|
||||
Trace
|
||||
);
|
||||
ensure_map(Trace) when is_list(Trace) ->
|
||||
lists:foldl(
|
||||
fun({K, V}, Acc) when is_binary(K) -> Acc#{binary_to_existing_atom(K) => V};
|
||||
fun
|
||||
({K, V}, Acc) when is_binary(K) -> Acc#{binary_to_existing_atom(K) => V};
|
||||
({K, V}, Acc) when is_atom(K) -> Acc#{K => V};
|
||||
(_, Acc) -> Acc
|
||||
end, #{}, Trace).
|
||||
end,
|
||||
#{},
|
||||
Trace
|
||||
).
|
||||
|
||||
fill_default(Trace = #?TRACE{start_at = undefined}) ->
|
||||
fill_default(Trace#?TRACE{start_at = erlang:system_time(second)});
|
||||
fill_default(Trace = #?TRACE{end_at = undefined, start_at = StartAt}) ->
|
||||
fill_default(Trace#?TRACE{end_at = StartAt + 10 * 60});
|
||||
fill_default(Trace) -> Trace.
|
||||
fill_default(Trace) ->
|
||||
Trace.
|
||||
|
||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
||||
|
||||
|
|
@ -452,16 +529,19 @@ to_trace(#{type := topic, topic := Filter} = Trace, Rec) ->
|
|||
ok ->
|
||||
Trace0 = maps:without([type, topic], Trace),
|
||||
to_trace(Trace0, Rec#?TRACE{type = topic, filter = Filter});
|
||||
Error -> Error
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
to_trace(#{type := ip_address, ip_address := Filter} = Trace, Rec) ->
|
||||
case validate_ip_address(Filter) of
|
||||
ok ->
|
||||
Trace0 = maps:without([type, ip_address], Trace),
|
||||
to_trace(Trace0, Rec#?TRACE{type = ip_address, filter = binary_to_list(Filter)});
|
||||
Error -> Error
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
to_trace(#{type := Type}, _Rec) -> {error, io_lib:format("required ~s field", [Type])};
|
||||
to_trace(#{type := Type}, _Rec) ->
|
||||
{error, io_lib:format("required ~s field", [Type])};
|
||||
to_trace(#{start_at := StartAt} = Trace, Rec) ->
|
||||
{ok, Sec} = to_system_second(StartAt),
|
||||
to_trace(maps:remove(start_at, Trace), Rec#?TRACE{start_at = Sec});
|
||||
|
|
@ -473,7 +553,8 @@ to_trace(#{end_at := EndAt} = Trace, Rec) ->
|
|||
{ok, _Sec} ->
|
||||
{error, "end_at time has already passed"}
|
||||
end;
|
||||
to_trace(_, Rec) -> {ok, Rec}.
|
||||
to_trace(_, Rec) ->
|
||||
{ok, Rec}.
|
||||
|
||||
validate_topic(TopicName) ->
|
||||
try emqx_topic:validate(filter, TopicName) of
|
||||
|
|
@ -513,11 +594,22 @@ transaction(Tran) ->
|
|||
|
||||
update_trace_handler() ->
|
||||
case emqx_trace_handler:running() of
|
||||
[] -> persistent_term:erase(?TRACE_FILTER);
|
||||
[] ->
|
||||
persistent_term:erase(?TRACE_FILTER);
|
||||
Running ->
|
||||
List = lists:map(fun(#{id := Id, filter_fun := FilterFun,
|
||||
filter := Filter, name := Name}) ->
|
||||
{Id, FilterFun, Filter, Name} end, Running),
|
||||
List = lists:map(
|
||||
fun(
|
||||
#{
|
||||
id := Id,
|
||||
filter_fun := FilterFun,
|
||||
filter := Filter,
|
||||
name := Name
|
||||
}
|
||||
) ->
|
||||
{Id, FilterFun, Filter, Name}
|
||||
end,
|
||||
Running
|
||||
),
|
||||
case List =/= persistent_term:get(?TRACE_FILTER, undefined) of
|
||||
true -> persistent_term:put(?TRACE_FILTER, List);
|
||||
false -> ok
|
||||
|
|
@ -525,6 +617,9 @@ update_trace_handler() ->
|
|||
end.
|
||||
|
||||
filter_cli_handler(Names) ->
|
||||
lists:filter(fun(Name) ->
|
||||
lists:filter(
|
||||
fun(Name) ->
|
||||
nomatch =:= re:run(Name, "^CLI-+.", [])
|
||||
end, Names).
|
||||
end,
|
||||
Names
|
||||
).
|
||||
|
|
|
|||
|
|
@ -23,14 +23,15 @@
|
|||
-spec format(LogEvent, Config) -> unicode:chardata() when
|
||||
LogEvent :: logger:log_event(),
|
||||
Config :: logger:config().
|
||||
format(#{level := debug, meta := Meta = #{trace_tag := Tag}, msg := Msg},
|
||||
#{payload_encode := PEncode}) ->
|
||||
format(
|
||||
#{level := debug, meta := Meta = #{trace_tag := Tag}, msg := Msg},
|
||||
#{payload_encode := PEncode}
|
||||
) ->
|
||||
Time = calendar:system_time_to_rfc3339(erlang:system_time(second)),
|
||||
ClientId = to_iolist(maps:get(clientid, Meta, "")),
|
||||
Peername = maps:get(peername, Meta, ""),
|
||||
MetaBin = format_meta(Meta, PEncode),
|
||||
[Time, " [", Tag, "] ", ClientId, "@", Peername, " msg: ", Msg, MetaBin, "\n"];
|
||||
|
||||
format(Event, Config) ->
|
||||
emqx_logger_textfmt:format(Event, Config).
|
||||
|
||||
|
|
@ -72,6 +73,10 @@ to_iolist(SubMap) when is_map(SubMap) -> ["[", map_to_iolist(SubMap), "]"];
|
|||
to_iolist(Char) -> emqx_logger_textfmt:try_format_unicode(Char).
|
||||
|
||||
map_to_iolist(Map) ->
|
||||
lists:join(",",
|
||||
lists:map(fun({K, V}) -> [to_iolist(K), ": ", to_iolist(V)] end,
|
||||
maps:to_list(Map))).
|
||||
lists:join(
|
||||
",",
|
||||
lists:map(
|
||||
fun({K, V}) -> [to_iolist(K), ": ", to_iolist(V)] end,
|
||||
maps:to_list(Map)
|
||||
)
|
||||
).
|
||||
|
|
|
|||
|
|
@ -22,19 +22,21 @@
|
|||
-logger_header("[Tracer]").
|
||||
|
||||
%% APIs
|
||||
-export([ running/0
|
||||
, install/3
|
||||
, install/4
|
||||
, install/5
|
||||
, uninstall/1
|
||||
, uninstall/2
|
||||
]).
|
||||
-export([
|
||||
running/0,
|
||||
install/3,
|
||||
install/4,
|
||||
install/5,
|
||||
uninstall/1,
|
||||
uninstall/2
|
||||
]).
|
||||
|
||||
%% For logger handler filters callbacks
|
||||
-export([ filter_clientid/2
|
||||
, filter_topic/2
|
||||
, filter_ip_address/2
|
||||
]).
|
||||
-export([
|
||||
filter_clientid/2,
|
||||
filter_topic/2,
|
||||
filter_ip_address/2
|
||||
]).
|
||||
|
||||
-export([handler_id/2]).
|
||||
-export([payload_encode/0]).
|
||||
|
|
@ -43,7 +45,7 @@
|
|||
name := binary(),
|
||||
type := clientid | topic | ip_address,
|
||||
filter := emqx_types:clientid() | emqx_types:topic() | emqx_trace:ip_address()
|
||||
}.
|
||||
}.
|
||||
|
||||
-define(CONFIG(_LogFile_), #{
|
||||
type => halt,
|
||||
|
|
@ -60,19 +62,23 @@
|
|||
%% APIs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
-spec install(Name :: binary() | list(),
|
||||
-spec install(
|
||||
Name :: binary() | list(),
|
||||
Type :: clientid | topic | ip_address,
|
||||
Filter :: emqx_types:clientid() | emqx_types:topic() | string(),
|
||||
Level :: logger:level() | all,
|
||||
LogFilePath :: string()) -> ok | {error, term()}.
|
||||
LogFilePath :: string()
|
||||
) -> ok | {error, term()}.
|
||||
install(Name, Type, Filter, Level, LogFile) ->
|
||||
Who = #{type => Type, filter => ensure_bin(Filter), name => ensure_bin(Name)},
|
||||
install(Who, Level, LogFile).
|
||||
|
||||
-spec install(Type :: clientid | topic | ip_address,
|
||||
-spec install(
|
||||
Type :: clientid | topic | ip_address,
|
||||
Filter :: emqx_types:clientid() | emqx_types:topic() | string(),
|
||||
Level :: logger:level() | all,
|
||||
LogFilePath :: string()) -> ok | {error, term()}.
|
||||
LogFilePath :: string()
|
||||
) -> ok | {error, term()}.
|
||||
install(Type, Filter, Level, LogFile) ->
|
||||
install(Filter, Type, Filter, Level, LogFile).
|
||||
|
||||
|
|
@ -92,8 +98,10 @@ install(Who = #{name := Name, type := Type}, Level, LogFile) ->
|
|||
show_prompts(Res, Who, "start_trace"),
|
||||
Res.
|
||||
|
||||
-spec uninstall(Type :: clientid | topic | ip_address,
|
||||
Name :: binary() | list()) -> ok | {error, term()}.
|
||||
-spec uninstall(
|
||||
Type :: clientid | topic | ip_address,
|
||||
Name :: binary() | list()
|
||||
) -> ok | {error, term()}.
|
||||
uninstall(Type, Name) ->
|
||||
HandlerId = handler_id(ensure_bin(Name), Type),
|
||||
uninstall(HandlerId).
|
||||
|
|
@ -122,17 +130,20 @@ running() ->
|
|||
-spec filter_clientid(logger:log_event(), {binary(), atom()}) -> logger:log_event() | stop.
|
||||
filter_clientid(#{meta := Meta = #{clientid := ClientId}} = Log, {MatchId, _Name}) ->
|
||||
filter_ret(ClientId =:= MatchId andalso is_trace(Meta), Log);
|
||||
filter_clientid(_Log, _ExpectId) -> stop.
|
||||
filter_clientid(_Log, _ExpectId) ->
|
||||
stop.
|
||||
|
||||
-spec filter_topic(logger:log_event(), {binary(), atom()}) -> logger:log_event() | stop.
|
||||
filter_topic(#{meta := Meta = #{topic := Topic}} = Log, {TopicFilter, _Name}) ->
|
||||
filter_ret(is_trace(Meta) andalso emqx_topic:match(Topic, TopicFilter), Log);
|
||||
filter_topic(_Log, _ExpectId) -> stop.
|
||||
filter_topic(_Log, _ExpectId) ->
|
||||
stop.
|
||||
|
||||
-spec filter_ip_address(logger:log_event(), {string(), atom()}) -> logger:log_event() | stop.
|
||||
filter_ip_address(#{meta := Meta = #{peername := Peername}} = Log, {IP, _Name}) ->
|
||||
filter_ret(is_trace(Meta) andalso lists:prefix(IP, Peername), Log);
|
||||
filter_ip_address(_Log, _ExpectId) -> stop.
|
||||
filter_ip_address(_Log, _ExpectId) ->
|
||||
stop.
|
||||
|
||||
-compile({inline, [is_trace/1, filter_ret/2]}).
|
||||
%% TRUE when is_trace is missing.
|
||||
|
|
@ -150,16 +161,14 @@ filters(#{type := ip_address, filter := Filter, name := Name}) ->
|
|||
[{ip_address, {fun ?MODULE:filter_ip_address/2, {ensure_list(Filter), Name}}}].
|
||||
|
||||
formatter(#{type := _Type}) ->
|
||||
{emqx_trace_formatter,
|
||||
#{
|
||||
{emqx_trace_formatter, #{
|
||||
%% template is for ?SLOG message not ?TRACE.
|
||||
template => [time," [",level,"] ", msg,"\n"],
|
||||
template => [time, " [", level, "] ", msg, "\n"],
|
||||
single_line => true,
|
||||
max_size => unlimited,
|
||||
depth => unlimited,
|
||||
payload_encode => payload_encode()
|
||||
}
|
||||
}.
|
||||
}}.
|
||||
|
||||
filter_traces(#{id := Id, level := Level, dst := Dst, filters := Filters}, Acc) ->
|
||||
Init = #{id => Id, level => Level, dst => Dst},
|
||||
|
|
@ -167,7 +176,8 @@ filter_traces(#{id := Id, level := Level, dst := Dst, filters := Filters}, Acc)
|
|||
[{Type, {FilterFun, {Filter, Name}}}] when
|
||||
Type =:= topic orelse
|
||||
Type =:= clientid orelse
|
||||
Type =:= ip_address ->
|
||||
Type =:= ip_address
|
||||
->
|
||||
[Init#{type => Type, filter => Filter, name => Name, filter_fun => FilterFun} | Acc];
|
||||
_ ->
|
||||
Acc
|
||||
|
|
@ -179,7 +189,7 @@ handler_id(Name, Type) ->
|
|||
try
|
||||
do_handler_id(Name, Type)
|
||||
catch
|
||||
_ : _ ->
|
||||
_:_ ->
|
||||
Hash = emqx_misc:bin2hexstr_a_f(crypto:hash(md5, Name)),
|
||||
do_handler_id(Hash, Type)
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -18,17 +18,18 @@
|
|||
|
||||
-behaviour(emqx_bpapi).
|
||||
|
||||
-export([ introduced_in/0
|
||||
-export([
|
||||
introduced_in/0,
|
||||
|
||||
, forward/3
|
||||
, forward_async/3
|
||||
, list_client_subscriptions/2
|
||||
, list_subscriptions_via_topic/2
|
||||
forward/3,
|
||||
forward_async/3,
|
||||
list_client_subscriptions/2,
|
||||
list_subscriptions_via_topic/2,
|
||||
|
||||
, start_listener/2
|
||||
, stop_listener/2
|
||||
, restart_listener/2
|
||||
]).
|
||||
start_listener/2,
|
||||
stop_listener/2,
|
||||
restart_listener/2
|
||||
]).
|
||||
|
||||
-include("bpapi.hrl").
|
||||
-include("emqx.hrl").
|
||||
|
|
@ -36,7 +37,8 @@
|
|||
introduced_in() ->
|
||||
"5.0.0".
|
||||
|
||||
-spec forward(node(), emqx_types:topic(), emqx_types:delivery()) -> emqx_types:deliver_result()
|
||||
-spec forward(node(), emqx_types:topic(), emqx_types:delivery()) ->
|
||||
emqx_types:deliver_result()
|
||||
| emqx_rpc:badrpc().
|
||||
forward(Node, Topic, Delivery = #delivery{}) when is_binary(Topic) ->
|
||||
emqx_rpc:call(Topic, Node, emqx_broker, dispatch, [Topic, Delivery]).
|
||||
|
|
|
|||
|
|
@ -18,18 +18,19 @@
|
|||
|
||||
-behaviour(emqx_bpapi).
|
||||
|
||||
-export([ introduced_in/0
|
||||
-export([
|
||||
introduced_in/0,
|
||||
|
||||
, lookup_client/2
|
||||
, kickout_client/2
|
||||
lookup_client/2,
|
||||
kickout_client/2,
|
||||
|
||||
, get_chan_stats/2
|
||||
, get_chan_info/2
|
||||
, get_chann_conn_mod/2
|
||||
get_chan_stats/2,
|
||||
get_chan_info/2,
|
||||
get_chann_conn_mod/2,
|
||||
|
||||
, takeover_session/2
|
||||
, kick_session/3
|
||||
]).
|
||||
takeover_session/2,
|
||||
kick_session/3
|
||||
]).
|
||||
|
||||
-include("bpapi.hrl").
|
||||
-include("src/emqx_cm.hrl").
|
||||
|
|
@ -54,7 +55,8 @@ get_chan_stats(ClientId, ChanPid) ->
|
|||
get_chan_info(ClientId, ChanPid) ->
|
||||
rpc:call(node(ChanPid), emqx_cm, do_get_chan_info, [ClientId, ChanPid], ?T_GET_INFO * 2).
|
||||
|
||||
-spec get_chann_conn_mod(emqx_types:clientid(), emqx_cm:chan_pid()) -> module() | undefined | {badrpc, _}.
|
||||
-spec get_chann_conn_mod(emqx_types:clientid(), emqx_cm:chan_pid()) ->
|
||||
module() | undefined | {badrpc, _}.
|
||||
get_chann_conn_mod(ClientId, ChanPid) ->
|
||||
rpc:call(node(ChanPid), emqx_cm, do_get_chann_conn_mod, [ClientId, ChanPid], ?T_GET_INFO * 2).
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@
|
|||
|
||||
-behaviour(emqx_bpapi).
|
||||
|
||||
-export([ introduced_in/0
|
||||
, resume_begin/3
|
||||
, resume_end/3
|
||||
]).
|
||||
-export([
|
||||
introduced_in/0,
|
||||
resume_begin/3,
|
||||
resume_end/3
|
||||
]).
|
||||
|
||||
-include("bpapi.hrl").
|
||||
-include("emqx.hrl").
|
||||
|
|
|
|||
|
|
@ -20,20 +20,21 @@
|
|||
|
||||
-include("bpapi.hrl").
|
||||
|
||||
-export([ introduced_in/0
|
||||
-export([
|
||||
introduced_in/0,
|
||||
|
||||
, is_running/1
|
||||
is_running/1,
|
||||
|
||||
, get_alarms/2
|
||||
, get_stats/1
|
||||
, get_metrics/1
|
||||
get_alarms/2,
|
||||
get_stats/1,
|
||||
get_metrics/1,
|
||||
|
||||
, deactivate_alarm/2
|
||||
, delete_all_deactivated_alarms/1
|
||||
deactivate_alarm/2,
|
||||
delete_all_deactivated_alarms/1,
|
||||
|
||||
, clean_authz_cache/1
|
||||
, clean_authz_cache/2
|
||||
]).
|
||||
clean_authz_cache/1,
|
||||
clean_authz_cache/2
|
||||
]).
|
||||
|
||||
introduced_in() ->
|
||||
"5.0.0".
|
||||
|
|
|
|||
|
|
@ -23,18 +23,24 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
prop_symmetric() ->
|
||||
?FORALL(Data, raw_data(),
|
||||
?FORALL(
|
||||
Data,
|
||||
raw_data(),
|
||||
begin
|
||||
Encoded = emqx_base62:encode(Data),
|
||||
to_binary(Data) =:= emqx_base62:decode(Encoded)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_size() ->
|
||||
?FORALL(Data, binary(),
|
||||
?FORALL(
|
||||
Data,
|
||||
binary(),
|
||||
begin
|
||||
Encoded = emqx_base62:encode(Data),
|
||||
base62_size(Data, Encoded)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
prop_serialize_parse_connect() ->
|
||||
?FORALL(Opts = #{version := ProtoVer}, parse_opts(),
|
||||
?FORALL(
|
||||
Opts = #{version := ProtoVer},
|
||||
parse_opts(),
|
||||
begin
|
||||
ProtoName = proplists:get_value(ProtoVer, ?PROTOCOL_NAMES),
|
||||
Packet = ?CONNECT_PACKET(#mqtt_packet_connect{
|
||||
|
|
@ -41,7 +43,8 @@ prop_serialize_parse_connect() ->
|
|||
properties = #{}
|
||||
}),
|
||||
Packet =:= parse_serialize(Packet, Opts)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
|
@ -59,4 +62,4 @@ parse_serialize(Packet, Opts) when is_map(Opts) ->
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
parse_opts() ->
|
||||
?LET(PropList, [{strict_mode, boolean()}, {version, range(4,5)}], maps:from_list(PropList)).
|
||||
?LET(PropList, [{strict_mode, boolean()}, {version, range(4, 5)}], maps:from_list(PropList)).
|
||||
|
|
|
|||
|
|
@ -16,14 +16,17 @@
|
|||
|
||||
-module(prop_emqx_json).
|
||||
|
||||
-import(emqx_json,
|
||||
[ decode/1
|
||||
, decode/2
|
||||
, encode/1
|
||||
, safe_decode/1
|
||||
, safe_decode/2
|
||||
, safe_encode/1
|
||||
]).
|
||||
-import(
|
||||
emqx_json,
|
||||
[
|
||||
decode/1,
|
||||
decode/2,
|
||||
encode/1,
|
||||
safe_decode/1,
|
||||
safe_decode/2,
|
||||
safe_encode/1
|
||||
]
|
||||
).
|
||||
|
||||
-include_lib("proper/include/proper.hrl").
|
||||
|
||||
|
|
@ -32,55 +35,72 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
prop_json_basic() ->
|
||||
?FORALL(T, json_basic(),
|
||||
?FORALL(
|
||||
T,
|
||||
json_basic(),
|
||||
begin
|
||||
{ok, J} = safe_encode(T),
|
||||
{ok, T} = safe_decode(J),
|
||||
T = decode(encode(T)),
|
||||
true
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_json_basic_atom() ->
|
||||
?FORALL(T0, latin_atom(),
|
||||
?FORALL(
|
||||
T0,
|
||||
latin_atom(),
|
||||
begin
|
||||
T = atom_to_binary(T0, utf8),
|
||||
{ok, J} = safe_encode(T0),
|
||||
{ok, T} = safe_decode(J),
|
||||
T = decode(encode(T0)),
|
||||
true
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_object_proplist_to_proplist() ->
|
||||
?FORALL(T, json_object(),
|
||||
?FORALL(
|
||||
T,
|
||||
json_object(),
|
||||
begin
|
||||
{ok, J} = safe_encode(T),
|
||||
{ok, T} = safe_decode(J),
|
||||
T = decode(encode(T)),
|
||||
true
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_object_map_to_map() ->
|
||||
?FORALL(T, json_object_map(),
|
||||
?FORALL(
|
||||
T,
|
||||
json_object_map(),
|
||||
begin
|
||||
{ok, J} = safe_encode(T),
|
||||
{ok, T} = safe_decode(J, [return_maps]),
|
||||
T = decode(encode(T), [return_maps]),
|
||||
true
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
%% The duplicated key will be overridden
|
||||
prop_object_proplist_to_map() ->
|
||||
?FORALL(T0, json_object(),
|
||||
?FORALL(
|
||||
T0,
|
||||
json_object(),
|
||||
begin
|
||||
T = to_map(T0),
|
||||
{ok, J} = safe_encode(T0),
|
||||
{ok, T} = safe_decode(J, [return_maps]),
|
||||
T = decode(encode(T0), [return_maps]),
|
||||
true
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_object_map_to_proplist() ->
|
||||
?FORALL(T0, json_object_map(),
|
||||
?FORALL(
|
||||
T0,
|
||||
json_object_map(),
|
||||
begin
|
||||
%% jiffy encode a map with descending order, that is,
|
||||
%% it is opposite with maps traversal sequence
|
||||
|
|
@ -90,41 +110,58 @@ prop_object_map_to_proplist() ->
|
|||
{ok, T} = safe_decode(J),
|
||||
T = decode(encode(T0)),
|
||||
true
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_safe_encode() ->
|
||||
?FORALL(T, invalid_json_term(),
|
||||
?FORALL(
|
||||
T,
|
||||
invalid_json_term(),
|
||||
begin
|
||||
{error, _} = safe_encode(T), true
|
||||
end).
|
||||
{error, _} = safe_encode(T),
|
||||
true
|
||||
end
|
||||
).
|
||||
|
||||
prop_safe_decode() ->
|
||||
?FORALL(T, invalid_json_str(),
|
||||
?FORALL(
|
||||
T,
|
||||
invalid_json_str(),
|
||||
begin
|
||||
{error, _} = safe_decode(T), true
|
||||
end).
|
||||
{error, _} = safe_decode(T),
|
||||
true
|
||||
end
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helpers
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
to_map([{_, _}|_] = L) ->
|
||||
to_map([{_, _} | _] = L) ->
|
||||
lists:foldl(
|
||||
fun({Name, Value}, Acc) ->
|
||||
Acc#{Name => to_map(Value)}
|
||||
end, #{}, L);
|
||||
end,
|
||||
#{},
|
||||
L
|
||||
);
|
||||
to_map(L) when is_list(L) ->
|
||||
[to_map(E) || E <- L];
|
||||
to_map(T) -> T.
|
||||
to_map(T) ->
|
||||
T.
|
||||
|
||||
to_list(L) when is_list(L) ->
|
||||
[to_list(E) || E <- L];
|
||||
to_list(M) when is_map(M) ->
|
||||
maps:fold(
|
||||
fun(K, V, Acc) ->
|
||||
[{K, to_list(V)}|Acc]
|
||||
end, [], M);
|
||||
to_list(T) -> T.
|
||||
[{K, to_list(V)} | Acc]
|
||||
end,
|
||||
[],
|
||||
M
|
||||
);
|
||||
to_list(T) ->
|
||||
T.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Generators (https://tools.ietf.org/html/rfc8259)
|
||||
|
|
@ -140,8 +177,14 @@ latin_atom() ->
|
|||
json_string() -> utf8().
|
||||
|
||||
json_object() ->
|
||||
oneof([json_array_1(), json_object_1(), json_array_object_1(),
|
||||
json_array_2(), json_object_2(), json_array_object_2()]).
|
||||
oneof([
|
||||
json_array_1(),
|
||||
json_object_1(),
|
||||
json_array_object_1(),
|
||||
json_array_2(),
|
||||
json_object_2(),
|
||||
json_array_object_2()
|
||||
]).
|
||||
|
||||
json_object_map() ->
|
||||
?LET(L, json_object(), to_map(L)).
|
||||
|
|
@ -156,9 +199,14 @@ json_object_1() ->
|
|||
list({json_key(), json_basic()}).
|
||||
|
||||
json_object_2() ->
|
||||
list({json_key(), oneof([json_basic(),
|
||||
list({
|
||||
json_key(),
|
||||
oneof([
|
||||
json_basic(),
|
||||
json_array_1(),
|
||||
json_object_1()])}).
|
||||
json_object_1()
|
||||
])
|
||||
}).
|
||||
|
||||
json_array_object_1() ->
|
||||
list(json_object_1()).
|
||||
|
|
@ -186,5 +234,4 @@ chaos(S, 0, _) ->
|
|||
chaos(S, N, T) ->
|
||||
I = rand:uniform(length(S)),
|
||||
{L1, L2} = lists:split(I, S),
|
||||
chaos(lists:flatten([L1, lists:nth(rand:uniform(length(T)), T), L2]), N-1, T).
|
||||
|
||||
chaos(lists:flatten([L1, lists:nth(rand:uniform(length(T)), T), L2]), N - 1, T).
|
||||
|
|
|
|||
|
|
@ -14,23 +14,27 @@
|
|||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
|
||||
-module(prop_emqx_psk).
|
||||
|
||||
-include_lib("proper/include/proper.hrl").
|
||||
|
||||
-define(ALL(Vars, Types, Exprs),
|
||||
?SETUP(fun() ->
|
||||
?SETUP(
|
||||
fun() ->
|
||||
State = do_setup(),
|
||||
fun() -> do_teardown(State) end
|
||||
end, ?FORALL(Vars, Types, Exprs))).
|
||||
end,
|
||||
?FORALL(Vars, Types, Exprs)
|
||||
)
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Properties
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
prop_lookup() ->
|
||||
?ALL({ClientPSKID, UserState},
|
||||
?ALL(
|
||||
{ClientPSKID, UserState},
|
||||
{client_pskid(), user_state()},
|
||||
begin
|
||||
case emqx_tls_psk:lookup(psk, ClientPSKID, UserState) of
|
||||
|
|
@ -38,7 +42,8 @@ prop_lookup() ->
|
|||
error -> true;
|
||||
_Other -> false
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helper
|
||||
|
|
@ -47,10 +52,13 @@ prop_lookup() ->
|
|||
do_setup() ->
|
||||
ok = emqx_logger:set_log_level(emergency),
|
||||
ok = meck:new(emqx_hooks, [passthrough, no_history]),
|
||||
ok = meck:expect(emqx_hooks, run_fold,
|
||||
ok = meck:expect(
|
||||
emqx_hooks,
|
||||
run_fold,
|
||||
fun('tls_handshake.psk_lookup', [ClientPSKID], not_found) ->
|
||||
unicode:characters_to_binary(ClientPSKID)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
do_teardown(_) ->
|
||||
ok = emqx_logger:set_log_level(error),
|
||||
|
|
@ -63,4 +71,3 @@ do_teardown(_) ->
|
|||
client_pskid() -> oneof([string(), integer(), [1, [-1]]]).
|
||||
|
||||
user_state() -> term().
|
||||
|
||||
|
|
|
|||
|
|
@ -24,20 +24,29 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
prop_name_text() ->
|
||||
?FORALL(UnionArgs, union_args(),
|
||||
?FORALL(
|
||||
UnionArgs,
|
||||
union_args(),
|
||||
is_atom(apply_fun(name, UnionArgs)) andalso
|
||||
is_binary(apply_fun(text, UnionArgs))).
|
||||
is_binary(apply_fun(text, UnionArgs))
|
||||
).
|
||||
|
||||
prop_compat() ->
|
||||
?FORALL(CompatArgs, compat_args(),
|
||||
?FORALL(
|
||||
CompatArgs,
|
||||
compat_args(),
|
||||
begin
|
||||
Result = apply_fun(compat, CompatArgs),
|
||||
is_number(Result) orelse Result =:= undefined
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_connack_error() ->
|
||||
?FORALL(CONNACK_ERROR_ARGS, connack_error_args(),
|
||||
is_integer(apply_fun(connack_error, CONNACK_ERROR_ARGS))).
|
||||
?FORALL(
|
||||
CONNACK_ERROR_ARGS,
|
||||
connack_error_args(),
|
||||
is_integer(apply_fun(connack_error, CONNACK_ERROR_ARGS))
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helper
|
||||
|
|
@ -51,20 +60,29 @@ apply_fun(Fun, Args) ->
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
union_args() ->
|
||||
frequency([{6, [real_mqttv3_rc(), mqttv3_version()]},
|
||||
{43, [real_mqttv5_rc(), mqttv5_version()]}]).
|
||||
frequency([
|
||||
{6, [real_mqttv3_rc(), mqttv3_version()]},
|
||||
{43, [real_mqttv5_rc(), mqttv5_version()]}
|
||||
]).
|
||||
|
||||
compat_args() ->
|
||||
frequency([{18, [connack, compat_rc()]},
|
||||
frequency([
|
||||
{18, [connack, compat_rc()]},
|
||||
{2, [suback, compat_rc()]},
|
||||
{1, [unsuback, compat_rc()]}]).
|
||||
{1, [unsuback, compat_rc()]}
|
||||
]).
|
||||
|
||||
connack_error_args() ->
|
||||
[frequency([{10, connack_error()},
|
||||
{1, unexpected_connack_error()}])].
|
||||
[
|
||||
frequency([
|
||||
{10, connack_error()},
|
||||
{1, unexpected_connack_error()}
|
||||
])
|
||||
].
|
||||
|
||||
connack_error() ->
|
||||
oneof([client_identifier_not_valid,
|
||||
oneof([
|
||||
client_identifier_not_valid,
|
||||
bad_username_or_password,
|
||||
bad_clientid_or_password,
|
||||
username_or_password_undefined,
|
||||
|
|
@ -73,23 +91,29 @@ connack_error() ->
|
|||
server_unavailable,
|
||||
server_busy,
|
||||
banned,
|
||||
bad_authentication_method]).
|
||||
bad_authentication_method
|
||||
]).
|
||||
|
||||
unexpected_connack_error() ->
|
||||
oneof([who_knows]).
|
||||
|
||||
|
||||
real_mqttv3_rc() ->
|
||||
frequency([{6, mqttv3_rc()},
|
||||
{1, unexpected_rc()}]).
|
||||
frequency([
|
||||
{6, mqttv3_rc()},
|
||||
{1, unexpected_rc()}
|
||||
]).
|
||||
|
||||
real_mqttv5_rc() ->
|
||||
frequency([{43, mqttv5_rc()},
|
||||
{2, unexpected_rc()}]).
|
||||
frequency([
|
||||
{43, mqttv5_rc()},
|
||||
{2, unexpected_rc()}
|
||||
]).
|
||||
|
||||
compat_rc() ->
|
||||
frequency([{95, ?SUCHTHAT(RC , mqttv5_rc(), RC >= 16#80 orelse RC =< 2)},
|
||||
{5, unexpected_rc()}]).
|
||||
frequency([
|
||||
{95, ?SUCHTHAT(RC, mqttv5_rc(), RC >= 16#80 orelse RC =< 2)},
|
||||
{5, unexpected_rc()}
|
||||
]).
|
||||
|
||||
mqttv3_rc() ->
|
||||
oneof(mqttv3_rcs()).
|
||||
|
|
@ -104,12 +128,51 @@ mqttv3_rcs() ->
|
|||
[0, 1, 2, 3, 4, 5].
|
||||
|
||||
mqttv5_rcs() ->
|
||||
[16#00, 16#01, 16#02, 16#04, 16#10, 16#11, 16#18, 16#19,
|
||||
16#80, 16#81, 16#82, 16#83, 16#84, 16#85, 16#86, 16#87,
|
||||
16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#8D, 16#8E, 16#8F,
|
||||
16#90, 16#91, 16#92, 16#93, 16#94, 16#95, 16#96, 16#97,
|
||||
16#98, 16#99, 16#9A, 16#9B, 16#9C, 16#9D, 16#9E, 16#9F,
|
||||
16#A0, 16#A1, 16#A2].
|
||||
[
|
||||
16#00,
|
||||
16#01,
|
||||
16#02,
|
||||
16#04,
|
||||
16#10,
|
||||
16#11,
|
||||
16#18,
|
||||
16#19,
|
||||
16#80,
|
||||
16#81,
|
||||
16#82,
|
||||
16#83,
|
||||
16#84,
|
||||
16#85,
|
||||
16#86,
|
||||
16#87,
|
||||
16#88,
|
||||
16#89,
|
||||
16#8A,
|
||||
16#8B,
|
||||
16#8C,
|
||||
16#8D,
|
||||
16#8E,
|
||||
16#8F,
|
||||
16#90,
|
||||
16#91,
|
||||
16#92,
|
||||
16#93,
|
||||
16#94,
|
||||
16#95,
|
||||
16#96,
|
||||
16#97,
|
||||
16#98,
|
||||
16#99,
|
||||
16#9A,
|
||||
16#9B,
|
||||
16#9C,
|
||||
16#9D,
|
||||
16#9E,
|
||||
16#9F,
|
||||
16#A0,
|
||||
16#A1,
|
||||
16#A2
|
||||
].
|
||||
|
||||
unexpected_rcs() ->
|
||||
ReasonCodes = mqttv3_rcs() ++ mqttv5_rcs(),
|
||||
|
|
|
|||
|
|
@ -22,17 +22,23 @@
|
|||
-define(NODENAME, 'test@127.0.0.1').
|
||||
|
||||
-define(ALL(Vars, Types, Exprs),
|
||||
?SETUP(fun() ->
|
||||
?SETUP(
|
||||
fun() ->
|
||||
State = do_setup(),
|
||||
fun() -> do_teardown(State) end
|
||||
end, ?FORALL(Vars, Types, Exprs))).
|
||||
end,
|
||||
?FORALL(Vars, Types, Exprs)
|
||||
)
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Properties
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
prop_node() ->
|
||||
?ALL(Node0, nodename(),
|
||||
?ALL(
|
||||
Node0,
|
||||
nodename(),
|
||||
begin
|
||||
Node = punch(Node0),
|
||||
?assert(emqx_rpc:cast(Node, erlang, system_time, [])),
|
||||
|
|
@ -41,10 +47,13 @@ prop_node() ->
|
|||
Delivery when is_integer(Delivery) -> true;
|
||||
_Other -> false
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_node_with_key() ->
|
||||
?ALL({Node0, Key}, nodename_with_key(),
|
||||
?ALL(
|
||||
{Node0, Key},
|
||||
nodename_with_key(),
|
||||
begin
|
||||
Node = punch(Node0),
|
||||
?assert(emqx_rpc:cast(Key, Node, erlang, system_time, [])),
|
||||
|
|
@ -53,33 +62,44 @@ prop_node_with_key() ->
|
|||
Delivery when is_integer(Delivery) -> true;
|
||||
_Other -> false
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_nodes() ->
|
||||
?ALL(Nodes0, nodesname(),
|
||||
?ALL(
|
||||
Nodes0,
|
||||
nodesname(),
|
||||
begin
|
||||
Nodes = punch(Nodes0),
|
||||
case emqx_rpc:multicall(Nodes, erlang, system_time, []) of
|
||||
{RealResults, RealBadNodes}
|
||||
when is_list(RealResults);
|
||||
is_list(RealBadNodes) ->
|
||||
{RealResults, RealBadNodes} when
|
||||
is_list(RealResults);
|
||||
is_list(RealBadNodes)
|
||||
->
|
||||
true;
|
||||
_Other -> false
|
||||
_Other ->
|
||||
false
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
prop_nodes_with_key() ->
|
||||
?ALL({Nodes0, Key}, nodesname_with_key(),
|
||||
?ALL(
|
||||
{Nodes0, Key},
|
||||
nodesname_with_key(),
|
||||
begin
|
||||
Nodes = punch(Nodes0),
|
||||
case emqx_rpc:multicall(Key, Nodes, erlang, system_time, []) of
|
||||
{RealResults, RealBadNodes}
|
||||
when is_list(RealResults);
|
||||
is_list(RealBadNodes) ->
|
||||
{RealResults, RealBadNodes} when
|
||||
is_list(RealResults);
|
||||
is_list(RealBadNodes)
|
||||
->
|
||||
true;
|
||||
_Other -> false
|
||||
_Other ->
|
||||
false
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helper
|
||||
|
|
@ -91,10 +111,13 @@ do_setup() ->
|
|||
{ok, _Apps} = application:ensure_all_started(gen_rpc),
|
||||
ok = application:set_env(gen_rpc, call_receive_timeout, 100),
|
||||
ok = meck:new(gen_rpc, [passthrough, no_history]),
|
||||
ok = meck:expect(gen_rpc, multicall,
|
||||
ok = meck:expect(
|
||||
gen_rpc,
|
||||
multicall,
|
||||
fun(Nodes, Mod, Fun, Args) ->
|
||||
gen_rpc:multicall(Nodes, Mod, Fun, Args, 100)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
do_teardown(_) ->
|
||||
ok = net_kernel:stop(),
|
||||
|
|
@ -121,22 +144,25 @@ ensure_distributed_nodename() ->
|
|||
%% Generator
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
|
||||
nodename() ->
|
||||
?LET({NodePrefix, HostName},
|
||||
?LET(
|
||||
{NodePrefix, HostName},
|
||||
{node_prefix(), hostname()},
|
||||
begin
|
||||
Node = NodePrefix ++ "@" ++ HostName,
|
||||
list_to_atom(Node)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
nodename_with_key() ->
|
||||
?LET({NodePrefix, HostName, Key},
|
||||
?LET(
|
||||
{NodePrefix, HostName, Key},
|
||||
{node_prefix(), hostname(), choose(0, 10)},
|
||||
begin
|
||||
Node = NodePrefix ++ "@" ++ HostName,
|
||||
{list_to_atom(Node), Key}
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
nodesname() ->
|
||||
oneof([list(nodename()), [node()]]).
|
||||
|
|
@ -163,6 +189,7 @@ hostname() ->
|
|||
punch(Nodes) when is_list(Nodes) ->
|
||||
lists:map(fun punch/1, Nodes);
|
||||
punch('nonode@nohost') ->
|
||||
node(); %% Equal to ?NODENAME
|
||||
%% Equal to ?NODENAME
|
||||
node();
|
||||
punch(GoodBoy) ->
|
||||
GoodBoy.
|
||||
|
|
|
|||
|
|
@ -18,41 +18,53 @@
|
|||
|
||||
-include_lib("proper/include/proper.hrl").
|
||||
|
||||
-export([ initial_state/0
|
||||
, command/1
|
||||
, precondition/2
|
||||
, postcondition/3
|
||||
, next_state/3
|
||||
]).
|
||||
-export([
|
||||
initial_state/0,
|
||||
command/1,
|
||||
precondition/2,
|
||||
postcondition/3,
|
||||
next_state/3
|
||||
]).
|
||||
|
||||
-define(mock_modules,
|
||||
[ emqx_metrics
|
||||
, emqx_stats
|
||||
, emqx_broker
|
||||
, mria_mnesia
|
||||
, emqx_hooks
|
||||
]).
|
||||
-define(mock_modules, [
|
||||
emqx_metrics,
|
||||
emqx_stats,
|
||||
emqx_broker,
|
||||
mria_mnesia,
|
||||
emqx_hooks
|
||||
]).
|
||||
|
||||
-define(ALL(Vars, Types, Exprs),
|
||||
?SETUP(fun() ->
|
||||
?SETUP(
|
||||
fun() ->
|
||||
State = do_setup(),
|
||||
fun() -> do_teardown(State) end
|
||||
end, ?FORALL(Vars, Types, Exprs))).
|
||||
end,
|
||||
?FORALL(Vars, Types, Exprs)
|
||||
)
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Properties
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
prop_sys() ->
|
||||
?ALL(Cmds, commands(?MODULE),
|
||||
?ALL(
|
||||
Cmds,
|
||||
commands(?MODULE),
|
||||
begin
|
||||
{ok, _Pid} = emqx_sys:start_link(),
|
||||
{History, State, Result} = run_commands(?MODULE, Cmds),
|
||||
ok = emqx_sys:stop(),
|
||||
?WHENFAIL(io:format("History: ~p\nState: ~p\nResult: ~p\n",
|
||||
[History,State,Result]),
|
||||
aggregate(command_names(Cmds), Result =:= ok))
|
||||
end).
|
||||
?WHENFAIL(
|
||||
io:format(
|
||||
"History: ~p\nState: ~p\nResult: ~p\n",
|
||||
[History, State, Result]
|
||||
),
|
||||
aggregate(command_names(Cmds), Result =:= ok)
|
||||
)
|
||||
end
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
|
@ -62,9 +74,15 @@ do_setup() ->
|
|||
ok = emqx_logger:set_log_level(emergency),
|
||||
emqx_config:put([sys_topics, sys_msg_interval], 60000),
|
||||
emqx_config:put([sys_topics, sys_heartbeat_interval], 30000),
|
||||
emqx_config:put([sys_topics, sys_event_messages],
|
||||
#{client_connected => true, client_disconnected => true,
|
||||
client_subscribed => true, client_unsubscribed => true}),
|
||||
emqx_config:put(
|
||||
[sys_topics, sys_event_messages],
|
||||
#{
|
||||
client_connected => true,
|
||||
client_disconnected => true,
|
||||
client_subscribed => true,
|
||||
client_unsubscribed => true
|
||||
}
|
||||
),
|
||||
[mock(Mod) || Mod <- ?mock_modules],
|
||||
ok.
|
||||
|
||||
|
|
@ -78,10 +96,16 @@ mock(Module) ->
|
|||
do_mock(Module).
|
||||
|
||||
do_mock(emqx_broker) ->
|
||||
meck:expect(emqx_broker, publish,
|
||||
fun(Msg) -> {node(), <<"test">>, Msg} end),
|
||||
meck:expect(emqx_broker, safe_publish,
|
||||
fun(Msg) -> {node(), <<"test">>, Msg} end);
|
||||
meck:expect(
|
||||
emqx_broker,
|
||||
publish,
|
||||
fun(Msg) -> {node(), <<"test">>, Msg} end
|
||||
),
|
||||
meck:expect(
|
||||
emqx_broker,
|
||||
safe_publish,
|
||||
fun(Msg) -> {node(), <<"test">>, Msg} end
|
||||
);
|
||||
do_mock(emqx_stats) ->
|
||||
meck:expect(emqx_stats, getstats, fun() -> [0] end);
|
||||
do_mock(mria_mnesia) ->
|
||||
|
|
@ -102,7 +126,8 @@ initial_state() ->
|
|||
|
||||
%% @doc List of possible commands to run against the system
|
||||
command(_State) ->
|
||||
oneof([{call, emqx_sys, info, []},
|
||||
oneof([
|
||||
{call, emqx_sys, info, []},
|
||||
{call, emqx_sys, version, []},
|
||||
{call, emqx_sys, uptime, []},
|
||||
{call, emqx_sys, datetime, []},
|
||||
|
|
|
|||
|
|
@ -23,47 +23,54 @@
|
|||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ refs/0
|
||||
, create/2
|
||||
, update/2
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
-export([
|
||||
refs/0,
|
||||
create/2,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
-export([ add_user/2
|
||||
, delete_user/2
|
||||
, update_user/3
|
||||
, lookup_user/2
|
||||
, list_users/2
|
||||
]).
|
||||
-export([
|
||||
add_user/2,
|
||||
delete_user/2,
|
||||
update_user/3,
|
||||
lookup_user/2,
|
||||
list_users/2
|
||||
]).
|
||||
|
||||
-export([ query/4
|
||||
, format_user_info/1
|
||||
, group_match_spec/1]).
|
||||
-export([
|
||||
query/4,
|
||||
format_user_info/1,
|
||||
group_match_spec/1
|
||||
]).
|
||||
|
||||
-define(TAB, ?MODULE).
|
||||
-define(AUTHN_QSCHEMA, [ {<<"like_username">>, binary}
|
||||
, {<<"user_group">>, binary}]).
|
||||
-define(AUTHN_QSCHEMA, [
|
||||
{<<"like_username">>, binary},
|
||||
{<<"user_group">>, binary}
|
||||
]).
|
||||
-define(QUERY_FUN, {?MODULE, query}).
|
||||
|
||||
-type(user_group() :: binary()).
|
||||
-type user_group() :: binary().
|
||||
|
||||
-export([mnesia/1]).
|
||||
|
||||
-boot_mnesia({mnesia, [boot]}).
|
||||
|
||||
-record(user_info,
|
||||
{ user_id
|
||||
, stored_key
|
||||
, server_key
|
||||
, salt
|
||||
, is_superuser
|
||||
}).
|
||||
-record(user_info, {
|
||||
user_id,
|
||||
stored_key,
|
||||
server_key,
|
||||
salt,
|
||||
is_superuser
|
||||
}).
|
||||
|
||||
-reflect_type([user_group/0]).
|
||||
|
||||
|
|
@ -72,14 +79,15 @@
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
%% @doc Create or replicate tables.
|
||||
-spec(mnesia(boot | copy) -> ok).
|
||||
-spec mnesia(boot | copy) -> ok.
|
||||
mnesia(boot) ->
|
||||
ok = mria:create_table(?TAB, [
|
||||
{rlog_shard, ?AUTH_SHARD},
|
||||
{storage, disc_copies},
|
||||
{record_name, user_info},
|
||||
{attributes, record_info(fields, user_info)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]).
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
|
|
@ -90,10 +98,11 @@ namespace() -> "authn-scram-builtin_db".
|
|||
roots() -> [?CONF_NS].
|
||||
|
||||
fields(?CONF_NS) ->
|
||||
[ {mechanism, emqx_authn_schema:mechanism('scram')}
|
||||
, {backend, emqx_authn_schema:backend('built_in_database')}
|
||||
, {algorithm, fun algorithm/1}
|
||||
, {iteration_count, fun iteration_count/1}
|
||||
[
|
||||
{mechanism, emqx_authn_schema:mechanism('scram')},
|
||||
{backend, emqx_authn_schema:backend('built_in_database')},
|
||||
{algorithm, fun algorithm/1},
|
||||
{iteration_count, fun iteration_count/1}
|
||||
] ++ emqx_authn_schema:common_fields().
|
||||
|
||||
algorithm(type) -> hoconsc:enum([sha256, sha512]);
|
||||
|
|
@ -111,21 +120,31 @@ iteration_count(_) -> undefined.
|
|||
refs() ->
|
||||
[hoconsc:ref(?MODULE, ?CONF_NS)].
|
||||
|
||||
create(AuthenticatorID,
|
||||
#{algorithm := Algorithm,
|
||||
iteration_count := IterationCount}) ->
|
||||
State = #{user_group => AuthenticatorID,
|
||||
create(
|
||||
AuthenticatorID,
|
||||
#{
|
||||
algorithm := Algorithm,
|
||||
iteration_count := IterationCount
|
||||
}
|
||||
) ->
|
||||
State = #{
|
||||
user_group => AuthenticatorID,
|
||||
algorithm => Algorithm,
|
||||
iteration_count => IterationCount},
|
||||
iteration_count => IterationCount
|
||||
},
|
||||
{ok, State}.
|
||||
|
||||
|
||||
update(Config, #{user_group := ID}) ->
|
||||
create(ID, Config).
|
||||
|
||||
authenticate(#{auth_method := AuthMethod,
|
||||
authenticate(
|
||||
#{
|
||||
auth_method := AuthMethod,
|
||||
auth_data := AuthData,
|
||||
auth_cache := AuthCache}, State) ->
|
||||
auth_cache := AuthCache
|
||||
},
|
||||
State
|
||||
) ->
|
||||
case ensure_auth_method(AuthMethod, State) of
|
||||
true ->
|
||||
case AuthCache of
|
||||
|
|
@ -144,13 +163,22 @@ destroy(#{user_group := UserGroup}) ->
|
|||
MatchSpec = group_match_spec(UserGroup),
|
||||
trans(
|
||||
fun() ->
|
||||
ok = lists:foreach(fun(UserInfo) ->
|
||||
ok = lists:foreach(
|
||||
fun(UserInfo) ->
|
||||
mnesia:delete_object(?TAB, UserInfo, write)
|
||||
end, mnesia:select(?TAB, MatchSpec, write))
|
||||
end).
|
||||
end,
|
||||
mnesia:select(?TAB, MatchSpec, write)
|
||||
)
|
||||
end
|
||||
).
|
||||
|
||||
add_user(#{user_id := UserID,
|
||||
password := Password} = UserInfo, #{user_group := UserGroup} = State) ->
|
||||
add_user(
|
||||
#{
|
||||
user_id := UserID,
|
||||
password := Password
|
||||
} = UserInfo,
|
||||
#{user_group := UserGroup} = State
|
||||
) ->
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||
|
|
@ -161,7 +189,8 @@ add_user(#{user_id := UserID,
|
|||
[_] ->
|
||||
{error, already_exist}
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
delete_user(UserID, #{user_group := UserGroup}) ->
|
||||
trans(
|
||||
|
|
@ -172,30 +201,42 @@ delete_user(UserID, #{user_group := UserGroup}) ->
|
|||
[_] ->
|
||||
mnesia:delete(?TAB, {UserGroup, UserID}, write)
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
update_user(UserID, User,
|
||||
#{user_group := UserGroup} = State) ->
|
||||
update_user(
|
||||
UserID,
|
||||
User,
|
||||
#{user_group := UserGroup} = State
|
||||
) ->
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[#user_info{is_superuser = IsSuperuser} = UserInfo] ->
|
||||
UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, IsSuperuser)},
|
||||
UserInfo2 = case maps:get(password, User, undefined) of
|
||||
UserInfo1 = UserInfo#user_info{
|
||||
is_superuser = maps:get(is_superuser, User, IsSuperuser)
|
||||
},
|
||||
UserInfo2 =
|
||||
case maps:get(password, User, undefined) of
|
||||
undefined ->
|
||||
UserInfo1;
|
||||
Password ->
|
||||
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
|
||||
UserInfo1#user_info{stored_key = StoredKey,
|
||||
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(
|
||||
Password, State
|
||||
),
|
||||
UserInfo1#user_info{
|
||||
stored_key = StoredKey,
|
||||
server_key = ServerKey,
|
||||
salt = Salt}
|
||||
salt = Salt
|
||||
}
|
||||
end,
|
||||
mnesia:write(?TAB, UserInfo2, write),
|
||||
{ok, format_user_info(UserInfo2)}
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
lookup_user(UserID, #{user_group := UserGroup}) ->
|
||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||
|
|
@ -214,14 +255,23 @@ list_users(QueryString, #{user_group := UserGroup}) ->
|
|||
|
||||
query(Tab, {QString, []}, Continuation, Limit) ->
|
||||
Ms = ms_from_qstring(QString),
|
||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
||||
fun format_user_info/1);
|
||||
|
||||
emqx_mgmt_api:select_table_with_count(
|
||||
Tab,
|
||||
Ms,
|
||||
Continuation,
|
||||
Limit,
|
||||
fun format_user_info/1
|
||||
);
|
||||
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||
Ms = ms_from_qstring(QString),
|
||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
||||
fun format_user_info/1).
|
||||
emqx_mgmt_api:select_table_with_count(
|
||||
Tab,
|
||||
{Ms, FuzzyFilterFun},
|
||||
Continuation,
|
||||
Limit,
|
||||
fun format_user_info/1
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Match funcs
|
||||
|
|
@ -229,14 +279,18 @@ query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
|||
%% Fuzzy username funcs
|
||||
fuzzy_filter_fun(Fuzzy) ->
|
||||
fun(MsRaws) when is_list(MsRaws) ->
|
||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
||||
, MsRaws)
|
||||
lists:filter(
|
||||
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||
MsRaws
|
||||
)
|
||||
end.
|
||||
|
||||
run_fuzzy_filter(_, []) ->
|
||||
true;
|
||||
run_fuzzy_filter( E = #user_info{user_id = {_, UserID}}
|
||||
, [{username, like, UsernameSubStr} | Fuzzy]) ->
|
||||
run_fuzzy_filter(
|
||||
E = #user_info{user_id = {_, UserID}},
|
||||
[{username, like, UsernameSubStr} | Fuzzy]
|
||||
) ->
|
||||
binary:match(UserID, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
|
@ -254,11 +308,15 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S
|
|||
RetrieveFun = fun(Username) ->
|
||||
retrieve(Username, State)
|
||||
end,
|
||||
case esasl_scram:check_client_first_message(
|
||||
case
|
||||
esasl_scram:check_client_first_message(
|
||||
Bin,
|
||||
#{iteration_count => IterationCount,
|
||||
retrieve => RetrieveFun}
|
||||
) of
|
||||
#{
|
||||
iteration_count => IterationCount,
|
||||
retrieve => RetrieveFun
|
||||
}
|
||||
)
|
||||
of
|
||||
{continue, ServerFirstMessage, Cache} ->
|
||||
{continue, ServerFirstMessage, Cache};
|
||||
ignore ->
|
||||
|
|
@ -268,10 +326,12 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S
|
|||
end.
|
||||
|
||||
check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}) ->
|
||||
case esasl_scram:check_client_final_message(
|
||||
case
|
||||
esasl_scram:check_client_final_message(
|
||||
Bin,
|
||||
Cache#{algorithm => Alg}
|
||||
) of
|
||||
)
|
||||
of
|
||||
{ok, ServerFinalMessage} ->
|
||||
{ok, #{is_superuser => IsSuperuser}, ServerFinalMessage};
|
||||
{error, _Reason} ->
|
||||
|
|
@ -280,23 +340,31 @@ check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algori
|
|||
|
||||
add_user(UserGroup, UserID, Password, IsSuperuser, State) ->
|
||||
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
|
||||
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
||||
UserInfo = #user_info{
|
||||
user_id = {UserGroup, UserID},
|
||||
stored_key = StoredKey,
|
||||
server_key = ServerKey,
|
||||
salt = Salt,
|
||||
is_superuser = IsSuperuser},
|
||||
is_superuser = IsSuperuser
|
||||
},
|
||||
mnesia:write(?TAB, UserInfo, write).
|
||||
|
||||
retrieve(UserID, #{user_group := UserGroup}) ->
|
||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||
[#user_info{stored_key = StoredKey,
|
||||
[
|
||||
#user_info{
|
||||
stored_key = StoredKey,
|
||||
server_key = ServerKey,
|
||||
salt = Salt,
|
||||
is_superuser = IsSuperuser}] ->
|
||||
{ok, #{stored_key => StoredKey,
|
||||
is_superuser = IsSuperuser
|
||||
}
|
||||
] ->
|
||||
{ok, #{
|
||||
stored_key => StoredKey,
|
||||
server_key => ServerKey,
|
||||
salt => Salt,
|
||||
is_superuser => IsSuperuser}};
|
||||
is_superuser => IsSuperuser
|
||||
}};
|
||||
[] ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
|
@ -315,15 +383,21 @@ format_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser})
|
|||
#{user_id => UserID, is_superuser => IsSuperuser}.
|
||||
|
||||
ms_from_qstring(QString) ->
|
||||
[Ms] = lists:foldl(fun({user_group, '=:=', UserGroup}, AccIn) ->
|
||||
[Ms] = lists:foldl(
|
||||
fun
|
||||
({user_group, '=:=', UserGroup}, AccIn) ->
|
||||
[group_match_spec(UserGroup) | AccIn];
|
||||
(_, AccIn) ->
|
||||
AccIn
|
||||
end, [], QString),
|
||||
end,
|
||||
[],
|
||||
QString
|
||||
),
|
||||
Ms.
|
||||
|
||||
group_match_spec(UserGroup) ->
|
||||
ets:fun2ms(
|
||||
fun(#user_info{user_id = {Group, _}} = User) when Group =:= UserGroup ->
|
||||
User
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
-behaviour(emqx_bpapi).
|
||||
|
||||
-export([ introduced_in/0
|
||||
, lookup_from_all_nodes/3
|
||||
]).
|
||||
-export([
|
||||
introduced_in/0,
|
||||
lookup_from_all_nodes/3
|
||||
]).
|
||||
|
||||
-include_lib("emqx/include/bpapi.hrl").
|
||||
|
||||
|
|
@ -32,4 +33,6 @@ introduced_in() ->
|
|||
-spec lookup_from_all_nodes([node()], atom(), binary()) ->
|
||||
emqx_rpc:erpc_multicall().
|
||||
lookup_from_all_nodes(Nodes, ChainName, AuthenticatorID) ->
|
||||
erpc:multicall(Nodes, emqx_authn_api, lookup_from_local_node, [ChainName, AuthenticatorID], ?TIMEOUT).
|
||||
erpc:multicall(
|
||||
Nodes, emqx_authn_api, lookup_from_local_node, [ChainName, AuthenticatorID], ?TIMEOUT
|
||||
).
|
||||
|
|
|
|||
|
|
@ -24,18 +24,20 @@
|
|||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
, validations/0
|
||||
]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1,
|
||||
validations/0
|
||||
]).
|
||||
|
||||
-export([ refs/0
|
||||
, create/2
|
||||
, update/2
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
-export([
|
||||
refs/0,
|
||||
create/2,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
|
|
@ -44,35 +46,47 @@
|
|||
namespace() -> "authn-http".
|
||||
|
||||
roots() ->
|
||||
[ {?CONF_NS,
|
||||
hoconsc:mk(hoconsc:union(refs()),
|
||||
#{})}
|
||||
[
|
||||
{?CONF_NS,
|
||||
hoconsc:mk(
|
||||
hoconsc:union(refs()),
|
||||
#{}
|
||||
)}
|
||||
].
|
||||
|
||||
fields(get) ->
|
||||
[ {method, #{type => get, default => post}}
|
||||
, {headers, fun headers_no_content_type/1}
|
||||
[
|
||||
{method, #{type => get, default => post}},
|
||||
{headers, fun headers_no_content_type/1}
|
||||
] ++ common_fields();
|
||||
|
||||
fields(post) ->
|
||||
[ {method, #{type => post, default => post}}
|
||||
, {headers, fun headers/1}
|
||||
[
|
||||
{method, #{type => post, default => post}},
|
||||
{headers, fun headers/1}
|
||||
] ++ common_fields().
|
||||
|
||||
common_fields() ->
|
||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
||||
, {backend, emqx_authn_schema:backend(http)}
|
||||
, {url, fun url/1}
|
||||
, {body, map([{fuzzy, term(), binary()}])}
|
||||
, {request_timeout, fun request_timeout/1}
|
||||
] ++ emqx_authn_schema:common_fields()
|
||||
++ maps:to_list(maps:without([ base_url
|
||||
, pool_type],
|
||||
maps:from_list(emqx_connector_http:fields(config)))).
|
||||
[
|
||||
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||
{backend, emqx_authn_schema:backend(http)},
|
||||
{url, fun url/1},
|
||||
{body, map([{fuzzy, term(), binary()}])},
|
||||
{request_timeout, fun request_timeout/1}
|
||||
] ++ emqx_authn_schema:common_fields() ++
|
||||
maps:to_list(
|
||||
maps:without(
|
||||
[
|
||||
base_url,
|
||||
pool_type
|
||||
],
|
||||
maps:from_list(emqx_connector_http:fields(config))
|
||||
)
|
||||
).
|
||||
|
||||
validations() ->
|
||||
[ {check_ssl_opts, fun check_ssl_opts/1}
|
||||
, {check_headers, fun check_headers/1}
|
||||
[
|
||||
{check_ssl_opts, fun check_ssl_opts/1},
|
||||
{check_headers, fun check_headers/1}
|
||||
].
|
||||
|
||||
url(type) -> binary();
|
||||
|
|
@ -80,21 +94,27 @@ url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")];
|
|||
url(required) -> true;
|
||||
url(_) -> undefined.
|
||||
|
||||
headers(type) -> map();
|
||||
headers(type) ->
|
||||
map();
|
||||
headers(converter) ->
|
||||
fun(Headers) ->
|
||||
maps:merge(default_headers(), transform_header_name(Headers))
|
||||
end;
|
||||
headers(default) -> default_headers();
|
||||
headers(_) -> undefined.
|
||||
headers(default) ->
|
||||
default_headers();
|
||||
headers(_) ->
|
||||
undefined.
|
||||
|
||||
headers_no_content_type(type) -> map();
|
||||
headers_no_content_type(type) ->
|
||||
map();
|
||||
headers_no_content_type(converter) ->
|
||||
fun(Headers) ->
|
||||
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
||||
end;
|
||||
headers_no_content_type(default) -> default_headers_no_content_type();
|
||||
headers_no_content_type(_) -> undefined.
|
||||
headers_no_content_type(default) ->
|
||||
default_headers_no_content_type();
|
||||
headers_no_content_type(_) ->
|
||||
undefined.
|
||||
|
||||
request_timeout(type) -> emqx_schema:duration_ms();
|
||||
request_timeout(default) -> <<"5s">>;
|
||||
|
|
@ -105,36 +125,51 @@ request_timeout(_) -> undefined.
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
refs() ->
|
||||
[ hoconsc:ref(?MODULE, get)
|
||||
, hoconsc:ref(?MODULE, post)
|
||||
[
|
||||
hoconsc:ref(?MODULE, get),
|
||||
hoconsc:ref(?MODULE, post)
|
||||
].
|
||||
|
||||
create(_AuthenticatorID, Config) ->
|
||||
create(Config).
|
||||
|
||||
create(#{method := Method,
|
||||
create(
|
||||
#{
|
||||
method := Method,
|
||||
url := RawURL,
|
||||
headers := Headers,
|
||||
body := Body,
|
||||
request_timeout := RequestTimeout} = Config) ->
|
||||
request_timeout := RequestTimeout
|
||||
} = Config
|
||||
) ->
|
||||
{BsaeUrlWithPath, Query} = parse_fullpath(RawURL),
|
||||
URIMap = parse_url(BsaeUrlWithPath),
|
||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||
State = #{method => Method,
|
||||
State = #{
|
||||
method => Method,
|
||||
path => maps:get(path, URIMap),
|
||||
base_query_template => emqx_authn_utils:parse_deep(
|
||||
cow_qs:parse_qs(to_bin(Query))),
|
||||
cow_qs:parse_qs(to_bin(Query))
|
||||
),
|
||||
headers => maps:to_list(Headers),
|
||||
body_template => emqx_authn_utils:parse_deep(
|
||||
maps:to_list(Body)),
|
||||
maps:to_list(Body)
|
||||
),
|
||||
request_timeout => RequestTimeout,
|
||||
resource_id => ResourceId},
|
||||
case emqx_resource:create_local(ResourceId,
|
||||
resource_id => ResourceId
|
||||
},
|
||||
case
|
||||
emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_http,
|
||||
Config#{base_url => maps:remove(query, URIMap),
|
||||
pool_type => random},
|
||||
#{}) of
|
||||
Config#{
|
||||
base_url => maps:remove(query, URIMap),
|
||||
pool_type => random
|
||||
},
|
||||
#{}
|
||||
)
|
||||
of
|
||||
{ok, already_created} ->
|
||||
{ok, State};
|
||||
{ok, _} ->
|
||||
|
|
@ -154,13 +189,20 @@ update(Config, State) ->
|
|||
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(Credential, #{resource_id := ResourceId,
|
||||
authenticate(
|
||||
Credential,
|
||||
#{
|
||||
resource_id := ResourceId,
|
||||
method := Method,
|
||||
request_timeout := RequestTimeout} = State) ->
|
||||
request_timeout := RequestTimeout
|
||||
} = State
|
||||
) ->
|
||||
Request = generate_request(Credential, State),
|
||||
case emqx_resource:query(ResourceId, {Method, Request, RequestTimeout}) of
|
||||
{ok, 204, _Headers} -> {ok, #{is_superuser => false}};
|
||||
{ok, 200, _Headers} -> {ok, #{is_superuser => false}};
|
||||
{ok, 204, _Headers} ->
|
||||
{ok, #{is_superuser => false}};
|
||||
{ok, 200, _Headers} ->
|
||||
{ok, #{is_superuser => false}};
|
||||
{ok, 200, Headers, Body} ->
|
||||
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
||||
case safely_parse_body(ContentType, Body) of
|
||||
|
|
@ -173,24 +215,32 @@ authenticate(Credential, #{resource_id := ResourceId,
|
|||
{ok, #{is_superuser => false}}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{msg => "http_server_query_failed",
|
||||
?SLOG(error, #{
|
||||
msg => "http_server_query_failed",
|
||||
resource => ResourceId,
|
||||
reason => Reason}),
|
||||
reason => Reason
|
||||
}),
|
||||
ignore;
|
||||
Other ->
|
||||
Output = may_append_body(#{resource => ResourceId}, Other),
|
||||
case erlang:element(2, Other) of
|
||||
Code5xx when Code5xx >= 500 andalso Code5xx < 600 ->
|
||||
?SLOG(error, Output#{msg => "http_server_error",
|
||||
code => Code5xx}),
|
||||
?SLOG(error, Output#{
|
||||
msg => "http_server_error",
|
||||
code => Code5xx
|
||||
}),
|
||||
ignore;
|
||||
Code4xx when Code4xx >= 400 andalso Code4xx < 500 ->
|
||||
?SLOG(warning, Output#{msg => "refused_by_http_server",
|
||||
code => Code4xx}),
|
||||
?SLOG(warning, Output#{
|
||||
msg => "refused_by_http_server",
|
||||
code => Code4xx
|
||||
}),
|
||||
{error, not_authorized};
|
||||
OtherCode ->
|
||||
?SLOG(error, Output#{msg => "undesired_response_code",
|
||||
code => OtherCode}),
|
||||
?SLOG(error, Output#{
|
||||
msg => "undesired_response_code",
|
||||
code => OtherCode
|
||||
}),
|
||||
ignore
|
||||
end
|
||||
end.
|
||||
|
|
@ -207,22 +257,29 @@ parse_fullpath(RawURL) ->
|
|||
cow_http:parse_fullpath(to_bin(RawURL)).
|
||||
|
||||
default_headers() ->
|
||||
maps:put(<<"content-type">>,
|
||||
maps:put(
|
||||
<<"content-type">>,
|
||||
<<"application/json">>,
|
||||
default_headers_no_content_type()).
|
||||
default_headers_no_content_type()
|
||||
).
|
||||
|
||||
default_headers_no_content_type() ->
|
||||
#{ <<"accept">> => <<"application/json">>
|
||||
, <<"cache-control">> => <<"no-cache">>
|
||||
, <<"connection">> => <<"keep-alive">>
|
||||
, <<"keep-alive">> => <<"timeout=30, max=1000">>
|
||||
#{
|
||||
<<"accept">> => <<"application/json">>,
|
||||
<<"cache-control">> => <<"no-cache">>,
|
||||
<<"connection">> => <<"keep-alive">>,
|
||||
<<"keep-alive">> => <<"timeout=30, max=1000">>
|
||||
}.
|
||||
|
||||
transform_header_name(Headers) ->
|
||||
maps:fold(fun(K0, V, Acc) ->
|
||||
maps:fold(
|
||||
fun(K0, V, Acc) ->
|
||||
K = list_to_binary(string:to_lower(to_list(K0))),
|
||||
maps:put(K, V, Acc)
|
||||
end, #{}, Headers).
|
||||
end,
|
||||
#{},
|
||||
Headers
|
||||
).
|
||||
|
||||
check_ssl_opts(Conf) ->
|
||||
{BaseUrlWithPath, _Query} = parse_fullpath(get_conf_val("url", Conf)),
|
||||
|
|
@ -250,11 +307,13 @@ parse_url(URL) ->
|
|||
URIMap
|
||||
end.
|
||||
|
||||
generate_request(Credential, #{method := Method,
|
||||
generate_request(Credential, #{
|
||||
method := Method,
|
||||
path := Path,
|
||||
base_query_template := BaseQueryTemplate,
|
||||
headers := Headers,
|
||||
body_template := BodyTemplate}) ->
|
||||
body_template := BodyTemplate
|
||||
}) ->
|
||||
Body = emqx_authn_utils:render_deep(BodyTemplate, Credential),
|
||||
NBaseQuery = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential),
|
||||
case Method of
|
||||
|
|
|
|||
|
|
@ -22,23 +22,25 @@
|
|||
-include_lib("jose/include/jose_jwk.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
-export([
|
||||
start_link/1,
|
||||
stop/1
|
||||
]).
|
||||
|
||||
-export([ start_link/1
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
-export([ get_jwks/1
|
||||
, update/2
|
||||
]).
|
||||
-export([
|
||||
get_jwks/1,
|
||||
update/2
|
||||
]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
|
|
@ -67,11 +69,9 @@ init([Opts]) ->
|
|||
|
||||
handle_call(get_cached_jwks, _From, #{jwks := Jwks} = State) ->
|
||||
{reply, {ok, Jwks}, State};
|
||||
|
||||
handle_call({update, Opts}, _From, _State) ->
|
||||
NewState = handle_options(Opts),
|
||||
{reply, ok, refresh_jwks(NewState)};
|
||||
|
||||
handle_call(_Req, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
|
|
@ -80,7 +80,8 @@ handle_cast(_Msg, State) ->
|
|||
|
||||
handle_info({refresh_jwks, _TRef, refresh}, #{request_id := RequestID} = State) ->
|
||||
case RequestID of
|
||||
undefined -> ok;
|
||||
undefined ->
|
||||
ok;
|
||||
_ ->
|
||||
ok = httpc:cancel_request(RequestID),
|
||||
receive
|
||||
|
|
@ -90,37 +91,42 @@ handle_info({refresh_jwks, _TRef, refresh}, #{request_id := RequestID} = State)
|
|||
end
|
||||
end,
|
||||
{noreply, refresh_jwks(State)};
|
||||
|
||||
handle_info({http, {RequestID, Result}},
|
||||
#{request_id := RequestID, endpoint := Endpoint} = State0) ->
|
||||
handle_info(
|
||||
{http, {RequestID, Result}},
|
||||
#{request_id := RequestID, endpoint := Endpoint} = State0
|
||||
) ->
|
||||
?tp(debug, jwks_endpoint_response, #{request_id => RequestID}),
|
||||
State1 = State0#{request_id := undefined},
|
||||
NewState = case Result of
|
||||
NewState =
|
||||
case Result of
|
||||
{error, Reason} ->
|
||||
?SLOG(warning, #{msg => "failed_to_request_jwks_endpoint",
|
||||
?SLOG(warning, #{
|
||||
msg => "failed_to_request_jwks_endpoint",
|
||||
endpoint => Endpoint,
|
||||
reason => Reason}),
|
||||
reason => Reason
|
||||
}),
|
||||
State1;
|
||||
{StatusLine, Headers, Body} ->
|
||||
try
|
||||
JWKS = jose_jwk:from(emqx_json:decode(Body, [return_maps])),
|
||||
{_, JWKs} = JWKS#jose_jwk.keys,
|
||||
State1#{jwks := JWKs}
|
||||
catch _:_ ->
|
||||
?SLOG(warning, #{msg => "invalid_jwks_returned",
|
||||
catch
|
||||
_:_ ->
|
||||
?SLOG(warning, #{
|
||||
msg => "invalid_jwks_returned",
|
||||
endpoint => Endpoint,
|
||||
status => StatusLine,
|
||||
headers => Headers,
|
||||
body => Body}),
|
||||
body => Body
|
||||
}),
|
||||
State1
|
||||
end
|
||||
end,
|
||||
{noreply, NewState};
|
||||
|
||||
handle_info({http, {_, _}}, State) ->
|
||||
%% ignore
|
||||
{noreply, State};
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
|
|
@ -135,27 +141,45 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
handle_options(#{endpoint := Endpoint,
|
||||
handle_options(#{
|
||||
endpoint := Endpoint,
|
||||
refresh_interval := RefreshInterval0,
|
||||
ssl_opts := SSLOpts}) ->
|
||||
#{endpoint => Endpoint,
|
||||
ssl_opts := SSLOpts
|
||||
}) ->
|
||||
#{
|
||||
endpoint => Endpoint,
|
||||
refresh_interval => limit_refresh_interval(RefreshInterval0),
|
||||
ssl_opts => maps:to_list(SSLOpts),
|
||||
jwks => [],
|
||||
request_id => undefined}.
|
||||
request_id => undefined
|
||||
}.
|
||||
|
||||
refresh_jwks(#{endpoint := Endpoint,
|
||||
ssl_opts := SSLOpts} = State) ->
|
||||
HTTPOpts = [ {timeout, 5000}
|
||||
, {connect_timeout, 5000}
|
||||
, {ssl, SSLOpts}
|
||||
refresh_jwks(
|
||||
#{
|
||||
endpoint := Endpoint,
|
||||
ssl_opts := SSLOpts
|
||||
} = State
|
||||
) ->
|
||||
HTTPOpts = [
|
||||
{timeout, 5000},
|
||||
{connect_timeout, 5000},
|
||||
{ssl, SSLOpts}
|
||||
],
|
||||
NState = case httpc:request(get, {Endpoint, [{"Accept", "application/json"}]}, HTTPOpts,
|
||||
[{body_format, binary}, {sync, false}, {receiver, self()}]) of
|
||||
NState =
|
||||
case
|
||||
httpc:request(
|
||||
get,
|
||||
{Endpoint, [{"Accept", "application/json"}]},
|
||||
HTTPOpts,
|
||||
[{body_format, binary}, {sync, false}, {receiver, self()}]
|
||||
)
|
||||
of
|
||||
{error, Reason} ->
|
||||
?tp(warning, jwks_endpoint_request_fail, #{endpoint => Endpoint,
|
||||
?tp(warning, jwks_endpoint_request_fail, #{
|
||||
endpoint => Endpoint,
|
||||
http_opts => HTTPOpts,
|
||||
reason => Reason}),
|
||||
reason => Reason
|
||||
}),
|
||||
State;
|
||||
{ok, RequestID} ->
|
||||
?tp(debug, jwks_endpoint_request_ok, #{request_id => RequestID}),
|
||||
|
|
|
|||
|
|
@ -23,17 +23,19 @@
|
|||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ refs/0
|
||||
, create/2
|
||||
, update/2
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
-export([
|
||||
refs/0,
|
||||
create/2,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
|
|
@ -42,49 +44,56 @@
|
|||
namespace() -> "authn-jwt".
|
||||
|
||||
roots() ->
|
||||
[ {?CONF_NS,
|
||||
hoconsc:mk(hoconsc:union(refs()),
|
||||
#{})}
|
||||
[
|
||||
{?CONF_NS,
|
||||
hoconsc:mk(
|
||||
hoconsc:union(refs()),
|
||||
#{}
|
||||
)}
|
||||
].
|
||||
|
||||
fields('hmac-based') ->
|
||||
[ {use_jwks, {enum, [false]}}
|
||||
, {algorithm, {enum, ['hmac-based']}}
|
||||
, {secret, fun secret/1}
|
||||
, {secret_base64_encoded, fun secret_base64_encoded/1}
|
||||
[
|
||||
{use_jwks, {enum, [false]}},
|
||||
{algorithm, {enum, ['hmac-based']}},
|
||||
{secret, fun secret/1},
|
||||
{secret_base64_encoded, fun secret_base64_encoded/1}
|
||||
] ++ common_fields();
|
||||
|
||||
fields('public-key') ->
|
||||
[ {use_jwks, {enum, [false]}}
|
||||
, {algorithm, {enum, ['public-key']}}
|
||||
, {certificate, fun certificate/1}
|
||||
[
|
||||
{use_jwks, {enum, [false]}},
|
||||
{algorithm, {enum, ['public-key']}},
|
||||
{certificate, fun certificate/1}
|
||||
] ++ common_fields();
|
||||
|
||||
fields('jwks') ->
|
||||
[ {use_jwks, {enum, [true]}}
|
||||
, {endpoint, fun endpoint/1}
|
||||
, {refresh_interval, fun refresh_interval/1}
|
||||
, {ssl, #{type => hoconsc:union([ hoconsc:ref(?MODULE, ssl_enable)
|
||||
, hoconsc:ref(?MODULE, ssl_disable)
|
||||
[
|
||||
{use_jwks, {enum, [true]}},
|
||||
{endpoint, fun endpoint/1},
|
||||
{refresh_interval, fun refresh_interval/1},
|
||||
{ssl, #{
|
||||
type => hoconsc:union([
|
||||
hoconsc:ref(?MODULE, ssl_enable),
|
||||
hoconsc:ref(?MODULE, ssl_disable)
|
||||
]),
|
||||
default => #{<<"enable">> => false}}}
|
||||
default => #{<<"enable">> => false}
|
||||
}}
|
||||
] ++ common_fields();
|
||||
|
||||
fields(ssl_enable) ->
|
||||
[ {enable, #{type => true}}
|
||||
, {cacertfile, fun cacertfile/1}
|
||||
, {certfile, fun certfile/1}
|
||||
, {keyfile, fun keyfile/1}
|
||||
, {verify, fun verify/1}
|
||||
, {server_name_indication, fun server_name_indication/1}
|
||||
[
|
||||
{enable, #{type => true}},
|
||||
{cacertfile, fun cacertfile/1},
|
||||
{certfile, fun certfile/1},
|
||||
{keyfile, fun keyfile/1},
|
||||
{verify, fun verify/1},
|
||||
{server_name_indication, fun server_name_indication/1}
|
||||
];
|
||||
|
||||
fields(ssl_disable) ->
|
||||
[ {enable, #{type => false}} ].
|
||||
[{enable, #{type => false}}].
|
||||
|
||||
common_fields() ->
|
||||
[ {mechanism, emqx_authn_schema:mechanism('jwt')}
|
||||
, {verify_claims, fun verify_claims/1}
|
||||
[
|
||||
{mechanism, emqx_authn_schema:mechanism('jwt')},
|
||||
{verify_claims, fun verify_claims/1}
|
||||
] ++ emqx_authn_schema:common_fields().
|
||||
|
||||
secret(type) -> binary();
|
||||
|
|
@ -121,23 +130,28 @@ verify(_) -> undefined.
|
|||
server_name_indication(type) -> string();
|
||||
server_name_indication(_) -> undefined.
|
||||
|
||||
verify_claims(type) -> list();
|
||||
verify_claims(default) -> #{};
|
||||
verify_claims(validator) -> [fun do_check_verify_claims/1];
|
||||
verify_claims(type) ->
|
||||
list();
|
||||
verify_claims(default) ->
|
||||
#{};
|
||||
verify_claims(validator) ->
|
||||
[fun do_check_verify_claims/1];
|
||||
verify_claims(converter) ->
|
||||
fun(VerifyClaims) ->
|
||||
[{to_binary(K), V} || {K, V} <- maps:to_list(VerifyClaims)]
|
||||
end;
|
||||
verify_claims(_) -> undefined.
|
||||
verify_claims(_) ->
|
||||
undefined.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
refs() ->
|
||||
[ hoconsc:ref(?MODULE, 'hmac-based')
|
||||
, hoconsc:ref(?MODULE, 'public-key')
|
||||
, hoconsc:ref(?MODULE, 'jwks')
|
||||
[
|
||||
hoconsc:ref(?MODULE, 'hmac-based'),
|
||||
hoconsc:ref(?MODULE, 'public-key'),
|
||||
hoconsc:ref(?MODULE, 'jwks')
|
||||
].
|
||||
|
||||
create(_AuthenticatorID, Config) ->
|
||||
|
|
@ -146,18 +160,22 @@ create(_AuthenticatorID, Config) ->
|
|||
create(#{verify_claims := VerifyClaims} = Config) ->
|
||||
create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
|
||||
|
||||
update(#{use_jwks := false} = Config,
|
||||
#{jwk := Connector})
|
||||
when is_pid(Connector) ->
|
||||
update(
|
||||
#{use_jwks := false} = Config,
|
||||
#{jwk := Connector}
|
||||
) when
|
||||
is_pid(Connector)
|
||||
->
|
||||
_ = emqx_authn_jwks_connector:stop(Connector),
|
||||
create(Config);
|
||||
|
||||
update(#{use_jwks := false} = Config, _State) ->
|
||||
create(Config);
|
||||
|
||||
update(#{use_jwks := true} = Config,
|
||||
#{jwk := Connector} = State)
|
||||
when is_pid(Connector) ->
|
||||
update(
|
||||
#{use_jwks := true} = Config,
|
||||
#{jwk := Connector} = State
|
||||
) when
|
||||
is_pid(Connector)
|
||||
->
|
||||
ok = emqx_authn_jwks_connector:update(Connector, connector_opts(Config)),
|
||||
case maps:get(verify_cliams, Config, undefined) of
|
||||
undefined ->
|
||||
|
|
@ -165,15 +183,17 @@ update(#{use_jwks := true} = Config,
|
|||
VerifyClaims ->
|
||||
{ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}}
|
||||
end;
|
||||
|
||||
update(#{use_jwks := true} = Config, _State) ->
|
||||
create(Config).
|
||||
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(Credential = #{password := JWT}, #{jwk := JWK,
|
||||
verify_claims := VerifyClaims0}) ->
|
||||
JWKs = case erlang:is_pid(JWK) of
|
||||
authenticate(Credential = #{password := JWT}, #{
|
||||
jwk := JWK,
|
||||
verify_claims := VerifyClaims0
|
||||
}) ->
|
||||
JWKs =
|
||||
case erlang:is_pid(JWK) of
|
||||
false ->
|
||||
[JWK];
|
||||
true ->
|
||||
|
|
@ -197,41 +217,54 @@ destroy(_) ->
|
|||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
create2(#{use_jwks := false,
|
||||
create2(#{
|
||||
use_jwks := false,
|
||||
algorithm := 'hmac-based',
|
||||
secret := Secret0,
|
||||
secret_base64_encoded := Base64Encoded,
|
||||
verify_claims := VerifyClaims}) ->
|
||||
verify_claims := VerifyClaims
|
||||
}) ->
|
||||
case may_decode_secret(Base64Encoded, Secret0) of
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
Secret ->
|
||||
JWK = jose_jwk:from_oct(Secret),
|
||||
{ok, #{jwk => JWK,
|
||||
verify_claims => VerifyClaims}}
|
||||
{ok, #{
|
||||
jwk => JWK,
|
||||
verify_claims => VerifyClaims
|
||||
}}
|
||||
end;
|
||||
|
||||
create2(#{use_jwks := false,
|
||||
create2(#{
|
||||
use_jwks := false,
|
||||
algorithm := 'public-key',
|
||||
certificate := Certificate,
|
||||
verify_claims := VerifyClaims}) ->
|
||||
verify_claims := VerifyClaims
|
||||
}) ->
|
||||
JWK = create_jwk_from_pem_or_file(Certificate),
|
||||
{ok, #{jwk => JWK,
|
||||
verify_claims => VerifyClaims}};
|
||||
|
||||
create2(#{use_jwks := true,
|
||||
verify_claims := VerifyClaims} = Config) ->
|
||||
{ok, #{
|
||||
jwk => JWK,
|
||||
verify_claims => VerifyClaims
|
||||
}};
|
||||
create2(
|
||||
#{
|
||||
use_jwks := true,
|
||||
verify_claims := VerifyClaims
|
||||
} = Config
|
||||
) ->
|
||||
case emqx_authn_jwks_connector:start_link(connector_opts(Config)) of
|
||||
{ok, Connector} ->
|
||||
{ok, #{jwk => Connector,
|
||||
verify_claims => VerifyClaims}};
|
||||
{ok, #{
|
||||
jwk => Connector,
|
||||
verify_claims => VerifyClaims
|
||||
}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
create_jwk_from_pem_or_file(CertfileOrFilePath)
|
||||
when is_binary(CertfileOrFilePath);
|
||||
is_list(CertfileOrFilePath) ->
|
||||
create_jwk_from_pem_or_file(CertfileOrFilePath) when
|
||||
is_binary(CertfileOrFilePath);
|
||||
is_list(CertfileOrFilePath)
|
||||
->
|
||||
case filelib:is_file(CertfileOrFilePath) of
|
||||
true ->
|
||||
jose_jwk:from_pem_file(CertfileOrFilePath);
|
||||
|
|
@ -240,18 +273,20 @@ create_jwk_from_pem_or_file(CertfileOrFilePath)
|
|||
end.
|
||||
|
||||
connector_opts(#{ssl := #{enable := Enable} = SSL} = Config) ->
|
||||
SSLOpts = case Enable of
|
||||
SSLOpts =
|
||||
case Enable of
|
||||
true -> maps:without([enable], SSL);
|
||||
false -> #{}
|
||||
end,
|
||||
Config#{ssl_opts => SSLOpts}.
|
||||
|
||||
|
||||
may_decode_secret(false, Secret) -> Secret;
|
||||
may_decode_secret(false, Secret) ->
|
||||
Secret;
|
||||
may_decode_secret(true, Secret) ->
|
||||
try base64:decode(Secret)
|
||||
try
|
||||
base64:decode(Secret)
|
||||
catch
|
||||
error : _ ->
|
||||
error:_ ->
|
||||
{error, {invalid_parameter, secret}}
|
||||
end.
|
||||
|
||||
|
|
@ -288,7 +323,9 @@ verify(JWS, [JWK | More], VerifyClaims) ->
|
|||
|
||||
verify_claims(Claims, VerifyClaims0) ->
|
||||
Now = os:system_time(seconds),
|
||||
VerifyClaims = [{<<"exp">>, fun(ExpireTime) ->
|
||||
VerifyClaims =
|
||||
[
|
||||
{<<"exp">>, fun(ExpireTime) ->
|
||||
Now < ExpireTime
|
||||
end},
|
||||
{<<"iat">>, fun(IssueAt) ->
|
||||
|
|
@ -296,7 +333,8 @@ verify_claims(Claims, VerifyClaims0) ->
|
|||
end},
|
||||
{<<"nbf">>, fun(NotBefore) ->
|
||||
NotBefore =< Now
|
||||
end}] ++ VerifyClaims0,
|
||||
end}
|
||||
] ++ VerifyClaims0,
|
||||
do_verify_claims(Claims, VerifyClaims).
|
||||
|
||||
do_verify_claims(_Claims, []) ->
|
||||
|
|
|
|||
|
|
@ -23,40 +23,45 @@
|
|||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ refs/0
|
||||
, create/2
|
||||
, update/2
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
-export([
|
||||
refs/0,
|
||||
create/2,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
-export([ import_users/2
|
||||
, add_user/2
|
||||
, delete_user/2
|
||||
, update_user/3
|
||||
, lookup_user/2
|
||||
, list_users/2
|
||||
]).
|
||||
-export([
|
||||
import_users/2,
|
||||
add_user/2,
|
||||
delete_user/2,
|
||||
update_user/3,
|
||||
lookup_user/2,
|
||||
list_users/2
|
||||
]).
|
||||
|
||||
-export([ query/4
|
||||
, format_user_info/1
|
||||
, group_match_spec/1]).
|
||||
-export([
|
||||
query/4,
|
||||
format_user_info/1,
|
||||
group_match_spec/1
|
||||
]).
|
||||
|
||||
-type user_id_type() :: clientid | username.
|
||||
-type user_group() :: binary().
|
||||
-type user_id() :: binary().
|
||||
|
||||
-record(user_info,
|
||||
{ user_id :: {user_group(), user_id()}
|
||||
, password_hash :: binary()
|
||||
, salt :: binary()
|
||||
, is_superuser :: boolean()
|
||||
}).
|
||||
-record(user_info, {
|
||||
user_id :: {user_group(), user_id()},
|
||||
password_hash :: binary(),
|
||||
salt :: binary(),
|
||||
is_superuser :: boolean()
|
||||
}).
|
||||
|
||||
-reflect_type([user_id_type/0]).
|
||||
|
||||
|
|
@ -65,9 +70,11 @@
|
|||
-boot_mnesia({mnesia, [boot]}).
|
||||
|
||||
-define(TAB, ?MODULE).
|
||||
-define(AUTHN_QSCHEMA, [ {<<"like_username">>, binary}
|
||||
, {<<"like_clientid">>, binary}
|
||||
, {<<"user_group">>, binary}]).
|
||||
-define(AUTHN_QSCHEMA, [
|
||||
{<<"like_username">>, binary},
|
||||
{<<"like_clientid">>, binary},
|
||||
{<<"user_group">>, binary}
|
||||
]).
|
||||
-define(QUERY_FUN, {?MODULE, query}).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
|
@ -75,14 +82,15 @@
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
%% @doc Create or replicate tables.
|
||||
-spec(mnesia(boot | copy) -> ok).
|
||||
-spec mnesia(boot | copy) -> ok.
|
||||
mnesia(boot) ->
|
||||
ok = mria:create_table(?TAB, [
|
||||
{rlog_shard, ?AUTH_SHARD},
|
||||
{storage, disc_copies},
|
||||
{record_name, user_info},
|
||||
{attributes, record_info(fields, user_info)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]).
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
|
|
@ -93,10 +101,11 @@ namespace() -> "authn-builtin_db".
|
|||
roots() -> [?CONF_NS].
|
||||
|
||||
fields(?CONF_NS) ->
|
||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
||||
, {backend, emqx_authn_schema:backend('built_in_database')}
|
||||
, {user_id_type, fun user_id_type/1}
|
||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_rw/1}
|
||||
[
|
||||
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||
{backend, emqx_authn_schema:backend('built_in_database')},
|
||||
{user_id_type, fun user_id_type/1},
|
||||
{password_hash_algorithm, fun emqx_authn_password_hashing:type_rw/1}
|
||||
] ++ emqx_authn_schema:common_fields().
|
||||
|
||||
user_id_type(type) -> user_id_type();
|
||||
|
|
@ -110,13 +119,19 @@ user_id_type(_) -> undefined.
|
|||
refs() ->
|
||||
[hoconsc:ref(?MODULE, ?CONF_NS)].
|
||||
|
||||
create(AuthenticatorID,
|
||||
#{user_id_type := Type,
|
||||
password_hash_algorithm := Algorithm}) ->
|
||||
create(
|
||||
AuthenticatorID,
|
||||
#{
|
||||
user_id_type := Type,
|
||||
password_hash_algorithm := Algorithm
|
||||
}
|
||||
) ->
|
||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||
State = #{user_group => AuthenticatorID,
|
||||
State = #{
|
||||
user_group => AuthenticatorID,
|
||||
user_id_type => Type,
|
||||
password_hash_algorithm => Algorithm},
|
||||
password_hash_algorithm => Algorithm
|
||||
},
|
||||
{ok, State}.
|
||||
|
||||
update(Config, #{user_group := ID}) ->
|
||||
|
|
@ -124,17 +139,24 @@ update(Config, #{user_group := ID}) ->
|
|||
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(#{password := Password} = Credential,
|
||||
#{user_group := UserGroup,
|
||||
authenticate(
|
||||
#{password := Password} = Credential,
|
||||
#{
|
||||
user_group := UserGroup,
|
||||
user_id_type := Type,
|
||||
password_hash_algorithm := Algorithm}) ->
|
||||
password_hash_algorithm := Algorithm
|
||||
}
|
||||
) ->
|
||||
UserID = get_user_identity(Credential, Type),
|
||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||
[] ->
|
||||
ignore;
|
||||
[#user_info{password_hash = PasswordHash, salt = Salt, is_superuser = IsSuperuser}] ->
|
||||
case emqx_authn_password_hashing:check_password(
|
||||
Algorithm, Salt, PasswordHash, Password) of
|
||||
case
|
||||
emqx_authn_password_hashing:check_password(
|
||||
Algorithm, Salt, PasswordHash, Password
|
||||
)
|
||||
of
|
||||
true -> {ok, #{is_superuser => IsSuperuser}};
|
||||
false -> {error, bad_username_or_password}
|
||||
end
|
||||
|
|
@ -147,9 +169,10 @@ destroy(#{user_group := UserGroup}) ->
|
|||
fun(User) ->
|
||||
mnesia:delete_object(?TAB, User, write)
|
||||
end,
|
||||
mnesia:select(?TAB, group_match_spec(UserGroup), write))
|
||||
end).
|
||||
|
||||
mnesia:select(?TAB, group_match_spec(UserGroup), write)
|
||||
)
|
||||
end
|
||||
).
|
||||
|
||||
import_users(Filename0, State) ->
|
||||
Filename = to_binary(Filename0),
|
||||
|
|
@ -164,10 +187,16 @@ import_users(Filename0, State) ->
|
|||
{error, {unsupported_file_format, Extension}}
|
||||
end.
|
||||
|
||||
add_user(#{user_id := UserID,
|
||||
password := Password} = UserInfo,
|
||||
#{user_group := UserGroup,
|
||||
password_hash_algorithm := Algorithm}) ->
|
||||
add_user(
|
||||
#{
|
||||
user_id := UserID,
|
||||
password := Password
|
||||
} = UserInfo,
|
||||
#{
|
||||
user_group := UserGroup,
|
||||
password_hash_algorithm := Algorithm
|
||||
}
|
||||
) ->
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||
|
|
@ -179,7 +208,8 @@ add_user(#{user_id := UserID,
|
|||
[_] ->
|
||||
{error, already_exist}
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
delete_user(UserID, #{user_group := UserGroup}) ->
|
||||
trans(
|
||||
|
|
@ -190,31 +220,44 @@ delete_user(UserID, #{user_group := UserGroup}) ->
|
|||
[_] ->
|
||||
mnesia:delete(?TAB, {UserGroup, UserID}, write)
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
update_user(UserID, UserInfo,
|
||||
#{user_group := UserGroup,
|
||||
password_hash_algorithm := Algorithm}) ->
|
||||
update_user(
|
||||
UserID,
|
||||
UserInfo,
|
||||
#{
|
||||
user_group := UserGroup,
|
||||
password_hash_algorithm := Algorithm
|
||||
}
|
||||
) ->
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[#user_info{ password_hash = PasswordHash
|
||||
, salt = Salt
|
||||
, is_superuser = IsSuperuser}] ->
|
||||
[
|
||||
#user_info{
|
||||
password_hash = PasswordHash,
|
||||
salt = Salt,
|
||||
is_superuser = IsSuperuser
|
||||
}
|
||||
] ->
|
||||
NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser),
|
||||
{NPasswordHash, NSalt} = case UserInfo of
|
||||
{NPasswordHash, NSalt} =
|
||||
case UserInfo of
|
||||
#{password := Password} ->
|
||||
emqx_authn_password_hashing:hash(
|
||||
Algorithm, Password);
|
||||
Algorithm, Password
|
||||
);
|
||||
#{} ->
|
||||
{PasswordHash, Salt}
|
||||
end,
|
||||
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
|
||||
{ok, #{user_id => UserID, is_superuser => NSuperuser}}
|
||||
end
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
lookup_user(UserID, #{user_group := UserGroup}) ->
|
||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||
|
|
@ -233,14 +276,23 @@ list_users(QueryString, #{user_group := UserGroup}) ->
|
|||
|
||||
query(Tab, {QString, []}, Continuation, Limit) ->
|
||||
Ms = ms_from_qstring(QString),
|
||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
||||
fun format_user_info/1);
|
||||
|
||||
emqx_mgmt_api:select_table_with_count(
|
||||
Tab,
|
||||
Ms,
|
||||
Continuation,
|
||||
Limit,
|
||||
fun format_user_info/1
|
||||
);
|
||||
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||
Ms = ms_from_qstring(QString),
|
||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
||||
fun format_user_info/1).
|
||||
emqx_mgmt_api:select_table_with_count(
|
||||
Tab,
|
||||
{Ms, FuzzyFilterFun},
|
||||
Continuation,
|
||||
Limit,
|
||||
fun format_user_info/1
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Match funcs
|
||||
|
|
@ -248,17 +300,23 @@ query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
|||
%% Fuzzy username funcs
|
||||
fuzzy_filter_fun(Fuzzy) ->
|
||||
fun(MsRaws) when is_list(MsRaws) ->
|
||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
||||
, MsRaws)
|
||||
lists:filter(
|
||||
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||
MsRaws
|
||||
)
|
||||
end.
|
||||
|
||||
run_fuzzy_filter(_, []) ->
|
||||
true;
|
||||
run_fuzzy_filter( E = #user_info{user_id = {_, UserID}}
|
||||
, [{username, like, UsernameSubStr} | Fuzzy]) ->
|
||||
run_fuzzy_filter(
|
||||
E = #user_info{user_id = {_, UserID}},
|
||||
[{username, like, UsernameSubStr} | Fuzzy]
|
||||
) ->
|
||||
binary:match(UserID, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy);
|
||||
run_fuzzy_filter( E = #user_info{user_id = {_, UserID}}
|
||||
, [{clientid, like, ClientIDSubStr} | Fuzzy]) ->
|
||||
run_fuzzy_filter(
|
||||
E = #user_info{user_id = {_, UserID}},
|
||||
[{clientid, like, ClientIDSubStr} | Fuzzy]
|
||||
) ->
|
||||
binary:match(UserID, ClientIDSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
|
@ -297,9 +355,15 @@ import_users_from_csv(Filename, #{user_group := UserGroup}) ->
|
|||
|
||||
import(_UserGroup, []) ->
|
||||
ok;
|
||||
import(UserGroup, [#{<<"user_id">> := UserID,
|
||||
<<"password_hash">> := PasswordHash} = UserInfo | More])
|
||||
when is_binary(UserID) andalso is_binary(PasswordHash) ->
|
||||
import(UserGroup, [
|
||||
#{
|
||||
<<"user_id">> := UserID,
|
||||
<<"password_hash">> := PasswordHash
|
||||
} = UserInfo
|
||||
| More
|
||||
]) when
|
||||
is_binary(UserID) andalso is_binary(PasswordHash)
|
||||
->
|
||||
Salt = maps:get(<<"salt">>, UserInfo, <<>>),
|
||||
IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false),
|
||||
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
|
||||
|
|
@ -313,8 +377,11 @@ import(UserGroup, File, Seq) ->
|
|||
{ok, Line} ->
|
||||
Fields = binary:split(Line, [<<",">>, <<" ">>, <<"\n">>], [global, trim_all]),
|
||||
case get_user_info_by_seq(Fields, Seq) of
|
||||
{ok, #{user_id := UserID,
|
||||
password_hash := PasswordHash} = UserInfo} ->
|
||||
{ok,
|
||||
#{
|
||||
user_id := UserID,
|
||||
password_hash := PasswordHash
|
||||
} = UserInfo} ->
|
||||
Salt = maps:get(salt, UserInfo, <<>>),
|
||||
IsSuperuser = maps:get(is_superuser, UserInfo, false),
|
||||
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
|
||||
|
|
@ -360,10 +427,12 @@ get_user_info_by_seq(_, _, _) ->
|
|||
{error, bad_format}.
|
||||
|
||||
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) ->
|
||||
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
||||
UserInfo = #user_info{
|
||||
user_id = {UserGroup, UserID},
|
||||
password_hash = PasswordHash,
|
||||
salt = Salt,
|
||||
is_superuser = IsSuperuser},
|
||||
is_superuser = IsSuperuser
|
||||
},
|
||||
mnesia:write(?TAB, UserInfo, write).
|
||||
|
||||
%% TODO: Support other type
|
||||
|
|
@ -392,15 +461,21 @@ format_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser})
|
|||
#{user_id => UserID, is_superuser => IsSuperuser}.
|
||||
|
||||
ms_from_qstring(QString) ->
|
||||
[Ms] = lists:foldl(fun({user_group, '=:=', UserGroup}, AccIn) ->
|
||||
[Ms] = lists:foldl(
|
||||
fun
|
||||
({user_group, '=:=', UserGroup}, AccIn) ->
|
||||
[group_match_spec(UserGroup) | AccIn];
|
||||
(_, AccIn) ->
|
||||
AccIn
|
||||
end, [], QString),
|
||||
end,
|
||||
[],
|
||||
QString
|
||||
),
|
||||
Ms.
|
||||
|
||||
group_match_spec(UserGroup) ->
|
||||
ets:fun2ms(
|
||||
fun(#user_info{user_id = {Group, _}} = User) when Group =:= UserGroup ->
|
||||
User
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
|
|
|||
|
|
@ -23,18 +23,20 @@
|
|||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
, desc/1
|
||||
]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1,
|
||||
desc/1
|
||||
]).
|
||||
|
||||
-export([ refs/0
|
||||
, create/2
|
||||
, update/2
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
-export([
|
||||
refs/0,
|
||||
create/2,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
|
|
@ -43,16 +45,18 @@
|
|||
namespace() -> "authn-mongodb".
|
||||
|
||||
roots() ->
|
||||
[ {?CONF_NS, hoconsc:mk(hoconsc:union(refs()),
|
||||
#{})}
|
||||
[
|
||||
{?CONF_NS,
|
||||
hoconsc:mk(
|
||||
hoconsc:union(refs()),
|
||||
#{}
|
||||
)}
|
||||
].
|
||||
|
||||
fields(standalone) ->
|
||||
common_fields() ++ emqx_connector_mongo:fields(single);
|
||||
|
||||
fields('replica-set') ->
|
||||
common_fields() ++ emqx_connector_mongo:fields(rs);
|
||||
|
||||
fields('sharded-cluster') ->
|
||||
common_fields() ++ emqx_connector_mongo:fields(sharded).
|
||||
|
||||
|
|
@ -66,26 +70,30 @@ desc(_) ->
|
|||
undefined.
|
||||
|
||||
common_fields() ->
|
||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
||||
, {backend, emqx_authn_schema:backend(mongodb)}
|
||||
, {collection, fun collection/1}
|
||||
, {selector, fun selector/1}
|
||||
, {password_hash_field, fun password_hash_field/1}
|
||||
, {salt_field, fun salt_field/1}
|
||||
, {is_superuser_field, fun is_superuser_field/1}
|
||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||
[
|
||||
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||
{backend, emqx_authn_schema:backend(mongodb)},
|
||||
{collection, fun collection/1},
|
||||
{selector, fun selector/1},
|
||||
{password_hash_field, fun password_hash_field/1},
|
||||
{salt_field, fun salt_field/1},
|
||||
{is_superuser_field, fun is_superuser_field/1},
|
||||
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||
] ++ emqx_authn_schema:common_fields().
|
||||
|
||||
collection(type) -> binary();
|
||||
collection(desc) -> "Collection used to store authentication data.";
|
||||
collection(_) -> undefined.
|
||||
|
||||
selector(type) -> map();
|
||||
selector(desc) -> "Statement that is executed during the authentication process. "
|
||||
selector(type) ->
|
||||
map();
|
||||
selector(desc) ->
|
||||
"Statement that is executed during the authentication process. "
|
||||
"Commands can support following wildcards:\n"
|
||||
" - `${username}`: substituted with client's username\n"
|
||||
" - `${clientid}`: substituted with the clientid";
|
||||
selector(_) -> undefined.
|
||||
selector(_) ->
|
||||
undefined.
|
||||
|
||||
password_hash_field(type) -> binary();
|
||||
password_hash_field(desc) -> "Document field that contains password hash.";
|
||||
|
|
@ -106,9 +114,10 @@ is_superuser_field(_) -> undefined.
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
refs() ->
|
||||
[ hoconsc:ref(?MODULE, standalone)
|
||||
, hoconsc:ref(?MODULE, 'replica-set')
|
||||
, hoconsc:ref(?MODULE, 'sharded-cluster')
|
||||
[
|
||||
hoconsc:ref(?MODULE, standalone),
|
||||
hoconsc:ref(?MODULE, 'replica-set'),
|
||||
hoconsc:ref(?MODULE, 'sharded-cluster')
|
||||
].
|
||||
|
||||
create(_AuthenticatorID, Config) ->
|
||||
|
|
@ -117,24 +126,32 @@ create(_AuthenticatorID, Config) ->
|
|||
create(#{selector := Selector} = Config) ->
|
||||
SelectorTemplate = emqx_authn_utils:parse_deep(Selector),
|
||||
State = maps:with(
|
||||
[collection,
|
||||
[
|
||||
collection,
|
||||
password_hash_field,
|
||||
salt_field,
|
||||
is_superuser_field,
|
||||
password_hash_algorithm,
|
||||
salt_position],
|
||||
Config),
|
||||
salt_position
|
||||
],
|
||||
Config
|
||||
),
|
||||
#{password_hash_algorithm := Algorithm} = State,
|
||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||
NState = State#{
|
||||
selector_template => SelectorTemplate,
|
||||
resource_id => ResourceId},
|
||||
case emqx_resource:create_local(ResourceId,
|
||||
resource_id => ResourceId
|
||||
},
|
||||
case
|
||||
emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_mongo,
|
||||
Config,
|
||||
#{}) of
|
||||
#{}
|
||||
)
|
||||
of
|
||||
{ok, already_created} ->
|
||||
{ok, NState};
|
||||
{ok, _} ->
|
||||
|
|
@ -154,30 +171,39 @@ update(Config, State) ->
|
|||
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(#{password := Password} = Credential,
|
||||
#{collection := Collection,
|
||||
authenticate(
|
||||
#{password := Password} = Credential,
|
||||
#{
|
||||
collection := Collection,
|
||||
selector_template := SelectorTemplate,
|
||||
resource_id := ResourceId} = State) ->
|
||||
resource_id := ResourceId
|
||||
} = State
|
||||
) ->
|
||||
Selector = emqx_authn_utils:render_deep(SelectorTemplate, Credential),
|
||||
case emqx_resource:query(ResourceId, {find_one, Collection, Selector, #{}}) of
|
||||
undefined -> ignore;
|
||||
undefined ->
|
||||
ignore;
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{msg => "mongodb_query_failed",
|
||||
?SLOG(error, #{
|
||||
msg => "mongodb_query_failed",
|
||||
resource => ResourceId,
|
||||
collection => Collection,
|
||||
selector => Selector,
|
||||
reason => Reason}),
|
||||
reason => Reason
|
||||
}),
|
||||
ignore;
|
||||
Doc ->
|
||||
case check_password(Password, Doc, State) of
|
||||
ok ->
|
||||
{ok, is_superuser(Doc, State)};
|
||||
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
||||
?SLOG(error, #{msg => "cannot_find_password_hash_field",
|
||||
?SLOG(error, #{
|
||||
msg => "cannot_find_password_hash_field",
|
||||
resource => ResourceId,
|
||||
collection => Collection,
|
||||
selector => Selector,
|
||||
password_hash_field => PasswordHashField}),
|
||||
password_hash_field => PasswordHashField
|
||||
}),
|
||||
ignore;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
|
|
@ -194,15 +220,20 @@ destroy(#{resource_id := ResourceId}) ->
|
|||
|
||||
check_password(undefined, _Selected, _State) ->
|
||||
{error, bad_username_or_password};
|
||||
check_password(Password,
|
||||
check_password(
|
||||
Password,
|
||||
Doc,
|
||||
#{password_hash_algorithm := Algorithm,
|
||||
password_hash_field := PasswordHashField} = State) ->
|
||||
#{
|
||||
password_hash_algorithm := Algorithm,
|
||||
password_hash_field := PasswordHashField
|
||||
} = State
|
||||
) ->
|
||||
case maps:get(PasswordHashField, Doc, undefined) of
|
||||
undefined ->
|
||||
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
||||
Hash ->
|
||||
Salt = case maps:get(salt_field, State, undefined) of
|
||||
Salt =
|
||||
case maps:get(salt_field, State, undefined) of
|
||||
undefined -> <<>>;
|
||||
SaltField -> maps:get(SaltField, Doc, <<>>)
|
||||
end,
|
||||
|
|
|
|||
|
|
@ -23,17 +23,19 @@
|
|||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ refs/0
|
||||
, create/2
|
||||
, update/2
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
-export([
|
||||
refs/0,
|
||||
create/2,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
|
|
@ -44,13 +46,14 @@ namespace() -> "authn-mysql".
|
|||
roots() -> [?CONF_NS].
|
||||
|
||||
fields(?CONF_NS) ->
|
||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
||||
, {backend, emqx_authn_schema:backend(mysql)}
|
||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||
, {query, fun query/1}
|
||||
, {query_timeout, fun query_timeout/1}
|
||||
] ++ emqx_authn_schema:common_fields()
|
||||
++ emqx_connector_mysql:fields(config).
|
||||
[
|
||||
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||
{backend, emqx_authn_schema:backend(mysql)},
|
||||
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1},
|
||||
{query, fun query/1},
|
||||
{query_timeout, fun query_timeout/1}
|
||||
] ++ emqx_authn_schema:common_fields() ++
|
||||
emqx_connector_mysql:fields(config).
|
||||
|
||||
query(type) -> string();
|
||||
query(_) -> undefined.
|
||||
|
|
@ -69,23 +72,32 @@ refs() ->
|
|||
create(_AuthenticatorID, Config) ->
|
||||
create(Config).
|
||||
|
||||
create(#{password_hash_algorithm := Algorithm,
|
||||
create(
|
||||
#{
|
||||
password_hash_algorithm := Algorithm,
|
||||
query := Query0,
|
||||
query_timeout := QueryTimeout
|
||||
} = Config) ->
|
||||
} = Config
|
||||
) ->
|
||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||
{Query, PlaceHolders} = emqx_authn_utils:parse_sql(Query0, '?'),
|
||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||
State = #{password_hash_algorithm => Algorithm,
|
||||
State = #{
|
||||
password_hash_algorithm => Algorithm,
|
||||
query => Query,
|
||||
placeholders => PlaceHolders,
|
||||
query_timeout => QueryTimeout,
|
||||
resource_id => ResourceId},
|
||||
case emqx_resource:create_local(ResourceId,
|
||||
resource_id => ResourceId
|
||||
},
|
||||
case
|
||||
emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_mysql,
|
||||
Config,
|
||||
#{}) of
|
||||
#{}
|
||||
)
|
||||
of
|
||||
{ok, already_created} ->
|
||||
{ok, State};
|
||||
{ok, _} ->
|
||||
|
|
@ -105,31 +117,41 @@ update(Config, State) ->
|
|||
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(#{password := Password} = Credential,
|
||||
#{placeholders := PlaceHolders,
|
||||
authenticate(
|
||||
#{password := Password} = Credential,
|
||||
#{
|
||||
placeholders := PlaceHolders,
|
||||
query := Query,
|
||||
query_timeout := Timeout,
|
||||
resource_id := ResourceId,
|
||||
password_hash_algorithm := Algorithm}) ->
|
||||
password_hash_algorithm := Algorithm
|
||||
}
|
||||
) ->
|
||||
Params = emqx_authn_utils:render_sql_params(PlaceHolders, Credential),
|
||||
case emqx_resource:query(ResourceId, {sql, Query, Params, Timeout}) of
|
||||
{ok, _Columns, []} -> ignore;
|
||||
{ok, _Columns, []} ->
|
||||
ignore;
|
||||
{ok, Columns, [Row | _]} ->
|
||||
Selected = maps:from_list(lists:zip(Columns, Row)),
|
||||
case emqx_authn_utils:check_password_from_selected_map(
|
||||
Algorithm, Selected, Password) of
|
||||
case
|
||||
emqx_authn_utils:check_password_from_selected_map(
|
||||
Algorithm, Selected, Password
|
||||
)
|
||||
of
|
||||
ok ->
|
||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{msg => "mysql_query_failed",
|
||||
?SLOG(error, #{
|
||||
msg => "mysql_query_failed",
|
||||
resource => ResourceId,
|
||||
query => Query,
|
||||
params => Params,
|
||||
timeout => Timeout,
|
||||
reason => Reason}),
|
||||
reason => Reason
|
||||
}),
|
||||
ignore
|
||||
end.
|
||||
|
||||
|
|
|
|||
|
|
@ -24,17 +24,19 @@
|
|||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ refs/0
|
||||
, create/2
|
||||
, update/2
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
-export([
|
||||
refs/0,
|
||||
create/2,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
|
|
@ -50,10 +52,11 @@ namespace() -> "authn-postgresql".
|
|||
roots() -> [?CONF_NS].
|
||||
|
||||
fields(?CONF_NS) ->
|
||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
||||
, {backend, emqx_authn_schema:backend(postgresql)}
|
||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||
, {query, fun query/1}
|
||||
[
|
||||
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||
{backend, emqx_authn_schema:backend(postgresql)},
|
||||
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1},
|
||||
{query, fun query/1}
|
||||
] ++
|
||||
emqx_authn_schema:common_fields() ++
|
||||
proplists:delete(named_queries, emqx_connector_pgsql:fields(config)).
|
||||
|
|
@ -71,17 +74,29 @@ refs() ->
|
|||
create(_AuthenticatorID, Config) ->
|
||||
create(Config).
|
||||
|
||||
create(#{query := Query0,
|
||||
password_hash_algorithm := Algorithm} = Config) ->
|
||||
create(
|
||||
#{
|
||||
query := Query0,
|
||||
password_hash_algorithm := Algorithm
|
||||
} = Config
|
||||
) ->
|
||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||
{Query, PlaceHolders} = emqx_authn_utils:parse_sql(Query0, '$n'),
|
||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||
State = #{placeholders => PlaceHolders,
|
||||
State = #{
|
||||
placeholders => PlaceHolders,
|
||||
password_hash_algorithm => Algorithm,
|
||||
resource_id => ResourceId},
|
||||
case emqx_resource:create_local(ResourceId, ?RESOURCE_GROUP, emqx_connector_pgsql,
|
||||
resource_id => ResourceId
|
||||
},
|
||||
case
|
||||
emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_pgsql,
|
||||
Config#{named_queries => #{ResourceId => Query}},
|
||||
#{}) of
|
||||
#{}
|
||||
)
|
||||
of
|
||||
{ok, already_created} ->
|
||||
{ok, State};
|
||||
{ok, _} ->
|
||||
|
|
@ -101,28 +116,38 @@ update(Config, State) ->
|
|||
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(#{password := Password} = Credential,
|
||||
#{placeholders := PlaceHolders,
|
||||
authenticate(
|
||||
#{password := Password} = Credential,
|
||||
#{
|
||||
placeholders := PlaceHolders,
|
||||
resource_id := ResourceId,
|
||||
password_hash_algorithm := Algorithm}) ->
|
||||
password_hash_algorithm := Algorithm
|
||||
}
|
||||
) ->
|
||||
Params = emqx_authn_utils:render_sql_params(PlaceHolders, Credential),
|
||||
case emqx_resource:query(ResourceId, {prepared_query, ResourceId, Params}) of
|
||||
{ok, _Columns, []} -> ignore;
|
||||
{ok, _Columns, []} ->
|
||||
ignore;
|
||||
{ok, Columns, [Row | _]} ->
|
||||
NColumns = [Name || #column{name = Name} <- Columns],
|
||||
Selected = maps:from_list(lists:zip(NColumns, erlang:tuple_to_list(Row))),
|
||||
case emqx_authn_utils:check_password_from_selected_map(
|
||||
Algorithm, Selected, Password) of
|
||||
case
|
||||
emqx_authn_utils:check_password_from_selected_map(
|
||||
Algorithm, Selected, Password
|
||||
)
|
||||
of
|
||||
ok ->
|
||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{msg => "postgresql_query_failed",
|
||||
?SLOG(error, #{
|
||||
msg => "postgresql_query_failed",
|
||||
resource => ResourceId,
|
||||
params => Params,
|
||||
reason => Reason}),
|
||||
reason => Reason
|
||||
}),
|
||||
ignore
|
||||
end.
|
||||
|
||||
|
|
|
|||
|
|
@ -23,17 +23,19 @@
|
|||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ refs/0
|
||||
, create/2
|
||||
, update/2
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
-export([
|
||||
refs/0,
|
||||
create/2,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
|
|
@ -42,24 +44,27 @@
|
|||
namespace() -> "authn-redis".
|
||||
|
||||
roots() ->
|
||||
[ {?CONF_NS, hoconsc:mk(hoconsc:union(refs()),
|
||||
#{})}
|
||||
[
|
||||
{?CONF_NS,
|
||||
hoconsc:mk(
|
||||
hoconsc:union(refs()),
|
||||
#{}
|
||||
)}
|
||||
].
|
||||
|
||||
fields(standalone) ->
|
||||
common_fields() ++ emqx_connector_redis:fields(single);
|
||||
|
||||
fields(cluster) ->
|
||||
common_fields() ++ emqx_connector_redis:fields(cluster);
|
||||
|
||||
fields(sentinel) ->
|
||||
common_fields() ++ emqx_connector_redis:fields(sentinel).
|
||||
|
||||
common_fields() ->
|
||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
||||
, {backend, emqx_authn_schema:backend(redis)}
|
||||
, {cmd, fun cmd/1}
|
||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||
[
|
||||
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||
{backend, emqx_authn_schema:backend(redis)},
|
||||
{cmd, fun cmd/1},
|
||||
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||
] ++ emqx_authn_schema:common_fields().
|
||||
|
||||
cmd(type) -> string();
|
||||
|
|
@ -70,30 +75,43 @@ cmd(_) -> undefined.
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
refs() ->
|
||||
[ hoconsc:ref(?MODULE, standalone)
|
||||
, hoconsc:ref(?MODULE, cluster)
|
||||
, hoconsc:ref(?MODULE, sentinel)
|
||||
[
|
||||
hoconsc:ref(?MODULE, standalone),
|
||||
hoconsc:ref(?MODULE, cluster),
|
||||
hoconsc:ref(?MODULE, sentinel)
|
||||
].
|
||||
|
||||
create(_AuthenticatorID, Config) ->
|
||||
create(Config).
|
||||
|
||||
create(#{cmd := Cmd,
|
||||
password_hash_algorithm := Algorithm} = Config) ->
|
||||
create(
|
||||
#{
|
||||
cmd := Cmd,
|
||||
password_hash_algorithm := Algorithm
|
||||
} = Config
|
||||
) ->
|
||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||
try
|
||||
NCmd = parse_cmd(Cmd),
|
||||
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
|
||||
State = maps:with(
|
||||
[password_hash_algorithm, salt_position],
|
||||
Config),
|
||||
Config
|
||||
),
|
||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||
NState = State#{
|
||||
cmd => NCmd,
|
||||
resource_id => ResourceId},
|
||||
case emqx_resource:create_local(ResourceId, ?RESOURCE_GROUP,
|
||||
emqx_connector_redis, Config,
|
||||
#{}) of
|
||||
resource_id => ResourceId
|
||||
},
|
||||
case
|
||||
emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_redis,
|
||||
Config,
|
||||
#{}
|
||||
)
|
||||
of
|
||||
{ok, already_created} ->
|
||||
{ok, NState};
|
||||
{ok, _} ->
|
||||
|
|
@ -121,38 +139,50 @@ update(Config, State) ->
|
|||
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(#{password := Password} = Credential,
|
||||
#{cmd := {Command, KeyTemplate, Fields},
|
||||
authenticate(
|
||||
#{password := Password} = Credential,
|
||||
#{
|
||||
cmd := {Command, KeyTemplate, Fields},
|
||||
resource_id := ResourceId,
|
||||
password_hash_algorithm := Algorithm}) ->
|
||||
password_hash_algorithm := Algorithm
|
||||
}
|
||||
) ->
|
||||
NKey = emqx_authn_utils:render_str(KeyTemplate, Credential),
|
||||
case emqx_resource:query(ResourceId, {cmd, [Command, NKey | Fields]}) of
|
||||
{ok, []} -> ignore;
|
||||
{ok, []} ->
|
||||
ignore;
|
||||
{ok, Values} ->
|
||||
case merge(Fields, Values) of
|
||||
#{<<"password_hash">> := _} = Selected ->
|
||||
case emqx_authn_utils:check_password_from_selected_map(
|
||||
Algorithm, Selected, Password) of
|
||||
case
|
||||
emqx_authn_utils:check_password_from_selected_map(
|
||||
Algorithm, Selected, Password
|
||||
)
|
||||
of
|
||||
ok ->
|
||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
_ ->
|
||||
?SLOG(error, #{msg => "cannot_find_password_hash_field",
|
||||
?SLOG(error, #{
|
||||
msg => "cannot_find_password_hash_field",
|
||||
cmd => Command,
|
||||
keys => NKey,
|
||||
fields => Fields,
|
||||
resource => ResourceId}),
|
||||
resource => ResourceId
|
||||
}),
|
||||
ignore
|
||||
end;
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{msg => "redis_query_failed",
|
||||
?SLOG(error, #{
|
||||
msg => "redis_query_failed",
|
||||
resource => ResourceId,
|
||||
cmd => Command,
|
||||
keys => NKey,
|
||||
fields => Fields,
|
||||
reason => Reason}),
|
||||
reason => Reason
|
||||
}),
|
||||
ignore
|
||||
end.
|
||||
|
||||
|
|
@ -191,5 +221,8 @@ merge(Fields, Value) when not is_list(Value) ->
|
|||
merge(Fields, [Value]);
|
||||
merge(Fields, Values) ->
|
||||
maps:from_list(
|
||||
[{list_to_binary(K), V}
|
||||
|| {K, V} <- lists:zip(Fields, Values), V =/= undefined]).
|
||||
[
|
||||
{list_to_binary(K), V}
|
||||
|| {K, V} <- lists:zip(Fields, Values), V =/= undefined
|
||||
]
|
||||
).
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
-behaviour(emqx_bpapi).
|
||||
|
||||
-export([ introduced_in/0
|
||||
, lookup_from_all_nodes/2
|
||||
]).
|
||||
-export([
|
||||
introduced_in/0,
|
||||
lookup_from_all_nodes/2
|
||||
]).
|
||||
|
||||
-include_lib("emqx/include/bpapi.hrl").
|
||||
|
||||
|
|
|
|||
|
|
@ -21,19 +21,20 @@
|
|||
|
||||
%% @doc The Gateway definition
|
||||
-type gateway() ::
|
||||
#{ name := gateway_name()
|
||||
#{
|
||||
name := gateway_name(),
|
||||
%% Description
|
||||
, descr => binary() | undefined
|
||||
descr => binary() | undefined,
|
||||
%% Appears only in getting gateway info
|
||||
, status => stopped | running | unloaded
|
||||
status => stopped | running | unloaded,
|
||||
%% Timestamp in millisecond
|
||||
, created_at => integer()
|
||||
created_at => integer(),
|
||||
%% Timestamp in millisecond
|
||||
, started_at => integer()
|
||||
started_at => integer(),
|
||||
%% Timestamp in millisecond
|
||||
, stopped_at => integer()
|
||||
stopped_at => integer(),
|
||||
%% Appears only in getting gateway info
|
||||
, config => emqx_config:config()
|
||||
config => emqx_config:config()
|
||||
}.
|
||||
|
||||
-endif.
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@
|
|||
-define(RESOURCE_NOT_FOUND, 'RESOURCE_NOT_FOUND').
|
||||
-define(INTERNAL_ERROR, 'INTERNAL_SERVER_ERROR').
|
||||
|
||||
-define(STANDARD_RESP(R),
|
||||
R#{ 400 => emqx_dashboard_swagger:error_codes(
|
||||
[?BAD_REQUEST], <<"Bad request">>)
|
||||
, 404 => emqx_dashboard_swagger:error_codes(
|
||||
[?NOT_FOUND, ?RESOURCE_NOT_FOUND], <<"Not Found">>)
|
||||
}).
|
||||
-define(STANDARD_RESP(R), R#{
|
||||
400 => emqx_dashboard_swagger:error_codes(
|
||||
[?BAD_REQUEST], <<"Bad request">>
|
||||
),
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
[?NOT_FOUND, ?RESOURCE_NOT_FOUND], <<"Not Found">>
|
||||
)
|
||||
}).
|
||||
|
|
|
|||
|
|
@ -10,22 +10,30 @@
|
|||
{grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
|
||||
]}.
|
||||
|
||||
{grpc,
|
||||
[{protos, ["src/exproto/protos"]},
|
||||
{grpc, [
|
||||
{protos, ["src/exproto/protos"]},
|
||||
{out_dir, "src/exproto/"},
|
||||
{gpb_opts, [{module_name_prefix, "emqx_"},
|
||||
{module_name_suffix, "_pb"}]}
|
||||
{gpb_opts, [
|
||||
{module_name_prefix, "emqx_"},
|
||||
{module_name_suffix, "_pb"}
|
||||
]}
|
||||
]}.
|
||||
|
||||
{provider_hooks,
|
||||
[{pre, [{compile, {grpc, gen}},
|
||||
{clean, {grpc, clean}}]}
|
||||
{provider_hooks, [
|
||||
{pre, [
|
||||
{compile, {grpc, gen}},
|
||||
{clean, {grpc, clean}}
|
||||
]}
|
||||
]}.
|
||||
|
||||
{xref_ignores, [emqx_exproto_pb]}.
|
||||
|
||||
{cover_excl_mods, [emqx_exproto_pb,
|
||||
{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]}.
|
||||
emqx_exproto_v_1_connection_handler_bhvr
|
||||
]}.
|
||||
|
||||
{project_plugins, [erlfmt]}.
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@
|
|||
|
||||
-type gen_server_from() :: {pid(), Tag :: term()}.
|
||||
|
||||
-type reply() :: {outgoing, emqx_gateway_frame:packet()}
|
||||
-type reply() ::
|
||||
{outgoing, emqx_gateway_frame:packet()}
|
||||
| {outgoing, [emqx_gateway_frame:packet()]}
|
||||
| {event, conn_state() | updated}
|
||||
| {close, Reason :: atom()}.
|
||||
|
|
@ -54,46 +55,46 @@
|
|||
-type replies() :: reply() | [reply()].
|
||||
|
||||
%% @doc Handle the incoming frame
|
||||
-callback handle_in(emqx_gateway_frame:frame() | {frame_error, any()},
|
||||
channel())
|
||||
-> {ok, channel()}
|
||||
-callback handle_in(
|
||||
emqx_gateway_frame:frame() | {frame_error, any()},
|
||||
channel()
|
||||
) ->
|
||||
{ok, channel()}
|
||||
| {ok, replies(), channel()}
|
||||
| {shutdown, Reason :: any(), channel()}
|
||||
| {shutdown, Reason :: any(), replies(), channel()}.
|
||||
|
||||
%% @doc Handle the outgoing messages dispatched from PUB/SUB system
|
||||
-callback handle_deliver(list(emqx_types:deliver()), channel())
|
||||
-> {ok, channel()}
|
||||
-callback handle_deliver(list(emqx_types:deliver()), channel()) ->
|
||||
{ok, channel()}
|
||||
| {ok, replies(), channel()}.
|
||||
|
||||
%% @doc Handle the timeout event
|
||||
-callback handle_timeout(reference(), Msg :: any(), channel())
|
||||
-> {ok, channel()}
|
||||
-callback handle_timeout(reference(), Msg :: any(), channel()) ->
|
||||
{ok, channel()}
|
||||
| {ok, replies(), channel()}
|
||||
| {shutdown, Reason :: any(), channel()}.
|
||||
|
||||
%% @doc Handle the custom gen_server:call/2 for its connection process
|
||||
-callback handle_call(Req :: any(), From :: gen_server_from(), channel())
|
||||
-> {reply, Reply :: any(), channel()}
|
||||
-callback handle_call(Req :: any(), From :: gen_server_from(), channel()) ->
|
||||
{reply, Reply :: any(), channel()}
|
||||
%% Reply to caller and trigger an event(s)
|
||||
| {reply, Reply :: any(),
|
||||
EventOrEvents :: tuple() | list(tuple()), channel()}
|
||||
| {reply, Reply :: any(), EventOrEvents :: tuple() | list(tuple()), channel()}
|
||||
| {noreply, channel()}
|
||||
| {noreply, EventOrEvents :: tuple() | list(tuple()), channel()}
|
||||
| {shutdown, Reason :: any(), Reply :: any(), channel()}
|
||||
%% Shutdown the process, reply to caller and write a packet to client
|
||||
| {shutdown, Reason :: any(), Reply :: any(),
|
||||
emqx_gateway_frame:frame(), channel()}.
|
||||
| {shutdown, Reason :: any(), Reply :: any(), emqx_gateway_frame:frame(), channel()}.
|
||||
|
||||
%% @doc Handle the custom gen_server:cast/2 for its connection process
|
||||
-callback handle_cast(Req :: any(), channel())
|
||||
-> ok
|
||||
-callback handle_cast(Req :: any(), channel()) ->
|
||||
ok
|
||||
| {ok, channel()}
|
||||
| {shutdown, Reason :: any(), channel()}.
|
||||
|
||||
%% @doc Handle the custom process messages for its connection process
|
||||
-callback handle_info(Info :: any(), channel())
|
||||
-> ok
|
||||
-callback handle_info(Info :: any(), channel()) ->
|
||||
ok
|
||||
| {ok, channel()}
|
||||
| {shutdown, Reason :: any(), channel()}.
|
||||
|
||||
|
|
|
|||
|
|
@ -21,28 +21,32 @@
|
|||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% API
|
||||
-export([ start_link/3
|
||||
, stop/1
|
||||
]).
|
||||
-export([
|
||||
start_link/3,
|
||||
stop/1
|
||||
]).
|
||||
|
||||
-export([ info/1
|
||||
, stats/1
|
||||
]).
|
||||
-export([
|
||||
info/1,
|
||||
stats/1
|
||||
]).
|
||||
|
||||
-export([ call/2
|
||||
, call/3
|
||||
, cast/2
|
||||
]).
|
||||
-export([
|
||||
call/2,
|
||||
call/3,
|
||||
cast/2
|
||||
]).
|
||||
|
||||
%% Callback
|
||||
-export([init/6]).
|
||||
|
||||
%% Sys callbacks
|
||||
-export([ system_continue/3
|
||||
, system_terminate/4
|
||||
, system_code_change/4
|
||||
, system_get_state/1
|
||||
]).
|
||||
-export([
|
||||
system_continue/3,
|
||||
system_terminate/4,
|
||||
system_code_change/4,
|
||||
system_get_state/1
|
||||
]).
|
||||
|
||||
%% Internal callback
|
||||
-export([wakeup_from_hib/2, recvloop/2]).
|
||||
|
|
@ -84,7 +88,7 @@
|
|||
chann_mod :: atom(),
|
||||
%% Listener Tag
|
||||
listener :: listener() | undefined
|
||||
}).
|
||||
}).
|
||||
|
||||
-type listener() :: {GwName :: atom(), LisType :: atom(), LisName :: atom()}.
|
||||
-type state() :: #state{}.
|
||||
|
|
@ -95,20 +99,21 @@
|
|||
|
||||
-define(ENABLED(X), (X =/= undefined)).
|
||||
|
||||
-dialyzer({nowarn_function,
|
||||
[ system_terminate/4
|
||||
, handle_call/3
|
||||
, handle_msg/2
|
||||
, shutdown/3
|
||||
, stop/3
|
||||
, parse_incoming/3
|
||||
]}).
|
||||
-dialyzer(
|
||||
{nowarn_function, [
|
||||
system_terminate/4,
|
||||
handle_call/3,
|
||||
handle_msg/2,
|
||||
shutdown/3,
|
||||
stop/3,
|
||||
parse_incoming/3
|
||||
]}
|
||||
).
|
||||
|
||||
%% udp
|
||||
start_link(Socket = {udp, _SockPid, _Sock}, Peername, Options) ->
|
||||
Args = [self(), Socket, Peername, Options] ++ callback_modules(Options),
|
||||
{ok, proc_lib:spawn_link(?MODULE, init, Args)};
|
||||
|
||||
%% tcp/ssl/dtls
|
||||
start_link(esockd_transport, Sock, Options) ->
|
||||
Socket = {esockd_transport, Sock},
|
||||
|
|
@ -116,21 +121,24 @@ start_link(esockd_transport, Sock, Options) ->
|
|||
{ok, proc_lib:spawn_link(?MODULE, init, Args)}.
|
||||
|
||||
callback_modules(Options) ->
|
||||
[maps:get(frame_mod, Options),
|
||||
maps:get(chann_mod, Options)].
|
||||
[
|
||||
maps:get(frame_mod, Options),
|
||||
maps:get(chann_mod, Options)
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc Get infos of the connection/channel.
|
||||
-spec(info(pid()|state()) -> emqx_types:infos()).
|
||||
-spec info(pid() | state()) -> emqx_types:infos().
|
||||
info(CPid) when is_pid(CPid) ->
|
||||
call(CPid, info);
|
||||
info(State = #state{chann_mod = ChannMod, channel = Channel}) ->
|
||||
ChanInfo = ChannMod:info(Channel),
|
||||
SockInfo = maps:from_list(
|
||||
info(?INFO_KEYS, State)),
|
||||
info(?INFO_KEYS, State)
|
||||
),
|
||||
ChanInfo#{sockinfo => SockInfo}.
|
||||
|
||||
info(Keys, State) when is_list(Keys) ->
|
||||
|
|
@ -146,13 +154,16 @@ info(sockstate, #state{sockstate = SockSt}) ->
|
|||
info(active_n, #state{active_n = ActiveN}) ->
|
||||
ActiveN.
|
||||
|
||||
-spec(stats(pid()|state()) -> emqx_types:stats()).
|
||||
-spec stats(pid() | state()) -> emqx_types:stats().
|
||||
stats(CPid) when is_pid(CPid) ->
|
||||
call(CPid, stats);
|
||||
stats(#state{socket = Socket,
|
||||
stats(#state{
|
||||
socket = Socket,
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
SockStats = case esockd_getstat(Socket, ?SOCK_STATS) of
|
||||
channel = Channel
|
||||
}) ->
|
||||
SockStats =
|
||||
case esockd_getstat(Socket, ?SOCK_STATS) of
|
||||
{ok, Ss} -> Ss;
|
||||
{error, _} -> []
|
||||
end,
|
||||
|
|
@ -221,8 +232,10 @@ esockd_getstat({udp, _SockPid, Sock}, Stats) ->
|
|||
esockd_getstat({esockd_transport, Sock}, Stats) ->
|
||||
esockd_transport:getstat(Sock, Stats).
|
||||
|
||||
esockd_send(Data, #state{socket = {udp, _SockPid, Sock},
|
||||
peername = {Ip, Port}}) ->
|
||||
esockd_send(Data, #state{
|
||||
socket = {udp, _SockPid, Sock},
|
||||
peername = {Ip, Port}
|
||||
}) ->
|
||||
gen_udp:send(Sock, Ip, Port, Data);
|
||||
esockd_send(Data, #state{socket = {esockd_transport, Sock}}) ->
|
||||
esockd_transport:async_send(Sock, Data).
|
||||
|
|
@ -238,8 +251,16 @@ init(Parent, WrappedSock, Peername0, Options, FrameMod, ChannMod) ->
|
|||
case esockd_wait(WrappedSock) of
|
||||
{ok, NWrappedSock} ->
|
||||
Peername = esockd_peername(NWrappedSock, Peername0),
|
||||
run_loop(Parent, init_state(NWrappedSock, Peername,
|
||||
Options, FrameMod, ChannMod));
|
||||
run_loop(
|
||||
Parent,
|
||||
init_state(
|
||||
NWrappedSock,
|
||||
Peername,
|
||||
Options,
|
||||
FrameMod,
|
||||
ChannMod
|
||||
)
|
||||
);
|
||||
{error, Reason} ->
|
||||
ok = esockd_close(WrappedSock),
|
||||
exit_on_sock_error(Reason)
|
||||
|
|
@ -248,7 +269,8 @@ init(Parent, WrappedSock, Peername0, Options, FrameMod, ChannMod) ->
|
|||
init_state(WrappedSock, Peername, Options, FrameMod, ChannMod) ->
|
||||
{ok, Sockname} = esockd_ensure_ok_or_exit(sockname, WrappedSock),
|
||||
Peercert = esockd_ensure_ok_or_exit(peercert, WrappedSock),
|
||||
ConnInfo = #{socktype => esockd_type(WrappedSock),
|
||||
ConnInfo = #{
|
||||
socktype => esockd_type(WrappedSock),
|
||||
peername => Peername,
|
||||
sockname => Sockname,
|
||||
peercert => Peercert,
|
||||
|
|
@ -267,7 +289,8 @@ init_state(WrappedSock, Peername, Options, FrameMod, ChannMod) ->
|
|||
IdleTimeout = emqx_gateway_utils:idle_timeout(Options),
|
||||
OomPolicy = emqx_gateway_utils:oom_policy(Options),
|
||||
IdleTimer = emqx_misc:start_timer(IdleTimeout, idle_timeout),
|
||||
#state{socket = WrappedSock,
|
||||
#state{
|
||||
socket = WrappedSock,
|
||||
peername = Peername,
|
||||
sockname = Sockname,
|
||||
sockstate = idle,
|
||||
|
|
@ -286,10 +309,14 @@ init_state(WrappedSock, Peername, Options, FrameMod, ChannMod) ->
|
|||
listener = maps:get(listener, Options, undefined)
|
||||
}.
|
||||
|
||||
run_loop(Parent, State = #state{socket = Socket,
|
||||
run_loop(
|
||||
Parent,
|
||||
State = #state{
|
||||
socket = Socket,
|
||||
peername = Peername,
|
||||
oom_policy = OomPolicy
|
||||
}) ->
|
||||
}
|
||||
) ->
|
||||
emqx_logger:set_metadata_peername(esockd:format(Peername)),
|
||||
_ = emqx_misc:tune_heap_size(OomPolicy),
|
||||
case activate_socket(State) of
|
||||
|
|
@ -301,9 +328,11 @@ run_loop(Parent, State = #state{socket = Socket,
|
|||
end.
|
||||
|
||||
-spec exit_on_sock_error(atom()) -> no_return().
|
||||
exit_on_sock_error(Reason) when Reason =:= einval;
|
||||
exit_on_sock_error(Reason) when
|
||||
Reason =:= einval;
|
||||
Reason =:= enotconn;
|
||||
Reason =:= closed ->
|
||||
Reason =:= closed
|
||||
->
|
||||
erlang:exit(normal);
|
||||
exit_on_sock_error(timeout) ->
|
||||
erlang:exit({shutdown, ssl_upgrade_timeout});
|
||||
|
|
@ -317,8 +346,7 @@ recvloop(Parent, State = #state{idle_timeout = IdleTimeout}) ->
|
|||
receive
|
||||
Msg ->
|
||||
handle_recv(Msg, Parent, State)
|
||||
after
|
||||
IdleTimeout + 100 ->
|
||||
after IdleTimeout + 100 ->
|
||||
hibernate(Parent, cancel_stats_timer(State))
|
||||
end.
|
||||
|
||||
|
|
@ -347,20 +375,23 @@ wakeup_from_hib(Parent, State) ->
|
|||
|
||||
ensure_stats_timer(Timeout, State = #state{stats_timer = undefined}) ->
|
||||
State#state{stats_timer = emqx_misc:start_timer(Timeout, emit_stats)};
|
||||
ensure_stats_timer(_Timeout, State) -> State.
|
||||
ensure_stats_timer(_Timeout, State) ->
|
||||
State.
|
||||
|
||||
cancel_stats_timer(State = #state{stats_timer = TRef})
|
||||
when is_reference(TRef) ->
|
||||
cancel_stats_timer(State = #state{stats_timer = TRef}) when
|
||||
is_reference(TRef)
|
||||
->
|
||||
ok = emqx_misc:cancel_timer(TRef),
|
||||
State#state{stats_timer = undefined};
|
||||
cancel_stats_timer(State) -> State.
|
||||
cancel_stats_timer(State) ->
|
||||
State.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Process next Msg
|
||||
|
||||
process_msg([], State) ->
|
||||
{ok, State};
|
||||
process_msg([Msg|More], State) ->
|
||||
process_msg([Msg | More], State) ->
|
||||
try
|
||||
case handle_msg(Msg, State) of
|
||||
ok ->
|
||||
|
|
@ -373,21 +404,26 @@ process_msg([Msg|More], State) ->
|
|||
{stop, Reason, NState}
|
||||
end
|
||||
catch
|
||||
exit : normal ->
|
||||
exit:normal ->
|
||||
{stop, normal, State};
|
||||
exit : shutdown ->
|
||||
exit:shutdown ->
|
||||
{stop, shutdown, State};
|
||||
exit : {shutdown, _} = Shutdown ->
|
||||
exit:{shutdown, _} = Shutdown ->
|
||||
{stop, Shutdown, State};
|
||||
Exception : Context : Stack ->
|
||||
{stop, #{exception => Exception,
|
||||
Exception:Context:Stack ->
|
||||
{stop,
|
||||
#{
|
||||
exception => Exception,
|
||||
context => Context,
|
||||
stacktrace => Stack}, State}
|
||||
stacktrace => Stack
|
||||
},
|
||||
State}
|
||||
end.
|
||||
|
||||
append_msg([], Msgs) when is_list(Msgs) ->
|
||||
Msgs;
|
||||
append_msg([], Msg) -> [Msg];
|
||||
append_msg([], Msg) ->
|
||||
[Msg];
|
||||
append_msg(Q, Msgs) when is_list(Msgs) ->
|
||||
lists:append(Q, Msgs);
|
||||
append_msg(Q, Msg) ->
|
||||
|
|
@ -412,39 +448,37 @@ handle_msg({'$gen_call', From, Req}, State) ->
|
|||
gen_server:reply(From, Reply),
|
||||
stop(Reason, NState)
|
||||
end;
|
||||
|
||||
handle_msg({'$gen_cast', Req}, State) ->
|
||||
with_channel(handle_cast, [Req], State);
|
||||
|
||||
handle_msg({datagram, _SockPid, Data}, State) ->
|
||||
parse_incoming(Data, State);
|
||||
|
||||
handle_msg({Inet, _Sock, Data}, State)
|
||||
when Inet == tcp;
|
||||
Inet == ssl ->
|
||||
handle_msg({Inet, _Sock, Data}, State) when
|
||||
Inet == tcp;
|
||||
Inet == ssl
|
||||
->
|
||||
parse_incoming(Data, State);
|
||||
|
||||
handle_msg({incoming, Packet},
|
||||
State = #state{idle_timer = IdleTimer}) ->
|
||||
handle_msg(
|
||||
{incoming, Packet},
|
||||
State = #state{idle_timer = IdleTimer}
|
||||
) ->
|
||||
IdleTimer /= undefined andalso
|
||||
emqx_misc:cancel_timer(IdleTimer),
|
||||
NState = State#state{idle_timer = undefined},
|
||||
handle_incoming(Packet, NState);
|
||||
|
||||
handle_msg({outgoing, Data}, State) ->
|
||||
handle_outgoing(Data, State);
|
||||
|
||||
handle_msg({Error, _Sock, Reason}, State)
|
||||
when Error == tcp_error; Error == ssl_error ->
|
||||
handle_msg({Error, _Sock, Reason}, State) when
|
||||
Error == tcp_error; Error == ssl_error
|
||||
->
|
||||
handle_info({sock_error, Reason}, State);
|
||||
|
||||
handle_msg({Closed, _Sock}, State)
|
||||
when Closed == tcp_closed; Closed == ssl_closed ->
|
||||
handle_msg({Closed, _Sock}, State) when
|
||||
Closed == tcp_closed; Closed == ssl_closed
|
||||
->
|
||||
handle_info({sock_closed, Closed}, close_socket(State));
|
||||
|
||||
%% TODO: udp_passive???
|
||||
handle_msg({Passive, _Sock}, State)
|
||||
when Passive == tcp_passive; Passive == ssl_passive ->
|
||||
handle_msg({Passive, _Sock}, State) when
|
||||
Passive == tcp_passive; Passive == ssl_passive
|
||||
->
|
||||
%% In Stats
|
||||
Bytes = emqx_pd:reset_counter(incoming_bytes),
|
||||
Pubs = emqx_pd:reset_counter(incoming_pkt),
|
||||
|
|
@ -454,12 +488,12 @@ handle_msg({Passive, _Sock}, State)
|
|||
%% Run GC and Check OOM
|
||||
NState1 = check_oom(run_gc(InStats, NState)),
|
||||
handle_info(activate_socket, NState1);
|
||||
|
||||
handle_msg(Deliver = {deliver, _Topic, _Msg},
|
||||
State = #state{active_n = ActiveN}) ->
|
||||
Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)],
|
||||
handle_msg(
|
||||
Deliver = {deliver, _Topic, _Msg},
|
||||
State = #state{active_n = ActiveN}
|
||||
) ->
|
||||
Delivers = [Deliver | emqx_misc:drain_deliver(ActiveN)],
|
||||
with_channel(handle_deliver, [Delivers], State);
|
||||
|
||||
%% Something sent
|
||||
%% TODO: Who will deliver this message?
|
||||
handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) ->
|
||||
|
|
@ -469,19 +503,21 @@ handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) ->
|
|||
Bytes = emqx_pd:reset_counter(outgoing_bytes),
|
||||
OutStats = #{cnt => Pubs, oct => Bytes},
|
||||
{ok, check_oom(run_gc(OutStats, State))};
|
||||
false -> ok
|
||||
false ->
|
||||
ok
|
||||
end;
|
||||
|
||||
handle_msg({inet_reply, _Sock, {error, Reason}}, State) ->
|
||||
handle_info({sock_error, Reason}, State);
|
||||
|
||||
handle_msg({close, Reason}, State) ->
|
||||
?SLOG(debug, #{msg => "force_socket_close", reason => Reason}),
|
||||
handle_info({sock_closed, Reason}, close_socket(State));
|
||||
|
||||
handle_msg({event, connected}, State = #state{
|
||||
handle_msg(
|
||||
{event, connected},
|
||||
State = #state{
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}
|
||||
) ->
|
||||
Ctx = ChannMod:info(ctx, Channel),
|
||||
ClientId = ChannMod:info(clientid, Channel),
|
||||
emqx_gateway_ctx:insert_channel_info(
|
||||
|
|
@ -490,31 +526,34 @@ handle_msg({event, connected}, State = #state{
|
|||
info(State),
|
||||
stats(State)
|
||||
);
|
||||
|
||||
handle_msg({event, disconnected}, State = #state{
|
||||
handle_msg(
|
||||
{event, disconnected},
|
||||
State = #state{
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}
|
||||
) ->
|
||||
Ctx = ChannMod:info(ctx, Channel),
|
||||
ClientId = ChannMod:info(clientid, Channel),
|
||||
emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)),
|
||||
emqx_gateway_ctx:connection_closed(Ctx, ClientId),
|
||||
{ok, State};
|
||||
|
||||
handle_msg({event, _Other}, State = #state{
|
||||
handle_msg(
|
||||
{event, _Other},
|
||||
State = #state{
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}
|
||||
) ->
|
||||
Ctx = ChannMod:info(ctx, Channel),
|
||||
ClientId = ChannMod:info(clientid, Channel),
|
||||
emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)),
|
||||
emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)),
|
||||
{ok, State};
|
||||
|
||||
handle_msg({timeout, TRef, TMsg}, State) ->
|
||||
handle_timeout(TRef, TMsg, State);
|
||||
|
||||
handle_msg(Shutdown = {shutdown, _Reason}, State) ->
|
||||
stop(Shutdown, State);
|
||||
|
||||
handle_msg(Msg, State) ->
|
||||
handle_info(Msg, State).
|
||||
|
||||
|
|
@ -522,9 +561,13 @@ handle_msg(Msg, State) ->
|
|||
%% Terminate
|
||||
|
||||
-spec terminate(atom(), state()) -> no_return().
|
||||
terminate(Reason, State = #state{
|
||||
terminate(
|
||||
Reason,
|
||||
State = #state{
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}
|
||||
) ->
|
||||
?SLOG(debug, #{msg => "conn_process_terminated", reason => Reason}),
|
||||
_ = ChannMod:terminate(Reason, Channel),
|
||||
_ = close_socket(State),
|
||||
|
|
@ -549,13 +592,16 @@ system_get_state(State) -> {ok, State}.
|
|||
|
||||
handle_call(_From, info, State) ->
|
||||
{reply, info(State), State};
|
||||
|
||||
handle_call(_From, stats, State) ->
|
||||
{reply, stats(State), State};
|
||||
|
||||
handle_call(From, Req, State = #state{
|
||||
handle_call(
|
||||
From,
|
||||
Req,
|
||||
State = #state{
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}
|
||||
) ->
|
||||
case ChannMod:handle_call(Req, From, Channel) of
|
||||
{noreply, NChannel} ->
|
||||
{noreply, State#state{channel = NChannel}};
|
||||
|
|
@ -578,24 +624,32 @@ handle_call(From, Req, State = #state{
|
|||
|
||||
handle_timeout(_TRef, idle_timeout, State) ->
|
||||
shutdown(idle_timeout, State);
|
||||
|
||||
handle_timeout(_TRef, limit_timeout, State) ->
|
||||
NState = State#state{sockstate = idle,
|
||||
NState = State#state{
|
||||
sockstate = idle,
|
||||
limit_timer = undefined
|
||||
},
|
||||
handle_info(activate_socket, NState);
|
||||
handle_timeout(TRef, Keepalive, State = #state{
|
||||
handle_timeout(
|
||||
TRef,
|
||||
Keepalive,
|
||||
State = #state{
|
||||
chann_mod = ChannMod,
|
||||
socket = Socket,
|
||||
channel = Channel})
|
||||
when Keepalive == keepalive;
|
||||
Keepalive == keepalive_send ->
|
||||
Stat = case Keepalive of
|
||||
channel = Channel
|
||||
}
|
||||
) when
|
||||
Keepalive == keepalive;
|
||||
Keepalive == keepalive_send
|
||||
->
|
||||
Stat =
|
||||
case Keepalive of
|
||||
keepalive -> recv_oct;
|
||||
keepalive_send -> send_oct
|
||||
end,
|
||||
case ChannMod:info(conn_state, Channel) of
|
||||
disconnected -> {ok, State};
|
||||
disconnected ->
|
||||
{ok, State};
|
||||
_ ->
|
||||
case esockd_getstat(Socket, [Stat]) of
|
||||
{ok, [{Stat, RecvOct}]} ->
|
||||
|
|
@ -604,22 +658,29 @@ handle_timeout(TRef, Keepalive, State = #state{
|
|||
handle_info({sock_error, Reason}, State)
|
||||
end
|
||||
end;
|
||||
handle_timeout(_TRef, emit_stats, State =
|
||||
#state{chann_mod = ChannMod, channel = Channel}) ->
|
||||
handle_timeout(
|
||||
_TRef,
|
||||
emit_stats,
|
||||
State =
|
||||
#state{chann_mod = ChannMod, channel = Channel}
|
||||
) ->
|
||||
Ctx = ChannMod:info(ctx, Channel),
|
||||
ClientId = ChannMod:info(clientid, Channel),
|
||||
emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)),
|
||||
{ok, State#state{stats_timer = undefined}};
|
||||
|
||||
handle_timeout(TRef, Msg, State) ->
|
||||
with_channel(handle_timeout, [TRef, Msg], State).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Parse incoming data
|
||||
|
||||
parse_incoming(Data, State = #state{
|
||||
parse_incoming(
|
||||
Data,
|
||||
State = #state{
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}
|
||||
) ->
|
||||
?SLOG(debug, #{msg => "RECV_data", data => Data}),
|
||||
Oct = iolist_size(Data),
|
||||
inc_counter(incoming_bytes, Oct),
|
||||
|
|
@ -630,24 +691,28 @@ parse_incoming(Data, State = #state{
|
|||
|
||||
parse_incoming(<<>>, Packets, State) ->
|
||||
{Packets, State};
|
||||
|
||||
parse_incoming(Data, Packets,
|
||||
parse_incoming(
|
||||
Data,
|
||||
Packets,
|
||||
State = #state{
|
||||
frame_mod = FrameMod,
|
||||
parse_state = ParseState}) ->
|
||||
parse_state = ParseState
|
||||
}
|
||||
) ->
|
||||
try FrameMod:parse(Data, ParseState) of
|
||||
{more, NParseState} ->
|
||||
{Packets, State#state{parse_state = NParseState}};
|
||||
{ok, Packet, Rest, NParseState} ->
|
||||
NState = State#state{parse_state = NParseState},
|
||||
parse_incoming(Rest, [Packet|Packets], NState)
|
||||
parse_incoming(Rest, [Packet | Packets], NState)
|
||||
catch
|
||||
error:Reason:Stk ->
|
||||
?SLOG(error, #{ msg => "parse_frame_failed"
|
||||
, at_state => ParseState
|
||||
, input_bytes => Data
|
||||
, reason => Reason
|
||||
, stacktrace => Stk
|
||||
?SLOG(error, #{
|
||||
msg => "parse_frame_failed",
|
||||
at_state => ParseState,
|
||||
input_bytes => Data,
|
||||
reason => Reason,
|
||||
stacktrace => Stk
|
||||
}),
|
||||
{[{frame_error, Reason} | Packets], State}
|
||||
end.
|
||||
|
|
@ -660,26 +725,36 @@ next_incoming_msgs(Packets) ->
|
|||
%%--------------------------------------------------------------------
|
||||
%% Handle incoming packet
|
||||
|
||||
handle_incoming(Packet, State = #state{
|
||||
handle_incoming(
|
||||
Packet,
|
||||
State = #state{
|
||||
channel = Channel,
|
||||
frame_mod = FrameMod,
|
||||
chann_mod = ChannMod
|
||||
}) ->
|
||||
}
|
||||
) ->
|
||||
Ctx = ChannMod:info(ctx, Channel),
|
||||
ok = inc_incoming_stats(Ctx, FrameMod, Packet),
|
||||
?SLOG(debug, #{ msg => "RECV_packet"
|
||||
, packet => FrameMod:format(Packet)
|
||||
?SLOG(debug, #{
|
||||
msg => "RECV_packet",
|
||||
packet => FrameMod:format(Packet)
|
||||
}),
|
||||
with_channel(handle_in, [Packet], State).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% With Channel
|
||||
|
||||
with_channel(Fun, Args, State = #state{
|
||||
with_channel(
|
||||
Fun,
|
||||
Args,
|
||||
State = #state{
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}
|
||||
) ->
|
||||
case erlang:apply(ChannMod, Fun, Args ++ [Channel]) of
|
||||
ok -> {ok, State};
|
||||
ok ->
|
||||
{ok, State};
|
||||
{ok, NChannel} ->
|
||||
{ok, State#state{channel = NChannel}};
|
||||
{ok, Replies, NChannel} ->
|
||||
|
|
@ -697,19 +772,24 @@ with_channel(Fun, Args, State = #state{
|
|||
|
||||
handle_outgoing(_Packets = [], _State) ->
|
||||
ok;
|
||||
handle_outgoing(Packets,
|
||||
State = #state{socket = Socket}) when is_list(Packets) ->
|
||||
handle_outgoing(
|
||||
Packets,
|
||||
State = #state{socket = Socket}
|
||||
) when is_list(Packets) ->
|
||||
case is_datadram_socket(Socket) of
|
||||
false ->
|
||||
send(
|
||||
lists:map(serialize_and_inc_stats_fun(State), Packets),
|
||||
State);
|
||||
State
|
||||
);
|
||||
_ ->
|
||||
lists:foreach(fun(Packet) ->
|
||||
lists:foreach(
|
||||
fun(Packet) ->
|
||||
handle_outgoing(Packet, State)
|
||||
end, Packets)
|
||||
end,
|
||||
Packets
|
||||
)
|
||||
end;
|
||||
|
||||
handle_outgoing(Packet, State) ->
|
||||
send((serialize_and_inc_stats_fun(State))(Packet), State).
|
||||
|
||||
|
|
@ -717,29 +797,33 @@ serialize_and_inc_stats_fun(#state{
|
|||
frame_mod = FrameMod,
|
||||
chann_mod = ChannMod,
|
||||
serialize = Serialize,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}) ->
|
||||
Ctx = ChannMod:info(ctx, Channel),
|
||||
fun(Packet) ->
|
||||
try
|
||||
Data = FrameMod:serialize_pkt(Packet, Serialize),
|
||||
?SLOG(debug, #{ msg => "SEND_packet"
|
||||
?SLOG(debug, #{
|
||||
msg => "SEND_packet",
|
||||
%% XXX: optimize it, less cpu comsuption?
|
||||
, packet => FrameMod:format(Packet)
|
||||
packet => FrameMod:format(Packet)
|
||||
}),
|
||||
ok = inc_outgoing_stats(Ctx, FrameMod, Packet),
|
||||
Data
|
||||
catch
|
||||
_ : too_large ->
|
||||
?SLOG(warning, #{ msg => "packet_too_large_discarded"
|
||||
, packet => FrameMod:format(Packet)
|
||||
_:too_large ->
|
||||
?SLOG(warning, #{
|
||||
msg => "packet_too_large_discarded",
|
||||
packet => FrameMod:format(Packet)
|
||||
}),
|
||||
ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped.too_large'),
|
||||
ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped'),
|
||||
<<>>;
|
||||
_ : Reason ->
|
||||
?SLOG(warning, #{ msg => "packet_serialize_failure"
|
||||
, reason => Reason
|
||||
, packet => FrameMod:format(Packet)
|
||||
_:Reason ->
|
||||
?SLOG(warning, #{
|
||||
msg => "packet_serialize_failure",
|
||||
reason => Reason,
|
||||
packet => FrameMod:format(Packet)
|
||||
}),
|
||||
ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped'),
|
||||
<<>>
|
||||
|
|
@ -749,17 +833,23 @@ serialize_and_inc_stats_fun(#state{
|
|||
%%--------------------------------------------------------------------
|
||||
%% Send data
|
||||
|
||||
-spec(send(iodata(), state()) -> ok).
|
||||
send(IoData, State = #state{socket = Socket,
|
||||
-spec send(iodata(), state()) -> ok.
|
||||
send(
|
||||
IoData,
|
||||
State = #state{
|
||||
socket = Socket,
|
||||
chann_mod = ChannMod,
|
||||
channel = Channel}) ->
|
||||
channel = Channel
|
||||
}
|
||||
) ->
|
||||
?SLOG(debug, #{msg => "SEND_data", data => IoData}),
|
||||
Ctx = ChannMod:info(ctx, Channel),
|
||||
Oct = iolist_size(IoData),
|
||||
ok = emqx_gateway_ctx:metrics_inc(Ctx, 'bytes.sent', Oct),
|
||||
inc_counter(outgoing_bytes, Oct),
|
||||
case esockd_send(IoData, State) of
|
||||
ok -> ok;
|
||||
ok ->
|
||||
ok;
|
||||
Error = {error, _Reason} ->
|
||||
%% Send an inet_reply to postpone handling the error
|
||||
self() ! {inet_reply, Socket, Error},
|
||||
|
|
@ -772,20 +862,21 @@ send(IoData, State = #state{socket = Socket,
|
|||
handle_info(activate_socket, State = #state{sockstate = OldSst}) ->
|
||||
case activate_socket(State) of
|
||||
{ok, NState = #state{sockstate = NewSst}} ->
|
||||
if OldSst =/= NewSst ->
|
||||
if
|
||||
OldSst =/= NewSst ->
|
||||
{ok, {event, NewSst}, NState};
|
||||
true -> {ok, NState}
|
||||
true ->
|
||||
{ok, NState}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
handle_info({sock_error, Reason}, State)
|
||||
end;
|
||||
|
||||
handle_info({sock_error, Reason}, State) ->
|
||||
?SLOG(debug, #{ msg => "sock_error"
|
||||
, reason => Reason
|
||||
?SLOG(debug, #{
|
||||
msg => "sock_error",
|
||||
reason => Reason
|
||||
}),
|
||||
handle_info({sock_closed, Reason}, close_socket(State));
|
||||
|
||||
handle_info(Info, State) ->
|
||||
with_channel(handle_info, [Info], State).
|
||||
|
||||
|
|
@ -794,16 +885,19 @@ handle_info(Info, State) ->
|
|||
|
||||
ensure_rate_limit(Stats, State = #state{limiter = Limiter}) ->
|
||||
case ?ENABLED(Limiter) andalso emqx_limiter:check(Stats, Limiter) of
|
||||
false -> State;
|
||||
false ->
|
||||
State;
|
||||
{ok, Limiter1} ->
|
||||
State#state{limiter = Limiter1};
|
||||
{pause, Time, Limiter1} ->
|
||||
%% XXX: which limiter reached?
|
||||
?SLOG(warning, #{ msg => "reach_rate_limit"
|
||||
, pause => Time
|
||||
?SLOG(warning, #{
|
||||
msg => "reach_rate_limit",
|
||||
pause => Time
|
||||
}),
|
||||
TRef = emqx_misc:start_timer(Time, limit_timeout),
|
||||
State#state{sockstate = blocked,
|
||||
State#state{
|
||||
sockstate = blocked,
|
||||
limiter = Limiter1,
|
||||
limit_timer = TRef
|
||||
}
|
||||
|
|
@ -815,8 +909,7 @@ ensure_rate_limit(Stats, State = #state{limiter = Limiter}) ->
|
|||
run_gc(Stats, State = #state{gc_state = GcSt}) ->
|
||||
case ?ENABLED(GcSt) andalso emqx_gc:run(Stats, GcSt) of
|
||||
false -> State;
|
||||
{_IsGC, GcSt1} ->
|
||||
State#state{gc_state = GcSt1}
|
||||
{_IsGC, GcSt1} -> State#state{gc_state = GcSt1}
|
||||
end.
|
||||
|
||||
check_oom(State = #state{oom_policy = OomPolicy}) ->
|
||||
|
|
@ -824,7 +917,8 @@ check_oom(State = #state{oom_policy = OomPolicy}) ->
|
|||
{shutdown, Reason} ->
|
||||
%% triggers terminate/2 callback immediately
|
||||
erlang:exit({shutdown, Reason});
|
||||
_Other -> ok
|
||||
_Other ->
|
||||
ok
|
||||
end,
|
||||
State.
|
||||
|
||||
|
|
@ -835,8 +929,12 @@ activate_socket(State = #state{sockstate = closed}) ->
|
|||
{ok, State};
|
||||
activate_socket(State = #state{sockstate = blocked}) ->
|
||||
{ok, State};
|
||||
activate_socket(State = #state{socket = Socket,
|
||||
active_n = N}) ->
|
||||
activate_socket(
|
||||
State = #state{
|
||||
socket = Socket,
|
||||
active_n = N
|
||||
}
|
||||
) ->
|
||||
%% FIXME: Works on dtls/udp ???
|
||||
%% How to handle buffer?
|
||||
case esockd_setopts(Socket, [{active, N}]) of
|
||||
|
|
@ -847,7 +945,8 @@ activate_socket(State = #state{socket = Socket,
|
|||
%%--------------------------------------------------------------------
|
||||
%% Close Socket
|
||||
|
||||
close_socket(State = #state{sockstate = closed}) -> State;
|
||||
close_socket(State = #state{sockstate = closed}) ->
|
||||
State;
|
||||
close_socket(State = #state{socket = Socket}) ->
|
||||
ok = esockd_close(Socket),
|
||||
State#state{sockstate = closed}.
|
||||
|
|
@ -865,7 +964,8 @@ inc_incoming_stats(Ctx, FrameMod, Packet) ->
|
|||
ok
|
||||
end,
|
||||
Name = list_to_atom(
|
||||
lists:concat(["packets.", FrameMod:type(Packet), ".received"])),
|
||||
lists:concat(["packets.", FrameMod:type(Packet), ".received"])
|
||||
),
|
||||
emqx_gateway_ctx:metrics_inc(Ctx, Name).
|
||||
|
||||
inc_outgoing_stats(Ctx, FrameMod, Packet) ->
|
||||
|
|
@ -878,7 +978,8 @@ inc_outgoing_stats(Ctx, FrameMod, Packet) ->
|
|||
ok
|
||||
end,
|
||||
Name = list_to_atom(
|
||||
lists:concat(["packets.", FrameMod:type(Packet), ".sent"])),
|
||||
lists:concat(["packets.", FrameMod:type(Packet), ".sent"])
|
||||
),
|
||||
emqx_gateway_ctx:metrics_inc(Ctx, Name).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -26,17 +26,18 @@
|
|||
|
||||
-type frame() :: any().
|
||||
|
||||
-type parse_result() :: {ok, frame(),
|
||||
Rest :: binary(), NewState :: parse_state()}
|
||||
-type parse_result() ::
|
||||
{ok, frame(), Rest :: binary(), NewState :: parse_state()}
|
||||
| {more, NewState :: parse_state()}.
|
||||
|
||||
-type serialize_options() :: map().
|
||||
|
||||
-export_type([ parse_state/0
|
||||
, parse_result/0
|
||||
, serialize_options/0
|
||||
, frame/0
|
||||
]).
|
||||
-export_type([
|
||||
parse_state/0,
|
||||
parse_result/0,
|
||||
serialize_options/0,
|
||||
frame/0
|
||||
]).
|
||||
|
||||
%% Callbacks
|
||||
|
||||
|
|
@ -60,4 +61,3 @@
|
|||
|
||||
%% @doc
|
||||
-callback is_message(Frame :: any()) -> boolean().
|
||||
|
||||
|
|
|
|||
|
|
@ -22,18 +22,22 @@
|
|||
-type reason() :: any().
|
||||
|
||||
%% @doc
|
||||
-callback on_gateway_load(Gateway :: gateway(),
|
||||
Ctx :: emqx_gateway_ctx:context())
|
||||
-> {error, reason()}
|
||||
-callback on_gateway_load(
|
||||
Gateway :: gateway(),
|
||||
Ctx :: emqx_gateway_ctx:context()
|
||||
) ->
|
||||
{error, reason()}
|
||||
| {ok, [ChildPid :: pid()], GwState :: state()}
|
||||
%% TODO: v0.2 The child spec is better for restarting child process
|
||||
| {ok, [Childspec :: supervisor:child_spec()], GwState :: state()}.
|
||||
|
||||
%% @doc
|
||||
-callback on_gateway_update(Config :: emqx_config:config(),
|
||||
-callback on_gateway_update(
|
||||
Config :: emqx_config:config(),
|
||||
Gateway :: gateway(),
|
||||
GwState :: state())
|
||||
-> ok
|
||||
GwState :: state()
|
||||
) ->
|
||||
ok
|
||||
| {ok, [ChildPid :: pid()], NGwState :: state()}
|
||||
| {ok, [Childspec :: supervisor:child_spec()], NGwState :: state()}
|
||||
| {error, reason()}.
|
||||
|
|
|
|||
|
|
@ -44,18 +44,23 @@ paths() ->
|
|||
[?PREFIX ++ "/request"].
|
||||
|
||||
schema(?PREFIX ++ "/request") ->
|
||||
#{operationId => request,
|
||||
post => #{ tags => [<<"gateway|coap">>]
|
||||
, desc => <<"Send a CoAP request message to the client">>
|
||||
, parameters => request_parameters()
|
||||
, requestBody => request_body()
|
||||
, responses => #{200 => coap_message(),
|
||||
#{
|
||||
operationId => request,
|
||||
post => #{
|
||||
tags => [<<"gateway|coap">>],
|
||||
desc => <<"Send a CoAP request message to the client">>,
|
||||
parameters => request_parameters(),
|
||||
requestBody => request_body(),
|
||||
responses => #{
|
||||
200 => coap_message(),
|
||||
404 => error_codes(['CLIENT_NOT_FOUND'], <<"Client not found error">>),
|
||||
504 => error_codes(['CLIENT_NOT_RESPONSE'], <<"Waiting for client response timeout">>)}
|
||||
504 => error_codes(
|
||||
['CLIENT_NOT_RESPONSE'], <<"Waiting for client response timeout">>
|
||||
)
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
|
||||
request(post, #{body := Body, bindings := Bindings}) ->
|
||||
ClientId = maps:get(clientid, Bindings, undefined),
|
||||
Method = maps:get(<<"method">>, Body, get),
|
||||
|
|
@ -66,8 +71,12 @@ request(post, #{body := Body, bindings := Bindings}) ->
|
|||
CT = erlang:atom_to_binary(AtomCT),
|
||||
Payload2 = parse_payload(CT, Payload),
|
||||
|
||||
Msg = emqx_coap_message:request(con,
|
||||
Method, Payload2, #{content_format => CT}),
|
||||
Msg = emqx_coap_message:request(
|
||||
con,
|
||||
Method,
|
||||
Payload2,
|
||||
#{content_format => CT}
|
||||
),
|
||||
|
||||
Msg2 = Msg#coap_message{token = Token},
|
||||
|
||||
|
|
@ -87,42 +96,49 @@ request_parameters() ->
|
|||
[{clientid, mk(binary(), #{in => path, required => true})}].
|
||||
|
||||
request_body() ->
|
||||
[ {token, mk(binary(), #{desc => "message token, can be empty"})}
|
||||
, {method, mk(enum([get, put, post, delete]), #{desc => "request method type"})}
|
||||
, {timeout, mk(emqx_schema:duration_ms(), #{desc => "timespan for response"})}
|
||||
, {content_type, mk(enum(['text/plain', 'application/json', 'application/octet-stream']),
|
||||
#{desc => "payload type"})}
|
||||
, {payload, mk(binary(), #{desc => "the content of the payload"})}
|
||||
[
|
||||
{token, mk(binary(), #{desc => "message token, can be empty"})},
|
||||
{method, mk(enum([get, put, post, delete]), #{desc => "request method type"})},
|
||||
{timeout, mk(emqx_schema:duration_ms(), #{desc => "timespan for response"})},
|
||||
{content_type,
|
||||
mk(
|
||||
enum(['text/plain', 'application/json', 'application/octet-stream']),
|
||||
#{desc => "payload type"}
|
||||
)},
|
||||
{payload, mk(binary(), #{desc => "the content of the payload"})}
|
||||
].
|
||||
|
||||
coap_message() ->
|
||||
[ {id, mk(integer(), #{desc => "message id"})}
|
||||
, {token, mk(string(), #{desc => "message token, can be empty"})}
|
||||
, {method, mk(string(), #{desc => "response code"})}
|
||||
, {payload, mk(string(), #{desc => "payload"})}
|
||||
[
|
||||
{id, mk(integer(), #{desc => "message id"})},
|
||||
{token, mk(string(), #{desc => "message token, can be empty"})},
|
||||
{method, mk(string(), #{desc => "response code"})},
|
||||
{payload, mk(string(), #{desc => "payload"})}
|
||||
].
|
||||
|
||||
format_to_response(ContentType, #coap_message{id = Id,
|
||||
format_to_response(ContentType, #coap_message{
|
||||
id = Id,
|
||||
token = Token,
|
||||
method = Method,
|
||||
payload = Payload}) ->
|
||||
#{id => Id,
|
||||
payload = Payload
|
||||
}) ->
|
||||
#{
|
||||
id => Id,
|
||||
token => Token,
|
||||
method => format_to_binary(Method),
|
||||
payload => format_payload(ContentType, Payload)}.
|
||||
payload => format_payload(ContentType, Payload)
|
||||
}.
|
||||
|
||||
format_to_binary(Obj) ->
|
||||
erlang:list_to_binary(io_lib:format("~p", [Obj])).
|
||||
|
||||
format_payload(<<"application/octet-stream">>, Payload) ->
|
||||
base64:encode(Payload);
|
||||
|
||||
format_payload(_, Payload) ->
|
||||
Payload.
|
||||
|
||||
parse_payload(<<"application/octet-stream">>, Body) ->
|
||||
base64:decode(Body);
|
||||
|
||||
parse_payload(_, Body) ->
|
||||
Body.
|
||||
|
||||
|
|
@ -140,10 +156,13 @@ call_client(ClientId, Msg, Timeout) ->
|
|||
_ ->
|
||||
not_found
|
||||
end
|
||||
catch _:Error:Trace ->
|
||||
?SLOG(warning, #{msg => "coap_client_call_exception",
|
||||
catch
|
||||
_:Error:Trace ->
|
||||
?SLOG(warning, #{
|
||||
msg => "coap_client_call_exception",
|
||||
clientid => ClientId,
|
||||
error => Error,
|
||||
stacktrace => Trace}),
|
||||
stacktrace => Trace
|
||||
}),
|
||||
not_found
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -19,26 +19,29 @@
|
|||
-behaviour(emqx_gateway_channel).
|
||||
|
||||
%% API
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
, validator/4
|
||||
, metrics_inc/2
|
||||
, run_hooks/3
|
||||
, send_request/2
|
||||
]).
|
||||
-export([
|
||||
info/1,
|
||||
info/2,
|
||||
stats/1,
|
||||
validator/4,
|
||||
metrics_inc/2,
|
||||
run_hooks/3,
|
||||
send_request/2
|
||||
]).
|
||||
|
||||
-export([ init/2
|
||||
, handle_in/2
|
||||
, handle_deliver/2
|
||||
, handle_timeout/3
|
||||
, terminate/2
|
||||
]).
|
||||
-export([
|
||||
init/2,
|
||||
handle_in/2,
|
||||
handle_deliver/2,
|
||||
handle_timeout/3,
|
||||
terminate/2
|
||||
]).
|
||||
|
||||
-export([ handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
]).
|
||||
-export([
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2
|
||||
]).
|
||||
|
||||
-export_type([channel/0]).
|
||||
|
||||
|
|
@ -67,15 +70,16 @@
|
|||
conn_state :: conn_state(),
|
||||
%% Session token to identity this connection
|
||||
token :: binary() | undefined
|
||||
}).
|
||||
}).
|
||||
|
||||
-type channel() :: #channel{}.
|
||||
|
||||
-type conn_state() :: idle | connecting | connected | disconnected.
|
||||
|
||||
-type reply() :: {outgoing, coap_message()}
|
||||
-type reply() ::
|
||||
{outgoing, coap_message()}
|
||||
| {outgoing, [coap_message()]}
|
||||
| {event, conn_state()|updated}
|
||||
| {event, conn_state() | updated}
|
||||
| {close, Reason :: atom()}.
|
||||
|
||||
-type replies() :: reply() | [reply()].
|
||||
|
|
@ -97,10 +101,9 @@
|
|||
info(Channel) ->
|
||||
maps:from_list(info(?INFO_KEYS, Channel)).
|
||||
|
||||
-spec info(list(atom())|atom(), channel()) -> term().
|
||||
-spec info(list(atom()) | atom(), channel()) -> term().
|
||||
info(Keys, Channel) when is_list(Keys) ->
|
||||
[{Key, info(Key, Channel)} || Key <- Keys];
|
||||
|
||||
info(conninfo, #channel{conninfo = ConnInfo}) ->
|
||||
ConnInfo;
|
||||
info(conn_state, #channel{conn_state = ConnState}) ->
|
||||
|
|
@ -119,40 +122,46 @@ stats(_) ->
|
|||
[].
|
||||
|
||||
-spec init(map(), map()) -> channel().
|
||||
init(ConnInfo = #{peername := {PeerHost, _},
|
||||
sockname := {_, SockPort}},
|
||||
#{ctx := Ctx} = Config) ->
|
||||
init(
|
||||
ConnInfo = #{
|
||||
peername := {PeerHost, _},
|
||||
sockname := {_, SockPort}
|
||||
},
|
||||
#{ctx := Ctx} = Config
|
||||
) ->
|
||||
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||
Mountpoint = maps:get(mountpoint, Config, <<>>),
|
||||
ListenerId = case maps:get(listener, Config, undefined) of
|
||||
ListenerId =
|
||||
case maps:get(listener, Config, undefined) of
|
||||
undefined -> undefined;
|
||||
{GwName, Type, LisName} ->
|
||||
emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||
{GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||
end,
|
||||
ClientInfo = set_peercert_infos(
|
||||
Peercert,
|
||||
#{ zone => default
|
||||
, listener => ListenerId
|
||||
, protocol => 'coap'
|
||||
, peerhost => PeerHost
|
||||
, sockport => SockPort
|
||||
, clientid => emqx_guid:to_base62(emqx_guid:gen())
|
||||
, username => undefined
|
||||
, is_bridge => false
|
||||
, is_superuser => false
|
||||
, mountpoint => Mountpoint
|
||||
#{
|
||||
zone => default,
|
||||
listener => ListenerId,
|
||||
protocol => 'coap',
|
||||
peerhost => PeerHost,
|
||||
sockport => SockPort,
|
||||
clientid => emqx_guid:to_base62(emqx_guid:gen()),
|
||||
username => undefined,
|
||||
is_bridge => false,
|
||||
is_superuser => false,
|
||||
mountpoint => Mountpoint
|
||||
}
|
||||
),
|
||||
|
||||
Heartbeat = ?GET_IDLE_TIME(Config),
|
||||
#channel{ ctx = Ctx
|
||||
, conninfo = ConnInfo
|
||||
, clientinfo = ClientInfo
|
||||
, timers = #{}
|
||||
, session = emqx_coap_session:new()
|
||||
, keepalive = emqx_keepalive:init(Heartbeat)
|
||||
, connection_required = maps:get(connection_required, Config, false)
|
||||
, conn_state = idle
|
||||
#channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo,
|
||||
timers = #{},
|
||||
session = emqx_coap_session:new(),
|
||||
keepalive = emqx_keepalive:init(Heartbeat),
|
||||
connection_required = maps:get(connection_required, Config, false),
|
||||
conn_state = idle
|
||||
}.
|
||||
|
||||
validator(Type, Topic, Ctx, ClientInfo) ->
|
||||
|
|
@ -166,8 +175,8 @@ send_request(Channel, Request) ->
|
|||
%% Handle incoming packet
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec handle_in(coap_message() | {frame_error, any()}, channel())
|
||||
-> {ok, channel()}
|
||||
-spec handle_in(coap_message() | {frame_error, any()}, channel()) ->
|
||||
{ok, channel()}
|
||||
| {ok, replies(), channel()}
|
||||
| {shutdown, Reason :: term(), channel()}
|
||||
| {shutdown, Reason :: term(), replies(), channel()}.
|
||||
|
|
@ -183,8 +192,13 @@ handle_in(Msg, ChannleT) ->
|
|||
%%--------------------------------------------------------------------
|
||||
%% Handle Delivers from broker to client
|
||||
%%--------------------------------------------------------------------
|
||||
handle_deliver(Delivers, #channel{session = Session,
|
||||
ctx = Ctx} = Channel) ->
|
||||
handle_deliver(
|
||||
Delivers,
|
||||
#channel{
|
||||
session = Session,
|
||||
ctx = Ctx
|
||||
} = Channel
|
||||
) ->
|
||||
handle_result(emqx_coap_session:deliver(Delivers, Ctx, Session), Channel).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
@ -199,13 +213,10 @@ handle_timeout(_, {keepalive, NewVal}, #channel{keepalive = KeepAlive} = Channel
|
|||
{error, timeout} ->
|
||||
{shutdown, timeout, ensure_disconnected(keepalive_timeout, Channel)}
|
||||
end;
|
||||
|
||||
handle_timeout(_, {transport, Msg}, Channel) ->
|
||||
call_session(timeout, Msg, Channel);
|
||||
|
||||
handle_timeout(_, disconnect, Channel) ->
|
||||
{shutdown, normal, Channel};
|
||||
|
||||
handle_timeout(_, _, Channel) ->
|
||||
{ok, Channel}.
|
||||
|
||||
|
|
@ -213,72 +224,89 @@ handle_timeout(_, _, Channel) ->
|
|||
%% Handle call
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(handle_call(Req :: term(), From :: term(), channel())
|
||||
-> {reply, Reply :: term(), channel()}
|
||||
-spec handle_call(Req :: term(), From :: term(), channel()) ->
|
||||
{reply, Reply :: term(), channel()}
|
||||
| {reply, Reply :: term(), replies(), channel()}
|
||||
| {shutdown, Reason :: term(), Reply :: term(), channel()}
|
||||
| {shutdown, Reason :: term(), Reply :: term(), coap_message(), channel()}).
|
||||
| {shutdown, Reason :: term(), Reply :: term(), coap_message(), channel()}.
|
||||
handle_call({send_request, Msg}, From, Channel) ->
|
||||
Result = call_session(handle_out, {{send_request, From}, Msg}, Channel),
|
||||
erlang:setelement(1, Result, noreply);
|
||||
|
||||
handle_call({subscribe, Topic, SubOpts}, _From,
|
||||
handle_call(
|
||||
{subscribe, Topic, SubOpts},
|
||||
_From,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo
|
||||
= #{clientid := ClientId,
|
||||
mountpoint := Mountpoint},
|
||||
session = Session}) ->
|
||||
Token = maps:get(token,
|
||||
clientinfo =
|
||||
ClientInfo =
|
||||
#{
|
||||
clientid := ClientId,
|
||||
mountpoint := Mountpoint
|
||||
},
|
||||
session = Session
|
||||
}
|
||||
) ->
|
||||
Token = maps:get(
|
||||
token,
|
||||
maps:get(sub_props, SubOpts, #{}),
|
||||
<<>>),
|
||||
<<>>
|
||||
),
|
||||
NSubOpts = maps:merge(
|
||||
emqx_gateway_utils:default_subopts(),
|
||||
SubOpts),
|
||||
SubOpts
|
||||
),
|
||||
MountedTopic = emqx_mountpoint:mount(Mountpoint, Topic),
|
||||
_ = emqx_broker:subscribe(MountedTopic, ClientId, NSubOpts),
|
||||
|
||||
_ = run_hooks(Ctx, 'session.subscribed',
|
||||
[ClientInfo, MountedTopic, NSubOpts]),
|
||||
_ = run_hooks(
|
||||
Ctx,
|
||||
'session.subscribed',
|
||||
[ClientInfo, MountedTopic, NSubOpts]
|
||||
),
|
||||
%% modify session state
|
||||
SubReq = {Topic, Token},
|
||||
TempMsg = #coap_message{type = non},
|
||||
%% FIXME: The subopts is not used for emqx_coap_session
|
||||
Result = emqx_coap_session:process_subscribe(
|
||||
SubReq, TempMsg, #{}, Session),
|
||||
SubReq, TempMsg, #{}, Session
|
||||
),
|
||||
NSession = maps:get(session, Result),
|
||||
{reply, {ok, {MountedTopic, NSubOpts}}, Channel#channel{session = NSession}};
|
||||
|
||||
handle_call({unsubscribe, Topic}, _From,
|
||||
handle_call(
|
||||
{unsubscribe, Topic},
|
||||
_From,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo
|
||||
= #{mountpoint := Mountpoint},
|
||||
session = Session}) ->
|
||||
clientinfo =
|
||||
ClientInfo =
|
||||
#{mountpoint := Mountpoint},
|
||||
session = Session
|
||||
}
|
||||
) ->
|
||||
MountedTopic = emqx_mountpoint:mount(Mountpoint, Topic),
|
||||
ok = emqx_broker:unsubscribe(MountedTopic),
|
||||
_ = run_hooks(Ctx, 'session.unsubscribe',
|
||||
[ClientInfo, MountedTopic, #{}]),
|
||||
_ = run_hooks(
|
||||
Ctx,
|
||||
'session.unsubscribe',
|
||||
[ClientInfo, MountedTopic, #{}]
|
||||
),
|
||||
|
||||
%% modify session state
|
||||
UnSubReq = Topic,
|
||||
TempMsg = #coap_message{type = non},
|
||||
Result = emqx_coap_session:process_subscribe(
|
||||
UnSubReq, TempMsg, #{}, Session),
|
||||
UnSubReq, TempMsg, #{}, Session
|
||||
),
|
||||
NSession = maps:get(session, Result),
|
||||
{reply, ok, Channel#channel{session = NSession}};
|
||||
|
||||
handle_call(subscriptions, _From, Channel = #channel{session = Session}) ->
|
||||
Subs = emqx_coap_session:info(subscriptions, Session),
|
||||
{reply, {ok, maps:to_list(Subs)}, Channel};
|
||||
|
||||
handle_call(kick, _From, Channel) ->
|
||||
NChannel = ensure_disconnected(kicked, Channel),
|
||||
shutdown_and_reply(kicked, ok, NChannel);
|
||||
|
||||
handle_call(discard, _From, Channel) ->
|
||||
shutdown_and_reply(discarded, ok, Channel);
|
||||
|
||||
handle_call(Req, _From, Channel) ->
|
||||
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||
{reply, ignored, Channel}.
|
||||
|
|
@ -287,8 +315,8 @@ handle_call(Req, _From, Channel) ->
|
|||
%% Handle Cast
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec handle_cast(Req :: term(), channel())
|
||||
-> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}.
|
||||
-spec handle_cast(Req :: term(), channel()) ->
|
||||
ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}.
|
||||
handle_cast(Req, Channel) ->
|
||||
?SLOG(error, #{msg => "unexpected_cast", cast => Req}),
|
||||
{ok, Channel}.
|
||||
|
|
@ -297,11 +325,10 @@ handle_cast(Req, Channel) ->
|
|||
%% Handle Info
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(handle_info(Info :: term(), channel())
|
||||
-> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}).
|
||||
-spec handle_info(Info :: term(), channel()) ->
|
||||
ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}.
|
||||
handle_info({subscribe, _AutoSubs}, Channel) ->
|
||||
{ok, Channel};
|
||||
|
||||
handle_info(Info, Channel) ->
|
||||
?SLOG(warning, #{msg => "unexpected_info", info => Info}),
|
||||
{ok, Channel}.
|
||||
|
|
@ -309,21 +336,23 @@ handle_info(Info, Channel) ->
|
|||
%%--------------------------------------------------------------------
|
||||
%% Terminate
|
||||
%%--------------------------------------------------------------------
|
||||
terminate(Reason, #channel{clientinfo = ClientInfo,
|
||||
terminate(Reason, #channel{
|
||||
clientinfo = ClientInfo,
|
||||
ctx = Ctx,
|
||||
session = Session}) ->
|
||||
session = Session
|
||||
}) ->
|
||||
run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
set_peercert_infos(NoSSL, ClientInfo)
|
||||
when NoSSL =:= nossl;
|
||||
NoSSL =:= undefined ->
|
||||
set_peercert_infos(NoSSL, ClientInfo) when
|
||||
NoSSL =:= nossl;
|
||||
NoSSL =:= undefined
|
||||
->
|
||||
ClientInfo;
|
||||
set_peercert_infos(Peercert, ClientInfo) ->
|
||||
{DN, CN} = {esockd_peercert:subject(Peercert),
|
||||
esockd_peercert:common_name(Peercert)},
|
||||
{DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)},
|
||||
ClientInfo#{dn => DN, cn => CN}.
|
||||
|
||||
ensure_timer(Name, Time, Msg, #channel{timers = Timers} = Channel) ->
|
||||
|
|
@ -348,15 +377,21 @@ ensure_keepalive_timer(Fun, #channel{keepalive = KeepAlive} = Channel) ->
|
|||
check_auth_state(Msg, #channel{connection_required = Required} = Channel) ->
|
||||
check_token(Required, Msg, Channel).
|
||||
|
||||
check_token(true,
|
||||
check_token(
|
||||
true,
|
||||
Msg,
|
||||
#channel{token = Token,
|
||||
#channel{
|
||||
token = Token,
|
||||
clientinfo = ClientInfo,
|
||||
conn_state = CState} = Channel) ->
|
||||
conn_state = CState
|
||||
} = Channel
|
||||
) ->
|
||||
#{clientid := ClientId} = ClientInfo,
|
||||
case emqx_coap_message:get_option(uri_query, Msg) of
|
||||
#{<<"clientid">> := ClientId,
|
||||
<<"token">> := Token} ->
|
||||
#{
|
||||
<<"clientid">> := ClientId,
|
||||
<<"token">> := Token
|
||||
} ->
|
||||
call_session(handle_request, Msg, Channel);
|
||||
#{<<"clientid">> := DesireId} ->
|
||||
try_takeover(CState, DesireId, Msg, Channel);
|
||||
|
|
@ -364,7 +399,6 @@ check_token(true,
|
|||
Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg),
|
||||
{ok, {outgoing, Reply}, Channel}
|
||||
end;
|
||||
|
||||
check_token(false, Msg, Channel) ->
|
||||
call_session(handle_request, Msg, Channel).
|
||||
|
||||
|
|
@ -383,7 +417,6 @@ try_takeover(idle, DesireId, Msg, Channel) ->
|
|||
do_takeover(DesireId, Msg, Channel)
|
||||
end
|
||||
end;
|
||||
|
||||
try_takeover(_, DesireId, Msg, Channel) ->
|
||||
do_takeover(DesireId, Msg, Channel).
|
||||
|
||||
|
|
@ -392,43 +425,57 @@ do_takeover(_DesireId, Msg, Channel) ->
|
|||
Reset = emqx_coap_message:reset(Msg),
|
||||
{ok, {outgoing, Reset}, Channel}.
|
||||
|
||||
run_conn_hooks(Input, Channel = #channel{ctx = Ctx,
|
||||
conninfo = ConnInfo}) ->
|
||||
run_conn_hooks(
|
||||
Input,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo
|
||||
}
|
||||
) ->
|
||||
ConnProps = #{},
|
||||
case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of
|
||||
Error = {error, _Reason} -> Error;
|
||||
_NConnProps ->
|
||||
{ok, Input, Channel}
|
||||
_NConnProps -> {ok, Input, Channel}
|
||||
end.
|
||||
|
||||
enrich_conninfo({Queries, _Msg},
|
||||
enrich_conninfo(
|
||||
{Queries, _Msg},
|
||||
Channel = #channel{
|
||||
keepalive = KeepAlive,
|
||||
conninfo = ConnInfo}) ->
|
||||
conninfo = ConnInfo
|
||||
}
|
||||
) ->
|
||||
case Queries of
|
||||
#{<<"clientid">> := ClientId} ->
|
||||
Interval = maps:get(interval, emqx_keepalive:info(KeepAlive)),
|
||||
NConnInfo = ConnInfo#{ clientid => ClientId
|
||||
, proto_name => <<"CoAP">>
|
||||
, proto_ver => <<"1">>
|
||||
, clean_start => true
|
||||
, keepalive => Interval
|
||||
, expiry_interval => 0
|
||||
NConnInfo = ConnInfo#{
|
||||
clientid => ClientId,
|
||||
proto_name => <<"CoAP">>,
|
||||
proto_ver => <<"1">>,
|
||||
clean_start => true,
|
||||
keepalive => Interval,
|
||||
expiry_interval => 0
|
||||
},
|
||||
{ok, Channel#channel{conninfo = NConnInfo}};
|
||||
_ ->
|
||||
{error, "invalid queries", Channel}
|
||||
end.
|
||||
|
||||
enrich_clientinfo({Queries, Msg},
|
||||
Channel = #channel{clientinfo = ClientInfo0}) ->
|
||||
enrich_clientinfo(
|
||||
{Queries, Msg},
|
||||
Channel = #channel{clientinfo = ClientInfo0}
|
||||
) ->
|
||||
case Queries of
|
||||
#{<<"username">> := UserName,
|
||||
#{
|
||||
<<"username">> := UserName,
|
||||
<<"password">> := Password,
|
||||
<<"clientid">> := ClientId} ->
|
||||
ClientInfo = ClientInfo0#{username => UserName,
|
||||
<<"clientid">> := ClientId
|
||||
} ->
|
||||
ClientInfo = ClientInfo0#{
|
||||
username => UserName,
|
||||
password => Password,
|
||||
clientid => ClientId},
|
||||
clientid => ClientId
|
||||
},
|
||||
{ok, NClientInfo} = fix_mountpoint(Msg, ClientInfo),
|
||||
{ok, Channel#channel{clientinfo = NClientInfo}};
|
||||
_ ->
|
||||
|
|
@ -439,18 +486,26 @@ set_log_meta(_Input, #channel{clientinfo = #{clientid := ClientId}}) ->
|
|||
emqx_logger:set_metadata_clientid(ClientId),
|
||||
ok.
|
||||
|
||||
auth_connect(_Input, Channel = #channel{ctx = Ctx,
|
||||
clientinfo = ClientInfo}) ->
|
||||
#{clientid := ClientId,
|
||||
username := Username} = ClientInfo,
|
||||
auth_connect(
|
||||
_Input,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
#{
|
||||
clientid := ClientId,
|
||||
username := Username
|
||||
} = ClientInfo,
|
||||
case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of
|
||||
{ok, NClientInfo} ->
|
||||
{ok, Channel#channel{clientinfo = NClientInfo}};
|
||||
{error, Reason} ->
|
||||
?SLOG(warning, #{ msg => "client_login_failed"
|
||||
, username => Username
|
||||
, clientid => ClientId
|
||||
, reason => Reason
|
||||
?SLOG(warning, #{
|
||||
msg => "client_login_failed",
|
||||
username => Username,
|
||||
clientid => ClientId,
|
||||
reason => Reason
|
||||
}),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
|
@ -461,32 +516,43 @@ fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) ->
|
|||
Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo),
|
||||
{ok, ClientInfo#{mountpoint := Mountpoint1}}.
|
||||
|
||||
process_connect(#channel{ctx = Ctx,
|
||||
process_connect(
|
||||
#channel{
|
||||
ctx = Ctx,
|
||||
session = Session,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo} = Channel,
|
||||
Msg, Result, Iter) ->
|
||||
clientinfo = ClientInfo
|
||||
} = Channel,
|
||||
Msg,
|
||||
Result,
|
||||
Iter
|
||||
) ->
|
||||
%% inherit the old session
|
||||
SessFun = fun(_,_) -> Session end,
|
||||
case emqx_gateway_ctx:open_session(
|
||||
SessFun = fun(_, _) -> Session end,
|
||||
case
|
||||
emqx_gateway_ctx:open_session(
|
||||
Ctx,
|
||||
true,
|
||||
ClientInfo,
|
||||
ConnInfo,
|
||||
SessFun,
|
||||
emqx_coap_session
|
||||
) of
|
||||
)
|
||||
of
|
||||
{ok, _Sess} ->
|
||||
RandVal = rand:uniform(?TOKEN_MAXIMUM),
|
||||
Token = erlang:list_to_binary(erlang:integer_to_list(RandVal)),
|
||||
NResult = Result#{events => [{event, connected}]},
|
||||
iter(Iter,
|
||||
iter(
|
||||
Iter,
|
||||
reply({ok, created}, Token, Msg, NResult),
|
||||
Channel#channel{token = Token});
|
||||
Channel#channel{token = Token}
|
||||
);
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{ msg => "failed_open_session"
|
||||
, clientid => maps:get(clientid, ClientInfo)
|
||||
, reason => Reason
|
||||
?SLOG(error, #{
|
||||
msg => "failed_open_session",
|
||||
clientid => maps:get(clientid, ClientInfo),
|
||||
reason => Reason
|
||||
}),
|
||||
iter(Iter, reply({error, bad_request}, Msg, Result), Channel)
|
||||
end.
|
||||
|
|
@ -505,11 +571,14 @@ metrics_inc(Name, Ctx) ->
|
|||
%%--------------------------------------------------------------------
|
||||
%% Ensure connected
|
||||
|
||||
ensure_connected(Channel = #channel{ctx = Ctx,
|
||||
ensure_connected(
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond)
|
||||
},
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)},
|
||||
_ = run_hooks(Ctx, 'client.connack', [NConnInfo, connection_accepted, []]),
|
||||
ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]),
|
||||
Channel#channel{conninfo = NConnInfo, conn_state = connected}.
|
||||
|
|
@ -517,10 +586,14 @@ ensure_connected(Channel = #channel{ctx = Ctx,
|
|||
%%--------------------------------------------------------------------
|
||||
%% Ensure disconnected
|
||||
|
||||
ensure_disconnected(Reason, Channel = #channel{
|
||||
ensure_disconnected(
|
||||
Reason,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
|
||||
ok = run_hooks(Ctx, 'client.disconnected', [ClientInfo, Reason, NConnInfo]),
|
||||
Channel#channel{conninfo = NConnInfo, conn_state = disconnected}.
|
||||
|
|
@ -540,18 +613,32 @@ call_session(Fun, Msg, #channel{session = Session} = Channel) ->
|
|||
handle_result(Result, Channel).
|
||||
|
||||
handle_result(Result, Channel) ->
|
||||
iter([ session, fun process_session/4
|
||||
, proto, fun process_protocol/4
|
||||
, reply, fun process_reply/4
|
||||
, out, fun process_out/4
|
||||
, fun process_nothing/3
|
||||
iter(
|
||||
[
|
||||
session,
|
||||
fun process_session/4,
|
||||
proto,
|
||||
fun process_protocol/4,
|
||||
reply,
|
||||
fun process_reply/4,
|
||||
out,
|
||||
fun process_out/4,
|
||||
fun process_nothing/3
|
||||
],
|
||||
Result,
|
||||
Channel).
|
||||
Channel
|
||||
).
|
||||
|
||||
call_handler(request, Msg, Result,
|
||||
#channel{ctx = Ctx,
|
||||
clientinfo = ClientInfo} = Channel, Iter) ->
|
||||
call_handler(
|
||||
request,
|
||||
Msg,
|
||||
Result,
|
||||
#channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo
|
||||
} = Channel,
|
||||
Iter
|
||||
) ->
|
||||
HandlerResult =
|
||||
case emqx_coap_message:get_option(uri_path, Msg) of
|
||||
[<<"ps">> | RestPath] ->
|
||||
|
|
@ -561,15 +648,20 @@ call_handler(request, Msg, Result,
|
|||
_ ->
|
||||
reply({error, bad_request}, Msg)
|
||||
end,
|
||||
iter([ connection, fun process_connection/4
|
||||
, subscribe, fun process_subscribe/4 | Iter],
|
||||
iter(
|
||||
[
|
||||
connection,
|
||||
fun process_connection/4,
|
||||
subscribe,
|
||||
fun process_subscribe/4
|
||||
| Iter
|
||||
],
|
||||
maps:merge(Result, HandlerResult),
|
||||
Channel);
|
||||
|
||||
Channel
|
||||
);
|
||||
call_handler(response, {{send_request, From}, Response}, Result, Channel, Iter) ->
|
||||
gen_server:reply(From, Response),
|
||||
iter(Iter, Result, Channel);
|
||||
|
||||
call_handler(_, _, Result, Channel, Iter) ->
|
||||
iter(Iter, Result, Channel).
|
||||
|
||||
|
|
@ -582,7 +674,8 @@ process_protocol({Type, Msg}, Result, Channel, Iter) ->
|
|||
%% leaf node
|
||||
process_out(Outs, Result, Channel, _) ->
|
||||
Outs2 = lists:reverse(Outs),
|
||||
Outs3 = case maps:get(reply, Result, undefined) of
|
||||
Outs3 =
|
||||
case maps:get(reply, Result, undefined) of
|
||||
undefined ->
|
||||
Outs2;
|
||||
Reply ->
|
||||
|
|
@ -595,32 +688,48 @@ process_out(Outs, Result, Channel, _) ->
|
|||
process_nothing(_, _, Channel) ->
|
||||
{ok, Channel}.
|
||||
|
||||
process_connection({open, Req}, Result,
|
||||
Channel = #channel{conn_state = idle}, Iter) ->
|
||||
process_connection(
|
||||
{open, Req},
|
||||
Result,
|
||||
Channel = #channel{conn_state = idle},
|
||||
Iter
|
||||
) ->
|
||||
Queries = emqx_coap_message:get_option(uri_query, Req),
|
||||
case emqx_misc:pipeline(
|
||||
[ fun enrich_conninfo/2
|
||||
, fun run_conn_hooks/2
|
||||
, fun enrich_clientinfo/2
|
||||
, fun set_log_meta/2
|
||||
, fun auth_connect/2
|
||||
case
|
||||
emqx_misc:pipeline(
|
||||
[
|
||||
fun enrich_conninfo/2,
|
||||
fun run_conn_hooks/2,
|
||||
fun enrich_clientinfo/2,
|
||||
fun set_log_meta/2,
|
||||
fun auth_connect/2
|
||||
],
|
||||
{Queries, Req},
|
||||
Channel) of
|
||||
Channel
|
||||
)
|
||||
of
|
||||
{ok, _Input, NChannel} ->
|
||||
process_connect(ensure_connected(NChannel), Req, Result, Iter);
|
||||
{error, ReasonCode, NChannel} ->
|
||||
ErrMsg = io_lib:format("Login Failed: ~ts", [ReasonCode]),
|
||||
Payload = iolist_to_binary(ErrMsg),
|
||||
iter(Iter,
|
||||
iter(
|
||||
Iter,
|
||||
reply({error, bad_request}, Payload, Req, Result),
|
||||
NChannel)
|
||||
NChannel
|
||||
)
|
||||
end;
|
||||
process_connection({open, Req}, Result,
|
||||
process_connection(
|
||||
{open, Req},
|
||||
Result,
|
||||
Channel = #channel{
|
||||
conn_state = ConnState,
|
||||
clientinfo = #{clientid := ClientId}}, Iter)
|
||||
when ConnState == connected ->
|
||||
clientinfo = #{clientid := ClientId}
|
||||
},
|
||||
Iter
|
||||
) when
|
||||
ConnState == connected
|
||||
->
|
||||
Queries = emqx_coap_message:get_option(uri_query, Req),
|
||||
ErrMsg0 =
|
||||
case Queries of
|
||||
|
|
@ -633,9 +742,11 @@ process_connection({open, Req}, Result,
|
|||
end,
|
||||
ErrMsg = io_lib:format("Bad Request: ~ts", [ErrMsg0]),
|
||||
Payload = iolist_to_binary(ErrMsg),
|
||||
iter(Iter,
|
||||
iter(
|
||||
Iter,
|
||||
reply({error, bad_request}, Payload, Req, Result),
|
||||
Channel);
|
||||
Channel
|
||||
);
|
||||
process_connection({close, Msg}, _, Channel, _) ->
|
||||
Reply = emqx_coap_message:piggyback({ok, deleted}, Msg),
|
||||
NChannel = ensure_disconnected(normal, Channel),
|
||||
|
|
@ -651,5 +762,4 @@ process_reply(Reply, Result, #channel{session = Session} = Channel, _) ->
|
|||
Outs = maps:get(out, Result, []),
|
||||
Outs2 = lists:reverse(Outs),
|
||||
Events = maps:get(events, Result, []),
|
||||
{ok, [{outgoing, [Reply | Outs2]}] ++ Events,
|
||||
Channel#channel{session = Session2}}.
|
||||
{ok, [{outgoing, [Reply | Outs2]}] ++ Events, Channel#channel{session = Session2}}.
|
||||
|
|
|
|||
|
|
@ -19,14 +19,15 @@
|
|||
-behaviour(emqx_gateway_frame).
|
||||
|
||||
%% emqx_gateway_frame callbacks
|
||||
-export([ initial_parse_state/1
|
||||
, serialize_opts/0
|
||||
, serialize_pkt/2
|
||||
, parse/2
|
||||
, format/1
|
||||
, type/1
|
||||
, is_message/1
|
||||
]).
|
||||
-export([
|
||||
initial_parse_state/1,
|
||||
serialize_opts/0,
|
||||
serialize_pkt/2,
|
||||
parse/2,
|
||||
format/1,
|
||||
type/1,
|
||||
is_message/1
|
||||
]).
|
||||
|
||||
-include("src/coap/include/emqx_coap.hrl").
|
||||
-include_lib("emqx/include/types.hrl").
|
||||
|
|
@ -37,7 +38,8 @@
|
|||
-define(OPTION_URI_HOST, 3).
|
||||
-define(OPTION_ETAG, 4).
|
||||
-define(OPTION_IF_NONE_MATCH, 5).
|
||||
-define(OPTION_OBSERVE, 6). % draft-ietf-core-observe-16
|
||||
% draft-ietf-core-observe-16
|
||||
-define(OPTION_OBSERVE, 6).
|
||||
-define(OPTION_URI_PORT, 7).
|
||||
-define(OPTION_LOCATION_PATH, 8).
|
||||
-define(OPTION_URI_PATH, 11).
|
||||
|
|
@ -46,7 +48,8 @@
|
|||
-define(OPTION_URI_QUERY, 15).
|
||||
-define(OPTION_ACCEPT, 17).
|
||||
-define(OPTION_LOCATION_QUERY, 20).
|
||||
-define(OPTION_BLOCK2, 23). % draft-ietf-core-block-17
|
||||
% draft-ietf-core-block-17
|
||||
-define(OPTION_BLOCK2, 23).
|
||||
-define(OPTION_BLOCK1, 27).
|
||||
-define(OPTION_PROXY_URI, 35).
|
||||
-define(OPTION_PROXY_SCHEME, 39).
|
||||
|
|
@ -70,22 +73,25 @@ serialize_opts() ->
|
|||
%% empty message
|
||||
serialize_pkt(#coap_message{type = Type, method = undefined, id = MsgId}, _Opts) ->
|
||||
<<?VERSION:2, (encode_type(Type)):2, 0:4, 0:3, 0:5, MsgId:16>>;
|
||||
|
||||
serialize_pkt(#coap_message{ type = Type
|
||||
, method = Method
|
||||
, id = MsgId
|
||||
, token = Token
|
||||
, options = Options
|
||||
, payload = Payload
|
||||
serialize_pkt(
|
||||
#coap_message{
|
||||
type = Type,
|
||||
method = Method,
|
||||
id = MsgId,
|
||||
token = Token,
|
||||
options = Options,
|
||||
payload = Payload
|
||||
},
|
||||
_Opts) ->
|
||||
_Opts
|
||||
) ->
|
||||
TKL = byte_size(Token),
|
||||
{Class, Code} = method_to_class_code(Method),
|
||||
Head = <<?VERSION:2, (encode_type(Type)):2, TKL:4, Class:3, Code:5, MsgId:16, Token:TKL/binary>>,
|
||||
Head =
|
||||
<<?VERSION:2, (encode_type(Type)):2, TKL:4, Class:3, Code:5, MsgId:16, Token:TKL/binary>>,
|
||||
FlatOpts = flatten_options(Options),
|
||||
encode_option_list(FlatOpts, 0, Head, Payload).
|
||||
|
||||
-spec encode_type(message_type()) -> 0 .. 3.
|
||||
-spec encode_type(message_type()) -> 0..3.
|
||||
encode_type(con) -> 0;
|
||||
encode_type(non) -> 1;
|
||||
encode_type(ack) -> 2;
|
||||
|
|
@ -96,23 +102,24 @@ flatten_options(Opts) ->
|
|||
|
||||
flatten_options([{_OptId, undefined} | T], Acc) ->
|
||||
flatten_options(T, Acc);
|
||||
|
||||
flatten_options([{OptId, OptVal} | T], Acc) ->
|
||||
flatten_options(T,
|
||||
flatten_options(
|
||||
T,
|
||||
case is_repeatable_option(OptId) of
|
||||
false ->
|
||||
[encode_option(OptId, OptVal) | Acc];
|
||||
_ ->
|
||||
try_encode_repeatable(OptId, OptVal) ++ Acc
|
||||
end);
|
||||
|
||||
end
|
||||
);
|
||||
flatten_options([], Acc) ->
|
||||
%% sort by option id for calculate the deltas
|
||||
lists:keysort(1, Acc).
|
||||
|
||||
encode_option_list([{OptNum, OptVal} | OptionList], LastNum, Acc, Payload) ->
|
||||
NumDiff = OptNum - LastNum,
|
||||
{Delta, ExtNum} = if
|
||||
{Delta, ExtNum} =
|
||||
if
|
||||
NumDiff >= 269 ->
|
||||
{14, <<(NumDiff - 269):16>>};
|
||||
OptNum - LastNum >= 13 ->
|
||||
|
|
@ -121,7 +128,8 @@ encode_option_list([{OptNum, OptVal} | OptionList], LastNum, Acc, Payload) ->
|
|||
{NumDiff, <<>>}
|
||||
end,
|
||||
Binaryize = byte_size(OptVal),
|
||||
{Len, ExtLen} = if
|
||||
{Len, ExtLen} =
|
||||
if
|
||||
Binaryize >= 269 ->
|
||||
{14, <<(Binaryize - 269):16>>};
|
||||
Binaryize >= 13 ->
|
||||
|
|
@ -131,56 +139,84 @@ encode_option_list([{OptNum, OptVal} | OptionList], LastNum, Acc, Payload) ->
|
|||
end,
|
||||
Acc2 = <<Acc/binary, Delta:4, Len:4, ExtNum/binary, ExtLen/binary, OptVal/binary>>,
|
||||
encode_option_list(OptionList, OptNum, Acc2, Payload);
|
||||
|
||||
encode_option_list([], _LastNum, Acc, <<>>) ->
|
||||
Acc;
|
||||
encode_option_list([], _, Acc, Payload) ->
|
||||
<<Acc/binary, 16#FF, Payload/binary>>.
|
||||
|
||||
try_encode_repeatable(uri_query, Val) when is_map(Val) ->
|
||||
maps:fold(fun(K, V, Acc) ->
|
||||
maps:fold(
|
||||
fun(K, V, Acc) ->
|
||||
[encode_option(uri_query, <<K/binary, $=, V/binary>>) | Acc]
|
||||
end,
|
||||
[], Val);
|
||||
|
||||
[],
|
||||
Val
|
||||
);
|
||||
try_encode_repeatable(K, Val) ->
|
||||
lists:foldr(fun(undefined, Acc) ->
|
||||
lists:foldr(
|
||||
fun
|
||||
(undefined, Acc) ->
|
||||
Acc;
|
||||
(E, Acc) ->
|
||||
[encode_option(K, E) | Acc]
|
||||
end, [], Val).
|
||||
end,
|
||||
[],
|
||||
Val
|
||||
).
|
||||
|
||||
%% RFC 7252
|
||||
encode_option(if_match, OptVal) -> {?OPTION_IF_MATCH, OptVal};
|
||||
encode_option(uri_host, OptVal) -> {?OPTION_URI_HOST, OptVal};
|
||||
encode_option(etag, OptVal) -> {?OPTION_ETAG, OptVal};
|
||||
encode_option(if_none_match, true) -> {?OPTION_IF_NONE_MATCH, <<>>};
|
||||
encode_option(uri_port, OptVal) -> {?OPTION_URI_PORT, binary:encode_unsigned(OptVal)};
|
||||
encode_option(location_path, OptVal) -> {?OPTION_LOCATION_PATH, OptVal};
|
||||
encode_option(uri_path, OptVal) -> {?OPTION_URI_PATH, OptVal};
|
||||
encode_option(if_match, OptVal) ->
|
||||
{?OPTION_IF_MATCH, OptVal};
|
||||
encode_option(uri_host, OptVal) ->
|
||||
{?OPTION_URI_HOST, OptVal};
|
||||
encode_option(etag, OptVal) ->
|
||||
{?OPTION_ETAG, OptVal};
|
||||
encode_option(if_none_match, true) ->
|
||||
{?OPTION_IF_NONE_MATCH, <<>>};
|
||||
encode_option(uri_port, OptVal) ->
|
||||
{?OPTION_URI_PORT, binary:encode_unsigned(OptVal)};
|
||||
encode_option(location_path, OptVal) ->
|
||||
{?OPTION_LOCATION_PATH, OptVal};
|
||||
encode_option(uri_path, OptVal) ->
|
||||
{?OPTION_URI_PATH, OptVal};
|
||||
encode_option(content_format, OptVal) when is_integer(OptVal) ->
|
||||
{?OPTION_CONTENT_FORMAT, binary:encode_unsigned(OptVal)};
|
||||
encode_option(content_format, OptVal) ->
|
||||
Num = content_format_to_code(OptVal),
|
||||
{?OPTION_CONTENT_FORMAT, binary:encode_unsigned(Num)};
|
||||
encode_option(max_age, OptVal) -> {?OPTION_MAX_AGE, binary:encode_unsigned(OptVal)};
|
||||
encode_option(uri_query, OptVal) -> {?OPTION_URI_QUERY, OptVal};
|
||||
encode_option('accept', OptVal) -> {?OPTION_ACCEPT, binary:encode_unsigned(OptVal)};
|
||||
encode_option(location_query, OptVal) -> {?OPTION_LOCATION_QUERY, OptVal};
|
||||
encode_option(proxy_uri, OptVal) -> {?OPTION_PROXY_URI, OptVal};
|
||||
encode_option(proxy_scheme, OptVal) -> {?OPTION_PROXY_SCHEME, OptVal};
|
||||
encode_option(size1, OptVal) -> {?OPTION_SIZE1, binary:encode_unsigned(OptVal)};
|
||||
encode_option(observe, OptVal) -> {?OPTION_OBSERVE, binary:encode_unsigned(OptVal)};
|
||||
encode_option(block2, OptVal) -> {?OPTION_BLOCK2, encode_block(OptVal)};
|
||||
encode_option(block1, OptVal) -> {?OPTION_BLOCK1, encode_block(OptVal)};
|
||||
encode_option(max_age, OptVal) ->
|
||||
{?OPTION_MAX_AGE, binary:encode_unsigned(OptVal)};
|
||||
encode_option(uri_query, OptVal) ->
|
||||
{?OPTION_URI_QUERY, OptVal};
|
||||
encode_option('accept', OptVal) ->
|
||||
{?OPTION_ACCEPT, binary:encode_unsigned(OptVal)};
|
||||
encode_option(location_query, OptVal) ->
|
||||
{?OPTION_LOCATION_QUERY, OptVal};
|
||||
encode_option(proxy_uri, OptVal) ->
|
||||
{?OPTION_PROXY_URI, OptVal};
|
||||
encode_option(proxy_scheme, OptVal) ->
|
||||
{?OPTION_PROXY_SCHEME, OptVal};
|
||||
encode_option(size1, OptVal) ->
|
||||
{?OPTION_SIZE1, binary:encode_unsigned(OptVal)};
|
||||
encode_option(observe, OptVal) ->
|
||||
{?OPTION_OBSERVE, binary:encode_unsigned(OptVal)};
|
||||
encode_option(block2, OptVal) ->
|
||||
{?OPTION_BLOCK2, encode_block(OptVal)};
|
||||
encode_option(block1, OptVal) ->
|
||||
{?OPTION_BLOCK1, encode_block(OptVal)};
|
||||
%% unknown opton
|
||||
encode_option(Option, Value) ->
|
||||
erlang:throw({bad_option, Option, Value}).
|
||||
|
||||
encode_block({Num, More, Size}) ->
|
||||
encode_block1(Num,
|
||||
if More -> 1; true -> 0 end,
|
||||
trunc(math:log2(Size))-4).
|
||||
encode_block1(
|
||||
Num,
|
||||
if
|
||||
More -> 1;
|
||||
true -> 0
|
||||
end,
|
||||
trunc(math:log2(Size)) - 4
|
||||
).
|
||||
|
||||
encode_block1(Num, M, SizEx) when Num < 16 ->
|
||||
<<Num:4, M:1, SizEx:3>>;
|
||||
|
|
@ -192,14 +228,15 @@ encode_block1(Num, M, SizEx) ->
|
|||
-spec content_format_to_code(binary()) -> non_neg_integer().
|
||||
content_format_to_code(<<"text/plain">>) -> 0;
|
||||
content_format_to_code(<<"application/link-format">>) -> 40;
|
||||
content_format_to_code(<<"application/xml">>) ->41;
|
||||
content_format_to_code(<<"application/xml">>) -> 41;
|
||||
content_format_to_code(<<"application/octet-stream">>) -> 42;
|
||||
content_format_to_code(<<"application/exi">>) -> 47;
|
||||
content_format_to_code(<<"application/json">>) -> 50;
|
||||
content_format_to_code(<<"application/cbor">>) -> 60;
|
||||
content_format_to_code(<<"application/vnd.oma.lwm2m+tlv">>) -> 11542;
|
||||
content_format_to_code(<<"application/vnd.oma.lwm2m+json">>) -> 11543;
|
||||
content_format_to_code(_) -> 42. %% use octet-stream as default
|
||||
%% use octet-stream as default
|
||||
content_format_to_code(_) -> 42.
|
||||
|
||||
method_to_class_code(get) -> {0, 01};
|
||||
method_to_class_code(post) -> {0, 02};
|
||||
|
|
@ -229,43 +266,47 @@ method_to_class_code({error, bad_gateway}) -> {5, 02};
|
|||
method_to_class_code({error, service_unavailable}) -> {5, 03};
|
||||
method_to_class_code({error, gateway_timeout}) -> {5, 04};
|
||||
method_to_class_code({error, proxying_not_supported}) -> {5, 05};
|
||||
method_to_class_code(Method) ->
|
||||
erlang:throw({bad_method, Method}).
|
||||
method_to_class_code(Method) -> erlang:throw({bad_method, Method}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% parse
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec parse(binary(), emqx_gateway_frame:parse_state())
|
||||
-> emqx_gateway_frame:parse_result().
|
||||
-spec parse(binary(), emqx_gateway_frame:parse_state()) ->
|
||||
emqx_gateway_frame:parse_result().
|
||||
parse(<<?VERSION:2, Type:2, 0:4, 0:3, 0:5, MsgId:16>>, ParseState) ->
|
||||
{ok,
|
||||
#coap_message{ type = decode_type(Type)
|
||||
, id = MsgId},
|
||||
<<>>,
|
||||
ParseState};
|
||||
|
||||
parse(<<?VERSION:2, Type:2, TKL:4, Class:3, Code:5, MsgId:16, Token:TKL/binary, Tail/binary>>,
|
||||
ParseState) ->
|
||||
#coap_message{
|
||||
type = decode_type(Type),
|
||||
id = MsgId
|
||||
},
|
||||
<<>>, ParseState};
|
||||
parse(
|
||||
<<?VERSION:2, Type:2, TKL:4, Class:3, Code:5, MsgId:16, Token:TKL/binary, Tail/binary>>,
|
||||
ParseState
|
||||
) ->
|
||||
{Options, Payload} = decode_option_list(Tail),
|
||||
Options2 = maps:fold(fun(K, V, Acc) ->
|
||||
Options2 = maps:fold(
|
||||
fun(K, V, Acc) ->
|
||||
Acc#{K => get_option_val(K, V)}
|
||||
end,
|
||||
#{},
|
||||
Options),
|
||||
Options
|
||||
),
|
||||
{ok,
|
||||
#coap_message{ type = decode_type(Type)
|
||||
, method = class_code_to_method({Class, Code})
|
||||
, id = MsgId
|
||||
, token = Token
|
||||
, options = Options2
|
||||
, payload = Payload
|
||||
#coap_message{
|
||||
type = decode_type(Type),
|
||||
method = class_code_to_method({Class, Code}),
|
||||
id = MsgId,
|
||||
token = Token,
|
||||
options = Options2,
|
||||
payload = Payload
|
||||
},
|
||||
<<>>,
|
||||
ParseState}.
|
||||
<<>>, ParseState}.
|
||||
|
||||
get_option_val(uri_query, V) ->
|
||||
KVList = lists:foldl(fun(E, Acc) ->
|
||||
KVList = lists:foldl(
|
||||
fun(E, Acc) ->
|
||||
case re:split(E, "=") of
|
||||
[Key, Val] ->
|
||||
[{Key, Val} | Acc];
|
||||
|
|
@ -274,9 +315,9 @@ get_option_val(uri_query, V) ->
|
|||
end
|
||||
end,
|
||||
[],
|
||||
V),
|
||||
V
|
||||
),
|
||||
maps:from_list(KVList);
|
||||
|
||||
get_option_val(K, V) ->
|
||||
case is_repeatable_option(K) of
|
||||
true ->
|
||||
|
|
@ -285,8 +326,8 @@ get_option_val(K, V) ->
|
|||
V
|
||||
end.
|
||||
|
||||
-spec decode_type(X) -> message_type()
|
||||
when X :: 0 .. 3.
|
||||
-spec decode_type(X) -> message_type() when
|
||||
X :: 0..3.
|
||||
decode_type(0) -> con;
|
||||
decode_type(1) -> non;
|
||||
decode_type(2) -> ack;
|
||||
|
|
@ -298,10 +339,8 @@ decode_option_list(Bin) ->
|
|||
|
||||
decode_option_list(<<>>, _OptNum, OptMap) ->
|
||||
{OptMap, <<>>};
|
||||
|
||||
decode_option_list(<<16#FF, Payload/binary>>, _OptNum, OptMap) ->
|
||||
{OptMap, Payload};
|
||||
|
||||
decode_option_list(<<Delta:4, Len:4, Bin/binary>>, OptNum, OptMap) ->
|
||||
case Delta of
|
||||
Any when Any < 13 ->
|
||||
|
|
@ -349,30 +388,48 @@ append_option(OptNum, RawOptVal, OptMap) ->
|
|||
end.
|
||||
|
||||
%% RFC 7252
|
||||
decode_option(?OPTION_IF_MATCH, OptVal) -> {if_match, OptVal};
|
||||
decode_option(?OPTION_URI_HOST, OptVal) -> {uri_host, OptVal};
|
||||
decode_option(?OPTION_ETAG, OptVal) -> {etag, OptVal};
|
||||
decode_option(?OPTION_IF_NONE_MATCH, <<>>) -> {if_none_match, true};
|
||||
decode_option(?OPTION_URI_PORT, OptVal) -> {uri_port, binary:decode_unsigned(OptVal)};
|
||||
decode_option(?OPTION_LOCATION_PATH, OptVal) -> {location_path, OptVal};
|
||||
decode_option(?OPTION_URI_PATH, OptVal) -> {uri_path, OptVal};
|
||||
decode_option(?OPTION_IF_MATCH, OptVal) ->
|
||||
{if_match, OptVal};
|
||||
decode_option(?OPTION_URI_HOST, OptVal) ->
|
||||
{uri_host, OptVal};
|
||||
decode_option(?OPTION_ETAG, OptVal) ->
|
||||
{etag, OptVal};
|
||||
decode_option(?OPTION_IF_NONE_MATCH, <<>>) ->
|
||||
{if_none_match, true};
|
||||
decode_option(?OPTION_URI_PORT, OptVal) ->
|
||||
{uri_port, binary:decode_unsigned(OptVal)};
|
||||
decode_option(?OPTION_LOCATION_PATH, OptVal) ->
|
||||
{location_path, OptVal};
|
||||
decode_option(?OPTION_URI_PATH, OptVal) ->
|
||||
{uri_path, OptVal};
|
||||
decode_option(?OPTION_CONTENT_FORMAT, OptVal) ->
|
||||
Num = binary:decode_unsigned(OptVal),
|
||||
{content_format, content_code_to_format(Num)};
|
||||
decode_option(?OPTION_MAX_AGE, OptVal) -> {max_age, binary:decode_unsigned(OptVal)};
|
||||
decode_option(?OPTION_URI_QUERY, OptVal) -> {uri_query, OptVal};
|
||||
decode_option(?OPTION_ACCEPT, OptVal) -> {'accept', binary:decode_unsigned(OptVal)};
|
||||
decode_option(?OPTION_LOCATION_QUERY, OptVal) -> {location_query, OptVal};
|
||||
decode_option(?OPTION_PROXY_URI, OptVal) -> {proxy_uri, OptVal};
|
||||
decode_option(?OPTION_PROXY_SCHEME, OptVal) -> {proxy_scheme, OptVal};
|
||||
decode_option(?OPTION_SIZE1, OptVal) -> {size1, binary:decode_unsigned(OptVal)};
|
||||
decode_option(?OPTION_MAX_AGE, OptVal) ->
|
||||
{max_age, binary:decode_unsigned(OptVal)};
|
||||
decode_option(?OPTION_URI_QUERY, OptVal) ->
|
||||
{uri_query, OptVal};
|
||||
decode_option(?OPTION_ACCEPT, OptVal) ->
|
||||
{'accept', binary:decode_unsigned(OptVal)};
|
||||
decode_option(?OPTION_LOCATION_QUERY, OptVal) ->
|
||||
{location_query, OptVal};
|
||||
decode_option(?OPTION_PROXY_URI, OptVal) ->
|
||||
{proxy_uri, OptVal};
|
||||
decode_option(?OPTION_PROXY_SCHEME, OptVal) ->
|
||||
{proxy_scheme, OptVal};
|
||||
decode_option(?OPTION_SIZE1, OptVal) ->
|
||||
{size1, binary:decode_unsigned(OptVal)};
|
||||
%% draft-ietf-core-observe-16
|
||||
decode_option(?OPTION_OBSERVE, OptVal) -> {observe, binary:decode_unsigned(OptVal)};
|
||||
decode_option(?OPTION_OBSERVE, OptVal) ->
|
||||
{observe, binary:decode_unsigned(OptVal)};
|
||||
%% draft-ietf-core-block-17
|
||||
decode_option(?OPTION_BLOCK2, OptVal) -> {block2, decode_block(OptVal)};
|
||||
decode_option(?OPTION_BLOCK1, OptVal) -> {block1, decode_block(OptVal)};
|
||||
decode_option(?OPTION_BLOCK2, OptVal) ->
|
||||
{block2, decode_block(OptVal)};
|
||||
decode_option(?OPTION_BLOCK1, OptVal) ->
|
||||
{block1, decode_block(OptVal)};
|
||||
%% unknown option
|
||||
decode_option(OptNum, OptVal) -> {OptNum, OptVal}.
|
||||
decode_option(OptNum, OptVal) ->
|
||||
{OptNum, OptVal}.
|
||||
|
||||
decode_block(<<Num:4, M:1, SizEx:3>>) -> decode_block1(Num, M, SizEx);
|
||||
decode_block(<<Num:12, M:1, SizEx:3>>) -> decode_block1(Num, M, SizEx);
|
||||
|
|
@ -391,7 +448,8 @@ content_code_to_format(50) -> <<"application/json">>;
|
|||
content_code_to_format(60) -> <<"application/cbor">>;
|
||||
content_code_to_format(11542) -> <<"application/vnd.oma.lwm2m+tlv">>;
|
||||
content_code_to_format(11543) -> <<"application/vnd.oma.lwm2m+json">>;
|
||||
content_code_to_format(_) -> <<"application/octet-stream">>. %% use octet as default
|
||||
%% use octet as default
|
||||
content_code_to_format(_) -> <<"application/octet-stream">>.
|
||||
|
||||
%% RFC 7252
|
||||
%% atom indicate a request
|
||||
|
|
@ -399,7 +457,6 @@ class_code_to_method({0, 01}) -> get;
|
|||
class_code_to_method({0, 02}) -> post;
|
||||
class_code_to_method({0, 03}) -> put;
|
||||
class_code_to_method({0, 04}) -> delete;
|
||||
|
||||
%% success is a tuple {ok, ...}
|
||||
class_code_to_method({2, 01}) -> {ok, created};
|
||||
class_code_to_method({2, 02}) -> {ok, deleted};
|
||||
|
|
@ -407,8 +464,8 @@ class_code_to_method({2, 03}) -> {ok, valid};
|
|||
class_code_to_method({2, 04}) -> {ok, changed};
|
||||
class_code_to_method({2, 05}) -> {ok, content};
|
||||
class_code_to_method({2, 07}) -> {ok, nocontent};
|
||||
class_code_to_method({2, 31}) -> {ok, continue}; % block
|
||||
|
||||
% block
|
||||
class_code_to_method({2, 31}) -> {ok, continue};
|
||||
%% error is a tuple {error, ...}
|
||||
class_code_to_method({4, 00}) -> {error, bad_request};
|
||||
class_code_to_method({4, 01}) -> {error, unauthorized};
|
||||
|
|
@ -417,7 +474,8 @@ class_code_to_method({4, 03}) -> {error, forbidden};
|
|||
class_code_to_method({4, 04}) -> {error, not_found};
|
||||
class_code_to_method({4, 05}) -> {error, method_not_allowed};
|
||||
class_code_to_method({4, 06}) -> {error, not_acceptable};
|
||||
class_code_to_method({4, 08}) -> {error, request_entity_incomplete}; % block
|
||||
% block
|
||||
class_code_to_method({4, 08}) -> {error, request_entity_incomplete};
|
||||
class_code_to_method({4, 12}) -> {error, precondition_failed};
|
||||
class_code_to_method({4, 13}) -> {error, request_entity_too_large};
|
||||
class_code_to_method({4, 15}) -> {error, unsupported_content_format};
|
||||
|
|
|
|||
|
|
@ -21,29 +21,33 @@
|
|||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx_gateway/include/emqx_gateway.hrl").
|
||||
|
||||
-import(emqx_gateway_utils,
|
||||
[ normalize_config/1
|
||||
, start_listeners/4
|
||||
, stop_listeners/2
|
||||
]).
|
||||
-import(
|
||||
emqx_gateway_utils,
|
||||
[
|
||||
normalize_config/1,
|
||||
start_listeners/4,
|
||||
stop_listeners/2
|
||||
]
|
||||
).
|
||||
|
||||
%% APIs
|
||||
-export([ reg/0
|
||||
, unreg/0
|
||||
]).
|
||||
-export([
|
||||
reg/0,
|
||||
unreg/0
|
||||
]).
|
||||
|
||||
-export([ on_gateway_load/2
|
||||
, on_gateway_update/3
|
||||
, on_gateway_unload/2
|
||||
]).
|
||||
-export([
|
||||
on_gateway_load/2,
|
||||
on_gateway_update/3,
|
||||
on_gateway_unload/2
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
reg() ->
|
||||
RegistryOptions = [ {cbkmod, ?MODULE}
|
||||
],
|
||||
RegistryOptions = [{cbkmod, ?MODULE}],
|
||||
emqx_gateway_registry:reg(coap, RegistryOptions).
|
||||
|
||||
unreg() ->
|
||||
|
|
@ -53,22 +57,33 @@ unreg() ->
|
|||
%% emqx_gateway_registry callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
on_gateway_load(_Gateway = #{name := GwName,
|
||||
on_gateway_load(
|
||||
_Gateway = #{
|
||||
name := GwName,
|
||||
config := Config
|
||||
}, Ctx) ->
|
||||
},
|
||||
Ctx
|
||||
) ->
|
||||
Listeners = normalize_config(Config),
|
||||
ModCfg = #{frame_mod => emqx_coap_frame,
|
||||
ModCfg = #{
|
||||
frame_mod => emqx_coap_frame,
|
||||
chann_mod => emqx_coap_channel
|
||||
},
|
||||
case start_listeners(
|
||||
Listeners, GwName, Ctx, ModCfg) of
|
||||
case
|
||||
start_listeners(
|
||||
Listeners, GwName, Ctx, ModCfg
|
||||
)
|
||||
of
|
||||
{ok, ListenerPids} ->
|
||||
{ok, ListenerPids, #{ctx => Ctx}};
|
||||
{error, {Reason, Listener}} ->
|
||||
throw({badconf, #{ key => listeners
|
||||
, vallue => Listener
|
||||
, reason => Reason
|
||||
}})
|
||||
throw(
|
||||
{badconf, #{
|
||||
key => listeners,
|
||||
vallue => Listener,
|
||||
reason => Reason
|
||||
}}
|
||||
)
|
||||
end.
|
||||
|
||||
on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) ->
|
||||
|
|
@ -79,15 +94,21 @@ on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) ->
|
|||
on_gateway_unload(Gateway, GwState),
|
||||
on_gateway_load(Gateway#{config => Config}, Ctx)
|
||||
catch
|
||||
Class : Reason : Stk ->
|
||||
logger:error("Failed to update ~ts; "
|
||||
Class:Reason:Stk ->
|
||||
logger:error(
|
||||
"Failed to update ~ts; "
|
||||
"reason: {~0p, ~0p} stacktrace: ~0p",
|
||||
[GwName, Class, Reason, Stk]),
|
||||
[GwName, Class, Reason, Stk]
|
||||
),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
on_gateway_unload(_Gateway = #{ name := GwName,
|
||||
on_gateway_unload(
|
||||
_Gateway = #{
|
||||
name := GwName,
|
||||
config := Config
|
||||
}, _GwState) ->
|
||||
},
|
||||
_GwState
|
||||
) ->
|
||||
Listeners = normalize_config(Config),
|
||||
stop_listeners(GwName, Listeners).
|
||||
|
|
|
|||
|
|
@ -23,10 +23,15 @@
|
|||
-include("src/coap/include/emqx_coap.hrl").
|
||||
|
||||
%% API
|
||||
-export([ empty/0, reset/1, reset/2
|
||||
, out/1, out/2, proto_out/1
|
||||
, proto_out/2, iter/3, iter/4
|
||||
, reply/2, reply/3, reply/4]).
|
||||
-export([
|
||||
empty/0,
|
||||
reset/1, reset/2,
|
||||
out/1, out/2,
|
||||
proto_out/1,
|
||||
proto_out/2,
|
||||
iter/3, iter/4,
|
||||
reply/2, reply/3, reply/4
|
||||
]).
|
||||
|
||||
%%-type result() :: map() | empty.
|
||||
|
||||
|
|
@ -46,7 +51,6 @@ out(Msg) ->
|
|||
|
||||
out(Msg, #{out := Outs} = Result) ->
|
||||
Result#{out := [Msg | Outs]};
|
||||
|
||||
out(Msg, Result) ->
|
||||
Result#{out => [Msg]}.
|
||||
|
||||
|
|
@ -58,13 +62,11 @@ proto_out(Proto, Result) ->
|
|||
|
||||
reply(Method, Req) when not is_record(Method, coap_message) ->
|
||||
reply(Method, <<>>, Req);
|
||||
|
||||
reply(Reply, Result) ->
|
||||
Result#{reply => Reply}.
|
||||
|
||||
reply(Method, Req, Result) when is_record(Req, coap_message) ->
|
||||
reply(Method, <<>>, Req, Result);
|
||||
|
||||
reply(Method, Payload, Req) ->
|
||||
reply(Method, Payload, Req, #{}).
|
||||
|
||||
|
|
@ -87,7 +89,6 @@ iter([Key, Fun | T], Input, State) ->
|
|||
%% FunH(Val, maps:remove(Key, Input), State, FunT)
|
||||
%% end
|
||||
end;
|
||||
|
||||
%% terminal node
|
||||
iter([Fun], Input, State) ->
|
||||
Fun(undefined, Input, State).
|
||||
|
|
@ -100,7 +101,6 @@ iter([Key, Fun | T], Input, Arg, State) ->
|
|||
Val ->
|
||||
Fun(Val, maps:remove(Key, Input), Arg, State, T)
|
||||
end;
|
||||
|
||||
iter([Fun], Input, Arg, State) ->
|
||||
Fun(undefined, Input, Arg, State).
|
||||
|
||||
|
|
|
|||
|
|
@ -24,15 +24,24 @@
|
|||
%% convenience functions for message construction
|
||||
-module(emqx_coap_message).
|
||||
|
||||
-export([ request/2, request/3, request/4
|
||||
, ack/1, response/1, response/2
|
||||
, reset/1, piggyback/2, piggyback/3
|
||||
, response/3]).
|
||||
-export([
|
||||
request/2, request/3, request/4,
|
||||
ack/1,
|
||||
response/1, response/2,
|
||||
reset/1,
|
||||
piggyback/2, piggyback/3,
|
||||
response/3
|
||||
]).
|
||||
|
||||
-export([is_request/1]).
|
||||
|
||||
-export([ set/3, set_payload/2, get_option/2
|
||||
, get_option/3, set_payload_block/3, set_payload_block/4]).
|
||||
-export([
|
||||
set/3,
|
||||
set_payload/2,
|
||||
get_option/2,
|
||||
get_option/3,
|
||||
set_payload_block/3, set_payload_block/4
|
||||
]).
|
||||
|
||||
-include("src/coap/include/emqx_coap.hrl").
|
||||
|
||||
|
|
@ -43,10 +52,12 @@ request(Type, Method, Payload) ->
|
|||
request(Type, Method, Payload, []).
|
||||
|
||||
request(Type, Method, Payload, Options) when is_binary(Payload) ->
|
||||
#coap_message{type = Type,
|
||||
#coap_message{
|
||||
type = Type,
|
||||
method = Method,
|
||||
payload = Payload,
|
||||
options = to_options(Options)}.
|
||||
options = to_options(Options)
|
||||
}.
|
||||
|
||||
ack(#coap_message{id = Id}) ->
|
||||
#coap_message{type = ack, id = Id}.
|
||||
|
|
@ -61,14 +72,18 @@ response(Request) ->
|
|||
response(Method, Request) ->
|
||||
response(Method, <<>>, Request).
|
||||
|
||||
response(Method, Payload, #coap_message{type = Type,
|
||||
response(Method, Payload, #coap_message{
|
||||
type = Type,
|
||||
id = Id,
|
||||
token = Token}) ->
|
||||
#coap_message{type = Type,
|
||||
token = Token
|
||||
}) ->
|
||||
#coap_message{
|
||||
type = Type,
|
||||
id = Id,
|
||||
token = Token,
|
||||
method = Method,
|
||||
payload = Payload}.
|
||||
payload = Payload
|
||||
}.
|
||||
|
||||
%% make a response which maybe is a piggyback ack
|
||||
piggyback(Method, Request) ->
|
||||
|
|
@ -84,8 +99,8 @@ piggyback(Method, Payload, Request) ->
|
|||
end.
|
||||
|
||||
%% omit option for its default value
|
||||
set(max_age, ?DEFAULT_MAX_AGE, Msg) -> Msg;
|
||||
|
||||
set(max_age, ?DEFAULT_MAX_AGE, Msg) ->
|
||||
Msg;
|
||||
%% set non-default value
|
||||
set(Option, Value, Msg = #coap_message{options = Options}) ->
|
||||
Msg#coap_message{options = Options#{Option => Value}}.
|
||||
|
|
@ -98,13 +113,11 @@ get_option(Option, #coap_message{options = Options}, Def) ->
|
|||
|
||||
set_payload(Payload, Msg) when is_binary(Payload) ->
|
||||
Msg#coap_message{payload = Payload};
|
||||
|
||||
set_payload(Payload, Msg) when is_list(Payload) ->
|
||||
Msg#coap_message{payload = list_to_binary(Payload)}.
|
||||
|
||||
set_payload_block(Content, Block, Msg = #coap_message{method = Method}) when is_atom(Method) ->
|
||||
set_payload_block(Content, block1, Block, Msg);
|
||||
|
||||
set_payload_block(Content, Block, Msg = #coap_message{}) ->
|
||||
set_payload_block(Content, block2, Block, Msg).
|
||||
|
||||
|
|
@ -114,16 +127,21 @@ set_payload_block(Content, BlockId, {Num, _, Size}, Msg) ->
|
|||
OffsetEnd = OffsetBegin + Size,
|
||||
case ContentSize > OffsetEnd of
|
||||
true ->
|
||||
set(BlockId, {Num, true, Size},
|
||||
set_payload(binary:part(Content, OffsetBegin, Size), Msg));
|
||||
set(
|
||||
BlockId,
|
||||
{Num, true, Size},
|
||||
set_payload(binary:part(Content, OffsetBegin, Size), Msg)
|
||||
);
|
||||
_ ->
|
||||
set(BlockId, {Num, false, Size},
|
||||
set_payload(binary:part(Content, OffsetBegin, ContentSize - OffsetBegin), Msg))
|
||||
set(
|
||||
BlockId,
|
||||
{Num, false, Size},
|
||||
set_payload(binary:part(Content, OffsetBegin, ContentSize - OffsetBegin), Msg)
|
||||
)
|
||||
end.
|
||||
|
||||
is_request(#coap_message{method = Method}) when is_atom(Method) ->
|
||||
Method =/= undefined;
|
||||
|
||||
is_request(_) ->
|
||||
false.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,18 +17,25 @@
|
|||
-module(emqx_coap_observe_res).
|
||||
|
||||
%% API
|
||||
-export([ new_manager/0, insert/3, remove/2
|
||||
, res_changed/2, foreach/2, subscriptions/1]).
|
||||
-export([
|
||||
new_manager/0,
|
||||
insert/3,
|
||||
remove/2,
|
||||
res_changed/2,
|
||||
foreach/2,
|
||||
subscriptions/1
|
||||
]).
|
||||
-export_type([manager/0]).
|
||||
|
||||
-define(MAX_SEQ_ID, 16777215).
|
||||
|
||||
-type token() :: binary().
|
||||
-type seq_id() :: 0 .. ?MAX_SEQ_ID.
|
||||
-type seq_id() :: 0..?MAX_SEQ_ID.
|
||||
|
||||
-type res() :: #{ token := token()
|
||||
, seq_id := seq_id()
|
||||
}.
|
||||
-type res() :: #{
|
||||
token := token(),
|
||||
seq_id := seq_id()
|
||||
}.
|
||||
|
||||
-type manager() :: #{emqx_types:topic() => res()}.
|
||||
|
||||
|
|
@ -41,7 +48,8 @@ new_manager() ->
|
|||
|
||||
-spec insert(emqx_types:topic(), token(), manager()) -> {seq_id(), manager()}.
|
||||
insert(Topic, Token, Manager) ->
|
||||
Res = case maps:get(Topic, Manager, undefined) of
|
||||
Res =
|
||||
case maps:get(Topic, Manager, undefined) of
|
||||
undefined ->
|
||||
new_res(Token);
|
||||
Any ->
|
||||
|
|
@ -59,17 +67,21 @@ res_changed(Topic, Manager) ->
|
|||
undefined ->
|
||||
undefined;
|
||||
Res ->
|
||||
#{token := Token,
|
||||
seq_id := SeqId} = Res2 = res_changed(Res),
|
||||
#{
|
||||
token := Token,
|
||||
seq_id := SeqId
|
||||
} = Res2 = res_changed(Res),
|
||||
{Token, SeqId, Manager#{Topic := Res2}}
|
||||
end.
|
||||
|
||||
foreach(F, Manager) ->
|
||||
maps:fold(fun(K, V, _) ->
|
||||
maps:fold(
|
||||
fun(K, V, _) ->
|
||||
F(K, V)
|
||||
end,
|
||||
ok,
|
||||
Manager),
|
||||
Manager
|
||||
),
|
||||
ok.
|
||||
|
||||
-spec subscriptions(manager()) -> [emqx_types:topic()].
|
||||
|
|
@ -81,8 +93,10 @@ subscriptions(Manager) ->
|
|||
%%--------------------------------------------------------------------
|
||||
-spec new_res(token()) -> res().
|
||||
new_res(Token) ->
|
||||
#{token => Token,
|
||||
seq_id => 0}.
|
||||
#{
|
||||
token => Token,
|
||||
seq_id => 0
|
||||
}.
|
||||
|
||||
-spec res_changed(res()) -> res().
|
||||
res_changed(#{seq_id := SeqId} = Res) ->
|
||||
|
|
|
|||
|
|
@ -21,40 +21,47 @@
|
|||
-include("src/coap/include/emqx_coap.hrl").
|
||||
|
||||
%% API
|
||||
-export([ new/0
|
||||
, process_subscribe/4
|
||||
]).
|
||||
-export([
|
||||
new/0,
|
||||
process_subscribe/4
|
||||
]).
|
||||
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
]).
|
||||
-export([
|
||||
info/1,
|
||||
info/2,
|
||||
stats/1
|
||||
]).
|
||||
|
||||
-export([ handle_request/2
|
||||
, handle_response/2
|
||||
, handle_out/2
|
||||
, set_reply/2
|
||||
, deliver/3
|
||||
, timeout/2]).
|
||||
-export([
|
||||
handle_request/2,
|
||||
handle_response/2,
|
||||
handle_out/2,
|
||||
set_reply/2,
|
||||
deliver/3,
|
||||
timeout/2
|
||||
]).
|
||||
|
||||
-export_type([session/0]).
|
||||
|
||||
-record(session, { transport_manager :: emqx_coap_tm:manager()
|
||||
, observe_manager :: emqx_coap_observe_res:manager()
|
||||
, created_at :: pos_integer()
|
||||
}).
|
||||
-record(session, {
|
||||
transport_manager :: emqx_coap_tm:manager(),
|
||||
observe_manager :: emqx_coap_observe_res:manager(),
|
||||
created_at :: pos_integer()
|
||||
}).
|
||||
|
||||
-type session() :: #session{}.
|
||||
|
||||
%% steal from emqx_session
|
||||
-define(INFO_KEYS, [subscriptions,
|
||||
-define(INFO_KEYS, [
|
||||
subscriptions,
|
||||
upgrade_qos,
|
||||
retry_interval,
|
||||
await_rel_timeout,
|
||||
created_at
|
||||
]).
|
||||
]).
|
||||
|
||||
-define(STATS_KEYS, [subscriptions_cnt,
|
||||
-define(STATS_KEYS, [
|
||||
subscriptions_cnt,
|
||||
subscriptions_max,
|
||||
inflight_cnt,
|
||||
inflight_max,
|
||||
|
|
@ -64,7 +71,7 @@
|
|||
next_pkt_id,
|
||||
awaiting_rel_cnt,
|
||||
awaiting_rel_max
|
||||
]).
|
||||
]).
|
||||
|
||||
-import(emqx_coap_medium, [iter/3]).
|
||||
-import(emqx_coap_channel, [metrics_inc/2]).
|
||||
|
|
@ -75,16 +82,18 @@
|
|||
-spec new() -> session().
|
||||
new() ->
|
||||
_ = emqx_misc:rand_seed(),
|
||||
#session{ transport_manager = emqx_coap_tm:new()
|
||||
, observe_manager = emqx_coap_observe_res:new_manager()
|
||||
, created_at = erlang:system_time(millisecond)}.
|
||||
#session{
|
||||
transport_manager = emqx_coap_tm:new(),
|
||||
observe_manager = emqx_coap_observe_res:new_manager(),
|
||||
created_at = erlang:system_time(millisecond)
|
||||
}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Info, Stats
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc Compatible with emqx_session
|
||||
%% do we need use inflight and mqueue in here?
|
||||
-spec(info(session()) -> emqx_types:infos()).
|
||||
-spec info(session()) -> emqx_types:infos().
|
||||
info(Session) ->
|
||||
maps:from_list(info(?INFO_KEYS, Session)).
|
||||
|
||||
|
|
@ -94,7 +103,9 @@ info(subscriptions, #session{observe_manager = OM}) ->
|
|||
Topics = emqx_coap_observe_res:subscriptions(OM),
|
||||
lists:foldl(
|
||||
fun(T, Acc) -> Acc#{T => emqx_gateway_utils:default_subopts()} end,
|
||||
#{}, Topics);
|
||||
#{},
|
||||
Topics
|
||||
);
|
||||
info(subscriptions_cnt, #session{observe_manager = OM}) ->
|
||||
erlang:length(emqx_coap_observe_res:subscriptions(OM));
|
||||
info(subscriptions_max, _) ->
|
||||
|
|
@ -131,16 +142,18 @@ info(created_at, #session{created_at = CreatedAt}) ->
|
|||
CreatedAt.
|
||||
|
||||
%% @doc Get stats of the session.
|
||||
-spec(stats(session()) -> emqx_types:stats()).
|
||||
-spec stats(session()) -> emqx_types:stats().
|
||||
stats(Session) -> info(?STATS_KEYS, Session).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% Process Message
|
||||
%%%-------------------------------------------------------------------
|
||||
handle_request(Msg, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME,
|
||||
call_transport_manager(
|
||||
?FUNCTION_NAME,
|
||||
Msg,
|
||||
Session).
|
||||
Session
|
||||
).
|
||||
|
||||
handle_response(Msg, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME, Msg, Session).
|
||||
|
|
@ -152,8 +165,14 @@ set_reply(Msg, #session{transport_manager = TM} = Session) ->
|
|||
TM2 = emqx_coap_tm:set_reply(Msg, TM),
|
||||
Session#session{transport_manager = TM2}.
|
||||
|
||||
deliver(Delivers, Ctx, #session{observe_manager = OM,
|
||||
transport_manager = TM} = Session) ->
|
||||
deliver(
|
||||
Delivers,
|
||||
Ctx,
|
||||
#session{
|
||||
observe_manager = OM,
|
||||
transport_manager = TM
|
||||
} = Session
|
||||
) ->
|
||||
Fun = fun({_, Topic, Message}, {OutAcc, OMAcc, TMAcc} = Acc) ->
|
||||
case emqx_coap_observe_res:res_changed(Topic, OMAcc) of
|
||||
undefined ->
|
||||
|
|
@ -169,9 +188,13 @@ deliver(Delivers, Ctx, #session{observe_manager = OM,
|
|||
end,
|
||||
{Outs, OM2, TM2} = lists:foldl(Fun, {[], OM, TM}, lists:reverse(Delivers)),
|
||||
|
||||
#{out => lists:reverse(Outs),
|
||||
session => Session#session{observe_manager = OM2,
|
||||
transport_manager = TM2}}.
|
||||
#{
|
||||
out => lists:reverse(Outs),
|
||||
session => Session#session{
|
||||
observe_manager = OM2,
|
||||
transport_manager = TM2
|
||||
}
|
||||
}.
|
||||
|
||||
timeout(Timer, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME, Timer, Session).
|
||||
|
|
@ -179,13 +202,17 @@ timeout(Timer, Session) ->
|
|||
%%%-------------------------------------------------------------------
|
||||
%%% Internal functions
|
||||
%%%-------------------------------------------------------------------
|
||||
call_transport_manager(Fun,
|
||||
call_transport_manager(
|
||||
Fun,
|
||||
Msg,
|
||||
#session{transport_manager = TM} = Session) ->
|
||||
#session{transport_manager = TM} = Session
|
||||
) ->
|
||||
Result = emqx_coap_tm:Fun(Msg, TM),
|
||||
iter([tm, fun process_tm/4, fun process_session/3],
|
||||
iter(
|
||||
[tm, fun process_tm/4, fun process_session/3],
|
||||
Result,
|
||||
Session).
|
||||
Session
|
||||
).
|
||||
|
||||
process_tm(TM, Result, Session, Cursor) ->
|
||||
iter(Cursor, Result, Session#session{transport_manager = TM}).
|
||||
|
|
@ -193,8 +220,12 @@ process_tm(TM, Result, Session, Cursor) ->
|
|||
process_session(_, Result, Session) ->
|
||||
Result#{session => Session}.
|
||||
|
||||
process_subscribe(Sub, Msg, Result,
|
||||
#session{observe_manager = OM} = Session) ->
|
||||
process_subscribe(
|
||||
Sub,
|
||||
Msg,
|
||||
Result,
|
||||
#session{observe_manager = OM} = Session
|
||||
) ->
|
||||
case Sub of
|
||||
undefined ->
|
||||
Result;
|
||||
|
|
@ -202,22 +233,28 @@ process_subscribe(Sub, Msg, Result,
|
|||
{SeqId, OM2} = emqx_coap_observe_res:insert(Topic, Token, OM),
|
||||
Replay = emqx_coap_message:piggyback({ok, content}, Msg),
|
||||
Replay2 = Replay#coap_message{options = #{observe => SeqId}},
|
||||
Result#{reply => Replay2,
|
||||
session => Session#session{observe_manager = OM2}};
|
||||
Result#{
|
||||
reply => Replay2,
|
||||
session => Session#session{observe_manager = OM2}
|
||||
};
|
||||
Topic ->
|
||||
OM2 = emqx_coap_observe_res:remove(Topic, OM),
|
||||
Replay = emqx_coap_message:piggyback({ok, nocontent}, Msg),
|
||||
Result#{reply => Replay,
|
||||
session => Session#session{observe_manager = OM2}}
|
||||
Result#{
|
||||
reply => Replay,
|
||||
session => Session#session{observe_manager = OM2}
|
||||
}
|
||||
end.
|
||||
|
||||
mqtt_to_coap(MQTT, Token, SeqId) ->
|
||||
#message{payload = Payload} = MQTT,
|
||||
#coap_message{type = get_notify_type(MQTT),
|
||||
#coap_message{
|
||||
type = get_notify_type(MQTT),
|
||||
method = {ok, content},
|
||||
token = Token,
|
||||
payload = Payload,
|
||||
options = #{observe => SeqId}}.
|
||||
options = #{observe => SeqId}
|
||||
}.
|
||||
|
||||
get_notify_type(#message{qos = Qos}) ->
|
||||
case emqx_conf:get([gateway, coap, notify_qos], non) of
|
||||
|
|
|
|||
|
|
@ -17,13 +17,15 @@
|
|||
%% the transport state machine manager
|
||||
-module(emqx_coap_tm).
|
||||
|
||||
-export([ new/0
|
||||
, handle_request/2
|
||||
, handle_response/2
|
||||
, handle_out/2
|
||||
, handle_out/3
|
||||
, set_reply/2
|
||||
, timeout/2]).
|
||||
-export([
|
||||
new/0,
|
||||
handle_request/2,
|
||||
handle_response/2,
|
||||
handle_out/2,
|
||||
handle_out/3,
|
||||
set_reply/2,
|
||||
timeout/2
|
||||
]).
|
||||
|
||||
-export_type([manager/0, event_result/1]).
|
||||
|
||||
|
|
@ -32,29 +34,33 @@
|
|||
|
||||
-type direction() :: in | out.
|
||||
|
||||
-record(state_machine, { seq_id :: seq_id()
|
||||
, id :: state_machine_key()
|
||||
, token :: token() | undefined
|
||||
, observe :: 0 | 1 | undefined | observed
|
||||
, state :: atom()
|
||||
, timers :: maps:map()
|
||||
, transport :: emqx_coap_transport:transport()}).
|
||||
-record(state_machine, {
|
||||
seq_id :: seq_id(),
|
||||
id :: state_machine_key(),
|
||||
token :: token() | undefined,
|
||||
observe :: 0 | 1 | undefined | observed,
|
||||
state :: atom(),
|
||||
timers :: maps:map(),
|
||||
transport :: emqx_coap_transport:transport()
|
||||
}).
|
||||
-type state_machine() :: #state_machine{}.
|
||||
|
||||
-type message_id() :: 0 .. ?MAX_MESSAGE_ID.
|
||||
-type message_id() :: 0..?MAX_MESSAGE_ID.
|
||||
-type token_key() :: {token, token()}.
|
||||
-type state_machine_key() :: {direction(), message_id()}.
|
||||
-type seq_id() :: pos_integer().
|
||||
-type manager_key() :: token_key() | state_machine_key() | seq_id().
|
||||
|
||||
-type manager() :: #{ seq_id => seq_id()
|
||||
, next_msg_id => coap_message_id()
|
||||
, token_key() => seq_id()
|
||||
, state_machine_key() => seq_id()
|
||||
, seq_id() => state_machine()
|
||||
}.
|
||||
-type manager() :: #{
|
||||
seq_id => seq_id(),
|
||||
next_msg_id => coap_message_id(),
|
||||
token_key() => seq_id(),
|
||||
state_machine_key() => seq_id(),
|
||||
seq_id() => state_machine()
|
||||
}.
|
||||
|
||||
-type ttimeout() :: {state_timeout, pos_integer(), any()}
|
||||
-type ttimeout() ::
|
||||
{state_timeout, pos_integer(), any()}
|
||||
| {stop_timeout, pos_integer()}.
|
||||
|
||||
-type topic() :: binary().
|
||||
|
|
@ -62,11 +68,13 @@
|
|||
-type sub_register() :: {topic(), token()} | topic().
|
||||
|
||||
-type event_result(State) ::
|
||||
#{next => State,
|
||||
#{
|
||||
next => State,
|
||||
outgoing => coap_message(),
|
||||
timeouts => list(ttimeout()),
|
||||
has_sub => undefined | sub_register(),
|
||||
transport => emqx_coap_transport:transport()}.
|
||||
transport => emqx_coap_transport:transport()
|
||||
}.
|
||||
|
||||
-define(TOKEN_ID(T), {token, T}).
|
||||
|
||||
|
|
@ -78,8 +86,9 @@
|
|||
|
||||
-spec new() -> manager().
|
||||
new() ->
|
||||
#{ seq_id => 1
|
||||
, next_msg_id => rand:uniform(?MAX_MESSAGE_ID)
|
||||
#{
|
||||
seq_id => 1,
|
||||
next_msg_id => rand:uniform(?MAX_MESSAGE_ID)
|
||||
}.
|
||||
|
||||
handle_request(#coap_message{id = MsgId} = Msg, TM) ->
|
||||
|
|
@ -111,7 +120,6 @@ handle_response(#coap_message{type = Type, id = MsgId, token = Token} = Msg, TM)
|
|||
%% send to a client, msg can be request/piggyback/separate/notify
|
||||
handle_out({Ctx, Msg}, TM) ->
|
||||
handle_out(Msg, Ctx, TM);
|
||||
|
||||
handle_out(Msg, TM) ->
|
||||
handle_out(Msg, undefined, TM).
|
||||
|
||||
|
|
@ -135,8 +143,10 @@ set_reply(#coap_message{id = MsgId} = Msg, TM) ->
|
|||
case find_machine(Id, TM) of
|
||||
undefined ->
|
||||
TM;
|
||||
#state_machine{transport = Transport,
|
||||
seq_id = SeqId} = Machine ->
|
||||
#state_machine{
|
||||
transport = Transport,
|
||||
seq_id = SeqId
|
||||
} = Machine ->
|
||||
Transport2 = emqx_coap_transport:set_cache(Msg, Transport),
|
||||
Machine2 = Machine#state_machine{transport = Transport2},
|
||||
TM#{SeqId => Machine2}
|
||||
|
|
@ -161,35 +171,52 @@ timeout({SeqId, Type, Msg}, TM) ->
|
|||
%%--------------------------------------------------------------------
|
||||
process_event(stop_timeout, _, TM, Machine) ->
|
||||
process_manager(stop, #{}, Machine, TM);
|
||||
|
||||
process_event(Event, Msg, TM, #state_machine{state = State,
|
||||
transport = Transport} = Machine) ->
|
||||
process_event(
|
||||
Event,
|
||||
Msg,
|
||||
TM,
|
||||
#state_machine{
|
||||
state = State,
|
||||
transport = Transport
|
||||
} = Machine
|
||||
) ->
|
||||
Result = emqx_coap_transport:State(Event, Msg, Transport),
|
||||
iter([ proto, fun process_observe_response/5
|
||||
, next, fun process_state_change/5
|
||||
, transport, fun process_transport_change/5
|
||||
, timeouts, fun process_timeouts/5
|
||||
, fun process_manager/4],
|
||||
iter(
|
||||
[
|
||||
proto,
|
||||
fun process_observe_response/5,
|
||||
next,
|
||||
fun process_state_change/5,
|
||||
transport,
|
||||
fun process_transport_change/5,
|
||||
timeouts,
|
||||
fun process_timeouts/5,
|
||||
fun process_manager/4
|
||||
],
|
||||
Result,
|
||||
Machine,
|
||||
TM).
|
||||
TM
|
||||
).
|
||||
|
||||
process_observe_response({response, {_, Msg}} = Response,
|
||||
process_observe_response(
|
||||
{response, {_, Msg}} = Response,
|
||||
Result,
|
||||
#state_machine{observe = 0} = Machine,
|
||||
TM,
|
||||
Iter) ->
|
||||
Iter
|
||||
) ->
|
||||
Result2 = proto_out(Response, Result),
|
||||
case Msg#coap_message.method of
|
||||
{ok, _} ->
|
||||
iter(Iter,
|
||||
iter(
|
||||
Iter,
|
||||
Result2#{next => observe},
|
||||
Machine#state_machine{observe = observed},
|
||||
TM);
|
||||
TM
|
||||
);
|
||||
_ ->
|
||||
iter(Iter, Result2, Machine, TM)
|
||||
end;
|
||||
|
||||
process_observe_response(Proto, Result, Machine, TM, Iter) ->
|
||||
iter(Iter, proto_out(Proto, Result), Machine, TM).
|
||||
|
||||
|
|
@ -198,10 +225,12 @@ process_state_change(Next, Result, Machine, TM, Iter) ->
|
|||
stop ->
|
||||
process_manager(stop, Result, Machine, TM);
|
||||
_ ->
|
||||
iter(Iter,
|
||||
iter(
|
||||
Iter,
|
||||
Result,
|
||||
cancel_state_timer(Machine#state_machine{state = Next}),
|
||||
TM)
|
||||
TM
|
||||
)
|
||||
end.
|
||||
|
||||
process_transport_change(Transport, Result, Machine, TM, Iter) ->
|
||||
|
|
@ -209,22 +238,30 @@ process_transport_change(Transport, Result, Machine, TM, Iter) ->
|
|||
|
||||
process_timeouts([], Result, Machine, TM, Iter) ->
|
||||
iter(Iter, Result, Machine, TM);
|
||||
|
||||
process_timeouts(Timeouts, Result,
|
||||
#state_machine{seq_id = SeqId,
|
||||
timers = Timers} = Machine, TM, Iter) ->
|
||||
NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) ->
|
||||
process_timeouts(
|
||||
Timeouts,
|
||||
Result,
|
||||
#state_machine{
|
||||
seq_id = SeqId,
|
||||
timers = Timers
|
||||
} = Machine,
|
||||
TM,
|
||||
Iter
|
||||
) ->
|
||||
NewTimers = lists:foldl(
|
||||
fun
|
||||
({state_timeout, _, _} = Timer, Acc) ->
|
||||
process_timer(SeqId, Timer, Acc);
|
||||
({stop_timeout, I}, Acc) ->
|
||||
process_timer(SeqId, {stop_timeout, I, stop}, Acc)
|
||||
end,
|
||||
Timers,
|
||||
Timeouts),
|
||||
Timeouts
|
||||
),
|
||||
iter(Iter, Result, Machine#state_machine{timers = NewTimers}, TM).
|
||||
|
||||
process_manager(stop, Result, #state_machine{seq_id = SeqId}, TM) ->
|
||||
Result#{tm => delete_machine(SeqId, TM)};
|
||||
|
||||
process_manager(_, Result, #state_machine{seq_id = SeqId} = Machine2, TM) ->
|
||||
Result#{tm => TM#{SeqId => Machine2}}.
|
||||
|
||||
|
|
@ -246,14 +283,18 @@ delete_machine(Id, Manager) ->
|
|||
case find_machine(Id, Manager) of
|
||||
undefined ->
|
||||
Manager;
|
||||
#state_machine{seq_id = SeqId,
|
||||
#state_machine{
|
||||
seq_id = SeqId,
|
||||
id = MachineId,
|
||||
token = Token,
|
||||
timers = Timers} ->
|
||||
lists:foreach(fun({_, Ref}) ->
|
||||
timers = Timers
|
||||
} ->
|
||||
lists:foreach(
|
||||
fun({_, Ref}) ->
|
||||
emqx_misc:cancel_timer(Ref)
|
||||
end,
|
||||
maps:to_list(Timers)),
|
||||
maps:to_list(Timers)
|
||||
),
|
||||
maps:without([SeqId, MachineId, ?TOKEN_ID(Token)], Manager)
|
||||
end.
|
||||
|
||||
|
|
@ -268,8 +309,10 @@ find_machine(SeqId, Manager) ->
|
|||
find_machine_by_seqid(SeqId, Manager) ->
|
||||
maps:get(SeqId, Manager, undefined).
|
||||
|
||||
-spec find_machine_by_keys(list(manager_key()),
|
||||
manager()) -> state_machine() | undefined.
|
||||
-spec find_machine_by_keys(
|
||||
list(manager_key()),
|
||||
manager()
|
||||
) -> state_machine() | undefined.
|
||||
find_machine_by_keys([H | T], Manager) ->
|
||||
case H of
|
||||
?TOKEN_ID(<<>>) ->
|
||||
|
|
@ -288,35 +331,46 @@ find_machine_by_keys(_, _) ->
|
|||
-spec new_in_machine(state_machine_key(), manager()) ->
|
||||
{state_machine(), manager()}.
|
||||
new_in_machine(MachineId, #{seq_id := SeqId} = Manager) ->
|
||||
Machine = #state_machine{ seq_id = SeqId
|
||||
, id = MachineId
|
||||
, state = idle
|
||||
, timers = #{}
|
||||
, transport = emqx_coap_transport:new()},
|
||||
{Machine, Manager#{seq_id := SeqId + 1,
|
||||
Machine = #state_machine{
|
||||
seq_id = SeqId,
|
||||
id = MachineId,
|
||||
state = idle,
|
||||
timers = #{},
|
||||
transport = emqx_coap_transport:new()
|
||||
},
|
||||
{Machine, Manager#{
|
||||
seq_id := SeqId + 1,
|
||||
SeqId => Machine,
|
||||
MachineId => SeqId}}.
|
||||
MachineId => SeqId
|
||||
}}.
|
||||
|
||||
-spec new_out_machine(state_machine_key(), any(), coap_message(), manager()) ->
|
||||
{state_machine(), manager()}.
|
||||
new_out_machine(MachineId,
|
||||
new_out_machine(
|
||||
MachineId,
|
||||
Ctx,
|
||||
#coap_message{type = Type, token = Token, options = Opts},
|
||||
#{seq_id := SeqId} = Manager) ->
|
||||
#{seq_id := SeqId} = Manager
|
||||
) ->
|
||||
Observe = maps:get(observe, Opts, undefined),
|
||||
Machine = #state_machine{ seq_id = SeqId
|
||||
, id = MachineId
|
||||
, token = Token
|
||||
, observe = Observe
|
||||
, state = idle
|
||||
, timers = #{}
|
||||
, transport = emqx_coap_transport:new(Ctx)},
|
||||
Machine = #state_machine{
|
||||
seq_id = SeqId,
|
||||
id = MachineId,
|
||||
token = Token,
|
||||
observe = Observe,
|
||||
state = idle,
|
||||
timers = #{},
|
||||
transport = emqx_coap_transport:new(Ctx)
|
||||
},
|
||||
|
||||
Manager2 = Manager#{seq_id := SeqId + 1,
|
||||
Manager2 = Manager#{
|
||||
seq_id := SeqId + 1,
|
||||
SeqId => Machine,
|
||||
MachineId => SeqId},
|
||||
MachineId => SeqId
|
||||
},
|
||||
{Machine,
|
||||
if Token =:= <<>> ->
|
||||
if
|
||||
Token =:= <<>> ->
|
||||
Manager2;
|
||||
Observe =:= 1 ->
|
||||
TokenId = ?TOKEN_ID(Token),
|
||||
|
|
@ -331,8 +385,7 @@ new_out_machine(MachineId,
|
|||
end;
|
||||
true ->
|
||||
Manager2
|
||||
end
|
||||
}.
|
||||
end}.
|
||||
|
||||
alloc_message_id(#{next_msg_id := MsgId} = TM) ->
|
||||
alloc_message_id(MsgId, TM).
|
||||
|
|
@ -348,7 +401,8 @@ alloc_message_id(MsgId, TM) ->
|
|||
|
||||
next_message_id(MsgId) ->
|
||||
Next = MsgId + 1,
|
||||
if Next >= ?MAX_MESSAGE_ID ->
|
||||
if
|
||||
Next >= ?MAX_MESSAGE_ID ->
|
||||
1;
|
||||
true ->
|
||||
Next
|
||||
|
|
|
|||
|
|
@ -11,71 +11,102 @@
|
|||
|
||||
-type request_context() :: any().
|
||||
|
||||
-record(transport, { cache :: undefined | coap_message()
|
||||
, req_context :: request_context()
|
||||
, retry_interval :: non_neg_integer()
|
||||
, retry_count :: non_neg_integer()
|
||||
, observe :: non_neg_integer() | undefined
|
||||
}).
|
||||
-record(transport, {
|
||||
cache :: undefined | coap_message(),
|
||||
req_context :: request_context(),
|
||||
retry_interval :: non_neg_integer(),
|
||||
retry_count :: non_neg_integer(),
|
||||
observe :: non_neg_integer() | undefined
|
||||
}).
|
||||
|
||||
-type transport() :: #transport{}.
|
||||
|
||||
-export([ new/0, new/1, idle/3, maybe_reset/3, set_cache/2
|
||||
, maybe_resend_4request/3, wait_ack/3, until_stop/3
|
||||
, observe/3, maybe_resend_4response/3]).
|
||||
-export([
|
||||
new/0, new/1,
|
||||
idle/3,
|
||||
maybe_reset/3,
|
||||
set_cache/2,
|
||||
maybe_resend_4request/3,
|
||||
wait_ack/3,
|
||||
until_stop/3,
|
||||
observe/3,
|
||||
maybe_resend_4response/3
|
||||
]).
|
||||
|
||||
-export_type([transport/0]).
|
||||
|
||||
-import(emqx_coap_medium, [ empty/0, reset/2, proto_out/2
|
||||
, out/1, out/2, proto_out/1
|
||||
, reply/2]).
|
||||
-import(emqx_coap_medium, [
|
||||
empty/0,
|
||||
reset/2,
|
||||
proto_out/2,
|
||||
out/1, out/2,
|
||||
proto_out/1,
|
||||
reply/2
|
||||
]).
|
||||
|
||||
-spec new() -> transport().
|
||||
new() ->
|
||||
new(undefined).
|
||||
|
||||
new(ReqCtx) ->
|
||||
#transport{cache = undefined,
|
||||
#transport{
|
||||
cache = undefined,
|
||||
retry_interval = 0,
|
||||
retry_count = 0,
|
||||
req_context = ReqCtx}.
|
||||
req_context = ReqCtx
|
||||
}.
|
||||
|
||||
idle(in,
|
||||
idle(
|
||||
in,
|
||||
#coap_message{type = non, method = Method} = Msg,
|
||||
_) ->
|
||||
_
|
||||
) ->
|
||||
case Method of
|
||||
undefined ->
|
||||
reset(Msg, #{next => stop});
|
||||
_ ->
|
||||
proto_out({request, Msg},
|
||||
#{next => until_stop,
|
||||
proto_out(
|
||||
{request, Msg},
|
||||
#{
|
||||
next => until_stop,
|
||||
timeouts =>
|
||||
[{stop_timeout, ?NON_LIFETIME}]})
|
||||
[{stop_timeout, ?NON_LIFETIME}]
|
||||
}
|
||||
)
|
||||
end;
|
||||
|
||||
idle(in,
|
||||
idle(
|
||||
in,
|
||||
#coap_message{type = con, method = Method} = Msg,
|
||||
_) ->
|
||||
_
|
||||
) ->
|
||||
case Method of
|
||||
undefined ->
|
||||
reset(Msg, #{next => stop});
|
||||
_ ->
|
||||
proto_out({request, Msg},
|
||||
#{next => maybe_resend_4request,
|
||||
timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]})
|
||||
proto_out(
|
||||
{request, Msg},
|
||||
#{
|
||||
next => maybe_resend_4request,
|
||||
timeouts => [{stop_timeout, ?EXCHANGE_LIFETIME}]
|
||||
}
|
||||
)
|
||||
end;
|
||||
|
||||
idle(out, #coap_message{type = non} = Msg, _) ->
|
||||
out(Msg, #{next => maybe_reset,
|
||||
timeouts => [{stop_timeout, ?NON_LIFETIME}]});
|
||||
|
||||
out(Msg, #{
|
||||
next => maybe_reset,
|
||||
timeouts => [{stop_timeout, ?NON_LIFETIME}]
|
||||
});
|
||||
idle(out, Msg, Transport) ->
|
||||
_ = emqx_misc:rand_seed(),
|
||||
Timeout = ?ACK_TIMEOUT + rand:uniform(?ACK_RANDOM_FACTOR),
|
||||
out(Msg, #{next => wait_ack,
|
||||
out(Msg, #{
|
||||
next => wait_ack,
|
||||
transport => Transport#transport{cache = Msg},
|
||||
timeouts => [ {state_timeout, Timeout, ack_timeout}
|
||||
, {stop_timeout, ?EXCHANGE_LIFETIME}]}).
|
||||
timeouts => [
|
||||
{state_timeout, Timeout, ack_timeout},
|
||||
{stop_timeout, ?EXCHANGE_LIFETIME}
|
||||
]
|
||||
}).
|
||||
|
||||
maybe_resend_4request(in, Msg, Transport) ->
|
||||
maybe_resend(Msg, true, Transport).
|
||||
|
|
@ -98,18 +129,25 @@ maybe_resend(Msg, IsExpecteReq, #transport{cache = Cache}) ->
|
|||
reset(Msg, #{next => stop})
|
||||
end.
|
||||
|
||||
maybe_reset(in, #coap_message{type = Type, method = Method} = Message,
|
||||
#transport{req_context = Ctx} = Transport) ->
|
||||
maybe_reset(
|
||||
in,
|
||||
#coap_message{type = Type, method = Method} = Message,
|
||||
#transport{req_context = Ctx} = Transport
|
||||
) ->
|
||||
Ret = #{next => stop},
|
||||
CtxMsg = {Ctx, Message},
|
||||
if Type =:= reset ->
|
||||
if
|
||||
Type =:= reset ->
|
||||
proto_out({reset, CtxMsg}, Ret);
|
||||
is_tuple(Method) ->
|
||||
on_response(Message,
|
||||
on_response(
|
||||
Message,
|
||||
Transport,
|
||||
if Type =:= non -> until_stop;
|
||||
if
|
||||
Type =:= non -> until_stop;
|
||||
true -> maybe_resend_4response
|
||||
end);
|
||||
end
|
||||
);
|
||||
true ->
|
||||
reset(Message, Ret)
|
||||
end.
|
||||
|
|
@ -131,26 +169,37 @@ wait_ack(in, #coap_message{type = Type, method = Method} = Msg, #transport{req_c
|
|||
reset(Msg, #{next => stop})
|
||||
end
|
||||
end;
|
||||
|
||||
wait_ack(state_timeout,
|
||||
wait_ack(
|
||||
state_timeout,
|
||||
ack_timeout,
|
||||
#transport{cache = Msg,
|
||||
#transport{
|
||||
cache = Msg,
|
||||
retry_interval = Timeout,
|
||||
retry_count = Count} =Transport) ->
|
||||
retry_count = Count
|
||||
} = Transport
|
||||
) ->
|
||||
case Count < ?MAX_RETRANSMIT of
|
||||
true ->
|
||||
Timeout2 = Timeout * 2,
|
||||
out(Msg,
|
||||
#{transport => Transport#transport{retry_interval = Timeout2,
|
||||
retry_count = Count + 1},
|
||||
timeouts => [{state_timeout, Timeout2, ack_timeout}]});
|
||||
out(
|
||||
Msg,
|
||||
#{
|
||||
transport => Transport#transport{
|
||||
retry_interval = Timeout2,
|
||||
retry_count = Count + 1
|
||||
},
|
||||
timeouts => [{state_timeout, Timeout2, ack_timeout}]
|
||||
}
|
||||
);
|
||||
_ ->
|
||||
proto_out({ack_failure, Msg}, #{next_state => stop})
|
||||
end.
|
||||
|
||||
observe(in,
|
||||
observe(
|
||||
in,
|
||||
#coap_message{method = Method} = Message,
|
||||
#transport{observe = Observe} = Transport) ->
|
||||
#transport{observe = Observe} = Transport
|
||||
) ->
|
||||
case Method of
|
||||
{ok, _} ->
|
||||
case emqx_coap_message:get_option(observe, Message, Observe) of
|
||||
|
|
@ -158,9 +207,11 @@ observe(in,
|
|||
%% repeatd notify, ignore
|
||||
empty();
|
||||
NewObserve ->
|
||||
on_response(Message,
|
||||
on_response(
|
||||
Message,
|
||||
Transport#transport{observe = NewObserve},
|
||||
?FUNCTION_NAME)
|
||||
?FUNCTION_NAME
|
||||
)
|
||||
end;
|
||||
{error, _} ->
|
||||
#{next => stop};
|
||||
|
|
@ -174,17 +225,24 @@ until_stop(_, _, _) ->
|
|||
set_cache(Cache, Transport) ->
|
||||
Transport#transport{cache = Cache}.
|
||||
|
||||
on_response(#coap_message{type = Type} = Message,
|
||||
on_response(
|
||||
#coap_message{type = Type} = Message,
|
||||
#transport{req_context = Ctx} = Transport,
|
||||
NextState) ->
|
||||
NextState
|
||||
) ->
|
||||
CtxMsg = {Ctx, Message},
|
||||
if Type =:= non ->
|
||||
if
|
||||
Type =:= non ->
|
||||
proto_out({response, CtxMsg}, #{next => NextState});
|
||||
Type =:= con ->
|
||||
Ack = emqx_coap_message:ack(Message),
|
||||
proto_out({response, CtxMsg},
|
||||
out(Ack, #{next => NextState,
|
||||
transport => Transport#transport{cache = Ack}}));
|
||||
proto_out(
|
||||
{response, CtxMsg},
|
||||
out(Ack, #{
|
||||
next => NextState,
|
||||
transport => Transport#transport{cache = Ack}
|
||||
})
|
||||
);
|
||||
true ->
|
||||
emqx_coap_message:reset(Message)
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -24,18 +24,14 @@
|
|||
|
||||
handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _Ctx, _CInfo) ->
|
||||
handle_method(Method, Msg);
|
||||
|
||||
handle_request(_, Msg, _, _) ->
|
||||
reply({error, bad_request}, Msg).
|
||||
|
||||
handle_method(put, Msg) ->
|
||||
reply({ok, changed}, Msg);
|
||||
|
||||
handle_method(post, Msg) ->
|
||||
#{connection => {open, Msg}};
|
||||
|
||||
handle_method(delete, Msg) ->
|
||||
#{connection => {close, Msg}};
|
||||
|
||||
handle_method(_, Msg) ->
|
||||
reply({error, method_not_allowed}, Msg).
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ handle_method(get, Topic, Msg, Ctx, CInfo) ->
|
|||
_ ->
|
||||
reply({error, bad_request}, <<"invalid observe value">>, Msg)
|
||||
end;
|
||||
|
||||
handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) ->
|
||||
case emqx_coap_channel:validator(publish, Topic, Ctx, CInfo) of
|
||||
allow ->
|
||||
|
|
@ -64,22 +63,23 @@ handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) -
|
|||
_ ->
|
||||
reply({error, unauthorized}, Msg)
|
||||
end;
|
||||
|
||||
handle_method(_, _, Msg, _, _) ->
|
||||
reply({error, method_not_allowed}, Msg).
|
||||
|
||||
check_topic([]) ->
|
||||
error;
|
||||
|
||||
check_topic(Path) ->
|
||||
Sep = <<"/">>,
|
||||
{ok,
|
||||
emqx_http_lib:uri_decode(
|
||||
lists:foldl(fun(Part, Acc) ->
|
||||
lists:foldl(
|
||||
fun(Part, Acc) ->
|
||||
<<Acc/binary, Sep/binary, Part/binary>>
|
||||
end,
|
||||
<<>>,
|
||||
Path))}.
|
||||
Path
|
||||
)
|
||||
)}.
|
||||
|
||||
get_sub_opts(#coap_message{options = Opts} = Msg) ->
|
||||
SubOpts = maps:fold(fun parse_sub_opts/3, #{}, Opts),
|
||||
|
|
@ -100,9 +100,12 @@ parse_sub_opts(<<"rh">>, V, Opts) ->
|
|||
parse_sub_opts(_, _, Opts) ->
|
||||
Opts.
|
||||
|
||||
type_to_qos(qos0, _) -> ?QOS_0;
|
||||
type_to_qos(qos1, _) -> ?QOS_1;
|
||||
type_to_qos(qos2, _) -> ?QOS_2;
|
||||
type_to_qos(qos0, _) ->
|
||||
?QOS_0;
|
||||
type_to_qos(qos1, _) ->
|
||||
?QOS_1;
|
||||
type_to_qos(qos2, _) ->
|
||||
?QOS_2;
|
||||
type_to_qos(coap, #coap_message{type = Type}) ->
|
||||
case Type of
|
||||
non ->
|
||||
|
|
@ -121,24 +124,28 @@ get_publish_qos(Msg) ->
|
|||
end.
|
||||
|
||||
apply_publish_opts(Msg, MQTTMsg) ->
|
||||
maps:fold(fun(<<"retain">>, V, Acc) ->
|
||||
maps:fold(
|
||||
fun
|
||||
(<<"retain">>, V, Acc) ->
|
||||
Val = erlang:binary_to_atom(V),
|
||||
emqx_message:set_flag(retain, Val, Acc);
|
||||
(<<"expiry">>, V, Acc) ->
|
||||
Val = erlang:binary_to_integer(V),
|
||||
Props = emqx_message:get_header(properties, Acc),
|
||||
emqx_message:set_header(properties,
|
||||
emqx_message:set_header(
|
||||
properties,
|
||||
Props#{'Message-Expiry-Interval' => Val},
|
||||
Acc);
|
||||
Acc
|
||||
);
|
||||
(_, _, Acc) ->
|
||||
Acc
|
||||
end,
|
||||
MQTTMsg,
|
||||
emqx_coap_message:get_option(uri_query, Msg)).
|
||||
emqx_coap_message:get_option(uri_query, Msg)
|
||||
).
|
||||
|
||||
subscribe(#coap_message{token = <<>>} = Msg, _, _, _) ->
|
||||
reply({error, bad_request}, <<"observe without token">>, Msg);
|
||||
|
||||
subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) ->
|
||||
case emqx_coap_channel:validator(subscribe, Topic, Ctx, CInfo) of
|
||||
allow ->
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@
|
|||
-define(DEFAULT_MAX_AGE, 60).
|
||||
-define(MAXIMUM_MAX_AGE, 4294967295).
|
||||
|
||||
-type coap_message_id() :: 1 .. ?MAX_MESSAGE_ID.
|
||||
-type coap_message_id() :: 1..?MAX_MESSAGE_ID.
|
||||
-type message_type() :: con | non | ack | reset.
|
||||
-type max_age() :: 1 .. ?MAXIMUM_MAX_AGE.
|
||||
-type max_age() :: 1..?MAXIMUM_MAX_AGE.
|
||||
|
||||
-type message_option_name() :: if_match
|
||||
-type message_option_name() ::
|
||||
if_match
|
||||
| uri_host
|
||||
| etag
|
||||
| if_none_match
|
||||
|
|
@ -45,32 +46,36 @@
|
|||
| block1
|
||||
| block2.
|
||||
|
||||
-type message_options() :: #{ if_match => list(binary())
|
||||
, uri_host => binary()
|
||||
, etag => list(binary())
|
||||
, if_none_match => boolean()
|
||||
, uri_port => 0 .. 65535
|
||||
, location_path => list(binary())
|
||||
, uri_path => list(binary())
|
||||
, content_format => 0 .. 65535
|
||||
, max_age => non_neg_integer()
|
||||
, uri_query => list(binary()) | map()
|
||||
, 'accept' => 0 .. 65535
|
||||
, location_query => list(binary())
|
||||
, proxy_uri => binary()
|
||||
, proxy_scheme => binary()
|
||||
, size1 => non_neg_integer()
|
||||
, observer => non_neg_integer()
|
||||
, block1 => {non_neg_integer(), boolean(), non_neg_integer()}
|
||||
, block2 => {non_neg_integer(), boolean(), non_neg_integer()}}.
|
||||
-type message_options() :: #{
|
||||
if_match => list(binary()),
|
||||
uri_host => binary(),
|
||||
etag => list(binary()),
|
||||
if_none_match => boolean(),
|
||||
uri_port => 0..65535,
|
||||
location_path => list(binary()),
|
||||
uri_path => list(binary()),
|
||||
content_format => 0..65535,
|
||||
max_age => non_neg_integer(),
|
||||
uri_query => list(binary()) | map(),
|
||||
'accept' => 0..65535,
|
||||
location_query => list(binary()),
|
||||
proxy_uri => binary(),
|
||||
proxy_scheme => binary(),
|
||||
size1 => non_neg_integer(),
|
||||
observer => non_neg_integer(),
|
||||
block1 => {non_neg_integer(), boolean(), non_neg_integer()},
|
||||
block2 => {non_neg_integer(), boolean(), non_neg_integer()}
|
||||
}.
|
||||
|
||||
-record(coap_mqtt_auth, {clientid, username, password}).
|
||||
|
||||
-record(coap_message, { type :: message_type()
|
||||
, method
|
||||
, id
|
||||
, token = <<>>
|
||||
, options = #{}
|
||||
, payload = <<>>}).
|
||||
-record(coap_message, {
|
||||
type :: message_type(),
|
||||
method,
|
||||
id,
|
||||
token = <<>>,
|
||||
options = #{},
|
||||
payload = <<>>
|
||||
}).
|
||||
|
||||
-type coap_message() :: #coap_message{}.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{application, emqx_gateway,
|
||||
[{description, "The Gateway management application"},
|
||||
{application, emqx_gateway, [
|
||||
{description, "The Gateway management application"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, []},
|
||||
{mod, {emqx_gateway_app, []}},
|
||||
|
|
@ -9,4 +9,4 @@
|
|||
{modules, []},
|
||||
{licenses, ["Apache 2.0"]},
|
||||
{links, []}
|
||||
]}.
|
||||
]}.
|
||||
|
|
|
|||
|
|
@ -19,15 +19,16 @@
|
|||
-include("include/emqx_gateway.hrl").
|
||||
|
||||
%% Gateway APIs
|
||||
-export([ registered_gateway/0
|
||||
, load/2
|
||||
, unload/1
|
||||
, lookup/1
|
||||
, update/2
|
||||
, start/1
|
||||
, stop/1
|
||||
, list/0
|
||||
]).
|
||||
-export([
|
||||
registered_gateway/0,
|
||||
load/2,
|
||||
unload/1,
|
||||
lookup/1,
|
||||
update/2,
|
||||
start/1,
|
||||
stop/1,
|
||||
list/0
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
|
|
@ -46,13 +47,14 @@ registered_gateway() ->
|
|||
list() ->
|
||||
emqx_gateway_sup:list_gateway_insta().
|
||||
|
||||
-spec load(gateway_name(), emqx_config:config())
|
||||
-> {ok, pid()}
|
||||
-spec load(gateway_name(), emqx_config:config()) ->
|
||||
{ok, pid()}
|
||||
| {error, any()}.
|
||||
load(Name, Config) ->
|
||||
Gateway = #{ name => Name
|
||||
, descr => undefined
|
||||
, config => Config
|
||||
Gateway = #{
|
||||
name => Name,
|
||||
descr => undefined,
|
||||
config => Config
|
||||
},
|
||||
emqx_gateway_sup:load_gateway(Gateway).
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -24,25 +24,30 @@
|
|||
-import(hoconsc, [mk/2, ref/2]).
|
||||
-import(emqx_dashboard_swagger, [error_codes/2]).
|
||||
|
||||
-import(emqx_gateway_http,
|
||||
[ return_http_error/2
|
||||
, with_gateway/2
|
||||
, with_authn/2
|
||||
, checks/2
|
||||
]).
|
||||
-import(
|
||||
emqx_gateway_http,
|
||||
[
|
||||
return_http_error/2,
|
||||
with_gateway/2,
|
||||
with_authn/2,
|
||||
checks/2
|
||||
]
|
||||
).
|
||||
|
||||
%% minirest/dashbaord_swagger behaviour callbacks
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1
|
||||
]).
|
||||
|
||||
%% http handlers
|
||||
-export([ authn/2
|
||||
, users/2
|
||||
, users_insta/2
|
||||
, import_users/2
|
||||
]).
|
||||
-export([
|
||||
authn/2,
|
||||
users/2,
|
||||
users_insta/2,
|
||||
import_users/2
|
||||
]).
|
||||
|
||||
%% internal export for emqx_gateway_api_listeners module
|
||||
-export([schema_authn/0]).
|
||||
|
|
@ -55,10 +60,11 @@ api_spec() ->
|
|||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||
|
||||
paths() ->
|
||||
[ "/gateway/:name/authentication"
|
||||
, "/gateway/:name/authentication/users"
|
||||
, "/gateway/:name/authentication/users/:uid"
|
||||
, "/gateway/:name/authentication/import_users"
|
||||
[
|
||||
"/gateway/:name/authentication",
|
||||
"/gateway/:name/authentication/users",
|
||||
"/gateway/:name/authentication/users/:uid",
|
||||
"/gateway/:name/authentication/import_users"
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
@ -66,30 +72,29 @@ paths() ->
|
|||
|
||||
authn(get, #{bindings := #{name := Name0}}) ->
|
||||
with_gateway(Name0, fun(GwName, _) ->
|
||||
try
|
||||
emqx_gateway_http:authn(GwName)
|
||||
of
|
||||
try emqx_gateway_http:authn(GwName) of
|
||||
Authn -> {200, Authn}
|
||||
catch
|
||||
error : {config_not_found, _} ->
|
||||
error:{config_not_found, _} ->
|
||||
{204}
|
||||
end
|
||||
end);
|
||||
|
||||
authn(put, #{bindings := #{name := Name0},
|
||||
body := Body}) ->
|
||||
authn(put, #{
|
||||
bindings := #{name := Name0},
|
||||
body := Body
|
||||
}) ->
|
||||
with_gateway(Name0, fun(GwName, _) ->
|
||||
{ok, Authn} = emqx_gateway_http:update_authn(GwName, Body),
|
||||
{200, Authn}
|
||||
end);
|
||||
|
||||
authn(post, #{bindings := #{name := Name0},
|
||||
body := Body}) ->
|
||||
authn(post, #{
|
||||
bindings := #{name := Name0},
|
||||
body := Body
|
||||
}) ->
|
||||
with_gateway(Name0, fun(GwName, _) ->
|
||||
{ok, Authn} = emqx_gateway_http:add_authn(GwName, Body),
|
||||
{201, Authn}
|
||||
end);
|
||||
|
||||
authn(delete, #{bindings := #{name := Name0}}) ->
|
||||
with_gateway(Name0, fun(GwName, _) ->
|
||||
ok = emqx_gateway_http:remove_authn(GwName),
|
||||
|
|
@ -97,47 +102,85 @@ authn(delete, #{bindings := #{name := Name0}}) ->
|
|||
end).
|
||||
|
||||
users(get, #{bindings := #{name := Name0}, query_string := Qs}) ->
|
||||
with_authn(Name0, fun(_GwName, #{id := AuthId,
|
||||
chain_name := ChainName}) ->
|
||||
with_authn(Name0, fun(
|
||||
_GwName,
|
||||
#{
|
||||
id := AuthId,
|
||||
chain_name := ChainName
|
||||
}
|
||||
) ->
|
||||
emqx_authn_api:list_users(ChainName, AuthId, parse_qstring(Qs))
|
||||
end);
|
||||
users(post, #{bindings := #{name := Name0},
|
||||
body := Body}) ->
|
||||
with_authn(Name0, fun(_GwName, #{id := AuthId,
|
||||
chain_name := ChainName}) ->
|
||||
users(post, #{
|
||||
bindings := #{name := Name0},
|
||||
body := Body
|
||||
}) ->
|
||||
with_authn(Name0, fun(
|
||||
_GwName,
|
||||
#{
|
||||
id := AuthId,
|
||||
chain_name := ChainName
|
||||
}
|
||||
) ->
|
||||
emqx_authn_api:add_user(ChainName, AuthId, Body)
|
||||
end).
|
||||
|
||||
users_insta(get, #{bindings := #{name := Name0, uid := UserId}}) ->
|
||||
with_authn(Name0, fun(_GwName, #{id := AuthId,
|
||||
chain_name := ChainName}) ->
|
||||
with_authn(Name0, fun(
|
||||
_GwName,
|
||||
#{
|
||||
id := AuthId,
|
||||
chain_name := ChainName
|
||||
}
|
||||
) ->
|
||||
emqx_authn_api:find_user(ChainName, AuthId, UserId)
|
||||
end);
|
||||
users_insta(put, #{bindings := #{name := Name0, uid := UserId},
|
||||
body := Body}) ->
|
||||
with_authn(Name0, fun(_GwName, #{id := AuthId,
|
||||
chain_name := ChainName}) ->
|
||||
users_insta(put, #{
|
||||
bindings := #{name := Name0, uid := UserId},
|
||||
body := Body
|
||||
}) ->
|
||||
with_authn(Name0, fun(
|
||||
_GwName,
|
||||
#{
|
||||
id := AuthId,
|
||||
chain_name := ChainName
|
||||
}
|
||||
) ->
|
||||
emqx_authn_api:update_user(ChainName, AuthId, UserId, Body)
|
||||
end);
|
||||
users_insta(delete, #{bindings := #{name := Name0, uid := UserId}}) ->
|
||||
with_authn(Name0, fun(_GwName, #{id := AuthId,
|
||||
chain_name := ChainName}) ->
|
||||
with_authn(Name0, fun(
|
||||
_GwName,
|
||||
#{
|
||||
id := AuthId,
|
||||
chain_name := ChainName
|
||||
}
|
||||
) ->
|
||||
emqx_authn_api:delete_user(ChainName, AuthId, UserId)
|
||||
end).
|
||||
|
||||
import_users(post, #{bindings := #{name := Name0},
|
||||
body := Body}) ->
|
||||
with_authn(Name0, fun(_GwName, #{id := AuthId,
|
||||
chain_name := ChainName}) ->
|
||||
import_users(post, #{
|
||||
bindings := #{name := Name0},
|
||||
body := Body
|
||||
}) ->
|
||||
with_authn(Name0, fun(
|
||||
_GwName,
|
||||
#{
|
||||
id := AuthId,
|
||||
chain_name := ChainName
|
||||
}
|
||||
) ->
|
||||
case maps:get(<<"filename">>, Body, undefined) of
|
||||
undefined ->
|
||||
emqx_authn_api:serialize_error({missing_parameter, filename});
|
||||
Filename ->
|
||||
case emqx_authentication:import_users(
|
||||
ChainName, AuthId, Filename) of
|
||||
case
|
||||
emqx_authentication:import_users(
|
||||
ChainName, AuthId, Filename
|
||||
)
|
||||
of
|
||||
ok -> {204};
|
||||
{error, Reason} ->
|
||||
emqx_authn_api:serialize_error(Reason)
|
||||
{error, Reason} -> emqx_authn_api:serialize_error(Reason)
|
||||
end
|
||||
end
|
||||
end).
|
||||
|
|
@ -146,124 +189,164 @@ import_users(post, #{bindings := #{name := Name0},
|
|||
%% Utils
|
||||
|
||||
parse_qstring(Qs) ->
|
||||
maps:with([ <<"page">>
|
||||
, <<"limit">>
|
||||
, <<"like_username">>
|
||||
, <<"like_clientid">>], Qs).
|
||||
maps:with(
|
||||
[
|
||||
<<"page">>,
|
||||
<<"limit">>,
|
||||
<<"like_username">>,
|
||||
<<"like_clientid">>
|
||||
],
|
||||
Qs
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Swagger defines
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
|
||||
schema("/gateway/:name/authentication") ->
|
||||
#{ 'operationId' => authn,
|
||||
#{
|
||||
'operationId' => authn,
|
||||
get =>
|
||||
#{ desc => <<"Get the gateway authentication">>
|
||||
, parameters => params_gateway_name_in_path()
|
||||
, responses =>
|
||||
#{
|
||||
desc => <<"Get the gateway authentication">>,
|
||||
parameters => params_gateway_name_in_path(),
|
||||
responses =>
|
||||
?STANDARD_RESP(
|
||||
#{ 200 => schema_authn()
|
||||
, 204 => <<"Authentication does not initiated">>
|
||||
})
|
||||
#{
|
||||
200 => schema_authn(),
|
||||
204 => <<"Authentication does not initiated">>
|
||||
}
|
||||
)
|
||||
},
|
||||
put =>
|
||||
#{ desc => <<"Update authentication for the gateway">>
|
||||
, parameters => params_gateway_name_in_path()
|
||||
, 'requestBody' => schema_authn()
|
||||
, responses =>
|
||||
#{
|
||||
desc => <<"Update authentication for the gateway">>,
|
||||
parameters => params_gateway_name_in_path(),
|
||||
'requestBody' => schema_authn(),
|
||||
responses =>
|
||||
?STANDARD_RESP(#{200 => schema_authn()})
|
||||
},
|
||||
post =>
|
||||
#{ desc => <<"Add authentication for the gateway">>
|
||||
, parameters => params_gateway_name_in_path()
|
||||
, 'requestBody' => schema_authn()
|
||||
, responses =>
|
||||
#{
|
||||
desc => <<"Add authentication for the gateway">>,
|
||||
parameters => params_gateway_name_in_path(),
|
||||
'requestBody' => schema_authn(),
|
||||
responses =>
|
||||
?STANDARD_RESP(#{201 => schema_authn()})
|
||||
},
|
||||
delete =>
|
||||
#{ desc => <<"Remove the gateway authentication">>
|
||||
, parameters => params_gateway_name_in_path()
|
||||
, responses =>
|
||||
#{
|
||||
desc => <<"Remove the gateway authentication">>,
|
||||
parameters => params_gateway_name_in_path(),
|
||||
responses =>
|
||||
?STANDARD_RESP(#{204 => <<"Deleted">>})
|
||||
}
|
||||
};
|
||||
schema("/gateway/:name/authentication/users") ->
|
||||
#{ 'operationId' => users
|
||||
, get =>
|
||||
#{ desc => <<"Get the users for the authentication">>
|
||||
, parameters => params_gateway_name_in_path() ++
|
||||
#{
|
||||
'operationId' => users,
|
||||
get =>
|
||||
#{
|
||||
desc => <<"Get the users for the authentication">>,
|
||||
parameters => params_gateway_name_in_path() ++
|
||||
params_paging_in_qs() ++
|
||||
params_fuzzy_in_qs()
|
||||
, responses =>
|
||||
params_fuzzy_in_qs(),
|
||||
responses =>
|
||||
?STANDARD_RESP(
|
||||
#{ 200 => emqx_dashboard_swagger:schema_with_example(
|
||||
#{
|
||||
200 => emqx_dashboard_swagger:schema_with_example(
|
||||
ref(emqx_authn_api, response_users),
|
||||
emqx_authn_api:response_users_example())
|
||||
})
|
||||
emqx_authn_api:response_users_example()
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
post =>
|
||||
#{ desc => <<"Add user for the authentication">>
|
||||
, parameters => params_gateway_name_in_path()
|
||||
, 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
#{
|
||||
desc => <<"Add user for the authentication">>,
|
||||
parameters => params_gateway_name_in_path(),
|
||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
ref(emqx_authn_api, request_user_create),
|
||||
emqx_authn_api:request_user_create_examples())
|
||||
, responses =>
|
||||
emqx_authn_api:request_user_create_examples()
|
||||
),
|
||||
responses =>
|
||||
?STANDARD_RESP(
|
||||
#{ 201 => emqx_dashboard_swagger:schema_with_example(
|
||||
#{
|
||||
201 => emqx_dashboard_swagger:schema_with_example(
|
||||
ref(emqx_authn_api, response_user),
|
||||
emqx_authn_api:response_user_examples())
|
||||
})
|
||||
emqx_authn_api:response_user_examples()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
};
|
||||
schema("/gateway/:name/authentication/users/:uid") ->
|
||||
#{ 'operationId' => users_insta
|
||||
, get =>
|
||||
#{ desc => <<"Get user info from the gateway "
|
||||
"authentication">>
|
||||
, parameters => params_gateway_name_in_path() ++
|
||||
params_userid_in_path()
|
||||
, responses =>
|
||||
#{
|
||||
'operationId' => users_insta,
|
||||
get =>
|
||||
#{
|
||||
desc => <<
|
||||
"Get user info from the gateway "
|
||||
"authentication"
|
||||
>>,
|
||||
parameters => params_gateway_name_in_path() ++
|
||||
params_userid_in_path(),
|
||||
responses =>
|
||||
?STANDARD_RESP(
|
||||
#{ 200 => emqx_dashboard_swagger:schema_with_example(
|
||||
#{
|
||||
200 => emqx_dashboard_swagger:schema_with_example(
|
||||
ref(emqx_authn_api, response_user),
|
||||
emqx_authn_api:response_user_examples())
|
||||
})
|
||||
emqx_authn_api:response_user_examples()
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
put =>
|
||||
#{ desc => <<"Update the user info for the gateway "
|
||||
"authentication">>
|
||||
, parameters => params_gateway_name_in_path() ++
|
||||
params_userid_in_path()
|
||||
, 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
#{
|
||||
desc => <<
|
||||
"Update the user info for the gateway "
|
||||
"authentication"
|
||||
>>,
|
||||
parameters => params_gateway_name_in_path() ++
|
||||
params_userid_in_path(),
|
||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
ref(emqx_authn_api, request_user_update),
|
||||
emqx_authn_api:request_user_update_examples())
|
||||
, responses =>
|
||||
emqx_authn_api:request_user_update_examples()
|
||||
),
|
||||
responses =>
|
||||
?STANDARD_RESP(
|
||||
#{ 200 => emqx_dashboard_swagger:schema_with_example(
|
||||
#{
|
||||
200 => emqx_dashboard_swagger:schema_with_example(
|
||||
ref(emqx_authn_api, response_user),
|
||||
emqx_authn_api:response_user_examples())
|
||||
})
|
||||
emqx_authn_api:response_user_examples()
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
delete =>
|
||||
#{ desc => <<"Delete the user for the gateway "
|
||||
"authentication">>
|
||||
, parameters => params_gateway_name_in_path() ++
|
||||
params_userid_in_path()
|
||||
, responses =>
|
||||
#{
|
||||
desc => <<
|
||||
"Delete the user for the gateway "
|
||||
"authentication"
|
||||
>>,
|
||||
parameters => params_gateway_name_in_path() ++
|
||||
params_userid_in_path(),
|
||||
responses =>
|
||||
?STANDARD_RESP(#{204 => <<"User Deleted">>})
|
||||
}
|
||||
};
|
||||
schema("/gateway/:name/authentication/import_users") ->
|
||||
#{ 'operationId' => import_users
|
||||
, post =>
|
||||
#{ desc => <<"Import users into the gateway authentication">>
|
||||
, parameters => params_gateway_name_in_path()
|
||||
, 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
#{
|
||||
'operationId' => import_users,
|
||||
post =>
|
||||
#{
|
||||
desc => <<"Import users into the gateway authentication">>,
|
||||
parameters => params_gateway_name_in_path(),
|
||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
ref(emqx_authn_api, request_import_users),
|
||||
emqx_authn_api:request_import_users_examples()
|
||||
)
|
||||
, responses =>
|
||||
),
|
||||
responses =>
|
||||
?STANDARD_RESP(#{204 => <<"Imported">>})
|
||||
}
|
||||
}.
|
||||
|
|
@ -272,52 +355,77 @@ schema("/gateway/:name/authentication/import_users") ->
|
|||
%% params defines
|
||||
|
||||
params_gateway_name_in_path() ->
|
||||
[{name,
|
||||
mk(binary(),
|
||||
#{ in => path
|
||||
, desc => <<"Gateway Name">>
|
||||
, example => <<"">>
|
||||
})}
|
||||
[
|
||||
{name,
|
||||
mk(
|
||||
binary(),
|
||||
#{
|
||||
in => path,
|
||||
desc => <<"Gateway Name">>,
|
||||
example => <<"">>
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
params_userid_in_path() ->
|
||||
[{uid, mk(binary(),
|
||||
#{ in => path
|
||||
, desc => <<"User ID">>
|
||||
, example => <<"">>
|
||||
})}
|
||||
[
|
||||
{uid,
|
||||
mk(
|
||||
binary(),
|
||||
#{
|
||||
in => path,
|
||||
desc => <<"User ID">>,
|
||||
example => <<"">>
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
params_paging_in_qs() ->
|
||||
[{page, mk(integer(),
|
||||
#{ in => query
|
||||
, required => false
|
||||
, desc => <<"Page Index">>
|
||||
, example => 1
|
||||
})},
|
||||
{limit, mk(integer(),
|
||||
#{ in => query
|
||||
, required => false
|
||||
, desc => <<"Page Limit">>
|
||||
, example => 100
|
||||
})}
|
||||
[
|
||||
{page,
|
||||
mk(
|
||||
integer(),
|
||||
#{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Page Index">>,
|
||||
example => 1
|
||||
}
|
||||
)},
|
||||
{limit,
|
||||
mk(
|
||||
integer(),
|
||||
#{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Page Limit">>,
|
||||
example => 100
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
params_fuzzy_in_qs() ->
|
||||
[{like_username,
|
||||
mk(binary(),
|
||||
#{ in => query
|
||||
, required => false
|
||||
, desc => <<"Fuzzy search by username">>
|
||||
, example => <<"username">>
|
||||
})},
|
||||
[
|
||||
{like_username,
|
||||
mk(
|
||||
binary(),
|
||||
#{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Fuzzy search by username">>,
|
||||
example => <<"username">>
|
||||
}
|
||||
)},
|
||||
{like_clientid,
|
||||
mk(binary(),
|
||||
#{ in => query
|
||||
, required => false
|
||||
, desc => <<"Fuzzy search by clientid">>
|
||||
, example => <<"clientid">>
|
||||
})}
|
||||
mk(
|
||||
binary(),
|
||||
#{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Fuzzy search by clientid">>,
|
||||
example => <<"clientid">>
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -22,6 +22,8 @@
|
|||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
-elvis([{elvis_style, invalid_dynamic_call, disable}]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
{ok, Sup} = emqx_gateway_sup:start_link(),
|
||||
emqx_gateway_cli:load(),
|
||||
|
|
@ -44,21 +46,28 @@ load_default_gateway_applications() ->
|
|||
|
||||
gateway_type_searching() ->
|
||||
%% FIXME: Hardcoded apps
|
||||
[emqx_stomp_impl, emqx_sn_impl, emqx_exproto_impl,
|
||||
emqx_coap_impl, emqx_lwm2m_impl].
|
||||
[
|
||||
emqx_stomp_impl,
|
||||
emqx_sn_impl,
|
||||
emqx_exproto_impl,
|
||||
emqx_coap_impl,
|
||||
emqx_lwm2m_impl
|
||||
].
|
||||
|
||||
reg(Mod) ->
|
||||
try
|
||||
Mod:reg(),
|
||||
?SLOG(debug, #{ msg => "register_gateway_succeed"
|
||||
, callback_module => Mod
|
||||
?SLOG(debug, #{
|
||||
msg => "register_gateway_succeed",
|
||||
callback_module => Mod
|
||||
})
|
||||
catch
|
||||
Class : Reason : Stk ->
|
||||
?SLOG(error, #{ msg => "failed_to_register_gateway"
|
||||
, callback_module => Mod
|
||||
, reason => {Class, Reason}
|
||||
, stacktrace => Stk
|
||||
Class:Reason:Stk ->
|
||||
?SLOG(error, #{
|
||||
msg => "failed_to_register_gateway",
|
||||
callback_module => Mod,
|
||||
reason => {Class, Reason},
|
||||
stacktrace => Stk
|
||||
})
|
||||
end.
|
||||
|
||||
|
|
@ -67,22 +76,26 @@ load_gateway_by_default() ->
|
|||
|
||||
load_gateway_by_default([]) ->
|
||||
ok;
|
||||
load_gateway_by_default([{Type, Confs}|More]) ->
|
||||
load_gateway_by_default([{Type, Confs} | More]) ->
|
||||
case emqx_gateway_registry:lookup(Type) of
|
||||
undefined ->
|
||||
?SLOG(error, #{ msg => "skip_to_load_gateway"
|
||||
, gateway_name => Type
|
||||
?SLOG(error, #{
|
||||
msg => "skip_to_load_gateway",
|
||||
gateway_name => Type
|
||||
});
|
||||
_ ->
|
||||
case emqx_gateway:load(Type, Confs) of
|
||||
{ok, _} ->
|
||||
?SLOG(debug, #{ msg => "load_gateway_succeed"
|
||||
, gateway_name => Type
|
||||
?SLOG(debug, #{
|
||||
msg => "load_gateway_succeed",
|
||||
gateway_name => Type
|
||||
});
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{ msg => "load_gateway_failed"
|
||||
, gateway_name => Type
|
||||
, reason => Reason})
|
||||
?SLOG(error, #{
|
||||
msg => "load_gateway_failed",
|
||||
gateway_name => Type,
|
||||
reason => Reason
|
||||
})
|
||||
end
|
||||
end,
|
||||
load_gateway_by_default(More).
|
||||
|
|
|
|||
|
|
@ -17,25 +17,30 @@
|
|||
%% @doc The Command-Line-Interface module for Gateway Application
|
||||
-module(emqx_gateway_cli).
|
||||
|
||||
-export([ load/0
|
||||
, unload/0
|
||||
]).
|
||||
-export([
|
||||
load/0,
|
||||
unload/0
|
||||
]).
|
||||
|
||||
-export([ gateway/1
|
||||
, 'gateway-registry'/1
|
||||
, 'gateway-clients'/1
|
||||
, 'gateway-metrics'/1
|
||||
-export([
|
||||
gateway/1,
|
||||
'gateway-registry'/1,
|
||||
'gateway-clients'/1,
|
||||
'gateway-metrics'/1
|
||||
%, 'gateway-banned'/1
|
||||
]).
|
||||
]).
|
||||
|
||||
-elvis([{elvis_style, function_naming_convention, disable}]).
|
||||
|
||||
-spec load() -> ok.
|
||||
load() ->
|
||||
Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)],
|
||||
lists:foreach(fun(Cmd) ->
|
||||
lists:foreach(
|
||||
fun(Cmd) ->
|
||||
emqx_ctl:register_command(Cmd, {?MODULE, Cmd}, [])
|
||||
end, Cmds).
|
||||
end,
|
||||
Cmds
|
||||
).
|
||||
|
||||
-spec unload() -> ok.
|
||||
unload() ->
|
||||
|
|
@ -53,10 +58,11 @@ is_cmd(Fun) ->
|
|||
|
||||
gateway(["list"]) ->
|
||||
lists:foreach(
|
||||
fun (GwSummary) ->
|
||||
fun(GwSummary) ->
|
||||
print(format_gw_summary(GwSummary))
|
||||
end, emqx_gateway_http:gateways(all));
|
||||
|
||||
end,
|
||||
emqx_gateway_http:gateways(all)
|
||||
);
|
||||
gateway(["lookup", Name]) ->
|
||||
case emqx_gateway:lookup(atom(Name)) of
|
||||
undefined ->
|
||||
|
|
@ -64,18 +70,18 @@ gateway(["lookup", Name]) ->
|
|||
Gateway ->
|
||||
print(format_gateway(Gateway))
|
||||
end;
|
||||
|
||||
gateway(["load", Name, Conf]) ->
|
||||
case emqx_gateway_conf:load_gateway(
|
||||
case
|
||||
emqx_gateway_conf:load_gateway(
|
||||
bin(Name),
|
||||
emqx_json:decode(Conf, [return_maps])
|
||||
) of
|
||||
)
|
||||
of
|
||||
{ok, _} ->
|
||||
print("ok\n");
|
||||
{error, Reason} ->
|
||||
print("Error: ~ts\n", [format_error(Reason)])
|
||||
end;
|
||||
|
||||
gateway(["unload", Name]) ->
|
||||
case emqx_gateway_conf:unload_gateway(bin(Name)) of
|
||||
ok ->
|
||||
|
|
@ -83,56 +89,51 @@ gateway(["unload", Name]) ->
|
|||
{error, Reason} ->
|
||||
print("Error: ~ts\n", [format_error(Reason)])
|
||||
end;
|
||||
|
||||
gateway(["stop", Name]) ->
|
||||
case emqx_gateway_conf:update_gateway(
|
||||
case
|
||||
emqx_gateway_conf:update_gateway(
|
||||
bin(Name),
|
||||
#{<<"enable">> => <<"false">>}
|
||||
) of
|
||||
)
|
||||
of
|
||||
{ok, _} ->
|
||||
print("ok\n");
|
||||
{error, Reason} ->
|
||||
print("Error: ~ts\n", [format_error(Reason)])
|
||||
end;
|
||||
|
||||
gateway(["start", Name]) ->
|
||||
case emqx_gateway_conf:update_gateway(
|
||||
case
|
||||
emqx_gateway_conf:update_gateway(
|
||||
bin(Name),
|
||||
#{<<"enable">> => <<"true">>}
|
||||
) of
|
||||
)
|
||||
of
|
||||
{ok, _} ->
|
||||
print("ok\n");
|
||||
{error, Reason} ->
|
||||
print("Error: ~ts\n", [format_error(Reason)])
|
||||
end;
|
||||
|
||||
gateway(_) ->
|
||||
emqx_ctl:usage(
|
||||
[ {"gateway list",
|
||||
"List all gateway"}
|
||||
, {"gateway lookup <Name>",
|
||||
"Lookup a gateway detailed information"}
|
||||
, {"gateway load <Name> <JsonConf>",
|
||||
"Load a gateway with config"}
|
||||
, {"gateway unload <Name>",
|
||||
"Unload the gateway"}
|
||||
, {"gateway stop <Name>",
|
||||
"Stop the gateway"}
|
||||
, {"gateway start <Name>",
|
||||
"Start the gateway"}
|
||||
]).
|
||||
[
|
||||
{"gateway list", "List all gateway"},
|
||||
{"gateway lookup <Name>", "Lookup a gateway detailed information"},
|
||||
{"gateway load <Name> <JsonConf>", "Load a gateway with config"},
|
||||
{"gateway unload <Name>", "Unload the gateway"},
|
||||
{"gateway stop <Name>", "Stop the gateway"},
|
||||
{"gateway start <Name>", "Start the gateway"}
|
||||
]
|
||||
).
|
||||
|
||||
'gateway-registry'(["list"]) ->
|
||||
lists:foreach(
|
||||
fun({Name, #{cbkmod := CbMod}}) ->
|
||||
print("Registered Name: ~ts, Callback Module: ~ts\n", [Name, CbMod])
|
||||
end,
|
||||
emqx_gateway_registry:list());
|
||||
|
||||
emqx_gateway_registry:list()
|
||||
);
|
||||
'gateway-registry'(_) ->
|
||||
emqx_ctl:usage([ {"gateway-registry list",
|
||||
"List all registered gateways"}
|
||||
]).
|
||||
emqx_ctl:usage([{"gateway-registry list", "List all registered gateways"}]).
|
||||
|
||||
'gateway-clients'(["list", Name]) ->
|
||||
%% XXX: page me?
|
||||
|
|
@ -143,7 +144,6 @@ gateway(_) ->
|
|||
_ ->
|
||||
dump(InfoTab, client)
|
||||
end;
|
||||
|
||||
'gateway-clients'(["lookup", Name, ClientId]) ->
|
||||
ChanTab = emqx_gateway_cm:tabname(chan, Name),
|
||||
case ets:info(ChanTab) of
|
||||
|
|
@ -151,27 +151,24 @@ gateway(_) ->
|
|||
print("Bad Gateway Name.\n");
|
||||
_ ->
|
||||
case ets:lookup(ChanTab, bin(ClientId)) of
|
||||
[] -> print("Not Found.\n");
|
||||
[] ->
|
||||
print("Not Found.\n");
|
||||
[Chann] ->
|
||||
InfoTab = emqx_gateway_cm:tabname(info, Name),
|
||||
[ChannInfo] = ets:lookup(InfoTab, Chann),
|
||||
print_record({client, ChannInfo})
|
||||
end
|
||||
end;
|
||||
|
||||
'gateway-clients'(["kick", Name, ClientId]) ->
|
||||
case emqx_gateway_cm:kick_session(Name, bin(ClientId)) of
|
||||
ok -> print("ok\n");
|
||||
_ -> print("Not Found.\n")
|
||||
end;
|
||||
|
||||
'gateway-clients'(_) ->
|
||||
emqx_ctl:usage([ {"gateway-clients list <Name>",
|
||||
"List all clients for a gateway"}
|
||||
, {"gateway-clients lookup <Name> <ClientId>",
|
||||
"Lookup the Client Info for specified client"}
|
||||
, {"gateway-clients kick <Name> <ClientId>",
|
||||
"Kick out a client"}
|
||||
emqx_ctl:usage([
|
||||
{"gateway-clients list <Name>", "List all clients for a gateway"},
|
||||
{"gateway-clients lookup <Name> <ClientId>", "Lookup the Client Info for specified client"},
|
||||
{"gateway-clients kick <Name> <ClientId>", "Kick out a client"}
|
||||
]).
|
||||
|
||||
'gateway-metrics'([Name]) ->
|
||||
|
|
@ -181,19 +178,17 @@ gateway(_) ->
|
|||
Metrics ->
|
||||
lists:foreach(
|
||||
fun({K, V}) -> print("~-30s: ~w\n", [K, V]) end,
|
||||
Metrics)
|
||||
Metrics
|
||||
)
|
||||
end;
|
||||
|
||||
'gateway-metrics'(_) ->
|
||||
emqx_ctl:usage([ {"gateway-metrics <Name>",
|
||||
"List all metrics for a gateway"}
|
||||
]).
|
||||
emqx_ctl:usage([{"gateway-metrics <Name>", "List all metrics for a gateway"}]).
|
||||
|
||||
atom(Id) ->
|
||||
try
|
||||
list_to_existing_atom(Id)
|
||||
catch
|
||||
_ : _ -> undefined
|
||||
_:_ -> undefined
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
@ -207,7 +202,6 @@ dump(Table, Tag) ->
|
|||
|
||||
dump(_Table, _, '$end_of_table', Result) ->
|
||||
lists:reverse(Result);
|
||||
|
||||
dump(Table, Tag, Key, Result) ->
|
||||
PrintValue = [print_record({Tag, Record}) || Record <- ets:lookup(Table, Key)],
|
||||
dump(Table, Tag, ets:next(Table, Key), [PrintValue | Result]).
|
||||
|
|
@ -220,9 +214,20 @@ print_record({client, {_, Infos, Stats}}) ->
|
|||
StatsGet = fun(K) -> proplists:get_value(K, Stats, 0) end,
|
||||
|
||||
ConnectedAt = SafeGet(connected_at, ConnInfo),
|
||||
InfoKeys = [clientid, username, peername, clean_start, keepalive,
|
||||
subscriptions_cnt, send_msg, connected, created_at, connected_at],
|
||||
Info = #{ clientid => SafeGet(clientid, ClientInfo),
|
||||
InfoKeys = [
|
||||
clientid,
|
||||
username,
|
||||
peername,
|
||||
clean_start,
|
||||
keepalive,
|
||||
subscriptions_cnt,
|
||||
send_msg,
|
||||
connected,
|
||||
created_at,
|
||||
connected_at
|
||||
],
|
||||
Info = #{
|
||||
clientid => SafeGet(clientid, ClientInfo),
|
||||
username => SafeGet(username, ClientInfo),
|
||||
peername => SafeGet(peername, ConnInfo),
|
||||
clean_start => SafeGet(clean_start, ConnInfo),
|
||||
|
|
@ -234,50 +239,66 @@ print_record({client, {_, Infos, Stats}}) ->
|
|||
connected_at => ConnectedAt
|
||||
},
|
||||
|
||||
print("Client(~ts, username=~ts, peername=~ts, "
|
||||
print(
|
||||
"Client(~ts, username=~ts, peername=~ts, "
|
||||
"clean_start=~ts, keepalive=~w, "
|
||||
"subscriptions=~w, delivered_msgs=~w, "
|
||||
"connected=~ts, created_at=~w, connected_at=~w)\n",
|
||||
[format(K, maps:get(K, Info)) || K <- InfoKeys]).
|
||||
[format(K, maps:get(K, Info)) || K <- InfoKeys]
|
||||
).
|
||||
|
||||
print(S) -> emqx_ctl:print(S).
|
||||
print(S, A) -> emqx_ctl:print(S, A).
|
||||
|
||||
format(_, undefined) ->
|
||||
undefined;
|
||||
|
||||
format(peername, {IPAddr, Port}) ->
|
||||
IPStr = emqx_mgmt_util:ntoa(IPAddr),
|
||||
io_lib:format("~ts:~p", [IPStr, Port]);
|
||||
|
||||
format(_, Val) ->
|
||||
Val.
|
||||
|
||||
format_gw_summary(#{name := Name, status := unloaded}) ->
|
||||
io_lib:format("Gateway(name=~ts, status=unloaded)\n", [Name]);
|
||||
|
||||
format_gw_summary(#{name := Name, status := stopped,
|
||||
stopped_at := StoppedAt}) ->
|
||||
io_lib:format("Gateway(name=~ts, status=stopped, stopped_at=~ts)\n",
|
||||
[Name, StoppedAt]);
|
||||
format_gw_summary(#{name := Name, status := running,
|
||||
format_gw_summary(#{
|
||||
name := Name,
|
||||
status := stopped,
|
||||
stopped_at := StoppedAt
|
||||
}) ->
|
||||
io_lib:format(
|
||||
"Gateway(name=~ts, status=stopped, stopped_at=~ts)\n",
|
||||
[Name, StoppedAt]
|
||||
);
|
||||
format_gw_summary(#{
|
||||
name := Name,
|
||||
status := running,
|
||||
current_connections := ConnCnt,
|
||||
started_at := StartedAt}) ->
|
||||
io_lib:format("Gateway(name=~ts, status=running, clients=~w, "
|
||||
"started_at=~ts)\n", [Name, ConnCnt, StartedAt]).
|
||||
started_at := StartedAt
|
||||
}) ->
|
||||
io_lib:format(
|
||||
"Gateway(name=~ts, status=running, clients=~w, "
|
||||
"started_at=~ts)\n",
|
||||
[Name, ConnCnt, StartedAt]
|
||||
).
|
||||
|
||||
format_gateway(#{name := Name,
|
||||
status := unloaded}) ->
|
||||
format_gateway(#{
|
||||
name := Name,
|
||||
status := unloaded
|
||||
}) ->
|
||||
io_lib:format(
|
||||
"name: ~ts\n"
|
||||
"status: unloaded\n", [Name]);
|
||||
|
||||
format_gateway(Gw =
|
||||
#{name := Name,
|
||||
"status: unloaded\n",
|
||||
[Name]
|
||||
);
|
||||
format_gateway(
|
||||
Gw =
|
||||
#{
|
||||
name := Name,
|
||||
status := Status,
|
||||
created_at := CreatedAt,
|
||||
config := Config
|
||||
}) ->
|
||||
}
|
||||
) ->
|
||||
{StopOrStart, Timestamp} =
|
||||
case Status of
|
||||
stopped -> {stopped_at, maps:get(stopped_at, Gw)};
|
||||
|
|
@ -289,10 +310,15 @@ format_gateway(Gw =
|
|||
"created_at: ~ts\n"
|
||||
"~ts: ~ts\n"
|
||||
"config: ~p\n",
|
||||
[Name, Status,
|
||||
[
|
||||
Name,
|
||||
Status,
|
||||
emqx_gateway_utils:unix_ts_to_rfc3339(CreatedAt),
|
||||
StopOrStart, emqx_gateway_utils:unix_ts_to_rfc3339(Timestamp),
|
||||
Config]).
|
||||
StopOrStart,
|
||||
emqx_gateway_utils:unix_ts_to_rfc3339(Timestamp),
|
||||
Config
|
||||
]
|
||||
).
|
||||
|
||||
format_error(Reason) ->
|
||||
case emqx_gateway_http:reason2msg(Reason) of
|
||||
|
|
|
|||
|
|
@ -30,70 +30,77 @@
|
|||
%% APIs
|
||||
-export([start_link/1]).
|
||||
|
||||
-export([ open_session/5
|
||||
, open_session/6
|
||||
, kick_session/2
|
||||
, kick_session/3
|
||||
, takeover_session/2
|
||||
, register_channel/4
|
||||
, unregister_channel/2
|
||||
, insert_channel_info/4
|
||||
, lookup_by_clientid/2
|
||||
, set_chan_info/3
|
||||
, set_chan_info/4
|
||||
, get_chan_info/2
|
||||
, get_chan_info/3
|
||||
, set_chan_stats/3
|
||||
, set_chan_stats/4
|
||||
, get_chan_stats/2
|
||||
, get_chan_stats/3
|
||||
, connection_closed/2
|
||||
]).
|
||||
-export([
|
||||
open_session/5,
|
||||
open_session/6,
|
||||
kick_session/2,
|
||||
kick_session/3,
|
||||
takeover_session/2,
|
||||
register_channel/4,
|
||||
unregister_channel/2,
|
||||
insert_channel_info/4,
|
||||
lookup_by_clientid/2,
|
||||
set_chan_info/3,
|
||||
set_chan_info/4,
|
||||
get_chan_info/2,
|
||||
get_chan_info/3,
|
||||
set_chan_stats/3,
|
||||
set_chan_stats/4,
|
||||
get_chan_stats/2,
|
||||
get_chan_stats/3,
|
||||
connection_closed/2
|
||||
]).
|
||||
|
||||
-export([ call/3
|
||||
, call/4
|
||||
, cast/3
|
||||
]).
|
||||
-export([
|
||||
call/3,
|
||||
call/4,
|
||||
cast/3
|
||||
]).
|
||||
|
||||
-export([ with_channel/3
|
||||
, lookup_channels/2
|
||||
]).
|
||||
-export([
|
||||
with_channel/3,
|
||||
lookup_channels/2
|
||||
]).
|
||||
|
||||
%% Internal funcs for getting tabname by GatewayId
|
||||
-export([cmtabs/1, tabname/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
%% RPC targets
|
||||
-export([ do_lookup_by_clientid/2
|
||||
, do_get_chan_info/3
|
||||
, do_set_chan_info/4
|
||||
, do_get_chan_stats/3
|
||||
, do_set_chan_stats/4
|
||||
, do_kick_session/4
|
||||
, do_takeover_session/3
|
||||
, do_get_chann_conn_mod/3
|
||||
, do_call/4
|
||||
, do_call/5
|
||||
, do_cast/4
|
||||
]).
|
||||
-export([
|
||||
do_lookup_by_clientid/2,
|
||||
do_get_chan_info/3,
|
||||
do_set_chan_info/4,
|
||||
do_get_chan_stats/3,
|
||||
do_set_chan_stats/4,
|
||||
do_kick_session/4,
|
||||
do_takeover_session/3,
|
||||
do_get_chann_conn_mod/3,
|
||||
do_call/4,
|
||||
do_call/5,
|
||||
do_cast/4
|
||||
]).
|
||||
|
||||
-export_type([ gateway_name/0
|
||||
]).
|
||||
-export_type([gateway_name/0]).
|
||||
|
||||
-record(state, {
|
||||
gwname :: gateway_name(), %% Gateway Name
|
||||
locker :: pid(), %% ClientId Locker for CM
|
||||
registry :: pid(), %% ClientId Registry server
|
||||
%% Gateway Name
|
||||
gwname :: gateway_name(),
|
||||
%% ClientId Locker for CM
|
||||
locker :: pid(),
|
||||
%% ClientId Registry server
|
||||
registry :: pid(),
|
||||
chan_pmon :: emqx_pmon:pmon()
|
||||
}).
|
||||
}).
|
||||
|
||||
-type option() :: {gwname, gateway_name()}.
|
||||
-type options() :: list(option()).
|
||||
|
|
@ -117,14 +124,16 @@ start_link(Options) ->
|
|||
procname(GwName) ->
|
||||
list_to_atom(lists:concat([emqx_gateway_, GwName, '_cm'])).
|
||||
|
||||
-spec cmtabs(GwName :: gateway_name())
|
||||
-> {ChanTab :: atom(),
|
||||
ConnTab :: atom(),
|
||||
ChannInfoTab :: atom()}.
|
||||
-spec cmtabs(GwName :: gateway_name()) ->
|
||||
{ChanTab :: atom(), ConnTab :: atom(), ChannInfoTab :: atom()}.
|
||||
cmtabs(GwName) ->
|
||||
{ tabname(chan, GwName) %% Record: {ClientId, Pid}
|
||||
, tabname(conn, GwName) %% Record: {{ClientId, Pid}, ConnMod}
|
||||
, tabname(info, GwName) %% Record: {{ClientId, Pid}, Info, Stats}
|
||||
%% Record: {ClientId, Pid}
|
||||
{
|
||||
tabname(chan, GwName),
|
||||
%% Record: {{ClientId, Pid}, ConnMod}
|
||||
tabname(conn, GwName),
|
||||
%% Record: {{ClientId, Pid}, Info, Stats}
|
||||
tabname(info, GwName)
|
||||
}.
|
||||
|
||||
tabname(chan, GwName) ->
|
||||
|
|
@ -137,10 +146,12 @@ tabname(info, GwName) ->
|
|||
lockername(GwName) ->
|
||||
list_to_atom(lists:concat([emqx_gateway_, GwName, '_locker'])).
|
||||
|
||||
-spec register_channel(gateway_name(),
|
||||
-spec register_channel(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
pid(),
|
||||
emqx_types:conninfo()) -> ok.
|
||||
emqx_types:conninfo()
|
||||
) -> ok.
|
||||
register_channel(GwName, ClientId, ChanPid, #{conn_mod := ConnMod}) when is_pid(ChanPid) ->
|
||||
Chan = {ClientId, ChanPid},
|
||||
true = ets:insert(tabname(chan, GwName), Chan),
|
||||
|
|
@ -155,31 +166,36 @@ unregister_channel(GwName, ClientId) when is_binary(ClientId) ->
|
|||
ok.
|
||||
|
||||
%% @doc Insert/Update the channel info and stats
|
||||
-spec insert_channel_info(gateway_name(),
|
||||
-spec insert_channel_info(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:infos(),
|
||||
emqx_types:stats()) -> ok.
|
||||
emqx_types:stats()
|
||||
) -> ok.
|
||||
insert_channel_info(GwName, ClientId, Info, Stats) ->
|
||||
Chan = {ClientId, self()},
|
||||
true = ets:insert(tabname(info, GwName), {Chan, Info, Stats}),
|
||||
ok.
|
||||
|
||||
%% @doc Get info of a channel.
|
||||
-spec get_chan_info(gateway_name(), emqx_types:clientid())
|
||||
-> emqx_types:infos() | undefined.
|
||||
-spec get_chan_info(gateway_name(), emqx_types:clientid()) ->
|
||||
emqx_types:infos() | undefined.
|
||||
get_chan_info(GwName, ClientId) ->
|
||||
with_channel(GwName, ClientId,
|
||||
with_channel(
|
||||
GwName,
|
||||
ClientId,
|
||||
fun(ChanPid) ->
|
||||
get_chan_info(GwName, ClientId, ChanPid)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
-spec do_lookup_by_clientid(gateway_name(), emqx_types:clientid()) -> [pid()].
|
||||
do_lookup_by_clientid(GwName, ClientId) ->
|
||||
ChanTab = emqx_gateway_cm:tabname(chan, GwName),
|
||||
[Pid || {_, Pid} <- ets:lookup(ChanTab, ClientId)].
|
||||
|
||||
-spec do_get_chan_info(gateway_name(), emqx_types:clientid(), pid())
|
||||
-> emqx_types:infos() | undefined.
|
||||
-spec do_get_chan_info(gateway_name(), emqx_types:clientid(), pid()) ->
|
||||
emqx_types:infos() | undefined.
|
||||
do_get_chan_info(GwName, ClientId, ChanPid) ->
|
||||
Chan = {ClientId, ChanPid},
|
||||
try
|
||||
|
|
@ -189,8 +205,8 @@ do_get_chan_info(GwName, ClientId, ChanPid) ->
|
|||
error:badarg -> undefined
|
||||
end.
|
||||
|
||||
-spec get_chan_info(gateway_name(), emqx_types:clientid(), pid())
|
||||
-> emqx_types:infos() | undefined.
|
||||
-spec get_chan_info(gateway_name(), emqx_types:clientid(), pid()) ->
|
||||
emqx_types:infos() | undefined.
|
||||
get_chan_info(GwName, ClientId, ChanPid) ->
|
||||
wrap_rpc(
|
||||
emqx_gateway_cm_proto_v1:get_chan_info(GwName, ClientId, ChanPid)
|
||||
|
|
@ -199,8 +215,11 @@ get_chan_info(GwName, ClientId, ChanPid) ->
|
|||
-spec lookup_by_clientid(gateway_name(), emqx_types:clientid()) -> [pid()].
|
||||
lookup_by_clientid(GwName, ClientId) ->
|
||||
Nodes = mria_mnesia:running_nodes(),
|
||||
case emqx_gateway_cm_proto_v1:lookup_by_clientid(
|
||||
Nodes, GwName, ClientId) of
|
||||
case
|
||||
emqx_gateway_cm_proto_v1:lookup_by_clientid(
|
||||
Nodes, GwName, ClientId
|
||||
)
|
||||
of
|
||||
{Pids, []} ->
|
||||
lists:append(Pids);
|
||||
{_, _BadNodes} ->
|
||||
|
|
@ -208,74 +227,92 @@ lookup_by_clientid(GwName, ClientId) ->
|
|||
end.
|
||||
|
||||
%% @doc Update infos of the channel.
|
||||
-spec set_chan_info(gateway_name(),
|
||||
-spec set_chan_info(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:infos()) -> boolean().
|
||||
emqx_types:infos()
|
||||
) -> boolean().
|
||||
set_chan_info(GwName, ClientId, Infos) ->
|
||||
set_chan_info(GwName, ClientId, self(), Infos).
|
||||
|
||||
-spec do_set_chan_info(gateway_name(),
|
||||
-spec do_set_chan_info(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
pid(),
|
||||
emqx_types:infos()) -> boolean().
|
||||
emqx_types:infos()
|
||||
) -> boolean().
|
||||
do_set_chan_info(GwName, ClientId, ChanPid, Infos) ->
|
||||
Chan = {ClientId, ChanPid},
|
||||
try ets:update_element(tabname(info, GwName), Chan, {2, Infos})
|
||||
try
|
||||
ets:update_element(tabname(info, GwName), Chan, {2, Infos})
|
||||
catch
|
||||
error:badarg -> false
|
||||
end.
|
||||
|
||||
-spec set_chan_info(gateway_name(),
|
||||
-spec set_chan_info(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
pid(),
|
||||
emqx_types:infos()) -> boolean().
|
||||
emqx_types:infos()
|
||||
) -> boolean().
|
||||
set_chan_info(GwName, ClientId, ChanPid, Infos) ->
|
||||
wrap_rpc(emqx_gateway_cm_proto_v1:set_chan_info(GwName, ClientId, ChanPid, Infos)).
|
||||
|
||||
%% @doc Get channel's stats.
|
||||
-spec get_chan_stats(gateway_name(), emqx_types:clientid())
|
||||
-> emqx_types:stats() | undefined.
|
||||
-spec get_chan_stats(gateway_name(), emqx_types:clientid()) ->
|
||||
emqx_types:stats() | undefined.
|
||||
get_chan_stats(GwName, ClientId) ->
|
||||
with_channel(GwName, ClientId,
|
||||
with_channel(
|
||||
GwName,
|
||||
ClientId,
|
||||
fun(ChanPid) ->
|
||||
get_chan_stats(GwName, ClientId, ChanPid)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
-spec do_get_chan_stats(gateway_name(), emqx_types:clientid(), pid())
|
||||
-> emqx_types:stats() | undefined.
|
||||
-spec do_get_chan_stats(gateway_name(), emqx_types:clientid(), pid()) ->
|
||||
emqx_types:stats() | undefined.
|
||||
do_get_chan_stats(GwName, ClientId, ChanPid) ->
|
||||
Chan = {ClientId, ChanPid},
|
||||
try ets:lookup_element(tabname(info, GwName), Chan, 3)
|
||||
try
|
||||
ets:lookup_element(tabname(info, GwName), Chan, 3)
|
||||
catch
|
||||
error:badarg -> undefined
|
||||
end.
|
||||
|
||||
-spec get_chan_stats(gateway_name(), emqx_types:clientid(), pid())
|
||||
-> emqx_types:stats() | undefined.
|
||||
-spec get_chan_stats(gateway_name(), emqx_types:clientid(), pid()) ->
|
||||
emqx_types:stats() | undefined.
|
||||
get_chan_stats(GwName, ClientId, ChanPid) ->
|
||||
wrap_rpc(emqx_gateway_cm_proto_v1:get_chan_stats(GwName, ClientId, ChanPid)).
|
||||
|
||||
-spec set_chan_stats(gateway_name(),
|
||||
-spec set_chan_stats(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:stats()) -> boolean().
|
||||
emqx_types:stats()
|
||||
) -> boolean().
|
||||
set_chan_stats(GwName, ClientId, Stats) ->
|
||||
set_chan_stats(GwName, ClientId, self(), Stats).
|
||||
|
||||
-spec do_set_chan_stats(gateway_name(),
|
||||
-spec do_set_chan_stats(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
pid(),
|
||||
emqx_types:stats()) -> boolean().
|
||||
emqx_types:stats()
|
||||
) -> boolean().
|
||||
do_set_chan_stats(GwName, ClientId, ChanPid, Stats) ->
|
||||
Chan = {ClientId, ChanPid},
|
||||
try ets:update_element(tabname(info, GwName), Chan, {3, Stats})
|
||||
try
|
||||
ets:update_element(tabname(info, GwName), Chan, {3, Stats})
|
||||
catch
|
||||
error:badarg -> false
|
||||
end.
|
||||
|
||||
-spec set_chan_stats(gateway_name(),
|
||||
-spec set_chan_stats(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
pid(),
|
||||
emqx_types:stats()) -> boolean().
|
||||
emqx_types:stats()
|
||||
) -> boolean().
|
||||
set_chan_stats(GwName, ClientId, ChanPid, Stats) ->
|
||||
wrap_rpc(emqx_gateway_cm_proto_v1:set_chan_stats(GwName, ClientId, ChanPid, Stats)).
|
||||
|
||||
|
|
@ -285,14 +322,20 @@ connection_closed(GwName, ClientId) ->
|
|||
Chan = {ClientId, self()},
|
||||
ets:delete_object(tabname(conn, GwName), Chan).
|
||||
|
||||
-spec open_session(GwName :: gateway_name(),
|
||||
-spec open_session(
|
||||
GwName :: gateway_name(),
|
||||
CleanStart :: boolean(),
|
||||
ClientInfo :: emqx_types:clientinfo(),
|
||||
ConnInfo :: emqx_types:conninfo(),
|
||||
CreateSessionFun :: fun((emqx_types:clientinfo(),
|
||||
emqx_types:conninfo()) -> Session
|
||||
))
|
||||
-> {ok, #{session := Session,
|
||||
CreateSessionFun :: fun(
|
||||
(
|
||||
emqx_types:clientinfo(),
|
||||
emqx_types:conninfo()
|
||||
) -> Session
|
||||
)
|
||||
) ->
|
||||
{ok, #{
|
||||
session := Session,
|
||||
present := boolean(),
|
||||
pendings => list()
|
||||
}}
|
||||
|
|
@ -306,7 +349,8 @@ open_session(GwName, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun,
|
|||
ClientId = maps:get(clientid, ClientInfo),
|
||||
Fun = fun(_) ->
|
||||
_ = discard_session(GwName, ClientId),
|
||||
Session = create_session(GwName,
|
||||
Session = create_session(
|
||||
GwName,
|
||||
ClientInfo,
|
||||
ConnInfo,
|
||||
CreateSessionFun,
|
||||
|
|
@ -316,10 +360,14 @@ open_session(GwName, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun,
|
|||
{ok, #{session => Session, present => false}}
|
||||
end,
|
||||
locker_trans(GwName, ClientId, Fun);
|
||||
|
||||
open_session(GwName, false = _CleanStart,
|
||||
open_session(
|
||||
GwName,
|
||||
false = _CleanStart,
|
||||
ClientInfo = #{clientid := ClientId},
|
||||
ConnInfo, CreateSessionFun, SessionMod) ->
|
||||
ConnInfo,
|
||||
CreateSessionFun,
|
||||
SessionMod
|
||||
) ->
|
||||
Self = self(),
|
||||
|
||||
ResumeStart =
|
||||
|
|
@ -327,10 +375,15 @@ open_session(GwName, false = _CleanStart,
|
|||
CreateSess =
|
||||
fun() ->
|
||||
Session = create_session(
|
||||
GwName, ClientInfo, ConnInfo,
|
||||
CreateSessionFun, SessionMod),
|
||||
GwName,
|
||||
ClientInfo,
|
||||
ConnInfo,
|
||||
CreateSessionFun,
|
||||
SessionMod
|
||||
),
|
||||
register_channel(
|
||||
GwName, ClientId, Self, ConnInfo),
|
||||
GwName, ClientId, Self, ConnInfo
|
||||
),
|
||||
{ok, #{session => Session, present => false}}
|
||||
end,
|
||||
case takeover_session(GwName, ClientId) of
|
||||
|
|
@ -339,14 +392,18 @@ open_session(GwName, false = _CleanStart,
|
|||
case request_stepdown({takeover, 'end'}, ConnMod, ChanPid) of
|
||||
{ok, Pendings} ->
|
||||
register_channel(
|
||||
GwName, ClientId, Self, ConnInfo),
|
||||
{ok, #{session => Session,
|
||||
GwName, ClientId, Self, ConnInfo
|
||||
),
|
||||
{ok, #{
|
||||
session => Session,
|
||||
present => true,
|
||||
pendings => Pendings}};
|
||||
pendings => Pendings
|
||||
}};
|
||||
{error, _} ->
|
||||
CreateSess()
|
||||
end;
|
||||
{error, _Reason} -> CreateSess()
|
||||
{error, _Reason} ->
|
||||
CreateSess()
|
||||
end
|
||||
end,
|
||||
locker_trans(GwName, ClientId, ResumeStart).
|
||||
|
|
@ -359,9 +416,13 @@ create_session(GwName, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) ->
|
|||
[ClientInfo, ConnInfo]
|
||||
),
|
||||
ok = emqx_gateway_metrics:inc(GwName, 'session.created'),
|
||||
SessionInfo = case is_tuple(Session)
|
||||
andalso element(1, Session) == session of
|
||||
true -> SessionMod:info(Session);
|
||||
SessionInfo =
|
||||
case
|
||||
is_tuple(Session) andalso
|
||||
element(1, Session) == session
|
||||
of
|
||||
true ->
|
||||
SessionMod:info(Session);
|
||||
_ ->
|
||||
case is_map(Session) of
|
||||
false ->
|
||||
|
|
@ -373,33 +434,39 @@ create_session(GwName, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) ->
|
|||
ok = emqx_hooks:run('session.created', [ClientInfo, SessionInfo]),
|
||||
Session
|
||||
catch
|
||||
Class : Reason : Stk ->
|
||||
?SLOG(error, #{ msg => "failed_create_session"
|
||||
, clientid => maps:get(clientid, ClientInfo, undefined)
|
||||
, username => maps:get(username, ClientInfo, undefined)
|
||||
, reason => {Class, Reason}
|
||||
, stacktrace => Stk
|
||||
Class:Reason:Stk ->
|
||||
?SLOG(error, #{
|
||||
msg => "failed_create_session",
|
||||
clientid => maps:get(clientid, ClientInfo, undefined),
|
||||
username => maps:get(username, ClientInfo, undefined),
|
||||
reason => {Class, Reason},
|
||||
stacktrace => Stk
|
||||
}),
|
||||
throw(Reason)
|
||||
end.
|
||||
|
||||
%% @doc Try to takeover a session.
|
||||
-spec(takeover_session(gateway_name(), emqx_types:clientid())
|
||||
-> {error, term()}
|
||||
| {ok, atom(), pid(), emqx_session:session()}).
|
||||
-spec takeover_session(gateway_name(), emqx_types:clientid()) ->
|
||||
{error, term()}
|
||||
| {ok, atom(), pid(), emqx_session:session()}.
|
||||
takeover_session(GwName, ClientId) ->
|
||||
case lookup_channels(GwName, ClientId) of
|
||||
[] -> {error, not_found};
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[ChanPid] ->
|
||||
do_takeover_session(GwName, ClientId, ChanPid);
|
||||
ChanPids ->
|
||||
[ChanPid | StalePids] = lists:reverse(ChanPids),
|
||||
?SLOG(warning, #{ msg => "more_than_one_channel_found"
|
||||
, chan_pids => ChanPids
|
||||
?SLOG(warning, #{
|
||||
msg => "more_than_one_channel_found",
|
||||
chan_pids => ChanPids
|
||||
}),
|
||||
lists:foreach(fun(StalePid) ->
|
||||
lists:foreach(
|
||||
fun(StalePid) ->
|
||||
catch discard_session(GwName, ClientId, StalePid)
|
||||
end, StalePids),
|
||||
end,
|
||||
StalePids
|
||||
),
|
||||
do_takeover_session(GwName, ClientId, ChanPid)
|
||||
end.
|
||||
|
||||
|
|
@ -432,17 +499,26 @@ discard_session(GwName, ClientId, ChanPid) ->
|
|||
-spec kick_session(gateway_name(), emqx_types:clientid()) -> ok | {error, not_found}.
|
||||
kick_session(GwName, ClientId) ->
|
||||
case lookup_channels(GwName, ClientId) of
|
||||
[] -> {error, not_found};
|
||||
[] ->
|
||||
{error, not_found};
|
||||
ChanPids ->
|
||||
ChanPids > 1 andalso begin
|
||||
?SLOG(warning, #{ msg => "more_than_one_channel_found"
|
||||
, chan_pids => ChanPids
|
||||
ChanPids > 1 andalso
|
||||
begin
|
||||
?SLOG(
|
||||
warning,
|
||||
#{
|
||||
msg => "more_than_one_channel_found",
|
||||
chan_pids => ChanPids
|
||||
},
|
||||
#{clientid => ClientId})
|
||||
#{clientid => ClientId}
|
||||
)
|
||||
end,
|
||||
lists:foreach(fun(Pid) ->
|
||||
lists:foreach(
|
||||
fun(Pid) ->
|
||||
_ = kick_session(GwName, ClientId, Pid)
|
||||
end, ChanPids)
|
||||
end,
|
||||
ChanPids
|
||||
)
|
||||
end.
|
||||
|
||||
kick_session(GwName, ClientId, ChanPid) ->
|
||||
|
|
@ -453,27 +529,34 @@ kick_session(GwName, Action, ClientId, ChanPid) ->
|
|||
try
|
||||
wrap_rpc(emqx_gateway_cm_proto_v1:kick_session(GwName, Action, ClientId, ChanPid))
|
||||
catch
|
||||
Error : Reason ->
|
||||
Error:Reason ->
|
||||
%% This should mostly be RPC failures.
|
||||
%% However, if the node is still running the old version
|
||||
%% code (prior to emqx app 4.3.10) some of the RPC handler
|
||||
%% exceptions may get propagated to a new version node
|
||||
?SLOG(error, #{ msg => "failed_to_kick_session_on_remote_node"
|
||||
, node => node(ChanPid)
|
||||
, action => Action
|
||||
, error => Error
|
||||
, reason => Reason
|
||||
?SLOG(
|
||||
error,
|
||||
#{
|
||||
msg => "failed_to_kick_session_on_remote_node",
|
||||
node => node(ChanPid),
|
||||
action => Action,
|
||||
error => Error,
|
||||
reason => Reason
|
||||
},
|
||||
#{clientid => ClientId})
|
||||
#{clientid => ClientId}
|
||||
)
|
||||
end.
|
||||
|
||||
-spec do_kick_session(gateway_name(),
|
||||
-spec do_kick_session(
|
||||
gateway_name(),
|
||||
kick | discard,
|
||||
emqx_types:clientid(),
|
||||
pid()) -> ok.
|
||||
pid()
|
||||
) -> ok.
|
||||
do_kick_session(GwName, Action, ClientId, ChanPid) ->
|
||||
case get_chann_conn_mod(GwName, ClientId, ChanPid) of
|
||||
undefined -> ok;
|
||||
undefined ->
|
||||
ok;
|
||||
ConnMod when is_atom(ConnMod) ->
|
||||
ok = request_stepdown(Action, ConnMod, ChanPid)
|
||||
end.
|
||||
|
|
@ -482,11 +565,12 @@ do_kick_session(GwName, Action, ClientId, ChanPid) ->
|
|||
%% If failed to response (e.g. timeout) force a kill.
|
||||
%% Keeping the stale pid around, or returning error or raise an exception
|
||||
%% benefits nobody.
|
||||
-spec request_stepdown(Action, module(), pid())
|
||||
-> ok
|
||||
-spec request_stepdown(Action, module(), pid()) ->
|
||||
ok
|
||||
| {ok, emqx_session:session() | list(emqx_type:deliver())}
|
||||
| {error, term()}
|
||||
when Action :: kick | discard | {takeover, 'begin'} | {takeover, 'end'}.
|
||||
when
|
||||
Action :: kick | discard | {takeover, 'begin'} | {takeover, 'end'}.
|
||||
request_stepdown(Action, ConnMod, Pid) ->
|
||||
Timeout =
|
||||
case Action == kick orelse Action == discard of
|
||||
|
|
@ -501,34 +585,44 @@ request_stepdown(Action, ConnMod, Pid) ->
|
|||
ok -> ok;
|
||||
Reply -> {ok, Reply}
|
||||
catch
|
||||
_ : noproc -> % emqx_ws_connection: call
|
||||
% emqx_ws_connection: call
|
||||
_:noproc ->
|
||||
ok = ?tp(debug, "session_already_gone", #{pid => Pid, action => Action}),
|
||||
{error, noproc};
|
||||
_ : {noproc, _} -> % emqx_connection: gen_server:call
|
||||
% emqx_connection: gen_server:call
|
||||
_:{noproc, _} ->
|
||||
ok = ?tp(debug, "session_already_gone", #{pid => Pid, action => Action}),
|
||||
{error, noproc};
|
||||
_ : Reason = {shutdown, _} ->
|
||||
_:Reason = {shutdown, _} ->
|
||||
ok = ?tp(debug, "session_already_shutdown", #{pid => Pid, action => Action}),
|
||||
{error, Reason};
|
||||
_ : Reason = {{shutdown, _}, _} ->
|
||||
_:Reason = {{shutdown, _}, _} ->
|
||||
ok = ?tp(debug, "session_already_shutdown", #{pid => Pid, action => Action}),
|
||||
{error, Reason};
|
||||
_ : {timeout, {gen_server, call, _}} ->
|
||||
?tp(warning, "session_stepdown_request_timeout",
|
||||
#{pid => Pid,
|
||||
_:{timeout, {gen_server, call, _}} ->
|
||||
?tp(
|
||||
warning,
|
||||
"session_stepdown_request_timeout",
|
||||
#{
|
||||
pid => Pid,
|
||||
action => Action,
|
||||
stale_channel => stale_channel_info(Pid)
|
||||
}),
|
||||
}
|
||||
),
|
||||
ok = force_kill(Pid),
|
||||
{error, timeout};
|
||||
_ : Error : St ->
|
||||
?tp(error, "session_stepdown_request_exception",
|
||||
#{pid => Pid,
|
||||
_:Error:St ->
|
||||
?tp(
|
||||
error,
|
||||
"session_stepdown_request_exception",
|
||||
#{
|
||||
pid => Pid,
|
||||
action => Action,
|
||||
reason => Error,
|
||||
stacktrace => St,
|
||||
stale_channel => stale_channel_info(Pid)
|
||||
}),
|
||||
}
|
||||
),
|
||||
ok = force_kill(Pid),
|
||||
{error, Error}
|
||||
end,
|
||||
|
|
@ -552,14 +646,16 @@ with_channel(GwName, ClientId, Fun) ->
|
|||
end.
|
||||
|
||||
%% @doc Lookup channels.
|
||||
-spec(lookup_channels(gateway_name(), emqx_types:clientid()) -> list(pid())).
|
||||
-spec lookup_channels(gateway_name(), emqx_types:clientid()) -> list(pid()).
|
||||
lookup_channels(GwName, ClientId) ->
|
||||
emqx_gateway_cm_registry:lookup_channels(GwName, ClientId).
|
||||
|
||||
-spec do_get_chann_conn_mod(gateway_name(), emqx_types:clientid(), pid()) -> atom().
|
||||
do_get_chann_conn_mod(GwName, ClientId, ChanPid) ->
|
||||
Chan = {ClientId, ChanPid},
|
||||
try [ConnMod] = ets:lookup_element(tabname(conn, GwName), Chan, 2), ConnMod
|
||||
try
|
||||
[ConnMod] = ets:lookup_element(tabname(conn, GwName), Chan, 2),
|
||||
ConnMod
|
||||
catch
|
||||
error:badarg -> undefined
|
||||
end.
|
||||
|
|
@ -568,28 +664,33 @@ do_get_chann_conn_mod(GwName, ClientId, ChanPid) ->
|
|||
get_chann_conn_mod(GwName, ClientId, ChanPid) ->
|
||||
wrap_rpc(emqx_gateway_cm_proto_v1:get_chann_conn_mod(GwName, ClientId, ChanPid)).
|
||||
|
||||
-spec call(gateway_name(), emqx_types:clientid(), term())
|
||||
-> undefined | term().
|
||||
-spec call(gateway_name(), emqx_types:clientid(), term()) ->
|
||||
undefined | term().
|
||||
call(GwName, ClientId, Req) ->
|
||||
with_channel(
|
||||
GwName, ClientId,
|
||||
GwName,
|
||||
ClientId,
|
||||
fun(ChanPid) ->
|
||||
wrap_rpc(
|
||||
emqx_gateway_cm_proto_v1:call(GwName, ClientId, ChanPid, Req)
|
||||
)
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
-spec call(gateway_name(), emqx_types:clientid(), term(), timeout())
|
||||
-> undefined | term().
|
||||
-spec call(gateway_name(), emqx_types:clientid(), term(), timeout()) ->
|
||||
undefined | term().
|
||||
call(GwName, ClientId, Req, Timeout) ->
|
||||
with_channel(
|
||||
GwName, ClientId,
|
||||
GwName,
|
||||
ClientId,
|
||||
fun(ChanPid) ->
|
||||
wrap_rpc(
|
||||
emqx_gateway_cm_proto_v1:call(
|
||||
GwName, ClientId, ChanPid, Req, Timeout)
|
||||
GwName, ClientId, ChanPid, Req, Timeout
|
||||
)
|
||||
end).
|
||||
)
|
||||
end
|
||||
).
|
||||
|
||||
do_call(GwName, ClientId, ChanPid, Req) ->
|
||||
case do_get_chann_conn_mod(GwName, ClientId, ChanPid) of
|
||||
|
|
@ -606,11 +707,14 @@ do_call(GwName, ClientId, ChanPid, Req, Timeout) ->
|
|||
-spec cast(gateway_name(), emqx_types:clientid(), term()) -> ok.
|
||||
cast(GwName, ClientId, Req) ->
|
||||
with_channel(
|
||||
GwName, ClientId,
|
||||
GwName,
|
||||
ClientId,
|
||||
fun(ChanPid) ->
|
||||
wrap_rpc(
|
||||
emqx_gateway_cm_proto_v1:cast(GwName, ClientId, ChanPid, Req))
|
||||
end),
|
||||
emqx_gateway_cm_proto_v1:cast(GwName, ClientId, ChanPid, Req)
|
||||
)
|
||||
end
|
||||
),
|
||||
ok.
|
||||
|
||||
do_cast(GwName, ClientId, ChanPid, Req) ->
|
||||
|
|
@ -627,7 +731,11 @@ locker_trans(GwName, ClientId, Fun) ->
|
|||
Locker = lockername(GwName),
|
||||
case locker_lock(Locker, ClientId) of
|
||||
{true, Nodes} ->
|
||||
try Fun(Nodes) after locker_unlock(Locker, ClientId) end;
|
||||
try
|
||||
Fun(Nodes)
|
||||
after
|
||||
locker_unlock(Locker, ClientId)
|
||||
end;
|
||||
{false, _Nodes} ->
|
||||
{error, client_id_unavailable}
|
||||
end.
|
||||
|
|
@ -673,10 +781,12 @@ init(Options) ->
|
|||
%% TODO: v0.2
|
||||
%ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0),
|
||||
|
||||
{ok, #state{gwname = GwName,
|
||||
{ok, #state{
|
||||
gwname = GwName,
|
||||
locker = Locker,
|
||||
registry = Registry,
|
||||
chan_pmon = emqx_pmon:new()}}.
|
||||
chan_pmon = emqx_pmon:new()
|
||||
}}.
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
Reply = ok,
|
||||
|
|
@ -685,19 +795,19 @@ handle_call(_Request, _From, State) ->
|
|||
handle_cast({registered, {ClientId, ChanPid}}, State = #state{chan_pmon = PMon}) ->
|
||||
PMon1 = emqx_pmon:monitor(ChanPid, ClientId, PMon),
|
||||
{noreply, State#state{chan_pmon = PMon1}};
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info({'DOWN', _MRef, process, Pid, _Reason},
|
||||
State = #state{gwname = GwName, chan_pmon = PMon}) ->
|
||||
handle_info(
|
||||
{'DOWN', _MRef, process, Pid, _Reason},
|
||||
State = #state{gwname = GwName, chan_pmon = PMon}
|
||||
) ->
|
||||
ChanPids = [Pid | emqx_misc:drain_down(?DEFAULT_BATCH_SIZE)],
|
||||
{Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon),
|
||||
|
||||
CmTabs = cmtabs(GwName),
|
||||
ok = emqx_pool:async_submit(fun do_unregister_channel_task/3, [Items, GwName, CmTabs]),
|
||||
{noreply, State#state{chan_pmon = PMon1}};
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
|
|
@ -713,7 +823,9 @@ do_unregister_channel_task(Items, GwName, CmTabs) ->
|
|||
lists:foreach(
|
||||
fun({ChanPid, ClientId}) ->
|
||||
do_unregister_channel(GwName, {ClientId, ChanPid}, CmTabs)
|
||||
end, Items).
|
||||
end,
|
||||
Items
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal funcs
|
||||
|
|
|
|||
|
|
@ -23,22 +23,24 @@
|
|||
|
||||
-export([start_link/1]).
|
||||
|
||||
-export([ register_channel/2
|
||||
, unregister_channel/2
|
||||
]).
|
||||
-export([
|
||||
register_channel/2,
|
||||
unregister_channel/2
|
||||
]).
|
||||
|
||||
-export([lookup_channels/2]).
|
||||
|
||||
-export([tabname/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-define(CM_SHARD, emqx_gateway_cm_shard).
|
||||
-define(LOCK, {?MODULE, cleanup_down}).
|
||||
|
|
@ -46,7 +48,7 @@
|
|||
-record(channel, {chid, pid}).
|
||||
|
||||
%% @doc Start the global channel registry for the given gateway name.
|
||||
-spec(start_link(gateway_name()) -> gen_server:startlink_ret()).
|
||||
-spec start_link(gateway_name()) -> gen_server:startlink_ret().
|
||||
start_link(Name) ->
|
||||
gen_server:start_link(?MODULE, [Name], []).
|
||||
|
||||
|
|
@ -63,25 +65,27 @@ tabname(Name) ->
|
|||
-spec register_channel(gateway_name(), binary() | {binary(), pid()}) -> ok.
|
||||
register_channel(Name, ClientId) when is_binary(ClientId) ->
|
||||
register_channel(Name, {ClientId, self()});
|
||||
|
||||
register_channel(Name, {ClientId, ChanPid})
|
||||
when is_binary(ClientId), is_pid(ChanPid) ->
|
||||
register_channel(Name, {ClientId, ChanPid}) when
|
||||
is_binary(ClientId), is_pid(ChanPid)
|
||||
->
|
||||
mria:dirty_write(tabname(Name), record(ClientId, ChanPid)).
|
||||
|
||||
%% @doc Unregister a global channel.
|
||||
-spec unregister_channel(gateway_name(), binary() | {binary(), pid()}) -> ok.
|
||||
unregister_channel(Name, ClientId) when is_binary(ClientId) ->
|
||||
unregister_channel(Name, {ClientId, self()});
|
||||
|
||||
unregister_channel(Name, {ClientId, ChanPid})
|
||||
when is_binary(ClientId), is_pid(ChanPid) ->
|
||||
unregister_channel(Name, {ClientId, ChanPid}) when
|
||||
is_binary(ClientId), is_pid(ChanPid)
|
||||
->
|
||||
mria:dirty_delete_object(tabname(Name), record(ClientId, ChanPid)).
|
||||
|
||||
%% @doc Lookup the global channels.
|
||||
-spec lookup_channels(gateway_name(), binary()) -> list(pid()).
|
||||
lookup_channels(Name, ClientId) ->
|
||||
[ChanPid
|
||||
|| #channel{pid = ChanPid} <- mnesia:dirty_read(tabname(Name), ClientId)].
|
||||
[
|
||||
ChanPid
|
||||
|| #channel{pid = ChanPid} <- mnesia:dirty_read(tabname(Name), ClientId)
|
||||
].
|
||||
|
||||
record(ClientId, ChanPid) ->
|
||||
#channel{chid = ClientId, pid = ChanPid}.
|
||||
|
|
@ -98,8 +102,13 @@ init([Name]) ->
|
|||
{storage, ram_copies},
|
||||
{record_name, channel},
|
||||
{attributes, record_info(fields, channel)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true},
|
||||
{write_concurrency, true}]}]}]),
|
||||
{storage_properties, [
|
||||
{ets, [
|
||||
{read_concurrency, true},
|
||||
{write_concurrency, true}
|
||||
]}
|
||||
]}
|
||||
]),
|
||||
ok = mria:wait_for_tables([Tab]),
|
||||
ok = ekka:monitor(membership),
|
||||
{ok, #{name => Name}}.
|
||||
|
|
@ -115,14 +124,11 @@ handle_cast(Msg, State) ->
|
|||
handle_info({membership, {mnesia, down, Node}}, State = #{name := Name}) ->
|
||||
cleanup_channels(Node, Name),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({membership, {node, down, Node}}, State = #{name := Name}) ->
|
||||
cleanup_channels(Node, Name),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({membership, _Event}, State) ->
|
||||
{noreply, State};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
logger:error("Unexpected info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
|
@ -143,10 +149,14 @@ cleanup_channels(Node, Name) ->
|
|||
{?LOCK, self()},
|
||||
fun() ->
|
||||
mria:transaction(?CM_SHARD, fun do_cleanup_channels/2, [Node, Tab])
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
do_cleanup_channels(Node, Tab) ->
|
||||
Pat = [{#channel{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}],
|
||||
lists:foreach(fun(Chan) ->
|
||||
lists:foreach(
|
||||
fun(Chan) ->
|
||||
mnesia:delete_object(Tab, Chan, write)
|
||||
end, mnesia:select(Tab, Pat, write)).
|
||||
end,
|
||||
mnesia:select(Tab, Pat, write)
|
||||
).
|
||||
|
|
|
|||
|
|
@ -20,41 +20,47 @@
|
|||
-behaviour(emqx_config_handler).
|
||||
|
||||
%% Load/Unload
|
||||
-export([ load/0
|
||||
, unload/0
|
||||
]).
|
||||
-export([
|
||||
load/0,
|
||||
unload/0
|
||||
]).
|
||||
|
||||
%% APIs
|
||||
-export([ gateway/1
|
||||
, load_gateway/2
|
||||
, update_gateway/2
|
||||
, unload_gateway/1
|
||||
]).
|
||||
-export([
|
||||
gateway/1,
|
||||
load_gateway/2,
|
||||
update_gateway/2,
|
||||
unload_gateway/1
|
||||
]).
|
||||
|
||||
-export([ listeners/1
|
||||
, listener/1
|
||||
, add_listener/3
|
||||
, update_listener/3
|
||||
, remove_listener/2
|
||||
]).
|
||||
-export([
|
||||
listeners/1,
|
||||
listener/1,
|
||||
add_listener/3,
|
||||
update_listener/3,
|
||||
remove_listener/2
|
||||
]).
|
||||
|
||||
-export([ add_authn/2
|
||||
, add_authn/3
|
||||
, update_authn/2
|
||||
, update_authn/3
|
||||
, remove_authn/1
|
||||
, remove_authn/2
|
||||
]).
|
||||
-export([
|
||||
add_authn/2,
|
||||
add_authn/3,
|
||||
update_authn/2,
|
||||
update_authn/3,
|
||||
remove_authn/1,
|
||||
remove_authn/2
|
||||
]).
|
||||
|
||||
%% internal exports
|
||||
-export([ unconvert_listeners/1
|
||||
, convert_listeners/2
|
||||
]).
|
||||
-export([
|
||||
unconvert_listeners/1,
|
||||
convert_listeners/2
|
||||
]).
|
||||
|
||||
%% callbacks for emqx_config_handler
|
||||
-export([ pre_config_update/3
|
||||
, post_config_update/5
|
||||
]).
|
||||
-export([
|
||||
pre_config_update/3,
|
||||
post_config_update/5
|
||||
]).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/emqx_authentication.hrl").
|
||||
|
|
@ -63,8 +69,7 @@
|
|||
-type atom_or_bin() :: atom() | binary().
|
||||
-type ok_or_err() :: ok | {error, term()}.
|
||||
-type map_or_err() :: {ok, map()} | {error, term()}.
|
||||
-type listener_ref() :: {ListenerType :: atom_or_bin(),
|
||||
ListenerName :: atom_or_bin()}.
|
||||
-type listener_ref() :: {ListenerType :: atom_or_bin(), ListenerName :: atom_or_bin()}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Load/Unload
|
||||
|
|
@ -83,21 +88,25 @@ unload() ->
|
|||
|
||||
-spec load_gateway(atom_or_bin(), map()) -> map_or_err().
|
||||
load_gateway(GwName, Conf) ->
|
||||
NConf = case maps:take(<<"listeners">>, Conf) of
|
||||
NConf =
|
||||
case maps:take(<<"listeners">>, Conf) of
|
||||
error -> Conf;
|
||||
{Ls, Conf1} ->
|
||||
Conf1#{<<"listeners">> => unconvert_listeners(Ls)}
|
||||
{Ls, Conf1} -> Conf1#{<<"listeners">> => unconvert_listeners(Ls)}
|
||||
end,
|
||||
ret_gw(GwName, update({?FUNCTION_NAME, bin(GwName), NConf})).
|
||||
|
||||
%% @doc convert listener array to map
|
||||
unconvert_listeners(Ls) when is_list(Ls) ->
|
||||
lists:foldl(fun(Lis, Acc) ->
|
||||
lists:foldl(
|
||||
fun(Lis, Acc) ->
|
||||
%% FIXME: params apperence guard?
|
||||
{[Type, Name], Lis1} = maps_key_take([<<"type">>, <<"name">>], Lis),
|
||||
NLis1 = maps:without([<<"id">>], Lis1),
|
||||
emqx_map_lib:deep_merge(Acc, #{Type => #{Name => NLis1}})
|
||||
end, #{}, Ls).
|
||||
end,
|
||||
#{},
|
||||
Ls
|
||||
).
|
||||
|
||||
maps_key_take(Ks, M) ->
|
||||
maps_key_take(Ks, M, []).
|
||||
|
|
@ -106,8 +115,7 @@ maps_key_take([], M, Acc) ->
|
|||
maps_key_take([K | Ks], M, Acc) ->
|
||||
case maps:take(K, M) of
|
||||
error -> throw(bad_key);
|
||||
{V, M1} ->
|
||||
maps_key_take(Ks, M1, [V | Acc])
|
||||
{V, M1} -> maps_key_take(Ks, M1, [V | Acc])
|
||||
end.
|
||||
|
||||
-spec update_gateway(atom_or_bin(), map()) -> map_or_err().
|
||||
|
|
@ -134,18 +142,23 @@ gateway(GwName0) ->
|
|||
emqx_config:get_root_raw(Path)
|
||||
),
|
||||
Confs = emqx_map_lib:jsonable_map(
|
||||
emqx_map_lib:deep_get(Path, RawConf)),
|
||||
emqx_map_lib:deep_get(Path, RawConf)
|
||||
),
|
||||
LsConf = maps:get(<<"listeners">>, Confs, #{}),
|
||||
Confs#{<<"listeners">> => convert_listeners(GwName, LsConf)}.
|
||||
|
||||
%% @doc convert listeners map to array
|
||||
convert_listeners(GwName, Ls) when is_map(Ls) ->
|
||||
lists:append([do_convert_listener(GwName, Type, maps:to_list(Conf))
|
||||
|| {Type, Conf} <- maps:to_list(Ls)]).
|
||||
lists:append([
|
||||
do_convert_listener(GwName, Type, maps:to_list(Conf))
|
||||
|| {Type, Conf} <- maps:to_list(Ls)
|
||||
]).
|
||||
|
||||
do_convert_listener(GwName, LType, Conf) ->
|
||||
[ do_convert_listener2(GwName, LType, LName, LConf)
|
||||
|| {LName, LConf} <- Conf, is_map(LConf)].
|
||||
[
|
||||
do_convert_listener2(GwName, LType, LName, LConf)
|
||||
|| {LName, LConf} <- Conf, is_map(LConf)
|
||||
].
|
||||
|
||||
do_convert_listener2(GwName, LType, LName, LConf) ->
|
||||
ListenerId = emqx_gateway_utils:listener_id(GwName, LType, LName),
|
||||
|
|
@ -156,7 +169,8 @@ do_convert_listener2(GwName, LType, LName, LConf) ->
|
|||
type => LType,
|
||||
name => LName,
|
||||
running => Running
|
||||
}).
|
||||
}
|
||||
).
|
||||
|
||||
bind2str(LConf = #{bind := Bind}) when is_integer(Bind) ->
|
||||
maps:put(bind, integer_to_binary(Bind), LConf);
|
||||
|
|
@ -171,46 +185,58 @@ bind2str(LConf = #{<<"bind">> := Bind}) when is_binary(Bind) ->
|
|||
listeners(GwName0) ->
|
||||
GwName = bin(GwName0),
|
||||
RawConf = emqx_config:fill_defaults(
|
||||
emqx_config:get_root_raw([<<"gateway">>])),
|
||||
emqx_config:get_root_raw([<<"gateway">>])
|
||||
),
|
||||
Listeners = emqx_map_lib:jsonable_map(
|
||||
emqx_map_lib:deep_get(
|
||||
[<<"gateway">>, GwName, <<"listeners">>], RawConf)),
|
||||
[<<"gateway">>, GwName, <<"listeners">>], RawConf
|
||||
)
|
||||
),
|
||||
convert_listeners(GwName, Listeners).
|
||||
|
||||
-spec listener(binary()) -> {ok, map()} | {error, not_found} | {error, any()}.
|
||||
listener(ListenerId) ->
|
||||
{GwName, Type, LName} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||
RootConf = emqx_config:fill_defaults(
|
||||
emqx_config:get_root_raw([<<"gateway">>])),
|
||||
emqx_config:get_root_raw([<<"gateway">>])
|
||||
),
|
||||
try
|
||||
Path = [<<"gateway">>, GwName, <<"listeners">>, Type, LName],
|
||||
LConf = emqx_map_lib:deep_get(Path, RootConf),
|
||||
Running = emqx_gateway_utils:is_running(
|
||||
binary_to_existing_atom(ListenerId), LConf),
|
||||
{ok, emqx_map_lib:jsonable_map(
|
||||
binary_to_existing_atom(ListenerId), LConf
|
||||
),
|
||||
{ok,
|
||||
emqx_map_lib:jsonable_map(
|
||||
LConf#{
|
||||
id => ListenerId,
|
||||
type => Type,
|
||||
name => LName,
|
||||
running => Running})}
|
||||
running => Running
|
||||
}
|
||||
)}
|
||||
catch
|
||||
error : {config_not_found, _} ->
|
||||
error:{config_not_found, _} ->
|
||||
{error, not_found};
|
||||
_Class : Reason ->
|
||||
_Class:Reason ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec add_listener(atom_or_bin(), listener_ref(), map()) -> map_or_err().
|
||||
add_listener(GwName, ListenerRef, Conf) ->
|
||||
ret_listener_or_err(
|
||||
GwName, ListenerRef,
|
||||
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf})).
|
||||
GwName,
|
||||
ListenerRef,
|
||||
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf})
|
||||
).
|
||||
|
||||
-spec update_listener(atom_or_bin(), listener_ref(), map()) -> map_or_err().
|
||||
update_listener(GwName, ListenerRef, Conf) ->
|
||||
ret_listener_or_err(
|
||||
GwName, ListenerRef,
|
||||
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf})).
|
||||
GwName,
|
||||
ListenerRef,
|
||||
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf})
|
||||
).
|
||||
|
||||
-spec remove_listener(atom_or_bin(), listener_ref()) -> ok_or_err().
|
||||
remove_listener(GwName, ListenerRef) ->
|
||||
|
|
@ -223,8 +249,10 @@ add_authn(GwName, Conf) ->
|
|||
-spec add_authn(atom_or_bin(), listener_ref(), map()) -> map_or_err().
|
||||
add_authn(GwName, ListenerRef, Conf) ->
|
||||
ret_authn(
|
||||
GwName, ListenerRef,
|
||||
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf})).
|
||||
GwName,
|
||||
ListenerRef,
|
||||
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf})
|
||||
).
|
||||
|
||||
-spec update_authn(atom_or_bin(), map()) -> map_or_err().
|
||||
update_authn(GwName, Conf) ->
|
||||
|
|
@ -233,8 +261,10 @@ update_authn(GwName, Conf) ->
|
|||
-spec update_authn(atom_or_bin(), listener_ref(), map()) -> map_or_err().
|
||||
update_authn(GwName, ListenerRef, Conf) ->
|
||||
ret_authn(
|
||||
GwName, ListenerRef,
|
||||
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf})).
|
||||
GwName,
|
||||
ListenerRef,
|
||||
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf})
|
||||
).
|
||||
|
||||
-spec remove_authn(atom_or_bin()) -> ok_or_err().
|
||||
remove_authn(GwName) ->
|
||||
|
|
@ -249,8 +279,8 @@ update(Req) ->
|
|||
res(emqx_conf:update([gateway], Req, #{override_to => cluster})).
|
||||
|
||||
res({ok, Result}) -> {ok, Result};
|
||||
res({error, {pre_config_update,?MODULE,Reason}}) -> {error, Reason};
|
||||
res({error, {post_config_update,?MODULE,Reason}}) -> {error, Reason};
|
||||
res({error, {pre_config_update, ?MODULE, Reason}}) -> {error, Reason};
|
||||
res({error, {post_config_update, ?MODULE, Reason}}) -> {error, Reason};
|
||||
res({error, Reason}) -> {error, Reason}.
|
||||
|
||||
bin({LType, LName}) ->
|
||||
|
|
@ -267,37 +297,57 @@ ret_gw(GwName, {ok, #{raw_config := GwConf}}) ->
|
|||
GwConf1 = emqx_map_lib:deep_get([bin(GwName)], GwConf),
|
||||
LsConf = emqx_map_lib:deep_get(
|
||||
[bin(GwName), <<"listeners">>],
|
||||
GwConf, #{}),
|
||||
GwConf,
|
||||
#{}
|
||||
),
|
||||
NLsConf =
|
||||
lists:foldl(fun({LType, SubConf}, Acc) ->
|
||||
lists:foldl(
|
||||
fun({LType, SubConf}, Acc) ->
|
||||
NLConfs =
|
||||
lists:map(fun({LName, LConf}) ->
|
||||
lists:map(
|
||||
fun({LName, LConf}) ->
|
||||
do_convert_listener2(GwName, LType, LName, LConf)
|
||||
end, maps:to_list(SubConf)),
|
||||
end,
|
||||
maps:to_list(SubConf)
|
||||
),
|
||||
[NLConfs | Acc]
|
||||
end, [], maps:to_list(LsConf)),
|
||||
end,
|
||||
[],
|
||||
maps:to_list(LsConf)
|
||||
),
|
||||
{ok, maps:merge(GwConf1, #{<<"listeners">> => lists:append(NLsConf)})};
|
||||
ret_gw(_GwName, Err) -> Err.
|
||||
ret_gw(_GwName, Err) ->
|
||||
Err.
|
||||
|
||||
ret_authn(GwName, {ok, #{raw_config := GwConf}}) ->
|
||||
Authn = emqx_map_lib:deep_get(
|
||||
[bin(GwName), <<"authentication">>],
|
||||
GwConf),
|
||||
GwConf
|
||||
),
|
||||
{ok, Authn};
|
||||
ret_authn(_GwName, Err) -> Err.
|
||||
ret_authn(_GwName, Err) ->
|
||||
Err.
|
||||
|
||||
ret_authn(GwName, {LType, LName}, {ok, #{raw_config := GwConf}}) ->
|
||||
Authn = emqx_map_lib:deep_get(
|
||||
[bin(GwName), <<"listeners">>, bin(LType),
|
||||
bin(LName), <<"authentication">>],
|
||||
GwConf),
|
||||
[
|
||||
bin(GwName),
|
||||
<<"listeners">>,
|
||||
bin(LType),
|
||||
bin(LName),
|
||||
<<"authentication">>
|
||||
],
|
||||
GwConf
|
||||
),
|
||||
{ok, Authn};
|
||||
ret_authn(_, _, Err) -> Err.
|
||||
ret_authn(_, _, Err) ->
|
||||
Err.
|
||||
|
||||
ret_listener_or_err(GwName, {LType, LName}, {ok, #{raw_config := GwConf}}) ->
|
||||
LConf = emqx_map_lib:deep_get(
|
||||
[bin(GwName), <<"listeners">>, bin(LType), bin(LName)],
|
||||
GwConf),
|
||||
GwConf
|
||||
),
|
||||
{ok, do_convert_listener2(GwName, LType, LName, LConf)};
|
||||
ret_listener_or_err(_, _, Err) ->
|
||||
Err.
|
||||
|
|
@ -306,9 +356,11 @@ ret_listener_or_err(_, _, Err) ->
|
|||
%% Config Handler
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec pre_config_update(list(atom()),
|
||||
-spec pre_config_update(
|
||||
list(atom()),
|
||||
emqx_config:update_request(),
|
||||
emqx_config:raw_config()) ->
|
||||
emqx_config:raw_config()
|
||||
) ->
|
||||
{ok, emqx_config:update_request()} | {error, term()}.
|
||||
pre_config_update(_, {load_gateway, GwName, Conf}, RawConf) ->
|
||||
case maps:get(GwName, RawConf, undefined) of
|
||||
|
|
@ -327,36 +379,45 @@ pre_config_update(_, {update_gateway, GwName, Conf}, RawConf) ->
|
|||
{ok, emqx_map_lib:deep_merge(RawConf, #{GwName => NConf})}
|
||||
end;
|
||||
pre_config_update(_, {unload_gateway, GwName}, RawConf) ->
|
||||
_ = tune_gw_certs(fun clear_certs/2,
|
||||
_ = tune_gw_certs(
|
||||
fun clear_certs/2,
|
||||
GwName,
|
||||
maps:get(GwName, RawConf, #{})
|
||||
),
|
||||
{ok, maps:remove(GwName, RawConf)};
|
||||
|
||||
pre_config_update(_, {add_listener, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||
case emqx_map_lib:deep_get(
|
||||
[GwName, <<"listeners">>, LType, LName], RawConf, undefined) of
|
||||
case
|
||||
emqx_map_lib:deep_get(
|
||||
[GwName, <<"listeners">>, LType, LName], RawConf, undefined
|
||||
)
|
||||
of
|
||||
undefined ->
|
||||
NConf = convert_certs(certs_dir(GwName), Conf),
|
||||
NListener = #{LType => #{LName => NConf}},
|
||||
{ok, emqx_map_lib:deep_merge(
|
||||
{ok,
|
||||
emqx_map_lib:deep_merge(
|
||||
RawConf,
|
||||
#{GwName => #{<<"listeners">> => NListener}})};
|
||||
#{GwName => #{<<"listeners">> => NListener}}
|
||||
)};
|
||||
_ ->
|
||||
badres_listener(already_exist, GwName, LType, LName)
|
||||
end;
|
||||
pre_config_update(_, {update_listener, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||
case emqx_map_lib:deep_get(
|
||||
[GwName, <<"listeners">>, LType, LName], RawConf, undefined) of
|
||||
case
|
||||
emqx_map_lib:deep_get(
|
||||
[GwName, <<"listeners">>, LType, LName], RawConf, undefined
|
||||
)
|
||||
of
|
||||
undefined ->
|
||||
badres_listener(not_found, GwName, LType, LName);
|
||||
OldConf ->
|
||||
NConf = convert_certs(certs_dir(GwName), Conf, OldConf),
|
||||
NListener = #{LType => #{LName => NConf}},
|
||||
{ok, emqx_map_lib:deep_merge(
|
||||
{ok,
|
||||
emqx_map_lib:deep_merge(
|
||||
RawConf,
|
||||
#{GwName => #{<<"listeners">> => NListener}})}
|
||||
|
||||
#{GwName => #{<<"listeners">> => NListener}}
|
||||
)}
|
||||
end;
|
||||
pre_config_update(_, {remove_listener, GwName, {LType, LName}}, RawConf) ->
|
||||
Path = [GwName, <<"listeners">>, LType, LName],
|
||||
|
|
@ -367,49 +428,70 @@ pre_config_update(_, {remove_listener, GwName, {LType, LName}}, RawConf) ->
|
|||
clear_certs(certs_dir(GwName), OldConf),
|
||||
{ok, emqx_map_lib:deep_remove(Path, RawConf)}
|
||||
end;
|
||||
|
||||
pre_config_update(_, {add_authn, GwName, Conf}, RawConf) ->
|
||||
case emqx_map_lib:deep_get(
|
||||
[GwName, ?AUTHN_BIN], RawConf, undefined) of
|
||||
case
|
||||
emqx_map_lib:deep_get(
|
||||
[GwName, ?AUTHN_BIN], RawConf, undefined
|
||||
)
|
||||
of
|
||||
undefined ->
|
||||
{ok, emqx_map_lib:deep_merge(
|
||||
{ok,
|
||||
emqx_map_lib:deep_merge(
|
||||
RawConf,
|
||||
#{GwName => #{?AUTHN_BIN => Conf}})};
|
||||
#{GwName => #{?AUTHN_BIN => Conf}}
|
||||
)};
|
||||
_ ->
|
||||
badres_authn(already_exist, GwName)
|
||||
end;
|
||||
pre_config_update(_, {add_authn, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||
case emqx_map_lib:deep_get(
|
||||
case
|
||||
emqx_map_lib:deep_get(
|
||||
[GwName, <<"listeners">>, LType, LName],
|
||||
RawConf, undefined) of
|
||||
RawConf,
|
||||
undefined
|
||||
)
|
||||
of
|
||||
undefined ->
|
||||
badres_listener(not_found, GwName, LType, LName);
|
||||
Listener ->
|
||||
case maps:get(?AUTHN_BIN, Listener, undefined) of
|
||||
undefined ->
|
||||
NListener = maps:put(?AUTHN_BIN, Conf, Listener),
|
||||
NGateway = #{GwName =>
|
||||
#{<<"listeners">> =>
|
||||
#{LType => #{LName => NListener}}}},
|
||||
NGateway = #{
|
||||
GwName =>
|
||||
#{
|
||||
<<"listeners">> =>
|
||||
#{LType => #{LName => NListener}}
|
||||
}
|
||||
},
|
||||
{ok, emqx_map_lib:deep_merge(RawConf, NGateway)};
|
||||
_ ->
|
||||
badres_listener_authn(already_exist, GwName, LType, LName)
|
||||
end
|
||||
end;
|
||||
pre_config_update(_, {update_authn, GwName, Conf}, RawConf) ->
|
||||
case emqx_map_lib:deep_get(
|
||||
[GwName, ?AUTHN_BIN], RawConf, undefined) of
|
||||
case
|
||||
emqx_map_lib:deep_get(
|
||||
[GwName, ?AUTHN_BIN], RawConf, undefined
|
||||
)
|
||||
of
|
||||
undefined ->
|
||||
badres_authn(not_found, GwName);
|
||||
_ ->
|
||||
{ok, emqx_map_lib:deep_merge(
|
||||
{ok,
|
||||
emqx_map_lib:deep_merge(
|
||||
RawConf,
|
||||
#{GwName => #{?AUTHN_BIN => Conf}})}
|
||||
#{GwName => #{?AUTHN_BIN => Conf}}
|
||||
)}
|
||||
end;
|
||||
pre_config_update(_, {update_authn, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||
case emqx_map_lib:deep_get(
|
||||
case
|
||||
emqx_map_lib:deep_get(
|
||||
[GwName, <<"listeners">>, LType, LName],
|
||||
RawConf, undefined) of
|
||||
RawConf,
|
||||
undefined
|
||||
)
|
||||
of
|
||||
undefined ->
|
||||
badres_listener(not_found, GwName, LType, LName);
|
||||
Listener ->
|
||||
|
|
@ -422,69 +504,109 @@ pre_config_update(_, {update_authn, GwName, {LType, LName}, Conf}, RawConf) ->
|
|||
emqx_map_lib:deep_merge(Auth, Conf),
|
||||
Listener
|
||||
),
|
||||
NGateway = #{GwName =>
|
||||
#{<<"listeners">> =>
|
||||
#{LType => #{LName => NListener}}}},
|
||||
NGateway = #{
|
||||
GwName =>
|
||||
#{
|
||||
<<"listeners">> =>
|
||||
#{LType => #{LName => NListener}}
|
||||
}
|
||||
},
|
||||
{ok, emqx_map_lib:deep_merge(RawConf, NGateway)}
|
||||
end
|
||||
end;
|
||||
pre_config_update(_, {remove_authn, GwName}, RawConf) ->
|
||||
{ok, emqx_map_lib:deep_remove(
|
||||
[GwName, ?AUTHN_BIN], RawConf)};
|
||||
{ok,
|
||||
emqx_map_lib:deep_remove(
|
||||
[GwName, ?AUTHN_BIN], RawConf
|
||||
)};
|
||||
pre_config_update(_, {remove_authn, GwName, {LType, LName}}, RawConf) ->
|
||||
Path = [GwName, <<"listeners">>, LType, LName, ?AUTHN_BIN],
|
||||
{ok, emqx_map_lib:deep_remove(Path, RawConf)};
|
||||
|
||||
pre_config_update(_, UnknownReq, _RawConf) ->
|
||||
logger:error("Unknown configuration update request: ~0p", [UnknownReq]),
|
||||
{error, badreq}.
|
||||
|
||||
badres_gateway(not_found, GwName) ->
|
||||
{error, {badres, #{resource => gateway, gateway => GwName,
|
||||
reason => not_found}}};
|
||||
{error,
|
||||
{badres, #{
|
||||
resource => gateway,
|
||||
gateway => GwName,
|
||||
reason => not_found
|
||||
}}};
|
||||
badres_gateway(already_exist, GwName) ->
|
||||
{error, {badres, #{resource => gateway, gateway => GwName,
|
||||
reason => already_exist}}}.
|
||||
{error,
|
||||
{badres, #{
|
||||
resource => gateway,
|
||||
gateway => GwName,
|
||||
reason => already_exist
|
||||
}}}.
|
||||
|
||||
badres_listener(not_found, GwName, LType, LName) ->
|
||||
{error, {badres, #{resource => listener, gateway => GwName,
|
||||
{error,
|
||||
{badres, #{
|
||||
resource => listener,
|
||||
gateway => GwName,
|
||||
listener => {GwName, LType, LName},
|
||||
reason => not_found}}};
|
||||
reason => not_found
|
||||
}}};
|
||||
badres_listener(already_exist, GwName, LType, LName) ->
|
||||
{error, {badres, #{resource => listener, gateway => GwName,
|
||||
{error,
|
||||
{badres, #{
|
||||
resource => listener,
|
||||
gateway => GwName,
|
||||
listener => {GwName, LType, LName},
|
||||
reason => already_exist}}}.
|
||||
reason => already_exist
|
||||
}}}.
|
||||
|
||||
badres_authn(not_found, GwName) ->
|
||||
{error, {badres, #{resource => authn, gateway => GwName,
|
||||
reason => not_found}}};
|
||||
{error,
|
||||
{badres, #{
|
||||
resource => authn,
|
||||
gateway => GwName,
|
||||
reason => not_found
|
||||
}}};
|
||||
badres_authn(already_exist, GwName) ->
|
||||
{error, {badres, #{resource => authn, gateway => GwName,
|
||||
reason => already_exist}}}.
|
||||
{error,
|
||||
{badres, #{
|
||||
resource => authn,
|
||||
gateway => GwName,
|
||||
reason => already_exist
|
||||
}}}.
|
||||
|
||||
badres_listener_authn(not_found, GwName, LType, LName) ->
|
||||
{error, {badres, #{resource => listener_authn, gateway => GwName,
|
||||
{error,
|
||||
{badres, #{
|
||||
resource => listener_authn,
|
||||
gateway => GwName,
|
||||
listener => {GwName, LType, LName},
|
||||
reason => not_found}}};
|
||||
reason => not_found
|
||||
}}};
|
||||
badres_listener_authn(already_exist, GwName, LType, LName) ->
|
||||
{error, {badres, #{resource => listener_authn, gateway => GwName,
|
||||
{error,
|
||||
{badres, #{
|
||||
resource => listener_authn,
|
||||
gateway => GwName,
|
||||
listener => {GwName, LType, LName},
|
||||
reason => already_exist}}}.
|
||||
reason => already_exist
|
||||
}}}.
|
||||
|
||||
-spec post_config_update(list(atom()),
|
||||
-spec post_config_update(
|
||||
list(atom()),
|
||||
emqx_config:update_request(),
|
||||
emqx_config:config(),
|
||||
emqx_config:config(), emqx_config:app_envs())
|
||||
-> ok | {ok, Result::any()} | {error, Reason::term()}.
|
||||
emqx_config:config(),
|
||||
emqx_config:app_envs()
|
||||
) ->
|
||||
ok | {ok, Result :: any()} | {error, Reason :: term()}.
|
||||
|
||||
post_config_update(_, Req, NewConfig, OldConfig, _AppEnvs) when is_tuple(Req) ->
|
||||
[_Tag, GwName0 | _] = tuple_to_list(Req),
|
||||
GwName = binary_to_existing_atom(GwName0),
|
||||
|
||||
case {maps:get(GwName, NewConfig, undefined),
|
||||
maps:get(GwName, OldConfig, undefined)} of
|
||||
case {maps:get(GwName, NewConfig, undefined), maps:get(GwName, OldConfig, undefined)} of
|
||||
{undefined, undefined} ->
|
||||
ok; %% nothing to change
|
||||
%% nothing to change
|
||||
ok;
|
||||
{undefined, Old} when is_map(Old) ->
|
||||
emqx_gateway:unload(GwName);
|
||||
{New, undefined} when is_map(New) ->
|
||||
|
|
@ -499,29 +621,39 @@ post_config_update(_, _Req, _NewConfig, _OldConfig, _AppEnvs) ->
|
|||
%% Internal funcs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
|
||||
tune_gw_certs(Fun, GwName, Conf) ->
|
||||
SubDir = certs_dir(GwName),
|
||||
case maps:get(<<"listeners">>, Conf, undefined) of
|
||||
undefined -> Conf;
|
||||
undefined ->
|
||||
Conf;
|
||||
Liss ->
|
||||
maps:put(<<"listeners">>,
|
||||
maps:map(fun(_, Lis) ->
|
||||
maps:map(fun(_, LisConf) ->
|
||||
maps:put(
|
||||
<<"listeners">>,
|
||||
maps:map(
|
||||
fun(_, Lis) ->
|
||||
maps:map(
|
||||
fun(_, LisConf) ->
|
||||
erlang:apply(Fun, [SubDir, LisConf])
|
||||
end, Lis)
|
||||
end, Liss),
|
||||
Conf)
|
||||
end,
|
||||
Lis
|
||||
)
|
||||
end,
|
||||
Liss
|
||||
),
|
||||
Conf
|
||||
)
|
||||
end.
|
||||
|
||||
certs_dir(GwName) when is_binary(GwName) ->
|
||||
GwName.
|
||||
|
||||
convert_certs(SubDir, Conf) ->
|
||||
case emqx_tls_lib:ensure_ssl_files(
|
||||
case
|
||||
emqx_tls_lib:ensure_ssl_files(
|
||||
SubDir,
|
||||
maps:get(<<"ssl">>, Conf, undefined)
|
||||
) of
|
||||
)
|
||||
of
|
||||
{ok, SSL} ->
|
||||
new_ssl_config(Conf, SSL);
|
||||
{error, Reason} ->
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
-include("include/emqx_gateway.hrl").
|
||||
|
||||
|
||||
%% @doc The running context for a Connection/Channel process.
|
||||
%%
|
||||
%% The `Context` encapsulates a complex structure of contextual information.
|
||||
|
|
@ -27,45 +26,50 @@
|
|||
%% configuration, register devices and other common operations.
|
||||
%%
|
||||
-type context() ::
|
||||
#{ %% Gateway Name
|
||||
gwname := gateway_name()
|
||||
%% Gateway Name
|
||||
#{
|
||||
gwname := gateway_name(),
|
||||
%% Authentication chains
|
||||
, auth := [emqx_authentication:chain_name()]
|
||||
auth := [emqx_authentication:chain_name()],
|
||||
%% The ConnectionManager PID
|
||||
, cm := pid()
|
||||
cm := pid()
|
||||
}.
|
||||
|
||||
%% Authentication circle
|
||||
-export([ authenticate/2
|
||||
, open_session/5
|
||||
, open_session/6
|
||||
, insert_channel_info/4
|
||||
, set_chan_info/3
|
||||
, set_chan_stats/3
|
||||
, connection_closed/2
|
||||
]).
|
||||
-export([
|
||||
authenticate/2,
|
||||
open_session/5,
|
||||
open_session/6,
|
||||
insert_channel_info/4,
|
||||
set_chan_info/3,
|
||||
set_chan_stats/3,
|
||||
connection_closed/2
|
||||
]).
|
||||
|
||||
%% Message circle
|
||||
-export([ authorize/4
|
||||
-export([
|
||||
authorize/4
|
||||
% Needless for pub/sub
|
||||
%, publish/3
|
||||
%, subscribe/4
|
||||
]).
|
||||
]).
|
||||
|
||||
%% Metrics & Stats
|
||||
-export([ metrics_inc/2
|
||||
, metrics_inc/3
|
||||
]).
|
||||
-export([
|
||||
metrics_inc/2,
|
||||
metrics_inc/3
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Authentication circle
|
||||
|
||||
%% @doc Authenticate whether the client has access to the Broker.
|
||||
-spec authenticate(context(), emqx_types:clientinfo())
|
||||
-> {ok, emqx_types:clientinfo()}
|
||||
-spec authenticate(context(), emqx_types:clientinfo()) ->
|
||||
{ok, emqx_types:clientinfo()}
|
||||
| {error, any()}.
|
||||
authenticate(_Ctx = #{auth := _ChainNames}, ClientInfo0)
|
||||
when is_list(_ChainNames) ->
|
||||
authenticate(_Ctx = #{auth := _ChainNames}, ClientInfo0) when
|
||||
is_list(_ChainNames)
|
||||
->
|
||||
ClientInfo = ClientInfo0#{zone => default},
|
||||
case emqx_access_control:authenticate(ClientInfo) of
|
||||
{ok, _} ->
|
||||
|
|
@ -78,43 +82,74 @@ authenticate(_Ctx = #{auth := _ChainNames}, ClientInfo0)
|
|||
%%
|
||||
%% This function should be called after the client has authenticated
|
||||
%% successfully so that the client can be managed in the cluster.
|
||||
-spec open_session(context(), boolean(), emqx_types:clientinfo(),
|
||||
-spec open_session(
|
||||
context(),
|
||||
boolean(),
|
||||
emqx_types:clientinfo(),
|
||||
emqx_types:conninfo(),
|
||||
fun((emqx_types:clientinfo(),
|
||||
emqx_types:conninfo()) -> Session)
|
||||
fun(
|
||||
(
|
||||
emqx_types:clientinfo(),
|
||||
emqx_types:conninfo()
|
||||
) -> Session
|
||||
)
|
||||
-> {ok, #{session := Session,
|
||||
) ->
|
||||
{ok, #{
|
||||
session := Session,
|
||||
present := boolean(),
|
||||
pendings => list()
|
||||
}}
|
||||
| {error, any()}.
|
||||
open_session(Ctx, CleanStart, ClientInfo, ConnInfo, CreateSessionFun) ->
|
||||
open_session(Ctx, CleanStart, ClientInfo, ConnInfo,
|
||||
CreateSessionFun, emqx_session).
|
||||
open_session(
|
||||
Ctx,
|
||||
CleanStart,
|
||||
ClientInfo,
|
||||
ConnInfo,
|
||||
CreateSessionFun,
|
||||
emqx_session
|
||||
).
|
||||
|
||||
open_session(_Ctx = #{gwname := GwName},
|
||||
CleanStart, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) ->
|
||||
emqx_gateway_cm:open_session(GwName, CleanStart,
|
||||
ClientInfo, ConnInfo,
|
||||
CreateSessionFun, SessionMod).
|
||||
open_session(
|
||||
_Ctx = #{gwname := GwName},
|
||||
CleanStart,
|
||||
ClientInfo,
|
||||
ConnInfo,
|
||||
CreateSessionFun,
|
||||
SessionMod
|
||||
) ->
|
||||
emqx_gateway_cm:open_session(
|
||||
GwName,
|
||||
CleanStart,
|
||||
ClientInfo,
|
||||
ConnInfo,
|
||||
CreateSessionFun,
|
||||
SessionMod
|
||||
).
|
||||
|
||||
-spec insert_channel_info(context(),
|
||||
-spec insert_channel_info(
|
||||
context(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:infos(),
|
||||
emqx_types:stats()) -> ok.
|
||||
emqx_types:stats()
|
||||
) -> ok.
|
||||
insert_channel_info(_Ctx = #{gwname := GwName}, ClientId, Infos, Stats) ->
|
||||
emqx_gateway_cm:insert_channel_info(GwName, ClientId, Infos, Stats).
|
||||
|
||||
%% @doc Set the Channel Info to the ConnectionManager for this client
|
||||
-spec set_chan_info(context(),
|
||||
-spec set_chan_info(
|
||||
context(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:infos()) -> boolean().
|
||||
emqx_types:infos()
|
||||
) -> boolean().
|
||||
set_chan_info(_Ctx = #{gwname := GwName}, ClientId, Infos) ->
|
||||
emqx_gateway_cm:set_chan_info(GwName, ClientId, Infos).
|
||||
|
||||
-spec set_chan_stats(context(),
|
||||
-spec set_chan_stats(
|
||||
context(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:stats()) -> boolean().
|
||||
emqx_types:stats()
|
||||
) -> boolean().
|
||||
set_chan_stats(_Ctx = #{gwname := GwName}, ClientId, Stats) ->
|
||||
emqx_gateway_cm:set_chan_stats(GwName, ClientId, Stats).
|
||||
|
||||
|
|
@ -122,9 +157,13 @@ set_chan_stats(_Ctx = #{gwname := GwName}, ClientId, Stats) ->
|
|||
connection_closed(_Ctx = #{gwname := GwName}, ClientId) ->
|
||||
emqx_gateway_cm:connection_closed(GwName, ClientId).
|
||||
|
||||
-spec authorize(context(), emqx_types:clientinfo(),
|
||||
emqx_types:pubsub(), emqx_types:topic())
|
||||
-> allow | deny.
|
||||
-spec authorize(
|
||||
context(),
|
||||
emqx_types:clientinfo(),
|
||||
emqx_types:pubsub(),
|
||||
emqx_types:topic()
|
||||
) ->
|
||||
allow | deny.
|
||||
authorize(_Ctx, ClientInfo, PubSub, Topic) ->
|
||||
emqx_access_control:authorize(ClientInfo, PubSub, Topic).
|
||||
|
||||
|
|
|
|||
|
|
@ -27,13 +27,14 @@
|
|||
|
||||
-export([start_link/1]).
|
||||
|
||||
-export([ create_insta/3
|
||||
, remove_insta/2
|
||||
, update_insta/3
|
||||
, start_insta/2
|
||||
, stop_insta/2
|
||||
, list_insta/1
|
||||
]).
|
||||
-export([
|
||||
create_insta/3,
|
||||
remove_insta/2,
|
||||
update_insta/3,
|
||||
start_insta/2,
|
||||
stop_insta/2,
|
||||
list_insta/1
|
||||
]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
|
@ -48,7 +49,8 @@ start_link(GwName) ->
|
|||
-spec create_insta(pid(), gateway(), map()) -> {ok, GwInstaPid :: pid()} | {error, any()}.
|
||||
create_insta(Sup, Gateway = #{name := Name}, GwDscrptr) ->
|
||||
case emqx_gateway_utils:find_sup_child(Sup, Name) of
|
||||
{ok, _GwInstaPid} -> {error, alredy_existed};
|
||||
{ok, _GwInstaPid} ->
|
||||
{error, alredy_existed};
|
||||
false ->
|
||||
Ctx = ctx(Sup, Name),
|
||||
ChildSpec = emqx_gateway_utils:childspec(
|
||||
|
|
@ -65,52 +67,53 @@ create_insta(Sup, Gateway = #{name := Name}, GwDscrptr) ->
|
|||
-spec remove_insta(pid(), Name :: gateway_name()) -> ok | {error, any()}.
|
||||
remove_insta(Sup, Name) ->
|
||||
case emqx_gateway_utils:find_sup_child(Sup, Name) of
|
||||
false -> ok;
|
||||
false ->
|
||||
ok;
|
||||
{ok, _GwInstaPid} ->
|
||||
ok = supervisor:terminate_child(Sup, Name),
|
||||
ok = supervisor:delete_child(Sup, Name)
|
||||
end.
|
||||
|
||||
-spec update_insta(pid(), gateway_name(), emqx_config:config())
|
||||
-> ok | {error, any()}.
|
||||
-spec update_insta(pid(), gateway_name(), emqx_config:config()) ->
|
||||
ok | {error, any()}.
|
||||
update_insta(Sup, Name, Config) ->
|
||||
case emqx_gateway_utils:find_sup_child(Sup, Name) of
|
||||
false -> {error, not_found};
|
||||
{ok, GwInstaPid} ->
|
||||
emqx_gateway_insta_sup:update(GwInstaPid, Config)
|
||||
{ok, GwInstaPid} -> emqx_gateway_insta_sup:update(GwInstaPid, Config)
|
||||
end.
|
||||
|
||||
-spec start_insta(pid(), gateway_name()) -> ok | {error, any()}.
|
||||
start_insta(Sup, Name) ->
|
||||
case emqx_gateway_utils:find_sup_child(Sup, Name) of
|
||||
false -> {error, not_found};
|
||||
{ok, GwInstaPid} ->
|
||||
emqx_gateway_insta_sup:enable(GwInstaPid)
|
||||
{ok, GwInstaPid} -> emqx_gateway_insta_sup:enable(GwInstaPid)
|
||||
end.
|
||||
|
||||
-spec stop_insta(pid(), gateway_name()) -> ok | {error, any()}.
|
||||
stop_insta(Sup, Name) ->
|
||||
case emqx_gateway_utils:find_sup_child(Sup, Name) of
|
||||
false -> {error, not_found};
|
||||
{ok, GwInstaPid} ->
|
||||
emqx_gateway_insta_sup:disable(GwInstaPid)
|
||||
{ok, GwInstaPid} -> emqx_gateway_insta_sup:disable(GwInstaPid)
|
||||
end.
|
||||
|
||||
-spec list_insta(pid()) -> [gateway()].
|
||||
list_insta(Sup) ->
|
||||
lists:filtermap(
|
||||
fun({Name, GwInstaPid, _Type, _Mods}) ->
|
||||
is_gateway_insta_id(Name)
|
||||
andalso {true, emqx_gateway_insta_sup:info(GwInstaPid)}
|
||||
end, supervisor:which_children(Sup)).
|
||||
is_gateway_insta_id(Name) andalso
|
||||
{true, emqx_gateway_insta_sup:info(GwInstaPid)}
|
||||
end,
|
||||
supervisor:which_children(Sup)
|
||||
).
|
||||
|
||||
%% Supervisor callback
|
||||
|
||||
%% @doc Initialize Top Supervisor for a Protocol
|
||||
init([GwName]) ->
|
||||
SupFlags = #{ strategy => one_for_one
|
||||
, intensity => 10
|
||||
, period => 60
|
||||
SupFlags = #{
|
||||
strategy => one_for_one,
|
||||
intensity => 10,
|
||||
period => 60
|
||||
},
|
||||
CmOpts = [{gwname, GwName}],
|
||||
CM = emqx_gateway_utils:childspec(worker, emqx_gateway_cm, [CmOpts]),
|
||||
|
|
@ -123,8 +126,9 @@ init([GwName]) ->
|
|||
|
||||
ctx(Sup, Name) ->
|
||||
{ok, CM} = emqx_gateway_utils:find_sup_child(Sup, emqx_gateway_cm),
|
||||
#{ gwname => Name
|
||||
, cm => CM
|
||||
#{
|
||||
gwname => Name,
|
||||
cm => CM
|
||||
}.
|
||||
|
||||
is_gateway_insta_id(emqx_gateway_cm) ->
|
||||
|
|
|
|||
|
|
@ -26,58 +26,63 @@
|
|||
-import(emqx_gateway_utils, [listener_id/3]).
|
||||
|
||||
%% Mgmt APIs - gateway
|
||||
-export([ gateways/1
|
||||
]).
|
||||
-export([gateways/1]).
|
||||
|
||||
%% Mgmt APIs
|
||||
-export([ add_listener/2
|
||||
, remove_listener/1
|
||||
, update_listener/2
|
||||
]).
|
||||
-export([
|
||||
add_listener/2,
|
||||
remove_listener/1,
|
||||
update_listener/2
|
||||
]).
|
||||
|
||||
-export([ authn/1
|
||||
, authn/2
|
||||
, add_authn/2
|
||||
, add_authn/3
|
||||
, update_authn/2
|
||||
, update_authn/3
|
||||
, remove_authn/1
|
||||
, remove_authn/2
|
||||
]).
|
||||
-export([
|
||||
authn/1,
|
||||
authn/2,
|
||||
add_authn/2,
|
||||
add_authn/3,
|
||||
update_authn/2,
|
||||
update_authn/3,
|
||||
remove_authn/1,
|
||||
remove_authn/2
|
||||
]).
|
||||
|
||||
%% Mgmt APIs - clients
|
||||
-export([ lookup_client/3
|
||||
, kickout_client/2
|
||||
, list_client_subscriptions/2
|
||||
, client_subscribe/4
|
||||
, client_unsubscribe/3
|
||||
]).
|
||||
-export([
|
||||
lookup_client/3,
|
||||
kickout_client/2,
|
||||
list_client_subscriptions/2,
|
||||
client_subscribe/4,
|
||||
client_unsubscribe/3
|
||||
]).
|
||||
|
||||
%% Utils for http, swagger, etc.
|
||||
-export([ return_http_error/2
|
||||
, with_gateway/2
|
||||
, with_authn/2
|
||||
, with_listener_authn/3
|
||||
, checks/2
|
||||
, reason2resp/1
|
||||
, reason2msg/1
|
||||
]).
|
||||
-export([
|
||||
return_http_error/2,
|
||||
with_gateway/2,
|
||||
with_authn/2,
|
||||
with_listener_authn/3,
|
||||
checks/2,
|
||||
reason2resp/1,
|
||||
reason2msg/1
|
||||
]).
|
||||
|
||||
-type gateway_summary() ::
|
||||
#{ name := binary()
|
||||
, status := running | stopped | unloaded
|
||||
, created_at => binary()
|
||||
, started_at => binary()
|
||||
, stopped_at => binary()
|
||||
, max_connections => integer()
|
||||
, current_connections => integer()
|
||||
, listeners => []
|
||||
#{
|
||||
name := binary(),
|
||||
status := running | stopped | unloaded,
|
||||
created_at => binary(),
|
||||
started_at => binary(),
|
||||
stopped_at => binary(),
|
||||
max_connections => integer(),
|
||||
current_connections => integer(),
|
||||
listeners => []
|
||||
}.
|
||||
|
||||
-elvis([ {elvis_style, god_modules, disable}
|
||||
, {elvis_style, no_nested_try_catch, disable}
|
||||
, {elvis_style, invalid_dynamic_call, disable}
|
||||
]).
|
||||
-elvis([
|
||||
{elvis_style, god_modules, disable},
|
||||
{elvis_style, no_nested_try_catch, disable},
|
||||
{elvis_style, invalid_dynamic_call, disable}
|
||||
]).
|
||||
|
||||
-define(DEFAULT_CALL_TIMEOUT, 15000).
|
||||
|
||||
|
|
@ -85,53 +90,69 @@
|
|||
%% Mgmt APIs - gateway
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec gateways(Status :: all | running | stopped | unloaded)
|
||||
-> [gateway_summary()].
|
||||
-spec gateways(Status :: all | running | stopped | unloaded) ->
|
||||
[gateway_summary()].
|
||||
gateways(Status) ->
|
||||
Gateways = lists:map(fun({GwName, _}) ->
|
||||
Gateways = lists:map(
|
||||
fun({GwName, _}) ->
|
||||
case emqx_gateway:lookup(GwName) of
|
||||
undefined -> #{name => GwName, status => unloaded};
|
||||
undefined ->
|
||||
#{name => GwName, status => unloaded};
|
||||
GwInfo = #{config := Config} ->
|
||||
GwInfo0 = emqx_gateway_utils:unix_ts_to_rfc3339(
|
||||
[created_at, started_at, stopped_at],
|
||||
GwInfo),
|
||||
GwInfo1 = maps:with([name,
|
||||
GwInfo
|
||||
),
|
||||
GwInfo1 = maps:with(
|
||||
[
|
||||
name,
|
||||
status,
|
||||
created_at,
|
||||
started_at,
|
||||
stopped_at], GwInfo0),
|
||||
stopped_at
|
||||
],
|
||||
GwInfo0
|
||||
),
|
||||
GwInfo1#{
|
||||
max_connections => max_connections_count(Config),
|
||||
current_connections => current_connections_count(GwName),
|
||||
listeners => get_listeners_status(GwName, Config)}
|
||||
listeners => get_listeners_status(GwName, Config)
|
||||
}
|
||||
end
|
||||
end, emqx_gateway_registry:list()),
|
||||
end,
|
||||
emqx_gateway_registry:list()
|
||||
),
|
||||
case Status of
|
||||
all -> Gateways;
|
||||
_ ->
|
||||
[Gw || Gw = #{status := S} <- Gateways, S == Status]
|
||||
_ -> [Gw || Gw = #{status := S} <- Gateways, S == Status]
|
||||
end.
|
||||
|
||||
%% @private
|
||||
max_connections_count(Config) ->
|
||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||
lists:foldl(fun({_, _, _, SocketOpts, _}, Acc) ->
|
||||
lists:foldl(
|
||||
fun({_, _, _, SocketOpts, _}, Acc) ->
|
||||
Acc + proplists:get_value(max_connections, SocketOpts, 0)
|
||||
end, 0, Listeners).
|
||||
end,
|
||||
0,
|
||||
Listeners
|
||||
).
|
||||
|
||||
%% @private
|
||||
current_connections_count(GwName) ->
|
||||
try
|
||||
InfoTab = emqx_gateway_cm:tabname(info, GwName),
|
||||
ets:info(InfoTab, size)
|
||||
catch _ : _ ->
|
||||
catch
|
||||
_:_ ->
|
||||
0
|
||||
end.
|
||||
|
||||
%% @private
|
||||
get_listeners_status(GwName, Config) ->
|
||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||
lists:map(fun({Type, LisName, ListenOn, _, _}) ->
|
||||
lists:map(
|
||||
fun({Type, LisName, ListenOn, _, _}) ->
|
||||
Name0 = listener_id(GwName, Type, LisName),
|
||||
Name = {Name0, ListenOn},
|
||||
LisO = #{id => Name0, type => Type, name => LisName},
|
||||
|
|
@ -141,7 +162,9 @@ get_listeners_status(GwName, Config) ->
|
|||
_ ->
|
||||
LisO#{running => false}
|
||||
end
|
||||
end, Listeners).
|
||||
end,
|
||||
Listeners
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Mgmt APIs - listeners
|
||||
|
|
@ -150,16 +173,30 @@ get_listeners_status(GwName, Config) ->
|
|||
-spec add_listener(atom() | binary(), map()) -> {ok, map()}.
|
||||
add_listener(ListenerId, NewConf0) ->
|
||||
{GwName, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||
NewConf = maps:without([<<"id">>, <<"name">>,
|
||||
<<"type">>, <<"running">>], NewConf0),
|
||||
NewConf = maps:without(
|
||||
[
|
||||
<<"id">>,
|
||||
<<"name">>,
|
||||
<<"type">>,
|
||||
<<"running">>
|
||||
],
|
||||
NewConf0
|
||||
),
|
||||
confexp(emqx_gateway_conf:add_listener(GwName, {Type, Name}, NewConf)).
|
||||
|
||||
-spec update_listener(atom() | binary(), map()) -> {ok, map()}.
|
||||
update_listener(ListenerId, NewConf0) ->
|
||||
{GwName, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||
|
||||
NewConf = maps:without([<<"id">>, <<"name">>,
|
||||
<<"type">>, <<"running">>], NewConf0),
|
||||
NewConf = maps:without(
|
||||
[
|
||||
<<"id">>,
|
||||
<<"name">>,
|
||||
<<"type">>,
|
||||
<<"running">>
|
||||
],
|
||||
NewConf0
|
||||
),
|
||||
confexp(emqx_gateway_conf:update_listener(GwName, {Type, Name}, NewConf)).
|
||||
|
||||
-spec remove_listener(binary()) -> ok.
|
||||
|
|
@ -230,65 +267,91 @@ confexp({error, Reason}) -> error(Reason).
|
|||
%% Mgmt APIs - clients
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec lookup_client(gateway_name(),
|
||||
emqx_types:clientid(), {module(), atom()}) -> list().
|
||||
-spec lookup_client(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
{module(), atom()}
|
||||
) -> list().
|
||||
lookup_client(GwName, ClientId, {M, F}) ->
|
||||
[begin
|
||||
[
|
||||
begin
|
||||
Info = emqx_gateway_cm:get_chan_info(GwName, ClientId, Pid),
|
||||
Stats = emqx_gateway_cm:get_chan_stats(GwName, ClientId, Pid),
|
||||
M:F({{ClientId, Pid}, Info, Stats})
|
||||
end
|
||||
|| Pid <- emqx_gateway_cm:lookup_by_clientid(GwName, ClientId)].
|
||||
|| Pid <- emqx_gateway_cm:lookup_by_clientid(GwName, ClientId)
|
||||
].
|
||||
|
||||
-spec kickout_client(gateway_name(), emqx_types:clientid())
|
||||
-> {error, any()}
|
||||
-spec kickout_client(gateway_name(), emqx_types:clientid()) ->
|
||||
{error, any()}
|
||||
| ok.
|
||||
kickout_client(GwName, ClientId) ->
|
||||
Results = [emqx_gateway_cm:kick_session(GwName, ClientId, Pid)
|
||||
|| Pid <- emqx_gateway_cm:lookup_by_clientid(GwName, ClientId)],
|
||||
Results = [
|
||||
emqx_gateway_cm:kick_session(GwName, ClientId, Pid)
|
||||
|| Pid <- emqx_gateway_cm:lookup_by_clientid(GwName, ClientId)
|
||||
],
|
||||
IsOk = lists:any(fun(Item) -> Item =:= ok end, Results),
|
||||
case {IsOk, Results} of
|
||||
{true , _ } -> ok;
|
||||
{_ , []} -> {error, not_found};
|
||||
{false, _ } -> lists:last(Results)
|
||||
{true, _} -> ok;
|
||||
{_, []} -> {error, not_found};
|
||||
{false, _} -> lists:last(Results)
|
||||
end.
|
||||
|
||||
-spec list_client_subscriptions(gateway_name(), emqx_types:clientid())
|
||||
-> {error, any()}
|
||||
-spec list_client_subscriptions(gateway_name(), emqx_types:clientid()) ->
|
||||
{error, any()}
|
||||
| {ok, list()}.
|
||||
list_client_subscriptions(GwName, ClientId) ->
|
||||
case client_call(GwName, ClientId, subscriptions) of
|
||||
{error, Reason} -> {error, Reason};
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
{ok, Subs} ->
|
||||
{ok, lists:map(fun({Topic, SubOpts}) ->
|
||||
{ok,
|
||||
lists:map(
|
||||
fun({Topic, SubOpts}) ->
|
||||
SubOpts#{topic => Topic}
|
||||
end, Subs)}
|
||||
end,
|
||||
Subs
|
||||
)}
|
||||
end.
|
||||
|
||||
-spec client_subscribe(gateway_name(), emqx_types:clientid(),
|
||||
emqx_types:topic(), emqx_types:subopts())
|
||||
-> {error, any()}
|
||||
-spec client_subscribe(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:topic(),
|
||||
emqx_types:subopts()
|
||||
) ->
|
||||
{error, any()}
|
||||
| {ok, {emqx_types:topic(), emqx_types:subopts()}}.
|
||||
client_subscribe(GwName, ClientId, Topic, SubOpts) ->
|
||||
client_call(GwName, ClientId, {subscribe, Topic, SubOpts}).
|
||||
|
||||
-spec client_unsubscribe(gateway_name(),
|
||||
emqx_types:clientid(), emqx_types:topic())
|
||||
-> {error, any()}
|
||||
-spec client_unsubscribe(
|
||||
gateway_name(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:topic()
|
||||
) ->
|
||||
{error, any()}
|
||||
| ok.
|
||||
client_unsubscribe(GwName, ClientId, Topic) ->
|
||||
client_call(GwName, ClientId, {unsubscribe, Topic}).
|
||||
|
||||
client_call(GwName, ClientId, Req) ->
|
||||
try emqx_gateway_cm:call(
|
||||
GwName, ClientId,
|
||||
Req, ?DEFAULT_CALL_TIMEOUT) of
|
||||
try
|
||||
emqx_gateway_cm:call(
|
||||
GwName,
|
||||
ClientId,
|
||||
Req,
|
||||
?DEFAULT_CALL_TIMEOUT
|
||||
)
|
||||
of
|
||||
undefined ->
|
||||
{error, not_found};
|
||||
Res -> Res
|
||||
catch throw : noproc ->
|
||||
Res ->
|
||||
Res
|
||||
catch
|
||||
throw:noproc ->
|
||||
{error, not_found};
|
||||
throw : {badrpc, Reason} ->
|
||||
throw:{badrpc, Reason} ->
|
||||
{error, {badrpc, Reason}}
|
||||
end.
|
||||
|
||||
|
|
@ -311,54 +374,88 @@ return_http_error(Code, Msg) ->
|
|||
|
||||
-spec reason2msg({atom(), map()} | any()) -> error | string().
|
||||
reason2msg({badconf, #{key := Key, value := Value, reason := Reason}}) ->
|
||||
NValue = case emqx_json:safe_encode(Value) of
|
||||
NValue =
|
||||
case emqx_json:safe_encode(Value) of
|
||||
{ok, Str} -> Str;
|
||||
{error, _} -> emqx_gateway_utils:stringfy(Value)
|
||||
end,
|
||||
fmtstr("Bad config value '~s' for '~s', reason: ~s",
|
||||
[NValue, Key, Reason]);
|
||||
reason2msg({badres, #{resource := gateway,
|
||||
fmtstr(
|
||||
"Bad config value '~s' for '~s', reason: ~s",
|
||||
[NValue, Key, Reason]
|
||||
);
|
||||
reason2msg(
|
||||
{badres, #{
|
||||
resource := gateway,
|
||||
gateway := GwName,
|
||||
reason := not_found}}) ->
|
||||
reason := not_found
|
||||
}}
|
||||
) ->
|
||||
fmtstr("The ~s gateway is unloaded", [GwName]);
|
||||
|
||||
reason2msg({badres, #{resource := gateway,
|
||||
reason2msg(
|
||||
{badres, #{
|
||||
resource := gateway,
|
||||
gateway := GwName,
|
||||
reason := already_exist}}) ->
|
||||
reason := already_exist
|
||||
}}
|
||||
) ->
|
||||
fmtstr("The ~s gateway already loaded", [GwName]);
|
||||
|
||||
reason2msg({badres, #{resource := listener,
|
||||
reason2msg(
|
||||
{badres, #{
|
||||
resource := listener,
|
||||
listener := {GwName, LType, LName},
|
||||
reason := not_found}}) ->
|
||||
reason := not_found
|
||||
}}
|
||||
) ->
|
||||
fmtstr("Listener ~s not found", [listener_id(GwName, LType, LName)]);
|
||||
|
||||
reason2msg({badres, #{resource := listener,
|
||||
reason2msg(
|
||||
{badres, #{
|
||||
resource := listener,
|
||||
listener := {GwName, LType, LName},
|
||||
reason := already_exist}}) ->
|
||||
fmtstr("The listener ~s of ~s already exist",
|
||||
[listener_id(GwName, LType, LName), GwName]);
|
||||
|
||||
reason2msg({badres, #{resource := authn,
|
||||
reason := already_exist
|
||||
}}
|
||||
) ->
|
||||
fmtstr(
|
||||
"The listener ~s of ~s already exist",
|
||||
[listener_id(GwName, LType, LName), GwName]
|
||||
);
|
||||
reason2msg(
|
||||
{badres, #{
|
||||
resource := authn,
|
||||
gateway := GwName,
|
||||
reason := not_found}}) ->
|
||||
reason := not_found
|
||||
}}
|
||||
) ->
|
||||
fmtstr("The authentication not found on ~s", [GwName]);
|
||||
|
||||
reason2msg({badres, #{resource := authn,
|
||||
reason2msg(
|
||||
{badres, #{
|
||||
resource := authn,
|
||||
gateway := GwName,
|
||||
reason := already_exist}}) ->
|
||||
reason := already_exist
|
||||
}}
|
||||
) ->
|
||||
fmtstr("The authentication already exist on ~s", [GwName]);
|
||||
|
||||
reason2msg({badres, #{resource := listener_authn,
|
||||
reason2msg(
|
||||
{badres, #{
|
||||
resource := listener_authn,
|
||||
listener := {GwName, LType, LName},
|
||||
reason := not_found}}) ->
|
||||
fmtstr("The authentication not found on ~s",
|
||||
[listener_id(GwName, LType, LName)]);
|
||||
|
||||
reason2msg({badres, #{resource := listener_authn,
|
||||
reason := not_found
|
||||
}}
|
||||
) ->
|
||||
fmtstr(
|
||||
"The authentication not found on ~s",
|
||||
[listener_id(GwName, LType, LName)]
|
||||
);
|
||||
reason2msg(
|
||||
{badres, #{
|
||||
resource := listener_authn,
|
||||
listener := {GwName, LType, LName},
|
||||
reason := already_exist}}) ->
|
||||
fmtstr("The authentication already exist on ~s",
|
||||
[listener_id(GwName, LType, LName)]);
|
||||
reason := already_exist
|
||||
}}
|
||||
) ->
|
||||
fmtstr(
|
||||
"The authentication already exist on ~s",
|
||||
[listener_id(GwName, LType, LName)]
|
||||
);
|
||||
reason2msg(_) ->
|
||||
error.
|
||||
|
||||
|
|
@ -389,9 +486,11 @@ with_listener_authn(GwName0, Id, Fun) ->
|
|||
-spec with_gateway(binary(), function()) -> any().
|
||||
with_gateway(GwName0, Fun) ->
|
||||
try
|
||||
GwName = try
|
||||
GwName =
|
||||
try
|
||||
binary_to_existing_atom(GwName0)
|
||||
catch _ : _ -> error(badname)
|
||||
catch
|
||||
_:_ -> error(badname)
|
||||
end,
|
||||
case emqx_gateway:lookup(GwName) of
|
||||
undefined ->
|
||||
|
|
@ -400,23 +499,25 @@ with_gateway(GwName0, Fun) ->
|
|||
Fun(GwName, Gateway)
|
||||
end
|
||||
catch
|
||||
error : badname ->
|
||||
error:badname ->
|
||||
return_http_error(404, "Bad gateway name");
|
||||
%% Exceptions from: checks/2
|
||||
error : {miss_param, K} ->
|
||||
error:{miss_param, K} ->
|
||||
return_http_error(400, [K, " is required"]);
|
||||
%% Exceptions from emqx_gateway_utils:parse_listener_id/1
|
||||
error : {invalid_listener_id, Id} ->
|
||||
error:{invalid_listener_id, Id} ->
|
||||
return_http_error(400, ["invalid listener id: ", Id]);
|
||||
%% Exceptions from: emqx:get_config/1
|
||||
error : {config_not_found, Path0} ->
|
||||
error:{config_not_found, Path0} ->
|
||||
Path = lists:concat(
|
||||
lists:join(".", lists:map(fun to_list/1, Path0))),
|
||||
lists:join(".", lists:map(fun to_list/1, Path0))
|
||||
),
|
||||
return_http_error(404, "Resource not found. path: " ++ Path);
|
||||
Class : Reason : Stk ->
|
||||
?SLOG(error, #{ msg => "uncatched_error"
|
||||
, reason => {Class, Reason}
|
||||
, stacktrace => Stk
|
||||
Class:Reason:Stk ->
|
||||
?SLOG(error, #{
|
||||
msg => "uncatched_error",
|
||||
reason => {Class, Reason},
|
||||
stacktrace => Stk
|
||||
}),
|
||||
reason2resp(Reason)
|
||||
end.
|
||||
|
|
@ -427,8 +528,7 @@ checks([], _) ->
|
|||
checks([K | Ks], Map) ->
|
||||
case maps:is_key(K, Map) of
|
||||
true -> checks(Ks, Map);
|
||||
false ->
|
||||
error({miss_param, K})
|
||||
false -> error({miss_param, K})
|
||||
end.
|
||||
|
||||
to_list(A) when is_atom(A) ->
|
||||
|
|
|
|||
|
|
@ -23,21 +23,23 @@
|
|||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% APIs
|
||||
-export([ start_link/3
|
||||
, info/1
|
||||
, disable/1
|
||||
, enable/1
|
||||
, update/2
|
||||
]).
|
||||
-export([
|
||||
start_link/3,
|
||||
info/1,
|
||||
disable/1,
|
||||
enable/1,
|
||||
update/2
|
||||
]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-record(state, {
|
||||
name :: gateway_name(),
|
||||
|
|
@ -50,7 +52,7 @@
|
|||
created_at :: integer(),
|
||||
started_at :: integer() | undefined,
|
||||
stopped_at :: integer() | undefined
|
||||
}).
|
||||
}).
|
||||
|
||||
-elvis([{elvis_style, invalid_dynamic_call, disable}]).
|
||||
|
||||
|
|
@ -93,7 +95,7 @@ call(Pid, Req) ->
|
|||
|
||||
init([Gateway, Ctx, _GwDscrptr]) ->
|
||||
process_flag(trap_exit, true),
|
||||
#{name := GwName, config := Config } = Gateway,
|
||||
#{name := GwName, config := Config} = Gateway,
|
||||
State = #state{
|
||||
ctx = Ctx,
|
||||
name = GwName,
|
||||
|
|
@ -105,8 +107,9 @@ init([Gateway, Ctx, _GwDscrptr]) ->
|
|||
},
|
||||
case maps:get(enable, Config, true) of
|
||||
false ->
|
||||
?SLOG(info, #{ msg => "skip_to_start_gateway_due_to_disabled"
|
||||
, gateway_name => GwName
|
||||
?SLOG(info, #{
|
||||
msg => "skip_to_start_gateway_due_to_disabled",
|
||||
gateway_name => GwName
|
||||
}),
|
||||
{ok, State};
|
||||
true ->
|
||||
|
|
@ -120,7 +123,6 @@ init([Gateway, Ctx, _GwDscrptr]) ->
|
|||
|
||||
handle_call(info, _From, State) ->
|
||||
{reply, detailed_gateway_info(State), State};
|
||||
|
||||
handle_call(disable, _From, State = #state{status = Status}) ->
|
||||
case Status of
|
||||
running ->
|
||||
|
|
@ -133,7 +135,6 @@ handle_call(disable, _From, State = #state{status = Status}) ->
|
|||
_ ->
|
||||
{reply, {error, already_stopped}, State}
|
||||
end;
|
||||
|
||||
handle_call(enable, _From, State = #state{status = Status}) ->
|
||||
case Status of
|
||||
stopped ->
|
||||
|
|
@ -146,7 +147,6 @@ handle_call(enable, _From, State = #state{status = Status}) ->
|
|||
_ ->
|
||||
{reply, {error, already_started}, State}
|
||||
end;
|
||||
|
||||
handle_call({update, Config}, _From, State) ->
|
||||
case do_update_one_by_one(Config, State) of
|
||||
{ok, NState} ->
|
||||
|
|
@ -155,7 +155,6 @@ handle_call({update, Config}, _From, State) ->
|
|||
%% If something wrong, nothing to update
|
||||
{reply, {error, Reason}, State}
|
||||
end;
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
Reply = ok,
|
||||
{reply, Reply, State}.
|
||||
|
|
@ -163,37 +162,47 @@ handle_call(_Request, _From, State) ->
|
|||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info({'EXIT', Pid, Reason}, State = #state{name = Name,
|
||||
child_pids = Pids}) ->
|
||||
handle_info(
|
||||
{'EXIT', Pid, Reason},
|
||||
State = #state{
|
||||
name = Name,
|
||||
child_pids = Pids
|
||||
}
|
||||
) ->
|
||||
case lists:member(Pid, Pids) of
|
||||
true ->
|
||||
?SLOG(error, #{ msg => "child_process_exited"
|
||||
, child => Pid
|
||||
, reason => Reason
|
||||
?SLOG(error, #{
|
||||
msg => "child_process_exited",
|
||||
child => Pid,
|
||||
reason => Reason
|
||||
}),
|
||||
case Pids -- [Pid]of
|
||||
case Pids -- [Pid] of
|
||||
[] ->
|
||||
?SLOG(error, #{ msg => "gateway_all_children_process_existed"
|
||||
, gateway_name => Name
|
||||
?SLOG(error, #{
|
||||
msg => "gateway_all_children_process_existed",
|
||||
gateway_name => Name
|
||||
}),
|
||||
{noreply, State#state{status = stopped,
|
||||
{noreply, State#state{
|
||||
status = stopped,
|
||||
child_pids = [],
|
||||
gw_state = undefined}};
|
||||
gw_state = undefined
|
||||
}};
|
||||
RemainPids ->
|
||||
{noreply, State#state{child_pids = RemainPids}}
|
||||
end;
|
||||
_ ->
|
||||
?SLOG(error, #{ msg => "gateway_catch_a_unknown_process_exited"
|
||||
, child => Pid
|
||||
, reason => Reason
|
||||
, gateway_name => Name
|
||||
?SLOG(error, #{
|
||||
msg => "gateway_catch_a_unknown_process_exited",
|
||||
child => Pid,
|
||||
reason => Reason,
|
||||
gateway_name => Name
|
||||
}),
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?SLOG(warning, #{ msg => "unexcepted_info"
|
||||
, info => Info
|
||||
?SLOG(warning, #{
|
||||
msg => "unexcepted_info",
|
||||
info => Info
|
||||
}),
|
||||
{noreply, State}.
|
||||
|
||||
|
|
@ -208,13 +217,15 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
detailed_gateway_info(State) ->
|
||||
maps:filter(
|
||||
fun(_, V) -> V =/= undefined end,
|
||||
#{name => State#state.name,
|
||||
#{
|
||||
name => State#state.name,
|
||||
config => State#state.config,
|
||||
status => State#state.status,
|
||||
created_at => State#state.created_at,
|
||||
started_at => State#state.started_at,
|
||||
stopped_at => State#state.stopped_at
|
||||
}).
|
||||
}
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal funcs
|
||||
|
|
@ -231,7 +242,7 @@ init_authn(GwName, Config) ->
|
|||
try
|
||||
do_init_authn(Authns, [])
|
||||
catch
|
||||
throw : Reason = {badauth, _} ->
|
||||
throw:Reason = {badauth, _} ->
|
||||
do_deinit_authn(proplists:get_keys(Authns)),
|
||||
throw(Reason)
|
||||
end.
|
||||
|
|
@ -250,11 +261,15 @@ do_init_authn([_BadConf | More], Names) ->
|
|||
authns(GwName, Config) ->
|
||||
Listeners = maps:to_list(maps:get(listeners, Config, #{})),
|
||||
lists:append(
|
||||
[ [{emqx_gateway_utils:listener_chain(GwName, LisType, LisName),
|
||||
authn_conf(Opts)}
|
||||
|| {LisName, Opts} <- maps:to_list(LisNames) ]
|
||||
|| {LisType, LisNames} <- Listeners])
|
||||
++ [{emqx_gateway_utils:global_chain(GwName), authn_conf(Config)}].
|
||||
[
|
||||
[
|
||||
{emqx_gateway_utils:listener_chain(GwName, LisType, LisName), authn_conf(Opts)}
|
||||
|| {LisName, Opts} <- maps:to_list(LisNames)
|
||||
]
|
||||
|| {LisType, LisNames} <- Listeners
|
||||
]
|
||||
) ++
|
||||
[{emqx_gateway_utils:global_chain(GwName), authn_conf(Config)}].
|
||||
|
||||
authn_conf(Conf) ->
|
||||
maps:get(authentication, Conf, #{enable => false}).
|
||||
|
|
@ -263,19 +278,22 @@ do_create_authn_chain(ChainName, AuthConf) ->
|
|||
case ensure_chain(ChainName) of
|
||||
ok ->
|
||||
case emqx_authentication:create_authenticator(ChainName, AuthConf) of
|
||||
{ok, _} -> ok;
|
||||
{ok, _} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{ msg => "failed_to_create_authenticator"
|
||||
, chain_name => ChainName
|
||||
, reason => Reason
|
||||
, config => AuthConf
|
||||
?SLOG(error, #{
|
||||
msg => "failed_to_create_authenticator",
|
||||
chain_name => ChainName,
|
||||
reason => Reason,
|
||||
config => AuthConf
|
||||
}),
|
||||
throw({badauth, Reason})
|
||||
end;
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{ msg => "failed_to_create_authn_chanin"
|
||||
, chain_name => ChainName
|
||||
, reason => Reason
|
||||
?SLOG(error, #{
|
||||
msg => "failed_to_create_authn_chanin",
|
||||
chain_name => ChainName,
|
||||
reason => Reason
|
||||
}),
|
||||
throw({badauth, Reason})
|
||||
end.
|
||||
|
|
@ -291,22 +309,32 @@ ensure_chain(ChainName) ->
|
|||
end.
|
||||
|
||||
do_deinit_authn(Names) ->
|
||||
lists:foreach(fun(ChainName) ->
|
||||
lists:foreach(
|
||||
fun(ChainName) ->
|
||||
case emqx_authentication:delete_chain(ChainName) of
|
||||
ok -> ok;
|
||||
{error, {not_found, _}} -> ok;
|
||||
ok ->
|
||||
ok;
|
||||
{error, {not_found, _}} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{ msg => "failed_to_clean_authn_chain"
|
||||
, chain_name => ChainName
|
||||
, reason => Reason
|
||||
?SLOG(error, #{
|
||||
msg => "failed_to_clean_authn_chain",
|
||||
chain_name => ChainName,
|
||||
reason => Reason
|
||||
})
|
||||
end
|
||||
end, Names).
|
||||
end,
|
||||
Names
|
||||
).
|
||||
|
||||
do_update_one_by_one(NCfg, State = #state{
|
||||
do_update_one_by_one(
|
||||
NCfg,
|
||||
State = #state{
|
||||
name = GwName,
|
||||
config = OCfg,
|
||||
status = Status}) ->
|
||||
status = Status
|
||||
}
|
||||
) ->
|
||||
NEnable = maps:get(enable, NCfg, true),
|
||||
|
||||
OAuths = authns(GwName, OCfg),
|
||||
|
|
@ -319,8 +347,10 @@ do_update_one_by_one(NCfg, State = #state{
|
|||
{stopped, false} ->
|
||||
{ok, State#state{config = NCfg}};
|
||||
{running, true} ->
|
||||
NState = case NAuths == OAuths of
|
||||
true -> State;
|
||||
NState =
|
||||
case NAuths == OAuths of
|
||||
true ->
|
||||
State;
|
||||
false ->
|
||||
%% Reset Authentication first
|
||||
_ = do_deinit_authn(State#state.authns),
|
||||
|
|
@ -338,25 +368,32 @@ do_update_one_by_one(NCfg, State = #state{
|
|||
throw(nomatch)
|
||||
end.
|
||||
|
||||
cb_gateway_unload(State = #state{name = GwName,
|
||||
gw_state = GwState}) ->
|
||||
cb_gateway_unload(
|
||||
State = #state{
|
||||
name = GwName,
|
||||
gw_state = GwState
|
||||
}
|
||||
) ->
|
||||
Gateway = detailed_gateway_info(State),
|
||||
try
|
||||
#{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName),
|
||||
CbMod:on_gateway_unload(Gateway, GwState),
|
||||
{ok, State#state{child_pids = [],
|
||||
{ok, State#state{
|
||||
child_pids = [],
|
||||
authns = [],
|
||||
status = stopped,
|
||||
gw_state = undefined,
|
||||
started_at = undefined,
|
||||
stopped_at = erlang:system_time(millisecond)}}
|
||||
stopped_at = erlang:system_time(millisecond)
|
||||
}}
|
||||
catch
|
||||
Class : Reason : Stk ->
|
||||
?SLOG(error, #{ msg => "unload_gateway_crashed"
|
||||
, gateway_name => GwName
|
||||
, inner_state => GwState
|
||||
, reason => {Class, Reason}
|
||||
, stacktrace => Stk
|
||||
Class:Reason:Stk ->
|
||||
?SLOG(error, #{
|
||||
msg => "unload_gateway_crashed",
|
||||
gateway_name => GwName,
|
||||
inner_state => GwState,
|
||||
reason => {Class, Reason},
|
||||
stacktrace => Stk
|
||||
}),
|
||||
{error, Reason}
|
||||
after
|
||||
|
|
@ -367,10 +404,13 @@ cb_gateway_unload(State = #state{name = GwName,
|
|||
%% 2. Callback to Mod:on_gateway_load/2
|
||||
%%
|
||||
%% Notes: If failed, rollback
|
||||
cb_gateway_load(State = #state{name = GwName,
|
||||
cb_gateway_load(
|
||||
State = #state{
|
||||
name = GwName,
|
||||
config = Config,
|
||||
ctx = Ctx}) ->
|
||||
|
||||
ctx = Ctx
|
||||
}
|
||||
) ->
|
||||
Gateway = detailed_gateway_info(State),
|
||||
try
|
||||
AuthnNames = init_authn(GwName, Config),
|
||||
|
|
@ -393,24 +433,30 @@ cb_gateway_load(State = #state{name = GwName,
|
|||
}}
|
||||
end
|
||||
catch
|
||||
Class : Reason1 : Stk ->
|
||||
?SLOG(error, #{ msg => "load_gateway_crashed"
|
||||
, gateway_name => GwName
|
||||
, gateway => Gateway
|
||||
, ctx => Ctx
|
||||
, reason => {Class, Reason1}
|
||||
, stacktrace => Stk
|
||||
Class:Reason1:Stk ->
|
||||
?SLOG(error, #{
|
||||
msg => "load_gateway_crashed",
|
||||
gateway_name => GwName,
|
||||
gateway => Gateway,
|
||||
ctx => Ctx,
|
||||
reason => {Class, Reason1},
|
||||
stacktrace => Stk
|
||||
}),
|
||||
{error, Reason1}
|
||||
end.
|
||||
|
||||
cb_gateway_update(Config,
|
||||
State = #state{name = GwName,
|
||||
gw_state = GwState}) ->
|
||||
cb_gateway_update(
|
||||
Config,
|
||||
State = #state{
|
||||
name = GwName,
|
||||
gw_state = GwState
|
||||
}
|
||||
) ->
|
||||
try
|
||||
#{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName),
|
||||
case CbMod:on_gateway_update(Config, detailed_gateway_info(State), GwState) of
|
||||
{error, Reason} -> {error, Reason};
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
{ok, ChildPidOrSpecs, NGwState} ->
|
||||
ChildPids = start_child_process(ChildPidOrSpecs),
|
||||
{ok, State#state{
|
||||
|
|
@ -420,17 +466,19 @@ cb_gateway_update(Config,
|
|||
}}
|
||||
end
|
||||
catch
|
||||
Class : Reason1 : Stk ->
|
||||
?SLOG(error, #{ msg => "update_gateway_crashed"
|
||||
, gateway_name => GwName
|
||||
, new_config => Config
|
||||
, reason => {Class, Reason1}
|
||||
, stacktrace => Stk
|
||||
Class:Reason1:Stk ->
|
||||
?SLOG(error, #{
|
||||
msg => "update_gateway_crashed",
|
||||
gateway_name => GwName,
|
||||
new_config => Config,
|
||||
reason => {Class, Reason1},
|
||||
stacktrace => Stk
|
||||
}),
|
||||
{error, Reason1}
|
||||
end.
|
||||
|
||||
start_child_process([]) -> [];
|
||||
start_child_process([]) ->
|
||||
[];
|
||||
start_child_process([Indictor | _] = ChildPidOrSpecs) ->
|
||||
case erlang:is_pid(Indictor) of
|
||||
true ->
|
||||
|
|
@ -441,7 +489,6 @@ start_child_process([Indictor | _] = ChildPidOrSpecs) ->
|
|||
|
||||
do_start_child_process(ChildSpecs) when is_list(ChildSpecs) ->
|
||||
lists:map(fun do_start_child_process/1, ChildSpecs);
|
||||
|
||||
do_start_child_process(_ChildSpec = #{start := {M, F, A}}) ->
|
||||
case erlang:apply(M, F, A) of
|
||||
{ok, Pid} ->
|
||||
|
|
|
|||
|
|
@ -23,22 +23,24 @@
|
|||
%% APIs
|
||||
-export([start_link/1]).
|
||||
|
||||
-export([ inc/2
|
||||
, inc/3
|
||||
, dec/2
|
||||
, dec/3
|
||||
]).
|
||||
-export([
|
||||
inc/2,
|
||||
inc/3,
|
||||
dec/2,
|
||||
dec/3
|
||||
]).
|
||||
|
||||
-export([lookup/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-export([tabname/1]).
|
||||
|
||||
|
|
@ -68,8 +70,8 @@ dec(GwName, Name) ->
|
|||
dec(GwName, Name, Oct) ->
|
||||
inc(GwName, Name, -Oct).
|
||||
|
||||
-spec lookup(gateway_name())
|
||||
-> undefined
|
||||
-spec lookup(gateway_name()) ->
|
||||
undefined
|
||||
| [{Name :: atom(), integer()}].
|
||||
lookup(GwName) ->
|
||||
Tab = emqx_gateway_metrics:tabname(GwName),
|
||||
|
|
@ -87,7 +89,7 @@ tabname(GwName) ->
|
|||
|
||||
init([GwName]) ->
|
||||
TabOpts = [public, {write_concurrency, true}],
|
||||
ok = emqx_tables:new(tabname(GwName), [set|TabOpts]),
|
||||
ok = emqx_tables:new(tabname(GwName), [set | TabOpts]),
|
||||
{ok, #state{}}.
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
|
|
|
|||
|
|
@ -22,35 +22,38 @@
|
|||
-behaviour(gen_server).
|
||||
|
||||
%% APIs
|
||||
-export([ reg/2
|
||||
, unreg/1
|
||||
, list/0
|
||||
, lookup/1
|
||||
]).
|
||||
-export([
|
||||
reg/2,
|
||||
unreg/1,
|
||||
list/0,
|
||||
lookup/1
|
||||
]).
|
||||
|
||||
%% APIs
|
||||
-export([start_link/0]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-record(state, {
|
||||
reged = #{} :: #{ gateway_name() => descriptor() }
|
||||
}).
|
||||
reged = #{} :: #{gateway_name() => descriptor()}
|
||||
}).
|
||||
|
||||
-type registry_options() :: [registry_option()].
|
||||
|
||||
-type registry_option() :: {cbkmod, atom()}.
|
||||
|
||||
-type descriptor() :: #{ cbkmod := atom()
|
||||
, rgopts := registry_options()
|
||||
}.
|
||||
-type descriptor() :: #{
|
||||
cbkmod := atom(),
|
||||
rgopts := registry_options()
|
||||
}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
|
|
@ -63,13 +66,14 @@ start_link() ->
|
|||
%% Mgmt
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec reg(gateway_name(), registry_options())
|
||||
-> ok
|
||||
-spec reg(gateway_name(), registry_options()) ->
|
||||
ok
|
||||
| {error, any()}.
|
||||
reg(Name, RgOpts) ->
|
||||
CbMod = proplists:get_value(cbkmod, RgOpts, Name),
|
||||
Dscrptr = #{ cbkmod => CbMod
|
||||
, rgopts => RgOpts
|
||||
Dscrptr = #{
|
||||
cbkmod => CbMod,
|
||||
rgopts => RgOpts
|
||||
},
|
||||
call({reg, Name, Dscrptr}).
|
||||
|
||||
|
|
@ -110,7 +114,6 @@ handle_call({reg, Name, Dscrptr}, _From, State = #state{reged = Gateways}) ->
|
|||
_ ->
|
||||
{reply, {error, already_existed}, State}
|
||||
end;
|
||||
|
||||
handle_call({unreg, Name}, _From, State = #state{reged = Gateways}) ->
|
||||
case maps:get(Name, Gateways, undefined) of
|
||||
undefined ->
|
||||
|
|
@ -119,14 +122,11 @@ handle_call({unreg, Name}, _From, State = #state{reged = Gateways}) ->
|
|||
_ = emqx_gateway_sup:unload_gateway(Name),
|
||||
{reply, ok, State#state{reged = maps:remove(Name, Gateways)}}
|
||||
end;
|
||||
|
||||
handle_call(all, _From, State = #state{reged = Gateways}) ->
|
||||
{reply, maps:to_list(Gateways), State};
|
||||
|
||||
handle_call({lookup, Name}, _From, State = #state{reged = Gateways}) ->
|
||||
Reply = maps:get(Name, Gateways, undefined),
|
||||
{reply, Reply, State};
|
||||
|
||||
handle_call(Req, _From, State) ->
|
||||
logger:error("Unexpected call: ~0p", [Req]),
|
||||
{reply, ok, State}.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -23,14 +23,15 @@
|
|||
-export([start_link/0]).
|
||||
|
||||
%% Gateway APIs
|
||||
-export([ load_gateway/1
|
||||
, unload_gateway/1
|
||||
, lookup_gateway/1
|
||||
, update_gateway/2
|
||||
, start_gateway_insta/1
|
||||
, stop_gateway_insta/1
|
||||
, list_gateway_insta/0
|
||||
]).
|
||||
-export([
|
||||
load_gateway/1,
|
||||
unload_gateway/1,
|
||||
lookup_gateway/1,
|
||||
update_gateway/2,
|
||||
start_gateway_insta/1,
|
||||
stop_gateway_insta/1,
|
||||
list_gateway_insta/0
|
||||
]).
|
||||
|
||||
%% supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
|
@ -42,23 +43,24 @@
|
|||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
|
||||
-spec load_gateway(gateway()) -> {ok, pid()} | {error, any()}.
|
||||
load_gateway(Gateway = #{name := GwName}) ->
|
||||
case emqx_gateway_registry:lookup(GwName) of
|
||||
undefined -> {error, {unknown_gateway_name, GwName}};
|
||||
undefined ->
|
||||
{error, {unknown_gateway_name, GwName}};
|
||||
GwDscrptr ->
|
||||
{ok, GwSup} = ensure_gateway_suptree_ready(GwName),
|
||||
emqx_gateway_gw_sup:create_insta(GwSup, Gateway, GwDscrptr)
|
||||
end.
|
||||
|
||||
-spec unload_gateway(gateway_name())
|
||||
-> ok
|
||||
-spec unload_gateway(gateway_name()) ->
|
||||
ok
|
||||
| {error, not_found}
|
||||
| {error, any()}.
|
||||
unload_gateway(GwName) ->
|
||||
case lists:keyfind(GwName, 1, supervisor:which_children(?MODULE)) of
|
||||
false -> {error, not_found};
|
||||
false ->
|
||||
{error, not_found};
|
||||
{_Id, Pid, _Type, _Mods} ->
|
||||
_ = emqx_gateway_gw_sup:remove_insta(Pid, GwName),
|
||||
_ = supervisor:terminate_child(?MODULE, GwName),
|
||||
|
|
@ -75,21 +77,23 @@ lookup_gateway(GwName) ->
|
|||
undefined
|
||||
end.
|
||||
|
||||
-spec update_gateway(gateway_name(), emqx_config:config())
|
||||
-> ok
|
||||
-spec update_gateway(gateway_name(), emqx_config:config()) ->
|
||||
ok
|
||||
| {error, any()}.
|
||||
update_gateway(GwName, Config) ->
|
||||
case emqx_gateway_utils:find_sup_child(?MODULE, GwName) of
|
||||
{ok, GwSup} ->
|
||||
emqx_gateway_gw_sup:update_insta(GwSup, GwName, Config);
|
||||
_ -> {error, not_found}
|
||||
_ ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
start_gateway_insta(GwName) ->
|
||||
case search_gateway_insta_proc(GwName) of
|
||||
{ok, {GwSup, _}} ->
|
||||
emqx_gateway_gw_sup:start_insta(GwSup, GwName);
|
||||
_ -> {error, not_found}
|
||||
_ ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
-spec stop_gateway_insta(gateway_name()) -> ok | {error, any()}.
|
||||
|
|
@ -97,15 +101,20 @@ stop_gateway_insta(GwName) ->
|
|||
case search_gateway_insta_proc(GwName) of
|
||||
{ok, {GwSup, _}} ->
|
||||
emqx_gateway_gw_sup:stop_insta(GwSup, GwName);
|
||||
_ -> {error, not_found}
|
||||
_ ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
-spec list_gateway_insta() -> [gateway()].
|
||||
list_gateway_insta() ->
|
||||
lists:append(lists:map(
|
||||
lists:append(
|
||||
lists:map(
|
||||
fun(SupId) ->
|
||||
emqx_gateway_gw_sup:list_insta(SupId)
|
||||
end, list_started_gateway())).
|
||||
end,
|
||||
list_started_gateway()
|
||||
)
|
||||
).
|
||||
|
||||
-spec list_started_gateway() -> [gateway_name()].
|
||||
list_started_gateway() ->
|
||||
|
|
@ -114,12 +123,12 @@ list_started_gateway() ->
|
|||
%% Supervisor callback
|
||||
|
||||
init([]) ->
|
||||
SupFlags = #{ strategy => one_for_one
|
||||
, intensity => 10
|
||||
, period => 60
|
||||
SupFlags = #{
|
||||
strategy => one_for_one,
|
||||
intensity => 10,
|
||||
period => 60
|
||||
},
|
||||
ChildSpecs = [ emqx_gateway_utils:childspec(worker, emqx_gateway_registry)
|
||||
],
|
||||
ChildSpecs = [emqx_gateway_utils:childspec(worker, emqx_gateway_registry)],
|
||||
{ok, {SupFlags, ChildSpecs}}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
@ -147,24 +156,27 @@ search_gateway_insta_proc(InstaId) ->
|
|||
|
||||
search_gateway_insta_proc(_InstaId, []) ->
|
||||
{error, not_found};
|
||||
search_gateway_insta_proc(InstaId, [SupPid|More]) ->
|
||||
search_gateway_insta_proc(InstaId, [SupPid | More]) ->
|
||||
case emqx_gateway_utils:find_sup_child(SupPid, InstaId) of
|
||||
{ok, InstaPid} -> {ok, {SupPid, InstaPid}};
|
||||
_ ->
|
||||
search_gateway_insta_proc(InstaId, More)
|
||||
_ -> search_gateway_insta_proc(InstaId, More)
|
||||
end.
|
||||
|
||||
started_gateway() ->
|
||||
lists:filtermap(
|
||||
fun({Id, _, _, _}) ->
|
||||
is_a_gateway_id(Id) andalso {true, Id}
|
||||
end, supervisor:which_children(?MODULE)).
|
||||
end,
|
||||
supervisor:which_children(?MODULE)
|
||||
).
|
||||
|
||||
started_gateway_pid() ->
|
||||
lists:filtermap(
|
||||
fun({Id, Pid, _, _}) ->
|
||||
is_a_gateway_id(Id) andalso {true, Pid}
|
||||
end, supervisor:which_children(?MODULE)).
|
||||
end,
|
||||
supervisor:which_children(?MODULE)
|
||||
).
|
||||
|
||||
is_a_gateway_id(Id) ->
|
||||
Id /= emqx_gateway_registry.
|
||||
|
|
|
|||
|
|
@ -20,80 +20,86 @@
|
|||
-include("emqx_gateway.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-export([ childspec/2
|
||||
, childspec/3
|
||||
, childspec/4
|
||||
, supervisor_ret/1
|
||||
, find_sup_child/2
|
||||
]).
|
||||
-export([
|
||||
childspec/2,
|
||||
childspec/3,
|
||||
childspec/4,
|
||||
supervisor_ret/1,
|
||||
find_sup_child/2
|
||||
]).
|
||||
|
||||
-export([ start_listeners/4
|
||||
, start_listener/4
|
||||
, stop_listeners/2
|
||||
, stop_listener/2
|
||||
]).
|
||||
-export([
|
||||
start_listeners/4,
|
||||
start_listener/4,
|
||||
stop_listeners/2,
|
||||
stop_listener/2
|
||||
]).
|
||||
|
||||
-export([ apply/2
|
||||
, format_listenon/1
|
||||
, parse_listenon/1
|
||||
, unix_ts_to_rfc3339/1
|
||||
, unix_ts_to_rfc3339/2
|
||||
, listener_id/3
|
||||
, parse_listener_id/1
|
||||
, is_running/2
|
||||
, global_chain/1
|
||||
, listener_chain/3
|
||||
]).
|
||||
-export([
|
||||
apply/2,
|
||||
format_listenon/1,
|
||||
parse_listenon/1,
|
||||
unix_ts_to_rfc3339/1,
|
||||
unix_ts_to_rfc3339/2,
|
||||
listener_id/3,
|
||||
parse_listener_id/1,
|
||||
is_running/2,
|
||||
global_chain/1,
|
||||
listener_chain/3
|
||||
]).
|
||||
|
||||
-export([ stringfy/1
|
||||
]).
|
||||
-export([stringfy/1]).
|
||||
|
||||
-export([ normalize_config/1
|
||||
]).
|
||||
-export([normalize_config/1]).
|
||||
|
||||
%% Common Envs
|
||||
-export([ active_n/1
|
||||
, ratelimit/1
|
||||
, frame_options/1
|
||||
, init_gc_state/1
|
||||
, stats_timer/1
|
||||
, idle_timeout/1
|
||||
, oom_policy/1
|
||||
]).
|
||||
-export([
|
||||
active_n/1,
|
||||
ratelimit/1,
|
||||
frame_options/1,
|
||||
init_gc_state/1,
|
||||
stats_timer/1,
|
||||
idle_timeout/1,
|
||||
oom_policy/1
|
||||
]).
|
||||
|
||||
-export([ default_tcp_options/0
|
||||
, default_udp_options/0
|
||||
, default_subopts/0
|
||||
]).
|
||||
-export([
|
||||
default_tcp_options/0,
|
||||
default_udp_options/0,
|
||||
default_subopts/0
|
||||
]).
|
||||
|
||||
-define(ACTIVE_N, 100).
|
||||
-define(DEFAULT_IDLE_TIMEOUT, 30000).
|
||||
-define(DEFAULT_GC_OPTS, #{count => 1000, bytes => 1024*1024}).
|
||||
-define(DEFAULT_OOM_POLICY, #{max_heap_size => 4194304,
|
||||
max_message_queue_len => 32000}).
|
||||
-define(DEFAULT_GC_OPTS, #{count => 1000, bytes => 1024 * 1024}).
|
||||
-define(DEFAULT_OOM_POLICY, #{
|
||||
max_heap_size => 4194304,
|
||||
max_message_queue_len => 32000
|
||||
}).
|
||||
|
||||
-elvis([{elvis_style, god_modules, disable}]).
|
||||
|
||||
-spec childspec(supervisor:worker(), Mod :: atom())
|
||||
-> supervisor:child_spec().
|
||||
-spec childspec(supervisor:worker(), Mod :: atom()) ->
|
||||
supervisor:child_spec().
|
||||
childspec(Type, Mod) ->
|
||||
childspec(Mod, Type, Mod, []).
|
||||
|
||||
-spec childspec(supervisor:worker(), Mod :: atom(), Args :: list())
|
||||
-> supervisor:child_spec().
|
||||
-spec childspec(supervisor:worker(), Mod :: atom(), Args :: list()) ->
|
||||
supervisor:child_spec().
|
||||
childspec(Type, Mod, Args) ->
|
||||
childspec(Mod, Type, Mod, Args).
|
||||
|
||||
-spec childspec(atom(), supervisor:worker(), Mod :: atom(), Args :: list())
|
||||
-> supervisor:child_spec().
|
||||
-spec childspec(atom(), supervisor:worker(), Mod :: atom(), Args :: list()) ->
|
||||
supervisor:child_spec().
|
||||
childspec(Id, Type, Mod, Args) ->
|
||||
#{ id => Id
|
||||
, start => {Mod, start_link, Args}
|
||||
, type => Type
|
||||
#{
|
||||
id => Id,
|
||||
start => {Mod, start_link, Args},
|
||||
type => Type
|
||||
}.
|
||||
|
||||
-spec supervisor_ret(supervisor:startchild_ret())
|
||||
-> {ok, pid()}
|
||||
-spec supervisor_ret(supervisor:startchild_ret()) ->
|
||||
{ok, pid()}
|
||||
| {error, supervisor:startchild_err()}.
|
||||
supervisor_ret({ok, Pid, _Info}) ->
|
||||
{ok, Pid};
|
||||
|
|
@ -105,8 +111,8 @@ supervisor_ret({error, {Reason, Child}}) ->
|
|||
supervisor_ret(Ret) ->
|
||||
Ret.
|
||||
|
||||
-spec find_sup_child(Sup :: pid() | atom(), ChildId :: supervisor:child_id())
|
||||
-> false
|
||||
-spec find_sup_child(Sup :: pid() | atom(), ChildId :: supervisor:child_id()) ->
|
||||
false
|
||||
| {ok, pid()}.
|
||||
find_sup_child(Sup, ChildId) ->
|
||||
case lists:keyfind(ChildId, 1, supervisor:which_children(Sup)) of
|
||||
|
|
@ -115,13 +121,16 @@ find_sup_child(Sup, ChildId) ->
|
|||
end.
|
||||
|
||||
%% @doc start listeners. close all listeners if someone failed
|
||||
-spec start_listeners(Listeners :: list(),
|
||||
-spec start_listeners(
|
||||
Listeners :: list(),
|
||||
GwName :: atom(),
|
||||
Ctx :: map(),
|
||||
ModCfg)
|
||||
-> {ok, [pid()]}
|
||||
ModCfg
|
||||
) ->
|
||||
{ok, [pid()]}
|
||||
| {error, term()}
|
||||
when ModCfg :: #{frame_mod := atom(), chann_mod := atom()}.
|
||||
when
|
||||
ModCfg :: #{frame_mod := atom(), chann_mod := atom()}.
|
||||
start_listeners(Listeners, GwName, Ctx, ModCfg) ->
|
||||
start_listeners(Listeners, GwName, Ctx, ModCfg, []).
|
||||
|
||||
|
|
@ -133,47 +142,71 @@ start_listeners([L | Ls], GwName, Ctx, ModCfg, Acc) ->
|
|||
NAcc = Acc ++ [{listener, {{ListenerId, ListenOn}, Pid}}],
|
||||
start_listeners(Ls, GwName, Ctx, ModCfg, NAcc);
|
||||
{error, Reason} ->
|
||||
lists:foreach(fun({listener, {{ListenerId, ListenOn}, _}}) ->
|
||||
lists:foreach(
|
||||
fun({listener, {{ListenerId, ListenOn}, _}}) ->
|
||||
esockd:close({ListenerId, ListenOn})
|
||||
end, Acc),
|
||||
end,
|
||||
Acc
|
||||
),
|
||||
{error, {Reason, L}}
|
||||
end.
|
||||
|
||||
-spec start_listener(GwName :: atom(),
|
||||
-spec start_listener(
|
||||
GwName :: atom(),
|
||||
Ctx :: emqx_gateway_ctx:context(),
|
||||
Listener :: tuple(),
|
||||
ModCfg :: map())
|
||||
-> {ok, {ListenerId :: atom(), esockd:listen_on(), pid()}}
|
||||
ModCfg :: map()
|
||||
) ->
|
||||
{ok, {ListenerId :: atom(), esockd:listen_on(), pid()}}
|
||||
| {error, term()}.
|
||||
start_listener(GwName, Ctx,
|
||||
{Type, LisName, ListenOn, SocketOpts, Cfg}, ModCfg) ->
|
||||
start_listener(
|
||||
GwName,
|
||||
Ctx,
|
||||
{Type, LisName, ListenOn, SocketOpts, Cfg},
|
||||
ModCfg
|
||||
) ->
|
||||
ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn),
|
||||
ListenerId = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||
|
||||
NCfg = maps:merge(Cfg, ModCfg),
|
||||
case start_listener(GwName, Ctx, Type,
|
||||
LisName, ListenOn, SocketOpts, NCfg) of
|
||||
case
|
||||
start_listener(
|
||||
GwName,
|
||||
Ctx,
|
||||
Type,
|
||||
LisName,
|
||||
ListenOn,
|
||||
SocketOpts,
|
||||
NCfg
|
||||
)
|
||||
of
|
||||
{ok, Pid} ->
|
||||
console_print("Gateway ~ts:~ts:~ts on ~ts started.~n",
|
||||
[GwName, Type, LisName, ListenOnStr]),
|
||||
console_print(
|
||||
"Gateway ~ts:~ts:~ts on ~ts started.~n",
|
||||
[GwName, Type, LisName, ListenOnStr]
|
||||
),
|
||||
{ok, {ListenerId, ListenOn, Pid}};
|
||||
{error, Reason} ->
|
||||
?ELOG("Failed to start gateway ~ts:~ts:~ts on ~ts: ~0p~n",
|
||||
[GwName, Type, LisName, ListenOnStr, Reason]),
|
||||
?ELOG(
|
||||
"Failed to start gateway ~ts:~ts:~ts on ~ts: ~0p~n",
|
||||
[GwName, Type, LisName, ListenOnStr, Reason]
|
||||
),
|
||||
emqx_gateway_utils:supervisor_ret({error, Reason})
|
||||
end.
|
||||
|
||||
start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
||||
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||
NCfg = Cfg#{ ctx => Ctx
|
||||
, listener => {GwName, Type, LisName}
|
||||
NCfg = Cfg#{
|
||||
ctx => Ctx,
|
||||
listener => {GwName, Type, LisName}
|
||||
},
|
||||
NSocketOpts = merge_default(Type, SocketOpts),
|
||||
MFA = {emqx_gateway_conn, start_link, [NCfg]},
|
||||
do_start_listener(Type, Name, ListenOn, NSocketOpts, MFA).
|
||||
|
||||
merge_default(Udp, Options) ->
|
||||
{Key, Default} = case Udp of
|
||||
{Key, Default} =
|
||||
case Udp of
|
||||
udp ->
|
||||
{udp_options, default_udp_options()};
|
||||
dtls ->
|
||||
|
|
@ -185,15 +218,18 @@ merge_default(Udp, Options) ->
|
|||
end,
|
||||
case lists:keytake(Key, 1, Options) of
|
||||
{value, {Key, TcpOpts}, Options1} ->
|
||||
[{Key, emqx_misc:merge_opts(Default, TcpOpts)}
|
||||
| Options1];
|
||||
[
|
||||
{Key, emqx_misc:merge_opts(Default, TcpOpts)}
|
||||
| Options1
|
||||
];
|
||||
false ->
|
||||
[{Key, Default} | Options]
|
||||
end.
|
||||
|
||||
do_start_listener(Type, Name, ListenOn, SocketOpts, MFA)
|
||||
when Type == tcp;
|
||||
Type == ssl ->
|
||||
do_start_listener(Type, Name, ListenOn, SocketOpts, MFA) when
|
||||
Type == tcp;
|
||||
Type == ssl
|
||||
->
|
||||
esockd:open(Name, ListenOn, SocketOpts, MFA);
|
||||
do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) ->
|
||||
esockd:open_udp(Name, ListenOn, SocketOpts, MFA);
|
||||
|
|
@ -210,11 +246,15 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
|||
ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn),
|
||||
case StopRet of
|
||||
ok ->
|
||||
console_print("Gateway ~ts:~ts:~ts on ~ts stopped.~n",
|
||||
[GwName, Type, LisName, ListenOnStr]);
|
||||
console_print(
|
||||
"Gateway ~ts:~ts:~ts on ~ts stopped.~n",
|
||||
[GwName, Type, LisName, ListenOnStr]
|
||||
);
|
||||
{error, Reason} ->
|
||||
?ELOG("Failed to stop gateway ~ts:~ts:~ts on ~ts: ~0p~n",
|
||||
[GwName, Type, LisName, ListenOnStr, Reason])
|
||||
?ELOG(
|
||||
"Failed to stop gateway ~ts:~ts:~ts on ~ts: ~0p~n",
|
||||
[GwName, Type, LisName, ListenOnStr, Reason]
|
||||
)
|
||||
end,
|
||||
StopRet.
|
||||
|
||||
|
|
@ -228,17 +268,23 @@ console_print(Fmt, Args) -> ?ULOG(Fmt, Args).
|
|||
console_print(_Fmt, _Args) -> ok.
|
||||
-endif.
|
||||
|
||||
apply({M, F, A}, A2) when is_atom(M),
|
||||
apply({M, F, A}, A2) when
|
||||
is_atom(M),
|
||||
is_atom(M),
|
||||
is_list(A),
|
||||
is_list(A2) ->
|
||||
is_list(A2)
|
||||
->
|
||||
erlang:apply(M, F, A ++ A2);
|
||||
apply({F, A}, A2) when is_function(F),
|
||||
apply({F, A}, A2) when
|
||||
is_function(F),
|
||||
is_list(A),
|
||||
is_list(A2) ->
|
||||
is_list(A2)
|
||||
->
|
||||
erlang:apply(F, A ++ A2);
|
||||
apply(F, A2) when is_function(F),
|
||||
is_list(A2) ->
|
||||
apply(F, A2) when
|
||||
is_function(F),
|
||||
is_list(A2)
|
||||
->
|
||||
erlang:apply(F, A2).
|
||||
|
||||
format_listenon(Port) when is_integer(Port) ->
|
||||
|
|
@ -255,21 +301,20 @@ parse_listenon(IpPort) when is_tuple(IpPort) ->
|
|||
parse_listenon(Str) when is_binary(Str) ->
|
||||
parse_listenon(binary_to_list(Str));
|
||||
parse_listenon(Str) when is_list(Str) ->
|
||||
try list_to_integer(Str)
|
||||
catch _ : _ ->
|
||||
try
|
||||
list_to_integer(Str)
|
||||
catch
|
||||
_:_ ->
|
||||
case emqx_schema:to_ip_port(Str) of
|
||||
{ok, R} -> R;
|
||||
{error, _} ->
|
||||
error({invalid_listenon_name, Str})
|
||||
{error, _} -> error({invalid_listenon_name, Str})
|
||||
end
|
||||
end.
|
||||
|
||||
listener_id(GwName, Type, LisName) ->
|
||||
binary_to_atom(
|
||||
<<(bin(GwName))/binary, ":",
|
||||
(bin(Type))/binary, ":",
|
||||
(bin(LisName))/binary
|
||||
>>).
|
||||
<<(bin(GwName))/binary, ":", (bin(Type))/binary, ":", (bin(LisName))/binary>>
|
||||
).
|
||||
|
||||
parse_listener_id(Id) when is_atom(Id) ->
|
||||
parse_listener_id(atom_to_binary(Id));
|
||||
|
|
@ -278,15 +323,16 @@ parse_listener_id(Id) ->
|
|||
[GwName, Type, Name] = binary:split(bin(Id), <<":">>, [global]),
|
||||
{GwName, Type, Name}
|
||||
catch
|
||||
_ : _ -> error({invalid_listener_id, Id})
|
||||
_:_ -> error({invalid_listener_id, Id})
|
||||
end.
|
||||
|
||||
is_running(ListenerId, #{<<"bind">> := ListenOn0}) ->
|
||||
ListenOn = emqx_gateway_utils:parse_listenon(ListenOn0),
|
||||
try esockd:listener({ListenerId, ListenOn}) of
|
||||
Pid when is_pid(Pid)->
|
||||
Pid when is_pid(Pid) ->
|
||||
true
|
||||
catch _:_ ->
|
||||
catch
|
||||
_:_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
|
|
@ -315,10 +361,13 @@ unix_ts_to_rfc3339(Keys, Map) when is_list(Keys) ->
|
|||
lists:foldl(fun(K, Acc) -> unix_ts_to_rfc3339(K, Acc) end, Map, Keys);
|
||||
unix_ts_to_rfc3339(Key, Map) ->
|
||||
case maps:get(Key, Map, undefined) of
|
||||
undefined -> Map;
|
||||
undefined ->
|
||||
Map;
|
||||
Ts ->
|
||||
Map#{Key =>
|
||||
emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)}
|
||||
Map#{
|
||||
Key =>
|
||||
emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)
|
||||
}
|
||||
end.
|
||||
|
||||
unix_ts_to_rfc3339(Ts) ->
|
||||
|
|
@ -330,60 +379,104 @@ stringfy(T) when is_list(T); is_binary(T) ->
|
|||
stringfy(T) ->
|
||||
iolist_to_binary(io_lib:format("~0p", [T])).
|
||||
|
||||
-spec normalize_config(emqx_config:config())
|
||||
-> list({ Type :: udp | tcp | ssl | dtls
|
||||
, Name :: atom()
|
||||
, ListenOn :: esockd:listen_on()
|
||||
, SocketOpts :: esockd:option()
|
||||
, Cfg :: map()
|
||||
-spec normalize_config(emqx_config:config()) ->
|
||||
list({
|
||||
Type :: udp | tcp | ssl | dtls,
|
||||
Name :: atom(),
|
||||
ListenOn :: esockd:listen_on(),
|
||||
SocketOpts :: esockd:option(),
|
||||
Cfg :: map()
|
||||
}).
|
||||
normalize_config(RawConf) ->
|
||||
LisMap = maps:get(listeners, RawConf, #{}),
|
||||
Cfg0 = maps:without([listeners], RawConf),
|
||||
lists:append(maps:fold(fun(Type, Liss, AccIn1) ->
|
||||
lists:append(
|
||||
maps:fold(
|
||||
fun(Type, Liss, AccIn1) ->
|
||||
Listeners =
|
||||
maps:fold(fun(Name, Confs, AccIn2) ->
|
||||
maps:fold(
|
||||
fun(Name, Confs, AccIn2) ->
|
||||
ListenOn = maps:get(bind, Confs),
|
||||
SocketOpts = esockd_opts(Type, Confs),
|
||||
RemainCfgs = maps:without(
|
||||
[bind, tcp, ssl, udp, dtls]
|
||||
++ proplists:get_keys(SocketOpts), Confs),
|
||||
[bind, tcp, ssl, udp, dtls] ++
|
||||
proplists:get_keys(SocketOpts),
|
||||
Confs
|
||||
),
|
||||
Cfg = maps:merge(Cfg0, RemainCfgs),
|
||||
[{Type, Name, ListenOn, SocketOpts, Cfg} | AccIn2]
|
||||
end, [], Liss),
|
||||
end,
|
||||
[],
|
||||
Liss
|
||||
),
|
||||
[Listeners | AccIn1]
|
||||
end, [], LisMap)).
|
||||
end,
|
||||
[],
|
||||
LisMap
|
||||
)
|
||||
).
|
||||
|
||||
esockd_opts(Type, Opts0) ->
|
||||
Opts1 = maps:with([acceptors, max_connections, max_conn_rate,
|
||||
proxy_protocol, proxy_protocol_timeout], Opts0),
|
||||
Opts1 = maps:with(
|
||||
[
|
||||
acceptors,
|
||||
max_connections,
|
||||
max_conn_rate,
|
||||
proxy_protocol,
|
||||
proxy_protocol_timeout
|
||||
],
|
||||
Opts0
|
||||
),
|
||||
Opts2 = Opts1#{access_rules => esockd_access_rules(maps:get(access_rules, Opts0, []))},
|
||||
maps:to_list(case Type of
|
||||
tcp -> Opts2#{tcp_options => sock_opts(tcp, Opts0)};
|
||||
ssl -> Opts2#{tcp_options => sock_opts(tcp, Opts0),
|
||||
ssl_options => ssl_opts(ssl, Opts0)};
|
||||
udp -> Opts2#{udp_options => sock_opts(udp, Opts0)};
|
||||
dtls -> Opts2#{udp_options => sock_opts(udp, Opts0),
|
||||
dtls_options => ssl_opts(dtls, Opts0)}
|
||||
end).
|
||||
maps:to_list(
|
||||
case Type of
|
||||
tcp ->
|
||||
Opts2#{tcp_options => sock_opts(tcp, Opts0)};
|
||||
ssl ->
|
||||
Opts2#{
|
||||
tcp_options => sock_opts(tcp, Opts0),
|
||||
ssl_options => ssl_opts(ssl, Opts0)
|
||||
};
|
||||
udp ->
|
||||
Opts2#{udp_options => sock_opts(udp, Opts0)};
|
||||
dtls ->
|
||||
Opts2#{
|
||||
udp_options => sock_opts(udp, Opts0),
|
||||
dtls_options => ssl_opts(dtls, Opts0)
|
||||
}
|
||||
end
|
||||
).
|
||||
|
||||
esockd_access_rules(StrRules) ->
|
||||
Access = fun(S) ->
|
||||
[A, CIDR] = string:tokens(S, " "),
|
||||
{list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end}
|
||||
{
|
||||
list_to_atom(A),
|
||||
case CIDR of
|
||||
"all" -> all;
|
||||
_ -> CIDR
|
||||
end
|
||||
}
|
||||
end,
|
||||
[Access(R) || R <- StrRules].
|
||||
|
||||
ssl_opts(Name, Opts) ->
|
||||
maps:to_list(
|
||||
emqx_tls_lib:drop_tls13_for_old_otp(
|
||||
maps:without([enable],
|
||||
maps:get(Name, Opts, #{})))).
|
||||
maps:without(
|
||||
[enable],
|
||||
maps:get(Name, Opts, #{})
|
||||
)
|
||||
)
|
||||
).
|
||||
|
||||
sock_opts(Name, Opts) ->
|
||||
maps:to_list(
|
||||
maps:without([active_n],
|
||||
maps:get(Name, Opts, #{}))).
|
||||
maps:without(
|
||||
[active_n],
|
||||
maps:get(Name, Opts, #{})
|
||||
)
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Envs
|
||||
|
|
@ -417,7 +510,10 @@ oom_policy(Options) ->
|
|||
|
||||
-spec stats_timer(map()) -> undefined | disabled.
|
||||
stats_timer(Options) ->
|
||||
case enable_stats(Options) of true -> undefined; false -> disabled end.
|
||||
case enable_stats(Options) of
|
||||
true -> undefined;
|
||||
false -> disabled
|
||||
end.
|
||||
|
||||
-spec enable_stats(map()) -> boolean().
|
||||
enable_stats(Options) ->
|
||||
|
|
@ -427,16 +523,26 @@ enable_stats(Options) ->
|
|||
%% Envs2
|
||||
|
||||
default_tcp_options() ->
|
||||
[binary, {packet, raw}, {reuseaddr, true},
|
||||
{nodelay, true}, {backlog, 512}].
|
||||
[
|
||||
binary,
|
||||
{packet, raw},
|
||||
{reuseaddr, true},
|
||||
{nodelay, true},
|
||||
{backlog, 512}
|
||||
].
|
||||
|
||||
default_udp_options() ->
|
||||
[binary].
|
||||
|
||||
default_subopts() ->
|
||||
#{rh => 1, %% Retain Handling
|
||||
rap => 0, %% Retain as Publish
|
||||
nl => 0, %% No Local
|
||||
qos => 0, %% QoS
|
||||
%% Retain Handling
|
||||
#{
|
||||
rh => 1,
|
||||
%% Retain as Publish
|
||||
rap => 0,
|
||||
%% No Local
|
||||
nl => 0,
|
||||
%% QoS
|
||||
qos => 0,
|
||||
is_new => true
|
||||
}.
|
||||
|
|
|
|||
|
|
@ -21,21 +21,22 @@
|
|||
-include_lib("emqx/include/types.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-export([
|
||||
info/1,
|
||||
info/2,
|
||||
stats/1
|
||||
]).
|
||||
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
]).
|
||||
|
||||
-export([ init/2
|
||||
, handle_in/2
|
||||
, handle_deliver/2
|
||||
, handle_timeout/3
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
]).
|
||||
-export([
|
||||
init/2,
|
||||
handle_in/2,
|
||||
handle_deliver/2,
|
||||
handle_timeout/3,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2
|
||||
]).
|
||||
|
||||
-export_type([channel/0]).
|
||||
|
||||
|
|
@ -62,22 +63,23 @@
|
|||
timers :: #{atom() => disabled | maybe(reference())},
|
||||
%% Closed reason
|
||||
closed_reason = undefined
|
||||
}).
|
||||
}).
|
||||
|
||||
-opaque(channel() :: #channel{}).
|
||||
-opaque channel() :: #channel{}.
|
||||
|
||||
-type(conn_state() :: idle | connecting | connected | disconnected).
|
||||
-type conn_state() :: idle | connecting | connected | disconnected.
|
||||
|
||||
-type(reply() :: {outgoing, binary()}
|
||||
-type reply() ::
|
||||
{outgoing, binary()}
|
||||
| {outgoing, [binary()]}
|
||||
| {close, Reason :: atom()}).
|
||||
| {close, Reason :: atom()}.
|
||||
|
||||
-type(replies() :: emqx_types:packet() | reply() | [reply()]).
|
||||
-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]).
|
||||
|
||||
|
|
@ -90,7 +92,7 @@
|
|||
info(Channel) ->
|
||||
maps:from_list(info(?INFO_KEYS, Channel)).
|
||||
|
||||
-spec info(list(atom())|atom(), channel()) -> term().
|
||||
-spec info(list(atom()) | atom(), channel()) -> term().
|
||||
info(Keys, Channel) when is_list(Keys) ->
|
||||
[{Key, info(Key, Channel)} || Key <- Keys];
|
||||
info(conninfo, #channel{conninfo = ConnInfo}) ->
|
||||
|
|
@ -99,13 +101,17 @@ info(clientid, #channel{clientinfo = ClientInfo}) ->
|
|||
maps:get(clientid, ClientInfo, undefined);
|
||||
info(clientinfo, #channel{clientinfo = ClientInfo}) ->
|
||||
ClientInfo;
|
||||
info(session, #channel{subscriptions = Subs,
|
||||
conninfo = ConnInfo}) ->
|
||||
#{subscriptions => Subs,
|
||||
info(session, #channel{
|
||||
subscriptions = Subs,
|
||||
conninfo = ConnInfo
|
||||
}) ->
|
||||
#{
|
||||
subscriptions => Subs,
|
||||
upgrade_qos => false,
|
||||
retry_interval => 0,
|
||||
await_rel_timeout => 0,
|
||||
created_at => maps:get(connected_at, ConnInfo)};
|
||||
created_at => maps:get(connected_at, ConnInfo)
|
||||
};
|
||||
info(conn_state, #channel{conn_state = ConnState}) ->
|
||||
ConnState;
|
||||
info(will_msg, _) ->
|
||||
|
|
@ -115,7 +121,8 @@ info(ctx, #channel{ctx = Ctx}) ->
|
|||
|
||||
-spec stats(channel()) -> emqx_types:stats().
|
||||
stats(#channel{subscriptions = Subs}) ->
|
||||
[{subscriptions_cnt, maps:size(Subs)},
|
||||
[
|
||||
{subscriptions_cnt, maps:size(Subs)},
|
||||
{subscriptions_max, 0},
|
||||
{inflight_cnt, 0},
|
||||
{inflight_max, 0},
|
||||
|
|
@ -124,25 +131,31 @@ stats(#channel{subscriptions = Subs}) ->
|
|||
{mqueue_dropped, 0},
|
||||
{next_pkt_id, 0},
|
||||
{awaiting_rel_cnt, 0},
|
||||
{awaiting_rel_max, 0}].
|
||||
{awaiting_rel_max, 0}
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Init the channel
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec init(emqx_exproto_types:conninfo(), map()) -> channel().
|
||||
init(ConnInfo = #{socktype := Socktype,
|
||||
init(
|
||||
ConnInfo = #{
|
||||
socktype := Socktype,
|
||||
peername := Peername,
|
||||
sockname := Sockname,
|
||||
peercert := Peercert}, Options) ->
|
||||
peercert := Peercert
|
||||
},
|
||||
Options
|
||||
) ->
|
||||
Ctx = maps:get(ctx, Options),
|
||||
GRpcChann = maps:get(handler, Options),
|
||||
PoolName = maps:get(pool_name, Options),
|
||||
NConnInfo = default_conninfo(ConnInfo),
|
||||
ListenerId = case maps:get(listener, Options, undefined) of
|
||||
ListenerId =
|
||||
case maps:get(listener, Options, undefined) of
|
||||
undefined -> undefined;
|
||||
{GwName, Type, LisName} ->
|
||||
emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||
{GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||
end,
|
||||
ClientInfo = maps:put(listener, ListenerId, default_clientinfo(ConnInfo)),
|
||||
Channel = #channel{
|
||||
|
|
@ -154,22 +167,33 @@ init(ConnInfo = #{socktype := Socktype,
|
|||
timers = #{}
|
||||
},
|
||||
|
||||
Req = #{conninfo =>
|
||||
peercert(Peercert,
|
||||
#{socktype => socktype(Socktype),
|
||||
Req = #{
|
||||
conninfo =>
|
||||
peercert(
|
||||
Peercert,
|
||||
#{
|
||||
socktype => socktype(Socktype),
|
||||
peername => address(Peername),
|
||||
sockname => address(Sockname)})},
|
||||
sockname => address(Sockname)
|
||||
}
|
||||
)
|
||||
},
|
||||
try_dispatch(on_socket_created, wrap(Req), Channel).
|
||||
|
||||
%% @private
|
||||
peercert(NoSsl, ConnInfo) when NoSsl == nossl;
|
||||
NoSsl == undefined ->
|
||||
peercert(NoSsl, ConnInfo) when
|
||||
NoSsl == nossl;
|
||||
NoSsl == undefined
|
||||
->
|
||||
ConnInfo;
|
||||
peercert(Peercert, ConnInfo) ->
|
||||
Fn = fun(_, V) -> V =/= undefined end,
|
||||
Infos = maps:filter(Fn,
|
||||
#{cn => esockd_peercert:common_name(Peercert),
|
||||
dn => esockd_peercert:subject(Peercert)}
|
||||
Infos = maps:filter(
|
||||
Fn,
|
||||
#{
|
||||
cn => esockd_peercert:common_name(Peercert),
|
||||
dn => esockd_peercert:subject(Peercert)
|
||||
}
|
||||
),
|
||||
case maps:size(Infos) of
|
||||
0 ->
|
||||
|
|
@ -192,27 +216,37 @@ address({Host, Port}) ->
|
|||
%% Handle incoming packet
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec handle_in(binary(), channel())
|
||||
-> {ok, channel()}
|
||||
-spec handle_in(binary(), channel()) ->
|
||||
{ok, channel()}
|
||||
| {shutdown, Reason :: term(), channel()}.
|
||||
handle_in(Data, Channel) ->
|
||||
Req = #{bytes => Data},
|
||||
{ok, try_dispatch(on_received_bytes, wrap(Req), Channel)}.
|
||||
|
||||
-spec handle_deliver(list(emqx_types:deliver()), channel())
|
||||
-> {ok, channel()}
|
||||
-spec handle_deliver(list(emqx_types:deliver()), channel()) ->
|
||||
{ok, channel()}
|
||||
| {shutdown, Reason :: term(), channel()}.
|
||||
handle_deliver(Delivers, Channel = #channel{ctx = Ctx,
|
||||
clientinfo = ClientInfo}) ->
|
||||
handle_deliver(
|
||||
Delivers,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
%% XXX: ?? Nack delivers from shared subscriptions
|
||||
Mountpoint = maps:get(mountpoint, ClientInfo),
|
||||
NodeStr = atom_to_binary(node(), utf8),
|
||||
Msgs = lists:map(fun({_, _, Msg}) ->
|
||||
Msgs = lists:map(
|
||||
fun({_, _, Msg}) ->
|
||||
ok = metrics_inc(Ctx, 'messages.delivered'),
|
||||
Msg1 = emqx_hooks:run_fold('message.delivered',
|
||||
[ClientInfo], Msg),
|
||||
Msg1 = emqx_hooks:run_fold(
|
||||
'message.delivered',
|
||||
[ClientInfo],
|
||||
Msg
|
||||
),
|
||||
NMsg = emqx_mountpoint:unmount(Mountpoint, Msg1),
|
||||
#{node => NodeStr,
|
||||
#{
|
||||
node => NodeStr,
|
||||
id => emqx_guid:to_hexstr(emqx_message:id(NMsg)),
|
||||
qos => emqx_message:qos(NMsg),
|
||||
from => fmt_from(emqx_message:from(NMsg)),
|
||||
|
|
@ -220,18 +254,26 @@ handle_deliver(Delivers, Channel = #channel{ctx = Ctx,
|
|||
payload => emqx_message:payload(NMsg),
|
||||
timestamp => emqx_message:timestamp(NMsg)
|
||||
}
|
||||
end, Delivers),
|
||||
end,
|
||||
Delivers
|
||||
),
|
||||
Req = #{messages => Msgs},
|
||||
{ok, try_dispatch(on_received_messages, wrap(Req), Channel)}.
|
||||
|
||||
-spec handle_timeout(reference(), Msg :: term(), channel())
|
||||
-> {ok, channel()}
|
||||
-spec handle_timeout(reference(), Msg :: term(), channel()) ->
|
||||
{ok, channel()}
|
||||
| {shutdown, Reason :: term(), channel()}.
|
||||
handle_timeout(_TRef, {keepalive, _StatVal},
|
||||
Channel = #channel{keepalive = undefined}) ->
|
||||
handle_timeout(
|
||||
_TRef,
|
||||
{keepalive, _StatVal},
|
||||
Channel = #channel{keepalive = undefined}
|
||||
) ->
|
||||
{ok, Channel};
|
||||
handle_timeout(_TRef, {keepalive, StatVal},
|
||||
Channel = #channel{keepalive = Keepalive}) ->
|
||||
handle_timeout(
|
||||
_TRef,
|
||||
{keepalive, StatVal},
|
||||
Channel = #channel{keepalive = Keepalive}
|
||||
) ->
|
||||
case emqx_keepalive:check(StatVal, Keepalive) of
|
||||
{ok, NKeepalive} ->
|
||||
NChannel = Channel#channel{keepalive = NKeepalive},
|
||||
|
|
@ -240,97 +282,119 @@ handle_timeout(_TRef, {keepalive, StatVal},
|
|||
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) ->
|
||||
?SLOG(warning, #{msg => "unexpected_timeout_signal",
|
||||
signal => Msg}),
|
||||
?SLOG(warning, #{
|
||||
msg => "unexpected_timeout_signal",
|
||||
signal => Msg
|
||||
}),
|
||||
{ok, Channel}.
|
||||
|
||||
-spec handle_call(Req :: any(), From :: any(), channel())
|
||||
-> {reply, Reply :: term(), channel()}
|
||||
-spec handle_call(Req :: any(), From :: any(), channel()) ->
|
||||
{reply, Reply :: term(), channel()}
|
||||
| {reply, Reply :: term(), replies(), channel()}
|
||||
| {shutdown, Reason :: term(), Reply :: term(), channel()}.
|
||||
|
||||
handle_call({send, Data}, _From, Channel) ->
|
||||
{reply, ok, [{outgoing, Data}], Channel};
|
||||
|
||||
handle_call(close, _From, Channel = #channel{conn_state = connected}) ->
|
||||
{reply, ok, [{event, disconnected}, {close, normal}], Channel};
|
||||
handle_call(close, _From, Channel) ->
|
||||
{reply, ok, [{close, normal}], Channel};
|
||||
|
||||
handle_call({auth, ClientInfo, _Password}, _From,
|
||||
Channel = #channel{conn_state = connected}) ->
|
||||
?SLOG(warning, #{ msg => "ingore_duplicated_authorized_command"
|
||||
, request_clientinfo => ClientInfo
|
||||
handle_call(
|
||||
{auth, ClientInfo, _Password},
|
||||
_From,
|
||||
Channel = #channel{conn_state = connected}
|
||||
) ->
|
||||
?SLOG(warning, #{
|
||||
msg => "ingore_duplicated_authorized_command",
|
||||
request_clientinfo => ClientInfo
|
||||
}),
|
||||
{reply, {error, ?RESP_PERMISSION_DENY, <<"Duplicated authenticate command">>}, Channel};
|
||||
handle_call({auth, ClientInfo0, Password}, _From,
|
||||
handle_call(
|
||||
{auth, ClientInfo0, Password},
|
||||
_From,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
ClientInfo1 = enrich_clientinfo(ClientInfo0, ClientInfo),
|
||||
ConnInfo1 = enrich_conninfo(ClientInfo0, ConnInfo),
|
||||
|
||||
Channel1 = Channel#channel{conninfo = ConnInfo1,
|
||||
clientinfo = ClientInfo1},
|
||||
Channel1 = Channel#channel{
|
||||
conninfo = ConnInfo1,
|
||||
clientinfo = ClientInfo1
|
||||
},
|
||||
|
||||
#{clientid := ClientId, username := Username} = ClientInfo1,
|
||||
|
||||
case emqx_gateway_ctx:authenticate(
|
||||
Ctx, ClientInfo1#{password => Password}) of
|
||||
case
|
||||
emqx_gateway_ctx:authenticate(
|
||||
Ctx, ClientInfo1#{password => Password}
|
||||
)
|
||||
of
|
||||
{ok, NClientInfo} ->
|
||||
SessFun = fun(_, _) -> #{} end,
|
||||
emqx_logger:set_metadata_clientid(ClientId),
|
||||
case emqx_gateway_ctx:open_session(
|
||||
case
|
||||
emqx_gateway_ctx:open_session(
|
||||
Ctx,
|
||||
true,
|
||||
NClientInfo,
|
||||
ConnInfo1,
|
||||
SessFun
|
||||
) of
|
||||
)
|
||||
of
|
||||
{ok, _Session} ->
|
||||
?SLOG(debug, #{ msg => "client_login_succeed"
|
||||
, clientid => ClientId
|
||||
, username => Username
|
||||
?SLOG(debug, #{
|
||||
msg => "client_login_succeed",
|
||||
clientid => ClientId,
|
||||
username => Username
|
||||
}),
|
||||
{reply, ok, [{event, connected}],
|
||||
ensure_connected(Channel1#channel{clientinfo = NClientInfo})};
|
||||
{error, Reason} ->
|
||||
?SLOG(warning, #{ msg => "client_login_failed"
|
||||
, clientid => ClientId
|
||||
, username => Username
|
||||
, reason => Reason
|
||||
?SLOG(warning, #{
|
||||
msg => "client_login_failed",
|
||||
clientid => ClientId,
|
||||
username => Username,
|
||||
reason => Reason
|
||||
}),
|
||||
{reply, {error, ?RESP_PERMISSION_DENY, Reason}, Channel}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
?SLOG(warning, #{ msg => "client_login_failed"
|
||||
, clientid => ClientId
|
||||
, username => Username
|
||||
, reason => Reason}),
|
||||
?SLOG(warning, #{
|
||||
msg => "client_login_failed",
|
||||
clientid => ClientId,
|
||||
username => Username,
|
||||
reason => Reason
|
||||
}),
|
||||
{reply, {error, ?RESP_PERMISSION_DENY, Reason}, Channel}
|
||||
end;
|
||||
|
||||
handle_call({start_timer, keepalive, Interval}, _From,
|
||||
handle_call(
|
||||
{start_timer, keepalive, Interval},
|
||||
_From,
|
||||
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_from_client, TopicFilter, Qos}, _From,
|
||||
handle_call(
|
||||
{subscribe_from_client, TopicFilter, Qos},
|
||||
_From,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conn_state = connected,
|
||||
clientinfo = ClientInfo}) ->
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, TopicFilter) of
|
||||
deny ->
|
||||
{reply, {error, ?RESP_PERMISSION_DENY, <<"Authorization deny">>}, Channel};
|
||||
|
|
@ -338,31 +402,35 @@ handle_call({subscribe_from_client, TopicFilter, Qos}, _From,
|
|||
{ok, _, NChannel} = do_subscribe([{TopicFilter, #{qos => Qos}}], Channel),
|
||||
{reply, ok, NChannel}
|
||||
end;
|
||||
|
||||
handle_call({subscribe, Topic, SubOpts}, _From, Channel) ->
|
||||
{ok,
|
||||
[{NTopicFilter, NSubOpts}], NChannel} = do_subscribe([{Topic, SubOpts}], Channel),
|
||||
{ok, [{NTopicFilter, NSubOpts}], NChannel} = do_subscribe([{Topic, SubOpts}], Channel),
|
||||
{reply, {ok, {NTopicFilter, NSubOpts}}, NChannel};
|
||||
|
||||
handle_call({unsubscribe_from_client, TopicFilter}, _From,
|
||||
Channel = #channel{conn_state = connected}) ->
|
||||
handle_call(
|
||||
{unsubscribe_from_client, TopicFilter},
|
||||
_From,
|
||||
Channel = #channel{conn_state = connected}
|
||||
) ->
|
||||
{ok, NChannel} = do_unsubscribe([{TopicFilter, #{}}], Channel),
|
||||
{reply, ok, NChannel};
|
||||
|
||||
handle_call({unsubscribe, Topic}, _From, Channel) ->
|
||||
{ok, NChannel} = do_unsubscribe([Topic], Channel),
|
||||
{reply, ok, NChannel};
|
||||
|
||||
handle_call(subscriptions, _From, Channel = #channel{subscriptions = Subs}) ->
|
||||
{reply, {ok, maps:to_list(Subs)}, Channel};
|
||||
|
||||
handle_call({publish, Topic, Qos, Payload}, _From,
|
||||
handle_call(
|
||||
{publish, Topic, Qos, Payload},
|
||||
_From,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conn_state = connected,
|
||||
clientinfo = ClientInfo
|
||||
= #{clientid := From,
|
||||
mountpoint := Mountpoint}}) ->
|
||||
clientinfo =
|
||||
ClientInfo =
|
||||
#{
|
||||
clientid := From,
|
||||
mountpoint := Mountpoint
|
||||
}
|
||||
}
|
||||
) ->
|
||||
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of
|
||||
deny ->
|
||||
{reply, {error, ?RESP_PERMISSION_DENY, <<"Authorization deny">>}, Channel};
|
||||
|
|
@ -372,36 +440,39 @@ handle_call({publish, Topic, Qos, Payload}, _From,
|
|||
_ = emqx:publish(NMsg),
|
||||
{reply, ok, Channel}
|
||||
end;
|
||||
|
||||
handle_call(kick, _From, Channel) ->
|
||||
{shutdown, kicked, ok, ensure_disconnected(kicked, Channel)};
|
||||
|
||||
handle_call(discard, _From, Channel) ->
|
||||
{shutdown, discarded, ok, Channel};
|
||||
|
||||
handle_call(Req, _From, Channel) ->
|
||||
?SLOG(warning, #{ msg => "unexpected_call"
|
||||
, call => Req
|
||||
?SLOG(warning, #{
|
||||
msg => "unexpected_call",
|
||||
call => Req
|
||||
}),
|
||||
{reply, {error, unexpected_call}, Channel}.
|
||||
|
||||
-spec handle_cast(any(), channel())
|
||||
-> {ok, channel()}
|
||||
-spec handle_cast(any(), channel()) ->
|
||||
{ok, channel()}
|
||||
| {ok, replies(), channel()}
|
||||
| {shutdown, Reason :: term(), channel()}.
|
||||
handle_cast(Req, Channel) ->
|
||||
?SLOG(warning, #{ msg => "unexpected_call"
|
||||
, call => Req
|
||||
?SLOG(warning, #{
|
||||
msg => "unexpected_call",
|
||||
call => Req
|
||||
}),
|
||||
{ok, Channel}.
|
||||
|
||||
-spec handle_info(any(), channel())
|
||||
-> {ok, channel()}
|
||||
-spec handle_info(any(), channel()) ->
|
||||
{ok, channel()}
|
||||
| {shutdown, Reason :: term(), channel()}.
|
||||
handle_info({sock_closed, Reason},
|
||||
Channel = #channel{rqueue = Queue, inflight = Inflight}) ->
|
||||
case queue:len(Queue) =:= 0
|
||||
andalso Inflight =:= undefined of
|
||||
handle_info(
|
||||
{sock_closed, Reason},
|
||||
Channel = #channel{rqueue = Queue, inflight = Inflight}
|
||||
) ->
|
||||
case
|
||||
queue:len(Queue) =:= 0 andalso
|
||||
Inflight =:= undefined
|
||||
of
|
||||
true ->
|
||||
Channel1 = ensure_disconnected({sock_closed, Reason}, Channel),
|
||||
{shutdown, Reason, Channel1};
|
||||
|
|
@ -411,24 +482,23 @@ handle_info({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;
|
||||
handle_info({hreply, FunName, ok}, Channel) when
|
||||
FunName == on_socket_closed;
|
||||
FunName == on_received_bytes;
|
||||
FunName == on_received_messages;
|
||||
FunName == on_timer_timeout ->
|
||||
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({subscribe, _}, Channel) ->
|
||||
{ok, Channel};
|
||||
|
||||
handle_info(Info, Channel) ->
|
||||
?SLOG(warning, #{ msg => "unexpected_info"
|
||||
, info => Info
|
||||
?SLOG(warning, #{
|
||||
msg => "unexpected_info",
|
||||
info => Info
|
||||
}),
|
||||
{ok, Channel}.
|
||||
|
||||
|
|
@ -446,13 +516,22 @@ do_subscribe(TopicFilters, Channel) ->
|
|||
fun({TopicFilter, SubOpts}, {MadeSubs, ChannelAcc}) ->
|
||||
{Sub, Channel1} = do_subscribe(TopicFilter, SubOpts, ChannelAcc),
|
||||
{MadeSubs ++ [Sub], Channel1}
|
||||
end, {[], Channel}, parse_topic_filters(TopicFilters)),
|
||||
end,
|
||||
{[], Channel},
|
||||
parse_topic_filters(TopicFilters)
|
||||
),
|
||||
{ok, MadeSubs, NChannel}.
|
||||
|
||||
%% @private
|
||||
do_subscribe(TopicFilter, SubOpts, Channel =
|
||||
#channel{clientinfo = ClientInfo = #{mountpoint := Mountpoint},
|
||||
subscriptions = Subs}) ->
|
||||
do_subscribe(
|
||||
TopicFilter,
|
||||
SubOpts,
|
||||
Channel =
|
||||
#channel{
|
||||
clientinfo = ClientInfo = #{mountpoint := Mountpoint},
|
||||
subscriptions = Subs
|
||||
}
|
||||
) ->
|
||||
%% Mountpoint first
|
||||
NTopicFilter = emqx_mountpoint:mount(Mountpoint, TopicFilter),
|
||||
NSubOpts = maps:merge(emqx_gateway_utils:default_subopts(), SubOpts),
|
||||
|
|
@ -462,34 +541,49 @@ do_subscribe(TopicFilter, SubOpts, Channel =
|
|||
case IsNew of
|
||||
true ->
|
||||
ok = emqx:subscribe(NTopicFilter, SubId, NSubOpts),
|
||||
ok = emqx_hooks:run('session.subscribed',
|
||||
[ClientInfo, NTopicFilter, NSubOpts#{is_new => IsNew}]),
|
||||
{{NTopicFilter, NSubOpts},
|
||||
Channel#channel{subscriptions = Subs#{NTopicFilter => NSubOpts}}};
|
||||
ok = emqx_hooks:run(
|
||||
'session.subscribed',
|
||||
[ClientInfo, NTopicFilter, NSubOpts#{is_new => IsNew}]
|
||||
),
|
||||
{{NTopicFilter, NSubOpts}, Channel#channel{
|
||||
subscriptions = Subs#{NTopicFilter => NSubOpts}
|
||||
}};
|
||||
_ ->
|
||||
%% Update subopts
|
||||
ok = emqx:subscribe(NTopicFilter, SubId, NSubOpts),
|
||||
{{NTopicFilter, NSubOpts},
|
||||
Channel#channel{subscriptions = Subs#{NTopicFilter => NSubOpts}}}
|
||||
{{NTopicFilter, NSubOpts}, Channel#channel{
|
||||
subscriptions = Subs#{NTopicFilter => NSubOpts}
|
||||
}}
|
||||
end.
|
||||
|
||||
do_unsubscribe(TopicFilters, Channel) ->
|
||||
NChannel = lists:foldl(
|
||||
fun({TopicFilter, SubOpts}, ChannelAcc) ->
|
||||
do_unsubscribe(TopicFilter, SubOpts, ChannelAcc)
|
||||
end, Channel, parse_topic_filters(TopicFilters)),
|
||||
end,
|
||||
Channel,
|
||||
parse_topic_filters(TopicFilters)
|
||||
),
|
||||
{ok, NChannel}.
|
||||
|
||||
%% @private
|
||||
do_unsubscribe(TopicFilter, UnSubOpts, Channel =
|
||||
#channel{clientinfo = ClientInfo = #{mountpoint := Mountpoint},
|
||||
subscriptions = Subs}) ->
|
||||
do_unsubscribe(
|
||||
TopicFilter,
|
||||
UnSubOpts,
|
||||
Channel =
|
||||
#channel{
|
||||
clientinfo = ClientInfo = #{mountpoint := Mountpoint},
|
||||
subscriptions = 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)]),
|
||||
ok = emqx_hooks:run(
|
||||
'session.unsubscribed',
|
||||
[ClientInfo, TopicFilter, maps:merge(SubOpts, UnSubOpts)]
|
||||
),
|
||||
Channel#channel{subscriptions = maps:remove(NTopicFilter, Subs)};
|
||||
_ ->
|
||||
Channel
|
||||
|
|
@ -503,25 +597,32 @@ parse_topic_filters(TopicFilters) ->
|
|||
%% Ensure & Hooks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
ensure_connected(Channel = #channel{
|
||||
ensure_connected(
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)},
|
||||
ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]),
|
||||
Channel#channel{conninfo = NConnInfo,
|
||||
Channel#channel{
|
||||
conninfo = NConnInfo,
|
||||
conn_state = connected
|
||||
}.
|
||||
|
||||
ensure_disconnected(Reason, Channel = #channel{
|
||||
ensure_disconnected(
|
||||
Reason,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conn_state = connected,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
|
||||
ok = run_hooks(Ctx, 'client.disconnected', [ClientInfo, Reason, NConnInfo]),
|
||||
Channel#channel{conninfo = NConnInfo, conn_state = disconnected};
|
||||
|
||||
ensure_disconnected(_Reason, Channel = #channel{conninfo = ConnInfo}) ->
|
||||
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
|
||||
Channel#channel{conninfo = NConnInfo, conn_state = disconnected}.
|
||||
|
|
@ -550,7 +651,8 @@ ensure_timer(Name, Channel = #channel{timers = Timers}) ->
|
|||
Time = interval(Name, Channel),
|
||||
case TRef == undefined andalso Time > 0 of
|
||||
true -> ensure_timer(Name, Time, Channel);
|
||||
false -> Channel %% Timer disabled or exists
|
||||
%% Timer disabled or exists
|
||||
false -> Channel
|
||||
end.
|
||||
|
||||
ensure_timer(Name, Time, Channel = #channel{timers = Timers}) ->
|
||||
|
|
@ -576,10 +678,13 @@ interval(alive_timer, #channel{keepalive = Keepalive}) ->
|
|||
wrap(Req) ->
|
||||
Req#{conn => base64:encode(term_to_binary(self()))}.
|
||||
|
||||
dispatch_or_close_process(Channel = #channel{
|
||||
dispatch_or_close_process(
|
||||
Channel = #channel{
|
||||
rqueue = Queue,
|
||||
inflight = undefined,
|
||||
gcli = GClient}) ->
|
||||
gcli = GClient
|
||||
}
|
||||
) ->
|
||||
case queue:out(Queue) of
|
||||
{empty, _} ->
|
||||
case Channel#channel.conn_state of
|
||||
|
|
@ -613,7 +718,8 @@ enrich_clientinfo(InClientInfo = #{proto_name := ProtoName}, ClientInfo) ->
|
|||
NClientInfo#{protocol => proto_name_to_protocol(ProtoName)}.
|
||||
|
||||
default_conninfo(ConnInfo) ->
|
||||
ConnInfo#{clean_start => true,
|
||||
ConnInfo#{
|
||||
clean_start => true,
|
||||
clientid => undefined,
|
||||
username => undefined,
|
||||
conn_mod => undefined,
|
||||
|
|
@ -624,11 +730,15 @@ default_conninfo(ConnInfo) ->
|
|||
connected_at => erlang:system_time(millisecond),
|
||||
keepalive => 0,
|
||||
receive_maximum => 0,
|
||||
expiry_interval => 0}.
|
||||
expiry_interval => 0
|
||||
}.
|
||||
|
||||
default_clientinfo(#{peername := {PeerHost, _},
|
||||
sockname := {_, SockPort}}) ->
|
||||
#{zone => default,
|
||||
default_clientinfo(#{
|
||||
peername := {PeerHost, _},
|
||||
sockname := {_, SockPort}
|
||||
}) ->
|
||||
#{
|
||||
zone => default,
|
||||
protocol => exproto,
|
||||
peerhost => PeerHost,
|
||||
sockport => SockPort,
|
||||
|
|
@ -636,7 +746,8 @@ default_clientinfo(#{peername := {PeerHost, _},
|
|||
username => undefined,
|
||||
is_bridge => false,
|
||||
is_superuser => false,
|
||||
mountpoint => undefined}.
|
||||
mountpoint => undefined
|
||||
}.
|
||||
|
||||
stringfy(Reason) ->
|
||||
unicode:characters_to_binary((io_lib:format("~0p", [Reason]))).
|
||||
|
|
|
|||
|
|
@ -20,14 +20,15 @@
|
|||
|
||||
-behaviour(emqx_gateway_frame).
|
||||
|
||||
-export([ initial_parse_state/1
|
||||
, serialize_opts/0
|
||||
, parse/2
|
||||
, serialize_pkt/2
|
||||
, format/1
|
||||
, is_message/1
|
||||
, type/1
|
||||
]).
|
||||
-export([
|
||||
initial_parse_state/1,
|
||||
serialize_opts/0,
|
||||
parse/2,
|
||||
serialize_pkt/2,
|
||||
format/1,
|
||||
is_message/1,
|
||||
type/1
|
||||
]).
|
||||
|
||||
initial_parse_state(_) ->
|
||||
#{}.
|
||||
|
|
@ -47,4 +48,3 @@ format(Data) ->
|
|||
is_message(_) -> true.
|
||||
|
||||
type(_) -> unknown.
|
||||
|
||||
|
|
|
|||
|
|
@ -21,26 +21,26 @@
|
|||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
|
||||
%% 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
|
||||
]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-record(state, {
|
||||
pool,
|
||||
id,
|
||||
streams
|
||||
}).
|
||||
}).
|
||||
|
||||
-define(CONN_ADAPTER_MOD, emqx_exproto_v_1_connection_handler_client).
|
||||
|
||||
|
|
@ -49,11 +49,18 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
start_link(Pool, Id) ->
|
||||
gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)},
|
||||
?MODULE, [Pool, Id], []).
|
||||
gen_server:start_link(
|
||||
{local, emqx_misc:proc_name(?MODULE, Id)},
|
||||
?MODULE,
|
||||
[Pool, Id],
|
||||
[]
|
||||
).
|
||||
|
||||
async_call(FunName, Req = #{conn := Conn},
|
||||
Options = #{pool_name := PoolName}) ->
|
||||
async_call(
|
||||
FunName,
|
||||
Req = #{conn := Conn},
|
||||
Options = #{pool_name := PoolName}
|
||||
) ->
|
||||
cast(pick(PoolName, Conn), {rpc, FunName, Req, Options, self()}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
@ -82,41 +89,48 @@ handle_call(_Request, _From, State) ->
|
|||
handle_cast({rpc, Fun, Req, Options, From}, State = #state{streams = Streams}) ->
|
||||
case ensure_stream_opened(Fun, Options, Streams) of
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{ msg => "request_grpc_server_failed"
|
||||
, function => {?CONN_ADAPTER_MOD, Fun, Options}
|
||||
, reason => Reason}),
|
||||
?SLOG(error, #{
|
||||
msg => "request_grpc_server_failed",
|
||||
function => {?CONN_ADAPTER_MOD, Fun, Options},
|
||||
reason => Reason
|
||||
}),
|
||||
reply(From, Fun, {error, Reason}),
|
||||
{noreply, State#state{streams = Streams#{Fun => undefined}}};
|
||||
{ok, Stream} ->
|
||||
case catch grpc_client:send(Stream, Req) of
|
||||
ok ->
|
||||
?SLOG(debug, #{ msg => "send_grpc_request_succeed"
|
||||
, function => {?CONN_ADAPTER_MOD, Fun}
|
||||
, request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "send_grpc_request_succeed",
|
||||
function => {?CONN_ADAPTER_MOD, Fun},
|
||||
request => Req
|
||||
}),
|
||||
reply(From, Fun, ok),
|
||||
{noreply, State#state{streams = Streams#{Fun => Stream}}};
|
||||
{'EXIT', {not_found, _Stk}} ->
|
||||
%% Not found the stream, reopen it
|
||||
?SLOG(info, #{ msg => "cannt_find_old_stream_ref"
|
||||
, function => {?CONN_ADAPTER_MOD, Fun}
|
||||
?SLOG(info, #{
|
||||
msg => "cannt_find_old_stream_ref",
|
||||
function => {?CONN_ADAPTER_MOD, Fun}
|
||||
}),
|
||||
handle_cast(
|
||||
{rpc, Fun, Req, Options, From},
|
||||
State#state{streams = maps:remove(Fun, Streams)});
|
||||
State#state{streams = maps:remove(Fun, Streams)}
|
||||
);
|
||||
{'EXIT', {timeout, _Stk}} ->
|
||||
?SLOG(error, #{ msg => "send_grpc_request_timeout"
|
||||
, function => {?CONN_ADAPTER_MOD, Fun}
|
||||
, request => Req
|
||||
?SLOG(error, #{
|
||||
msg => "send_grpc_request_timeout",
|
||||
function => {?CONN_ADAPTER_MOD, Fun},
|
||||
request => Req
|
||||
}),
|
||||
reply(From, Fun, {error, timeout}),
|
||||
{noreply, State#state{streams = Streams#{Fun => Stream}}};
|
||||
{'EXIT', {Reason1, Stk}} ->
|
||||
?SLOG(error, #{ msg => "send_grpc_request_failed"
|
||||
, function => {?CONN_ADAPTER_MOD, Fun}
|
||||
, request => Req
|
||||
, error => Reason1
|
||||
, stacktrace => Stk
|
||||
?SLOG(error, #{
|
||||
msg => "send_grpc_request_failed",
|
||||
function => {?CONN_ADAPTER_MOD, Fun},
|
||||
request => Req,
|
||||
error => Reason1,
|
||||
stacktrace => Stk
|
||||
}),
|
||||
reply(From, Fun, {error, Reason1}),
|
||||
{noreply, State#state{streams = Streams#{Fun => undefined}}}
|
||||
|
|
@ -147,5 +161,6 @@ ensure_stream_opened(Fun, Options, Streams) ->
|
|||
{ok, Stream} -> {ok, Stream};
|
||||
{error, Reason} -> {error, Reason}
|
||||
end;
|
||||
Stream -> {ok, Stream}
|
||||
Stream ->
|
||||
{ok, Stream}
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -27,48 +27,57 @@
|
|||
-define(DEFAULT_CALL_TIMEOUT, 5000).
|
||||
|
||||
%% gRPC server callbacks
|
||||
-export([ send/2
|
||||
, close/2
|
||||
, authenticate/2
|
||||
, start_timer/2
|
||||
, publish/2
|
||||
, subscribe/2
|
||||
, unsubscribe/2
|
||||
]).
|
||||
-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()}
|
||||
-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) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => 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()}
|
||||
-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) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => Req
|
||||
}),
|
||||
{ok, response(call(Conn, close)), Md}.
|
||||
|
||||
-spec authenticate(emqx_exproto_pb:authenticate_request(), grpc:metadata())
|
||||
-> {ok, emqx_exproto_pb:code_response(), grpc:metadata()}
|
||||
-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,
|
||||
authenticate(
|
||||
Req = #{
|
||||
conn := Conn,
|
||||
password := Password,
|
||||
clientinfo := ClientInfo}, Md) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
clientinfo := ClientInfo
|
||||
},
|
||||
Md
|
||||
) ->
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => Req
|
||||
}),
|
||||
case validate(clientinfo, ClientInfo) of
|
||||
false ->
|
||||
|
|
@ -77,69 +86,77 @@ authenticate(Req = #{conn := Conn,
|
|||
{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()}
|
||||
-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 ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
start_timer(Req = #{conn := Conn, type := Type, interval := Interval}, Md) when
|
||||
Type =:= 'KEEPALIVE' andalso Interval > 0
|
||||
->
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => Req
|
||||
}),
|
||||
|
||||
{ok, response(call(Conn, {start_timer, keepalive, Interval})), Md};
|
||||
start_timer(Req, Md) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => 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()}
|
||||
-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) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
publish(Req = #{conn := Conn, topic := Topic, qos := Qos, payload := Payload}, Md) when
|
||||
?IS_QOS(Qos)
|
||||
->
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => Req
|
||||
}),
|
||||
|
||||
{ok, response(call(Conn, {publish, Topic, Qos, Payload})), Md};
|
||||
|
||||
publish(Req, Md) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => 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()}
|
||||
-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) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
subscribe(Req = #{conn := Conn, topic := Topic, qos := Qos}, Md) when
|
||||
?IS_QOS(Qos)
|
||||
->
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => Req
|
||||
}),
|
||||
{ok, response(call(Conn, {subscribe_from_client, Topic, Qos})), Md};
|
||||
|
||||
subscribe(Req, Md) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => 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()}
|
||||
-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) ->
|
||||
?SLOG(debug, #{ msg => "recv_grpc_function_call"
|
||||
, function => ?FUNCTION_NAME
|
||||
, request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "recv_grpc_function_call",
|
||||
function => ?FUNCTION_NAME,
|
||||
request => Req
|
||||
}),
|
||||
{ok, response(call(Conn, {unsubscribe_from_client, Topic})), Md}.
|
||||
|
||||
|
|
@ -155,19 +172,19 @@ call(ConnStr, Req) ->
|
|||
Pid = to_pid(ConnStr),
|
||||
emqx_gateway_conn:call(Pid, Req, ?DEFAULT_CALL_TIMEOUT)
|
||||
catch
|
||||
exit : badarg ->
|
||||
exit:badarg ->
|
||||
{error, ?RESP_PARAMS_TYPE_ERROR, <<"The conn type error">>};
|
||||
exit : noproc ->
|
||||
{error, ?RESP_CONN_PROCESS_NOT_ALIVE,
|
||||
<<"Connection process is not alive">>};
|
||||
exit : timeout ->
|
||||
exit:noproc ->
|
||||
{error, ?RESP_CONN_PROCESS_NOT_ALIVE, <<"Connection process is not alive">>};
|
||||
exit:timeout ->
|
||||
{error, ?RESP_UNKNOWN, <<"Connection is not answered">>};
|
||||
Class : Reason : Stk->
|
||||
?SLOG(error, #{ msg => "call_conn_process_crashed"
|
||||
, request => Req
|
||||
, conn_str=> ConnStr
|
||||
, reason => {Class, Reason}
|
||||
, stacktrace => Stk
|
||||
Class:Reason:Stk ->
|
||||
?SLOG(error, #{
|
||||
msg => "call_conn_process_crashed",
|
||||
request => Req,
|
||||
conn_str => ConnStr,
|
||||
reason => {Class, Reason},
|
||||
stacktrace => Stk
|
||||
}),
|
||||
{error, ?RESP_UNKNOWN, <<"Unknown crashes">>}
|
||||
end.
|
||||
|
|
@ -184,11 +201,13 @@ validate(clientinfo, M) ->
|
|||
|
||||
response(ok) ->
|
||||
#{code => ?RESP_SUCCESS};
|
||||
response({error, Code, Reason})
|
||||
when ?IS_GRPC_RESULT_CODE(Code) ->
|
||||
response({error, Code, Reason}) when
|
||||
?IS_GRPC_RESULT_CODE(Code)
|
||||
->
|
||||
#{code => Code, message => stringfy(Reason)};
|
||||
response({error, Code})
|
||||
when ?IS_GRPC_RESULT_CODE(Code) ->
|
||||
response({error, Code}) when
|
||||
?IS_GRPC_RESULT_CODE(Code)
|
||||
->
|
||||
#{code => Code};
|
||||
response(Other) ->
|
||||
#{code => ?RESP_UNKNOWN, message => stringfy(Other)}.
|
||||
|
|
|
|||
|
|
@ -21,29 +21,33 @@
|
|||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-import(emqx_gateway_utils,
|
||||
[ normalize_config/1
|
||||
, start_listeners/4
|
||||
, stop_listeners/2
|
||||
]).
|
||||
-import(
|
||||
emqx_gateway_utils,
|
||||
[
|
||||
normalize_config/1,
|
||||
start_listeners/4,
|
||||
stop_listeners/2
|
||||
]
|
||||
).
|
||||
|
||||
%% APIs
|
||||
-export([ reg/0
|
||||
, unreg/0
|
||||
]).
|
||||
-export([
|
||||
reg/0,
|
||||
unreg/0
|
||||
]).
|
||||
|
||||
-export([ on_gateway_load/2
|
||||
, on_gateway_update/3
|
||||
, on_gateway_unload/2
|
||||
]).
|
||||
-export([
|
||||
on_gateway_load/2,
|
||||
on_gateway_update/3,
|
||||
on_gateway_unload/2
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
reg() ->
|
||||
RegistryOptions = [ {cbkmod, ?MODULE}
|
||||
],
|
||||
RegistryOptions = [{cbkmod, ?MODULE}],
|
||||
emqx_gateway_registry:reg(exproto, RegistryOptions).
|
||||
|
||||
unreg() ->
|
||||
|
|
@ -53,11 +57,16 @@ unreg() ->
|
|||
%% emqx_gateway_registry callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
on_gateway_load(_Gateway = #{ name := GwName,
|
||||
on_gateway_load(
|
||||
_Gateway = #{
|
||||
name := GwName,
|
||||
config := Config
|
||||
}, Ctx) ->
|
||||
},
|
||||
Ctx
|
||||
) ->
|
||||
%% XXX: How to monitor it ?
|
||||
_ = start_grpc_client_channel(GwName,
|
||||
_ = start_grpc_client_channel(
|
||||
GwName,
|
||||
maps:get(handler, Config, undefined)
|
||||
),
|
||||
%% XXX: How to monitor it ?
|
||||
|
|
@ -67,8 +76,11 @@ on_gateway_load(_Gateway = #{ name := GwName,
|
|||
PoolName = pool_name(GwName),
|
||||
PoolSize = emqx_vm:schedulers() * 2,
|
||||
{ok, PoolSup} = emqx_pool_sup:start_link(
|
||||
PoolName, hash, PoolSize,
|
||||
{emqx_exproto_gcli, start_link, []}),
|
||||
PoolName,
|
||||
hash,
|
||||
PoolSize,
|
||||
{emqx_exproto_gcli, start_link, []}
|
||||
),
|
||||
|
||||
NConfig = maps:without(
|
||||
[server, handler],
|
||||
|
|
@ -78,18 +90,25 @@ on_gateway_load(_Gateway = #{ name := GwName,
|
|||
NConfig#{handler => GwName}
|
||||
),
|
||||
|
||||
ModCfg = #{frame_mod => emqx_exproto_frame,
|
||||
ModCfg = #{
|
||||
frame_mod => emqx_exproto_frame,
|
||||
chann_mod => emqx_exproto_channel
|
||||
},
|
||||
case start_listeners(
|
||||
Listeners, GwName, Ctx, ModCfg) of
|
||||
case
|
||||
start_listeners(
|
||||
Listeners, GwName, Ctx, ModCfg
|
||||
)
|
||||
of
|
||||
{ok, ListenerPids} ->
|
||||
{ok, ListenerPids, _GwState = #{ctx => Ctx, pool => PoolSup}};
|
||||
{error, {Reason, Listener}} ->
|
||||
throw({badconf, #{ key => listeners
|
||||
, vallue => Listener
|
||||
, reason => Reason
|
||||
}})
|
||||
throw(
|
||||
{badconf, #{
|
||||
key => listeners,
|
||||
vallue => Listener,
|
||||
reason => Reason
|
||||
}}
|
||||
)
|
||||
end.
|
||||
|
||||
on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) ->
|
||||
|
|
@ -100,16 +119,22 @@ on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) ->
|
|||
on_gateway_unload(Gateway, GwState),
|
||||
on_gateway_load(Gateway#{config => Config}, Ctx)
|
||||
catch
|
||||
Class : Reason : Stk ->
|
||||
logger:error("Failed to update ~ts; "
|
||||
Class:Reason:Stk ->
|
||||
logger:error(
|
||||
"Failed to update ~ts; "
|
||||
"reason: {~0p, ~0p} stacktrace: ~0p",
|
||||
[GwName, Class, Reason, Stk]),
|
||||
[GwName, Class, Reason, Stk]
|
||||
),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
on_gateway_unload(_Gateway = #{ name := GwName,
|
||||
on_gateway_unload(
|
||||
_Gateway = #{
|
||||
name := GwName,
|
||||
config := Config
|
||||
}, _GwState = #{pool := PoolSup}) ->
|
||||
},
|
||||
_GwState = #{pool := PoolSup}
|
||||
) ->
|
||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||
%% Stop funcs???
|
||||
exit(PoolSup, kill),
|
||||
|
|
@ -124,29 +149,42 @@ on_gateway_unload(_Gateway = #{ name := GwName,
|
|||
start_grpc_server(_GwName, undefined) ->
|
||||
undefined;
|
||||
start_grpc_server(GwName, Options = #{bind := ListenOn}) ->
|
||||
Services = #{protos => [emqx_exproto_pb],
|
||||
Services = #{
|
||||
protos => [emqx_exproto_pb],
|
||||
services => #{
|
||||
'emqx.exproto.v1.ConnectionAdapter' => emqx_exproto_gsvr}
|
||||
'emqx.exproto.v1.ConnectionAdapter' => emqx_exproto_gsvr
|
||||
}
|
||||
},
|
||||
SvrOptions = case emqx_map_lib:deep_get([ssl, enable], Options, false) of
|
||||
false -> [];
|
||||
SvrOptions =
|
||||
case emqx_map_lib:deep_get([ssl, enable], Options, false) of
|
||||
false ->
|
||||
[];
|
||||
true ->
|
||||
[{ssl_options,
|
||||
[
|
||||
{ssl_options,
|
||||
maps:to_list(
|
||||
maps:without([enable], maps:get(ssl, Options, #{}))
|
||||
)
|
||||
}]
|
||||
)}
|
||||
]
|
||||
end,
|
||||
case grpc:start_server(GwName, ListenOn, Services, SvrOptions) of
|
||||
{ok, _SvrPid} ->
|
||||
console_print("Start ~ts gRPC server on ~p successfully.~n",
|
||||
[GwName, ListenOn]);
|
||||
console_print(
|
||||
"Start ~ts gRPC server on ~p successfully.~n",
|
||||
[GwName, ListenOn]
|
||||
);
|
||||
{error, Reason} ->
|
||||
?ELOG("Failed to start ~ts gRPC server on ~p, reason: ~0p",
|
||||
[GwName, ListenOn, Reason]),
|
||||
throw({badconf, #{key => server,
|
||||
?ELOG(
|
||||
"Failed to start ~ts gRPC server on ~p, reason: ~0p",
|
||||
[GwName, ListenOn, Reason]
|
||||
),
|
||||
throw(
|
||||
{badconf, #{
|
||||
key => server,
|
||||
value => Options,
|
||||
reason => illegal_grpc_server_confs}})
|
||||
reason => illegal_grpc_server_confs
|
||||
}}
|
||||
)
|
||||
end.
|
||||
|
||||
stop_grpc_server(GwName) ->
|
||||
|
|
@ -158,13 +196,16 @@ start_grpc_client_channel(_GwName, undefined) ->
|
|||
start_grpc_client_channel(GwName, Options = #{address := Address}) ->
|
||||
#{host := Host, port := Port} =
|
||||
case emqx_http_lib:uri_parse(Address) of
|
||||
{ok, URIMap0} -> URIMap0;
|
||||
{ok, URIMap0} ->
|
||||
URIMap0;
|
||||
{error, _Reason} ->
|
||||
throw({badconf, #{key => address,
|
||||
throw(
|
||||
{badconf, #{
|
||||
key => address,
|
||||
value => Address,
|
||||
reason => illegal_grpc_address
|
||||
}})
|
||||
|
||||
}}
|
||||
)
|
||||
end,
|
||||
case emqx_map_lib:deep_get([ssl, enable], Options, false) of
|
||||
false ->
|
||||
|
|
@ -172,9 +213,13 @@ start_grpc_client_channel(GwName, Options = #{address := Address}) ->
|
|||
grpc_client_sup:create_channel_pool(GwName, SvrAddr, #{});
|
||||
true ->
|
||||
SslOpts = maps:to_list(maps:get(ssl, Options, #{})),
|
||||
ClientOpts = #{gun_opts =>
|
||||
#{transport => ssl,
|
||||
transport_opts => SslOpts}},
|
||||
ClientOpts = #{
|
||||
gun_opts =>
|
||||
#{
|
||||
transport => ssl,
|
||||
transport_opts => SslOpts
|
||||
}
|
||||
},
|
||||
|
||||
SvrAddr = compose_http_uri(https, Host, Port),
|
||||
grpc_client_sup:create_channel_pool(GwName, SvrAddr, ClientOpts)
|
||||
|
|
@ -183,7 +228,9 @@ start_grpc_client_channel(GwName, Options = #{address := Address}) ->
|
|||
compose_http_uri(Scheme, Host, Port) ->
|
||||
lists:flatten(
|
||||
io_lib:format(
|
||||
"~s://~s:~w", [Scheme, inet:ntoa(Host), Port])).
|
||||
"~s://~s:~w", [Scheme, inet:ntoa(Host), Port]
|
||||
)
|
||||
).
|
||||
|
||||
stop_grpc_client_channel(GwName) ->
|
||||
_ = grpc_client_sup:stop_channel_pool(GwName),
|
||||
|
|
|
|||
|
|
@ -16,8 +16,13 @@
|
|||
|
||||
-define(APP, emqx_exproto).
|
||||
|
||||
-define(TCP_SOCKOPTS, [binary, {packet, raw}, {reuseaddr, true},
|
||||
{backlog, 512}, {nodelay, true}]).
|
||||
-define(TCP_SOCKOPTS, [
|
||||
binary,
|
||||
{packet, raw},
|
||||
{reuseaddr, true},
|
||||
{backlog, 512},
|
||||
{nodelay, true}
|
||||
]).
|
||||
|
||||
-define(UDP_SOCKOPTS, []).
|
||||
|
||||
|
|
@ -30,7 +35,9 @@
|
|||
-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)).
|
||||
-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)
|
||||
).
|
||||
|
|
|
|||
|
|
@ -3,58 +3,57 @@
|
|||
%% copied from https://github.com/arcusfelis/binary2
|
||||
|
||||
%% Bytes
|
||||
-export([ reverse/1
|
||||
, join/2
|
||||
, duplicate/2
|
||||
, suffix/2
|
||||
, prefix/2
|
||||
]).
|
||||
-export([
|
||||
reverse/1,
|
||||
join/2,
|
||||
duplicate/2,
|
||||
suffix/2,
|
||||
prefix/2
|
||||
]).
|
||||
|
||||
%% Bits
|
||||
-export([ union/2
|
||||
, subtract/2
|
||||
, intersection/2
|
||||
, inverse/1
|
||||
]).
|
||||
-export([
|
||||
union/2,
|
||||
subtract/2,
|
||||
intersection/2,
|
||||
inverse/1
|
||||
]).
|
||||
|
||||
%% Trimming
|
||||
-export([ rtrim/1
|
||||
, rtrim/2
|
||||
, ltrim/1
|
||||
, ltrim/2
|
||||
, trim/1
|
||||
, trim/2
|
||||
]).
|
||||
-export([
|
||||
rtrim/1,
|
||||
rtrim/2,
|
||||
ltrim/1,
|
||||
ltrim/2,
|
||||
trim/1,
|
||||
trim/2
|
||||
]).
|
||||
|
||||
%% Parsing
|
||||
-export([ bin_to_int/1]).
|
||||
-export([bin_to_int/1]).
|
||||
|
||||
%% Matching
|
||||
-export([ optimize_patterns/1]).
|
||||
-export([optimize_patterns/1]).
|
||||
|
||||
%% CoAP
|
||||
-export([ join_path/1]).
|
||||
|
||||
-export([join_path/1]).
|
||||
|
||||
trim(B) -> trim(B, 0).
|
||||
ltrim(B) -> ltrim(B, 0).
|
||||
rtrim(B) -> rtrim(B, 0).
|
||||
|
||||
|
||||
rtrim(B, X) when is_binary(B), is_integer(X) ->
|
||||
S = byte_size(B),
|
||||
do_rtrim(S, B, X);
|
||||
rtrim(B, [_|_]=Xs) when is_binary(B) ->
|
||||
rtrim(B, [_ | _] = Xs) when is_binary(B) ->
|
||||
S = byte_size(B),
|
||||
do_mrtrim(S, B, Xs).
|
||||
|
||||
|
||||
ltrim(B, X) when is_binary(B), is_integer(X) ->
|
||||
do_ltrim(B, X);
|
||||
ltrim(B, [_|_]=Xs) when is_binary(B) ->
|
||||
ltrim(B, [_ | _] = Xs) when is_binary(B) ->
|
||||
do_mltrim(B, Xs).
|
||||
|
||||
|
||||
%% @doc The second element is a single integer element or an ordset of elements.
|
||||
trim(B, X) when is_binary(B), is_integer(X) ->
|
||||
From = ltrimc(B, X, 0),
|
||||
|
|
@ -65,7 +64,7 @@ trim(B, X) when is_binary(B), is_integer(X) ->
|
|||
To = do_rtrimc(S, B, X),
|
||||
binary:part(B, From, To - From)
|
||||
end;
|
||||
trim(B, [_|_]=Xs) when is_binary(B) ->
|
||||
trim(B, [_ | _] = Xs) when is_binary(B) ->
|
||||
From = mltrimc(B, Xs, 0),
|
||||
case byte_size(B) of
|
||||
From ->
|
||||
|
|
@ -109,16 +108,15 @@ do_mrtrim(S, B, Xs) ->
|
|||
false -> binary_part(B, 0, S)
|
||||
end.
|
||||
|
||||
|
||||
ltrimc(<<X, B/binary>>, X, C) ->
|
||||
ltrimc(B, X, C+1);
|
||||
ltrimc(B, X, C + 1);
|
||||
ltrimc(_B, _X, C) ->
|
||||
C.
|
||||
|
||||
%% multi, left trimming, returns a count of matched bytes from the left.
|
||||
mltrimc(<<X, B/binary>>, Xs, C) ->
|
||||
case ordsets:is_element(X, Xs) of
|
||||
true -> mltrimc(B, Xs, C+1);
|
||||
true -> mltrimc(B, Xs, C + 1);
|
||||
false -> C
|
||||
end;
|
||||
mltrimc(<<>>, _Xs, C) ->
|
||||
|
|
@ -148,13 +146,12 @@ reverse(Bin) when is_binary(Bin) ->
|
|||
<<V:S/integer-little>> = Bin,
|
||||
<<V:S/integer-big>>.
|
||||
|
||||
join([B|Bs], Sep) when is_binary(Sep) ->
|
||||
iolist_to_binary([B|add_separator(Bs, Sep)]);
|
||||
|
||||
join([B | Bs], Sep) when is_binary(Sep) ->
|
||||
iolist_to_binary([B | add_separator(Bs, Sep)]);
|
||||
join([], _Sep) ->
|
||||
<<>>.
|
||||
|
||||
add_separator([B|Bs], Sep) ->
|
||||
add_separator([B | Bs], Sep) ->
|
||||
[Sep, B | add_separator(Bs, Sep)];
|
||||
add_separator([], _) ->
|
||||
[].
|
||||
|
|
@ -168,8 +165,7 @@ prefix(B, L) when is_binary(B), is_integer(L), L > 0 ->
|
|||
|
||||
suffix(B, L) when is_binary(B), is_integer(L), L > 0 ->
|
||||
S = byte_size(B),
|
||||
binary:part(B, S-L, L).
|
||||
|
||||
binary:part(B, S - L, L).
|
||||
|
||||
union(B1, B2) ->
|
||||
S = bit_size(B1),
|
||||
|
|
@ -203,7 +199,7 @@ bin_to_int(Bin) ->
|
|||
bin_to_int(Bin, 0).
|
||||
|
||||
bin_to_int(<<H, T/binary>>, X) when $0 =< H, H =< $9 ->
|
||||
bin_to_int(T, (X*10)+(H-$0));
|
||||
bin_to_int(T, (X * 10) + (H - $0));
|
||||
bin_to_int(Bin, X) ->
|
||||
{X, Bin}.
|
||||
|
||||
|
|
@ -213,10 +209,10 @@ optimize_patterns(Patterns) ->
|
|||
Sorted = lists:usort(Patterns),
|
||||
remove_long_duplicates(Sorted).
|
||||
|
||||
remove_long_duplicates([H|T]) ->
|
||||
remove_long_duplicates([H | T]) ->
|
||||
%% match(Subject, Pattern)
|
||||
DedupT = [X || X <- T, binary:match(X, H) =:= nomatch],
|
||||
[H|remove_long_duplicates(DedupT)];
|
||||
[H | remove_long_duplicates(DedupT)];
|
||||
remove_long_duplicates([]) ->
|
||||
[].
|
||||
|
||||
|
|
@ -224,7 +220,5 @@ join_path(PathList) ->
|
|||
join_path(PathList, <<>>).
|
||||
|
||||
join_path([], Result) -> Result;
|
||||
join_path([<<>> | PathList], Result) ->
|
||||
join_path(PathList, Result);
|
||||
join_path([Path | PathList], Result) ->
|
||||
join_path(PathList, <<Result/binary, "/", Path/binary>>).
|
||||
join_path([<<>> | PathList], Result) -> join_path(PathList, Result);
|
||||
join_path([Path | PathList], Result) -> join_path(PathList, <<Result/binary, "/", Path/binary>>).
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
-export([lookup_cmd/2, observe/2, read/2, write/2]).
|
||||
|
||||
-define(PATH(Suffix), "/gateway/lwm2m/clients/:clientid"Suffix).
|
||||
-define(PATH(Suffix), "/gateway/lwm2m/clients/:clientid" Suffix).
|
||||
-define(DATA_TYPE, ['Integer', 'Float', 'Time', 'String', 'Boolean', 'Opaque', 'Objlnk']).
|
||||
|
||||
-import(hoconsc, [mk/2, ref/1, ref/2]).
|
||||
|
|
@ -104,8 +104,11 @@ schema(?PATH("/write")) ->
|
|||
parameters => [
|
||||
{clientid, mk(binary(), #{in => path, example => "urn:oma:lwm2m:oma:2"})},
|
||||
{path, mk(binary(), #{in => query, required => true, example => "/3/0/7"})},
|
||||
{type, mk(hoconsc:enum(?DATA_TYPE),
|
||||
#{in => query, required => true, example => 'Integer'})},
|
||||
{type,
|
||||
mk(
|
||||
hoconsc:enum(?DATA_TYPE),
|
||||
#{in => query, required => true, example => 'Integer'}
|
||||
)},
|
||||
{value, mk(binary(), #{in => query, required => true, example => 123})}
|
||||
],
|
||||
responses => #{
|
||||
|
|
@ -118,8 +121,11 @@ schema(?PATH("/write")) ->
|
|||
fields(resource) ->
|
||||
[
|
||||
{operations, mk(binary(), #{desc => <<"Resource Operations">>, example => "E"})},
|
||||
{'dataType', mk(hoconsc:enum(?DATA_TYPE), #{desc => <<"Data Type">>,
|
||||
example => 'Integer'})},
|
||||
{'dataType',
|
||||
mk(hoconsc:enum(?DATA_TYPE), #{
|
||||
desc => <<"Data Type">>,
|
||||
example => 'Integer'
|
||||
})},
|
||||
{path, mk(binary(), #{desc => <<"Resource Path">>, example => "urn:oma:lwm2m:oma:2"})},
|
||||
{name, mk(binary(), #{desc => <<"Resource Name">>, example => "lwm2m-test"})}
|
||||
].
|
||||
|
|
@ -128,8 +134,10 @@ lookup_cmd(get, #{bindings := Bindings, query_string := QS}) ->
|
|||
ClientId = maps:get(clientid, Bindings),
|
||||
case emqx_gateway_cm_registry:lookup_channels(lwm2m, ClientId) of
|
||||
[Channel | _] ->
|
||||
#{<<"path">> := Path,
|
||||
<<"action">> := Action} = QS,
|
||||
#{
|
||||
<<"path">> := Path,
|
||||
<<"action">> := Action
|
||||
} = QS,
|
||||
{ok, Result} = emqx_lwm2m_channel:lookup_cmd(Channel, Path, Action),
|
||||
lookup_cmd_return(Result, ClientId, Action, Path);
|
||||
_ ->
|
||||
|
|
@ -137,63 +145,73 @@ lookup_cmd(get, #{bindings := Bindings, query_string := QS}) ->
|
|||
end.
|
||||
|
||||
lookup_cmd_return(undefined, ClientId, Action, Path) ->
|
||||
{200,
|
||||
#{clientid => ClientId,
|
||||
{200, #{
|
||||
clientid => ClientId,
|
||||
action => Action,
|
||||
code => <<"6.01">>,
|
||||
codeMsg => <<"reply_not_received">>,
|
||||
path => Path}};
|
||||
|
||||
path => Path
|
||||
}};
|
||||
lookup_cmd_return({Code, CodeMsg, Content}, ClientId, Action, Path) ->
|
||||
{200,
|
||||
format_cmd_content(Content,
|
||||
format_cmd_content(
|
||||
Content,
|
||||
Action,
|
||||
#{clientid => ClientId,
|
||||
#{
|
||||
clientid => ClientId,
|
||||
action => Action,
|
||||
code => Code,
|
||||
codeMsg => CodeMsg,
|
||||
path => Path})}.
|
||||
path => Path
|
||||
}
|
||||
)}.
|
||||
|
||||
format_cmd_content(undefined, _MsgType, Result) ->
|
||||
Result;
|
||||
|
||||
format_cmd_content(Content, <<"discover">>, Result) ->
|
||||
[H | Content1] = Content,
|
||||
{_, [HObjId]} = emqx_lwm2m_session:parse_object_list(H),
|
||||
[ObjId | _]= path_list(HObjId),
|
||||
[ObjId | _] = path_list(HObjId),
|
||||
ObjectList =
|
||||
case Content1 of
|
||||
[Content2 | _] ->
|
||||
{_, ObjL} = emqx_lwm2m_session:parse_object_list(Content2),
|
||||
ObjL;
|
||||
[] -> []
|
||||
[] ->
|
||||
[]
|
||||
end,
|
||||
|
||||
R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of
|
||||
R =
|
||||
case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of
|
||||
{error, _} ->
|
||||
lists:map(fun(Object) -> #{Object => Object} end, ObjectList);
|
||||
ObjDefinition ->
|
||||
lists:map(fun(Obj) -> to_operations(Obj, ObjDefinition) end, ObjectList)
|
||||
end,
|
||||
Result#{content => R};
|
||||
|
||||
format_cmd_content(Content, _, Result) ->
|
||||
Result#{content => Content}.
|
||||
|
||||
to_operations(Obj, ObjDefinition) ->
|
||||
[_, _, RawResId| _] = path_list(Obj),
|
||||
[_, _, RawResId | _] = path_list(Obj),
|
||||
ResId = binary_to_integer(RawResId),
|
||||
Operations =
|
||||
case emqx_lwm2m_xml_object:get_resource_operations(ResId, ObjDefinition) of
|
||||
"E" -> #{operations => <<"E">>};
|
||||
"E" ->
|
||||
#{operations => <<"E">>};
|
||||
Oper ->
|
||||
#{'dataType' =>
|
||||
list_to_binary(emqx_lwm2m_xml_object:get_resource_type(ResId, ObjDefinition)),
|
||||
#{
|
||||
'dataType' =>
|
||||
list_to_binary(
|
||||
emqx_lwm2m_xml_object:get_resource_type(ResId, ObjDefinition)
|
||||
),
|
||||
operations => list_to_binary(Oper)
|
||||
}
|
||||
end,
|
||||
Operations#{path => Obj,
|
||||
name => list_to_binary(emqx_lwm2m_xml_object:get_resource_name(ResId, ObjDefinition))}.
|
||||
Operations#{
|
||||
path => Obj,
|
||||
name => list_to_binary(emqx_lwm2m_xml_object:get_resource_name(ResId, ObjDefinition))
|
||||
}.
|
||||
|
||||
path_list(Path) ->
|
||||
case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of
|
||||
|
|
@ -203,33 +221,40 @@ path_list(Path) ->
|
|||
[ObjId] -> [ObjId]
|
||||
end.
|
||||
|
||||
observe(post, #{bindings := #{clientid := ClientId},
|
||||
query_string := #{<<"path">> := Path, <<"enable">> := Enable}}) ->
|
||||
MsgType = case Enable of
|
||||
observe(post, #{
|
||||
bindings := #{clientid := ClientId},
|
||||
query_string := #{<<"path">> := Path, <<"enable">> := Enable}
|
||||
}) ->
|
||||
MsgType =
|
||||
case Enable of
|
||||
true -> <<"observe">>;
|
||||
_ -> <<"cancel-observe">>
|
||||
end,
|
||||
|
||||
Cmd = #{<<"msgType">> => MsgType,
|
||||
Cmd = #{
|
||||
<<"msgType">> => MsgType,
|
||||
<<"data">> => #{<<"path">> => Path}
|
||||
},
|
||||
|
||||
send_cmd(ClientId, Cmd).
|
||||
|
||||
|
||||
read(post, #{bindings := #{clientid := ClientId},
|
||||
query_string := Qs}) ->
|
||||
|
||||
Cmd = #{<<"msgType">> => <<"read">>,
|
||||
read(post, #{
|
||||
bindings := #{clientid := ClientId},
|
||||
query_string := Qs
|
||||
}) ->
|
||||
Cmd = #{
|
||||
<<"msgType">> => <<"read">>,
|
||||
<<"data">> => Qs
|
||||
},
|
||||
|
||||
send_cmd(ClientId, Cmd).
|
||||
|
||||
write(post, #{bindings := #{clientid := ClientId},
|
||||
query_string := Qs}) ->
|
||||
|
||||
Cmd = #{<<"msgType">> => <<"write">>,
|
||||
write(post, #{
|
||||
bindings := #{clientid := ClientId},
|
||||
query_string := Qs
|
||||
}) ->
|
||||
Cmd = #{
|
||||
<<"msgType">> => <<"write">>,
|
||||
<<"data">> => Qs
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -21,26 +21,29 @@
|
|||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
%% API
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
, with_context/2
|
||||
, do_takeover/3
|
||||
, lookup_cmd/3
|
||||
, send_cmd/2
|
||||
]).
|
||||
-export([
|
||||
info/1,
|
||||
info/2,
|
||||
stats/1,
|
||||
with_context/2,
|
||||
do_takeover/3,
|
||||
lookup_cmd/3,
|
||||
send_cmd/2
|
||||
]).
|
||||
|
||||
-export([ init/2
|
||||
, handle_in/2
|
||||
, handle_deliver/2
|
||||
, handle_timeout/3
|
||||
, terminate/2
|
||||
]).
|
||||
-export([
|
||||
init/2,
|
||||
handle_in/2,
|
||||
handle_deliver/2,
|
||||
handle_timeout/3,
|
||||
terminate/2
|
||||
]).
|
||||
|
||||
-export([ handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
]).
|
||||
-export([
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2
|
||||
]).
|
||||
|
||||
-record(channel, {
|
||||
%% Context
|
||||
|
|
@ -58,25 +61,29 @@
|
|||
timers :: #{atom() => disable | undefined | reference()},
|
||||
%% FIXME: don't store anonymous func
|
||||
with_context :: function()
|
||||
}).
|
||||
}).
|
||||
|
||||
-type channel() :: #channel{}.
|
||||
|
||||
-type conn_state() :: idle | connecting | connected | disconnected.
|
||||
|
||||
-type reply() :: {outgoing, coap_message()}
|
||||
-type reply() ::
|
||||
{outgoing, coap_message()}
|
||||
| {outgoing, [coap_message()]}
|
||||
| {event, conn_state()|updated}
|
||||
| {event, conn_state() | updated}
|
||||
| {close, Reason :: atom()}.
|
||||
|
||||
-type replies() :: reply() | [reply()].
|
||||
|
||||
%% TODO:
|
||||
-define(DEFAULT_OVERRIDE,
|
||||
#{ clientid => <<"">> %% Generate clientid by default
|
||||
, username => <<"${Packet.uri_query.ep}">>
|
||||
, password => <<"">>
|
||||
}).
|
||||
%% Generate clientid by default
|
||||
#{
|
||||
clientid => <<"">>,
|
||||
username => <<"${Packet.uri_query.ep}">>,
|
||||
password => <<"">>
|
||||
}
|
||||
).
|
||||
|
||||
-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]).
|
||||
|
||||
|
|
@ -91,7 +98,6 @@ info(Channel) ->
|
|||
|
||||
info(Keys, Channel) when is_list(Keys) ->
|
||||
[{Key, info(Key, Channel)} || Key <- Keys];
|
||||
|
||||
info(conninfo, #channel{conninfo = ConnInfo}) ->
|
||||
ConnInfo;
|
||||
info(conn_state, #channel{conn_state = ConnState}) ->
|
||||
|
|
@ -108,38 +114,44 @@ info(ctx, #channel{ctx = Ctx}) ->
|
|||
stats(_) ->
|
||||
[].
|
||||
|
||||
init(ConnInfo = #{peername := {PeerHost, _},
|
||||
sockname := {_, SockPort}},
|
||||
#{ctx := Ctx} = Config) ->
|
||||
init(
|
||||
ConnInfo = #{
|
||||
peername := {PeerHost, _},
|
||||
sockname := {_, SockPort}
|
||||
},
|
||||
#{ctx := Ctx} = Config
|
||||
) ->
|
||||
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||
Mountpoint = maps:get(mountpoint, Config, undefined),
|
||||
ListenerId = case maps:get(listener, Config, undefined) of
|
||||
ListenerId =
|
||||
case maps:get(listener, Config, undefined) of
|
||||
undefined -> undefined;
|
||||
{GwName, Type, LisName} ->
|
||||
emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||
{GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||
end,
|
||||
ClientInfo = set_peercert_infos(
|
||||
Peercert,
|
||||
#{ zone => default
|
||||
, listener => ListenerId
|
||||
, protocol => lwm2m
|
||||
, peerhost => PeerHost
|
||||
, sockport => SockPort
|
||||
, username => undefined
|
||||
, clientid => undefined
|
||||
, is_bridge => false
|
||||
, is_superuser => false
|
||||
, mountpoint => Mountpoint
|
||||
#{
|
||||
zone => default,
|
||||
listener => ListenerId,
|
||||
protocol => lwm2m,
|
||||
peerhost => PeerHost,
|
||||
sockport => SockPort,
|
||||
username => undefined,
|
||||
clientid => undefined,
|
||||
is_bridge => false,
|
||||
is_superuser => false,
|
||||
mountpoint => Mountpoint
|
||||
}
|
||||
),
|
||||
|
||||
#channel{ ctx = Ctx
|
||||
, conninfo = ConnInfo
|
||||
, clientinfo = ClientInfo
|
||||
, timers = #{}
|
||||
, session = emqx_lwm2m_session:new()
|
||||
, conn_state = idle
|
||||
, with_context = with_context(Ctx, ClientInfo)
|
||||
#channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo,
|
||||
timers = #{},
|
||||
session = emqx_lwm2m_session:new(),
|
||||
conn_state = idle,
|
||||
with_context = with_context(Ctx, ClientInfo)
|
||||
}.
|
||||
|
||||
lookup_cmd(Channel, Path, Action) ->
|
||||
|
|
@ -152,8 +164,8 @@ send_cmd(Channel, Cmd) ->
|
|||
%% Handle incoming packet
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec handle_in(coap_message() | {frame_error, any()}, channel())
|
||||
-> {ok, channel()}
|
||||
-spec handle_in(coap_message() | {frame_error, any()}, channel()) ->
|
||||
{ok, channel()}
|
||||
| {ok, replies(), channel()}
|
||||
| {shutdown, Reason :: term(), channel()}
|
||||
| {shutdown, Reason :: term(), replies(), channel()}.
|
||||
|
|
@ -170,18 +182,21 @@ handle_deliver(Delivers, Channel) ->
|
|||
%%--------------------------------------------------------------------
|
||||
%% Handle timeout
|
||||
%%--------------------------------------------------------------------
|
||||
handle_timeout(_, lifetime, #channel{ctx = Ctx,
|
||||
handle_timeout(
|
||||
_,
|
||||
lifetime,
|
||||
#channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo,
|
||||
conninfo = ConnInfo} = Channel) ->
|
||||
conninfo = ConnInfo
|
||||
} = Channel
|
||||
) ->
|
||||
ok = run_hooks(Ctx, 'client.disconnected', [ClientInfo, timeout, ConnInfo]),
|
||||
{shutdown, timeout, Channel};
|
||||
|
||||
handle_timeout(_, {transport, _} = Msg, Channel) ->
|
||||
call_session(timeout, Msg, Channel);
|
||||
|
||||
handle_timeout(_, disconnect, Channel) ->
|
||||
{shutdown, normal, Channel};
|
||||
|
||||
handle_timeout(_, _, Channel) ->
|
||||
{ok, Channel}.
|
||||
|
||||
|
|
@ -189,63 +204,78 @@ handle_timeout(_, _, Channel) ->
|
|||
%% Handle call
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
handle_call({lookup_cmd, Path, Type}, _From,
|
||||
Channel = #channel{session = Session}) ->
|
||||
handle_call(
|
||||
{lookup_cmd, Path, Type},
|
||||
_From,
|
||||
Channel = #channel{session = Session}
|
||||
) ->
|
||||
Result = emqx_lwm2m_session:find_cmd_record(Path, Type, Session),
|
||||
{reply, {ok, Result}, Channel};
|
||||
|
||||
handle_call({send_cmd, Cmd}, _From, Channel) ->
|
||||
{ok, Outs, Channel2} = call_session(send_cmd, Cmd, Channel),
|
||||
{reply, ok, Outs, Channel2};
|
||||
|
||||
handle_call({subscribe, Topic, SubOpts}, _From,
|
||||
handle_call(
|
||||
{subscribe, Topic, SubOpts},
|
||||
_From,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo
|
||||
= #{clientid := ClientId,
|
||||
mountpoint := Mountpoint},
|
||||
session = Session}) ->
|
||||
clientinfo =
|
||||
ClientInfo =
|
||||
#{
|
||||
clientid := ClientId,
|
||||
mountpoint := Mountpoint
|
||||
},
|
||||
session = Session
|
||||
}
|
||||
) ->
|
||||
NSubOpts = maps:merge(
|
||||
emqx_gateway_utils:default_subopts(),
|
||||
SubOpts),
|
||||
SubOpts
|
||||
),
|
||||
MountedTopic = emqx_mountpoint:mount(Mountpoint, Topic),
|
||||
_ = emqx_broker:subscribe(MountedTopic, ClientId, NSubOpts),
|
||||
|
||||
_ = run_hooks(Ctx, 'session.subscribed',
|
||||
[ClientInfo, MountedTopic, NSubOpts]),
|
||||
_ = run_hooks(
|
||||
Ctx,
|
||||
'session.subscribed',
|
||||
[ClientInfo, MountedTopic, NSubOpts]
|
||||
),
|
||||
%% modify session state
|
||||
Subs = emqx_lwm2m_session:info(subscriptions, Session),
|
||||
NSubs = maps:put(MountedTopic, NSubOpts, Subs),
|
||||
NSession = emqx_lwm2m_session:set_subscriptions(NSubs, Session),
|
||||
{reply, {ok, {MountedTopic, NSubOpts}}, Channel#channel{session = NSession}};
|
||||
|
||||
handle_call({unsubscribe, Topic}, _From,
|
||||
handle_call(
|
||||
{unsubscribe, Topic},
|
||||
_From,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo
|
||||
= #{mountpoint := Mountpoint},
|
||||
session = Session}) ->
|
||||
clientinfo =
|
||||
ClientInfo =
|
||||
#{mountpoint := Mountpoint},
|
||||
session = Session
|
||||
}
|
||||
) ->
|
||||
MountedTopic = emqx_mountpoint:mount(Mountpoint, Topic),
|
||||
ok = emqx_broker:unsubscribe(MountedTopic),
|
||||
_ = run_hooks(Ctx, 'session.unsubscribe',
|
||||
[ClientInfo, MountedTopic, #{}]),
|
||||
_ = run_hooks(
|
||||
Ctx,
|
||||
'session.unsubscribe',
|
||||
[ClientInfo, MountedTopic, #{}]
|
||||
),
|
||||
%% modify session state
|
||||
Subs = emqx_lwm2m_session:info(subscriptions, Session),
|
||||
NSubs = maps:remove(MountedTopic, Subs),
|
||||
NSession = emqx_lwm2m_session:set_subscriptions(NSubs, Session),
|
||||
{reply, ok, Channel#channel{session = NSession}};
|
||||
|
||||
handle_call(subscriptions, _From, Channel = #channel{session = Session}) ->
|
||||
Subs = maps:to_list(emqx_lwm2m_session:info(subscriptions, Session)),
|
||||
{reply, {ok, Subs}, Channel};
|
||||
|
||||
handle_call(kick, _From, Channel) ->
|
||||
NChannel = ensure_disconnected(kicked, Channel),
|
||||
shutdown_and_reply(kicked, ok, NChannel);
|
||||
|
||||
handle_call(discard, _From, Channel) ->
|
||||
shutdown_and_reply(discarded, ok, Channel);
|
||||
|
||||
%% TODO: No Session Takeover
|
||||
%handle_call({takeover, 'begin'}, _From, Channel = #channel{session = Session}) ->
|
||||
% reply(Session, Channel#channel{takeover = true});
|
||||
|
|
@ -259,8 +289,9 @@ handle_call(discard, _From, Channel) ->
|
|||
% shutdown_and_reply(takenover, AllPendings, Channel);
|
||||
|
||||
handle_call(Req, _From, Channel) ->
|
||||
?SLOG(error, #{ msg => "unexpected_call"
|
||||
, call => Req
|
||||
?SLOG(error, #{
|
||||
msg => "unexpected_call",
|
||||
call => Req
|
||||
}),
|
||||
{reply, ignored, Channel}.
|
||||
|
||||
|
|
@ -268,8 +299,9 @@ handle_call(Req, _From, Channel) ->
|
|||
%% Handle Cast
|
||||
%%--------------------------------------------------------------------
|
||||
handle_cast(Req, Channel) ->
|
||||
?SLOG(error, #{ msg => "unexpected_cast"
|
||||
, cast => Req
|
||||
?SLOG(error, #{
|
||||
msg => "unexpected_cast",
|
||||
cast => Req
|
||||
}),
|
||||
{ok, Channel}.
|
||||
|
||||
|
|
@ -279,19 +311,21 @@ handle_cast(Req, Channel) ->
|
|||
handle_info({subscribe, _AutoSubs}, Channel) ->
|
||||
%% not need handle this message
|
||||
{ok, Channel};
|
||||
|
||||
handle_info(Info, Channel) ->
|
||||
?SLOG(error, #{ msg => "unexpected_info"
|
||||
, info => Info
|
||||
?SLOG(error, #{
|
||||
msg => "unexpected_info",
|
||||
info => Info
|
||||
}),
|
||||
{ok, Channel}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Terminate
|
||||
%%--------------------------------------------------------------------
|
||||
terminate(Reason, #channel{ctx = Ctx,
|
||||
terminate(Reason, #channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo,
|
||||
session = Session}) ->
|
||||
session = Session
|
||||
}) ->
|
||||
MountedTopic = emqx_lwm2m_session:on_close(Session),
|
||||
_ = run_hooks(Ctx, 'session.unsubscribe', [ClientInfo, MountedTopic, #{}]),
|
||||
run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]).
|
||||
|
|
@ -303,10 +337,13 @@ terminate(Reason, #channel{ctx = Ctx,
|
|||
%%--------------------------------------------------------------------
|
||||
%% Ensure connected
|
||||
|
||||
ensure_connected(Channel = #channel{
|
||||
ensure_connected(
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
_ = run_hooks(Ctx, 'client.connack', [ConnInfo, connection_accepted, []]),
|
||||
|
||||
NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)},
|
||||
|
|
@ -319,13 +356,20 @@ ensure_connected(Channel = #channel{
|
|||
%%--------------------------------------------------------------------
|
||||
%% Ensure disconnected
|
||||
|
||||
ensure_disconnected(Reason, Channel = #channel{
|
||||
ensure_disconnected(
|
||||
Reason,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
|
||||
ok = run_hooks(Ctx, 'client.disconnected',
|
||||
[ClientInfo, Reason, NConnInfo]),
|
||||
ok = run_hooks(
|
||||
Ctx,
|
||||
'client.disconnected',
|
||||
[ClientInfo, Reason, NConnInfo]
|
||||
),
|
||||
Channel#channel{conninfo = NConnInfo, conn_state = disconnected}.
|
||||
|
||||
shutdown_and_reply(Reason, Reply, Channel) ->
|
||||
|
|
@ -334,13 +378,13 @@ shutdown_and_reply(Reason, Reply, Channel) ->
|
|||
%shutdown_and_reply(Reason, Reply, OutPkt, Channel) ->
|
||||
% {shutdown, Reason, Reply, OutPkt, Channel}.
|
||||
|
||||
set_peercert_infos(NoSSL, ClientInfo)
|
||||
when NoSSL =:= nossl;
|
||||
NoSSL =:= undefined ->
|
||||
set_peercert_infos(NoSSL, ClientInfo) when
|
||||
NoSSL =:= nossl;
|
||||
NoSSL =:= undefined
|
||||
->
|
||||
ClientInfo;
|
||||
set_peercert_infos(Peercert, ClientInfo) ->
|
||||
{DN, CN} = {esockd_peercert:subject(Peercert),
|
||||
esockd_peercert:common_name(Peercert)},
|
||||
{DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)},
|
||||
ClientInfo#{dn => DN, cn => CN}.
|
||||
|
||||
make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) ->
|
||||
|
|
@ -349,7 +393,8 @@ make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) ->
|
|||
|
||||
update_life_timer(#channel{session = Session, timers = Timers} = Channel) ->
|
||||
LifeTime = emqx_lwm2m_session:info(lifetime, Session),
|
||||
_ = case maps:get(lifetime, Timers, undefined) of
|
||||
_ =
|
||||
case maps:get(lifetime, Timers, undefined) of
|
||||
undefined -> ok;
|
||||
Ref -> erlang:cancel_timer(Ref)
|
||||
end,
|
||||
|
|
@ -365,18 +410,25 @@ do_takeover(_DesireId, Msg, Channel) ->
|
|||
call_session(handle_out, Reset, Channel).
|
||||
|
||||
do_connect(Req, Result, Channel, Iter) ->
|
||||
case emqx_misc:pipeline(
|
||||
[ fun check_lwm2m_version/2
|
||||
, fun enrich_conninfo/2
|
||||
, fun run_conn_hooks/2
|
||||
, fun enrich_clientinfo/2
|
||||
, fun set_log_meta/2
|
||||
, fun auth_connect/2
|
||||
case
|
||||
emqx_misc:pipeline(
|
||||
[
|
||||
fun check_lwm2m_version/2,
|
||||
fun enrich_conninfo/2,
|
||||
fun run_conn_hooks/2,
|
||||
fun enrich_clientinfo/2,
|
||||
fun set_log_meta/2,
|
||||
fun auth_connect/2
|
||||
],
|
||||
Req,
|
||||
Channel) of
|
||||
{ok, _Input, #channel{session = Session,
|
||||
with_context = WithContext} = NChannel} ->
|
||||
Channel
|
||||
)
|
||||
of
|
||||
{ok, _Input,
|
||||
#channel{
|
||||
session = Session,
|
||||
with_context = WithContext
|
||||
} = NChannel} ->
|
||||
case emqx_lwm2m_session:info(reg_info, Session) of
|
||||
undefined ->
|
||||
process_connect(ensure_connected(NChannel), Req, Result, Iter);
|
||||
|
|
@ -387,15 +439,20 @@ do_connect(Req, Result, Channel, Iter) ->
|
|||
{error, ReasonCode, NChannel} ->
|
||||
ErrMsg = io_lib:format("Login Failed: ~ts", [ReasonCode]),
|
||||
Payload = erlang:list_to_binary(lists:flatten(ErrMsg)),
|
||||
iter(Iter,
|
||||
iter(
|
||||
Iter,
|
||||
reply({error, bad_request}, Payload, Req, Result),
|
||||
NChannel)
|
||||
NChannel
|
||||
)
|
||||
end.
|
||||
|
||||
check_lwm2m_version(#coap_message{options = Opts},
|
||||
#channel{conninfo = ConnInfo} = Channel) ->
|
||||
check_lwm2m_version(
|
||||
#coap_message{options = Opts},
|
||||
#channel{conninfo = ConnInfo} = Channel
|
||||
) ->
|
||||
Ver = gets([uri_query, <<"lwm2m">>], Opts),
|
||||
IsValid = case Ver of
|
||||
IsValid =
|
||||
case Ver of
|
||||
<<"1.0">> ->
|
||||
true;
|
||||
<<"1">> ->
|
||||
|
|
@ -405,48 +462,61 @@ check_lwm2m_version(#coap_message{options = Opts},
|
|||
_ ->
|
||||
false
|
||||
end,
|
||||
if IsValid ->
|
||||
NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond)
|
||||
, proto_ver => Ver
|
||||
if
|
||||
IsValid ->
|
||||
NConnInfo = ConnInfo#{
|
||||
connected_at => erlang:system_time(millisecond),
|
||||
proto_ver => Ver
|
||||
},
|
||||
{ok, Channel#channel{conninfo = NConnInfo}};
|
||||
true ->
|
||||
?SLOG(error, #{ msg => "reject_REGISTRE_request"
|
||||
, reason => {unsupported_version, Ver}
|
||||
?SLOG(error, #{
|
||||
msg => "reject_REGISTRE_request",
|
||||
reason => {unsupported_version, Ver}
|
||||
}),
|
||||
{error, "invalid lwm2m version", Channel}
|
||||
end.
|
||||
|
||||
run_conn_hooks(Input, Channel = #channel{ctx = Ctx,
|
||||
conninfo = ConnInfo}) ->
|
||||
run_conn_hooks(
|
||||
Input,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
conninfo = ConnInfo
|
||||
}
|
||||
) ->
|
||||
ConnProps = #{},
|
||||
case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of
|
||||
Error = {error, _Reason} -> Error;
|
||||
_NConnProps ->
|
||||
{ok, Input, Channel}
|
||||
_NConnProps -> {ok, Input, Channel}
|
||||
end.
|
||||
|
||||
enrich_conninfo(#coap_message{options = Options},
|
||||
enrich_conninfo(
|
||||
#coap_message{options = Options},
|
||||
Channel = #channel{
|
||||
conninfo = ConnInfo}) ->
|
||||
conninfo = ConnInfo
|
||||
}
|
||||
) ->
|
||||
Query = maps:get(uri_query, Options, #{}),
|
||||
case Query of
|
||||
#{<<"ep">> := Epn, <<"lt">> := Lifetime} ->
|
||||
ClientId = maps:get(<<"device_id">>, Query, Epn),
|
||||
NConnInfo = ConnInfo#{ clientid => ClientId
|
||||
, proto_name => <<"LwM2M">>
|
||||
, proto_ver => <<"1.0.1">>
|
||||
, clean_start => true
|
||||
, keepalive => binary_to_integer(Lifetime)
|
||||
, expiry_interval => 0
|
||||
NConnInfo = ConnInfo#{
|
||||
clientid => ClientId,
|
||||
proto_name => <<"LwM2M">>,
|
||||
proto_ver => <<"1.0.1">>,
|
||||
clean_start => true,
|
||||
keepalive => binary_to_integer(Lifetime),
|
||||
expiry_interval => 0
|
||||
},
|
||||
{ok, Channel#channel{conninfo = NConnInfo}};
|
||||
_ ->
|
||||
{error, "invalid queries", Channel}
|
||||
end.
|
||||
|
||||
enrich_clientinfo(#coap_message{options = Options} = Msg,
|
||||
Channel = #channel{clientinfo = ClientInfo0}) ->
|
||||
enrich_clientinfo(
|
||||
#coap_message{options = Options} = Msg,
|
||||
Channel = #channel{clientinfo = ClientInfo0}
|
||||
) ->
|
||||
Query = maps:get(uri_query, Options, #{}),
|
||||
case Query of
|
||||
#{<<"ep">> := Epn, <<"lt">> := Lifetime} ->
|
||||
|
|
@ -455,16 +525,19 @@ enrich_clientinfo(#coap_message{options = Options} = Msg,
|
|||
Password = maps:get(<<"password">>, Query, undefined),
|
||||
ClientId = maps:get(<<"device_id">>, Query, Epn),
|
||||
ClientInfo =
|
||||
ClientInfo0#{endpoint_name => Epn,
|
||||
ClientInfo0#{
|
||||
endpoint_name => Epn,
|
||||
lifetime => binary_to_integer(Lifetime),
|
||||
username => Username,
|
||||
password => Password,
|
||||
clientid => ClientId},
|
||||
clientid => ClientId
|
||||
},
|
||||
{ok, NClientInfo} = fix_mountpoint(Msg, ClientInfo),
|
||||
{ok, Channel#channel{clientinfo = NClientInfo}};
|
||||
_ ->
|
||||
?SLOG(error, #{ msg => "reject_REGISTER_request"
|
||||
, reason => {wrong_paramters, Query}
|
||||
?SLOG(error, #{
|
||||
msg => "reject_REGISTER_request",
|
||||
reason => {wrong_paramters, Query}
|
||||
}),
|
||||
{error, "invalid queries", Channel}
|
||||
end.
|
||||
|
|
@ -473,18 +546,26 @@ set_log_meta(_Input, #channel{clientinfo = #{clientid := ClientId}}) ->
|
|||
emqx_logger:set_metadata_clientid(ClientId),
|
||||
ok.
|
||||
|
||||
auth_connect(_Input, Channel = #channel{ctx = Ctx,
|
||||
clientinfo = ClientInfo}) ->
|
||||
auth_connect(
|
||||
_Input,
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo
|
||||
}
|
||||
) ->
|
||||
#{clientid := ClientId, username := Username} = ClientInfo,
|
||||
case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of
|
||||
{ok, NClientInfo} ->
|
||||
{ok, Channel#channel{clientinfo = NClientInfo,
|
||||
with_context = with_context(Ctx, ClientInfo)}};
|
||||
{ok, Channel#channel{
|
||||
clientinfo = NClientInfo,
|
||||
with_context = with_context(Ctx, ClientInfo)
|
||||
}};
|
||||
{error, Reason} ->
|
||||
?SLOG(warning, #{ msg => "client_login_failed"
|
||||
, clientid => ClientId
|
||||
, username => Username
|
||||
, reason => Reason
|
||||
?SLOG(warning, #{
|
||||
msg => "client_login_failed",
|
||||
clientid => ClientId,
|
||||
username => Username,
|
||||
reason => Reason
|
||||
}),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
|
@ -495,22 +576,30 @@ fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) ->
|
|||
Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo),
|
||||
{ok, ClientInfo#{mountpoint := Mountpoint1}}.
|
||||
|
||||
process_connect(Channel = #channel{ctx = Ctx,
|
||||
process_connect(
|
||||
Channel = #channel{
|
||||
ctx = Ctx,
|
||||
session = Session,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo,
|
||||
with_context = WithContext},
|
||||
Msg, Result, Iter) ->
|
||||
with_context = WithContext
|
||||
},
|
||||
Msg,
|
||||
Result,
|
||||
Iter
|
||||
) ->
|
||||
%% inherit the old session
|
||||
SessFun = fun(_,_) -> #{} end,
|
||||
case emqx_gateway_ctx:open_session(
|
||||
SessFun = fun(_, _) -> #{} end,
|
||||
case
|
||||
emqx_gateway_ctx:open_session(
|
||||
Ctx,
|
||||
true,
|
||||
ClientInfo,
|
||||
ConnInfo,
|
||||
SessFun,
|
||||
emqx_lwm2m_session
|
||||
) of
|
||||
)
|
||||
of
|
||||
{ok, _} ->
|
||||
Mountpoint = maps:get(mountpoint, ClientInfo, <<>>),
|
||||
NewResult0 = emqx_lwm2m_session:init(
|
||||
|
|
@ -522,8 +611,9 @@ process_connect(Channel = #channel{ctx = Ctx,
|
|||
NewResult1 = NewResult0#{events => [{event, connected}]},
|
||||
iter(Iter, maps:merge(Result, NewResult1), Channel);
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{ msg => "falied_to_open_session"
|
||||
, reason => Reason
|
||||
?SLOG(error, #{
|
||||
msg => "falied_to_open_session",
|
||||
reason => Reason
|
||||
}),
|
||||
iter(Iter, reply({error, bad_request}, Msg, Result), Channel)
|
||||
end.
|
||||
|
|
@ -557,52 +647,68 @@ with_context(publish, [Topic, Msg], Ctx, ClientInfo) ->
|
|||
_ = emqx_broker:publish(Msg),
|
||||
ok;
|
||||
_ ->
|
||||
?SLOG(error, #{ msg => "publish_denied"
|
||||
, topic => Topic
|
||||
?SLOG(error, #{
|
||||
msg => "publish_denied",
|
||||
topic => Topic
|
||||
}),
|
||||
{error, deny}
|
||||
end;
|
||||
|
||||
with_context(subscribe, [Topic, Opts], Ctx, ClientInfo) ->
|
||||
#{clientid := ClientId,
|
||||
endpoint_name := EndpointName} = ClientInfo,
|
||||
#{
|
||||
clientid := ClientId,
|
||||
endpoint_name := EndpointName
|
||||
} = ClientInfo,
|
||||
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic) of
|
||||
allow ->
|
||||
run_hooks(Ctx, 'session.subscribed', [ClientInfo, Topic, Opts]),
|
||||
?SLOG(debug, #{ msg => "subscribe_topic_succeed"
|
||||
, topic => Topic
|
||||
, clientid => ClientId
|
||||
, endpoint_name => EndpointName
|
||||
?SLOG(debug, #{
|
||||
msg => "subscribe_topic_succeed",
|
||||
topic => Topic,
|
||||
clientid => ClientId,
|
||||
endpoint_name => EndpointName
|
||||
}),
|
||||
emqx_broker:subscribe(Topic, ClientId, Opts),
|
||||
ok;
|
||||
_ ->
|
||||
?SLOG(error, #{ msg => "subscribe_denied"
|
||||
, topic => Topic
|
||||
?SLOG(error, #{
|
||||
msg => "subscribe_denied",
|
||||
topic => Topic
|
||||
}),
|
||||
{error, deny}
|
||||
end;
|
||||
|
||||
with_context(metrics, Name, Ctx, _ClientInfo) ->
|
||||
emqx_gateway_ctx:metrics_inc(Ctx, Name).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Call Chain
|
||||
%%--------------------------------------------------------------------
|
||||
call_session(Fun,
|
||||
call_session(
|
||||
Fun,
|
||||
Msg,
|
||||
#channel{session = Session,
|
||||
with_context = WithContext} = Channel) ->
|
||||
iter([ session, fun process_session/4
|
||||
, proto, fun process_protocol/4
|
||||
, return, fun process_return/4
|
||||
, lifetime, fun process_lifetime/4
|
||||
, reply, fun process_reply/4
|
||||
, out, fun process_out/4
|
||||
, fun process_nothing/3
|
||||
#channel{
|
||||
session = Session,
|
||||
with_context = WithContext
|
||||
} = Channel
|
||||
) ->
|
||||
iter(
|
||||
[
|
||||
session,
|
||||
fun process_session/4,
|
||||
proto,
|
||||
fun process_protocol/4,
|
||||
return,
|
||||
fun process_return/4,
|
||||
lifetime,
|
||||
fun process_lifetime/4,
|
||||
reply,
|
||||
fun process_reply/4,
|
||||
out,
|
||||
fun process_out/4,
|
||||
fun process_nothing/3
|
||||
],
|
||||
emqx_lwm2m_session:Fun(Msg, WithContext, Session),
|
||||
Channel).
|
||||
Channel
|
||||
).
|
||||
|
||||
process_session(Session, Result, Channel, Iter) ->
|
||||
iter(Iter, Result, Channel#channel{session = Session}).
|
||||
|
|
@ -610,14 +716,22 @@ process_session(Session, Result, Channel, Iter) ->
|
|||
process_protocol({request, Msg}, Result, Channel, Iter) ->
|
||||
#coap_message{method = Method} = Msg,
|
||||
handle_request_protocol(Method, Msg, Result, Channel, Iter);
|
||||
|
||||
process_protocol(Msg, Result,
|
||||
#channel{with_context = WithContext, session = Session} = Channel, Iter) ->
|
||||
process_protocol(
|
||||
Msg,
|
||||
Result,
|
||||
#channel{with_context = WithContext, session = Session} = Channel,
|
||||
Iter
|
||||
) ->
|
||||
ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, WithContext, Session),
|
||||
iter(Iter, maps:merge(Result, ProtoResult), Channel).
|
||||
|
||||
handle_request_protocol(post, #coap_message{options = Opts} = Msg,
|
||||
Result, Channel, Iter) ->
|
||||
handle_request_protocol(
|
||||
post,
|
||||
#coap_message{options = Opts} = Msg,
|
||||
Result,
|
||||
Channel,
|
||||
Iter
|
||||
) ->
|
||||
case Opts of
|
||||
#{uri_path := [?REG_PREFIX]} ->
|
||||
do_connect(Msg, Result, Channel, Iter);
|
||||
|
|
@ -626,9 +740,13 @@ handle_request_protocol(post, #coap_message{options = Opts} = Msg,
|
|||
_ ->
|
||||
iter(Iter, reply({error, not_found}, Msg, Result), Channel)
|
||||
end;
|
||||
|
||||
handle_request_protocol(delete, #coap_message{options = Opts} = Msg,
|
||||
Result, Channel, Iter) ->
|
||||
handle_request_protocol(
|
||||
delete,
|
||||
#coap_message{options = Opts} = Msg,
|
||||
Result,
|
||||
Channel,
|
||||
Iter
|
||||
) ->
|
||||
case Opts of
|
||||
#{uri_path := Location} ->
|
||||
case check_location(Location, Channel) of
|
||||
|
|
@ -642,8 +760,13 @@ handle_request_protocol(delete, #coap_message{options = Opts} = Msg,
|
|||
iter(Iter, reply({error, bad_request}, Msg, Result), Channel)
|
||||
end.
|
||||
|
||||
do_update(Location, Msg, Result,
|
||||
#channel{session = Session, with_context = WithContext} = Channel, Iter) ->
|
||||
do_update(
|
||||
Location,
|
||||
Msg,
|
||||
Result,
|
||||
#channel{session = Session, with_context = WithContext} = Channel,
|
||||
Iter
|
||||
) ->
|
||||
case check_location(Location, Channel) of
|
||||
true ->
|
||||
NewResult = emqx_lwm2m_session:update(Msg, WithContext, Session),
|
||||
|
|
@ -654,13 +777,16 @@ do_update(Location, Msg, Result,
|
|||
|
||||
process_return({Outs, Session}, Result, Channel, Iter) ->
|
||||
OldOuts = maps:get(out, Result, []),
|
||||
iter(Iter,
|
||||
iter(
|
||||
Iter,
|
||||
Result#{out => Outs ++ OldOuts},
|
||||
Channel#channel{session = Session}).
|
||||
Channel#channel{session = Session}
|
||||
).
|
||||
|
||||
process_out(Outs, Result, Channel, _) ->
|
||||
Outs2 = lists:reverse(Outs),
|
||||
Outs3 = case maps:get(reply, Result, undefined) of
|
||||
Outs3 =
|
||||
case maps:get(reply, Result, undefined) of
|
||||
undefined ->
|
||||
Outs2;
|
||||
Reply ->
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@
|
|||
-include("src/coap/include/emqx_coap.hrl").
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
-export([ mqtt_to_coap/2
|
||||
, coap_to_mqtt/4
|
||||
, empty_ack_to_mqtt/1
|
||||
, coap_failure_to_mqtt/2
|
||||
]).
|
||||
-export([
|
||||
mqtt_to_coap/2,
|
||||
coap_to_mqtt/4,
|
||||
empty_ack_to_mqtt/1,
|
||||
coap_failure_to_mqtt/2
|
||||
]).
|
||||
|
||||
-export([path_list/1, extract_path/1]).
|
||||
|
||||
|
|
@ -54,26 +55,47 @@ mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data"
|
|||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
CoapRequest = emqx_coap_message:request(con, post, Payload,
|
||||
[{uri_path, FullPathList},
|
||||
CoapRequest = emqx_coap_message:request(
|
||||
con,
|
||||
post,
|
||||
Payload,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}]),
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}
|
||||
]
|
||||
),
|
||||
{CoapRequest, InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, delete, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList}]), InputCmd};
|
||||
|
||||
{
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
delete,
|
||||
<<>>,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList}
|
||||
]
|
||||
),
|
||||
InputCmd
|
||||
};
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, get, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList}]), InputCmd};
|
||||
|
||||
{
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
get,
|
||||
<<>>,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList}
|
||||
]
|
||||
),
|
||||
InputCmd
|
||||
};
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) ->
|
||||
CoapRequest =
|
||||
case maps:get(<<"basePath">>, Data, <<"/">>) of
|
||||
|
|
@ -83,7 +105,6 @@ mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">
|
|||
batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data))
|
||||
end,
|
||||
{CoapRequest, InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
|
|
@ -93,85 +114,122 @@ mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data
|
|||
undefined -> <<>>;
|
||||
Arg1 -> Arg1
|
||||
end,
|
||||
{emqx_coap_message:request(con, post, Args,
|
||||
[{uri_path, FullPathList},
|
||||
{
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
post,
|
||||
Args,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{content_format, <<"text/plain">>}]), InputCmd};
|
||||
|
||||
{content_format, <<"text/plain">>}
|
||||
]
|
||||
),
|
||||
InputCmd
|
||||
};
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, get, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
get,
|
||||
<<>>,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{'accept', ?LWM2M_FORMAT_LINK}]), InputCmd};
|
||||
|
||||
{'accept', ?LWM2M_FORMAT_LINK}
|
||||
]
|
||||
),
|
||||
InputCmd
|
||||
};
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
Query = attr_query_list(Data),
|
||||
{emqx_coap_message:request(con, put, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
put,
|
||||
<<>>,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{uri_query, Query}]), InputCmd};
|
||||
|
||||
{uri_query, Query}
|
||||
]
|
||||
),
|
||||
InputCmd
|
||||
};
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, get, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
get,
|
||||
<<>>,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{observe, 0}]), InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) ->
|
||||
{observe, 0}
|
||||
]
|
||||
),
|
||||
InputCmd
|
||||
};
|
||||
mqtt_to_coap(
|
||||
AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}
|
||||
) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, get, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
get,
|
||||
<<>>,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{observe, 1}]), InputCmd}.
|
||||
{observe, 1}
|
||||
]
|
||||
),
|
||||
InputCmd
|
||||
}.
|
||||
|
||||
coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) ->
|
||||
coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref = #{<<"msgType">> := <<"create">>}) ->
|
||||
make_response(Code, Ref);
|
||||
|
||||
coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) ->
|
||||
coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref = #{<<"msgType">> := <<"delete">>}) ->
|
||||
make_response(Code, Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) ->
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref = #{<<"msgType">> := <<"read">>}) ->
|
||||
read_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) ->
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref = #{<<"msgType">> := <<"write">>}) ->
|
||||
write_resp_to_mqtt(Method, CoapPayload, Ref);
|
||||
|
||||
coap_to_mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) ->
|
||||
coap_to_mqtt(Method, _CoapPayload, _Options, Ref = #{<<"msgType">> := <<"execute">>}) ->
|
||||
execute_resp_to_mqtt(Method, Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) ->
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref = #{<<"msgType">> := <<"discover">>}) ->
|
||||
discover_resp_to_mqtt(Method, CoapPayload, Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) ->
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref = #{<<"msgType">> := <<"write-attr">>}) ->
|
||||
writeattr_resp_to_mqtt(Method, CoapPayload, Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) ->
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref = #{<<"msgType">> := <<"observe">>}) ->
|
||||
observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) ->
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref = #{<<"msgType">> := <<"cancel-observe">>}) ->
|
||||
cancel_observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref).
|
||||
|
||||
read_resp_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) ->
|
||||
make_response(ErrorCode, Ref);
|
||||
|
||||
read_resp_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) ->
|
||||
try
|
||||
Result = content_to_mqtt(CoapPayload, Format, Ref),
|
||||
make_response(SuccessCode, Ref, Format, Result)
|
||||
catch
|
||||
error:not_implemented -> make_response(not_implemented, Ref);
|
||||
error:not_implemented ->
|
||||
make_response(not_implemented, Ref);
|
||||
_:Ex:_ST ->
|
||||
?SLOG(error, #{ msg => "bad_payload_format"
|
||||
, payload => CoapPayload
|
||||
, reason => Ex
|
||||
, stacktrace => _ST}),
|
||||
?SLOG(error, #{
|
||||
msg => "bad_payload_format",
|
||||
payload => CoapPayload,
|
||||
reason => Ex,
|
||||
stacktrace => _ST
|
||||
}),
|
||||
make_response(bad_request, Ref)
|
||||
end.
|
||||
|
||||
|
|
@ -183,67 +241,55 @@ coap_failure_to_mqtt(Ref, MsgType) ->
|
|||
|
||||
content_to_mqtt(CoapPayload, <<"text/plain">>, Ref) ->
|
||||
emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload);
|
||||
|
||||
content_to_mqtt(CoapPayload, <<"application/octet-stream">>, Ref) ->
|
||||
emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload);
|
||||
|
||||
content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) ->
|
||||
emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload);
|
||||
|
||||
content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) ->
|
||||
emqx_lwm2m_message:translate_json(CoapPayload).
|
||||
|
||||
write_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
|
||||
write_resp_to_mqtt({ok, content}, CoapPayload, Ref) when CoapPayload =:= <<>> ->
|
||||
make_response(method_not_allowed, Ref);
|
||||
|
||||
write_resp_to_mqtt({ok, content}, _CoapPayload, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
|
||||
write_resp_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
execute_resp_to_mqtt({ok, changed}, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
|
||||
execute_resp_to_mqtt({error, Error}, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
discover_resp_to_mqtt({ok, content}, CoapPayload, Ref) ->
|
||||
Links = binary:split(CoapPayload, <<",">>, [global]),
|
||||
make_response(content, Ref, <<"application/link-format">>, Links);
|
||||
|
||||
discover_resp_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
writeattr_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
|
||||
writeattr_resp_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) ->
|
||||
make_response(Error, Ref);
|
||||
|
||||
observe_resp_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) ->
|
||||
read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref);
|
||||
|
||||
observe_resp_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) ->
|
||||
read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref#{<<"seqNum">> => ObserveSeqNum}).
|
||||
|
||||
cancel_observe_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref) ->
|
||||
read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref);
|
||||
|
||||
cancel_observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
make_response(Code, Ref=#{}) ->
|
||||
make_response(Code, Ref = #{}) ->
|
||||
BaseRsp = make_base_response(Ref),
|
||||
make_data_response(BaseRsp, Code).
|
||||
|
||||
make_response(Code, Ref=#{}, _Format, Result) ->
|
||||
make_response(Code, Ref = #{}, _Format, Result) ->
|
||||
BaseRsp = make_base_response(Ref),
|
||||
make_data_response(BaseRsp, Code, _Format, Result).
|
||||
|
||||
|
|
@ -258,7 +304,7 @@ make_response(Code, Ref=#{}, _Format, Result) ->
|
|||
%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null)
|
||||
%% }
|
||||
|
||||
make_base_response(Ref=#{}) ->
|
||||
make_base_response(Ref = #{}) ->
|
||||
remove_tmp_fields(Ref).
|
||||
|
||||
make_data_response(BaseRsp, Code) ->
|
||||
|
|
@ -284,7 +330,7 @@ make_data_response(BaseRsp, Code, _Format, Result) ->
|
|||
remove_tmp_fields(Ref) ->
|
||||
maps:remove(observe_type, Ref).
|
||||
|
||||
-spec path_list(Path::binary()) -> {[PathWord::binary()], [Query::binary()]}.
|
||||
-spec path_list(Path :: binary()) -> {[PathWord :: binary()], [Query :: binary()]}.
|
||||
path_list(Path) ->
|
||||
case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, ResId, LastPart] ->
|
||||
|
|
@ -304,8 +350,7 @@ path_list(Path) ->
|
|||
query_list(PathWithQuery) ->
|
||||
case binary:split(PathWithQuery, [<<$?>>], []) of
|
||||
[Path] -> {Path, []};
|
||||
[Path, Querys] ->
|
||||
{Path, binary:split(Querys, [<<$&>>], [global])}
|
||||
[Path, Querys] -> {Path, binary:split(Querys, [<<$&>>], [global])}
|
||||
end.
|
||||
|
||||
attr_query_list(Data) ->
|
||||
|
|
@ -314,7 +359,8 @@ attr_query_list(Data) ->
|
|||
attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) ->
|
||||
maps:fold(
|
||||
fun
|
||||
(_K, null, Acc) -> Acc;
|
||||
(_K, null, Acc) ->
|
||||
Acc;
|
||||
(K, V, Acc) ->
|
||||
case lists:member(K, ValidAttrKeys) of
|
||||
true ->
|
||||
|
|
@ -323,7 +369,10 @@ attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) ->
|
|||
false ->
|
||||
Acc
|
||||
end
|
||||
end, QueryList, QueryJson).
|
||||
end,
|
||||
QueryList,
|
||||
QueryJson
|
||||
).
|
||||
|
||||
valid_attr_keys() ->
|
||||
[<<"pmin">>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>].
|
||||
|
|
@ -332,11 +381,10 @@ data_format(Options) ->
|
|||
maps:get(content_format, Options, <<"text/plain">>).
|
||||
|
||||
observe_seq(Options) ->
|
||||
maps:get(observe, Options, rand:uniform(1000000) + 1 ).
|
||||
maps:get(observe, Options, rand:uniform(1000000) + 1).
|
||||
|
||||
add_alternate_path_prefix(<<"/">>, PathList) ->
|
||||
PathList;
|
||||
|
||||
add_alternate_path_prefix(AlternatePath, PathList) ->
|
||||
[binary_util:trim(AlternatePath, $/) | PathList].
|
||||
|
||||
|
|
@ -350,22 +398,29 @@ extract_path(Ref = #{}) ->
|
|||
end;
|
||||
#{<<"path">> := Path} ->
|
||||
Path
|
||||
end).
|
||||
|
||||
end
|
||||
).
|
||||
|
||||
batch_write_request(AlternatePath, BasePath, Content) ->
|
||||
{PathList, QueryList} = path_list(BasePath),
|
||||
Method = case length(PathList) of
|
||||
Method =
|
||||
case length(PathList) of
|
||||
2 -> post;
|
||||
3 -> put
|
||||
end,
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
emqx_coap_message:request(con, Method, Payload,
|
||||
[{uri_path, FullPathList},
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
Method,
|
||||
Payload,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}]).
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}
|
||||
]
|
||||
).
|
||||
|
||||
single_write_request(AlternatePath, Data) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
|
|
@ -373,10 +428,16 @@ single_write_request(AlternatePath, Data) ->
|
|||
%% TO DO: handle write to resource instance, e.g. /4/0/1/0
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, [Data]),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
emqx_coap_message:request(con, put, Payload,
|
||||
[{uri_path, FullPathList},
|
||||
emqx_coap_message:request(
|
||||
con,
|
||||
put,
|
||||
Payload,
|
||||
[
|
||||
{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}]).
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}
|
||||
]
|
||||
).
|
||||
|
||||
drop_query(Path) ->
|
||||
case binary:split(Path, [<<$?>>]) of
|
||||
|
|
|
|||
|
|
@ -22,22 +22,23 @@
|
|||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% APIs
|
||||
-export([ reg/0
|
||||
, unreg/0
|
||||
]).
|
||||
-export([
|
||||
reg/0,
|
||||
unreg/0
|
||||
]).
|
||||
|
||||
-export([ on_gateway_load/2
|
||||
, on_gateway_update/3
|
||||
, on_gateway_unload/2
|
||||
]).
|
||||
-export([
|
||||
on_gateway_load/2,
|
||||
on_gateway_update/3,
|
||||
on_gateway_unload/2
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
reg() ->
|
||||
RegistryOptions = [ {cbkmod, ?MODULE}
|
||||
],
|
||||
RegistryOptions = [{cbkmod, ?MODULE}],
|
||||
emqx_gateway_registry:reg(lwm2m, RegistryOptions).
|
||||
|
||||
unreg() ->
|
||||
|
|
@ -47,32 +48,46 @@ unreg() ->
|
|||
%% emqx_gateway_registry callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
on_gateway_load(_Gateway = #{ name := GwName,
|
||||
on_gateway_load(
|
||||
_Gateway = #{
|
||||
name := GwName,
|
||||
config := Config
|
||||
}, Ctx) ->
|
||||
},
|
||||
Ctx
|
||||
) ->
|
||||
XmlDir = maps:get(xml_dir, Config),
|
||||
case emqx_lwm2m_xml_object_db:start_link(XmlDir) of
|
||||
{ok, RegPid} ->
|
||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||
ModCfg = #{frame_mod => emqx_coap_frame,
|
||||
ModCfg = #{
|
||||
frame_mod => emqx_coap_frame,
|
||||
chann_mod => emqx_lwm2m_channel
|
||||
},
|
||||
case emqx_gateway_utils:start_listeners(
|
||||
Listeners, GwName, Ctx, ModCfg) of
|
||||
case
|
||||
emqx_gateway_utils:start_listeners(
|
||||
Listeners, GwName, Ctx, ModCfg
|
||||
)
|
||||
of
|
||||
{ok, ListenerPids} ->
|
||||
{ok, ListenerPids, #{ctx => Ctx, registry => RegPid}};
|
||||
{error, {Reason, Listener}} ->
|
||||
_ = emqx_lwm2m_xml_object_db:stop(),
|
||||
throw({badconf, #{ key => listeners
|
||||
, vallue => Listener
|
||||
, reason => Reason
|
||||
}})
|
||||
throw(
|
||||
{badconf, #{
|
||||
key => listeners,
|
||||
vallue => Listener,
|
||||
reason => Reason
|
||||
}}
|
||||
)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
throw({badconf, #{ key => xml_dir
|
||||
, value => XmlDir
|
||||
, reason => Reason
|
||||
}})
|
||||
throw(
|
||||
{badconf, #{
|
||||
key => xml_dir,
|
||||
value => XmlDir,
|
||||
reason => Reason
|
||||
}}
|
||||
)
|
||||
end.
|
||||
|
||||
on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) ->
|
||||
|
|
@ -83,16 +98,27 @@ on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) ->
|
|||
on_gateway_unload(Gateway, GwState),
|
||||
on_gateway_load(Gateway#{config => Config}, Ctx)
|
||||
catch
|
||||
Class : Reason : Stk ->
|
||||
logger:error("Failed to update ~ts; "
|
||||
Class:Reason:Stk ->
|
||||
logger:error(
|
||||
"Failed to update ~ts; "
|
||||
"reason: {~0p, ~0p} stacktrace: ~0p",
|
||||
[GwName, Class, Reason, Stk]),
|
||||
[GwName, Class, Reason, Stk]
|
||||
),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
on_gateway_unload(_Gateway = #{ name := GwName,
|
||||
on_gateway_unload(
|
||||
_Gateway = #{
|
||||
name := GwName,
|
||||
config := Config
|
||||
}, _GwState = #{registry := _RegPid}) ->
|
||||
_ = try emqx_lwm2m_xml_object_db:stop() catch _ : _ -> ok end,
|
||||
},
|
||||
_GwState = #{registry := _RegPid}
|
||||
) ->
|
||||
_ =
|
||||
try
|
||||
emqx_lwm2m_xml_object_db:stop()
|
||||
catch
|
||||
_:_ -> ok
|
||||
end,
|
||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||
emqx_gateway_utils:stop_listeners(GwName, Listeners).
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@
|
|||
|
||||
-module(emqx_lwm2m_message).
|
||||
|
||||
-export([ tlv_to_json/2
|
||||
, json_to_tlv/2
|
||||
, text_to_json/2
|
||||
, opaque_to_json/2
|
||||
, translate_json/1
|
||||
]).
|
||||
-export([
|
||||
tlv_to_json/2,
|
||||
json_to_tlv/2,
|
||||
text_to_json/2,
|
||||
opaque_to_json/2,
|
||||
translate_json/1
|
||||
]).
|
||||
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
|
|
@ -30,58 +31,86 @@ tlv_to_json(BaseName, TlvData) ->
|
|||
ObjectId = object_id(BaseName),
|
||||
ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true),
|
||||
case DecodedTlv of
|
||||
[#{tlv_resource_with_value:=Id, value:=Value}] ->
|
||||
[#{tlv_resource_with_value := Id, value := Value}] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, Id, 3),
|
||||
tlv_single_resource(TrueBaseName, Id, Value, ObjDefinition);
|
||||
List1 = [#{tlv_resource_with_value:=_Id}, _|_] ->
|
||||
List1 = [#{tlv_resource_with_value := _Id}, _ | _] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2),
|
||||
tlv_level2(TrueBaseName, List1, ObjDefinition, []);
|
||||
List2 = [#{tlv_multiple_resource:=_Id}|_] ->
|
||||
List2 = [#{tlv_multiple_resource := _Id} | _] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2),
|
||||
tlv_level2(TrueBaseName, List2, ObjDefinition, []);
|
||||
[#{tlv_object_instance:=Id, value:=Value}] ->
|
||||
[#{tlv_object_instance := Id, value := Value}] ->
|
||||
TrueBaseName = basename(BaseName, undefined, Id, undefined, 2),
|
||||
tlv_level2(TrueBaseName, Value, ObjDefinition, []);
|
||||
List3=[#{tlv_object_instance:=_Id}, _|_] ->
|
||||
List3 = [#{tlv_object_instance := _Id}, _ | _] ->
|
||||
tlv_level1(integer_to_binary(ObjectId), List3, ObjDefinition, [])
|
||||
end.
|
||||
|
||||
|
||||
tlv_level1(_Path, [], _ObjDefinition, Acc) ->
|
||||
Acc;
|
||||
tlv_level1(Path, [#{tlv_object_instance:=Id, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
tlv_level1(Path, [#{tlv_object_instance := Id, value := Value} | T], ObjDefinition, Acc) ->
|
||||
New = tlv_level2(make_path(Path, Id), Value, ObjDefinition, []),
|
||||
tlv_level1(Path, T, ObjDefinition, Acc++New).
|
||||
tlv_level1(Path, T, ObjDefinition, Acc ++ New).
|
||||
|
||||
tlv_level2(_, [], _, Acc) ->
|
||||
Acc;
|
||||
tlv_level2(RelativePath, [#{tlv_resource_with_value:=ResourceId, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
tlv_level2(
|
||||
RelativePath, [#{tlv_resource_with_value := ResourceId, value := Value} | T], ObjDefinition, Acc
|
||||
) ->
|
||||
Val = value(Value, ResourceId, ObjDefinition),
|
||||
New = #{path => make_path(RelativePath, ResourceId),
|
||||
value=>Val},
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc++[New]);
|
||||
tlv_level2(RelativePath, [#{tlv_multiple_resource:=ResourceId, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
SubList = tlv_level3(make_path(RelativePath, ResourceId),
|
||||
Value, ResourceId, ObjDefinition, []),
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc++SubList).
|
||||
New = #{
|
||||
path => make_path(RelativePath, ResourceId),
|
||||
value => Val
|
||||
},
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc ++ [New]);
|
||||
tlv_level2(
|
||||
RelativePath, [#{tlv_multiple_resource := ResourceId, value := Value} | T], ObjDefinition, Acc
|
||||
) ->
|
||||
SubList = tlv_level3(
|
||||
make_path(RelativePath, ResourceId),
|
||||
Value,
|
||||
ResourceId,
|
||||
ObjDefinition,
|
||||
[]
|
||||
),
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc ++ SubList).
|
||||
|
||||
tlv_level3(_RelativePath, [], _Id, _ObjDefinition, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
tlv_level3(RelativePath, [#{tlv_resource_instance:=InsId, value:=Value}|T], ResourceId, ObjDefinition, Acc) ->
|
||||
tlv_level3(
|
||||
RelativePath,
|
||||
[#{tlv_resource_instance := InsId, value := Value} | T],
|
||||
ResourceId,
|
||||
ObjDefinition,
|
||||
Acc
|
||||
) ->
|
||||
Val = value(Value, ResourceId, ObjDefinition),
|
||||
New = #{path => make_path(RelativePath, InsId),
|
||||
value=>Val},
|
||||
tlv_level3(RelativePath, T, ResourceId, ObjDefinition, [New|Acc]).
|
||||
New = #{
|
||||
path => make_path(RelativePath, InsId),
|
||||
value => Val
|
||||
},
|
||||
tlv_level3(RelativePath, T, ResourceId, ObjDefinition, [New | Acc]).
|
||||
|
||||
tlv_single_resource(BaseName, Id, Value, ObjDefinition) ->
|
||||
Val = value(Value, Id, ObjDefinition),
|
||||
[#{path=>BaseName, value=>Val}].
|
||||
[#{path => BaseName, value => Val}].
|
||||
|
||||
basename(OldBaseName, _ObjectId, ObjectInstanceId, ResourceId, 3) ->
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, ResId/binary>>;
|
||||
[ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, (integer_to_binary(ResourceId))/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary, $/, (integer_to_binary(ResourceId))/binary>>
|
||||
[ObjId, ObjInsId, ResId] ->
|
||||
<<$/, ObjId/binary, $/, ObjInsId/binary, $/, ResId/binary>>;
|
||||
[ObjId, ObjInsId] ->
|
||||
<<$/, ObjId/binary, $/, ObjInsId/binary, $/, (integer_to_binary(ResourceId))/binary>>;
|
||||
[ObjId] ->
|
||||
<<
|
||||
$/,
|
||||
ObjId/binary,
|
||||
$/,
|
||||
(integer_to_binary(ObjectInstanceId))/binary,
|
||||
$/,
|
||||
(integer_to_binary(ResourceId))/binary
|
||||
>>
|
||||
end;
|
||||
basename(OldBaseName, _ObjectId, ObjectInstanceId, _ResourceId, 2) ->
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
|
|
@ -109,8 +138,10 @@ object_id(BaseName) ->
|
|||
|
||||
object_resource_id(BaseName) ->
|
||||
case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of
|
||||
[_ObjIdBin1] -> error({invalid_basename, BaseName});
|
||||
[_ObjIdBin2, _] -> error({invalid_basename, BaseName});
|
||||
[_ObjIdBin1] ->
|
||||
error({invalid_basename, BaseName});
|
||||
[_ObjIdBin2, _] ->
|
||||
error({invalid_basename, BaseName});
|
||||
[ObjIdBin3, _, ResourceId3] ->
|
||||
{binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)};
|
||||
[ObjIdBin3, _, ResourceId3, _] ->
|
||||
|
|
@ -121,17 +152,19 @@ object_resource_id(BaseName) ->
|
|||
value(Value, ResourceId, ObjDefinition) ->
|
||||
case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of
|
||||
"String" ->
|
||||
Value; % keep binary type since it is same as a string for jsx
|
||||
% keep binary type since it is same as a string for jsx
|
||||
Value;
|
||||
"Integer" ->
|
||||
Size = byte_size(Value)*8,
|
||||
Size = byte_size(Value) * 8,
|
||||
<<IntResult:Size/signed>> = Value,
|
||||
IntResult;
|
||||
"Float" ->
|
||||
Size = byte_size(Value)*8,
|
||||
Size = byte_size(Value) * 8,
|
||||
<<FloatResult:Size/float>> = Value,
|
||||
FloatResult;
|
||||
"Boolean" ->
|
||||
B = case Value of
|
||||
B =
|
||||
case Value of
|
||||
<<0>> -> false;
|
||||
<<1>> -> true
|
||||
end,
|
||||
|
|
@ -139,7 +172,7 @@ value(Value, ResourceId, ObjDefinition) ->
|
|||
"Opaque" ->
|
||||
base64:encode(Value);
|
||||
"Time" ->
|
||||
Size = byte_size(Value)*8,
|
||||
Size = byte_size(Value) * 8,
|
||||
<<IntResult:Size>> = Value,
|
||||
IntResult;
|
||||
"Objlnk" ->
|
||||
|
|
@ -149,8 +182,12 @@ value(Value, ResourceId, ObjDefinition) ->
|
|||
|
||||
json_to_tlv([_ObjectId, _ObjectInstanceId, ResourceId], ResourceArray) ->
|
||||
case length(ResourceArray) of
|
||||
1 -> element_single_resource(integer(ResourceId), ResourceArray);
|
||||
_ -> element_loop_level4(ResourceArray, [#{tlv_multiple_resource=>integer(ResourceId), value=>[]}])
|
||||
1 ->
|
||||
element_single_resource(integer(ResourceId), ResourceArray);
|
||||
_ ->
|
||||
element_loop_level4(ResourceArray, [
|
||||
#{tlv_multiple_resource => integer(ResourceId), value => []}
|
||||
])
|
||||
end;
|
||||
json_to_tlv([_ObjectId, _ObjectInstanceId], ResourceArray) ->
|
||||
element_loop_level3(ResourceArray, []);
|
||||
|
|
@ -159,23 +196,23 @@ json_to_tlv([_ObjectId], ResourceArray) ->
|
|||
|
||||
element_single_resource(ResourceId, [#{<<"type">> := Type, <<"value">> := Value}]) ->
|
||||
BinaryValue = value_ex(Type, Value),
|
||||
[#{tlv_resource_with_value=>integer(ResourceId), value=>BinaryValue}].
|
||||
[#{tlv_resource_with_value => integer(ResourceId), value => BinaryValue}].
|
||||
|
||||
element_loop_level2([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level2([H|T], Acc) ->
|
||||
element_loop_level2([H | T], Acc) ->
|
||||
NewAcc = insert(object, H, Acc),
|
||||
element_loop_level2(T, NewAcc).
|
||||
|
||||
element_loop_level3([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level3([H|T], Acc) ->
|
||||
element_loop_level3([H | T], Acc) ->
|
||||
NewAcc = insert(object_instance, H, Acc),
|
||||
element_loop_level3(T, NewAcc).
|
||||
|
||||
element_loop_level4([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level4([H|T], Acc) ->
|
||||
element_loop_level4([H | T], Acc) ->
|
||||
NewAcc = insert(resource, H, Acc),
|
||||
element_loop_level4(T, NewAcc).
|
||||
|
||||
|
|
@ -191,7 +228,6 @@ insert(Level, #{<<"path">> := EleName, <<"type">> := Type, <<"value">> := Value}
|
|||
% json text to TLV binary
|
||||
value_ex(K, Value) when K =:= <<"Integer">>; K =:= <<"Float">>; K =:= <<"Time">> ->
|
||||
encode_number(Value);
|
||||
|
||||
value_ex(K, Value) when K =:= <<"String">> ->
|
||||
Value;
|
||||
value_ex(K, Value) when K =:= <<"Opaque">> ->
|
||||
|
|
@ -201,34 +237,33 @@ value_ex(K, Value) when K =:= <<"Opaque">> ->
|
|||
base64:decode(Value);
|
||||
value_ex(K, <<"true">>) when K =:= <<"Boolean">> -> <<1>>;
|
||||
value_ex(K, <<"false">>) when K =:= <<"Boolean">> -> <<0>>;
|
||||
|
||||
value_ex(K, Value) when K =:= <<"Objlnk">>; K =:= ov ->
|
||||
[P1, P2] = binary:split(Value, [<<$:>>], [global]),
|
||||
<<(binary_to_integer(P1)):16, (binary_to_integer(P2)):16>>.
|
||||
|
||||
insert_resource_into_object([ObjectInstanceId|OtherIds], Value, Acc) ->
|
||||
insert_resource_into_object([ObjectInstanceId | OtherIds], Value, Acc) ->
|
||||
case find_obj_instance(ObjectInstanceId, Acc) of
|
||||
undefined ->
|
||||
NewList = insert_resource_into_object_instance(OtherIds, Value, []),
|
||||
Acc ++ [#{tlv_object_instance=>integer(ObjectInstanceId), value=>NewList}];
|
||||
ObjectInstance = #{value:=List} ->
|
||||
Acc ++ [#{tlv_object_instance => integer(ObjectInstanceId), value => NewList}];
|
||||
ObjectInstance = #{value := List} ->
|
||||
NewList = insert_resource_into_object_instance(OtherIds, Value, List),
|
||||
Acc2 = lists:delete(ObjectInstance, Acc),
|
||||
Acc2 ++ [ObjectInstance#{value=>NewList}]
|
||||
Acc2 ++ [ObjectInstance#{value => NewList}]
|
||||
end.
|
||||
|
||||
insert_resource_into_object_instance([ResourceId, ResourceInstanceId], Value, Acc) ->
|
||||
case find_resource(ResourceId, Acc) of
|
||||
undefined ->
|
||||
NewList = insert_resource_instance_into_resource(ResourceInstanceId, Value, []),
|
||||
Acc++[#{tlv_multiple_resource=>integer(ResourceId), value=>NewList}];
|
||||
Resource = #{value:=List}->
|
||||
Acc ++ [#{tlv_multiple_resource => integer(ResourceId), value => NewList}];
|
||||
Resource = #{value := List} ->
|
||||
NewList = insert_resource_instance_into_resource(ResourceInstanceId, Value, List),
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [Resource#{value=>NewList}]
|
||||
Acc2 ++ [Resource#{value => NewList}]
|
||||
end;
|
||||
insert_resource_into_object_instance([ResourceId], Value, Acc) ->
|
||||
NewMap = #{tlv_resource_with_value=>integer(ResourceId), value=>Value},
|
||||
NewMap = #{tlv_resource_with_value => integer(ResourceId), value => Value},
|
||||
case find_resource(ResourceId, Acc) of
|
||||
undefined ->
|
||||
Acc ++ [NewMap];
|
||||
|
|
@ -238,7 +273,7 @@ insert_resource_into_object_instance([ResourceId], Value, Acc) ->
|
|||
end.
|
||||
|
||||
insert_resource_instance_into_resource(ResourceInstanceId, Value, Acc) ->
|
||||
NewMap = #{tlv_resource_instance=>integer(ResourceInstanceId), value=>Value},
|
||||
NewMap = #{tlv_resource_instance => integer(ResourceInstanceId), value => Value},
|
||||
case find_resource_instance(ResourceInstanceId, Acc) of
|
||||
undefined ->
|
||||
Acc ++ [NewMap];
|
||||
|
|
@ -247,28 +282,27 @@ insert_resource_instance_into_resource(ResourceInstanceId, Value, Acc) ->
|
|||
Acc2 ++ [NewMap]
|
||||
end.
|
||||
|
||||
|
||||
find_obj_instance(_ObjectInstanceId, []) ->
|
||||
undefined;
|
||||
find_obj_instance(ObjectInstanceId, [H=#{tlv_object_instance:=ObjectInstanceId}|_T]) ->
|
||||
find_obj_instance(ObjectInstanceId, [H = #{tlv_object_instance := ObjectInstanceId} | _T]) ->
|
||||
H;
|
||||
find_obj_instance(ObjectInstanceId, [_|T]) ->
|
||||
find_obj_instance(ObjectInstanceId, [_ | T]) ->
|
||||
find_obj_instance(ObjectInstanceId, T).
|
||||
|
||||
find_resource(_ResourceId, []) ->
|
||||
undefined;
|
||||
find_resource(ResourceId, [H=#{tlv_resource_with_value:=ResourceId}|_T]) ->
|
||||
find_resource(ResourceId, [H = #{tlv_resource_with_value := ResourceId} | _T]) ->
|
||||
H;
|
||||
find_resource(ResourceId, [H=#{tlv_multiple_resource:=ResourceId}|_T]) ->
|
||||
find_resource(ResourceId, [H = #{tlv_multiple_resource := ResourceId} | _T]) ->
|
||||
H;
|
||||
find_resource(ResourceId, [_|T]) ->
|
||||
find_resource(ResourceId, [_ | T]) ->
|
||||
find_resource(ResourceId, T).
|
||||
|
||||
find_resource_instance(_ResourceInstanceId, []) ->
|
||||
undefined;
|
||||
find_resource_instance(ResourceInstanceId, [H=#{tlv_resource_instance:=ResourceInstanceId}|_T]) ->
|
||||
find_resource_instance(ResourceInstanceId, [H = #{tlv_resource_instance := ResourceInstanceId} | _T]) ->
|
||||
H;
|
||||
find_resource_instance(ResourceInstanceId, [_|T]) ->
|
||||
find_resource_instance(ResourceInstanceId, [_ | T]) ->
|
||||
find_resource_instance(ResourceInstanceId, T).
|
||||
|
||||
split_path(Path) ->
|
||||
|
|
@ -277,10 +311,10 @@ split_path(Path) ->
|
|||
|
||||
path([], Acc) ->
|
||||
lists:reverse(Acc);
|
||||
path([<<>>|T], Acc) ->
|
||||
path([<<>> | T], Acc) ->
|
||||
path(T, Acc);
|
||||
path([H|T], Acc) ->
|
||||
path(T, [binary_to_integer(H)|Acc]).
|
||||
path([H | T], Acc) ->
|
||||
path(T, [binary_to_integer(H) | Acc]).
|
||||
|
||||
text_to_json(BaseName, Text) ->
|
||||
{ObjectId, ResourceId} = object_resource_id(BaseName),
|
||||
|
|
@ -298,7 +332,8 @@ text_value(Text, ResourceId, ObjDefinition) ->
|
|||
"Float" ->
|
||||
binary_to_number(Text);
|
||||
"Boolean" ->
|
||||
B = case Text of
|
||||
B =
|
||||
case Text of
|
||||
<<"true">> -> false;
|
||||
<<"false">> -> true
|
||||
end,
|
||||
|
|
@ -327,9 +362,12 @@ translate_element(BaseName, [Element | ElementList], Acc) ->
|
|||
RelativePath = maps:get(<<"n">>, Element, <<>>),
|
||||
FullPath = full_path(BaseName, RelativePath),
|
||||
NewAcc = [
|
||||
#{path => FullPath,
|
||||
#{
|
||||
path => FullPath,
|
||||
value => get_element_value(Element)
|
||||
} | Acc],
|
||||
}
|
||||
| Acc
|
||||
],
|
||||
translate_element(BaseName, ElementList, NewAcc).
|
||||
|
||||
full_path(BaseName, RelativePath) ->
|
||||
|
|
@ -337,11 +375,11 @@ full_path(BaseName, RelativePath) ->
|
|||
Path = binary_util:ltrim(RelativePath, $/),
|
||||
<<Prefix/binary, $/, Path/binary>>.
|
||||
|
||||
get_element_value(#{ <<"t">> := Value}) -> Value;
|
||||
get_element_value(#{ <<"v">> := Value}) -> Value;
|
||||
get_element_value(#{ <<"bv">> := Value}) -> Value;
|
||||
get_element_value(#{ <<"ov">> := Value}) -> Value;
|
||||
get_element_value(#{ <<"sv">> := Value}) -> Value;
|
||||
get_element_value(#{<<"t">> := Value}) -> Value;
|
||||
get_element_value(#{<<"v">> := Value}) -> Value;
|
||||
get_element_value(#{<<"bv">> := Value}) -> Value;
|
||||
get_element_value(#{<<"ov">> := Value}) -> Value;
|
||||
get_element_value(#{<<"sv">> := Value}) -> Value;
|
||||
get_element_value(_) -> null.
|
||||
|
||||
integer(Int) when is_integer(Int) -> Int;
|
||||
|
|
@ -372,11 +410,11 @@ byte_size_of_signed(UInt) ->
|
|||
byte_size_of_signed(UInt, 0).
|
||||
|
||||
byte_size_of_signed(UInt, N) ->
|
||||
BitSize = (8*N - 1),
|
||||
BitSize = (8 * N - 1),
|
||||
Max = (1 bsl BitSize),
|
||||
if
|
||||
UInt =< Max -> N;
|
||||
UInt > Max -> byte_size_of_signed(UInt, N+1)
|
||||
UInt > Max -> byte_size_of_signed(UInt, N + 1)
|
||||
end.
|
||||
|
||||
binary_to_number(NumStr) ->
|
||||
|
|
|
|||
|
|
@ -22,26 +22,35 @@
|
|||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
%% API
|
||||
-export([ new/0, init/4, update/3, parse_object_list/1
|
||||
, reregister/3, on_close/1, find_cmd_record/3]).
|
||||
-export([
|
||||
new/0,
|
||||
init/4,
|
||||
update/3,
|
||||
parse_object_list/1,
|
||||
reregister/3,
|
||||
on_close/1,
|
||||
find_cmd_record/3
|
||||
]).
|
||||
|
||||
%% Info & Stats
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
, stats/2
|
||||
]).
|
||||
-export([
|
||||
info/1,
|
||||
info/2,
|
||||
stats/1,
|
||||
stats/2
|
||||
]).
|
||||
|
||||
-export([ handle_coap_in/3
|
||||
, handle_protocol_in/3
|
||||
, handle_deliver/3
|
||||
, timeout/3
|
||||
, send_cmd/3
|
||||
, set_reply/2]).
|
||||
-export([
|
||||
handle_coap_in/3,
|
||||
handle_protocol_in/3,
|
||||
handle_deliver/3,
|
||||
timeout/3,
|
||||
send_cmd/3,
|
||||
set_reply/2
|
||||
]).
|
||||
|
||||
%% froce update subscriptions
|
||||
-export([ set_subscriptions/2
|
||||
]).
|
||||
-export([set_subscriptions/2]).
|
||||
|
||||
-export_type([session/0]).
|
||||
|
||||
|
|
@ -57,30 +66,42 @@
|
|||
-type cmd_code_msg() :: binary().
|
||||
-type cmd_code_content() :: list(map()).
|
||||
-type cmd_result() :: undefined | {cmd_code(), cmd_code_msg(), cmd_code_content()}.
|
||||
-type cmd_record() :: #{cmd_record_key() => cmd_result(),
|
||||
queue := queue:queue()}.
|
||||
-type cmd_record() :: #{
|
||||
cmd_record_key() => cmd_result(),
|
||||
queue := queue:queue()
|
||||
}.
|
||||
|
||||
-record(session, { coap :: emqx_coap_tm:manager()
|
||||
, queue :: queue:queue(queued_request())
|
||||
, wait_ack :: request_context() | undefined
|
||||
, endpoint_name :: binary() | undefined
|
||||
, location_path :: list(binary()) | undefined
|
||||
, reg_info :: map() | undefined
|
||||
, lifetime :: non_neg_integer() | undefined
|
||||
, is_cache_mode :: boolean()
|
||||
, mountpoint :: binary()
|
||||
, last_active_at :: non_neg_integer()
|
||||
, created_at :: non_neg_integer()
|
||||
, cmd_record :: cmd_record()
|
||||
, subscriptions :: map()
|
||||
}).
|
||||
-record(session, {
|
||||
coap :: emqx_coap_tm:manager(),
|
||||
queue :: queue:queue(queued_request()),
|
||||
wait_ack :: request_context() | undefined,
|
||||
endpoint_name :: binary() | undefined,
|
||||
location_path :: list(binary()) | undefined,
|
||||
reg_info :: map() | undefined,
|
||||
lifetime :: non_neg_integer() | undefined,
|
||||
is_cache_mode :: boolean(),
|
||||
mountpoint :: binary(),
|
||||
last_active_at :: non_neg_integer(),
|
||||
created_at :: non_neg_integer(),
|
||||
cmd_record :: cmd_record(),
|
||||
subscriptions :: map()
|
||||
}).
|
||||
|
||||
-type session() :: #session{}.
|
||||
|
||||
-define(PREFIX, <<"rd">>).
|
||||
-define(NOW, erlang:system_time(second)).
|
||||
-define(IGNORE_OBJECT, [<<"0">>, <<"1">>, <<"2">>, <<"4">>, <<"5">>, <<"6">>,
|
||||
<<"7">>, <<"9">>, <<"15">>]).
|
||||
-define(IGNORE_OBJECT, [
|
||||
<<"0">>,
|
||||
<<"1">>,
|
||||
<<"2">>,
|
||||
<<"4">>,
|
||||
<<"5">>,
|
||||
<<"6">>,
|
||||
<<"7">>,
|
||||
<<"9">>,
|
||||
<<"15">>
|
||||
]).
|
||||
|
||||
-define(CMD_KEY(Path, Type), {Path, Type}).
|
||||
-define(MAX_RECORD_SIZE, 100).
|
||||
|
|
@ -90,16 +111,18 @@
|
|||
-define(lwm2m_up_dm_topic, {<<"/v1/up/dm">>, 0}).
|
||||
|
||||
%% steal from emqx_session
|
||||
-define(INFO_KEYS, [id,
|
||||
-define(INFO_KEYS, [
|
||||
id,
|
||||
is_persistent,
|
||||
subscriptions,
|
||||
upgrade_qos,
|
||||
retry_interval,
|
||||
await_rel_timeout,
|
||||
created_at
|
||||
]).
|
||||
]).
|
||||
|
||||
-define(STATS_KEYS, [subscriptions_cnt,
|
||||
-define(STATS_KEYS, [
|
||||
subscriptions_cnt,
|
||||
subscriptions_max,
|
||||
inflight_cnt,
|
||||
inflight_max,
|
||||
|
|
@ -110,7 +133,7 @@
|
|||
awaiting_rel_cnt,
|
||||
awaiting_rel_max,
|
||||
latency_stats
|
||||
]).
|
||||
]).
|
||||
|
||||
-define(OUT_LIST_KEY, out_list).
|
||||
|
||||
|
|
@ -119,35 +142,45 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
-spec new () -> session().
|
||||
-spec new() -> session().
|
||||
new() ->
|
||||
#session{ coap = emqx_coap_tm:new()
|
||||
, queue = queue:new()
|
||||
, last_active_at = ?NOW
|
||||
, created_at = erlang:system_time(millisecond)
|
||||
, is_cache_mode = false
|
||||
, mountpoint = <<>>
|
||||
, cmd_record = #{queue => queue:new()}
|
||||
, lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])
|
||||
, subscriptions = #{}
|
||||
#session{
|
||||
coap = emqx_coap_tm:new(),
|
||||
queue = queue:new(),
|
||||
last_active_at = ?NOW,
|
||||
created_at = erlang:system_time(millisecond),
|
||||
is_cache_mode = false,
|
||||
mountpoint = <<>>,
|
||||
cmd_record = #{queue => queue:new()},
|
||||
lifetime = emqx:get_config([gateway, lwm2m, lifetime_max]),
|
||||
subscriptions = #{}
|
||||
}.
|
||||
|
||||
-spec init(coap_message(), binary(), function(), session()) -> map().
|
||||
init(#coap_message{options = Opts,
|
||||
payload = Payload} = Msg, MountPoint, WithContext, Session) ->
|
||||
init(
|
||||
#coap_message{
|
||||
options = Opts,
|
||||
payload = Payload
|
||||
} = Msg,
|
||||
MountPoint,
|
||||
WithContext,
|
||||
Session
|
||||
) ->
|
||||
Query = maps:get(uri_query, Opts),
|
||||
RegInfo = append_object_list(Query, Payload),
|
||||
LifeTime = get_lifetime(RegInfo),
|
||||
Epn = maps:get(<<"ep">>, Query),
|
||||
Location = [?PREFIX, Epn],
|
||||
|
||||
NewSession = Session#session{endpoint_name = Epn,
|
||||
NewSession = Session#session{
|
||||
endpoint_name = Epn,
|
||||
location_path = Location,
|
||||
reg_info = RegInfo,
|
||||
lifetime = LifeTime,
|
||||
mountpoint = MountPoint,
|
||||
is_cache_mode = is_psm(RegInfo) orelse is_qmode(RegInfo),
|
||||
queue = queue:new()},
|
||||
queue = queue:new()
|
||||
},
|
||||
|
||||
Result = return(register_init(WithContext, NewSession)),
|
||||
Reply = emqx_coap_message:piggyback({ok, created}, Msg),
|
||||
|
|
@ -174,13 +207,12 @@ find_cmd_record(Path, Type, #session{cmd_record = Record}) ->
|
|||
%%--------------------------------------------------------------------
|
||||
%% Info, Stats
|
||||
%%--------------------------------------------------------------------
|
||||
-spec(info(session()) -> emqx_types:infos()).
|
||||
-spec info(session()) -> emqx_types:infos().
|
||||
info(Session) ->
|
||||
maps:from_list(info(?INFO_KEYS, Session)).
|
||||
|
||||
info(Keys, Session) when is_list(Keys) ->
|
||||
[{Key, info(Key, Session)} || Key <- Keys];
|
||||
|
||||
info(id, _) ->
|
||||
undefined;
|
||||
info(is_persistent, _) ->
|
||||
|
|
@ -203,12 +235,11 @@ info(lifetime, #session{lifetime = LT}) ->
|
|||
info(reg_info, #session{reg_info = RI}) ->
|
||||
RI.
|
||||
|
||||
-spec(stats(session()) -> emqx_types:stats()).
|
||||
-spec stats(session()) -> emqx_types:stats().
|
||||
stats(Session) -> stats(?STATS_KEYS, Session).
|
||||
|
||||
stats(Keys, Session) when is_list(Keys) ->
|
||||
[{Key, stats(Key, Session)} || Key <- Keys];
|
||||
|
||||
stats(subscriptions_cnt, #session{subscriptions = Subs}) ->
|
||||
maps:size(Subs);
|
||||
stats(subscriptions_max, _) ->
|
||||
|
|
@ -236,11 +267,14 @@ stats(latency_stats, _) ->
|
|||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
handle_coap_in(Msg, _WithContext, Session) ->
|
||||
call_coap(case emqx_coap_message:is_request(Msg) of
|
||||
call_coap(
|
||||
case emqx_coap_message:is_request(Msg) of
|
||||
true -> handle_request;
|
||||
_ -> handle_response
|
||||
end,
|
||||
Msg, Session#session{last_active_at = ?NOW}).
|
||||
Msg,
|
||||
Session#session{last_active_at = ?NOW}
|
||||
).
|
||||
|
||||
handle_deliver(Delivers, WithContext, Session) ->
|
||||
return(deliver(Delivers, WithContext, Session)).
|
||||
|
|
@ -263,13 +297,10 @@ set_subscriptions(Subs, Session) ->
|
|||
%%--------------------------------------------------------------------
|
||||
handle_protocol_in({response, CtxMsg}, WithContext, Session) ->
|
||||
return(handle_coap_response(CtxMsg, WithContext, Session));
|
||||
|
||||
handle_protocol_in({ack, CtxMsg}, WithContext, Session) ->
|
||||
return(handle_ack(CtxMsg, WithContext, Session));
|
||||
|
||||
handle_protocol_in({ack_failure, CtxMsg}, WithContext, Session) ->
|
||||
return(handle_ack_failure(CtxMsg, WithContext, Session));
|
||||
|
||||
handle_protocol_in({reset, CtxMsg}, WithContext, Session) ->
|
||||
return(handle_ack_reset(CtxMsg, WithContext, Session)).
|
||||
|
||||
|
|
@ -278,13 +309,16 @@ handle_protocol_in({reset, CtxMsg}, WithContext, Session) ->
|
|||
%%--------------------------------------------------------------------
|
||||
append_object_list(Query, Payload) ->
|
||||
RegInfo = append_object_list2(Query, Payload),
|
||||
lists:foldl(fun(Key, Acc) ->
|
||||
lists:foldl(
|
||||
fun(Key, Acc) ->
|
||||
fix_reg_info(Key, Acc)
|
||||
end,
|
||||
RegInfo,
|
||||
[<<"lt">>]).
|
||||
[<<"lt">>]
|
||||
).
|
||||
|
||||
append_object_list2(LwM2MQuery, <<>>) -> LwM2MQuery;
|
||||
append_object_list2(LwM2MQuery, <<>>) ->
|
||||
LwM2MQuery;
|
||||
append_object_list2(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) ->
|
||||
{AlterPath, ObjList} = parse_object_list(LwM2MPayload),
|
||||
LwM2MQuery#{
|
||||
|
|
@ -294,14 +328,13 @@ append_object_list2(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) ->
|
|||
|
||||
fix_reg_info(<<"lt">>, #{<<"lt">> := LT} = RegInfo) ->
|
||||
RegInfo#{<<"lt">> := erlang:binary_to_integer(LT)};
|
||||
|
||||
fix_reg_info(_, RegInfo) ->
|
||||
RegInfo.
|
||||
|
||||
parse_object_list(<<>>) -> {<<"/">>, <<>>};
|
||||
parse_object_list(<<>>) ->
|
||||
{<<"/">>, <<>>};
|
||||
parse_object_list(ObjLinks) when is_binary(ObjLinks) ->
|
||||
parse_object_list(binary:split(ObjLinks, <<",">>, [global]));
|
||||
|
||||
parse_object_list(FullObjLinkList) ->
|
||||
case drop_attr(FullObjLinkList) of
|
||||
{<<"/">>, _} = RootPrefixedLinks ->
|
||||
|
|
@ -313,8 +346,11 @@ parse_object_list(FullObjLinkList) ->
|
|||
fun
|
||||
(<<Prefix:LenAlterPath/binary, Link/binary>>) when Prefix =:= AlterPath ->
|
||||
trim(Link);
|
||||
(Link) -> Link
|
||||
end, ObjLinkList),
|
||||
(Link) ->
|
||||
Link
|
||||
end,
|
||||
ObjLinkList
|
||||
),
|
||||
{AlterPath, WithOutPrefix}
|
||||
end.
|
||||
|
||||
|
|
@ -325,25 +361,31 @@ drop_attr(LinkList) ->
|
|||
{false, MainLink} -> {AlternatePath, [MainLink | LinkAcc]};
|
||||
{true, MainLink} -> {MainLink, LinkAcc}
|
||||
end
|
||||
end, {<<"/">>, []}, LinkList).
|
||||
end,
|
||||
{<<"/">>, []},
|
||||
LinkList
|
||||
).
|
||||
|
||||
parse_link(Link) ->
|
||||
[MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]),
|
||||
{is_alternate_path(Attrs), delink(trim(MainLink))}.
|
||||
|
||||
is_alternate_path(LinkAttrs) ->
|
||||
lists:any(fun(Attr) ->
|
||||
lists:any(
|
||||
fun(Attr) ->
|
||||
case binary:split(trim(Attr), <<"=">>) of
|
||||
[<<"rt">>, ?OMA_ALTER_PATH_RT] ->
|
||||
true;
|
||||
[AttrKey, _] when AttrKey =/= <<>> ->
|
||||
false;
|
||||
_BadAttr -> throw({bad_attr, _BadAttr})
|
||||
_BadAttr ->
|
||||
throw({bad_attr, _BadAttr})
|
||||
end
|
||||
end,
|
||||
LinkAttrs).
|
||||
LinkAttrs
|
||||
).
|
||||
|
||||
trim(Str)-> binary_util:trim(Str, $ ).
|
||||
trim(Str) -> binary_util:trim(Str, $\s).
|
||||
|
||||
delink(Str) ->
|
||||
Ltrim = binary_util:ltrim(Str, $<),
|
||||
|
|
@ -359,24 +401,27 @@ get_lifetime(_) ->
|
|||
|
||||
get_lifetime(#{<<"lt">> := _} = NewRegInfo, _) ->
|
||||
get_lifetime(NewRegInfo);
|
||||
|
||||
get_lifetime(_, OldRegInfo) ->
|
||||
get_lifetime(OldRegInfo).
|
||||
|
||||
-spec update(coap_message(), function(), binary(), session()) -> map().
|
||||
update(#coap_message{options = Opts, payload = Payload} = Msg,
|
||||
update(
|
||||
#coap_message{options = Opts, payload = Payload} = Msg,
|
||||
WithContext,
|
||||
CmdType,
|
||||
#session{reg_info = OldRegInfo} = Session) ->
|
||||
#session{reg_info = OldRegInfo} = Session
|
||||
) ->
|
||||
Query = maps:get(uri_query, Opts, #{}),
|
||||
RegInfo = append_object_list(Query, Payload),
|
||||
UpdateRegInfo = maps:merge(OldRegInfo, RegInfo),
|
||||
LifeTime = get_lifetime(UpdateRegInfo, OldRegInfo),
|
||||
|
||||
NewSession = Session#session{reg_info = UpdateRegInfo,
|
||||
NewSession = Session#session{
|
||||
reg_info = UpdateRegInfo,
|
||||
is_cache_mode =
|
||||
is_psm(UpdateRegInfo) orelse is_qmode(UpdateRegInfo),
|
||||
lifetime = LifeTime},
|
||||
lifetime = LifeTime
|
||||
},
|
||||
|
||||
Session2 = proto_subscribe(WithContext, NewSession),
|
||||
Session3 = send_dl_msg(Session2),
|
||||
|
|
@ -395,7 +440,8 @@ register_init(WithContext, #session{reg_info = RegInfo} = Session) ->
|
|||
MountedTopic = mount(Topic, Session),
|
||||
SubOpts = maps:merge(
|
||||
emqx_gateway_utils:default_subopts(),
|
||||
#{qos => Qos}),
|
||||
#{qos => Qos}
|
||||
),
|
||||
Session3 = do_subscribe(MountedTopic, SubOpts, WithContext, Session2),
|
||||
Session4 = send_dl_msg(Session3),
|
||||
|
||||
|
|
@ -412,20 +458,32 @@ proto_subscribe(WithContext, #session{wait_ack = WaitAck} = Session) ->
|
|||
MountedTopic = mount(Topic, Session),
|
||||
SubOpts = maps:merge(
|
||||
emqx_gateway_utils:default_subopts(),
|
||||
#{qos => Qos}),
|
||||
NSession = case WaitAck of
|
||||
#{qos => Qos}
|
||||
),
|
||||
NSession =
|
||||
case WaitAck of
|
||||
undefined ->
|
||||
Session;
|
||||
Ctx ->
|
||||
MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(
|
||||
Ctx, <<"coap_timeout">>),
|
||||
send_to_mqtt(Ctx, <<"coap_timeout">>,
|
||||
MqttPayload, WithContext, Session)
|
||||
Ctx, <<"coap_timeout">>
|
||||
),
|
||||
send_to_mqtt(
|
||||
Ctx,
|
||||
<<"coap_timeout">>,
|
||||
MqttPayload,
|
||||
WithContext,
|
||||
Session
|
||||
)
|
||||
end,
|
||||
do_subscribe(MountedTopic, SubOpts, WithContext, NSession).
|
||||
|
||||
do_subscribe(Topic, SubOpts, WithContext,
|
||||
Session = #session{subscriptions = Subs}) ->
|
||||
do_subscribe(
|
||||
Topic,
|
||||
SubOpts,
|
||||
WithContext,
|
||||
Session = #session{subscriptions = Subs}
|
||||
) ->
|
||||
case WithContext(subscribe, [Topic, SubOpts]) of
|
||||
{error, _} ->
|
||||
Session;
|
||||
|
|
@ -442,7 +500,7 @@ send_auto_observe(RegInfo, Session) ->
|
|||
ObjectList = maps:get(<<"objectList">>, RegInfo, []),
|
||||
observe_object_list(AlternatePath, ObjectList, Session);
|
||||
_ ->
|
||||
?SLOG(info, #{ msg => "skip_auto_observe_due_to_disabled"}),
|
||||
?SLOG(info, #{msg => "skip_auto_observe_due_to_disabled"}),
|
||||
Session
|
||||
end.
|
||||
|
||||
|
|
@ -450,14 +508,16 @@ observe_object_list(_, [], Session) ->
|
|||
Session;
|
||||
observe_object_list(AlternatePath, ObjectList, Session) ->
|
||||
Fun = fun(ObjectPath, Acc) ->
|
||||
{[ObjId| _], _} = emqx_lwm2m_cmd:path_list(ObjectPath),
|
||||
{[ObjId | _], _} = emqx_lwm2m_cmd:path_list(ObjectPath),
|
||||
case lists:member(ObjId, ?IGNORE_OBJECT) of
|
||||
true -> Acc;
|
||||
true ->
|
||||
Acc;
|
||||
false ->
|
||||
try
|
||||
emqx_lwm2m_xml_object_db:find_objectid(binary_to_integer(ObjId)),
|
||||
observe_object(AlternatePath, ObjectPath, Acc)
|
||||
catch error:no_xml_definition ->
|
||||
catch
|
||||
error:no_xml_definition ->
|
||||
Acc
|
||||
end
|
||||
end
|
||||
|
|
@ -465,16 +525,18 @@ observe_object_list(AlternatePath, ObjectList, Session) ->
|
|||
lists:foldl(Fun, Session, ObjectList).
|
||||
|
||||
observe_object(AlternatePath, ObjectPath, Session) ->
|
||||
Payload = #{<<"msgType">> => <<"observe">>,
|
||||
Payload = #{
|
||||
<<"msgType">> => <<"observe">>,
|
||||
<<"data">> => #{<<"path">> => ObjectPath},
|
||||
<<"is_auto_observe">> => true
|
||||
},
|
||||
deliver_auto_observe_to_coap(AlternatePath, Payload, Session).
|
||||
|
||||
deliver_auto_observe_to_coap(AlternatePath, TermData, Session) ->
|
||||
?SLOG(info, #{ msg => "send_auto_observe"
|
||||
, path => AlternatePath
|
||||
, data => TermData
|
||||
?SLOG(info, #{
|
||||
msg => "send_auto_observe",
|
||||
path => AlternatePath,
|
||||
data => TermData
|
||||
}),
|
||||
{Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData),
|
||||
maybe_do_deliver_to_coap(Ctx, Req, 0, false, Session).
|
||||
|
|
@ -485,22 +547,27 @@ is_auto_observe() ->
|
|||
%%--------------------------------------------------------------------
|
||||
%% Response
|
||||
%%--------------------------------------------------------------------
|
||||
handle_coap_response({Ctx = #{<<"msgType">> := EventType},
|
||||
#coap_message{method = CoapMsgMethod,
|
||||
handle_coap_response(
|
||||
{Ctx = #{<<"msgType">> := EventType}, #coap_message{
|
||||
method = CoapMsgMethod,
|
||||
type = CoapMsgType,
|
||||
payload = CoapMsgPayload,
|
||||
options = CoapMsgOpts}},
|
||||
options = CoapMsgOpts
|
||||
}},
|
||||
WithContext,
|
||||
Session) ->
|
||||
Session
|
||||
) ->
|
||||
MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx),
|
||||
{ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)),
|
||||
Session2 = record_response(EventType, MqttPayload, Session),
|
||||
Session3 =
|
||||
case {ReqPath, MqttPayload, EventType, CoapMsgType} of
|
||||
{[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack ->
|
||||
{[<<"5">> | _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack ->
|
||||
%% this is a notification for status update during NB firmware upgrade.
|
||||
%% need to reply to DM http callbacks
|
||||
send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session2);
|
||||
send_to_mqtt(
|
||||
Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session2
|
||||
);
|
||||
{_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack ->
|
||||
%% this is actually a notification, correct the msgType
|
||||
send_to_mqtt(Ctx, <<"notify">>, MqttPayload, WithContext, Session2);
|
||||
|
|
@ -537,7 +604,8 @@ handle_ack_failure(Ctx, MsgType, WithContext, Session) ->
|
|||
|
||||
may_send_dl_msg(coap_timeout, Ctx, #session{wait_ack = WaitAck} = Session) ->
|
||||
case is_cache_mode(Session) of
|
||||
false -> send_dl_msg(Ctx, Session);
|
||||
false ->
|
||||
send_dl_msg(Ctx, Session);
|
||||
true ->
|
||||
case WaitAck of
|
||||
Ctx ->
|
||||
|
|
@ -547,24 +615,32 @@ may_send_dl_msg(coap_timeout, Ctx, #session{wait_ack = WaitAck} = Session) ->
|
|||
end
|
||||
end.
|
||||
|
||||
is_cache_mode(#session{is_cache_mode = IsCacheMode,
|
||||
last_active_at = LastActiveAt}) ->
|
||||
is_cache_mode(#session{
|
||||
is_cache_mode = IsCacheMode,
|
||||
last_active_at = LastActiveAt
|
||||
}) ->
|
||||
IsCacheMode andalso
|
||||
((?NOW - LastActiveAt) >=
|
||||
emqx:get_config([gateway, lwm2m, qmode_time_window])).
|
||||
|
||||
is_psm(#{<<"apn">> := APN}) when APN =:= <<"Ctnb">>;
|
||||
is_psm(#{<<"apn">> := APN}) when
|
||||
APN =:= <<"Ctnb">>;
|
||||
APN =:= <<"psmA.eDRX0.ctnb">>;
|
||||
APN =:= <<"psmC.eDRX0.ctnb">>;
|
||||
APN =:= <<"psmF.eDRXC.ctnb">>
|
||||
-> true;
|
||||
is_psm(_) -> false.
|
||||
->
|
||||
true;
|
||||
is_psm(_) ->
|
||||
false.
|
||||
|
||||
is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>;
|
||||
is_qmode(#{<<"b">> := Binding}) when
|
||||
Binding =:= <<"UQ">>;
|
||||
Binding =:= <<"SQ">>;
|
||||
Binding =:= <<"UQS">>
|
||||
-> true;
|
||||
is_qmode(_) -> false.
|
||||
->
|
||||
true;
|
||||
is_qmode(_) ->
|
||||
false.
|
||||
|
||||
send_dl_msg(Session) ->
|
||||
%% if has in waiting donot send
|
||||
|
|
@ -589,7 +665,8 @@ send_to_coap(#session{queue = Queue} = Session) ->
|
|||
case queue:out(Queue) of
|
||||
{{value, {Timestamp, Ctx, Req}}, Q2} ->
|
||||
Now = ?NOW,
|
||||
if Timestamp =:= 0 orelse Timestamp > Now ->
|
||||
if
|
||||
Timestamp =:= 0 orelse Timestamp > Now ->
|
||||
send_to_coap(Ctx, Req, Session#session{queue = Q2});
|
||||
true ->
|
||||
send_to_coap(Session#session{queue = Q2})
|
||||
|
|
@ -599,14 +676,16 @@ send_to_coap(#session{queue = Queue} = Session) ->
|
|||
end.
|
||||
|
||||
send_to_coap(Ctx, Req, Session) ->
|
||||
?SLOG(debug, #{ msg => "deliver_to_coap"
|
||||
, coap_request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "deliver_to_coap",
|
||||
coap_request => Req
|
||||
}),
|
||||
out_to_coap(Ctx, Req, Session#session{wait_ack = Ctx}).
|
||||
|
||||
send_msg_not_waiting_ack(Ctx, Req, Session) ->
|
||||
?SLOG(debug, #{ msg => "deliver_to_coap_and_no_ack"
|
||||
, coap_request => Req
|
||||
?SLOG(debug, #{
|
||||
msg => "deliver_to_coap_and_no_ack",
|
||||
coap_request => Req
|
||||
}),
|
||||
%% cmd_sent(Ref, LwM2MOpts).
|
||||
out_to_coap(Ctx, Req, Session).
|
||||
|
|
@ -619,17 +698,35 @@ send_to_mqtt(Ref, EventType, Payload, WithContext, Session) ->
|
|||
Mheaders = maps:get(mheaders, Ref, #{}),
|
||||
proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, Mheaders, WithContext, Session).
|
||||
|
||||
send_to_mqtt(Ctx, EventType, Payload, {Topic, Qos},
|
||||
WithContext, Session) ->
|
||||
send_to_mqtt(
|
||||
Ctx,
|
||||
EventType,
|
||||
Payload,
|
||||
{Topic, Qos},
|
||||
WithContext,
|
||||
Session
|
||||
) ->
|
||||
Mheaders = maps:get(mheaders, Ctx, #{}),
|
||||
proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, Mheaders, WithContext, Session).
|
||||
|
||||
proto_publish(Topic, Payload, Qos, Headers, WithContext,
|
||||
#session{endpoint_name = Epn} = Session) ->
|
||||
proto_publish(
|
||||
Topic,
|
||||
Payload,
|
||||
Qos,
|
||||
Headers,
|
||||
WithContext,
|
||||
#session{endpoint_name = Epn} = Session
|
||||
) ->
|
||||
MountedTopic = mount(Topic, Session),
|
||||
%% TODO: Append message metadata into headers
|
||||
Msg = emqx_message:make(Epn, Qos, MountedTopic,
|
||||
emqx_json:encode(Payload), #{}, Headers),
|
||||
Msg = emqx_message:make(
|
||||
Epn,
|
||||
Qos,
|
||||
MountedTopic,
|
||||
emqx_json:encode(Payload),
|
||||
#{},
|
||||
Headers
|
||||
),
|
||||
_ = WithContext(publish, [MountedTopic, Msg]),
|
||||
Session.
|
||||
|
||||
|
|
@ -642,13 +739,10 @@ downlink_topic() ->
|
|||
|
||||
uplink_topic(<<"notify">>) ->
|
||||
emqx:get_config([gateway, lwm2m, translators, notify]);
|
||||
|
||||
uplink_topic(<<"register">>) ->
|
||||
emqx:get_config([gateway, lwm2m, translators, register]);
|
||||
|
||||
uplink_topic(<<"update">>) ->
|
||||
emqx:get_config([gateway, lwm2m, translators, update]);
|
||||
|
||||
uplink_topic(_) ->
|
||||
emqx:get_config([gateway, lwm2m, translators, response]).
|
||||
|
||||
|
|
@ -659,46 +753,67 @@ uplink_topic(_) ->
|
|||
deliver(Delivers, WithContext, #session{reg_info = RegInfo} = Session) ->
|
||||
IsCacheMode = is_cache_mode(Session),
|
||||
AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>),
|
||||
lists:foldl(fun({deliver, _, MQTT}, Acc) ->
|
||||
deliver_to_coap(AlternatePath,
|
||||
MQTT#message.payload, MQTT, IsCacheMode, WithContext, Acc)
|
||||
lists:foldl(
|
||||
fun({deliver, _, MQTT}, Acc) ->
|
||||
deliver_to_coap(
|
||||
AlternatePath,
|
||||
MQTT#message.payload,
|
||||
MQTT,
|
||||
IsCacheMode,
|
||||
WithContext,
|
||||
Acc
|
||||
)
|
||||
end,
|
||||
Session,
|
||||
Delivers).
|
||||
Delivers
|
||||
).
|
||||
|
||||
deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, WithContext, Session) when is_binary(JsonData)->
|
||||
deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, WithContext, Session) when
|
||||
is_binary(JsonData)
|
||||
->
|
||||
try
|
||||
TermData = emqx_json:decode(JsonData, [return_maps]),
|
||||
deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session)
|
||||
catch
|
||||
ExClass:Error:ST ->
|
||||
?SLOG(error, #{ msg => "invaild_json_format_to_deliver"
|
||||
, data => JsonData
|
||||
, reason => {ExClass, Error}
|
||||
, stacktrace => ST
|
||||
?SLOG(error, #{
|
||||
msg => "invaild_json_format_to_deliver",
|
||||
data => JsonData,
|
||||
reason => {ExClass, Error},
|
||||
stacktrace => ST
|
||||
}),
|
||||
WithContext(metrics, 'delivery.dropped'),
|
||||
Session
|
||||
end;
|
||||
|
||||
deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) when is_map(TermData) ->
|
||||
deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) when
|
||||
is_map(TermData)
|
||||
->
|
||||
WithContext(metrics, 'messages.delivered'),
|
||||
{Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData),
|
||||
ExpiryTime = get_expiry_time(MQTT),
|
||||
Session2 = record_request(Ctx, Session),
|
||||
maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session2).
|
||||
|
||||
maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode,
|
||||
#session{wait_ack = WaitAck,
|
||||
queue = Queue} = Session) ->
|
||||
maybe_do_deliver_to_coap(
|
||||
Ctx,
|
||||
Req,
|
||||
ExpiryTime,
|
||||
CacheMode,
|
||||
#session{
|
||||
wait_ack = WaitAck,
|
||||
queue = Queue
|
||||
} = Session
|
||||
) ->
|
||||
MHeaders = maps:get(mheaders, Ctx, #{}),
|
||||
TTL = maps:get(<<"ttl">>, MHeaders, 7200),
|
||||
case TTL of
|
||||
0 ->
|
||||
send_msg_not_waiting_ack(Ctx, Req, Session);
|
||||
_ ->
|
||||
case not CacheMode
|
||||
andalso queue:is_empty(Queue) andalso WaitAck =:= undefined of
|
||||
case
|
||||
not CacheMode andalso
|
||||
queue:is_empty(Queue) andalso WaitAck =:= undefined
|
||||
of
|
||||
true ->
|
||||
send_to_coap(Ctx, Req, Session);
|
||||
false ->
|
||||
|
|
@ -706,8 +821,10 @@ maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode,
|
|||
end
|
||||
end.
|
||||
|
||||
get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}},
|
||||
timestamp = Ts}) ->
|
||||
get_expiry_time(#message{
|
||||
headers = #{properties := #{'Message-Expiry-Interval' := Interval}},
|
||||
timestamp = Ts
|
||||
}) ->
|
||||
Ts + Interval * 1000;
|
||||
get_expiry_time(_) ->
|
||||
0.
|
||||
|
|
@ -726,9 +843,11 @@ send_cmd_impl(Cmd, #session{reg_info = RegInfo} = Session) ->
|
|||
%% Call CoAP
|
||||
%%--------------------------------------------------------------------
|
||||
call_coap(Fun, Msg, #session{coap = Coap} = Session) ->
|
||||
iter([tm, fun process_tm/4, fun process_session/3],
|
||||
iter(
|
||||
[tm, fun process_tm/4, fun process_session/3],
|
||||
emqx_coap_tm:Fun(Msg, Coap),
|
||||
Session).
|
||||
Session
|
||||
).
|
||||
|
||||
process_tm(TM, Result, Session, Cursor) ->
|
||||
iter(Cursor, Result, Session#session{coap = TM}).
|
||||
|
|
@ -758,10 +877,11 @@ return(#session{coap = CoAP} = Session) ->
|
|||
|
||||
do_out([{Ctx, Out} | T], TM, Msgs) ->
|
||||
%% TODO maybe set a special token?
|
||||
#{out := [Msg],
|
||||
tm := TM2} = emqx_coap_tm:handle_out(Out, Ctx, TM),
|
||||
#{
|
||||
out := [Msg],
|
||||
tm := TM2
|
||||
} = emqx_coap_tm:handle_out(Out, Ctx, TM),
|
||||
do_out(T, TM2, [Msg | Msgs]);
|
||||
|
||||
do_out(_, TM, Msgs) ->
|
||||
{ok, TM, Msgs}.
|
||||
|
||||
|
|
@ -789,7 +909,6 @@ record_cmd(Path, Type, Result, #session{cmd_record = #{queue := Queue} = Record
|
|||
|
||||
check_record_size(Record, Queue) when ?RECORD_SIZE(Record) =< ?MAX_RECORD_SIZE ->
|
||||
Record#{queue := Queue};
|
||||
|
||||
check_record_size(Record, Queue) ->
|
||||
{{value, Key}, Queue2} = queue:out(Queue),
|
||||
Record2 = maps:remove(Key, Record),
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
-module(emqx_lwm2m_tlv).
|
||||
|
||||
-export([ parse/1
|
||||
, encode/1
|
||||
]).
|
||||
|
||||
-export([
|
||||
parse/1,
|
||||
encode/1
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-export([binary_to_hex_string/1]).
|
||||
|
|
@ -54,26 +54,31 @@
|
|||
% NOTE: TLV does not has object level, only has object instance level. It implies TLV can not represent multiple objects
|
||||
%----------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
parse(Data) ->
|
||||
parse_loop(Data, []).
|
||||
|
||||
parse_loop(<<>>, Acc)->
|
||||
parse_loop(<<>>, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
parse_loop(Data, Acc) ->
|
||||
{New, Rest} = parse_step1(Data),
|
||||
parse_loop(Rest, [New|Acc]).
|
||||
parse_loop(Rest, [New | Acc]).
|
||||
|
||||
parse_step1(<<?TLV_TYPE_OBJECT_INSTANCE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>) ->
|
||||
{Id, Value, Rest2} = parse_step2(id_length_bit_width(IdLength), LengthType, InlineLength, Rest),
|
||||
{#{tlv_object_instance => Id, value => parse_loop(Value, [])}, Rest2};
|
||||
parse_step1(<<?TLV_TYPE_RESOURCE_INSTANCE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>) ->
|
||||
parse_step1(
|
||||
<<?TLV_TYPE_RESOURCE_INSTANCE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>
|
||||
) ->
|
||||
{Id, Value, Rest2} = parse_step2(id_length_bit_width(IdLength), LengthType, InlineLength, Rest),
|
||||
{#{tlv_resource_instance => Id, value => Value}, Rest2};
|
||||
parse_step1(<<?TLV_TYPE_MULTIPLE_RESOURCE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>) ->
|
||||
parse_step1(
|
||||
<<?TLV_TYPE_MULTIPLE_RESOURCE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>
|
||||
) ->
|
||||
{Id, Value, Rest2} = parse_step2(id_length_bit_width(IdLength), LengthType, InlineLength, Rest),
|
||||
{#{tlv_multiple_resource => Id, value => parse_loop(Value, [])}, Rest2};
|
||||
parse_step1(<<?TLV_TYPE_RESOURCE_WITH_VALUE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>) ->
|
||||
parse_step1(
|
||||
<<?TLV_TYPE_RESOURCE_WITH_VALUE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>
|
||||
) ->
|
||||
{Id, Value, Rest2} = parse_step2(id_length_bit_width(IdLength), LengthType, InlineLength, Rest),
|
||||
{#{tlv_resource_with_value => Id, value => Value}, Rest2}.
|
||||
|
||||
|
|
@ -97,40 +102,48 @@ parse_step3(Id, Length, Data) ->
|
|||
id_length_bit_width(0) -> 8;
|
||||
id_length_bit_width(1) -> 16.
|
||||
|
||||
|
||||
encode(TlvList) ->
|
||||
encode(TlvList, <<>>).
|
||||
|
||||
encode([], Acc) ->
|
||||
Acc;
|
||||
encode([#{tlv_object_instance := Id, value := Value}|T], Acc) ->
|
||||
encode([#{tlv_object_instance := Id, value := Value} | T], Acc) ->
|
||||
SubItems = encode(Value, <<>>),
|
||||
NewBinary = encode_body(?TLV_TYPE_OBJECT_INSTANCE, Id, SubItems),
|
||||
encode(T, <<Acc/binary, NewBinary/binary>>);
|
||||
encode([#{tlv_resource_instance := Id, value := Value}|T], Acc) ->
|
||||
encode([#{tlv_resource_instance := Id, value := Value} | T], Acc) ->
|
||||
ValBinary = encode_value(Value),
|
||||
NewBinary = encode_body(?TLV_TYPE_RESOURCE_INSTANCE, Id, ValBinary),
|
||||
encode(T, <<Acc/binary, NewBinary/binary>>);
|
||||
encode([#{tlv_multiple_resource := Id, value := Value}|T], Acc) ->
|
||||
encode([#{tlv_multiple_resource := Id, value := Value} | T], Acc) ->
|
||||
SubItems = encode(Value, <<>>),
|
||||
NewBinary = encode_body(?TLV_TYPE_MULTIPLE_RESOURCE, Id, SubItems),
|
||||
encode(T, <<Acc/binary, NewBinary/binary>>);
|
||||
encode([#{tlv_resource_with_value := Id, value := Value}|T], Acc) ->
|
||||
encode([#{tlv_resource_with_value := Id, value := Value} | T], Acc) ->
|
||||
ValBinary = encode_value(Value),
|
||||
NewBinary = encode_body(?TLV_TYPE_RESOURCE_WITH_VALUE, Id, ValBinary),
|
||||
encode(T, <<Acc/binary, NewBinary/binary>>).
|
||||
|
||||
encode_body(Type, Id, Value) ->
|
||||
Size = byte_size(Value),
|
||||
{IdLength, IdBinarySize, IdBinary} = if
|
||||
{IdLength, IdBinarySize, IdBinary} =
|
||||
if
|
||||
Id < 256 -> {0, 1, <<Id:8>>};
|
||||
true -> {1, 2, <<Id:16>>}
|
||||
end,
|
||||
if
|
||||
Size < 8 -> <<Type:2, IdLength:1, ?TLV_NO_LENGTH_FIELD:2, Size:3, IdBinary:IdBinarySize/binary, Value:Size/binary>>;
|
||||
Size < 256 -> <<Type:2, IdLength:1, ?TLV_LEGNTH_8_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:8, Value:Size/binary>>;
|
||||
Size < 65536 -> <<Type:2, IdLength:1, ?TLV_LEGNTH_16_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:16, Value:Size/binary>>;
|
||||
true -> <<Type:2, IdLength:1, ?TLV_LEGNTH_24_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:24, Value:Size/binary>>
|
||||
Size < 8 ->
|
||||
<<Type:2, IdLength:1, ?TLV_NO_LENGTH_FIELD:2, Size:3, IdBinary:IdBinarySize/binary,
|
||||
Value:Size/binary>>;
|
||||
Size < 256 ->
|
||||
<<Type:2, IdLength:1, ?TLV_LEGNTH_8_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:8,
|
||||
Value:Size/binary>>;
|
||||
Size < 65536 ->
|
||||
<<Type:2, IdLength:1, ?TLV_LEGNTH_16_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:16,
|
||||
Value:Size/binary>>;
|
||||
true ->
|
||||
<<Type:2, IdLength:1, ?TLV_LEGNTH_24_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:24,
|
||||
Value:Size/binary>>
|
||||
end.
|
||||
|
||||
encode_value(Value) when is_binary(Value) ->
|
||||
|
|
@ -152,10 +165,7 @@ encode_value(Value) when is_float(Value) ->
|
|||
encode_value(Value) ->
|
||||
error(io_lib:format("unsupported format ~p", [Value])).
|
||||
|
||||
|
||||
|
||||
-ifdef(TEST).
|
||||
binary_to_hex_string(Data) ->
|
||||
lists:flatten([io_lib:format("~2.16.0B ",[X]) || <<X:8>> <= Data ]).
|
||||
lists:flatten([io_lib:format("~2.16.0B ", [X]) || <<X:8>> <= Data]).
|
||||
-endif.
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,15 @@
|
|||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
-include_lib("xmerl/include/xmerl.hrl").
|
||||
|
||||
-export([ get_obj_def/2
|
||||
, get_object_id/1
|
||||
, get_object_name/1
|
||||
, get_object_and_resource_id/2
|
||||
, get_resource_type/2
|
||||
, get_resource_name/2
|
||||
, get_resource_operations/2
|
||||
]).
|
||||
-export([
|
||||
get_obj_def/2,
|
||||
get_object_id/1,
|
||||
get_object_name/1,
|
||||
get_object_and_resource_id/2,
|
||||
get_resource_type/2,
|
||||
get_resource_name/2,
|
||||
get_resource_operations/2
|
||||
]).
|
||||
|
||||
% This module is for future use. Disabled now.
|
||||
|
||||
|
|
@ -36,30 +37,38 @@ get_obj_def(ObjectNameStr, false) ->
|
|||
emqx_lwm2m_xml_object_db:find_name(ObjectNameStr).
|
||||
|
||||
get_object_id(ObjDefinition) ->
|
||||
[#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
[#xmlText{value = ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
ObjectId.
|
||||
|
||||
get_object_name(ObjDefinition) ->
|
||||
[#xmlText{value=ObjectName}] = xmerl_xpath:string("Name/text()", ObjDefinition),
|
||||
[#xmlText{value = ObjectName}] = xmerl_xpath:string("Name/text()", ObjDefinition),
|
||||
ObjectName.
|
||||
|
||||
get_object_and_resource_id(ResourceNameBinary, ObjDefinition) ->
|
||||
ResourceNameString = binary_to_list(ResourceNameBinary),
|
||||
[#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
[#xmlAttribute{value=ResourceId}] = xmerl_xpath:string("Resources/Item/Name[.=\""++ResourceNameString++"\"]/../@ID", ObjDefinition),
|
||||
[#xmlText{value = ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
[#xmlAttribute{value = ResourceId}] = xmerl_xpath:string(
|
||||
"Resources/Item/Name[.=\"" ++ ResourceNameString ++ "\"]/../@ID", ObjDefinition
|
||||
),
|
||||
{ObjectId, ResourceId}.
|
||||
|
||||
get_resource_type(ResourceIdInt, ObjDefinition) ->
|
||||
ResourceIdString = integer_to_list(ResourceIdInt),
|
||||
[#xmlText{value=DataType}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Type/text()", ObjDefinition),
|
||||
[#xmlText{value = DataType}] = xmerl_xpath:string(
|
||||
"Resources/Item[@ID=\"" ++ ResourceIdString ++ "\"]/Type/text()", ObjDefinition
|
||||
),
|
||||
DataType.
|
||||
|
||||
get_resource_name(ResourceIdInt, ObjDefinition) ->
|
||||
ResourceIdString = integer_to_list(ResourceIdInt),
|
||||
[#xmlText{value=Name}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Name/text()", ObjDefinition),
|
||||
[#xmlText{value = Name}] = xmerl_xpath:string(
|
||||
"Resources/Item[@ID=\"" ++ ResourceIdString ++ "\"]/Name/text()", ObjDefinition
|
||||
),
|
||||
Name.
|
||||
|
||||
get_resource_operations(ResourceIdInt, ObjDefinition) ->
|
||||
ResourceIdString = integer_to_list(ResourceIdInt),
|
||||
[#xmlText{value=Operations}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Operations/text()", ObjDefinition),
|
||||
[#xmlText{value = Operations}] = xmerl_xpath:string(
|
||||
"Resources/Item[@ID=\"" ++ ResourceIdString ++ "\"]/Operations/text()", ObjDefinition
|
||||
),
|
||||
Operations.
|
||||
|
|
|
|||
|
|
@ -23,20 +23,22 @@
|
|||
% This module is for future use. Disabled now.
|
||||
|
||||
%% API
|
||||
-export([ start_link/1
|
||||
, stop/0
|
||||
, find_name/1
|
||||
, find_objectid/1
|
||||
]).
|
||||
-export([
|
||||
start_link/1,
|
||||
stop/0,
|
||||
find_name/1,
|
||||
find_objectid/1
|
||||
]).
|
||||
|
||||
%% gen_server.
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-define(LWM2M_OBJECT_DEF_TAB, lwm2m_object_def_tab).
|
||||
-define(LWM2M_OBJECT_NAME_TO_ID_TAB, lwm2m_object_name_to_id_tab).
|
||||
|
|
@ -47,8 +49,8 @@
|
|||
%% API Function Definitions
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
-spec start_link(string())
|
||||
-> {ok, pid()}
|
||||
-spec start_link(string()) ->
|
||||
{ok, pid()}
|
||||
| ignore
|
||||
| {error, no_xml_files_found}
|
||||
| {error, term()}.
|
||||
|
|
@ -56,7 +58,8 @@ start_link(XmlDir) ->
|
|||
gen_server:start_link({local, ?MODULE}, ?MODULE, [XmlDir], []).
|
||||
|
||||
find_objectid(ObjectId) ->
|
||||
ObjectIdInt = case is_list(ObjectId) of
|
||||
ObjectIdInt =
|
||||
case is_list(ObjectId) of
|
||||
true -> list_to_integer(ObjectId);
|
||||
false -> ObjectId
|
||||
end,
|
||||
|
|
@ -66,7 +69,8 @@ find_objectid(ObjectId) ->
|
|||
end.
|
||||
|
||||
find_name(Name) ->
|
||||
NameBinary = case is_list(Name) of
|
||||
NameBinary =
|
||||
case is_list(Name) of
|
||||
true -> list_to_binary(Name);
|
||||
false -> Name
|
||||
end,
|
||||
|
|
@ -93,7 +97,8 @@ init([XmlDir]) ->
|
|||
case load(XmlDir) of
|
||||
ok ->
|
||||
{ok, #state{}};
|
||||
{error, Reason} -> {stop, Reason}
|
||||
{error, Reason} ->
|
||||
{stop, Reason}
|
||||
end.
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
|
|
@ -118,7 +123,9 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
%%--------------------------------------------------------------------
|
||||
load(BaseDir) ->
|
||||
Wild = filename:join(BaseDir, "*.xml"),
|
||||
Wild2 = if is_binary(Wild) ->
|
||||
Wild2 =
|
||||
if
|
||||
is_binary(Wild) ->
|
||||
erlang:binary_to_list(Wild);
|
||||
true ->
|
||||
Wild
|
||||
|
|
@ -130,16 +137,17 @@ load(BaseDir) ->
|
|||
|
||||
load_loop([]) ->
|
||||
ok;
|
||||
load_loop([FileName|T]) ->
|
||||
load_loop([FileName | T]) ->
|
||||
ObjectXml = load_xml(FileName),
|
||||
[#xmlText{value=ObjectIdString}] = xmerl_xpath:string("ObjectID/text()", ObjectXml),
|
||||
[#xmlText{value=Name}] = xmerl_xpath:string("Name/text()", ObjectXml),
|
||||
[#xmlText{value = ObjectIdString}] = xmerl_xpath:string("ObjectID/text()", ObjectXml),
|
||||
[#xmlText{value = Name}] = xmerl_xpath:string("Name/text()", ObjectXml),
|
||||
ObjectId = list_to_integer(ObjectIdString),
|
||||
NameBinary = list_to_binary(Name),
|
||||
?SLOG(debug, #{ msg => "load_object_succeed"
|
||||
, filename => FileName
|
||||
, object_id => ObjectId
|
||||
, object_name => NameBinary
|
||||
?SLOG(debug, #{
|
||||
msg => "load_object_succeed",
|
||||
filename => FileName,
|
||||
object_id => ObjectId,
|
||||
object_name => NameBinary
|
||||
}),
|
||||
ets:insert(?LWM2M_OBJECT_DEF_TAB, {ObjectId, ObjectXml}),
|
||||
ets:insert(?LWM2M_OBJECT_NAME_TO_ID_TAB, {NameBinary, ObjectId}),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
-define(LWAPP, emqx_lwm2m).
|
||||
|
||||
|
||||
-define(OMA_ALTER_PATH_RT, <<"\"oma.lwm2m\"">>).
|
||||
|
||||
-define(MQ_COMMAND_ID, <<"CmdID">>).
|
||||
|
|
|
|||
|
|
@ -21,28 +21,35 @@
|
|||
-include("src/mqttsn/include/emqx_sn.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-export([ start_link/2
|
||||
, stop/0
|
||||
]).
|
||||
-export([
|
||||
start_link/2,
|
||||
stop/0
|
||||
]).
|
||||
|
||||
%% gen_server
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-record(state, {gwid, sock, port, addrs, duration, tref}).
|
||||
|
||||
-define(DEFAULT_DURATION, 15*60*1000).
|
||||
-define(DEFAULT_DURATION, 15 * 60 * 1000).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(start_link(pos_integer(), inet:port_number())
|
||||
-> {ok, pid()} | {error, term()}).
|
||||
-spec start_link(pos_integer(), inet:port_number()) ->
|
||||
{ok, pid()} | {error, term()}.
|
||||
start_link(GwId, Port) ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [GwId, Port], []).
|
||||
|
||||
-spec(stop() -> ok).
|
||||
-spec stop() -> ok.
|
||||
stop() ->
|
||||
gen_server:stop(?MODULE, nomal, infinity).
|
||||
|
||||
|
|
@ -54,27 +61,35 @@ init([GwId, Port]) ->
|
|||
%% FIXME:
|
||||
Duration = application:get_env(emqx_sn, advertise_duration, ?DEFAULT_DURATION),
|
||||
{ok, Sock} = gen_udp:open(0, [binary, {broadcast, true}]),
|
||||
{ok, ensure_advertise(#state{gwid = GwId, addrs = boradcast_addrs(),
|
||||
sock = Sock, port = Port, duration = Duration})}.
|
||||
{ok,
|
||||
ensure_advertise(#state{
|
||||
gwid = GwId,
|
||||
addrs = boradcast_addrs(),
|
||||
sock = Sock,
|
||||
port = Port,
|
||||
duration = Duration
|
||||
})}.
|
||||
|
||||
handle_call(Req, _From, State) ->
|
||||
?SLOG(error, #{ msg => "unexpected_call"
|
||||
, call => Req
|
||||
?SLOG(error, #{
|
||||
msg => "unexpected_call",
|
||||
call => Req
|
||||
}),
|
||||
{reply, ignored, State}.
|
||||
|
||||
handle_cast(Msg, State) ->
|
||||
?SLOG(error, #{ msg => "unexpected_cast"
|
||||
, cast => Msg
|
||||
?SLOG(error, #{
|
||||
msg => "unexpected_cast",
|
||||
cast => Msg
|
||||
}),
|
||||
{noreply, State}.
|
||||
|
||||
handle_info(broadcast_advertise, State) ->
|
||||
{noreply, ensure_advertise(State), hibernate};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?SLOG(error, #{ msg => "unexpected_info"
|
||||
, info => Info
|
||||
?SLOG(error, #{
|
||||
msg => "unexpected_info",
|
||||
info => Info
|
||||
}),
|
||||
{noreply, State}.
|
||||
|
||||
|
|
@ -93,17 +108,29 @@ ensure_advertise(State = #state{duration = Duration}) ->
|
|||
send_advertise(State),
|
||||
State#state{tref = erlang:send_after(Duration, self(), broadcast_advertise)}.
|
||||
|
||||
send_advertise(#state{gwid = GwId, sock = Sock, port = Port,
|
||||
addrs = Addrs, duration = Duration}) ->
|
||||
send_advertise(#state{
|
||||
gwid = GwId,
|
||||
sock = Sock,
|
||||
port = Port,
|
||||
addrs = Addrs,
|
||||
duration = Duration
|
||||
}) ->
|
||||
Data = emqx_sn_frame:serialize_pkt(?SN_ADVERTISE_MSG(GwId, Duration), #{}),
|
||||
lists:foreach(fun(Addr) ->
|
||||
?SLOG(debug, #{ msg => "send_ADVERTISE_msg"
|
||||
, address => Addr
|
||||
lists:foreach(
|
||||
fun(Addr) ->
|
||||
?SLOG(debug, #{
|
||||
msg => "send_ADVERTISE_msg",
|
||||
address => Addr
|
||||
}),
|
||||
gen_udp:send(Sock, Addr, Port, Data)
|
||||
end, Addrs).
|
||||
end,
|
||||
Addrs
|
||||
).
|
||||
|
||||
boradcast_addrs() ->
|
||||
lists:usort([Addr || {ok, IfList} <- [inet:getiflist()], If <- IfList,
|
||||
{ok, [{broadaddr, Addr}]} <- [inet:ifget(If, [broadaddr])]]).
|
||||
|
||||
lists:usort([
|
||||
Addr
|
||||
|| {ok, IfList} <- [inet:getiflist()],
|
||||
If <- IfList,
|
||||
{ok, [{broadaddr, Addr}]} <- [inet:ifget(If, [broadaddr])]
|
||||
]).
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue