Remove the emqx_protocol module and use 'conninfo' to store the MQTT CONNECT fields (#2914)

* Use 'peerhost' to replace 'peername'

* Add more functions

* Export type 'opts/0'

* Use 'peerhost' to replace 'peername'

* Add function 'get_caps/3'

* Rename 'get|set_property' fuctions to 'get|set'

* Improve the 'open_session/3' API

* Remove the function 'update_expiry_interval/2'

* Rewrite the 'presence' extended module

* Update the 'attrs/1' and 'handle_timeout/3' functions

* Update test cases

* Rewrite the test cases for extended modules

* Add test cases for 'reason/1'

* Remove the default value of 'headers' field

* Add function 'get_flags/1' for test

* Add test case 't_undefined_headers'

* Add test cases for emqx_ctl module

* Improve the 'channel' module and add more test cases

- Rename the 'Client' field to 'ClientInfo'
- Remove the 'expiry_interval' from session record
- Add more test cases for emqx_zone module
- Add more test cases for emqx_banned module
- Add more test cases for emqx_message module
- Remove 'sockname', 'conn_mod' fields from type 'client'

* Fix the type specs for cmd usage
This commit is contained in:
Feng Lee 2019-09-20 16:21:39 +08:00 committed by GitHub
commit 45eee42c76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1094 additions and 963 deletions

View File

@ -64,7 +64,7 @@
%% Message flags %% Message flags
flags :: #{atom() => boolean()}, flags :: #{atom() => boolean()},
%% Message headers, or MQTT 5.0 Properties %% Message headers, or MQTT 5.0 Properties
headers = #{}, headers :: map(),
%% Topic that the message is published to %% Topic that the message is published to
topic :: binary(), topic :: binary(),
%% Message Payload %% Message Payload

View File

@ -108,9 +108,9 @@ match_who(#{client_id := ClientId}, {client, ClientId}) ->
true; true;
match_who(#{username := Username}, {user, Username}) -> match_who(#{username := Username}, {user, Username}) ->
true; true;
match_who(#{peername := undefined}, {ipaddr, _Tup}) -> match_who(#{peerhost := undefined}, {ipaddr, _Tup}) ->
false; false;
match_who(#{peername := {IP, _}}, {ipaddr, CIDR}) -> match_who(#{peerhost := IP}, {ipaddr, CIDR}) ->
esockd_cidr:match(IP, CIDR); esockd_cidr:match(IP, CIDR);
match_who(Client, {'and', Conds}) when is_list(Conds) -> match_who(Client, {'and', Conds}) when is_list(Conds) ->
lists:foldl(fun(Who, Allow) -> lists:foldl(fun(Who, Allow) ->

View File

@ -30,7 +30,7 @@
-boot_mnesia({mnesia, [boot]}). -boot_mnesia({mnesia, [boot]}).
-copy_mnesia({mnesia, [copy]}). -copy_mnesia({mnesia, [copy]}).
-export([start_link/0]). -export([start_link/0, stop/0]).
-export([ check/1 -export([ check/1
, add/1 , add/1
@ -69,11 +69,14 @@ mnesia(copy) ->
start_link() -> start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%% for tests
-spec(stop() -> ok).
stop() -> gen_server:stop(?MODULE).
-spec(check(emqx_types:client()) -> boolean()). -spec(check(emqx_types:client()) -> boolean()).
check(#{client_id := ClientId, check(#{client_id := ClientId,
username := Username, username := Username,
peername := {IPAddr, _} peerhost := IPAddr}) ->
}) ->
ets:member(?BANNED_TAB, {client_id, ClientId}) ets:member(?BANNED_TAB, {client_id, ClientId})
orelse ets:member(?BANNED_TAB, {username, Username}) orelse ets:member(?BANNED_TAB, {username, Username})
orelse ets:member(?BANNED_TAB, {ipaddr, IPAddr}). orelse ets:member(?BANNED_TAB, {ipaddr, IPAddr}).
@ -82,11 +85,10 @@ check(#{client_id := ClientId,
add(Banned) when is_record(Banned, banned) -> add(Banned) when is_record(Banned, banned) ->
mnesia:dirty_write(?BANNED_TAB, Banned). mnesia:dirty_write(?BANNED_TAB, Banned).
-spec(delete({client_id, emqx_types:client_id()} | -spec(delete({client_id, emqx_types:client_id()}
{username, emqx_types:username()} | | {username, emqx_types:username()}
{peername, emqx_types:peername()}) -> ok). | {peerhost, emqx_types:peerhost()}) -> ok).
delete(Key) -> delete(Key) -> mnesia:dirty_delete(?BANNED_TAB, Key).
mnesia:dirty_delete(?BANNED_TAB, Key).
info(InfoKey) -> info(InfoKey) ->
mnesia:table_info(?BANNED_TAB, InfoKey). mnesia:table_info(?BANNED_TAB, InfoKey).
@ -107,8 +109,7 @@ handle_cast(Msg, State) ->
{noreply, State}. {noreply, State}.
handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) ->
mnesia:async_dirty(fun expire_banned_items/1, mnesia:async_dirty(fun expire_banned_items/1, [erlang:system_time(second)]),
[erlang:system_time(second)]),
{noreply, ensure_expiry_timer(State), hibernate}; {noreply, ensure_expiry_timer(State), hibernate};
handle_info(Info, State) -> handle_info(Info, State) ->
@ -127,7 +128,7 @@ code_change(_OldVsn, State, _Extra) ->
-ifdef(TEST). -ifdef(TEST).
ensure_expiry_timer(State) -> ensure_expiry_timer(State) ->
State#{expiry_timer := emqx_misc:start_timer(timer:seconds(1), expire)}. State#{expiry_timer := emqx_misc:start_timer(10, expire)}.
-else. -else.
ensure_expiry_timer(State) -> ensure_expiry_timer(State) ->
State#{expiry_timer := emqx_misc:start_timer(timer:minutes(1), expire)}. State#{expiry_timer := emqx_misc:start_timer(timer:minutes(1), expire)}.

View File

@ -24,8 +24,6 @@
-logger_header("[Channel]"). -logger_header("[Channel]").
-export([init/2]).
-export([ info/1 -export([ info/1
, info/2 , info/2
, attrs/1 , attrs/1
@ -36,12 +34,13 @@
%% Exports for unit tests:( %% Exports for unit tests:(
-export([set_field/3]). -export([set_field/3]).
-export([ handle_in/2 -export([ init/2
, handle_in/2
, handle_out/2 , handle_out/2
, handle_call/2 , handle_call/2
, handle_cast/2 , handle_cast/2
, handle_info/2 , handle_info/2
, timeout/3 , handle_timeout/3
, terminate/2 , terminate/2
]). ]).
@ -58,19 +57,25 @@
-export_type([channel/0]). -export_type([channel/0]).
-record(channel, { -record(channel, {
%% MQTT Client %% MQTT ConnInfo
conninfo :: emqx_types:conninfo(),
%% MQTT ClientInfo
client :: emqx_types:client(), client :: emqx_types:client(),
%% MQTT Session %% MQTT Session
session :: emqx_session:session(), session :: emqx_session:session(),
%% MQTT Protocol
protocol :: emqx_protocol:protocol(),
%% Keepalive %% Keepalive
keepalive :: emqx_keepalive:keepalive(), keepalive :: emqx_keepalive:keepalive(),
%% MQTT Will Msg
will_msg :: emqx_types:message(),
%% MQTT Topic Aliases
topic_aliases :: maybe(map()),
%% MQTT Topic Alias Maximum
alias_maximum :: maybe(map()),
%% Timers %% Timers
timers :: #{atom() => disabled | maybe(reference())}, timers :: #{atom() => disabled | maybe(reference())},
%% GC State %% GC State
gc_state :: maybe(emqx_gc:gc_state()), gc_state :: maybe(emqx_gc:gc_state()),
%% OOM Policy %% OOM Policy TODO: should be removed from channel.
oom_policy :: maybe(emqx_oom:oom_policy()), oom_policy :: maybe(emqx_oom:oom_policy()),
%% Connected %% Connected
connected :: undefined | boolean(), connected :: undefined | boolean(),
@ -97,53 +102,8 @@
will_timer => will_message will_timer => will_message
}). }).
-define(ATTR_KEYS, [client, session, protocol, connected, connected_at, disconnected_at]). -define(ATTR_KEYS, [conninfo, client, session, connected, connected_at, disconnected_at]).
-define(INFO_KEYS, ?ATTR_KEYS ++ [keepalive, topic_aliases, alias_maximum, gc_state, disconnected_at]).
-define(INFO_KEYS, ?ATTR_KEYS ++ [keepalive, gc_state, disconnected_at]).
%%--------------------------------------------------------------------
%% Init the channel
%%--------------------------------------------------------------------
-spec(init(emqx_types:conninfo(), proplists:proplist()) -> channel()).
init(ConnInfo, Options) ->
Zone = proplists:get_value(zone, Options),
Peercert = maps:get(peercert, ConnInfo, undefined),
Username = case peer_cert_as_username(Options) of
cn -> esockd_peercert:common_name(Peercert);
dn -> esockd_peercert:subject(Peercert);
crt -> Peercert;
_ -> undefined
end,
MountPoint = emqx_zone:get_env(Zone, mountpoint),
Client = maps:merge(#{zone => Zone,
username => Username,
client_id => <<>>,
mountpoint => MountPoint,
is_bridge => false,
is_superuser => false
}, ConnInfo),
EnableStats = emqx_zone:get_env(Zone, enable_stats, true),
StatsTimer = if
EnableStats -> undefined;
?Otherwise -> disabled
end,
GcState = maybe_apply(fun emqx_gc:init/1,
emqx_zone:get_env(Zone, force_gc_policy)),
OomPolicy = maybe_apply(fun emqx_oom:init/1,
emqx_zone:get_env(Zone, force_shutdown_policy)),
#channel{client = Client,
gc_state = GcState,
oom_policy = OomPolicy,
timers = #{stats_timer => StatsTimer},
connected = undefined,
takeover = false,
resuming = false,
pendings = []
}.
peer_cert_as_username(Options) ->
proplists:get_value(peer_cert_as_username, Options).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Info, Attrs and Caps %% Info, Attrs and Caps
@ -157,14 +117,18 @@ info(Channel) ->
-spec(info(list(atom())|atom(), channel()) -> term()). -spec(info(list(atom())|atom(), channel()) -> term()).
info(Keys, Channel) when is_list(Keys) -> info(Keys, Channel) when is_list(Keys) ->
[{Key, info(Key, Channel)} || Key <- Keys]; [{Key, info(Key, Channel)} || Key <- Keys];
info(client, #channel{client = Client}) -> info(conninfo, #channel{conninfo = ConnInfo}) ->
Client; ConnInfo;
info(client, #channel{client = ClientInfo}) ->
ClientInfo;
info(session, #channel{session = Session}) -> info(session, #channel{session = Session}) ->
maybe_apply(fun emqx_session:info/1, Session); maybe_apply(fun emqx_session:info/1, Session);
info(protocol, #channel{protocol = Protocol}) ->
maybe_apply(fun emqx_protocol:info/1, Protocol);
info(keepalive, #channel{keepalive = Keepalive}) -> info(keepalive, #channel{keepalive = Keepalive}) ->
maybe_apply(fun emqx_keepalive:info/1, Keepalive); maybe_apply(fun emqx_keepalive:info/1, Keepalive);
info(topic_aliases, #channel{topic_aliases = Aliases}) ->
Aliases;
info(alias_maximum, #channel{alias_maximum = Limits}) ->
Limits;
info(gc_state, #channel{gc_state = GcState}) -> info(gc_state, #channel{gc_state = GcState}) ->
maybe_apply(fun emqx_gc:info/1, GcState); maybe_apply(fun emqx_gc:info/1, GcState);
info(oom_policy, #channel{oom_policy = OomPolicy}) -> info(oom_policy, #channel{oom_policy = OomPolicy}) ->
@ -181,8 +145,8 @@ info(disconnected_at, #channel{disconnected_at = DisconnectedAt}) ->
attrs(Channel) -> attrs(Channel) ->
maps:from_list([{Key, attr(Key, Channel)} || Key <- ?ATTR_KEYS]). maps:from_list([{Key, attr(Key, Channel)} || Key <- ?ATTR_KEYS]).
attr(protocol, #channel{protocol = Proto}) -> attr(conninfo, #channel{conninfo = ConnInfo}) ->
maybe_apply(fun emqx_protocol:attrs/1, Proto); ConnInfo;
attr(session, #channel{session = Session}) -> attr(session, #channel{session = Session}) ->
maybe_apply(fun emqx_session:attrs/1, Session); maybe_apply(fun emqx_session:attrs/1, Session);
attr(Key, Channel) -> info(Key, Channel). attr(Key, Channel) -> info(Key, Channel).
@ -201,6 +165,54 @@ set_field(Name, Val, Channel) ->
Pos = emqx_misc:index_of(Name, Fields), Pos = emqx_misc:index_of(Name, Fields),
setelement(Pos+1, Channel, Val). setelement(Pos+1, Channel, Val).
%%--------------------------------------------------------------------
%% Init the channel
%%--------------------------------------------------------------------
-spec(init(emqx_types:conninfo(), proplists:proplist()) -> channel()).
init(ConnInfo = #{peername := {PeerHost, _Port}}, Options) ->
Zone = proplists:get_value(zone, Options),
Peercert = maps:get(peercert, ConnInfo, undefined),
Username = case peer_cert_as_username(Options) of
cn -> esockd_peercert:common_name(Peercert);
dn -> esockd_peercert:subject(Peercert);
crt -> Peercert;
_ -> undefined
end,
MountPoint = emqx_zone:get_env(Zone, mountpoint),
ClientInfo = #{zone => Zone,
peerhost => PeerHost,
peercert => Peercert,
client_id => undefined,
username => Username,
mountpoint => MountPoint,
is_bridge => false,
is_superuser => false
},
StatsTimer = case emqx_zone:enable_stats(Zone) of
true -> undefined;
false -> disabled
end,
#channel{conninfo = ConnInfo,
client = ClientInfo,
gc_state = init_gc_state(Zone),
oom_policy = init_oom_policy(Zone),
timers = #{stats_timer => StatsTimer},
connected = undefined,
takeover = false,
resuming = false,
pendings = []
}.
peer_cert_as_username(Options) ->
proplists:get_value(peer_cert_as_username, Options).
init_gc_state(Zone) ->
maybe_apply(fun emqx_gc:init/1, emqx_zone:force_gc_policy(Zone)).
init_oom_policy(Zone) ->
maybe_apply(fun emqx_oom:init/1, emqx_zone:force_shutdown_policy(Zone)).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle incoming packet %% Handle incoming packet
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -215,8 +227,8 @@ handle_in(?CONNECT_PACKET(_), Channel = #channel{connected = true}) ->
handle_out({disconnect, ?RC_PROTOCOL_ERROR}, Channel); handle_out({disconnect, ?RC_PROTOCOL_ERROR}, Channel);
handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> handle_in(?CONNECT_PACKET(ConnPkt), Channel) ->
case pipeline([fun check_connpkt/2, case pipeline([fun enrich_conninfo/2,
fun init_protocol/2, fun check_connect/2,
fun enrich_client/2, fun enrich_client/2,
fun set_logger_meta/2, fun set_logger_meta/2,
fun check_banned/2, fun check_banned/2,
@ -225,31 +237,25 @@ handle_in(?CONNECT_PACKET(ConnPkt), Channel) ->
{ok, NConnPkt, NChannel} -> {ok, NConnPkt, NChannel} ->
process_connect(NConnPkt, NChannel); process_connect(NConnPkt, NChannel);
{error, ReasonCode, NChannel} -> {error, ReasonCode, NChannel} ->
handle_out({connack, ReasonCode}, NChannel) handle_out({connack, ReasonCode, ConnPkt}, NChannel)
end; end;
handle_in(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId), handle_in(Packet = ?PUBLISH_PACKET(_QoS), Channel) ->
Channel = #channel{protocol = Protocol}) -> case emqx_packet:check(Packet) of
case pipeline([fun emqx_packet:check/1, ok ->
fun process_alias/2, handle_publish(Packet, Channel);
fun check_publish/2], Packet, Channel) of {error, ReasonCode} ->
{ok, NPacket, NChannel} -> handle_out({disconnect, ReasonCode}, Channel)
process_publish(NPacket, NChannel);
{error, ReasonCode, NChannel} ->
ProtoVer = emqx_protocol:info(proto_ver, Protocol),
?LOG(warning, "Cannot publish message to ~s due to ~s",
[Topic, emqx_reason_codes:text(ReasonCode, ProtoVer)]),
handle_out({disconnect, ReasonCode}, NChannel)
end; end;
handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), handle_in(?PUBACK_PACKET(PacketId, _ReasonCode),
Channel = #channel{client = Client, session = Session}) -> Channel = #channel{client = ClientInfo, session = Session}) ->
case emqx_session:puback(PacketId, Session) of case emqx_session:puback(PacketId, Session) of
{ok, Msg, Publishes, NSession} -> {ok, Msg, Publishes, NSession} ->
ok = emqx_hooks:run('message.acked', [Client, Msg]), ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]),
handle_out({publish, Publishes}, Channel#channel{session = NSession}); handle_out({publish, Publishes}, Channel#channel{session = NSession});
{ok, Msg, NSession} -> {ok, Msg, NSession} ->
ok = emqx_hooks:run('message.acked', [Client, Msg]), ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]),
{ok, Channel#channel{session = NSession}}; {ok, Channel#channel{session = NSession}};
{error, ?RC_PACKET_IDENTIFIER_IN_USE} -> {error, ?RC_PACKET_IDENTIFIER_IN_USE} ->
?LOG(warning, "The PUBACK PacketId ~w is inuse.", [PacketId]), ?LOG(warning, "The PUBACK PacketId ~w is inuse.", [PacketId]),
@ -262,10 +268,10 @@ handle_in(?PUBACK_PACKET(PacketId, _ReasonCode),
end; end;
handle_in(?PUBREC_PACKET(PacketId, _ReasonCode), handle_in(?PUBREC_PACKET(PacketId, _ReasonCode),
Channel = #channel{client = Client, session = Session}) -> Channel = #channel{client = ClientInfo, session = Session}) ->
case emqx_session:pubrec(PacketId, Session) of case emqx_session:pubrec(PacketId, Session) of
{ok, Msg, NSession} -> {ok, Msg, NSession} ->
ok = emqx_hooks:run('message.acked', [Client, Msg]), ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]),
NChannel = Channel#channel{session = NSession}, NChannel = Channel#channel{session = NSession},
handle_out({pubrel, PacketId, ?RC_SUCCESS}, NChannel); handle_out({pubrel, PacketId, ?RC_SUCCESS}, NChannel);
{error, RC = ?RC_PACKET_IDENTIFIER_IN_USE} -> {error, RC = ?RC_PACKET_IDENTIFIER_IN_USE} ->
@ -301,10 +307,10 @@ handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = S
end; end;
handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
Channel = #channel{client = Client}) -> Channel = #channel{client = ClientInfo}) ->
case emqx_packet:check(Packet) of case emqx_packet:check(Packet) of
ok -> TopicFilters1 = emqx_hooks:run_fold('client.subscribe', ok -> TopicFilters1 = emqx_hooks:run_fold('client.subscribe',
[Client, Properties], [ClientInfo, Properties],
parse_topic_filters(TopicFilters)), parse_topic_filters(TopicFilters)),
TopicFilters2 = enrich_subid(Properties, TopicFilters1), TopicFilters2 = enrich_subid(Properties, TopicFilters1),
{ReasonCodes, NChannel} = process_subscribe(TopicFilters2, Channel), {ReasonCodes, NChannel} = process_subscribe(TopicFilters2, Channel),
@ -314,10 +320,10 @@ handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
end; end;
handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
Channel = #channel{client = Client}) -> Channel = #channel{client = ClientInfo}) ->
case emqx_packet:check(Packet) of case emqx_packet:check(Packet) of
ok -> TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe', ok -> TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe',
[Client, Properties], [ClientInfo, Properties],
parse_topic_filters(TopicFilters)), parse_topic_filters(TopicFilters)),
{ReasonCodes, NChannel} = process_unsubscribe(TopicFilters1, Channel), {ReasonCodes, NChannel} = process_unsubscribe(TopicFilters1, Channel),
handle_out({unsuback, PacketId, ReasonCodes}, NChannel); handle_out({unsuback, PacketId, ReasonCodes}, NChannel);
@ -328,9 +334,10 @@ handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
handle_in(?PACKET(?PINGREQ), Channel) -> handle_in(?PACKET(?PINGREQ), Channel) ->
{ok, ?PACKET(?PINGRESP), Channel}; {ok, ?PACKET(?PINGRESP), Channel};
handle_in(?DISCONNECT_PACKET(ReasonCode, Properties), Channel = #channel{session = Session, protocol = Protocol}) -> handle_in(?DISCONNECT_PACKET(ReasonCode, Properties), Channel = #channel{session = Session,
conninfo = ConnInfo = #{expiry_interval := OldInterval}}) ->
OldInterval = emqx_session:info(expiry_interval, Session), OldInterval = emqx_session:info(expiry_interval, Session),
Interval = get_property('Session-Expiry-Interval', Properties, OldInterval), Interval = emqx_mqtt_props:get('Session-Expiry-Interval', Props, OldInterval),
case OldInterval =:= 0 andalso Interval =/= OldInterval of case OldInterval =:= 0 andalso Interval =/= OldInterval of
true -> true ->
handle_out({disconnect, ?RC_PROTOCOL_ERROR}, Channel); handle_out({disconnect, ?RC_PROTOCOL_ERROR}, Channel);
@ -361,28 +368,43 @@ handle_in(Packet, Channel) ->
%% Process Connect %% Process Connect
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
process_connect(ConnPkt, Channel) -> process_connect(ConnPkt = #mqtt_packet_connect{clean_start = CleanStart},
case open_session(ConnPkt, Channel) of Channel = #channel{conninfo = ConnInfo, client = ClientInfo}) ->
case emqx_cm:open_session(CleanStart, ClientInfo, ConnInfo) of
{ok, #{session := Session, present := false}} -> {ok, #{session := Session, present := false}} ->
NChannel = Channel#channel{session = Session}, NChannel = Channel#channel{session = Session},
handle_out({connack, ?RC_SUCCESS, sp(false)}, NChannel); handle_out({connack, ?RC_SUCCESS, sp(false), ConnPkt}, NChannel);
{ok, #{session := Session, present := true, pendings := Pendings}} -> {ok, #{session := Session, present := true, pendings := Pendings}} ->
%%TODO: improve later. %%TODO: improve later.
NPendings = lists:usort(lists:append(Pendings, emqx_misc:drain_deliver())), NPendings = lists:usort(lists:append(Pendings, emqx_misc:drain_deliver())),
NChannel = Channel#channel{session = Session, NChannel = Channel#channel{session = Session,
resuming = true, resuming = true,
pendings = NPendings}, pendings = NPendings},
handle_out({connack, ?RC_SUCCESS, sp(true)}, NChannel); handle_out({connack, ?RC_SUCCESS, sp(true), ConnPkt}, NChannel);
{error, Reason} -> {error, Reason} ->
%% TODO: Unknown error? %% TODO: Unknown error?
?LOG(error, "Failed to open session: ~p", [Reason]), ?LOG(error, "Failed to open session: ~p", [Reason]),
handle_out({connack, ?RC_UNSPECIFIED_ERROR}, Channel) handle_out({connack, ?RC_UNSPECIFIED_ERROR, ConnPkt}, Channel)
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Process Publish %% Process Publish
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
handle_publish(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId),
Channel = #channel{conninfo = #{proto_ver := ProtoVer}}) ->
case pipeline([fun process_alias/2,
fun check_pub_acl/2,
fun check_pub_alias/2,
fun check_pub_caps/2], Packet, Channel) of
{ok, NPacket, NChannel} ->
process_publish(NPacket, NChannel);
{error, ReasonCode, NChannel} ->
?LOG(warning, "Cannot publish message to ~s due to ~s",
[Topic, emqx_reason_codes:text(ReasonCode, ProtoVer)]),
handle_out({disconnect, ReasonCode}, NChannel)
end.
process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), Channel) -> process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), Channel) ->
Msg = publish_to_msg(Packet, Channel), Msg = publish_to_msg(Packet, Channel),
process_publish(PacketId, Msg, Channel). process_publish(PacketId, Msg, Channel).
@ -417,11 +439,10 @@ process_publish(PacketId, Msg = #message{qos = ?QOS_2},
handle_out({pubrec, PacketId, RC}, Channel) handle_out({pubrec, PacketId, RC}, Channel)
end. end.
publish_to_msg(Packet, #channel{client = Client = #{mountpoint := MountPoint}, publish_to_msg(Packet, #channel{conninfo = #{proto_ver := ProtoVer},
protocol = Protocol}) -> client = ClientInfo = #{mountpoint := MountPoint}}) ->
Msg = emqx_packet:to_message(Client, Packet), Msg = emqx_packet:to_message(ClientInfo, Packet),
Msg1 = emqx_message:set_flag(dup, false, Msg), Msg1 = emqx_message:set_flag(dup, false, Msg),
ProtoVer = emqx_protocol:info(proto_ver, Protocol),
Msg2 = emqx_message:set_header(proto_ver, ProtoVer, Msg1), Msg2 = emqx_message:set_header(proto_ver, ProtoVer, Msg1),
emqx_mountpoint:mount(MountPoint, Msg2). emqx_mountpoint:mount(MountPoint, Msg2).
@ -440,13 +461,13 @@ process_subscribe([{TopicFilter, SubOpts}|More], Acc, Channel) ->
process_subscribe(More, [RC|Acc], NChannel). process_subscribe(More, [RC|Acc], NChannel).
do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, Channel = do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, Channel =
#channel{client = Client = #{mountpoint := MountPoint}, #channel{client = ClientInfo = #{mountpoint := MountPoint},
session = Session}) -> session = Session}) ->
case check_subscribe(TopicFilter, SubOpts, Channel) of case check_subscribe(TopicFilter, SubOpts, Channel) of
ok -> ok ->
TopicFilter1 = emqx_mountpoint:mount(MountPoint, TopicFilter), TopicFilter1 = emqx_mountpoint:mount(MountPoint, TopicFilter),
SubOpts1 = enrich_subopts(maps:merge(?DEFAULT_SUBOPTS, SubOpts), Channel), SubOpts1 = enrich_subopts(maps:merge(?DEFAULT_SUBOPTS, SubOpts), Channel),
case emqx_session:subscribe(Client, TopicFilter1, SubOpts1, Session) of case emqx_session:subscribe(ClientInfo, TopicFilter1, SubOpts1, Session) of
{ok, NSession} -> {ok, NSession} ->
{QoS, Channel#channel{session = NSession}}; {QoS, Channel#channel{session = NSession}};
{error, RC} -> {RC, Channel} {error, RC} -> {RC, Channel}
@ -470,10 +491,10 @@ process_unsubscribe([{TopicFilter, SubOpts}|More], Acc, Channel) ->
process_unsubscribe(More, [RC|Acc], NChannel). process_unsubscribe(More, [RC|Acc], NChannel).
do_unsubscribe(TopicFilter, _SubOpts, Channel = do_unsubscribe(TopicFilter, _SubOpts, Channel =
#channel{client = Client = #{mountpoint := MountPoint}, #channel{client = ClientInfo = #{mountpoint := MountPoint},
session = Session}) -> session = Session}) ->
TopicFilter1 = emqx_mountpoint:mount(MountPoint, TopicFilter), TopicFilter1 = emqx_mountpoint:mount(MountPoint, TopicFilter),
case emqx_session:unsubscribe(Client, TopicFilter1, Session) of case emqx_session:unsubscribe(ClientInfo, TopicFilter1, Session) of
{ok, NSession} -> {ok, NSession} ->
{?RC_SUCCESS, Channel#channel{session = NSession}}; {?RC_SUCCESS, Channel#channel{session = NSession}};
{error, RC} -> {RC, Channel} {error, RC} -> {RC, Channel}
@ -484,35 +505,37 @@ do_unsubscribe(TopicFilter, _SubOpts, Channel =
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%%TODO: RunFold or Pipeline %%TODO: RunFold or Pipeline
handle_out({connack, ?RC_SUCCESS, SP}, Channel = #channel{client = Client}) -> handle_out({connack, ?RC_SUCCESS, SP, ConnPkt},
Channel = #channel{conninfo = ConnInfo, client = ClientInfo}) ->
AckProps = run_fold([fun enrich_caps/2, AckProps = run_fold([fun enrich_caps/2,
fun enrich_server_keepalive/2, fun enrich_server_keepalive/2,
fun enrich_assigned_clientid/2 fun enrich_assigned_clientid/2
], #{}, Channel), ], #{}, Channel),
Channel1 = ensure_keepalive(AckProps, ensure_connected(Channel)), Channel1 = Channel#channel{will_msg = emqx_packet:will_msg(ConnPkt),
ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, attrs(Channel1)]), alias_maximum = init_alias_maximum(ConnPkt, ClientInfo),
connected = true,
connected_at = os:timestamp()
},
Channel2 = ensure_keepalive(AckProps, Channel1),
ok = emqx_hooks:run('client.connected', [ClientInfo, ?RC_SUCCESS, ConnInfo]),
AckPacket = ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps), AckPacket = ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps),
case maybe_resume_session(Channel1) of case maybe_resume_session(Channel2) of
ignore -> {ok, AckPacket, Channel1}; ignore ->
{ok, AckPacket, Channel2};
{ok, Publishes, NSession} -> {ok, Publishes, NSession} ->
Channel2 = Channel1#channel{session = NSession, Channel3 = Channel2#channel{session = NSession,
resuming = false, resuming = false,
pendings = []}, pendings = []},
{ok, Packets, _} = handle_out({publish, Publishes}, Channel2), {ok, Packets, _} = handle_out({publish, Publishes}, Channel3),
{ok, [AckPacket|Packets], Channel2} {ok, [AckPacket|Packets], Channel2}
end; end;
handle_out({connack, ReasonCode}, Channel = #channel{client = Client, handle_out({connack, ReasonCode, _ConnPkt}, Channel = #channel{conninfo = ConnInfo,
protocol = Protocol client = ClientInfo}) ->
}) -> ok = emqx_hooks:run('client.connected', [ClientInfo, ReasonCode, ConnInfo]),
ok = emqx_hooks:run('client.connected', [Client, ReasonCode, attrs(Channel)]), ReasonCode1 = case ProtoVer = maps:get(proto_ver, ConnInfo) of
ProtoVer = case Protocol of ?MQTT_PROTO_V5 -> ReasonCode;
undefined -> ?MQTT_PROTO_V5; _Ver -> emqx_reason_codes:compat(connack, ReasonCode)
_ -> emqx_protocol:info(proto_ver, Protocol)
end,
ReasonCode1 = if
ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode;
true -> emqx_reason_codes:compat(connack, ReasonCode)
end, end,
Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer),
{stop, {shutdown, Reason}, ?CONNACK_PACKET(ReasonCode1), Channel}; {stop, {shutdown, Reason}, ?CONNACK_PACKET(ReasonCode1), Channel};
@ -553,9 +576,9 @@ handle_out({publish, _PacketId, #message{from = ClientId,
{ok, Channel}; {ok, Channel};
handle_out({publish, PacketId, Msg}, Channel = handle_out({publish, PacketId, Msg}, Channel =
#channel{client = Client = #{mountpoint := MountPoint}}) -> #channel{client = ClientInfo = #{mountpoint := MountPoint}}) ->
Msg1 = emqx_message:update_expiry(Msg), Msg1 = emqx_message:update_expiry(Msg),
Msg2 = emqx_hooks:run_fold('message.delivered', [Client], Msg1), Msg2 = emqx_hooks:run_fold('message.delivered', [ClientInfo], Msg1),
Msg3 = emqx_mountpoint:unmount(MountPoint, Msg2), Msg3 = emqx_mountpoint:unmount(MountPoint, Msg2),
{ok, emqx_message:to_packet(PacketId, Msg3), Channel}; {ok, emqx_message:to_packet(PacketId, Msg3), Channel};
@ -571,24 +594,23 @@ handle_out({pubrec, PacketId, ReasonCode}, Channel) ->
handle_out({pubcomp, PacketId, ReasonCode}, Channel) -> handle_out({pubcomp, PacketId, ReasonCode}, Channel) ->
{ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), Channel}; {ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), Channel};
handle_out({suback, PacketId, ReasonCodes}, Channel = #channel{protocol = Protocol}) -> handle_out({suback, PacketId, ReasonCodes},
ReasonCodes1 = case emqx_protocol:info(proto_ver, Protocol) of Channel = #channel{conninfo = #{proto_ver := ?MQTT_PROTO_V5}}) ->
?MQTT_PROTO_V5 -> ReasonCodes; {ok, ?SUBACK_PACKET(PacketId, ReasonCodes), Channel};
_Ver ->
[emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes] handle_out({suback, PacketId, ReasonCodes}, Channel) ->
end, ReasonCodes1 = [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes],
{ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), Channel}; {ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), Channel};
handle_out({unsuback, PacketId, ReasonCodes}, Channel = #channel{protocol = Protocol}) -> handle_out({unsuback, PacketId, ReasonCodes},
Unsuback = case emqx_protocol:info(proto_ver, Protocol) of Channel = #channel{conninfo = #{proto_ver := ?MQTT_PROTO_V5}}) ->
?MQTT_PROTO_V5 -> {ok, ?UNSUBACK_PACKET(PacketId, ReasonCodes), Channel};
?UNSUBACK_PACKET(PacketId, ReasonCodes);
_Ver -> ?UNSUBACK_PACKET(PacketId)
end,
{ok, Unsuback, Channel};
handle_out({disconnect, ReasonCode}, Channel = #channel{protocol = Protocol}) -> handle_out({unsuback, PacketId, _ReasonCodes}, Channel) ->
case emqx_protocol:info(proto_ver, Protocol) of {ok, ?UNSUBACK_PACKET(PacketId), Channel};
handle_out({disconnect, ReasonCode}, Channel = #channel{conninfo = ConnInfo}) ->
case maps:get(proto_ver, ConnInfo) of
?MQTT_PROTO_V5 -> ?MQTT_PROTO_V5 ->
Reason = emqx_reason_codes:name(ReasonCode), Reason = emqx_reason_codes:name(ReasonCode),
Packet = ?DISCONNECT_PACKET(ReasonCode), Packet = ?DISCONNECT_PACKET(ReasonCode),
@ -650,16 +672,16 @@ handle_cast(Msg, Channel) ->
-spec(handle_info(Info :: term(), channel()) -spec(handle_info(Info :: term(), channel())
-> {ok, channel()} | {stop, Reason :: term(), channel()}). -> {ok, channel()} | {stop, Reason :: term(), channel()}).
handle_info({subscribe, TopicFilters}, Channel = #channel{client = Client}) -> handle_info({subscribe, TopicFilters}, Channel = #channel{client = ClientInfo}) ->
TopicFilters1 = emqx_hooks:run_fold('client.subscribe', TopicFilters1 = emqx_hooks:run_fold('client.subscribe',
[Client, #{'Internal' => true}], [ClientInfo, #{'Internal' => true}],
parse_topic_filters(TopicFilters)), parse_topic_filters(TopicFilters)),
{_ReasonCodes, NChannel} = process_subscribe(TopicFilters1, Channel), {_ReasonCodes, NChannel} = process_subscribe(TopicFilters1, Channel),
{ok, NChannel}; {ok, NChannel};
handle_info({unsubscribe, TopicFilters}, Channel = #channel{client = Client}) -> handle_info({unsubscribe, TopicFilters}, Channel = #channel{client = ClientInfo}) ->
TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe', TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe',
[Client, #{'Internal' => true}], [ClientInfo, #{'Internal' => true}],
parse_topic_filters(TopicFilters)), parse_topic_filters(TopicFilters)),
{_ReasonCodes, NChannel} = process_unsubscribe(TopicFilters1, Channel), {_ReasonCodes, NChannel} = process_unsubscribe(TopicFilters1, Channel),
{ok, NChannel}; {ok, NChannel};
@ -670,19 +692,20 @@ handle_info(disconnected, Channel = #channel{connected = undefined}) ->
handle_info(disconnected, Channel = #channel{connected = false}) -> handle_info(disconnected, Channel = #channel{connected = false}) ->
{ok, Channel}; {ok, Channel};
handle_info(disconnected, Channel = #channel{protocol = Protocol, handle_info(disconnected, Channel = #channel{conninfo = #{expiry_interval := ExpiryInterval},
client = ClientInfo = #{zone := Zone},
session = Session, session = Session,
client = Client = #{zone := Zone}}) -> will_msg = WillMsg}) ->
emqx_zone:enable_flapping_detect(Zone) andalso emqx_flapping:detect(Client), emqx_zone:enable_flapping_detect(Zone) andalso emqx_flapping:detect(ClientInfo),
Channel1 = ensure_disconnected(Channel), Channel1 = ensure_disconnected(Channel),
Channel2 = case timer:seconds(emqx_protocol:info(will_delay_interval, Protocol)) of Channel2 = case timer:seconds(will_delay_interval(WillMsg)) of
0 -> 0 ->
publish_will_msg(emqx_protocol:info(will_msg, Protocol)), publish_will_msg(WillMsg),
Channel1#channel{protocol = emqx_protocol:clear_will_msg(Protocol)}; Channel1#channel{will_msg = undefined};
_ -> _ ->
ensure_timer(will_timer, Channel1) ensure_timer(will_timer, Channel1)
end, end,
case emqx_session:info(expiry_interval, Session) of case ExpiryInterval of
?UINT_MAX -> ?UINT_MAX ->
{ok, Channel2}; {ok, Channel2};
Int when Int > 0 -> Int when Int > 0 ->
@ -699,20 +722,19 @@ handle_info(Info, Channel) ->
%% Handle timeout %% Handle timeout
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(timeout(reference(), Msg :: term(), channel()) -spec(handle_timeout(reference(), Msg :: term(), channel())
-> {ok, channel()} -> {ok, channel()}
| {ok, Result :: term(), channel()} | {ok, Result :: term(), channel()}
| {stop, Reason :: term(), channel()}). | {stop, Reason :: term(), channel()}).
timeout(TRef, {emit_stats, Stats}, handle_timeout(TRef, {emit_stats, Stats},
Channel = #channel{client = #{client_id := ClientId}, Channel = #channel{client = #{client_id := ClientId},
timers = #{stats_timer := TRef} timers = #{stats_timer := TRef}}) ->
}) ->
ok = emqx_cm:set_chan_stats(ClientId, Stats), ok = emqx_cm:set_chan_stats(ClientId, Stats),
{ok, clean_timer(stats_timer, Channel)}; {ok, clean_timer(stats_timer, Channel)};
timeout(TRef, {keepalive, StatVal}, Channel = #channel{keepalive = Keepalive, handle_timeout(TRef, {keepalive, StatVal},
timers = #{alive_timer := TRef} Channel = #channel{keepalive = Keepalive,
}) -> timers = #{alive_timer := TRef}}) ->
case emqx_keepalive:check(StatVal, Keepalive) of case emqx_keepalive:check(StatVal, Keepalive) of
{ok, NKeepalive} -> {ok, NKeepalive} ->
NChannel = Channel#channel{keepalive = NKeepalive}, NChannel = Channel#channel{keepalive = NKeepalive},
@ -721,9 +743,9 @@ timeout(TRef, {keepalive, StatVal}, Channel = #channel{keepalive = Keepalive,
{wait_session_expire, {shutdown, keepalive_timeout}, Channel} {wait_session_expire, {shutdown, keepalive_timeout}, Channel}
end; end;
timeout(TRef, retry_delivery, Channel = #channel{session = Session, handle_timeout(TRef, retry_delivery,
timers = #{retry_timer := TRef} Channel = #channel{session = Session,
}) -> timers = #{retry_timer := TRef}}) ->
case emqx_session:retry(Session) of case emqx_session:retry(Session) of
{ok, NSession} -> {ok, NSession} ->
{ok, clean_timer(retry_timer, Channel#channel{session = NSession})}; {ok, clean_timer(retry_timer, Channel#channel{session = NSession})};
@ -735,8 +757,9 @@ timeout(TRef, retry_delivery, Channel = #channel{session = Session,
handle_out({publish, Publishes}, reset_timer(retry_timer, Timeout, NChannel)) handle_out({publish, Publishes}, reset_timer(retry_timer, Timeout, NChannel))
end; end;
timeout(TRef, expire_awaiting_rel, Channel = #channel{session = Session, handle_timeout(TRef, expire_awaiting_rel,
timers = #{await_timer := TRef}}) -> Channel = #channel{session = Session,
timers = #{await_timer := TRef}}) ->
case emqx_session:expire(awaiting_rel, Session) of case emqx_session:expire(awaiting_rel, Session) of
{ok, Session} -> {ok, Session} ->
{ok, clean_timer(await_timer, Channel#channel{session = Session})}; {ok, clean_timer(await_timer, Channel#channel{session = Session})};
@ -744,15 +767,15 @@ timeout(TRef, expire_awaiting_rel, Channel = #channel{session = Session,
{ok, reset_timer(await_timer, Timeout, Channel#channel{session = Session})} {ok, reset_timer(await_timer, Timeout, Channel#channel{session = Session})}
end; end;
timeout(TRef, expire_session, Channel = #channel{timers = #{expire_timer := TRef}}) -> handle_timeout(TRef, expire_session, Channel = #channel{timers = #{expire_timer := TRef}}) ->
shutdown(expired, Channel); shutdown(expired, Channel);
timeout(TRef, will_message, Channel = #channel{protocol = Protocol, handle_timeout(TRef, will_message, Channel = #channel{will_msg = WillMsg,
timers = #{will_timer := TRef}}) -> timers = #{will_timer := TRef}}) ->
publish_will_msg(emqx_protocol:info(will_msg, Protocol)), publish_will_msg(WillMsg),
{ok, clean_timer(will_timer, Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol)})}; {ok, clean_timer(will_timer, Channel#channel{will_msg = undefined})};
timeout(_TRef, Msg, Channel) -> handle_timeout(_TRef, Msg, Channel) ->
?LOG(error, "Unexpected timeout: ~p~n", [Msg]), ?LOG(error, "Unexpected timeout: ~p~n", [Msg]),
{ok, Channel}. {ok, Channel}.
@ -796,24 +819,28 @@ interval(retry_timer, #channel{session = Session}) ->
emqx_session:info(retry_interval, Session); emqx_session:info(retry_interval, Session);
interval(await_timer, #channel{session = Session}) -> interval(await_timer, #channel{session = Session}) ->
emqx_session:info(await_rel_timeout, Session); emqx_session:info(await_rel_timeout, Session);
interval(expire_timer, #channel{session = Session}) -> interval(expire_timer, #channel{conninfo = ConnInfo}) ->
timer:seconds(emqx_session:info(expiry_interval, Session)); timer:seconds(maps:get(expiry_interval, ConnInfo));
interval(will_timer, #channel{protocol = Protocol}) -> interval(will_timer, #channel{will_msg = WillMsg}) ->
timer:seconds(emqx_protocol:info(will_delay_interval, Protocol)). %% TODO: Ensure the header exists.
timer:seconds(will_delay_interval(WillMsg)).
will_delay_interval(undefined) -> 0;
will_delay_interval(WillMsg) ->
emqx_message:get_header('Will-Delay-Interval', WillMsg, 0).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Terminate %% Terminate
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
terminate(normal, #channel{client = Client}) -> terminate(normal, #channel{conninfo = ConnInfo, client = ClientInfo}) ->
ok = emqx_hooks:run('client.disconnected', [Client, normal]); ok = emqx_hooks:run('client.disconnected', [ClientInfo, normal, ConnInfo]);
terminate({shutdown, Reason}, #channel{client = Client}) terminate({shutdown, Reason}, #channel{conninfo = ConnInfo, client = ClientInfo,})
when Reason =:= kicked orelse Reason =:= discarded orelse Reason =:= takeovered -> when Reason =:= kicked orelse Reason =:= discarded orelse Reason =:= takeovered ->
ok = emqx_hooks:run('client.disconnected', [Client, Reason]); ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]);
terminate(Reason, #channel{client = Client, terminate(Reason, #channel{conninfo = ConnInfo, client = ClientInfo, will_msg = WillMsg}) ->
protocol = Protocol publish_will_msg(WillMsg),
}) -> ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]).
ok = emqx_hooks:run('client.disconnected', [Client, Reason]),
if if
Protocol == undefined -> ok; Protocol == undefined -> ok;
true -> publish_will_msg(emqx_protocol:info(will_msg, Protocol)) true -> publish_will_msg(emqx_protocol:info(will_msg, Protocol))
@ -833,51 +860,78 @@ publish_will_msg(undefined) ->
publish_will_msg(Msg) -> publish_will_msg(Msg) ->
emqx_broker:publish(Msg). emqx_broker:publish(Msg).
%% @doc Enrich MQTT Connect Info.
enrich_conninfo(#mqtt_packet_connect{
proto_name = ProtoName,
proto_ver = ProtoVer,
clean_start = CleanStart,
keepalive = Keepalive,
properties = ConnProps,
client_id = ClientId,
username = Username}, Channel) ->
#channel{conninfo = ConnInfo, client = #{zone := Zone}} = Channel,
MaxInflight = emqx_mqtt_props:get('Receive-Maximum',
ConnProps, emqx_zone:max_inflight(Zone)),
Interval = if ProtoVer == ?MQTT_PROTO_V5 ->
emqx_mqtt_props:get('Session-Expiry-Interval', ConnProps, 0);
true -> case CleanStart of
true -> 0;
false -> emqx_zone:session_expiry_interval(Zone)
end
end,
NConnInfo = ConnInfo#{proto_name => ProtoName,
proto_ver => ProtoVer,
clean_start => CleanStart,
keepalive => Keepalive,
client_id => ClientId,
username => Username,
conn_props => ConnProps,
receive_maximum => MaxInflight,
expiry_interval => Interval
},
{ok, Channel#channel{conninfo = NConnInfo}}.
%% @doc Check connect packet. %% @doc Check connect packet.
check_connpkt(ConnPkt, #channel{client = #{zone := Zone}}) -> check_connect(ConnPkt, #channel{client = #{zone := Zone}}) ->
emqx_packet:check(ConnPkt, emqx_mqtt_caps:get_caps(Zone)). emqx_packet:check(ConnPkt, emqx_mqtt_caps:get_caps(Zone)).
%% @doc Init protocol record.
init_protocol(ConnPkt, Channel = #channel{client = #{zone := Zone}}) ->
{ok, Channel#channel{protocol = emqx_protocol:init(ConnPkt, Zone)}}.
%% @doc Enrich client %% @doc Enrich client
enrich_client(ConnPkt, Channel = #channel{client = Client}) -> enrich_client(ConnPkt, Channel = #channel{client = ClientInfo}) ->
{ok, NConnPkt, NClient} = pipeline([fun set_username/2, {ok, NConnPkt, NClientInfo} =
fun set_bridge_mode/2, pipeline([fun set_username/2,
fun maybe_username_as_clientid/2, fun set_bridge_mode/2,
fun maybe_assign_clientid/2, fun maybe_username_as_clientid/2,
fun fix_mountpoint/2 fun maybe_assign_clientid/2,
], ConnPkt, Client), fun fix_mountpoint/2], ConnPkt, ClientInfo),
{ok, NConnPkt, Channel#channel{client = NClient}}. {ok, NConnPkt, Channel#channel{client = NClientInfo}}.
set_username(#mqtt_packet_connect{username = Username}, Client = #{username := undefined}) -> set_username(#mqtt_packet_connect{username = Username},
{ok, Client#{username => Username}}; ClientInfo = #{username := undefined}) ->
set_username(_ConnPkt, Client) -> {ok, ClientInfo#{username => Username}};
{ok, Client}. set_username(_ConnPkt, ClientInfo) ->
{ok, ClientInfo}.
set_bridge_mode(#mqtt_packet_connect{is_bridge = true}, Client) -> set_bridge_mode(#mqtt_packet_connect{is_bridge = true}, ClientInfo) ->
{ok, Client#{is_bridge => true}}; {ok, ClientInfo#{is_bridge => true}};
set_bridge_mode(_ConnPkt, _Client) -> ok. set_bridge_mode(_ConnPkt, _ClientInfo) -> ok.
maybe_username_as_clientid(_ConnPkt, Client = #{username := undefined}) -> maybe_username_as_clientid(_ConnPkt, ClientInfo = #{username := undefined}) ->
{ok, Client}; {ok, ClientInfo};
maybe_username_as_clientid(_ConnPkt, Client = #{zone := Zone, username := Username}) -> maybe_username_as_clientid(_ConnPkt, ClientInfo = #{zone := Zone, username := Username}) ->
case emqx_zone:use_username_as_clientid(Zone) of case emqx_zone:use_username_as_clientid(Zone) of
true -> {ok, Client#{client_id => Username}}; true -> {ok, ClientInfo#{client_id => Username}};
false -> ok false -> ok
end. end.
maybe_assign_clientid(#mqtt_packet_connect{client_id = <<>>}, Client) -> maybe_assign_clientid(#mqtt_packet_connect{client_id = <<>>}, ClientInfo) ->
%% Generate a rand clientId %% Generate a rand clientId
RandId = emqx_guid:to_base62(emqx_guid:gen()), {ok, ClientInfo#{client_id => emqx_guid:to_base62(emqx_guid:gen())}};
{ok, Client#{client_id => RandId}}; maybe_assign_clientid(#mqtt_packet_connect{client_id = ClientId}, ClientInfo) ->
maybe_assign_clientid(#mqtt_packet_connect{client_id = ClientId}, Client) -> {ok, ClientInfo#{client_id => ClientId}}.
{ok, Client#{client_id => ClientId}}.
fix_mountpoint(_ConnPkt, #{mountpoint := undefined}) -> ok; fix_mountpoint(_ConnPkt, #{mountpoint := undefined}) -> ok;
fix_mountpoint(_ConnPkt, Client = #{mountpoint := Mountpoint}) -> fix_mountpoint(_ConnPkt, ClientInfo = #{mountpoint := Mountpoint}) ->
{ok, Client#{mountpoint := emqx_mountpoint:replvar(Mountpoint, Client)}}. {ok, ClientInfo#{mountpoint := emqx_mountpoint:replvar(Mountpoint, ClientInfo)}}.
%% @doc Set logger metadata. %% @doc Set logger metadata.
set_logger_meta(_ConnPkt, #channel{client = #{client_id := ClientId}}) -> set_logger_meta(_ConnPkt, #channel{client = #{client_id := ClientId}}) ->
@ -887,15 +941,15 @@ set_logger_meta(_ConnPkt, #channel{client = #{client_id := ClientId}}) ->
%% Check banned/flapping %% Check banned/flapping
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
check_banned(_ConnPkt, #channel{client = Client = #{zone := Zone}}) -> check_banned(_ConnPkt, #channel{client = ClientInfo = #{zone := Zone}}) ->
case emqx_zone:enable_banned(Zone) andalso emqx_banned:check(Client) of case emqx_zone:enable_ban(Zone) andalso emqx_banned:check(ClientInfo) of
true -> {error, ?RC_BANNED}; true -> {error, ?RC_BANNED};
false -> ok false -> ok
end. end.
check_flapping(_ConnPkt, #channel{client = Client = #{zone := Zone}}) -> check_flapping(_ConnPkt, #channel{client = ClientInfo = #{zone := Zone}}) ->
case emqx_zone:enable_flapping_detect(Zone) case emqx_zone:enable_flapping_detect(Zone)
andalso emqx_flapping:check(Client) of andalso emqx_flapping:check(ClientInfo) of
true -> {error, ?RC_CONNECTION_RATE_EXCEEDED}; true -> {error, ?RC_CONNECTION_RATE_EXCEEDED};
false -> ok false -> ok
end. end.
@ -907,38 +961,16 @@ check_flapping(_ConnPkt, #channel{client = Client = #{zone := Zone}}) ->
auth_connect(#mqtt_packet_connect{client_id = ClientId, auth_connect(#mqtt_packet_connect{client_id = ClientId,
username = Username, username = Username,
password = Password}, password = Password},
Channel = #channel{client = Client}) -> Channel = #channel{client = ClientInfo}) ->
case emqx_access_control:authenticate(Client#{password => Password}) of case emqx_access_control:authenticate(ClientInfo#{password => Password}) of
{ok, AuthResult} -> {ok, AuthResult} ->
{ok, Channel#channel{client = maps:merge(Client, AuthResult)}}; {ok, Channel#channel{client = maps:merge(ClientInfo, AuthResult)}};
{error, Reason} -> {error, Reason} ->
?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p",
[ClientId, Username, Reason]), [ClientId, Username, Reason]),
{error, emqx_reason_codes:connack_error(Reason)} {error, emqx_reason_codes:connack_error(Reason)}
end. end.
%%--------------------------------------------------------------------
%% Open session
%%--------------------------------------------------------------------
open_session(#mqtt_packet_connect{clean_start = CleanStart,
properties = ConnProps},
#channel{client = Client = #{zone := Zone}, protocol = Protocol}) ->
MaxInflight = get_property('Receive-Maximum', ConnProps,
emqx_zone:get_env(Zone, max_inflight, 65535)),
Interval =
case emqx_protocol:info(proto_ver, Protocol) of
?MQTT_PROTO_V5 -> get_property('Session-Expiry-Interval', ConnProps, 0);
_ ->
case CleanStart of
true -> 0;
false -> emqx_zone:get_env(Zone, session_expiry_interval, 0)
end
end,
emqx_cm:open_session(CleanStart, Client, #{max_inflight => MaxInflight,
expiry_interval => Interval
}).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Process publish message: Client -> Broker %% Process publish message: Client -> Broker
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -948,8 +980,8 @@ process_alias(Packet = #mqtt_packet{
properties = #{'Topic-Alias' := AliasId} properties = #{'Topic-Alias' := AliasId}
} = Publish } = Publish
}, },
Channel = #channel{protocol = Protocol}) -> Channel = #channel{topic_aliases = Aliases}) ->
case emqx_protocol:find_alias(AliasId, Protocol) of case find_alias(AliasId, Aliases) of
{ok, Topic} -> {ok, Topic} ->
{ok, Packet#mqtt_packet{ {ok, Packet#mqtt_packet{
variable = Publish#mqtt_packet_publish{ variable = Publish#mqtt_packet_publish{
@ -961,23 +993,23 @@ process_alias(#mqtt_packet{
variable = #mqtt_packet_publish{topic_name = Topic, variable = #mqtt_packet_publish{topic_name = Topic,
properties = #{'Topic-Alias' := AliasId} properties = #{'Topic-Alias' := AliasId}
} }
}, Channel = #channel{protocol = Protocol}) -> }, Channel = #channel{topic_aliases = Aliases}) ->
{ok, Channel#channel{protocol = emqx_protocol:save_alias(AliasId, Topic, Protocol)}}; {ok, Channel#channel{topic_aliases = save_alias(AliasId, Topic, Aliases)}};
process_alias(_Packet, Channel) -> process_alias(_Packet, Channel) ->
{ok, Channel}. {ok, Channel}.
%% Check Publish find_alias(_AliasId, undefined) -> false;
check_publish(Packet, Channel) -> find_alias(AliasId, Aliases) -> maps:find(AliasId, Aliases).
pipeline([fun check_pub_acl/2,
fun check_pub_alias/2, save_alias(AliasId, Topic, undefined) -> #{AliasId => Topic};
fun check_pub_caps/2], Packet, Channel). save_alias(AliasId, Topic, Aliases) -> maps:put(AliasId, Topic, Aliases).
%% Check Pub ACL %% Check Pub ACL
check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}},
#channel{client = Client}) -> #channel{client = ClientInfo}) ->
case is_acl_enabled(Client) andalso case is_acl_enabled(ClientInfo) andalso
emqx_access_control:check_acl(Client, publish, Topic) of emqx_access_control:check_acl(ClientInfo, publish, Topic) of
false -> ok; false -> ok;
allow -> ok; allow -> ok;
deny -> {error, ?RC_NOT_AUTHORIZED} deny -> {error, ?RC_NOT_AUTHORIZED}
@ -989,9 +1021,8 @@ check_pub_alias(#mqtt_packet{
properties = #{'Topic-Alias' := AliasId} properties = #{'Topic-Alias' := AliasId}
} }
}, },
#channel{protocol = Protocol}) -> #channel{alias_maximum = Limits}) ->
%% TODO: Move to Protocol %% TODO: Move to Protocol
Limits = emqx_protocol:info(alias_maximum, Protocol),
case (Limits == undefined) case (Limits == undefined)
orelse (Max = maps:get(inbound, Limits, 0)) == 0 orelse (Max = maps:get(inbound, Limits, 0)) == 0
orelse (AliasId > Max) of orelse (AliasId > Max) of
@ -1016,9 +1047,9 @@ check_subscribe(TopicFilter, SubOpts, Channel) ->
end. end.
%% Check Sub ACL %% Check Sub ACL
check_sub_acl(TopicFilter, #channel{client = Client}) -> check_sub_acl(TopicFilter, #channel{client = ClientInfo}) ->
case is_acl_enabled(Client) andalso case is_acl_enabled(ClientInfo) andalso
emqx_access_control:check_acl(Client, subscribe, TopicFilter) of emqx_access_control:check_acl(ClientInfo, subscribe, TopicFilter) of
false -> allow; false -> allow;
Result -> Result Result -> Result
end. end.
@ -1032,64 +1063,63 @@ enrich_subid(#{'Subscription-Identifier' := SubId}, TopicFilters) ->
enrich_subid(_Properties, TopicFilters) -> enrich_subid(_Properties, TopicFilters) ->
TopicFilters. TopicFilters.
enrich_subopts(SubOpts, #channel{client = Client, protocol = Proto}) -> enrich_subopts(SubOpts, #channel{conninfo = #{proto_ver := ?MQTT_PROTO_V5}}) ->
#{zone := Zone, is_bridge := IsBridge} = Client, SubOpts;
case emqx_protocol:info(proto_ver, Proto) of
?MQTT_PROTO_V5 -> SubOpts;
_Ver -> Rap = flag(IsBridge),
Nl = flag(emqx_zone:get_env(Zone, ignore_loop_deliver, false)),
SubOpts#{rap => Rap, nl => Nl}
end.
enrich_caps(AckProps, #channel{client = #{zone := Zone}, protocol = Protocol}) -> enrich_subopts(SubOpts, #channel{client = #{zone := Zone, is_bridge := IsBridge}}) ->
case emqx_protocol:info(proto_ver, Protocol) of NL = flag(emqx_zone:ignore_loop_deliver(Zone)),
?MQTT_PROTO_V5 -> SubOpts#{rap => flag(IsBridge), nl => NL}.
#{max_packet_size := MaxPktSize,
max_qos_allowed := MaxQoS, enrich_caps(AckProps, #channel{conninfo = #{proto_ver := ?MQTT_PROTO_V5},
retain_available := Retain, client = #{zone := Zone}}) ->
max_topic_alias := MaxAlias, #{max_packet_size := MaxPktSize,
shared_subscription := Shared, max_qos_allowed := MaxQoS,
wildcard_subscription := Wildcard retain_available := Retain,
} = emqx_mqtt_caps:get_caps(Zone), max_topic_alias := MaxAlias,
AckProps#{'Retain-Available' => flag(Retain), shared_subscription := Shared,
'Maximum-Packet-Size' => MaxPktSize, wildcard_subscription := Wildcard
'Topic-Alias-Maximum' => MaxAlias, } = emqx_mqtt_caps:get_caps(Zone),
'Wildcard-Subscription-Available' => flag(Wildcard), AckProps#{'Retain-Available' => flag(Retain),
'Subscription-Identifier-Available' => 1, 'Maximum-Packet-Size' => MaxPktSize,
'Shared-Subscription-Available' => flag(Shared), 'Topic-Alias-Maximum' => MaxAlias,
'Maximum-QoS' => MaxQoS 'Wildcard-Subscription-Available' => flag(Wildcard),
}; 'Subscription-Identifier-Available' => 1,
_Ver -> AckProps 'Shared-Subscription-Available' => flag(Shared),
end. 'Maximum-QoS' => MaxQoS
};
enrich_caps(AckProps, _Channel) ->
AckProps.
enrich_server_keepalive(AckProps, #channel{client = #{zone := Zone}}) -> enrich_server_keepalive(AckProps, #channel{client = #{zone := Zone}}) ->
case emqx_zone:get_env(Zone, server_keepalive) of case emqx_zone:server_keepalive(Zone) of
undefined -> AckProps; undefined -> AckProps;
Keepalive -> AckProps#{'Server-Keep-Alive' => Keepalive} Keepalive -> AckProps#{'Server-Keep-Alive' => Keepalive}
end. end.
enrich_assigned_clientid(AckProps, #channel{client = #{client_id := ClientId}, enrich_assigned_clientid(AckProps, #channel{conninfo = ConnInfo,
protocol = Protocol}) -> client = #{client_id := ClientId}
case emqx_protocol:info(client_id, Protocol) of }) ->
<<>> -> %% Original ClientId. case maps:get(client_id, ConnInfo) of
<<>> -> %% Original ClientId is null.
AckProps#{'Assigned-Client-Identifier' => ClientId}; AckProps#{'Assigned-Client-Identifier' => ClientId};
_Origin -> AckProps _Origin -> AckProps
end. end.
ensure_connected(Channel) -> init_alias_maximum(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5,
Channel#channel{connected = true, connected_at = os:timestamp(), disconnected_at = undefined}. properties = Properties}, #{zone := Zone}) ->
#{outbound => emqx_mqtt_props:get('Topic-Alias-Maximum', Properties, 0),
inbound => emqx_mqtt_caps:get_caps(Zone, max_topic_alias, 0)};
init_alias_maximum(_ConnPkt, _ClientInfo) -> undefined.
ensure_disconnected(Channel) -> ensure_disconnected(Channel) ->
Channel#channel{connected = false, disconnected_at = os:timestamp()}. Channel#channel{connected = false, disconnected_at = os:timestamp()}.
ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel) -> ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel) ->
ensure_keepalive_timer(Interval, Channel); ensure_keepalive_timer(Interval, Channel);
ensure_keepalive(_AckProp, Channel = #channel{protocol = Protocol}) -> ensure_keepalive(_AckProps, Channel = #channel{conninfo = ConnInfo}) ->
case emqx_protocol:info(keepalive, Protocol) of ensure_keepalive_timer(maps:get(keepalive, ConnInfo), Channel).
0 -> Channel;
Interval -> ensure_keepalive_timer(Interval, Channel)
end.
ensure_keepalive_timer(0, Channel) -> Channel;
ensure_keepalive_timer(Interval, Channel = #channel{client = #{zone := Zone}}) -> ensure_keepalive_timer(Interval, Channel = #channel{client = #{zone := Zone}}) ->
Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75),
Keepalive = emqx_keepalive:init(round(timer:seconds(Interval) * Backoff)), Keepalive = emqx_keepalive:init(round(timer:seconds(Interval) * Backoff)),
@ -1140,11 +1170,6 @@ check_oom(OomPolicy) ->
%% Helper functions %% Helper functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
get_property(_Name, undefined, Default) ->
Default;
get_property(Name, Props, Default) ->
maps:get(Name, Props, Default).
sp(true) -> 1; sp(true) -> 1;
sp(false) -> 0. sp(false) -> 0.

View File

@ -161,15 +161,15 @@ set_chan_stats(ClientId, ChanPid, Stats) ->
present := boolean(), present := boolean(),
pendings => list()}} pendings => list()}}
| {error, Reason :: term()}). | {error, Reason :: term()}).
open_session(true, Client = #{client_id := ClientId}, Options) -> open_session(true, ClientInfo = #{client_id := ClientId}, ConnInfo) ->
CleanStart = fun(_) -> CleanStart = fun(_) ->
ok = discard_session(ClientId), ok = discard_session(ClientId),
Session = emqx_session:init(Client, Options), Session = emqx_session:init(ClientInfo, ConnInfo),
{ok, #{session => Session, present => false}} {ok, #{session => Session, present => false}}
end, end,
emqx_cm_locker:trans(ClientId, CleanStart); emqx_cm_locker:trans(ClientId, CleanStart);
open_session(false, Client = #{client_id := ClientId}, Options) -> open_session(false, ClientInfo = #{client_id := ClientId}, ConnInfo) ->
ResumeStart = fun(_) -> ResumeStart = fun(_) ->
case takeover_session(ClientId) of case takeover_session(ClientId) of
{ok, ConnMod, ChanPid, Session} -> {ok, ConnMod, ChanPid, Session} ->
@ -179,7 +179,7 @@ open_session(false, Client = #{client_id := ClientId}, Options) ->
present => true, present => true,
pendings => Pendings}}; pendings => Pendings}};
{error, not_found} -> {error, not_found} ->
Session = emqx_session:init(Client, Options), Session = emqx_session:init(ClientInfo, ConnInfo),
{ok, #{session => Session, present => false}} {ok, #{session => Session, present => false}}
end end
end, end,

View File

@ -102,9 +102,9 @@ start_link(Transport, Socket, Options) ->
info(CPid) when is_pid(CPid) -> info(CPid) when is_pid(CPid) ->
call(CPid, info); call(CPid, info);
info(Conn = #connection{chan_state = ChanState}) -> info(Conn = #connection{chan_state = ChanState}) ->
ConnInfo = info(?INFO_KEYS, Conn),
ChanInfo = emqx_channel:info(ChanState), ChanInfo = emqx_channel:info(ChanState),
maps:merge(ChanInfo, #{conninfo => maps:from_list(ConnInfo)}). SockInfo = maps:from_list(info(?INFO_KEYS, Conn)),
maps:merge(ChanInfo, #{sockinfo => SockInfo}).
info(Keys, Conn) when is_list(Keys) -> info(Keys, Conn) when is_list(Keys) ->
[{Key, info(Key, Conn)} || Key <- Keys]; [{Key, info(Key, Conn)} || Key <- Keys];
@ -133,9 +133,9 @@ limit_info(Limit) ->
attrs(CPid) when is_pid(CPid) -> attrs(CPid) when is_pid(CPid) ->
call(CPid, attrs); call(CPid, attrs);
attrs(Conn = #connection{chan_state = ChanState}) -> attrs(Conn = #connection{chan_state = ChanState}) ->
ConnAttrs = info(?ATTR_KEYS, Conn),
ChanAttrs = emqx_channel:attrs(ChanState), ChanAttrs = emqx_channel:attrs(ChanState),
maps:merge(ChanAttrs, #{conninfo => maps:from_list(ConnAttrs)}). SockAttrs = maps:from_list(info(?ATTR_KEYS, Conn)),
maps:merge(ChanAttrs, #{sockinfo => SockAttrs}).
%% @doc Get stats of the channel. %% @doc Get stats of the channel.
-spec(stats(pid()|connection()) -> emqx_types:stats()). -spec(stats(pid()|connection()) -> emqx_types:stats()).
@ -219,7 +219,7 @@ idle(timeout, _Timeout, State) ->
idle(cast, {incoming, Packet = ?CONNECT_PACKET(ConnPkt)}, State) -> idle(cast, {incoming, Packet = ?CONNECT_PACKET(ConnPkt)}, State) ->
#mqtt_packet_connect{proto_ver = ProtoVer, properties = Properties} = ConnPkt, #mqtt_packet_connect{proto_ver = ProtoVer, properties = Properties} = ConnPkt,
MaxPacketSize = emqx_mqtt_props:get_property('Maximum-Packet-Size', Properties, undefined), MaxPacketSize = emqx_mqtt_props:get('Maximum-Packet-Size', Properties, undefined),
NState = State#connection{serialize = serialize_fun(ProtoVer, MaxPacketSize)}, NState = State#connection{serialize = serialize_fun(ProtoVer, MaxPacketSize)},
SuccFun = fun(NewSt) -> {next_state, connected, NewSt} end, SuccFun = fun(NewSt) -> {next_state, connected, NewSt} end,
handle_incoming(Packet, SuccFun, NState); handle_incoming(Packet, SuccFun, NState);
@ -529,7 +529,7 @@ send(IoData, SuccFun, State = #connection{transport = Transport,
%% Handle timeout %% Handle timeout
handle_timeout(TRef, Msg, State = #connection{chan_state = ChanState}) -> handle_timeout(TRef, Msg, State = #connection{chan_state = ChanState}) ->
case emqx_channel:timeout(TRef, Msg, ChanState) of case emqx_channel:handle_timeout(TRef, Msg, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
keep_state(State#connection{chan_state = NChanState}); keep_state(State#connection{chan_state = NChanState});
{ok, Packets, NChanState} -> {ok, Packets, NChanState} ->

View File

@ -18,11 +18,12 @@
-behaviour(gen_server). -behaviour(gen_server).
-include("types.hrl").
-include("logger.hrl"). -include("logger.hrl").
-logger_header("[Ctl]"). -logger_header("[Ctl]").
-export([start_link/0]). -export([start_link/0, stop/0]).
-export([ register_command/2 -export([ register_command/2
, register_command/3 , register_command/3
@ -32,6 +33,7 @@
-export([ run_command/1 -export([ run_command/1
, run_command/2 , run_command/2
, lookup_command/1 , lookup_command/1
, get_commands/0
]). ]).
-export([ print/1 -export([ print/1
@ -40,7 +42,7 @@
, usage/2 , usage/2
]). ]).
%% format/1,2 and format_usage/1,2 are exported mainly for test cases %% Exports mainly for test cases
-export([ format/1 -export([ format/1
, format/2 , format/2
, format_usage/1 , format_usage/1
@ -59,38 +61,44 @@
-record(state, {seq = 0}). -record(state, {seq = 0}).
-type(cmd() :: atom()). -type(cmd() :: atom()).
-type(cmd_params() :: string()).
-type(cmd_descr() :: string()). -type(cmd_descr() :: string()).
-type(cmd_usage() :: {cmd(), cmd_descr()}). -type(cmd_usage() :: {cmd_params(), cmd_descr()}).
-define(SERVER, ?MODULE). -define(SERVER, ?MODULE).
-define(TAB, emqx_command). -define(CMD_TAB, emqx_command).
-spec(start_link() -> startlink_ret()).
start_link() -> start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-spec(stop() -> ok).
stop() -> gen_server:stop(?SERVER).
-spec(register_command(cmd(), {module(), atom()}) -> ok). -spec(register_command(cmd(), {module(), atom()}) -> ok).
register_command(Cmd, MF) when is_atom(Cmd) -> register_command(Cmd, MF) when is_atom(Cmd) ->
register_command(Cmd, MF, []). register_command(Cmd, MF, []).
-spec(register_command(cmd(), {module(), atom()}, list()) -> ok). -spec(register_command(cmd(), {module(), atom()}, list()) -> ok).
register_command(Cmd, MF, Opts) when is_atom(Cmd) -> register_command(Cmd, MF, Opts) when is_atom(Cmd) ->
cast({register_command, Cmd, MF, Opts}). call({register_command, Cmd, MF, Opts}).
-spec(unregister_command(cmd()) -> ok). -spec(unregister_command(cmd()) -> ok).
unregister_command(Cmd) when is_atom(Cmd) -> unregister_command(Cmd) when is_atom(Cmd) ->
cast({unregister_command, Cmd}). cast({unregister_command, Cmd}).
cast(Msg) -> call(Req) -> gen_server:call(?SERVER, Req).
gen_server:cast(?SERVER, Msg).
cast(Msg) -> gen_server:cast(?SERVER, Msg).
-spec(run_command(list(string())) -> ok | {error, term()}).
run_command([]) -> run_command([]) ->
run_command(help, []); run_command(help, []);
run_command([Cmd | Args]) -> run_command([Cmd | Args]) ->
run_command(list_to_atom(Cmd), Args). run_command(list_to_atom(Cmd), Args).
-spec(run_command(cmd(), [string()]) -> ok | {error, term()}). -spec(run_command(cmd(), list(string())) -> ok | {error, term()}).
run_command(help, []) -> run_command(help, []) -> help();
help();
run_command(Cmd, Args) when is_atom(Cmd) -> run_command(Cmd, Args) when is_atom(Cmd) ->
case lookup_command(Cmd) of case lookup_command(Cmd) of
[{Mod, Fun}] -> [{Mod, Fun}] ->
@ -107,15 +115,19 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
-spec(lookup_command(cmd()) -> [{module(), atom()}]). -spec(lookup_command(cmd()) -> [{module(), atom()}]).
lookup_command(Cmd) when is_atom(Cmd) -> lookup_command(Cmd) when is_atom(Cmd) ->
case ets:match(?TAB, {{'_', Cmd}, '$1', '_'}) of case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of
[El] -> El; [El] -> El;
[] -> [] [] -> []
end. end.
-spec(get_commands() -> list({cmd(), module(), atom()})).
get_commands() ->
[{Cmd, M, F} || {{_Seq, Cmd}, {M, F}, _Opts} <- ets:tab2list(?CMD_TAB)].
help() -> help() ->
print("Usage: ~s~n", [?MODULE]), print("Usage: ~s~n", [?MODULE]),
[begin print("~80..-s~n", [""]), Mod:Cmd(usage) end [begin print("~80..-s~n", [""]), Mod:Cmd(usage) end
|| {_, {Mod, Cmd}, _} <- ets:tab2list(?TAB)]. || {_, {Mod, Cmd}, _} <- ets:tab2list(?CMD_TAB)].
-spec(print(io:format()) -> ok). -spec(print(io:format()) -> ok).
print(Msg) -> print(Msg) ->
@ -129,13 +141,13 @@ print(Format, Args) ->
usage(UsageList) -> usage(UsageList) ->
io:format(format_usage(UsageList)). io:format(format_usage(UsageList)).
-spec(usage(cmd(), cmd_descr()) -> ok). -spec(usage(cmd_params(), cmd_descr()) -> ok).
usage(Cmd, Desc) -> usage(CmdParams, Desc) ->
io:format(format_usage(Cmd, Desc)). io:format(format_usage(CmdParams, Desc)).
-spec(format(io:format()) -> string()). -spec(format(io:format()) -> string()).
format(Msg) -> format(Msg) ->
lists:flatten(io_lib:format("~p", [Msg])). lists:flatten(io_lib:format("~s", [Msg])).
-spec(format(io:format(), [term()]) -> string()). -spec(format(io:format(), [term()]) -> string()).
format(Format, Args) -> format(Format, Args) ->
@ -144,42 +156,41 @@ format(Format, Args) ->
-spec(format_usage([cmd_usage()]) -> ok). -spec(format_usage([cmd_usage()]) -> ok).
format_usage(UsageList) -> format_usage(UsageList) ->
lists:map( lists:map(
fun({Cmd, Desc}) -> fun({CmdParams, Desc}) ->
format_usage(Cmd, Desc) format_usage(CmdParams, Desc)
end, UsageList). end, UsageList).
-spec(format_usage(cmd(), cmd_descr()) -> string()). -spec(format_usage(cmd_params(), cmd_descr()) -> string()).
format_usage(Cmd, Desc) -> format_usage(CmdParams, Desc) ->
CmdLines = split_cmd(Cmd), CmdLines = split_cmd(CmdParams),
DescLines = split_cmd(Desc), DescLines = split_cmd(Desc),
lists:foldl( lists:foldl(fun({CmdStr, DescStr}, Usage) ->
fun({CmdStr, DescStr}, Usage) -> Usage ++ format("~-48s# ~s~n", [CmdStr, DescStr])
Usage ++ format("~-48s# ~s~n", [CmdStr, DescStr]) end, "", zip_cmd(CmdLines, DescLines)).
end, "", zip_cmd(CmdLines, DescLines)).
%%------------------------------------------------------------------------------ %%--------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks
%%------------------------------------------------------------------------------ %%--------------------------------------------------------------------
init([]) -> init([]) ->
ok = emqx_tables:new(?TAB, [protected, ordered_set]), ok = emqx_tables:new(?CMD_TAB, [protected, ordered_set]),
{ok, #state{seq = 0}}. {ok, #state{seq = 0}}.
handle_call({register_command, Cmd, MF, Opts}, _From, State = #state{seq = Seq}) ->
case ets:match(?CMD_TAB, {{'$1', Cmd}, '_', '_'}) of
[] -> ets:insert(?CMD_TAB, {{Seq, Cmd}, MF, Opts});
[[OriginSeq] | _] ->
?LOG(warning, "CMD ~s is overidden by ~p", [Cmd, MF]),
true = ets:insert(?CMD_TAB, {{OriginSeq, Cmd}, MF, Opts})
end,
{reply, ok, next_seq(State)};
handle_call(Req, _From, State) -> handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]), ?LOG(error, "Unexpected call: ~p", [Req]),
{reply, ignored, State}. {reply, ignored, State}.
handle_cast({register_command, Cmd, MF, Opts}, State = #state{seq = Seq}) ->
case ets:match(?TAB, {{'$1', Cmd}, '_', '_'}) of
[] -> ets:insert(?TAB, {{Seq, Cmd}, MF, Opts});
[[OriginSeq] | _] ->
?LOG(warning, "CMD ~s is overidden by ~p", [Cmd, MF]),
ets:insert(?TAB, {{OriginSeq, Cmd}, MF, Opts})
end,
noreply(next_seq(State));
handle_cast({unregister_command, Cmd}, State) -> handle_cast({unregister_command, Cmd}, State) ->
ets:match_delete(?TAB, {{'_', Cmd}, '_', '_'}), ets:match_delete(?CMD_TAB, {{'_', Cmd}, '_', '_'}),
noreply(State); noreply(State);
handle_cast(Msg, State) -> handle_cast(Msg, State) ->
@ -214,3 +225,4 @@ zip_cmd([X | Xs], [Y | Ys]) -> [{X, Y} | zip_cmd(Xs, Ys)];
zip_cmd([X | Xs], []) -> [{X, ""} | zip_cmd(Xs, [])]; zip_cmd([X | Xs], []) -> [{X, ""} | zip_cmd(Xs, [])];
zip_cmd([], [Y | Ys]) -> [{"", Y} | zip_cmd([], Ys)]; zip_cmd([], [Y | Ys]) -> [{"", Y} | zip_cmd([], Ys)];
zip_cmd([], []) -> []. zip_cmd([], []) -> [].

View File

@ -52,7 +52,7 @@
-record(flapping, { -record(flapping, {
client_id :: emqx_types:client_id(), client_id :: emqx_types:client_id(),
peername :: emqx_types:peername(), peerhost :: emqx_types:peerhost(),
started_at :: pos_integer(), started_at :: pos_integer(),
detect_cnt :: pos_integer(), detect_cnt :: pos_integer(),
banned_at :: pos_integer() banned_at :: pos_integer()
@ -84,7 +84,7 @@ check(ClientId, #{banned_interval := Interval}) ->
-spec(detect(emqx_types:client()) -> boolean()). -spec(detect(emqx_types:client()) -> boolean()).
detect(Client) -> detect(Client, get_policy()). detect(Client) -> detect(Client, get_policy()).
detect(#{client_id := ClientId, peername := Peername}, detect(#{client_id := ClientId, peerhost := PeerHost},
Policy = #{threshold := Threshold}) -> Policy = #{threshold := Threshold}) ->
try ets:update_counter(?FLAPPING_TAB, ClientId, {#flapping.detect_cnt, 1}) of try ets:update_counter(?FLAPPING_TAB, ClientId, {#flapping.detect_cnt, 1}) of
Cnt when Cnt < Threshold -> false; Cnt when Cnt < Threshold -> false;
@ -98,7 +98,7 @@ detect(#{client_id := ClientId, peername := Peername},
error:badarg -> error:badarg ->
%% Create a flapping record. %% Create a flapping record.
Flapping = #flapping{client_id = ClientId, Flapping = #flapping{client_id = ClientId,
peername = Peername, peerhost = PeerHost,
started_at = emqx_time:now_ms(), started_at = emqx_time:now_ms(),
detect_cnt = 1 detect_cnt = 1
}, },
@ -132,7 +132,7 @@ handle_call(Req, _From, State) ->
{reply, ignored, State}. {reply, ignored, State}.
handle_cast({detected, Flapping = #flapping{client_id = ClientId, handle_cast({detected, Flapping = #flapping{client_id = ClientId,
peername = Peername, peerhost = PeerHost,
started_at = StartedAt, started_at = StartedAt,
detect_cnt = DetectCnt}, detect_cnt = DetectCnt},
#{duration := Duration}}, State) -> #{duration := Duration}}, State) ->
@ -140,7 +140,7 @@ handle_cast({detected, Flapping = #flapping{client_id = ClientId,
true -> %% Flapping happened:( true -> %% Flapping happened:(
%% Log first %% Log first
?LOG(error, "Flapping detected: ~s(~s) disconnected ~w times in ~wms", ?LOG(error, "Flapping detected: ~s(~s) disconnected ~w times in ~wms",
[ClientId, esockd_net:format(Peername), DetectCnt, Duration]), [ClientId, esockd_net:ntoa(PeerHost), DetectCnt, Duration]),
%% Banned. %% Banned.
BannedFlapping = Flapping#flapping{client_id = {banned, ClientId}, BannedFlapping = Flapping#flapping{client_id = {banned, ClientId},
banned_at = emqx_time:now_ms() banned_at = emqx_time:now_ms()
@ -149,7 +149,7 @@ handle_cast({detected, Flapping = #flapping{client_id = ClientId,
ets:insert(?FLAPPING_TAB, BannedFlapping); ets:insert(?FLAPPING_TAB, BannedFlapping);
false -> false ->
?LOG(warning, "~s(~s) disconnected ~w times in ~wms", ?LOG(warning, "~s(~s) disconnected ~w times in ~wms",
[ClientId, esockd_net:format(Peername), DetectCnt, Interval]), [ClientId, esockd_net:ntoa(PeerHost), DetectCnt, Interval]),
ets:delete_object(?FLAPPING_TAB, Flapping) ets:delete_object(?FLAPPING_TAB, Flapping)
end, end,
{noreply, State}; {noreply, State};

View File

@ -34,7 +34,7 @@
, reset/1 , reset/1
]). ]).
-export_type([gc_state/0]). -export_type([opts/0, gc_state/0]).
-type(opts() :: #{count => integer(), -type(opts() :: #{count => integer(),
bytes => integer()}). bytes => integer()}).

View File

@ -38,6 +38,7 @@
%% Flags %% Flags
-export([ get_flag/2 -export([ get_flag/2
, get_flag/3 , get_flag/3
, get_flags/1
, set_flag/2 , set_flag/2
, set_flag/3 , set_flag/3
, set_flags/2 , set_flags/2
@ -85,6 +86,7 @@ make(From, QoS, Topic, Payload) when ?QOS_0 =< QoS, QoS =< ?QOS_2 ->
qos = QoS, qos = QoS,
from = From, from = From,
flags = #{dup => false}, flags = #{dup => false},
headers = #{},
topic = Topic, topic = Topic,
payload = Payload, payload = Payload,
timestamp = os:timestamp()}. timestamp = os:timestamp()}.
@ -119,6 +121,9 @@ get_flag(Flag, Msg) ->
get_flag(Flag, #message{flags = Flags}, Default) -> get_flag(Flag, #message{flags = Flags}, Default) ->
maps:get(Flag, Flags, Default). maps:get(Flag, Flags, Default).
-spec(get_flags(emqx_types:message()) -> maybe(map())).
get_flags(#message{flags = Flags}) -> Flags.
-spec(set_flag(flag(), emqx_types:message()) -> emqx_types:message()). -spec(set_flag(flag(), emqx_types:message()) -> emqx_types:message()).
set_flag(Flag, Msg = #message{flags = undefined}) when is_atom(Flag) -> set_flag(Flag, Msg = #message{flags = undefined}) when is_atom(Flag) ->
Msg#message{flags = #{Flag => true}}; Msg#message{flags = #{Flag => true}};
@ -144,8 +149,7 @@ unset_flag(Flag, Msg = #message{flags = Flags}) ->
set_headers(Headers, Msg = #message{headers = undefined}) when is_map(Headers) -> set_headers(Headers, Msg = #message{headers = undefined}) when is_map(Headers) ->
Msg#message{headers = Headers}; Msg#message{headers = Headers};
set_headers(New, Msg = #message{headers = Old}) when is_map(New) -> set_headers(New, Msg = #message{headers = Old}) when is_map(New) ->
Msg#message{headers = maps:merge(Old, New)}; Msg#message{headers = maps:merge(Old, New)}.
set_headers(undefined, Msg) -> Msg.
-spec(get_headers(emqx_types:message()) -> map()). -spec(get_headers(emqx_types:message()) -> map()).
get_headers(Msg) -> get_headers(Msg) ->

View File

@ -23,70 +23,78 @@
-logger_header("[Presence]"). -logger_header("[Presence]").
%% APIs
-export([ on_client_connected/4
, on_client_disconnected/3
]).
%% emqx_gen_mod callbacks %% emqx_gen_mod callbacks
-export([ load/1 -export([ load/1
, unload/1 , unload/1
]). ]).
%%-------------------------------------------------------------------- -export([ on_client_connected/4
%% APIs , on_client_disconnected/4
%%-------------------------------------------------------------------- ]).
load(_Env) -> -ifdef(TEST).
ok. -export([ reason/1 ]).
%% emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Env]}), -endif.
%% emqx_hooks:add('client.disconnected', {?MODULE, on_client_disconnected, [Env]}).
on_client_connected(#{client_id := ClientId, load(Env) ->
username := Username, emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Env]}),
peername := {IpAddr, _} emqx_hooks:add('client.disconnected', {?MODULE, on_client_disconnected, [Env]}).
}, ConnAck,
#{session := Session,
proto_name := ProtoName,
proto_ver := ProtoVer,
keepalive := Keepalive
}, Env) ->
case emqx_json:safe_encode(maps:merge(#{clientid => ClientId,
username => Username,
ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)),
proto_name => ProtoName,
proto_ver => ProtoVer,
keepalive => Keepalive,
connack => ConnAck,
ts => erlang:system_time(millisecond)
}, maps:with([clean_start, expiry_interval], Session))) of
{ok, Payload} ->
emqx:publish(message(qos(Env), topic(connected, ClientId), Payload));
{error, Reason} ->
?LOG(error, "Encoding connected event error: ~p", [Reason])
end.
on_client_disconnected(#{client_id := ClientId,
username := Username}, Reason, Env) ->
case emqx_json:safe_encode(#{clientid => ClientId,
username => Username,
reason => reason(Reason),
ts => erlang:system_time(millisecond)
}) of
{ok, Payload} ->
emqx_broker:publish(message(qos(Env), topic(disconnected, ClientId), Payload));
{error, Reason} ->
?LOG(error, "Encoding disconnected event error: ~p", [Reason])
end.
unload(_Env) -> unload(_Env) ->
emqx_hooks:del('client.connected', {?MODULE, on_client_connected}), emqx_hooks:del('client.connected', {?MODULE, on_client_connected}),
emqx_hooks:del('client.disconnected', {?MODULE, on_client_disconnected}). emqx_hooks:del('client.disconnected', {?MODULE, on_client_disconnected}).
message(QoS, Topic, Payload) -> on_client_connected(ClientInfo, ConnAck, ConnInfo, Env) ->
#{peerhost := PeerHost} = ClientInfo,
#{clean_start := CleanStart,
proto_name := ProtoName,
proto_ver := ProtoVer,
keepalive := Keepalive,
expiry_interval := ExpiryInterval} = ConnInfo,
ClientId = clientid(ClientInfo, ConnInfo),
Username = username(ClientInfo, ConnInfo),
Presence = #{clientid => ClientId,
username => Username,
ipaddress => ntoa(PeerHost),
proto_name => ProtoName,
proto_ver => ProtoVer,
keepalive => Keepalive,
connack => ConnAck,
clean_start => CleanStart,
expiry_interval => ExpiryInterval,
ts => emqx_time:now_ms()
},
case emqx_json:safe_encode(Presence) of
{ok, Payload} ->
emqx_broker:safe_publish(
make_msg(qos(Env), topic(connected, ClientId), Payload));
{error, _Reason} ->
?LOG(error, "Failed to encode 'connected' presence: ~p", [Presence])
end.
on_client_disconnected(ClientInfo, Reason, ConnInfo, Env) ->
ClientId = clientid(ClientInfo, ConnInfo),
Username = username(ClientInfo, ConnInfo),
Presence = #{clientid => ClientId,
username => Username,
reason => reason(Reason),
ts => emqx_time:now_ms()
},
case emqx_json:safe_encode(Presence) of
{ok, Payload} ->
emqx_broker:safe_publish(
make_msg(qos(Env), topic(disconnected, ClientId), Payload));
{error, _Reason} ->
?LOG(error, "Failed to encode 'disconnected' presence: ~p", [Presence])
end.
clientid(#{client_id := undefined}, #{client_id := ClientId}) -> ClientId;
clientid(#{client_id := ClientId}, _ConnInfo) -> ClientId.
username(#{username := undefined}, #{username := Username}) -> Username;
username(#{username := Username}, _ConnInfo) -> Username.
make_msg(QoS, Topic, Payload) ->
emqx_message:set_flag( emqx_message:set_flag(
sys, emqx_message:make( sys, emqx_message:make(
?MODULE, QoS, Topic, iolist_to_binary(Payload))). ?MODULE, QoS, Topic, iolist_to_binary(Payload))).
@ -99,6 +107,10 @@ topic(disconnected, ClientId) ->
qos(Env) -> proplists:get_value(qos, Env, 0). qos(Env) -> proplists:get_value(qos, Env, 0).
reason(Reason) when is_atom(Reason) -> Reason; reason(Reason) when is_atom(Reason) -> Reason;
reason({shutdown, Reason}) when is_atom(Reason) -> Reason;
reason({Error, _}) when is_atom(Error) -> Error; reason({Error, _}) when is_atom(Error) -> Error;
reason(_) -> internal_error. reason(_) -> internal_error.
-compile({inline, [ntoa/1]}).
ntoa(IpAddr) -> iolist_to_binary(esockd_net:ntoa(IpAddr)).

View File

@ -22,8 +22,9 @@
-include_lib("emqx_mqtt.hrl"). -include_lib("emqx_mqtt.hrl").
-ifdef(TEST). -ifdef(TEST).
-compile(export_all). -export([ compile/1
-compile(nowarn_export_all). , match_and_rewrite/2
]).
-endif. -endif.
%% APIs %% APIs
@ -47,14 +48,14 @@ load(RawRules) ->
emqx_hooks:add('client.unsubscribe', {?MODULE, rewrite_unsubscribe, [Rules]}), emqx_hooks:add('client.unsubscribe', {?MODULE, rewrite_unsubscribe, [Rules]}),
emqx_hooks:add('message.publish', {?MODULE, rewrite_publish, [Rules]}). emqx_hooks:add('message.publish', {?MODULE, rewrite_publish, [Rules]}).
rewrite_subscribe(_Client, _Properties, TopicFilters, Rules) -> rewrite_subscribe(_ClientInfo, _Properties, TopicFilters, Rules) ->
{ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicFilters]}. {ok, [{match_and_rewrite(Topic, Rules), Opts} || {Topic, Opts} <- TopicFilters]}.
rewrite_unsubscribe(_Client, _Properties, TopicFilters, Rules) -> rewrite_unsubscribe(_ClientInfo, _Properties, TopicFilters, Rules) ->
{ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicFilters]}. {ok, [{match_and_rewrite(Topic, Rules), Opts} || {Topic, Opts} <- TopicFilters]}.
rewrite_publish(Message = #message{topic = Topic}, Rules) -> rewrite_publish(Message = #message{topic = Topic}, Rules) ->
{ok, Message#message{topic = match_rule(Topic, Rules)}}. {ok, Message#message{topic = match_and_rewrite(Topic, Rules)}}.
unload(_) -> unload(_) ->
emqx_hooks:del('client.subscribe', {?MODULE, rewrite_subscribe}), emqx_hooks:del('client.subscribe', {?MODULE, rewrite_subscribe}),
@ -65,16 +66,22 @@ unload(_) ->
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
match_rule(Topic, []) -> compile(Rules) ->
lists:map(fun({rewrite, Topic, Re, Dest}) ->
{ok, MP} = re:compile(Re),
{rewrite, Topic, MP, Dest}
end, Rules).
match_and_rewrite(Topic, []) ->
Topic; Topic;
match_rule(Topic, [{rewrite, Filter, MP, Dest} | Rules]) -> match_and_rewrite(Topic, [{rewrite, Filter, MP, Dest} | Rules]) ->
case emqx_topic:match(Topic, Filter) of case emqx_topic:match(Topic, Filter) of
true -> match_regx(Topic, MP, Dest); true -> rewrite(Topic, MP, Dest);
false -> match_rule(Topic, Rules) false -> match_and_rewrite(Topic, Rules)
end. end.
match_regx(Topic, MP, Dest) -> rewrite(Topic, MP, Dest) ->
case re:run(Topic, MP, [{capture, all_but_first, list}]) of case re:run(Topic, MP, [{capture, all_but_first, list}]) of
{match, Captured} -> {match, Captured} ->
Vars = lists:zip(["\\$" ++ integer_to_list(I) Vars = lists:zip(["\\$" ++ integer_to_list(I)
@ -86,8 +93,3 @@ match_regx(Topic, MP, Dest) ->
nomatch -> Topic nomatch -> Topic
end. end.
compile(Rules) ->
lists:map(fun({rewrite, Topic, Re, Dest}) ->
{ok, MP} = re:compile(Re),
{rewrite, Topic, MP, Dest}
end, Rules).

View File

@ -21,14 +21,14 @@
-include_lib("emqx.hrl"). -include_lib("emqx.hrl").
-include_lib("emqx_mqtt.hrl"). -include_lib("emqx_mqtt.hrl").
%% APIs
-export([on_client_connected/4]).
%% emqx_gen_mod callbacks %% emqx_gen_mod callbacks
-export([ load/1 -export([ load/1
, unload/1 , unload/1
]). ]).
%% APIs
-export([on_client_connected/4]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Load/Unload Hook %% Load/Unload Hook
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -37,7 +37,7 @@ load(Topics) ->
emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Topics]}). emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Topics]}).
on_client_connected(#{client_id := ClientId, on_client_connected(#{client_id := ClientId,
username := Username}, ?RC_SUCCESS, _ConnAttrs, Topics) -> username := Username}, ?RC_SUCCESS, _ConnInfo, Topics) ->
Replace = fun(Topic) -> Replace = fun(Topic) ->
rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic))
end, end,

View File

@ -26,6 +26,7 @@
-export([ get_caps/1 -export([ get_caps/1
, get_caps/2 , get_caps/2
, get_caps/3
]). ]).
-export([default/0]). -export([default/0]).
@ -114,10 +115,13 @@ get_caps(Zone) ->
-spec(get_caps(emqx_zone:zone(), publish|subscribe) -> caps()). -spec(get_caps(emqx_zone:zone(), publish|subscribe) -> caps()).
get_caps(Zone, publish) -> get_caps(Zone, publish) ->
with_env(Zone, '$mqtt_pub_caps', fun pub_caps/1); with_env(Zone, '$mqtt_pub_caps', fun pub_caps/1);
get_caps(Zone, subscribe) -> get_caps(Zone, subscribe) ->
with_env(Zone, '$mqtt_sub_caps', fun sub_caps/1). with_env(Zone, '$mqtt_sub_caps', fun sub_caps/1).
-spec(get_caps(emqx_zone:zone(), atom(), term()) -> term()).
get_caps(Zone, Cap, Def) ->
emqx_zone:get_env(Zone, Cap, Def).
pub_caps(Zone) -> pub_caps(Zone) ->
filter_caps(?PUBCAP_KEYS, get_caps(Zone)). filter_caps(?PUBCAP_KEYS, get_caps(Zone)).

View File

@ -28,8 +28,8 @@
%% For tests %% For tests
-export([all/0]). -export([all/0]).
-export([ set_property/3 -export([ set/3
, get_property/3 , get/3
]). ]).
-type(prop_name() :: atom()). -type(prop_name() :: atom()).
@ -183,13 +183,13 @@ validate_value(_Type, _Val) -> false.
-spec(all() -> map()). -spec(all() -> map()).
all() -> ?PROPS_TABLE. all() -> ?PROPS_TABLE.
set_property(Name, Value, undefined) -> set(Name, Value, undefined) ->
#{Name => Value}; #{Name => Value};
set_property(Name, Value, Props) -> set(Name, Value, Props) ->
Props#{Name => Value}. Props#{Name => Value}.
get_property(_Name, undefined, Default) -> get(_Name, undefined, Default) ->
Default; Default;
get_property(Name, Props, Default) -> get(Name, Props, Default) ->
maps:get(Name, Props, Default). maps:get(Name, Props, Default).

View File

@ -28,7 +28,7 @@
, info/1 , info/1
]). ]).
-export_type([oom_policy/0]). -export_type([opts/0, oom_policy/0]).
-type(opts() :: #{message_queue_len => non_neg_integer(), -type(opts() :: #{message_queue_len => non_neg_integer(),
max_heap_size => non_neg_integer() max_heap_size => non_neg_integer()

View File

@ -89,7 +89,7 @@ proto_name(#mqtt_packet_connect{proto_name = Name}) ->
%% @doc Protocol version of the CONNECT Packet. %% @doc Protocol version of the CONNECT Packet.
-spec(proto_ver(emqx_types:packet()|connect()) -> emqx_types:version()). -spec(proto_ver(emqx_types:packet()|connect()) -> emqx_types:version()).
proto_ver(?CONNACK_PACKET(ConnPkt)) -> proto_ver(?CONNECT_PACKET(ConnPkt)) ->
proto_ver(ConnPkt); proto_ver(ConnPkt);
proto_ver(#mqtt_packet_connect{proto_ver = Ver}) -> proto_ver(#mqtt_packet_connect{proto_ver = Ver}) ->
Ver. Ver.
@ -241,7 +241,7 @@ validate_topic_filters(TopicFilters) ->
%% @doc Publish Packet to Message. %% @doc Publish Packet to Message.
-spec(to_message(emqx_types:client(), emqx_ypes:packet()) -> emqx_types:message()). -spec(to_message(emqx_types:client(), emqx_ypes:packet()) -> emqx_types:message()).
to_message(#{client_id := ClientId, username := Username, peername := Peername}, to_message(#{client_id := ClientId, username := Username, peerhost := PeerHost},
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
retain = Retain, retain = Retain,
qos = QoS, qos = QoS,
@ -252,7 +252,7 @@ to_message(#{client_id := ClientId, username := Username, peername := Peername},
Msg = emqx_message:make(ClientId, QoS, Topic, Payload), Msg = emqx_message:make(ClientId, QoS, Topic, Payload),
Msg#message{flags = #{dup => Dup, retain => Retain}, Msg#message{flags = #{dup => Dup, retain => Retain},
headers = merge_props(#{username => Username, headers = merge_props(#{username => Username,
peername => Peername}, Props)}. peerhost => PeerHost}, Props)}.
-spec(will_msg(#mqtt_packet_connect{}) -> emqx_types:message()). -spec(will_msg(#mqtt_packet_connect{}) -> emqx_types:message()).
will_msg(#mqtt_packet_connect{will_flag = false}) -> will_msg(#mqtt_packet_connect{will_flag = false}) ->

View File

@ -1,133 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% MQTT Protocol
-module(emqx_protocol).
-include("types.hrl").
-include("emqx_mqtt.hrl").
-export([ init/2
, info/1
, info/2
, attrs/1
]).
-export([ find_alias/2
, save_alias/3
, clear_will_msg/1
]).
-export_type([protocol/0]).
-record(protocol, {
%% MQTT Proto Name
proto_name :: binary(),
%% MQTT Proto Version
proto_ver :: emqx_types:ver(),
%% Clean Start Flag
clean_start :: boolean(),
%% MQTT Keepalive interval
keepalive :: non_neg_integer(),
%% ClientId in CONNECT Packet
client_id :: emqx_types:client_id(),
%% Username in CONNECT Packet
username :: emqx_types:username(),
%% MQTT Will Msg
will_msg :: emqx_types:message(),
%% MQTT Topic Aliases
topic_aliases :: maybe(map()),
%% MQTT Topic Alias Maximum
alias_maximum :: maybe(map())
}).
-opaque(protocol() :: #protocol{}).
-define(INFO_KEYS, record_info(fields, protocol)).
-define(ATTR_KEYS, [proto_name, proto_ver, clean_start, keepalive]).
-spec(init(#mqtt_packet_connect{}, atom()) -> protocol()).
init(#mqtt_packet_connect{proto_name = ProtoName,
proto_ver = ProtoVer,
clean_start = CleanStart,
keepalive = Keepalive,
properties = Properties,
client_id = ClientId,
username = Username} = ConnPkt, Zone) ->
WillMsg = emqx_packet:will_msg(ConnPkt),
#protocol{proto_name = ProtoName,
proto_ver = ProtoVer,
clean_start = CleanStart,
keepalive = Keepalive,
client_id = ClientId,
username = Username,
will_msg = WillMsg,
alias_maximum = #{outbound => emqx_mqtt_props:get_property('Topic-Alias-Maximum', Properties, 0),
inbound => maps:get(max_topic_alias, emqx_mqtt_caps:get_caps(Zone), 0)}
}.
-spec(info(protocol()) -> emqx_types:infos()).
info(Proto) ->
maps:from_list(info(?INFO_KEYS, Proto)).
-spec(info(atom()|list(atom()), protocol()) -> term()).
info(Keys, Proto) when is_list(Keys) ->
[{Key, info(Key, Proto)} || Key <- Keys];
info(proto_name, #protocol{proto_name = ProtoName}) ->
ProtoName;
info(proto_ver, #protocol{proto_ver = ProtoVer}) ->
ProtoVer;
info(clean_start, #protocol{clean_start = CleanStart}) ->
CleanStart;
info(keepalive, #protocol{keepalive = Keepalive}) ->
Keepalive;
info(client_id, #protocol{client_id = ClientId}) ->
ClientId;
info(username, #protocol{username = Username}) ->
Username;
info(will_msg, #protocol{will_msg = WillMsg}) ->
WillMsg;
info(will_delay_interval, #protocol{will_msg = undefined}) ->
0;
info(will_delay_interval, #protocol{will_msg = WillMsg}) ->
emqx_message:get_header('Will-Delay-Interval', WillMsg, 0);
info(topic_aliases, #protocol{topic_aliases = Aliases}) ->
Aliases;
info(alias_maximum, #protocol{alias_maximum = AliasMaximum}) ->
AliasMaximum.
-spec(attrs(protocol()) -> emqx_types:attrs()).
attrs(Proto) ->
maps:from_list(info(?ATTR_KEYS, Proto)).
-spec(find_alias(emqx_types:alias_id(), protocol())
-> {ok, emqx_types:topic()} | false).
find_alias(_AliasId, #protocol{topic_aliases = undefined}) ->
false;
find_alias(AliasId, #protocol{topic_aliases = Aliases}) ->
maps:find(AliasId, Aliases).
-spec(save_alias(emqx_types:alias_id(), emqx_types:topic(), protocol())
-> protocol()).
save_alias(AliasId, Topic, Proto = #protocol{topic_aliases = undefined}) ->
Proto#protocol{topic_aliases = #{AliasId => Topic}};
save_alias(AliasId, Topic, Proto = #protocol{topic_aliases = Aliases}) ->
Proto#protocol{topic_aliases = maps:put(AliasId, Topic, Aliases)}.
clear_will_msg(Protocol) ->
Protocol#protocol{will_msg = undefined}.

View File

@ -61,8 +61,6 @@
%% Exports for unit tests %% Exports for unit tests
-export([set_field/3]). -export([set_field/3]).
-export([update_expiry_interval/2]).
-export([ subscribe/4 -export([ subscribe/4
, unsubscribe/3 , unsubscribe/3
]). ]).
@ -116,13 +114,10 @@
max_awaiting_rel :: non_neg_integer(), max_awaiting_rel :: non_neg_integer(),
%% Awaiting PUBREL Timeout %% Awaiting PUBREL Timeout
await_rel_timeout :: timeout(), await_rel_timeout :: timeout(),
%% Session Expiry Interval
expiry_interval :: timeout(),
%% Enqueue Count %% Enqueue Count
enqueue_cnt :: non_neg_integer(), enqueue_cnt :: non_neg_integer(),
%% Created at %% Created at
created_at :: erlang:timestamp() created_at :: erlang:timestamp()
}). }).
-opaque(session() :: #session{}). -opaque(session() :: #session{}).
@ -130,11 +125,12 @@
-type(publish() :: {publish, emqx_types:packet_id(), emqx_types:message()}). -type(publish() :: {publish, emqx_types:packet_id(), emqx_types:message()}).
-define(DEFAULT_BATCH_N, 1000). -define(DEFAULT_BATCH_N, 1000).
-define(ATTR_KEYS, [expiry_interval, created_at]). -define(ATTR_KEYS, [max_inflight, max_mqueue, retry_interval,
max_awaiting_rel, await_rel_timeout, created_at]).
-define(INFO_KEYS, [subscriptions, max_subscriptions, upgrade_qos, inflight, -define(INFO_KEYS, [subscriptions, max_subscriptions, upgrade_qos, inflight,
max_inflight, retry_interval, mqueue_len, max_mqueue, max_inflight, retry_interval, mqueue_len, max_mqueue,
mqueue_dropped, next_pkt_id, awaiting_rel, max_awaiting_rel, mqueue_dropped, next_pkt_id, awaiting_rel, max_awaiting_rel,
await_rel_timeout, expiry_interval, created_at]). await_rel_timeout, created_at]).
-define(STATS_KEYS, [subscriptions_cnt, max_subscriptions, inflight, max_inflight, -define(STATS_KEYS, [subscriptions_cnt, max_subscriptions, inflight, max_inflight,
mqueue_len, max_mqueue, mqueue_dropped, awaiting_rel, mqueue_len, max_mqueue, mqueue_dropped, awaiting_rel,
max_awaiting_rel, enqueue_cnt]). max_awaiting_rel, enqueue_cnt]).
@ -145,8 +141,7 @@
%% @doc Init a session. %% @doc Init a session.
-spec(init(emqx_types:client(), Options :: map()) -> session()). -spec(init(emqx_types:client(), Options :: map()) -> session()).
init(#{zone := Zone}, #{max_inflight := MaxInflight, init(#{zone := Zone}, #{receive_maximum := MaxInflight}) ->
expiry_interval := ExpiryInterval}) ->
#session{max_subscriptions = get_env(Zone, max_subscriptions, 0), #session{max_subscriptions = get_env(Zone, max_subscriptions, 0),
subscriptions = #{}, subscriptions = #{},
upgrade_qos = get_env(Zone, upgrade_qos, false), upgrade_qos = get_env(Zone, upgrade_qos, false),
@ -157,7 +152,6 @@ init(#{zone := Zone}, #{max_inflight := MaxInflight,
awaiting_rel = #{}, awaiting_rel = #{},
max_awaiting_rel = get_env(Zone, max_awaiting_rel, 100), max_awaiting_rel = get_env(Zone, max_awaiting_rel, 100),
await_rel_timeout = get_env(Zone, await_rel_timeout, 3600*1000), await_rel_timeout = get_env(Zone, await_rel_timeout, 3600*1000),
expiry_interval = ExpiryInterval,
enqueue_cnt = 0, enqueue_cnt = 0,
created_at = os:timestamp() created_at = os:timestamp()
}. }.
@ -213,8 +207,6 @@ info(max_awaiting_rel, #session{max_awaiting_rel = MaxAwaitingRel}) ->
MaxAwaitingRel; MaxAwaitingRel;
info(await_rel_timeout, #session{await_rel_timeout = Timeout}) -> info(await_rel_timeout, #session{await_rel_timeout = Timeout}) ->
Timeout; Timeout;
info(expiry_interval, #session{expiry_interval = Interval}) ->
Interval;
info(enqueue_cnt, #session{enqueue_cnt = Cnt}) -> info(enqueue_cnt, #session{enqueue_cnt = Cnt}) ->
Cnt; Cnt;
info(created_at, #session{created_at = CreatedAt}) -> info(created_at, #session{created_at = CreatedAt}) ->
@ -226,9 +218,6 @@ set_field(Name, Val, Channel) ->
Pos = emqx_misc:index_of(Name, Fields), Pos = emqx_misc:index_of(Name, Fields),
setelement(Pos+1, Channel, Val). setelement(Pos+1, Channel, Val).
update_expiry_interval(ExpiryInterval, Session) ->
Session#session{expiry_interval = ExpiryInterval}.
-spec(takeover(session()) -> ok). -spec(takeover(session()) -> ok).
takeover(#session{subscriptions = Subs}) -> takeover(#session{subscriptions = Subs}) ->
lists:foreach(fun({TopicFilter, _SubOpts}) -> lists:foreach(fun({TopicFilter, _SubOpts}) ->

View File

@ -36,6 +36,7 @@
, client_id/0 , client_id/0
, username/0 , username/0
, password/0 , password/0
, peerhost/0
, peername/0 , peername/0
, protocol/0 , protocol/0
]). ]).
@ -102,9 +103,7 @@
atom() => term() atom() => term()
}). }).
-type(client() :: #{zone := zone(), -type(client() :: #{zone := zone(),
conn_mod := maybe(module()), peerhost := peerhost(),
peername := peername(),
sockname := peername(),
client_id := client_id(), client_id := client_id(),
username := username(), username := username(),
peercert := esockd_peercert:peercert(), peercert := esockd_peercert:peercert(),
@ -117,9 +116,10 @@
anonymous => boolean(), anonymous => boolean(),
atom() => term() atom() => term()
}). }).
-type(client_id() :: binary() | atom()). -type(client_id() :: binary()|atom()).
-type(username() :: maybe(binary())). -type(username() :: maybe(binary())).
-type(password() :: maybe(binary())). -type(password() :: maybe(binary())).
-type(peerhost() :: inet:ip_address()).
-type(peername() :: {inet:ip_address(), inet:port_number()}). -type(peername() :: {inet:ip_address(), inet:port_number()}).
-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). -type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()).
-type(auth_result() :: success -type(auth_result() :: success

View File

@ -74,9 +74,9 @@
info(WsPid) when is_pid(WsPid) -> info(WsPid) when is_pid(WsPid) ->
call(WsPid, info); call(WsPid, info);
info(WsConn = #ws_connection{chan_state = ChanState}) -> info(WsConn = #ws_connection{chan_state = ChanState}) ->
ConnInfo = info(?INFO_KEYS, WsConn),
ChanInfo = emqx_channel:info(ChanState), ChanInfo = emqx_channel:info(ChanState),
maps:merge(ChanInfo, #{conninfo => maps:from_list(ConnInfo)}). SockInfo = maps:from_list(info(?INFO_KEYS, WsConn)),
maps:merge(ChanInfo, #{sockinfo => SockInfo}).
info(Keys, WsConn) when is_list(Keys) -> info(Keys, WsConn) when is_list(Keys) ->
[{Key, info(Key, WsConn)} || Key <- Keys]; [{Key, info(Key, WsConn)} || Key <- Keys];
@ -95,9 +95,9 @@ info(chan_state, #ws_connection{chan_state = ChanState}) ->
attrs(WsPid) when is_pid(WsPid) -> attrs(WsPid) when is_pid(WsPid) ->
call(WsPid, attrs); call(WsPid, attrs);
attrs(WsConn = #ws_connection{chan_state = ChanState}) -> attrs(WsConn = #ws_connection{chan_state = ChanState}) ->
ConnAttrs = info(?ATTR_KEYS, WsConn),
ChanAttrs = emqx_channel:attrs(ChanState), ChanAttrs = emqx_channel:attrs(ChanState),
maps:merge(ChanAttrs, #{conninfo => maps:from_list(ConnAttrs)}). SockAttrs = maps:from_list(info(?ATTR_KEYS, WsConn)),
maps:merge(ChanAttrs, #{sockinfo => SockAttrs}).
-spec(stats(pid()|ws_connection()) -> emqx_types:stats()). -spec(stats(pid()|ws_connection()) -> emqx_types:stats()).
stats(WsPid) when is_pid(WsPid) -> stats(WsPid) when is_pid(WsPid) ->
@ -273,7 +273,7 @@ websocket_info({incoming, {error, _Reason}}, State = #ws_connection{fsm_state =
websocket_info({incoming, Packet = ?CONNECT_PACKET(ConnPkt)}, websocket_info({incoming, Packet = ?CONNECT_PACKET(ConnPkt)},
State = #ws_connection{fsm_state = idle}) -> State = #ws_connection{fsm_state = idle}) ->
#mqtt_packet_connect{proto_ver = ProtoVer, properties = Properties} = ConnPkt, #mqtt_packet_connect{proto_ver = ProtoVer, properties = Properties} = ConnPkt,
MaxPacketSize = emqx_mqtt_props:get_property('Maximum-Packet-Size', Properties, undefined), MaxPacketSize = emqx_mqtt_props:get('Maximum-Packet-Size', Properties, undefined),
NState = State#ws_connection{serialize = serialize_fun(ProtoVer, MaxPacketSize)}, NState = State#ws_connection{serialize = serialize_fun(ProtoVer, MaxPacketSize)},
handle_incoming(Packet, fun connected/1, NState); handle_incoming(Packet, fun connected/1, NState);
@ -341,7 +341,7 @@ disconnected(State) ->
%% Handle timeout %% Handle timeout
handle_timeout(TRef, Msg, State = #ws_connection{chan_state = ChanState}) -> handle_timeout(TRef, Msg, State = #ws_connection{chan_state = ChanState}) ->
case emqx_channel:timeout(TRef, Msg, ChanState) of case emqx_channel:handle_timeout(TRef, Msg, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
{ok, State#ws_connection{chan_state = NChanState}}; {ok, State#ws_connection{chan_state = NChanState}};
{ok, Packets, NChanState} -> {ok, Packets, NChanState} ->

View File

@ -28,18 +28,26 @@
-export([start_link/0, stop/0]). -export([start_link/0, stop/0]).
-export([ use_username_as_clientid/1 -export([ use_username_as_clientid/1
, enable_stats/1
, enable_acl/1 , enable_acl/1
, enable_banned/1 , enable_ban/1
, enable_flapping_detect/1 , enable_flapping_detect/1
, ignore_loop_deliver/1
, server_keepalive/1
, max_inflight/1
, session_expiry_interval/1
, force_gc_policy/1
, force_shutdown_policy/1
]). ]).
-export([ get_env/2 -export([ get_env/2
, get_env/3 , get_env/3
, set_env/3 , set_env/3
, unset_env/2 , unset_env/2
, force_reload/0
]). ]).
-export([force_reload/0]).
%% gen_server callbacks %% gen_server callbacks
-export([ init/1 -export([ init/1
, handle_call/3 , handle_call/3
@ -72,18 +80,46 @@ start_link() ->
use_username_as_clientid(Zone) -> use_username_as_clientid(Zone) ->
get_env(Zone, use_username_as_clientid, false). get_env(Zone, use_username_as_clientid, false).
-spec(enable_stats(zone()) -> boolean()).
enable_stats(Zone) ->
get_env(Zone, enable_stats, true).
-spec(enable_acl(zone()) -> boolean()). -spec(enable_acl(zone()) -> boolean()).
enable_acl(Zone) -> enable_acl(Zone) ->
get_env(Zone, enable_acl, true). get_env(Zone, enable_acl, true).
-spec(enable_banned(zone()) -> boolean()). -spec(enable_ban(zone()) -> boolean()).
enable_banned(Zone) -> enable_ban(Zone) ->
get_env(Zone, enable_banned, false). get_env(Zone, enable_ban, false).
-spec(enable_flapping_detect(zone()) -> boolean()). -spec(enable_flapping_detect(zone()) -> boolean()).
enable_flapping_detect(Zone) -> enable_flapping_detect(Zone) ->
get_env(Zone, enable_flapping_detect, false). get_env(Zone, enable_flapping_detect, false).
-spec(ignore_loop_deliver(zone()) -> boolean()).
ignore_loop_deliver(Zone) ->
get_env(Zone, ignore_loop_deliver, false).
-spec(server_keepalive(zone()) -> pos_integer()).
server_keepalive(Zone) ->
get_env(Zone, server_keepalive).
-spec(max_inflight(zone()) -> 0..65535).
max_inflight(Zone) ->
get_env(Zone, max_inflight, 65535).
-spec(session_expiry_interval(zone()) -> non_neg_integer()).
session_expiry_interval(Zone) ->
get_env(Zone, session_expiry_interval, 0).
-spec(force_gc_policy(zone()) -> maybe(emqx_gc:opts())).
force_gc_policy(Zone) ->
get_env(Zone, force_gc_policy).
-spec(force_shutdown_policy(zone()) -> maybe(emqx_oom:opts())).
force_shutdown_policy(Zone) ->
get_env(Zone, force_shutdown_policy).
-spec(get_env(maybe(zone()), atom()) -> maybe(term())). -spec(get_env(maybe(zone()), atom()) -> maybe(term())).
get_env(undefined, Key) -> emqx:get_env(Key); get_env(undefined, Key) -> emqx:get_env(Key);
get_env(Zone, Key) -> get_env(Zone, Key) ->

View File

@ -344,35 +344,35 @@ t_compile_rule(_) ->
{deny, all} = compile({deny, all}). {deny, all} = compile({deny, all}).
t_match_rule(_) -> t_match_rule(_) ->
Client1 = #{zone => external, ClientInfo1 = #{zone => external,
client_id => <<"testClient">>, client_id => <<"testClient">>,
username => <<"TestUser">>, username => <<"TestUser">>,
peername => {{127,0,0,1}, 2948} peerhost => {127,0,0,1}
}, },
Client2 = #{zone => external, ClientInfo2 = #{zone => external,
client_id => <<"testClient">>, client_id => <<"testClient">>,
username => <<"TestUser">>, username => <<"TestUser">>,
peername => {{192,168,0,10}, 3028} peerhost => {192,168,0,10}
}, },
{matched, allow} = match(Client1, <<"Test/Topic">>, {allow, all}), {matched, allow} = match(ClientInfo1, <<"Test/Topic">>, {allow, all}),
{matched, deny} = match(Client1, <<"Test/Topic">>, {deny, all}), {matched, deny} = match(ClientInfo1, <<"Test/Topic">>, {deny, all}),
{matched, allow} = match(Client1, <<"Test/Topic">>, {matched, allow} = match(ClientInfo1, <<"Test/Topic">>,
compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]})), compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]})),
{matched, allow} = match(Client2, <<"Test/Topic">>, {matched, allow} = match(ClientInfo2, <<"Test/Topic">>,
compile({allow, {ipaddr, "192.168.0.1/24"}, subscribe, ["$SYS/#", "#"]})), compile({allow, {ipaddr, "192.168.0.1/24"}, subscribe, ["$SYS/#", "#"]})),
{matched, allow} = match(Client1, <<"d/e/f/x">>, {matched, allow} = match(ClientInfo1, <<"d/e/f/x">>,
compile({allow, {user, "TestUser"}, subscribe, ["a/b/c", "d/e/f/#"]})), compile({allow, {user, "TestUser"}, subscribe, ["a/b/c", "d/e/f/#"]})),
nomatch = match(Client1, <<"d/e/f/x">>, compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]})), nomatch = match(ClientInfo1, <<"d/e/f/x">>, compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]})),
{matched, allow} = match(Client1, <<"testTopics/testClient">>, {matched, allow} = match(ClientInfo1, <<"testTopics/testClient">>,
compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]})), compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]})),
{matched, allow} = match(Client1, <<"clients/testClient">>, compile({allow, all, pubsub, ["clients/%c"]})), {matched, allow} = match(ClientInfo1, <<"clients/testClient">>, compile({allow, all, pubsub, ["clients/%c"]})),
{matched, allow} = match(#{username => <<"user2">>}, <<"users/user2/abc/def">>, {matched, allow} = match(#{username => <<"user2">>}, <<"users/user2/abc/def">>,
compile({allow, all, subscribe, ["users/%u/#"]})), compile({allow, all, subscribe, ["users/%u/#"]})),
{matched, deny} = match(Client1, <<"d/e/f">>, compile({deny, all, subscribe, ["$SYS/#", "#"]})), {matched, deny} = match(ClientInfo1, <<"d/e/f">>, compile({deny, all, subscribe, ["$SYS/#", "#"]})),
Rule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, <<"Topic">>}), Rule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, <<"Topic">>}),
nomatch = match(Client1, <<"Topic">>, Rule), nomatch = match(ClientInfo1, <<"Topic">>, Rule),
AndRule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"TestUser">>}]}, publish, <<"Topic">>}), AndRule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"TestUser">>}]}, publish, <<"Topic">>}),
{matched, allow} = match(Client1, <<"Topic">>, AndRule), {matched, allow} = match(ClientInfo1, <<"Topic">>, AndRule),
OrRule = compile({allow, {'or', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, ["Topic"]}), OrRule = compile({allow, {'or', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, ["Topic"]}),
{matched, allow} = match(Client1, <<"Topic">>, OrRule). {matched, allow} = match(ClientInfo1, <<"Topic">>, OrRule).

View File

@ -27,6 +27,8 @@ all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
application:load(emqx), application:load(emqx),
ok = ekka:start(), ok = ekka:start(),
%% for coverage
ok = emqx_banned:mnesia(copy),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
@ -51,32 +53,43 @@ t_check(_) ->
ok = emqx_banned:add(#banned{who = {username, <<"BannedUser">>}}), ok = emqx_banned:add(#banned{who = {username, <<"BannedUser">>}}),
ok = emqx_banned:add(#banned{who = {ipaddr, {192,168,0,1}}}), ok = emqx_banned:add(#banned{who = {ipaddr, {192,168,0,1}}}),
?assertEqual(3, emqx_banned:info(size)), ?assertEqual(3, emqx_banned:info(size)),
Client1 = #{client_id => <<"BannedClient">>, ClientInfo1 = #{client_id => <<"BannedClient">>,
username => <<"user">>, username => <<"user">>,
peername => {{127,0,0,1}, 5000} peerhost => {127,0,0,1}
}, },
Client2 = #{client_id => <<"client">>, ClientInfo2 = #{client_id => <<"client">>,
username => <<"BannedUser">>, username => <<"BannedUser">>,
peername => {{127,0,0,1}, 5000} peerhost => {127,0,0,1}
}, },
Client3 = #{client_id => <<"client">>, ClientInfo3 = #{client_id => <<"client">>,
username => <<"user">>, username => <<"user">>,
peername => {{192,168,0,1}, 5000} peerhost => {192,168,0,1}
}, },
Client4 = #{client_id => <<"client">>, ClientInfo4 = #{client_id => <<"client">>,
username => <<"user">>, username => <<"user">>,
peername => {{127,0,0,1}, 5000} peerhost => {127,0,0,1}
}, },
?assert(emqx_banned:check(Client1)), ?assert(emqx_banned:check(ClientInfo1)),
?assert(emqx_banned:check(Client2)), ?assert(emqx_banned:check(ClientInfo2)),
?assert(emqx_banned:check(Client3)), ?assert(emqx_banned:check(ClientInfo3)),
?assertNot(emqx_banned:check(Client4)), ?assertNot(emqx_banned:check(ClientInfo4)),
ok = emqx_banned:delete({client_id, <<"BannedClient">>}), ok = emqx_banned:delete({client_id, <<"BannedClient">>}),
ok = emqx_banned:delete({username, <<"BannedUser">>}), ok = emqx_banned:delete({username, <<"BannedUser">>}),
ok = emqx_banned:delete({ipaddr, {192,168,0,1}}), ok = emqx_banned:delete({ipaddr, {192,168,0,1}}),
?assertNot(emqx_banned:check(Client1)), ?assertNot(emqx_banned:check(ClientInfo1)),
?assertNot(emqx_banned:check(Client2)), ?assertNot(emqx_banned:check(ClientInfo2)),
?assertNot(emqx_banned:check(Client3)), ?assertNot(emqx_banned:check(ClientInfo3)),
?assertNot(emqx_banned:check(Client4)), ?assertNot(emqx_banned:check(ClientInfo4)),
?assertEqual(0, emqx_banned:info(size)). ?assertEqual(0, emqx_banned:info(size)).
t_unused(_) ->
{ok, Banned} = emqx_banned:start_link(),
ok = emqx_banned:add(#banned{who = {client_id, <<"BannedClient">>},
until = erlang:system_time(second)
}),
?assertEqual(ignored, gen_server:call(Banned, unexpected_req)),
?assertEqual(ok, gen_server:cast(Banned, unexpected_msg)),
?assertEqual(ok, Banned ! ok),
timer:sleep(500), %% expiry timer
ok = emqx_banned:stop().

View File

@ -176,13 +176,20 @@ t_handle_deliver(_) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
t_handle_connack(_) -> t_handle_connack(_) ->
ConnPkt = #mqtt_packet_connect{
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4,
clean_start = true,
properties = #{},
client_id = <<"clientid">>
},
with_channel( with_channel(
fun(Channel) -> fun(Channel) ->
{ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _} {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _}
= handle_out({connack, ?RC_SUCCESS, 0}, Channel), = handle_out({connack, ?RC_SUCCESS, 0, ConnPkt}, Channel),
{stop, {shutdown, not_authorized}, {stop, {shutdown, not_authorized},
?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _}
= handle_out({connack, ?RC_NOT_AUTHORIZED}, Channel) = handle_out({connack, ?RC_NOT_AUTHORIZED, ConnPkt}, Channel)
end). end).
t_handle_out_publish(_) -> t_handle_out_publish(_) ->
@ -271,30 +278,31 @@ t_terminate(_) ->
%% Helper functions %% Helper functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
with_channel(Fun) -> with_channel(TestFun) ->
ConnInfo = #{peername => {{127,0,0,1}, 3456}, ConnInfo = #{peername => {{127,0,0,1}, 3456},
sockname => {{127,0,0,1}, 1883}, sockname => {{127,0,0,1}, 1883},
conn_mod => emqx_connection,
proto_name => <<"MQTT">>,
proto_ver => ?MQTT_PROTO_V5,
clean_start => true,
keepalive => 30,
client_id => <<"clientid">>, client_id => <<"clientid">>,
username => <<"username">> username => <<"username">>,
conn_props => #{},
receive_maximum => 100,
expiry_interval => 60
}, },
Options = [{zone, testing}], ClientInfo = #{zone => <<"external">>,
Channel = emqx_channel:init(ConnInfo, Options), peerhost => {127,0,0,1},
ConnPkt = #mqtt_packet_connect{ client_id => <<"clientid">>,
proto_name = <<"MQTT">>, username => <<"username">>,
proto_ver = ?MQTT_PROTO_V5, peercert => undefined,
clean_start = true, is_bridge => false,
keepalive = 30, is_superuser => false,
properties = #{}, mountpoint => undefined
client_id = <<"clientid">>, },
username = <<"username">>, Channel = emqx_channel:init(ConnInfo, [{zone, testing}]),
password = <<"passwd">> Session = emqx_session:init(ClientInfo, ConnInfo),
}, Channel1 = emqx_channel:set_field(client, ClientInfo, Channel),
Protocol = emqx_protocol:init(ConnPkt, testing), TestFun(emqx_channel:set_field(session, Session, Channel1)).
Session = emqx_session:init(#{zone => testing},
#{max_inflight => 100,
expiry_interval => 0
}),
Fun(emqx_channel:set_field(protocol, Protocol,
emqx_channel:set_field(
session, Session, Channel))).

View File

@ -14,39 +14,47 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_mod_subscription_SUITE). -module(emqx_cm_SUITE).
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include("emqx_mqtt.hrl").
-include("emqx.hrl"). -include("emqx.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]),
emqx_ct_helpers:start_apps([emqx]),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([emqx]). emqx_ct_helpers:stop_apps([]).
t_reg_unreg_channel(_) ->
error(not_implemented).
t_get_set_chan_attrs(_) ->
error(not_implemented).
t_get_set_chan_stats(_) ->
error(not_implemented).
t_open_session(_) ->
error(not_implemented).
t_discard_session(_) ->
error(not_implemented).
t_takeover_session(_) ->
error(not_implemented).
t_lookup_channels(_) ->
error(not_implemented).
t_lock_clientid(_) ->
error(not_implemented).
t_unlock_clientid(_) ->
error(not_implemented).
t_mod_subscription(_) ->
emqx_mod_subscription:load([{<<"connected/%c/%u">>, ?QOS_0}]),
{ok, C} = emqtt:start_link([{host, "localhost"}, {client_id, "myclient"}, {username, "admin"}]),
{ok, _} = emqtt:connect(C),
% ct:sleep(100),
emqtt:publish(C, <<"connected/myclient/admin">>, <<"Hello world">>, ?QOS_0),
receive
{publish, #{topic := Topic, payload := Payload}} ->
?assertEqual(<<"connected/myclient/admin">>, Topic),
?assertEqual(<<"Hello world">>, Payload)
after 100 ->
ct:fail("no_message")
end,
ok = emqtt:disconnect(C),
emqx_mod_subscription:unload([]).

View File

@ -25,30 +25,101 @@
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
emqx_ct_helpers:boot_modules([]),
emqx_ct_helpers:start_apps([]),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]). ok.
t_command(_) -> %%--------------------------------------------------------------------
emqx_ctl:start_link(), %% Test cases
emqx_ctl:register_command(test, {?MODULE, test}), %%--------------------------------------------------------------------
ct:sleep(50),
?assertEqual([{emqx_ctl_SUITE,test}], emqx_ctl:lookup_command(test)),
?assertEqual(ok, emqx_ctl:run_command(["test", "ok"])),
?assertEqual({error, test_failed}, emqx_ctl:run_command(["test", "error"])),
?assertEqual({error, cmd_not_found}, emqx_ctl:run_command(["test2", "ok"])),
emqx_ctl:unregister_command(test),
ct:sleep(50),
?assertEqual([], emqx_ctl:lookup_command(test)).
test(["ok"]) -> t_reg_unreg_command(_) ->
ok; with_ctl_server(
test(["error"]) -> fun(_CtlSrv) ->
error(test_failed); emqx_ctl:register_command(cmd1, {?MODULE, cmd1_fun}),
test(_) -> emqx_ctl:register_command(cmd2, {?MODULE, cmd2_fun}),
io:format("Hello world"). ?assertEqual([{?MODULE, cmd1_fun}], emqx_ctl:lookup_command(cmd1)),
?assertEqual([{?MODULE, cmd2_fun}], emqx_ctl:lookup_command(cmd2)),
?assertEqual([{cmd1, ?MODULE, cmd1_fun}, {cmd2, ?MODULE, cmd2_fun}],
emqx_ctl:get_commands()),
emqx_ctl:unregister_command(cmd1),
emqx_ctl:unregister_command(cmd2),
ct:sleep(100),
?assertEqual([], emqx_ctl:lookup_command(cmd1)),
?assertEqual([], emqx_ctl:lookup_command(cmd2)),
?assertEqual([], emqx_ctl:get_commands())
end).
t_run_commands(_) ->
with_ctl_server(
fun(_CtlSrv) ->
?assertEqual({error, cmd_not_found}, emqx_ctl:run_command(["cmd", "arg"])),
emqx_ctl:register_command(cmd1, {?MODULE, cmd1_fun}),
emqx_ctl:register_command(cmd2, {?MODULE, cmd2_fun}),
ok = emqx_ctl:run_command(["cmd1", "arg"]),
{error, badarg} = emqx_ctl:run_command(["cmd1", "badarg"]),
ok = emqx_ctl:run_command(["cmd2", "arg1", "arg2"]),
{error, badarg} = emqx_ctl:run_command(["cmd2", "arg1", "badarg"])
end).
t_print(_) ->
ok = emqx_ctl:print("help"),
ok = emqx_ctl:print("~s", [help]),
% - check the output of the usage
print_mock(),
?assertEqual("help", emqx_ctl:print("help")),
?assertEqual("help", emqx_ctl:print("~s", [help])).
t_usage(_) ->
CmdParams1 = "emqx_cmd_1 param1 param2",
CmdDescr1 = "emqx_cmd_1 is a test command means nothing",
Output1 = "emqx_cmd_1 param1 param2 # emqx_cmd_1 is a test command means nothing\n",
% - usage/1,2 should return ok
ok = emqx_ctl:usage([{CmdParams1, CmdDescr1}, {CmdParams1, CmdDescr1}]),
ok = emqx_ctl:usage(CmdParams1, CmdDescr1),
% - check the output of the usage
print_mock(),
?assertEqual(Output1, emqx_ctl:usage(CmdParams1, CmdDescr1)),
?assertEqual([Output1, Output1], emqx_ctl:usage([{CmdParams1, CmdDescr1}, {CmdParams1, CmdDescr1}])),
% - for the commands or descriptions have multi-lines
CmdParams2 = "emqx_cmd_2 param1 param2",
CmdDescr2 = "emqx_cmd_2 is a test command\nmeans nothing",
Output2 = "emqx_cmd_2 param1 param2 # emqx_cmd_2 is a test command\n"
" ""# means nothing\n",
?assertEqual(Output2, emqx_ctl:usage(CmdParams2, CmdDescr2)),
?assertEqual([Output2, Output2], emqx_ctl:usage([{CmdParams2, CmdDescr2}, {CmdParams2, CmdDescr2}])).
t_unexpected(_) ->
with_ctl_server(
fun(CtlSrv) ->
ignored = gen_server:call(CtlSrv, unexpected_call),
ok = gen_server:cast(CtlSrv, unexpected_cast),
CtlSrv ! unexpected_info,
?assert(is_process_alive(CtlSrv))
end).
%%--------------------------------------------------------------------
%% Cmds for test
%%--------------------------------------------------------------------
cmd1_fun(["arg"]) -> ok;
cmd1_fun(["badarg"]) -> error(badarg).
cmd2_fun(["arg1", "arg2"]) -> ok;
cmd2_fun(["arg1", "badarg"]) -> error(badarg).
with_ctl_server(Fun) ->
{ok, Pid} = emqx_ctl:start_link(),
_ = Fun(Pid),
ok = emqx_ctl:stop().
print_mock() ->
%% proxy usage/1,2 and print/1,2 to format_xx/1,2 funcs
meck:new(emqx_ctl, [non_strict, passthrough]),
meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end),
meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end),
meck:expect(emqx_ctl, usage, fun(Usages) -> emqx_ctl:format_usage(Usages) end),
meck:expect(emqx_ctl, usage, fun(CmdParams, CmdDescr) -> emqx_ctl:format_usage(CmdParams, CmdDescr) end).

View File

@ -1,17 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_ctl_SUTIES).

View File

@ -40,18 +40,18 @@ end_per_suite(_Config) ->
ok. ok.
t_detect_check(_) -> t_detect_check(_) ->
Client = #{zone => external, ClientInfo = #{zone => external,
client_id => <<"clientid">>, client_id => <<"clientid">>,
peername => {{127,0,0,1}, 5000} peerhost => {127,0,0,1}
}, },
false = emqx_flapping:detect(Client), false = emqx_flapping:detect(ClientInfo),
false = emqx_flapping:check(Client), false = emqx_flapping:check(ClientInfo),
false = emqx_flapping:detect(Client), false = emqx_flapping:detect(ClientInfo),
false = emqx_flapping:check(Client), false = emqx_flapping:check(ClientInfo),
true = emqx_flapping:detect(Client), true = emqx_flapping:detect(ClientInfo),
timer:sleep(50), timer:sleep(50),
true = emqx_flapping:check(Client), true = emqx_flapping:check(ClientInfo),
timer:sleep(300), timer:sleep(300),
false = emqx_flapping:check(Client), false = emqx_flapping:check(ClientInfo),
ok = emqx_flapping:stop(). ok = emqx_flapping:stop().

View File

@ -84,8 +84,9 @@ t_is_empty(_) ->
?assert(emqx_inflight:is_empty(Inflight1)). ?assert(emqx_inflight:is_empty(Inflight1)).
t_window(_) -> t_window(_) ->
?assertEqual([], emqx_inflight:window(emqx_inflight:new(0))),
Inflight = emqx_inflight:insert( Inflight = emqx_inflight:insert(
b, 2, emqx_inflight:insert( b, 2, emqx_inflight:insert(
a, 1, emqx_inflight:new(2))), a, 1, emqx_inflight:new(2))),
[a, b] = emqx_inflight:window(Inflight). ?assertEqual([a, b], emqx_inflight:window(Inflight)).

View File

@ -19,22 +19,10 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-export([ t_make/1
, t_flag/1
, t_header/1
, t_format/1
, t_expired/1
, t_to_packet/1
, t_to_map/1
]).
-export([ all/0
, suite/0
]).
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
suite() -> suite() ->
@ -55,7 +43,12 @@ t_make(_) ->
?assertEqual(<<"topic">>, emqx_message:topic(Msg2)), ?assertEqual(<<"topic">>, emqx_message:topic(Msg2)),
?assertEqual(<<"payload">>, emqx_message:payload(Msg2)). ?assertEqual(<<"payload">>, emqx_message:payload(Msg2)).
t_flag(_) -> t_get_set_flags(_) ->
Msg = #message{id = <<"id">>, qos = ?QOS_1, flags = undefined},
Msg1 = emqx_message:set_flags(#{retain => true}, Msg),
?assertEqual(#{retain => true}, emqx_message:get_flags(Msg1)).
t_get_set_flag(_) ->
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
Msg2 = emqx_message:set_flag(retain, false, Msg), Msg2 = emqx_message:set_flag(retain, false, Msg),
Msg3 = emqx_message:set_flag(dup, Msg2), Msg3 = emqx_message:set_flag(dup, Msg2),
@ -63,32 +56,62 @@ t_flag(_) ->
?assertNot(emqx_message:get_flag(retain, Msg3)), ?assertNot(emqx_message:get_flag(retain, Msg3)),
Msg4 = emqx_message:unset_flag(dup, Msg3), Msg4 = emqx_message:unset_flag(dup, Msg3),
Msg5 = emqx_message:unset_flag(retain, Msg4), Msg5 = emqx_message:unset_flag(retain, Msg4),
Msg5 = emqx_message:unset_flag(badflag, Msg5),
?assertEqual(undefined, emqx_message:get_flag(dup, Msg5, undefined)), ?assertEqual(undefined, emqx_message:get_flag(dup, Msg5, undefined)),
?assertEqual(undefined, emqx_message:get_flag(retain, Msg5, undefined)), ?assertEqual(undefined, emqx_message:get_flag(retain, Msg5, undefined)),
Msg6 = emqx_message:set_flags(#{dup => true, retain => true}, Msg5), Msg6 = emqx_message:set_flags(#{dup => true, retain => true}, Msg5),
?assert(emqx_message:get_flag(dup, Msg6)), ?assert(emqx_message:get_flag(dup, Msg6)),
?assert(emqx_message:get_flag(retain, Msg6)). ?assert(emqx_message:get_flag(retain, Msg6)),
Msg7 = #message{id = <<"id">>, qos = ?QOS_1, flags = undefined},
Msg8 = emqx_message:set_flag(retain, Msg7),
Msg9 = emqx_message:set_flag(retain, true, Msg7),
?assertEqual(#{retain => true}, emqx_message:get_flags(Msg8)),
?assertEqual(#{retain => true}, emqx_message:get_flags(Msg9)).
t_header(_) -> t_get_set_headers(_) ->
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
Msg1 = emqx_message:set_headers(#{a => 1, b => 2}, Msg), Msg1 = emqx_message:set_headers(#{a => 1, b => 2}, Msg),
Msg2 = emqx_message:set_header(c, 3, Msg1), Msg2 = emqx_message:set_headers(#{c => 3}, Msg1),
?assertEqual(1, emqx_message:get_header(a, Msg2)), ?assertEqual(#{a => 1, b => 2, c => 3}, emqx_message:get_headers(Msg2)).
t_get_set_header(_) ->
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
Msg1 = emqx_message:set_header(a, 1, Msg),
Msg2 = emqx_message:set_header(b, 2, Msg1),
Msg3 = emqx_message:set_header(c, 3, Msg2),
?assertEqual(1, emqx_message:get_header(a, Msg3)),
?assertEqual(4, emqx_message:get_header(d, Msg2, 4)), ?assertEqual(4, emqx_message:get_header(d, Msg2, 4)),
Msg3 = emqx_message:remove_header(a, Msg2), Msg4 = emqx_message:remove_header(a, Msg3),
?assertEqual(#{b => 2, c => 3}, emqx_message:get_headers(Msg3)). Msg4 = emqx_message:remove_header(a, Msg4),
?assertEqual(#{b => 2, c => 3}, emqx_message:get_headers(Msg4)).
t_undefined_headers(_) ->
Msg = #message{id = <<"id">>, qos = ?QOS_0, headers = undefined},
Msg1 = emqx_message:set_headers(#{a => 1, b => 2}, Msg),
?assertEqual(1, emqx_message:get_header(a, Msg1)),
Msg2 = emqx_message:set_header(c, 3, Msg),
?assertEqual(3, emqx_message:get_header(c, Msg2)).
t_format(_) -> t_format(_) ->
io:format("~s", [emqx_message:format(emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>))]). Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
io:format("~s~n", [emqx_message:format(Msg)]),
Msg1 = #message{id = <<"id">>,
qos = ?QOS_0,
flags = undefined,
headers = undefined
},
io:format("~s~n", [emqx_message:format(Msg1)]).
t_expired(_) -> t_expired(_) ->
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
?assertNot(emqx_message:is_expired(Msg)),
Msg1 = emqx_message:set_headers(#{'Message-Expiry-Interval' => 1}, Msg), Msg1 = emqx_message:set_headers(#{'Message-Expiry-Interval' => 1}, Msg),
timer:sleep(500), timer:sleep(500),
?assertNot(emqx_message:is_expired(Msg1)), ?assertNot(emqx_message:is_expired(Msg1)),
timer:sleep(600), timer:sleep(600),
?assert(emqx_message:is_expired(Msg1)), ?assert(emqx_message:is_expired(Msg1)),
timer:sleep(1000), timer:sleep(1000),
Msg = emqx_message:update_expiry(Msg),
Msg2 = emqx_message:update_expiry(Msg1), Msg2 = emqx_message:update_expiry(Msg1),
?assertEqual(1, emqx_message:get_header('Message-Expiry-Interval', Msg2)). ?assertEqual(1, emqx_message:get_header('Message-Expiry-Interval', Msg2)).

View File

@ -1,50 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_mod_rewrite_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-define(rules, [{rewrite,<<"x/#">>,<<"^x/y/(.+)$">>,<<"z/y/$1">>},
{rewrite,<<"y/+/z/#">>,<<"^y/(.+)/z/(.+)$">>,<<"y/z/$2">>}]).
all() -> emqx_ct:all(?MODULE).
t_rewrite_rule(_Config) ->
{ok, _} = emqx_hooks:start_link(),
ok = emqx_mod_rewrite:load(?rules),
RawTopicFilters = [{<<"x/y/2">>, opts},
{<<"x/1/2">>, opts},
{<<"y/a/z/b">>, opts},
{<<"y/def">>, opts}],
SubTopicFilters = emqx_hooks:run_fold('client.subscribe', [client, properties], RawTopicFilters),
UnSubTopicFilters = emqx_hooks:run_fold('client.unsubscribe', [client, properties], RawTopicFilters),
Messages = [emqx_hooks:run_fold('message.publish', [], emqx_message:make(Topic, <<"payload">>))
|| {Topic, _Opts} <- RawTopicFilters],
ExpectedTopicFilters = [{<<"z/y/2">>, opts},
{<<"x/1/2">>, opts},
{<<"y/z/b">>, opts},
{<<"y/def">>, opts}],
?assertEqual(ExpectedTopicFilters, SubTopicFilters),
?assertEqual(ExpectedTopicFilters, UnSubTopicFilters),
[?assertEqual(ExpectedTopic, emqx_message:topic(Message))
|| {{ExpectedTopic, _opts}, Message} <- lists:zip(ExpectedTopicFilters, Messages)],
ok = emqx_mod_rewrite:unload(?rules),
ok = emqx_hooks:stop().

154
test/emqx_modules_SUITE.erl Normal file
View File

@ -0,0 +1,154 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_modules_SUITE).
%% API
-compile(export_all).
-compile(nowarn_export_all).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
%%-include_lib("proper/include/proper.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
%%-define(PROPTEST(M,F), true = proper:quickcheck(M:F())).
-define(RULES, [{rewrite,<<"x/#">>,<<"^x/y/(.+)$">>,<<"z/y/$1">>},
{rewrite,<<"y/+/z/#">>,<<"^y/(.+)/z/(.+)$">>,<<"y/z/$2">>}
]).
all() -> emqx_ct:all(?MODULE).
suite() ->
[{ct_hooks,[cth_surefire]}, {timetrap, {seconds, 30}}].
init_per_suite(Config) ->
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:start_apps([emqx]),
%% Ensure all the modules unloaded.
ok = emqx_modules:unload(),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([emqx]).
%%--------------------------------------------------------------------
%% Test cases
%%--------------------------------------------------------------------
%% Test case for emqx_mod_presence
t_mod_presence(_) ->
ok = emqx_mod_presence:load([{qos, ?QOS_1}]),
{ok, C1} = emqtt:start_link([{client_id, <<"monsys">>}]),
{ok, _} = emqtt:connect(C1),
{ok, _Props, [?QOS_1]} = emqtt:subscribe(C1, <<"$SYS/brokers/+/clients/#">>, qos1),
%% Connected Presence
{ok, C2} = emqtt:start_link([{client_id, <<"clientid">>},
{username, <<"username">>}]),
{ok, _} = emqtt:connect(C2),
ok = recv_and_check_presence(<<"clientid">>, <<"connected">>),
%% Disconnected Presence
ok = emqtt:disconnect(C2),
ok = recv_and_check_presence(<<"clientid">>, <<"disconnected">>),
ok = emqtt:disconnect(C1),
ok = emqx_mod_presence:unload([{qos, ?QOS_1}]).
t_mod_presence_reason(_) ->
?assertEqual(normal, emqx_mod_presence:reason(normal)),
?assertEqual(discarded, emqx_mod_presence:reason({shutdown, discarded})),
?assertEqual(tcp_error, emqx_mod_presence:reason({tcp_error, einval})),
?assertEqual(internal_error, emqx_mod_presence:reason(<<"unknown error">>)).
recv_and_check_presence(ClientId, Presence) ->
{ok, #{qos := ?QOS_1, topic := Topic, payload := Payload}} = receive_publish(100),
?assertMatch([<<"$SYS">>, <<"brokers">>, _Node, <<"clients">>, ClientId, Presence],
binary:split(Topic, <<"/">>, [global])),
case Presence of
<<"connected">> ->
?assertMatch(#{clientid := <<"clientid">>,
username := <<"username">>,
ipaddress := <<"127.0.0.1">>,
proto_name := <<"MQTT">>,
proto_ver := ?MQTT_PROTO_V4,
connack := ?RC_SUCCESS,
clean_start := true}, emqx_json:decode(Payload, [{labels, atom}, return_maps]));
<<"disconnected">> ->
?assertMatch(#{clientid := <<"clientid">>,
username := <<"username">>,
reason := <<"normal">>}, emqx_json:decode(Payload, [{labels, atom}, return_maps]))
end.
%% Test case for emqx_mod_subscription
t_mod_subscription(_) ->
emqx_mod_subscription:load([{<<"connected/%c/%u">>, ?QOS_0}]),
{ok, C} = emqtt:start_link([{host, "localhost"},
{client_id, "myclient"},
{username, "admin"}]),
{ok, _} = emqtt:connect(C),
emqtt:publish(C, <<"connected/myclient/admin">>, <<"Hello world">>, ?QOS_0),
{ok, #{topic := Topic, payload := Payload}} = receive_publish(100),
?assertEqual(<<"connected/myclient/admin">>, Topic),
?assertEqual(<<"Hello world">>, Payload),
ok = emqtt:disconnect(C),
emqx_mod_subscription:unload([]).
%% Test case for emqx_mod_write
t_mod_rewrite(_Config) ->
ok = emqx_mod_rewrite:load(?RULES),
{ok, C} = emqtt:start_link([{client_id, <<"rewrite_client">>}]),
{ok, _} = emqtt:connect(C),
OrigTopics = [<<"x/y/2">>, <<"x/1/2">>, <<"y/a/z/b">>, <<"y/def">>],
DestTopics = [<<"z/y/2">>, <<"x/1/2">>, <<"y/z/b">>, <<"y/def">>],
%% Subscribe
{ok, _Props, _} = emqtt:subscribe(C, [{Topic, ?QOS_1} || Topic <- OrigTopics]),
timer:sleep(100),
Subscriptions = emqx_broker:subscriptions(<<"rewrite_client">>),
?assertEqual(DestTopics, [Topic || {Topic, _SubOpts} <- Subscriptions]),
%% Publish
RecvTopics = [begin
ok = emqtt:publish(C, Topic, <<"payload">>),
{ok, #{topic := RecvTopic}} = receive_publish(100),
RecvTopic
end || Topic <- OrigTopics],
?assertEqual(DestTopics, RecvTopics),
%% Unsubscribe
{ok, _, _} = emqtt:unsubscribe(C, OrigTopics),
timer:sleep(100),
?assertEqual([], emqx_broker:subscriptions(<<"rewrite_client">>)),
ok = emqtt:disconnect(C),
ok = emqx_mod_rewrite:unload(?RULES).
t_rewrite_rule(_Config) ->
Rules = emqx_mod_rewrite:compile(?RULES),
?assertEqual(<<"z/y/2">>, emqx_mod_rewrite:match_and_rewrite(<<"x/y/2">>, Rules)),
?assertEqual(<<"x/1/2">>, emqx_mod_rewrite:match_and_rewrite(<<"x/1/2">>, Rules)),
?assertEqual(<<"y/z/b">>, emqx_mod_rewrite:match_and_rewrite(<<"y/a/z/b">>, Rules)),
?assertEqual(<<"y/def">>, emqx_mod_rewrite:match_and_rewrite(<<"y/def">>, Rules)).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
receive_publish(Timeout) ->
receive
{publish, Publish} -> {ok, Publish}
after
Timeout -> {error, timeout}
end.

View File

@ -24,45 +24,41 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(PACKETS,
[{?CONNECT, 'CONNECT', ?CONNECT_PACKET(#mqtt_packet_connect{})},
{?CONNACK, 'CONNACK', ?CONNACK_PACKET(?RC_SUCCESS)},
{?PUBLISH, 'PUBLISH', ?PUBLISH_PACKET(?QOS_1)},
{?PUBACK, 'PUBACK', ?PUBACK_PACKET(1)},
{?PUBREC, 'PUBREC', ?PUBREC_PACKET(1)},
{?PUBREL, 'PUBREL', ?PUBREL_PACKET(1)},
{?PUBCOMP, 'PUBCOMP', ?PUBCOMP_PACKET(1)},
{?SUBSCRIBE, 'SUBSCRIBE', ?SUBSCRIBE_PACKET(1, [])},
{?SUBACK, 'SUBACK', ?SUBACK_PACKET(1, [0])},
{?UNSUBSCRIBE, 'UNSUBSCRIBE', ?UNSUBSCRIBE_PACKET(1, [])},
{?UNSUBACK, 'UNSUBACK', ?UNSUBACK_PACKET(1)},
{?DISCONNECT, 'DISCONNECT', ?DISCONNECT_PACKET(?RC_SUCCESS)},
{?AUTH, 'AUTH', ?AUTH_PACKET()}
]).
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_type(_) -> t_type(_) ->
?assertEqual(?CONNECT, emqx_packet:type(?CONNECT_PACKET(#mqtt_packet_connect{}))), lists:foreach(fun({Type, _Name, Packet}) ->
?assertEqual(?CONNACK, emqx_packet:type(?CONNACK_PACKET(?RC_SUCCESS))), ?assertEqual(Type, emqx_packet:type(Packet))
?assertEqual(?PUBLISH, emqx_packet:type(?PUBLISH_PACKET(?QOS_1))), end, ?PACKETS).
?assertEqual(?PUBACK, emqx_packet:type(?PUBACK_PACKET(1))),
?assertEqual(?PUBREC, emqx_packet:type(?PUBREC_PACKET(1))),
?assertEqual(?PUBREL, emqx_packet:type(?PUBREL_PACKET(1))),
?assertEqual(?PUBCOMP, emqx_packet:type(?PUBCOMP_PACKET(1))),
?assertEqual(?SUBSCRIBE, emqx_packet:type(?SUBSCRIBE_PACKET(1, []))),
?assertEqual(?SUBACK, emqx_packet:type(?SUBACK_PACKET(1, [0]))),
?assertEqual(?UNSUBSCRIBE, emqx_packet:type(?UNSUBSCRIBE_PACKET(1, []))),
?assertEqual(?UNSUBACK, emqx_packet:type(?UNSUBACK_PACKET(1))),
?assertEqual(?DISCONNECT, emqx_packet:type(?DISCONNECT_PACKET(?RC_SUCCESS))),
?assertEqual(?AUTH, emqx_packet:type(?AUTH_PACKET())).
t_type_name(_) -> t_type_name(_) ->
?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT_PACKET(#mqtt_packet_connect{}))), lists:foreach(fun({_Type, Name, Packet}) ->
?assertEqual('CONNACK', emqx_packet:type_name(?CONNACK_PACKET(?RC_SUCCESS))), ?assertEqual(Name, emqx_packet:type_name(Packet))
?assertEqual('PUBLISH', emqx_packet:type_name(?PUBLISH_PACKET(?QOS_1))), end, ?PACKETS).
?assertEqual('PUBACK', emqx_packet:type_name(?PUBACK_PACKET(1))),
?assertEqual('PUBREC', emqx_packet:type_name(?PUBREC_PACKET(1))),
?assertEqual('PUBREL', emqx_packet:type_name(?PUBREL_PACKET(1))),
?assertEqual('PUBCOMP', emqx_packet:type_name(?PUBCOMP_PACKET(1))),
?assertEqual('SUBSCRIBE', emqx_packet:type_name(?SUBSCRIBE_PACKET(1, []))),
?assertEqual('SUBACK', emqx_packet:type_name(?SUBACK_PACKET(1, [0]))),
?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE_PACKET(1, []))),
?assertEqual('UNSUBACK', emqx_packet:type_name(?UNSUBACK_PACKET(1))),
?assertEqual('DISCONNECT', emqx_packet:type_name(?DISCONNECT_PACKET(?RC_SUCCESS))),
?assertEqual('AUTH', emqx_packet:type_name(?AUTH_PACKET())).
t_dup(_) -> t_dup(_) ->
?assertEqual(false, emqx_packet:dup(?PUBLISH_PACKET(?QOS_1))). ?assertEqual(false, emqx_packet:dup(?PUBLISH_PACKET(?QOS_1))).
t_qos(_) -> t_qos(_) ->
?assertEqual(?QOS_0, emqx_packet:qos(?PUBLISH_PACKET(?QOS_0))), lists:foreach(fun(QoS) ->
?assertEqual(?QOS_1, emqx_packet:qos(?PUBLISH_PACKET(?QOS_1))), ?assertEqual(QoS, emqx_packet:qos(?PUBLISH_PACKET(QoS)))
?assertEqual(?QOS_2, emqx_packet:qos(?PUBLISH_PACKET(?QOS_2))). end, [?QOS_0, ?QOS_1, ?QOS_2]).
t_retain(_) -> t_retain(_) ->
?assertEqual(false, emqx_packet:retain(?PUBLISH_PACKET(?QOS_1))). ?assertEqual(false, emqx_packet:retain(?PUBLISH_PACKET(?QOS_1))).
@ -78,15 +74,16 @@ t_proto_name(_) ->
t_proto_ver(_) -> t_proto_ver(_) ->
lists:foreach( lists:foreach(
fun(Ver) -> fun(Ver) ->
?assertEqual(Ver, emqx_packet:proto_ver(#mqtt_packet_connect{proto_ver = Ver})) ConnPkt = ?CONNECT_PACKET(#mqtt_packet_connect{proto_ver = Ver}),
?assertEqual(Ver, emqx_packet:proto_ver(ConnPkt))
end, [?MQTT_PROTO_V3, ?MQTT_PROTO_V4, ?MQTT_PROTO_V5]). end, [?MQTT_PROTO_V3, ?MQTT_PROTO_V4, ?MQTT_PROTO_V5]).
t_check_publish(_) -> t_check_publish(_) ->
Props = #{'Response-Topic' => <<"responsetopic">>, 'Topic-Alias' => 1}, Props = #{'Response-Topic' => <<"responsetopic">>, 'Topic-Alias' => 1},
ok = emqx_packet:check(?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, Props, <<"payload">>)), ok = emqx_packet:check(?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, Props, <<"payload">>)),
ok = emqx_packet:check(#mqtt_packet_publish{packet_id = 1, topic_name = <<"t">>}), ok = emqx_packet:check(#mqtt_packet_publish{packet_id = 1, topic_name = <<"t">>}),
{error, ?RC_PROTOCOL_ERROR} = emqx_packet:check(?PUBLISH_PACKET(1,<<>>,1,#{},<<"payload">>)), {error, ?RC_TOPIC_NAME_INVALID} = emqx_packet:check(?PUBLISH_PACKET(?QOS_1, <<>>, 1, #{}, <<"payload">>)),
{error, ?RC_TOPIC_NAME_INVALID} = emqx_packet:check(?PUBLISH_PACKET(1, <<"+/+">>, 1, #{}, <<"payload">>)), {error, ?RC_TOPIC_NAME_INVALID} = emqx_packet:check(?PUBLISH_PACKET(?QOS_1, <<"+/+">>, 1, #{}, <<"payload">>)),
{error, ?RC_TOPIC_ALIAS_INVALID} = emqx_packet:check(?PUBLISH_PACKET(1, <<"topic">>, 1, #{'Topic-Alias' => 0}, <<"payload">>)), {error, ?RC_TOPIC_ALIAS_INVALID} = emqx_packet:check(?PUBLISH_PACKET(1, <<"topic">>, 1, #{'Topic-Alias' => 0}, <<"payload">>)),
%% TODO:: %% TODO::
%% {error, ?RC_PROTOCOL_ERROR} = emqx_packet:check(?PUBLISH_PACKET(1, <<"topic">>, 1, #{'Subscription-Identifier' => 10}, <<"payload">>)), %% {error, ?RC_PROTOCOL_ERROR} = emqx_packet:check(?PUBLISH_PACKET(1, <<"topic">>, 1, #{'Subscription-Identifier' => 10}, <<"payload">>)),
@ -143,10 +140,10 @@ t_check_connect(_) ->
properties = #{'Receive-Maximum' => 0}}), Opts). properties = #{'Receive-Maximum' => 0}}), Opts).
t_from_to_message(_) -> t_from_to_message(_) ->
ExpectedMsg = emqx_message:set_headers( ExpectedMsg = emqx_message:make(<<"clientid">>, ?QOS_0, <<"topic">>, <<"payload">>),
#{peername => {{127,0,0,1}, 9527}, username => <<"test">>},
emqx_message:make(<<"clientid">>, ?QOS_0, <<"topic">>, <<"payload">>)),
ExpectedMsg1 = emqx_message:set_flag(retain, false, ExpectedMsg), ExpectedMsg1 = emqx_message:set_flag(retain, false, ExpectedMsg),
ExpectedMsg2 = emqx_message:set_headers(#{peerhost => {127,0,0,1},
username => <<"test">>}, ExpectedMsg1),
Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = ?QOS_0, qos = ?QOS_0,
retain = false, retain = false,
@ -157,8 +154,8 @@ t_from_to_message(_) ->
payload = <<"payload">>}, payload = <<"payload">>},
MsgFromPkt = emqx_packet:to_message(#{client_id => <<"clientid">>, MsgFromPkt = emqx_packet:to_message(#{client_id => <<"clientid">>,
username => <<"test">>, username => <<"test">>,
peername => {{127,0,0,1}, 9527}}, Pkt), peerhost => {127,0,0,1}}, Pkt),
?assertEqual(ExpectedMsg1, MsgFromPkt#message{id = emqx_message:id(ExpectedMsg), ?assertEqual(ExpectedMsg2, MsgFromPkt#message{id = emqx_message:id(ExpectedMsg),
timestamp = emqx_message:timestamp(ExpectedMsg) timestamp = emqx_message:timestamp(ExpectedMsg)
}). }).

View File

@ -1,81 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_protocol_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) ->
[{proto, init_protocol()}|Config].
init_protocol() ->
emqx_protocol:init(#mqtt_packet_connect{
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V5,
is_bridge = false,
clean_start = true,
keepalive = 30,
properties = #{},
client_id = <<"clientid">>,
username = <<"username">>,
password = <<"passwd">>
}, testing).
end_per_suite(_Config) -> ok.
t_init_info_1(Config) ->
Proto = proplists:get_value(proto, Config),
?assertEqual(#{proto_name => <<"MQTT">>,
proto_ver => ?MQTT_PROTO_V5,
clean_start => true,
keepalive => 30,
will_msg => undefined,
client_id => <<"clientid">>,
username => <<"username">>,
topic_aliases => undefined,
alias_maximum => #{outbound => 0, inbound => 0}
}, emqx_protocol:info(Proto)).
t_init_info_2(Config) ->
Proto = proplists:get_value(proto, Config),
?assertEqual(<<"MQTT">>, emqx_protocol:info(proto_name, Proto)),
?assertEqual(?MQTT_PROTO_V5, emqx_protocol:info(proto_ver, Proto)),
?assertEqual(true, emqx_protocol:info(clean_start, Proto)),
?assertEqual(30, emqx_protocol:info(keepalive, Proto)),
?assertEqual(<<"clientid">>, emqx_protocol:info(client_id, Proto)),
?assertEqual(<<"username">>, emqx_protocol:info(username, Proto)),
?assertEqual(undefined, emqx_protocol:info(will_msg, Proto)),
?assertEqual(0, emqx_protocol:info(will_delay_interval, Proto)),
?assertEqual(undefined, emqx_protocol:info(topic_aliases, Proto)),
?assertEqual(#{outbound => 0, inbound => 0}, emqx_protocol:info(alias_maximum, Proto)).
t_find_save_alias(Config) ->
Proto = proplists:get_value(proto, Config),
?assertEqual(undefined, emqx_protocol:info(topic_aliases, Proto)),
?assertEqual(false, emqx_protocol:find_alias(1, Proto)),
Proto1 = emqx_protocol:save_alias(1, <<"t1">>, Proto),
Proto2 = emqx_protocol:save_alias(2, <<"t2">>, Proto1),
?assertEqual(#{1 => <<"t1">>, 2 => <<"t2">>},
emqx_protocol:info(topic_aliases, Proto2)),
?assertEqual({ok, <<"t1">>}, emqx_protocol:find_alias(1, Proto2)),
?assertEqual({ok, <<"t2">>}, emqx_protocol:find_alias(2, Proto2)).

View File

@ -273,7 +273,7 @@ max_inflight() -> choose(0, 10).
expiry_interval() -> ?LET(EI, choose(1, 10), EI * 3600). expiry_interval() -> ?LET(EI, choose(1, 10), EI * 3600).
option() -> option() ->
?LET(Option, [{max_inflight, max_inflight()}, ?LET(Option, [{receive_maximum , max_inflight()},
{expiry_interval, expiry_interval()}], {expiry_interval, expiry_interval()}],
maps:from_list(Option)). maps:from_list(Option)).

View File

@ -21,29 +21,81 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(OPTS, [{enable_acl, true}, -define(ENVS, [{use_username_as_clientid, false},
{enable_banned, false} {server_keepalive, 60},
{upgrade_qos, false},
{session_expiry_interval, 7200},
{retry_interval, 20000},
{mqueue_store_qos0, true},
{mqueue_priorities, none},
{mqueue_default_priority, highest},
{max_subscriptions, 0},
{max_mqueue_len, 1000},
{max_inflight, 32},
{max_awaiting_rel, 100},
{keepalive_backoff, 0.75},
{ignore_loop_deliver, false},
{idle_timeout, 15000},
{force_shutdown_policy, #{max_heap_size => 838860800,
message_queue_len => 8000}},
{force_gc_policy, #{bytes => 1048576, count => 1000}},
{enable_stats, true},
{enable_flapping_detect, false},
{enable_ban, true},
{enable_acl, true},
{await_rel_timeout, 300000},
{acl_deny_action, ignore}
]). ]).
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_set_get_env(_) -> init_per_suite(Config) ->
_ = application:load(emqx), _ = application:load(emqx),
application:set_env(emqx, zones, [{external, ?OPTS}]), application:set_env(emqx, zone_env, val),
{ok, _} = emqx_zone:start_link(), application:set_env(emqx, zones, [{zone, ?ENVS}]),
?assert(emqx_zone:get_env(external, enable_acl)), Config.
?assertNot(emqx_zone:get_env(external, enable_banned)),
end_per_suite(_Config) ->
application:unset_env(emqx, zone_env),
application:unset_env(emqx, zones).
t_zone_env_func(_) ->
lists:foreach(fun({Env, Val}) ->
case erlang:function_exported(emqx_zone, Env, 1) of
true ->
?assertEqual(Val, erlang:apply(emqx_zone, Env, [zone]));
false -> ok
end
end, ?ENVS).
t_get_env(_) ->
?assertEqual(val, emqx_zone:get_env(undefined, zone_env)),
?assertEqual(val, emqx_zone:get_env(undefined, zone_env, def)),
?assert(emqx_zone:get_env(zone, enable_acl)),
?assert(emqx_zone:get_env(zone, enable_ban)),
?assertEqual(defval, emqx_zone:get_env(extenal, key, defval)), ?assertEqual(defval, emqx_zone:get_env(extenal, key, defval)),
?assertEqual(undefined, emqx_zone:get_env(external, key)), ?assertEqual(undefined, emqx_zone:get_env(external, key)),
?assertEqual(undefined, emqx_zone:get_env(internal, key)), ?assertEqual(undefined, emqx_zone:get_env(internal, key)),
?assertEqual(def, emqx_zone:get_env(internal, key, def)), ?assertEqual(def, emqx_zone:get_env(internal, key, def)).
emqx_zone:stop().
t_get_set_env(_) ->
ok = emqx_zone:set_env(zone, key, val),
?assertEqual(val, emqx_zone:get_env(zone, key)),
true = emqx_zone:unset_env(zone, key),
?assertEqual(undefined, emqx_zone:get_env(zone, key)).
t_force_reload(_) -> t_force_reload(_) ->
{ok, _} = emqx_zone:start_link(), {ok, _} = emqx_zone:start_link(),
application:set_env(emqx, zones, [{zone, [{key, val}]}]), ?assertEqual(undefined, emqx_zone:get_env(xzone, key)),
?assertEqual(undefined, emqx_zone:get_env(zone, key)), application:set_env(emqx, zones, [{xzone, [{key, val}]}]),
ok = emqx_zone:force_reload(), ok = emqx_zone:force_reload(),
?assertEqual(val, emqx_zone:get_env(zone, key)), ?assertEqual(val, emqx_zone:get_env(xzone, key)),
emqx_zone:stop().
t_uncovered_func(_) ->
{ok, Pid} = emqx_zone:start_link(),
ignored = gen_server:call(Pid, unexpected_call),
ok = gen_server:cast(Pid, unexpected_cast),
ok = Pid ! ok,
emqx_zone:stop(). emqx_zone:stop().