From ca4cdfe4ee71c5bf676e441f86acc947068adaf6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 23:29:20 +0800 Subject: [PATCH 01/33] Move the passwd_hash/2 function to emqx-passwd project --- src/emqx_auth_mod.erl | 34 ---------------------------------- 1 file changed, 34 deletions(-) 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])). - From 6c58bbab2bdff16c36d1194ba1e5fc23987e29a1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 23:29:38 +0800 Subject: [PATCH 02/33] Remove emqx_lager_backend module --- src/emqx_lager_backend.erl | 84 -------------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 src/emqx_lager_backend.erl 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]))). - From 1735786ffbcde26b28e11f4e2d43154de0c11980 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 23:30:16 +0800 Subject: [PATCH 03/33] Remove pbkdf2 lager_syslog bcrypt deps --- Makefile | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 349baab95..38051db1c 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 mochiweb 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_mochiweb = git https://github.com/emqtt/mochiweb emqx30 +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 From 7ee54aac289e8db632d07d405c6cc3dee497bb6b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 18 Jul 2018 23:41:00 +0800 Subject: [PATCH 04/33] Add 'qos' field to message record --- TODO | 4 +- etc/emqx.conf | 10 +-- priv/emqx.schema | 42 ++++------ src/emqx.app.src | 2 +- src/emqx_alarm_mgr.erl | 2 +- src/emqx_connection.erl | 44 ++++------ src/emqx_listeners.erl | 22 ++--- src/emqx_message.erl | 1 + src/emqx_mqueue.erl | 4 +- src/emqx_protocol.erl | 74 +++++++---------- src/emqx_session.erl | 180 ++++++++++++++++++---------------------- src/emqx_sup.erl | 32 ++++--- src/emqx_sys.erl | 2 +- src/emqx_sys_mon.erl | 2 +- 14 files changed, 183 insertions(+), 238 deletions(-) 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/etc/emqx.conf b/etc/emqx.conf index 5aa4bf3ea..7101c3a7b 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -702,25 +702,25 @@ listener.tcp.external.acceptors = 16 ## Value: Number listener.tcp.external.max_clients = 102400 -## TODO: Zone of the external MQTT/TCP listener belonged to. +## Zone of the external MQTT/TCP listener belonged to. ## ## Value: String -## listener.tcp.external.zone = external +listener.tcp.external.zone = devicebound ## 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 +## Notice that supports wildcard mount:%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'. ## ## Value: burst,rate ## Unit: KB/sec -## listener.tcp.external.rate_limit = 100,10 +listener.tcp.external.rate_limit = 100,10 ## The access control rules for the MQTT/TCP listener. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index b4187397a..fe43f8f53 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1409,21 +1409,18 @@ 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, - 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)]) + {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, + {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)} | AccOpts(Prefix)]) end, TcpOpts = fun(Prefix) -> Filter([{backlog, cuttlefish:conf_get(Prefix ++ ".backlog", Conf, undefined)}, @@ -1460,11 +1457,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,9 +1469,8 @@ 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, @@ -1486,12 +1480,8 @@ end}. undefined -> []; ListenOn -> - SslOpts1 = case SslOpts(Prefix) of - [] -> []; - SslOpts0 -> [{sslopts, SslOpts0}] - end, - [{Atom(Type), ListenOn, [{connopts, ConnOpts(Prefix)}, - {sockopts, TcpOpts(Prefix)}| LisOpts(Prefix)] ++ SslOpts1}] + SslOpts1 = case SslOpts(Prefix) of [] -> []; SslOpts0 -> [{ssl_options, SslOpts0}] end, + [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)}|LisOpts(Prefix)] ++ SslOpts1}] end end, diff --git a/src/emqx.app.src b/src/emqx.app.src index f3ccb8fa3..36807e47b 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,ekka,esockd,mochiweb]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index f6901c325..e041341e2 100644 --- a/src/emqx_alarm_mgr.erl +++ b/src/emqx_alarm_mgr.erl @@ -131,7 +131,7 @@ 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). + emqx_message:make(?ALARM_MGR, #{sys => true}, topic(Type, AlarmId), Json). topic(alert, AlarmId) -> emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 544646701..cd71b1af4 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -17,24 +17,17 @@ -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([session/1]). @@ -48,15 +41,12 @@ keepalive, enable_stats, idle_timeout, force_gc_count}). -define(INFO_KEYS, [peername, conn_state, await_recv]). - -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("Conn(~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, Sock, Options) -> + {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Options]])}. info(CPid) -> gen_server:call(CPid, info). @@ -85,26 +75,27 @@ session(CPid) -> clean_acl_cache(CPid, Topic) -> gen_server:call(CPid, {clean_acl_cache, Topic}). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% gen_server callbacks -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -init([Transport, Sock, Env]) -> +init([Transport, Sock, Options]) -> case Transport:wait(Sock) of {ok, NewSock} -> {ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), - do_init(Transport, Sock, Peername, Env); + do_init(Transport, Sock, Peername, Options); {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), +do_init(Transport, Sock, Peername, Options) -> + io:format("Options: ~p~n", [Options]), + RateLimit = get_value(rate_limit, Options), + PacketSize = get_value(max_packet_size, Options, ?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), + ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Options), + EnableStats = get_value(client_enable_stats, Options, false), + IdleTimout = get_value(client_idle_timeout, Options, 30000), ForceGcCount = emqx_gc:conn_max_gc_count(), State = run_socket(#state{transport = Transport, socket = Sock, @@ -136,8 +127,7 @@ send_fun(Transport, Sock, Peername) -> 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})}. + State#state{parse_state = emqx_frame:initial_state(#{max_packet_size => Size, version => Version})}. handle_call(info, From, State = #state{proto_state = ProtoState}) -> ProtoInfo = emqx_protocol:info(ProtoState), @@ -194,10 +184,6 @@ handle_info({suback, PacketId, GrantedQos}, State) -> 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) -> diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 02de39123..c9697f0ca 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -42,10 +42,14 @@ start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> start_http_listener('mqtt:wss', ListenOn, Options). 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, []}). + SockOpts = esockd:parse_opt(Options), + MFA = {emqx_ws, handle_request, [Options -- SockOpts]}, + {ok, _} = mochiweb:start_http(Name, ListenOn, SockOpts, MFA). %% @doc Restart all listeners -spec(restart_all() -> ok). @@ -56,7 +60,7 @@ restart_all() -> restart_listener({tcp, ListenOn, _Opts}) -> esockd:reopen('mqtt:tcp', ListenOn); restart_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> - esockd:reopen('mqtt:tls', ListenOn); + esockd:reopen('mqtt:ssl', 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 -> @@ -73,7 +77,7 @@ stop_all() -> 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); + esockd:close('mqtt:ssl', 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 -> @@ -81,7 +85,7 @@ stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == 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 +93,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..77dbfbf82 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -37,6 +37,7 @@ new(From, Flags, Topic, Payload) when is_atom(From); is_record(From, client) -> -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(), + qos = ?QOS0, from = From, sender = self(), flags = Flags, diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index e6bd16540..db2f30cb6 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -21,7 +21,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 ----------------->| @@ -154,7 +154,7 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped %% @doc Enqueue a message. -spec(in(message(), mqueue()) -> mqueue()). -in(#message{flags = #{qos := ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> +in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index c932089e7..b2e9965e8 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -1,44 +1,36 @@ -%%%=================================================================== -%%% 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]). -ifdef(TEST). -compile(export_all). -endif. --record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0, - send_pkt = 0, send_msg = 0}). +-record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0}). %% Protocol State %% ws_initial_headers: Headers from first HTTP request for WebSocket Client. @@ -76,23 +68,22 @@ init(Peername, SendFun, Opts) -> keepalive_backoff = Backoff, stats_data = #proto_stats{enable_stats = EnableStats}}. -init(_Transport, _Sock, Peername, SendFun, Opts) -> - init(Peername, SendFun, Opts). - %%enrich_opt(Conn:opts(), Conn, ). +init(_Transport, _Sock, Peername, SendFun, Options) -> + enrich_opt(Options, init(Peername, SendFun, Options)). -enrich_opt([], _Conn, State) -> +enrich_opt([], 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). +enrich_opt([{mountpoint, MountPoint} | ConnOpts], State) -> + enrich_opt(ConnOpts, State#proto_state{mountpoint = MountPoint}); +%%enrich_opt([{peer_cert_as_username, N} | ConnOpts], State) -> +%% enrich_opt(ConnOpts, State#proto_state{peercert_username = peercert_username(N, Conn)}); +enrich_opt([_ | ConnOpts], State) -> + enrich_opt(ConnOpts, State). -peercert_username(cn, Conn) -> - Conn:peer_cert_common_name(); -peercert_username(dn, Conn) -> - Conn:peer_cert_subject(). +%%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; @@ -122,17 +113,14 @@ client(#proto_state{client_id = ClientId, proto_ver = ProtoVer, keepalive = Keepalive, will_msg = WillMsg, - ws_initial_headers = WsInitialHeaders, - mountpoint = MountPoint, - connected_at = Time}) -> + 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{id = ClientId, pid = ClientPid, username = Username, peername = Peername}. session(#proto_state{session = Session}) -> Session. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 83d53abc6..607a3644f 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -1,48 +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. -%%%=================================================================== - --module(emqx_session). - --behaviour(gen_server). - --include("emqx.hrl"). - --include("emqx_mqtt.hrl"). - --include("emqx_misc.hrl"). - --import(emqx_misc, [start_timer/2]). - --import(proplists, [get_value/2, get_value/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). +%% 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. %% A stateful interaction between a Client and a Server. Some Sessions %% last only as long as the Network Connection, others can span multiple @@ -66,6 +34,32 @@ %% %% If the session is currently disconnected, the time at which the Session state %% will be deleted. +-module(emqx_session). + +-behaviour(gen_server). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). +-include("emqx_misc.hrl"). + +-import(emqx_misc, [start_timer/2]). +-import(proplists, [get_value/2, get_value/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]). +-export([publish/2, puback/2, pubrec/2, pubrel/2, pubcomp/2]). +-export([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). + -record(state, { %% Clean Start Flag clean_start = false :: boolean(), @@ -145,9 +139,7 @@ }). -define(TIMEOUT, 60000). - -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, max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, @@ -169,71 +161,71 @@ start_link(Attrs) -> %% @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}). +subscribe(SPid, TopicTable) -> %%TODO: the ack function??... + gen_server:cast(SPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). -spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SessionPid, PacketId, TopicTable) -> %%TODO: the ack function??... +subscribe(SPid, PacketId, TopicTable) -> %%TODO: the ack function??... From = self(), AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, - gen_server:cast(SessionPid, {subscribe, From, TopicTable, AckFun}). + gen_server:cast(SPid, {subscribe, From, TopicTable, AckFun}). %% @doc Publish Message --spec(publish(pid(), message()) -> ok | {error, term()}). -publish(_SessionPid, Msg = #message{qos = ?QOS_0}) -> +-spec(publish(pid(), message()) -> {ok, delivery()} | {error, term()}). +publish(_SPid, Msg = #message{qos = ?QOS_0}) -> %% Publish QoS0 Directly - emqx_broker:publish(Msg), ok; + emqx_broker:publish(Msg); -publish(_SessionPid, Msg = #message{qos = ?QOS_1}) -> +publish(_SPid, Msg = #message{qos = ?QOS_1}) -> %% Publish QoS1 message directly for client will PubAck automatically - emqx_broker:publish(Msg), ok; + emqx_broker:publish(Msg); -publish(SessionPid, Msg = #message{qos = ?QOS_2}) -> +publish(SPid, Msg = #message{qos = ?QOS_2}) -> %% Publish QoS2 to Session - gen_server:call(SessionPid, {publish, Msg}, ?TIMEOUT). + gen_server:call(SPid, {publish, Msg}, infinity). %% @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}). -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}). -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}). +unsubscribe(SPid, TopicTable) -> + gen_server:cast(SPid, {unsubscribe, self(), TopicTable}). %% @doc Resume the session -spec(resume(pid(), pid()) -> ok). -resume(SessionPid, ClientPid) -> - gen_server:cast(SessionPid, {resume, ClientPid}). +resume(SPid, ClientPid) -> + gen_server:cast(SPid, {resume, ClientPid}). %% @doc Get session state -state(SessionPid) when is_pid(SessionPid) -> - gen_server:call(SessionPid, state). +state(SPid) when is_pid(SPid) -> + gen_server:call(SPid, state). %% @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, @@ -257,8 +249,8 @@ 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}). %%-------------------------------------------------------------------- %% gen_server Callbacks @@ -342,41 +334,34 @@ handle_call(state, _From, State) -> reply(?record_to_proplist(state, State, ?STATE_KEYS), 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}) -> + 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, + NewQos = get_value(qos, Opts), 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}]), + %% TODO:.... + emqx_broker:set_subopts(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), + ?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, + %% TODO:.... + emqx:subscribe(Topic, ClientId, Opts), 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}; @@ -501,7 +486,7 @@ 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 @@ -546,16 +531,15 @@ handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) -> 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). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 61d48042e..3df468f1e 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). @@ -74,7 +72,7 @@ init([]) -> %% Connection Manager CMSup = supervisor_spec(emqx_cm_sup), %% WebSocket Connection Sup - WSConnSup = supervisor_spec(emqx_ws_connection_sup), + %% WSConnSup = supervisor_spec(emqx_ws_connection_sup), %% Sys Sup SysSup = supervisor_spec(emqx_sys_sup), {ok, {{one_for_all, 0, 1}, @@ -86,7 +84,7 @@ init([]) -> SMSup, SessionSup, CMSup, - WSConnSup, + %%WSConnSup, SysSup]}}. %%-------------------------------------------------------------------- diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 8644f8801..667ef0f1a 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -171,6 +171,6 @@ publish(metrics, Metrics) -> safe_publish(Topic, Payload) -> safe_publish(Topic, #{}, Payload). safe_publish(Topic, Flags, Payload) -> - Flags1 = maps:merge(#{sys => true, qos => 0}, Flags), + Flags1 = maps:merge(#{sys => true}, Flags), emqx_broker:safe_publish(emqx_message:new(?SYS, Flags1, Topic, iolist_to_binary(Payload))). diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index af6d39138..435dfaaee 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -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:new(?SYSMON, #{sys => true}, Topic, Payload). From e2a34ec98d3c377d0b2cf6f29bbdf81d5fdb50b7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 09:34:49 +0800 Subject: [PATCH 05/33] Comment lager syslog to be compatible with Erlang/OTP R21 --- etc/emqx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 7101c3a7b..b95e8b1f1 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -391,12 +391,12 @@ log.crash.file = {{ platform_log_dir }}/crash.log ## Enable syslog. ## ## Values: on | off -log.syslog = on +## 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 From cbbc231210df5604719db57887b080637fd0e030 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 10:08:37 +0800 Subject: [PATCH 06/33] Comment log.syslog.* to fix building issue --- etc/emqx.conf | 2 +- priv/emqx.schema | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index b95e8b1f1..d13348908 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -391,7 +391,7 @@ log.crash.file = {{ platform_log_dir }}/crash.log ## Enable syslog. ## ## Values: on | off -## log.syslog = on +log.syslog = on ## Sets the severity level for syslog. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index fe43f8f53..a3d2cfa32 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -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}, @@ -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 }. From 429703341583a2fe3149582e23e6c63004a6c2a6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 10:19:25 +0800 Subject: [PATCH 07/33] Fix undefined emqx_vm:schedulers/1 --- src/emqx.app.src | 2 +- src/emqx_pool_sup.erl | 4 ++-- src/emqx_sys_sup.erl | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/emqx.app.src b/src/emqx.app.src index 36807e47b..7d0dd0c4d 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -3,7 +3,7 @@ {vsn,"3.0"}, {modules,[]}, {registered,[emqx_sup]}, - {applications,[kernel, stdlib,jsx,gproc,gen_rpc,lager,ekka,esockd,mochiweb]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,mochiweb]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, 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_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, From eeeed35e2a46ecbeee958c551c34351d7af10835 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 10:46:05 +0800 Subject: [PATCH 08/33] Remove the alarm_fun arg from emqx_mqueue:new/3 --- src/emqx_bridge.erl | 4 +--- src/emqx_mqueue.erl | 42 +++++++++--------------------------------- src/emqx_session.erl | 2 +- 3 files changed, 11 insertions(+), 37 deletions(-) diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index ee2aa1535..517b32dcd 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}} diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index db2f30cb6..43bb8654a 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 @@ -43,14 +42,13 @@ -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, +-export([new/2, 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). @@ -79,24 +77,22 @@ %% 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]). -%% @doc New Queue. --spec(new(iolist(), list(option()), fun()) -> mqueue()). -new(Name, Opts, AlarmFun) -> +%% @doc New queue. +-spec(new(iolist(), list(option())) -> mqueue()). +new(Name, Opts) -> Type = get_value(type, Opts, simple), MaxLen = get_value(max_length, Opts, 0), 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). + qos0 = get_value(store_qos0, Opts, false)}, Opts). init_q(MQ = #mqueue{type = simple}, _Opts) -> MQ#mqueue{q = queue:new()}; @@ -163,7 +159,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, @@ -199,28 +195,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_session.erl b/src/emqx_session.erl index 607a3644f..02c7152b9 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -268,7 +268,7 @@ init(#{clean_start := CleanStart, 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()), + MQueue = ?MQueue:new(ClientId, QEnv), State = #state{clean_start = CleanStart, binding = binding(ClientPid), client_id = ClientId, From 83dee0e34009caf1b64df20d6ddfb098a39b4f87 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 19 Jul 2018 10:53:41 +0800 Subject: [PATCH 09/33] Rename emqx_alarm to emqx_alarm_mgr --- src/emqx_kernel_sup.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_kernel_sup.erl b/src/emqx_kernel_sup.erl index 96dde598c..25e96b930 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -26,7 +26,7 @@ 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), From 7d0cba94273f81c69cea7cb8ae3f00d20869e590 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 6 Aug 2018 16:33:10 +0800 Subject: [PATCH 10/33] Add MQTT 5.0 supports for connection, protocol and session modules --- docs/mqtt-v5.0.pdf | Bin 2605289 -> 2895576 bytes etc/emqx.conf | 38 ++- etc/zone.conf | 126 +++++++++ include/emqx.hrl | 154 +++++------ include/emqx_mqtt.hrl | 109 +++----- priv/emqx.schema | 21 +- src/emqx.erl | 8 +- src/emqx_alarm_mgr.erl | 6 +- src/emqx_bridge.erl | 4 +- src/emqx_broker.erl | 326 ++++++++++++----------- src/emqx_broker_helper.erl | 6 +- src/emqx_client.erl | 109 ++++---- src/emqx_connection.erl | 365 ++++++++++++++------------ src/emqx_frame.erl | 13 +- src/emqx_kernel_sup.erl | 1 + src/emqx_listeners.erl | 2 +- src/emqx_message.erl | 63 ++--- src/emqx_metrics.erl | 10 +- src/emqx_mod_presence.erl | 12 +- src/emqx_mod_subscription.erl | 2 +- src/emqx_mqtt_properties.erl | 25 +- src/emqx_mqueue.erl | 2 +- src/emqx_packet.erl | 57 ++-- src/emqx_protocol.erl | 471 +++++++++++++++++----------------- src/emqx_router.erl | 6 +- src/emqx_session.erl | 415 +++++++++++++++--------------- src/emqx_sm.erl | 2 +- src/emqx_sm_registry.erl | 3 +- src/emqx_sys.erl | 6 +- src/emqx_sys_mon.erl | 6 +- src/emqx_topic.erl | 24 +- src/emqx_tracer.erl | 12 +- src/emqx_vm.erl | 14 +- src/emqx_ws_connection.erl | 43 ++-- src/emqx_zone.erl | 78 ++++++ 35 files changed, 1382 insertions(+), 1157 deletions(-) create mode 100644 etc/zone.conf create mode 100644 src/emqx_zone.erl diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index 6e5cd42056adbbc99e718a7f8700aa15491751fa..5b7e403f86ee30f1518b3400acdcc06f77243f1b 100644 GIT binary patch delta 139568 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 delta 1643 zcmZuxJ#Q324Amv%2unz~gz#~MupAsqLE-Uy*wfKKg+QX_572;+s9BI)Q?e2TNc;c< z2#LQ!gM^fdCK3%bbGwIbZhhUzAZgOzt-?|5nBK2czoo*SFiZhqWn90RZSddBidu0m`RkVLdEv0>#CR zVRnlvwzRlH#8A@zf{f8G?z$%Ioy}wQtTTta$UHHEejGV#*5yU8kQbcQix4ur^E|WM zxXnX2)QOl4+x+=e?bq`PmW=A;Pw2xCTIF)G3iWv2qut%jq@GbZyd&NJ$ CZc)zw diff --git a/etc/emqx.conf b/etc/emqx.conf index d13348908..555a6d63b 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -702,10 +702,15 @@ listener.tcp.external.acceptors = 16 ## Value: Number listener.tcp.external.max_clients = 102400 +## Maximum connection per second. +## +## Value: Number +listener.tcp.external.max_conn_rate = 1000 + ## Zone of the external MQTT/TCP listener belonged to. ## ## Value: String -listener.tcp.external.zone = devicebound +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 @@ -831,10 +836,10 @@ listener.tcp.internal.acceptors = 4 ## Value: Number listener.tcp.internal.max_clients = 102400 -## TODO: Zone of the internal MQTT/TCP listener belonged to. +## 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. ## @@ -932,10 +937,15 @@ listener.ssl.external.acceptors = 16 ## Value: Number listener.ssl.external.max_clients = 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 = 1000 + +## 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. ## @@ -1166,10 +1176,15 @@ listener.ws.external.acceptors = 4 ## Value: Number listener.ws.external.max_clients = 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 + +## 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. ## @@ -1294,10 +1309,15 @@ listener.wss.external.acceptors = 4 ## Value: Number listener.wss.external.max_clients = 64 -## TODO: Zone of the external MQTT/WebSocket/SSL listener belonged to. +## Maximum MQTT/WebSocket/SSL connections per second. +## +## Value: Number +listener.wss.external.max_conn_rate = 1000 + +## 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. ## diff --git a/etc/zone.conf b/etc/zone.conf new file mode 100644 index 000000000..5c546fe46 --- /dev/null +++ b/etc/zone.conf @@ -0,0 +1,126 @@ + +## Limits and Capabilities + +##-------------------------------------------------------------------- +## Connection + +zone.${name}.idle_timeout = 30s + +zone.${name}.rate_limit = 10,100 + +## 10 messages per second, with a bucket 100 messages. +zone.${name}.publish_limit = 10,100 + +## Enable stats +zone.${name}.enable_stats = on + +## zone.${name}.shutdown_policy = ??? + +##-------------------------------------------------------------------- +## Protocol + +## Capabilities: + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +zone.${name}.max_clientid_len = 1024 + +## Maximum MQTT packet size allowed. +## +## Value: Bytes +## +## Default: 64K +zone.${name}.max_packet_size = 64K +zone.${name}.max_topic_alias = 0 +zone.${name}.max_qos_allowed = 2 +zone.${name}.retain_available = on +zone.${name}.wildcard_subscription = on +zone.${name}.shared_subscription = off + +## The backoff for MQTT keepalive timeout. +## EMQ will kick a MQTT connection out until 'Keepalive * backoff * 2' timeout. +## +## Value: Float > 0.5 +zone.${name}.keepalive_backoff = 0.75 + +##-------------------------------------------------------------------- +## Authentication + +zone.${name}.allow_anonymous = true + +##-------------------------------------------------------------------- +## Session + +zone.${name}.max_subscriptions = 0 +zone.${name}.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.${name}.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.${name}.retry_interval = 20s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.${name}.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.${name}.await_rel_timeout = 30s + +## Whether to ignore loop delivery of messages. +## +## Value: true | false +## +## Default: false +zone.${name}.ignore_loop_deliver = false + +## Max session expiration time. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.${name}.session_expiry_interval = 2h + +##-------------------------------------------------------------------- +## Queue + +## Message queue type. +## +## Value: simple | priority +zone.${name}.mqueue_type = simple + +## Topic priority. Default is 0. +## +## Value: Number [0-255] +## +## zone.${name}.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 +zone.${name}.max_mqueue_len = 100 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.${name}.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## General + +zone.${name}.enable_stats = on + + diff --git a/include/emqx.hrl b/include/emqx.hrl index f0b2b46d1..7340cf835 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,8 +191,8 @@ %% Plugin %%-------------------------------------------------------------------- --record(plugin, - { name :: atom(), +-record(plugin, { + name :: atom(), version :: string(), dir :: string(), descr :: string(), @@ -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..35a8a5082 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -83,13 +83,12 @@ %%-------------------------------------------------------------------- %% 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 a3d2cfa32..7ff5fd0a3 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -933,6 +933,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.tcp.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.tcp.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1024,6 +1028,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.ssl.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.ssl.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1165,8 +1173,8 @@ end}. {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", [ @@ -1261,6 +1269,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1404,7 +1416,7 @@ 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, @@ -1413,9 +1425,10 @@ end}. LisOpts = fun(Prefix) -> Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {max_clients, cuttlefish:conf_get(Prefix ++ ".max_clients", 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, cuttlefish:conf_get(Prefix ++ ".rate_limit", 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))}, diff --git a/src/emqx.erl b/src/emqx.erl index a539bbd42..cbe37d12e 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)). diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index e041341e2..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}, 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_bridge.erl b/src/emqx_bridge.erl index 517b32dcd..eef5d249b 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -72,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_broker.erl b/src/emqx_broker.erl index 43cc81155..9d332a5f2 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_client.erl b/src/emqx_client.erl index 87b1e2bf3..8c742fbfd 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,29 @@ 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}}, State. -packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload)) -> +packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Props, 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}. + #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 +1033,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_connection.erl b/src/emqx_connection.erl index cd71b1af4..7dc70e6ce 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -20,33 +20,51 @@ -include("emqx_mqtt.hrl"). -include("emqx_misc.hrl"). --import(proplists, [get_value/2, get_value/3]). - -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([session/1]). -%% gen_server Function Exports +-export([info/1, stats/1, kick/1]). +-export([get_session/1]). +-export([clean_acl_cache/1]). +-export([get_rate_limit/1, set_rate_limit/2]). +-export([get_pub_limit/1, set_pub_limit/2]). + +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -%% 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, %% Throughput rate limit + limit_timer, %% Rate limit timer + proto_state, %% MQTT protocol state + parse_state, %% MQTT parse state + keepalive, %% MQTT keepalive timer + enable_stats, %% Enable stats + stats_timer, %% Stats timer + idle_timeout %% Connection idle timeout + }). + +-define(INFO_KEYS, [peername, sockname, conn_state, await_recv, rate_limit, pub_limit]). --define(INFO_KEYS, [peername, conn_state, await_recv]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). --define(LOG(Level, Format, Args, State), - emqx_logger:Level("Conn(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). -start_link(Transport, Sock, Options) -> - {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Options]])}. +-define(LOG(Level, Format, Args, State), + emqx_logger:Level("Conn(~s): " ++ Format, + [esockd_net:format(State#state.peername) | Args])). + +start_link(Transport, Socket, Options) -> + {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Socket, Options]])}. + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ info(CPid) -> gen_server:call(CPid, info). @@ -57,152 +75,151 @@ stats(CPid) -> kick(CPid) -> gen_server:call(CPid, kick). -set_rate_limit(CPid, Rl) -> - gen_server:call(CPid, {set_rate_limit, Rl}). +get_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). - -clean_acl_cache(CPid, Topic) -> - gen_server:call(CPid, {clean_acl_cache, Topic}). +set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> + gen_server:call(CPid, {set_pub_limit, Rl}). %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ -init([Transport, Sock, Options]) -> - case Transport:wait(Sock) of - {ok, NewSock} -> - {ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), - do_init(Transport, Sock, Peername, Options); +init([Transport, RawSocket, Options]) -> + case Transport:wait(RawSocket) of + {ok, Socket} -> + {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]), + Zone = proplists:get_value(zone, Options), + RateLimit = init_rate_limit(emqx_zone:get_env(Zone, rate_limit)), + PubLimit = init_rate_limit(emqx_zone:get_env(Zone, publish_limit)), + EnableStats = emqx_zone:get_env(Zone, enable_stats, false), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + SendFun = send_fun(Transport, Socket, Peername), + ProtoState = emqx_protocol:init(#{zone => Zone, + peername => Peername, + sockname => Sockname, + peercert => Peercert, + sendfun => SendFun}, Options), + ParseState = 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, + parse_state = ParseState, + 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, Options) -> - io:format("Options: ~p~n", [Options]), - RateLimit = get_value(rate_limit, Options), - PacketSize = get_value(max_packet_size, Options, ?MAX_PACKET_SIZE), - SendFun = send_fun(Transport, Sock, Peername), - ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Options), - EnableStats = get_value(client_enable_stats, Options, false), - IdleTimout = get_value(client_idle_timeout, Options, 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). +init_rate_limit(undefined) -> + undefined; +init_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(get_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) -> +handle_info(SubReq = {subscribe, _TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:subscribe(TopicTable, ProtoState) + emqx_protocol:process(SubReq, ProtoState) end, State); -handle_info({unsubscribe, Topics}, State) -> +handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:unsubscribe(Topics, ProtoState) + emqx_protocol:process(UnsubReq, ProtoState) end, State); -%% Asynchronous SUBACK -handle_info({suback, PacketId, GrantedQos}, State) -> +handle_info({deliver, PubOrAck}, State) -> with_proto( fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, GrantedQos), - emqx_protocol:send(Packet, ProtoState) - end, State); + emqx_protocol:deliver(PubOrAck, ProtoState) + end, maybe_gc(ensure_stats_timer(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); @@ -211,25 +228,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 @@ -258,20 +275,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} -> @@ -281,25 +296,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 TCP 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}) -> +handle_packet(Bytes, State = #state{incoming = Incoming, + 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} -> + {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})); + ParseState1 = emqx_protocol:parser(ProtoState1), + handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), + proto_state = ProtoState1, + parse_state = ParseState1}); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); @@ -312,22 +331,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; @@ -338,29 +368,21 @@ run_socket(State = #state{transport = Transport, socket = Sock}) -> 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), - State. - -sock_stats(#state{transport = Transport, socket = Sock}) -> - case Transport:getstat(Sock, ?SOCK_STATS) of - {ok, Ss} -> Ss; - _Error -> [] + case Fun(ProtoState) of + {ok, ProtoState1} -> + {noreply, State#state{proto_state = ProtoState1}}; + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) end. -reply(Reply, State) -> - {reply, Reply, State, hibernate}. +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. shutdown(Reason, State) -> stop({shutdown, Reason}, State). @@ -368,7 +390,8 @@ 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_frame.erl b/src/emqx_frame.erl index 0a261db6e..7385b7116 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, @@ -242,6 +242,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 +331,7 @@ parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> {Value + Len * Multiplier, Rest}. parse_topic_filters(subscribe, Bin) -> - [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} || <> <= Bin]; parse_topic_filters(unsubscribe, Bin) -> @@ -382,7 +385,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 +403,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_kernel_sup.erl b/src/emqx_kernel_sup.erl index 25e96b930..40ec7cfd7 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -31,6 +31,7 @@ init([]) -> 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_listeners.erl b/src/emqx_listeners.erl index c9697f0ca..9e8445414 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -33,7 +33,7 @@ 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); diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 77dbfbf82..ae8670942 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -17,45 +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(), - qos = ?QOS0, +-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)}. @@ -64,27 +62,22 @@ 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)}. + 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 43bb8654a..31811583f 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -150,7 +150,7 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped %% @doc Enqueue a message. -spec(in(message(), mqueue()) -> mqueue()). -in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> +in(#message{flags = #{qos := ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index dc88d59d7..8baa6f088 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,8 +106,8 @@ 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); @@ -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_protocol.erl b/src/emqx_protocol.erl index b2e9965e8..30b2c0294 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -18,108 +18,86 @@ -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([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, {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 = #{zone := Zone, peercert := Peercert}, Options) -> + MountPoint = emqx_zone:get_env(Zone, mountpoint), + Backoff = emqx_zone:get_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{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, Options) -> - enrich_opt(Options, init(Peername, SendFun, Options)). +capabilities(Zone) -> + Capabilities = emqx_zone:get_env(Zone, mqtt_capabilities, []), + maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)). -enrich_opt([], State) -> - State; -enrich_opt([{mountpoint, MountPoint} | ConnOpts], State) -> - enrich_opt(ConnOpts, State#proto_state{mountpoint = MountPoint}); -%%enrich_opt([{peer_cert_as_username, N} | ConnOpts], State) -> -%% enrich_opt(ConnOpts, State#proto_state{peercert_username = peercert_username(N, Conn)}); -enrich_opt([_ | ConnOpts], State) -> - enrich_opt(ConnOpts, 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(#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}) -> @@ -129,117 +107,93 @@ 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{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 + ProtoState2 = maybe_set_clientid(ProtoState1), + %% Open session case emqx_sm:open_session(#{clean_start => CleanStart, - client_id => clientid(State2), + 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) @@ -266,36 +220,49 @@ 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} end end; +process({subscribe, RawTopicTable}, + State = #proto_state{client_id = ClientId, + username = Username, + session = Session}) -> + TopicTable = parse_topic_filters(RawTopicTable), + case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of + {ok, TopicTable1} -> + emqx_session:subscribe(Session, TopicTable1); + {stop, _} -> ok + end, + {ok, State}; + %% Protect from empty topic list 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, @@ -308,84 +275,106 @@ process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics), end, send(?UNSUBACK_PACKET(PacketId), State); -process(?PACKET(?PINGREQ), State) -> - send(?PACKET(?PINGRESP), State); +process({unsubscribe, RawTopics}, State = #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, State}; -process(?PACKET(?DISCONNECT), State) -> +process(?PACKET(?PINGREQ), ProtoState) -> + send(?PACKET(?PINGRESP), ProtoState); + +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 + ok -> emqx_metrics:sent(Packet), + trace(send, Packet, ProtoState), + {ok, inc_stats(send, Type, ProtoState)}; + {error, Reason} -> + {error, Reason} + 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}; @@ -403,19 +392,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. @@ -430,10 +418,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; @@ -459,7 +447,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; @@ -481,7 +469,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} @@ -501,11 +489,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; @@ -518,17 +506,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..85a6a63ad 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -33,10 +33,8 @@ -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()}). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 02c7152b9..75e927a9c 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -11,29 +11,30 @@ %% 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 Session State in the Server consists of: %% -%% The existence of a Session, even if the rest of the Session state is empty. +%% The existence of a Session, even if the rest of the Session State is empty. %% -%% The Client’s subscriptions. +%% 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. +%% 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. +%% QoS 2 messages which have been received from the Client, but have not been +%% completely acknowledged.The Will Message and the Will Delay Interval %% -%% 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. +%% 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). @@ -42,26 +43,20 @@ -include("emqx_mqtt.hrl"). -include("emqx_misc.hrl"). --import(emqx_misc, [start_timer/2]). --import(proplists, [get_value/2, get_value/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]). --export([publish/2, puback/2, pubrec/2, pubrel/2, pubcomp/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, pubrec/2, pubrel/2, pubcomp/2]). -export([unsubscribe/2]). -%% gen_server Function Exports +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(MQueue, emqx_mqueue). - --record(state, - { %% Clean Start Flag +-record(state, { + %% Clean Start Flag clean_start = false :: boolean(), %% Client Binding: local | remote @@ -73,21 +68,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. @@ -106,18 +105,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 @@ -135,64 +134,63 @@ %% Ignore loop deliver? ignore_loop_deliver = false :: boolean(), + %% Created at created_at :: erlang:timestamp() }). -define(TIMEOUT, 60000). -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()}). +%% @doc Start a session +-spec(start_link(Attrs :: map()) -> {ok, pid()} | {error, term()}). start_link(Attrs) -> gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% PubSub API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -%% @doc Subscribe topics --spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SPid, TopicTable) -> %%TODO: the ack function??... - gen_server:cast(SPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). +%% for mqtt 5.0 +-spec(subscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +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(SPid, PacketId, TopicTable) -> %%TODO: the ack function??... - From = self(), - AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, - gen_server:cast(SPid, {subscribe, From, TopicTable, AckFun}). - -%% @doc Publish Message --spec(publish(pid(), message()) -> {ok, delivery()} | {error, term()}). -publish(_SPid, Msg = #message{qos = ?QOS_0}) -> - %% Publish QoS0 Directly +-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); -publish(_SPid, Msg = #message{qos = ?QOS_1}) -> - %% Publish QoS1 message directly for client will PubAck automatically +publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) -> + %% Publish QoS1 message to broker directly emqx_broker:publish(Msg); -publish(SPid, Msg = #message{qos = ?QOS_2}) -> - %% Publish QoS2 to Session - gen_server:call(SPid, {publish, Msg}, infinity). +publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> + %% Publish QoS2 message to session + gen_server:call(SPid, {publish, PacketId, Msg}, infinity). -%% @doc PubAck Message -spec(puback(pid(), mqtt_packet_id()) -> ok). 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(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(SPid, PacketId) -> gen_server:cast(SPid, {pubrel, PacketId}). @@ -201,20 +199,14 @@ pubrel(SPid, PacketId) -> pubcomp(SPid, PacketId) -> gen_server:cast(SPid, {pubcomp, PacketId}). -%% @doc Unsubscribe the topics --spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok). -unsubscribe(SPid, TopicTable) -> - gen_server:cast(SPid, {unsubscribe, self(), TopicTable}). +-spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +unsubscribe(SPid, UnsubReq = {PacketId, Properties, TopicFilters}) -> + gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). -%% @doc Resume the session -spec(resume(pid(), pid()) -> ok). resume(SPid, ClientPid) -> gen_server:cast(SPid, {resume, ClientPid}). -%% @doc Get session state -state(SPid) when is_pid(SPid) -> - gen_server:call(SPid, state). - %% @doc Get session info -spec(info(pid() | #state{}) -> list(tuple())). info(SPid) when is_pid(SPid) -> @@ -239,9 +231,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)}, @@ -250,41 +242,42 @@ stats(#state{max_subscriptions = MaxSubscriptions, %% @doc Discard the session -spec(discard(pid(), client_id()) -> ok). discard(SPid, ClientId) -> - gen_server:call(SPid, {discard, 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, - client_id := ClientId, - username := Username, - client_pid := ClientPid}) -> +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init(#{clean_start := CleanStart, client_id := ClientId, username := Username, client_pid := ClientPid}) -> 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), + MaxInflight = proplists:get_value(max_inflight, Env, 0), + EnableStats = proplists:get_value(enable_stats, Env, false), + IgnoreLoopDeliver = proplists:get_value(ignore_loop_deliver, Env, false), + MQueue = emqx_mqueue:new(ClientId, QEnv), 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 = proplists:get_value(max_subscriptions, Env, 0), + upgrade_qos = proplists:get_value(upgrade_qos, Env, false), max_inflight = MaxInflight, inflight = emqx_inflight:new(MaxInflight), mqueue = MQueue, - retry_interval = get_value(retry_interval, Env), + retry_interval = proplists:get_value(retry_interval, Env), 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), + await_rel_timeout = proplists:get_value(await_rel_timeout, Env), + max_awaiting_rel = proplists:get_value(max_awaiting_rel, Env), + expiry_interval = proplists:get_value(expiry_interval, Env), enable_stats = EnableStats, ignore_loop_deliver = IgnoreLoopDeliver, created_at = os:timestamp()}, @@ -307,19 +300,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; @@ -330,62 +323,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 call: ~p", [Req]), {reply, ignored, State}. -handle_cast({subscribe, From, TopicTable, AckFun}, +handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> - ?LOG(info, "Subscribe ~p", [TopicTable], State), - {GrantedQos, Subscriptions1} = - lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) -> - NewQos = get_value(qos, Opts), - SubMap1 = - case maps:find(Topic, SubMap) of - {ok, NewQos} -> - ?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State), - SubMap; - {ok, OldQos} -> - %% TODO:.... - emqx_broker:set_subopts(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 -> - %% TODO:.... - emqx:subscribe(Topic, ClientId, Opts), - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), - maps:put(Topic, NewQos, SubMap) - end, - {[NewQos|QosAcc], SubMap1} - end, {[], Subscriptions}, TopicTable), - AckFun(lists:reverse(GrantedQos)), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; + ?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, Username], {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, Username], {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, username = Username, 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, Username], {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}) -> @@ -490,12 +474,12 @@ handle_cast(Msg, State) -> {noreply, State}. %% Ignore Messages delivered by self -handle_info({dispatch, _Topic, #message{from = {ClientId, _}}}, +handle_info({dispatch, _Topic, #message{from = ClientId}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> {noreply, State}; %% Dispatch Message -handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> +handle_info({dispatch, Topic, Msg}, State) -> {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. @@ -521,7 +505,7 @@ 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}; @@ -543,12 +527,25 @@ 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; @@ -560,32 +557,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), @@ -593,12 +590,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 @@ -619,12 +616,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; @@ -635,18 +632,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}) -> @@ -657,53 +654,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, @@ -735,9 +729,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}) -> @@ -750,7 +744,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} -> @@ -758,9 +752,8 @@ 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}) -> @@ -775,26 +768,23 @@ tune_qos(Topic, Msg = #message{qos = PubQoS}, 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; @@ -806,7 +796,6 @@ inc_stats(Key) -> put(Key, get(Key) + 1). %%-------------------------------------------------------------------- %% Helper functions -%%-------------------------------------------------------------------- reply(Reply, State) -> {reply, Reply, State, hibernate}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index de16a2b0f..00f4bff3d 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -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_sys.erl b/src/emqx_sys.erl index 667ef0f1a..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}, 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 435dfaaee..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}, Topic, Payload). + emqx_message:make(?SYSMON, #{sys => true}, Topic, Payload). 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..0f3a4eaa4 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) -> +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_connection.erl b/src/emqx_ws_connection.erl index 83a55c21e..dadc2cc57 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.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_ws_connection). @@ -157,8 +155,8 @@ handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState end; handle_cast(Msg, State) -> - ?WSLOG(error, "Unexpected Msg: ~p", [Msg], State), - {noreply, State, hibernate}. + ?WSLOG(error, "unexpected msg: ~p", [Msg], State), + {noreply, State}. handle_info({subscribe, TopicTable}, State) -> with_proto( @@ -172,10 +170,17 @@ handle_info({unsubscribe, Topics}, State) -> emqx_protocol:unsubscribe(Topics, ProtoState) end, State); -handle_info({suback, PacketId, GrantedQos}, State) -> +handle_info({suback, PacketId, ReasonCodes}, State) -> with_proto( fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, GrantedQos), + Packet = ?SUBACK_PACKET(PacketId, ReasonCodes), + emqx_protocol:send(Packet, ProtoState) + end, State); + +handle_info({unsuback, PacketId, ReasonCodes}, State) -> + with_proto( + fun(ProtoState) -> + Packet = ?UNSUBACK_PACKET(PacketId, ReasonCodes), emqx_protocol:send(Packet, ProtoState) end, State); diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl new file mode 100644 index 000000000..0d874a38b --- /dev/null +++ b/src/emqx_zone.erl @@ -0,0 +1,78 @@ +%% 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([get_env/2, get_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). +-define(SERVER, ?MODULE). + +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +get_env(Zone, Par) -> + get_env(Zone, Par, undefined). + +get_env(Zone, Par, Def) -> + try ets:lookup_element(?TAB, {Zone, Par}, 2) catch error:badarg -> Def 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, Options}) -> + [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Options] + 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)}. + From 645c971a07210f4a38feb01ea5fb4fe198e43388 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 6 Aug 2018 17:09:14 +0800 Subject: [PATCH 11/33] Fix QoS tuning --- src/emqx_session.erl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 75e927a9c..03bcae3f2 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -758,14 +758,12 @@ dequeue2(State = #state{mqueue = Q}) -> 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. %%------------------------------------------------------------------------------ From 79481db659cb752efe33eb9b3f82e82389a5d36e Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Mon, 6 Aug 2018 23:45:27 +0800 Subject: [PATCH 12/33] Enhance base62 encode/decode Funs --- src/emqx_base62.erl | 109 ++++++++++++++++++++++++++----------- src/emqx_guid.erl | 5 +- test/emqx_base62_SUITE.erl | 12 ++-- 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 929089f1b..0649900c2 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -16,41 +16,86 @@ -export([encode/1, decode/1]). -%% @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, [])). -encode(I, Acc) when I < 62 -> - [char(I) | Acc]; -encode(I, Acc) -> - encode(I div 62, [char(I rem 62) | Acc]). +%% @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, <<>>). -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)). +%% encode_base62(<>, Acc) -> +%% encode_byte_group(H, Acc); +%% encode_base62(<>, Acc) -> +%% encode_byte_group(H, Acc); -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. +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(<>, 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_guid.erl b/src/emqx_guid.erl index 43855c734..8fe0c7b6e 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -126,8 +126,9 @@ from_hexstr(S) -> I = list_to_integer(binary_to_list(S), 16), <>. to_base62(<>) -> - emqx_base62:encode(I). + binary_to_list(emqx_base62:encode(I)). from_base62(S) -> - I = emqx_base62:decode(S), <>. + I = binary_to_integer(emqx_base62:decode(S)), + <>. diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl index e0cb0e26a..cf4bc9756 100644 --- a/test/emqx_base62_SUITE.erl +++ b/test/emqx_base62_SUITE.erl @@ -26,11 +26,11 @@ 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 = erlang:binary_to_integer(?BASE62:decode(?BASE62:encode(X))), + Y = erlang:binary_to_integer(?BASE62:decode(?BASE62:encode(Y))). From 96d251ec3cb224e8e0712fb0083987e38f039d78 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 7 Aug 2018 10:27:04 +0800 Subject: [PATCH 13/33] Add encode and decode options and test suites --- src/emqx_base62.erl | 24 ++++++++++++++++++------ src/emqx_guid.erl | 4 ++-- test/emqx_base62_SUITE.erl | 7 +++++-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 0649900c2..690115ec2 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -14,8 +14,10 @@ -module(emqx_base62). --export([encode/1, decode/1]). - +-export([encode/1, + encode/2, + decode/1, + decode/2]). %% @doc Encode any data to base62 binary -spec encode(string() @@ -28,18 +30,23 @@ encode(S) when is_list(S)-> encode(B) when is_binary(B) -> encode(B, <<>>). +%% encode(D, string) -> +%% binary_to_list(encode(D)). + %% @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(B, <<>>). -%% encode_base62(<>, Acc) -> -%% encode_byte_group(H, Acc); -%% encode_base62(<>, Acc) -> -%% encode_byte_group(H, Acc); +%%==================================================================== +%% 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 = <>, @@ -55,6 +62,10 @@ encode(<>, Acc) -> 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 @@ -99,3 +110,4 @@ decode_char(I) when I >= $A andalso I =< $Z-> decode_char(9, I) -> I + 61 - $A. + diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index 8fe0c7b6e..fa9139ebd 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -126,9 +126,9 @@ from_hexstr(S) -> I = list_to_integer(binary_to_list(S), 16), <>. to_base62(<>) -> - binary_to_list(emqx_base62:encode(I)). + emqx_base62:encode(I). from_base62(S) -> - I = binary_to_integer(emqx_base62:decode(S)), + I = emqx_base62:decode(S, integer), <>. diff --git a/test/emqx_base62_SUITE.erl b/test/emqx_base62_SUITE.erl index cf4bc9756..820c7ec32 100644 --- a/test/emqx_base62_SUITE.erl +++ b/test/emqx_base62_SUITE.erl @@ -32,5 +32,8 @@ t_base62_encode(_) -> <<"65535">> = ?BASE62:decode(?BASE62:encode(<<"65535">>)), <> = emqx_guid:gen(), <> = emqx_guid:gen(), - X = erlang:binary_to_integer(?BASE62:decode(?BASE62:encode(X))), - Y = erlang:binary_to_integer(?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). + From 288e03c91427a24c5b72daf016e05cf9104f03c8 Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Tue, 7 Aug 2018 10:31:57 +0800 Subject: [PATCH 14/33] Update OTP version for travis-CI --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 8418be0a5be1a386ccd7aab5df1559c2b0376d88 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 7 Aug 2018 11:00:04 +0800 Subject: [PATCH 15/33] Use the new emqx_session:unsubscribe/2 API --- src/emqx_protocol.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 30b2c0294..85fe35e52 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -262,14 +262,14 @@ process({subscribe, RawTopicTable}, process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> send(?UNSUBACK_PACKET(PacketId), State); -process(?UNSUBSCRIBE_PACKET(PacketId, _Properties, 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, @@ -280,7 +280,7 @@ process({unsubscribe, RawTopics}, State = #proto_state{client_id = ClientId, session = Session}) -> case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of {ok, TopicTable} -> - emqx_session:unsubscribe(Session, TopicTable); + emqx_session:unsubscribe(Session, {undefined, #{}, TopicTable}); {stop, _} -> ok end, {ok, State}; From 4d9e03a8038e71f46138eca3987b098514359f1e Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 8 Aug 2018 18:36:14 +0800 Subject: [PATCH 16/33] Refactor websocket conn using cowboy --- Makefile | 4 +- etc/emqx.conf | 99 +----------------- include/emqx.hrl | 14 +-- priv/emqx.schema | 185 +-------------------------------- src/emqx.app.src | 2 +- src/emqx_cli.erl | 4 +- src/emqx_cm.erl | 2 +- src/emqx_ctl.erl | 16 ++- src/emqx_listeners.erl | 24 +++-- src/emqx_protocol.erl | 8 +- src/emqx_shared_sub.erl | 22 ++-- src/emqx_sm.erl | 2 +- src/emqx_sup.erl | 4 +- src/emqx_vm.erl | 2 +- src/emqx_ws.erl | 135 +++++++++++------------- src/emqx_ws_connection.erl | 154 +++++++++++---------------- src/emqx_ws_connection_sup.erl | 8 +- 17 files changed, 194 insertions(+), 491 deletions(-) diff --git a/Makefile b/Makefile index 38051db1c..509794bbd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = jsx gproc gen_rpc lager ekka esockd mochiweb clique +DEPS = jsx gproc gen_rpc lager ekka esockd minirest clique dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_gproc = git https://github.com/uwiger/gproc 0.8.0 @@ -12,7 +12,7 @@ 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_mochiweb = git https://github.com/emqtt/mochiweb emqx30 +dep_minirest = git https://github.com/emqx/minirest emqx30 dep_clique = git https://github.com/emqx/clique NO_AUTOPATCH = gen_rpc cuttlefish diff --git a/etc/emqx.conf b/etc/emqx.conf index 555a6d63b..9db1761bc 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1176,57 +1176,11 @@ listener.ws.external.acceptors = 4 ## Value: Number listener.ws.external.max_clients = 102400 -## Maximum MQTT/WebSocket connections per second. -## -## Value: Number -listener.ws.external.max_conn_rate = 1000 - ## Zone of the external MQTT/WebSocket listener belonged to. ## ## Value: String listener.ws.external.zone = external -## Mountpoint of the MQTT/WebSocket Listener. -## -## See: listener.tcp..mountpoint -## -## Value: String -## listener.ws.external.mountpoint = external/ - -## The access control for the MQTT/WebSocket listener. -## -## See: listener.tcp..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 -## 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 -## deployed behind NGINX or HAProxy. -## -## Value: String -## listener.ws.external.proxy_port_header = X-Forwarded-Port - -## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind -## HAProxy or Nginx. -## -## See: listener.tcp..proxy_protocol -## -## Value: on | off -## listener.ws.external.proxy_protocol = on - -## Sets the timeout for proxy protocol. -## -## See: listener.tcp..proxy_protocol_timeout -## -## Value: Duration -## listener.ws.external.proxy_protocol_timeout = 3s - ## The TCP backlog of external MQTT/WebSocket Listener. ## ## See: listener.tcp..backlog @@ -1283,11 +1237,6 @@ listener.ws.external.send_timeout_close = on ## 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 @@ -1309,11 +1258,6 @@ listener.wss.external.acceptors = 4 ## Value: Number listener.wss.external.max_clients = 64 -## Maximum MQTT/WebSocket/SSL connections per second. -## -## Value: Number -listener.wss.external.max_conn_rate = 1000 - ## Zone of the external MQTT/WebSocket/SSL listener belonged to. ## ## Value: String @@ -1326,37 +1270,6 @@ listener.wss.external.zone = external ## Value: String ## listener.wss.external.mountpoint = inbound/ -## The access control rules for the MQTT/WebSocket/SSL listener. -## -## See: listener.tcp..access. -## -## Value: ACL Rule -listener.wss.external.access.1 = allow all - -## See: listener.ws.external.proxy_address_header -## -## Value: String -## listener.wss.external.proxy_address_header = X-Forwarded-For - -## See: listener.ws.external.proxy_port_header -## -## Value: String -## listener.wss.external.proxy_port_header = X-Forwarded-Port - -## Enable the Proxy Protocol V1/2 support. -## -## See: listener.tcp..proxy_protocol -## -## Value: on | off -## listener.wss.external.proxy_protocol = on - -## Sets the timeout for proxy protocol. -## -## See: listener.tcp..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 @@ -1364,13 +1277,6 @@ listener.wss.external.access.1 = allow all ## 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 @@ -1481,10 +1387,7 @@ listener.wss.external.send_timeout_close = on ## Value: true | false ## listener.wss.external.nodelay = true -## The SO_REUSEADDR flag for WebSocket/SSL listener. -## -## Value: true | false -listener.wss.external.reuseaddr = true +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 ##------------------------------------------------------------------- ## System Monitor diff --git a/include/emqx.hrl b/include/emqx.hrl index 7340cf835..3b42713ff 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -192,13 +192,13 @@ %%-------------------------------------------------------------------- -record(plugin, { - name :: atom(), - version :: string(), - dir :: string(), - descr :: string(), - vendor :: string(), - active :: boolean(), - info :: map() + name :: atom(), + version :: string(), + dir :: string(), + descr :: string(), + vendor :: string(), + active = false :: boolean(), + info :: map() }). -type(plugin() :: #plugin{}). diff --git a/priv/emqx.schema b/priv/emqx.schema index 7ff5fd0a3..7db77f1fb 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -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)}, @@ -1173,10 +1173,6 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - {mapping, "listener.ws.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1185,28 +1181,6 @@ end}. {datatype, string} ]}. -{mapping, "listener.ws.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ws.$name.proxy_address_header", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{mapping, "listener.ws.$name.proxy_port_header", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{mapping, "listener.ws.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ws.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - {mapping, "listener.ws.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} @@ -1247,11 +1221,6 @@ end}. hidden ]}. -{mapping, "listener.ws.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - %%-------------------------------------------------------------------- %% MQTT/WebSocket/SSL Listeners @@ -1269,10 +1238,6 @@ end}. {datatype, integer} ]}. -{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - {mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1281,32 +1246,6 @@ end}. {datatype, string} ]}. -{mapping, "listener.wss.$name.rate_limit", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.proxy_address_header", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{mapping, "listener.wss.$name.proxy_port_header", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{mapping, "listener.wss.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - {mapping, "listener.wss.$name.backlog", "emqx.listeners", [ {default, 1024}, {datatype, integer} @@ -1347,11 +1286,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} ]}. @@ -1360,11 +1294,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} ]}. @@ -1444,7 +1373,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, @@ -1488,17 +1417,6 @@ end}. 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 -> [{ssl_options, SslOpts0}] end, - [{Atom(Type), ListenOn, [{tcp_options, TcpOpts(Prefix)}|LisOpts(Prefix)] ++ SslOpts1}] - end - end, - lists:flatten([TcpListeners(Type, Name) || {["listener", Type, Name], ListenOn} <- cuttlefish_variable:filter_by_prefix("listener.tcp", Conf) @@ -1506,106 +1424,9 @@ end}. ++ [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 - -{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", [ - {datatype, {duration, ms}} -]}. - -{mapping, "listener.api.$name.keyfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.certfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.cacertfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.api.$name.verify", "emqx.listeners", [ - {datatype, atom} -]}. - -{mapping, "listener.api.$name.fail_if_no_peer_cert", "emqx.listeners", [ - {datatype, {enum, [true, false]}} -]}. - %%-------------------------------------------------------------------- %% System Monitor %%-------------------------------------------------------------------- diff --git a/src/emqx.app.src b/src/emqx.app.src index 7d0dd0c4d..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,jsx,gproc,gen_rpc,lager,esockd,mochiweb]}, + {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,minirest]}, {env,[]}, {mod,{emqx_app,[]}}, {maintainers,["Feng Lee "]}, 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_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_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_listeners.erl b/src/emqx_listeners.erl index 9e8445414..257820fb1 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -36,21 +36,31 @@ start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> 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 = [{"/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, []), + Options1 = [{port, ListenOn}, + {num_acceptors, NumAcceptors}, + {max_connections, MaxConnections} | TcpOptions], + minirest:start_http(Proto, Options1, Dispatch); %% Start MQTT/WSS listener start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> - start_http_listener('mqtt:wss', ListenOn, Options). + Dispatch = [{"/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, []), + Options1 = [{port, ListenOn}, + {num_acceptors, NumAcceptors}, + {max_connections, MaxConnections} | TcpOptions ++ SslOptions], + minirest:start_https(Proto, Options1, Dispatch). start_mqtt_listener(Name, ListenOn, Options) -> 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) -> - SockOpts = esockd:parse_opt(Options), - MFA = {emqx_ws, handle_request, [Options -- SockOpts]}, - {ok, _} = mochiweb:start_http(Name, ListenOn, SockOpts, MFA). - %% @doc Restart all listeners -spec(restart_all() -> ok). restart_all() -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 30b2c0294..58c2279c9 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -352,11 +352,11 @@ send(Packet = ?PACKET(Type), ProtoState = #proto_state{proto_ver = Ver, sockprops = #{sendfun := SendFun}}) -> Data = emqx_frame:serialize(Packet, #{version => Ver}), case SendFun(Data) of - ok -> emqx_metrics:sent(Packet), - trace(send, Packet, ProtoState), - {ok, inc_stats(send, Type, ProtoState)}; {error, Reason} -> - {error, Reason} + {error, Reason}; + _ -> emqx_metrics:sent(Packet), + trace(send, Packet, ProtoState), + {ok, inc_stats(send, Type, ProtoState)} end. trace(recv, Packet, ProtoState) -> 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 00f4bff3d..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]). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 3df468f1e..b3378ba91 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -72,7 +72,7 @@ init([]) -> %% Connection Manager CMSup = supervisor_spec(emqx_cm_sup), %% WebSocket Connection Sup - %% WSConnSup = supervisor_spec(emqx_ws_connection_sup), + WSConnSup = supervisor_spec(emqx_ws_connection_sup), %% Sys Sup SysSup = supervisor_spec(emqx_sys_sup), {ok, {{one_for_all, 0, 1}, @@ -84,7 +84,7 @@ init([]) -> SMSup, SessionSup, CMSup, - %%WSConnSup, + WSConnSup, SysSup]}}. %%-------------------------------------------------------------------- diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index 0f3a4eaa4..bf6388232 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -199,7 +199,7 @@ mem_info() -> {used_memory, proplists:get_value(total_memory, Dataset) - proplists:get_value(free_memory, Dataset)}]. ftos(F) -> - [S] = io_lib:format("~.2f", [F]), S. + S = io_lib:format("~.2f", [F]), S. %%%% erlang vm scheduler_usage fun copied from recon scheduler_usage(Interval) when is_integer(Interval) -> diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index e7ad63d61..b84c603af 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -20,100 +20,83 @@ -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}). +-record(wsocket_state, {req, 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])). + lager:Level("WsClient(~s): " ++ Format, + [esockd_net:format(State#wsocket_state.peername) | Args])). +-export([init/2]). +-export([websocket_init/1]). +-export([websocket_handle/2]). +-export([websocket_info/2]). -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" +init(Req0, State) -> + case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req0) of + undefined -> + {cowboy_websocket, Req0, #wsocket_state{}}; + Subprotocols -> + case lists:member(<<"mqtt">>, Subprotocols) of + true -> + Peername = cowboy_req:peer(Req0), + Req = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req0), + {cowboy_websocket, Req, #wsocket_state{req = Req, peername = Peername}, #{idle_timeout => 86400000}}; + false -> + Req = cowboy_req:reply(400, Req0), + {ok, Req, #wsocket_state{}} + end end. -get_protocol_header(Req) -> - case Req:get_header_value("EMQ-WebSocket-Protocol") of - undefined -> Req:get_header_value("Sec-WebSocket-Protocol"); - Proto -> Proto +websocket_init(State = #wsocket_state{req = Req}) -> + case emqx_ws_connection_sup:start_connection(self(), Req) of + {ok, ClientPid} -> + {ok, ProtoEnv} = emqx_config:get_env(protocol), + PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), + Parser = emqx_frame:initial_state(#{max_packet_size => PacketSize}), + NewState = State#wsocket_state{parser = Parser, + max_packet_size = PacketSize, + client_pid = ClientPid}, + {ok, NewState}; + Error -> + ?WSLOG(error, "Start client fail: ~p", [Error], State), + {stop, State} end. -%%-------------------------------------------------------------------- -%% Receive Loop -%%-------------------------------------------------------------------- +websocket_handle({binary, <<>>}, State) -> + {ok, State}; +websocket_handle({binary, [<<>>]}, State) -> + {ok, State}; -%% @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) -> +websocket_handle({binary, Data}, State = #wsocket_state{client_pid = ClientPid, parser = Parser}) -> ?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 + BinSize = iolist_size(Data), + emqx_metrics:inc('bytes/received', BinSize), + case catch emqx_frame:parse(iolist_to_binary(Data), Parser) of {more, NewParser} -> - State#wsocket_state{parser = NewParser}; + {ok, State#wsocket_state{parser = NewParser}}; {ok, Packet, Rest} -> - gen_server:cast(ClientPid, {received, Packet}), - ws_loop(Rest, reset_parser(State), ReplyChannel); + gen_server:cast(ClientPid, {received, Packet, BinSize}), + websocket_handle({binary, Rest}, reset_parser(State)); {error, Error} -> ?WSLOG(error, "Frame error: ~p", [Error], State), - exit({shutdown, Error}); + {stop, State}; {'EXIT', Reason} -> ?WSLOG(error, "Frame error: ~p", [Reason], State), ?WSLOG(error, "Error data: ~p", [Data], State), - exit({shutdown, parser_error}) + {stop, State} end. -reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) -> - State#wsocket_state{parser = emqx_parser:initial_state(PacketSize)}. +websocket_info({binary, Data}, State) -> + {reply, {binary, Data}, State}; + +websocket_info({'EXIT', _Pid, {shutdown, kick}}, State) -> + {stop, State}; + +websocket_info(_Info, State) -> + {ok, State}. + +reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) -> + State#wsocket_state{parser = emqx_frame:initial_state(#{max_packet_size => PacketSize})}. + diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index dadc2cc57..93a289636 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -23,7 +23,7 @@ -import(proplists, [get_value/2, get_value/3]). %% API Exports --export([start_link/4]). +-export([start_link/3]). %% Management and Monitor API -export([info/1, stats/1, kick/1, clean_acl_cache/2]). @@ -38,13 +38,15 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% TODO: remove ... --export([handle_pre_hibernate/1]). - %% WebSocket Client State --record(wsclient_state, {ws_pid, transport, socket, peername, - proto_state, keepalive, enable_stats, - force_gc_count}). +-record(wsclient_state, {ws_pid, peername, proto_state, keepalive, + enable_stats, force_gc_count}). + +%% recv_oct +%% Number of bytes received by the socket. + +%% recv_cnt +%% Number of packets received by the socket. -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -53,8 +55,8 @@ [esockd_net:format(State#wsclient_state.peername) | Args])). %% @doc Start WebSocket Client. -start_link(Env, WsPid, Req, ReplyChannel) -> - gen_server:start_link(?MODULE, [Env, WsPid, Req, ReplyChannel], +start_link(Env, WsPid, Req) -> + gen_server:start_link(?MODULE, [Env, WsPid, Req], [[{hibernate_after, 10000}]]). info(CPid) -> @@ -82,38 +84,29 @@ clean_acl_cache(CPid, Topic) -> %% gen_server Callbacks %%-------------------------------------------------------------------- -init([Env, WsPid, Req, ReplyChannel]) -> +init([Options, WsPid, Req]) -> + init_stas(), 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}) - 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))}. + Peername = cowboy_req:peer(Req), + Headers = cowboy_req:headers(Req), + Sockname = cowboy_req:sock(Req), + Peercert = cowboy_req:cert(Req), + Zone = proplists:get_value(zone, Options), + ProtoState = emqx_protocol:init(#{zone => Zone, + peername => Peername, + sockname => Sockname, + peercert => Peercert, + sendfun => send_fun(WsPid)}, + [{ws_initial_headers, Headers} | Options]), + IdleTimeout = get_value(client_idle_timeout, Options, 30000), + EnableStats = get_value(client_enable_stats, Options, false), + ForceGcCount = emqx_gc:conn_max_gc_count(), + {ok, #wsclient_state{ws_pid = WsPid, + peername = Peername, + proto_state = ProtoState, + enable_stats = EnableStats, + force_gc_count = ForceGcCount}, IdleTimeout}. handle_call(info, From, State = #wsclient_state{peername = Peername, proto_state = ProtoState}) -> @@ -123,7 +116,7 @@ handle_call(info, From, State = #wsclient_state{peername = Peername, handle_call(stats, _From, State = #wsclient_state{proto_state = ProtoState}) -> reply(lists:append([emqx_misc:proc_stats(), - wsock_stats(State), + wsock_stats(), emqx_protocol:stats(ProtoState)]), State); handle_call(kick, _From, State) -> @@ -140,7 +133,9 @@ 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}) -> +handle_cast({received, Packet, BinSize}, State = #wsclient_state{proto_state = ProtoState}) -> + put(recv_oct, get(recv_oct) + BinSize), + put(recv_cnt, get(recv_cnt) + 1), emqx_metrics:received(Packet), case emqx_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> @@ -158,48 +153,24 @@ handle_cast(Msg, State) -> ?WSLOG(error, "unexpected msg: ~p", [Msg], State), {noreply, State}. -handle_info({subscribe, TopicTable}, State) -> +handle_info(SubReq ={subscribe, _TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:subscribe(TopicTable, ProtoState) + emqx_protocol:process(SubReq, ProtoState) end, State); -handle_info({unsubscribe, Topics}, State) -> +handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:unsubscribe(Topics, ProtoState) + emqx_protocol:process(UnsubReq, ProtoState) end, State); -handle_info({suback, PacketId, ReasonCodes}, State) -> +handle_info({deliver, PubOrAck}, State) -> with_proto( fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, ReasonCodes), - emqx_protocol:send(Packet, ProtoState) - end, State); - -handle_info({unsuback, PacketId, ReasonCodes}, State) -> - with_proto( - fun(ProtoState) -> - Packet = ?UNSUBACK_PACKET(PacketId, ReasonCodes), - 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) + emqx_protocol:deliver(PubOrAck, 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}; @@ -213,10 +184,9 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) -> handle_info({shutdown, Reason}, State) -> shutdown(Reason, State); -handle_info({keepalive, start, Interval}, - State = #wsclient_state{transport = Transport, socket =Sock}) -> +handle_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}; {error, Error} -> @@ -271,23 +241,18 @@ code_change(_OldVsn, State, _Extra) -> %% 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 +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. -stat_fun(Transport, Sock) -> +stat_fun() -> fun() -> - case Transport:getstat(Sock, [recv_oct]) of - {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; - {error, Error} -> {error, Error} - end + {ok, get(recv_oct)} end. emit_stats(State = #wsclient_state{proto_state = ProtoState}) -> @@ -302,11 +267,8 @@ emit_stats(ClientId, State) -> emqx_cm:set_client_stats(ClientId, Stats), State. -wsock_stats(#wsclient_state{transport = Transport, socket = Sock}) -> - case Transport:getstat(Sock, ?SOCK_STATS) of - {ok, Ss} -> Ss; - {error, _} -> [] - end. +wsock_stats() -> + [{Key, get(Key)}|| Key <- ?SOCK_STATS]. with_proto(Fun, State = #wsclient_state{proto_state = ProtoState}) -> {ok, ProtoState1} = Fun(ProtoState), @@ -325,3 +287,9 @@ gc(State) -> Cb = fun() -> emit_stats(State) end, emqx_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb). +init_stas() -> + put(recv_oct, 0), + put(recv_cnt, 0), + put(send_oct, 0), + put(send_cnt, 0). + diff --git a/src/emqx_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl index b58e7c956..f627abfbb 100644 --- a/src/emqx_ws_connection_sup.erl +++ b/src/emqx_ws_connection_sup.erl @@ -18,7 +18,7 @@ -behavior(supervisor). --export([start_link/0, start_connection/3]). +-export([start_link/0, start_connection/2]). -export([init/1]). @@ -27,9 +27,9 @@ 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]). +-spec(start_connection(pid(), mochiweb_request:request()) -> {ok, pid()}). +start_connection(WsPid, Req) -> + supervisor:start_child(?MODULE, [WsPid, Req]). %%-------------------------------------------------------------------- %% Supervisor callbacks From b5a1960b6342bfaa9eacc6dba081f366deb7c56c Mon Sep 17 00:00:00 2001 From: turtled Date: Wed, 8 Aug 2018 18:42:11 +0800 Subject: [PATCH 17/33] Stop emqx_ws --- src/emqx_ws.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl index b84c603af..d7f6dc6e8 100644 --- a/src/emqx_ws.erl +++ b/src/emqx_ws.erl @@ -32,7 +32,7 @@ -export([websocket_handle/2]). -export([websocket_info/2]). -init(Req0, State) -> +init(Req0, _State) -> case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req0) of undefined -> {cowboy_websocket, Req0, #wsocket_state{}}; @@ -90,7 +90,8 @@ websocket_handle({binary, Data}, State = #wsocket_state{client_pid = ClientPid, websocket_info({binary, Data}, State) -> {reply, {binary, Data}, State}; -websocket_info({'EXIT', _Pid, {shutdown, kick}}, State) -> +websocket_info({'EXIT', Pid, Reason}, State = #wsocket_state{client_pid = Pid}) -> + ?WSLOG(debug, "EXIT: ~p", [Reason], State), {stop, State}; websocket_info(_Info, State) -> From 4cf181503080fa31169bb449b5128518bb14305b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 8 Aug 2018 19:23:32 +0800 Subject: [PATCH 18/33] Add more configurations for EMQ X R3.0 --- etc/emqx.conf | 928 ++++++++++++++++++++++++++++------------------- priv/emqx.schema | 874 ++++++++++++++++++++++---------------------- 2 files changed, 973 insertions(+), 829 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 555a6d63b..9458e4fed 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 ##-------------------------------------------------------------------- @@ -399,283 +415,35 @@ log.syslog = on ## 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. -## -## Value: true | false -mqtt.cache_acl = true - -##-------------------------------------------------------------------- -## MQTT Protocol -##-------------------------------------------------------------------- - -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -mqtt.max_clientid_len = 1024 - -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## -## Default: 64K -mqtt.max_packet_size = 64KB - -## Check if the websocket protocol header is valid. -## Turn off the option when developing WeChat App. +## Whether to enable ACL cache for publish. ## ## Value: on | off -mqtt.websocket_protocol_header = on +enable_acl_cache = 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. +## The ACL cache age. ## ## Value: Duration -mqtt.client.idle_timeout = 30s +## Default: 5 minute +acl_cache_age = 5m -## TODO: Maximum publish rate of MQTT messages per second. +## The ACL cache size, 0 means no limit. ## -## Value: Number -## mqtt.client.max_publish_rate = 5 - -## 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. -## -## 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 +## Value: Integer +acl_cache_size = 0 ##-------------------------------------------------------------------- ## Listeners @@ -684,7 +452,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,37 +463,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 -## Maximum connection per second. +## 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 -## 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 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 = devicebound/ +## 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. ## @@ -736,7 +507,7 @@ listener.tcp.external.rate_limit = 100,10 ## 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/ @@ -744,18 +515,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. @@ -778,14 +548,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 @@ -797,7 +567,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. ## @@ -834,7 +604,12 @@ 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 + +## Maximum internal connections per second. +## +## Value: Number +listener.tcp.internal.max_conn_rate = 1000 ## Zone of the internal MQTT/TCP listener belonged to. ## @@ -843,42 +618,43 @@ 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 @@ -892,21 +668,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 @@ -919,7 +695,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 @@ -935,12 +711,12 @@ 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 ## Maximum MQTT/SSL connections per second. ## ## Value: Number -listener.ssl.external.max_conn_rate = 1000 +listener.ssl.external.max_conn_rate = 500 ## Zone of the external MQTT/SSL listener belonged to. ## @@ -950,31 +726,32 @@ 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 @@ -1088,64 +865,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 @@ -1156,9 +933,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 @@ -1174,13 +951,19 @@ 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 ## 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 @@ -1188,25 +971,30 @@ 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 @@ -1215,70 +1003,70 @@ 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 @@ -1291,7 +1079,7 @@ 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 @@ -1307,13 +1095,21 @@ 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 ## 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 @@ -1321,18 +1117,23 @@ 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 @@ -1345,138 +1146,138 @@ 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 +## See: listener.ssl.$name.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 @@ -1486,9 +1287,376 @@ listener.wss.external.send_timeout_close = on ## Value: true | false listener.wss.external.reuseaddr = true +##-------------------------------------------------------------------- +## 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: 64KB +zone.external.max_packet_size = 1MB + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +zone.external.max_clientid_len = 65535 + +## Maximum topic levels allowed. 0 means no limit. +## +## Value: Number +zone.external.max_topic_levels = 0 + +## 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 = true + +## Whether the Server supports Shared Subscriptions +## +## Value: boolean +zone.external.shared_subscription = true + +## The backoff for MQTT keepalive timeout. The broker will kick a connection out +## until 'Keepalive * backoff * 2' timeout. +## +## Value: Float > 0.5 +zone.external.keepalive_backoff = 0.75 + +## Maximum number of subscriptions allowed, 0 means no limit. +## +## Value: Number +zone.external.max_subscriptions = 0 + +## Force to upgrade QoS according to subscription. +## +## Value: on | off +zone.external.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.external.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.external.retry_interval = 20s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.external.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.external.await_rel_timeout = 60s + +## Whether to ignore loop delivery of messages. +## +## Value: true | false +## Default: false +zone.external.ignore_loop_deliver = false + +## Default session expiry interval for MQTT V3.1.1 connections. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.external.session_expiry_interval = 2h + +## Maximum queue length. Enqueued messages when persistent client disconnected, +## or inflight window is full. 0 means no limit. +## +## Value: Number >= 0 +zone.external.max_mqueue_len = 1000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.external.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## Internal Zone + +## Enable per connection stats. +## +## Value: Flag +zone.internal.enable_stats = on + +## Enable ACL check. +## +## Value: Flag +zone.internal.enable_acl = off + +## See zone.$name.wildcard_subscription. +## +## Value: boolean +zone.internal.wildcard_subscription = true + +## See zone.$name.shared_subscription. +## +## Value: boolean +zone.internal.shared_subscription = true + +## See zone.$name.max_subscriptions. +## +## Value: Integer +zone.internal.max_subscriptions = 0 + +## See zone.$name.max_inflight +## +## Value: Number +zone.internal.max_inflight = 32 + +## See zone.$name.max_awaiting_rel +## +## Value: Number +zone.internal.max_awaiting_rel = 100 + +## See zone.$name.max_mqueue_len +## +## Value: Number >= 0 +zone.internal.max_mqueue_len = 1000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.internal.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## Bridges +##-------------------------------------------------------------------- + +## Bridge Type. +## +## Value: local | remote +bridge.name.type = local + +## 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.name.address = emqx@127.0.0.1 + +## Protocol version of the bridge. +## +## Value: Enum +## - mqtt5 +## - mqtt4 +## - mqtt3 +bridge.name.proto_ver = mqtt4 + +## The ClientId of a remote bridge. +## +## Value: String +bridge.name.client_id = bridge:$name + +## The Clean start flag of a remote bridge. +## +## Value: boolean +bridge.name.clean_start = false + +## The username for a remote bridge. +## +## Value: String +bridge.name.username = user + +## The password for a remote bridge. +## +## Value: String +bridge.name.password = passwd + +## Mountpoint of the bridge. +## +## Value: String +bridge.name.mountpoint = bridge/$name/ + +## PEM-encoded CA certificates of the bridge. +## +## Value: File +bridge.name.cacertfile = cacert.pem + +## SSL Certfile of the bridge. +## +## Value: File +bridge.name.certfile = cert.pem + +## SSL Keyfile of the bridge. +## +## Value: File +bridge.name.keyfile = key.pem + +## SSL Ciphers used by the bridge. +## +## Value: String +bridge.name.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 + +## TLS versions used by the bridge. +## +## Value: String +bridge.name.tls_versions = tlsv1.2,tlsv1.1,tlsv1 + +## The pending message queue of a bridge. +## +## Value: Number +bridge.name.max_pending_messages = 10000 + +## Ping interval of a down bridge. +## +## Value: Duration +## Default: 10 seconds +bridge.name.keepalive = 10s + +## Subscriptions of the bridge. +## +## Default: 10 seconds +bridge.name.subscription.1.topic = topic1/ +bridge.name.subscription.1.qos = 2 +## bridge.name.subscription.2.topic = topic2/ +## bridge.name.subscription.2.qos = 2 + +##-------------------------------------------------------------------- +## 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 + ##------------------------------------------------------------------- +## 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: diff --git a/priv/emqx.schema b/priv/emqx.schema index 7ff5fd0a3..3a3267930 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. @@ -550,368 +550,39 @@ 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]}} -]}. - -%%-------------------------------------------------------------------- -%% MQTT Protocol -%%-------------------------------------------------------------------- - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "mqtt.max_clientid_len", "emqx.protocol", [ - {default, 1024}, - {datatype, integer} -]}. - -%% @doc Max Packet Size Allowed, 64K by default. -{mapping, "mqtt.max_packet_size", "emqx.protocol", [ - {default, "64KB"}, - {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", [ +%% @doc Enable ACL cache for publish. +{mapping, "enable_acl_cache", "emqx.enable_acl_cache", [ {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", [ - {datatype, integer} -]}. - -%%-------------------------------------------------------------------- -%% MQTT Client -%%-------------------------------------------------------------------- - -%% @doc Max Publish Rate of Message -{mapping, "mqtt.client.max_publish_rate", "emqx.client", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Client Idle Timeout. -{mapping, "mqtt.client.idle_timeout", "emqx.client", [ - {default, "30s"}, +%% @doc ACL cache age. +{mapping, "acl_cache_age", "emqx.acl_cache_age", [ + {default, "5m"}, {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", [ +%% @doc ACL cache size. +{mapping, "acl_cache_size", "emqx.acl_cache_size", [ {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}, - {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}. - - %%-------------------------------------------------------------------- %% Listeners %%-------------------------------------------------------------------- @@ -928,7 +599,7 @@ end}. {datatype, integer} ]}. -{mapping, "listener.tcp.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.tcp.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. @@ -963,7 +634,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", [ @@ -1023,7 +695,7 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ssl.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.ssl.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. @@ -1168,7 +840,7 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ws.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.ws.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. @@ -1185,10 +857,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 @@ -1264,7 +946,7 @@ end}. {datatype, integer} ]}. -{mapping, "listener.wss.$name.max_clients", "emqx.listeners", [ +{mapping, "listener.wss.$name.max_connections", "emqx.listeners", [ {default, 1024}, {datatype, integer} ]}. @@ -1285,6 +967,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} ]}. @@ -1422,16 +1109,23 @@ end}. MountPoint = fun(undefined) -> undefined; (S) -> list_to_binary(S) 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)}, + {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, cuttlefish:conf_get(Prefix ++ ".rate_limit", 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)]) @@ -1488,124 +1182,406 @@ end}. 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 -> [{ssl_options, SslOpts0}] end, - [{Atom(Type), ListenOn, [{tcp_options, 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", [ + {default, "64KB"}, + {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", [ + {default, 65535}, + {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", [ + {default, 0}, + {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", [ + {default, 2}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +%% @doc Set the Maximum topic alias. +{mapping, "zone.$name.max_topic_alias", "emqx.zones", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Whether the server supports retained messages. +{mapping, "zone.$name.retain_available", "emqx.zones", [ + {default, true}, {datatype, {enum, [true, false]}} ]}. +%% @doc Whether the Server supports Wildcard Subscriptions. +{mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports Shared Subscriptions. +{mapping, "zone.$name.shared_subscription", "emqx.zones", [ + {default, true}, + {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, 0}, + {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) -> + maps:to_list( + lists:foldl( + fun({["zone", Name, Opt], Val}, Acc) -> + ZName = list_to_atom(Name), + case maps:find(ZName, Acc) of + {ok, Opts} -> + maps:put(ZName, [{list_to_atom(Opt), Val} | Opts], Acc); + error -> + maps:put(ZName, [{list_to_atom(Opt), Val}], Acc) + end + end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) +end}. + +%%-------------------------------------------------------------------- +%% Bridges +%%-------------------------------------------------------------------- + +{mapping, "bridge.$name.type", "emqx.bridges", [ + {default, local}, + {datatype, {enum, [local,remote]}} +]}. + +{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.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} +]}. + +{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, [{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 %%-------------------------------------------------------------------- @@ -1642,10 +1618,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}. From 4005d58166f1edabc45715b13229a05d91b4c316 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 8 Aug 2018 19:31:25 +0800 Subject: [PATCH 19/33] Move the 'rate_limit' option from zone to listener --- src/emqx_connection.erl | 17 +++++++++-------- src/emqx_frame.erl | 1 - src/emqx_protocol.erl | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 7dc70e6ce..89233fb43 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -41,7 +41,7 @@ await_recv, %% Awaiting recv incoming, %% Incoming bytes and packets pub_limit, %% Publish rate limit - rate_limit, %% Throughput rate limit + rate_limit, %% Traffic rate limit limit_timer, %% Rate limit timer proto_state, %% MQTT protocol state parse_state, %% MQTT parse state @@ -56,7 +56,7 @@ -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(LOG(Level, Format, Args, State), - emqx_logger:Level("Conn(~s): " ++ Format, + emqx_logger:Level("Client(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). start_link(Transport, Socket, Options) -> @@ -100,11 +100,12 @@ set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> init([Transport, RawSocket, Options]) -> case Transport:wait(RawSocket) of {ok, Socket} -> + io:format("Options: ~p~n", [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]), Zone = proplists:get_value(zone, Options), - RateLimit = init_rate_limit(emqx_zone:get_env(Zone, rate_limit)), + RateLimit = init_rate_limit(proplists:get_value(rate_limit, Options)), PubLimit = init_rate_limit(emqx_zone:get_env(Zone, publish_limit)), EnableStats = emqx_zone:get_env(Zone, enable_stats, false), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), @@ -194,16 +195,16 @@ handle_cast(Msg, State) -> ?LOG(error, "unexpected cast: ~p", [Msg], State), {noreply, State}. -handle_info(SubReq = {subscribe, _TopicTable}, State) -> +handle_info(Sub = {subscribe, _TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:process(SubReq, ProtoState) + emqx_protocol:process(Sub, ProtoState) end, State); -handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> +handle_info(Unsub = {unsubscribe, _Topics}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:process(UnsubReq, ProtoState) + emqx_protocol:process(Unsub, ProtoState) end, State); handle_info({deliver, PubOrAck}, State) -> @@ -300,7 +301,7 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -%% Receive and parse TCP data +%% Receive and parse data handle_packet(<<>>, State) -> {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 7385b7116..10498afcf 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -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, diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 85fe35e52..9a13c6538 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -19,6 +19,7 @@ -include("emqx_misc.hrl"). -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]). From 854132d0c3c4d7433ead3238bafab792bdb66537 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 09:13:30 +0800 Subject: [PATCH 20/33] Add max_conn_rate, handshake_timeout options for wss listeners --- priv/emqx.schema | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/priv/emqx.schema b/priv/emqx.schema index 7787a28f9..425c0f4f3 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -845,6 +845,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.ws.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -942,6 +946,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -981,6 +989,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} From 18116ac3b5be93669e251d55edefc5e9c22c7162 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 09:32:56 +0800 Subject: [PATCH 21/33] Remove etc/zone.conf --- etc/zone.conf | 126 -------------------------------------------------- 1 file changed, 126 deletions(-) delete mode 100644 etc/zone.conf diff --git a/etc/zone.conf b/etc/zone.conf deleted file mode 100644 index 5c546fe46..000000000 --- a/etc/zone.conf +++ /dev/null @@ -1,126 +0,0 @@ - -## Limits and Capabilities - -##-------------------------------------------------------------------- -## Connection - -zone.${name}.idle_timeout = 30s - -zone.${name}.rate_limit = 10,100 - -## 10 messages per second, with a bucket 100 messages. -zone.${name}.publish_limit = 10,100 - -## Enable stats -zone.${name}.enable_stats = on - -## zone.${name}.shutdown_policy = ??? - -##-------------------------------------------------------------------- -## Protocol - -## Capabilities: - -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -zone.${name}.max_clientid_len = 1024 - -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## -## Default: 64K -zone.${name}.max_packet_size = 64K -zone.${name}.max_topic_alias = 0 -zone.${name}.max_qos_allowed = 2 -zone.${name}.retain_available = on -zone.${name}.wildcard_subscription = on -zone.${name}.shared_subscription = off - -## The backoff for MQTT keepalive timeout. -## EMQ will kick a MQTT connection out until 'Keepalive * backoff * 2' timeout. -## -## Value: Float > 0.5 -zone.${name}.keepalive_backoff = 0.75 - -##-------------------------------------------------------------------- -## Authentication - -zone.${name}.allow_anonymous = true - -##-------------------------------------------------------------------- -## Session - -zone.${name}.max_subscriptions = 0 -zone.${name}.upgrade_qos = off - -## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. -## -## Value: Number -zone.${name}.max_inflight = 32 - -## Retry interval for QoS1/2 message delivering. -## -## Value: Duration -zone.${name}.retry_interval = 20s - -## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. -## -## Value: Number -zone.${name}.max_awaiting_rel = 100 - -## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. -## -## Value: Duration -zone.${name}.await_rel_timeout = 30s - -## Whether to ignore loop delivery of messages. -## -## Value: true | false -## -## Default: false -zone.${name}.ignore_loop_deliver = false - -## Max session expiration time. -## -## Value: Duration -## -d: day -## -h: hour -## -m: minute -## -s: second -## -## Default: 2h, 2 hours -zone.${name}.session_expiry_interval = 2h - -##-------------------------------------------------------------------- -## Queue - -## Message queue type. -## -## Value: simple | priority -zone.${name}.mqueue_type = simple - -## Topic priority. Default is 0. -## -## Value: Number [0-255] -## -## zone.${name}.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 -zone.${name}.max_mqueue_len = 100 - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -zone.${name}.mqueue_store_qos0 = true - -##-------------------------------------------------------------------- -## General - -zone.${name}.enable_stats = on - - From 3ac4be84e401761028c78ad5c448b38ee9d132c1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 14:26:39 +0800 Subject: [PATCH 22/33] Remove 'listener.wss.external.handshake_timeout' for cowboy does not support this option --- etc/acl.conf | 1 + etc/emqx.conf | 7 ------- priv/emqx.schema | 8 ++++---- 3 files changed, 5 insertions(+), 11 deletions(-) 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 6566bc365..78e0a9f1a 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1160,13 +1160,6 @@ listener.wss.external.verify_protocol_header = on ## Value: String, seperated by ',' ## listener.wss.external.tls_versions = tlsv1.2,tlsv1.1,tlsv1 -## TLS Handshake timeout. -## -## See: listener.ssl.$name.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.$name.keyfile diff --git a/priv/emqx.schema b/priv/emqx.schema index 425c0f4f3..d138a4f50 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -989,10 +989,10 @@ end}. {datatype, {duration, ms}} ]}. -{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ - {default, "15s"}, - {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}, From 919eb9fa1e37b9784c677706a904bc1ebe71abc4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 14:27:49 +0800 Subject: [PATCH 23/33] Use cowboy to replace minirest --- src/emqx_app.erl | 2 +- src/emqx_listeners.erl | 66 +++++++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 30 deletions(-) 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_listeners.erl b/src/emqx_listeners.erl index 257820fb1..78fd5db80 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -12,75 +12,83 @@ %% 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:ssl', ListenOn, Options); + %% Start MQTT/WS listener start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> - Dispatch = [{"/mqtt", emqx_ws, []}], + Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws, []}]}]), NumAcceptors = proplists:get_value(acceptors, Options, 4), - MaxConnections = proplists:get_value(max_clients, Options, 1024), + MaxConnections = proplists:get_value(max_connections, Options, 1024), TcpOptions = proplists:get_value(tcp_options, Options, []), - Options1 = [{port, ListenOn}, - {num_acceptors, NumAcceptors}, - {max_connections, MaxConnections} | TcpOptions], - minirest:start_http(Proto, Options1, Dispatch); + 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 -> - Dispatch = [{"/mqtt", emqx_ws, []}], + 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, []), - Options1 = [{port, ListenOn}, - {num_acceptors, NumAcceptors}, - {max_connections, MaxConnections} | TcpOptions ++ SslOptions], - minirest:start_https(Proto, Options1, Dispatch). + 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) -> SockOpts = esockd:parse_opt(Options), MFA = {emqx_connection, start_link, [Options -- SockOpts]}, {ok, _} = esockd:open(Name, ListenOn, merge_default(SockOpts), MFA). +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 -> +restart_listener({Proto, ListenOn, _Options}) when Proto == ssl; Proto == tls -> esockd:reopen('mqtt:ssl', 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 == 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()}). @@ -88,10 +96,10 @@ stop_listener({tcp, ListenOn, _Opts}) -> esockd:close('mqtt:tcp', ListenOn); stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> esockd:close('mqtt:ssl', 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); +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). From 09b55352601542991fb8c86333ef3e9de6ea3d7c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 9 Aug 2018 15:17:42 +0800 Subject: [PATCH 24/33] Depends on cowboy 2.4.0 --- Makefile | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 509794bbd..7b1cc1cdd 100644 --- a/Makefile +++ b/Makefile @@ -4,16 +4,16 @@ PROJECT = emqx PROJECT_DESCRIPTION = EMQ X Broker PROJECT_VERSION = 3.0 -DEPS = jsx gproc gen_rpc lager ekka esockd minirest clique +DEPS = jsx gproc gen_rpc lager ekka esockd cowboy 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_minirest = git https://github.com/emqx/minirest emqx30 -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 From d516b8c24193dfbc17875ba24b76f19b0226d8a6 Mon Sep 17 00:00:00 2001 From: turtled Date: Thu, 9 Aug 2018 15:19:45 +0800 Subject: [PATCH 25/33] mochiweb -> cowboy --- src/emqx.erl | 4 ++-- src/emqx_ws_connection_sup.erl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqx.erl b/src/emqx.erl index cbe37d12e..475428fd4 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -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_ws_connection_sup.erl b/src/emqx_ws_connection_sup.erl index f627abfbb..1216eeb75 100644 --- a/src/emqx_ws_connection_sup.erl +++ b/src/emqx_ws_connection_sup.erl @@ -27,7 +27,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @doc Start a MQTT/WebSocket Connection. --spec(start_connection(pid(), mochiweb_request:request()) -> {ok, pid()}). +-spec(start_connection(pid(), cowboy_req:req()) -> {ok, pid()}). start_connection(WsPid, Req) -> supervisor:start_child(?MODULE, [WsPid, Req]). From d9004d4cfb55019825315cb4cad3cbaf669349e6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 10 Aug 2018 12:43:32 +0800 Subject: [PATCH 26/33] Add MQTT section in configuration file --- etc/emqx.conf | 68 +++++++++++++++++++++++++++++++-------- priv/emqx.schema | 84 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 121 insertions(+), 31 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 78e0a9f1a..83ddcf6a8 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -440,10 +440,50 @@ enable_acl_cache = on ## Default: 5 minute acl_cache_age = 5m -## The ACL cache size, 0 means no limit. +##-------------------------------------------------------------------- +## MQTT +##-------------------------------------------------------------------- + +## Maximum MQTT packet size allowed. ## -## Value: Integer -acl_cache_size = 0 +## Value: Bytes +## Default: 1MB +mqtt.max_packet_size = 1MB + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +mqtt.max_clientid_len = 65535 + +## Maximum topic levels allowed. 0 means no limit. +## +## Value: Number +mqtt.max_topic_levels = 0 + +## Maximum QoS allowed. +## +## Value: 0 | 1 | 2 +mqtt.max_qos_allowed = 2 + +## Maximum Topic Alias, 0 means no limit. +## +## Value: 0-65535 +mqtt.max_topic_alias = 0 + +## Whether the Server supports MQTT retained messages. +## +## Value: boolean +mqtt.retain_available = true + +## 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 @@ -1303,43 +1343,43 @@ zone.external.enable_stats = on ## Maximum MQTT packet size allowed. ## ## Value: Bytes -## Default: 64KB -zone.external.max_packet_size = 1MB +## Default: 1MB +## zone.external.max_packet_size = 64KB ## Maximum length of MQTT clientId allowed. ## ## Value: Number [23-65535] -zone.external.max_clientid_len = 65535 +## zone.external.max_clientid_len = 1024 ## Maximum topic levels allowed. 0 means no limit. ## ## Value: Number -zone.external.max_topic_levels = 0 +## zone.external.max_topic_levels = 7 ## Maximum QoS allowed. ## ## Value: 0 | 1 | 2 -zone.external.max_qos_allowed = 2 +## zone.external.max_qos_allowed = 2 ## Maximum Topic Alias, 0 means no limit. ## ## Value: 0-65535 -zone.external.max_topic_alias = 0 +## zone.external.max_topic_alias = 0 ## Whether the Server supports retained messages. ## ## Value: boolean -zone.external.retain_available = true +## zone.external.retain_available = true ## Whether the Server supports Wildcard Subscriptions ## ## Value: boolean -zone.external.wildcard_subscription = true +## zone.external.wildcard_subscription = false ## Whether the Server supports Shared Subscriptions ## ## Value: boolean -zone.external.shared_subscription = true +## zone.external.shared_subscription = false ## The backoff for MQTT keepalive timeout. The broker will kick a connection out ## until 'Keepalive * backoff * 2' timeout. @@ -1421,12 +1461,12 @@ zone.internal.enable_acl = off ## See zone.$name.wildcard_subscription. ## ## Value: boolean -zone.internal.wildcard_subscription = true +## zone.internal.wildcard_subscription = true ## See zone.$name.shared_subscription. ## ## Value: boolean -zone.internal.shared_subscription = true +## zone.internal.shared_subscription = true ## See zone.$name.max_subscriptions. ## diff --git a/priv/emqx.schema b/priv/emqx.schema index d138a4f50..a81d21799 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -578,11 +578,64 @@ end}. ]}. %% @doc ACL cache size. -{mapping, "acl_cache_size", "emqx.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 Set the Max ClientId Length Allowed. +{mapping, "mqtt.max_clientid_len", "emqx.max_clientid_len", [ + {default, 65535}, + {datatype, integer} +]}. + +%% @doc Set the Maximum topic levels. +{mapping, "mqtt.max_topic_levels", "emqx.max_topic_levels", [ {default, 0}, {datatype, integer} ]}. +%% @doc Set the Maximum QoS allowed. +{mapping, "mqtt.max_qos_allowed", "emqx.max_qos_allowed", [ + {default, 2}, + {datatype, integer}, + {validators, ["range:0-2"]} +]}. + +%% @doc Set the Maximum topic alias. +{mapping, "mqtt.max_topic_alias", "emqx.max_topic_alias", [ + {default, 0}, + {datatype, integer} +]}. + +%% @doc Whether the server supports MQTT retained messages. +{mapping, "mqtt.retain_available", "emqx.mqtt_retain_available", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports MQTT Wildcard Subscriptions. +{mapping, "mqtt.wildcard_subscription", "emqx.mqtt_wildcard_subscription", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + +%% @doc Whether the Server supports MQTT Shared Subscriptions. +{mapping, "mqtt.shared_subscription", "emqx.mqtt_shared_subscription", [ + {default, true}, + {datatype, {enum, [true, false]}} +]}. + %%-------------------------------------------------------------------- %% Listeners %%-------------------------------------------------------------------- @@ -1211,50 +1264,42 @@ end}. %% @doc Max Packet Size Allowed, 64K by default. {mapping, "zone.$name.max_packet_size", "emqx.zones", [ - {default, "64KB"}, {datatype, bytesize} ]}. %% @doc Set the Max ClientId Length Allowed. {mapping, "zone.$name.max_clientid_len", "emqx.zones", [ - {default, 65535}, {datatype, integer} ]}. %% @doc Set the Maximum topic levels. {mapping, "zone.$name.max_topic_levels", "emqx.zones", [ - {default, 0}, {datatype, integer} ]}. %% @doc Set the Maximum QoS allowed. {mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ - {default, 2}, {datatype, integer}, {validators, ["range:0-2"]} ]}. %% @doc Set the Maximum topic alias. {mapping, "zone.$name.max_topic_alias", "emqx.zones", [ - {default, 0}, {datatype, integer} ]}. %% @doc Whether the server supports retained messages. {mapping, "zone.$name.retain_available", "emqx.zones", [ - {default, true}, {datatype, {enum, [true, false]}} ]}. %% @doc Whether the Server supports Wildcard Subscriptions. {mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ - {default, true}, {datatype, {enum, [true, false]}} ]}. %% @doc Whether the Server supports Shared Subscriptions. {mapping, "zone.$name.shared_subscription", "emqx.zones", [ - {default, true}, {datatype, {enum, [true, false]}} ]}. @@ -1327,16 +1372,21 @@ end}. ]}. {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) -> {Opt, Val} + end, maps:to_list( lists:foldl( - fun({["zone", Name, Opt], Val}, Acc) -> - ZName = list_to_atom(Name), - case maps:find(ZName, Acc) of - {ok, Opts} -> - maps:put(ZName, [{list_to_atom(Opt), Val} | Opts], Acc); - error -> - maps:put(ZName, [{list_to_atom(Opt), Val}], Acc) - end + fun({["zone", Name, Opt], Val}, Zones) -> + maps:update_with(list_to_atom(Name), + fun(Opts) -> + [Mapping(list_to_atom(Opt), Val)|Opts] + end, [], Zones) end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) end}. From 6ab489a9b5f2e50cc141acbaeabee76624398076 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 10 Aug 2018 15:38:08 +0800 Subject: [PATCH 27/33] Change the MAX_CLIENTID_LEN to 65535 --- include/emqx_mqtt.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 35a8a5082..007de4dd1 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -78,7 +78,7 @@ %% Maximum ClientId Length. %%-------------------------------------------------------------------- --define(MAX_CLIENTID_LEN, 1024). +-define(MAX_CLIENTID_LEN, 65535). %%-------------------------------------------------------------------- %% MQTT Client From bc8302dae9e4802a0a8090da941747f9f569235c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 10 Aug 2018 15:39:24 +0800 Subject: [PATCH 28/33] Change default value of max_mqueue_len to 1000 --- priv/emqx.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/emqx.schema b/priv/emqx.schema index a81d21799..44b5e4bc1 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1361,7 +1361,7 @@ end}. %% @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, 0}, + {default, 1000}, {datatype, integer} ]}. From 3d05954d5b2b400e7545e1ee6719edac3e1e570b Mon Sep 17 00:00:00 2001 From: Gilbert Wong Date: Sat, 11 Aug 2018 16:17:39 +0800 Subject: [PATCH 29/33] Optimize emqx_time module --- src/emqx_time.erl | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 2e69638dc..40c71faa6 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -14,23 +14,13 @@ -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]). seed() -> rand:seed(exsplus, erlang:timestamp()). now_ms() -> - now_ms(os:timestamp()). - -now_ms({MegaSecs, Secs, MicroSecs}) -> - (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). + erlang:system_time(millisecond). 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}. - + erlang:system_time(second). From f80cd2d98686f78af522fd7006ea4ecd6181efaf Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 11 Aug 2018 17:57:19 +0800 Subject: [PATCH 30/33] Improve the MQTT over Websocket connection --- priv/emqx.schema | 18 +- src/emqx_access_control.erl | 14 +- src/emqx_config.erl | 10 +- src/emqx_connection.erl | 88 +++---- src/emqx_gc.erl | 4 +- src/emqx_listeners.erl | 2 +- src/emqx_mqueue.erl | 58 ++--- src/emqx_packet.erl | 2 +- src/emqx_plugins.erl | 51 ++-- src/emqx_protocol.erl | 46 +--- src/emqx_router.erl | 1 - src/emqx_session.erl | 85 +++--- src/emqx_sup.erl | 3 - src/emqx_time.erl | 2 +- src/emqx_ws.erl | 103 -------- src/emqx_ws_connection.erl | 459 ++++++++++++++++----------------- src/emqx_ws_connection_sup.erl | 44 ---- src/emqx_zone.erl | 26 +- 18 files changed, 407 insertions(+), 609 deletions(-) delete mode 100644 src/emqx_ws.erl delete mode 100644 src/emqx_ws_connection_sup.erl diff --git a/priv/emqx.schema b/priv/emqx.schema index 44b5e4bc1..c502a8d24 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1372,21 +1372,21 @@ end}. ]}. {translation, "emqx.zones", fun(Conf) -> - Mapping = fun(retain_available, Val) -> + Mapping = fun("retain_available", Val) -> {mqtt_retain_available, Val}; - (wildcard_subscription, Val) -> + ("wildcard_subscription", Val) -> {mqtt_wildcard_subscription, Val}; - (shared_subscription, Val) -> + ("shared_subscription", Val) -> {mqtt_shared_subscription, Val}; - (Opt, Val) -> {Opt, 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(list_to_atom(Opt), Val)|Opts] - end, [], Zones) + fun(Opts) -> [Mapping(Opt, Val)|Opts] end, + [Mapping(Opt, Val)], Zones) end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) end}. @@ -1507,9 +1507,11 @@ end}. maps:update_with(list_to_atom(Name), fun(Opts) -> Merge(list_to_atom(Opt), Val, Opts) - end, [{subscriptions, Subscriptions(Name)}], Acc); + end, [{list_to_atom(Opt), Val}, + {subscriptions, Subscriptions(Name)}], Acc); (_, Acc) -> Acc end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("bridge.", Conf)))) + end}. %%-------------------------------------------------------------------- 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_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 89233fb43..38c100297 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -21,9 +21,8 @@ -include("emqx_misc.hrl"). -export([start_link/3]). - -export([info/1, stats/1, kick/1]). --export([get_session/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]). @@ -44,7 +43,7 @@ rate_limit, %% Traffic rate limit limit_timer, %% Rate limit timer proto_state, %% MQTT protocol state - parse_state, %% MQTT parse state + parser_state, %% MQTT parser state keepalive, %% MQTT keepalive timer enable_stats, %% Enable stats stats_timer, %% Stats timer @@ -75,7 +74,7 @@ stats(CPid) -> kick(CPid) -> gen_server:call(CPid, kick). -get_session(CPid) -> +session(CPid) -> gen_server:call(CPid, session, infinity). clean_acl_cache(CPid) -> @@ -100,22 +99,20 @@ set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> init([Transport, RawSocket, Options]) -> case Transport:wait(RawSocket) of {ok, Socket} -> - io:format("Options: ~p~n", [Options]), + 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]), - Zone = proplists:get_value(zone, Options), - RateLimit = init_rate_limit(proplists:get_value(rate_limit, Options)), - PubLimit = init_rate_limit(emqx_zone:get_env(Zone, publish_limit)), - EnableStats = emqx_zone:get_env(Zone, enable_stats, false), - IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + 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(#{zone => Zone, - peername => Peername, + ProtoState = emqx_protocol:init(#{peername => Peername, sockname => Sockname, peercert => Peercert, sendfun => SendFun}, Options), - ParseState = emqx_protocol:parser(ProtoState), + ParserState = emqx_protocol:parser(ProtoState), State = run_socket(#state{transport = Transport, socket = Socket, peername = Peername, @@ -124,7 +121,7 @@ init([Transport, RawSocket, Options]) -> rate_limit = RateLimit, pub_limit = PubLimit, proto_state = ProtoState, - parse_state = ParseState, + parser_state = ParserState, enable_stats = EnableStats, idle_timeout = IdleTimout}), gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], @@ -133,9 +130,9 @@ init([Transport, RawSocket, Options]) -> {stop, Reason} end. -init_rate_limit(undefined) -> +rate_limit(undefined) -> undefined; -init_rate_limit({Rate, Burst}) -> +rate_limit({Rate, Burst}) -> esockd_rate_limit:new(Rate, Burst). send_fun(Transport, Socket, Peername) -> @@ -152,8 +149,7 @@ send_fun(Transport, Socket, Peername) -> 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)], + 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}; @@ -169,7 +165,7 @@ handle_call(stats, _From, State = #state{transport = Transport, socket = Sock, p handle_call(kick, _From, State) -> {stop, {shutdown, kick}, ok, State}; -handle_call(get_session, _From, State = #state{proto_state = ProtoState}) -> +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}) -> @@ -195,28 +191,20 @@ handle_cast(Msg, State) -> ?LOG(error, "unexpected cast: ~p", [Msg], State), {noreply, State}. -handle_info(Sub = {subscribe, _TopicTable}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:process(Sub, ProtoState) - end, State); - -handle_info(Unsub = {unsubscribe, _Topics}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:process(Unsub, ProtoState) - end, State); - -handle_info({deliver, PubOrAck}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:deliver(PubOrAck, ProtoState) - end, maybe_gc(ensure_stats_timer(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(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}; + {noreply, State#state{stats_timer = undefined}, hibernate}; handle_info(timeout, State) -> shutdown(idle_timeout, State); @@ -306,20 +294,20 @@ handle_packet(<<>>, State) -> {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; handle_packet(Bytes, State = #state{incoming = Incoming, - parse_state = ParseState, + parser_state = ParserState, proto_state = ProtoState, idle_timeout = IdleTimeout}) -> - case catch emqx_frame:parse(Bytes, ParseState) of - {more, NewParseState} -> - {noreply, State#state{parse_state = NewParseState}, 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} -> - ParseState1 = emqx_protocol:parser(ProtoState1), - handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), - proto_state = ProtoState1, - parse_state = ParseState1}); + 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); @@ -368,16 +356,6 @@ 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}) -> - case Fun(ProtoState) of - {ok, ProtoState1} -> - {noreply, State#state{proto_state = ProtoState1}}; - {error, Reason} -> - shutdown(Reason, State); - {error, Reason, ProtoState1} -> - shutdown(Reason, State#state{proto_state = ProtoState1}) - end. - ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, idle_timeout = IdleTimeout}) -> 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_listeners.erl b/src/emqx_listeners.erl index 78fd5db80..084ffe7c2 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -38,7 +38,7 @@ start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> %% Start MQTT/WS listener start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> - Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws, []}]}]), + 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, []), diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 31811583f..458c301fc 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -39,32 +39,25 @@ %% %% @end +%% TODO: ... -module(emqx_mqueue). -%% TODO: XYZ -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). -import(proplists, [get_value/3]). --export([new/2, 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()} @@ -76,29 +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}). -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())) -> mqueue()). -new(Name, Opts) -> - 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)}, 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; @@ -110,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. @@ -172,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 diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 8baa6f088..65f125f68 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -114,7 +114,7 @@ format_variable(#mqtt_packet_connect{ 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}) -> 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_protocol.erl b/src/emqx_protocol.erl index 47a2c16e4..705674000 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -36,7 +36,7 @@ {shared_subscription, true}, {wildcard_subscription, true}]). --record(proto_state, {sockprops, capabilities, connected, client_id, client_pid, +-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, @@ -56,15 +56,17 @@ -export_type([proto_state/0]). -init(SockProps = #{zone := Zone, peercert := Peercert}, Options) -> - MountPoint = emqx_zone:get_env(Zone, mountpoint), - Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), +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{sockprops = SockProps, + #proto_state{zone = Zone, + sockprops = SockProps, capabilities = capabilities(Zone), connected = false, clean_start = true, @@ -82,7 +84,7 @@ init(SockProps = #{zone := Zone, peercert := Peercert}, Options) -> send_msg = 0}. capabilities(Zone) -> - Capabilities = emqx_zone:get_env(Zone, mqtt_capabilities, []), + Capabilities = emqx_zone:env(Zone, mqtt_capabilities, []), maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)). parser(#proto_state{capabilities = #{max_packet_size := Size}, proto_ver = Ver}) -> @@ -128,7 +130,9 @@ received(Packet = ?PACKET(Type), ProtoState) -> {error, Reason, ProtoState} end. -process(?CONNECT_PACKET(Var), ProtoState = #proto_state{username = Username0, client_pid = ClientPid}) -> +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, @@ -160,7 +164,8 @@ process(?CONNECT_PACKET(Var), ProtoState = #proto_state{username = Username0, cl %% Generate clientId if null ProtoState2 = maybe_set_clientid(ProtoState1), %% Open session - case emqx_sm:open_session(#{clean_start => CleanStart, + case emqx_sm:open_session(#{zone => Zone, + clean_start => CleanStart, client_id => clientid(ProtoState2), username => Username, client_pid => ClientPid}) of @@ -242,23 +247,10 @@ process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), State) -> {ok, TopicFilters1} -> ok = emqx_session:subscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicFilters1)}), {ok, State}; - {stop, _} -> - {ok, State} + {stop, _} -> {ok, State} end end; -process({subscribe, RawTopicTable}, - State = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> - TopicTable = parse_topic_filters(RawTopicTable), - case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of - {ok, TopicTable1} -> - emqx_session:subscribe(Session, TopicTable1); - {stop, _} -> ok - end, - {ok, State}; - %% Protect from empty topic list process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> send(?UNSUBACK_PACKET(PacketId), State); @@ -276,16 +268,6 @@ process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopics), end, send(?UNSUBACK_PACKET(PacketId), State); -process({unsubscribe, RawTopics}, State = #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, {undefined, #{}, TopicTable}); - {stop, _} -> ok - end, - {ok, State}; - process(?PACKET(?PINGREQ), ProtoState) -> send(?PACKET(?PINGRESP), ProtoState); diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 85a6a63ad..863214617 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -39,7 +39,6 @@ -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 03bcae3f2..1253de5cc 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -48,7 +48,9 @@ -export([resume/2, discard/2]). -export([subscribe/2]).%%, subscribe/3]). -export([publish/3]). --export([puback/2, pubrec/2, pubrel/2, pubcomp/2]). +-export([puback/2, puback/3]). +-export([pubrec/2, pubrec/3]). +-export([pubrel/2, pubcomp/2]). -export([unsubscribe/2]). %% gen_server callbacks @@ -139,7 +141,11 @@ }). -define(TIMEOUT, 60000). + +-define(DEFAULT_SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). + -define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]). + -define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight, max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, @@ -151,16 +157,21 @@ "Session(~s): " ++ Format, [State#state.client_id | Args])). %% @doc Start a session --spec(start_link(Attrs :: map()) -> {ok, pid()} | {error, term()}). -start_link(Attrs) -> - gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]). +-spec(start_link(SessAttrs :: map()) -> {ok, pid()} | {error, term()}). +start_link(SessAttrs) -> + gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, 30000}]). %%------------------------------------------------------------------------------ %% PubSub API %%------------------------------------------------------------------------------ +-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 --spec(subscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). subscribe(SPid, SubReq = {PacketId, Props, TopicFilters}) -> gen_server:cast(SPid, {subscribe, self(), SubReq}). @@ -200,6 +211,9 @@ pubcomp(SPid, PacketId) -> gen_server:cast(SPid, {pubcomp, PacketId}). -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}). @@ -252,40 +266,43 @@ close(SPid) -> %% gen_server callbacks %%------------------------------------------------------------------------------ -init(#{clean_start := CleanStart, client_id := ClientId, username := Username, client_pid := ClientPid}) -> +init(#{zone := Zone, + client_id := ClientId, + 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 = proplists:get_value(max_inflight, Env, 0), - EnableStats = proplists:get_value(enable_stats, Env, false), - IgnoreLoopDeliver = proplists:get_value(ignore_loop_deliver, Env, false), - MQueue = emqx_mqueue:new(ClientId, QEnv), + 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 = proplists:get_value(max_subscriptions, Env, 0), - upgrade_qos = proplists: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 = proplists:get_value(retry_interval, Env), + mqueue = init_mqueue(Zone, ClientId), + retry_interval = emqx_zone:env(Zone, retry_interval, 0), awaiting_rel = #{}, - await_rel_timeout = proplists:get_value(await_rel_timeout, Env), - max_awaiting_rel = proplists:get_value(max_awaiting_rel, Env), - expiry_interval = proplists: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). @@ -331,7 +348,7 @@ handle_call(Req, _From, State) -> {reply, ignored, State}. handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, - State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> + State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> ?LOG(info, "Subscribe ~p", [TopicFilters], State), {ReasonCodes, Subscriptions1} = lists:foldl(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) -> @@ -342,12 +359,12 @@ handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, SubMap; {ok, OldOpts} -> emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, 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, Username], {Topic, SubOpts}), + emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]), maps:put(Topic, SubOpts, SubMap) end} end, {[], Subscriptions}, TopicFilters), @@ -355,14 +372,14 @@ handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, {noreply, emit_stats(State#state{subscriptions = Subscriptions1})}; handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, - State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> + State = #state{client_id = ClientId, subscriptions = Subscriptions}) -> ?LOG(info, "Unsubscribe ~p", [TopicFilters], State), {ReasonCodes, Subscriptions1} = lists:foldl(fun(Topic, {RcAcc, SubMap}) -> case maps:find(Topic, SubMap) of {ok, SubOpts} -> emqx_broker:unsubscribe(Topic, ClientId), - emqx_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, SubOpts}), + emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]), {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)}; error -> {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap} @@ -473,13 +490,18 @@ handle_cast(Msg, State) -> emqx_logger:error("[Session] unexpected cast: ~p", [Msg]), {noreply, State}. -%% Ignore Messages delivered by self +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}; %% Dispatch Message -handle_info({dispatch, Topic, Msg}, State) -> +handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. @@ -510,11 +532,10 @@ handle_info({'EXIT', ClientPid, Reason}, {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", [ClientPid, Pid, Reason], State), {noreply, State, hibernate}; diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index b3378ba91..563244232 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -71,8 +71,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}, @@ -84,7 +82,6 @@ init([]) -> SMSup, SessionSup, CMSup, - WSConnSup, SysSup]}}. %%-------------------------------------------------------------------- diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 2e69638dc..623c4a543 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -20,7 +20,7 @@ seed() -> rand:seed(exsplus, erlang:timestamp()). now_ms() -> - now_ms(os:timestamp()). + os:system_time(milli_seconds). now_ms({MegaSecs, Secs, MicroSecs}) -> (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). diff --git a/src/emqx_ws.erl b/src/emqx_ws.erl deleted file mode 100644 index d7f6dc6e8..000000000 --- a/src/emqx_ws.erl +++ /dev/null @@ -1,103 +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]). - -%% WebSocket Loop State --record(wsocket_state, {req, peername, client_pid, max_packet_size, parser}). - --define(WSLOG(Level, Format, Args, State), - lager:Level("WsClient(~s): " ++ Format, - [esockd_net:format(State#wsocket_state.peername) | Args])). - --export([init/2]). --export([websocket_init/1]). --export([websocket_handle/2]). --export([websocket_info/2]). - -init(Req0, _State) -> - case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req0) of - undefined -> - {cowboy_websocket, Req0, #wsocket_state{}}; - Subprotocols -> - case lists:member(<<"mqtt">>, Subprotocols) of - true -> - Peername = cowboy_req:peer(Req0), - Req = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req0), - {cowboy_websocket, Req, #wsocket_state{req = Req, peername = Peername}, #{idle_timeout => 86400000}}; - false -> - Req = cowboy_req:reply(400, Req0), - {ok, Req, #wsocket_state{}} - end - end. - -websocket_init(State = #wsocket_state{req = Req}) -> - case emqx_ws_connection_sup:start_connection(self(), Req) of - {ok, ClientPid} -> - {ok, ProtoEnv} = emqx_config:get_env(protocol), - PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE), - Parser = emqx_frame:initial_state(#{max_packet_size => PacketSize}), - NewState = State#wsocket_state{parser = Parser, - max_packet_size = PacketSize, - client_pid = ClientPid}, - {ok, NewState}; - Error -> - ?WSLOG(error, "Start client fail: ~p", [Error], State), - {stop, State} - end. - -websocket_handle({binary, <<>>}, State) -> - {ok, State}; -websocket_handle({binary, [<<>>]}, State) -> - {ok, State}; - -websocket_handle({binary, Data}, State = #wsocket_state{client_pid = ClientPid, parser = Parser}) -> - ?WSLOG(debug, "RECV ~p", [Data], State), - BinSize = iolist_size(Data), - emqx_metrics:inc('bytes/received', BinSize), - case catch emqx_frame:parse(iolist_to_binary(Data), Parser) of - {more, NewParser} -> - {ok, State#wsocket_state{parser = NewParser}}; - {ok, Packet, Rest} -> - gen_server:cast(ClientPid, {received, Packet, BinSize}), - websocket_handle({binary, Rest}, reset_parser(State)); - {error, Error} -> - ?WSLOG(error, "Frame error: ~p", [Error], State), - {stop, State}; - {'EXIT', Reason} -> - ?WSLOG(error, "Frame error: ~p", [Reason], State), - ?WSLOG(error, "Error data: ~p", [Data], State), - {stop, State} - end. - -websocket_info({binary, Data}, State) -> - {reply, {binary, Data}, State}; - -websocket_info({'EXIT', Pid, Reason}, State = #wsocket_state{client_pid = Pid}) -> - ?WSLOG(debug, "EXIT: ~p", [Reason], State), - {stop, State}; - -websocket_info(_Info, State) -> - {ok, State}. - -reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) -> - State#wsocket_state{parser = emqx_frame:initial_state(#{max_packet_size => PacketSize})}. - - diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 93a289636..5932b9ef7 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -14,232 +14,111 @@ -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/3]). - -%% 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]). -%% WebSocket Client State --record(wsclient_state, {ws_pid, peername, proto_state, keepalive, - enable_stats, force_gc_count}). +-record(state, { + request, + options, + peername, + sockname, + proto_state, + parser_state, + keepalive, + enable_stats, + stats_timer, + idle_timeout, + shutdown_reason + }). -%% recv_oct -%% Number of bytes received by the socket. +-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -%% recv_cnt -%% Number of packets received by the socket. - --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) -> - gen_server:start_link(?MODULE, [Env, WsPid, Req], - [[{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([Options, WsPid, Req]) -> - init_stas(), - process_flag(trap_exit, true), - true = link(WsPid), - Peername = cowboy_req:peer(Req), - Headers = cowboy_req:headers(Req), - Sockname = cowboy_req:sock(Req), - Peercert = cowboy_req:cert(Req), - Zone = proplists:get_value(zone, Options), - ProtoState = emqx_protocol:init(#{zone => Zone, - peername => Peername, - sockname => Sockname, - peercert => Peercert, - sendfun => send_fun(WsPid)}, - [{ws_initial_headers, Headers} | Options]), - IdleTimeout = get_value(client_idle_timeout, Options, 30000), - EnableStats = get_value(client_enable_stats, Options, false), - ForceGcCount = emqx_gc:conn_max_gc_count(), - {ok, #wsclient_state{ws_pid = WsPid, - peername = Peername, - proto_state = ProtoState, - enable_stats = EnableStats, - force_gc_count = ForceGcCount}, IdleTimeout}. - -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); - -handle_call(stats, _From, State = #wsclient_state{proto_state = ProtoState}) -> - reply(lists:append([emqx_misc:proc_stats(), - wsock_stats(), - emqx_protocol:stats(ProtoState)]), State); - -handle_call(kick, _From, State) -> - {stop, {shutdown, kick}, ok, State}; - -handle_call(session, _From, State = #wsclient_state{proto_state = ProtoState}) -> - reply(emqx_protocol:session(ProtoState), State); - -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, BinSize}, State = #wsclient_state{proto_state = ProtoState}) -> - put(recv_oct, get(recv_oct) + BinSize), - put(recv_cnt, get(recv_cnt) + 1), - emqx_metrics:received(Packet), - case emqx_protocol:received(Packet, ProtoState) of - {ok, ProtoState1} -> - {noreply, gc(State#wsclient_state{proto_state = ProtoState1}), hibernate}; - {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}) - end; - -handle_cast(Msg, State) -> - ?WSLOG(error, "unexpected msg: ~p", [Msg], State), - {noreply, State}. - -handle_info(SubReq ={subscribe, _TopicTable}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:process(SubReq, ProtoState) - end, State); - -handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:process(UnsubReq, ProtoState) - end, State); - -handle_info({deliver, PubOrAck}, State) -> - with_proto( - fun(ProtoState) -> - emqx_protocol:deliver(PubOrAck, ProtoState) - end, gc(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) -> - ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), - case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of - {ok, KeepAlive} -> - {noreply, State#wsclient_state{keepalive = KeepAlive}, hibernate}; - {error, Error} -> - ?WSLOG(warning, "Keepalive error - ~p", [Error], State), - shutdown(Error, State) - end; - -handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive}) -> - case emqx_keepalive:check(KeepAlive) of - {ok, KeepAlive1} -> - {noreply, emit_stats(State#wsclient_state{keepalive = KeepAlive1}), hibernate}; - {error, timeout} -> - ?WSLOG(debug, "Keepalive Timeout!", [], State), - shutdown(keepalive_timeout, State); - {error, Error} -> - ?WSLOG(warning, "Keepalive error - ~p", [Error], State), - shutdown(keepalive_error, State) - end; - -handle_info({'EXIT', WsPid, normal}, State = #wsclient_state{ws_pid = WsPid}) -> - stop(normal, State); - -handle_info({'EXIT', WsPid, Reason}, State = #wsclient_state{ws_pid = WsPid}) -> - ?WSLOG(error, "shutdown: ~p",[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) +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. -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +%%------------------------------------------------------------------------------ +%% WebSocket callbacks +%%------------------------------------------------------------------------------ -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- +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. + +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}}. send_fun(WsPid) -> fun(Data) -> @@ -251,45 +130,143 @@ send_fun(WsPid) -> end. stat_fun() -> - fun() -> - {ok, get(recv_oct)} + fun() -> {ok, get(recv_oct)} end. + +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, "Frame error: ~p", [Error], State), + {stop, State}; + {'EXIT', Reason} -> + ?WSLOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data], State), + {stop, State} end. -emit_stats(State = #wsclient_state{proto_state = ProtoState}) -> - emit_stats(emqx_protocol:clientid(ProtoState), State). +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}; -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), +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; + +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}; + +websocket_info({keepalive, start, Interval}, State) -> + ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), + case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of + {ok, KeepAlive} -> + {ok, State#state{keepalive = KeepAlive}}; + {error, Error} -> + ?WSLOG(warning, "Keepalive error - ~p", [Error], State), + shutdown(Error, State) + end; + +websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> + case emqx_keepalive:check(KeepAlive) of + {ok, KeepAlive1} -> + {ok, State#state{keepalive = KeepAlive1}}; + {error, timeout} -> + ?WSLOG(debug, "Keepalive Timeout!", [], State), + shutdown(keepalive_timeout, State); + {error, Error} -> + ?WSLOG(warning, "Keepalive error - ~p", [Error], State), + shutdown(keepalive_error, State) + end; + +websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> + ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State), + shutdown(conflict, State); + +websocket_info({binary, Data}, State) -> + {reply, {binary, Data}, State}; + +websocket_info({shutdown, Reason}, State) -> + shutdown(Reason, State); + +websocket_info(Info, State) -> + ?WSLOG(error, "unexpected info: ~p", [Info], State), + {ok, State}. + +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. + +reset_parser(State = #state{proto_state = ProtoState}) -> + State#state{parser_state = emqx_protocol:parser(ProtoState)}. + +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() -> - [{Key, get(Key)}|| Key <- ?SOCK_STATS]. - -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). - -init_stas() -> - put(recv_oct, 0), - put(recv_cnt, 0), - put(send_oct, 0), - put(send_cnt, 0). +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 1216eeb75..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/2]). - --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(), cowboy_req:req()) -> {ok, pid()}). -start_connection(WsPid, Req) -> - supervisor:start_child(?MODULE, [WsPid, Req]). - -%%-------------------------------------------------------------------- -%% 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 index 0d874a38b..830f08b89 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -17,24 +17,32 @@ -behaviour(gen_server). -export([start_link/0]). --export([get_env/2, get_env/3]). + +-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). --define(SERVER, ?MODULE). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -get_env(Zone, Par) -> - get_env(Zone, Par, undefined). +env(undefined, Par) -> + emqx_config:get_env(Par); +env(Zone, Par) -> + env(Zone, Par, undefined). -get_env(Zone, Par, Def) -> - try ets:lookup_element(?TAB, {Zone, Par}, 2) catch error:badarg -> Def end. +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 @@ -54,8 +62,8 @@ handle_cast(Msg, State) -> handle_info(reload, State) -> lists:foreach( - fun({Zone, Options}) -> - [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Options] + fun({Zone, Opts}) -> + [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Opts] end, emqx_config:get_env(zones, [])), {noreply, ensure_reload_timer(State), hibernate}; From c9d604ed02aff0fa40aa5f4dc5e3029986eb551d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 13 Aug 2018 16:49:53 +0800 Subject: [PATCH 31/33] Fix the badmatch error of packet_to_msg/1 --- src/emqx_client.erl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 8c742fbfd..695b9d10c 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -992,8 +992,14 @@ deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, topic => Topic, properties => Props, payload => Payload}}, State. -packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Props, Payload)) -> - #mqtt_packet_header{qos = QoS, retain = R, dup = Dup} = Header, +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}. From 22e8b07a3d41602180774178a3d0b06ded7d16ec Mon Sep 17 00:00:00 2001 From: turtled Date: Sun, 19 Aug 2018 20:31:44 +0800 Subject: [PATCH 32/33] Receive/send messages by bridge --- etc/emqx.conf | 241 ++++++++++++++++++++++++++++++------- priv/emqx.schema | 27 ++++- src/emqx_bridge1.erl | 254 +++++++++++++++++++++++++++++++++++++++ src/emqx_bridge1_sup.erl | 45 +++++++ src/emqx_broker.erl | 4 +- src/emqx_message.erl | 4 +- src/emqx_sup.erl | 2 + src/emqx_time.erl | 4 +- 8 files changed, 529 insertions(+), 52 deletions(-) create mode 100644 src/emqx_bridge1.erl create mode 100644 src/emqx_bridge1_sup.erl diff --git a/etc/emqx.conf b/etc/emqx.conf index 83ddcf6a8..6b31c63c3 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -1497,16 +1497,20 @@ zone.internal.mqueue_store_qos0 = true ## Bridges ##-------------------------------------------------------------------- -## Bridge Type. +##-------------------------------------------------------------------- +## Bridges to edge +##-------------------------------------------------------------------- +## Bridge type. ## -## Value: local | remote -bridge.name.type = local +## 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.name.address = emqx@127.0.0.1 +bridge.edge.address = 127.0.0.1:1883 ## Protocol version of the bridge. ## @@ -1514,76 +1518,221 @@ bridge.name.address = emqx@127.0.0.1 ## - mqtt5 ## - mqtt4 ## - mqtt3 -bridge.name.proto_ver = mqtt4 +bridge.edge.proto_ver = mqtt4 ## The ClientId of a remote bridge. ## ## Value: String -bridge.name.client_id = bridge:$name +bridge.edge.client_id = bridge_edge ## The Clean start flag of a remote bridge. ## ## Value: boolean -bridge.name.clean_start = false +bridge.edge.clean_start = false ## The username for a remote bridge. ## ## Value: String -bridge.name.username = user +bridge.edge.username = user ## The password for a remote bridge. ## ## Value: String -bridge.name.password = passwd +bridge.edge.password = passwd ## Mountpoint of the bridge. ## ## Value: String -bridge.name.mountpoint = bridge/$name/ - -## PEM-encoded CA certificates of the bridge. -## -## Value: File -bridge.name.cacertfile = cacert.pem - -## SSL Certfile of the bridge. -## -## Value: File -bridge.name.certfile = cert.pem - -## SSL Keyfile of the bridge. -## -## Value: File -bridge.name.keyfile = key.pem - -## SSL Ciphers used by the bridge. -## -## Value: String -bridge.name.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384 - -## TLS versions used by the bridge. -## -## Value: String -bridge.name.tls_versions = tlsv1.2,tlsv1.1,tlsv1 - -## The pending message queue of a bridge. -## -## Value: Number -bridge.name.max_pending_messages = 10000 +## bridge.edge.mountpoint = bridge/edge/ ## Ping interval of a down bridge. ## ## Value: Duration ## Default: 10 seconds -bridge.name.keepalive = 10s +bridge.edge.keepalive = 10s -## Subscriptions of the bridge. +## 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.name.subscription.1.topic = topic1/ -bridge.name.subscription.1.qos = 2 -## bridge.name.subscription.2.topic = topic2/ -## bridge.name.subscription.2.qos = 2 +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 diff --git a/priv/emqx.schema b/priv/emqx.schema index c502a8d24..1626b645f 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1395,8 +1395,11 @@ end}. %%-------------------------------------------------------------------- {mapping, "bridge.$name.type", "emqx.bridges", [ - {default, local}, - {datatype, {enum, [local,remote]}} + {datatype, {enum, [in, out]}} +]}. + +{mapping, "bridge.$name.store_type", "emqx.bridges", [ + {datatype, {enum, [memory, disk]}} ]}. {mapping, "bridge.$name.address", "emqx.bridges", [ @@ -1428,6 +1431,10 @@ end}. {datatype, string} ]}. +{mapping, "bridge.$name.forward_rule", "emqx.bridges", [ + {datatype, string} +]}. + {mapping, "bridge.$name.cacertfile", "emqx.bridges", [ {datatype, string} ]}. @@ -1466,6 +1473,22 @@ end}. {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, 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 9d332a5f2..7015590d8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -69,9 +69,9 @@ subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) -> -spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok). subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, []); + subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) -> - subscribe(Topic, SubPid, SubId, []); + subscribe(Topic, SubPid, SubId, #{}); subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) -> subscribe(Topic, SubPid, undefined, SubOpts); subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) -> diff --git a/src/emqx_message.erl b/src/emqx_message.erl index ae8670942..da762703e 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -69,7 +69,9 @@ unset_flag(Flag, Msg = #message{flags = Flags}) -> 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)}. + Msg#message{headers = maps:merge(Old, New)}; +set_headers(_, Msg) -> + Msg. get_header(Hdr, Msg) -> get_header(Hdr, Msg, undefined). diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index 563244232..cddfea8b5 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -63,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 @@ -78,6 +79,7 @@ init([]) -> RouterSup, BrokerSup, BridgeSup, + BridgeSup1, AccessControl, SMSup, SessionSup, diff --git a/src/emqx_time.erl b/src/emqx_time.erl index 97ea4b573..0d74168c4 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -14,7 +14,7 @@ -module(emqx_time). --export([seed/0, now_secs/0, now_ms/0]). +-export([seed/0, now_secs/0, now_ms/0, now_ms/1]). seed() -> rand:seed(exsplus, erlang:timestamp()). @@ -25,3 +25,5 @@ now_secs() -> now_ms() -> erlang:system_time(millisecond). +now_ms({MegaSecs, Secs, MicroSecs}) -> + (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). \ No newline at end of file From d4176461ff6ae736e08a2efd0c466ef300d8d34c Mon Sep 17 00:00:00 2001 From: Petr Gotthard Date: Mon, 20 Aug 2018 11:58:19 +0200 Subject: [PATCH 33/33] Send client_pid to distinguish multiple clients When a controlling process starts multiple clients that make multiple subscriptions it may be desirable to identify from which client a message is comming from. The topic id may not be sufficient. --- src/emqx_client.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 695b9d10c..27f8be353 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -989,7 +989,8 @@ 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}}, + topic => Topic, properties => Props, payload => Payload, + client_pid => self()}}, State. packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,