From 0f052ce352597ef09453d66a333563a481c64193 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 24 Aug 2018 18:39:59 +0800 Subject: [PATCH] Upgrade connection, protocol and session modules for MQTT 5.0 --- docs/mqtt-v5.0.pdf | Bin 2895576 -> 3215041 bytes etc/emqx.conf | 373 +++--- include/emqx.hrl | 25 +- include/emqx_mqtt.hrl | 37 +- priv/emqx.schema | 330 +++--- src/emqx.app.src | 2 +- src/emqx_access_control.erl | 49 +- src/emqx_broker.erl | 4 +- src/emqx_client.erl | 30 +- src/emqx_connection.erl | 214 ++-- src/emqx_frame.erl | 9 +- src/emqx_mqtt_caps.erl | 139 +++ src/emqx_packet.erl | 46 +- src/emqx_pmon.erl | 6 +- src/emqx_protocol.erl | 1030 +++++++++-------- src/emqx_session.erl | 358 +++--- src/emqx_sm.erl | 73 +- src/emqx_topic.erl | 66 +- src/emqx_ws_connection.erl | 8 +- src/emqx_zone.erl | 38 +- .../emqx_mqtt_caps_SUITE.erl | 16 +- test/emqx_topic_SUITE.erl | 28 +- 22 files changed, 1640 insertions(+), 1241 deletions(-) create mode 100644 src/emqx_mqtt_caps.erl rename include/emqx_misc.hrl => test/emqx_mqtt_caps_SUITE.erl (59%) diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index 5b7e403f86ee30f1518b3400acdcc06f77243f1b..c3d0c97258e0c619fc4d44a0b80250f7def82b96 100644 GIT binary patch delta 31475 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 0.5 +zone.external.keepalive_backoff = 0.75 + +## Maximum number of subscriptions allowed, 0 means no limit. +## +## Value: Number +zone.external.max_subscriptions = 0 + +## Force to upgrade QoS according to subscription. +## +## Value: on | off +zone.external.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.external.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.external.retry_interval = 20s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.external.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.external.await_rel_timeout = 60s + +## Whether to ignore loop delivery of messages. +## +## Value: true | false +## Default: false +zone.external.ignore_loop_deliver = false + +## Default session expiry interval for MQTT V3.1.1 connections. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.external.session_expiry_interval = 2h + +## Maximum queue length. Enqueued messages when persistent client disconnected, +## or inflight window is full. 0 means no limit. +## +## Value: Number >= 0 +zone.external.max_mqueue_len = 1000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.external.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## Internal Zone + +zone.internal.allow_anonymous = true + +## Enable per connection stats. +## +## Value: Flag +zone.internal.enable_stats = on + +## Enable ACL check. +## +## Value: Flag +zone.internal.enable_acl = off + +## See zone.$name.wildcard_subscription. +## +## Value: boolean +## zone.internal.wildcard_subscription = true + +## See zone.$name.shared_subscription. +## +## Value: boolean +## zone.internal.shared_subscription = true + +## See zone.$name.max_subscriptions. +## +## Value: Integer +zone.internal.max_subscriptions = 0 + +## See zone.$name.max_inflight +## +## Value: Number +zone.internal.max_inflight = 32 + +## See zone.$name.max_awaiting_rel +## +## Value: Number +zone.internal.max_awaiting_rel = 100 + +## See zone.$name.max_mqueue_len +## +## Value: Number >= 0 +zone.internal.max_mqueue_len = 1000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.internal.mqueue_store_qos0 = true + ##-------------------------------------------------------------------- ## Listeners ##-------------------------------------------------------------------- @@ -1312,187 +1500,6 @@ listener.wss.external.send_timeout_close = on listener.wss.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA -##-------------------------------------------------------------------- -## Zones -##-------------------------------------------------------------------- - -##-------------------------------------------------------------------- -## External Zone - -## Idle timeout of the external MQTT connections. -## -## Value: duration -zone.external.idle_timeout = 15s - -## Publish limit for the external MQTT connections. -## -## Value: rate,burst -## Default: 10 messages per second, and 100 messages burst. -## zone.external.publish_limit = 10,100 - -## Enable ACL check. -## -## Value: Flag -zone.external.enable_acl = on - -## Enable per connection statistics. -## -## Value: on | off -zone.external.enable_stats = on - -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## Default: 1MB -## zone.external.max_packet_size = 64KB - -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -## zone.external.max_clientid_len = 1024 - -## Maximum topic levels allowed. 0 means no limit. -## -## Value: Number -## zone.external.max_topic_levels = 7 - -## Maximum QoS allowed. -## -## Value: 0 | 1 | 2 -## zone.external.max_qos_allowed = 2 - -## Maximum Topic Alias, 0 means no limit. -## -## Value: 0-65535 -## zone.external.max_topic_alias = 0 - -## Whether the Server supports retained messages. -## -## Value: boolean -## zone.external.retain_available = true - -## Whether the Server supports Wildcard Subscriptions -## -## Value: boolean -## zone.external.wildcard_subscription = false - -## Whether the Server supports Shared Subscriptions -## -## Value: boolean -## zone.external.shared_subscription = false - -## The backoff for MQTT keepalive timeout. The broker will kick a connection out -## until 'Keepalive * backoff * 2' timeout. -## -## Value: Float > 0.5 -zone.external.keepalive_backoff = 0.75 - -## Maximum number of subscriptions allowed, 0 means no limit. -## -## Value: Number -zone.external.max_subscriptions = 0 - -## Force to upgrade QoS according to subscription. -## -## Value: on | off -zone.external.upgrade_qos = off - -## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. -## -## Value: Number -zone.external.max_inflight = 32 - -## Retry interval for QoS1/2 message delivering. -## -## Value: Duration -zone.external.retry_interval = 20s - -## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. -## -## Value: Number -zone.external.max_awaiting_rel = 100 - -## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. -## -## Value: Duration -zone.external.await_rel_timeout = 60s - -## Whether to ignore loop delivery of messages. -## -## Value: true | false -## Default: false -zone.external.ignore_loop_deliver = false - -## Default session expiry interval for MQTT V3.1.1 connections. -## -## Value: Duration -## -d: day -## -h: hour -## -m: minute -## -s: second -## -## Default: 2h, 2 hours -zone.external.session_expiry_interval = 2h - -## Maximum queue length. Enqueued messages when persistent client disconnected, -## or inflight window is full. 0 means no limit. -## -## Value: Number >= 0 -zone.external.max_mqueue_len = 1000 - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -zone.external.mqueue_store_qos0 = true - -##-------------------------------------------------------------------- -## Internal Zone - -## Enable per connection stats. -## -## Value: Flag -zone.internal.enable_stats = on - -## Enable ACL check. -## -## Value: Flag -zone.internal.enable_acl = off - -## See zone.$name.wildcard_subscription. -## -## Value: boolean -## zone.internal.wildcard_subscription = true - -## See zone.$name.shared_subscription. -## -## Value: boolean -## zone.internal.shared_subscription = true - -## See zone.$name.max_subscriptions. -## -## Value: Integer -zone.internal.max_subscriptions = 0 - -## See zone.$name.max_inflight -## -## Value: Number -zone.internal.max_inflight = 32 - -## See zone.$name.max_awaiting_rel -## -## Value: Number -zone.internal.max_awaiting_rel = 100 - -## See zone.$name.max_mqueue_len -## -## Value: Number >= 0 -zone.internal.max_mqueue_len = 1000 - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -zone.internal.mqueue_store_qos0 = true - ##-------------------------------------------------------------------- ## Bridges ##-------------------------------------------------------------------- diff --git a/include/emqx.hrl b/include/emqx.hrl index 3b42713ff..0372f547e 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -53,7 +53,9 @@ -type(subid() :: binary() | atom()). --type(subopts() :: #{qos => integer(), share => '$queue' | binary(), atom() => term()}). +-type(subopts() :: #{qos => integer(), + share => '$queue' | binary(), + atom() => term()}). -record(subscription, { topic :: topic(), @@ -82,20 +84,21 @@ -type(zone() :: atom()). -record(client, { - id :: client_id(), - pid :: pid(), - zone :: zone(), - protocol :: protocol(), - peername :: peername(), - peercert :: nossl | binary(), - username :: username(), - clean_start :: boolean(), - attributes :: map() + id :: client_id(), + pid :: pid(), + zone :: zone(), + peername :: peername(), + username :: username(), + protocol :: protocol(), + attributes :: map() }). -type(client() :: #client{}). --record(session, {sid :: client_id(), pid :: pid()}). +-record(session, { + sid :: client_id(), + pid :: pid() + }). -type(session() :: #session{}). diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 007de4dd1..e429aa4a3 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -229,10 +229,17 @@ -type(mqtt_properties() :: #{atom() => term()} | undefined). -%% nl: no local, rap: retain as publish, rh: retain handling --record(mqtt_subopts, {rh = 0, rap = 0, nl = 0, qos = ?QOS_0}). +-type(mqtt_subopts() :: #{atom() => term()}). --type(mqtt_subopts() :: #mqtt_subopts{}). +-define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling + rap => 0, %% Retain as Publish + nl => 0, %% No Local + qos => ?QOS_0, + rc => 0, %% Reason Code + subid => 0 %% Subscription-Identifier + }). + +-type(mqtt_topic_filters() :: [{mqtt_topic(), mqtt_subopts()}]). -record(mqtt_packet_connect, { proto_name = <<"MQTT">> :: binary(), @@ -273,7 +280,7 @@ -record(mqtt_packet_subscribe, { packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), - topic_filters :: [{mqtt_topic(), mqtt_subopts()}] + topic_filters :: mqtt_topic_filters() }). -record(mqtt_packet_suback, { @@ -391,6 +398,11 @@ variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). +-define(PUBACK_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode}}). + -define(PUBACK_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, variable = #mqtt_packet_puback{packet_id = PacketId, @@ -399,8 +411,13 @@ -define(PUBREC_PACKET(PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, - variable = #mqtt_packet_puback{packet_id = PacketId, - reason_code = 0}}). + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0}}). + +-define(PUBREC_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode}}). -define(PUBREC_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, @@ -412,6 +429,10 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). +-define(PUBREL_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode}}). -define(PUBREL_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, @@ -423,6 +444,10 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). +-define(PUBCOMP_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode}}). -define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, diff --git a/priv/emqx.schema b/priv/emqx.schema index c502a8d24..b35136d70 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -559,6 +559,12 @@ end}. {datatype, {enum, [true, false]}} ]}. +%% @doc ACL nomatch. +{mapping, "acl_nomatch", "emqx.acl_nomatch", [ + {default, deny}, + {datatype, {enum, [allow, deny]}} +]}. + %% @doc Default ACL file. {mapping, "acl_file", "emqx.acl_file", [ {datatype, string}, @@ -584,7 +590,7 @@ end}. %% ]}. %%-------------------------------------------------------------------- -%% MQTT +%% MQTT Protocol %%-------------------------------------------------------------------- %% @doc Max Packet Size Allowed, 1MB by default. @@ -636,6 +642,172 @@ end}. {datatype, {enum, [true, false]}} ]}. +%%-------------------------------------------------------------------- +%% Zones +%%-------------------------------------------------------------------- + +%% @doc Idle timeout of the MQTT connection. +{mapping, "zone.$name.idle_timeout", "emqx.zones", [ + {default, "15s"}, + {datatype, {duration, ms}} +]}. + +{mapping, "zone.$name.allow_anonymous", "emqx.zones", [ + {default, false}, + {datatype, {enum, [true, false]}} +]}. + +{mapping, "zone.$name.acl_nomatch", "emqx.zones", [ + {default, deny}, + {datatype, {enum, [allow, deny]}} +]}. + +%% @doc Enable ACL check. +{mapping, "zone.$name.enable_acl", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Enable per connection statistics. +{mapping, "zone.$name.enable_stats", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Publish limit of the MQTT connections. +{mapping, "zone.$name.publish_limit", "emqx.zones", [ + {default, undefined}, + {datatype, string} +]}. + +%% @doc Max Packet Size Allowed, 64K by default. +{mapping, "zone.$name.max_packet_size", "emqx.zones", [ + {datatype, bytesize} +]}. + +%% @doc Set the Max ClientId Length Allowed. +{mapping, "zone.$name.max_clientid_len", "emqx.zones", [ + {datatype, integer} +]}. + +%% @doc Set the Maximum topic levels. +{mapping, "zone.$name.max_topic_levels", "emqx.zones", [ + {datatype, integer} +]}. + +%% @doc Set the Maximum QoS allowed. +{mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +%% @doc Set the Maximum topic alias. +{mapping, "zone.$name.max_topic_alias", "emqx.zones", [ + {datatype, integer} +]}. + +%% @doc Whether the server supports retained messages. +{mapping, "zone.$name.retain_available", "emqx.zones", [ + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports Wildcard Subscriptions. +{mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports Shared Subscriptions. +{mapping, "zone.$name.shared_subscription", "emqx.zones", [ + {datatype, {enum, [true, false]}} +]}. + +%% @doc Keepalive backoff +{mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ + {default, 0.75}, + {datatype, float} +]}. + +%% @doc Max Number of Subscriptions Allowed. +{mapping, "zone.$name.max_subscriptions", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Upgrade QoS according to subscription? +{mapping, "zone.$name.upgrade_qos", "emqx.zones", [ + {default, off}, + {datatype, flag} +]}. + +%% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. +%% 0 means no limit +{mapping, "zone.$name.max_inflight", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Retry interval for redelivering QoS1/2 messages. +{mapping, "zone.$name.retry_interval", "emqx.zones", [ + {default, "20s"}, + {datatype, {duration, ms}} +]}. + +%% @doc Max Packets that Awaiting PUBREL, 0 means no limit +{mapping, "zone.$name.max_awaiting_rel", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Awaiting PUBREL timeout +{mapping, "zone.$name.await_rel_timeout", "emqx.zones", [ + {default, "60s"}, + {datatype, {duration, ms}} +]}. + +%% @doc Ignore loop delivery of messages +{mapping, "zone.$name.ignore_loop_deliver", "emqx.zones", [ + {default, false}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Session Expiry Interval +{mapping, "zone.$name.session_expiry_interval", "emqx.zones", [ + {default, "2h"}, + {datatype, {duration, ms}} +]}. + +%% @doc Max queue length. Enqueued messages when persistent client +%% disconnected, or inflight window is full. 0 means no limit. +{mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ + {default, 1000}, + {datatype, integer} +]}. + +%% @doc Queue Qos0 messages? +{mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +{translation, "emqx.zones", fun(Conf) -> + Mapping = fun("retain_available", Val) -> + {mqtt_retain_available, Val}; + ("wildcard_subscription", Val) -> + {mqtt_wildcard_subscription, Val}; + ("shared_subscription", Val) -> + {mqtt_shared_subscription, Val}; + (Opt, Val) -> + {list_to_atom(Opt), Val} + end, + maps:to_list( + lists:foldl( + fun({["zone", Name, Opt], Val}, Zones) -> + maps:update_with(list_to_atom(Name), + fun(Opts) -> [Mapping(Opt, Val)|Opts] end, + [Mapping(Opt, Val)], Zones) + end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) +end}. + %%-------------------------------------------------------------------- %% Listeners %%-------------------------------------------------------------------- @@ -1234,162 +1406,6 @@ end}. ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)]) end}. -%%-------------------------------------------------------------------- -%% Zones -%%-------------------------------------------------------------------- - -%% @doc Idle timeout of the MQTT connection. -{mapping, "zone.$name.idle_timeout", "emqx.zones", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Enable ACL check. -{mapping, "zone.$name.enable_acl", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Enable per connection statistics. -{mapping, "zone.$name.enable_stats", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Publish limit of the MQTT connections. -{mapping, "zone.$name.publish_limit", "emqx.zones", [ - {default, undefined}, - {datatype, string} -]}. - -%% @doc Max Packet Size Allowed, 64K by default. -{mapping, "zone.$name.max_packet_size", "emqx.zones", [ - {datatype, bytesize} -]}. - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "zone.$name.max_clientid_len", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Set the Maximum topic levels. -{mapping, "zone.$name.max_topic_levels", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Set the Maximum QoS allowed. -{mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -%% @doc Set the Maximum topic alias. -{mapping, "zone.$name.max_topic_alias", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Whether the server supports retained messages. -{mapping, "zone.$name.retain_available", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports Wildcard Subscriptions. -{mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports Shared Subscriptions. -{mapping, "zone.$name.shared_subscription", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Keepalive backoff -{mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ - {default, 0.75}, - {datatype, float} -]}. - -%% @doc Max Number of Subscriptions Allowed. -{mapping, "zone.$name.max_subscriptions", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Upgrade QoS according to subscription? -{mapping, "zone.$name.upgrade_qos", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. -%% 0 means no limit -{mapping, "zone.$name.max_inflight", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Retry interval for redelivering QoS1/2 messages. -{mapping, "zone.$name.retry_interval", "emqx.zones", [ - {default, "20s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Max Packets that Awaiting PUBREL, 0 means no limit -{mapping, "zone.$name.max_awaiting_rel", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Awaiting PUBREL timeout -{mapping, "zone.$name.await_rel_timeout", "emqx.zones", [ - {default, "60s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Ignore message from self publish -{mapping, "zone.$name.ignore_loop_deliver", "emqx.zones", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Session Expiry Interval -{mapping, "zone.$name.session_expiry_interval", "emqx.zones", [ - {default, "2h"}, - {datatype, {duration, ms}} -]}. - -%% @doc Max queue length. Enqueued messages when persistent client -%% disconnected, or inflight window is full. 0 means no limit. -{mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ - {default, 1000}, - {datatype, integer} -]}. - -%% @doc Queue Qos0 messages? -{mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.zones", fun(Conf) -> - Mapping = fun("retain_available", Val) -> - {mqtt_retain_available, Val}; - ("wildcard_subscription", Val) -> - {mqtt_wildcard_subscription, Val}; - ("shared_subscription", Val) -> - {mqtt_shared_subscription, Val}; - (Opt, Val) -> - {list_to_atom(Opt), Val} - end, - maps:to_list( - lists:foldl( - fun({["zone", Name, Opt], Val}, Zones) -> - maps:update_with(list_to_atom(Name), - fun(Opts) -> [Mapping(Opt, Val)|Opts] end, - [Mapping(Opt, Val)], Zones) - end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) -end}. - %%-------------------------------------------------------------------- %% Bridges %%-------------------------------------------------------------------- diff --git a/src/emqx.app.src b/src/emqx.app.src index b7a195c8b..d963e3662 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,7 +3,7 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,minirest]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,cowboy]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 2b7630f1e..f43309088 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -18,15 +18,16 @@ -include("emqx.hrl"). -%% API Function Exports --export([start_link/0, auth/2, check_acl/3, reload_acl/0, lookup_mods/1, - register_mod/3, register_mod/4, unregister_mod/2, stop/0]). - +-export([start_link/0]). +-export([authenticate/2]). +-export([check_acl/3, reload_acl/0, lookup_mods/1]). -export([clean_acl_cache/1, clean_acl_cache/2]). +-export([register_mod/3, register_mod/4, unregister_mod/2]). +-export([stop/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(TAB, ?MODULE). -define(SERVER, ?MODULE). @@ -35,9 +36,9 @@ -record(state, {}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% @doc Start access control server. -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). @@ -58,34 +59,34 @@ register_default_mod() -> end. %% @doc Authenticate Client. --spec(auth(Client :: client(), Password :: password()) +-spec(authenticate(Client :: client(), Password :: password()) -> ok | {ok, boolean()} | {error, term()}). -auth(Client, Password) when is_record(Client, client) -> - auth(Client, Password, lookup_mods(auth)). -auth(_Client, _Password, []) -> - case emqx_config:get_env(allow_anonymous, false) of +authenticate(Client, Password) when is_record(Client, client) -> + authenticate(Client, Password, lookup_mods(auth)). + +authenticate(#client{zone = Zone}, _Password, []) -> + case emqx_zone:get_env(Zone, allow_anonymous, false) of true -> ok; false -> {error, "No auth module to check!"} end; -auth(Client, Password, [{Mod, State, _Seq} | Mods]) -> + +authenticate(Client, Password, [{Mod, State, _Seq} | Mods]) -> case catch Mod:check(Client, Password, State) of ok -> ok; {ok, IsSuper} -> {ok, IsSuper}; - ignore -> auth(Client, Password, Mods); + ignore -> authenticate(Client, Password, Mods); {error, Reason} -> {error, Reason}; {'EXIT', Error} -> {error, Error} end. %% @doc Check ACL --spec(check_acl(Client, PubSub, Topic) -> allow | deny when - Client :: client(), - PubSub :: pubsub(), - Topic :: topic()). +-spec(check_acl(client(), pubsub(), topic()) -> allow | deny). check_acl(Client, PubSub, Topic) when ?PS(PubSub) -> check_acl(Client, PubSub, Topic, lookup_mods(acl)). -check_acl(_Client, _PubSub, _Topic, []) -> - emqx_config:get_env(acl_nomatch, allow); +check_acl(#client{zone = Zone}, _PubSub, _Topic, []) -> + emqx_zone:get_env(Zone, acl_nomatch, deny); + check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) -> case Mod:check_acl({Client, PubSub, Topic}, State) of allow -> allow; @@ -175,15 +176,15 @@ handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Req, _From, State) -> - emqx_logger:error("[AccessControl] Unexpected request: ~p", [Req]), + emqx_logger:error("[AccessControl] unexpected request: ~p", [Req]), {reply, ignore, State}. handle_cast(Msg, State) -> - emqx_logger:error("[AccessControl] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[AccessControl] unexpected msg: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> - emqx_logger:error("[AccessControl] Unexpected info: ~p", [Info]), + emqx_logger:error("[AccessControl] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 9d332a5f2..7015590d8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -69,9 +69,9 @@ subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> -spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok). subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, []); + subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, []); + subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> subscribe(Topic, SubPid, undefined, SubOpts); subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) -> diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 695b9d10c..7923f1da7 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -231,22 +231,22 @@ subscribe(Client, Properties, Topic, Opts) subscribe(Client, Properties, [{Topic, Opts}]). parse_subopt(Opts) -> - parse_subopt(Opts, #mqtt_subopts{}). + parse_subopt(Opts, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). -parse_subopt([], Rec) -> - Rec; -parse_subopt([{rh, I} | Opts], Rec) when I >= 0, I =< 2 -> - parse_subopt(Opts, Rec#mqtt_subopts{rh = I}); -parse_subopt([{rap, true} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{rap =1}); -parse_subopt([{rap, false} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{rap = 0}); -parse_subopt([{nl, true} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{nl = 1}); -parse_subopt([{nl, false} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{nl = 0}); -parse_subopt([{qos, QoS} | Opts], Rec) -> - parse_subopt(Opts, Rec#mqtt_subopts{qos = ?QOS_I(QoS)}). +parse_subopt([], Result) -> + Result; +parse_subopt([{rh, I} | Opts], Result) when I >= 0, I =< 2 -> + parse_subopt(Opts, Result#{rh := I}); +parse_subopt([{rap, true} | Opts], Result) -> + parse_subopt(Opts, Result#{rap := 1}); +parse_subopt([{rap, false} | Opts], Result) -> + parse_subopt(Opts, Result#{rap := 0}); +parse_subopt([{nl, true} | Opts], Result) -> + parse_subopt(Opts, Result#{nl := 1}); +parse_subopt([{nl, false} | Opts], Result) -> + parse_subopt(Opts, Result#{nl := 0}); +parse_subopt([{qos, QoS} | Opts], Result) -> + parse_subopt(Opts, Result#{qos := ?QOS_I(QoS)}). -spec(publish(client(), topic(), payload()) -> ok | {error, term()}). publish(Client, Topic, Payload) when is_binary(Topic) -> diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 38c100297..5ebd54dd0 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -18,40 +18,34 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_misc.hrl"). -export([start_link/3]). -export([info/1, stats/1, kick/1]). -export([session/1]). --export([clean_acl_cache/1]). --export([get_rate_limit/1, set_rate_limit/2]). --export([get_pub_limit/1, set_pub_limit/2]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, - terminate/2]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, { - transport, %% Network transport module - socket, %% TCP or SSL Socket - peername, %% Peername of the socket - sockname, %% Sockname of the socket - conn_state, %% Connection state: running | blocked - await_recv, %% Awaiting recv - incoming, %% Incoming bytes and packets - pub_limit, %% Publish rate limit - rate_limit, %% Traffic rate limit - limit_timer, %% Rate limit timer - proto_state, %% MQTT protocol state - parser_state, %% MQTT parser state - keepalive, %% MQTT keepalive timer - enable_stats, %% Enable stats - stats_timer, %% Stats timer - idle_timeout %% Connection idle timeout + transport, + socket, + peername, + sockname, + conn_state, + await_recv, + proto_state, + parser_state, + keepalive, + enable_stats, + stats_timer, + incoming, + rate_limit, + publish_limit, + limit_timer, + idle_timeout }). --define(INFO_KEYS, [peername, sockname, conn_state, await_recv, rate_limit, pub_limit]). - -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(LOG(Level, Format, Args, State), @@ -66,31 +60,19 @@ start_link(Transport, Socket, Options) -> %%------------------------------------------------------------------------------ info(CPid) -> - gen_server:call(CPid, info). + call(CPid, info). stats(CPid) -> - gen_server:call(CPid, stats). + call(CPid, stats). kick(CPid) -> - gen_server:call(CPid, kick). + call(CPid, kick). session(CPid) -> - gen_server:call(CPid, session, infinity). + call(CPid, session). -clean_acl_cache(CPid) -> - gen_server:call(CPid, clean_acl_cache). - -get_rate_limit(CPid) -> - gen_server:call(CPid, get_rate_limit). - -set_rate_limit(CPid, Rl = {_Rate, _Burst}) -> - gen_server:call(CPid, {set_rate_limit, Rl}). - -get_pub_limit(CPid) -> - gen_server:call(CPid, get_pub_limit). - -set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> - gen_server:call(CPid, {set_pub_limit, Rl}). +call(CPid, Req) -> + gen_server:call(CPid, Req, infinity). %%------------------------------------------------------------------------------ %% gen_server callbacks @@ -103,60 +85,76 @@ init([Transport, RawSocket, Options]) -> {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), - PubLimit = rate_limit(emqx_zone:env(Zone, publish_limit)), - RateLimit = rate_limit(proplists:get_value(rate_limit, Options)), - EnableStats = emqx_zone:env(Zone, enable_stats, true), - IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000), + RateLimit = init_limiter(proplists:get_value(rate_limit, Options)), + PubLimit = init_limiter(emqx_zone:get_env(Zone, publish_limit)), + EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), SendFun = send_fun(Transport, Socket, Peername), ProtoState = emqx_protocol:init(#{peername => Peername, sockname => Sockname, peercert => Peercert, sendfun => SendFun}, Options), ParserState = emqx_protocol:parser(ProtoState), - State = run_socket(#state{transport = Transport, - socket = Socket, - peername = Peername, - await_recv = false, - conn_state = running, - rate_limit = RateLimit, - pub_limit = PubLimit, - proto_state = ProtoState, - parser_state = ParserState, - enable_stats = EnableStats, - idle_timeout = IdleTimout}), + State = run_socket(#state{transport = Transport, + socket = Socket, + peername = Peername, + await_recv = false, + conn_state = running, + rate_limit = RateLimit, + publish_limit = PubLimit, + proto_state = ProtoState, + parser_state = ParserState, + enable_stats = EnableStats, + idle_timeout = IdleTimout}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State, self(), IdleTimout); {error, Reason} -> {stop, Reason} end. -rate_limit(undefined) -> +init_limiter(undefined) -> undefined; -rate_limit({Rate, Burst}) -> +init_limiter({Rate, Burst}) -> esockd_rate_limit:new(Rate, Burst). send_fun(Transport, Socket, Peername) -> fun(Data) -> try Transport:async_send(Socket, Data) of - ok -> + ok -> ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), emqx_metrics:inc('bytes/sent', iolist_size(Data)), ok; Error -> Error catch - error:Error -> {error, Error} + error:Error -> + {error, Error} end end. -handle_call(info, From, State = #state{transport = Transport, socket = Socket, proto_state = ProtoState}) -> +handle_call(info, _From, State = #state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + conn_state = ConnState, + await_recv = AwaitRecv, + rate_limit = RateLimit, + publish_limit = PubLimit, + proto_state = ProtoState}) -> + ConnInfo = [{socktype, Transport:type(Socket)}, + {peername, Peername}, + {sockname, Sockname}, + {conn_state, ConnState}, + {await_recv, AwaitRecv}, + {rate_limit, esockd_rate_limit:info(RateLimit)}, + {publish_limit, esockd_rate_limit:info(PubLimit)}], ProtoInfo = emqx_protocol:info(ProtoState), - ConnInfo = [{socktype, Transport:type(Socket)} | ?record_to_proplist(state, State, ?INFO_KEYS)], - StatsInfo = element(2, handle_call(stats, From, State)), - {reply, lists:append([ConnInfo, StatsInfo, ProtoInfo]), State}; + {reply, lists:usort(lists:append([ConnInfo, ProtoInfo])), State}; -handle_call(stats, _From, State = #state{transport = Transport, socket = Sock, proto_state = ProtoState}) -> +handle_call(stats, _From, State = #state{transport = Transport, + socket = Socket, + proto_state = ProtoState}) -> ProcStats = emqx_misc:proc_stats(), ProtoStats = emqx_protocol:stats(ProtoState), - SockStats = case Transport:getstat(Sock, ?SOCK_STATS) of + SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end, @@ -168,21 +166,6 @@ handle_call(kick, _From, State) -> handle_call(session, _From, State = #state{proto_state = ProtoState}) -> {reply, emqx_protocol:session(ProtoState), State}; -handle_call(clean_acl_cache, _From, State = #state{proto_state = ProtoState}) -> - {reply, ok, State#state{proto_state = emqx_protocol:clean_acl_cache(ProtoState)}}; - -handle_call(get_rate_limit, _From, State = #state{rate_limit = Rl}) -> - {reply, esockd_rate_limit:info(Rl), State}; - -handle_call({set_rate_limit, {Rate, Burst}}, _From, State) -> - {reply, ok, State#state{rate_limit = esockd_rate_limit:new(Rate, Burst)}}; - -handle_call(get_publish_limit, _From, State = #state{pub_limit = Rl}) -> - {reply, esockd_rate_limit:info(Rl), State}; - -handle_call({set_publish_limit, {Rate, Burst}}, _From, State) -> - {reply, ok, State#state{pub_limit = esockd_rate_limit:new(Rate, Burst)}}; - handle_call(Req, _From, State) -> ?LOG(error, "unexpected call: ~p", [Req], State), {reply, ignored, State}. @@ -203,7 +186,7 @@ handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> handle_info(emit_stats, State = #state{proto_state = ProtoState}) -> Stats = element(2, handle_call(stats, undefined, State)), - emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats), + emqx_cm:set_client_stats(emqx_protocol:client_id(ProtoState), Stats), {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> @@ -220,8 +203,8 @@ handle_info(activate_sock, State) -> {noreply, run_socket(State#state{conn_state = running, limit_timer = undefined})}; handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> - Size = iolist_size(Data), ?LOG(debug, "RECV ~p", [Data], State), + Size = iolist_size(Data), emqx_metrics:inc('bytes/received', Size), Incoming = #{bytes => Size, packets => 0}, handle_packet(Data, State#state{await_recv = false, incoming = Incoming}); @@ -247,7 +230,6 @@ handle_info({keepalive, start, Interval}, State = #state{transport = Transport, {ok, KeepAlive} -> {noreply, State#state{keepalive = KeepAlive}}; {error, Error} -> - ?LOG(warning, "Keepalive error - ~p", [Error], State), shutdown(Error, State) end; @@ -256,10 +238,8 @@ handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> {ok, KeepAlive1} -> {noreply, State#state{keepalive = KeepAlive1}}; {error, timeout} -> - ?LOG(debug, "Keepalive timeout", [], State), shutdown(keepalive_timeout, State); {error, Error} -> - ?LOG(warning, "Keepalive error - ~p", [Error], State), shutdown(Error, State) end; @@ -286,28 +266,25 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%------------------------------------------------------------------------------ -%% Internal functions +%% Parse and handle packets %%------------------------------------------------------------------------------ %% Receive and parse data handle_packet(<<>>, State) -> {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; -handle_packet(Bytes, State = #state{incoming = Incoming, - parser_state = ParserState, - proto_state = ProtoState, - idle_timeout = IdleTimeout}) -> - case catch emqx_frame:parse(Bytes, ParserState) of +handle_packet(Data, State = #state{proto_state = ProtoState, + parser_state = ParserState, + idle_timeout = IdleTimeout}) -> + case catch emqx_frame:parse(Data, ParserState) of {more, NewParserState} -> {noreply, State#state{parser_state = NewParserState}, IdleTimeout}; {ok, Packet = ?PACKET(Type), Rest} -> emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - ParserState1 = emqx_protocol:parser(ProtoState1), - handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), - proto_state = ProtoState1, - parser_state = ParserState1}); + NewState = State#state{proto_state = ProtoState1}, + handle_packet(Rest, inc_publish_cnt(Type, reset_parser(NewState))); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); @@ -320,20 +297,27 @@ handle_packet(Bytes, State = #state{incoming = Incoming, ?LOG(error, "Framing error - ~p", [Error], State), shutdown(Error, State); {'EXIT', Reason} -> - ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Bytes], State), + ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Data], State), shutdown(parse_error, State) end. -count_packets(?PUBLISH, Incoming = #{packets := Num}) -> - Incoming#{packets := Num + 1}; -count_packets(?SUBSCRIBE, Incoming = #{packets := Num}) -> - Incoming#{packets := Num + 1}; -count_packets(_Type, Incoming) -> - Incoming. +reset_parser(State = #state{proto_state = ProtoState}) -> + State#state{parser_state = emqx_protocol:parser(ProtoState)}. -ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl, - incoming = #{bytes := Bytes, packets := Pkts}}) -> - ensure_rate_limit([{Pl, #state.pub_limit, Pkts}, {Rl, #state.rate_limit, Bytes}], State). +inc_publish_cnt(Type, State = #state{incoming = Incoming = #{packets := Cnt}}) + when Type == ?PUBLISH; Type == ?SUBSCRIBE -> + State#state{incoming = Incoming#{packets := Cnt + 1}}; +inc_publish_cnt(_Type, State) -> + State. + +%%------------------------------------------------------------------------------ +%% Ensure rate limit +%%------------------------------------------------------------------------------ + +ensure_rate_limit(State = #state{rate_limit = Rl, publish_limit = Pl, + incoming = #{packets := Packets, bytes := Bytes}}) -> + ensure_rate_limit([{Pl, #state.publish_limit, Packets}, + {Rl, #state.rate_limit, Bytes}], State). ensure_rate_limit([], State) -> run_socket(State); @@ -356,12 +340,15 @@ run_socket(State = #state{transport = Transport, socket = Sock}) -> Transport:async_recv(Sock, 0, infinity), State#state{await_recv = true}. +%%------------------------------------------------------------------------------ +%% Ensure stats timer +%%------------------------------------------------------------------------------ + ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined, - idle_timeout = IdleTimeout}) -> + stats_timer = undefined, + idle_timeout = IdleTimeout}) -> State#state{stats_timer = erlang:send_after(IdleTimeout, self(), emit_stats)}; -ensure_stats_timer(State) -> - State. +ensure_stats_timer(State) -> State. shutdown(Reason, State) -> stop({shutdown, Reason}, State). @@ -370,7 +357,6 @@ stop(Reason, State) -> {stop, Reason, State}. maybe_gc(State) -> - State. %% TODO:... - %%Cb = fun() -> Transport:gc(Sock), end, - %%emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). + %% TODO: gc and shutdown policy + State. diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 10498afcf..82db0acf5 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -31,7 +31,8 @@ -export_type([options/0, parse_state/0]). --define(DEFAULT_OPTIONS, #{max_packet_size => ?MAX_PACKET_SIZE, version => ?MQTT_PROTO_V4}). +-define(DEFAULT_OPTIONS, #{max_packet_size => ?MAX_PACKET_SIZE, + version => ?MQTT_PROTO_V4}). %%------------------------------------------------------------------------------ %% Init parse state @@ -330,7 +331,7 @@ parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> {Value + Len * Multiplier, Rest}. parse_topic_filters(subscribe, Bin) -> - [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + [{Topic, #{rh => Rh, rap => Rap, nl => Nl, qos => QoS}} || <> <= Bin]; parse_topic_filters(unsubscribe, Bin) -> @@ -576,12 +577,12 @@ serialize_property('Shared-Subscription-Available', Val) -> serialize_topic_filters(subscribe, TopicFilters, ?MQTT_PROTO_V5) -> << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:2, Rh:2, (flag(Rap)):1,(flag(Nl)):1, QoS:2 >> - || {Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + || {Topic, #{rh := Rh, rap := Rap, nl := Nl, qos := QoS}} <- TopicFilters >>; serialize_topic_filters(subscribe, TopicFilters, _Ver) -> << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:6, QoS:2>> - || {Topic, #mqtt_subopts{qos = QoS}} <- TopicFilters >>; + || {Topic, #{qos := QoS}} <- TopicFilters >>; serialize_topic_filters(unsubscribe, TopicFilters, _Ver) -> << <<(serialize_utf8_string(Topic))/binary>> || Topic <- TopicFilters >>. diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl new file mode 100644 index 000000000..184e03673 --- /dev/null +++ b/src/emqx_mqtt_caps.erl @@ -0,0 +1,139 @@ +%% Copyright (c) 2018 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. + +%% @doc MQTTv5 capabilities +-module(emqx_mqtt_caps). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). + +-export([check_pub/2, check_sub/2]). +-export([get_caps/1, get_caps/2]). + +-type(caps() :: #{max_packet_size => integer(), + max_clientid_len => integer(), + max_topic_alias => integer(), + max_topic_levels => integer(), + max_qos_allowed => mqtt_qos(), + mqtt_retain_available => boolean(), + mqtt_shared_subscription => boolean(), + mqtt_wildcard_subscription => boolean()}). + +-export_type([caps/0]). + +-define(UNLIMITED, 0). +-define(DEFAULT_CAPS, [{max_packet_size, ?MAX_PACKET_SIZE}, + {max_clientid_len, ?MAX_CLIENTID_LEN}, + {max_topic_alias, ?UNLIMITED}, + {max_topic_levels, ?UNLIMITED}, + {max_qos_allowed, ?QOS_2}, + {mqtt_retain_available, true}, + {mqtt_shared_subscription, true}, + {mqtt_wildcard_subscription, true}]). + +-define(PUBCAP_KEYS, [max_qos_allowed, + mqtt_retain_available]). +-define(SUBCAP_KEYS, [max_qos_allowed, + max_topic_levels, + mqtt_retain_available, + mqtt_shared_subscription, + mqtt_wildcard_subscription]). + +-spec(check_pub(zone(), map()) -> ok | {error, mqtt_reason_code()}). +check_pub(Zone, Props) when is_map(Props) -> + do_check_pub(Props, maps:to_list(get_caps(Zone, publish))). + +do_check_pub(_Props, []) -> + ok; +do_check_pub(Props = #{qos := QoS}, [{max_qos_allowed, MaxQoS}|Caps]) -> + case QoS > MaxQoS of + true -> {error, ?RC_QOS_NOT_SUPPORTED}; + false -> do_check_pub(Props, Caps) + end; +do_check_pub(#{retain := true}, [{mqtt_retain_available, false}|_Caps]) -> + {error, ?RC_RETAIN_NOT_SUPPORTED}; +do_check_pub(Props, [{mqtt_retain_available, true}|Caps]) -> + do_check_pub(Props, Caps). + +-spec(check_sub(zone(), mqtt_topic_filters()) -> {ok | error, mqtt_topic_filters()}). +check_sub(Zone, TopicFilters) -> + Caps = maps:to_list(get_caps(Zone, subscribe)), + lists:foldr(fun({Topic, Opts}, {Ok, Result}) -> + case check_sub(Topic, Opts, Caps) of + {ok, Opts1} -> + {Ok, [{Topic, Opts1}|Result]}; + {error, Opts1} -> + {error, [{Topic, Opts1}|Result]} + end + end, {ok, []}, TopicFilters). + +check_sub(_Topic, Opts, []) -> + {ok, Opts}; +check_sub(Topic, Opts = #{qos := QoS}, [{max_qos_allowed, MaxQoS}|Caps]) -> + check_sub(Topic, Opts#{qos := min(QoS, MaxQoS)}, Caps); +check_sub(Topic, Opts, [{mqtt_shared_subscription, true}|Caps]) -> + check_sub(Topic, Opts, Caps); +check_sub(Topic, Opts, [{mqtt_shared_subscription, false}|Caps]) -> + case maps:is_key(share, Opts) of + true -> + {error, Opts#{rc := ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED}}; + false -> check_sub(Topic, Opts, Caps) + end; +check_sub(Topic, Opts, [{mqtt_wildcard_subscription, true}|Caps]) -> + check_sub(Topic, Opts, Caps); +check_sub(Topic, Opts, [{mqtt_wildcard_subscription, false}|Caps]) -> + case emqx_topic:wildcard(Topic) of + true -> + {error, Opts#{rc := ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}}; + false -> check_sub(Topic, Opts, Caps) + end; +check_sub(Topic, Opts, [{max_topic_levels, ?UNLIMITED}|Caps]) -> + check_sub(Topic, Opts, Caps); +check_sub(Topic, Opts, [{max_topic_levels, Limit}|Caps]) -> + case emqx_topic:levels(Topic) of + Levels when Levels > Limit -> + {error, Opts#{rc := ?RC_TOPIC_FILTER_INVALID}}; + _ -> check_sub(Topic, Opts, Caps) + end. + +get_caps(Zone, publish) -> + with_env(Zone, '$mqtt_pub_caps', + fun() -> + filter_caps(?PUBCAP_KEYS, get_caps(Zone)) + end); + +get_caps(Zone, subscribe) -> + with_env(Zone, '$mqtt_sub_caps', + fun() -> + filter_caps(?SUBCAP_KEYS, get_caps(Zone)) + end). + +get_caps(Zone) -> + with_env(Zone, '$mqtt_caps', + fun() -> + maps:from_list([{Cap, emqx_zone:get_env(Zone, Cap, Def)} + || {Cap, Def} <- ?DEFAULT_CAPS]) + end). + +filter_caps(Keys, Caps) -> + maps:filter(fun(Key, _Val) -> lists:member(Key, Keys) end, Caps). + +with_env(Zone, Key, InitFun) -> + case emqx_zone:get_env(Zone, Key) of + undefined -> Caps = InitFun(), + ok = emqx_zone:set_env(Zone, Key, Caps), + Caps; + ZoneCaps -> ZoneCaps + end. + diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 65f125f68..67d1bffff 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -17,21 +17,59 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([protocol_name/1, type_name/1]). +-export([protocol_name/1]). +-export([type_name/1]). +-export([validate/1]). -export([format/1]). -export([to_message/2, from_message/2]). %% @doc Protocol name of version -spec(protocol_name(mqtt_version()) -> binary()). -protocol_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>; -protocol_name(?MQTT_PROTO_V4) -> <<"MQTT">>; -protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>. +protocol_name(?MQTT_PROTO_V3) -> + <<"MQIsdp">>; +protocol_name(?MQTT_PROTO_V4) -> + <<"MQTT">>; +protocol_name(?MQTT_PROTO_V5) -> + <<"MQTT">>. %% @doc Name of MQTT packet type -spec(type_name(mqtt_packet_type()) -> atom()). type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). +validate(?SUBSCRIBE_PACKET(_PacketId, _Properties, [])) -> + error(packet_empty_topic_filters); +validate(?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters)) -> + validate_packet_id(PacketId) + andalso validate_properties(?SUBSCRIBE, Properties) + andalso ok == lists:foreach(fun validate_subscription/1, TopicFilters); + +validate(?UNSUBSCRIBE_PACKET(_PacketId, [])) -> + error(packet_empty_topic_filters); +validate(?UNSUBSCRIBE_PACKET(PacketId, TopicFilters)) -> + validate_packet_id(PacketId) + andalso ok == lists:foreach(fun emqx_topic:validate/1, TopicFilters); + +validate(_Packet) -> + true. + +validate_packet_id(0) -> + error(bad_packet_id); +validate_packet_id(_) -> + true. + +validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := 0}) -> + error(bad_subscription_identifier); +validate_properties(?SUBSCRIBE, _) -> + true. + +validate_subscription({Topic, #{qos := QoS}}) -> + emqx_topic:validate(filter, Topic) andalso validate_qos(QoS). + +validate_qos(QoS) when ?QOS0 =< QoS, QoS =< ?QOS2 -> + true; +validate_qos(_) -> error(bad_qos). + %% @doc From Message to Packet -spec(from_message(mqtt_packet_id(), message()) -> mqtt_packet()). from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) -> diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 8b421f20a..9b874041e 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -15,7 +15,11 @@ -module(emqx_pmon). -export([new/0]). --export([monitor/2, monitor/3, demonitor/2, find/2, erase/2]). +-export([monitor/2, monitor/3]). +-export([demonitor/2]). +-export([find/2]). +-export([erase/2]). + -compile({no_auto_import,[monitor/3]}). -type(pmon() :: {?MODULE, map()}). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 705674000..077c7a00c 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -16,523 +16,595 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_misc.hrl"). --export([init/2, info/1, stats/1, clientid/1, session/1]). -%%-export([capabilities/1]). +-export([init/2, info/1, caps/1, stats/1]). +-export([client/1, client_id/1]). +-export([session/1]). -export([parser/1]). -export([received/2, process/2, deliver/2, send/2]). -export([shutdown/2]). --ifdef(TEST). --compile(export_all). --endif. +-record(pstate, { + zone, + sendfun, + peername, + peercert, + proto_ver, + proto_name, + ackprops, + client_id, + client_pid, + conn_props, + ack_props, + username, + session, + clean_start, + packet_size, + will_msg, + keepalive, + mountpoint, + is_super, + is_bridge, + enable_acl, + recv_stats, + send_stats, + connected, + connected_at + }). --define(CAPABILITIES, [{max_packet_size, ?MAX_PACKET_SIZE}, - {max_clientid_len, ?MAX_CLIENTID_LEN}, - {max_topic_alias, 0}, - {max_qos_allowed, ?QOS2}, - {retain_available, true}, - {shared_subscription, true}, - {wildcard_subscription, true}]). +-type(state() :: #pstate{}). --record(proto_state, {zone, sockprops, capabilities, connected, client_id, client_pid, - clean_start, proto_ver, proto_name, username, connprops, - is_superuser, will_msg, keepalive, keepalive_backoff, session, - recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0, - mountpoint, is_bridge, connected_at}). +-export_type([state/0]). --define(INFO_KEYS, [capabilities, connected, client_id, clean_start, username, proto_ver, proto_name, - keepalive, will_msg, mountpoint, is_bridge, connected_at]). +-define(LOG(Level, Format, Args, PState), + emqx_logger:Level([{client, PState#pstate.client_id}], "Client(~s@~s): " ++ Format, + [PState#pstate.client_id, esockd_net:format(PState#pstate.peername) | Args])). --define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]). +%%------------------------------------------------------------------------------ +%% Init +%%------------------------------------------------------------------------------ --define(LOG(Level, Format, Args, State), - emqx_logger:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, - [State#proto_state.client_id, - esockd_net:format(maps:get(peername, State#proto_state.sockprops)) | Args])). - --type(proto_state() :: #proto_state{}). - --export_type([proto_state/0]). - -init(SockProps = #{peercert := Peercert}, Options) -> +-spec(init(map(), list()) -> state()). +init(#{peername := Peername, peercert := Peercert, sendfun := SendFun}, Options) -> Zone = proplists:get_value(zone, Options), - MountPoint = emqx_zone:env(Zone, mountpoint), - Backoff = emqx_zone:env(Zone, keepalive_backoff, 0.75), - Username = case proplists:get_value(peer_cert_as_username, Options) of - cn -> esockd_peercert:common_name(Peercert); - dn -> esockd_peercert:subject(Peercert); - _ -> undefined - end, - #proto_state{zone = Zone, - sockprops = SockProps, - capabilities = capabilities(Zone), - connected = false, - clean_start = true, - client_pid = self(), - proto_ver = ?MQTT_PROTO_V4, - proto_name = <<"MQTT">>, - username = Username, - is_superuser = false, - keepalive_backoff = Backoff, - mountpoint = MountPoint, - is_bridge = false, - recv_pkt = 0, - recv_msg = 0, - send_pkt = 0, - send_msg = 0}. + #pstate{zone = Zone, + sendfun = SendFun, + peername = Peername, + peercert = Peercert, + proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + client_pid = self(), + username = init_username(Peercert, Options), + is_super = false, + clean_start = false, + packet_size = emqx_zone:get_env(Zone, max_packet_size), + mountpoint = emqx_zone:get_env(Zone, mountpoint), + is_bridge = false, + enable_acl = emqx_zone:get_env(Zone, enable_acl), + recv_stats = #{msg => 0, pkt => 0}, + send_stats = #{msg => 0, pkt => 0}, + connected = fasle}. -capabilities(Zone) -> - Capabilities = emqx_zone:env(Zone, mqtt_capabilities, []), - maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)). +init_username(Peercert, Options) -> + case proplists:get_value(peer_cert_as_username, Options) of + cn -> esockd_peercert:common_name(Peercert); + dn -> esockd_peercert:subject(Peercert); + _ -> undefined + end. -parser(#proto_state{capabilities = #{max_packet_size := Size}, proto_ver = Ver}) -> - emqx_frame:initial_state(#{max_packet_size => Size, version => Ver}). +set_username(Username, PState = #pstate{username = undefined}) -> + PState#pstate{username = Username}; +set_username(_Username, PState) -> + PState. -info(ProtoState) -> - ?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS). +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ -stats(ProtoState) -> - ?record_to_proplist(proto_state, ProtoState, ?STATS_KEYS). +info(#pstate{zone = Zone, + peername = Peername, + proto_ver = ProtoVer, + proto_name = ProtoName, + conn_props = ConnProps, + client_id = ClientId, + username = Username, + clean_start = CleanStart, + keepalive = Keepalive, + mountpoint = Mountpoint, + is_super = IsSuper, + is_bridge = IsBridge, + connected = Connected, + connected_at = ConnectedAt}) -> + [{zone, Zone}, + {peername, Peername}, + {proto_ver, ProtoVer}, + {proto_name, ProtoName}, + {conn_props, ConnProps}, + {client_id, ClientId}, + {username, Username}, + {clean_start, CleanStart}, + {keepalive, Keepalive}, + {mountpoint, Mountpoint}, + {is_super, IsSuper}, + {is_bridge, IsBridge}, + {connected, Connected}, + {connected_at, ConnectedAt}]. -clientid(#proto_state{client_id = ClientId}) -> +caps(#pstate{zone = Zone}) -> + emqx_mqtt_caps:get_caps(Zone). + +client(#pstate{zone = Zone, + client_id = ClientId, + client_pid = ClientPid, + peername = Peername, + username = Username}) -> + #client{id = ClientId, + pid = ClientPid, + zone = Zone, + peername = Peername, + username = Username}. + +client_id(#pstate{client_id = ClientId}) -> ClientId. -client(#proto_state{sockprops = #{peername := Peername}, - client_id = ClientId, client_pid = ClientPid, username = Username}) -> - #client{id = ClientId, pid = ClientPid, username = Username, peername = Peername}. +stats(#pstate{recv_stats = #{pkt := RecvPkt, msg := RecvMsg}, + send_stats = #{pkt := SendPkt, msg := SendMsg}}) -> + [{recv_pkt, RecvPkt}, + {recv_msg, RecvMsg}, + {send_pkt, SendPkt}, + {send_msg, SendMsg}]. -session(#proto_state{session = Session}) -> - Session. +session(#pstate{session = SPid}) -> + SPid. -%% CONNECT – Client requests a connection to a Server +parser(#pstate{packet_size = Size, proto_ver = Ver}) -> + emqx_frame:initial_state(#{packet_size => Size, version => Ver}). -%% A Client can only send the CONNECT Packet once over a Network Connection. --spec(received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, term()}). -received(Packet = ?PACKET(?CONNECT), ProtoState = #proto_state{connected = false}) -> - trace(recv, Packet, ProtoState), - process(Packet, inc_stats(recv, ?CONNECT, ProtoState#proto_state{connected = true})); +%%------------------------------------------------------------------------------ +%% Packet Received +%%------------------------------------------------------------------------------ -received(?PACKET(?CONNECT), State = #proto_state{connected = true}) -> - {error, protocol_bad_connect, State}; +-spec(received(mqtt_packet(), state()) + -> {ok, state()} | {error, term()} | {error, term(), state()}). +received(?PACKET(Type), PState = #pstate{connected = false}) + when Type =/= ?CONNECT -> + {error, proto_not_connected, PState}; -%% Received other packets when CONNECT not arrived. -received(_Packet, ProtoState = #proto_state{connected = false}) -> - {error, protocol_not_connected, ProtoState}; +received(?PACKET(?CONNECT), PState = #pstate{connected = true}) -> + {error, proto_bad_connect, PState}; -received(Packet = ?PACKET(Type), ProtoState) -> - trace(recv, Packet, ProtoState), - case validate_packet(Packet) of - ok -> - process(Packet, inc_stats(recv, Type, ProtoState)); - {error, Reason} -> - {error, Reason, ProtoState} +received(Packet = ?PACKET(Type), PState) -> + trace(recv, Packet, PState), + case catch emqx_packet:validate(Packet) of + true -> + process(Packet, inc_stats(recv, Type, PState)); + {'EXIT', {ReasonCode, _Stacktrace}} when is_integer(ReasonCode) -> + deliver({disconnect, ReasonCode}, PState), + {error, protocol_error, PState}; + {'EXIT', {Reason, _Stacktrace}} -> + deliver({disconnect, ?RC_MALFORMED_PACKET}, PState), + {error, Reason, PState} end. -process(?CONNECT_PACKET(Var), ProtoState = #proto_state{zone = Zone, - username = Username0, - client_pid = ClientPid}) -> - #mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - clean_start = CleanStart, - keepalive = Keepalive, - properties = ConnProps, - client_id = ClientId, - username = Username, - password = Password} = Var, - ProtoState1 = ProtoState#proto_state{proto_ver = ProtoVer, +%%------------------------------------------------------------------------------ +%% Process Packet +%%------------------------------------------------------------------------------ + +process(?CONNECT_PACKET( + #mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + clean_start = CleanStart, + keepalive = Keepalive, + properties = ConnProps, + client_id = ClientId, + username = Username, + password = Password} = Connect), PState) -> + + PState1 = set_username(Username, + PState#pstate{client_id = ClientId, + proto_ver = ProtoVer, proto_name = ProtoName, - username = if Username0 == undefined -> - Username; - true -> Username0 - end, %% TODO: fixme later. - client_id = ClientId, clean_start = CleanStart, keepalive = Keepalive, - connprops = ConnProps, - will_msg = willmsg(Var, ProtoState), + conn_props = ConnProps, + will_msg = willmsg(Connect, PState), is_bridge = IsBridge, - connected_at = os:timestamp()}, + connected = true, + connected_at = os:timestamp()}), - {ReturnCode1, SessPresent, ProtoState3} = - case validate_connect(Var, ProtoState1) of - ?RC_SUCCESS -> - case authenticate(client(ProtoState1), Password) of - {ok, IsSuperuser} -> - %% Generate clientId if null - ProtoState2 = maybe_set_clientid(ProtoState1), - %% Open session - case emqx_sm:open_session(#{zone => Zone, - clean_start => CleanStart, - client_id => clientid(ProtoState2), - username => Username, - client_pid => ClientPid}) of - {ok, Session} -> %% TODO:... - SP = true, %% TODO:... - %% TODO: Register the client - emqx_cm:register_client(clientid(ProtoState2)), - %%emqx_cm:reg(client(State2)), - %% Start keepalive - start_keepalive(Keepalive, ProtoState2), - %% Emit Stats - %% self() ! emit_stats, - %% ACCEPT - {?RC_SUCCESS, SP, ProtoState2#proto_state{session = Session, is_superuser = IsSuperuser}}; - {error, Error} -> - ?LOG(error, "Failed to open session: ~p", [Error], ProtoState2), - {?RC_UNSPECIFIED_ERROR, false, ProtoState2} %% TODO: the error reason??? + connack( + case check_connect(Connect, PState1) of + ok -> + case authenticate(client(PState1), Password) of + {ok, IsSuper} -> + %% Maybe assign a clientId + PState2 = maybe_assign_client_id(PState1#pstate{is_super = IsSuper}), + %% Open session + case try_open_session(PState2) of + {ok, SPid, SP} -> + PState3 = PState2#pstate{session = SPid}, + ok = emqx_cm:register_client({client_id(PState3), self()}, info(PState3)), + %% Start keepalive + start_keepalive(Keepalive, PState3), + %% TODO: 'Run hooks' before open_session? + emqx_hooks:run('client.connected', [?RC_SUCCESS], client(PState3)), + %% Success + {?RC_SUCCESS, SP, replvar(PState3)}; + {error, Error} -> + ?LOG(error, "Failed to open session: ~p", [Error], PState1), + {?RC_UNSPECIFIED_ERROR, PState1} end; - {error, Reason}-> - ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], ProtoState1), - {?RC_BAD_USER_NAME_OR_PASSWORD, false, ProtoState1} - end; - ReturnCode -> - {ReturnCode, false, ProtoState1} - end, - %% Run hooks - emqx_hooks:run('client.connected', [ReturnCode1], client(ProtoState3)), - %%TODO: Send Connack - send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), ProtoState3), - %% stop if authentication failure - stop_if_auth_failure(ReturnCode1, ProtoState3); + {error, Reason} -> + ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], PState1), + {?RC_NOT_AUTHORIZED, PState1} + end; + {error, ReasonCode} -> + {ReasonCode, PState1} + end); -process(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload), - State = #proto_state{is_superuser = IsSuper}) -> - case IsSuper orelse allow == check_acl(publish, Topic, client(State)) of - true -> publish(Packet, State); - false -> ?LOG(error, "Cannot publish to ~s for ACL Deny", [Topic], State) - end, - {ok, State}; +process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState) -> + case check_publish(Packet, PState) of + ok -> + do_publish(Packet, PState); + {error, ReasonCode} -> + ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", [Topic, ReasonCode], PState), + {ok, PState} + end; -process(?PUBACK_PACKET(PacketId), State = #proto_state{session = Session}) -> - emqx_session:puback(Session, PacketId), - {ok, State}; +process(Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), PState) -> + case check_publish(Packet, PState) of + ok -> + do_publish(Packet, PState); + {error, ReasonCode} -> + deliver({puback, PacketId, ReasonCode}, PState) + end; -process(?PUBREC_PACKET(PacketId), State = #proto_state{session = Session}) -> - emqx_session:pubrec(Session, PacketId), - send(?PUBREL_PACKET(PacketId), State); +process(Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), PState) -> + case check_publish(Packet, PState) of + ok -> + do_publish(Packet, PState); + {error, ReasonCode} -> + deliver({pubrec, PacketId, ReasonCode}, PState) + end; -process(?PUBREL_PACKET(PacketId), State = #proto_state{session = Session}) -> - emqx_session:pubrel(Session, PacketId), - send(?PUBCOMP_PACKET(PacketId), State); +process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + ok = emqx_session:puback(SPid, PacketId, ReasonCode), + {ok, PState}; -process(?PUBCOMP_PACKET(PacketId), State = #proto_state{session = Session})-> - emqx_session:pubcomp(Session, PacketId), {ok, State}; +process(?PUBREC_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + ok = emqx_session:pubrec(SPid, PacketId, ReasonCode), + send(?PUBREL_PACKET(PacketId), PState); -%% Protect from empty topic table -process(?SUBSCRIBE_PACKET(PacketId, []), State) -> - send(?SUBACK_PACKET(PacketId, []), State); +process(?PUBREL_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + ok = emqx_session:pubrel(SPid, PacketId, ReasonCode), + send(?PUBCOMP_PACKET(PacketId), PState); -%% TODO: refactor later... -process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), State) -> - #proto_state{client_id = ClientId, - username = Username, - is_superuser = IsSuperuser, - mountpoint = MountPoint, - session = Session} = State, - Client = client(State), - TopicFilters = parse_topic_filters(RawTopicFilters), - AllowDenies = if - IsSuperuser -> []; - true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicFilters] - end, - case lists:member(deny, AllowDenies) of - true -> - ?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicFilters], State), - send(?SUBACK_PACKET(PacketId, [?RC_NOT_AUTHORIZED || _ <- TopicFilters]), State); - false -> - case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicFilters) of +process(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> + ok = emqx_session:pubcomp(SPid, PacketId, ReasonCode), + {ok, PState}; + +process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), + PState = #pstate{client_id = ClientId, session = SPid}) -> + case check_subscribe( + parse_topic_filters(?SUBSCRIBE, RawTopicFilters), PState) of + {ok, TopicFilters} -> + case emqx_hooks:run('client.subscribe', [ClientId], TopicFilters) of {ok, TopicFilters1} -> - ok = emqx_session:subscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicFilters1)}), - {ok, State}; - {stop, _} -> {ok, State} - end - end; - -%% Protect from empty topic list -process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> - send(?UNSUBACK_PACKET(PacketId), State); - -process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopics), - State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - session = Session}) -> - case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of - {ok, TopicTable} -> - emqx_session:unsubscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicTable)}); - {stop, _} -> - ok - end, - send(?UNSUBACK_PACKET(PacketId), State); - -process(?PACKET(?PINGREQ), ProtoState) -> - send(?PACKET(?PINGRESP), ProtoState); - -process(?PACKET(?DISCONNECT), ProtoState) -> - % Clean willmsg - {stop, normal, ProtoState#proto_state{will_msg = undefined}}. - -deliver({publish, PacketId, Msg}, - State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - is_bridge = IsBridge}) -> - emqx_hooks:run('message.delivered', [ClientId], - emqx_message:set_header(username, Username, Msg)), - Msg1 = unmount(MountPoint, clean_retain(IsBridge, Msg)), - send(emqx_packet:from_message(PacketId, Msg1), State); - -deliver({pubrel, PacketId}, State) -> - send(?PUBREL_PACKET(PacketId), State); - -deliver({suback, PacketId, ReasonCodes}, ProtoState) -> - send(?SUBACK_PACKET(PacketId, ReasonCodes), ProtoState); - -deliver({unsuback, PacketId, ReasonCodes}, ProtoState) -> - send(?UNSUBACK_PACKET(PacketId, ReasonCodes), ProtoState). - -publish(Packet = ?PUBLISH_PACKET(?QOS_0, PacketId), - State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - session = Session}) -> - Msg = emqx_message:set_header(username, Username, - emqx_packet:to_message(ClientId, Packet)), - emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg)); - -publish(Packet = ?PUBLISH_PACKET(?QOS_1), State) -> - with_puback(?PUBACK, Packet, State); - -publish(Packet = ?PUBLISH_PACKET(?QOS_2), State) -> - with_puback(?PUBREC, Packet, State). - -with_puback(Type, Packet = ?PUBLISH_PACKET(_QoS, PacketId), - State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - session = Session}) -> - Msg = emqx_message:set_header(username, Username, - emqx_packet:to_message(ClientId, Packet)), - case emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg)) of - {error, Error} -> - ?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State); - _Delivery -> send({Type, PacketId}, State) %% TODO: - end. - --spec(send({mqtt_packet_type(), mqtt_packet_id()} | - {mqtt_packet_id(), message()} | - mqtt_packet(), proto_state()) -> {ok, proto_state()}). -send({?PUBACK, PacketId}, State) -> - send(?PUBACK_PACKET(PacketId), State); - -send({?PUBREC, PacketId}, State) -> - send(?PUBREC_PACKET(PacketId), State); - -send(Packet = ?PACKET(Type), ProtoState = #proto_state{proto_ver = Ver, - sockprops = #{sendfun := SendFun}}) -> - Data = emqx_frame:serialize(Packet, #{version => Ver}), - case SendFun(Data) of - {error, Reason} -> - {error, Reason}; - _ -> emqx_metrics:sent(Packet), - trace(send, Packet, ProtoState), - {ok, inc_stats(send, Type, ProtoState)} - end. - -trace(recv, Packet, ProtoState) -> - ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)], ProtoState); - -trace(send, Packet, ProtoState) -> - ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)], ProtoState). - -inc_stats(recv, Type, ProtoState = #proto_state{recv_pkt = PktCnt, recv_msg = MsgCnt}) -> - ProtoState#proto_state{recv_pkt = PktCnt + 1, - recv_msg = if Type =:= ?PUBLISH -> MsgCnt + 1; - true -> MsgCnt - end}; -inc_stats(send, Type, ProtoState = #proto_state{send_pkt = PktCnt, send_msg = MsgCnt}) -> - ProtoState#proto_state{send_pkt = PktCnt + 1, - send_msg = if Type =:= ?PUBLISH -> MsgCnt + 1; - true -> MsgCnt - end}. - -stop_if_auth_failure(?RC_SUCCESS, State) -> - {ok, State}; -stop_if_auth_failure(RC, State) when RC =/= ?RC_SUCCESS -> - {stop, {shutdown, auth_failure}, State}. - -shutdown(_Error, #proto_state{client_id = undefined}) -> - ignore; -shutdown(conflict, _State = #proto_state{client_id = ClientId}) -> - emqx_cm:unregister_client(ClientId), - ignore; -shutdown(mnesia_conflict, _State = #proto_state{client_id = ClientId}) -> - emqx_cm:unregister_client(ClientId), - ignore; -shutdown(Error, State = #proto_state{client_id = ClientId, - will_msg = WillMsg}) -> - ?LOG(info, "Shutdown for ~p", [Error], State), - %% Auth failure not publish the will message - case Error =:= auth_failure of - true -> ok; - false -> send_willmsg(ClientId, WillMsg) - end, - emqx_hooks:run('client.disconnected', [Error], client(State)), - emqx_cm:unregister_client(ClientId), - ok. - -willmsg(Packet, State = #proto_state{client_id = ClientId, mountpoint = MountPoint}) - when is_record(Packet, mqtt_packet_connect) -> - case emqx_packet:to_message(ClientId, Packet) of - undefined -> undefined; - Msg -> mount(replvar(MountPoint, State), Msg) - end. - -%% Generate a client if if nulll -maybe_set_clientid(State = #proto_state{client_id = NullId}) - when NullId =:= undefined orelse NullId =:= <<>> -> - {_, NPid, _} = emqx_guid:new(), - ClientId = iolist_to_binary(["emqx_", integer_to_list(NPid)]), - State#proto_state{client_id = ClientId}; - -maybe_set_clientid(State) -> - State. - -send_willmsg(_ClientId, undefined) -> - ignore; -send_willmsg(ClientId, WillMsg) -> - emqx_broker:publish(WillMsg#message{from = ClientId}). - -start_keepalive(0, _State) -> ignore; - -start_keepalive(Sec, #proto_state{keepalive_backoff = Backoff}) when Sec > 0 -> - self() ! {keepalive, start, round(Sec * Backoff)}. - -%%-------------------------------------------------------------------- -%% Validate Packets -%%-------------------------------------------------------------------- - -validate_connect(Connect = #mqtt_packet_connect{}, ProtoState) -> - case validate_protocol(Connect) of - true -> - case validate_clientid(Connect, ProtoState) of - true -> ?RC_SUCCESS; - false -> ?RC_CLIENT_IDENTIFIER_NOT_VALID + ok = emqx_session:subscribe(SPid, PacketId, Properties, mount(TopicFilters1, PState)), + {ok, PState}; + {stop, _} -> + ReasonCodes = lists:duplicate(length(TopicFilters), + ?RC_IMPLEMENTATION_SPECIFIC_ERROR), + deliver({suback, PacketId, ReasonCodes}, PState) end; - false -> - ?RC_UNSUPPORTED_PROTOCOL_VERSION - end. - -validate_protocol(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}) -> - lists:member({Ver, Name}, ?PROTOCOL_NAMES). - -validate_clientid(#mqtt_packet_connect{client_id = ClientId}, - #proto_state{capabilities = #{max_clientid_len := MaxLen}}) - when (byte_size(ClientId) >= 1) andalso (byte_size(ClientId) =< MaxLen) -> - true; - -%% Issue#599: Null clientId and clean_start = false -validate_clientid(#mqtt_packet_connect{client_id = ClientId, - clean_start = CleanStart}, _ProtoState) - when byte_size(ClientId) == 0 andalso (not CleanStart) -> - false; - -%% MQTT3.1.1 allow null clientId. -validate_clientid(#mqtt_packet_connect{proto_ver =?MQTT_PROTO_V4, - client_id = ClientId}, _ProtoState) - when byte_size(ClientId) =:= 0 -> - true; - -validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer, - clean_start = CleanStart}, ProtoState) -> - ?LOG(warning, "Invalid clientId. ProtoVer: ~p, CleanStart: ~s", - [ProtoVer, CleanStart], ProtoState), - false. - -validate_packet(?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload)) -> - case emqx_topic:validate({name, Topic}) of - true -> ok; - false -> {error, badtopic} + {error, TopicFilters} -> + ReasonCodes = lists:map(fun({_, #{rc := ?RC_SUCCESS}}) -> + ?RC_IMPLEMENTATION_SPECIFIC_ERROR; + ({_, #{rc := ReasonCode}}) -> + ReasonCode + end, TopicFilters), + deliver({suback, PacketId, ReasonCodes}, PState) end; -validate_packet(?SUBSCRIBE_PACKET(_PacketId, TopicTable)) -> - validate_topics(filter, TopicTable); - -validate_packet(?UNSUBSCRIBE_PACKET(_PacketId, Topics)) -> - validate_topics(filter, Topics); - -validate_packet(_Packet) -> - ok. - -validate_topics(_Type, []) -> - {error, empty_topics}; - -validate_topics(Type, TopicTable = [{_Topic, _SubOpts}|_]) - when Type =:= name orelse Type =:= filter -> - Valid = fun(Topic, QoS) -> - emqx_topic:validate({Type, Topic}) and validate_qos(QoS) - end, - case [Topic || {Topic, SubOpts} <- TopicTable, - not Valid(Topic, SubOpts#mqtt_subopts.qos)] of - [] -> ok; - _ -> {error, badtopic} +process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), + PState = #pstate{client_id = ClientId, session = SPid}) -> + case emqx_hooks:run('client.unsubscribe', [ClientId], + parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)) of + {ok, TopicFilters} -> + ok = emqx_session:unsubscribe(SPid, PacketId, Properties, mount(TopicFilters, PState)), + {ok, PState}; + {stop, _Acc} -> + ReasonCodes = lists:duplicate(length(RawTopicFilters), + ?RC_IMPLEMENTATION_SPECIFIC_ERROR), + deliver({unsuback, PacketId, ReasonCodes}, PState) end; -validate_topics(Type, Topics = [Topic0|_]) when is_binary(Topic0) -> - case [Topic || Topic <- Topics, not emqx_topic:validate({Type, Topic})] of - [] -> ok; - _ -> {error, badtopic} +process(?PACKET(?PINGREQ), PState) -> + send(?PACKET(?PINGRESP), PState); + +process(?PACKET(?DISCONNECT), PState) -> + %% Clean willmsg + {stop, normal, PState#pstate{will_msg = undefined}}. + +%%------------------------------------------------------------------------------ +%% ConnAck -> Client +%%------------------------------------------------------------------------------ + +connack({?RC_SUCCESS, SP, PState}) -> + deliver({connack, ?RC_SUCCESS, sp(SP)}, PState); + +connack({ReasonCode, PState}) -> + deliver({connack, ReasonCode, 0}, PState), + {error, emqx_reason_codes:name(ReasonCode), PState}. + +%%------------------------------------------------------------------------------ +%% Publish Message -> Broker +%%------------------------------------------------------------------------------ + +do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), + PState = #pstate{client_id = ClientId, session = SPid}) -> + Msg = mount(emqx_packet:to_message(ClientId, Packet), PState), + _ = emqx_session:publish(SPid, PacketId, Msg), + puback(QoS, PacketId, PState). + +%%------------------------------------------------------------------------------ +%% Puback -> Client +%%------------------------------------------------------------------------------ + +puback(?QOS_0, _PacketId, PState) -> + {ok, PState}; +puback(?QOS_1, PacketId, PState) -> + deliver({puback, PacketId, ?RC_SUCCESS}, PState); +puback(?QOS_2, PacketId, PState) -> + deliver({pubrec, PacketId, ?RC_SUCCESS}, PState). + +%%------------------------------------------------------------------------------ +%% Deliver Packet -> Client +%%------------------------------------------------------------------------------ + +deliver({connack, ReasonCode}, PState) -> + send(?CONNACK_PACKET(ReasonCode), PState); + +deliver({connack, ReasonCode, SP}, PState) -> + send(?CONNACK_PACKET(ReasonCode, SP), PState); + +deliver({publish, PacketId, Msg}, PState = #pstate{client_id = ClientId, + is_bridge = IsBridge}) -> + _ = emqx_hooks:run('message.delivered', [ClientId], Msg), + Msg1 = unmount(clean_retain(IsBridge, Msg), PState), + send(emqx_packet:from_message(PacketId, Msg1), PState); + +deliver({puback, PacketId, ReasonCode}, PState) -> + send(?PUBACK_PACKET(PacketId, ReasonCode), PState); + +deliver({pubrel, PacketId}, PState) -> + send(?PUBREL_PACKET(PacketId), PState); + +deliver({pubrec, PacketId, ReasonCode}, PState) -> + send(?PUBREC_PACKET(PacketId, ReasonCode), PState); + +deliver({suback, PacketId, ReasonCodes}, PState) -> + send(?SUBACK_PACKET(PacketId, ReasonCodes), PState); + +deliver({unsuback, PacketId, ReasonCodes}, PState) -> + send(?UNSUBACK_PACKET(PacketId, ReasonCodes), PState); + +%% Deliver a disconnect for mqtt 5.0 +deliver({disconnect, ReasonCode}, PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> + send(?DISCONNECT_PACKET(ReasonCode), PState); + +deliver({disconnect, _ReasonCode}, PState) -> + {ok, PState}. + +%%------------------------------------------------------------------------------ +%% Send Packet to Client + +-spec(send(mqtt_packet(), state()) -> {ok, state()} | {error, term()}). +send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, + sendfun = SendFun}) -> + case SendFun(emqx_frame:serialize(Packet, #{version => Ver})) of + ok -> emqx_metrics:sent(Packet), + trace(send, Packet, PState), + {ok, inc_stats(send, Type, PState)}; + {error, Reason} -> + {error, Reason} end. -validate_qos(undefined) -> - true; -validate_qos(QoS) when ?IS_QOS(QoS) -> - true; -validate_qos(_) -> - false. +%%------------------------------------------------------------------------------ +%% Assign a clientid -parse_topic_filters(TopicFilters) -> - [begin - {Topic, Opts} = emqx_topic:parse(RawTopic), - {Topic, maps:merge(?record_to_map(mqtt_subopts, SubOpts), Opts)} - end || {RawTopic, SubOpts} <- TopicFilters]. +maybe_assign_client_id(PState = #pstate{client_id = <<>>, ackprops = AckProps}) -> + ClientId = iolist_to_binary(["emqx_", emqx_guid:gen()]), + AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), + PState#pstate{client_id = ClientId, ackprops = AckProps1}; +maybe_assign_client_id(PState) -> + PState. -parse_topics(Topics) -> - [emqx_topic:parse(Topic) || Topic <- Topics]. +try_open_session(#pstate{zone = Zone, + client_id = ClientId, + client_pid = ClientPid, + conn_props = ConnProps, + username = Username, + clean_start = CleanStart}) -> + case emqx_sm:open_session(#{zone => Zone, + client_id => ClientId, + client_pid => ClientPid, + username => Username, + clean_start => CleanStart, + conn_props => ConnProps}) of + {ok, SPid} -> {ok, SPid, false}; + Other -> Other + end. authenticate(Client, Password) -> - case emqx_access_control:auth(Client, Password) of + case emqx_access_control:authenticate(Client, Password) of ok -> {ok, false}; {ok, IsSuper} -> {ok, IsSuper}; {error, Error} -> {error, Error} end. -%% PUBLISH ACL is cached in process dictionary. -check_acl(publish, Topic, Client) -> - IfCache = emqx_config:get_env(cache_acl, true), - case {IfCache, get({acl, publish, Topic})} of - {true, undefined} -> - AllowDeny = emqx_access_control:check_acl(Client, publish, Topic), - put({acl, publish, Topic}, AllowDeny), - AllowDeny; - {true, AllowDeny} -> - AllowDeny; - {false, _} -> - emqx_access_control:check_acl(Client, publish, Topic) - end; +set_property(Name, Value, undefined) -> + #{Name => Value}; +set_property(Name, Value, Props) -> + Props#{Name => Value}. -check_acl(subscribe, Topic, Client) -> - emqx_access_control:check_acl(Client, subscribe, Topic). +%%------------------------------------------------------------------------------ +%% Check Packet +%%------------------------------------------------------------------------------ -sp(true) -> 1; -sp(false) -> 0. +check_connect(Packet, PState) -> + run_check_steps([fun check_proto_ver/2, + fun check_client_id/2], Packet, PState). -%%-------------------------------------------------------------------- +check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, + proto_name = Name}, _PState) -> + case lists:member({Ver, Name}, ?PROTOCOL_NAMES) of + true -> ok; + false -> {error, ?RC_PROTOCOL_ERROR} + end. + +%% Issue#599: Null clientId and clean_start = false +check_client_id(#mqtt_packet_connect{client_id = ClientId, + clean_start = false}, _PState) + when ClientId == undefined; ClientId == <<>> -> + {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; + +%% MQTT3.1 does not allow null clientId +check_client_id(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, + client_id = ClientId}, _PState) + when ClientId == undefined; ClientId == <<>> -> + {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; + +check_client_id(#mqtt_packet_connect{client_id = ClientId}, #pstate{zone = Zone}) -> + Len = byte_size(ClientId), + MaxLen = emqx_zone:get_env(Zone, max_clientid_len), + case (1 =< Len) andalso (Len =< MaxLen) of + true -> ok; + false -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID} + end. + +check_publish(Packet, PState) -> + run_check_steps([fun check_pub_caps/2, + fun check_pub_acl/2], Packet, PState). + +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = R}}, + #pstate{zone = Zone}) -> + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => R}). + +check_pub_acl(_Packet, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) + when IsSuper orelse (not EnableAcl) -> + ok; + +check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, PState) -> + case emqx_access_control:check_acl(client(PState), publish, Topic) of + allow -> ok; + deny -> {error, ?RC_NOT_AUTHORIZED} + end. + +run_check_steps([], _Packet, PState) -> + {ok, PState}; +run_check_steps([Check|Steps], Packet, PState) -> + case Check(Packet, PState) of + ok -> + run_check_steps(Steps, Packet, PState); + {ok, PState1} -> + run_check_steps(Steps, Packet, PState1); + Error = {error, _RC} -> + Error + end. + +check_subscribe(TopicFilters, PState = #pstate{zone = Zone}) -> + case emqx_mqtt_caps:check_sub(Zone, TopicFilters) of + {ok, TopicFilter1} -> + check_sub_acl(TopicFilter1, PState); + {error, TopicFilter1} -> + {error, TopicFilter1} + end. + +check_sub_acl(TopicFilters, #pstate{is_super = IsSuper, enable_acl = EnableAcl}) + when IsSuper orelse (not EnableAcl) -> + {ok, TopicFilters}; + +check_sub_acl(TopicFilters, PState) -> + Client = client(PState), + lists:foldr( + fun({Topic, SubOpts}, {Ok, Acc}) -> + case emqx_access_control:check_acl(Client, subscribe, Topic) of + allow -> {Ok, [{Topic, SubOpts}|Acc]}; + deny -> {error, [{Topic, SubOpts#{rc := ?RC_NOT_AUTHORIZED}}|Acc]} + end + end, {ok, []}, TopicFilters). + +trace(recv, Packet, PState) -> + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)], PState); +trace(send, Packet, PState) -> + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)], PState). + +inc_stats(recv, Type, PState = #pstate{recv_stats = Stats}) -> + PState#pstate{recv_stats = inc_stats(Type, Stats)}; + +inc_stats(send, Type, PState = #pstate{send_stats = Stats}) -> + PState#pstate{send_stats = inc_stats(Type, Stats)}. + +inc_stats(Type, Stats = #{pkt := PktCnt, msg := MsgCnt}) -> + Stats#{pkt := PktCnt + 1, msg := case Type =:= ?PUBLISH of + true -> MsgCnt + 1; + false -> MsgCnt + end}. + +shutdown(_Error, #pstate{client_id = undefined}) -> + ignore; +shutdown(conflict, #pstate{client_id = ClientId}) -> + emqx_cm:unregister_client(ClientId), + ignore; +shutdown(mnesia_conflict, #pstate{client_id = ClientId}) -> + emqx_cm:unregister_client(ClientId), + ignore; +shutdown(Error, PState = #pstate{client_id = ClientId, will_msg = WillMsg}) -> + ?LOG(info, "Shutdown for ~p", [Error], PState), + %% TODO: Auth failure not publish the will message + case Error =:= auth_failure of + true -> ok; + false -> send_willmsg(WillMsg) + end, + emqx_hooks:run('client.disconnected', [Error], client(PState)), + emqx_cm:unregister_client(ClientId). + +willmsg(Packet, PState = #pstate{client_id = ClientId}) + when is_record(Packet, mqtt_packet_connect) -> + case emqx_packet:to_message(ClientId, Packet) of + undefined -> undefined; + Msg -> mount(Msg, PState) + end. + +send_willmsg(undefined) -> + ignore; +send_willmsg(WillMsg) -> + emqx_broker:publish(WillMsg). + +start_keepalive(0, _PState) -> + ignore; +start_keepalive(Secs, #pstate{zone = Zone}) when Secs > 0 -> + Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), + self() ! {keepalive, start, round(Secs * Backoff)}. + +%%----------------------------------------------------------------------------- +%% Parse topic filters +%%----------------------------------------------------------------------------- + +parse_topic_filters(?SUBSCRIBE, TopicFilters) -> + [begin + {Topic, TOpts} = emqx_topic:parse(RawTopic), + {Topic, maps:merge(SubOpts, TOpts)} + end || {RawTopic, SubOpts} <- TopicFilters]; + +parse_topic_filters(?UNSUBSCRIBE, TopicFilters) -> + lists:map(fun emqx_topic:parse/1, TopicFilters). + +%%----------------------------------------------------------------------------- %% The retained flag should be propagated for bridge. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- clean_retain(false, Msg = #message{flags = #{retain := true}, headers = Headers}) -> case maps:get(retained, Headers, false) of @@ -542,14 +614,30 @@ clean_retain(false, Msg = #message{flags = #{retain := true}, headers = Headers} clean_retain(_IsBridge, Msg) -> Msg. -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- %% Mount Point -%%-------------------------------------------------------------------- +%%----------------------------------------------------------------------------- -replvar(undefined, _State) -> - undefined; -replvar(MountPoint, #proto_state{client_id = ClientId, username = Username}) -> - lists:foldl(fun feed_var/2, MountPoint, [{<<"%c">>, ClientId}, {<<"%u">>, Username}]). +mount(Any, #pstate{mountpoint = undefined}) -> + Any; +mount(Msg = #message{topic = Topic}, #pstate{mountpoint = MountPoint}) -> + Msg#message{topic = <>}; +mount(TopicFilters, #pstate{mountpoint = MountPoint}) when is_list(TopicFilters) -> + [{<>, SubOpts} || {Topic, SubOpts} <- TopicFilters]. + +unmount(Any, #pstate{mountpoint = undefined}) -> + Any; +unmount(Msg = #message{topic = Topic}, #pstate{mountpoint = MountPoint}) -> + case catch split_binary(Topic, byte_size(MountPoint)) of + {MountPoint, Topic1} -> Msg#message{topic = Topic1}; + _Other -> Msg + end. + +replvar(PState = #pstate{mountpoint = undefined}) -> + PState; +replvar(PState = #pstate{client_id = ClientId, username = Username, mountpoint = MountPoint}) -> + Vars = [{<<"%c">>, ClientId}, {<<"%u">>, Username}], + PState#pstate{mountpoint = lists:foldl(fun feed_var/2, MountPoint, Vars)}. feed_var({<<"%c">>, ClientId}, MountPoint) -> emqx_topic:feed_var(<<"%c">>, ClientId, MountPoint); @@ -558,18 +646,6 @@ feed_var({<<"%u">>, undefined}, MountPoint) -> feed_var({<<"%u">>, Username}, MountPoint) -> emqx_topic:feed_var(<<"%u">>, Username, MountPoint). -mount(undefined, Any) -> - Any; -mount(MountPoint, Msg = #message{topic = Topic}) -> - Msg#message{topic = <>}; -mount(MountPoint, TopicTable) when is_list(TopicTable) -> - [{<>, Opts} || {Topic, Opts} <- TopicTable]. - -unmount(undefined, Any) -> - Any; -unmount(MountPoint, Msg = #message{topic = Topic}) -> - case catch split_binary(Topic, byte_size(MountPoint)) of - {MountPoint, Topic0} -> Msg#message{topic = Topic0}; - _ -> Msg - end. +sp(true) -> 1; +sp(false) -> 0. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 1253de5cc..0c994d947 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -11,7 +11,7 @@ %% 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. -%% + %% @doc %% A stateful interaction between a Client and a Server. Some Sessions %% last only as long as the Network Connection, others can span multiple @@ -35,28 +35,31 @@ %% If the Session is currently not connected, the time at which the Session %% will end and Session State will be discarded. %% @end + -module(emqx_session). -behaviour(gen_server). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_misc.hrl"). --export([start_link/1, close/1]). +-export([start_link/1]). -export([info/1, stats/1]). -export([resume/2, discard/2]). --export([subscribe/2]).%%, subscribe/3]). +-export([subscribe/2, subscribe/4]). -export([publish/3]). -export([puback/2, puback/3]). -export([pubrec/2, pubrec/3]). --export([pubrel/2, pubcomp/2]). --export([unsubscribe/2]). +-export([pubrel/3, pubcomp/3]). +-export([unsubscribe/2, unsubscribe/4]). +-export([close/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-import(emqx_zone, [get_env/2, get_env/3]). + -record(state, { %% Clean Start Flag clean_start = false :: boolean(), @@ -76,9 +79,6 @@ %% Old client Pid that has been kickout old_client_pid :: pid(), - %% Pending sub/unsub requests - requests :: map(), - %% Next packet id of the session next_pkt_id = 1 :: mqtt_packet_id(), @@ -130,27 +130,28 @@ %% Enable Stats enable_stats :: boolean(), - %% Force GC reductions - reductions = 0 :: non_neg_integer(), + %% Stats timer + stats_timer :: reference() | undefined, %% Ignore loop deliver? ignore_loop_deliver = false :: boolean(), + %% TODO: + deliver_stats = 0, + + %% TODO: + enqueue_stats = 0, + %% Created at created_at :: erlang:timestamp() }). -define(TIMEOUT, 60000). --define(DEFAULT_SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). - --define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]). - --define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, - next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, - max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, - await_rel_timeout, expiry_interval, enable_stats, force_gc_count, - created_at]). +-define(INFO_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, + next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, + max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, + await_rel_timeout, expiry_interval, enable_stats, created_at]). -define(LOG(Level, Format, Args, State), emqx_logger:Level([{client, State#state.client_id}], @@ -159,7 +160,8 @@ %% @doc Start a session -spec(start_link(SessAttrs :: map()) -> {ok, pid()} | {error, term()}). start_link(SessAttrs) -> - gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, 30000}]). + IdleTimeout = maps:get(idle_timeout, SessAttrs, 30000), + gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, IdleTimeout}]). %%------------------------------------------------------------------------------ %% PubSub API @@ -167,12 +169,17 @@ start_link(SessAttrs) -> -spec(subscribe(pid(), list({topic(), map()}) | {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). -%% internal call subscribe(SPid, TopicFilters) when is_list(TopicFilters) -> - %%TODO: Parse the topic filters? - subscribe(SPid, {undefined, #{}, TopicFilters}); + gen_server:cast(SPid, {subscribe, [begin + {Topic, Opts} = emqx_topic:parse(RawTopic), + {Topic, maps:merge( + maps:merge( + ?DEFAULT_SUBOPTS, SubOpts), Opts)} + end || {RawTopic, SubOpts} <- TopicFilters]}). + %% for mqtt 5.0 -subscribe(SPid, SubReq = {PacketId, Props, TopicFilters}) -> +subscribe(SPid, PacketId, Properties, TopicFilters) -> + SubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {subscribe, self(), SubReq}). -spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, delivery()} | {error, term()}). @@ -190,31 +197,34 @@ publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> -spec(puback(pid(), mqtt_packet_id()) -> ok). puback(SPid, PacketId) -> - gen_server:cast(SPid, {puback, PacketId}). + gen_server:cast(SPid, {puback, PacketId, ?RC_SUCCESS}). -puback(SPid, PacketId, {ReasonCode, Props}) -> - gen_server:cast(SPid, {puback, PacketId, {ReasonCode, Props}}). +puback(SPid, PacketId, ReasonCode) -> + gen_server:cast(SPid, {puback, PacketId, ReasonCode}). -spec(pubrec(pid(), mqtt_packet_id()) -> ok). pubrec(SPid, PacketId) -> gen_server:cast(SPid, {pubrec, PacketId}). -pubrec(SPid, PacketId, {ReasonCode, Props}) -> - gen_server:cast(SPid, {pubrec, PacketId, {ReasonCode, Props}}). +pubrec(SPid, PacketId, ReasonCode) -> + gen_server:cast(SPid, {pubrec, PacketId, ReasonCode}). --spec(pubrel(pid(), mqtt_packet_id()) -> ok). -pubrel(SPid, PacketId) -> - gen_server:cast(SPid, {pubrel, PacketId}). +-spec(pubrel(pid(), mqtt_packet_id(), mqtt_reason_code()) -> ok). +pubrel(SPid, PacketId, ReasonCode) -> + gen_server:cast(SPid, {pubrel, PacketId, ReasonCode}). --spec(pubcomp(pid(), mqtt_packet_id()) -> ok). -pubcomp(SPid, PacketId) -> - gen_server:cast(SPid, {pubcomp, PacketId}). +-spec(pubcomp(pid(), mqtt_packet_id(), mqtt_reason_code()) -> ok). +pubcomp(SPid, PacketId, ReasonCode) -> + gen_server:cast(SPid, {pubcomp, PacketId, ReasonCode}). -spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). unsubscribe(SPid, TopicFilters) when is_list(TopicFilters) -> %%TODO: Parse the topic filters? - unsubscribe(SPid, {undefined, #{}, TopicFilters}); -unsubscribe(SPid, UnsubReq = {PacketId, Properties, TopicFilters}) -> + unsubscribe(SPid, {undefined, #{}, TopicFilters}). + +%% TODO:... +unsubscribe(SPid, PacketId, Properties, TopicFilters) -> + UnsubReq = {PacketId, Properties, TopicFilters}, gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). -spec(resume(pid(), pid()) -> ok). @@ -226,20 +236,52 @@ resume(SPid, ClientPid) -> info(SPid) when is_pid(SPid) -> gen_server:call(SPid, info); -info(State) when is_record(State, state) -> - ?record_to_proplist(state, State, ?INFO_KEYS). +info(#state{clean_start = CleanStart, + binding = Binding, + client_id = ClientId, + username = Username, + max_subscriptions = MaxSubscriptions, + subscriptions = Subscriptions, + upgrade_qos = UpgradeQoS, + inflight = Inflight, + max_inflight = MaxInflight, + retry_interval = RetryInterval, + mqueue = MQueue, + awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxAwaitingRel, + await_rel_timeout = AwaitRelTimeout, + expiry_interval = ExpiryInterval, + created_at = CreatedAt}) -> + [{clean_start, CleanStart}, + {binding, Binding}, + {client_id, ClientId}, + {username, Username}, + {max_subscriptions, MaxSubscriptions}, + {subscriptions, maps:size(Subscriptions)}, + {upgrade_qos, UpgradeQoS}, + {inflight, emqx_inflight:size(Inflight)}, + {max_inflight, MaxInflight}, + {retry_interval, RetryInterval}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {awaiting_rel, maps:size(AwaitingRel)}, + {max_awaiting_rel, MaxAwaitingRel}, + {await_rel_timeout, AwaitRelTimeout}, + {expiry_interval, ExpiryInterval}, + {created_at, CreatedAt}]. -spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). stats(SPid) when is_pid(SPid) -> - gen_server:call(SPid, stats); + gen_server:call(SPid, stats, infinity); stats(#state{max_subscriptions = MaxSubscriptions, - subscriptions = Subscriptions, - inflight = Inflight, - max_inflight = MaxInflight, - mqueue = MQueue, - max_awaiting_rel = MaxAwaitingRel, - awaiting_rel = AwaitingRel}) -> + subscriptions = Subscriptions, + inflight = Inflight, + max_inflight = MaxInflight, + mqueue = MQueue, + max_awaiting_rel = MaxAwaitingRel, + awaiting_rel = AwaitingRel, + deliver_stats = DeliverMsg, + enqueue_stats = EnqueueMsg}) -> lists:append(emqx_misc:proc_stats(), [{max_subscriptions, MaxSubscriptions}, {subscriptions, maps:size(Subscriptions)}, @@ -250,8 +292,8 @@ stats(#state{max_subscriptions = MaxSubscriptions, {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, {max_awaiting_rel, MaxAwaitingRel}, {awaiting_rel_len, maps:size(AwaitingRel)}, - {deliver_msg, get(deliver_msg)}, - {enqueue_msg, get(enqueue_msg)}]). + {deliver_msg, DeliverMsg}, + {enqueue_msg, EnqueueMsg}]). %% @doc Discard the session -spec(discard(pid(), client_id()) -> ok). @@ -268,43 +310,43 @@ close(SPid) -> init(#{zone := Zone, client_id := ClientId, - client_pid := ClientPid, + conn_pid := ClientPid, clean_start := CleanStart, - username := Username}) -> + username := Username, + %% TODO: + conn_props := _ConnProps}) -> process_flag(trap_exit, true), true = link(ClientPid), - init_stats([deliver_msg, enqueue_msg]), - MaxInflight = emqx_zone:env(Zone, max_inflight), + MaxInflight = get_env(Zone, max_inflight), State = #state{clean_start = CleanStart, binding = binding(ClientPid), client_id = ClientId, client_pid = ClientPid, username = Username, subscriptions = #{}, - max_subscriptions = emqx_zone:env(Zone, max_subscriptions, 0), - upgrade_qos = emqx_zone:env(Zone, upgrade_qos, false), + max_subscriptions = get_env(Zone, max_subscriptions, 0), + upgrade_qos = get_env(Zone, upgrade_qos, false), max_inflight = MaxInflight, inflight = emqx_inflight:new(MaxInflight), mqueue = init_mqueue(Zone, ClientId), - retry_interval = emqx_zone:env(Zone, retry_interval, 0), + retry_interval = get_env(Zone, retry_interval, 0), awaiting_rel = #{}, - await_rel_timeout = emqx_zone:env(Zone, await_rel_timeout), - max_awaiting_rel = emqx_zone:env(Zone, max_awaiting_rel), - expiry_interval = emqx_zone:env(Zone, session_expiry_interval), - enable_stats = emqx_zone:env(Zone, enable_stats, true), - ignore_loop_deliver = emqx_zone:env(Zone, ignore_loop_deliver, true), + await_rel_timeout = get_env(Zone, await_rel_timeout), + max_awaiting_rel = get_env(Zone, max_awaiting_rel), + expiry_interval = get_env(Zone, session_expiry_interval), + enable_stats = get_env(Zone, enable_stats, true), + ignore_loop_deliver = get_env(Zone, ignore_loop_deliver, false), + deliver_stats = 0, + enqueue_stats = 0, created_at = os:timestamp()}, emqx_sm:register_session(ClientId, info(State)), emqx_hooks:run('session.created', [ClientId]), - {ok, emit_stats(State), hibernate}. + {ok, ensure_stats_timer(State), hibernate}. init_mqueue(Zone, ClientId) -> emqx_mqueue:new(ClientId, #{type => simple, - max_len => emqx_zone:env(Zone, max_mqueue_len), - store_qos0 => emqx_zone:env(Zone, mqueue_store_qos0)}). - -init_stats(Keys) -> - lists:foreach(fun(K) -> put(K, 0) end, Keys). + max_len => get_env(Zone, max_mqueue_len), + store_qos0 => get_env(Zone, mqueue_store_qos0)}). binding(ClientPid) -> case node(ClientPid) =:= node() of true -> local; false -> remote end. @@ -347,11 +389,27 @@ handle_call(Req, _From, State) -> emqx_logger:error("[Session] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, +%% SUBSCRIBE: +handle_cast({subscribe, TopicFilters}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> + Subscriptions1 = lists:foldl( + fun({Topic, SubOpts}, SubMap) -> + case maps:find(Topic, SubMap) of + {ok, _OldOpts} -> + emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), + ?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State); + error -> + emqx_broker:subscribe(Topic, ClientId, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]) + end, + maps:put(Topic, SubOpts, SubMap) + end, Subscriptions, TopicFilters), + {noreply, State#state{subscriptions = Subscriptions1}}; + +handle_cast({subscribe, From, {PacketId, Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> - ?LOG(info, "Subscribe ~p", [TopicFilters], State), {ReasonCodes, Subscriptions1} = - lists:foldl(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> + lists:foldr(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> {[QoS|RcAcc], case maps:find(Topic, SubMap) of {ok, SubOpts} -> @@ -361,58 +419,54 @@ handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), ?LOG(warning, "Duplicated subscribe ~s, old_opts: ~p, new_opts: ~p", [Topic, OldOpts, SubOpts], State), - maps:put(Topic, SubOpts, SubMap); + maps:put(Topic, with_subid(Properties, SubOpts), SubMap); error -> emqx_broker:subscribe(Topic, ClientId, SubOpts), emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), - maps:put(Topic, SubOpts, SubMap) + maps:put(Topic, with_subid(Properties, SubOpts), SubMap) end} end, {[], Subscriptions}, TopicFilters), - suback(From, PacketId, lists:reverse(ReasonCodes)), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; + suback(From, PacketId, ReasonCodes), + {noreply, State#state{subscriptions = Subscriptions1}}; +%% UNSUBSCRIBE: handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> - ?LOG(info, "Unsubscribe ~p", [TopicFilters], State), {ReasonCodes, Subscriptions1} = - lists:foldl(fun(Topic, {RcAcc, SubMap}) -> - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - emqx_broker:unsubscribe(Topic, ClientId), - emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), - {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; - error -> - {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} - end - end, {[], Subscriptions}, TopicFilters), - unsuback(From, PacketId, lists:reverse(ReasonCodes)), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; + lists:foldr(fun(Topic, {RcAcc, SubMap}) -> + case maps:find(Topic, SubMap) of + {ok, SubOpts} -> + emqx_broker:unsubscribe(Topic, ClientId), + emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), + {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; + error -> + {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} + end + end, {[], Subscriptions}, TopicFilters), + unsuback(From, PacketId, ReasonCodes), + {noreply, State#state{subscriptions = Subscriptions1}}; %% PUBACK: -handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> - {noreply, - case emqx_inflight:contain(PacketId, Inflight) of - true -> - dequeue(acked(puback, PacketId, State)); - false -> - ?LOG(warning, "PUBACK ~p missed inflight: ~p", - [PacketId, emqx_inflight:window(Inflight)], State), - emqx_metrics:inc('packets/puback/missed'), - State - end, hibernate}; +handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {noreply, dequeue(acked(puback, PacketId, State))}; + false -> + ?LOG(warning, "The PUBACK PacketId is not found: ~p", [PacketId], State), + emqx_metrics:inc('packets/puback/missed'), + {noreply, State} + end; -%% PUBREC: -handle_cast({pubrec, PacketId}, State = #state{inflight = Inflight}) -> - {noreply, - case emqx_inflight:contain(PacketId, Inflight) of - true -> - acked(pubrec, PacketId, State); - false -> - ?LOG(warning, "PUBREC ~p missed inflight: ~p", - [PacketId, emqx_inflight:window(Inflight)], State), - emqx_metrics:inc('packets/pubrec/missed'), - State - end, hibernate}; +%% PUBREC: How to handle ReasonCode? +handle_cast({pubrec, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {noreply, acked(pubrec, PacketId, State)}; + false -> + ?LOG(warning, "The PUBREC PacketId is not found: ~w", [PacketId], State), + emqx_metrics:inc('packets/pubrec/missed'), + {noreply, State} + end; %% PUBREL: handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> @@ -422,7 +476,7 @@ handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> %% Implement Qos2 by method A [MQTT 4.33] %% Dispatch to subscriber when received PUBREL emqx_broker:publish(Msg), %% FIXME: - gc(State#state{awaiting_rel = AwaitingRel1}); + maybe_gc(State#state{awaiting_rel = AwaitingRel1}); error -> ?LOG(warning, "Cannot find PUBREL: ~p", [PacketId], State), emqx_metrics:inc('packets/pubrel/missed'), @@ -430,17 +484,15 @@ handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) -> end, hibernate}; %% PUBCOMP: -handle_cast({pubcomp, PacketId}, State = #state{inflight = Inflight}) -> - {noreply, - case emqx_inflight:contain(PacketId, Inflight) of - true -> - dequeue(acked(pubcomp, PacketId, State)); - false -> - ?LOG(warning, "The PUBCOMP ~p is not inflight: ~p", - [PacketId, emqx_inflight:window(Inflight)], State), - emqx_metrics:inc('packets/pubcomp/missed'), - State - end, hibernate}; +handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {noreply, dequeue(acked(pubcomp, PacketId, State))}; + false -> + ?LOG(warning, "The PUBCOMP Packet Identifier is not found: ~w", [PacketId], State), + emqx_metrics:inc('packets/pubcomp/missed'), + {noreply, State} + end; %% RESUME: handle_cast({resume, ClientPid}, @@ -484,7 +536,7 @@ handle_cast({resume, ClientPid}, end, %% Replay delivery and Dequeue pending messages - {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; + {noreply, ensure_stats_timer(dequeue(retry_delivery(true, State1)))}; handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), @@ -502,17 +554,17 @@ handle_info({dispatch, _Topic, #message{from = ClientId}}, %% Dispatch Message handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> - {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; + {noreply, maybe_gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. handle_info({timeout, _Timer, retry_delivery}, State = #state{client_pid = undefined}) -> - {noreply, emit_stats(State#state{retry_timer = undefined})}; + {noreply, ensure_stats_timer(State#state{retry_timer = undefined})}; handle_info({timeout, _Timer, retry_delivery}, State) -> - {noreply, emit_stats(retry_delivery(false, State#state{retry_timer = undefined}))}; + {noreply, ensure_stats_timer(retry_delivery(false, State#state{retry_timer = undefined}))}; handle_info({timeout, _Timer, check_awaiting_rel}, State) -> - {noreply, expire_awaiting_rel(emit_stats(State#state{await_rel_timer = undefined}))}; + {noreply, ensure_stats_timer(expire_awaiting_rel(State#state{await_rel_timer = undefined}))}; handle_info({timeout, _Timer, expired}, State) -> ?LOG(info, "Expired, shutdown now.", [], State), @@ -529,7 +581,7 @@ handle_info({'EXIT', ClientPid, Reason}, ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), ExpireTimer = emqx_misc:start_timer(Interval, expired), State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, - {noreply, emit_stats(State1), hibernate}; + {noreply, State1, hibernate}; handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) -> %% ignore @@ -540,6 +592,10 @@ handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> [ClientPid, Pid, Reason], State), {noreply, State, hibernate}; +handle_info(emit_stats, State = #state{client_id = ClientId}) -> + emqx_sm:set_session_stats(ClientId, stats(State)), + {noreply, State#state{stats_timer = undefined}, hibernate}; + handle_info(Info, State) -> emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. @@ -555,6 +611,10 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ +with_subid(#{'Subscription-Identifier' := SubId}, Opts) -> + maps:put(subid, SubId, Opts); +with_subid(_Props, Opts) -> Opts. + suback(_From, undefined, _ReasonCodes) -> ignore; suback(From, PacketId, ReasonCodes) -> @@ -675,36 +735,39 @@ dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> %% Deliver qos0 message directly to client dispatch(Msg = #message{qos = ?QOS0}, State) -> - deliver(undefined, Msg, State), State; + deliver(undefined, Msg, State), + inc_stats(deliver, State); dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> case emqx_inflight:is_full(Inflight) of - true -> + true -> enqueue_msg(Msg, State); false -> deliver(PacketId, Msg, State), - await(PacketId, Msg, next_pkt_id(State)) + %% TODO inc_stats?? + await(PacketId, Msg, next_pkt_id(inc_stats(deliver, State))) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> - inc_stats(enqueue_msg), - State#state{mqueue = emqx_mqueue:in(Msg, Q)}. + inc_stats(enqueue, State#state{mqueue = emqx_mqueue:in(Msg, Q)}). %%------------------------------------------------------------------------------ %% Deliver %%------------------------------------------------------------------------------ redeliver({PacketId, Msg = #message{qos = QoS}}, State) -> - deliver(PacketId, if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); + deliver(PacketId, if QoS =:= ?QOS2 -> Msg; + true -> emqx_message:set_flag(dup, Msg) + end, State); redeliver({pubrel, PacketId}, #state{client_pid = Pid}) -> Pid ! {deliver, {pubrel, PacketId}}. deliver(PacketId, Msg, #state{client_pid = Pid, binding = local}) -> - inc_stats(deliver_msg), Pid ! {deliver, {publish, PacketId, Msg}}; + Pid ! {deliver, {publish, PacketId, Msg}}; deliver(PacketId, Msg, #state{client_pid = Pid, binding = remote}) -> - inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]). + emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]). %%------------------------------------------------------------------------------ %% Awaiting ACK for QoS1/QoS2 Messages @@ -802,27 +865,28 @@ next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) -> next_pkt_id(State = #state{next_pkt_id = Id}) -> State#state{next_pkt_id = Id + 1}. -%%-------------------------------------------------------------------- -%% Emit session stats +%%------------------------------------------------------------------------------ +%% Ensure stats timer -emit_stats(State = #state{enable_stats = false}) -> - State; -emit_stats(State = #state{client_id = ClientId}) -> - emqx_sm:set_session_stats(ClientId, stats(State)), +ensure_stats_timer(State = #state{enable_stats = true, + stats_timer = undefined}) -> + State#state{stats_timer = erlang:send_after(30000, self(), emit_stats)}; +ensure_stats_timer(State) -> State. -inc_stats(Key) -> put(Key, get(Key) + 1). +inc_stats(deliver, State = #state{deliver_stats = I}) -> + State#state{deliver_stats = I + 1}; +inc_stats(enqueue, State = #state{enqueue_stats = I}) -> + State#state{enqueue_stats = I + 1}. %%-------------------------------------------------------------------- %% Helper functions reply(Reply, State) -> - {reply, Reply, State, hibernate}. + {reply, Reply, State}. shutdown(Reason, State) -> {stop, {shutdown, Reason}, State}. -gc(State) -> - State. - %%emqx_gc:maybe_force_gc(#state.force_gc_count, State). +maybe_gc(State) -> State. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index afa2d6b06..2dac00263 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -20,8 +20,10 @@ -export([start_link/0]). --export([open_session/1, lookup_session/1, close_session/1, lookup_session_pid/1]). --export([resume_session/1, resume_session/2, discard_session/1, discard_session/2]). +-export([open_session/1, close_session/1]). +-export([lookup_session/1, lookup_session_pid/1]). +-export([resume_session/1, resume_session/2]). +-export([discard_session/1, discard_session/2]). -export([register_session/2, get_session_attrs/1, unregister_session/1]). -export([get_session_stats/1, set_session_stats/2]). @@ -29,7 +31,8 @@ -export([dispatch/3]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -record(state, {session_pmon}). @@ -46,7 +49,7 @@ start_link() -> gen_server:start_link({local, ?SM}, ?MODULE, [], []). %% @doc Open a session. --spec(open_session(map()) -> {ok, pid(), boolean()} | {error, term()}). +-spec(open_session(map()) -> {ok, pid()} | {ok, pid(), boolean()} | {error, term()}). open_session(Attrs = #{clean_start := true, client_id := ClientId, client_pid := ClientPid}) -> @@ -61,8 +64,8 @@ open_session(Attrs = #{clean_start := false, client_pid := ClientPid}) -> ResumeStart = fun(_) -> case resume_session(ClientId, ClientPid) of - {ok, SessionPid} -> - {ok, SessionPid}; + {ok, SPid} -> + {ok, SPid, true}; {error, not_found} -> emqx_session_sup:start_session(Attrs); {error, Reason} -> @@ -78,10 +81,10 @@ discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, ClientPid) when is_binary(ClientId) -> lists:foreach( - fun({_ClientId, SessionPid}) -> - case catch emqx_session:discard(SessionPid, ClientPid) of + fun({_ClientId, SPid}) -> + case catch emqx_session:discard(SPid, ClientPid) of {Err, Reason} when Err =:= 'EXIT'; Err =:= error -> - emqx_logger:error("[SM] Failed to discard ~p: ~p", [SessionPid, Reason]); + emqx_logger:error("[SM] Failed to discard ~p: ~p", [SPid, Reason]); ok -> ok end end, lookup_session(ClientId)). @@ -94,25 +97,25 @@ resume_session(ClientId) -> resume_session(ClientId, ClientPid) -> case lookup_session(ClientId) of [] -> {error, not_found}; - [{_ClientId, SessionPid}] -> - ok = emqx_session:resume(SessionPid, ClientPid), - {ok, SessionPid}; + [{_ClientId, SPid}] -> + ok = emqx_session:resume(SPid, ClientPid), + {ok, SPid}; Sessions -> - [{_, SessionPid}|StaleSessions] = lists:reverse(Sessions), + [{_, SPid}|StaleSessions] = lists:reverse(Sessions), emqx_logger:error("[SM] More than one session found: ~p", [Sessions]), lists:foreach(fun({_, StalePid}) -> catch emqx_session:discard(StalePid, ClientPid) end, StaleSessions), - ok = emqx_session:resume(SessionPid, ClientPid), - {ok, SessionPid} + ok = emqx_session:resume(SPid, ClientPid), + {ok, SPid} end. %% @doc Close a session. -spec(close_session({client_id(), pid()} | pid()) -> ok). -close_session({_ClientId, SessionPid}) -> - emqx_session:close(SessionPid); -close_session(SessionPid) when is_pid(SessionPid) -> - emqx_session:close(SessionPid). +close_session({_ClientId, SPid}) -> + emqx_session:close(SPid); +close_session(SPid) when is_pid(SPid) -> + emqx_session:close(SPid). %% @doc Register a session with attributes. -spec(register_session(client_id() | {client_id(), pid()}, @@ -120,8 +123,8 @@ close_session(SessionPid) when is_pid(SessionPid) -> register_session(ClientId, Attrs) when is_binary(ClientId) -> register_session({ClientId, self()}, Attrs); -register_session(Session = {ClientId, SessionPid}, Attrs) - when is_binary(ClientId), is_pid(SessionPid) -> +register_session(Session = {ClientId, SPid}, Attrs) + when is_binary(ClientId), is_pid(SPid) -> ets:insert(?SESSION, Session), ets:insert(?SESSION_ATTRS, {Session, Attrs}), case proplists:get_value(clean_start, Attrs, true) of @@ -129,13 +132,13 @@ register_session(Session = {ClientId, SessionPid}, Attrs) false -> ets:insert(?SESSION_P, Session) end, emqx_sm_registry:register_session(Session), - notify({registered, ClientId, SessionPid}). + notify({registered, ClientId, SPid}). %% @doc Get session attrs -spec(get_session_attrs({client_id(), pid()}) -> list(emqx_session:attribute())). -get_session_attrs(Session = {ClientId, SessionPid}) - when is_binary(ClientId), is_pid(SessionPid) -> +get_session_attrs(Session = {ClientId, SPid}) + when is_binary(ClientId), is_pid(SPid) -> safe_lookup_element(?SESSION_ATTRS, Session, []). %% @doc Unregister a session @@ -143,19 +146,19 @@ get_session_attrs(Session = {ClientId, SessionPid}) unregister_session(ClientId) when is_binary(ClientId) -> unregister_session({ClientId, self()}); -unregister_session(Session = {ClientId, SessionPid}) - when is_binary(ClientId), is_pid(SessionPid) -> +unregister_session(Session = {ClientId, SPid}) + when is_binary(ClientId), is_pid(SPid) -> emqx_sm_registry:unregister_session(Session), ets:delete(?SESSION_STATS, Session), ets:delete(?SESSION_ATTRS, Session), ets:delete_object(?SESSION_P, Session), ets:delete_object(?SESSION, Session), - notify({unregistered, ClientId, SessionPid}). + notify({unregistered, ClientId, SPid}). %% @doc Get session stats -spec(get_session_stats({client_id(), pid()}) -> list(emqx_stats:stats())). -get_session_stats(Session = {ClientId, SessionPid}) - when is_binary(ClientId), is_pid(SessionPid) -> +get_session_stats(Session = {ClientId, SPid}) + when is_binary(ClientId), is_pid(SPid) -> safe_lookup_element(?SESSION_STATS, Session, []). %% @doc Set session stats @@ -164,8 +167,8 @@ get_session_stats(Session = {ClientId, SessionPid}) set_session_stats(ClientId, Stats) when is_binary(ClientId) -> set_session_stats({ClientId, self()}, Stats); -set_session_stats(Session = {ClientId, SessionPid}, Stats) - when is_binary(ClientId), is_pid(SessionPid) -> +set_session_stats(Session = {ClientId, SPid}, Stats) + when is_binary(ClientId), is_pid(SPid) -> ets:insert(?SESSION_STATS, {Session, Stats}). %% @doc Lookup a session from registry @@ -217,11 +220,11 @@ handle_call(Req, _From, State) -> emqx_logger:error("[SM] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, SessionPid}}, State = #state{session_pmon = PMon}) -> - {noreply, State#state{session_pmon = emqx_pmon:monitor(SessionPid, ClientId, PMon)}}; +handle_cast({notify, {registered, ClientId, SPid}}, State = #state{session_pmon = PMon}) -> + {noreply, State#state{session_pmon = emqx_pmon:monitor(SPid, ClientId, PMon)}}; -handle_cast({notify, {unregistered, _ClientId, SessionPid}}, State = #state{session_pmon = PMon}) -> - {noreply, State#state{session_pmon = emqx_pmon:demonitor(SessionPid, PMon)}}; +handle_cast({notify, {unregistered, _ClientId, SPid}}, State = #state{session_pmon = PMon}) -> + {noreply, State#state{session_pmon = emqx_pmon:demonitor(SPid, PMon)}}; handle_cast(Msg, State) -> emqx_logger:error("[SM] unexpected cast: ~p", [Msg]), diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 3bf42f6ac..74a405f65 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -17,10 +17,15 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --import(lists, [reverse/1]). - --export([match/2, validate/1, triples/1, words/1, wildcard/1]). --export([join/1, feed_var/3, systop/1]). +-export([match/2]). +-export([validate/1, validate/2]). +-export([levels/1]). +-export([triples/1]). +-export([words/1]). +-export([wildcard/1]). +-export([join/1]). +-export([feed_var/3]). +-export([systop/1]). -export([parse/1, parse/2]). -type(word() :: '' | '+' | '#' | binary()). @@ -69,15 +74,21 @@ match([_H1|_], []) -> match([], [_H|_T2]) -> false. -%% @doc Validate Topic --spec(validate({name | filter, topic()}) -> boolean()). -validate({_, <<>>}) -> - false; -validate({_, Topic}) when is_binary(Topic) and (size(Topic) > ?MAX_TOPIC_LEN) -> - false; -validate({filter, Topic}) when is_binary(Topic) -> +%% @doc Validate topic name or filter +-spec(validate(topic() | {name | filter, topic()}) -> true). +validate(Topic) when is_binary(Topic) -> + validate(filter, Topic); +validate({Type, Topic}) when Type =:= name; Type =:= filter -> + validate(Type, Topic). + +-spec(validate(name | filter, topic()) -> true). +validate(_, <<>>) -> + error(empty_topic); +validate(_, Topic) when is_binary(Topic) and (size(Topic) > ?MAX_TOPIC_LEN) -> + error(topic_too_long); +validate(filter, Topic) when is_binary(Topic) -> validate2(words(Topic)); -validate({name, Topic}) when is_binary(Topic) -> +validate(name, Topic) when is_binary(Topic) -> Words = words(Topic), validate2(Words) and (not wildcard(Words)). @@ -86,7 +97,7 @@ validate2([]) -> validate2(['#']) -> % end with '#' true; validate2(['#'|Words]) when length(Words) > 0 -> - false; + error('topic_invalid_#'); validate2([''|Words]) -> validate2(Words); validate2(['+'|Words]) -> @@ -97,7 +108,7 @@ validate2([W|Words]) -> validate3(<<>>) -> true; validate3(<>) when C == $#; C == $+; C == 0 -> - false; + error('topic_invalid_char'); validate3(<<_/utf8, Rest/binary>>) -> validate3(Rest). @@ -107,7 +118,7 @@ triples(Topic) when is_binary(Topic) -> triples(words(Topic), root, []). triples([], _Parent, Acc) -> - reverse(Acc); + lists:reverse(Acc); triples([W|Words], Parent, Acc) -> Node = join(Parent, W), triples(Words, Node, [{Parent, W, Node}|Acc]). @@ -122,6 +133,9 @@ bin('+') -> <<"+">>; bin('#') -> <<"#">>; bin(B) when is_binary(B) -> B. +levels(Topic) when is_binary(Topic) -> + length(words(Topic)). + %% @doc Split Topic Path to Words -spec(words(topic()) -> words()). words(Topic) when is_binary(Topic) -> @@ -142,7 +156,7 @@ systop(Name) when is_binary(Name) -> feed_var(Var, Val, Topic) -> feed_var(Var, Val, words(Topic), []). feed_var(_Var, _Val, [], Acc) -> - join(reverse(Acc)); + join(lists:reverse(Acc)); feed_var(Var, Val, [Var|Words], Acc) -> feed_var(Var, Val, Words, [Val|Acc]); feed_var(Var, Val, [W|Words], Acc) -> @@ -166,17 +180,15 @@ join(Words) -> parse(Topic) when is_binary(Topic) -> parse(Topic, #{}). -parse(Topic = <<"$queue/", Topic1/binary>>, Options) -> - case maps:find(share, Options) of - {ok, _} -> error({invalid_topic, Topic}); - error -> parse(Topic1, maps:put(share, '$queue', Options)) - end; -parse(Topic = <<"$share/", Topic1/binary>>, Options) -> - case maps:find(share, Options) of - {ok, _} -> error({invalid_topic, Topic}); - error -> [Group, Topic2] = binary:split(Topic1, <<"/">>), - {Topic2, maps:put(share, Group, Options)} - end; +parse(Topic = <<"$queue/", _/binary>>, #{share := _Group}) -> + error({invalid_topic, Topic}); +parse(Topic = <<"$share/", _/binary>>, #{share := _Group}) -> + error({invalid_topic, Topic}); +parse(<<"$queue/", Topic1/binary>>, Options) -> + parse(Topic1, maps:put(share, '$queue', Options)); +parse(<<"$share/", Topic1/binary>>, Options) -> + [Group, Topic2] = binary:split(Topic1, <<"/">>), + {Topic2, maps:put(share, Group, Options)}; parse(Topic, Options) -> {Topic, Options}. diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 5932b9ef7..c36b484c6 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -16,7 +16,6 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --include("emqx_misc.hrl"). -export([info/1]). -export([stats/1]). @@ -44,9 +43,8 @@ shutdown_reason }). --define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). - -define(INFO_KEYS, [peername, sockname]). +-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(WSLOG(Level, Format, Args, State), lager:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). @@ -110,8 +108,8 @@ websocket_init(#state{request = Req, options = Options}) -> sendfun => send_fun(self())}, Options), ParserState = emqx_protocol:parser(ProtoState), Zone = proplists:get_value(zone, Options), - EnableStats = emqx_zone:env(Zone, enable_stats, true), - IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000), + EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), {ok, #state{peername = Peername, sockname = Sockname, diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index 830f08b89..fdcfc37a5 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -16,9 +16,11 @@ -behaviour(gen_server). --export([start_link/0]). +-include("emqx.hrl"). --export([env/2, env/3]). +-export([start_link/0]). +-export([get_env/2, get_env/3]). +-export([set_env/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -31,19 +33,25 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -env(undefined, Par) -> - emqx_config:get_env(Par); -env(Zone, Par) -> - env(Zone, Par, undefined). +-spec(get_env(zone() | undefined, atom()) -> undefined | term()). +get_env(undefined, Key) -> + emqx_config:get_env(Key); +get_env(Zone, Key) -> + get_env(Zone, Key, undefined). -env(undefined, Par, Default) -> - emqx_config:get_env(Par, Default); -env(Zone, Par, Default) -> - try ets:lookup_element(?TAB, {Zone, Par}, 2) +-spec(get_env(zone() | undefined, atom(), term()) -> undefined | term()). +get_env(undefined, Key, Def) -> + emqx_config:get_env(Key, Def); +get_env(Zone, Key, Def) -> + try ets:lookup_element(?TAB, {Zone, Key}, 2) catch error:badarg -> - emqx_config:get_env(Par, Default) + emqx_config:get_env(Key, Def) end. +-spec(set_env(zone(), atom(), term()) -> ok). +set_env(Zone, Key, Val) -> + gen_server:cast(?MODULE, {set_env, Zone, Key, Val}). + %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ @@ -56,6 +64,10 @@ handle_call(Req, _From, State) -> emqx_logger:error("[Zone] unexpected call: ~p", [Req]), {reply, ignored, State}. +handle_cast({set_env, Zone, Key, Val}, State) -> + true = ets:insert(?TAB, {{Zone, Key}, Val}), + {noreply, State}; + handle_cast(Msg, State) -> emqx_logger:error("[Zone] unexpected cast: ~p", [Msg]), {noreply, State}. @@ -63,7 +75,7 @@ handle_cast(Msg, State) -> handle_info(reload, State) -> lists:foreach( fun({Zone, Opts}) -> - [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Opts] + [ets:insert(?TAB, {{Zone, Key}, Val}) || {Key, Val} <- Opts] end, emqx_config:get_env(zones, [])), {noreply, ensure_reload_timer(State), hibernate}; @@ -82,5 +94,5 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ ensure_reload_timer(State) -> - State#state{timer = erlang:send_after(5000, self(), reload)}. + State#state{timer = erlang:send_after(10000, self(), reload)}. diff --git a/include/emqx_misc.hrl b/test/emqx_mqtt_caps_SUITE.erl similarity index 59% rename from include/emqx_misc.hrl rename to test/emqx_mqtt_caps_SUITE.erl index e904b71e3..3fbb422d5 100644 --- a/include/emqx_misc.hrl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -12,15 +12,15 @@ %% See the License for the specific language governing permissions and %% limitations under the License. --define(record_to_map(Def, Rec), - maps:from_list(?record_to_proplist(Def, Rec))). +-module(emqx_mqtt_caps_SUITE). --define(record_to_map(Def, Rec, Fields), - maps:from_list(?record_to_proplist(Def, Rec, Fields))). +-include_lib("eunit/include/eunit.hrl"). --define(record_to_proplist(Def, Rec), - lists:zip(record_info(fields, Def), tl(tuple_to_list(Rec)))). +%% CT +-compile(export_all). +-compile(nowarn_export_all). + +all() -> + []. --define(record_to_proplist(Def, Rec, Fields), - [{K, V} || {K, V} <- ?record_to_proplist(Def, Rec), lists:member(K, Fields)]). diff --git a/test/emqx_topic_SUITE.erl b/test/emqx_topic_SUITE.erl index 87fb2c906..f6f2c007e 100644 --- a/test/emqx_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -1,5 +1,4 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. All Rights Reserved. +%% Copyright (c) 2018 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. @@ -12,7 +11,6 @@ %% 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_topic_SUITE). @@ -27,10 +25,23 @@ -define(N, 10000). -all() -> [t_wildcard, t_match, t_match2, t_match3, t_validate, t_triples, t_join, - t_words, t_systop, t_feed_var, t_sys_match, 't_#_match', - t_sigle_level_validate, t_sigle_level_match, t_match_perf, - t_triples_perf, t_parse]. +all() -> + [t_wildcard, + t_match, t_match2, t_match3, + t_validate, + t_triples, + t_join, + t_levels, + t_words, + t_systop, + t_feed_var, + t_sys_match, + 't_#_match', + t_sigle_level_validate, + t_sigle_level_match, + t_match_perf, + t_triples_perf, + t_parse]. t_wildcard(_) -> true = wildcard(<<"a/b/#">>), @@ -149,6 +160,9 @@ t_triples_perf(_) -> end), io:format("Time for triples: ~p(micro)", [Time/?N]). +t_levels(_) -> + ?assertEqual(4, emqx_topic:levels(<<"a/b/c/d">>)). + t_words(_) -> ['', <<"a">>, '+', '#'] = words(<<"/a/+/#">>), ['', <<"abkc">>, <<"19383">>, '+', <<"akakdkkdkak">>, '#'] = words(<<"/abkc/19383/+/akakdkkdkak/#">>),