diff --git a/.travis.yml b/.travis.yml index c625470d0..b2d01f3fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: erlang otp_release: - - 20.0 - - 20.1 + - 21.0 + - 21.0.4 script: - make diff --git a/Makefile b/Makefile index 349baab95..7b1cc1cdd 100644 --- a/Makefile +++ b/Makefile @@ -4,20 +4,16 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx +DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique -dep_goldrush = git https://github.com/basho/goldrush 0.1.9 -dep_gproc = git https://github.com/uwiger/gproc 0.7.0 -dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 -dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 -dep_lager = git https://github.com/basho/lager master -dep_lager_syslog = git https://github.com/basho/lager_syslog -dep_esockd = git https://github.com/emqtt/esockd emqx30 -dep_ekka = git https://github.com/emqtt/ekka develop -dep_mochiweb = git https://github.com/emqtt/mochiweb emqx30 -dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1 -dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master -dep_clique = git https://github.com/emqx/clique +dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 +dep_gproc = git https://github.com/uwiger/gproc 0.8.0 +dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.1.1 +dep_lager = git https://github.com/erlang-lager/lager 3.6.4 +dep_esockd = git https://github.com/emqx/esockd emqx30 +dep_ekka = git https://github.com/emqx/ekka emqx30 +dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0 +dep_clique = git https://github.com/emqx/clique NO_AUTOPATCH = gen_rpc cuttlefish @@ -25,7 +21,7 @@ ERLC_OPTS += +debug_info ERLC_OPTS += +'{parse_transform, lager_transform}' BUILD_DEPS = cuttlefish -dep_cuttlefish = git https://github.com/emqtt/cuttlefish +dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30 TEST_DEPS = emqx_ct_helplers dep_emqx_ct_helplers = git git@github.com:emqx/emqx_ct_helpers @@ -47,8 +43,7 @@ COVER = true PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets public_key ssl lager compiler mnesia DIALYZER_DIRS := ebin/ -DIALYZER_OPTS := --verbose --statistics -Werror_handling \ - -Wrace_conditions #-Wunmatched_returns +DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns include erlang.mk diff --git a/TODO b/TODO index 055d2b8d4..87e6dea16 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,7 @@ 1. Update the README.md 2. Update the Documentation -3. Shared subscription strategy and dispatch strategy +3. Shared subscription and dispatch strategy +4. Remove lager syslog: + dep_lager_syslog = git https://github.com/basho/lager_syslog diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index 6e5cd4205..5b7e403f8 100644 Binary files a/docs/mqtt-v5.0.pdf and b/docs/mqtt-v5.0.pdf differ diff --git a/etc/acl.conf b/etc/acl.conf index 2560bf80d..fb85f3f20 100644 --- a/etc/acl.conf +++ b/etc/acl.conf @@ -24,3 +24,4 @@ {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. +{allow, all}. diff --git a/etc/emqx.conf b/etc/emqx.conf index 5aa4bf3ea..6b31c63c3 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -9,7 +9,7 @@ ## Cluster name. ## ## Value: String -cluster.name = emqxcluster +cluster.name = emqxcl ## Cluster auto-discovery strategy. ## @@ -48,7 +48,7 @@ cluster.autoclean = 5m ## Node list of the cluster. ## ## Value: String -## cluster.static.seeds = emq1@127.0.0.1,emq2@127.0.0.1 +## cluster.static.seeds = emqx1@127.0.0.1,emqx2@127.0.0.1 ##-------------------------------------------------------------------- ## Cluster using IP Multicast. @@ -91,7 +91,7 @@ cluster.autoclean = 5m ## The App name is used to build 'node.name' with IP address. ## ## Value: String -## cluster.dns.app = emq +## cluster.dns.app = emqx ##-------------------------------------------------------------------- ## Cluster using etcd @@ -105,7 +105,7 @@ cluster.autoclean = 5m ## will create a path in etcd: v2/keys/// ## ## Value: String -## cluster.etcd.prefix = emqcl +## cluster.etcd.prefix = emqxcl ## The TTL for node's path in etcd. ## @@ -125,7 +125,7 @@ cluster.autoclean = 5m ## The service name helps lookup EMQ nodes in the cluster. ## ## Value: String -## cluster.k8s.service_name = emq +## cluster.k8s.service_name = emqx ## The address type is used to extract host from k8s service. ## @@ -135,7 +135,7 @@ cluster.autoclean = 5m ## The app name helps build 'node.name'. ## ## Value: String -## cluster.k8s.app_name = emq +## cluster.k8s.app_name = emqx ## Kubernates Namespace ## @@ -143,7 +143,7 @@ cluster.autoclean = 5m ## cluster.k8s.namespace = default ##-------------------------------------------------------------------- -## Node Args +## Node ##-------------------------------------------------------------------- ## Node name. @@ -276,38 +276,54 @@ node.dist_listen_min = 6369 node.dist_listen_max = 6369 ##-------------------------------------------------------------------- -## RPC Args +## RPC ##-------------------------------------------------------------------- -## TCP server port. +## TCP server port for RPC. +## +## Value: Port [1024-65535] rpc.tcp_server_port = 5369 -## Default TCP port for outgoing connections +## TCP port for outgoing RPC connections. +## +## Value: Port [1024-65535] rpc.tcp_client_port = 5369 -## Client connect timeout +## RCP Client connect timeout. +## +## Value: Seconds rpc.connect_timeout = 5000 -## Client and Server send timeout +## TCP send timeout of RPC client and server. +## +## Value: Seconds rpc.send_timeout = 5000 ## Authentication timeout +## +## Value: Seconds rpc.authentication_timeout = 5000 ## Default receive timeout for call() functions +## +## Value: Seconds rpc.call_receive_timeout = 15000 -## Socket keepalive configuration +## Socket idle keepalive. +## +## Value: Seconds rpc.socket_keepalive_idle = 900 -## Seconds between probes +## TCP Keepalive probes interval. +## +## Value: Integer rpc.socket_keepalive_interval = 75 ## Probes lost to close the connection +## +## Value: Integer rpc.socket_keepalive_count = 9 -## TODO: sndbuf, rcvbuf and buffer - ##-------------------------------------------------------------------- ## Log ##-------------------------------------------------------------------- @@ -396,286 +412,78 @@ log.syslog = on ## Sets the severity level for syslog. ## ## Value: debug | info | notice | warning | error | critical | alert | emergency -log.syslog.level = error +## log.syslog.level = error ##-------------------------------------------------------------------- -## Allow Anonymous Authentication and Default ACL +## Authentication/Access Control ##-------------------------------------------------------------------- -## Allow Anonymous Authentication. -## -## Notice: Disable the option for production deployment. +## Allow anonymous authentication by default if no auth plugins loaded. +## Notice: Disable the option in production deployment! ## ## Value: true | false -mqtt.allow_anonymous = true - -## Default behaviour when ACL nomatch. -## -## Value: allow | deny -mqtt.acl_nomatch = allow +allow_anonymous = true ## Default ACL File. ## ## Value: File Name -mqtt.acl_file = {{ platform_etc_dir }}/acl.conf +acl_file = {{ platform_etc_dir }}/acl.conf -## Whether to cache ACL for publish messages. +## Whether to enable ACL cache for publish. ## -## Value: true | false -mqtt.cache_acl = true +## Value: on | off +enable_acl_cache = on + +## The ACL cache age. +## +## Value: Duration +## Default: 5 minute +acl_cache_age = 5m ##-------------------------------------------------------------------- -## MQTT Protocol +## MQTT ##-------------------------------------------------------------------- -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -mqtt.max_clientid_len = 1024 - ## Maximum MQTT packet size allowed. ## ## Value: Bytes +## Default: 1MB +mqtt.max_packet_size = 1MB + +## Maximum length of MQTT clientId allowed. ## -## Default: 64K -mqtt.max_packet_size = 64KB +## Value: Number [23-65535] +mqtt.max_clientid_len = 65535 -## Check if the websocket protocol header is valid. -## Turn off the option when developing WeChat App. -## -## Value: on | off -mqtt.websocket_protocol_header = on - -## Check Websocket Upgrade Header. -## -## Value: on | off -mqtt.websocket_check_upgrade_header = on - -## The backoff for MQTT keepalive timeout. -## EMQ will kick a MQTT connection out until 'Keepalive * backoff * 2' timeout. -## -## Value: Float > 0.5 -mqtt.keepalive_backoff = 0.75 - -##-------------------------------------------------------------------- -## MQTT Connection -##-------------------------------------------------------------------- - -## Force GC the MQTT connections. Value 0 will disable the Force GC. -## -## Value: Number >= 0 -mqtt.conn.force_gc_count = 100 - -##-------------------------------------------------------------------- -## MQTT Client -##-------------------------------------------------------------------- - -## MQTT client idle timeout, specified in seconds. -## -## Value: Duration -mqtt.client.idle_timeout = 30s - -## TODO: Maximum publish rate of MQTT messages per second. +## Maximum topic levels allowed. 0 means no limit. ## ## Value: Number -## mqtt.client.max_publish_rate = 5 +mqtt.max_topic_levels = 0 -## Enable per client statistics. -## -## Value: on | off -mqtt.client.enable_stats = off - -##-------------------------------------------------------------------- -## MQTT Session -##-------------------------------------------------------------------- - -## Maximum number of subscriptions allowed, 0 means no limit. -## -## Value: Number -mqtt.session.max_subscriptions = 0 - -## Force to upgrade QoS according to subscription. -## -## Value: on | off -mqtt.session.upgrade_qos = off - -## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. -## -## Value: Number -mqtt.session.max_inflight = 32 - -## Retry interval for QoS1/2 message delivering. -## -## Value: Duration -mqtt.session.retry_interval = 20s - -## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. -## -## Value: Number -mqtt.session.max_awaiting_rel = 1000 - -## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. -## -## Value: Duration -mqtt.session.await_rel_timeout = 30s - -## Enable per session statistics. -## -## Value: on | off -mqtt.session.enable_stats = on - -## Session expiration time. -## -## Value: Duration -## -d: day -## -h: hour -## -m: minute -## -s: second -## -## Default: 2h, 2 hours -mqtt.session.expiry_interval = 2h - -## Whether to ignore loop delivery of messages. -## -## Value: true | false -## -## Default: false -mqtt.session.ignore_loop_deliver = false - -##-------------------------------------------------------------------- -## MQTT Message Queue -##-------------------------------------------------------------------- - -## Message queue type. -## -## Value: simple | priority -mqtt.mqueue.type = simple - -## Topic priority. Default is 0. -## -## Value: Number [0-255] -## -## mqtt.mqueue.priority = topic/1=10,topic/2=8 - -## Maximum queue length. Enqueued messages when persistent client disconnected, -## or inflight window is full. 0 means no limit. -## -## Value: Number >= 0 -mqtt.mqueue.max_length = 1000 - -## Low-water mark of queued messages. -## -## Value: Percent -mqtt.mqueue.low_watermark = 20% - -## High-water mark of queued messages. -## -## Value: Percent -mqtt.mqueue.high_watermark = 60% - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -mqtt.mqueue.store_qos0 = true - -##-------------------------------------------------------------------- -## MQTT Broker and PubSub -##-------------------------------------------------------------------- - -## System interval of publishing $SYS messages. -## -## Value: Duration -## -## Default: 1m, 1 minute -mqtt.broker.sys_interval = 1m - -## The PubSub pool size. Default value should be same as scheduler numbers. -## -## Value: Number > 1 -mqtt.pubsub.pool_size = 8 - -## TODO: Subscribe asynchronously. -## -## Value: true | false -mqtt.pubsub.async = true - -##-------------------------------------------------------------------- -## MQTT Bridge -##-------------------------------------------------------------------- - -## The pending message queue size of bridge. -## -## Value: Number -mqtt.bridge.max_queue_len = 10000 - -## Ping interval of bridge node. -## -## Value: Duration -## -## Default: 1s, 1 second -mqtt.bridge.ping_down_interval = 1s - -##------------------------------------------------------------------- -## Plugins -##------------------------------------------------------------------- - -## The etc dir for plugins' config. -## -## Value: Folder -mqtt.plugins.etc_dir ={{ platform_etc_dir }}/plugins/ - -## The file to store loaded plugin names. -## -## Value: File -mqtt.plugins.loaded_file = {{ platform_data_dir }}/loaded_plugins - -## File to store loaded plugin names. -mqtt.plugins.expand_plugins_dir = {{ platform_plugins_dir }}/ - -##-------------------------------------------------------------------- -## Modules -##-------------------------------------------------------------------- - -##-------------------------------------------------------------------- -## Presence Module - -## Enable Presence Module. -## -## Value: on | off -module.presence = on - -## Sets the QoS for presence MQTT message. +## Maximum QoS allowed. ## ## Value: 0 | 1 | 2 -module.presence.qos = 1 +mqtt.max_qos_allowed = 2 -##-------------------------------------------------------------------- -## Subscription Module - -## Enable Subscription Module. +## Maximum Topic Alias, 0 means no limit. ## -## Value: on | off -module.subscription = off +## Value: 0-65535 +mqtt.max_topic_alias = 0 -## Subscribe the Topics automatically when client connected. -## module.subscription.1.topic = $client/%c -## Qos of the subscription: 0 | 1 | 2 -## module.subscription.1.qos = 1 - -## module.subscription.2.topic = $user/%u -## module.subscription.2.qos = 1 - -##-------------------------------------------------------------------- -## Rewrite Module - -## Enable Rewrite Module. +## Whether the Server supports MQTT retained messages. ## -## Value: on | off -module.rewrite = off +## Value: boolean +mqtt.retain_available = true -## {rewrite, Topic, Re, Dest} -## module.rewrite.rule.1 = x/# ^x/y/(.+)$ z/y/$1 -## module.rewrite.rule.2 = y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2 +## Whether the Server supports MQTT Wildcard Subscriptions +## +## Value: boolean +mqtt.wildcard_subscription = true + +## Whether the Server supports MQTT Shared Subscriptions +## +## Value: boolean +mqtt.shared_subscription = true ##-------------------------------------------------------------------- ## Listeners @@ -684,7 +492,7 @@ module.rewrite = off ##-------------------------------------------------------------------- ## MQTT/TCP - External TCP Listener for MQTT Protocol -## listener.tcp. is the IP address and port that the MQTT/TCP +## listener.tcp.$name is the IP address and port that the MQTT/TCP ## listener will bind. ## ## Value: IP:Port | Port @@ -695,32 +503,40 @@ listener.tcp.external = 0.0.0.0:1883 ## The acceptor pool for external MQTT/TCP listener. ## ## Value: Number -listener.tcp.external.acceptors = 16 +listener.tcp.external.acceptors = 8 ## Maximum number of concurrent MQTT/TCP connections. ## ## Value: Number -listener.tcp.external.max_clients = 102400 +listener.tcp.external.max_connections = 1024000 -## TODO: Zone of the external MQTT/TCP listener belonged to. +## Maximum external connections per second. +## +## Value: Number +listener.tcp.external.max_conn_rate = 1000 + +## Zone of the external MQTT/TCP listener belonged to. +## +## See: zone.$name.* ## ## Value: String -## listener.tcp.external.zone = external +listener.tcp.external.zone = external -## Mountpoint of the MQTT/TCP Listener. All the topics of this -## listener will be prefixed with the mount point if this option -## is enabled. -## Notice that EMQ X supports wildcard mount:%c clientid, %u username +## Mountpoint of the MQTT/TCP Listener. All the topics will be prefixed +## with the mountpoint path if this option is enabled. +## +## Variables in mountpoint path: +## - %c: clientid +## - %u: username ## ## Value: String -## listener.tcp.external.mountpoint = external/ +## listener.tcp.external.mountpoint = devicebound/ -## Rate limit for the external MQTT/TCP connections. -## Format is 'burst,rate'. +## Rate limit for the external MQTT/TCP connections. Format is 'rate,burst'. ## -## Value: burst,rate -## Unit: KB/sec -## listener.tcp.external.rate_limit = 100,10 +## Value: rate,burst +## Unit: Bps +## listener.tcp.external.rate_limit = 1024,4096 ## The access control rules for the MQTT/TCP listener. ## @@ -731,7 +547,7 @@ listener.tcp.external.max_clients = 102400 ## Example: allow 192.168.0.0/24 listener.tcp.external.access.1 = allow all -## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed +## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed ## behind HAProxy or Nginx. ## ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ @@ -739,18 +555,17 @@ listener.tcp.external.access.1 = allow all ## Value: on | off ## listener.tcp.external.proxy_protocol = on -## Sets the timeout for proxy protocol. EMQ will close the TCP connection +## Sets the timeout for proxy protocol. EMQ X will close the TCP connection ## if no proxy protocol packet recevied within the timeout. ## ## Value: Duration ## listener.tcp.external.proxy_protocol_timeout = 3s ## Enable the option for X.509 certificate based authentication. -## EMQ will Use the PP2_SUBTYPE_SSL_CN field in Proxy Protocol V2 -## as MQTT username. +## EMQX will use the common name of certificate as MQTT username. ## -## Value: cn -## listener.tcp.external.peer_cert_as_username = cn +## Value: boolean +## listener.tcp.external.peer_cert_as_username = true ## The TCP backlog defines the maximum length that the queue of pending ## connections can grow to. @@ -773,14 +588,14 @@ listener.tcp.external.send_timeout_close = on ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes -## listener.tcp.external.recbuf = 4KB +## listener.tcp.external.recbuf = 2KB ## The TCP send buffer(os kernel) for MQTT connections. ## ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes -## listener.tcp.external.sndbuf = 4KB +## listener.tcp.external.sndbuf = 2KB ## The size of the user-level software buffer used by the driver. ## Not to be confused with options sndbuf and recbuf, which correspond @@ -792,7 +607,7 @@ listener.tcp.external.send_timeout_close = on ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes -## listener.tcp.external.buffer = 4KB +## listener.tcp.external.buffer = 2KB ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. ## @@ -829,51 +644,57 @@ listener.tcp.internal.acceptors = 4 ## Maximum number of concurrent MQTT/TCP connections. ## ## Value: Number -listener.tcp.internal.max_clients = 102400 +listener.tcp.internal.max_connections = 10240000 -## TODO: Zone of the internal MQTT/TCP listener belonged to. +## Maximum internal connections per second. +## +## Value: Number +listener.tcp.internal.max_conn_rate = 1000 + +## Zone of the internal MQTT/TCP listener belonged to. ## ## Value: String -## listener.tcp.internal.zone = internal +listener.tcp.internal.zone = internal ## Mountpoint of the MQTT/TCP Listener. ## -## See: listener.tcp..mountpoint +## See: listener.tcp.$name.mountpoint ## ## Value: String ## listener.tcp.internal.mountpoint = internal/ ## Rate limit for the internal MQTT/TCP connections. ## -## See: listener.tcp..rate_limit +## See: listener.tcp.$name.rate_limit ## -## Value: burst,rate -## listener.tcp.internal.rate_limit = 1000,100 +## Value: rate,burst +## Unit: Bps +## listener.tcp.internal.rate_limit = 1000000,2000000 ## The TCP backlog of internal MQTT/TCP Listener. ## -## See: listener.tcp..backlog +## See: listener.tcp.$name.backlog ## ## Value: Number >= 0 listener.tcp.internal.backlog = 512 ## The TCP send timeout for internal MQTT connections. ## -## See: listener.tcp..send_timeout +## See: listener.tcp.$name.send_timeout ## ## Value: Duration listener.tcp.internal.send_timeout = 5s ## Close the MQTT/TCP connection if send timeout. ## -## See: listener.tcp..send_timeout_close +## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off listener.tcp.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for internal MQTT connections. ## -## See: listener.tcp..recbuf +## See: listener.tcp.$name.recbuf ## ## Value: Bytes ## listener.tcp.internal.recbuf = 16KB @@ -887,21 +708,21 @@ listener.tcp.external.send_timeout_close = on ## The size of the user-level software buffer used by the driver. ## -## See: listener.tcp..buffer +## See: listener.tcp.$name.buffer ## ## Value: Bytes ## listener.tcp.internal.buffer = 16KB ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. ## -## See: listener.tcp..tune_buffer +## See: listener.tcp.$name.tune_buffer ## ## Value: on | off ## listener.tcp.internal.tune_buffer = off ## The TCP_NODELAY flag for internal MQTT connections. ## -## See: listener.tcp..nodelay +## See: listener.tcp.$name.nodelay ## ## Value: true | false listener.tcp.internal.nodelay = false @@ -914,7 +735,7 @@ listener.tcp.internal.reuseaddr = true ##-------------------------------------------------------------------- ## MQTT/SSL - External SSL Listener for MQTT Protocol -## listener.ssl. is the IP address and port that the MQTT/SSL +## listener.ssl.$name is the IP address and port that the MQTT/SSL ## listener will bind. ## ## Value: IP:Port | Port @@ -930,41 +751,47 @@ listener.ssl.external.acceptors = 16 ## Maximum number of concurrent MQTT/SSL connections. ## ## Value: Number -listener.ssl.external.max_clients = 102400 +listener.ssl.external.max_connections = 102400 -## TODO: Zone of the external MQTT/SSL listener belonged to. +## Maximum MQTT/SSL connections per second. +## +## Value: Number +listener.ssl.external.max_conn_rate = 500 + +## Zone of the external MQTT/SSL listener belonged to. ## ## Value: String -## listener.ssl.external.zone = external +listener.ssl.external.zone = external ## Mountpoint of the MQTT/SSL Listener. ## ## Value: String -## listener.ssl.external.mountpoint = inbound/ +## listener.ssl.external.mountpoint = devicebound/ ## The access control rules for the MQTT/SSL listener. ## -## See: listener.tcp..access +## See: listener.tcp.$name.access ## ## Value: ACL Rule listener.ssl.external.access.1 = allow all ## Rate limit for the external MQTT/SSL connections. ## -## Value: burst,rate -## listener.ssl.external.rate_limit = 100,10 +## Value: rate,burst +## Unit: Bps +## listener.ssl.external.rate_limit = 1024,4096 ## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind ## HAProxy or Nginx. ## -## See: listener.tcp..proxy_protocol +## See: listener.tcp.$name.proxy_protocol ## ## Value: on | off ## listener.ssl.external.proxy_protocol = on ## Sets the timeout for proxy protocol. ## -## See: listener.tcp..proxy_protocol_timeout +## See: listener.tcp.$name.proxy_protocol_timeout ## ## Value: Duration ## listener.ssl.external.proxy_protocol_timeout = 3s @@ -1078,64 +905,64 @@ listener.ssl.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ## Value: on | off ## listener.ssl.external.honor_cipher_order = on -## Use the CN or DN value from the client certificate as a username. +## Use the CN field from the client certificate as a username. ## Notice that 'verify' should be set as 'verify_peer'. ## -## Value: cn | dn +## Value: boolean ## listener.ssl.external.peer_cert_as_username = cn ## TCP backlog for the SSL connection. ## -## See listener.tcp..backlog +## See listener.tcp.$name.backlog ## ## Value: Number >= 0 ## listener.ssl.external.backlog = 1024 ## The TCP send timeout for the SSL connection. ## -## See listener.tcp..send_timeout +## See listener.tcp.$name.send_timeout ## ## Value: Duration ## listener.ssl.external.send_timeout = 15s ## Close the SSL connection if send timeout. ## -## See: listener.tcp..send_timeout_close +## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off ## listener.ssl.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for the SSL connections. ## -## See: listener.tcp..recbuf +## See: listener.tcp.$name.recbuf ## ## Value: Bytes ## listener.ssl.external.recbuf = 4KB ## The TCP send buffer(os kernel) for internal MQTT connections. ## -## See: listener.tcp..sndbuf +## See: listener.tcp.$name.sndbuf ## ## Value: Bytes ## listener.ssl.external.sndbuf = 4KB ## The size of the user-level software buffer used by the driver. ## -## See: listener.tcp..buffer +## See: listener.tcp.$name.buffer ## ## Value: Bytes ## listener.ssl.external.buffer = 4KB ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. ## -## See: listener.tcp..tune_buffer +## See: listener.tcp.$name.tune_buffer ## ## Value: on | off ## listener.ssl.external.tune_buffer = off ## The TCP_NODELAY flag for SSL connections. ## -## See: listener.tcp..nodelay +## See: listener.tcp.$name.nodelay ## ## Value: true | false ## listener.ssl.external.nodelay = true @@ -1146,9 +973,9 @@ listener.ssl.external.certfile = {{ platform_etc_dir }}/certs/cert.pem listener.ssl.external.reuseaddr = true ##-------------------------------------------------------------------- -## External WebSocket Listener for MQTT Protocol +## External WebSocket listener for MQTT protocol -## listener.ws. is the IP address and port that the MQTT/WebSocket +## listener.ws.$name is the IP address and port that the MQTT/WebSocket ## listener will bind. ## ## Value: IP:Port | Port @@ -1164,34 +991,50 @@ listener.ws.external.acceptors = 4 ## Maximum number of concurrent MQTT/WebSocket connections. ## ## Value: Number -listener.ws.external.max_clients = 102400 +listener.ws.external.max_connections = 102400 -## TODO: Zone of the external MQTT/WebSocket listener belonged to. +## Maximum MQTT/WebSocket connections per second. +## +## Value: Number +listener.ws.external.max_conn_rate = 1000 + +## Rate limit for the MQTT/WebSocket connections. +## +## Value: rate,burst +## Unit: Bps +## listener.ws.external.rate_limit = 1024,4096 + +## Zone of the external MQTT/WebSocket listener belonged to. ## ## Value: String -## listener.ws.external.zone = external +listener.ws.external.zone = external ## Mountpoint of the MQTT/WebSocket Listener. ## -## See: listener.tcp..mountpoint +## See: listener.tcp.$name.mountpoint ## ## Value: String -## listener.ws.external.mountpoint = external/ +## listener.ws.external.mountpoint = devicebound/ ## The access control for the MQTT/WebSocket listener. ## -## See: listener.tcp..access +## See: listener.tcp.$name.access ## ## Value: ACL Rule listener.ws.external.access.1 = allow all -## Use X-Forwarded-For header for real source IP if the EMQ cluster is +## Verify if the protocol header is valid. Turn off for WeChat MiniApp. +## +## Value: on | off +listener.ws.external.verify_protocol_header = on + +## Use X-Forwarded-For header for real source IP if the EMQ X cluster is ## deployed behind NGINX or HAProxy. ## ## Value: String ## listener.ws.external.proxy_address_header = X-Forwarded-For -## Use X-Forwarded-Port header for real source port if the EMQ cluster is +## Use X-Forwarded-Port header for real source port if the EMQ X cluster is ## deployed behind NGINX or HAProxy. ## ## Value: String @@ -1200,83 +1043,78 @@ listener.ws.external.access.1 = allow all ## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind ## HAProxy or Nginx. ## -## See: listener.tcp..proxy_protocol +## See: listener.tcp.$name.proxy_protocol ## ## Value: on | off ## listener.ws.external.proxy_protocol = on ## Sets the timeout for proxy protocol. ## -## See: listener.tcp..proxy_protocol_timeout +## See: listener.tcp.$name.proxy_protocol_timeout ## ## Value: Duration ## listener.ws.external.proxy_protocol_timeout = 3s ## The TCP backlog of external MQTT/WebSocket Listener. ## -## See: listener.tcp..backlog +## See: listener.tcp.$name.backlog ## ## Value: Number >= 0 listener.ws.external.backlog = 1024 ## The TCP send timeout for external MQTT/WebSocket connections. ## -## See: listener.tcp..send_timeout +## See: listener.tcp.$name.send_timeout ## ## Value: Duration listener.ws.external.send_timeout = 15s ## Close the MQTT/WebSocket connection if send timeout. ## -## See: listener.tcp..send_timeout_close +## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off listener.ws.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for external MQTT/WebSocket connections. ## -## See: listener.tcp..recbuf +## See: listener.tcp.$name.recbuf ## ## Value: Bytes -## listener.ws.external.recbuf = 4KB +## listener.ws.external.recbuf = 2KB ## The TCP send buffer(os kernel) for external MQTT/WebSocket connections. ## -## See: listener.tcp..sndbuf +## See: listener.tcp.$name.sndbuf ## ## Value: Bytes -## listener.ws.external.sndbuf = 4KB +## listener.ws.external.sndbuf = 2KB ## The size of the user-level software buffer used by the driver. ## -## See: listener.tcp..buffer +## See: listener.tcp.$name.buffer ## ## Value: Bytes -## listener.ws.external.buffer = 4KB +## listener.ws.external.buffer = 2KB ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. ## -## See: listener.tcp..tune_buffer +## See: listener.tcp.$name.tune_buffer ## ## Value: on | off ## listener.ws.external.tune_buffer = off ## The TCP_NODELAY flag for external MQTT/WebSocket connections. ## -## See: listener.tcp..nodelay +## See: listener.tcp.$name.nodelay ## ## Value: true | false listener.ws.external.nodelay = true -## The SO_REUSEADDR flag for MQTT/WebSocket Listener. -## -## Value: true | false -listener.ws.external.reuseaddr = true - ##-------------------------------------------------------------------- ## External WebSocket/SSL listener for MQTT Protocol -## listener.wss. is the IP address and port that the MQTT/WebSocket/SSL +## listener.wss.$name is the IP address and port that the MQTT/WebSocket/SSL ## listener will bind. ## ## Value: IP:Port | Port @@ -1292,27 +1130,45 @@ listener.wss.external.acceptors = 4 ## Maximum number of concurrent MQTT/Webwocket/SSL connections. ## ## Value: Number -listener.wss.external.max_clients = 64 +listener.wss.external.max_connections = 16 -## TODO: Zone of the external MQTT/WebSocket/SSL listener belonged to. +## Maximum MQTT/WebSocket/SSL connections per second. +## +## See: listener.tcp.$name.max_conn_rate +## +## Value: Number +listener.wss.external.max_conn_rate = 1000 + +## Rate limit for the MQTT/WebSocket/SSL connections. +## +## Value: rate,burst +## Unit: Bps +## listener.wss.external.rate_limit = 1024,4096 + +## Zone of the external MQTT/WebSocket/SSL listener belonged to. ## ## Value: String -## listener.wss.external.zone = external +listener.wss.external.zone = external ## Mountpoint of the MQTT/WebSocket/SSL Listener. ## -## See: listener.tcp..mountpoint +## See: listener.tcp.$name.mountpoint ## ## Value: String -## listener.wss.external.mountpoint = inbound/ +## listener.wss.external.mountpoint = devicebound/ ## The access control rules for the MQTT/WebSocket/SSL listener. ## -## See: listener.tcp..access. +## See: listener.tcp.$name.access. ## ## Value: ACL Rule listener.wss.external.access.1 = allow all +## See: listener.ws.external.verify_protocol_header +## +## Value: on | off +listener.wss.external.verify_protocol_header = on + ## See: listener.ws.external.proxy_address_header ## ## Value: String @@ -1325,151 +1181,657 @@ listener.wss.external.access.1 = allow all ## Enable the Proxy Protocol V1/2 support. ## -## See: listener.tcp..proxy_protocol +## See: listener.tcp.$name.proxy_protocol ## ## Value: on | off ## listener.wss.external.proxy_protocol = on ## Sets the timeout for proxy protocol. ## -## See: listener.tcp..proxy_protocol_timeout +## See: listener.tcp.$name.proxy_protocol_timeout ## ## Value: Duration ## listener.wss.external.proxy_protocol_timeout = 3s ## TLS versions only to protect from POODLE attack. ## -## See: listener.ssl..tls_versions +## See: listener.ssl.$name.tls_versions ## ## Value: String, seperated by ',' ## listener.wss.external.tls_versions = tlsv1.2,tlsv1.1,tlsv1 -## TLS Handshake timeout. -## -## See: listener.ssl..handshake_timeout -## -## Value: Duration -listener.wss.external.handshake_timeout = 15s - ## Path to the file containing the user's private PEM-encoded key. ## -## See: listener.ssl..keyfile +## See: listener.ssl.$name.keyfile ## ## Value: File listener.wss.external.keyfile = {{ platform_etc_dir }}/certs/key.pem ## Path to a file containing the user certificate. ## -## See: listener.ssl..certfile +## See: listener.ssl.$name.certfile ## ## Value: File listener.wss.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ## Path to the file containing PEM-encoded CA certificates. ## -## See: listener.ssl..cacert +## See: listener.ssl.$name.cacert ## ## Value: File ## listener.wss.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem -## See: listener.ssl..dhfile +## See: listener.ssl.$name.dhfile ## ## Value: File ## listener.ssl.external.dhfile = {{ platform_etc_dir }}/certs/dh-params.pem -## See: listener.ssl..vefify +## See: listener.ssl.$name.vefify ## ## Value: vefify_peer | verify_none ## listener.wss.external.verify = verify_peer -## See: listener.ssl..fail_if_no_peer_cert +## See: listener.ssl.$name.fail_if_no_peer_cert ## ## Value: false | true ## listener.wss.external.fail_if_no_peer_cert = true -## See: listener.ssl..ciphers +## See: listener.ssl.$name.ciphers ## ## Value: Ciphers ## listener.wss.external.ciphers = -## See: listener.ssl..secure_renegotiate +## See: listener.ssl.$name.secure_renegotiate ## ## Value: on | off ## listener.wss.external.secure_renegotiate = off -## See: listener.ssl..reuse_sessions +## See: listener.ssl.$name.reuse_sessions ## ## Value: on | off ## listener.wss.external.reuse_sessions = on -## See: listener.ssl..honor_cipher_order +## See: listener.ssl.$name.honor_cipher_order ## ## Value: on | off ## listener.wss.external.honor_cipher_order = on -## See: listener.ssl..peer_cert_as_username +## See: listener.ssl.$name.peer_cert_as_username ## ## Value: cn | dn ## listener.wss.external.peer_cert_as_username = cn ## TCP backlog for the WebSocket/SSL connection. ## -## See: listener.tcp..backlog +## See: listener.tcp.$name.backlog ## ## Value: Number >= 0 listener.wss.external.backlog = 1024 ## The TCP send timeout for the WebSocket/SSL connection. ## -## See: listener.tcp..send_timeout +## See: listener.tcp.$name.send_timeout ## ## Value: Duration listener.wss.external.send_timeout = 15s ## Close the WebSocket/SSL connection if send timeout. ## -## See: listener.tcp..send_timeout_close +## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off listener.wss.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for the WebSocket/SSL connections. ## -## See: listener.tcp..recbuf +## See: listener.tcp.$name.recbuf ## ## Value: Bytes ## listener.wss.external.recbuf = 4KB ## The TCP send buffer(os kernel) for the WebSocket/SSL connections. ## -## See: listener.tcp..sndbuf +## See: listener.tcp.$name.sndbuf ## ## Value: Bytes ## listener.wss.external.sndbuf = 4KB ## The size of the user-level software buffer used by the driver. ## -## See: listener.tcp..buffer +## See: listener.tcp.$name.buffer ## ## Value: Bytes ## listener.wss.external.buffer = 4KB ## The TCP_NODELAY flag for WebSocket/SSL connections. ## -## See: listener.tcp..nodelay +## See: listener.tcp.$name.nodelay ## ## Value: true | false ## listener.wss.external.nodelay = true -## The SO_REUSEADDR flag for WebSocket/SSL listener. +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 -listener.wss.external.reuseaddr = true +## 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 +##-------------------------------------------------------------------- + +##-------------------------------------------------------------------- +## Bridges to edge +##-------------------------------------------------------------------- +## Bridge type. +## +## Value: Enum +## Example: out | in +bridge.edge.type = in + +## Bridge address: node name for local bridge, host:port for remote. +## +## Value: String +## Example: emqx@127.0.0.1, 127.0.0.1:1883 +bridge.edge.address = 127.0.0.1:1883 + +## Protocol version of the bridge. +## +## Value: Enum +## - mqtt5 +## - mqtt4 +## - mqtt3 +bridge.edge.proto_ver = mqtt4 + +## The ClientId of a remote bridge. +## +## Value: String +bridge.edge.client_id = bridge_edge + +## The Clean start flag of a remote bridge. +## +## Value: boolean +bridge.edge.clean_start = false + +## The username for a remote bridge. +## +## Value: String +bridge.edge.username = user + +## The password for a remote bridge. +## +## Value: String +bridge.edge.password = passwd + +## Mountpoint of the bridge. +## +## Value: String +## bridge.edge.mountpoint = bridge/edge/ + +## Ping interval of a down bridge. +## +## Value: Duration +## Default: 10 seconds +bridge.edge.keepalive = 10s + +## Subscriptions of the bridge topic. +## +## Value: String +bridge.edge.subscription.1.topic = # + +## Subscriptions of the bridge qos. +## +## Value: Number +bridge.edge.subscription.1.qos = 1 + +## The pending message queue of a bridge. +## +## Value: Number +bridge.edge.max_pending_messages = 10000 + +## Start type of the bridge. +## +## Value: enum +## manual +## auto +bridge.edge.start_type = manual + +## Bridge reconnect count. +## +## Value: Number +bridge.edge.reconnect_count = 10 + +## Bridge reconnect time. +## +## Value: Duration +## Default: 30 seconds +bridge.edge.reconnect_time = 30s + +## PEM-encoded CA certificates of the bridge. +## +## Value: File +## bridge.edge.cacertfile = cacert.pem + +## SSL Certfile of the bridge. +## +## Value: File +## bridge.edge.certfile = cert.pem + +## SSL Keyfile of the bridge. +## +## Value: File +## bridge.edge.keyfile = key.pem + +## SSL Ciphers used by the bridge. +## +## Value: String +## bridge.edge.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 + +## TLS versions used by the bridge. +## +## Value: String +## bridge.edge.tls_versions = tlsv1.2,tlsv1.1,tlsv1 + + +##-------------------------------------------------------------------- +## Bridges to cloud +##-------------------------------------------------------------------- +## Bridge type. +## +## Value: Enum +## Example: out | in +bridge.cloud.type = out + +## Bridge address: node name for local bridge, host:port for remote. +## +## Value: String +## Example: emqx@127.0.0.1, 127.0.0.1:1883 +bridge.cloud.address = 127.0.0.1:1883 + +## Protocol version of the bridge. +## +## Value: Enum +## - mqtt5 +## - mqtt4 +## - mqtt3 +bridge.cloud.proto_ver = mqtt4 + +## The ClientId of a remote bridge. +## +## Value: String +bridge.cloud.client_id = bridge_cloud + +## The Clean start flag of a remote bridge. +## +## Value: boolean +bridge.cloud.clean_start = false + +## The username for a remote bridge. +## +## Value: String +bridge.cloud.username = user + +## The password for a remote bridge. +## +## Value: String +bridge.cloud.password = passwd + +## Mountpoint of the bridge. +## +## Value: String +bridge.cloud.mountpoint = bridge/edge/${node}/ + +## Ping interval of a down bridge. +## +## Value: Duration +## Default: 10 seconds +bridge.cloud.keepalive = 10s + +## Forward message topics +## +## Value: String +## Example: topic1/#,topic2/# +bridge.cloud.forward_rule = # + +## Subscriptions of the bridge topic. +## +## Value: String +bridge.cloud.subscription.1.topic = $share/cmd/topic1 + +## Subscriptions of the bridge qos. +## +## Value: Number +bridge.cloud.subscription.1.qos = 1 + +## Bridge store message type. +## +## Value: Enum +## Example: memory | disk +bridge.cloud.store_type = memory + +## The pending message queue of a bridge. +## +## Value: Number +bridge.cloud.max_pending_messages = 10000 + +## Start type of the bridge. +## +## Value: enum +## manual +## auto +bridge.cloud.start_type = manual + +## Bridge reconnect count. +## +## Value: Number +bridge.cloud.reconnect_count = 10 + +## Bridge reconnect time. +## +## Value: Duration +## Default: 30 seconds +bridge.cloud.reconnect_time = 30s + +## PEM-encoded CA certificates of the bridge. +## +## Value: File +## bridge.cloud.cacertfile = cacert.pem + +## SSL Certfile of the bridge. +## +## Value: File +## bridge.cloud.certfile = cert.pem + +## SSL Keyfile of the bridge. +## +## Value: File +## bridge.cloud.keyfile = key.pem + +## SSL Ciphers used by the bridge. +## +## Value: String +## bridge.cloud.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 + +## TLS versions used by the bridge. +## +## Value: String +## bridge.cloud.tls_versions = tlsv1.2,tlsv1.1,tlsv1 + +##-------------------------------------------------------------------- +## Modules +##-------------------------------------------------------------------- + +##-------------------------------------------------------------------- +## Presence Module + +## Enable Presence Module. +## +## Value: on | off +module.presence = on + +## Sets the QoS for presence MQTT message. +## +## Value: 0 | 1 | 2 +module.presence.qos = 1 + +##-------------------------------------------------------------------- +## Subscription Module + +## Enable Subscription Module. +## +## Value: on | off +module.subscription = off + +## Subscribe the Topics automatically when client connected. +## module.subscription.1.topic = $client/%c +## Qos of the subscription: 0 | 1 | 2 +## module.subscription.1.qos = 1 + +## module.subscription.2.topic = $user/%u +## module.subscription.2.qos = 1 + +##-------------------------------------------------------------------- +## Rewrite Module + +## Enable Rewrite Module. +## +## Value: on | off +module.rewrite = off + +## {rewrite, Topic, Re, Dest} +## module.rewrite.rule.1 = x/# ^x/y/(.+)$ z/y/$1 +## module.rewrite.rule.2 = y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2 ##------------------------------------------------------------------- -## System Monitor +## Plugins ##------------------------------------------------------------------- +## The etc dir for plugins' config. +## +## Value: Folder +plugins.etc_dir ={{ platform_etc_dir }}/plugins/ + +## The file to store loaded plugin names. +## +## Value: File +plugins.loaded_file = {{ platform_data_dir }}/loaded_plugins + +## File to store loaded plugin names. +plugins.expand_plugins_dir = {{ platform_plugins_dir }}/ + +##-------------------------------------------------------------------- +## Broker +##-------------------------------------------------------------------- + +## System interval of publishing $SYS messages. +## +## Value: Duration +## Default: 1m, 1 minute +broker.sys_interval = 1m + +## Session locking strategy in a cluster. +## +## Value: Enum +## - local +## - one +## - quorum +## - all +broker.session_locking_strategy = quorum + +## Dispatch strategy for shared subscription +## +## Value: Enum +## - random +## - round_robbin +## - hash +broker.shared_subscription_strategy = random + +## Enable batch clean for deleted routes. +## +## Value: Flag +broker.route_batch_clean = on + +##-------------------------------------------------------------------- +## System Monitor +##-------------------------------------------------------------------- + ## Enable Long GC monitoring. ## Notice: don't enable the monitor in production for: ## https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 diff --git a/include/emqx.hrl b/include/emqx.hrl index f0b2b46d1..3b42713ff 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -24,42 +24,51 @@ -define(ERTS_MINIMUM_REQUIRED, "10.0"). +%%-------------------------------------------------------------------- +%% PubSub +%%-------------------------------------------------------------------- + +-type(pubsub() :: publish | subscribe). + +-define(PS(I), (I =:= publish orelse I =:= subscribe)). + %%-------------------------------------------------------------------- %% Topics' prefix: $SYS | $queue | $share %%-------------------------------------------------------------------- -%% System Topic +%% System topic -define(SYSTOP, <<"$SYS/">>). -%% Queue Topic +%% Queue topic -define(QUEUE, <<"$queue/">>). -%% Shared Topic +%% Shared topic -define(SHARE, <<"$share/">>). %%-------------------------------------------------------------------- %% Topic, subscription and subscriber %%-------------------------------------------------------------------- --type(qos() :: integer()). - -type(topic() :: binary()). --type(suboption() :: {qos, qos()} - | {share, '$queue'} - | {share, binary()} - | {atom(), term()}). +-type(subid() :: binary() | atom()). --record(subscription, {subid :: binary() | atom(), - topic :: topic(), - subopts :: list(suboption())}). +-type(subopts() :: #{qos => integer(), share => '$queue' | binary(), atom() => term()}). + +-record(subscription, { + topic :: topic(), + subid :: subid(), + subopts :: subopts() + }). -type(subscription() :: #subscription{}). --type(subscriber() :: binary() | pid() | {binary(), pid()}). +-type(subscriber() :: {pid(), subid()}). + +-type(topic_table() :: [{topic(), subopts()}]). %%-------------------------------------------------------------------- -%% Client and session +%% Client and Session %%-------------------------------------------------------------------- -type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). @@ -70,18 +79,19 @@ -type(username() :: binary() | atom()). --type(mountpoint() :: binary()). +-type(zone() :: atom()). --type(zone() :: undefined | atom()). - --record(client, {id :: client_id(), - pid :: pid(), - zone :: zone(), - peername :: peername(), - username :: username(), - protocol :: protocol(), - attributes :: #{atom() => term()}, - connected_at :: erlang:timestamp()}). +-record(client, { + id :: client_id(), + pid :: pid(), + zone :: zone(), + protocol :: protocol(), + peername :: peername(), + peercert :: nossl | binary(), + username :: username(), + clean_start :: boolean(), + attributes :: map() + }). -type(client() :: #client{}). @@ -90,63 +100,53 @@ -type(session() :: #session{}). %%-------------------------------------------------------------------- -%% Message and delivery +%% Payload, Message and Delivery %%-------------------------------------------------------------------- --type(message_id() :: binary() | undefined). +-type(qos() :: integer()). --type(message_flag() :: sys | qos | dup | retain | atom()). +-type(payload() :: binary() | iodata()). --type(message_flags() :: #{message_flag() => boolean() | integer()}). - --type(message_headers() :: #{protocol => protocol(), - packet_id => pos_integer(), - priority => non_neg_integer(), - ttl => pos_integer(), - atom() => term()}). - --type(payload() :: binary()). +-type(message_flag() :: dup | sys | retain | atom()). %% See 'Application Message' in MQTT Version 5.0 --record(message, - { id :: message_id(), %% Message guid - qos :: qos(), %% Message qos - from :: atom() | client(), %% Message from - sender :: pid(), %% The pid of the sender/publisher - flags :: message_flags(), %% Message flags - headers :: message_headers(), %% Message headers - topic :: topic(), %% Message topic - properties :: map(), %% Message user properties - payload :: payload(), %% Message payload - timestamp :: erlang:timestamp() %% Timestamp +-record(message, { + %% Global unique message ID + id :: binary() | pos_integer(), + %% Message QoS + qos = 0 :: qos(), + %% Message from + from :: atom() | client_id(), + %% Message flags + flags :: #{message_flag() => boolean()}, + %% Message headers, or MQTT 5.0 Properties + headers = #{} :: map(), + %% Topic that the message is published to + topic :: topic(), + %% Message Payload + payload :: binary(), + %% Timestamp + timestamp :: erlang:timestamp() }). -type(message() :: #message{}). --record(delivery, - { node :: node(), %% The node that created the delivery +-record(delivery, { + sender :: pid(), %% Sender of the delivery message :: message(), %% The message delivered - flows :: list() %% The message flow path + flows :: list() %% The dispatch path of message }). -type(delivery() :: #delivery{}). -%%-------------------------------------------------------------------- -%% PubSub -%%-------------------------------------------------------------------- - --type(pubsub() :: publish | subscribe). - --define(PS(I), (I =:= publish orelse I =:= subscribe)). - %%-------------------------------------------------------------------- %% Route %%-------------------------------------------------------------------- --record(route, - { topic :: topic(), - dest :: node() | {binary(), node()} - }). +-record(route, { + topic :: topic(), + dest :: node() | {binary(), node()} + }). -type(route() :: #route{}). @@ -156,20 +156,20 @@ -type(trie_node_id() :: binary() | atom()). --record(trie_node, - { node_id :: trie_node_id(), +-record(trie_node, { + node_id :: trie_node_id(), edge_count = 0 :: non_neg_integer(), topic :: topic() | undefined, flags :: list(atom()) }). --record(trie_edge, - { node_id :: trie_node_id(), +-record(trie_edge, { + node_id :: trie_node_id(), word :: binary() | atom() }). --record(trie, - { edge :: #trie_edge{}, +-record(trie, { + edge :: #trie_edge{}, node_id :: trie_node_id() }). @@ -177,11 +177,11 @@ %% Alarm %%-------------------------------------------------------------------- --record(alarm, - { id :: binary(), +-record(alarm, { + id :: binary(), severity :: notice | warning | error | critical, - title :: iolist() | binary(), - summary :: iolist() | binary(), + title :: iolist(), + summary :: iolist(), timestamp :: erlang:timestamp() }). @@ -191,14 +191,14 @@ %% Plugin %%-------------------------------------------------------------------- --record(plugin, - { name :: atom(), - version :: string(), - dir :: string(), - descr :: string(), - vendor :: string(), - active :: boolean(), - info :: map() +-record(plugin, { + name :: atom(), + version :: string(), + dir :: string(), + descr :: string(), + vendor :: string(), + active = false :: boolean(), + info :: map() }). -type(plugin() :: #plugin{}). @@ -207,8 +207,8 @@ %% Command %%-------------------------------------------------------------------- --record(command, - { name :: atom(), +-record(command, { + name :: atom(), action :: atom(), args = [] :: list(), opts = [] :: list(), diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index f5bb13604..007de4dd1 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -78,18 +78,17 @@ %% Maximum ClientId Length. %%-------------------------------------------------------------------- --define(MAX_CLIENTID_LEN, 1024). +-define(MAX_CLIENTID_LEN, 65535). %%-------------------------------------------------------------------- %% MQTT Client %%-------------------------------------------------------------------- - --record(mqtt_client, - { client_id :: binary() | undefined, +-record(mqtt_client, { + client_id :: binary() | undefined, client_pid :: pid(), username :: binary() | undefined, peername :: {inet:ip_address(), inet:port_number()}, - clean_sess :: boolean(), + clean_start :: boolean(), proto_ver :: mqtt_version(), keepalive = 0 :: non_neg_integer(), will_topic :: undefined | binary(), @@ -207,8 +206,8 @@ %% MQTT Packet Fixed Header %%-------------------------------------------------------------------- --record(mqtt_packet_header, - { type = ?RESERVED :: mqtt_packet_type(), +-record(mqtt_packet_header, { + type = ?RESERVED :: mqtt_packet_type(), dup = false :: boolean(), qos = ?QOS_0 :: mqtt_qos(), retain = false :: boolean() @@ -235,8 +234,8 @@ -type(mqtt_subopts() :: #mqtt_subopts{}). --record(mqtt_packet_connect, - { proto_name = <<"MQTT">> :: binary(), +-record(mqtt_packet_connect, { + proto_name = <<"MQTT">> :: binary(), proto_ver = ?MQTT_PROTO_V4 :: mqtt_version(), is_bridge = false :: boolean(), clean_start = true :: boolean(), @@ -253,55 +252,55 @@ password = undefined :: undefined | binary() }). --record(mqtt_packet_connack, - { ack_flags :: 0 | 1, +-record(mqtt_packet_connack, { + ack_flags :: 0 | 1, reason_code :: mqtt_reason_code(), properties :: mqtt_properties() }). --record(mqtt_packet_publish, - { topic_name :: mqtt_topic(), +-record(mqtt_packet_publish, { + topic_name :: mqtt_topic(), packet_id :: mqtt_packet_id(), properties :: mqtt_properties() }). --record(mqtt_packet_puback, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_puback, { + packet_id :: mqtt_packet_id(), reason_code :: mqtt_reason_code(), properties :: mqtt_properties() }). --record(mqtt_packet_subscribe, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_subscribe, { + packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), topic_filters :: [{mqtt_topic(), mqtt_subopts()}] }). --record(mqtt_packet_suback, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_suback, { + packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), reason_codes :: list(mqtt_reason_code()) }). --record(mqtt_packet_unsubscribe, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_unsubscribe, { + packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), topic_filters :: [mqtt_topic()] }). --record(mqtt_packet_unsuback, - { packet_id :: mqtt_packet_id(), +-record(mqtt_packet_unsuback, { + packet_id :: mqtt_packet_id(), properties :: mqtt_properties(), reason_codes :: list(mqtt_reason_code()) }). --record(mqtt_packet_disconnect, - { reason_code :: mqtt_reason_code(), +-record(mqtt_packet_disconnect, { + reason_code :: mqtt_reason_code(), properties :: mqtt_properties() }). --record(mqtt_packet_auth, - { reason_code :: mqtt_reason_code(), +-record(mqtt_packet_auth, { + reason_code :: mqtt_reason_code(), properties :: mqtt_properties() }). @@ -309,8 +308,8 @@ %% MQTT Control Packet %%-------------------------------------------------------------------- --record(mqtt_packet, - { header :: #mqtt_packet_header{}, +-record(mqtt_packet, { + header :: #mqtt_packet_header{}, variable :: #mqtt_packet_connect{} | #mqtt_packet_connack{} | #mqtt_packet_publish{} @@ -364,9 +363,12 @@ variable = #mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}}). --define(PUBLISH_PACKET(Qos, PacketId), +-define(PUBLISH_PACKET(QoS), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}}). + +-define(PUBLISH_PACKET(QoS, PacketId), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos}, + qos = QoS}, variable = #mqtt_packet_publish{packet_id = PacketId}}). -define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), @@ -396,7 +398,7 @@ properties = Properties}}). -define(PUBREC_PACKET(PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). @@ -464,6 +466,11 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, variable = #mqtt_packet_unsuback{packet_id = PacketId}}). +-define(UNSUBACK_PACKET(PacketId, ReasonCodes), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, + variable = #mqtt_packet_unsuback{packet_id = PacketId, + reason_codes = ReasonCodes}}). + -define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, variable = #mqtt_packet_unsuback{packet_id = PacketId, @@ -486,43 +493,3 @@ -define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). -%%-------------------------------------------------------------------- -%% MQTT Message -%%-------------------------------------------------------------------- - --type(mqtt_msg_id() :: binary() | undefined). - --type(mqtt_msg_from() :: atom() | {binary(), undefined | binary()}). - --record(mqtt_message, - { %% Global unique message ID - id :: mqtt_msg_id(), - %% PacketId - packet_id :: mqtt_packet_id(), - %% ClientId and Username - from :: mqtt_msg_from(), - %% Topic that the message is published to - topic :: binary(), - %% Message QoS - qos = ?QOS0 :: mqtt_qos(), - %% Message Flags - flags = [] :: [retain | dup | sys], - %% Retain flag - retain = false :: boolean(), - %% Dup flag - dup = false :: boolean(), - %% $SYS flag - sys = false :: boolean(), - %% Properties - properties = [] :: list(), - %% Payload - payload :: binary(), - %% Timestamp - timestamp :: erlang:timestamp() - }). - --type(mqtt_message() :: #mqtt_message{}). - --define(WILL_MSG(Qos, Retain, Topic, Props, Payload), - #mqtt_message{qos = Qos, retain = Retain, topic = Topic, properties = Props, payload = Payload}). - diff --git a/priv/emqx.schema b/priv/emqx.schema index b4187397a..1626b645f 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -165,10 +165,10 @@ end}. %%-------------------------------------------------------------------- -%% Erlang Node +%% Node %%-------------------------------------------------------------------- -%% @doc Erlang node name +%% @doc Node name {mapping, "node.name", "vm_args.-name", [ {default, "emqx@127.0.0.1"} ]}. @@ -321,7 +321,7 @@ end}. ]}. %%-------------------------------------------------------------------- -%% RPC Args +%% RPC %%-------------------------------------------------------------------- %% RPC server port. @@ -442,7 +442,7 @@ end}. ]}. {mapping, "log.syslog", "lager.handlers", [ - {default, off}, + %%{default, off}, {datatype, flag} ]}. @@ -456,10 +456,10 @@ end}. {datatype, {enum, [daemon, local0, local1, local2, local3, local4, local5, local6, local7]}} ]}. -{mapping, "log.syslog.level", "lager.handlers", [ - {default, error}, - {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency]}} -]}. +%%{mapping, "log.syslog.level", "lager.handlers", [ +%% {default, error}, +%% {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency]}} +%%]}. {mapping, "log.error.redirect", "lager.error_logger_redirect", [ {default, on}, @@ -497,7 +497,7 @@ end}. ConsoleLogLevel = cuttlefish:conf_get("log.console.level", Conf), ConsoleLogFile = cuttlefish:conf_get("log.console.file", Conf), - ConsoleHandler = {lager_console_backend, [ConsoleLogLevel]}, + ConsoleHandler = {lager_console_backend, [{level, ConsoleLogLevel}]}, ConsoleFileHandler = {lager_file_backend, [{file, ConsoleLogFile}, {level, ConsoleLogLevel}, {size, cuttlefish:conf_get("log.console.size", Conf)}, @@ -511,13 +511,14 @@ end}. both -> [ConsoleHandler, ConsoleFileHandler]; _ -> [] end, - SyslogHandler = case cuttlefish:conf_get("log.syslog", Conf) of - false -> []; - true -> [{lager_syslog_backend, - [cuttlefish:conf_get("log.syslog.identity", Conf), - cuttlefish:conf_get("log.syslog.facility", Conf), - cuttlefish:conf_get("log.syslog.level", Conf)]}] - end, + SyslogHandler = [], + %%case cuttlefish:conf_get("log.syslog", Conf, false) of + %% false -> []; + %% true -> [{lager_syslog_backend, + %% [cuttlefish:conf_get("log.syslog.identity", Conf), + %% cuttlefish:conf_get("log.syslog.facility", Conf), + %% cuttlefish:conf_get("log.syslog.level", Conf)]}] + %%end, ConsoleHandlers ++ ErrorHandler ++ InfoHandler ++ SyslogHandler end }. @@ -549,368 +550,92 @@ end}. ]}. %%-------------------------------------------------------------------- -%% Allow Anonymous and Default ACL +%% Authentication/ACL %%-------------------------------------------------------------------- -%% @doc Allow Anonymous -{mapping, "mqtt.allow_anonymous", "emqx.allow_anonymous", [ +%% @doc Allow anonymous authentication. +{mapping, "allow_anonymous", "emqx.allow_anonymous", [ {default, false}, {datatype, {enum, [true, false]}} ]}. -%% @doc ACL nomatch -{mapping, "mqtt.acl_nomatch", "emqx.acl_nomatch", [ - {default, allow}, - {datatype, {enum, [allow, deny]}} -]}. - -%% @doc Default ACL File -{mapping, "mqtt.acl_file", "emqx.acl_file", [ +%% @doc Default ACL file. +{mapping, "acl_file", "emqx.acl_file", [ {datatype, string}, hidden ]}. -%% @doc Cache ACL for PUBLISH -{mapping, "mqtt.cache_acl", "emqx.cache_acl", [ - {default, true}, - {datatype, {enum, [true, false]}} +%% @doc Enable ACL cache for publish. +{mapping, "enable_acl_cache", "emqx.enable_acl_cache", [ + {default, on}, + {datatype, flag} ]}. -%%-------------------------------------------------------------------- -%% MQTT Protocol -%%-------------------------------------------------------------------- - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "mqtt.max_clientid_len", "emqx.protocol", [ - {default, 1024}, - {datatype, integer} +%% @doc ACL cache age. +{mapping, "acl_cache_age", "emqx.acl_cache_age", [ + {default, "5m"}, + {datatype, {duration, ms}} ]}. -%% @doc Max Packet Size Allowed, 64K by default. -{mapping, "mqtt.max_packet_size", "emqx.protocol", [ - {default, "64KB"}, +%% @doc ACL cache size. +%% {mapping, "acl_cache_size", "emqx.acl_cache_size", [ +%% {default, 0}, +%% {datatype, integer} +%% ]}. + +%%-------------------------------------------------------------------- +%% MQTT +%%-------------------------------------------------------------------- + +%% @doc Max Packet Size Allowed, 1MB by default. +{mapping, "mqtt.max_packet_size", "emqx.max_packet_size", [ + {default, "1MB"}, {datatype, bytesize} ]}. -%% @doc Keepalive backoff -{mapping, "mqtt.keepalive_backoff", "emqx.protocol", [ - {default, 1.25}, - {datatype, float} -]}. - -{translation, "emqx.protocol", fun(Conf) -> - [{max_clientid_len, cuttlefish:conf_get("mqtt.max_clientid_len", Conf)}, - {max_packet_size, cuttlefish:conf_get("mqtt.max_packet_size", Conf)}, - {keepalive_backoff, cuttlefish:conf_get("mqtt.keepalive_backoff", Conf)}] -end}. - -{mapping, "mqtt.websocket_protocol_header", "emqx.websocket_protocol_header", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "mqtt.websocket_check_upgrade_header", "emqx.websocket_check_upgrade_header", [ - {default, on}, - {datatype, flag} -]}. - -%%-------------------------------------------------------------------- -%% MQTT Connection -%%-------------------------------------------------------------------- - -%% @doc Force the client to GC: integer -{mapping, "mqtt.conn.force_gc_count", "emqx.conn_force_gc_count", [ +%% @doc Set the Max ClientId Length Allowed. +{mapping, "mqtt.max_clientid_len", "emqx.max_clientid_len", [ + {default, 65535}, {datatype, integer} ]}. -%%-------------------------------------------------------------------- -%% MQTT Client -%%-------------------------------------------------------------------- - -%% @doc Max Publish Rate of Message -{mapping, "mqtt.client.max_publish_rate", "emqx.client", [ +%% @doc Set the Maximum topic levels. +{mapping, "mqtt.max_topic_levels", "emqx.max_topic_levels", [ {default, 0}, {datatype, integer} ]}. -%% @doc Client Idle Timeout. -{mapping, "mqtt.client.idle_timeout", "emqx.client", [ - {default, "30s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Enable Stats of Client. -{mapping, "mqtt.client.enable_stats", "emqx.client", [ - {default, off}, - {datatype, flag} -]}. - -{translation, "emqx.client", fun(Conf) -> - [{max_publish_rate, cuttlefish:conf_get("mqtt.client.max_publish_rate", Conf)}, - {client_idle_timeout, cuttlefish:conf_get("mqtt.client.idle_timeout", Conf)}, - {client_enable_stats, cuttlefish:conf_get("mqtt.client.enable_stats", Conf)}] -end}. - -%%-------------------------------------------------------------------- -%% MQTT Session -%%-------------------------------------------------------------------- - -%% @doc Max Number of Subscriptions Allowed -{mapping, "mqtt.session.max_subscriptions", "emqx.session", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Upgrade QoS? -{mapping, "mqtt.session.upgrade_qos", "emqx.session", [ - {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, "mqtt.session.max_inflight", "emqx.session", [ - {default, 100}, - {datatype, integer} -]}. - -%% @doc Retry interval for redelivering QoS1/2 messages. -{mapping, "mqtt.session.retry_interval", "emqx.session", [ - {default, "20s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Max Packets that Awaiting PUBREL, 0 means no limit -{mapping, "mqtt.session.max_awaiting_rel", "emqx.session", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Awaiting PUBREL Timeout -{mapping, "mqtt.session.await_rel_timeout", "emqx.session", [ - {default, "20s"}, - {datatype, {duration, ms}} -]}. - -%% @doc Enable Stats -{mapping, "mqtt.session.enable_stats", "emqx.session", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Session Expiry Interval -{mapping, "mqtt.session.expiry_interval", "emqx.session", [ - {default, "2h"}, - {datatype, {duration, ms}} -]}. - -%% @doc Ignore message from self publish -{mapping, "mqtt.session.ignore_loop_deliver", "emqx.session", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.session", fun(Conf) -> - [{max_subscriptions, cuttlefish:conf_get("mqtt.session.max_subscriptions", Conf)}, - {upgrade_qos, cuttlefish:conf_get("mqtt.session.upgrade_qos", Conf)}, - {max_inflight, cuttlefish:conf_get("mqtt.session.max_inflight", Conf)}, - {retry_interval, cuttlefish:conf_get("mqtt.session.retry_interval", Conf)}, - {max_awaiting_rel, cuttlefish:conf_get("mqtt.session.max_awaiting_rel", Conf)}, - {await_rel_timeout, cuttlefish:conf_get("mqtt.session.await_rel_timeout", Conf)}, - {enable_stats, cuttlefish:conf_get("mqtt.session.enable_stats", Conf)}, - {expiry_interval, cuttlefish:conf_get("mqtt.session.expiry_interval", Conf)}, - {ignore_loop_deliver, cuttlefish:conf_get("mqtt.session.ignore_loop_deliver", Conf)}] -end}. - -%%-------------------------------------------------------------------- -%% MQTT MQueue -%%-------------------------------------------------------------------- - -%% @doc Type: simple | priority -{mapping, "mqtt.mqueue.type", "emqx.mqueue", [ - {default, simple}, - {datatype, atom} -]}. - -%% @doc Topic Priority: 0~255, Default is 0 -{mapping, "mqtt.mqueue.priority", "emqx.mqueue", [ - {default, ""}, - {datatype, string} -]}. - -%% @doc Max queue length. Enqueued messages when persistent client disconnected, or inflight window is full. 0 means no limit. -{mapping, "mqtt.mqueue.max_length", "emqx.mqueue", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Low-water mark of queued messages -{mapping, "mqtt.mqueue.low_watermark", "emqx.mqueue", [ - {default, "20%"}, - {datatype, {percent, float}} -]}. - -%% @doc High-water mark of queued messages -{mapping, "mqtt.mqueue.high_watermark", "emqx.mqueue", [ - {default, "60%"}, - {datatype, {percent, float}} -]}. - -%% @doc Queue Qos0 messages? -{mapping, "mqtt.mqueue.store_qos0", "emqx.mqueue", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.mqueue", fun(Conf) -> - Opts = [{type, cuttlefish:conf_get("mqtt.mqueue.type", Conf, simple)}, - {max_length, cuttlefish:conf_get("mqtt.mqueue.max_length", Conf)}, - {low_watermark, cuttlefish:conf_get("mqtt.mqueue.low_watermark", Conf)}, - {high_watermark, cuttlefish:conf_get("mqtt.mqueue.high_watermark", Conf)}, - {store_qos0, cuttlefish:conf_get("mqtt.mqueue.store_qos0", Conf)}], - case cuttlefish:conf_get("mqtt.mqueue.priority", Conf) of - undefined -> Opts; - V -> [{priority, - [begin [T, P] = string:tokens(S, "="), - {T, list_to_integer(P)} - end || S <- string:tokens(V, ",")]} | Opts] - end -end}. - -%%-------------------------------------------------------------------- -%% MQTT Broker -%%-------------------------------------------------------------------- - -{mapping, "mqtt.broker.sys_interval", "emqx.broker_sys_interval", [ - {datatype, {duration, ms}}, - {default, "1m"} -]}. - -%%-------------------------------------------------------------------- -%% MQTT PubSub -%%-------------------------------------------------------------------- - -{mapping, "mqtt.pubsub.pool_size", "emqx.pubsub", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "mqtt.pubsub.async", "emqx.pubsub", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.pubsub", fun(Conf) -> - [{pool_size, cuttlefish:conf_get("mqtt.pubsub.pool_size", Conf)}, - {async, cuttlefish:conf_get("mqtt.pubsub.async", Conf)}] -end}. - -%%-------------------------------------------------------------------- -%% MQTT Bridge -%%-------------------------------------------------------------------- - -{mapping, "mqtt.bridge.max_queue_len", "emqx.bridge", [ - {default, 10000}, - {datatype, integer} -]}. - -{mapping, "mqtt.bridge.ping_down_interval", "emqx.bridge", [ - {datatype, {duration, ms}}, - {default, "1s"} -]}. - -{translation, "emqx.bridge", fun(Conf) -> - [{max_queue_len, cuttlefish:conf_get("mqtt.bridge.max_queue_len", Conf)}, - {ping_down_interval, cuttlefish:conf_get("mqtt.bridge.ping_down_interval", Conf)}] -end}. - -%%------------------------------------------------------------------- -%% Plugins -%%------------------------------------------------------------------- - -{mapping, "mqtt.plugins.etc_dir", "emqx.plugins_etc_dir", [ - {datatype, string} -]}. - -{mapping, "mqtt.plugins.loaded_file", "emqx.plugins_loaded_file", [ - {datatype, string} -]}. - -{mapping, "mqtt.plugins.expand_plugins_dir", "emqx.expand_plugins_dir", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Modules -%%-------------------------------------------------------------------- - -{mapping, "module.presence", "emqx.modules", [ - {default, off}, - {datatype, flag} -]}. - -{mapping, "module.presence.qos", "emqx.modules", [ - {default, 1}, +%% @doc Set the Maximum QoS allowed. +{mapping, "mqtt.max_qos_allowed", "emqx.max_qos_allowed", [ + {default, 2}, {datatype, integer}, {validators, ["range:0-2"]} ]}. -{mapping, "module.subscription", "emqx.modules", [ - {default, off}, - {datatype, flag} +%% @doc Set the Maximum topic alias. +{mapping, "mqtt.max_topic_alias", "emqx.max_topic_alias", [ + {default, 0}, + {datatype, integer} ]}. -{mapping, "module.subscription.$id.topic", "emqx.modules", [ - {datatype, string} +%% @doc Whether the server supports MQTT retained messages. +{mapping, "mqtt.retain_available", "emqx.mqtt_retain_available", [ + {default, true}, + {datatype, {enum, [true, false]}} ]}. -{mapping, "module.subscription.$id.qos", "emqx.modules", [ - {default, 1}, - {datatype, integer}, - {validators, ["range:0-2"]} +%% @doc Whether the Server supports MQTT Wildcard Subscriptions. +{mapping, "mqtt.wildcard_subscription", "emqx.mqtt_wildcard_subscription", [ + {default, true}, + {datatype, {enum, [true, false]}} ]}. -{mapping, "module.rewrite", "emqx.modules", [ - {default, off}, - {datatype, flag} +%% @doc Whether the Server supports MQTT Shared Subscriptions. +{mapping, "mqtt.shared_subscription", "emqx.mqtt_shared_subscription", [ + {default, true}, + {datatype, {enum, [true, false]}} ]}. -{mapping, "module.rewrite.rule.$id", "emqx.modules", [ - {datatype, string} -]}. - -{translation, "emqx.modules", fun(Conf) -> - Subscriptions = fun() -> - List = cuttlefish_variable:filter_by_prefix("module.subscription", Conf), - QosList = [Qos || {_, Qos} <- lists:sort([{I, Qos} || {[_,"subscription", I,"qos"], Qos} <- List])], - TopicList = [iolist_to_binary(Topic) || {_, Topic} <- - lists:sort([{I, Topic} || {[_,"subscription", I, "topic"], Topic} <- List])], - lists:zip(TopicList, QosList) - end, - Rewrites = fun() -> - Rules = cuttlefish_variable:filter_by_prefix("module.rewrite.rule", Conf), - lists:map(fun({[_, "rewrite", "rule", I], Rule}) -> - [Topic, Re, Dest] = string:tokens(Rule, " "), - {rewrite, list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} - end, Rules) - end, - lists:append([ - case cuttlefish:conf_get("module.presence", Conf) of %% Presence - true -> [{emqx_mod_presence, [{qos, cuttlefish:conf_get("module.presence.qos", Conf, 1)}]}]; - false -> [] - end, - case cuttlefish:conf_get("module.subscription", Conf) of %% Subscription - true -> [{emqx_mod_subscription, Subscriptions()}]; - false -> [] - end, - case cuttlefish:conf_get("module.rewrite", Conf) of %% Rewrite - true -> [{emqx_mod_rewrite, Rewrites()}]; - false -> [] - end - ]) -end}. - - %%-------------------------------------------------------------------- %% Listeners %%-------------------------------------------------------------------- @@ -927,11 +652,15 @@ end}. {datatype, integer} ]}. -{mapping, "listener.tcp.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.tcp.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. +{mapping, "listener.tcp.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.tcp.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -958,7 +687,8 @@ end}. ]}. {mapping, "listener.tcp.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn]}} + {default, false}, + {datatype, {enum, [true, false]}} ]}. {mapping, "listener.tcp.$name.backlog", "emqx.listeners", [ @@ -1018,11 +748,15 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ssl.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.ssl.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. +{mapping, "listener.ssl.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.ssl.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1159,13 +893,13 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ws.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.ws.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. -{mapping, "listener.ws.$name.rate_limit", "emqx.listeners", [ - {datatype, string} +{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} ]}. {mapping, "listener.ws.$name.zone", "emqx.listeners", [ @@ -1176,10 +910,20 @@ end}. {datatype, string} ]}. +{mapping, "listener.ws.$name.rate_limit", "emqx.listeners", [ + {default, undefined}, + {datatype, string} +]}. + {mapping, "listener.ws.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. +{mapping, "listener.ws.$name.verify_protocol_header", "emqx.listeners", [ + {default, on}, + {datatype, flag} +]}. + {mapping, "listener.ws.$name.proxy_address_header", "emqx.listeners", [ {datatype, string}, hidden @@ -1238,11 +982,6 @@ end}. hidden ]}. -{mapping, "listener.ws.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - %%-------------------------------------------------------------------- %% MQTT/WebSocket/SSL Listeners @@ -1255,11 +994,15 @@ end}. {datatype, integer} ]}. -{mapping, "listener.wss.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.wss.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. +{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1272,6 +1015,11 @@ end}. {datatype, string} ]}. +{mapping, "listener.wss.$name.verify_protocol_header", "emqx.listeners", [ + {default, on}, + {datatype, flag} +]}. + {mapping, "listener.wss.$name.access.$id", "emqx.listeners", [ {datatype, string} ]}. @@ -1294,6 +1042,11 @@ end}. {datatype, {duration, ms}} ]}. +%%{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ +%% {default, "15s"}, +%% {datatype, {duration, ms}} +%%]}. + {mapping, "listener.wss.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} @@ -1334,11 +1087,6 @@ end}. hidden ]}. -{mapping, "listener.wss.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - {mapping, "listener.wss.$name.tls_versions", "emqx.listeners", [ {datatype, string} ]}. @@ -1347,11 +1095,6 @@ end}. {datatype, string} ]}. -{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - {mapping, "listener.wss.$name.keyfile", "emqx.listeners", [ {datatype, string} ]}. @@ -1403,27 +1146,32 @@ end}. AccOpts = fun(Prefix) -> case cuttlefish_variable:filter_by_prefix(Prefix ++ ".access", Conf) of [] -> []; - Rules -> [{access, [Access(Rule) || {_, Rule} <- Rules]}] + Rules -> [{access_rules, [Access(Rule) || {_, Rule} <- Rules]}] end end, MountPoint = fun(undefined) -> undefined; (S) -> list_to_binary(S) end, - ConnOpts = fun(Prefix) -> - Filter([{zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, - {rate_limit, cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined)}, - {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, - {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, - {mountpoint, MountPoint(cuttlefish:conf_get(Prefix ++ ".mountpoint", Conf, undefined))}, - {peer_cert_as_username, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_username", Conf, undefined)}, - {proxy_port_header, cuttlefish:conf_get(Prefix ++ ".proxy_port_header", Conf, undefined)}, - {proxy_address_header, cuttlefish:conf_get(Prefix ++ ".proxy_address_header", Conf, undefined)}]) - end, + Ratelimit = fun(undefined) -> + undefined; + (S) -> + list_to_tuple([list_to_integer(Token) || Token <- string:tokens(S, ",")]) + end, LisOpts = fun(Prefix) -> Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, - {max_clients, cuttlefish:conf_get(Prefix ++ ".max_clients", Conf)}, - {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)} | AccOpts(Prefix)]) + {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, + {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, + {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, + {zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, + {rate_limit, Ratelimit(cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined))}, + {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, + {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, + {mountpoint, MountPoint(cuttlefish:conf_get(Prefix ++ ".mountpoint", Conf, undefined))}, + {verify_protocol_header, cuttlefish:conf_get(Prefix ++ ".verify_protocol_header", Conf, undefined)}, + {peer_cert_as_username, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_username", Conf, undefined)}, + {proxy_port_header, cuttlefish:conf_get(Prefix ++ ".proxy_port_header", Conf, undefined)}, + {proxy_address_header, cuttlefish:conf_get(Prefix ++ ".proxy_address_header", Conf, undefined)} | AccOpts(Prefix)]) end, TcpOpts = fun(Prefix) -> Filter([{backlog, cuttlefish:conf_get(Prefix ++ ".backlog", Conf, undefined)}, @@ -1433,7 +1181,7 @@ end}. {sndbuf, cuttlefish:conf_get(Prefix ++ ".sndbuf", Conf, undefined)}, {buffer, cuttlefish:conf_get(Prefix ++ ".buffer", Conf, undefined)}, {nodelay, cuttlefish:conf_get(Prefix ++ ".nodelay", Conf, true)}, - {reuseaddr, cuttlefish:conf_get(Prefix ++ ".reuseaddr", Conf, true)}]) + {reuseaddr, cuttlefish:conf_get(Prefix ++ ".reuseaddr", Conf, undefined)}]) end, SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, @@ -1460,11 +1208,9 @@ end}. TcpListeners = fun(Type, Name) -> Prefix = string:join(["listener", Type, Name], "."), case cuttlefish:conf_get(Prefix, Conf, undefined) of - undefined -> - []; - ListenOn -> - [{Atom(Type), ListenOn, [{connopts, ConnOpts(Prefix)}, - {sockopts, TcpOpts(Prefix)} | LisOpts(Prefix)]}] + undefined -> []; + ListenOn -> + [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)} | LisOpts(Prefix)]}] end end, @@ -1474,134 +1220,433 @@ end}. undefined -> []; ListenOn -> - [{Atom(Type), ListenOn, [{connopts, ConnOpts(Prefix)}, - {sockopts, TcpOpts(Prefix)}, - {sslopts, SslOpts(Prefix)} | LisOpts(Prefix)]}] + [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)}, + {ssl_options, SslOpts(Prefix)} | LisOpts(Prefix)]}] end end, - ApiListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - case cuttlefish:conf_get(Prefix, Conf, undefined) of - undefined -> - []; - ListenOn -> - SslOpts1 = case SslOpts(Prefix) of - [] -> []; - SslOpts0 -> [{sslopts, SslOpts0}] - end, - [{Atom(Type), ListenOn, [{connopts, ConnOpts(Prefix)}, - {sockopts, TcpOpts(Prefix)}| LisOpts(Prefix)] ++ SslOpts1}] - end - end, - - lists:flatten([TcpListeners(Type, Name) || {["listener", Type, Name], ListenOn} <- cuttlefish_variable:filter_by_prefix("listener.tcp", Conf) ++ cuttlefish_variable:filter_by_prefix("listener.ws", Conf)] ++ [SslListeners(Type, Name) || {["listener", Type, Name], ListenOn} <- cuttlefish_variable:filter_by_prefix("listener.ssl", Conf) - ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)] - ++ - [ApiListeners(Type, Name) || {["listener", Type, Name], ListenOn} - <- cuttlefish_variable:filter_by_prefix("listener.api", Conf)]) + ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)]) end}. %%-------------------------------------------------------------------- -%% MQTT REST API Listeners +%% Zones +%%-------------------------------------------------------------------- -{mapping, "listener.api.$name", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.api.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.max_clients", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.rate_limit", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.api.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.api.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.api.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.api.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.api.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.api.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.api.$name.handshake_timeout", "emqx.listeners", [ +%% @doc Idle timeout of the MQTT connection. +{mapping, "zone.$name.idle_timeout", "emqx.zones", [ + {default, "15s"}, {datatype, {duration, ms}} ]}. -{mapping, "listener.api.$name.keyfile", "emqx.listeners", [ +%% @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} ]}. -{mapping, "listener.api.$name.certfile", "emqx.listeners", [ - {datatype, string} +%% @doc Max Packet Size Allowed, 64K by default. +{mapping, "zone.$name.max_packet_size", "emqx.zones", [ + {datatype, bytesize} ]}. -{mapping, "listener.api.$name.cacertfile", "emqx.listeners", [ - {datatype, string} +%% @doc Set the Max ClientId Length Allowed. +{mapping, "zone.$name.max_clientid_len", "emqx.zones", [ + {datatype, integer} ]}. -{mapping, "listener.api.$name.verify", "emqx.listeners", [ - {datatype, atom} +%% @doc Set the Maximum topic levels. +{mapping, "zone.$name.max_topic_levels", "emqx.zones", [ + {datatype, integer} ]}. -{mapping, "listener.api.$name.fail_if_no_peer_cert", "emqx.listeners", [ +%% @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 +%%-------------------------------------------------------------------- + +{mapping, "bridge.$name.type", "emqx.bridges", [ + {datatype, {enum, [in, out]}} +]}. + +{mapping, "bridge.$name.store_type", "emqx.bridges", [ + {datatype, {enum, [memory, disk]}} +]}. + +{mapping, "bridge.$name.address", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.proto_ver", "emqx.bridges", [ + {datatype, {enum, [mqtt3, mqtt4, mqtt5]}} +]}. + +{mapping, "bridge.$name.client_id", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.clean_start", "emqx.bridges", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +{mapping, "bridge.$name.username", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.password", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.mountpoint", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.forward_rule", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.cacertfile", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.certfile", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.keyfile", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.ciphers", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.max_pending_messages", "emqx.bridges", [ + {default, 10000}, + {datatype, integer} +]}. + +{mapping, "bridge.$name.keepalive", "emqx.bridges", [ + {default, "10s"}, + {datatype, {duration, s}} +]}. + +{mapping, "bridge.$name.tls_versions", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.subscription.$id.topic", "emqx.bridges", [ + {datatype, string} +]}. + +{mapping, "bridge.$name.subscription.$id.qos", "emqx.bridges", [ + {datatype, integer} +]}. + +{mapping, "bridge.$name.start_type", "emqx.bridges", [ + {datatype, {enum, [manual, auto]}}, + {default, auto} +]}. + +{mapping, "bridge.$name.reconnect_count", "emqx.bridges", [ + {default, 10}, + {datatype, integer} +]}. + +{mapping, "bridge.$name.reconnect_time", "emqx.bridges", [ + {default, "30s"}, + {datatype, {duration, s}} +]}. + + +{translation, "emqx.bridges", fun(Conf) -> + + Split = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, + + IsSsl = fun(cacertfile) -> true; + (certfile) -> true; + (keyfile) -> true; + (ciphers) -> true; + (tls_versions) -> true; + (_Opt) -> false + end, + + Parse = fun(tls_versions, Vers) -> + {versions, [list_to_atom(S) || S <- Split(Vers)]}; + (ciphers, Ciphers) -> + {ciphers, Split(Ciphers)}; + (Opt, Val) -> + {Opt, Val} + end, + + Merge = fun(Opt, Val, Opts) -> + case IsSsl(Opt) of + true -> + SslOpts = [Parse(Opt, Val)|proplists:get_value(ssl_opts, Opts, [])], + lists:ukeymerge(1, [{ssl_opts, SslOpts}], Opts); + false -> + [{Opt, Val}|Opts] + end + end, + Subscriptions = fun(Name) -> + Configs = cuttlefish_variable:filter_by_prefix("bridge." ++ Name ++ ".subscription", Conf), + lists:zip([Topic || {_, Topic} <- lists:sort([{I, Topic} || {[_, _, "subscription", I, "topic"], Topic} <- Configs])], + [QoS || {_, QoS} <- lists:sort([{I, QoS} || {[_, _, "subscription", I, "qos"], QoS} <- Configs])]) + end, + + maps:to_list( + lists:foldl( + fun({["bridge", Name, Opt], Val}, Acc) -> + maps:update_with(list_to_atom(Name), + fun(Opts) -> + Merge(list_to_atom(Opt), Val, Opts) + end, [{list_to_atom(Opt), Val}, + {subscriptions, Subscriptions(Name)}], Acc); + (_, Acc) -> Acc + end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("bridge.", Conf)))) + +end}. + +%%-------------------------------------------------------------------- +%% Modules +%%-------------------------------------------------------------------- + +{mapping, "module.presence", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.presence.qos", "emqx.modules", [ + {default, 1}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +{mapping, "module.subscription", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.subscription.$id.topic", "emqx.modules", [ + {datatype, string} +]}. + +{mapping, "module.subscription.$id.qos", "emqx.modules", [ + {default, 1}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +{mapping, "module.rewrite", "emqx.modules", [ + {default, off}, + {datatype, flag} +]}. + +{mapping, "module.rewrite.rule.$id", "emqx.modules", [ + {datatype, string} +]}. + +{translation, "emqx.modules", fun(Conf) -> + Subscriptions = fun() -> + List = cuttlefish_variable:filter_by_prefix("module.subscription", Conf), + QosList = [Qos || {_, Qos} <- lists:sort([{I, Qos} || {[_,"subscription", I,"qos"], Qos} <- List])], + TopicList = [iolist_to_binary(Topic) || {_, Topic} <- + lists:sort([{I, Topic} || {[_,"subscription", I, "topic"], Topic} <- List])], + lists:zip(TopicList, QosList) + end, + Rewrites = fun() -> + Rules = cuttlefish_variable:filter_by_prefix("module.rewrite.rule", Conf), + lists:map(fun({[_, "rewrite", "rule", I], Rule}) -> + [Topic, Re, Dest] = string:tokens(Rule, " "), + {rewrite, list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} + end, Rules) + end, + lists:append([ + case cuttlefish:conf_get("module.presence", Conf) of %% Presence + true -> [{emqx_mod_presence, [{qos, cuttlefish:conf_get("module.presence.qos", Conf, 1)}]}]; + false -> [] + end, + case cuttlefish:conf_get("module.subscription", Conf) of %% Subscription + true -> [{emqx_mod_subscription, Subscriptions()}]; + false -> [] + end, + case cuttlefish:conf_get("module.rewrite", Conf) of %% Rewrite + true -> [{emqx_mod_rewrite, Rewrites()}]; + false -> [] + end + ]) +end}. + +%%------------------------------------------------------------------- +%% Plugins +%%------------------------------------------------------------------- + +{mapping, "plugins.etc_dir", "emqx.plugins_etc_dir", [ + {datatype, string} +]}. + +{mapping, "plugins.loaded_file", "emqx.plugins_loaded_file", [ + {datatype, string} +]}. + +{mapping, "plugins.expand_plugins_dir", "emqx.expand_plugins_dir", [ + {datatype, string} +]}. + +%%-------------------------------------------------------------------- +%% Broker +%%-------------------------------------------------------------------- + +{mapping, "broker.sys_interval", "emqx.broker_sys_interval", [ + {datatype, {duration, ms}}, + {default, "1m"} +]}. + +{mapping, "broker.session_locking_strategy", "emqx.session_locking_strategy", [ + {default, quorum}, + {datatype, {enum, [local,one,quorum,all]}} +]}. + +{mapping, "broker.shared_subscription_strategy", "emqx.shared_subscription_strategy", [ + {default, random}, + {datatype, {enum, [random, round_robbin, hash]}} +]}. + +{mapping, "broker.route_batch_clean", "emqx.route_batch_clean", [ + {default, on}, + {datatype, flag} +]}. + %%-------------------------------------------------------------------- %% System Monitor %%-------------------------------------------------------------------- @@ -1638,10 +1683,10 @@ end}. ]}. {translation, "emqx.sysmon", fun(Conf) -> - [{long_gc, cuttlefish:conf_get("sysmon.long_gc", Conf)}, - {long_schedule, cuttlefish:conf_get("sysmon.long_schedule", Conf)}, - {large_heap, cuttlefish:conf_get("sysmon.large_heap", Conf)}, - {busy_port, cuttlefish:conf_get("sysmon.busy_port", Conf)}, - {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] + [{long_gc, cuttlefish:conf_get("sysmon.long_gc", Conf)}, + {long_schedule, cuttlefish:conf_get("sysmon.long_schedule", Conf)}, + {large_heap, cuttlefish:conf_get("sysmon.large_heap", Conf)}, + {busy_port, cuttlefish:conf_get("sysmon.busy_port", Conf)}, + {busy_dist_port, cuttlefish:conf_get("sysmon.busy_dist_port", Conf)}] end}. diff --git a/src/emqx.app.src b/src/emqx.app.src index f3ccb8fa3..b7a195c8b 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,gproc,lager,esockd,mochiweb,lager_syslog,pbkdf2,bcrypt,clique,jsx]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,minirest]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx.erl b/src/emqx.erl index a539bbd42..475428fd4 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -74,7 +74,7 @@ subscribe(Topic) -> subscribe(Topic, Subscriber) -> emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(subscribe(topic() | string(), subscriber() | string(), [suboption()]) -> ok | {error, term()}). +-spec(subscribe(topic() | string(), subscriber() | string(), subopts()) -> ok | {error, term()}). subscribe(Topic, Subscriber, Options) -> emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). @@ -95,11 +95,11 @@ unsubscribe(Topic, Subscriber) -> %% PubSub management API %%-------------------------------------------------------------------- --spec(get_subopts(topic() | string(), subscriber()) -> [suboption()]). +-spec(get_subopts(topic() | string(), subscriber()) -> subopts()). get_subopts(Topic, Subscriber) -> emqx_broker:get_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber)). --spec(set_subopts(topic() | string(), subscriber(), [suboption()]) -> ok). +-spec(set_subopts(topic() | string(), subscriber(), subopts()) -> ok). set_subopts(Topic, Subscriber, Options) when is_list(Options) -> emqx_broker:set_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). @@ -110,7 +110,7 @@ topics() -> emqx_router:topics(). subscribers(Topic) -> emqx_broker:subscribers(iolist_to_binary(Topic)). --spec(subscriptions(subscriber() | string()) -> [{topic(), list(suboption())}]). +-spec(subscriptions(subscriber() | string()) -> [{topic(), subopts()}]). subscriptions(Subscriber) -> emqx_broker:subscriptions(list_to_subid(Subscriber)). @@ -166,8 +166,8 @@ shutdown() -> shutdown(Reason) -> emqx_logger:error("emqx shutdown for ~s", [Reason]), emqx_plugins:unload(), - lists:foreach(fun application:stop/1, [emqx, ekka, mochiweb, esockd, gproc]). + lists:foreach(fun application:stop/1, [emqx, ekka, cowboy, esockd, gproc]). reboot() -> - lists:foreach(fun application:start/1, [gproc, esockd, mochiweb, ekka, emqx]). + lists:foreach(fun application:start/1, [gproc, esockd, cowboy, ekka, emqx]). diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 75e49fe07..2b7630f1e 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -22,6 +22,8 @@ -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([clean_acl_cache/1, clean_acl_cache/2]). + %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -50,9 +52,9 @@ start_link() -> register_default_mod() -> case emqx_config:get_env(acl_file) of - {ok, File} -> - emqx_access_control:register_mod(acl, emqx_acl_internal, [File]); - undefined -> ok + undefined -> ok; + File -> + emqx_access_control:register_mod(acl, emqx_acl_internal, [File]) end. %% @doc Authenticate Client. @@ -127,6 +129,12 @@ tab_key(acl) -> acl_modules. stop() -> gen_server:stop(?MODULE, normal, infinity). +%%TODO: Support ACL cache... +clean_acl_cache(_ClientId) -> + ok. +clean_acl_cache(_ClientId, _Topic) -> + ok. + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index f6901c325..41da4e705 100644 --- a/src/emqx_alarm_mgr.erl +++ b/src/emqx_alarm_mgr.erl @@ -81,7 +81,7 @@ handle_event({set_alarm, Alarm = #alarm{timestamp = undefined}}, State)-> handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #state{alarms = Alarms}) -> case encode_alarm(Alarm) of {ok, Json} -> - emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); + ok = emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); {error, Reason} -> emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason]) end, @@ -131,7 +131,9 @@ encode_alarm(#alarm{id = AlarmId, severity = Severity, title = Title, {ts, emqx_time:now_secs(Ts)}]). alarm_msg(Type, AlarmId, Json) -> - emqx_message:make(?ALARM_MGR, #{sys => true, qos => 0}, topic(Type, AlarmId), Json). + Msg = emqx_message:make(?ALARM_MGR, topic(Type, AlarmId), Json), + emqx_message:set_headers(#{'Content-Type' => <<"application/json">>}, + emqx_message:set_flags(#{sys => true}, Msg)). topic(alert, AlarmId) -> emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 7a5426bae..d5ca8f6ae 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -31,7 +31,7 @@ start(_Type, _Args) -> emqx_modules:load(), emqx_plugins:init(), emqx_plugins:load(), - emqx_listeners:start_all(), + emqx_listeners:start(), start_autocluster(), register(emqx, self()), print_vsn(), diff --git a/src/emqx_auth_mod.erl b/src/emqx_auth_mod.erl index a5c3844a9..65298ef9b 100644 --- a/src/emqx_auth_mod.erl +++ b/src/emqx_auth_mod.erl @@ -16,10 +16,6 @@ -include("emqx.hrl"). --export([passwd_hash/2]). - --type(hash_type() :: plain | md5 | sha | sha256 | pbkdf2 | bcrypt). - %%-------------------------------------------------------------------- %% Authentication behavihour %%-------------------------------------------------------------------- @@ -46,33 +42,3 @@ behaviour_info(_Other) -> -endif. -%% @doc Password Hash --spec(passwd_hash(hash_type(), binary() | tuple()) -> binary()). -passwd_hash(plain, Password) -> - Password; -passwd_hash(md5, Password) -> - hexstring(crypto:hash(md5, Password)); -passwd_hash(sha, Password) -> - hexstring(crypto:hash(sha, Password)); -passwd_hash(sha256, Password) -> - hexstring(crypto:hash(sha256, Password)); -passwd_hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) -> - case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of - {ok, Hexstring} -> pbkdf2:to_hex(Hexstring); - {error, Error} -> - emqx_logger:error("[AuthMod] PasswdHash with pbkdf2 error:~p", [Error]), <<>> - end; -passwd_hash(bcrypt, {Salt, Password}) -> - case bcrypt:hashpw(Password, Salt) of - {ok, HashPassword} -> list_to_binary(HashPassword); - {error, Error}-> - emqx_logger:error("[AuthMod] PasswdHash with bcrypt error:~p", [Error]), <<>> - end. - -hexstring(<>) -> - iolist_to_binary(io_lib:format("~32.16.0b", [X])); -hexstring(<>) -> - iolist_to_binary(io_lib:format("~40.16.0b", [X])); -hexstring(<>) -> - iolist_to_binary(io_lib:format("~64.16.0b", [X])). - diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 929089f1b..690115ec2 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -14,43 +14,100 @@ -module(emqx_base62). --export([encode/1, decode/1]). +-export([encode/1, + encode/2, + decode/1, + decode/2]). -%% @doc Encode an integer to base62 string --spec(encode(non_neg_integer()) -> binary()). -encode(I) when is_integer(I) andalso I > 0 -> - list_to_binary(encode(I, [])). +%% @doc Encode any data to base62 binary +-spec encode(string() + | integer() + | binary()) -> float(). +encode(I) when is_integer(I) -> + encode(integer_to_binary(I)); +encode(S) when is_list(S)-> + encode(list_to_binary(S)); +encode(B) when is_binary(B) -> + encode(B, <<>>). -encode(I, Acc) when I < 62 -> - [char(I) | Acc]; -encode(I, Acc) -> - encode(I div 62, [char(I rem 62) | Acc]). +%% encode(D, string) -> +%% binary_to_list(encode(D)). -char(I) when I < 10 -> - $0 + I; - -char(I) when I < 36 -> - $A + I - 10; - -char(I) when I < 62 -> - $a + I - 36. - -%% @doc Decode base62 string to an integer --spec(decode(string() | binary()) -> integer()). +%% @doc Decode base62 binary to origin data binary +decode(L) when is_list(L) -> + decode(list_to_binary(L)); decode(B) when is_binary(B) -> - decode(binary_to_list(B)); -decode(S) when is_list(S) -> - decode(S, 0). + decode(B, <<>>). -decode([], I) -> - I; -decode([C|S], I) -> - decode(S, I * 62 + byte(C)). -byte(C) when $0 =< C andalso C =< $9 -> - C - $0; -byte(C) when $A =< C andalso C =< $Z -> - C - $A + 10; -byte(C) when $a =< C andalso C =< $z -> - C - $a + 36. + +%%==================================================================== +%% Internal functions +%%==================================================================== + +encode(D, string) -> + binary_to_list(encode(D)); +encode(<>, Acc) -> + CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3), encode_char(Index4)], + NewAcc = <>, + encode(Rest, NewAcc); +encode(<>, Acc) -> + CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3)], + NewAcc = <>, + encode(<<>>, NewAcc); +encode(<>, Acc) -> + CharList = [encode_char(Index1), encode_char(Index2)], + NewAcc = <>, + encode(<<>>, NewAcc); +encode(<<>>, Acc) -> + Acc. + +decode(D, integer) -> + binary_to_integer(decode(D)); +decode(D, string) -> + binary_to_list(decode(D)); +decode(<>, Acc) + when bit_size(Rest) >= 8-> + case Head == $9 of + true -> + <> = Rest, + DecodeChar = decode_char(9, Head1), + <<_:2, RestBit:6>> = <>, + NewAcc = <>, + decode(Rest1, NewAcc); + false -> + DecodeChar = decode_char(Head), + <<_:2, RestBit:6>> = <>, + NewAcc = <>, + decode(Rest, NewAcc) + end; +decode(<>, Acc) -> + DecodeChar = decode_char(Head), + LeftBitSize = bit_size(Acc) rem 8, + RightBitSize = 8 - LeftBitSize, + <<_:LeftBitSize, RestBit:RightBitSize>> = <>, + NewAcc = <>, + decode(Rest, NewAcc); +decode(<<>>, Acc) -> + Acc. + + +encode_char(I) when I < 26 -> + $A + I; +encode_char(I) when I < 52 -> + $a + I - 26; +encode_char(I) when I < 61 -> + $0 + I - 52; +encode_char(I) -> + [$9, $A + I - 61]. + +decode_char(I) when I >= $a andalso I =< $z -> + I + 26 - $a; +decode_char(I) when I >= $0 andalso I =< $8-> + I + 52 - $0; +decode_char(I) when I >= $A andalso I =< $Z-> + I - $A. + +decode_char(9, I) -> + I + 61 - $A. diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index ee2aa1535..eef5d249b 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -64,9 +64,7 @@ init([Pool, Id, Node, Topic, Options]) -> emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]), State = parse_opts(Options, #state{node = Node, subtopic = Topic}), %%TODO: queue.... - MQueue = emqx_mqueue:new(qname(Node, Topic), - [{max_len, State#state.max_queue_len}], - emqx_alarm:alarm_fun()), + MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}]), {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}}; false -> {stop, {cannot_connect_node, Node}} @@ -74,8 +72,8 @@ init([Pool, Id, Node, Topic, Options]) -> parse_opts([], State) -> State; -parse_opts([{qos, Qos} | Opts], State) -> - parse_opts(Opts, State#state{qos = Qos}); +parse_opts([{qos, QoS} | Opts], State) -> + parse_opts(Opts, State#state{qos = QoS}); parse_opts([{topic_suffix, Suffix} | Opts], State) -> parse_opts(Opts, State#state{topic_suffix= Suffix}); parse_opts([{topic_prefix, Prefix} | Opts], State) -> diff --git a/src/emqx_bridge1.erl b/src/emqx_bridge1.erl new file mode 100644 index 000000000..139711932 --- /dev/null +++ b/src/emqx_bridge1.erl @@ -0,0 +1,254 @@ +%% 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. + +-module(emqx_bridge1). + +-behaviour(gen_server). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). + + -import(proplists, [get_value/2, get_value/3]). + +-export([start_link/2, start_bridge/1, stop_bridge/1, status/1]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-record(state, {client_pid, options, reconnect_time, reconnect_count, + def_reconnect_count, type, mountpoint, queue, store_type, + max_pending_messages}). + +-record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, + packet_id, topic, props, payload}). + +start_link(Name, Options) -> + gen_server:start_link({local, name(Name)}, ?MODULE, [Options], []). + +start_bridge(Name) -> + gen_server:call(name(Name), start_bridge). + +stop_bridge(Name) -> + gen_server:call(name(Name), stop_bridge). + +status(Pid) -> + gen_server:call(Pid, status). + +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init([Options]) -> + process_flag(trap_exit, true), + case get_value(start_type, Options, manual) of + manual -> ok; + auto -> erlang:send_after(1000, self(), start) + end, + ReconnectCount = get_value(reconnect_count, Options, 10), + ReconnectTime = get_value(reconnect_time, Options, 30000), + MaxPendingMsg = get_value(max_pending_messages, Options, 10000), + Mountpoint = format_mountpoint(get_value(mountpoint, Options)), + StoreType = get_value(store_type, Options, memory), + Type = get_value(type, Options, in), + Queue = [], + {ok, #state{type = Type, + mountpoint = Mountpoint, + queue = Queue, + store_type = StoreType, + options = Options, + reconnect_count = ReconnectCount, + reconnect_time = ReconnectTime, + def_reconnect_count = ReconnectCount, + max_pending_messages = MaxPendingMsg}}. + +handle_call(start_bridge, _From, State = #state{client_pid = undefined}) -> + {noreply, NewState} = handle_info(start, State), + {reply, <<"start bridge successfully">>, NewState}; + +handle_call(start_bridge, _From, State) -> + {reply, <<"bridge already started">>, State}; + +handle_call(stop_bridge, _From, State = #state{client_pid = undefined}) -> + {reply, <<"bridge not started">>, State}; + +handle_call(stop_bridge, _From, State = #state{client_pid = Pid}) -> + emqx_client:disconnect(Pid), + {reply, <<"stop bridge successfully">>, State}; + +handle_call(status, _From, State = #state{client_pid = undefined}) -> + {reply, <<"Stopped">>, State}; +handle_call(status, _From, State = #state{client_pid = _Pid})-> + {reply, <<"Running">>, State}; + +handle_call(Req, _From, State) -> + emqx_logger:error("[Bridge] unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + emqx_logger:error("[Bridge] unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(start, State = #state{reconnect_count = 0}) -> + {noreply, State}; + +%%---------------------------------------------------------------- +%% start in message bridge +%%---------------------------------------------------------------- +handle_info(start, State = #state{options = Options, + client_pid = undefined, + reconnect_time = ReconnectTime, + reconnect_count = ReconnectCount, + type = in}) -> + case emqx_client:start_link([{owner, self()}|options(Options)]) of + {ok, ClientPid, _} -> + Subs = get_value(subscriptions, Options, []), + [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], + {noreply, State#state{client_pid = ClientPid}}; + {error,_} -> + erlang:send_after(ReconnectTime, self(), start), + {noreply, State = #state{reconnect_count = ReconnectCount-1}} + end; + +%%---------------------------------------------------------------- +%% start out message bridge +%%---------------------------------------------------------------- +handle_info(start, State = #state{options = Options, + client_pid = undefined, + reconnect_time = ReconnectTime, + reconnect_count = ReconnectCount, + type = out}) -> + case emqx_client:start_link([{owner, self()}|options(Options)]) of + {ok, ClientPid, _} -> + Subs = get_value(subscriptions, Options, []), + [emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs], + ForwardRules = string:tokens(get_value(forward_rule, Options, ""), ","), + [emqx_broker:subscribe(i2b(Topic)) || Topic <- ForwardRules, emqx_topic:validate({filter, i2b(Topic)})], + {noreply, State#state{client_pid = ClientPid}}; + {error,_} -> + erlang:send_after(ReconnectTime, self(), start), + {noreply, State = #state{reconnect_count = ReconnectCount-1}} + end; + +%%---------------------------------------------------------------- +%% received local node message +%%---------------------------------------------------------------- +handle_info({dispatch, _, #message{topic = Topic, payload = Payload, flags = #{retain := Retain}}}, + State = #state{client_pid = Pid, mountpoint = Mountpoint, queue = Queue, + store_type = StoreType, max_pending_messages = MaxPendingMsg}) -> + Msg = #mqtt_msg{qos = 1, + retain = Retain, + topic = mountpoint(Mountpoint, Topic), + payload = Payload}, + case emqx_client:publish(Pid, Msg) of + {ok, PkgId} -> + {noreply, State#state{queue = store(StoreType, {PkgId, Msg}, Queue, MaxPendingMsg)}}; + {error, Reason} -> + emqx_logger:error("Publish fail:~p", [Reason]), + {noreply, State} + end; + +%%---------------------------------------------------------------- +%% received remote node message +%%---------------------------------------------------------------- +handle_info({publish, #{qos := QoS, dup := Dup, retain := Retain, topic := Topic, + properties := Props, payload := Payload}}, State) -> + NewMsg0 = emqx_message:make(bridge, QoS, Topic, Payload), + NewMsg1 = emqx_message:set_headers(Props, emqx_message:set_flags(#{dup => Dup, retain=> Retain}, NewMsg0)), + emqx_broker:publish(NewMsg1), + {noreply, State}; + +%%---------------------------------------------------------------- +%% received remote puback message +%%---------------------------------------------------------------- +handle_info({puback, #{packet_id := PkgId}}, State = #state{queue = Queue, store_type = StoreType}) -> + % lists:keydelete(PkgId, 1, Queue) + {noreply, State#state{queue = delete(StoreType, PkgId, Queue)}}; + +handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) -> + {noreply, State#state{client_pid = undefined}}; + +handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid, + reconnect_time = ReconnectTime, + def_reconnect_count = DefReconnectCount}) -> + lager:warning("emqx bridge stop reason:~p", [Reason]), + erlang:send_after(ReconnectTime, self(), start), + {noreply, State#state{client_pid = undefined, reconnect_count = DefReconnectCount}}; + +handle_info(Info, State) -> + emqx_logger:error("[Bridge] unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #state{}) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +proto_ver(mqtt3) -> v3; +proto_ver(mqtt4) -> v4; +proto_ver(mqtt5) -> v5. +address(Address) -> + case string:tokens(Address, ":") of + [Host] -> {Host, 1883}; + [Host, Port] -> {Host, list_to_integer(Port)} + end. +options(Options) -> + options(Options, []). +options([], Acc) -> + Acc; +options([{username, Username}| Options], Acc) -> + options(Options, [{username, Username}|Acc]); +options([{proto_ver, ProtoVer}| Options], Acc) -> + options(Options, [{proto_ver, proto_ver(ProtoVer)}|Acc]); +options([{password, Password}| Options], Acc) -> + options(Options, [{password, Password}|Acc]); +options([{keepalive, Keepalive}| Options], Acc) -> + options(Options, [{keepalive, Keepalive}|Acc]); +options([{client_id, ClientId}| Options], Acc) -> + options(Options, [{client_id, ClientId}|Acc]); +options([{clean_start, CleanStart}| Options], Acc) -> + options(Options, [{clean_start, CleanStart}|Acc]); +options([{address, Address}| Options], Acc) -> + {Host, Port} = address(Address), + options(Options, [{host, Host}, {port, Port}|Acc]); +options([_Option | Options], Acc) -> + options(Options, Acc). + +name(Id) -> + list_to_atom(lists:concat([?MODULE, "_", Id])). + +i2b(L) -> iolist_to_binary(L). + +mountpoint(undefined, Topic) -> + Topic; +mountpoint(Prefix, Topic) -> + <>. + +format_mountpoint(undefined) -> + undefined; +format_mountpoint(Prefix) -> + binary:replace(i2b(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). + +store(memory, Data, Queue, MaxPendingMsg) when length(Queue) =< MaxPendingMsg -> + [Data | Queue]; +store(memory, _Data, Queue, _MaxPendingMsg) -> + lager:error("Beyond max pending messages"), + Queue; +store(disk, Data, Queue, _MaxPendingMsg)-> + [Data | Queue]. + +delete(memory, PkgId, Queue) -> + lists:keydelete(PkgId, 1, Queue); +delete(disk, PkgId, Queue) -> + lists:keydelete(PkgId, 1, Queue). \ No newline at end of file diff --git a/src/emqx_bridge1_sup.erl b/src/emqx_bridge1_sup.erl new file mode 100644 index 000000000..444c7cfb5 --- /dev/null +++ b/src/emqx_bridge1_sup.erl @@ -0,0 +1,45 @@ +%% 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. + +-module(emqx_bridge1_sup). + +-behavior(supervisor). + +-include("emqx.hrl"). + +-export([start_link/0, bridges/0]). + +%% Supervisor callbacks +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% @doc List all bridges +-spec(bridges() -> [{node(), topic(), pid()}]). +bridges() -> + [{Name, emqx_bridge1:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?MODULE)]. + +init([]) -> + BridgesOpts = emqx_config:get_env(bridges, []), + Bridges = [spec(Opts)|| Opts <- BridgesOpts], + {ok, {{one_for_one, 10, 100}, Bridges}}. + +spec({Id, Options})-> + #{id => Id, + start => {emqx_bridge1, start_link, [Id, Options]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_bridge1]}. \ No newline at end of file diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 43cc81155..7015590d8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -20,8 +20,10 @@ -export([start_link/2]). -export([subscribe/1, subscribe/2, subscribe/3, subscribe/4]). --export([publish/1, publish/2, safe_publish/1]). --export([unsubscribe/1, unsubscribe/2]). +-export([multi_subscribe/1, multi_subscribe/2, multi_subscribe/3]). +-export([publish/1, safe_publish/1]). +-export([unsubscribe/1, unsubscribe/2, unsubscribe/3]). +-export([multi_unsubscribe/1, multi_unsubscribe/2, multi_unsubscribe/3]). -export([dispatch/2, dispatch/3]). -export([subscriptions/1, subscribers/1, subscribed/2]). -export([get_subopts/2, set_subopts/3]). @@ -31,102 +33,141 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id, submon}). +-record(state, {pool, id, submap, submon}). +-record(subscribe, {topic, subpid, subid, subopts = #{}}). +-record(unsubscribe, {topic, subpid, subid}). +%% The default request timeout +-define(TIMEOUT, 60000). -define(BROKER, ?MODULE). --define(TIMEOUT, 120000). %% ETS tables -define(SUBOPTION, emqx_suboption). -define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIPTION, emqx_subscription). +-define(is_subid(Id), (is_binary(Id) orelse is_atom(Id))). + -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). start_link(Pool, Id) -> - gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, - ?MODULE, [Pool, Id], [{hibernate_after, 2000}]). + gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, + [Pool, Id], [{hibernate_after, 2000}]). %%------------------------------------------------------------------------------ -%% Subscribe/Unsubscribe +%% Subscribe %%------------------------------------------------------------------------------ --spec(subscribe(topic()) -> ok | {error, term()}). +-spec(subscribe(topic()) -> ok). subscribe(Topic) when is_binary(Topic) -> subscribe(Topic, self()). --spec(subscribe(topic(), subscriber()) -> ok | {error, term()}). -subscribe(Topic, Subscriber) when is_binary(Topic) -> - subscribe(Topic, Subscriber, []). +-spec(subscribe(topic(), pid() | subid()) -> ok). +subscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> + subscribe(Topic, SubPid, undefined); +subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> + subscribe(Topic, self(), SubId). --spec(subscribe(topic(), subscriber(), [suboption()]) -> ok | {error, term()}). -subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> - subscribe(Topic, Subscriber, Options, ?TIMEOUT). +-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) when is_binary(Topic), is_pid(SubPid), ?is_subid(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) -> + subscribe(Topic, self(), SubId, SubOpts). --spec(subscribe(topic(), subscriber(), [suboption()], timeout()) - -> ok | {error, term()}). -subscribe(Topic, Subscriber, Options, Timeout) -> - {Topic1, Options1} = emqx_topic:parse(Topic, Options), - SubReq = {subscribe, Topic1, with_subpid(Subscriber), Options1}, - async_call(pick(Subscriber), SubReq, Timeout). +-spec(subscribe(topic(), pid(), subid(), subopts()) -> ok). +subscribe(Topic, SubPid, SubId, SubOpts) when is_binary(Topic), is_pid(SubPid), + ?is_subid(SubId), is_map(SubOpts) -> + Broker = pick(SubPid), + SubReq = #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}, + wait_for_reply(async_call(Broker, SubReq), ?TIMEOUT). --spec(unsubscribe(topic()) -> ok | {error, term()}). +-spec(multi_subscribe(topic_table()) -> ok). +multi_subscribe(TopicTable) when is_list(TopicTable) -> + multi_subscribe(TopicTable, self()). + +-spec(multi_subscribe(topic_table(), pid() | subid()) -> ok). +multi_subscribe(TopicTable, SubPid) when is_pid(SubPid) -> + multi_subscribe(TopicTable, SubPid, undefined); +multi_subscribe(TopicTable, SubId) when ?is_subid(SubId) -> + multi_subscribe(TopicTable, self(), SubId). + +-spec(multi_subscribe(topic_table(), pid(), subid()) -> ok). +multi_subscribe(TopicTable, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) -> + Broker = pick(SubPid), + SubReq = fun(Topic, SubOpts) -> + #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts} + end, + wait_for_replies([async_call(Broker, SubReq(Topic, SubOpts)) + || {Topic, SubOpts} <- TopicTable], ?TIMEOUT). + +%%------------------------------------------------------------------------------ +%% Unsubscribe +%%------------------------------------------------------------------------------ + +-spec(unsubscribe(topic()) -> ok). unsubscribe(Topic) when is_binary(Topic) -> unsubscribe(Topic, self()). --spec(unsubscribe(topic(), subscriber()) -> ok | {error, term()}). -unsubscribe(Topic, Subscriber) when is_binary(Topic) -> - unsubscribe(Topic, Subscriber, ?TIMEOUT). +-spec(unsubscribe(topic(), pid() | subid()) -> ok). +unsubscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> + unsubscribe(Topic, SubPid, undefined); +unsubscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> + unsubscribe(Topic, self(), SubId). --spec(unsubscribe(topic(), subscriber(), timeout()) -> ok | {error, term()}). -unsubscribe(Topic, Subscriber, Timeout) -> - {Topic1, _} = emqx_topic:parse(Topic), - UnsubReq = {unsubscribe, Topic1, with_subpid(Subscriber)}, - async_call(pick(Subscriber), UnsubReq, Timeout). +-spec(unsubscribe(topic(), pid(), subid()) -> ok). +unsubscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> + Broker = pick(SubPid), + UnsubReq = #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}, + wait_for_reply(async_call(Broker, UnsubReq), ?TIMEOUT). + +-spec(multi_unsubscribe([topic()]) -> ok). +multi_unsubscribe(Topics) -> + multi_unsubscribe(Topics, self()). + +-spec(multi_unsubscribe([topic()], pid() | subid()) -> ok). +multi_unsubscribe(Topics, SubPid) when is_pid(SubPid) -> + multi_unsubscribe(Topics, SubPid, undefined); +multi_unsubscribe(Topics, SubId) when ?is_subid(SubId) -> + multi_unsubscribe(Topics, self(), SubId). + +-spec(multi_unsubscribe([topic()], pid(), subid()) -> ok). +multi_unsubscribe(Topics, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) -> + Broker = pick(SubPid), + UnsubReq = fun(Topic) -> + #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId} + end, + wait_for_replies([async_call(Broker, UnsubReq(Topic)) || Topic <- Topics], ?TIMEOUT). %%------------------------------------------------------------------------------ %% Publish %%------------------------------------------------------------------------------ --spec(publish(topic(), payload()) -> delivery() | stopped). -publish(Topic, Payload) when is_binary(Topic), is_binary(Payload) -> - publish(emqx_message:make(Topic, Payload)). - --spec(publish(message()) -> {ok, delivery()} | {error, stopped}). -publish(Msg = #message{from = From}) -> - %% Hook to trace? - _ = trace(publish, From, Msg), +-spec(publish(message()) -> delivery()). +publish(Msg) when is_record(Msg, message) -> + _ = emqx_tracer:trace(publish, Msg), case emqx_hooks:run('message.publish', [], Msg) of {ok, Msg1 = #message{topic = Topic}} -> - {ok, route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1))}; + route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)); {stop, Msg1} -> - emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), - {error, stopped} + emqx_logger:warning("Stop publishing: ~p", [Msg]), delivery(Msg1) end. -%% called internally -safe_publish(Msg) -> +%% Called internally +safe_publish(Msg) when is_record(Msg, message) -> try publish(Msg) catch _:Error:Stacktrace -> emqx_logger:error("[Broker] publish error: ~p~n~p~n~p", [Error, Msg, Stacktrace]) + after + ok end. -%%------------------------------------------------------------------------------ -%% Trace -%%------------------------------------------------------------------------------ - -trace(publish, From, _Msg) when is_atom(From) -> - %% Dont' trace '$SYS' publish - ignore; -trace(public, #client{id = ClientId, username = Username}, - #message{topic = Topic, payload = Payload}) -> - emqx_logger:info([{client, ClientId}, {topic, Topic}], - "~s/~s PUBLISH to ~s: ~p", [Username, ClientId, Topic, Payload]); -trace(public, From, #message{topic = Topic, payload = Payload}) - when is_binary(From); is_list(From) -> - emqx_logger:info([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). +delivery(Msg) -> + #delivery{sender = self(), message = Msg, flows = []}. %%------------------------------------------------------------------------------ %% Route @@ -186,12 +227,8 @@ dispatch(Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> Delivery#delivery{flows = [{dispatch, Topic, Count}|Flows]} end. -dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> +dispatch({SubPid, _SubId}, Topic, Msg) when is_pid(SubPid) -> SubPid ! {dispatch, Topic, Msg}; -dispatch({SubId, SubPid}, Topic, Msg) when is_binary(SubId), is_pid(SubPid) -> - SubPid ! {dispatch, Topic, Msg}; -dispatch(SubId, Topic, Msg) when is_binary(SubId) -> - emqx_sm:dispatch(SubId, Topic, Msg); dispatch({share, _Group, _Sub}, _Topic, _Msg) -> ignored. @@ -200,12 +237,11 @@ dropped(<<"$SYS/", _/binary>>) -> dropped(_Topic) -> emqx_metrics:inc('messages/dropped'). -delivery(Msg) -> - #delivery{node = node(), message = Msg, flows = []}. - +-spec(subscribers(topic()) -> [subscriber()]). subscribers(Topic) -> try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end. +-spec(subscriptions(subscriber()) -> [{topic(), subopts()}]). subscriptions(Subscriber) -> lists:map(fun({_, {share, _Group, Topic}}) -> subscription(Topic, Subscriber); @@ -216,51 +252,49 @@ subscriptions(Subscriber) -> subscription(Topic, Subscriber) -> {Topic, ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)}. --spec(subscribed(topic(), subscriber()) -> boolean()). +-spec(subscribed(topic(), pid() | subid() | subscriber()) -> boolean()). subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> - ets:member(?SUBOPTION, {Topic, SubPid}); -subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) -> - length(ets:match_object(?SUBOPTION, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1; -subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) -> - ets:member(?SUBOPTION, {Topic, {SubId, SubPid}}). + length(ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1)) == 1; +subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> + length(ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1)) == 1; +subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> + ets:member(?SUBOPTION, {Topic, {SubPid, SubId}}). --spec(get_subopts(topic(), subscriber()) -> [suboption()]). +-spec(get_subopts(topic(), subscriber()) -> subopts()). get_subopts(Topic, Subscriber) when is_binary(Topic) -> try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) catch error:badarg -> [] end. --spec(set_subopts(topic(), subscriber(), [suboption()]) -> boolean()). -set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_list(Opts) -> +-spec(set_subopts(topic(), subscriber(), subopts()) -> boolean()). +set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_map(Opts) -> case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of [{_, OldOpts}] -> - Opts1 = lists:usort(lists:umerge(Opts, OldOpts)), - ets:insert(?SUBOPTION, {{Topic, Subscriber}, Opts1}); + ets:insert(?SUBOPTION, {{Topic, Subscriber}, maps:merge(OldOpts, Opts)}); [] -> false end. -with_subpid(SubPid) when is_pid(SubPid) -> - SubPid; -with_subpid(SubId) when is_binary(SubId) -> - {SubId, self()}; -with_subpid({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - {SubId, SubPid}. - -async_call(Broker, Msg, Timeout) -> +async_call(Broker, Req) -> From = {self(), Tag = make_ref()}, - ok = gen_server:cast(Broker, {From, Msg}), + ok = gen_server:cast(Broker, {From, Req}), + Tag. + +wait_for_replies(Tags, Timeout) -> + lists:foreach( + fun(Tag) -> + wait_for_reply(Tag, Timeout) + end, Tags). + +wait_for_reply(Tag, Timeout) -> receive {Tag, Reply} -> Reply after Timeout -> - {error, timeout} + exit(timeout) end. +%% Pick a broker pick(SubPid) when is_pid(SubPid) -> - gproc_pool:pick_worker(broker, SubPid); -pick(SubId) when is_binary(SubId) -> - gproc_pool:pick_worker(broker, SubId); -pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) -> - pick(SubPid). + gproc_pool:pick_worker(broker, SubPid). -spec(topics() -> [topic()]). topics() -> emqx_router:topics(). @@ -271,33 +305,35 @@ topics() -> emqx_router:topics(). init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), - {ok, #state{pool = Pool, id = Id, submon = emqx_pmon:new()}}. + {ok, #state{pool = Pool, id = Id, submap = #{}, submon = emqx_pmon:new()}}. handle_call(Req, _From, State) -> emqx_logger:error("[Broker] unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> - case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [] -> - Group = proplists:get_value(share, Options), - true = do_subscribe(Group, Topic, Subscriber, Options), - emqx_shared_sub:subscribe(Group, Topic, subpid(Subscriber)), - emqx_router:add_route(From, Topic, destination(Options)), +handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}}, State) -> + Subscriber = {SubPid, SubId}, + case ets:member(?SUBOPTION, {Topic, Subscriber}) of + false -> + Group = maps:get(share, SubOpts, undefined), + true = do_subscribe(Group, Topic, Subscriber, SubOpts), + emqx_shared_sub:subscribe(Group, Topic, SubPid), + emqx_router:add_route(From, Topic, dest(Group)), {noreply, monitor_subscriber(Subscriber, State)}; - [_] -> + true -> gen_server:reply(From, ok), {noreply, State} end; -handle_cast({From, {unsubscribe, Topic, Subscriber}}, State) -> +handle_cast({From, #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}}, State) -> + Subscriber = {SubPid, SubId}, case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [{_, Options}] -> - Group = proplists:get_value(share, Options), + [{_, SubOpts}] -> + Group = maps:get(share, SubOpts, undefined), true = do_unsubscribe(Group, Topic, Subscriber), - emqx_shared_sub:unsubscribe(Group, Topic, subpid(Subscriber)), + emqx_shared_sub:unsubscribe(Group, Topic, SubPid), case ets:member(?SUBSCRIBER, Topic) of - false -> emqx_router:del_route(From, Topic, destination(Options)); + false -> emqx_router:del_route(From, Topic, dest(Group)); true -> gen_server:reply(From, ok) end; [] -> gen_server:reply(From, ok) @@ -308,37 +344,22 @@ handle_cast(Msg, State) -> emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{submon = SubMon}) -> - Subscriber = case SubMon:find(SubPid) of - undefined -> SubPid; - SubId -> {SubId, SubPid} - end, - Topics = lists:map(fun({_, {share, _, Topic}}) -> - Topic; - ({_, Topic}) -> - Topic - end, ets:lookup(?SUBSCRIPTION, Subscriber)), - lists:foreach( - fun(Topic) -> - case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of - [{_, Options}] -> - Group = proplists:get_value(share, Options), - true = do_unsubscribe(Group, Topic, Subscriber), - case ets:member(?SUBSCRIBER, Topic) of - false -> emqx_router:del_route(Topic, destination(Options)); - true -> ok - end; - [] -> ok - end - end, Topics), - {noreply, demonitor_subscriber(SubPid, State)}; +handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #state{submap = SubMap}) -> + case maps:find(SubPid, SubMap) of + {ok, SubIds} -> + lists:foreach(fun(SubId) -> subscriber_down({SubPid, SubId}) end, SubIds), + {noreply, demonitor_subscriber(SubPid, State)}; + error -> + emqx_logger:error("unexpected 'DOWN': ~p, reason: ~p", [SubPid, Reason]), + {noreply, State} + end; handle_info(Info, State) -> emqx_logger:error("[Broker] unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{pool = Pool, id = Id}) -> - true = gproc_pool:disconnect_worker(Pool, {Pool, Id}). + gproc_pool:disconnect_worker(Pool, {Pool, Id}). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -347,35 +368,44 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -do_subscribe(Group, Topic, Subscriber, Options) -> +do_subscribe(Group, Topic, Subscriber, SubOpts) -> ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), - ets:insert(?SUBOPTION, {{Topic, Subscriber}, Options}). + ets:insert(?SUBOPTION, {{Topic, Subscriber}, SubOpts}). do_unsubscribe(Group, Topic, Subscriber) -> ets:delete_object(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), ets:delete(?SUBOPTION, {Topic, Subscriber}). -monitor_subscriber(SubPid, State = #state{submon = SubMon}) when is_pid(SubPid) -> - State#state{submon = SubMon:monitor(SubPid)}; +subscriber_down(Subscriber) -> + Topics = lists:map(fun({_, {share, _, Topic}}) -> + Topic; + ({_, Topic}) -> + Topic + end, ets:lookup(?SUBSCRIPTION, Subscriber)), + lists:foreach(fun(Topic) -> + case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of + [{_, SubOpts}] -> + Group = maps:get(share, SubOpts, undefined), + true = do_unsubscribe(Group, Topic, Subscriber), + ets:member(?SUBSCRIBER, Topic) + orelse emqx_router:del_route(Topic, dest(Group)); + [] -> ok + end + end, Topics). -monitor_subscriber({SubId, SubPid}, State = #state{submon = SubMon}) -> - State#state{submon = SubMon:monitor(SubPid, SubId)}. +monitor_subscriber({SubPid, SubId}, State = #state{submap = SubMap, submon = SubMon}) -> + UpFun = fun(SubIds) -> lists:usort([SubId|SubIds]) end, + State#state{submap = maps:update_with(SubPid, UpFun, [SubId], SubMap), + submon = emqx_pmon:monitor(SubPid, SubMon)}. -demonitor_subscriber(SubPid, State = #state{submon = SubMon}) -> - State#state{submon = SubMon:demonitor(SubPid)}. +demonitor_subscriber(SubPid, State = #state{submap = SubMap, submon = SubMon}) -> + State#state{submap = maps:remove(SubPid, SubMap), + submon = emqx_pmon:demonitor(SubPid, SubMon)}. -destination(Options) -> - case proplists:get_value(share, Options) of - undefined -> node(); - Group -> {Group, node()} - end. - -subpid(SubPid) when is_pid(SubPid) -> - SubPid; -subpid({_SubId, SubPid}) when is_pid(SubPid) -> - SubPid. +dest(undefined) -> node(); +dest(Group) -> {Group, node()}. shared(undefined, Name) -> Name; shared(Group, Name) -> {share, Group, Name}. diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index 975b2bf0d..fecf98a7b 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -17,9 +17,7 @@ -behaviour(gen_server). -export([start_link/0]). - --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(HELPER, ?MODULE). @@ -39,7 +37,7 @@ init([]) -> handle_call(Req, _From, State) -> emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]), - {reply, ignore, State}. + {reply, ignored, State}. handle_cast(Msg, State) -> emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]), diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index 4463f2b27..6be9093b5 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -14,7 +14,7 @@ -module(emqx_cli). --export([print/1, print/2, usage/1]). +-export([print/1, print/2, usage/1, usage/2]). print(Msg) -> io:format(Msg). @@ -28,3 +28,5 @@ usage(CmdList) -> io:format("~-48s# ~s~n", [Cmd, Descr]) end, CmdList). +usage(Format, Args) -> + usage([{Format, Args}]). \ No newline at end of file diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 87b1e2bf3..27f8be353 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -69,6 +69,11 @@ -export_type([host/0, option/0]). +-record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false, + packet_id, topic, props, payload}). + +-type(mqtt_msg() :: #mqtt_msg{}). + -record(state, {name :: atom(), owner :: pid(), host :: host(), @@ -89,7 +94,7 @@ force_ping :: boolean(), paused :: boolean(), will_flag :: boolean(), - will_msg :: mqtt_message(), + will_msg :: mqtt_msg(), properties :: properties(), pending_calls :: list(), subscriptions :: map(), @@ -140,6 +145,9 @@ -define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). +-define(WILL_MSG(QoS, Retain, Topic, Props, Payload), + #mqtt_msg{qos = QoS, retain = Retain, topic = Topic, props = Props, payload = Payload}). + %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ @@ -242,8 +250,7 @@ parse_subopt([{qos, QoS} | Opts], Rec) -> -spec(publish(client(), topic(), payload()) -> ok | {error, term()}). publish(Client, Topic, Payload) when is_binary(Topic) -> - publish(Client, #mqtt_message{topic = Topic, qos = ?QOS_0, - payload = iolist_to_binary(Payload)}). + publish(Client, #mqtt_msg{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}). -spec(publish(client(), topic(), payload(), qos() | [pubopt()]) -> ok | {ok, packet_id()} | {error, term()}). @@ -261,15 +268,14 @@ publish(Client, Topic, Properties, Payload, Opts) ok = emqx_mqtt_properties:validate(Properties), Retain = proplists:get_bool(retain, Opts), QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), - publish(Client, #mqtt_message{qos = QoS, - retain = Retain, - topic = Topic, - properties = Properties, - payload = iolist_to_binary(Payload)}). + publish(Client, #mqtt_msg{qos = QoS, + retain = Retain, + topic = Topic, + props = Properties, + payload = iolist_to_binary(Payload)}). --spec(publish(client(), mqtt_message()) - -> ok | {ok, packet_id()} | {error, term()}). -publish(Client, Msg) when is_record(Msg, mqtt_message) -> +-spec(publish(client(), #mqtt_msg{}) -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Msg) when is_record(Msg, mqtt_msg) -> gen_statem:call(Client, {publish, Msg}). -spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()). @@ -380,7 +386,7 @@ init([Options]) -> force_ping = false, paused = false, will_flag = false, - will_msg = #mqtt_message{}, + will_msg = #mqtt_msg{}, pending_calls = [], subscriptions = #{}, max_inflight = infinity, @@ -488,15 +494,15 @@ init([_Opt | Opts], State) -> init(Opts, State). init_will_msg({topic, Topic}, WillMsg) -> - WillMsg#mqtt_message{topic = iolist_to_binary(Topic)}; -init_will_msg({props, Properties}, WillMsg) -> - WillMsg#mqtt_message{properties = Properties}; + WillMsg#mqtt_msg{topic = iolist_to_binary(Topic)}; +init_will_msg({props, Props}, WillMsg) -> + WillMsg#mqtt_msg{props = Props}; init_will_msg({payload, Payload}, WillMsg) -> - WillMsg#mqtt_message{payload = iolist_to_binary(Payload)}; + WillMsg#mqtt_msg{payload = iolist_to_binary(Payload)}; init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> - WillMsg#mqtt_message{retain = Retain}; + WillMsg#mqtt_msg{retain = Retain}; init_will_msg({qos, QoS}, WillMsg) -> - WillMsg#mqtt_message{qos = ?QOS_I(QoS)}. + WillMsg#mqtt_msg{qos = ?QOS_I(QoS)}. init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> Size = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), @@ -534,15 +540,16 @@ mqtt_connect(State = #state{client_id = ClientId, will_flag = WillFlag, will_msg = WillMsg, properties = Properties}) -> - ?WILL_MSG(WillQos, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, - ConnProps = emqx_mqtt_properties:filter(?CONNECT, maps:to_list(Properties)), + ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, + ConnProps = emqx_mqtt_properties:filter(?CONNECT, Properties), + io:format("ConnProps: ~p~n", [ConnProps]), send(?CONNECT_PACKET( #mqtt_packet_connect{proto_ver = ProtoVer, proto_name = ProtoName, is_bridge = IsBridge, clean_start = CleanStart, will_flag = WillFlag, - will_qos = WillQos, + will_qos = WillQoS, will_retain = WillRetain, keepalive = KeepAlive, properties = ConnProps, @@ -624,7 +631,7 @@ connected({call, From}, SubReq = {subscribe, Properties, Topics}, {stop_and_reply, Reason, [{reply, From, Error}]} end; -connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) -> +connected({call, From}, {publish, Msg = #mqtt_msg{qos = ?QOS_0}}, State) -> case send(Msg, State) of {ok, NewState} -> {keep_state, NewState, [{reply, From, ok}]}; @@ -632,14 +639,14 @@ connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) -> {stop_and_reply, Reason, [{reply, From, Error}]} end; -connected({call, From}, {publish, Msg = #mqtt_message{qos = Qos}}, +connected({call, From}, {publish, Msg = #mqtt_msg{qos = QoS}}, State = #state{inflight = Inflight, last_packet_id = PacketId}) - when (Qos =:= ?QOS_1); (Qos =:= ?QOS_2) -> + when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) -> case emqx_inflight:is_full(Inflight) of true -> {keep_state, State, [{reply, From, {error, inflight_full}}]}; false -> - Msg1 = Msg#mqtt_message{packet_id = PacketId}, + Msg1 = Msg#mqtt_msg{packet_id = PacketId}, case send(Msg1, State) of {ok, NewState} -> Inflight1 = emqx_inflight:insert(PacketId, {publish, Msg1, os:timestamp()}, Inflight), @@ -690,7 +697,7 @@ connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) -> send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State); connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> - {keep_state, deliver_msg(packet_to_msg(Packet), State)}; + {keep_state, deliver(packet_to_msg(Packet), State)}; connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true}) -> {keep_state, State}; @@ -698,7 +705,7 @@ connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true}) connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), State = #state{auto_ack = AutoAck}) -> - _ = deliver_msg(packet_to_msg(Packet), State), + _ = deliver(packet_to_msg(Packet), State), case AutoAck of true -> send_puback(?PUBACK_PACKET(PacketId), State); false -> {keep_state, State} @@ -716,7 +723,7 @@ connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties), State = #state{owner = Owner, inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, #mqtt_message{packet_id = PacketId}, _Ts}} -> + {value, {publish, #mqtt_msg{packet_id = PacketId}, _Ts}} -> Owner ! {puback, #{packet_id => PacketId, reason_code => ReasonCode, properties => Properties}}, @@ -745,8 +752,7 @@ connected(cast, ?PUBREL_PACKET(PacketId), State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) -> case maps:take(PacketId, AwaitingRel) of {Packet, AwaitingRel1} -> - NewState = deliver_msg(packet_to_msg(Packet), - State#state{awaiting_rel = AwaitingRel1}), + NewState = deliver(packet_to_msg(Packet), State#state{awaiting_rel = AwaitingRel1}), case AutoAck of true -> send_puback(?PUBCOMP_PACKET(PacketId), NewState); false -> {keep_state, NewState} @@ -960,9 +966,9 @@ retry_send([{Type, Msg, Ts} | Msgs], Now, State = #state{retry_interval = Interv false -> {keep_state, ensure_retry_timer(Interval - Diff, State)} end. -retry_send(publish, Msg = #mqtt_message{qos = QoS, packet_id = PacketId}, +retry_send(publish, Msg = #mqtt_msg{qos = QoS, packet_id = PacketId}, Now, State = #state{inflight = Inflight}) -> - Msg1 = Msg#mqtt_message{dup = (QoS =:= ?QOS1)}, + Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS1)}, case send(Msg1, State) of {ok, NewState} -> Inflight1 = emqx_inflight:update(PacketId, {publish, Msg1, Now}, Inflight), @@ -979,42 +985,36 @@ retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> Error end. -deliver_msg(#mqtt_message{qos = QoS, - dup = Dup, - retain = Retain, - topic = Topic, - packet_id = PacketId, - properties = Properties, - payload = Payload}, - State = #state{owner = Owner}) -> - Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, - packet_id => PacketId, topic => Topic, - properties => Properties, payload => Payload}}, +deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}, + State = #state{owner = Owner}) -> + Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId, + topic => Topic, properties => Props, payload => Payload, + client_pid => self()}}, State. -packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload)) -> - #mqtt_packet_header{qos = QoS, retain = R, dup = Dup} = Header, - #mqtt_message{qos = QoS, retain = R, dup = Dup, - packet_id = PacketId, topic = Topic, - properties = Properties, payload = Payload}. +packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + dup = Dup, + qos = QoS, + retain = R}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Props}, + payload = Payload}) -> + #mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}. -msg_to_packet(#mqtt_message{qos = Qos, - dup = Dup, - retain = Retain, - topic = Topic, - packet_id = PacketId, - properties = Properties, - payload = Payload}) -> +msg_to_packet(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}) -> #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos, + qos = QoS, retain = Retain, dup = Dup}, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId, - properties = Properties}, + properties = Props}, payload = Payload}. - %%------------------------------------------------------------------------------ %% Socket Connect/Send @@ -1040,7 +1040,7 @@ send_puback(Packet, State) -> {error, Reason} -> {stop, Reason} end. -send(Msg, State) when is_record(Msg, mqtt_message) -> +send(Msg, State) when is_record(Msg, mqtt_msg) -> send(msg_to_packet(Msg), State); send(Packet, State = #state{socket = Sock, proto_ver = Ver}) diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 0adb07603..f05d63a29 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -84,7 +84,7 @@ unregister_client(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid %% @doc Lookup client pid -spec(lookup_client_pid(client_id()) -> pid() | undefined). lookup_client_pid(ClientId) when is_binary(ClientId) -> - case lookup_client_pid(ClientId) of + case ets:lookup(?CLIENT, ClientId) of [] -> undefined; [{_, Pid}] -> Pid end. diff --git a/src/emqx_config.erl b/src/emqx_config.erl index d1456f644..2b96f88fc 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -33,15 +33,15 @@ -define(APP, emqx). +%% @doc Get environment +-spec(get_env(Key :: atom()) -> term() | undefined). +get_env(Key) -> + get_env(Key, undefined). + -spec(get_env(Key :: atom(), Default :: term()) -> term()). get_env(Key, Default) -> application:get_env(?APP, Key, Default). -%% @doc Get environment --spec(get_env(Key :: atom()) -> {ok, any()} | undefined). -get_env(Key) -> - application:get_env(?APP, Key). - %% TODO: populate(_App) -> ok. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 544646701..38c100297 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -17,46 +17,53 @@ -behaviour(gen_server). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). - -include("emqx_misc.hrl"). --import(proplists, [get_value/2, get_value/3]). - -%% API Function Exports -export([start_link/3]). - -%% Management and Monitor API --export([info/1, stats/1, kick/1, clean_acl_cache/2]). - --export([set_rate_limit/2, get_rate_limit/1]). - -%% SUB/UNSUB Asynchronously. Called by plugins. --export([subscribe/2, unsubscribe/2]). - -%% Get the session proc? +-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 Function Exports +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -%% Unused fields: connname, peerhost, peerport --record(state, {transport, socket, peername, conn_state, await_recv, - rate_limit, max_packet_size, proto_state, parse_state, - keepalive, enable_stats, idle_timeout, force_gc_count}). +-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 + }). --define(INFO_KEYS, [peername, conn_state, await_recv]). +-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), - emqx_logger:Level("Client(~s): " ++ Format, - [esockd_net:format(State#state.peername) | Args])). + emqx_logger:Level("Client(~s): " ++ Format, + [esockd_net:format(State#state.peername) | Args])). -start_link(Transport, Sock, Env) -> - {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Env]])}. +start_link(Transport, Socket, Options) -> + {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Socket, Options]])}. + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ info(CPid) -> gen_server:call(CPid, info). @@ -67,156 +74,141 @@ stats(CPid) -> kick(CPid) -> gen_server:call(CPid, kick). -set_rate_limit(CPid, Rl) -> - gen_server:call(CPid, {set_rate_limit, Rl}). +session(CPid) -> + gen_server:call(CPid, session, infinity). + +clean_acl_cache(CPid) -> + gen_server:call(CPid, clean_acl_cache). get_rate_limit(CPid) -> gen_server:call(CPid, get_rate_limit). -subscribe(CPid, TopicTable) -> - CPid ! {subscribe, TopicTable}. +set_rate_limit(CPid, Rl = {_Rate, _Burst}) -> + gen_server:call(CPid, {set_rate_limit, Rl}). -unsubscribe(CPid, Topics) -> - CPid ! {unsubscribe, Topics}. +get_pub_limit(CPid) -> + gen_server:call(CPid, get_pub_limit). -session(CPid) -> - gen_server:call(CPid, session, infinity). +set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> + gen_server:call(CPid, {set_pub_limit, Rl}). -clean_acl_cache(CPid, Topic) -> - gen_server:call(CPid, {clean_acl_cache, Topic}). - -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -init([Transport, Sock, Env]) -> - case Transport:wait(Sock) of - {ok, NewSock} -> - {ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), - do_init(Transport, Sock, Peername, Env); +init([Transport, RawSocket, Options]) -> + case Transport:wait(RawSocket) of + {ok, Socket} -> + Zone = proplists:get_value(zone, 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), + 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}), + gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], + State, self(), IdleTimout); {error, Reason} -> {stop, Reason} end. -do_init(Transport, Sock, Peername, Env) -> - RateLimit = get_value(rate_limit, Env), - PacketSize = get_value(max_packet_size, Env, ?MAX_PACKET_SIZE), - SendFun = send_fun(Transport, Sock, Peername), - ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Env), - EnableStats = get_value(client_enable_stats, Env, false), - IdleTimout = get_value(client_idle_timeout, Env, 30000), - ForceGcCount = emqx_gc:conn_max_gc_count(), - State = run_socket(#state{transport = Transport, - socket = Sock, - peername = Peername, - await_recv = false, - conn_state = running, - rate_limit = RateLimit, - max_packet_size = PacketSize, - proto_state = ProtoState, - enable_stats = EnableStats, - idle_timeout = IdleTimout, - force_gc_count = ForceGcCount}), - gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], - init_parse_state(State), self(), IdleTimout). +rate_limit(undefined) -> + undefined; +rate_limit({Rate, Burst}) -> + esockd_rate_limit:new(Rate, Burst). -send_fun(Transport, Sock, Peername) -> - Self = self(), - fun(Packet) -> - Data = emqx_frame:serialize(Packet), - ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), - emqx_metrics:inc('bytes/sent', iolist_size(Data)), - try Transport:async_send(Sock, Data) of - ok -> ok; - {error, Reason} -> Self ! {shutdown, Reason} +send_fun(Transport, Socket, Peername) -> + fun(Data) -> + try Transport:async_send(Socket, Data) of + ok -> + ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), + emqx_metrics:inc('bytes/sent', iolist_size(Data)), ok; + Error -> Error catch - error:Error -> Self ! {shutdown, Error} + error:Error -> {error, Error} end end. -init_parse_state(State = #state{max_packet_size = Size, proto_state = ProtoState}) -> - Version = emqx_protocol:get(proto_ver, ProtoState), - State#state{parse_state = emqx_frame:initial_state( - #{max_packet_size => Size, version => Version})}. +handle_call(info, From, State = #state{transport = Transport, socket = Socket, proto_state = ProtoState}) -> + 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}; -handle_call(info, From, State = #state{proto_state = ProtoState}) -> - ProtoInfo = emqx_protocol:info(ProtoState), - ClientInfo = ?record_to_proplist(state, State, ?INFO_KEYS), - {reply, Stats, _, _} = handle_call(stats, From, State), - reply(lists:append([ClientInfo, ProtoInfo, Stats]), State); - -handle_call(stats, _From, State = #state{proto_state = ProtoState}) -> - reply(lists:append([emqx_misc:proc_stats(), - emqx_protocol:stats(ProtoState), - sock_stats(State)]), State); +handle_call(stats, _From, State = #state{transport = Transport, socket = Sock, proto_state = ProtoState}) -> + ProcStats = emqx_misc:proc_stats(), + ProtoStats = emqx_protocol:stats(ProtoState), + SockStats = case Transport:getstat(Sock, ?SOCK_STATS) of + {ok, Ss} -> Ss; + {error, _} -> [] + end, + {reply, lists:append([ProcStats, ProtoStats, SockStats]), State}; handle_call(kick, _From, State) -> {stop, {shutdown, kick}, ok, State}; -handle_call({set_rate_limit, Rl}, _From, State) -> - reply(ok, State#state{rate_limit = Rl}); +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(Rl, State); + {reply, esockd_rate_limit:info(Rl), State}; -handle_call(session, _From, State = #state{proto_state = ProtoState}) -> - reply(emqx_protocol:session(ProtoState), State); +handle_call({set_rate_limit, {Rate, Burst}}, _From, State) -> + {reply, ok, State#state{rate_limit = esockd_rate_limit:new(Rate, Burst)}}; -handle_call({clean_acl_cache, Topic}, _From, State) -> - erase({acl, publish, Topic}), - reply(ok, State); +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, ignore, State}. + ?LOG(error, "unexpected call: ~p", [Req], State), + {reply, ignored, State}. handle_cast(Msg, State) -> - ?LOG(error, "Unexpected Cast: ~p", [Msg], State), + ?LOG(error, "unexpected cast: ~p", [Msg], State), {noreply, State}. -handle_info({subscribe, TopicTable}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:subscribe(TopicTable, ProtoState) - end, State); +handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> + case emqx_protocol:deliver(PubOrAck, ProtoState) of + {ok, ProtoState1} -> + {noreply, maybe_gc(ensure_stats_timer(State#state{proto_state = ProtoState1}))}; + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) + end; -handle_info({unsubscribe, Topics}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:unsubscribe(Topics, ProtoState) - end, State); - -%% Asynchronous SUBACK -handle_info({suback, PacketId, GrantedQos}, State) -> - with_proto( - fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, GrantedQos), - emqx_protocol:send(Packet, ProtoState) - end, State); - -%% Fastlane -handle_info({dispatch, _Topic, Msg}, State) -> - handle_info({deliver, emqx_message:set_flag(qos, ?QOS_0, Msg)}, State); - -handle_info({deliver, Message}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:send(Message, ProtoState) - end, State); - -handle_info({redeliver, {?PUBREL, PacketId}}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:pubrel(PacketId, ProtoState) - end, State); - -handle_info(emit_stats, State) -> - {noreply, emit_stats(State), hibernate}; +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), + {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> shutdown(idle_timeout, State); -%% Fix issue #535 handle_info({shutdown, Error}, State) -> shutdown(Error, State); @@ -225,25 +217,25 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> shutdown(conflict, State); handle_info(activate_sock, State) -> - {noreply, run_socket(State#state{conn_state = running})}; + {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), emqx_metrics:inc('bytes/received', Size), - received(Data, rate_limit(Size, State#state{await_recv = false})); + Incoming = #{bytes => Size, packets => 0}, + handle_packet(Data, State#state{await_recv = false, incoming = Incoming}); handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> shutdown(Reason, State); handle_info({inet_reply, _Sock, ok}, State) -> - {noreply, gc(State)}; %% Tune GC + {noreply, State}; handle_info({inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, - State = #state{transport = Transport, socket = Sock}) -> +handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Sock}) -> ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), StatFun = fun() -> case Transport:getstat(Sock, [recv_oct]) of @@ -272,20 +264,18 @@ handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> end; handle_info(Info, State) -> - ?LOG(error, "Unexpected Info: ~p", [Info], State), + ?LOG(error, "unexpected info: ~p", [Info], State), {noreply, State}. terminate(Reason, State = #state{transport = Transport, socket = Sock, keepalive = KeepAlive, proto_state = ProtoState}) -> - ?LOG(debug, "Terminated for ~p", [Reason], State), Transport:fast_close(Sock), emqx_keepalive:cancel(KeepAlive), case {ProtoState, Reason} of - {undefined, _} -> - ok; + {undefined, _} -> ok; {_, {shutdown, Error}} -> emqx_protocol:shutdown(Error, ProtoState); {_, Reason} -> @@ -295,25 +285,29 @@ terminate(Reason, State = #state{transport = Transport, code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Internal functions -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -%% Receive and Parse TCP Data -received(<<>>, State) -> - {noreply, gc(State)}; +%% Receive and parse data +handle_packet(<<>>, State) -> + {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; -received(Bytes, State = #state{parse_state = ParseState, - proto_state = ProtoState, - idle_timeout = IdleTimeout}) -> - case catch emqx_frame:parse(Bytes, ParseState) of - {more, NewParseState} -> - {noreply, State#state{parse_state = NewParseState}, IdleTimeout}; - {ok, Packet, Rest} -> +handle_packet(Bytes, State = #state{incoming = Incoming, + parser_state = ParserState, + proto_state = ProtoState, + idle_timeout = IdleTimeout}) -> + case catch emqx_frame:parse(Bytes, 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} -> - received(Rest, init_parse_state(State#state{proto_state = ProtoState1})); + ParserState1 = emqx_protocol:parser(ProtoState1), + handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), + proto_state = ProtoState1, + parser_state = ParserState1}); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); @@ -326,22 +320,33 @@ received(Bytes, State = #state{parse_state = ParseState, ?LOG(error, "Framing error - ~p", [Error], State), shutdown(Error, State); {'EXIT', Reason} -> - ?LOG(error, "Parser failed for ~p", [Reason], State), - ?LOG(error, "Error data: ~p", [Bytes], State), - shutdown(parser_error, State) + ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Bytes], State), + shutdown(parse_error, State) end. -rate_limit(_Size, State = #state{rate_limit = undefined}) -> +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. + +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). + +ensure_rate_limit([], State) -> run_socket(State); -rate_limit(Size, State = #state{rate_limit = Rl}) -> - case Rl:check(Size) of - {0, Rl1} -> - run_socket(State#state{conn_state = running, rate_limit = Rl1}); - {Pause, Rl1} -> - ?LOG(warning, "Rate limiter pause for ~p", [Pause], State), - erlang:send_after(Pause, self(), activate_sock), - State#state{conn_state = blocked, rate_limit = Rl1} - end. +ensure_rate_limit([{undefined, _Pos, _Num}|Limiters], State) -> + ensure_rate_limit(Limiters, State); +ensure_rate_limit([{Rl, Pos, Num}|Limiters], State) -> + case esockd_rate_limit:check(Num, Rl) of + {0, Rl1} -> + ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); + {Pause, Rl1} -> + TRef = erlang:send_after(Pause, self(), activate_sock), + setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1) + end. run_socket(State = #state{conn_state = blocked}) -> State; @@ -351,38 +356,21 @@ run_socket(State = #state{transport = Transport, socket = Sock}) -> Transport:async_recv(Sock, 0, infinity), State#state{await_recv = true}. -with_proto(Fun, State = #state{proto_state = ProtoState}) -> - {ok, ProtoState1} = Fun(ProtoState), - {noreply, State#state{proto_state = ProtoState1}}. - -emit_stats(State = #state{proto_state = ProtoState}) -> - emit_stats(emqx_protocol:clientid(ProtoState), State). - -emit_stats(_ClientId, State = #state{enable_stats = false}) -> - State; -emit_stats(undefined, State) -> - State; -emit_stats(ClientId, State) -> - {reply, Stats, _, _} = handle_call(stats, undefined, State), - emqx_cm:set_client_stats(ClientId, Stats), +ensure_stats_timer(State = #state{enable_stats = true, + stats_timer = undefined, + idle_timeout = IdleTimeout}) -> + State#state{stats_timer = erlang:send_after(IdleTimeout, self(), emit_stats)}; +ensure_stats_timer(State) -> State. -sock_stats(#state{transport = Transport, socket = Sock}) -> - case Transport:getstat(Sock, ?SOCK_STATS) of - {ok, Ss} -> Ss; - _Error -> [] - end. - -reply(Reply, State) -> - {reply, Reply, State, hibernate}. - shutdown(Reason, State) -> stop({shutdown, Reason}, State). stop(Reason, State) -> {stop, Reason, State}. -gc(State = #state{transport = Transport, socket = Sock}) -> - Cb = fun() -> Transport:gc(Sock), emit_stats(State) end, - emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). +maybe_gc(State) -> + State. %% TODO:... + %%Cb = fun() -> Transport:gc(Sock), end, + %%emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 46d97e757..c16bd23cb 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -18,7 +18,7 @@ -export([start_link/0]). -export([register_command/2, register_command/3, unregister_command/1]). --export([run_command/2, lookup_command/1]). +-export([run_command/1, run_command/2, lookup_command/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -48,7 +48,21 @@ unregister_command(Cmd) when is_atom(Cmd) -> cast(Msg) -> gen_server:cast(?SERVER, Msg). +run_command([]) -> + run_command(help, []); +run_command([Cmd | Args]) -> + run_command(list_to_atom(Cmd), Args). + -spec(run_command(cmd(), [string()]) -> ok | {error, term()}). +run_command(set, []) -> + emqx_mgmt_cli_cfg:set_usage(), ok; + +run_command(set, Args) -> + emqx_mgmt_cli_cfg:run(["config" | Args]), ok; + +run_command(show, Args) -> + emqx_mgmt_cli_cfg:run(["config" | Args]), ok; + run_command(help, []) -> usage(); run_command(Cmd, Args) when is_atom(Cmd) -> diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 0a261db6e..10498afcf 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -121,7 +121,7 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> < is_bridge = (BridgeTag =:= 8), clean_start = bool(CleanStart), will_flag = bool(WillFlag), - will_qos = WillQos, + will_qos = WillQoS, will_retain = bool(WillRetain), keepalive = KeepAlive, properties = Properties, @@ -162,7 +162,6 @@ parse_packet(#mqtt_packet_header{type = ?PUBLISH, qos = QoS}, Bin, ?QOS_0 -> {undefined, Rest}; _ -> parse_packet_id(Rest) end, - io:format("Rest1: ~p~n", [Rest1]), {Properties, Payload} = parse_properties(Rest1, Ver), {#mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId, @@ -242,6 +241,9 @@ parse_packet_id(<>) -> parse_properties(Bin, Ver) when Ver =/= ?MQTT_PROTO_V5 -> {undefined, Bin}; +%% TODO: version mess? +parse_properties(<<>>, ?MQTT_PROTO_V5) -> + {#{}, <<>>}; parse_properties(<<0, Rest/binary>>, ?MQTT_PROTO_V5) -> {#{}, Rest}; parse_properties(Bin, ?MQTT_PROTO_V5) -> @@ -328,7 +330,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, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} || <> <= Bin]; parse_topic_filters(unsubscribe, Bin) -> @@ -382,7 +384,7 @@ serialize_variable(#mqtt_packet_connect{ is_bridge = IsBridge, clean_start = CleanStart, will_flag = WillFlag, - will_qos = WillQos, + will_qos = WillQoS, will_retain = WillRetain, keepalive = KeepAlive, properties = Properties, @@ -400,7 +402,7 @@ serialize_variable(#mqtt_packet_connect{ (flag(Username)):1, (flag(Password)):1, (flag(WillRetain)):1, - WillQos:2, + WillQoS:2, (flag(WillFlag)):1, (flag(CleanStart)):1, 0:1, diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 2cc0b2a1a..6b1d43207 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -27,8 +27,8 @@ -spec(conn_max_gc_count() -> integer()). conn_max_gc_count() -> case emqx_config:get_env(conn_force_gc_count) of - {ok, I} when I > 0 -> I + rand:uniform(I); - {ok, I} when I =< 0 -> undefined; + I when is_integer(I), I > 0 -> I + rand:uniform(I); + I when is_integer(I), I =< 0 -> undefined; undefined -> undefined end. diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index 43855c734..fa9139ebd 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -129,5 +129,6 @@ to_base62(<>) -> emqx_base62:encode(I). from_base62(S) -> - I = emqx_base62:decode(S), <>. + I = emqx_base62:decode(S, integer), + <>. diff --git a/src/emqx_kernel_sup.erl b/src/emqx_kernel_sup.erl index 96dde598c..40ec7cfd7 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -26,11 +26,12 @@ start_link() -> init([]) -> {ok, {{one_for_one, 10, 100}, [child_spec(emqx_pool, supervisor), - child_spec(emqx_alarm, worker), + child_spec(emqx_alarm_mgr, worker), child_spec(emqx_hooks, worker), child_spec(emqx_stats, worker), child_spec(emqx_metrics, worker), child_spec(emqx_ctl, worker), + child_spec(emqx_zone, worker), child_spec(emqx_tracer, worker)]}}. child_spec(M, worker) -> diff --git a/src/emqx_lager_backend.erl b/src/emqx_lager_backend.erl deleted file mode 100644 index b002ff7af..000000000 --- a/src/emqx_lager_backend.erl +++ /dev/null @@ -1,84 +0,0 @@ -%% 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. - --module(emqx_lager_backend). - --behaviour(gen_event). - --include_lib("lager/include/lager.hrl"). - --export([init/1, handle_call/2, handle_event/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {level :: {'mask', integer()}, - formatter :: atom(), - format_config :: any()}). - --define(DEFAULT_FORMAT, [time, " ", pid, " [",severity, "] ", message]). - -init([Level]) when is_atom(Level) -> - init(Level); - -init(Level) when is_atom(Level) -> - init([Level,{lager_default_formatter, ?DEFAULT_FORMAT}]); - -init([Level,{Formatter, FormatterConfig}]) when is_atom(Formatter) -> - Levels = lager_util:config_to_mask(Level), - {ok, #state{level = Levels, formatter = Formatter, - format_config = FormatterConfig}}. - -handle_call(get_loglevel, #state{level = Level} = State) -> - {ok, Level, State}; - -handle_call({set_loglevel, Level}, State) -> - try lager_util:config_to_mask(Level) of - Levels -> {ok, ok, State#state{level = Levels}} - catch - _:_ -> {ok, {error, bad_log_level}, State} - end; - -handle_call(_Request, State) -> - {ok, ok, State}. - -handle_event({log, Message}, State = #state{level = L}) -> - case lager_util:is_loggable(Message, L, ?MODULE) of - true -> - publish_log(Message, State); - false -> - {ok, State} - end; - -handle_event(_Event, State) -> - {ok, State}. - -handle_info(_Info, State) -> - {ok, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -publish_log(Message, State = #state{formatter = Formatter, - format_config = FormatConfig}) -> - Severity = lager_msg:severity(Message), - Payload = Formatter:format(Message, FormatConfig), - Msg = emqx_message:make(log, topic(Severity), iolist_to_binary(Payload)), - emqx:publish(emqx_message:set_flag(sys, Msg)), - {ok, State}. - -topic(Severity) -> - emqx_topic:systop(list_to_binary(lists:concat(['logs/', Severity]))). - diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 02de39123..084ffe7c2 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -12,76 +12,98 @@ %% See the License for the specific language governing permissions and %% limitations under the License. -%% @doc start/stop MQTT listeners. +%% @doc Start/Stop MQTT listeners. -module(emqx_listeners). -include("emqx_mqtt.hrl"). --export([start_all/0, restart_all/0, stop_all/0]). +-export([start/0, restart/0, stop/0]). -export([start_listener/1, stop_listener/1, restart_listener/1]). -type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). %% @doc Start all listeners --spec(start_all() -> ok). -start_all() -> +-spec(start() -> ok). +start() -> lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])). %% Start MQTT/TCP listener -spec(start_listener(listener()) -> {ok, pid()} | {error, term()}). start_listener({tcp, ListenOn, Options}) -> start_mqtt_listener('mqtt:tcp', ListenOn, Options); + %% Start MQTT/TLS listener start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> - start_mqtt_listener('mqtt:tls', ListenOn, Options); + start_mqtt_listener('mqtt:ssl', ListenOn, Options); + %% Start MQTT/WS listener start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> - start_http_listener('mqtt:ws', ListenOn, Options); + Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]), + NumAcceptors = proplists:get_value(acceptors, Options, 4), + MaxConnections = proplists:get_value(max_connections, Options, 1024), + TcpOptions = proplists:get_value(tcp_options, Options, []), + RanchOpts = [{num_acceptors, NumAcceptors}, + {max_connections, MaxConnections} | TcpOptions], + cowboy:start_clear('mqtt:ws', with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}); + %% Start MQTT/WSS listener start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> - start_http_listener('mqtt:wss', ListenOn, Options). + Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws, []}]}]), + NumAcceptors = proplists:get_value(acceptors, Options, 4), + MaxConnections = proplists:get_value(max_clients, Options, 1024), + TcpOptions = proplists:get_value(tcp_options, Options, []), + SslOptions = proplists:get_value(ssl_options, Options, []), + RanchOpts = [{num_acceptors, NumAcceptors}, + {max_connections, MaxConnections} | TcpOptions ++ SslOptions], + cowboy:start_tls('mqtt:wss', with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}). start_mqtt_listener(Name, ListenOn, Options) -> - {ok, _} = esockd:open(Name, ListenOn, merge_sockopts(Options), {emqx_connection, start_link, []}). + SockOpts = esockd:parse_opt(Options), + MFA = {emqx_connection, start_link, [Options -- SockOpts]}, + {ok, _} = esockd:open(Name, ListenOn, merge_default(SockOpts), MFA). -start_http_listener(Name, ListenOn, Options) -> - {ok, _} = mochiweb:start_http(Name, ListenOn, Options, {emqx_ws, handle_request, []}). +with_port(Port, Opts) when is_integer(Port) -> + [{port, Port}|Opts]; +with_port({Addr, Port}, Opts) -> + [{ip, Addr}, {port, Port}|Opts]. %% @doc Restart all listeners --spec(restart_all() -> ok). -restart_all() -> +-spec(restart() -> ok). +restart() -> lists:foreach(fun restart_listener/1, emqx_config:get_env(listeners, [])). -spec(restart_listener(listener()) -> any()). -restart_listener({tcp, ListenOn, _Opts}) -> +restart_listener({tcp, ListenOn, _Options}) -> esockd:reopen('mqtt:tcp', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> - esockd:reopen('mqtt:tls', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:restart_http('mqtt:ws', ListenOn); -restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:restart_http('mqtt:wss', ListenOn); +restart_listener({Proto, ListenOn, _Options}) when Proto == ssl; Proto == tls -> + esockd:reopen('mqtt:ssl', ListenOn); +restart_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> + cowboy:stop_listener('mqtt:ws'), + start_listener({Proto, ListenOn, Options}); +restart_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> + cowboy:stop_listener('mqtt:wss'), + start_listener({Proto, ListenOn, Options}); restart_listener({Proto, ListenOn, _Opts}) -> esockd:reopen(Proto, ListenOn). %% @doc Stop all listeners --spec(stop_all() -> ok). -stop_all() -> +-spec(stop() -> ok). +stop() -> lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])). -spec(stop_listener(listener()) -> ok | {error, any()}). stop_listener({tcp, ListenOn, _Opts}) -> esockd:close('mqtt:tcp', ListenOn); stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> - esockd:close('mqtt:tls', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> - mochiweb:stop_http('mqtt:ws', ListenOn); -stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> - mochiweb:stop_http('mqtt:wss', ListenOn); + esockd:close('mqtt:ssl', ListenOn); +stop_listener({Proto, _ListenOn, _Opts}) when Proto == http; Proto == ws -> + cowboy:stop_listener('mqtt:ws'); +stop_listener({Proto, _ListenOn, _Opts}) when Proto == https; Proto == wss -> + cowboy:stop_listener('mqtt:wss'); stop_listener({Proto, ListenOn, _Opts}) -> esockd:close(Proto, ListenOn). -merge_sockopts(Options) -> +merge_default(Options) -> case lists:keytake(tcp_options, 1, Options) of {value, {tcp_options, TcpOpts}, Options1} -> [{tcp_options, emqx_misc:merge_opts(?MQTT_SOCKOPTS, TcpOpts)} | Options1]; @@ -89,11 +111,3 @@ merge_sockopts(Options) -> [{tcp_options, ?MQTT_SOCKOPTS} | Options] end. -%% all() -> -%% [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)]. -%%is_mqtt('mqtt:tcp') -> true; -%%is_mqtt('mqtt:tls') -> true; -%%is_mqtt('mqtt:ws') -> true; -%%is_mqtt('mqtt:wss') -> true; -%%is_mqtt(_Proto) -> false. - diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 3a96f75a6..da762703e 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -17,44 +17,43 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([new/2, new/3, new/4, new/5]). +-export([make/2, make/3, make/4]). +-export([set_flags/2]). -export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]). +-export([set_headers/2]). -export([get_header/2, get_header/3, set_header/3]). --export([get_user_property/2, get_user_property/3, set_user_property/3]). --spec(new(topic(), payload()) -> message()). -new(Topic, Payload) -> - new(undefined, Topic, Payload). +-spec(make(topic(), payload()) -> message()). +make(Topic, Payload) -> + make(undefined, Topic, Payload). --spec(new(atom() | client(), topic(), payload()) -> message()). -new(From, Topic, Payload) when is_atom(From); is_record(From, client) -> - new(From, #{qos => ?QOS0}, Topic, Payload). +-spec(make(atom() | client_id(), topic(), payload()) -> message()). +make(From, Topic, Payload) -> + make(From, ?QOS0, Topic, Payload). --spec(new(atom() | client(), message_flags(), topic(), payload()) -> message()). -new(From, Flags, Topic, Payload) when is_atom(From); is_record(From, client) -> - new(From, Flags, #{}, Topic, Payload). - --spec(new(atom() | client(), message_flags(), message_headers(), topic(), payload()) -> message()). -new(From, Flags, Headers, Topic, Payload) when is_atom(From); is_record(From, client) -> - #message{id = msgid(), +-spec(make(atom() | client_id(), qos(), topic(), payload()) -> message()). +make(From, QoS, Topic, Payload) -> + #message{id = msgid(QoS), + qos = QoS, from = From, - sender = self(), - flags = Flags, - headers = Headers, + flags = #{dup => false}, topic = Topic, - properties = #{}, payload = Payload, timestamp = os:timestamp()}. -msgid() -> emqx_guid:gen(). +msgid(?QOS0) -> undefined; +msgid(_QoS) -> emqx_guid:gen(). + +set_flags(Flags, Msg = #message{flags = undefined}) when is_map(Flags) -> + Msg#message{flags = Flags}; +set_flags(New, Msg = #message{flags = Old}) when is_map(New) -> + Msg#message{flags = maps:merge(Old, New)}. -%% @doc Get flag get_flag(Flag, Msg) -> get_flag(Flag, Msg, false). get_flag(Flag, #message{flags = Flags}, Default) -> maps:get(Flag, Flags, Default). -%% @doc Set flag -spec(set_flag(message_flag(), message()) -> message()). set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, true, Flags)}. @@ -63,27 +62,24 @@ set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> set_flag(Flag, Val, Msg = #message{flags = Flags}) when is_atom(Flag) -> Msg#message{flags = maps:put(Flag, Val, Flags)}. -%% @doc Unset flag -spec(unset_flag(message_flag(), message()) -> message()). unset_flag(Flag, Msg = #message{flags = Flags}) -> Msg#message{flags = maps:remove(Flag, Flags)}. -%% @doc Get header +set_headers(Headers, Msg = #message{headers = undefined}) when is_map(Headers) -> + Msg#message{headers = Headers}; +set_headers(New, Msg = #message{headers = Old}) when is_map(New) -> + Msg#message{headers = maps:merge(Old, New)}; +set_headers(_, Msg) -> + Msg. + get_header(Hdr, Msg) -> get_header(Hdr, Msg, undefined). get_header(Hdr, #message{headers = Headers}, Default) -> maps:get(Hdr, Headers, Default). -%% @doc Set header +set_header(Hdr, Val, Msg = #message{headers = undefined}) -> + Msg#message{headers = #{Hdr => Val}}; set_header(Hdr, Val, Msg = #message{headers = Headers}) -> Msg#message{headers = maps:put(Hdr, Val, Headers)}. -%% @doc Get user property -get_user_property(Key, Msg) -> - get_user_property(Key, Msg, undefined). -get_user_property(Key, #message{properties = Props}, Default) -> - maps:get(Key, Props, Default). - -set_user_property(Key, Val, Msg = #message{properties = Props}) -> - Msg#message{properties = maps:put(Key, Val, Props)}. - diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 519b96fe4..506ff2c0d 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -171,10 +171,10 @@ update_counter(Key, UpOp) -> received(Packet) -> inc('packets/received'), received1(Packet). -received1(?PUBLISH_PACKET(Qos, _PktId)) -> +received1(?PUBLISH_PACKET(QoS, _PktId)) -> inc('packets/publish/received'), inc('messages/received'), - qos_received(Qos); + qos_received(QoS); received1(?PACKET(Type)) -> received2(Type). received2(?CONNECT) -> @@ -206,15 +206,15 @@ qos_received(?QOS_2) -> %% @doc Count packets received. Will not count $SYS PUBLISH. -spec(sent(mqtt_packet()) -> ignore | non_neg_integer()). -sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) -> +sent(?PUBLISH_PACKET(_QoS, <<"$SYS/", _/binary>>, _, _)) -> ignore; sent(Packet) -> inc('packets/sent'), sent1(Packet). -sent1(?PUBLISH_PACKET(Qos, _PktId)) -> +sent1(?PUBLISH_PACKET(QoS, _PktId)) -> inc('packets/publish/sent'), inc('messages/sent'), - qos_sent(Qos); + qos_sent(QoS); sent1(?PACKET(Type)) -> sent2(Type). sent2(?CONNACK) -> diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index e41bd0587..ef70dc28d 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -39,8 +39,7 @@ on_client_connected(ConnAck, Client = #client{id = ClientId, {connack, ConnAck}, {ts, emqx_time:now_secs()}]) of {ok, Payload} -> - Msg = message(qos(Env), topic(connected, ClientId), Payload), - emqx:publish(emqx_message:set_flag(sys, Msg)); + emqx:publish(message(qos(Env), topic(connected, ClientId), Payload)); {error, Reason} -> emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) end, @@ -52,8 +51,7 @@ on_client_disconnected(Reason, #client{id = ClientId, username = Username}, Env) {reason, reason(Reason)}, {ts, emqx_time:now_secs()}]) of {ok, Payload} -> - Msg = message(qos(Env), topic(disconnected, ClientId), Payload), - emqx:publish(emqx_message:set_flag(sys, Msg)); + emqx_broker:publish(message(qos(Env), topic(disconnected, ClientId), Payload)); {error, Reason} -> emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) end, ok. @@ -62,9 +60,9 @@ unload(_Env) -> emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3), emqx:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3). -message(Qos, Topic, Payload) -> - Msg = emqx_message:make(?MODULE, Topic, iolist_to_binary(Payload)), - emqx_message:set_header(qos, Qos, Msg). +message(QoS, Topic, Payload) -> + Msg = emqx_message:make(?MODULE, QoS, Topic, iolist_to_binary(Payload)), + emqx_message:set_flags(#{sys => true}, Msg). topic(connected, ClientId) -> emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/connected"])); diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index 6db5e30f3..978b46a3b 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -34,7 +34,7 @@ load(Topics) -> on_client_connected(RC, Client = #client{id = ClientId, pid = ClientPid, username = Username}, Topics) when RC < 16#80 -> Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, - TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], + TopicTable = [{Replace(Topic), QoS} || {Topic, QoS} <- Topics], ClientPid ! {subscribe, TopicTable}, {ok, Client}; diff --git a/src/emqx_mqtt_properties.erl b/src/emqx_mqtt_properties.erl index 4634d5bdc..643156013 100644 --- a/src/emqx_mqtt_properties.erl +++ b/src/emqx_mqtt_properties.erl @@ -104,17 +104,20 @@ id('Wildcard-Subscription-Available') -> 16#28; id('Subscription-Identifier-Available') -> 16#29; id('Shared-Subscription-Available') -> 16#2A. -filter(Packet, Props) when ?CONNECT =< Packet, Packet =< ?AUTH -> - Fun = fun(Name) -> - case maps:find(id(Name), ?PROPS_TABLE) of - {ok, {Name, _Type, 'ALL'}} -> - true; - {ok, {Name, _Type, Packets}} -> - lists:member(Packet, Packets); - error -> false - end - end, - [Prop || Prop = {Name, _} <- Props, Fun(Name)]. +filter(PacketType, Props) when is_map(Props) -> + maps:from_list(filter(PacketType, maps:to_list(Props))); + +filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_list(Props) -> + Filter = fun(Name) -> + case maps:find(id(Name), ?PROPS_TABLE) of + {ok, {Name, _Type, 'ALL'}} -> + true; + {ok, {Name, _Type, AllowedTypes}} -> + lists:member(PacketType, AllowedTypes); + error -> false + end + end, + [Prop || Prop = {Name, _} <- Props, Filter(Name)]. validate(Props) when is_map(Props) -> lists:foreach(fun validate_prop/1, maps:to_list(Props)). diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index e6bd16540..458c301fc 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -5,8 +5,7 @@ %% 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 +%%%% 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 @@ -21,7 +20,7 @@ %% This module implements a simple in-memory queue for MQTT persistent session. %% %% If the broker restarted or crashed, all the messages queued will be gone. -%% +%% %% Concept of Message Queue and Inflight Window: %% %% |<----------------- Max Len ----------------->| @@ -40,33 +39,25 @@ %% %% @end +%% TODO: ... -module(emqx_mqueue). -%% TODO: XYZ -%% -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). -import(proplists, [get_value/3]). --export([new/3, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1, - dropped/1, stats/1]). - --define(LOW_WM, 0.2). - --define(HIGH_WM, 0.6). +-export([new/2, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1]). +-export([dropped/1, stats/1]). -define(PQUEUE, emqx_pqueue). -type(priority() :: {iolist(), pos_integer()}). --type(option() :: {type, simple | priority} - | {max_length, non_neg_integer()} %% Max queue length - | {priority, list(priority())} - | {low_watermark, float()} %% Low watermark - | {high_watermark, float()} %% High watermark - | {store_qos0, boolean()}). %% Queue Qos0? +-type(options() :: #{type => simple | priority, + max_len => non_neg_integer(), + priority => list(priority()), + store_qos0 => boolean()}). -type(stat() :: {max_len, non_neg_integer()} | {len, non_neg_integer()} @@ -78,31 +69,22 @@ pseq = 0, priorities = [], %% len of simple queue len = 0, max_len = 0, - low_wm = ?LOW_WM, high_wm = ?HIGH_WM, - qos0 = false, dropped = 0, - alarm_fun}). + qos0 = false, dropped = 0}). -type(mqueue() :: #mqueue{}). --export_type([mqueue/0, priority/0, option/0]). +-export_type([mqueue/0, priority/0, options/0]). -%% @doc New Queue. --spec(new(iolist(), list(option()), fun()) -> mqueue()). -new(Name, Opts, AlarmFun) -> - Type = get_value(type, Opts, simple), - MaxLen = get_value(max_length, Opts, 0), +-spec(new(iolist(), options()) -> mqueue()). +new(Name, #{type := Type, max_len := MaxLen, store_qos0 := StoreQos0}) -> init_q(#mqueue{type = Type, name = iolist_to_binary(Name), - len = 0, max_len = MaxLen, - low_wm = low_wm(MaxLen, Opts), - high_wm = high_wm(MaxLen, Opts), - qos0 = get_value(store_qos0, Opts, false), - alarm_fun = AlarmFun}, Opts). + len = 0, max_len = MaxLen, qos0 = StoreQos0}). -init_q(MQ = #mqueue{type = simple}, _Opts) -> +init_q(MQ = #mqueue{type = simple}) -> MQ#mqueue{q = queue:new()}; -init_q(MQ = #mqueue{type = priority}, Opts) -> - Priorities = get_value(priority, Opts, []), - init_p(Priorities, MQ#mqueue{q = ?PQUEUE:new()}). +init_q(MQ = #mqueue{type = priority}) -> + %%Priorities = get_value(priority, Opts, []), + init_p([], MQ#mqueue{q = ?PQUEUE:new()}). init_p([], MQ) -> MQ; @@ -114,16 +96,6 @@ insert_p(Topic, P, MQ = #mqueue{priorities = Tab, pseq = Seq}) -> <> = <>, {PInt, MQ#mqueue{priorities = [{Topic, PInt} | Tab], pseq = Seq + 1}}. -low_wm(0, _Opts) -> - undefined; -low_wm(MaxLen, Opts) -> - round(MaxLen * get_value(low_watermark, Opts, ?LOW_WM)). - -high_wm(0, _Opts) -> - undefined; -high_wm(MaxLen, Opts) -> - round(MaxLen * get_value(high_watermark, Opts, ?HIGH_WM)). - -spec(name(mqueue()) -> iolist()). name(#mqueue{name = Name}) -> Name. @@ -163,7 +135,7 @@ in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = MaxLen, dropped {{value, _Old}, Q2} = queue:out(Q), MQ#mqueue{q = queue:in(Msg, Q2), dropped = Dropped +1}; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len}) -> - maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}); + MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, priorities = Priorities, @@ -176,8 +148,8 @@ in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, MQ1#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)} end; in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, - priorities = Priorities, - max_len = MaxLen}) -> + priorities = Priorities, + max_len = MaxLen}) -> case lists:keysearch(Topic, 1, Priorities) of {value, {_, Pri}} -> case ?PQUEUE:plen(Pri, Q) >= MaxLen of @@ -199,28 +171,8 @@ out(MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> {R, MQ#mqueue{q = Q2, len = Len - 1}}; out(MQ = #mqueue{type = simple, q = Q, len = Len}) -> {R, Q2} = queue:out(Q), - {R, maybe_clear_alarm(MQ#mqueue{q = Q2, len = Len - 1})}; + {R, MQ#mqueue{q = Q2, len = Len - 1}}; out(MQ = #mqueue{type = priority, q = Q}) -> {R, Q2} = ?PQUEUE:out(Q), {R, MQ#mqueue{q = Q2}}. -maybe_set_alarm(MQ = #mqueue{high_wm = undefined}) -> - MQ; -maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm_fun = AlarmFun}) - when Len > HighWM -> - Alarm = #alarm{id = iolist_to_binary(["queue_high_watermark.", Name]), - severity = warning, - title = io_lib:format("Queue ~s high-water mark", [Name]), - summary = io_lib:format("queue len ~p > high_watermark ~p", [Len, HighWM])}, - MQ#mqueue{alarm_fun = AlarmFun(alert, Alarm)}; -maybe_set_alarm(MQ) -> - MQ. - -maybe_clear_alarm(MQ = #mqueue{low_wm = undefined}) -> - MQ; -maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_wm = LowWM, alarm_fun = AlarmFun}) - when Len < LowWM -> - MQ#mqueue{alarm_fun = AlarmFun(clear, list_to_binary(["queue_high_watermark.", Name]))}; -maybe_clear_alarm(MQ) -> - MQ. - diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index dc88d59d7..65f125f68 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -15,12 +15,11 @@ -module(emqx_packet). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). -export([protocol_name/1, type_name/1]). -export([format/1]). --export([to_message/1, from_message/1]). +-export([to_message/2, from_message/2]). %% @doc Protocol name of version -spec(protocol_name(mqtt_version()) -> binary()). @@ -34,43 +33,40 @@ type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). %% @doc From Message to Packet --spec(from_message(message()) -> mqtt_packet()). -from_message(Msg = #message{topic = Topic, payload = Payload}) -> - Qos = emqx_message:get_flag(qos, Msg, 0), +-spec(from_message(mqtt_packet_id(), message()) -> mqtt_packet()). +from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) -> Dup = emqx_message:get_flag(dup, Msg, false), Retain = emqx_message:get_flag(retain, Msg, false), - PacketId = emqx_message:get_header(packet_id, Msg), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos, + qos = QoS, retain = Retain, dup = Dup}, variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId}, + packet_id = PacketId, + properties = #{}}, %%TODO: payload = Payload}. %% @doc Message from Packet --spec(to_message(mqtt_packet()) -> message()). -to_message(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - retain = Retain, - qos = Qos, - dup = Dup}, - variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId, - properties = Properties}, - payload = Payload}) -> - Flags = #{dup => Dup, retain => Retain, qos => Qos}, - Msg = emqx_message:new(undefined, Flags, #{packet_id => PacketId}, Topic, Payload), - Msg#message{properties = Properties}; +-spec(to_message(client_id(), mqtt_packet()) -> message()). +to_message(ClientId, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + retain = Retain, + qos = QoS, + dup = Dup}, + variable = #mqtt_packet_publish{topic_name = Topic, + properties = Props}, + payload = Payload}) -> + Msg = emqx_message:make(ClientId, QoS, Topic, Payload), + Msg#message{flags = #{dup => Dup, retain => Retain}, headers = Props}; -to_message(#mqtt_packet_connect{will_flag = false}) -> +to_message(_ClientId, #mqtt_packet_connect{will_flag = false}) -> undefined; -to_message(#mqtt_packet_connect{will_retain = Retain, - will_qos = Qos, - will_topic = Topic, - will_props = Props, - will_payload = Payload}) -> - Msg = emqx_message:new(undefined, #{qos => Qos, retain => Retain}, Topic, Payload), - Msg#message{properties = Props}. +to_message(ClientId, #mqtt_packet_connect{will_retain = Retain, + will_qos = QoS, + will_topic = Topic, + will_props = Props, + will_payload = Payload}) -> + Msg = emqx_message:make(ClientId, QoS, Topic, Payload), + Msg#message{flags = #{qos => QoS, retain => Retain}, headers = Props}. %% @doc Format packet -spec(format(mqtt_packet()) -> iolist()). @@ -110,15 +106,15 @@ format_variable(#mqtt_packet_connect{ Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanStart=~s, KeepAlive=~p, Username=~s, Password=~s", Args = [ClientId, ProtoName, ProtoVer, CleanStart, KeepAlive, Username, format_password(Password)], {Format1, Args1} = if - WillFlag -> { Format ++ ", Will(Q~p, R~p, Topic=~s, Payload=~p)", - Args ++ [WillQoS, i(WillRetain), WillTopic, WillPayload] }; + WillFlag -> {Format ++ ", Will(Q~p, R~p, Topic=~s, Payload=~p)", + Args ++ [WillQoS, i(WillRetain), WillTopic, WillPayload]}; true -> {Format, Args} end, io_lib:format(Format1, Args1); format_variable(#mqtt_packet_connack{ack_flags = AckFlags, reason_code = ReasonCode}) -> - io_lib:format("AckFlags=~p, RetainCode=~p", [AckFlags, ReasonCode]); + io_lib:format("AckFlags=~p, ReasonCode=~p", [AckFlags, ReasonCode]); format_variable(#mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId}) -> @@ -153,3 +149,4 @@ format_password(_Password) -> '******'. i(true) -> 1; i(false) -> 0; i(I) when is_integer(I) -> I. + diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 837ed8a0e..0c03e827e 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -32,12 +32,11 @@ -spec(init() -> ok). init() -> case emqx_config:get_env(plugins_etc_dir) of - {ok, PluginsEtc} -> + undefined -> ok; + PluginsEtc -> CfgFiles = [filename:join(PluginsEtc, File) || - File <- filelib:wildcard("*.config", PluginsEtc)], - lists:foreach(fun init_config/1, CfgFiles); - undefined -> - ok + File <- filelib:wildcard("*.config", PluginsEtc)], + lists:foreach(fun init_config/1, CfgFiles) end. init_config(CfgFile) -> @@ -51,25 +50,24 @@ init_config(CfgFile) -> load() -> load_expand_plugins(), case emqx_config:get_env(plugins_loaded_file) of - {ok, File} -> + undefined -> %% No plugins available + ignore; + File -> ensure_file(File), - with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end); - undefined -> - %% No plugins available - ignore + with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end) end. load_expand_plugins() -> case emqx_config:get_env(expand_plugins_dir) of - {ok, Dir} -> + undefined -> ok; + Dir -> PluginsDir = filelib:wildcard("*", Dir), lists:foreach(fun(PluginDir) -> case filelib:is_dir(Dir ++ PluginDir) of true -> load_expand_plugin(Dir ++ PluginDir); false -> ok end - end, PluginsDir); - _ -> ok + end, PluginsDir) end. load_expand_plugin(PluginDir) -> @@ -102,7 +100,8 @@ init_expand_plugin_config(PluginDir) -> get_expand_plugin_config() -> case emqx_config:get_env(expand_plugins_dir) of - {ok, Dir} -> + undefined -> ok; + Dir -> PluginsDir = filelib:wildcard("*", Dir), lists:foldl(fun(PluginDir, Acc) -> case filelib:is_dir(Dir ++ PluginDir) of @@ -115,11 +114,9 @@ get_expand_plugin_config() -> false -> Acc end - end, [], PluginsDir); - _ -> ok + end, [], PluginsDir) end. - ensure_file(File) -> case filelib:is_file(File) of false -> write_loaded([]); true -> ok end. @@ -145,10 +142,10 @@ load_plugins(Names, Persistent) -> -spec(unload() -> list() | {error, term()}). unload() -> case emqx_config:get_env(plugins_loaded_file) of - {ok, File} -> - with_loaded_file(File, fun stop_plugins/1); undefined -> - ignore + ignore; + File -> + with_loaded_file(File, fun stop_plugins/1) end. %% stop plugins @@ -159,7 +156,9 @@ stop_plugins(Names) -> -spec(list() -> [plugin()]). list() -> case emqx_config:get_env(plugins_etc_dir) of - {ok, PluginsEtc} -> + undefined -> + []; + PluginsEtc -> CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc) ++ get_expand_plugin_config(), Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], StartedApps = names(started_app), @@ -168,9 +167,7 @@ list() -> true -> Plugin#plugin{active = true}; false -> Plugin end - end, Plugins); - undefined -> - [] + end, Plugins) end. plugin(CfgFile) -> @@ -314,14 +311,14 @@ plugin_unloaded(Name, true) -> read_loaded() -> case emqx_config:get_env(plugins_loaded_file) of - {ok, File} -> read_loaded(File); - undefined -> {error, not_found} + undefined -> {error, not_found}; + File -> read_loaded(File) end. read_loaded(File) -> file:consult(File). write_loaded(AppNames) -> - {ok, File} = emqx_config:get_env(plugins_loaded_file), + File = emqx_config:get_env(plugins_loaded_file), case file:open(File, [binary, write]) of {ok, Fd} -> lists:foreach(fun(Name) -> diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index efcde5d2f..b71c15f1e 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -27,11 +27,11 @@ spec(Args) -> -spec(spec(any(), list()) -> supervisor:child_spec()). spec(ChildId, Args) -> {ChildId, {?MODULE, start_link, Args}, - transient, infinity, supervisor, [?MODULE]}. + transient, infinity, supervisor, [?MODULE]}. -spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}). start_link(Pool, Type, MFA) -> - start_link(Pool, Type, emqx_vm:schedulers(schedulers), MFA). + start_link(Pool, Type, emqx_vm:schedulers(), MFA). -spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, term()}). start_link(Pool, Type, Size, MFA) when is_atom(Pool) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index c932089e7..705674000 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -1,138 +1,107 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% 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. -module(emqx_protocol). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). - -include("emqx_misc.hrl"). --import(proplists, [get_value/2, get_value/3]). - -%% API --export([init/3, init/5, get/2, info/1, stats/1, clientid/1, client/1, session/1]). - --export([subscribe/2, unsubscribe/2, pubrel/2, shutdown/2]). - --export([received/2, send/2]). - --export([process/2]). +-export([init/2, info/1, stats/1, clientid/1, session/1]). +%%-export([capabilities/1]). +-export([parser/1]). +-export([received/2, process/2, deliver/2, send/2]). +-export([shutdown/2]). -ifdef(TEST). -compile(export_all). -endif. --record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0, - send_pkt = 0, send_msg = 0}). +-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}]). -%% Protocol State -%% ws_initial_headers: Headers from first HTTP request for WebSocket Client. --record(proto_state, {peername, sendfun, connected = false, client_id, client_pid, - clean_start, proto_ver, proto_name, username, is_superuser, - will_msg, keepalive, keepalive_backoff, max_clientid_len, - session, stats_data, mountpoint, ws_initial_headers, - peercert_username, is_bridge, connected_at}). +-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}). --type(proto_state() :: #proto_state{}). - --define(INFO_KEYS, [client_id, username, clean_start, proto_ver, proto_name, - keepalive, will_msg, ws_initial_headers, mountpoint, - peercert_username, connected_at]). +-define(INFO_KEYS, [capabilities, connected, client_id, clean_start, username, proto_ver, proto_name, + keepalive, will_msg, mountpoint, is_bridge, connected_at]). -define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]). -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(State#proto_state.peername) | Args])). + 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])). -%% @doc Init protocol -init(Peername, SendFun, Opts) -> - Backoff = get_value(keepalive_backoff, Opts, 0.75), - EnableStats = get_value(client_enable_stats, Opts, false), - MaxLen = get_value(max_clientid_len, Opts, ?MAX_CLIENTID_LEN), - WsInitialHeaders = get_value(ws_initial_headers, Opts), - #proto_state{peername = Peername, - sendfun = SendFun, - max_clientid_len = MaxLen, - is_superuser = false, +-type(proto_state() :: #proto_state{}). + +-export_type([proto_state/0]). + +init(SockProps = #{peercert := Peercert}, 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(), - peercert_username = undefined, - ws_initial_headers = WsInitialHeaders, + proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + username = Username, + is_superuser = false, keepalive_backoff = Backoff, - stats_data = #proto_stats{enable_stats = EnableStats}}. + mountpoint = MountPoint, + is_bridge = false, + recv_pkt = 0, + recv_msg = 0, + send_pkt = 0, + send_msg = 0}. -init(_Transport, _Sock, Peername, SendFun, Opts) -> - init(Peername, SendFun, Opts). - %%enrich_opt(Conn:opts(), Conn, ). +capabilities(Zone) -> + Capabilities = emqx_zone:env(Zone, mqtt_capabilities, []), + maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)). -enrich_opt([], _Conn, State) -> - State; -enrich_opt([{mountpoint, MountPoint} | ConnOpts], Conn, State) -> - enrich_opt(ConnOpts, Conn, State#proto_state{mountpoint = MountPoint}); -enrich_opt([{peer_cert_as_username, N} | ConnOpts], Conn, State) -> - enrich_opt(ConnOpts, Conn, State#proto_state{peercert_username = peercert_username(N, Conn)}); -enrich_opt([_ | ConnOpts], Conn, State) -> - enrich_opt(ConnOpts, Conn, State). - -peercert_username(cn, Conn) -> - Conn:peer_cert_common_name(); -peercert_username(dn, Conn) -> - Conn:peer_cert_subject(). - -repl_username_with_peercert(State = #proto_state{peercert_username = undefined}) -> - State; -repl_username_with_peercert(State = #proto_state{peercert_username = PeerCert}) -> - State#proto_state{username = PeerCert}. - -%%TODO:: -get(proto_ver, #proto_state{proto_ver = Ver}) -> - Ver; -get(_, _ProtoState) -> - undefined. +parser(#proto_state{capabilities = #{max_packet_size := Size}, proto_ver = Ver}) -> + emqx_frame:initial_state(#{max_packet_size => Size, version => Ver}). info(ProtoState) -> ?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS). -stats(#proto_state{stats_data = Stats}) -> - tl(?record_to_proplist(proto_stats, Stats)). +stats(ProtoState) -> + ?record_to_proplist(proto_state, ProtoState, ?STATS_KEYS). clientid(#proto_state{client_id = ClientId}) -> ClientId. -client(#proto_state{client_id = ClientId, - client_pid = ClientPid, - peername = Peername, - username = Username, - clean_start = CleanStart, - proto_ver = ProtoVer, - keepalive = Keepalive, - will_msg = WillMsg, - ws_initial_headers = WsInitialHeaders, - mountpoint = MountPoint, - connected_at = Time}) -> - WillTopic = if - WillMsg =:= undefined -> undefined; - true -> WillMsg#message.topic - end, - #client{id = ClientId, - pid = ClientPid, - username = Username, - peername = Peername}. +client(#proto_state{sockprops = #{peername := Peername}, + client_id = ClientId, client_pid = ClientPid, username = Username}) -> + #client{id = ClientId, pid = ClientPid, username = Username, peername = Peername}. session(#proto_state{session = Session}) -> Session. @@ -141,117 +110,96 @@ session(#proto_state{session = Session}) -> %% 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), - State = #proto_state{connected = false, stats_data = Stats}) -> - trace(recv, Packet, State), Stats1 = inc_stats(recv, ?CONNECT, Stats), - process(Packet, State#proto_state{connected = true, stats_data = Stats1}); +received(Packet = ?PACKET(?CONNECT), ProtoState = #proto_state{connected = false}) -> + trace(recv, Packet, ProtoState), + process(Packet, inc_stats(recv, ?CONNECT, ProtoState#proto_state{connected = true})); received(?PACKET(?CONNECT), State = #proto_state{connected = true}) -> {error, protocol_bad_connect, State}; %% Received other packets when CONNECT not arrived. -received(_Packet, State = #proto_state{connected = false}) -> - {error, protocol_not_connected, State}; +received(_Packet, ProtoState = #proto_state{connected = false}) -> + {error, protocol_not_connected, ProtoState}; -received(Packet = ?PACKET(Type), State = #proto_state{stats_data = Stats}) -> - trace(recv, Packet, State), Stats1 = inc_stats(recv, Type, Stats), +received(Packet = ?PACKET(Type), ProtoState) -> + trace(recv, Packet, ProtoState), case validate_packet(Packet) of ok -> - process(Packet, State#proto_state{stats_data = Stats1}); + process(Packet, inc_stats(recv, Type, ProtoState)); {error, Reason} -> - {error, Reason, State} + {error, Reason, ProtoState} end. -subscribe(RawTopicTable, ProtoState = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> - TopicTable = parse_topic_table(RawTopicTable), - case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of - {ok, TopicTable1} -> - emqx_session:subscribe(Session, TopicTable1); - {stop, _} -> - ok - end, - {ok, ProtoState}. +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, + 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), + is_bridge = IsBridge, + connected_at = os:timestamp()}, -unsubscribe(RawTopics, ProtoState = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> - case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of - {ok, TopicTable} -> - emqx_session:unsubscribe(Session, TopicTable); - {stop, _} -> - ok - end, - {ok, ProtoState}. - -%% @doc Send PUBREL -pubrel(PacketId, State) -> send(?PUBREL_PACKET(PacketId), State). - -process(?CONNECT_PACKET(Var), State0) -> - - #mqtt_packet_connect{proto_ver = ProtoVer, - proto_name = ProtoName, - username = Username, - password = Password, - clean_start= CleanStart, - keepalive = KeepAlive, - client_id = ClientId, - is_bridge = IsBridge} = Var, - - State1 = repl_username_with_peercert( - State0#proto_state{proto_ver = ProtoVer, - proto_name = ProtoName, - username = Username, - client_id = ClientId, - clean_start = CleanStart, - keepalive = KeepAlive, - will_msg = willmsg(Var, State0), - is_bridge = IsBridge, - connected_at = os:timestamp()}), - - {ReturnCode1, SessPresent, State3} = - case validate_connect(Var, State1) of + {ReturnCode1, SessPresent, ProtoState3} = + case validate_connect(Var, ProtoState1) of ?RC_SUCCESS -> - case authenticate(client(State1), Password) of + case authenticate(client(ProtoState1), Password) of {ok, IsSuperuser} -> %% Generate clientId if null - State2 = maybe_set_clientid(State1), - - %% Start session - case emqx_sm:open_session(#{clean_start => CleanStart, - client_id => clientid(State2), + 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 => self()}) of + client_pid => ClientPid}) of {ok, Session} -> %% TODO:... SP = true, %% TODO:... %% TODO: Register the client - emqx_cm:register_client(clientid(State2)), + emqx_cm:register_client(clientid(ProtoState2)), %%emqx_cm:reg(client(State2)), %% Start keepalive - start_keepalive(KeepAlive, State2), + start_keepalive(Keepalive, ProtoState2), %% Emit Stats - self() ! emit_stats, + %% self() ! emit_stats, %% ACCEPT - {?RC_SUCCESS, SP, State2#proto_state{session = Session, is_superuser = IsSuperuser}}; + {?RC_SUCCESS, SP, ProtoState2#proto_state{session = Session, is_superuser = IsSuperuser}}; {error, Error} -> - {stop, {shutdown, Error}, State2} + ?LOG(error, "Failed to open session: ~p", [Error], ProtoState2), + {?RC_UNSPECIFIED_ERROR, false, ProtoState2} %% TODO: the error reason??? end; {error, Reason}-> - ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], State1), - {?RC_BAD_USER_NAME_OR_PASSWORD, false, State1} + ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], ProtoState1), + {?RC_BAD_USER_NAME_OR_PASSWORD, false, ProtoState1} end; ReturnCode -> - {ReturnCode, false, State1} + {ReturnCode, false, ProtoState1} end, %% Run hooks - emqx_hooks:run('client.connected', [ReturnCode1], client(State3)), + emqx_hooks:run('client.connected', [ReturnCode1], client(ProtoState3)), %%TODO: Send Connack - send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3), + send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), ProtoState3), %% stop if authentication failure - stop_if_auth_failure(ReturnCode1, State3); + stop_if_auth_failure(ReturnCode1, ProtoState3); -process(Packet = ?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload), State = #proto_state{is_superuser = IsSuper}) -> +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) @@ -278,28 +226,28 @@ process(?SUBSCRIBE_PACKET(PacketId, []), State) -> send(?SUBACK_PACKET(PacketId, []), State); %% TODO: refactor later... -process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable), - State = #proto_state{client_id = ClientId, - username = Username, - is_superuser = IsSuperuser, - mountpoint = MountPoint, - session = Session}) -> - Client = client(State), TopicTable = parse_topic_table(RawTopicTable), +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} <- TopicTable] + 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", [TopicTable], State), - send(?SUBACK_PACKET(PacketId, [16#80 || _ <- TopicTable]), State); + ?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], TopicTable) of - {ok, TopicTable1} -> - emqx_session:subscribe(Session, PacketId, mount(replvar(MountPoint, State), TopicTable1)), + case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicFilters) of + {ok, TopicFilters1} -> + ok = emqx_session:subscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicFilters1)}), {ok, State}; - {stop, _} -> - {ok, State} + {stop, _} -> {ok, State} end end; @@ -307,97 +255,109 @@ process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable), process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> send(?UNSUBACK_PACKET(PacketId), State); -process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics), +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, mount(replvar(MountPoint, State), TopicTable)); + emqx_session:unsubscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicTable)}); {stop, _} -> ok end, send(?UNSUBACK_PACKET(PacketId), State); -process(?PACKET(?PINGREQ), State) -> - send(?PACKET(?PINGRESP), State); +process(?PACKET(?PINGREQ), ProtoState) -> + send(?PACKET(?PINGRESP), ProtoState); -process(?PACKET(?DISCONNECT), State) -> +process(?PACKET(?DISCONNECT), ProtoState) -> % Clean willmsg - {stop, normal, State#proto_state{will_msg = undefined}}. + {stop, normal, ProtoState#proto_state{will_msg = undefined}}. -publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), +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_packet:to_message(Packet), - Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, - emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)); + 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, _PacketId), State) -> +publish(Packet = ?PUBLISH_PACKET(?QOS_1), State) -> with_puback(?PUBACK, Packet, State); -publish(Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) -> +publish(Packet = ?PUBLISH_PACKET(?QOS_2), State) -> with_puback(?PUBREC, Packet, State). -with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), +with_puback(Type, Packet = ?PUBLISH_PACKET(_QoS, PacketId), State = #proto_state{client_id = ClientId, username = Username, mountpoint = MountPoint, session = Session}) -> - %% TODO: ... - Msg = emqx_packet:to_message(Packet), - Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, - case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)) of - ok -> - case Type of - ?PUBACK -> send(?PUBACK_PACKET(PacketId), State); - ?PUBREC -> send(?PUBREC_PACKET(PacketId), State) - end; + 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) + ?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State); + _Delivery -> send({Type, PacketId}, State) %% TODO: end. --spec(send(message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}). -send(Msg, State = #proto_state{client_id = ClientId, - username = Username, - mountpoint = MountPoint, - is_bridge = IsBridge}) - when is_record(Msg, message) -> - emqx_hooks:run('message.delivered', [ClientId, Username], Msg), - send(emqx_packet:from_message(unmount(MountPoint, clean_retain(IsBridge, Msg))), State); +-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(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) -> - trace(send, Packet, State), - emqx_metrics:sent(Packet), - SendFun(Packet), - {ok, State#proto_state{stats_data = inc_stats(send, Type, Stats)}}. +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(info, "RECV ~s", [emqx_packet:format(Packet)], ProtoState); + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)], ProtoState); trace(send, Packet, ProtoState) -> - ?LOG(info, "SEND ~s", [emqx_packet:format(Packet)], ProtoState). + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)], ProtoState). -inc_stats(_Direct, _Type, Stats = #proto_stats{enable_stats = false}) -> - Stats; - -inc_stats(recv, Type, Stats) -> - #proto_stats{recv_pkt = PktCnt, recv_msg = MsgCnt} = Stats, - inc_stats(Type, #proto_stats.recv_pkt, PktCnt, #proto_stats.recv_msg, MsgCnt, Stats); - -inc_stats(send, Type, Stats) -> - #proto_stats{send_pkt = PktCnt, send_msg = MsgCnt} = Stats, - inc_stats(Type, #proto_stats.send_pkt, PktCnt, #proto_stats.send_msg, MsgCnt, Stats). - -inc_stats(Type, PktPos, PktCnt, MsgPos, MsgCnt, Stats) -> - Stats1 = setelement(PktPos, Stats, PktCnt + 1), - case Type =:= ?PUBLISH of - true -> setelement(MsgPos, Stats1, MsgCnt + 1); - false -> Stats1 - end. +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}; @@ -415,19 +375,18 @@ shutdown(mnesia_conflict, _State = #proto_state{client_id = ClientId}) -> shutdown(Error, State = #proto_state{client_id = ClientId, will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], State), - Client = client(State), %% Auth failure not publish the will message case Error =:= auth_failure of true -> ok; - false -> send_willmsg(Client, WillMsg) + false -> send_willmsg(ClientId, WillMsg) end, - emqx_hooks:run('client.disconnected', [Error], Client), + emqx_hooks:run('client.disconnected', [Error], client(State)), emqx_cm:unregister_client(ClientId), ok. -willmsg(Packet, State = #proto_state{mountpoint = MountPoint}) +willmsg(Packet, State = #proto_state{client_id = ClientId, mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) -> - case emqx_packet:to_message(Packet) of + case emqx_packet:to_message(ClientId, Packet) of undefined -> undefined; Msg -> mount(replvar(MountPoint, State), Msg) end. @@ -442,10 +401,10 @@ maybe_set_clientid(State = #proto_state{client_id = NullId}) maybe_set_clientid(State) -> State. -send_willmsg(_Client, undefined) -> +send_willmsg(_ClientId, undefined) -> ignore; -send_willmsg(Client, WillMsg) -> - emqx_broker:publish(WillMsg#message{from = Client}). +send_willmsg(ClientId, WillMsg) -> + emqx_broker:publish(WillMsg#message{from = ClientId}). start_keepalive(0, _State) -> ignore; @@ -471,7 +430,7 @@ 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{max_clientid_len = MaxLen}) + #proto_state{capabilities = #{max_clientid_len := MaxLen}}) when (byte_size(ClientId) >= 1) andalso (byte_size(ClientId) =< MaxLen) -> true; @@ -493,7 +452,7 @@ validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer, [ProtoVer, CleanStart], ProtoState), false. -validate_packet(?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload)) -> +validate_packet(?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload)) -> case emqx_topic:validate({name, Topic}) of true -> ok; false -> {error, badtopic} @@ -513,11 +472,11 @@ validate_topics(_Type, []) -> 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) + Valid = fun(Topic, QoS) -> + emqx_topic:validate({Type, Topic}) and validate_qos(QoS) end, case [Topic || {Topic, SubOpts} <- TopicTable, - not Valid(Topic, proplists:get_value(qos, SubOpts))] of + not Valid(Topic, SubOpts#mqtt_subopts.qos)] of [] -> ok; _ -> {error, badtopic} end; @@ -530,17 +489,16 @@ validate_topics(Type, Topics = [Topic0|_]) when is_binary(Topic0) -> validate_qos(undefined) -> true; -validate_qos(Qos) when ?IS_QOS(Qos) -> +validate_qos(QoS) when ?IS_QOS(QoS) -> true; validate_qos(_) -> false. -parse_topic_table(TopicTable) -> - lists:map(fun({Topic0, SubOpts}) -> - {Topic, Opts} = emqx_topic:parse(Topic0), - %%TODO: - {Topic, lists:usort(lists:umerge(Opts, SubOpts))} - end, TopicTable). +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]. parse_topics(Topics) -> [emqx_topic:parse(Topic) || Topic <- Topics]. diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 8f6375720..863214617 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -33,15 +33,12 @@ -export([del_route/1, del_route/2, del_route/3]). -export([has_routes/1, match_routes/1, print_routes/1]). -export([topics/0]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, - code_change/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -type(destination() :: node() | {binary(), node()}). -record(batch, {enabled, timer, pending}). - -record(state, {pool, id, batch :: #batch{}}). -define(ROUTE, emqx_route). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 83d53abc6..1253de5cc 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -1,73 +1,64 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== - +%% 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 +%% A stateful interaction between a Client and a Server. Some Sessions +%% last only as long as the Network Connection, others can span multiple +%% consecutive Network Connections between a Client and a Server. +%% +%% The Session State in the Server consists of: +%% +%% The existence of a Session, even if the rest of the Session State is empty. +%% +%% The Clients subscriptions, including any Subscription Identifiers. +%% +%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not +%% been completely acknowledged. +%% +%% QoS 1 and QoS 2 messages pending transmission to the Client and OPTIONALLY +%% QoS 0 messages pending transmission to the Client. +%% +%% QoS 2 messages which have been received from the Client, but have not been +%% completely acknowledged.The Will Message and the Will Delay Interval +%% +%% 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"). --import(emqx_misc, [start_timer/2]). +-export([start_link/1, close/1]). +-export([info/1, stats/1]). +-export([resume/2, discard/2]). +-export([subscribe/2]).%%, subscribe/3]). +-export([publish/3]). +-export([puback/2, puback/3]). +-export([pubrec/2, pubrec/3]). +-export([pubrel/2, pubcomp/2]). +-export([unsubscribe/2]). --import(proplists, [get_value/2, get_value/3]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -%% Session API --export([start_link/1, resume/2, discard/2]). - -%% Management and Monitor API --export([state/1, info/1, stats/1]). - -%% PubSub API --export([subscribe/2, subscribe/3, publish/2, puback/2, pubrec/2, - pubrel/2, pubcomp/2, unsubscribe/2]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --define(MQueue, emqx_mqueue). - -%% A stateful interaction between a Client and a Server. Some Sessions -%% last only as long as the Network Connection, others can span multiple -%% consecutive Network Connections between a Client and a Server. -%% -%% The Session state in the Server consists of: -%% -%% The existence of a Session, even if the rest of the Session state is empty. -%% -%% The Client’s subscriptions. -%% -%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not -%% been completely acknowledged. -%% -%% QoS 1 and QoS 2 messages pending transmission to the Client. -%% -%% QoS 2 messages which have been received from the Client, but have not -%% been completely acknowledged. -%% -%% Optionally, QoS 0 messages pending transmission to the Client. -%% -%% If the session is currently disconnected, the time at which the Session state -%% will be deleted. --record(state, - { %% Clean Start Flag +-record(state, { + %% Clean Start Flag clean_start = false :: boolean(), %% Client Binding: local | remote @@ -79,21 +70,25 @@ %% Username username :: binary() | undefined, - %% Client Pid binding with session + %% Client pid binding with session client_pid :: pid(), - %% Old Client Pid that has been kickout + %% Old client Pid that has been kickout old_client_pid :: pid(), - %% Next message id of the session - next_msg_id = 1 :: mqtt_packet_id(), + %% Pending sub/unsub requests + requests :: map(), + %% Next packet id of the session + next_pkt_id = 1 :: mqtt_packet_id(), + + %% Max subscriptions max_subscriptions :: non_neg_integer(), - %% Client’s subscriptions. + %% Client’s Subscriptions. subscriptions :: map(), - %% Upgrade Qos? + %% Upgrade QoS? upgrade_qos = false :: boolean(), %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. @@ -112,18 +107,18 @@ %% QoS 1 and QoS 2 messages pending transmission to the Client. %% %% Optionally, QoS 0 messages pending transmission to the Client. - mqueue :: ?MQueue:mqueue(), + mqueue :: emqx_mqueue:mqueue(), %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. awaiting_rel :: map(), - %% Max Packets that Awaiting PUBREL + %% Max Packets Awaiting PUBREL max_awaiting_rel = 100 :: non_neg_integer(), - %% Awaiting PUBREL timeout + %% Awaiting PUBREL Timeout await_rel_timeout = 20000 :: timeout(), - %% Awaiting PUBREL timer + %% Awaiting PUBREL Timer await_rel_timer :: reference() | undefined, %% Session Expiry Interval @@ -141,99 +136,102 @@ %% Ignore loop deliver? ignore_loop_deliver = false :: boolean(), + %% 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_msg_id, max_subscriptions, subscriptions, upgrade_qos, inflight, + 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(LOG(Level, Format, Args, State), - emqx_logger:Level([{client, State#state.client_id}], - "Session(~s): " ++ Format, [State#state.client_id | Args])). + emqx_logger:Level([{client, State#state.client_id}], + "Session(~s): " ++ Format, [State#state.client_id | Args])). -%% @doc Start a Session --spec(start_link(map()) -> {ok, pid()} | {error, term()}). -start_link(Attrs) -> - gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]). +%% @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}]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% PubSub API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -%% @doc Subscribe topics --spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SessionPid, TopicTable) -> %%TODO: the ack function??... - gen_server:cast(SessionPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). +-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}); +%% for mqtt 5.0 +subscribe(SPid, SubReq = {PacketId, Props, TopicFilters}) -> + gen_server:cast(SPid, {subscribe, self(), SubReq}). --spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SessionPid, PacketId, TopicTable) -> %%TODO: the ack function??... - From = self(), - AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, - gen_server:cast(SessionPid, {subscribe, From, TopicTable, AckFun}). +-spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, delivery()} | {error, term()}). +publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> + %% Publish QoS0 message to broker directly + emqx_broker:publish(Msg); -%% @doc Publish Message --spec(publish(pid(), message()) -> ok | {error, term()}). -publish(_SessionPid, Msg = #message{qos = ?QOS_0}) -> - %% Publish QoS0 Directly - emqx_broker:publish(Msg), ok; +publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) -> + %% Publish QoS1 message to broker directly + emqx_broker:publish(Msg); -publish(_SessionPid, Msg = #message{qos = ?QOS_1}) -> - %% Publish QoS1 message directly for client will PubAck automatically - emqx_broker:publish(Msg), ok; +publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> + %% Publish QoS2 message to session + gen_server:call(SPid, {publish, PacketId, Msg}, infinity). -publish(SessionPid, Msg = #message{qos = ?QOS_2}) -> - %% Publish QoS2 to Session - gen_server:call(SessionPid, {publish, Msg}, ?TIMEOUT). - -%% @doc PubAck Message -spec(puback(pid(), mqtt_packet_id()) -> ok). -puback(SessionPid, PacketId) -> - gen_server:cast(SessionPid, {puback, PacketId}). +puback(SPid, PacketId) -> + gen_server:cast(SPid, {puback, PacketId}). + +puback(SPid, PacketId, {ReasonCode, Props}) -> + gen_server:cast(SPid, {puback, PacketId, {ReasonCode, Props}}). -spec(pubrec(pid(), mqtt_packet_id()) -> ok). -pubrec(SessionPid, PacketId) -> - gen_server:cast(SessionPid, {pubrec, PacketId}). +pubrec(SPid, PacketId) -> + gen_server:cast(SPid, {pubrec, PacketId}). + +pubrec(SPid, PacketId, {ReasonCode, Props}) -> + gen_server:cast(SPid, {pubrec, PacketId, {ReasonCode, Props}}). -spec(pubrel(pid(), mqtt_packet_id()) -> ok). -pubrel(SessionPid, PacketId) -> - gen_server:cast(SessionPid, {pubrel, PacketId}). +pubrel(SPid, PacketId) -> + gen_server:cast(SPid, {pubrel, PacketId}). -spec(pubcomp(pid(), mqtt_packet_id()) -> ok). -pubcomp(SessionPid, PacketId) -> - gen_server:cast(SessionPid, {pubcomp, PacketId}). +pubcomp(SPid, PacketId) -> + gen_server:cast(SPid, {pubcomp, PacketId}). -%% @doc Unsubscribe the topics --spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok). -unsubscribe(SessionPid, TopicTable) -> - gen_server:cast(SessionPid, {unsubscribe, self(), TopicTable}). +-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}) -> + gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). -%% @doc Resume the session -spec(resume(pid(), pid()) -> ok). -resume(SessionPid, ClientPid) -> - gen_server:cast(SessionPid, {resume, ClientPid}). - -%% @doc Get session state -state(SessionPid) when is_pid(SessionPid) -> - gen_server:call(SessionPid, state). +resume(SPid, ClientPid) -> + gen_server:cast(SPid, {resume, ClientPid}). %% @doc Get session info -spec(info(pid() | #state{}) -> list(tuple())). -info(SessionPid) when is_pid(SessionPid) -> - gen_server:call(SessionPid, info); +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). -spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). -stats(SessionPid) when is_pid(SessionPid) -> - gen_server:call(SessionPid, stats); +stats(SPid) when is_pid(SPid) -> + gen_server:call(SPid, stats); stats(#state{max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, @@ -247,9 +245,9 @@ stats(#state{max_subscriptions = MaxSubscriptions, {subscriptions, maps:size(Subscriptions)}, {max_inflight, MaxInflight}, {inflight_len, emqx_inflight:size(Inflight)}, - {max_mqueue, ?MQueue:max_len(MQueue)}, - {mqueue_len, ?MQueue:len(MQueue)}, - {mqueue_dropped, ?MQueue:dropped(MQueue)}, + {max_mqueue, emqx_mqueue:max_len(MQueue)}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, {max_awaiting_rel, MaxAwaitingRel}, {awaiting_rel_len, maps:size(AwaitingRel)}, {deliver_msg, get(deliver_msg)}, @@ -257,50 +255,54 @@ stats(#state{max_subscriptions = MaxSubscriptions, %% @doc Discard the session -spec(discard(pid(), client_id()) -> ok). -discard(SessionPid, ClientId) -> - gen_server:call(SessionPid, {discard, ClientId}). +discard(SPid, ClientId) -> + gen_server:call(SPid, {discard, ClientId}, infinity). -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- +-spec(close(pid()) -> ok). +close(SPid) -> + gen_server:call(SPid, close, infinity). -init(#{clean_start := CleanStart, +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init(#{zone := Zone, client_id := ClientId, - username := Username, - client_pid := ClientPid}) -> + client_pid := ClientPid, + clean_start := CleanStart, + username := Username}) -> process_flag(trap_exit, true), true = link(ClientPid), init_stats([deliver_msg, enqueue_msg]), - {ok, Env} = emqx_config:get_env(session), - {ok, QEnv} = emqx_config:get_env(mqueue), - MaxInflight = get_value(max_inflight, Env, 0), - EnableStats = get_value(enable_stats, Env, false), - IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false), - MQueue = ?MQueue:new(ClientId, QEnv, emqx_alarm:alarm_fun()), + MaxInflight = emqx_zone:env(Zone, max_inflight), State = #state{clean_start = CleanStart, binding = binding(ClientPid), client_id = ClientId, client_pid = ClientPid, username = Username, subscriptions = #{}, - max_subscriptions = get_value(max_subscriptions, Env, 0), - upgrade_qos = get_value(upgrade_qos, Env, false), + max_subscriptions = emqx_zone:env(Zone, max_subscriptions, 0), + upgrade_qos = emqx_zone:env(Zone, upgrade_qos, false), max_inflight = MaxInflight, inflight = emqx_inflight:new(MaxInflight), - mqueue = MQueue, - retry_interval = get_value(retry_interval, Env), + mqueue = init_mqueue(Zone, ClientId), + retry_interval = emqx_zone:env(Zone, retry_interval, 0), awaiting_rel = #{}, - await_rel_timeout = get_value(await_rel_timeout, Env), - max_awaiting_rel = get_value(max_awaiting_rel, Env), - expiry_interval = get_value(expiry_interval, Env), - enable_stats = EnableStats, - ignore_loop_deliver = IgnoreLoopDeliver, + 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), created_at = os:timestamp()}, emqx_sm:register_session(ClientId, info(State)), - emqx_hooks:run('session.created', [ClientId, Username]), - io:format("Session started: ~p~n", [self()]), + emqx_hooks:run('session.created', [ClientId]), {ok, emit_stats(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). @@ -315,19 +317,19 @@ handle_call({discard, ClientPid}, _From, State = #state{client_pid = OldClientPi ?LOG(warning, " ~p kickout ~p", [ClientPid, OldClientPid], State), {stop, {shutdown, conflict}, ok, State}; -handle_call({publish, Msg = #message{qos = ?QOS_2, headers = #{packet_id := PacketId}}}, _From, +handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2}}, _From, State = #state{awaiting_rel = AwaitingRel, await_rel_timer = Timer, await_rel_timeout = Timeout}) -> case is_awaiting_full(State) of false -> State1 = case Timer == undefined of - true -> State#state{await_rel_timer = start_timer(Timeout, check_awaiting_rel)}; + true -> State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; false -> State end, reply(ok, State1#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)}); true -> - ?LOG(warning, "Dropped Qos2 Message for too many awaiting_rel: ~p", [Msg], State), + ?LOG(warning, "Dropped QoS2 Message for too many awaiting_rel: ~p", [Msg], State), emqx_metrics:inc('messages/qos2/dropped'), reply({error, dropped}, State) end; @@ -338,69 +340,53 @@ handle_call(info, _From, State) -> handle_call(stats, _From, State) -> reply(stats(State), State); -handle_call(state, _From, State) -> - reply(?record_to_proplist(state, State, ?STATE_KEYS), State); +handle_call(close, _From, State) -> + {stop, normal, State}; handle_call(Req, _From, State) -> - emqx_logger:error("[Session] Unexpected request: ~p", [Req]), - {reply, ignore, State}. + emqx_logger:error("[Session] unexpected call: ~p", [Req]), + {reply, ignored, State}. -handle_cast({subscribe, From, TopicTable, AckFun}, - State = #state{client_id = ClientId, - username = Username, - subscriptions = Subscriptions}) -> - ?LOG(info, "Subscribe ~p", [TopicTable], State), - {GrantedQos, Subscriptions1} = - lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) -> - io:format("SubOpts: ~p~n", [Opts]), - Fastlane = lists:member(fastlane, Opts), - NewQos = if Fastlane == true -> ?QOS_0; true -> get_value(qos, Opts) end, - SubMap1 = - case maps:find(Topic, SubMap) of - {ok, NewQos} -> - ?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State), - SubMap; - {ok, OldQos} -> - emqx_broker:setopts(Topic, ClientId, [{qos, NewQos}]), - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), - ?LOG(warning, "Duplicated subscribe ~s, old_qos=~w, new_qos=~w", - [Topic, OldQos, NewQos], State), - maps:put(Topic, NewQos, SubMap); - error -> - case Fastlane of - true -> emqx:subscribe(Topic, From, Opts); - false -> emqx:subscribe(Topic, ClientId, Opts) - end, - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), - maps:put(Topic, NewQos, SubMap) - end, - {[NewQos|QosAcc], SubMap1} - end, {[], Subscriptions}, TopicTable), - io:format("GrantedQos: ~p~n", [GrantedQos]), - AckFun(lists:reverse(GrantedQos)), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; +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}) -> + {[QoS|RcAcc], + case maps:find(Topic, SubMap) of + {ok, SubOpts} -> + ?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State), + SubMap; + {ok, OldOpts} -> + 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); + error -> + emqx_broker:subscribe(Topic, ClientId, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), + maps:put(Topic, SubOpts, SubMap) + end} + end, {[], Subscriptions}, TopicFilters), + suback(From, PacketId, lists:reverse(ReasonCodes)), + {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; -handle_cast({unsubscribe, From, TopicTable}, - State = #state{client_id = ClientId, - username = Username, - subscriptions = Subscriptions}) -> - ?LOG(info, "Unsubscribe ~p", [TopicTable], State), - Subscriptions1 = - lists:foldl(fun({Topic, Opts}, SubMap) -> - Fastlane = lists:member(fastlane, Opts), +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, _Qos} -> - case Fastlane of - true -> emqx:unsubscribe(Topic, From); - false -> emqx:unsubscribe(Topic, ClientId) - end, - emqx_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, Opts}), - maps:remove(Topic, SubMap); + {ok, SubOpts} -> + emqx_broker:unsubscribe(Topic, ClientId), + emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), + {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; error -> - SubMap + {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} end - end, Subscriptions, TopicTable), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; + end, {[], Subscriptions}, TopicFilters), + unsuback(From, PacketId, lists:reverse(ReasonCodes)), + {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; %% PUBACK: handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> @@ -501,11 +487,16 @@ handle_cast({resume, ClientPid}, {noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; handle_cast(Msg, State) -> - emqx_logger:error("[Session] Unexpected msg: ~p", [Msg]), + emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), {noreply, State}. -%% Ignore Messages delivered by self -handle_info({dispatch, _Topic, #message{from = {ClientId, _}}}, +handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> + {noreply, lists:foldl(fun(Msg, NewState) -> + element(2, handle_info({dispatch, Topic, Msg}, NewState)) + end, State, Msgs)}; + +%% Ignore messages delivered by self +handle_info({dispatch, _Topic, #message{from = ClientId}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> {noreply, State}; @@ -536,35 +527,46 @@ handle_info({'EXIT', ClientPid, Reason}, client_pid = ClientPid, expiry_interval = Interval}) -> ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), - ExpireTimer = start_timer(Interval, expired), + ExpireTimer = emqx_misc:start_timer(Interval, expired), State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, {noreply, emit_stats(State1), hibernate}; handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) -> - %%ignore + %% ignore {noreply, State, hibernate}; handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> - - ?LOG(error, "Unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p", + ?LOG(error, "unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p", [ClientPid, Pid, Reason], State), {noreply, State, hibernate}; handle_info(Info, State) -> - emqx_logger:error("[Session] Unexpected info: ~p", [Info]), + emqx_logger:error("[Session] unexpected info: ~p", [Info]), {noreply, State}. terminate(Reason, #state{client_id = ClientId, username = Username}) -> - emqx_hooks:run('session.terminated', [ClientId, Username, Reason]), emqx_sm:unregister_session(ClientId). -code_change(_OldVsn, Session, _Extra) -> - {ok, Session}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +suback(_From, undefined, _ReasonCodes) -> + ignore; +suback(From, PacketId, ReasonCodes) -> + From ! {deliver, {suback, PacketId, ReasonCodes}}. + +unsuback(_From, undefined, _ReasonCodes) -> + ignore; +unsuback(From, PacketId, ReasonCodes) -> + From ! {deliver, {unsuback, PacketId, ReasonCodes}}. + +%%------------------------------------------------------------------------------ %% Kickout old client -%%-------------------------------------------------------------------- kick(_ClientId, undefined, _Pid) -> ignore; @@ -576,32 +578,32 @@ kick(ClientId, OldPid, Pid) -> %% Clean noproc receive {'EXIT', OldPid, _} -> ok after 0 -> ok end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Replay or Retry Delivery -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -%% Redeliver at once if Force is true +%% Redeliver at once if force is true retry_delivery(Force, State = #state{inflight = Inflight}) -> case emqx_inflight:is_empty(Inflight) of - true -> State; - false -> Msgs = lists:sort(sortfun(inflight), - emqx_inflight:values(Inflight)), - retry_delivery(Force, Msgs, os:timestamp(), State) + true -> + State; + false -> + Msgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)), + retry_delivery(Force, Msgs, os:timestamp(), State) end. retry_delivery(_Force, [], _Now, State = #state{retry_interval = Interval}) -> - State#state{retry_timer = start_timer(Interval, retry_delivery)}; + State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; -retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, - State = #state{inflight = Inflight, - retry_interval = Interval}) -> +retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, + State = #state{inflight = Inflight, retry_interval = Interval}) -> Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms if Force orelse (Diff >= Interval) -> - case {Type, Msg} of - {publish, Msg = #message{headers = #{packet_id := PacketId}}} -> - redeliver(Msg, State), - Inflight1 = emqx_inflight:update(PacketId, {publish, Msg, Now}, Inflight), + case {Type, Msg0} of + {publish, {PacketId, Msg}} -> + redeliver({PacketId, Msg}, State), + Inflight1 = emqx_inflight:update(PacketId, {publish, {PacketId, Msg}, Now}, Inflight), retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); {pubrel, PacketId} -> redeliver({pubrel, PacketId}, State), @@ -609,12 +611,12 @@ retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}) end; true -> - State#state{retry_timer = start_timer(Interval - Diff, retry_delivery)} + State#state{retry_timer = emqx_misc:start_timer(Interval - Diff, retry_delivery)} end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Expire Awaiting Rel -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> case maps:size(AwaitingRel) of @@ -635,12 +637,12 @@ expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs], emqx_metrics:inc('messages/qos2/dropped'), expire_awaiting_rel(Msgs, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); Diff -> - State#state{await_rel_timer = start_timer(Timeout - Diff, check_awaiting_rel)} + State#state{await_rel_timer = emqx_misc:start_timer(Timeout - Diff, check_awaiting_rel)} end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Sort Inflight, AwaitingRel -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ sortfun(inflight) -> fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end; @@ -651,18 +653,18 @@ sortfun(awaiting_rel) -> Ts1 < Ts2 end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Check awaiting rel -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ is_awaiting_full(#state{max_awaiting_rel = 0}) -> false; is_awaiting_full(#state{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) -> maps:size(AwaitingRel) >= MaxLen. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Dispatch Messages -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Enqueue message if the client has been disconnected dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> @@ -673,53 +675,50 @@ dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> %% Deliver qos0 message directly to client dispatch(Msg = #message{qos = ?QOS0}, State) -> - deliver(Msg, State), State; + deliver(undefined, Msg, State), State; -dispatch(Msg = #message{qos = QoS}, - State = #state{next_msg_id = MsgId, inflight = Inflight}) +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 -> enqueue_msg(Msg, State); false -> - Msg1 = emqx_message:set_header(packet_id, MsgId, Msg), - deliver(Msg1, State), - await(Msg1, next_msg_id(State)) + deliver(PacketId, Msg, State), + await(PacketId, Msg, next_pkt_id(State)) end. enqueue_msg(Msg, State = #state{mqueue = Q}) -> inc_stats(enqueue_msg), - State#state{mqueue = ?MQueue:in(Msg, Q)}. + State#state{mqueue = emqx_mqueue:in(Msg, Q)}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Deliver -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -redeliver(Msg = #message{qos = QoS}, State) -> - deliver(if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); +redeliver({PacketId, Msg = #message{qos = QoS}}, State) -> + deliver(PacketId, if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); redeliver({pubrel, PacketId}, #state{client_pid = Pid}) -> - Pid ! {redeliver, {?PUBREL, PacketId}}. + Pid ! {deliver, {pubrel, PacketId}}. -deliver(Msg, #state{client_pid = Pid, binding = local}) -> - inc_stats(deliver_msg), Pid ! {deliver, Msg}; -deliver(Msg, #state{client_pid = Pid, binding = remote}) -> - inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, Msg}]). +deliver(PacketId, Msg, #state{client_pid = Pid, binding = local}) -> + inc_stats(deliver_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}]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Awaiting ACK for QoS1/QoS2 Messages -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -await(Msg = #message{headers = #{packet_id := PacketId}}, - State = #state{inflight = Inflight, - retry_timer = RetryTimer, - retry_interval = Interval}) -> +await(PacketId, Msg, State = #state{inflight = Inflight, + retry_timer = RetryTimer, + retry_interval = Interval}) -> %% Start retry timer if the Inflight is still empty State1 = case RetryTimer == undefined of - true -> State#state{retry_timer = start_timer(Interval, retry_delivery)}; + true -> State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; false -> State end, - State1#state{inflight = emqx_inflight:insert(PacketId, {publish, Msg, os:timestamp()}, Inflight)}. + State1#state{inflight = emqx_inflight:insert(PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight)}. acked(puback, PacketId, State = #state{client_id = ClientId, username = Username, @@ -751,9 +750,9 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId, acked(pubcomp, PacketId, State = #state{inflight = Inflight}) -> State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Dequeue -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Do nothing if client is disconnected dequeue(State = #state{client_pid = undefined}) -> @@ -766,7 +765,7 @@ dequeue(State = #state{inflight = Inflight}) -> end. dequeue2(State = #state{mqueue = Q}) -> - case ?MQueue:out(Q) of + case emqx_mqueue:out(Q) of {empty, _Q} -> State; {{value, Msg}, Q1} -> @@ -774,43 +773,37 @@ dequeue2(State = #state{mqueue = Q}) -> dequeue(dispatch(Msg, State#state{mqueue = Q1})) end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Tune QoS -%%-------------------------------------------------------------------- tune_qos(Topic, Msg = #message{qos = PubQoS}, #state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) -> case maps:find(Topic, SubMap) of - {ok, SubQoS} when UpgradeQoS andalso (SubQoS > PubQoS) -> + {ok, #{qos := SubQoS}} when UpgradeQoS andalso (SubQoS > PubQoS) -> Msg#message{qos = SubQoS}; - {ok, SubQoS} when (not UpgradeQoS) andalso (SubQoS < PubQoS) -> + {ok, #{qos := SubQoS}} when (not UpgradeQoS) andalso (SubQoS < PubQoS) -> Msg#message{qos = SubQoS}; - {ok, _} -> - Msg; - error -> - Msg + {ok, _} -> Msg; + error -> Msg end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Reset Dup -%%-------------------------------------------------------------------- reset_dup(Msg) -> emqx_message:unset_flag(dup, Msg). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Next Msg Id -%%-------------------------------------------------------------------- -next_msg_id(State = #state{next_msg_id = 16#FFFF}) -> - State#state{next_msg_id = 1}; +next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) -> + State#state{next_pkt_id = 1}; -next_msg_id(State = #state{next_msg_id = Id}) -> - State#state{next_msg_id = Id + 1}. +next_pkt_id(State = #state{next_pkt_id = Id}) -> + State#state{next_pkt_id = Id + 1}. %%-------------------------------------------------------------------- %% Emit session stats -%%-------------------------------------------------------------------- emit_stats(State = #state{enable_stats = false}) -> State; @@ -822,7 +815,6 @@ inc_stats(Key) -> put(Key, get(Key) + 1). %%-------------------------------------------------------------------- %% Helper functions -%%-------------------------------------------------------------------- reply(Reply, State) -> {reply, Reply, State, hibernate}. diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 9380f38f7..2e4772f05 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -38,7 +38,7 @@ -define(TAB, emqx_shared_subscription). -record(state, {pmon}). --record(shared_subscription, {group, topic, subpid}). +-record(emqx_shared_subscription, {group, topic, subpid}). %%------------------------------------------------------------------------------ %% Mnesia bootstrap @@ -48,8 +48,8 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, {ram_copies, [node()]}, - {record_name, shared_subscription}, - {attributes, record_info(fields, shared_subscription)}]); + {record_name, emqx_shared_subscription}, + {attributes, record_info(fields, emqx_shared_subscription)}]); mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB). @@ -78,7 +78,7 @@ unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) -> mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)). record(Group, Topic, SubPid) -> - #shared_subscription{group = Group, topic = Topic, subpid = SubPid}. + #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. %% TODO: dispatch strategy, ensure the delivery... dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> @@ -110,7 +110,7 @@ init([]) -> init_monitors() -> mnesia:foldl( - fun(#shared_subscription{subpid = SubPid}, Mon) -> + fun(#emqx_shared_subscription{subpid = SubPid}, Mon) -> emqx_pmon:monitor(SubPid, Mon) end, emqx_pmon:new(), ?TAB). @@ -126,11 +126,11 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) -> - #shared_subscription{subpid = SubPid} = NewRecord, + #emqx_shared_subscription{subpid = SubPid} = NewRecord, {noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) -> - #shared_subscription{subpid = SubPid} = OldRecord, + #emqx_shared_subscription{subpid = SubPid} = OldRecord, {noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})}; handle_info({mnesia_table_event, _Event}, State) -> @@ -138,7 +138,7 @@ handle_info({mnesia_table_event, _Event}, State) -> handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> emqx_logger:info("[SharedSub] shared subscriber down: ~p", [SubPid]), - mnesia:async_dirty(fun cleanup_down/1, [SubPid]), + cleanup_down(SubPid), {noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})}; handle_info(Info, State) -> @@ -156,8 +156,10 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- cleanup_down(SubPid) -> - lists:foreach(fun(Record) -> mnesia:delete_object(?TAB, Record) end, - mnesia:match_object(#shared_subscription{_ = '_', subpid = SubPid})). + lists:foreach( + fun(Record) -> + mnesia:dirty_delete_object(?TAB, Record) + end,mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})). update_stats(State) -> emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index de16a2b0f..afa2d6b06 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -20,7 +20,7 @@ -export([start_link/0]). --export([open_session/1, lookup_session/1, close_session/1]). +-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([register_session/2, get_session_attrs/1, unregister_session/1]). -export([get_session_stats/1, set_session_stats/2]). @@ -46,7 +46,7 @@ start_link() -> gen_server:start_link({local, ?SM}, ?MODULE, [], []). %% @doc Open a session. --spec(open_session(map()) -> {ok, pid()} | {error, term()}). +-spec(open_session(map()) -> {ok, pid(), boolean()} | {error, term()}). open_session(Attrs = #{clean_start := true, client_id := ClientId, client_pid := ClientPid}) -> diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 4a9be13c0..701b9ae4e 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -22,7 +22,8 @@ -export([is_enabled/0]). -export([register_session/1, lookup_session/1, unregister_session/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). -define(REGISTRY, ?MODULE). -define(TAB, emqx_session_registry). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 61d48042e..cddfea8b5 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -1,18 +1,16 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% 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. -module(emqx_sup). @@ -65,6 +63,7 @@ init([]) -> BrokerSup = supervisor_spec(emqx_broker_sup), %% BridgeSup BridgeSup = supervisor_spec(emqx_bridge_sup_sup), + BridgeSup1 = supervisor_spec(emqx_bridge1_sup), %% AccessControl AccessControl = worker_spec(emqx_access_control), %% Session Manager @@ -73,8 +72,6 @@ init([]) -> SessionSup = supervisor_spec(emqx_session_sup), %% Connection Manager CMSup = supervisor_spec(emqx_cm_sup), - %% WebSocket Connection Sup - WSConnSup = supervisor_spec(emqx_ws_connection_sup), %% Sys Sup SysSup = supervisor_spec(emqx_sys_sup), {ok, {{one_for_all, 0, 1}, @@ -82,11 +79,11 @@ init([]) -> RouterSup, BrokerSup, BridgeSup, + BridgeSup1, AccessControl, SMSup, SessionSup, CMSup, - WSConnSup, SysSup]}}. %%-------------------------------------------------------------------- diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 8644f8801..57dc41703 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -171,6 +171,8 @@ publish(metrics, Metrics) -> safe_publish(Topic, Payload) -> safe_publish(Topic, #{}, Payload). safe_publish(Topic, Flags, Payload) -> - Flags1 = maps:merge(#{sys => true, qos => 0}, Flags), - emqx_broker:safe_publish(emqx_message:new(?SYS, Flags1, Topic, iolist_to_binary(Payload))). + emqx_broker:safe_publish( + emqx_message:set_flags( + maps:merge(#{sys => true}, Flags), + emqx_message:make(?SYS, Topic, iolist_to_binary(Payload)))). diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index af6d39138..b42d96aa4 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -43,7 +43,7 @@ init([Opts]) -> {ok, start_timer(#state{events = []})}. start_timer(State) -> - State#state{timer = emqx_misc:start_timer(timer:seconds(2), tick)}. + State#state{timer = emqx_misc:start_timer(timer:seconds(2), reset)}. parse_opt(Opts) -> parse_opt(Opts, []). @@ -126,7 +126,7 @@ handle_info({monitor, SusPid, busy_dist_port, Port}, State) -> safe_publish(busy_dist_port, WarnMsg) end, State); -handle_info(reset, State) -> +handle_info({timeout, _Ref, reset}, State) -> {noreply, State#state{events = []}, hibernate}; handle_info(Info, State) -> @@ -158,5 +158,5 @@ safe_publish(Event, WarnMsg) -> emqx_broker:safe_publish(sysmon_msg(Topic, iolist_to_binary(WarnMsg))). sysmon_msg(Topic, Payload) -> - emqx_message:new(?SYSMON, #{sys => true, qos => 0}, Topic, Payload). + emqx_message:make(?SYSMON, #{sys => true}, Topic, Payload). diff --git a/src/emqx_sys_sup.erl b/src/emqx_sys_sup.erl index 0e56e5fc5..b450ddd66 100644 --- a/src/emqx_sys_sup.erl +++ b/src/emqx_sys_sup.erl @@ -31,8 +31,7 @@ init([]) -> type => worker, modules => [emqx_sys]}, Sysmon = #{id => sys_mon, - start => {emqx_sys_mon, start_link, - [emqx_config:get_env(sysmon, [])]}, + start => {emqx_sys_mon, start_link, [emqx_config:get_env(sysmon, [])]}, restart => permanent, shutdown => 5000, type => worker, diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 2e69638dc..0d74168c4 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -14,23 +14,16 @@ -module(emqx_time). --export([seed/0, now_secs/0, now_secs/1, now_ms/0, now_ms/1, ts_from_ms/1]). +-export([seed/0, now_secs/0, now_ms/0, now_ms/1]). seed() -> rand:seed(exsplus, erlang:timestamp()). +now_secs() -> + erlang:system_time(second). + now_ms() -> - now_ms(os:timestamp()). + erlang:system_time(millisecond). now_ms({MegaSecs, Secs, MicroSecs}) -> - (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). - -now_secs() -> - now_secs(os:timestamp()). - -now_secs({MegaSecs, Secs, _MicroSecs}) -> - MegaSecs * 1000000 + Secs. - -ts_from_ms(Ms) -> - {Ms div 1000000, Ms rem 1000000, 0}. - + (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). \ No newline at end of file diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 43ab8e0df..3bf42f6ac 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -25,10 +25,9 @@ -type(word() :: '' | '+' | '#' | binary()). -type(words() :: list(word())). --type(option() :: {qos, mqtt_qos()} | {share, '$queue' | binary()}). -type(triple() :: {root | binary(), word(), binary()}). --export_type([option/0, word/0, triple/0]). +-export_type([word/0, triple/0]). -define(MAX_TOPIC_LEN, 4096). @@ -163,20 +162,21 @@ join(Words) -> end, {true, <<>>}, [bin(W) || W <- Words]), Bin. --spec(parse(topic()) -> {topic(), [option()]}). +-spec(parse(topic()) -> {topic(), #{}}). parse(Topic) when is_binary(Topic) -> - parse(Topic, []). + parse(Topic, #{}). parse(Topic = <<"$queue/", Topic1/binary>>, Options) -> - case lists:keyfind(share, 1, Options) of - {share, _} -> error({invalid_topic, Topic}); - false -> parse(Topic1, [{share, '$queue'} | 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 lists:keyfind(share, 1, Options) of - {share, _} -> error({invalid_topic, Topic}); - false -> [Group, Topic2] = binary:split(Topic1, <<"/">>), - {Topic2, [{share, Group} | 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, Options) -> {Topic, Options}. +parse(Topic, Options) -> + {Topic, Options}. diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 0f16c858c..65e6f6378 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -19,6 +19,7 @@ -include("emqx.hrl"). -export([start_link/0]). +-export([trace/2]). -export([start_trace/2, lookup_traces/0, stop_trace/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, @@ -31,14 +32,17 @@ -define(TRACER, ?MODULE). -define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]). -%%------------------------------------------------------------------------------ -%% Start the tracer -%%------------------------------------------------------------------------------ - -spec(start_link() -> {ok, pid()} | ignore | {error, term()}). start_link() -> gen_server:start_link({local, ?TRACER}, ?MODULE, [], []). +trace(publish, #message{topic = <<"$SYS/", _/binary>>}) -> + %% Dont' trace '$SYS' publish + ignore; +trace(publish, #message{from = From, topic = Topic, payload = Payload}) + when is_binary(From); is_atom(From) -> + emqx_logger:info([{client, From}, {topic, Topic}], "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + %%------------------------------------------------------------------------------ %% Start/Stop trace %%------------------------------------------------------------------------------ diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index c367ddbc4..bf6388232 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -17,21 +17,15 @@ -module(emqx_vm). -export([schedulers/0]). - -export([microsecs/0]). - -export([loads/0, get_system_info/0, get_system_info/1, mem_info/0, scheduler_usage/1]). - -export([get_memory/0]). - -export([get_process_list/0, get_process_info/0, get_process_info/1, get_process_gc/0, get_process_gc/1, get_process_group_leader_info/1, get_process_limit/0]). - -export([get_ets_list/0, get_ets_info/0, get_ets_info/1, get_ets_object/0, get_ets_object/1]). - -export([get_port_types/0, get_port_info/0, get_port_info/1]). -define(UTIL_ALLOCATORS, [temp_alloc, @@ -204,13 +198,13 @@ mem_info() -> [{total_memory, proplists:get_value(total_memory, Dataset)}, {used_memory, proplists:get_value(total_memory, Dataset) - proplists:get_value(free_memory, Dataset)}]. -ftos(F) -> - [S] = io_lib:format("~.2f", [F]), S. +ftos(F) -> + S = io_lib:format("~.2f", [F]), S. -%%%% erlang vm scheduler_usage fun copied from recon +%%%% erlang vm scheduler_usage fun copied from recon scheduler_usage(Interval) when is_integer(Interval) -> %% We start and stop the scheduler_wall_time system flag - %% if it wasn't in place already. Usually setting the flag + %% if it wasn't in place already. Usually setting the flag %% should have a CPU impact(make it higher) only when under low usage. FormerFlag = erlang:system_flag(scheduler_wall_time, true), First = erlang:statistics(scheduler_wall_time), @@ -300,7 +294,7 @@ get_process_group_leader_info(LeaderPid) when is_pid(LeaderPid) -> [{Key, Value}|| {Key, Value} <- process_info(LeaderPid), lists:member(Key, ?PROCESS_INFO)]. get_process_limit() -> - erlang:system_info(process_limit). + erlang:system_info(process_limit). get_ets_list() -> ets:all(). diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl deleted file mode 100644 index e7ad63d61..000000000 --- a/src/emqx_ws.erl +++ /dev/null @@ -1,119 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_ws). - --include("emqx_mqtt.hrl"). - --import(proplists, [get_value/3]). - --export([handle_request/1, ws_loop/3]). - -%% WebSocket Loop State --record(wsocket_state, {peername, client_pid, max_packet_size, parser}). - --define(WSLOG(Level, Format, Args, State), - emqx_logger:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsocket_state.peername) | Args])). - - -handle_request(Req) -> - handle_request(Req:get(method), Req:get(path), Req). - -%%-------------------------------------------------------------------- -%% MQTT Over WebSocket -%%-------------------------------------------------------------------- - -handle_request('GET', "/mqtt", Req) -> - emqx_logger:debug("WebSocket Connection from: ~s", [Req:get(peer)]), - Upgrade = Req:get_header_value("Upgrade"), - Proto = check_protocol_header(Req), - case {is_websocket(Upgrade), Proto} of - {true, "mqtt" ++ _Vsn} -> - case Req:get(peername) of - {ok, Peername} -> - {ok, ProtoEnv} = emqx_config:get_env(protocol), - PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), - Parser = emqx_parser:initial_state(PacketSize), - %% Upgrade WebSocket. - {ReentryWs, ReplyChannel} = mochiweb_websocket:upgrade_connection(Req, fun ?MODULE:ws_loop/3), - {ok, ClientPid} = emqx_ws_conn_sup:start_connection(self(), Req, ReplyChannel), - ReentryWs(#wsocket_state{peername = Peername, - parser = Parser, - max_packet_size = PacketSize, - client_pid = ClientPid}); - {error, Reason} -> - emqx_logger:error("Get peername with error ~s", [Reason]), - Req:respond({400, [], <<"Bad Request">>}) - end; - {false, _} -> - emqx_logger:error("Not WebSocket: Upgrade = ~s", [Upgrade]), - Req:respond({400, [], <<"Bad Request">>}); - {_, Proto} -> - emqx_logger:error("WebSocket with error Protocol: ~s", [Proto]), - Req:respond({400, [], <<"Bad WebSocket Protocol">>}) - end; - -handle_request(Method, Path, Req) -> - emqx_logger:error("Unexpected WS Request: ~s ~s", [Method, Path]), - Req:not_found(). - -is_websocket(Upgrade) -> - (not emqx_config:get_env(websocket_check_upgrade_header, true)) orelse - (Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket"). - -check_protocol_header(Req) -> - case emqx_config:get_env(websocket_protocol_header, false) of - true -> get_protocol_header(Req); - false -> "mqtt-v3.1.1" - end. - -get_protocol_header(Req) -> - case Req:get_header_value("EMQ-WebSocket-Protocol") of - undefined -> Req:get_header_value("Sec-WebSocket-Protocol"); - Proto -> Proto - end. - -%%-------------------------------------------------------------------- -%% Receive Loop -%%-------------------------------------------------------------------- - -%% @doc WebSocket frame receive loop. -ws_loop(<<>>, State, _ReplyChannel) -> - State; -ws_loop([<<>>], State, _ReplyChannel) -> - State; -ws_loop(Data, State = #wsocket_state{client_pid = ClientPid, parser = Parser}, ReplyChannel) -> - ?WSLOG(debug, "RECV ~p", [Data], State), - emqx_metrics:inc('bytes/received', iolist_size(Data)), - case catch emqx_parser:parse(iolist_to_binary(Data), Parser) of - {more, NewParser} -> - State#wsocket_state{parser = NewParser}; - {ok, Packet, Rest} -> - gen_server:cast(ClientPid, {received, Packet}), - ws_loop(Rest, reset_parser(State), ReplyChannel); - {error, Error} -> - ?WSLOG(error, "Frame error: ~p", [Error], State), - exit({shutdown, Error}); - {'EXIT', Reason} -> - ?WSLOG(error, "Frame error: ~p", [Reason], State), - ?WSLOG(error, "Error data: ~p", [Data], State), - exit({shutdown, parser_error}) - end. - -reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) -> - State#wsocket_state{parser = emqx_parser:initial_state(PacketSize)}. - diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 83a55c21e..5932b9ef7 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -1,228 +1,224 @@ -%%%=================================================================== -%%% Copyright (c) 2013-2018 EMQ Inc. 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. -%%%=================================================================== +%% 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. -module(emqx_ws_connection). --behaviour(gen_server). - -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). +-include("emqx_misc.hrl"). --import(proplists, [get_value/2, get_value/3]). - -%% API Exports --export([start_link/4]). - -%% Management and Monitor API --export([info/1, stats/1, kick/1, clean_acl_cache/2]). - -%% SUB/UNSUB Asynchronously --export([subscribe/2, unsubscribe/2]). - -%% Get the session proc? +-export([info/1]). +-export([stats/1]). +-export([kick/1]). -export([session/1]). -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +%% websocket callbacks +-export([init/2]). +-export([websocket_init/1]). +-export([websocket_handle/2]). +-export([websocket_info/2]). +-export([terminate/3]). -%% TODO: remove ... --export([handle_pre_hibernate/1]). +-record(state, { + request, + options, + peername, + sockname, + proto_state, + parser_state, + keepalive, + enable_stats, + stats_timer, + idle_timeout, + shutdown_reason + }). -%% WebSocket Client State --record(wsclient_state, {ws_pid, transport, socket, peername, - proto_state, keepalive, enable_stats, - force_gc_count}). +-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). --define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). +-define(INFO_KEYS, [peername, sockname]). -define(WSLOG(Level, Format, Args, State), - emqx_logger:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsclient_state.peername) | Args])). + lager:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). -%% @doc Start WebSocket Client. -start_link(Env, WsPid, Req, ReplyChannel) -> - gen_server:start_link(?MODULE, [Env, WsPid, Req, ReplyChannel], - [[{hibernate_after, 10000}]]). +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ -info(CPid) -> - gen_server:call(CPid, info). +info(WSPid) -> + call(WSPid, info). -stats(CPid) -> - gen_server:call(CPid, stats). +stats(WSPid) -> + call(WSPid, stats). -kick(CPid) -> - gen_server:call(CPid, kick). +kick(WSPid) -> + call(WSPid, kick). -subscribe(CPid, TopicTable) -> - CPid ! {subscribe, TopicTable}. +session(WSPid) -> + call(WSPid, session). -unsubscribe(CPid, Topics) -> - CPid ! {unsubscribe, Topics}. - -session(CPid) -> - gen_server:call(CPid, session). - -clean_acl_cache(CPid, Topic) -> - gen_server:call(CPid, {clean_acl_cache, Topic}). - -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- - -init([Env, WsPid, Req, ReplyChannel]) -> - process_flag(trap_exit, true), - true = link(WsPid), - Transport = mochiweb_request:get(transport, Req), - Sock = mochiweb_request:get(socket, Req), - case mochiweb_request:get(peername, Req) of - {ok, Peername} -> - Headers = mochiweb_headers:to_list(mochiweb_request:get(headers, Req)), - ProtoState = emqx_protocol:init(Transport, Sock, Peername, send_fun(ReplyChannel), - [{ws_initial_headers, Headers} | Env]), - IdleTimeout = get_value(client_idle_timeout, Env, 30000), - EnableStats = get_value(client_enable_stats, Env, false), - ForceGcCount = emqx_gc:conn_max_gc_count(), - {ok, #wsclient_state{transport = Transport, - socket = Sock, - ws_pid = WsPid, - peername = Peername, - proto_state = ProtoState, - enable_stats = EnableStats, - force_gc_count = ForceGcCount}, - IdleTimeout, {backoff, 2000, 2000, 20000}, ?MODULE}; - {error, enotconn} -> Transport:fast_close(Sock), - exit(WsPid, normal), - exit(normal); - {error, Reason} -> Transport:fast_close(Sock), - exit(WsPid, normal), - exit({shutdown, Reason}) +call(WSPid, Req) -> + Mref = erlang:monitor(process, WSPid), + WSPid ! {call, {self(), Mref}, Req}, + receive + {Mref, Reply} -> + erlang:demonitor(Mref, [flush]), + Reply; + {'DOWN', Mref, _, _, Reason} -> + exit(Reason) + after 5000 -> + erlang:demonitor(Mref, [flush]), + exit(timeout) end. -handle_pre_hibernate(State = #wsclient_state{ws_pid = WsPid}) -> - erlang:garbage_collect(WsPid), - {hibernate, emqx_gc:reset_conn_gc_count(#wsclient_state.force_gc_count, emit_stats(State))}. +%%------------------------------------------------------------------------------ +%% WebSocket callbacks +%%------------------------------------------------------------------------------ -handle_call(info, From, State = #wsclient_state{peername = Peername, - proto_state = ProtoState}) -> - Info = [{websocket, true}, {peername, Peername} | emqx_protocol:info(ProtoState)], - {reply, Stats, _, _} = handle_call(stats, From, State), - reply(lists:append(Info, Stats), State); +init(Req, Opts) -> + io:format("Opts: ~p~n", [Opts]), + case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of + undefined -> + {cowboy_websocket, Req, #state{}}; + Subprotocols -> + case lists:member(<<"mqtt">>, Subprotocols) of + true -> + Resp = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req), + {cowboy_websocket, Resp, #state{request = Req, options = Opts}, #{idle_timeout => 86400000}}; + false -> + {ok, cowboy_req:reply(400, Req), #state{}} + end + end. -handle_call(stats, _From, State = #wsclient_state{proto_state = ProtoState}) -> - reply(lists:append([emqx_misc:proc_stats(), - wsock_stats(State), - emqx_protocol:stats(ProtoState)]), State); +websocket_init(#state{request = Req, options = Options}) -> + Peername = cowboy_req:peer(Req), + Sockname = cowboy_req:sock(Req), + Peercert = cowboy_req:cert(Req), + ProtoState = emqx_protocol:init(#{peername => Peername, + sockname => Sockname, + peercert => Peercert, + 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), + lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS), + {ok, #state{peername = Peername, + sockname = Sockname, + parser_state = ParserState, + proto_state = ProtoState, + enable_stats = EnableStats, + idle_timeout = IdleTimout}}. -handle_call(kick, _From, State) -> - {stop, {shutdown, kick}, ok, State}; +send_fun(WsPid) -> + fun(Data) -> + BinSize = iolist_size(Data), + emqx_metrics:inc('bytes/sent', BinSize), + put(send_oct, get(send_oct) + BinSize), + put(send_cnt, get(send_cnt) + 1), + WsPid ! {binary, iolist_to_binary(Data)} + end. -handle_call(session, _From, State = #wsclient_state{proto_state = ProtoState}) -> - reply(emqx_protocol:session(ProtoState), State); +stat_fun() -> + fun() -> {ok, get(recv_oct)} end. -handle_call({clean_acl_cache, Topic}, _From, State) -> - erase({acl, publish, Topic}), - reply(ok, State); - -handle_call(Req, _From, State) -> - ?WSLOG(error, "Unexpected request: ~p", [Req], State), - reply({error, unexpected_request}, State). - -handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState}) -> - emqx_metrics:received(Packet), - case emqx_protocol:received(Packet, ProtoState) of - {ok, ProtoState1} -> - {noreply, gc(State#wsclient_state{proto_state = ProtoState1}), hibernate}; +websocket_handle({binary, <<>>}, State) -> + {ok, State}; +websocket_handle({binary, [<<>>]}, State) -> + {ok, State}; +websocket_handle({binary, Data}, State = #state{parser_state = ParserState, + proto_state = ProtoState}) -> + BinSize = iolist_size(Data), + put(recv_oct, get(recv_oct) + BinSize), + ?WSLOG(debug, "RECV ~p", [Data], State), + emqx_metrics:inc('bytes/received', BinSize), + case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of + {more, NewParserState} -> + {ok, State#state{parser_state = NewParserState}}; + {ok, Packet, Rest} -> + emqx_metrics:received(Packet), + put(recv_cnt, get(recv_cnt) + 1), + case emqx_protocol:received(Packet, ProtoState) of + {ok, ProtoState1} -> + websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1})); + {error, Error} -> + ?WSLOG(error, "Protocol error - ~p", [Error], State), + {stop, State}; + {error, Error, ProtoState1} -> + shutdown(Error, State#state{proto_state = ProtoState1}); + {stop, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) + end; {error, Error} -> - ?WSLOG(error, "Protocol error - ~p", [Error], State), - shutdown(Error, State); - {error, Error, ProtoState1} -> - shutdown(Error, State#wsclient_state{proto_state = ProtoState1}); - {stop, Reason, ProtoState1} -> - stop(Reason, State#wsclient_state{proto_state = ProtoState1}) + ?WSLOG(error, "Frame error: ~p", [Error], State), + {stop, State}; + {'EXIT', Reason} -> + ?WSLOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data], State), + {stop, State} + end. + +websocket_info({call, From, info}, State = #state{peername = Peername, + sockname = Sockname, + proto_state = ProtoState}) -> + ProtoInfo = emqx_protocol:info(ProtoState), + ConnInfo = [{socktype, websocket}, {conn_state, running}, + {peername, Peername}, {sockname, Sockname}], + gen_server:reply(From, lists:append([ConnInfo, ProtoInfo])), + {ok, State}; + +websocket_info({call, From, stats}, State = #state{proto_state = ProtoState}) -> + Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState)]), + gen_server:reply(From, Stats), + {ok, State}; + +websocket_info({call, From, kick}, State) -> + gen_server:reply(From, ok), + shutdown(kick, State); + +websocket_info({call, From, session}, State = #state{proto_state = ProtoState}) -> + gen_server:reply(From, emqx_protocol:session(ProtoState)), + {ok, State}; + +websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> + case emqx_protocol:deliver(PubOrAck, ProtoState) of + {ok, ProtoState1} -> + {ok, ensure_stats_timer(State#state{proto_state = ProtoState1})}; + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) end; -handle_cast(Msg, State) -> - ?WSLOG(error, "Unexpected Msg: ~p", [Msg], State), - {noreply, State, hibernate}. +websocket_info(emit_stats, State = #state{proto_state = ProtoState}) -> + Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), + emqx_protocol:stats(ProtoState)]), + emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats), + {ok, State#state{stats_timer = undefined}, hibernate}; -handle_info({subscribe, TopicTable}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:subscribe(TopicTable, ProtoState) - end, State); - -handle_info({unsubscribe, Topics}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:unsubscribe(Topics, ProtoState) - end, State); - -handle_info({suback, PacketId, GrantedQos}, State) -> - with_proto( - fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, GrantedQos), - emqx_protocol:send(Packet, ProtoState) - end, State); - -%% Fastlane -handle_info({dispatch, _Topic, Message}, State) -> - handle_info({deliver, Message#message{qos = ?QOS_0}}, State); - -handle_info({deliver, Message}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:send(Message, ProtoState) - end, gc(State)); - -handle_info({redeliver, {?PUBREL, PacketId}}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:pubrel(PacketId, ProtoState) - end, State); - -handle_info(emit_stats, State) -> - {noreply, emit_stats(State), hibernate}; - -handle_info(timeout, State) -> - shutdown(idle_timeout, State); - -handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> - ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), - shutdown(conflict, State); - -handle_info({shutdown, Reason}, State) -> - shutdown(Reason, State); - -handle_info({keepalive, start, Interval}, - State = #wsclient_state{transport = Transport, socket =Sock}) -> +websocket_info({keepalive, start, Interval}, State) -> ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqx_keepalive:start(stat_fun(Transport, Sock), Interval, {keepalive, check}) of + case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of {ok, KeepAlive} -> - {noreply, State#wsclient_state{keepalive = KeepAlive}, hibernate}; + {ok, State#state{keepalive = KeepAlive}}; {error, Error} -> ?WSLOG(warning, "Keepalive error - ~p", [Error], State), shutdown(Error, State) end; -handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive}) -> +websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> - {noreply, emit_stats(State#wsclient_state{keepalive = KeepAlive1}), hibernate}; + {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> ?WSLOG(debug, "Keepalive Timeout!", [], State), shutdown(keepalive_timeout, State); @@ -231,92 +227,46 @@ handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive}) shutdown(keepalive_error, State) end; -handle_info({'EXIT', WsPid, normal}, State = #wsclient_state{ws_pid = WsPid}) -> - stop(normal, State); +websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> + ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), + shutdown(conflict, State); -handle_info({'EXIT', WsPid, Reason}, State = #wsclient_state{ws_pid = WsPid}) -> - ?WSLOG(error, "shutdown: ~p",[Reason], State), +websocket_info({binary, Data}, State) -> + {reply, {binary, Data}, State}; + +websocket_info({shutdown, Reason}, State) -> shutdown(Reason, State); -%% The session process exited unexpectedly. -handle_info({'EXIT', Pid, Reason}, State = #wsclient_state{proto_state = ProtoState}) -> - case emqx_protocol:session(ProtoState) of - Pid -> stop(Reason, State); - _ -> ?WSLOG(error, "Unexpected EXIT: ~p, Reason: ~p", [Pid, Reason], State), - {noreply, State, hibernate} - end; - -handle_info(Info, State) -> - ?WSLOG(error, "Unexpected Info: ~p", [Info], State), - {noreply, State, hibernate}. - -terminate(Reason, #wsclient_state{proto_state = ProtoState, keepalive = KeepAlive}) -> - emqx_keepalive:cancel(KeepAlive), - case Reason of - {shutdown, Error} -> - emqx_protocol:shutdown(Error, ProtoState); - _ -> - emqx_protocol:shutdown(Reason, ProtoState) - end. - -code_change(_OldVsn, State, _Extra) -> +websocket_info(Info, State) -> + ?WSLOG(error, "unexpected info: ~p", [Info], State), {ok, State}. -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -send_fun(ReplyChannel) -> - Self = self(), - fun(Packet) -> - Data = emqx_frame:serialize(Packet), - emqx_metrics:inc('bytes/sent', iolist_size(Data)), - case ReplyChannel({binary, Data}) of - ok -> ok; - {error, Reason} -> Self ! {shutdown, Reason} - end +terminate(SockError, _Req, #state{keepalive = Keepalive, + proto_state = ProtoState, + shutdown_reason = Reason}) -> + emqx_keepalive:cancel(Keepalive), + io:format("Websocket shutdown for ~p, sockerror: ~p~n", [Reason, SockError]), + case Reason of + undefined -> + ok; + %%emqx_protocol:shutdown(SockError, ProtoState); + _ -> + ok%%emqx_protocol:shutdown(Reason, ProtoState) end. -stat_fun(Transport, Sock) -> - fun() -> - case Transport:getstat(Sock, [recv_oct]) of - {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; - {error, Error} -> {error, Error} - end - end. +reset_parser(State = #state{proto_state = ProtoState}) -> + State#state{parser_state = emqx_protocol:parser(ProtoState)}. -emit_stats(State = #wsclient_state{proto_state = ProtoState}) -> - emit_stats(emqx_protocol:clientid(ProtoState), State). - -emit_stats(_ClientId, State = #wsclient_state{enable_stats = false}) -> - State; -emit_stats(undefined, State) -> - State; -emit_stats(ClientId, State) -> - {reply, Stats, _, _} = handle_call(stats, undefined, State), - emqx_cm:set_client_stats(ClientId, Stats), +ensure_stats_timer(State = #state{enable_stats = true, + stats_timer = undefined, + idle_timeout = Timeout}) -> + State#state{stats_timer = erlang:send_after(Timeout, self(), emit_stats)}; +ensure_stats_timer(State) -> State. -wsock_stats(#wsclient_state{transport = Transport, socket = Sock}) -> - case Transport:getstat(Sock, ?SOCK_STATS) of - {ok, Ss} -> Ss; - {error, _} -> [] - end. - -with_proto(Fun, State = #wsclient_state{proto_state = ProtoState}) -> - {ok, ProtoState1} = Fun(ProtoState), - {noreply, State#wsclient_state{proto_state = ProtoState1}, hibernate}. - -reply(Reply, State) -> - {reply, Reply, State, hibernate}. - shutdown(Reason, State) -> - stop({shutdown, Reason}, State). + {stop, State#state{shutdown_reason = Reason}}. -stop(Reason, State) -> - {stop, Reason, State}. - -gc(State) -> - Cb = fun() -> emit_stats(State) end, - emqx_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb). +wsock_stats() -> + [{Key, get(Key)} || Key <- ?SOCK_STATS]. diff --git a/src/emqx_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl deleted file mode 100644 index b58e7c956..000000000 --- a/src/emqx_ws_connection_sup.erl +++ /dev/null @@ -1,44 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_ws_connection_sup). - --behavior(supervisor). - --export([start_link/0, start_connection/3]). - --export([init/1]). - --spec(start_link() -> {ok, pid()}). -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% @doc Start a MQTT/WebSocket Connection. --spec(start_connection(pid(), mochiweb_request:request(), fun()) -> {ok, pid()}). -start_connection(WsPid, Req, ReplyChannel) -> - supervisor:start_child(?MODULE, [WsPid, Req, ReplyChannel]). - -%%-------------------------------------------------------------------- -%% Supervisor callbacks -%%-------------------------------------------------------------------- - -init([]) -> - %%TODO: Cannot upgrade the environments, Use zone? - Env = lists:append(emqx_config:get_env(client, []), emqx_config:get_env(protocol, [])), - {ok, {{simple_one_for_one, 0, 1}, - [{ws_connection, {emqx_ws_connection, start_link, [Env]}, - temporary, 5000, worker, [emqx_ws_connection]}]}}. - diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl new file mode 100644 index 000000000..830f08b89 --- /dev/null +++ b/src/emqx_zone.erl @@ -0,0 +1,86 @@ +%% 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. + +-module(emqx_zone). + +-behaviour(gen_server). + +-export([start_link/0]). + +-export([env/2, env/3]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-record(state, {timer}). + +-define(TAB, ?MODULE). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +env(undefined, Par) -> + emqx_config:get_env(Par); +env(Zone, Par) -> + env(Zone, Par, undefined). + +env(undefined, Par, Default) -> + emqx_config:get_env(Par, Default); +env(Zone, Par, Default) -> + try ets:lookup_element(?TAB, {Zone, Par}, 2) + catch error:badarg -> + emqx_config:get_env(Par, Default) + end. + +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init([]) -> + _ = emqx_tables:new(?TAB, [set, {read_concurrency, true}]), + {ok, element(2, handle_info(reload, #state{}))}. + +handle_call(Req, _From, State) -> + emqx_logger:error("[Zone] unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + emqx_logger:error("[Zone] unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info(reload, State) -> + lists:foreach( + fun({Zone, Opts}) -> + [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Opts] + end, emqx_config:get_env(zones, [])), + {noreply, ensure_reload_timer(State), hibernate}; + +handle_info(Info, State) -> + emqx_logger:error("[Zone] unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +ensure_reload_timer(State) -> + State#state{timer = erlang:send_after(5000, self(), reload)}. + diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl index e0cb0e26a..820c7ec32 100644 --- a/test/emqx_base62_SUITE.erl +++ b/test/emqx_base62_SUITE.erl @@ -26,11 +26,14 @@ all() -> [t_base62_encode]. t_base62_encode(_) -> - 10 = ?BASE62:decode(?BASE62:encode(10)), - 100 = ?BASE62:decode(?BASE62:encode(100)), - 9999 = ?BASE62:decode(?BASE62:encode(9999)), - 65535 = ?BASE62:decode(?BASE62:encode(65535)), + <<"10">> = ?BASE62:decode(?BASE62:encode(<<"10">>)), + <<"100">> = ?BASE62:decode(?BASE62:encode(<<"100">>)), + <<"9999">> = ?BASE62:decode(?BASE62:encode(<<"9999">>)), + <<"65535">> = ?BASE62:decode(?BASE62:encode(<<"65535">>)), <> = emqx_guid:gen(), <> = emqx_guid:gen(), - X = ?BASE62:decode(?BASE62:encode(X)), - Y = ?BASE62:decode(?BASE62:encode(Y)). + X = ?BASE62:decode(?BASE62:encode(X), integer), + Y = ?BASE62:decode(?BASE62:encode(Y), integer), + <<"helloworld">> = ?BASE62:decode(?BASE62:encode("helloworld")), + "helloworld" = ?BASE62:decode(?BASE62:encode("helloworld", string), string). +