Merge branch 'emqx30'

This commit is contained in:
Feng Lee 2018-12-26 13:14:59 +08:00
commit ff9fccdb07
246 changed files with 23857 additions and 20482 deletions

27
.editorconfig Normal file
View File

@ -0,0 +1,27 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{erl, src, hrl}]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Matches the exact files either package.json or .travis.yml
[{.travis.yml}]
indent_style = space
indent_size = 2

13
.gitignore vendored
View File

@ -17,17 +17,24 @@ log/
*.so
.erlang.mk/
cover/
emqttd.d
emqx.d
eunit.coverdata
test/ct.cover.spec
logs
ct.coverdata
.idea/
emqttd.iml
emqx.iml
_rel/
data/
_build
.rebar3
rebar3.crashdump
.DS_Store
rebar.config
emqx.iml
bbmustache/
etc/gen.emqx.conf
compile_commands.json
cuttlefish
rebar.lock
xrefr
erlang.mk

View File

@ -1,9 +1,20 @@
language: erlang
otp_release:
- 20.0
- 21.0.4
before_install:
- git clone https://github.com/erlang/rebar3.git; cd rebar3; ./bootstrap; sudo mv rebar3 /usr/local/bin/; cd ..
script:
- make
- make dep-vsn-check
- make rebar-compile
- make rebar-xref
- make rebar-eunit
- make rebar-ct
- make rebar-cover
after_success:
- make coveralls
sudo: false

View File

@ -1,26 +0,0 @@
* [@callbay](https://github.com/callbay)
* [@lsxredrain](https://github.com/lsxredrain)
* [@hejin1026](https://github.com/hejin1026)
* [@desoulter](https://github.com/desoulter)
* [@turtleDeng](https://github.com/turtleDeng)
* [@Hades32](https://github.com/Hades32)
* [@huangdan](https://github.com/huangdan)
* [@phanimahesh](https://github.com/phanimahesh)
* [@dvliman](https://github.com/dvliman)
* [@vowstar](https://github.com/vowstar)
* [@TheWaWaR](https://github.com/TheWaWaR)
* [@hejin1026](https://github.com/hejin1026)
* [@farhadi](https://github.com/farhadi)

View File

@ -1,455 +0,0 @@
MOZILLA PUBLIC LICENSE
Version 1.1
---------------
1. Definitions.
1.0.1. "Commercial Use" means distribution or otherwise making the
Covered Code available to a third party.
1.1. "Contributor" means each entity that creates or contributes to
the creation of Modifications.
1.2. "Contributor Version" means the combination of the Original
Code, prior Modifications used by a Contributor, and the Modifications
made by that particular Contributor.
1.3. "Covered Code" means the Original Code or Modifications or the
combination of the Original Code and Modifications, in each case
including portions thereof.
1.4. "Electronic Distribution Mechanism" means a mechanism generally
accepted in the software development community for the electronic
transfer of data.
1.5. "Executable" means Covered Code in any form other than Source
Code.
1.6. "Initial Developer" means the individual or entity identified
as the Initial Developer in the Source Code notice required by Exhibit
A.
1.7. "Larger Work" means a work which combines Covered Code or
portions thereof with code not governed by the terms of this License.
1.8. "License" means this document.
1.8.1. "Licensable" means having the right to grant, to the maximum
extent possible, whether at the time of the initial grant or
subsequently acquired, any and all of the rights conveyed herein.
1.9. "Modifications" means any addition to or deletion from the
substance or structure of either the Original Code or any previous
Modifications. When Covered Code is released as a series of files, a
Modification is:
A. Any addition to or deletion from the contents of a file
containing Original Code or previous Modifications.
B. Any new file that contains any part of the Original Code or
previous Modifications.
1.10. "Original Code" means Source Code of computer software code
which is described in the Source Code notice required by Exhibit A as
Original Code, and which, at the time of its release under this
License is not already Covered Code governed by this License.
1.10.1. "Patent Claims" means any patent claim(s), now owned or
hereafter acquired, including without limitation, method, process,
and apparatus claims, in any patent Licensable by grantor.
1.11. "Source Code" means the preferred form of the Covered Code for
making modifications to it, including all modules it contains, plus
any associated interface definition files, scripts used to control
compilation and installation of an Executable, or source code
differential comparisons against either the Original Code or another
well known, available Covered Code of the Contributor's choice. The
Source Code can be in a compressed or archival form, provided the
appropriate decompression or de-archiving software is widely available
for no charge.
1.12. "You" (or "Your") means an individual or a legal entity
exercising rights under, and complying with all of the terms of, this
License or a future version of this License issued under Section 6.1.
For legal entities, "You" includes any entity which controls, is
controlled by, or is under common control with You. For purposes of
this definition, "control" means (a) the power, direct or indirect,
to cause the direction or management of such entity, whether by
contract or otherwise, or (b) ownership of more than fifty percent
(50%) of the outstanding shares or beneficial ownership of such
entity.
2. Source Code License.
2.1. The Initial Developer Grant.
The Initial Developer hereby grants You a world-wide, royalty-free,
non-exclusive license, subject to third party intellectual property
claims:
(a) under intellectual property rights (other than patent or
trademark) Licensable by Initial Developer to use, reproduce,
modify, display, perform, sublicense and distribute the Original
Code (or portions thereof) with or without Modifications, and/or
as part of a Larger Work; and
(b) under Patents Claims infringed by the making, using or
selling of Original Code, to make, have made, use, practice,
sell, and offer for sale, and/or otherwise dispose of the
Original Code (or portions thereof).
(c) the licenses granted in this Section 2.1(a) and (b) are
effective on the date Initial Developer first distributes
Original Code under the terms of this License.
(d) Notwithstanding Section 2.1(b) above, no patent license is
granted: 1) for code that You delete from the Original Code; 2)
separate from the Original Code; or 3) for infringements caused
by: i) the modification of the Original Code or ii) the
combination of the Original Code with other software or devices.
2.2. Contributor Grant.
Subject to third party intellectual property claims, each Contributor
hereby grants You a world-wide, royalty-free, non-exclusive license
(a) under intellectual property rights (other than patent or
trademark) Licensable by Contributor, to use, reproduce, modify,
display, perform, sublicense and distribute the Modifications
created by such Contributor (or portions thereof) either on an
unmodified basis, with other Modifications, as Covered Code
and/or as part of a Larger Work; and
(b) under Patent Claims infringed by the making, using, or
selling of Modifications made by that Contributor either alone
and/or in combination with its Contributor Version (or portions
of such combination), to make, use, sell, offer for sale, have
made, and/or otherwise dispose of: 1) Modifications made by that
Contributor (or portions thereof); and 2) the combination of
Modifications made by that Contributor with its Contributor
Version (or portions of such combination).
(c) the licenses granted in Sections 2.2(a) and 2.2(b) are
effective on the date Contributor first makes Commercial Use of
the Covered Code.
(d) Notwithstanding Section 2.2(b) above, no patent license is
granted: 1) for any code that Contributor has deleted from the
Contributor Version; 2) separate from the Contributor Version;
3) for infringements caused by: i) third party modifications of
Contributor Version or ii) the combination of Modifications made
by that Contributor with other software (except as part of the
Contributor Version) or other devices; or 4) under Patent Claims
infringed by Covered Code in the absence of Modifications made by
that Contributor.
3. Distribution Obligations.
3.1. Application of License.
The Modifications which You create or to which You contribute are
governed by the terms of this License, including without limitation
Section 2.2. The Source Code version of Covered Code may be
distributed only under the terms of this License or a future version
of this License released under Section 6.1, and You must include a
copy of this License with every copy of the Source Code You
distribute. You may not offer or impose any terms on any Source Code
version that alters or restricts the applicable version of this
License or the recipients' rights hereunder. However, You may include
an additional document offering the additional rights described in
Section 3.5.
3.2. Availability of Source Code.
Any Modification which You create or to which You contribute must be
made available in Source Code form under the terms of this License
either on the same media as an Executable version or via an accepted
Electronic Distribution Mechanism to anyone to whom you made an
Executable version available; and if made available via Electronic
Distribution Mechanism, must remain available for at least twelve (12)
months after the date it initially became available, or at least six
(6) months after a subsequent version of that particular Modification
has been made available to such recipients. You are responsible for
ensuring that the Source Code version remains available even if the
Electronic Distribution Mechanism is maintained by a third party.
3.3. Description of Modifications.
You must cause all Covered Code to which You contribute to contain a
file documenting the changes You made to create that Covered Code and
the date of any change. You must include a prominent statement that
the Modification is derived, directly or indirectly, from Original
Code provided by the Initial Developer and including the name of the
Initial Developer in (a) the Source Code, and (b) in any notice in an
Executable version or related documentation in which You describe the
origin or ownership of the Covered Code.
3.4. Intellectual Property Matters
(a) Third Party Claims.
If Contributor has knowledge that a license under a third party's
intellectual property rights is required to exercise the rights
granted by such Contributor under Sections 2.1 or 2.2,
Contributor must include a text file with the Source Code
distribution titled "LEGAL" which describes the claim and the
party making the claim in sufficient detail that a recipient will
know whom to contact. If Contributor obtains such knowledge after
the Modification is made available as described in Section 3.2,
Contributor shall promptly modify the LEGAL file in all copies
Contributor makes available thereafter and shall take other steps
(such as notifying appropriate mailing lists or newsgroups)
reasonably calculated to inform those who received the Covered
Code that new knowledge has been obtained.
(b) Contributor APIs.
If Contributor's Modifications include an application programming
interface and Contributor has knowledge of patent licenses which
are reasonably necessary to implement that API, Contributor must
also include this information in the LEGAL file.
(c) Representations.
Contributor represents that, except as disclosed pursuant to
Section 3.4(a) above, Contributor believes that Contributor's
Modifications are Contributor's original creation(s) and/or
Contributor has sufficient rights to grant the rights conveyed by
this License.
3.5. Required Notices.
You must duplicate the notice in Exhibit A in each file of the Source
Code. If it is not possible to put such notice in a particular Source
Code file due to its structure, then You must include such notice in a
location (such as a relevant directory) where a user would be likely
to look for such a notice. If You created one or more Modification(s)
You may add your name as a Contributor to the notice described in
Exhibit A. You must also duplicate this License in any documentation
for the Source Code where You describe recipients' rights or ownership
rights relating to Covered Code. You may choose to offer, and to
charge a fee for, warranty, support, indemnity or liability
obligations to one or more recipients of Covered Code. However, You
may do so only on Your own behalf, and not on behalf of the Initial
Developer or any Contributor. You must make it absolutely clear than
any such warranty, support, indemnity or liability obligation is
offered by You alone, and You hereby agree to indemnify the Initial
Developer and every Contributor for any liability incurred by the
Initial Developer or such Contributor as a result of warranty,
support, indemnity or liability terms You offer.
3.6. Distribution of Executable Versions.
You may distribute Covered Code in Executable form only if the
requirements of Section 3.1-3.5 have been met for that Covered Code,
and if You include a notice stating that the Source Code version of
the Covered Code is available under the terms of this License,
including a description of how and where You have fulfilled the
obligations of Section 3.2. The notice must be conspicuously included
in any notice in an Executable version, related documentation or
collateral in which You describe recipients' rights relating to the
Covered Code. You may distribute the Executable version of Covered
Code or ownership rights under a license of Your choice, which may
contain terms different from this License, provided that You are in
compliance with the terms of this License and that the license for the
Executable version does not attempt to limit or alter the recipient's
rights in the Source Code version from the rights set forth in this
License. If You distribute the Executable version under a different
license You must make it absolutely clear that any terms which differ
from this License are offered by You alone, not by the Initial
Developer or any Contributor. You hereby agree to indemnify the
Initial Developer and every Contributor for any liability incurred by
the Initial Developer or such Contributor as a result of any such
terms You offer.
3.7. Larger Works.
You may create a Larger Work by combining Covered Code with other code
not governed by the terms of this License and distribute the Larger
Work as a single product. In such a case, You must make sure the
requirements of this License are fulfilled for the Covered Code.
4. Inability to Comply Due to Statute or Regulation.
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Code due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description
must be included in the LEGAL file described in Section 3.4 and must
be included with all distributions of the Source Code. Except to the
extent prohibited by statute or regulation, such description must be
sufficiently detailed for a recipient of ordinary skill to be able to
understand it.
5. Application of this License.
This License applies to code to which the Initial Developer has
attached the notice in Exhibit A and to related Covered Code.
6. Versions of the License.
6.1. New Versions.
Netscape Communications Corporation ("Netscape") may publish revised
and/or new versions of the License from time to time. Each version
will be given a distinguishing version number.
6.2. Effect of New Versions.
Once Covered Code has been published under a particular version of the
License, You may always continue to use it under the terms of that
version. You may also choose to use such Covered Code under the terms
of any subsequent version of the License published by Netscape. No one
other than Netscape has the right to modify the terms applicable to
Covered Code created under this License.
6.3. Derivative Works.
If You create or use a modified version of this License (which you may
only do in order to apply it to code which is not already Covered Code
governed by this License), You must (a) rename Your license so that
the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
"MPL", "NPL" or any confusingly similar phrase do not appear in your
license (except to note that your license differs from this License)
and (b) otherwise make it clear that Your version of the license
contains terms which differ from the Mozilla Public License and
Netscape Public License. (Filling in the name of the Initial
Developer, Original Code or Contributor in the notice described in
Exhibit A shall not of themselves be deemed to be modifications of
this License.)
7. DISCLAIMER OF WARRANTY.
COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
8. TERMINATION.
8.1. This License and the rights granted hereunder will terminate
automatically if You fail to comply with terms herein and fail to cure
such breach within 30 days of becoming aware of the breach. All
sublicenses to the Covered Code which are properly granted shall
survive any termination of this License. Provisions which, by their
nature, must remain in effect beyond the termination of this License
shall survive.
8.2. If You initiate litigation by asserting a patent infringement
claim (excluding declatory judgment actions) against Initial Developer
or a Contributor (the Initial Developer or Contributor against whom
You file such action is referred to as "Participant") alleging that:
(a) such Participant's Contributor Version directly or indirectly
infringes any patent, then any and all rights granted by such
Participant to You under Sections 2.1 and/or 2.2 of this License
shall, upon 60 days notice from Participant terminate prospectively,
unless if within 60 days after receipt of notice You either: (i)
agree in writing to pay Participant a mutually agreeable reasonable
royalty for Your past and future use of Modifications made by such
Participant, or (ii) withdraw Your litigation claim with respect to
the Contributor Version against such Participant. If within 60 days
of notice, a reasonable royalty and payment arrangement are not
mutually agreed upon in writing by the parties or the litigation claim
is not withdrawn, the rights granted by Participant to You under
Sections 2.1 and/or 2.2 automatically terminate at the expiration of
the 60 day notice period specified above.
(b) any software, hardware, or device, other than such Participant's
Contributor Version, directly or indirectly infringes any patent, then
any rights granted to You by such Participant under Sections 2.1(b)
and 2.2(b) are revoked effective as of the date You first made, used,
sold, distributed, or had made, Modifications made by that
Participant.
8.3. If You assert a patent infringement claim against Participant
alleging that such Participant's Contributor Version directly or
indirectly infringes any patent where such claim is resolved (such as
by license or settlement) prior to the initiation of patent
infringement litigation, then the reasonable value of the licenses
granted by such Participant under Sections 2.1 or 2.2 shall be taken
into account in determining the amount or value of any payment or
license.
8.4. In the event of termination under Sections 8.1 or 8.2 above,
all end user license agreements (excluding distributors and resellers)
which have been validly granted by You or any distributor hereunder
prior to termination shall survive termination.
9. LIMITATION OF LIABILITY.
UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
(INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
10. U.S. GOVERNMENT END USERS.
The Covered Code is a "commercial item," as that term is defined in
48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
software" and "commercial computer software documentation," as such
terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
all U.S. Government End Users acquire Covered Code with only those
rights set forth herein.
11. MISCELLANEOUS.
This License represents the complete agreement concerning subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. This License shall be governed by
California law provisions (except to the extent applicable law, if
any, provides otherwise), excluding its conflict-of-law provisions.
With respect to disputes in which at least one party is a citizen of,
or an entity chartered or registered to do business in the United
States of America, any litigation relating to this License shall be
subject to the jurisdiction of the Federal Courts of the Northern
District of California, with venue lying in Santa Clara County,
California, with the losing party responsible for costs, including
without limitation, court costs and reasonable attorneys' fees and
expenses. The application of the United Nations Convention on
Contracts for the International Sale of Goods is expressly excluded.
Any law or regulation which provides that the language of a contract
shall be construed against the drafter shall not apply to this
License.
12. RESPONSIBILITY FOR CLAIMS.
As between Initial Developer and the Contributors, each party is
responsible for claims and damages arising, directly or indirectly,
out of its utilization of rights under this License and You agree to
work with Initial Developer and Contributors to distribute such
responsibility on an equitable basis. Nothing herein is intended or
shall be deemed to constitute any admission of liability.
13. MULTIPLE-LICENSED CODE.
Initial Developer may designate portions of the Covered Code as
"Multiple-Licensed". "Multiple-Licensed" means that the Initial
Developer permits you to utilize portions of the Covered Code under
Your choice of the NPL or the alternative licenses, if any, specified
by the Initial Developer in the file described in Exhibit A.
EXHIBIT A -Mozilla Public License.
``The contents of this file are subject to the Mozilla Public License
Version 1.1 (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.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS"
basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
License for the specific language governing rights and limitations
under the License.
The Original Code is RabbitMQ.
The Initial Developer of the Original Code is Pivotal Software, Inc.
Copyright (c) 2007-2016 Pivotal Software, Inc. All rights reserved.''
[NOTE: The text of this Exhibit A may differ slightly from the text of
the notices in the Source Code files of the Original Code. You should
use the text of this Exhibit A rather than the text found in the
Original Code Source Code for Your Modifications.]

149
Makefile
View File

@ -1,57 +1,136 @@
PROJECT = emqttd
PROJECT_DESCRIPTION = Erlang MQTT Broker
PROJECT_VERSION = 2.3.11
.PHONY: plugins tests
DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx
PROJECT = emqx
PROJECT_DESCRIPTION = EMQ X Broker
dep_goldrush = git https://github.com/basho/goldrush 0.1.9
dep_gproc = git https://github.com/uwiger/gproc 0.8.0
dep_getopt = git https://github.com/jcomellas/getopt v0.8.2
dep_lager = git https://github.com/basho/lager 3.2.4
dep_esockd = git https://github.com/emqtt/esockd v5.2.2
dep_ekka = git https://github.com/emqtt/ekka v0.2.3
dep_mochiweb = git https://github.com/emqtt/mochiweb v4.2.2
dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1
dep_lager_syslog = git https://github.com/basho/lager_syslog 3.0.1
dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master
dep_clique = git https://github.com/emqtt/clique v0.3.10
dep_jsx = git https://github.com/talentdeficit/jsx v2.8.3
DEPS = jsx gproc gen_rpc ekka esockd cowboy
ERLC_OPTS += +debug_info
ERLC_OPTS += +'{parse_transform, lager_transform}'
dep_jsx = hex-emqx 2.9.0
dep_gproc = hex-emqx 0.8.0
dep_gen_rpc = git-emqx https://github.com/emqx/gen_rpc 2.3.0
dep_esockd = git-emqx https://github.com/emqx/esockd v5.4.3
dep_ekka = git-emqx https://github.com/emqx/ekka v0.5.1
dep_cowboy = hex-emqx 2.4.0
NO_AUTOPATCH = cuttlefish
ERLC_OPTS += +debug_info -DAPPLICATION=emqx
BUILD_DEPS = cuttlefish
dep_cuttlefish = git https://github.com/emqtt/cuttlefish v2.0.11
dep_cuttlefish = git-emqx https://github.com/emqx/cuttlefish v2.2.0
TEST_DEPS = emqttc emq_dashboard
dep_emqttc = git https://github.com/emqtt/emqttc
dep_emq_dashboard = git https://github.com/emqtt/emq_dashboard develop
#TEST_DEPS = emqx_ct_helplers
#dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers
TEST_ERLC_OPTS += +debug_info
TEST_ERLC_OPTS += +'{parse_transform, lager_transform}'
TEST_ERLC_OPTS += +debug_info -DAPPLICATION=emqx
EUNIT_OPTS = verbose
# EUNIT_ERL_OPTS =
CT_SUITES = emqttd emqttd_access emqttd_lib emqttd_inflight emqttd_mod \
emqttd_net emqttd_mqueue emqttd_protocol emqttd_topic \
emqttd_router emqttd_trie emqttd_vm emqttd_config
# CT_SUITES = emqx_frame
## emqx_trie emqx_router emqx_frame emqx_mqtt_compat
CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqttd_ct@127.0.0.1
CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \
emqx_access emqx_broker emqx_cm emqx_frame emqx_guid emqx_inflight emqx_json \
emqx_keepalive emqx_lib emqx_metrics emqx_mod emqx_mod_sup emqx_mqtt_caps \
emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \
emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \
emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge \
emqx_hooks emqx_batch emqx_sequence emqx_pmon emqx_pd emqx_gc
CT_NODE_NAME = emqxct@127.0.0.1
CT_OPTS = -cover test/ct.cover.spec -erl_args -name $(CT_NODE_NAME)
COVER = true
PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets public_key ssl lager compiler mnesia
PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets public_key ssl 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
$(shell [ -f erlang.mk ] || curl -s -o erlang.mk https://raw.githubusercontent.com/emqx/erlmk/master/erlang.mk)
include erlang.mk
app:: rebar.config
clean:: gen-clean
app.config::
./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/emq.conf -i priv/emq.schema -d data/
.PHONY: gen-clean
gen-clean:
@rm -rf bbmustache
@rm -f etc/gen.emqx.conf
bbmustache:
$(verbose) git clone https://github.com/soranoba/bbmustache.git && cd bbmustache && ./rebar3 compile && cd ..
# This hack is to generate a conf file for testing
# relx overlay is used for release
etc/gen.emqx.conf: bbmustache etc/emqx.conf
$(verbose) erl -noshell -pa bbmustache/_build/default/lib/bbmustache/ebin -eval \
"{ok, Temp} = file:read_file('etc/emqx.conf'), \
{ok, Vars0} = file:consult('vars'), \
Vars = [{atom_to_list(N), list_to_binary(V)} || {N, V} <- Vars0], \
Targ = bbmustache:render(Temp, Vars), \
ok = file:write_file('etc/gen.emqx.conf', Targ), \
halt(0)."
CUTTLEFISH_SCRIPT = _build/default/lib/cuttlefish/cuttlefish
app.config: $(CUTTLEFISH_SCRIPT) etc/gen.emqx.conf
$(verbose) $(CUTTLEFISH_SCRIPT) -l info -e etc/ -c etc/gen.emqx.conf -i priv/emqx.schema -d data/
ct: app.config
rebar-cover:
@rebar3 cover
coveralls:
@rebar3 coveralls send
$(CUTTLEFISH_SCRIPT): rebar-deps
@if [ ! -f cuttlefish ]; then make -C _build/default/lib/cuttlefish; fi
rebar-xref:
@rebar3 xref
rebar-deps:
@rebar3 get-deps
rebar-eunit: $(CUTTLEFISH_SCRIPT)
@rebar3 eunit
rebar-compile:
@rebar3 compile
rebar-ct: app.config
@rebar3 as test compile
@ln -s -f '../../../../etc' _build/test/lib/emqx/
@ln -s -f '../../../../data' _build/test/lib/emqx/
@rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(shell echo $(foreach var,$(CT_SUITES),test/$(var)_SUITE) | tr ' ' ',')
rebar-clean:
@rebar3 clean
distclean::
@rm -rf _build cover deps logs log data
@rm -f rebar.lock compile_commands.json cuttlefish
## Below are for version consistency check during erlang.mk and rebar3 dual mode support
none=
space = $(none) $(none)
comma = ,
quote = \"
curly_l = "{"
curly_r = "}"
dep-versions = [$(foreach dep,$(DEPS) $(BUILD_DEPS),$(curly_l)$(dep),$(quote)$(word $(words $(dep_$(dep))),$(dep_$(dep)))$(quote)$(curly_r)$(comma))[]]
.PHONY: dep-vsn-check
dep-vsn-check:
$(verbose) erl -noshell -eval \
"MkVsns = lists:sort(lists:flatten($(dep-versions))), \
{ok, Conf} = file:consult('rebar.config'), \
{_, Deps1} = lists:keyfind(deps, 1, Conf), \
{_, Deps2} = lists:keyfind(github_emqx_deps, 1, Conf), \
F = fun({N, V}) when is_list(V) -> {N, V}; ({N, {git, _, {branch, V}}}) -> {N, V} end, \
RebarVsns = lists:sort(lists:map(F, Deps1 ++ Deps2)), \
case {RebarVsns -- MkVsns, MkVsns -- RebarVsns} of \
{[], []} -> halt(0); \
{Rebar, Mk} -> erlang:error({deps_version_discrepancy, [{rebar, Rebar}, {mk, Mk}]}) \
end."

146
README.md
View File

@ -1,121 +1,85 @@
# EMQ X Broker
# *EMQ* - Erlang MQTT Broker
*EMQ X* broker is a fully open source, highly scalable, highly available distributed MQTT messaging broker for IoT, M2M and Mobile applications that can handle tens of millions of concurrent clients.
[![Build Status](https://travis-ci.org/emqtt/emqttd.svg?branch=master)](https://travis-ci.org/emqtt/emqttd)
Starting from 3.0 release, *EMQ X* broker fully supports MQTT V5.0 protocol specifications and backward compatible with MQTT V3.1 and V3.1.1, as well as other communication protocols such as MQTT-SN, CoAP, LwM2M, WebSocket and STOMP. The 3.0 release of the *EMQ X* broker can scaled to 10+ million concurrent MQTT connections on one cluster.
*EMQ* (Erlang MQTT Broker) is a distributed, massively scalable, highly extensible MQTT message broker written in Erlang/OTP.
*EMQ* is fully open source and licensed under the Apache Version 2.0. *EMQ* implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports MQTT-SN, CoAP, WebSocket, STOMP and SockJS at the same time.
- For full list of new features, please read *EMQ X* broker 3.0 [release notes](https://github.com/emqx/emqx/releases/).
- For more information, please visit [EMQ X homepage](http://emqtt.io).
*EMQ* provides a scalable, reliable, enterprise-grade MQTT message Hub for IoT, M2M, Smart Hardware and Mobile Messaging Applications.
The 1.0 release of the EMQ broker has scaled to 1.3 million concurrent MQTT connections on a 12 Core, 32G CentOS server.
Please visit [emqtt.io](http://emqtt.io) for more service. Follow us on Twitter: [@emqtt](https://twitter.com/emqtt)
## Features
* Full MQTT V3.1/V3.1.1 support
* QoS0, QoS1, QoS2 Publish/Subscribe
* Session Management and Offline Messages
* Retained Message
* Last Will Message
* TCP/SSL Connection
* MQTT Over WebSocket(SSL)
* HTTP Publish API
* MQTT-SN Protocol
* STOMP protocol
* STOMP over SockJS
* $SYS/# Topics
* ClientID Authentication
* IpAddress Authentication
* Username and Password Authentication
* Access control based on IpAddress, ClientID, Username
* JWT Authentication
* LDAP Authentication/ACL
* HTTP Authentication/ACL
* MySQL Authentication/ACL
* Redis Authentication/ACL
* PostgreSQL Authentication/ACL
* MongoDB Authentication/ACL
* Cluster brokers on several nodes
* Bridge brokers locally or remotely
* mosquitto, RSMB bridge
* Extensible architecture with Hooks and Plugins
* Passed eclipse paho interoperability tests
* Local Subscription
* Shared Subscription
* Proxy Protocol V1/2
* Lua Hook and Web Hook
* LWM2M Prototol Support
## Installation
The *EMQ* broker is cross-platform, which can be deployed on Linux, Unix, Mac, Windows and even Raspberry Pi.
The *EMQ X* broker is cross-platform, which can be deployed on Linux, Unix, Mac, Windows and even Raspberry Pi.
Download the binary package for your platform from http://emqtt.io/downloads.
Download the binary package for your platform from [here](http://emqtt.io/downloads).
- [Single Node Install](http://emqtt.io/docs/v3/install.html)
- [Multi Node Install](http://emqtt.io/docs/v3/cluster.html)
Documentation on [emqtt.io/docs/v2/](http://emqtt.io/docs/v2/install.html), [docs.emqtt.com](http://docs.emqtt.com/en/latest/install.html) for installation and configuration guide.
## Build From Source
The *EMQ* broker requires Erlang/OTP R19+ to build since 2.1 release.
The *EMQ X* broker requires Erlang/OTP R21+ to build since 3.0 release.
```
git clone https://github.com/emqtt/emq-relx.git
git clone https://github.com/emqx/emqx-rel.git
cd emq-relx && make
cd emqx-rel && make
cd _rel/emqx && ./bin/emqx console
cd _rel/emqttd && ./bin/emqttd console
```
## Plugins
## Quick Start
The *EMQ* broker is highly extensible, with many hooks and plugins for customizing the authentication/ACL and integrating with other systems:
# Start emqx
./bin/emqx start
Plugin | Description
-----------------------------------------------------------------------|--------------------------------------
[emq_plugin_template](https://github.com/emqtt/emq_plugin_template) | Plugin template and demo
[emq_dashboard](https://github.com/emqtt/emq_dashboard) | Web Dashboard
[emq_retainer](https://github.com/emqtt/emq-retainer) | Store MQTT Retained Messages
[emq_modules](https://github.com/emqtt/emq-modules) | Presence, Subscription and Rewrite Modules
[emq_auth_username](https://github.com/emqtt/emq_auth_username) | Username/Password Authentication Plugin
[emq_auth_clientid](https://github.com/emqtt/emq_auth_clientid) | ClientId Authentication Plugin
[emq_auth_mysql](https://github.com/emqtt/emq_auth_mysql) | MySQL Authentication/ACL Plugin
[emq_auth_pgsql](https://github.com/emqtt/emq_auth_pgsql) | PostgreSQL Authentication/ACL Plugin
[emq_auth_redis](https://github.com/emqtt/emq_auth_redis) | Redis Authentication/ACL Plugin
[emq_auth_mongo](https://github.com/emqtt/emq_auth_mongo) | MongoDB Authentication/ACL Plugin
[emq_auth_http](https://github.com/emqtt/emq_auth_http) | Authentication/ACL by HTTP API
[emq_auth_ldap](https://github.com/emqtt/emq_auth_ldap) | LDAP Authentication Plugin
[emq_auth_jwt](https://github.com/emqtt/emq-auth-jwt) | JWT Authentication Plugin
[emq_web_hook](https://github.com/emqtt/emq-web-hook) | Web Hook Plugin
[emq_lua_hook](https://github.com/emqtt/emq-lua-hook) | Lua Hook Plugin
[emq_sn](https://github.com/emqtt/emq_sn) | MQTT-SN Protocol Plugin
[emq_coap](https://github.com/emqtt/emq_coap) | CoAP Protocol Plugin
[emq_stomp](https://github.com/emqtt/emq_stomp) | Stomp Protocol Plugin
[emq_lwm2m](https://github.com/emqx/emqx-lwm2m) | LWM2M Prototol Plugin
[emq_recon](https://github.com/emqtt/emq_recon) | Recon Plugin
[emq_reloader](https://github.com/emqtt/emq_reloader) | Reloader Plugin
[emq_sockjs](https://github.com/emqtt/emq_sockjs) | SockJS(Stomp) Plugin
# Check Status
./bin/emqx_ctl status
## Supports
# Stop emqx
./bin/emqx stop
* Twitter: [@emqtt](https://twitter.com/emqtt)
* Homepage: http://emqtt.io
* Downloads: http://emqtt.io/downloads
* Documentation: http://emqtt.io/docs/v2/
* Forum: https://groups.google.com/d/forum/emqtt
* Mailing List: <emqtt@googlegroups.com>
* Issues: https://github.com/emqtt/emqttd/issues
* QQ Group: 12222225
To view the dashboard after running, use your browser to open: http://localhost:18083
## Test Servers
The **q.emqtt.com** hosts a public Four-Node *EMQ* cluster on [QingCloud](https://qingcloud.com):
## Roadmap
![qing_cluster](http://emqtt.io/static/img/public_cluster.png)
The [EMQ X Roadmap uses Github milestones](https://github.com/emqx/emqx/milestones) to track the progress of the project.
## Community, discussion, contribution, and support
You can reach the EMQ community and developers via the following channels:
- [EMQX Slack](http://emqx.slack.com)
-[#emqx-users](https://emqx.slack.com/messages/CBUF2TTB8/)
-[#emqx-devs](https://emqx.slack.com/messages/CBSL57DUH/)
- [Mailing Lists](<emqtt@googlegroups.com>)
- [Twitter](https://twitter.com/emqtt)
- [Forum](https://groups.google.com/d/forum/emqtt)
- [Blog](https://medium.com/@emqtt)
Please submit any bugs, issues, and feature requests to [emqx/emqx](https://github.com/emqx/emqx/issues).
## MQTT Specifications
You can read the mqtt protocol via the following links:
[MQTT Version 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
[MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html)
[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf)
## License
Apache License Version 2.0
Copyright (c) 2018 [EMQ Technologies Co., Ltd](http://emqtt.io). 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](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.

Binary file not shown.

View File

@ -1,11 +0,0 @@
http://emqttd.io/docs/v2/
or
http://docs.emqtt.com/
or
http://emqttd-docs.rtfd.org

Binary file not shown.

2741
erlang.mk vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
%%--------------------------------------------------------------------
%%
%% [ACL](https://github.com/emqtt/emqttd/wiki/ACL)
%% [ACL](http://emqtt.io/docs/v2/config.html#allow-anonymous-and-acl-file)
%%
%% -type who() :: all | binary() |
%% {ipaddr, esockd_access:cidr()} |
@ -24,3 +24,4 @@
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
{allow, all}.

14
etc/acl.conf.paho Normal file
View File

@ -0,0 +1,14 @@
%%--------------------------------------------------------------------
%% For paho interoperability test cases
%%--------------------------------------------------------------------
{deny, {client, "myclientid"}, subscribe, ["test/nosubscribe"]}.
{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}.
{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}.
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
{allow, all}.

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
%% The options in the {server, Opts} tuple are used when calling ssl:ssl_accept/3,
%% and the options in the {client, Opts} tuple are used when calling ssl:connect/4.
%%
%%
%% More information at: http://erlang.org/doc/apps/ssl/ssl_distribution.html
[{server,
[{certfile, "{{ platform_etc_dir }}/certs/cert.pem"},

95
etc/vm.args Normal file
View File

@ -0,0 +1,95 @@
##############################
# Erlang VM Args
##############################
## NOTE:
##
## Arguments configured in this file might be overridden by configs from `emqx.conf`.
##
## Some basic VM arguments are to be configured in `emqx.conf`,
## such as `node.name` for `-name` and `node.cooke` for `-setcookie`.
## Sets the maximum number of simultaneously existing processes for this system.
#+P 2048000
## Sets the maximum number of simultaneously existing ports for this system.
#+Q 1024000
## Sets the maximum number of ETS tables
#+e 256000
## Sets the maximum number of atoms the virtual machine can handle.
#+t 1048576
## Set the location of crash dumps
#-env ERL_CRASH_DUMP {{ platform_log_dir }}/crash.dump
## Set how many times generational garbages collections can be done without
## forcing a fullsweep collection.
#-env ERL_FULLSWEEP_AFTER 1000
## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive
## (Disabled by default..use with caution!)
#-heart
## Specify the erlang distributed protocol.
## Can be one of: inet_tcp, inet6_tcp, inet_tls
#-proto_dist inet_tcp
## Specify SSL Options in the file if using SSL for Erlang Distribution.
## Used only when -proto_dist set to inet_tls
#-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf
## Specifies the net_kernel tick time in seconds.
## This is the approximate time a connected node may be unresponsive until
## it is considered down and thereby disconnected.
#-kernel net_ticktime 60
## Sets the distribution buffer busy limit (dist_buf_busy_limit).
#+zdbbl 8192
## Sets default scheduler hint for port parallelism.
+spp true
## Sets the number of threads in async thread pool. Valid range is 0-1024.
#+A 8
## Sets the default heap size of processes to the size Size.
#+hms 233
## Sets the default binary virtual heap size of processes to the size Size.
#+hmbs 46422
## Sets the number of IO pollsets to use when polling for I/O.
#+IOp 1
## Sets the number of IO poll threads to use when polling for I/O.
#+IOt 1
## Sets the number of scheduler threads to create and scheduler threads to set online.
#+S 8:8
## Sets the number of dirty CPU scheduler threads to create and dirty CPU scheduler threads to set online.
#+SDcpu 8:8
## Sets the number of dirty I/O scheduler threads to create.
#+SDio 10
## Suggested stack size, in kilowords, for scheduler threads.
#+sss 32
## Suggested stack size, in kilowords, for dirty CPU scheduler threads.
#+sssdcpu 40
## Suggested stack size, in kilowords, for dirty IO scheduler threads.
#+sssdio 40
## Sets scheduler bind type.
## Can be one of: u, ns, ts, ps, s, nnts, nnps, tnnps, db
#+sbt db
## Sets a user-defined CPU topology.
#+sct L0-3c0-3p0N0:L4-7c0-3p1N1
## Sets the mapping of warning messages for error_logger
#+W w

95
etc/vm.args.edge Normal file
View File

@ -0,0 +1,95 @@
##############################
# Erlang VM Args
##############################
## NOTE:
##
## Arguments configured in this file might be overridden by configs from `emqx.conf`.
##
## Some basic VM arguments are to be configured in `emqx.conf`,
## such as `node.name` for `-name` and `node.cooke` for `-setcookie`.
## Sets the maximum number of simultaneously existing processes for this system.
+P 20480
## Sets the maximum number of simultaneously existing ports for this system.
+Q 4096
## Sets the maximum number of ETS tables
+e 512
## Sets the maximum number of atoms the virtual machine can handle.
+t 65536
## Set the location of crash dumps
-env ERL_CRASH_DUMP {{ platform_log_dir }}/crash.dump
## Set how many times generational garbages collections can be done without
## forcing a fullsweep collection.
-env ERL_FULLSWEEP_AFTER 0
## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive
## (Disabled by default..use with caution!)
#-heart
## Specify the erlang distributed protocol.
## Can be one of: inet_tcp, inet6_tcp, inet_tls
#-proto_dist inet_tcp
## Specify SSL Options in the file if using SSL for Erlang Distribution.
## Used only when -proto_dist set to inet_tls
#-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf
## Specifies the net_kernel tick time in seconds.
## This is the approximate time a connected node may be unresponsive until
## it is considered down and thereby disconnected.
#-kernel net_ticktime 60
## Sets the distribution buffer busy limit (dist_buf_busy_limit).
+zdbbl 1024
## Sets default scheduler hint for port parallelism.
+spp false
## Sets the number of threads in async thread pool. Valid range is 0-1024.
+A 1
## Sets the default heap size of processes to the size Size.
#+hms 233
## Sets the default binary virtual heap size of processes to the size Size.
#+hmbs 46422
## Sets the number of IO pollsets to use when polling for I/O.
+IOp 1
## Sets the number of IO poll threads to use when polling for I/O.
+IOt 1
## Sets the number of scheduler threads to create and scheduler threads to set online.
+S 1:1
## Sets the number of dirty CPU scheduler threads to create and dirty CPU scheduler threads to set online.
+SDcpu 1:1
## Sets the number of dirty I/O scheduler threads to create.
+SDio 1
## Suggested stack size, in kilowords, for scheduler threads.
#+sss 32
## Suggested stack size, in kilowords, for dirty CPU scheduler threads.
#+sssdcpu 40
## Suggested stack size, in kilowords, for dirty IO scheduler threads.
#+sssdio 40
## Sets scheduler bind type.
## Can be one of: u, ns, ts, ps, s, nnts, nnps, tnnps, db
#+sbt db
## Sets a user-defined CPU topology.
#+sct L0-3c0-3p0N0:L4-7c0-3p1N1
## Sets the mapping of warning messages for error_logger
#+W w

View File

@ -1,196 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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.
%%--------------------------------------------------------------------
%%--------------------------------------------------------------------
%% Banner
%%--------------------------------------------------------------------
-define(COPYRIGHT, "Copyright (c) 2013-2018 EMQ Enterprise, Inc.").
-define(LICENSE_MESSAGE, "Licensed under the Apache License, Version 2.0").
-define(PROTOCOL_VERSION, "MQTT/5.0").
-define(ERTS_MINIMUM, "8.0").
%%--------------------------------------------------------------------
%% Sys/Queue/Share Topics' Prefix
%%--------------------------------------------------------------------
-define(SYSTOP, <<"$SYS/">>). %% System Topic
-define(QUEUE, <<"$queue/">>). %% Queue Topic
-define(SHARE, <<"$share/">>). %% Shared Topic
%%--------------------------------------------------------------------
%% PubSub
%%--------------------------------------------------------------------
-type(pubsub() :: publish | subscribe).
-define(PS(PS), (PS =:= publish orelse PS =:= subscribe)).
%%--------------------------------------------------------------------
%% MQTT Topic
%%--------------------------------------------------------------------
-record(mqtt_topic,
{ topic :: binary(),
flags = [] :: [retained | static]
}).
-type(mqtt_topic() :: #mqtt_topic{}).
%%--------------------------------------------------------------------
%% MQTT Subscription
%%--------------------------------------------------------------------
-record(mqtt_subscription,
{ subid :: binary() | atom(),
topic :: binary(),
qos :: 0 | 1 | 2
}).
-type(mqtt_subscription() :: #mqtt_subscription{}).
%%--------------------------------------------------------------------
%% MQTT Client
%%--------------------------------------------------------------------
-type(ws_header_key() :: atom() | binary() | string()).
-type(ws_header_val() :: atom() | binary() | string() | integer()).
-record(mqtt_client,
{ client_id :: binary() | undefined,
client_pid :: pid(),
username :: binary() | undefined,
peername :: {inet:ip_address(), inet:port_number()},
clean_sess :: boolean(),
proto_ver :: 3 | 4,
keepalive = 0,
will_topic :: undefined | binary(),
ws_initial_headers :: list({ws_header_key(), ws_header_val()}),
mountpoint :: undefined | binary(),
connected_at :: erlang:timestamp()
}).
-type(mqtt_client() :: #mqtt_client{}).
%%--------------------------------------------------------------------
%% MQTT Session
%%--------------------------------------------------------------------
-record(mqtt_session,
{ client_id :: binary(),
sess_pid :: pid(),
clean_sess :: boolean()
}).
-type(mqtt_session() :: #mqtt_session{}).
%%--------------------------------------------------------------------
%% MQTT Message
%%--------------------------------------------------------------------
-type(mqtt_msg_id() :: binary() | undefined).
-type(mqtt_pktid() :: 1..16#ffff | undefined).
-type(mqtt_msg_from() :: atom() | {binary(), undefined | binary()}).
-record(mqtt_message,
{ %% Global unique message ID
id :: mqtt_msg_id(),
%% PacketId
pktid :: mqtt_pktid(),
%% ClientId and Username
from :: mqtt_msg_from(),
%% Topic that the message is published to
topic :: binary(),
%% Message QoS
qos = 0 :: 0 | 1 | 2,
%% Message Flags
flags = [] :: [retain | dup | sys],
%% Retain flag
retain = false :: boolean(),
%% Dup flag
dup = false :: boolean(),
%% $SYS flag
sys = false :: boolean(),
%% Headers
headers = [] :: list(),
%% Payload
payload :: binary(),
%% Timestamp
timestamp :: erlang:timestamp()
}).
-type(mqtt_message() :: #mqtt_message{}).
%%--------------------------------------------------------------------
%% MQTT Delivery
%%--------------------------------------------------------------------
-record(mqtt_delivery,
{ sender :: pid(), %% Pid of the sender/publisher
message :: mqtt_message(), %% Message
flows :: list()
}).
-type(mqtt_delivery() :: #mqtt_delivery{}).
%%--------------------------------------------------------------------
%% MQTT Route
%%--------------------------------------------------------------------
-record(mqtt_route,
{ topic :: binary(),
node :: node()
}).
-type(mqtt_route() :: #mqtt_route{}).
%%--------------------------------------------------------------------
%% MQTT Alarm
%%--------------------------------------------------------------------
-record(mqtt_alarm,
{ id :: binary(),
severity :: warning | error | critical,
title :: iolist() | binary(),
summary :: iolist() | binary(),
timestamp :: erlang:timestamp()
}).
-type(mqtt_alarm() :: #mqtt_alarm{}).
%%--------------------------------------------------------------------
%% MQTT Plugin
%%--------------------------------------------------------------------
-record(mqtt_plugin, { name, version, descr, active = false }).
-type(mqtt_plugin() :: #mqtt_plugin{}).
%%--------------------------------------------------------------------
%% MQTT CLI Command. For example: 'broker metrics'
%%--------------------------------------------------------------------
-record(mqtt_cli, { name, action, args = [], opts = [], usage, descr }).
-type(mqtt_cli() :: #mqtt_cli{}).

View File

@ -1,77 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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.
%%--------------------------------------------------------------------
%% Internal Header File
-define(GPROC_POOL(JoinOrLeave, Pool, Id),
(begin
case JoinOrLeave of
join -> gproc_pool:connect_worker(Pool, {Pool, Id});
leave -> gproc_pool:disconnect_worker(Pool, {Pool, Id})
end
end)).
-define(PROC_NAME(M, I), (list_to_atom(lists:concat([M, "_", I])))).
-define(record_to_proplist(Def, Rec),
lists:zip(record_info(fields, Def), tl(tuple_to_list(Rec)))).
-define(record_to_proplist(Def, Rec, Fields),
[{K, V} || {K, V} <- ?record_to_proplist(Def, Rec),
lists:member(K, Fields)]).
-define(UNEXPECTED_REQ(Req, State),
(begin
lager:error("Unexpected Request: ~p", [Req]),
{reply, {error, unexpected_request}, State}
end)).
-define(UNEXPECTED_MSG(Msg, State),
(begin
lager:error("Unexpected Message: ~p", [Msg]),
{noreply, State}
end)).
-define(UNEXPECTED_INFO(Info, State),
(begin
lager:error("Unexpected Info: ~p", [Info]),
{noreply, State}
end)).
-define(IF(Cond, TrueFun, FalseFun),
(case (Cond) of
true -> (TrueFun);
false-> (FalseFun)
end)).
-define(FULLSWEEP_OPTS, [{fullsweep_after, 10}]).
-define(SUCCESS, 0). %% Success
-define(ERROR1, 101). %% badrpc
-define(ERROR2, 102). %% Unknown error
-define(ERROR3, 103). %% Username or password error
-define(ERROR4, 104). %% Empty username or password
-define(ERROR5, 105). %% User does not exist
-define(ERROR6, 106). %% Admin can not be deleted
-define(ERROR7, 107). %% Missing request parameter
-define(ERROR8, 108). %% Request parameter type error
-define(ERROR9, 109). %% Request parameter is not a json
-define(ERROR10, 110). %% Plugin has been loaded
-define(ERROR11, 111). %% Plugin has been loaded
-define(ERROR12, 112). %% Client not online
-define(ERROR13, 113). %% User already exist
-define(ERROR14, 114). %% OldPassword error

View File

@ -1,282 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%%--------------------------------------------------------------------
%% MQTT SockOpts
%%--------------------------------------------------------------------
-define(MQTT_SOCKOPTS, [binary, {packet, raw}, {reuseaddr, true},
{backlog, 512}, {nodelay, true}]).
%%--------------------------------------------------------------------
%% MQTT Protocol Version and Levels
%%--------------------------------------------------------------------
-define(MQTT_PROTO_V3, 3).
-define(MQTT_PROTO_V4, 4).
-define(MQTT_PROTO_V5, 5).
-define(PROTOCOL_NAMES, [
{?MQTT_PROTO_V3, <<"MQIsdp">>},
{?MQTT_PROTO_V4, <<"MQTT">>},
{?MQTT_PROTO_V5, <<"MQTT">>}]).
-type(mqtt_vsn() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5).
%%--------------------------------------------------------------------
%% MQTT QoS Level
%%--------------------------------------------------------------------
-define(QOS_0, 0). %% At most once
-define(QOS_1, 1). %% At least once
-define(QOS_2, 2). %% Exactly once
-define(QOS0, 0). %% At most once
-define(QOS1, 1). %% At least once
-define(QOS2, 2). %% Exactly once
-define(IS_QOS(I), (I >= ?QOS0 andalso I =< ?QOS2)).
-type(mqtt_qos() :: ?QOS0 | ?QOS1 | ?QOS2).
-type(mqtt_qos_name() :: qos0 | at_most_once |
qos1 | at_least_once |
qos2 | exactly_once).
-define(QOS_I(Name),
begin
(case Name of
?QOS_0 -> ?QOS_0;
qos0 -> ?QOS_0;
at_most_once -> ?QOS_0;
?QOS_1 -> ?QOS_1;
qos1 -> ?QOS_1;
at_least_once -> ?QOS_1;
?QOS_2 -> ?QOS_2;
qos2 -> ?QOS_2;
exactly_once -> ?QOS_2
end)
end).
%%--------------------------------------------------------------------
%% Max ClientId Length. Why 1024?
%%--------------------------------------------------------------------
-define(MAX_CLIENTID_LEN, 1024).
%%--------------------------------------------------------------------
%% MQTT Control Packet Types
%%--------------------------------------------------------------------
-define(RESERVED, 0). %% Reserved
-define(CONNECT, 1). %% Client request to connect to Server
-define(CONNACK, 2). %% Server to Client: Connect acknowledgment
-define(PUBLISH, 3). %% Publish message
-define(PUBACK, 4). %% Publish acknowledgment
-define(PUBREC, 5). %% Publish received (assured delivery part 1)
-define(PUBREL, 6). %% Publish release (assured delivery part 2)
-define(PUBCOMP, 7). %% Publish complete (assured delivery part 3)
-define(SUBSCRIBE, 8). %% Client subscribe request
-define(SUBACK, 9). %% Server Subscribe acknowledgment
-define(UNSUBSCRIBE, 10). %% Unsubscribe request
-define(UNSUBACK, 11). %% Unsubscribe acknowledgment
-define(PINGREQ, 12). %% PING request
-define(PINGRESP, 13). %% PING response
-define(DISCONNECT, 14). %% Client or Server is disconnecting
-define(AUTH, 15). %% Authentication exchange
-define(TYPE_NAMES, [
'CONNECT',
'CONNACK',
'PUBLISH',
'PUBACK',
'PUBREC',
'PUBREL',
'PUBCOMP',
'SUBSCRIBE',
'SUBACK',
'UNSUBSCRIBE',
'UNSUBACK',
'PINGREQ',
'PINGRESP',
'DISCONNECT',
'AUTH']).
-type(mqtt_packet_type() :: ?RESERVED..?DISCONNECT).
%%--------------------------------------------------------------------
%% MQTT Connect Return Codes
%%--------------------------------------------------------------------
-define(CONNACK_ACCEPT, 0). %% Connection accepted
-define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version
-define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server
-define(CONNACK_SERVER, 3). %% Server unavailable
-define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed
-define(CONNACK_AUTH, 5). %% Client is not authorized to connect
-type(mqtt_connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH).
%%--------------------------------------------------------------------
%% Max MQTT Packet Length
%%--------------------------------------------------------------------
-define(MAX_PACKET_SIZE, 16#fffffff).
%%--------------------------------------------------------------------
%% MQTT Parser and Serializer
%%--------------------------------------------------------------------
-define(HIGHBIT, 2#10000000).
-define(LOWBITS, 2#01111111).
%%--------------------------------------------------------------------
%% MQTT Packet Fixed Header
%%--------------------------------------------------------------------
-record(mqtt_packet_header,
{ type = ?RESERVED :: mqtt_packet_type(),
dup = false :: boolean(),
qos = ?QOS_0 :: mqtt_qos(),
retain = false :: boolean()
}).
%%--------------------------------------------------------------------
%% MQTT Packets
%%--------------------------------------------------------------------
-type(mqtt_client_id() :: binary()).
-type(mqtt_username() :: binary() | undefined).
-type(mqtt_packet_id() :: 1..16#ffff | undefined).
-record(mqtt_packet_connect,
{ client_id = <<>> :: mqtt_client_id(),
proto_ver = ?MQTT_PROTO_V4 :: mqtt_vsn(),
proto_name = <<"MQTT">> :: binary(),
will_retain = false :: boolean(),
will_qos = ?QOS_1 :: mqtt_qos(),
will_flag = false :: boolean(),
clean_sess = false :: boolean(),
keep_alive = 60 :: non_neg_integer(),
will_topic = undefined :: undefined | binary(),
will_msg = undefined :: undefined | binary(),
username = undefined :: undefined | binary(),
password = undefined :: undefined | binary(),
is_bridge = false :: boolean()
}).
-record(mqtt_packet_connack,
{ ack_flags = ?RESERVED :: 0 | 1,
return_code :: mqtt_connack()
}).
-record(mqtt_packet_publish,
{ topic_name :: binary(),
packet_id :: mqtt_packet_id()
}).
-record(mqtt_packet_puback,
{ packet_id :: mqtt_packet_id() }).
-record(mqtt_packet_subscribe,
{ packet_id :: mqtt_packet_id(),
topic_table :: list({binary(), mqtt_qos()})
}).
-record(mqtt_packet_unsubscribe,
{ packet_id :: mqtt_packet_id(),
topics :: list(binary())
}).
-record(mqtt_packet_suback,
{ packet_id :: mqtt_packet_id(),
qos_table :: list(mqtt_qos() | 128)
}).
-record(mqtt_packet_unsuback,
{ packet_id :: mqtt_packet_id() }).
%%--------------------------------------------------------------------
%% MQTT Control Packet
%%--------------------------------------------------------------------
-record(mqtt_packet,
{ header :: #mqtt_packet_header{},
variable :: #mqtt_packet_connect{} | #mqtt_packet_connack{}
| #mqtt_packet_publish{} | #mqtt_packet_puback{}
| #mqtt_packet_subscribe{} | #mqtt_packet_suback{}
| #mqtt_packet_unsubscribe{} | #mqtt_packet_unsuback{}
| mqtt_packet_id() | undefined,
payload :: binary() | undefined
}).
-type(mqtt_packet() :: #mqtt_packet{}).
%%--------------------------------------------------------------------
%% MQTT Packet Match
%%--------------------------------------------------------------------
-define(CONNECT_PACKET(Var),
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, variable = Var}).
-define(CONNACK_PACKET(ReturnCode),
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK},
variable = #mqtt_packet_connack{return_code = ReturnCode}}).
-define(CONNACK_PACKET(ReturnCode, SessPresent),
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK},
variable = #mqtt_packet_connack{ack_flags = SessPresent,
return_code = ReturnCode}}).
-define(PUBLISH_PACKET(Qos, PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = Qos},
variable = #mqtt_packet_publish{packet_id = PacketId}}).
-define(PUBLISH_PACKET(Qos, Topic, PacketId, Payload),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = Qos},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId},
payload = Payload}).
-define(PUBACK_PACKET(Type, PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = Type},
variable = #mqtt_packet_puback{packet_id = PacketId}}).
-define(PUBREL_PACKET(PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1},
variable = #mqtt_packet_puback{packet_id = PacketId}}).
-define(SUBSCRIBE_PACKET(PacketId, TopicTable),
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1},
variable = #mqtt_packet_subscribe{packet_id = PacketId,
topic_table = TopicTable}}).
-define(SUBACK_PACKET(PacketId, QosTable),
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK},
variable = #mqtt_packet_suback{packet_id = PacketId,
qos_table = QosTable}}).
-define(UNSUBSCRIBE_PACKET(PacketId, Topics),
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, qos = ?QOS_1},
variable = #mqtt_packet_unsubscribe{packet_id = PacketId,
topics = Topics}}).
-define(UNSUBACK_PACKET(PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
variable = #mqtt_packet_unsuback{packet_id = PacketId}}).
-define(PACKET(Type),
#mqtt_packet{header = #mqtt_packet_header{type = Type}}).

View File

@ -1,35 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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.
%%--------------------------------------------------------------------
-type(trie_node_id() :: binary() | atom()).
-record(trie_node,
{ node_id :: trie_node_id(),
edge_count = 0 :: non_neg_integer(),
topic :: binary() | undefined,
flags :: [retained | static]
}).
-record(trie_edge,
{ node_id :: trie_node_id(),
word :: binary() | atom()
}).
-record(trie,
{ edge :: #trie_edge{},
node_id :: trie_node_id()
}).

166
include/emqx.hrl Normal file
View File

@ -0,0 +1,166 @@
%% 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.
-ifndef(EMQ_X_HRL).
-define(EMQ_X_HRL, true).
%%--------------------------------------------------------------------
%% Banner
%%--------------------------------------------------------------------
-define(COPYRIGHT, "Copyright (c) 2018 EMQ Technologies Co., Ltd").
-define(LICENSE_MESSAGE, "Licensed under the Apache License, Version 2.0").
-define(PROTOCOL_VERSION, "MQTT/5.0").
-define(ERTS_MINIMUM_REQUIRED, "10.0").
%%--------------------------------------------------------------------
%% Configs
%%--------------------------------------------------------------------
-define(NO_PRIORITY_TABLE, none).
%%--------------------------------------------------------------------
%% Topics' prefix: $SYS | $queue | $share
%%--------------------------------------------------------------------
%% System topic
-define(SYSTOP, <<"$SYS/">>).
%% Queue topic
-define(QUEUE, <<"$queue/">>).
%%--------------------------------------------------------------------
%% Message and Delivery
%%--------------------------------------------------------------------
-record(session, {sid, pid}).
-record(subscription, {topic, subid, subopts}).
%% See 'Application Message' in MQTT Version 5.0
-record(message, {
%% Global unique message ID
id :: binary(),
%% Message QoS
qos = 0,
%% Message from
from :: atom() | binary(),
%% Message flags
flags :: #{atom() => boolean()},
%% Message headers, or MQTT 5.0 Properties
headers = #{},
%% Topic that the message is published to
topic :: binary(),
%% Message Payload
payload :: binary(),
%% Timestamp
timestamp :: erlang:timestamp()
}).
-record(delivery, {
sender :: pid(), %% Sender of the delivery
message :: #message{}, %% The message delivered
results :: list() %% Dispatches of the message
}).
%%--------------------------------------------------------------------
%% Route
%%--------------------------------------------------------------------
-record(route, {
topic :: binary(),
dest :: node() | {binary(), node()}
}).
%%--------------------------------------------------------------------
%% Trie
%%--------------------------------------------------------------------
-type(trie_node_id() :: binary() | atom()).
-record(trie_node, {
node_id :: trie_node_id(),
edge_count = 0 :: non_neg_integer(),
topic :: binary() | undefined,
flags :: list(atom())
}).
-record(trie_edge, {
node_id :: trie_node_id(),
word :: binary() | atom()
}).
-record(trie, {
edge :: #trie_edge{},
node_id :: trie_node_id()
}).
%%--------------------------------------------------------------------
%% Alarm
%%--------------------------------------------------------------------
-record(alarm, {
id :: binary(),
severity :: notice | warning | error | critical,
title :: iolist(),
summary :: iolist(),
timestamp :: erlang:timestamp()
}).
%%--------------------------------------------------------------------
%% Plugin
%%--------------------------------------------------------------------
-record(plugin, {
name :: atom(),
version :: string(),
dir :: string(),
descr :: string(),
vendor :: string(),
active = false :: boolean(),
info :: map()
}).
%%--------------------------------------------------------------------
%% Command
%%--------------------------------------------------------------------
-record(command, {
name :: atom(),
action :: atom(),
args = [] :: list(),
opts = [] :: list(),
usage :: string(),
descr :: string()
}).
%%--------------------------------------------------------------------
%% Banned
%%--------------------------------------------------------------------
-type(banned_who() :: {client_id, binary()}
| {username, binary()}
| {ip_address, inet:ip_address()}).
-record(banned, {
who :: banned_who(),
reason :: binary(),
by :: binary(),
desc :: binary(),
until :: integer()
}).
-endif.

530
include/emqx_mqtt.hrl Normal file
View File

@ -0,0 +1,530 @@
%% 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.
-ifndef(EMQ_X_MQTT_HRL).
-define(EMQ_X_MQTT_HRL, true).
%%--------------------------------------------------------------------
%% MQTT SockOpts
%%--------------------------------------------------------------------
-define(MQTT_SOCKOPTS, [binary, {packet, raw}, {reuseaddr, true},
{backlog, 512}, {nodelay, true}]).
%%--------------------------------------------------------------------
%% MQTT Protocol Version and Names
%%--------------------------------------------------------------------
-define(MQTT_PROTO_V3, 3).
-define(MQTT_PROTO_V4, 4).
-define(MQTT_PROTO_V5, 5).
-define(PROTOCOL_NAMES, [
{?MQTT_PROTO_V3, <<"MQIsdp">>},
{?MQTT_PROTO_V4, <<"MQTT">>},
{?MQTT_PROTO_V5, <<"MQTT">>}]).
%%--------------------------------------------------------------------
%% MQTT QoS Levels
%%--------------------------------------------------------------------
-define(QOS_0, 0). %% At most once
-define(QOS_1, 1). %% At least once
-define(QOS_2, 2). %% Exactly once
-define(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)).
-define(QOS_I(Name),
begin
(case Name of
?QOS_0 -> ?QOS_0;
qos0 -> ?QOS_0;
at_most_once -> ?QOS_0;
?QOS_1 -> ?QOS_1;
qos1 -> ?QOS_1;
at_least_once -> ?QOS_1;
?QOS_2 -> ?QOS_2;
qos2 -> ?QOS_2;
exactly_once -> ?QOS_2
end)
end).
-define(IS_QOS_NAME(I),
(I =:= qos0 orelse I =:= at_most_once orelse
I =:= qos1 orelse I =:= at_least_once orelse
I =:= qos2 orelse I =:= exactly_once)).
%%--------------------------------------------------------------------
%% Maximum ClientId Length.
%%--------------------------------------------------------------------
-define(MAX_CLIENTID_LEN, 65535).
%%--------------------------------------------------------------------
%% MQTT Control Packet Types
%%--------------------------------------------------------------------
-define(RESERVED, 0). %% Reserved
-define(CONNECT, 1). %% Client request to connect to Server
-define(CONNACK, 2). %% Server to Client: Connect acknowledgment
-define(PUBLISH, 3). %% Publish message
-define(PUBACK, 4). %% Publish acknowledgment
-define(PUBREC, 5). %% Publish received (assured delivery part 1)
-define(PUBREL, 6). %% Publish release (assured delivery part 2)
-define(PUBCOMP, 7). %% Publish complete (assured delivery part 3)
-define(SUBSCRIBE, 8). %% Client subscribe request
-define(SUBACK, 9). %% Server Subscribe acknowledgment
-define(UNSUBSCRIBE, 10). %% Unsubscribe request
-define(UNSUBACK, 11). %% Unsubscribe acknowledgment
-define(PINGREQ, 12). %% PING request
-define(PINGRESP, 13). %% PING response
-define(DISCONNECT, 14). %% Client or Server is disconnecting
-define(AUTH, 15). %% Authentication exchange
-define(TYPE_NAMES, [
'CONNECT',
'CONNACK',
'PUBLISH',
'PUBACK',
'PUBREC',
'PUBREL',
'PUBCOMP',
'SUBSCRIBE',
'SUBACK',
'UNSUBSCRIBE',
'UNSUBACK',
'PINGREQ',
'PINGRESP',
'DISCONNECT',
'AUTH']).
%%--------------------------------------------------------------------
%% MQTT V3.1.1 Connect Return Codes
%%--------------------------------------------------------------------
-define(CONNACK_ACCEPT, 0). %% Connection accepted
-define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version
-define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server
-define(CONNACK_SERVER, 3). %% Server unavailable
-define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed
-define(CONNACK_AUTH, 5). %% Client is not authorized to connect
%%--------------------------------------------------------------------
%% MQTT V5.0 Reason Codes
%%--------------------------------------------------------------------
-define(RC_SUCCESS, 16#00).
-define(RC_NORMAL_DISCONNECTION, 16#00).
-define(RC_GRANTED_QOS_0, 16#00).
-define(RC_GRANTED_QOS_1, 16#01).
-define(RC_GRANTED_QOS_2, 16#02).
-define(RC_DISCONNECT_WITH_WILL_MESSAGE, 16#04).
-define(RC_NO_MATCHING_SUBSCRIBERS, 16#10).
-define(RC_NO_SUBSCRIPTION_EXISTED, 16#11).
-define(RC_CONTINUE_AUTHENTICATION, 16#18).
-define(RC_RE_AUTHENTICATE, 16#19).
-define(RC_UNSPECIFIED_ERROR, 16#80).
-define(RC_MALFORMED_PACKET, 16#81).
-define(RC_PROTOCOL_ERROR, 16#82).
-define(RC_IMPLEMENTATION_SPECIFIC_ERROR, 16#83).
-define(RC_UNSUPPORTED_PROTOCOL_VERSION, 16#84).
-define(RC_CLIENT_IDENTIFIER_NOT_VALID, 16#85).
-define(RC_BAD_USER_NAME_OR_PASSWORD, 16#86).
-define(RC_NOT_AUTHORIZED, 16#87).
-define(RC_SERVER_UNAVAILABLE, 16#88).
-define(RC_SERVER_BUSY, 16#89).
-define(RC_BANNED, 16#8A).
-define(RC_SERVER_SHUTTING_DOWN, 16#8B).
-define(RC_BAD_AUTHENTICATION_METHOD, 16#8C).
-define(RC_KEEP_ALIVE_TIMEOUT, 16#8D).
-define(RC_SESSION_TAKEN_OVER, 16#8E).
-define(RC_TOPIC_FILTER_INVALID, 16#8F).
-define(RC_TOPIC_NAME_INVALID, 16#90).
-define(RC_PACKET_IDENTIFIER_IN_USE, 16#91).
-define(RC_PACKET_IDENTIFIER_NOT_FOUND, 16#92).
-define(RC_RECEIVE_MAXIMUM_EXCEEDED, 16#93).
-define(RC_TOPIC_ALIAS_INVALID, 16#94).
-define(RC_PACKET_TOO_LARGE, 16#95).
-define(RC_MESSAGE_RATE_TOO_HIGH, 16#96).
-define(RC_QUOTA_EXCEEDED, 16#97).
-define(RC_ADMINISTRATIVE_ACTION, 16#98).
-define(RC_PAYLOAD_FORMAT_INVALID, 16#99).
-define(RC_RETAIN_NOT_SUPPORTED, 16#9A).
-define(RC_QOS_NOT_SUPPORTED, 16#9B).
-define(RC_USE_ANOTHER_SERVER, 16#9C).
-define(RC_SERVER_MOVED, 16#9D).
-define(RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED, 16#9E).
-define(RC_CONNECTION_RATE_EXCEEDED, 16#9F).
-define(RC_MAXIMUM_CONNECT_TIME, 16#A0).
-define(RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED, 16#A1).
-define(RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED, 16#A2).
%%--------------------------------------------------------------------
%% Maximum MQTT Packet Length
%%--------------------------------------------------------------------
-define(MAX_PACKET_SIZE, 16#fffffff).
%%--------------------------------------------------------------------
%% MQTT Frame Mask
%%--------------------------------------------------------------------
-define(HIGHBIT, 2#10000000).
-define(LOWBITS, 2#01111111).
%%--------------------------------------------------------------------
%% MQTT Packet Fixed Header
%%--------------------------------------------------------------------
-record(mqtt_packet_header, {
type = ?RESERVED,
dup = false,
qos = ?QOS_0,
retain = false
}).
%%--------------------------------------------------------------------
%% MQTT Packets
%%--------------------------------------------------------------------
-define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling
rap => 0, %% Retain as Publish
nl => 0, %% No Local
qos => 0, %% QoS
rc => 0 %% Reason Code
}).
-record(mqtt_packet_connect, {
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4,
is_bridge = false,
clean_start = true,
will_flag = false,
will_qos = ?QOS_0,
will_retain = false,
keepalive = 0,
properties = undefined,
client_id = <<>>,
will_props = undefined,
will_topic = undefined,
will_payload = undefined,
username = undefined,
password = undefined
}).
-record(mqtt_packet_connack, {
ack_flags,
reason_code,
properties
}).
-record(mqtt_packet_publish, {
topic_name,
packet_id,
properties
}).
-record(mqtt_packet_puback, {
packet_id,
reason_code,
properties
}).
-record(mqtt_packet_subscribe, {
packet_id,
properties,
topic_filters
}).
-record(mqtt_packet_suback, {
packet_id,
properties,
reason_codes
}).
-record(mqtt_packet_unsubscribe, {
packet_id,
properties,
topic_filters
}).
-record(mqtt_packet_unsuback, {
packet_id,
properties,
reason_codes
}).
-record(mqtt_packet_disconnect, {
reason_code,
properties
}).
-record(mqtt_packet_auth, {
reason_code,
properties
}).
%%--------------------------------------------------------------------
%% MQTT Control Packet
%%--------------------------------------------------------------------
-record(mqtt_packet, {
header :: #mqtt_packet_header{},
variable :: #mqtt_packet_connect{}
| #mqtt_packet_connack{}
| #mqtt_packet_publish{}
| #mqtt_packet_puback{}
| #mqtt_packet_subscribe{}
| #mqtt_packet_suback{}
| #mqtt_packet_unsubscribe{}
| #mqtt_packet_unsuback{}
| #mqtt_packet_disconnect{}
| #mqtt_packet_auth{}
| pos_integer()
| undefined,
payload :: binary() | undefined
}).
%%--------------------------------------------------------------------
%% MQTT Packet Match
%%--------------------------------------------------------------------
-define(CONNECT_PACKET(Var),
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT},
variable = Var}).
-define(CONNACK_PACKET(ReasonCode),
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK},
variable = #mqtt_packet_connack{ack_flags = 0,
reason_code = ReasonCode}
}).
-define(CONNACK_PACKET(ReasonCode, SessPresent),
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK},
variable = #mqtt_packet_connack{ack_flags = SessPresent,
reason_code = ReasonCode}
}).
-define(CONNACK_PACKET(ReasonCode, SessPresent, Properties),
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK},
variable = #mqtt_packet_connack{ack_flags = SessPresent,
reason_code = ReasonCode,
properties = Properties}
}).
-define(AUTH_PACKET(),
#mqtt_packet{header = #mqtt_packet_header{type = ?AUTH},
variable = #mqtt_packet_auth{reason_code = 0}
}).
-define(AUTH_PACKET(ReasonCode),
#mqtt_packet{header = #mqtt_packet_header{type = ?AUTH},
variable = #mqtt_packet_auth{reason_code = ReasonCode}
}).
-define(AUTH_PACKET(ReasonCode, Properties),
#mqtt_packet{header = #mqtt_packet_header{type = ?AUTH},
variable = #mqtt_packet_auth{reason_code = ReasonCode,
properties = Properties}
}).
-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},
variable = #mqtt_packet_publish{packet_id = PacketId}
}).
-define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = QoS},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId},
payload = Payload
}).
-define(PUBLISH_PACKET(QoS, Topic, PacketId, Properties, Payload),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = QoS},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId,
properties = Properties},
payload = Payload
}).
-define(PUBACK_PACKET(PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = 0}
}).
-define(PUBACK_PACKET(PacketId, ReasonCode),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = ReasonCode}
}).
-define(PUBACK_PACKET(PacketId, ReasonCode, Properties),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = ReasonCode,
properties = Properties}
}).
-define(PUBREC_PACKET(PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = 0}
}).
-define(PUBREC_PACKET(PacketId, ReasonCode),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = ReasonCode}
}).
-define(PUBREC_PACKET(PacketId, ReasonCode, Properties),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = ReasonCode,
properties = Properties}
}).
-define(PUBREL_PACKET(PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL,
qos = ?QOS_1},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = 0}
}).
-define(PUBREL_PACKET(PacketId, ReasonCode),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL,
qos = ?QOS_1},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = ReasonCode}
}).
-define(PUBREL_PACKET(PacketId, ReasonCode, Properties),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL,
qos = ?QOS_1},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = ReasonCode,
properties = Properties}
}).
-define(PUBCOMP_PACKET(PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = 0}
}).
-define(PUBCOMP_PACKET(PacketId, ReasonCode),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = ReasonCode}
}).
-define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP},
variable = #mqtt_packet_puback{packet_id = PacketId,
reason_code = ReasonCode,
properties = Properties}
}).
-define(SUBSCRIBE_PACKET(PacketId, TopicFilters),
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE,
qos = ?QOS_1},
variable = #mqtt_packet_subscribe{packet_id = PacketId,
topic_filters = TopicFilters}
}).
-define(SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE,
qos = ?QOS_1},
variable = #mqtt_packet_subscribe{packet_id = PacketId,
properties = Properties,
topic_filters = TopicFilters}
}).
-define(SUBACK_PACKET(PacketId, ReasonCodes),
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK},
variable = #mqtt_packet_suback{packet_id = PacketId,
reason_codes = ReasonCodes}
}).
-define(SUBACK_PACKET(PacketId, Properties, ReasonCodes),
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK},
variable = #mqtt_packet_suback{packet_id = PacketId,
properties = Properties,
reason_codes = ReasonCodes}
}).
-define(UNSUBSCRIBE_PACKET(PacketId, TopicFilters),
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE,
qos = ?QOS_1},
variable = #mqtt_packet_unsubscribe{packet_id = PacketId,
topic_filters = TopicFilters}
}).
-define(UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE,
qos = ?QOS_1},
variable = #mqtt_packet_unsubscribe{packet_id = PacketId,
properties = Properties,
topic_filters = TopicFilters}
}).
-define(UNSUBACK_PACKET(PacketId),
#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,
properties = Properties,
reason_codes = ReasonCodes}
}).
-define(DISCONNECT_PACKET(),
#mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT},
variable = #mqtt_packet_disconnect{reason_code = 0}
}).
-define(DISCONNECT_PACKET(ReasonCode),
#mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT},
variable = #mqtt_packet_disconnect{reason_code = ReasonCode}
}).
-define(DISCONNECT_PACKET(ReasonCode, Properties),
#mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT},
variable = #mqtt_packet_disconnect{reason_code = ReasonCode,
properties = Properties}
}).
-define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}).
-define(SHARE, "$share").
-define(SHARE(Group, Topic), emqx_topic:join([<<?SHARE>>, Group, Topic])).
-define(IS_SHARE(Topic), case Topic of <<?SHARE, _/binary>> -> true; _ -> false end).
-endif.

23
include/logger.hrl Normal file
View File

@ -0,0 +1,23 @@
%%--------------------------------------------------------------------
%% Logs with a header prefixed to the log message.
%% And the log args are puted into report_cb for lazy evaluation.
%%--------------------------------------------------------------------
-ifdef(LOG_HEADER).
%% with header
-define(LOG(Level, Format, Args),
begin
(logger:log(Level,#{},#{report_cb =>
fun(_) ->
{?LOG_HEADER ++ " "++ (Format), (Args)}
end}))
end).
-else.
%% without header
-define(LOG(Level, Format, Args),
begin
(logger:log(Level,#{},#{report_cb =>
fun(_) ->
{(Format), (Args)}
end}))
end).
-endif.

File diff suppressed because it is too large Load Diff

1809
priv/emqx.schema Normal file

File diff suppressed because it is too large Load Diff

29
rebar.config Normal file
View File

@ -0,0 +1,29 @@
{deps, [{jsx, "2.9.0"},
{gproc, "0.8.0"},
{cowboy, "2.4.0"}
]}.
%% appended to deps in rebar.config.script
{github_emqx_deps,
[{gen_rpc, "2.3.0"},
{ekka, "v0.5.1"},
{esockd, "v5.4.3"},
{cuttlefish, "v2.2.0"}
]}.
{edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars,
warn_shadow_vars,
warn_unused_import,
warn_obsolete_guard,
debug_info,
{d, 'APPLICATION', emqx}]}.
{xref_checks, [undefined_function_calls, undefined_functions,
locals_not_used, deprecated_function_calls,
warnings_as_errors, deprecated_functions]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
{cover_export_enabled, true}.
{plugins, [coveralls]}.

26
rebar.config.script Normal file
View File

@ -0,0 +1,26 @@
CONFIG0 = case os:getenv("REBAR_GIT_CLONE_OPTIONS") of
"--depth 1" ->
CONFIG;
_ ->
os:putenv("REBAR_GIT_CLONE_OPTIONS", "--depth 1"),
CONFIG
end,
CONFIG1 = case os:getenv("TRAVIS") of
"true" ->
JobId = os:getenv("TRAVIS_JOB_ID"),
[{coveralls_service_job_id, JobId},
{coveralls_coverdata, "_build/test/cover/*.coverdata"},
{coveralls_service_name , "travis-ci"} | CONFIG];
_ ->
CONFIG
end,
{_, Deps} = lists:keyfind(deps, 1, CONFIG1),
{_, OurDeps} = lists:keyfind(github_emqx_deps, 1, CONFIG1),
UrlPrefix = "https://github.com/emqx/",
NewDeps = Deps ++ [{Name, {git, UrlPrefix ++ atom_to_list(Name), {branch, Branch}}} || {Name, Branch} <- OurDeps],
CONFIG2 = lists:keystore(deps, 1, CONFIG1, {deps, NewDeps}),
CONFIG2.

View File

@ -1,36 +0,0 @@
[{<<"esockd">>,
{git,"https://github.com/emqtt/esockd",
{ref,"87d0d3b672e0f25e474f5f8298da568cbb6b168a"}},
0},
{<<"gen_logger">>,
{git,"https://github.com/emqtt/gen_logger.git",
{ref,"f6e9f2f373d99f41ffe0579ab5a5f3b19472c9c5"}},
1},
{<<"goldrush">>,
{git,"https://github.com/basho/goldrush.git",
{ref,"8f1b715d36b650ec1e1f5612c00e28af6ab0de82"}},
1},
{<<"gproc">>,
{git,"https://github.com/uwiger/gproc",
{ref,"01c8fbfdd5e4701e8e4b57b0c8279872f9574b0b"}},
0},
{<<"lager">>,
{git,"https://github.com/basho/lager",
{ref,"81eaef0ce98fdbf64ab95665e3bc2ec4b24c7dac"}},
0},
{<<"lager_syslog">>,
{git,"https://github.com/basho/lager_syslog",
{ref,"126dd0284fcac9b01613189a82facf8d803411a2"}},
0},
{<<"mochiweb">>,
{git,"https://github.com/emqtt/mochiweb",
{ref,"c75d88e451b4fe26580a58223f645d99482f51af"}},
0},
{<<"pbkdf2">>,
{git,"https://github.com/comtihon/erlang-pbkdf2.git",
{ref,"7076584f5377e98600a7e2cb81980b2992fb2f71"}},
0},
{<<"syslog">>,
{git,"git://github.com/Vagabond/erlang-syslog",
{ref,"0e4f0e95c361af298c5d1d17ceccfa831efc036d"}},
1}].

View File

@ -1,12 +0,0 @@
{application,emqttd,
[{description,"Erlang MQTT Broker"},
{vsn,"2.3.11"},
{modules,[]},
{registered,[emqttd_sup]},
{applications,[kernel,stdlib,gproc,lager,esockd,mochiweb,
lager_syslog,pbkdf2,bcrypt]},
{env,[]},
{mod,{emqttd_app,[]}},
{maintainers,["Feng Lee <feng@emqtt.io>"]},
{licenses,["Apache-2.0"]},
{links,[{"Github","https://github.com/emqtt/emqttd"}]}]}.

View File

@ -1,181 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc EMQ Main Module.
-module(emqttd).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-export([start/0, env/1, env/2, is_running/1, stop/0]).
%% PubSub API
-export([subscribe/1, subscribe/2, subscribe/3, publish/1,
unsubscribe/1, unsubscribe/2]).
%% PubSub Management API
-export([setqos/3, topics/0, subscriptions/1, subscribers/1, subscribed/2]).
%% Hooks API
-export([hook/4, hook/3, unhook/2, run_hooks/2, run_hooks/3]).
%% Debug API
-export([dump/0]).
%% Shutdown and reboot
-export([shutdown/0, shutdown/1, reboot/0]).
-type(subid() :: binary()).
-type(subscriber() :: pid() | subid() | {subid(), pid()}).
-type(suboption() :: local | {qos, non_neg_integer()} | {share, {'$queue' | binary()}}).
-export_type([subscriber/0, suboption/0]).
-define(APP, ?MODULE).
%%--------------------------------------------------------------------
%% Bootstrap, environment, configuration, is_running...
%%--------------------------------------------------------------------
%% @doc Start emqttd application.
-spec(start() -> ok | {error, term()}).
start() -> application:start(?APP).
%% @doc Stop emqttd application.
-spec(stop() -> ok | {error, term()}).
stop() -> application:stop(?APP).
%% @doc Environment
-spec(env(Key :: atom()) -> {ok, any()} | undefined).
env(Key) -> application:get_env(?APP, Key).
%% @doc Get environment
-spec(env(Key :: atom(), Default :: any()) -> undefined | any()).
env(Key, Default) -> application:get_env(?APP, Key, Default).
%% @doc Is running?
-spec(is_running(node()) -> boolean()).
is_running(Node) ->
case rpc:call(Node, erlang, whereis, [?APP]) of
{badrpc, _} -> false;
undefined -> false;
Pid when is_pid(Pid) -> true
end.
%%--------------------------------------------------------------------
%% PubSub APIs
%%--------------------------------------------------------------------
%% @doc Subscribe
-spec(subscribe(iodata()) -> ok | {error, term()}).
subscribe(Topic) ->
emqttd_server:subscribe(iolist_to_binary(Topic)).
-spec(subscribe(iodata(), subscriber()) -> ok | {error, term()}).
subscribe(Topic, Subscriber) ->
emqttd_server:subscribe(iolist_to_binary(Topic), Subscriber).
-spec(subscribe(iodata(), subscriber(), [suboption()]) -> ok | {error, term()}).
subscribe(Topic, Subscriber, Options) ->
emqttd_server:subscribe(iolist_to_binary(Topic), Subscriber, Options).
%% @doc Publish MQTT Message
-spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore).
publish(Msg) ->
emqttd_server:publish(Msg).
%% @doc Unsubscribe
-spec(unsubscribe(iodata()) -> ok | {error, term()}).
unsubscribe(Topic) ->
emqttd_server:unsubscribe(iolist_to_binary(Topic)).
-spec(unsubscribe(iodata(), subscriber()) -> ok | {error, term()}).
unsubscribe(Topic, Subscriber) ->
emqttd_server:unsubscribe(iolist_to_binary(Topic), Subscriber).
-spec(setqos(binary(), subscriber(), mqtt_qos()) -> ok).
setqos(Topic, Subscriber, Qos) ->
emqttd_server:setqos(iolist_to_binary(Topic), Subscriber, Qos).
-spec(topics() -> [binary()]).
topics() -> emqttd_router:topics().
-spec(subscribers(iodata()) -> list(subscriber())).
subscribers(Topic) ->
emqttd_server:subscribers(iolist_to_binary(Topic)).
-spec(subscriptions(subscriber()) -> [{emqttd:subscriber(), binary(), list(emqttd:suboption())}]).
subscriptions(Subscriber) ->
emqttd_server:subscriptions(Subscriber).
-spec(subscribed(iodata(), subscriber()) -> boolean()).
subscribed(Topic, Subscriber) ->
emqttd_server:subscribed(iolist_to_binary(Topic), Subscriber).
%%--------------------------------------------------------------------
%% Hooks API
%%--------------------------------------------------------------------
-spec(hook(atom(), function() | {emqttd_hooks:hooktag(), function()}, list(any()))
-> ok | {error, term()}).
hook(Hook, TagFunction, InitArgs) ->
emqttd_hooks:add(Hook, TagFunction, InitArgs).
-spec(hook(atom(), function() | {emqttd_hooks:hooktag(), function()}, list(any()), integer())
-> ok | {error, term()}).
hook(Hook, TagFunction, InitArgs, Priority) ->
emqttd_hooks:add(Hook, TagFunction, InitArgs, Priority).
-spec(unhook(atom(), function() | {emqttd_hooks:hooktag(), function()})
-> ok | {error, term()}).
unhook(Hook, TagFunction) ->
emqttd_hooks:delete(Hook, TagFunction).
-spec(run_hooks(atom(), list(any())) -> ok | stop).
run_hooks(Hook, Args) ->
emqttd_hooks:run(Hook, Args).
-spec(run_hooks(atom(), list(any()), any()) -> {ok | stop, any()}).
run_hooks(Hook, Args, Acc) ->
emqttd_hooks:run(Hook, Args, Acc).
%%--------------------------------------------------------------------
%% Shutdown and reboot
%%--------------------------------------------------------------------
shutdown() ->
shutdown(normal).
shutdown(Reason) ->
lager:error("EMQ shutdown for ~s", [Reason]),
emqttd_plugins:unload(),
lists:foreach(fun application:stop/1, [emqttd, ekka, mochiweb, esockd, gproc]).
reboot() ->
lists:foreach(fun application:start/1, [gproc, esockd, mochiweb, ekka, emqttd]).
%%--------------------------------------------------------------------
%% Debug
%%--------------------------------------------------------------------
dump() -> lists:append([emqttd_server:dump(), emqttd_router:dump()]).

View File

@ -1,180 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_access_control).
-behaviour(gen_server).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
%% API Function Exports
-export([start_link/0, auth/2, check_acl/3, reload_acl/0, lookup_mods/1,
register_mod/3, register_mod/4, unregister_mod/2, stop/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(ACCESS_CONTROL_TAB, mqtt_access_control).
-type(password() :: undefined | binary()).
-record(state, {}).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Start access control server.
-spec(start_link() -> {ok, pid()} | ignore | {error, term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @doc Authenticate MQTT Client.
-spec(auth(Client :: mqtt_client(), Password :: password()) -> ok | {ok, boolean()} | {error, term()}).
auth(Client, Password) when is_record(Client, mqtt_client) ->
auth(Client, Password, lookup_mods(auth)).
auth(_Client, _Password, []) ->
case emqttd:env(allow_anonymous, false) of
true -> ok;
false -> {error, "No auth module to check!"}
end;
auth(Client, Password, [{Mod, State, _Seq} | Mods]) ->
case catch Mod:check(Client, Password, State) of
ok -> ok;
{ok, IsSuper} -> {ok, IsSuper};
ignore -> auth(Client, Password, Mods);
{error, Reason} -> {error, Reason};
{'EXIT', Error} -> {error, Error}
end.
%% @doc Check ACL
-spec(check_acl(Client, PubSub, Topic) -> allow | deny when
Client :: mqtt_client(),
PubSub :: pubsub(),
Topic :: binary()).
check_acl(Client, PubSub, Topic) when ?PS(PubSub) ->
check_acl(Client, PubSub, Topic, lookup_mods(acl)).
check_acl(_Client, _PubSub, _Topic, []) ->
emqttd:env(acl_nomatch, allow);
check_acl(Client, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) ->
case Mod:check_acl({Client, PubSub, Topic}, State) of
allow -> allow;
deny -> deny;
ignore -> check_acl(Client, PubSub, Topic, AclMods)
end.
%% @doc Reload ACL Rules.
-spec(reload_acl() -> list(ok | {error, already_existed})).
reload_acl() ->
[Mod:reload_acl(State) || {Mod, State, _Seq} <- lookup_mods(acl)].
%% @doc Register Authentication or ACL module.
-spec(register_mod(auth | acl, atom(), list()) -> ok | {error, term()}).
register_mod(Type, Mod, Opts) when Type =:= auth; Type =:= acl->
register_mod(Type, Mod, Opts, 0).
-spec(register_mod(auth | acl, atom(), list(), non_neg_integer()) -> ok | {error, term()}).
register_mod(Type, Mod, Opts, Seq) when Type =:= auth; Type =:= acl->
gen_server:call(?SERVER, {register_mod, Type, Mod, Opts, Seq}).
%% @doc Unregister authentication or ACL module
-spec(unregister_mod(Type :: auth | acl, Mod :: atom()) -> ok | {error, not_found | term()}).
unregister_mod(Type, Mod) when Type =:= auth; Type =:= acl ->
gen_server:call(?SERVER, {unregister_mod, Type, Mod}).
%% @doc Lookup authentication or ACL modules.
-spec(lookup_mods(auth | acl) -> list()).
lookup_mods(Type) ->
case ets:lookup(?ACCESS_CONTROL_TAB, tab_key(Type)) of
[] -> [];
[{_, Mods}] -> Mods
end.
tab_key(auth) -> auth_modules;
tab_key(acl) -> acl_modules.
%% @doc Stop access control server.
stop() -> gen_server:call(?MODULE, stop).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([]) ->
ets:new(?ACCESS_CONTROL_TAB, [set, named_table, protected, {read_concurrency, true}]),
{ok, #state{}}.
handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) ->
Mods = lookup_mods(Type),
Existed = lists:keyfind(Mod, 1, Mods),
{reply, if_existed(Existed, fun() ->
case catch Mod:init(Opts) of
{ok, ModState} ->
NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) ->
Seq1 >= Seq2
end, [{Mod, ModState, Seq} | Mods]),
ets:insert(?ACCESS_CONTROL_TAB, {tab_key(Type), NewMods}),
ok;
{error, Error} ->
{error, Error};
{'EXIT', Reason} ->
{error, Reason}
end
end), State};
handle_call({unregister_mod, Type, Mod}, _From, State) ->
Mods = lookup_mods(Type),
case lists:keyfind(Mod, 1, Mods) of
false ->
{reply, {error, not_found}, State};
_ ->
ets:insert(?ACCESS_CONTROL_TAB, {tab_key(Type), lists:keydelete(Mod, 1, Mods)}),
{reply, ok, State}
end;
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(Req, _From, State) ->
lager:error("Bad Request: ~p", [Req]),
{reply, {error, badreq}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
if_existed(false, Fun) -> Fun();
if_existed(_Mod, _Fun) -> {error, already_existed}.

View File

@ -1,158 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_access_rule).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-type(who() :: all | binary() |
{ipaddr, esockd_cidr:cidr_string()} |
{client, binary()} |
{user, binary()}).
-type(access() :: subscribe | publish | pubsub).
-type(topic() :: binary()).
-type(rule() :: {allow, all} |
{allow, who(), access(), list(topic())} |
{deny, all} |
{deny, who(), access(), list(topic())}).
-export_type([rule/0]).
-export([compile/1, match/3]).
-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))).
%% @doc Compile Access Rule.
compile({A, all}) when ?ALLOW_DENY(A) ->
{A, all};
compile({A, Who, Access, Topic}) when ?ALLOW_DENY(A) andalso is_binary(Topic) ->
{A, compile(who, Who), Access, [compile(topic, Topic)]};
compile({A, Who, Access, TopicFilters}) when ?ALLOW_DENY(A) ->
{A, compile(who, Who), Access, [compile(topic, Topic) || Topic <- TopicFilters]}.
compile(who, all) ->
all;
compile(who, {ipaddr, CIDR}) ->
{ipaddr, esockd_cidr:parse(CIDR, true)};
compile(who, {client, all}) ->
{client, all};
compile(who, {client, ClientId}) ->
{client, bin(ClientId)};
compile(who, {user, all}) ->
{user, all};
compile(who, {user, Username}) ->
{user, bin(Username)};
compile(who, {'and', Conds}) when is_list(Conds) ->
{'and', [compile(who, Cond) || Cond <- Conds]};
compile(who, {'or', Conds}) when is_list(Conds) ->
{'or', [compile(who, Cond) || Cond <- Conds]};
compile(topic, {eq, Topic}) ->
{eq, emqttd_topic:words(bin(Topic))};
compile(topic, Topic) ->
Words = emqttd_topic:words(bin(Topic)),
case 'pattern?'(Words) of
true -> {pattern, Words};
false -> Words
end.
'pattern?'(Words) ->
lists:member(<<"%u">>, Words)
orelse lists:member(<<"%c">>, Words).
bin(L) when is_list(L) ->
list_to_binary(L);
bin(B) when is_binary(B) ->
B.
%% @doc Match Access Rule
-spec(match(mqtt_client(), topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch).
match(_Client, _Topic, {AllowDeny, all}) when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) ->
{matched, AllowDeny};
match(Client, Topic, {AllowDeny, Who, _PubSub, TopicFilters})
when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) ->
case match_who(Client, Who) andalso match_topics(Client, Topic, TopicFilters) of
true -> {matched, AllowDeny};
false -> nomatch
end.
match_who(_Client, all) ->
true;
match_who(_Client, {user, all}) ->
true;
match_who(_Client, {client, all}) ->
true;
match_who(#mqtt_client{client_id = ClientId}, {client, ClientId}) ->
true;
match_who(#mqtt_client{username = Username}, {user, Username}) ->
true;
match_who(#mqtt_client{peername = undefined}, {ipaddr, _Tup}) ->
false;
match_who(#mqtt_client{peername = {IP, _}}, {ipaddr, CIDR}) ->
esockd_cidr:match(IP, CIDR);
match_who(Client, {'and', Conds}) when is_list(Conds) ->
lists:foldl(fun(Who, Allow) ->
match_who(Client, Who) andalso Allow
end, true, Conds);
match_who(Client, {'or', Conds}) when is_list(Conds) ->
lists:foldl(fun(Who, Allow) ->
match_who(Client, Who) orelse Allow
end, false, Conds);
match_who(_Client, _Who) ->
false.
match_topics(_Client, _Topic, []) ->
false;
match_topics(Client, Topic, [{pattern, PatternFilter}|Filters]) ->
TopicFilter = feed_var(Client, PatternFilter),
case match_topic(emqttd_topic:words(Topic), TopicFilter) of
true -> true;
false -> match_topics(Client, Topic, Filters)
end;
match_topics(Client, Topic, [TopicFilter|Filters]) ->
case match_topic(emqttd_topic:words(Topic), TopicFilter) of
true -> true;
false -> match_topics(Client, Topic, Filters)
end.
match_topic(Topic, {eq, TopicFilter}) ->
Topic =:= TopicFilter;
match_topic(Topic, TopicFilter) ->
emqttd_topic:match(Topic, TopicFilter).
feed_var(Client, Pattern) ->
feed_var(Client, Pattern, []).
feed_var(_Client, [], Acc) ->
lists:reverse(Acc);
feed_var(Client = #mqtt_client{client_id = undefined}, [<<"%c">>|Words], Acc) ->
feed_var(Client, Words, [<<"%c">>|Acc]);
feed_var(Client = #mqtt_client{client_id = ClientId}, [<<"%c">>|Words], Acc) ->
feed_var(Client, Words, [ClientId |Acc]);
feed_var(Client = #mqtt_client{username = undefined}, [<<"%u">>|Words], Acc) ->
feed_var(Client, Words, [<<"%u">>|Acc]);
feed_var(Client = #mqtt_client{username = Username}, [<<"%u">>|Words], Acc) ->
feed_var(Client, Words, [Username|Acc]);
feed_var(Client, [W|Words], Acc) ->
feed_var(Client, Words, [W|Acc]).

View File

@ -1,125 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_acl_internal).
-behaviour(emqttd_acl_mod).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_cli.hrl").
-export([all_rules/0]).
%% ACL callbacks
-export([init/1, check_acl/2, reload_acl/1, description/0]).
-define(ACL_RULE_TAB, mqtt_acl_rule).
-record(state, {config}).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Read all rules
-spec(all_rules() -> list(emqttd_access_rule:rule())).
all_rules() ->
case ets:lookup(?ACL_RULE_TAB, all_rules) of
[] -> [];
[{_, Rules}] -> Rules
end.
%%--------------------------------------------------------------------
%% ACL callbacks
%%--------------------------------------------------------------------
%% @doc Init internal ACL
-spec(init([File :: string()]) -> {ok, State :: any()}).
init([File]) ->
ets:new(?ACL_RULE_TAB, [set, public, named_table, {read_concurrency, true}]),
State = #state{config = File},
true = load_rules_from_file(State),
{ok, State}.
load_rules_from_file(#state{config = AclFile}) ->
{ok, Terms} = file:consult(AclFile),
Rules = [emqttd_access_rule:compile(Term) || Term <- Terms],
lists:foreach(fun(PubSub) ->
ets:insert(?ACL_RULE_TAB, {PubSub,
lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)})
end, [publish, subscribe]),
ets:insert(?ACL_RULE_TAB, {all_rules, Terms}).
filter(_PubSub, {allow, all}) ->
true;
filter(_PubSub, {deny, all}) ->
true;
filter(publish, {_AllowDeny, _Who, publish, _Topics}) ->
true;
filter(_PubSub, {_AllowDeny, _Who, pubsub, _Topics}) ->
true;
filter(subscribe, {_AllowDeny, _Who, subscribe, _Topics}) ->
true;
filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) ->
false.
%% @doc Check ACL
-spec(check_acl({Client, PubSub, Topic}, State) -> allow | deny | ignore when
Client :: mqtt_client(),
PubSub :: pubsub(),
Topic :: binary(),
State :: #state{}).
check_acl(_Who, #state{config = undefined}) ->
allow;
check_acl({Client, PubSub, Topic}, #state{}) ->
case match(Client, Topic, lookup(PubSub)) of
{matched, allow} -> allow;
{matched, deny} -> deny;
nomatch -> ignore
end.
lookup(PubSub) ->
case ets:lookup(?ACL_RULE_TAB, PubSub) of
[] -> [];
[{PubSub, Rules}] -> Rules
end.
match(_Client, _Topic, []) ->
nomatch;
match(Client, Topic, [Rule|Rules]) ->
case emqttd_access_rule:match(Client, Topic, Rule) of
nomatch -> match(Client, Topic, Rules);
{matched, AllowDeny} -> {matched, AllowDeny}
end.
%% @doc Reload ACL
-spec(reload_acl(State :: #state{}) -> ok | {error, Reason :: any()}).
reload_acl(#state{config = undefined}) ->
ok;
reload_acl(State) ->
case catch load_rules_from_file(State) of
{'EXIT', Error} -> {error, Error};
true -> ?PRINT("~s~n", ["reload acl_internal successfully"]), ok
end.
%% @doc ACL Module Description
-spec(description() -> string()).
description() ->
"Internal ACL with etc/acl.conf".

View File

@ -1,140 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_alarm).
-author("Feng Lee <feng@emqtt.io>").
-behaviour(gen_event).
-include("emqttd.hrl").
-define(ALARM_MGR, ?MODULE).
%% API Function Exports
-export([start_link/0, alarm_fun/0, get_alarms/0,
set_alarm/1, clear_alarm/1,
add_alarm_handler/1, add_alarm_handler/2,
delete_alarm_handler/1]).
%% gen_event callbacks
-export([init/1, handle_event/2, handle_call/2, handle_info/2,
terminate/2, code_change/3]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() ->
start_with(fun(Pid) -> gen_event:add_handler(Pid, ?MODULE, []) end).
start_with(Fun) ->
case gen_event:start_link({local, ?ALARM_MGR}) of
{ok, Pid} -> Fun(Pid), {ok, Pid};
Error -> Error
end.
alarm_fun() -> alarm_fun(false).
alarm_fun(Bool) ->
fun(alert, _Alarm) when Bool =:= true -> alarm_fun(true);
(alert, Alarm) when Bool =:= false -> set_alarm(Alarm), alarm_fun(true);
(clear, AlarmId) when Bool =:= true -> clear_alarm(AlarmId), alarm_fun(false);
(clear, _AlarmId) when Bool =:= false -> alarm_fun(false)
end.
-spec(set_alarm(mqtt_alarm()) -> ok).
set_alarm(Alarm) when is_record(Alarm, mqtt_alarm) ->
gen_event:notify(?ALARM_MGR, {set_alarm, Alarm}).
-spec(clear_alarm(any()) -> ok).
clear_alarm(AlarmId) when is_binary(AlarmId) ->
gen_event:notify(?ALARM_MGR, {clear_alarm, AlarmId}).
-spec(get_alarms() -> list(mqtt_alarm())).
get_alarms() ->
gen_event:call(?ALARM_MGR, ?MODULE, get_alarms).
add_alarm_handler(Module) when is_atom(Module) ->
gen_event:add_handler(?ALARM_MGR, Module, []).
add_alarm_handler(Module, Args) when is_atom(Module) ->
gen_event:add_handler(?ALARM_MGR, Module, Args).
delete_alarm_handler(Module) when is_atom(Module) ->
gen_event:delete_handler(?ALARM_MGR, Module, []).
%%--------------------------------------------------------------------
%% Default Alarm handler
%%--------------------------------------------------------------------
init(_) -> {ok, []}.
handle_event({set_alarm, Alarm = #mqtt_alarm{id = AlarmId,
severity = Severity,
title = Title,
summary = Summary}}, Alarms)->
TS = os:timestamp(),
Json = mochijson2:encode([{id, AlarmId},
{severity, Severity},
{title, iolist_to_binary(Title)},
{summary, iolist_to_binary(Summary)},
{ts, emqttd_time:now_secs(TS)}]),
emqttd:publish(alarm_msg(alert, AlarmId, Json)),
{ok, [Alarm#mqtt_alarm{timestamp = TS} | Alarms]};
handle_event({clear_alarm, AlarmId}, Alarms) ->
Json = mochijson2:encode([{id, AlarmId}, {ts, emqttd_time:now_secs()}]),
emqttd:publish(alarm_msg(clear, AlarmId, Json)),
{ok, lists:keydelete(AlarmId, 2, Alarms), hibernate};
handle_event(_, Alarms)->
{ok, Alarms}.
handle_info(_, Alarms) ->
{ok, Alarms}.
handle_call(get_alarms, Alarms) ->
{ok, Alarms, Alarms};
handle_call(_Query, Alarms) ->
{ok, {error, bad_query}, Alarms}.
terminate(swap, Alarms) ->
{?MODULE, Alarms};
terminate(_, _) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
alarm_msg(Type, AlarmId, Json) ->
Msg = emqttd_message:make(alarm,
topic(Type, AlarmId),
iolist_to_binary(Json)),
emqttd_message:set_flag(sys, Msg).
topic(alert, AlarmId) ->
emqttd_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>);
topic(clear, AlarmId) ->
emqttd_topic:systop(<<"alarms/", AlarmId/binary, "/clear">>).

View File

@ -1,242 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_app).
-behaviour(application).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd_cli.hrl").
-include("emqttd_protocol.hrl").
%% Application callbacks
-export([start/2, stop/1]).
-export([start_listener/1, stop_listener/1, restart_listener/1]).
-type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}).
-define(APP, emqttd).
%%--------------------------------------------------------------------
%% Application Callbacks
%%--------------------------------------------------------------------
start(_Type, _Args) ->
print_banner(),
ekka:start(),
{ok, Sup} = emqttd_sup:start_link(),
start_servers(Sup),
emqttd_cli:load(),
register_acl_mod(),
start_autocluster(),
register(emqttd, self()),
print_vsn(),
{ok, Sup}.
-spec(stop(State :: term()) -> term()).
stop(_State) ->
catch stop_listeners().
%%--------------------------------------------------------------------
%% Print Banner
%%--------------------------------------------------------------------
print_banner() ->
?PRINT("starting ~s on node '~s'~n", [?APP, node()]).
print_vsn() ->
{ok, Vsn} = application:get_key(vsn),
?PRINT("~s ~s is running now~n", [?APP, Vsn]).
%%--------------------------------------------------------------------
%% Start Servers
%%--------------------------------------------------------------------
start_servers(Sup) ->
Servers = [{"emqttd ctl", emqttd_ctl},
{"emqttd hook", emqttd_hooks},
{"emqttd router", emqttd_router},
{"emqttd pubsub", {supervisor, emqttd_pubsub_sup}},
{"emqttd stats", emqttd_stats},
{"emqttd metrics", emqttd_metrics},
{"emqttd pooler", {supervisor, emqttd_pooler}},
{"emqttd trace", {supervisor, emqttd_trace_sup}},
{"emqttd client manager", {supervisor, emqttd_cm_sup}},
{"emqttd session manager", {supervisor, emqttd_sm_sup}},
{"emqttd session supervisor", {supervisor, emqttd_session_sup}},
{"emqttd wsclient supervisor", {supervisor, emqttd_ws_client_sup}},
{"emqttd broker", emqttd_broker},
{"emqttd alarm", emqttd_alarm},
{"emqttd mod supervisor", emqttd_mod_sup},
{"emqttd bridge supervisor", {supervisor, emqttd_bridge_sup_sup}},
{"emqttd access control", emqttd_access_control},
{"emqttd system monitor", {supervisor, emqttd_sysmon_sup}}],
[start_server(Sup, Server) || Server <- Servers].
start_server(_Sup, {Name, F}) when is_function(F) ->
?PRINT("~s is starting...", [Name]),
F(),
?PRINT_MSG("[ok]~n");
start_server(Sup, {Name, Server}) ->
?PRINT("~s is starting...", [Name]),
start_child(Sup, Server),
?PRINT_MSG("[ok]~n");
start_server(Sup, {Name, Server, Opts}) ->
?PRINT("~s is starting...", [ Name]),
start_child(Sup, Server, Opts),
?PRINT_MSG("[ok]~n").
start_child(Sup, {supervisor, Module}) ->
supervisor:start_child(Sup, supervisor_spec(Module));
start_child(Sup, Module) when is_atom(Module) ->
{ok, _ChiId} = supervisor:start_child(Sup, worker_spec(Module)).
start_child(Sup, {supervisor, Module}, Opts) ->
supervisor:start_child(Sup, supervisor_spec(Module, Opts));
start_child(Sup, Module, Opts) when is_atom(Module) ->
supervisor:start_child(Sup, worker_spec(Module, Opts)).
supervisor_spec(Module) when is_atom(Module) ->
supervisor_spec(Module, start_link, []).
supervisor_spec(Module, Opts) ->
supervisor_spec(Module, start_link, [Opts]).
supervisor_spec(M, F, A) ->
{M, {M, F, A}, permanent, infinity, supervisor, [M]}.
worker_spec(Module) when is_atom(Module) ->
worker_spec(Module, start_link, []).
worker_spec(Module, Opts) when is_atom(Module) ->
worker_spec(Module, start_link, [Opts]).
worker_spec(M, F, A) ->
{M, {M, F, A}, permanent, 10000, worker, [M]}.
%%--------------------------------------------------------------------
%% Register default ACL File
%%--------------------------------------------------------------------
register_acl_mod() ->
case emqttd:env(acl_file) of
{ok, File} -> emqttd_access_control:register_mod(acl, emqttd_acl_internal, [File]);
undefined -> ok
end.
%%--------------------------------------------------------------------
%% Autocluster
%%--------------------------------------------------------------------
start_autocluster() ->
ekka:callback(prepare, fun emqttd:shutdown/1),
ekka:callback(reboot, fun emqttd:reboot/0),
ekka:autocluster(?APP, fun after_autocluster/0).
after_autocluster() ->
emqttd_plugins:init(),
emqttd_plugins:load(),
start_listeners().
%%--------------------------------------------------------------------
%% Start Listeners
%%--------------------------------------------------------------------
%% @doc Start Listeners of the broker.
-spec(start_listeners() -> any()).
start_listeners() -> lists:foreach(fun start_listener/1, emqttd:env(listeners, [])).
%% Start mqtt listener
-spec(start_listener(listener()) -> any()).
start_listener({tcp, ListenOn, Opts}) ->
start_listener('mqtt:tcp', ListenOn, Opts);
%% Start mqtt(SSL) listener
start_listener({ssl, ListenOn, Opts}) ->
start_listener('mqtt:ssl', ListenOn, Opts);
%% Start http listener
start_listener({Proto, ListenOn, Opts}) when Proto == http; Proto == ws ->
mochiweb:start_http('mqtt:ws', ListenOn, Opts, {emqttd_ws, handle_request, []});
%% Start https listener
start_listener({Proto, ListenOn, Opts}) when Proto == https; Proto == wss ->
mochiweb:start_http('mqtt:wss', ListenOn, Opts, {emqttd_ws, handle_request, []});
start_listener({Proto, ListenOn, Opts}) when Proto == api ->
mochiweb:start_http('mqtt:api', ListenOn, Opts, emqttd_http:http_handler()).
start_listener(Proto, ListenOn, Opts) ->
Env = lists:append(emqttd:env(client, []), emqttd:env(protocol, [])),
MFArgs = {emqttd_client, start_link, [Env]},
{ok, _} = esockd:open(Proto, ListenOn, merge_sockopts(Opts), MFArgs).
merge_sockopts(Options) ->
SockOpts = emqttd_misc:merge_opts(
?MQTT_SOCKOPTS, proplists:get_value(sockopts, Options, [])),
emqttd_misc:merge_opts(Options, [{sockopts, SockOpts}]).
%%--------------------------------------------------------------------
%% Stop Listeners
%%--------------------------------------------------------------------
%% @doc Stop Listeners
stop_listeners() -> lists:foreach(fun stop_listener/1, emqttd:env(listeners, [])).
%% @private
stop_listener({tcp, ListenOn, _Opts}) ->
esockd:close('mqtt:tcp', ListenOn);
stop_listener({ssl, ListenOn, _Opts}) ->
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 == api ->
mochiweb:stop_http('mqtt:api', ListenOn);
stop_listener({Proto, ListenOn, _Opts}) ->
esockd:close(Proto, ListenOn).
%% @doc Restart Listeners
restart_listener({tcp, ListenOn, _Opts}) ->
esockd:reopen('mqtt:tcp', ListenOn);
restart_listener({ssl, ListenOn, _Opts}) ->
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, _Opts}) when Proto == api ->
mochiweb:restart_http('mqtt:api', ListenOn);
restart_listener({Proto, ListenOn, _Opts}) ->
esockd:reopen(Proto, ListenOn).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
merge_sockopts_test_() ->
Opts = [{acceptors, 16}, {max_clients, 512}],
?_assert(merge_sockopts(Opts) == [{sockopts, ?MQTT_SOCKOPTS} | Opts]).
-endif.

View File

@ -1,80 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_auth_mod).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-export([passwd_hash/2]).
-type(hash_type() :: plain | md5 | sha | sha256 | pbkdf2 | bcrypt).
%%--------------------------------------------------------------------
%% Authentication behavihour
%%--------------------------------------------------------------------
-ifdef(use_specs).
-callback(init(AuthOpts :: list()) -> {ok, State :: any()}).
-callback(check(Client :: mqtt_client(),
Password :: binary(),
State :: any())
-> ok | | {ok, boolean()} | ignore | {error, string()}).
-callback(description() -> string()).
-else.
-export([behaviour_info/1]).
behaviour_info(callbacks) ->
[{init, 1}, {check, 3}, {description, 0}];
behaviour_info(_Other) ->
undefined.
-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} -> lager:error("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}-> lager:error("PasswdHash with bcrypt error:~p", [Error]), <<>>
end.
hexstring(<<X:128/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~32.16.0b", [X]));
hexstring(<<X:160/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~40.16.0b", [X]));
hexstring(<<X:256/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~64.16.0b", [X])).

View File

@ -1,60 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_base62).
-author("Feng Lee <feng@emqtt.io>").
-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]).
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()).
decode(B) when is_binary(B) ->
decode(binary_to_list(B));
decode(S) when is_list(S) ->
decode(S, 0).
decode([], I) ->
I;
decode([C|S], I) ->
decode(S, I * 62 + byte(C)).
byte(C) when $0 =< C andalso C =< $9 ->
C - $0;
byte(C) when $A =< C andalso C =< $Z ->
C - $A + 10;
byte(C) when $a =< C andalso C =< $z ->
C - $a + 36.

View File

@ -1,65 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_boot).
-author("Feng Lee <feng@emqtt.io>").
-export([apply_module_attributes/1, all_module_attributes/1]).
%% only {F, Args}...
apply_module_attributes(Name) ->
[{Module, [apply(Module, F, Args) || {F, Args} <- Attrs]} ||
{_App, Module, Attrs} <- all_module_attributes(Name)].
%% Copy from rabbit_misc.erl
all_module_attributes(Name) ->
Targets =
lists:usort(
lists:append(
[[{App, Module} || Module <- Modules] ||
{App, _, _} <- ignore_lib_apps(application:loaded_applications()),
{ok, Modules} <- [application:get_key(App, modules)]])),
lists:foldl(
fun ({App, Module}, Acc) ->
case lists:append([Atts || {N, Atts} <- module_attributes(Module),
N =:= Name]) of
[] -> Acc;
Atts -> [{App, Module, Atts} | Acc]
end
end, [], Targets).
%% Copy from rabbit_misc.erl
module_attributes(Module) ->
case catch Module:module_info(attributes) of
{'EXIT', {undef, [{Module, module_info, [attributes], []} | _]}} ->
[];
{'EXIT', Reason} ->
exit(Reason);
V ->
V
end.
ignore_lib_apps(Apps) ->
LibApps = [kernel, stdlib, sasl, appmon, eldap, erts,
syntax_tools, ssl, crypto, mnesia, os_mon,
inets, goldrush, lager, gproc, runtime_tools,
snmp, otp_mibs, public_key, asn1, ssh, hipe,
common_test, observer, webtool, xmerl, tools,
test_server, compiler, debugger, eunit, et,
wx],
[App || App = {Name, _, _} <- Apps, not lists:member(Name, LibApps)].

View File

@ -1,30 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_bridge_sup).
-export([start_link/3]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Start bridge pool supervisor
-spec(start_link(atom(), binary(), [emqttd_bridge:option()]) -> {ok, pid()} | {error, term()}).
start_link(Node, Topic, Options) ->
MFA = {emqttd_bridge, start_link, [Node, Topic, Options]},
emqttd_pool_sup:start_link({bridge, Node, Topic}, random, MFA).

View File

@ -1,204 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_broker).
-behaviour(gen_server).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_internal.hrl").
%% API Function Exports
-export([start_link/0]).
%% Event API
-export([subscribe/1, notify/2]).
%% Broker API
-export([version/0, uptime/0, datetime/0, sysdescr/0, info/0]).
%% Tick API
-export([start_tick/1, stop_tick/1]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {started_at, sys_interval, heartbeat, ticker, version, sysdescr}).
-define(APP, emqttd).
-define(SERVER, ?MODULE).
-define(BROKER_TAB, mqtt_broker).
%% $SYS Topics of Broker
-define(SYSTOP_BROKERS, [
version, % Broker version
uptime, % Broker uptime
datetime, % Broker local datetime
sysdescr % Broker description
]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Start emqttd broker
-spec(start_link() -> {ok, pid()} | ignore | {error, term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @doc Subscribe broker event
-spec(subscribe(EventType :: any()) -> ok).
subscribe(EventType) ->
gproc:reg({p, l, {broker, EventType}}).
%% @doc Notify broker event
-spec(notify(EventType :: any(), Event :: any()) -> ok).
notify(EventType, Event) ->
gproc:send({p, l, {broker, EventType}}, {notify, EventType, self(), Event}).
%% @doc Get broker info
-spec(info() -> list(tuple())).
info() ->
[{version, version()},
{sysdescr, sysdescr()},
{uptime, uptime()},
{datetime, datetime()}].
%% @doc Get broker version
-spec(version() -> string()).
version() ->
{ok, Version} = application:get_key(?APP, vsn), Version.
%% @doc Get broker description
-spec(sysdescr() -> string()).
sysdescr() ->
{ok, Descr} = application:get_key(?APP, description), Descr.
%% @doc Get broker uptime
-spec(uptime() -> string()).
uptime() -> gen_server:call(?SERVER, uptime).
%% @doc Get broker datetime
-spec(datetime() -> string()).
datetime() ->
{{Y, M, D}, {H, MM, S}} = calendar:local_time(),
lists:flatten(
io_lib:format(
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])).
%% @doc Start a tick timer.
start_tick(Msg) ->
start_tick(emqttd:env(broker_sys_interval, 60000), Msg).
start_tick(0, _Msg) ->
undefined;
start_tick(Interval, Msg) when Interval > 0 ->
{ok, TRef} = timer:send_interval(Interval, Msg), TRef.
%% @doc Stop tick timer
stop_tick(undefined) ->
ok;
stop_tick(TRef) ->
timer:cancel(TRef).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([]) ->
emqttd_time:seed(),
ets:new(?BROKER_TAB, [set, public, named_table]),
% Tick
{ok, #state{started_at = os:timestamp(),
heartbeat = start_tick(1000, heartbeat),
version = list_to_binary(version()),
sysdescr = list_to_binary(sysdescr()),
ticker = start_tick(tick)}, hibernate}.
handle_call(uptime, _From, State) ->
{reply, uptime(State), State};
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info(heartbeat, State) ->
publish(uptime, list_to_binary(uptime(State))),
publish(datetime, list_to_binary(datetime())),
{noreply, State, hibernate};
handle_info(tick, State = #state{version = Version, sysdescr = Descr}) ->
retain(brokers),
retain(version, Version),
retain(sysdescr, Descr),
{noreply, State, hibernate};
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(_Reason, #state{heartbeat = Hb, ticker = TRef}) ->
stop_tick(Hb),
stop_tick(TRef),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
retain(brokers) ->
Payload = list_to_binary(string:join([atom_to_list(N) ||
N <- ekka_mnesia:running_nodes()], ",")),
Msg = emqttd_message:make(broker, <<"$SYS/brokers">>, Payload),
emqttd:publish(emqttd_message:set_flag(sys, emqttd_message:set_flag(retain, Msg))).
retain(Topic, Payload) when is_binary(Payload) ->
Msg = emqttd_message:make(broker, emqttd_topic:systop(Topic), Payload),
emqttd:publish(emqttd_message:set_flag(sys, emqttd_message:set_flag(retain, Msg))).
publish(Topic, Payload) when is_binary(Payload) ->
Msg = emqttd_message:make(broker, emqttd_topic:systop(Topic), Payload),
emqttd:publish(emqttd_message:set_flag(sys, Msg)).
uptime(#state{started_at = Ts}) ->
Secs = timer:now_diff(os:timestamp(), Ts) div 1000000,
lists:flatten(uptime(seconds, Secs)).
uptime(seconds, Secs) when Secs < 60 ->
[integer_to_list(Secs), " seconds"];
uptime(seconds, Secs) ->
[uptime(minutes, Secs div 60), integer_to_list(Secs rem 60), " seconds"];
uptime(minutes, M) when M < 60 ->
[integer_to_list(M), " minutes, "];
uptime(minutes, M) ->
[uptime(hours, M div 60), integer_to_list(M rem 60), " minutes, "];
uptime(hours, H) when H < 24 ->
[integer_to_list(H), " hours, "];
uptime(hours, H) ->
[uptime(days, H div 24), integer_to_list(H rem 24), " hours, "];
uptime(days, D) ->
[integer_to_list(D), " days,"].

View File

@ -1,613 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_cli).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_cli.hrl").
-include("emqttd_protocol.hrl").
-import(lists, [foreach/2]).
-import(proplists, [get_value/2]).
-export([load/0]).
-export([status/1, broker/1, cluster/1, clients/1, sessions/1,
routes/1, topics/1, subscriptions/1, plugins/1, bridges/1,
listeners/1, vm/1, mnesia/1, trace/1, acl/1]).
-define(PROC_INFOKEYS, [status,
memory,
message_queue_len,
total_heap_size,
heap_size,
stack_size,
reductions]).
-define(MAX_LIMIT, 10000).
-define(APP, emqttd).
load() ->
Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)],
[emqttd_ctl:register_cmd(Cmd, {?MODULE, Cmd}, []) || Cmd <- Cmds],
emqttd_cli_config:register_config().
is_cmd(Fun) ->
not lists:member(Fun, [init, load, module_info]).
%%--------------------------------------------------------------------
%% Commands
%%--------------------------------------------------------------------
%%--------------------------------------------------------------------
%% @doc Node status
status([]) ->
{InternalStatus, _ProvidedStatus} = init:get_status(),
?PRINT("Node ~p is ~p~n", [node(), InternalStatus]),
case lists:keysearch(?APP, 1, application:which_applications()) of
false ->
?PRINT_MSG("emqttd is not running~n");
{value, {?APP, _Desc, Vsn}} ->
?PRINT("emqttd ~s is running~n", [Vsn])
end;
status(_) ->
?PRINT_CMD("status", "Show broker status").
%%--------------------------------------------------------------------
%% @doc Query broker
broker([]) ->
Funs = [sysdescr, version, uptime, datetime],
foreach(fun(Fun) ->
?PRINT("~-10s: ~s~n", [Fun, emqttd_broker:Fun()])
end, Funs);
broker(["stats"]) ->
foreach(fun({Stat, Val}) ->
?PRINT("~-20s: ~w~n", [Stat, Val])
end, emqttd_stats:getstats());
broker(["metrics"]) ->
foreach(fun({Metric, Val}) ->
?PRINT("~-24s: ~w~n", [Metric, Val])
end, lists:sort(emqttd_metrics:all()));
broker(["pubsub"]) ->
Pubsubs = supervisor:which_children(emqttd_pubsub_sup:pubsub_pool()),
foreach(fun({{_, Id}, Pid, _, _}) ->
ProcInfo = erlang:process_info(Pid, ?PROC_INFOKEYS),
?PRINT("pubsub: ~w~n", [Id]),
foreach(fun({Key, Val}) ->
?PRINT(" ~-18s: ~w~n", [Key, Val])
end, ProcInfo)
end, lists:reverse(Pubsubs));
broker(_) ->
?USAGE([{"broker", "Show broker version, uptime and description"},
{"broker pubsub", "Show process_info of pubsub"},
{"broker stats", "Show broker statistics of clients, topics, subscribers"},
{"broker metrics", "Show broker metrics"}]).
%%--------------------------------------------------------------------
%% @doc Cluster with other nodes
cluster(["join", SNode]) ->
case ekka:join(ekka_node:parse_name(SNode)) of
ok ->
?PRINT_MSG("Join the cluster successfully.~n"),
cluster(["status"]);
ignore ->
?PRINT_MSG("Ignore.~n");
{error, Error} ->
?PRINT("Failed to join the cluster: ~p~n", [Error])
end;
cluster(["leave"]) ->
case ekka:leave() of
ok ->
?PRINT_MSG("Leave the cluster successfully.~n"),
cluster(["status"]);
{error, Error} ->
?PRINT("Failed to leave the cluster: ~p~n", [Error])
end;
cluster(["force-leave", SNode]) ->
case ekka:force_leave(ekka_node:parse_name(SNode)) of
ok ->
?PRINT_MSG("Remove the node from cluster successfully.~n"),
cluster(["status"]);
ignore ->
?PRINT_MSG("Ignore.~n");
{error, Error} ->
?PRINT("Failed to remove the node from cluster: ~p~n", [Error])
end;
cluster(["status"]) ->
?PRINT("Cluster status: ~p~n", [ekka_cluster:status()]);
cluster(_) ->
?USAGE([{"cluster join <Node>", "Join the cluster"},
{"cluster leave", "Leave the cluster"},
{"cluster force-leave <Node>","Force the node leave from cluster"},
{"cluster status", "Cluster status"}]).
%%--------------------------------------------------------------------
%% @doc ACL reload
acl(["reload"]) -> emqttd_access_control:reload_acl();
acl(_) -> ?USAGE([{"acl reload", "reload etc/acl.conf"}]).
%%--------------------------------------------------------------------
%% @doc Query clients
clients(["list"]) ->
dump(mqtt_client);
clients(["show", ClientId]) ->
if_client(ClientId, fun print/1);
clients(["kick", ClientId]) ->
if_client(ClientId, fun(#mqtt_client{client_pid = Pid}) -> emqttd_client:kick(Pid) end);
clients(_) ->
?USAGE([{"clients list", "List all clients"},
{"clients show <ClientId>", "Show a client"},
{"clients kick <ClientId>", "Kick out a client"}]).
if_client(ClientId, Fun) ->
case emqttd_cm:lookup(bin(ClientId)) of
undefined -> ?PRINT_MSG("Not Found.~n");
Client -> Fun(Client)
end.
%%--------------------------------------------------------------------
%% @doc Sessions Command
sessions(["list"]) ->
dump(mqtt_local_session);
%% performance issue?
sessions(["list", "persistent"]) ->
lists:foreach(fun print/1, ets:match_object(mqtt_local_session, {'_', '_', false, '_'}));
%% performance issue?
sessions(["list", "transient"]) ->
lists:foreach(fun print/1, ets:match_object(mqtt_local_session, {'_', '_', true, '_'}));
sessions(["show", ClientId]) ->
case ets:lookup(mqtt_local_session, bin(ClientId)) of
[] -> ?PRINT_MSG("Not Found.~n");
[SessInfo] -> print(SessInfo)
end;
sessions(_) ->
?USAGE([{"sessions list", "List all sessions"},
{"sessions list persistent", "List all persistent sessions"},
{"sessions list transient", "List all transient sessions"},
{"sessions show <ClientId>", "Show a session"}]).
%%--------------------------------------------------------------------
%% @doc Routes Command
routes(["list"]) ->
Routes = emqttd_router:dump(),
foreach(fun print/1, Routes);
routes(["show", Topic]) ->
Routes = lists:append(ets:lookup(mqtt_route, bin(Topic)),
ets:lookup(mqtt_local_route, bin(Topic))),
foreach(fun print/1, Routes);
routes(_) ->
?USAGE([{"routes list", "List all routes"},
{"routes show <Topic>", "Show a route"}]).
%%--------------------------------------------------------------------
%% @doc Topics Command
topics(["list"]) ->
lists:foreach(fun(Topic) -> ?PRINT("~s~n", [Topic]) end, emqttd:topics());
topics(["show", Topic]) ->
print(mnesia:dirty_read(mqtt_route, bin(Topic)));
topics(_) ->
?USAGE([{"topics list", "List all topics"},
{"topics show <Topic>", "Show a topic"}]).
subscriptions(["list"]) ->
lists:foreach(fun(Subscription) ->
print(subscription, Subscription)
end, ets:tab2list(mqtt_subscription));
subscriptions(["show", ClientId]) ->
case emqttd:subscriptions(bin(ClientId)) of
[] ->
?PRINT_MSG("Not Found.~n");
Subscriptions ->
[print(subscription, Sub) || Sub <- Subscriptions]
end;
subscriptions(["add", ClientId, Topic, QoS]) ->
if_valid_qos(QoS, fun(IntQos) ->
case emqttd_sm:lookup_session(bin(ClientId)) of
undefined ->
?PRINT_MSG("Error: Session not found!");
#mqtt_session{sess_pid = SessPid} ->
{Topic1, Options} = emqttd_topic:parse(bin(Topic)),
emqttd_session:subscribe(SessPid, [{Topic1, [{qos, IntQos}|Options]}]),
?PRINT_MSG("ok~n")
end
end);
subscriptions(["del", ClientId, Topic]) ->
case emqttd_sm:lookup_session(bin(ClientId)) of
undefined ->
?PRINT_MSG("Error: Session not found!");
#mqtt_session{sess_pid = SessPid} ->
emqttd_session:unsubscribe(SessPid, [emqttd_topic:parse(bin(Topic))]),
?PRINT_MSG("ok~n")
end;
subscriptions(_) ->
?USAGE([{"subscriptions list", "List all subscriptions"},
{"subscriptions show <ClientId>", "Show subscriptions of a client"},
{"subscriptions add <ClientId> <Topic> <QoS>", "Add a static subscription manually"},
{"subscriptions del <ClientId> <Topic>", "Delete a static subscription manually"}]).
% if_could_print(Tab, Fun) ->
% case mnesia:table_info(Tab, size) of
% Size when Size >= ?MAX_LIMIT ->
% ?PRINT("Could not list, too many ~ss: ~p~n", [Tab, Size]);
% _Size ->
% Keys = mnesia:dirty_all_keys(Tab),
% foreach(fun(Key) -> Fun(ets:lookup(Tab, Key)) end, Keys)
% end.
if_valid_qos(QoS, Fun) ->
try list_to_integer(QoS) of
Int when ?IS_QOS(Int) -> Fun(Int);
_ -> ?PRINT_MSG("QoS should be 0, 1, 2~n")
catch _:_ ->
?PRINT_MSG("QoS should be 0, 1, 2~n")
end.
plugins(["list"]) ->
foreach(fun print/1, emqttd_plugins:list());
plugins(["load", Name]) ->
case emqttd_plugins:load(list_to_atom(Name)) of
{ok, StartedApps} ->
?PRINT("Start apps: ~p~nPlugin ~s loaded successfully.~n", [StartedApps, Name]);
{error, Reason} ->
?PRINT("load plugin error: ~p~n", [Reason])
end;
plugins(["unload", Name]) ->
case emqttd_plugins:unload(list_to_atom(Name)) of
ok ->
?PRINT("Plugin ~s unloaded successfully.~n", [Name]);
{error, Reason} ->
?PRINT("unload plugin error: ~p~n", [Reason])
end;
plugins(_) ->
?USAGE([{"plugins list", "Show loaded plugins"},
{"plugins load <Plugin>", "Load plugin"},
{"plugins unload <Plugin>", "Unload plugin"}]).
%%--------------------------------------------------------------------
%% @doc Bridges command
bridges(["list"]) ->
foreach(fun({Node, Topic, _Pid}) ->
?PRINT("bridge: ~s--~s-->~s~n", [node(), Topic, Node])
end, emqttd_bridge_sup_sup:bridges());
bridges(["options"]) ->
?PRINT_MSG("Options:~n"),
?PRINT_MSG(" prefix = string~n"),
?PRINT_MSG(" suffix = string~n"),
?PRINT_MSG(" queue = integer~n"),
?PRINT_MSG("Example:~n"),
?PRINT_MSG(" prefix=abc/,suffix=/yxz,queue=1000~n");
bridges(["start", SNode, Topic]) ->
case emqttd_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic)) of
{ok, _} -> ?PRINT_MSG("bridge is started.~n");
{error, Error} -> ?PRINT("error: ~p~n", [Error])
end;
bridges(["start", SNode, Topic, OptStr]) ->
Opts = parse_opts(bridge, OptStr),
case emqttd_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic), Opts) of
{ok, _} -> ?PRINT_MSG("bridge is started.~n");
{error, Error} -> ?PRINT("error: ~p~n", [Error])
end;
bridges(["stop", SNode, Topic]) ->
case emqttd_bridge_sup_sup:stop_bridge(list_to_atom(SNode), list_to_binary(Topic)) of
ok -> ?PRINT_MSG("bridge is stopped.~n");
{error, Error} -> ?PRINT("error: ~p~n", [Error])
end;
bridges(_) ->
?USAGE([{"bridges list", "List bridges"},
{"bridges options", "Bridge options"},
{"bridges start <Node> <Topic>", "Start a bridge"},
{"bridges start <Node> <Topic> <Options>", "Start a bridge with options"},
{"bridges stop <Node> <Topic>", "Stop a bridge"}]).
parse_opts(Cmd, OptStr) ->
Tokens = string:tokens(OptStr, ","),
[parse_opt(Cmd, list_to_atom(Opt), Val)
|| [Opt, Val] <- [string:tokens(S, "=") || S <- Tokens]].
parse_opt(bridge, suffix, Suffix) ->
{topic_suffix, bin(Suffix)};
parse_opt(bridge, prefix, Prefix) ->
{topic_prefix, bin(Prefix)};
parse_opt(bridge, queue, Len) ->
{max_queue_len, list_to_integer(Len)};
parse_opt(_Cmd, Opt, _Val) ->
?PRINT("Bad Option: ~s~n", [Opt]).
%%--------------------------------------------------------------------
%% @doc vm command
vm([]) ->
vm(["all"]);
vm(["all"]) ->
[vm([Name]) || Name <- ["load", "memory", "process", "io", "ports"]];
vm(["load"]) ->
[?PRINT("cpu/~-20s: ~s~n", [L, V]) || {L, V} <- emqttd_vm:loads()];
vm(["memory"]) ->
[?PRINT("memory/~-17s: ~w~n", [Cat, Val]) || {Cat, Val} <- erlang:memory()];
vm(["process"]) ->
foreach(fun({Name, Key}) ->
?PRINT("process/~-16s: ~w~n", [Name, erlang:system_info(Key)])
end, [{limit, process_limit}, {count, process_count}]);
vm(["io"]) ->
IoInfo = erlang:system_info(check_io),
foreach(fun(Key) ->
?PRINT("io/~-21s: ~w~n", [Key, get_value(Key, IoInfo)])
end, [max_fds, active_fds]);
vm(["ports"]) ->
foreach(fun({Name, Key}) ->
?PRINT("ports/~-16s: ~w~n", [Name, erlang:system_info(Key)])
end, [{count, port_count}, {limit, port_limit}]);
vm(_) ->
?USAGE([{"vm all", "Show info of Erlang VM"},
{"vm load", "Show load of Erlang VM"},
{"vm memory", "Show memory of Erlang VM"},
{"vm process", "Show process of Erlang VM"},
{"vm io", "Show IO of Erlang VM"},
{"vm ports", "Show Ports of Erlang VM"}]).
%%--------------------------------------------------------------------
%% @doc mnesia Command
mnesia([]) ->
mnesia:system_info();
mnesia(_) ->
?PRINT_CMD("mnesia", "Mnesia system info").
%%--------------------------------------------------------------------
%% @doc Trace Command
trace(["list"]) ->
foreach(fun({{Who, Name}, LogFile}) ->
?PRINT("trace ~s ~s -> ~s~n", [Who, Name, LogFile])
end, emqttd_trace:all_traces());
trace(["client", ClientId, "off"]) ->
trace_off(client, ClientId);
trace(["client", ClientId, LogFile]) ->
trace_on(client, ClientId, LogFile);
trace(["topic", Topic, "off"]) ->
trace_off(topic, Topic);
trace(["topic", Topic, LogFile]) ->
trace_on(topic, Topic, LogFile);
trace(_) ->
?USAGE([{"trace list", "List all traces"},
{"trace client <ClientId> <LogFile>","Trace a client"},
{"trace client <ClientId> off", "Stop tracing a client"},
{"trace topic <Topic> <LogFile>", "Trace a topic"},
{"trace topic <Topic> off", "Stop tracing a Topic"}]).
trace_on(Who, Name, LogFile) ->
case emqttd_trace:start_trace({Who, iolist_to_binary(Name)}, LogFile) of
ok ->
?PRINT("trace ~s ~s successfully.~n", [Who, Name]);
{error, Error} ->
?PRINT("trace ~s ~s error: ~p~n", [Who, Name, Error])
end.
trace_off(Who, Name) ->
case emqttd_trace:stop_trace({Who, iolist_to_binary(Name)}) of
ok ->
?PRINT("stop tracing ~s ~s successfully.~n", [Who, Name]);
{error, Error} ->
?PRINT("stop tracing ~s ~s error: ~p.~n", [Who, Name, Error])
end.
%%--------------------------------------------------------------------
%% @doc Listeners Command
listeners([]) ->
foreach(fun({{Protocol, ListenOn}, Pid}) ->
Info = [{acceptors, esockd:get_acceptors(Pid)},
{max_clients, esockd:get_max_clients(Pid)},
{current_clients,esockd:get_current_clients(Pid)},
{shutdown_count, esockd:get_shutdown_count(Pid)}],
?PRINT("listener on ~s:~s~n", [Protocol, esockd:to_string(ListenOn)]),
foreach(fun({Key, Val}) ->
?PRINT(" ~-16s: ~w~n", [Key, Val])
end, Info)
end, esockd:listeners());
listeners(["start", Proto, ListenOn]) ->
case emqttd_app:start_listener({list_to_atom(Proto), parse_listenon(ListenOn), []}) of
{ok, _Pid} ->
io:format("Start ~s listener on ~s successfully.~n", [Proto, ListenOn]);
{error, Error} ->
io:format("Failed to Start ~s listener on ~s, error:~p~n", [Proto, ListenOn, Error])
end;
listeners(["restart", Proto, ListenOn]) ->
case emqttd_app:restart_listener({list_to_atom(Proto), parse_listenon(ListenOn), []}) of
{ok, _Pid} ->
io:format("Restart ~s listener on ~s successfully.~n", [Proto, ListenOn]);
{error, Error} ->
io:format("Failed to restart ~s listener on ~s, error:~p~n", [Proto, ListenOn, Error])
end;
listeners(["stop", Proto, ListenOn]) ->
case emqttd_app:stop_listener({list_to_atom(Proto), parse_listenon(ListenOn), []}) of
ok ->
io:format("Stop ~s listener on ~s successfully.~n", [Proto, ListenOn]);
{error, Error} ->
io:format("Failed to stop ~s listener on ~s, error:~p~n", [Proto, ListenOn, Error])
end;
listeners(_) ->
?USAGE([{"listeners", "List listeners"},
{"listeners restart <Proto> <Port>", "Restart a listener"},
{"listeners stop <Proto> <Port>", "Stop a listener"}]).
%%--------------------------------------------------------------------
%% Dump ETS
%%--------------------------------------------------------------------
dump(Table) ->
dump(Table, ets:first(Table)).
dump(_Table, '$end_of_table') ->
ok;
dump(Table, Key) ->
case ets:lookup(Table, Key) of
[Record] -> print(Record);
[] -> ok
end,
dump(Table, ets:next(Table, Key)).
print([]) ->
ok;
print(Routes = [#mqtt_route{topic = Topic} | _]) ->
Nodes = [atom_to_list(Node) || #mqtt_route{node = Node} <- Routes],
?PRINT("~s -> ~s~n", [Topic, string:join(Nodes, ",")]);
%% print(Subscriptions = [#mqtt_subscription{subid = ClientId} | _]) ->
%% TopicTable = [io_lib:format("~s:~w", [Topic, Qos])
%% || #mqtt_subscription{topic = Topic, qos = Qos} <- Subscriptions],
%% ?PRINT("~s -> ~s~n", [ClientId, string:join(TopicTable, ",")]);
%% print(Topics = [#mqtt_topic{}|_]) ->
%% foreach(fun print/1, Topics);
print(#mqtt_plugin{name = Name, version = Ver, descr = Descr, active = Active}) ->
?PRINT("Plugin(~s, version=~s, description=~s, active=~s)~n",
[Name, Ver, Descr, Active]);
print(#mqtt_client{client_id = ClientId, clean_sess = CleanSess, username = Username,
peername = Peername, connected_at = ConnectedAt}) ->
?PRINT("Client(~s, clean_sess=~s, username=~s, peername=~s, connected_at=~p)~n",
[ClientId, CleanSess, Username, emqttd_net:format(Peername),
emqttd_time:now_secs(ConnectedAt)]);
%% print(#mqtt_topic{topic = Topic, flags = Flags}) ->
%% ?PRINT("~s: ~s~n", [Topic, string:join([atom_to_list(F) || F <- Flags], ",")]);
print({route, Routes}) ->
foreach(fun print/1, Routes);
print({local_route, Routes}) ->
foreach(fun print/1, Routes);
print(#mqtt_route{topic = Topic, node = Node}) ->
?PRINT("~s -> ~s~n", [Topic, Node]);
print({Topic, Node}) ->
?PRINT("~s -> ~s~n", [Topic, Node]);
print({ClientId, _ClientPid, _Persistent, SessInfo}) ->
Data = lists:append(SessInfo, emqttd_stats:get_session_stats(ClientId)),
InfoKeys = [clean_sess,
subscriptions,
max_inflight,
inflight_len,
mqueue_len,
mqueue_dropped,
awaiting_rel_len,
deliver_msg,
enqueue_msg,
created_at],
?PRINT("Session(~s, clean_sess=~s, subscriptions=~w, max_inflight=~w, inflight=~w, "
"mqueue_len=~w, mqueue_dropped=~w, awaiting_rel=~w, "
"deliver_msg=~w, enqueue_msg=~w, created_at=~w)~n",
[ClientId | [format(Key, get_value(Key, Data)) || Key <- InfoKeys]]).
print(subscription, {Sub, {share, _Share, Topic}}) when is_pid(Sub) ->
?PRINT("~p -> ~s~n", [Sub, Topic]);
print(subscription, {Sub, Topic}) when is_pid(Sub) ->
?PRINT("~p -> ~s~n", [Sub, Topic]);
print(subscription, {{SubId, SubPid}, {share, _Share, Topic}})
when is_binary(SubId), is_pid(SubPid) ->
?PRINT("~s~p -> ~s~n", [SubId, SubPid, Topic]);
print(subscription, {{SubId, SubPid}, Topic})
when is_binary(SubId), is_pid(SubPid) ->
?PRINT("~s~p -> ~s~n", [SubId, SubPid, Topic]);
print(subscription, {Sub, Topic, Props}) ->
print(subscription, {Sub, Topic}),
lists:foreach(fun({K, V}) when is_binary(V) ->
?PRINT(" ~-8s: ~s~n", [K, V]);
({K, V}) ->
?PRINT(" ~-8s: ~w~n", [K, V]);
(K) ->
?PRINT(" ~-8s: true~n", [K])
end, Props).
format(created_at, Val) ->
emqttd_time:now_secs(Val);
format(_, Val) ->
Val.
bin(S) -> iolist_to_binary(S).
parse_listenon(ListenOn) ->
case string:tokens(ListenOn, ":") of
[Port] -> list_to_integer(Port);
[IP, Port] -> {IP, list_to_integer(Port)}
end.

View File

@ -1,362 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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 (emqttd_cli_config).
-export ([register_config_cli/0,
register_config/0,
run/1,
set_usage/0,
all_cfgs/0,
get_cfg/2,
get_cfg/3,
read_config/1,
write_config/2]).
-define(APP, emqttd).
-define(TAB, emqttd_config).
register_config() ->
application:start(clique),
F = fun() -> ekka_mnesia:running_nodes() end,
clique:register_node_finder(F),
register_config_cli(),
create_config_tab().
create_config_tab() ->
case ets:info(?TAB, name) of
undefined ->
ets:new(?TAB, [named_table, public]),
{ok, PluginsEtcDir} = emqttd:env(plugins_etc_dir),
Files = filelib:wildcard("*.conf", PluginsEtcDir),
lists:foreach(fun(File) ->
[FileName | _] = string:tokens(File, "."),
Configs = cuttlefish_conf:file(lists:concat([PluginsEtcDir, File])),
ets:insert(?TAB, {list_to_atom(FileName), Configs})
end, Files);
_ ->
ok
end.
read_config(App) ->
case ets:lookup(?TAB, App) of
[] -> [];
[{_, Value}] -> Value
end.
write_config(App, Terms) ->
ets:insert(?TAB, {App, Terms}).
run(Cmd) ->
clique:run(Cmd).
register_config_cli() ->
ok = clique_config:load_schema([code:priv_dir(?APP)], ?APP),
register_protocol_formatter(),
register_client_formatter(),
register_session_formatter(),
register_queue_formatter(),
register_lager_formatter(),
register_auth_config(),
register_protocol_config(),
register_connection_config(),
register_client_config(),
register_session_config(),
register_queue_config(),
register_broker_config(),
register_lager_config().
set_usage() ->
io:format("~-40s# ~-20s# ~-20s ~p~n", ["key", "value", "datatype", "app"]),
io:format("------------------------------------------------------------------------------------------------~n"),
lists:foreach(fun({Key, Val, Datatype, App}) ->
io:format("~-40s# ~-20s# ~-20s ~p~n", [Key, Val, Datatype, App])
end, all_cfgs()),
io:format("------------------------------------------------------------------------------------------------~n"),
io:format("Usage: set key=value --app=appname~n").
all_cfgs() ->
{Mappings, Mappings1} = lists:foldl(
fun({Key, {_, Map, _}}, {Acc, Acc1}) ->
Map1 = lists:map(fun(M) -> {cuttlefish_mapping:variable(M), Key} end, Map),
{Acc ++ Map, Acc1 ++ Map1}
end, {[], []}, ets:tab2list(clique_schema)),
lists:foldl(fun({Key, _}, Acc) ->
case lists:keyfind(cuttlefish_variable:tokenize(Key), 2, Mappings) of
false -> Acc;
Map ->
Datatype = format_datatype(cuttlefish_mapping:datatype(Map)),
App = proplists:get_value(cuttlefish_variable:tokenize(Key), Mappings1),
[{_, [Val0]}] = clique_config:show([Key], [{app, App}]),
Val = any_to_string(proplists:get_value(Key, Val0)),
[{Key, Val, Datatype, App} | Acc]
end
end, [],lists:sort(ets:tab2list(clique_config))).
get_cfg(App, Key) ->
get_cfg(App, Key, undefined).
get_cfg(App, Key, Def) ->
[{_, [Val0]}] = clique_config:show([Key], [{app, App}]),
proplists:get_value(Key, Val0, Def).
format_datatype(Value) ->
format_datatype(Value, "").
format_datatype([Head], Acc) when is_tuple(Head) ->
[Head1 | _] = erlang:tuple_to_list(Head),
lists:concat([Acc, Head1]);
format_datatype([Head], Acc) ->
lists:concat([Acc, Head]);
format_datatype([Head | Tail], Acc) when is_tuple(Head)->
[Head1 | _] = erlang:tuple_to_list(Head),
format_datatype(Tail, Acc ++ lists:concat([Head1, ", "]));
format_datatype([Head | Tail], Acc) ->
format_datatype(Tail, Acc ++ lists:concat([Head, ", "])).
%%--------------------------------------------------------------------
%% Auth/Acl
%%--------------------------------------------------------------------
register_auth_config() ->
ConfigKeys = ["mqtt.allow_anonymous",
"mqtt.acl_nomatch",
"mqtt.acl_file",
"mqtt.cache_acl"],
[clique:register_config(Key , fun auth_config_callback/2) || Key <- ConfigKeys],
ok = register_config_whitelist(ConfigKeys).
auth_config_callback([_, KeyStr], Value) ->
application:set_env(?APP, l2a(KeyStr), Value), " successfully\n".
%%--------------------------------------------------------------------
%% MQTT Protocol
%%--------------------------------------------------------------------
register_protocol_formatter() ->
ConfigKeys = ["max_clientid_len",
"max_packet_size",
"websocket_protocol_header",
"keepalive_backoff"],
[clique:register_formatter(["mqtt", Key], fun protocol_formatter_callback/2) || Key <- ConfigKeys].
protocol_formatter_callback([_, "websocket_protocol_header"], Params) ->
Params;
protocol_formatter_callback([_, Key], Params) ->
proplists:get_value(l2a(Key), Params).
register_protocol_config() ->
ConfigKeys = ["mqtt.max_clientid_len",
"mqtt.max_packet_size",
"mqtt.websocket_protocol_header",
"mqtt.keepalive_backoff"],
[clique:register_config(Key , fun protocol_config_callback/2) || Key <- ConfigKeys],
ok = register_config_whitelist(ConfigKeys).
protocol_config_callback([_AppStr, KeyStr], Value) ->
protocol_config_callback(protocol, l2a(KeyStr), Value).
protocol_config_callback(_App, websocket_protocol_header, Value) ->
application:set_env(?APP, websocket_protocol_header, Value),
" successfully\n";
protocol_config_callback(App, Key, Value) ->
{ok, Env} = emqttd:env(App),
application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})),
" successfully\n".
%%--------------------------------------------------------------------
%% MQTT Connection
%%--------------------------------------------------------------------
register_connection_config() ->
ConfigKeys = ["mqtt.conn.force_gc_count"],
[clique:register_config(Key , fun connection_config_callback/2) || Key <- ConfigKeys],
ok = register_config_whitelist(ConfigKeys).
connection_config_callback([_, KeyStr0, KeyStr1], Value) ->
KeyStr = lists:concat([KeyStr0, "_", KeyStr1]),
application:set_env(?APP, l2a(KeyStr), Value),
" successfully\n".
%%--------------------------------------------------------------------
%% MQTT Client
%%--------------------------------------------------------------------
register_client_formatter() ->
ConfigKeys = ["max_publish_rate",
"idle_timeout",
"enable_stats"],
[clique:register_formatter(["mqtt", "client", Key], fun client_formatter_callback/2) || Key <- ConfigKeys].
client_formatter_callback([_, _, Key], Params) ->
proplists:get_value(list_to_atom(Key), Params).
register_client_config() ->
ConfigKeys = ["mqtt.client.max_publish_rate",
"mqtt.client.idle_timeout",
"mqtt.client.enable_stats"],
[clique:register_config(Key , fun client_config_callback/2) || Key <- ConfigKeys],
ok = register_config_whitelist(ConfigKeys).
client_config_callback([_, AppStr, KeyStr], Value) ->
client_config_callback(l2a(AppStr), l2a(KeyStr), Value).
client_config_callback(App, idle_timeout, Value) ->
{ok, Env} = emqttd:env(App),
application:set_env(?APP, App, lists:keyreplace(client_idle_timeout, 1, Env, {client_idle_timeout, Value})),
" successfully\n";
client_config_callback(App, enable_stats, Value) ->
{ok, Env} = emqttd:env(App),
application:set_env(?APP, App, lists:keyreplace(client_enable_stats, 1, Env, {client_enable_stats, Value})),
" successfully\n";
client_config_callback(App, Key, Value) ->
{ok, Env} = emqttd:env(App),
application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})),
" successfully\n".
%%--------------------------------------------------------------------
%% session
%%--------------------------------------------------------------------
register_session_formatter() ->
ConfigKeys = ["max_subscriptions",
"upgrade_qos",
"max_inflight",
"retry_interval",
"max_awaiting_rel",
"await_rel_timeout",
"enable_stats",
"expiry_interval",
"ignore_loop_deliver"],
[clique:register_formatter(["mqtt", "session", Key], fun session_formatter_callback/2) || Key <- ConfigKeys].
session_formatter_callback([_, _, Key], Params) ->
proplists:get_value(list_to_atom(Key), Params).
register_session_config() ->
ConfigKeys = ["mqtt.session.max_subscriptions",
"mqtt.session.upgrade_qos",
"mqtt.session.max_inflight",
"mqtt.session.retry_interval",
"mqtt.session.max_awaiting_rel",
"mqtt.session.await_rel_timeout",
"mqtt.session.enable_stats",
"mqtt.session.expiry_interval",
"mqtt.session.ignore_loop_deliver"],
[clique:register_config(Key , fun session_config_callback/2) || Key <- ConfigKeys],
ok = register_config_whitelist(ConfigKeys).
session_config_callback([_, AppStr, KeyStr], Value) ->
session_config_callback(l2a(AppStr), l2a(KeyStr), Value).
session_config_callback(App, Key, Value) ->
{ok, Env} = emqttd:env(App),
application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})),
" successfully\n".
l2a(List) -> list_to_atom(List).
%%--------------------------------------------------------------------
%% MQTT MQueue
%%--------------------------------------------------------------------
register_queue_formatter() ->
ConfigKeys = ["type",
"priority",
"max_length",
"low_watermark",
"high_watermark",
"store_qos0"],
[clique:register_formatter(["mqtt", "mqueue", Key], fun queue_formatter_callback/2) || Key <- ConfigKeys].
queue_formatter_callback([_, _, Key], Params) ->
proplists:get_value(list_to_atom(Key), Params).
register_queue_config() ->
ConfigKeys = ["mqtt.mqueue.type",
"mqtt.mqueue.priority",
"mqtt.mqueue.max_length",
"mqtt.mqueue.low_watermark",
"mqtt.mqueue.high_watermark",
"mqtt.mqueue.store_qos0"],
[clique:register_config(Key , fun queue_config_callback/2) || Key <- ConfigKeys],
ok = register_config_whitelist(ConfigKeys).
queue_config_callback([_, AppStr, KeyStr], Value) ->
queue_config_callback(l2a(AppStr), l2a(KeyStr), Value).
queue_config_callback(App, low_watermark, Value) ->
{ok, Env} = emqttd:env(App),
application:set_env(?APP, App, lists:keyreplace(low_watermark, 1, Env, {low_watermark, Value})),
" successfully\n";
queue_config_callback(App, high_watermark, Value) ->
{ok, Env} = emqttd:env(App),
application:set_env(?APP, App, lists:keyreplace(high_watermark, 1, Env, {high_watermark, Value})),
" successfully\n";
queue_config_callback(App, Key, Value) ->
{ok, Env} = emqttd:env(App),
application:set_env(?APP, App, lists:keyreplace(Key, 1, Env, {Key, Value})),
" successfully\n".
%%--------------------------------------------------------------------
%% MQTT Broker
%%--------------------------------------------------------------------
register_broker_config() ->
ConfigKeys = ["mqtt.broker.sys_interval"],
[clique:register_config(Key , fun broker_config_callback/2) || Key <- ConfigKeys],
ok = register_config_whitelist(ConfigKeys).
broker_config_callback([_, KeyStr0, KeyStr1], Value) ->
KeyStr = lists:concat([KeyStr0, "_", KeyStr1]),
application:set_env(?APP, l2a(KeyStr), Value),
" successfully\n".
%%--------------------------------------------------------------------
%% MQTT Lager
%%--------------------------------------------------------------------
register_lager_formatter() ->
ConfigKeys = ["level"],
[clique:register_formatter(["log", "console", Key], fun lager_formatter_callback/2) || Key <- ConfigKeys].
lager_formatter_callback(_, Params) ->
proplists:get_value(lager_console_backend, Params).
register_lager_config() ->
ConfigKeys = ["log.console.level"],
[clique:register_config(Key , fun lager_config_callback/2) || Key <- ConfigKeys],
ok = register_config_whitelist(ConfigKeys).
lager_config_callback(_, Value) ->
lager:set_loglevel(lager_console_backend, Value),
" successfully\n".
register_config_whitelist(ConfigKeys) ->
clique:register_config_whitelist(ConfigKeys, ?APP).
%%--------------------------------------------------------------------
%% Inner Function
%%--------------------------------------------------------------------
any_to_string(I) when is_integer(I) ->
integer_to_list(I);
any_to_string(F) when is_float(F)->
float_to_list(F,[{decimals, 4}]);
any_to_string(A) when is_atom(A) ->
atom_to_list(A);
any_to_string(B) when is_binary(B) ->
binary_to_list(B);
any_to_string(L) when is_list(L) ->
L.

View File

@ -1,396 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc MQTT/TCP Connection.
-module(emqttd_client).
-behaviour(gen_server2).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-include("emqttd_internal.hrl").
-import(proplists, [get_value/2, get_value/3]).
%% API Function Exports
-export([start_link/2]).
%% 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([init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2]).
%% gen_server2 Callbacks
-export([prioritise_call/4, prioritise_info/3, handle_pre_hibernate/1]).
%% Client State
%% Unused fields: connname, peerhost, peerport
-record(client_state, {connection, peername, conn_state, await_recv,
rate_limit, packet_size, parser, proto_state,
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),
lager:Level("Client(~s): " ++ Format,
[esockd_net:format(State#client_state.peername) | Args])).
start_link(Conn, Env) ->
{ok, proc_lib:spawn_link(?MODULE, init, [[Conn, Env]])}.
info(CPid) ->
gen_server2:call(CPid, info).
stats(CPid) ->
gen_server2:call(CPid, stats).
kick(CPid) ->
gen_server2:call(CPid, kick).
set_rate_limit(Cpid, Rl) ->
gen_server2:call(Cpid, {set_rate_limit, Rl}).
get_rate_limit(Cpid) ->
gen_server2:call(Cpid, get_rate_limit).
subscribe(CPid, TopicTable) ->
CPid ! {subscribe, TopicTable}.
unsubscribe(CPid, Topics) ->
CPid ! {unsubscribe, Topics}.
session(CPid) ->
gen_server2:call(CPid, session, infinity).
clean_acl_cache(CPid, Topic) ->
gen_server2:call(CPid, {clean_acl_cache, Topic}).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([Conn0, Env]) ->
{ok, Conn} = Conn0:wait(),
case Conn:peername() of
{ok, Peername} -> do_init(Conn, Env, Peername);
{error, enotconn} -> Conn:fast_close(),
exit(normal);
{error, Reason} -> Conn:fast_close(),
exit({shutdown, Reason})
end.
do_init(Conn, Env, Peername) ->
%% Send Fun
SendFun = send_fun(Conn, Peername),
RateLimit = get_value(rate_limit, Conn:opts()),
PacketSize = get_value(max_packet_size, Env, ?MAX_PACKET_SIZE),
Parser = emqttd_parser:initial_state(PacketSize),
ProtoState = emqttd_protocol:init(Conn, Peername, SendFun, Env),
EnableStats = get_value(client_enable_stats, Env, false),
IdleTimout = get_value(client_idle_timeout, Env, 30000),
ForceGcCount = emqttd_gc:conn_max_gc_count(),
State = run_socket(#client_state{connection = Conn,
peername = Peername,
await_recv = false,
conn_state = running,
rate_limit = RateLimit,
packet_size = PacketSize,
parser = Parser,
proto_state = ProtoState,
enable_stats = EnableStats,
idle_timeout = IdleTimout,
force_gc_count = ForceGcCount}),
gen_server2:enter_loop(?MODULE, [], State, self(), IdleTimout,
{backoff, 2000, 2000, 20000}).
send_fun(Conn, Peername) ->
Self = self(),
fun(Packet) ->
Data = emqttd_serializer:serialize(Packet),
?LOG(debug, "SEND ~p", [Data], #client_state{peername = Peername}),
emqttd_metrics:inc('bytes/sent', iolist_size(Data)),
try Conn:async_send(Data) of
ok -> ok;
true -> ok; %% Compatible with esockd 4.x
{error, Reason} -> Self ! {shutdown, Reason}
catch
error:Error -> Self ! {shutdown, Error}
end
end.
prioritise_call(Msg, _From, _Len, _State) ->
case Msg of info -> 10; stats -> 10; state -> 10; _ -> 5 end.
prioritise_info(Msg, _Len, _State) ->
case Msg of {redeliver, _} -> 5; _ -> 0 end.
handle_pre_hibernate(State) ->
{hibernate, emqttd_gc:reset_conn_gc_count(#client_state.force_gc_count, emit_stats(State))}.
handle_call(info, From, State = #client_state{proto_state = ProtoState}) ->
ProtoInfo = emqttd_protocol:info(ProtoState),
ClientInfo = ?record_to_proplist(client_state, State, ?INFO_KEYS),
{reply, Stats, _, _} = handle_call(stats, From, State),
reply(lists:append([ClientInfo, ProtoInfo, Stats]), State);
handle_call(stats, _From, State = #client_state{proto_state = ProtoState}) ->
reply(lists:append([emqttd_misc:proc_stats(),
emqttd_protocol:stats(ProtoState),
sock_stats(State)]), State);
handle_call(kick, _From, State) ->
{stop, {shutdown, kick}, ok, State};
handle_call({set_rate_limit, Rl}, _From, State) ->
reply(ok, State#client_state{rate_limit = Rl});
handle_call(get_rate_limit, _From, State = #client_state{rate_limit = Rl}) ->
reply(Rl, State);
handle_call(session, _From, State = #client_state{proto_state = ProtoState}) ->
reply(emqttd_protocol:session(ProtoState), State);
handle_call({clean_acl_cache, Topic}, _From, State) ->
erase({acl, publish, Topic}),
reply(ok, State);
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info({subscribe, TopicTable}, State) ->
with_proto(
fun(ProtoState) ->
emqttd_protocol:subscribe(TopicTable, ProtoState)
end, State);
handle_info({unsubscribe, Topics}, State) ->
with_proto(
fun(ProtoState) ->
emqttd_protocol:unsubscribe(Topics, ProtoState)
end, State);
%% Asynchronous SUBACK
handle_info({suback, PacketId, GrantedQos}, State) ->
with_proto(
fun(ProtoState) ->
Packet = ?SUBACK_PACKET(PacketId, GrantedQos),
emqttd_protocol:send(Packet, ProtoState)
end, State);
handle_info({deliver, Message}, State) ->
with_proto(
fun(ProtoState) ->
emqttd_protocol:send(Message, ProtoState)
end, State);
handle_info({redeliver, {?PUBREL, PacketId}}, State) ->
with_proto(
fun(ProtoState) ->
emqttd_protocol:pubrel(PacketId, ProtoState)
end, State);
handle_info(emit_stats, State) ->
{noreply, emit_stats(State), hibernate};
handle_info(timeout, State) ->
shutdown(idle_timeout, State);
%% Fix issue #535
handle_info({shutdown, Error}, State) ->
shutdown(Error, State);
handle_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
?LOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State),
shutdown(conflict, State);
handle_info(activate_sock, State) ->
{noreply, run_socket(State#client_state{conn_state = running}), hibernate};
handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) ->
Size = iolist_size(Data),
?LOG(debug, "RECV ~p", [Data], State),
emqttd_metrics:inc('bytes/received', Size),
received(Data, rate_limit(Size, State#client_state{await_recv = false}));
handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) ->
shutdown(Reason, State);
handle_info({inet_reply, _Sock, ok}, State) ->
{noreply, gc(State), hibernate}; %% Tune GC
handle_info({inet_reply, _Sock, {error, Reason}}, State) ->
shutdown(Reason, State);
handle_info({keepalive, start, Interval}, State = #client_state{connection = Conn}) ->
?LOG(debug, "Keepalive at the interval of ~p", [Interval], State),
StatFun = fun() ->
case Conn:getstat([recv_oct]) of
{ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct};
{error, Error} -> {error, Error}
end
end,
case emqttd_keepalive:start(StatFun, Interval, {keepalive, check}) of
{ok, KeepAlive} ->
{noreply, State#client_state{keepalive = KeepAlive}, hibernate};
{error, Error} ->
?LOG(warning, "Keepalive error - ~p", [Error], State),
shutdown(Error, State)
end;
handle_info({keepalive, check}, State = #client_state{keepalive = KeepAlive}) ->
case emqttd_keepalive:check(KeepAlive) of
{ok, KeepAlive1} ->
{noreply, State#client_state{keepalive = KeepAlive1}, hibernate};
{error, timeout} ->
?LOG(debug, "Keepalive timeout", [], State),
shutdown(keepalive_timeout, State);
{error, Error} ->
?LOG(warning, "Keepalive error - ~p", [Error], State),
shutdown(Error, State)
end;
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(Reason, State = #client_state{connection = Conn,
keepalive = KeepAlive,
proto_state = ProtoState}) ->
?LOG(debug, "Terminated for ~p", [Reason], State),
Conn:fast_close(),
emqttd_keepalive:cancel(KeepAlive),
case {ProtoState, Reason} of
{undefined, _} ->
ok;
{_, {shutdown, Error}} ->
emqttd_protocol:shutdown(Error, ProtoState);
{_, Reason} ->
emqttd_protocol:shutdown(Reason, ProtoState)
end.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
%% Receive and Parse TCP Data
received(<<>>, State) ->
{noreply, gc(State), hibernate};
received(Bytes, State = #client_state{parser = Parser,
packet_size = PacketSize,
proto_state = ProtoState,
idle_timeout = IdleTimeout}) ->
case catch emqttd_parser:parse(Bytes, Parser) of
{more, NewParser} ->
{noreply, run_socket(State#client_state{parser = NewParser}), IdleTimeout};
{ok, Packet, Rest} ->
emqttd_metrics:received(Packet),
case emqttd_protocol:received(Packet, ProtoState) of
{ok, ProtoState1} ->
received(Rest, State#client_state{parser = emqttd_parser:initial_state(PacketSize),
proto_state = ProtoState1});
{error, Error} ->
?LOG(error, "Protocol error - ~p", [Error], State),
shutdown(Error, State);
{error, Error, ProtoState1} ->
shutdown(Error, State#client_state{proto_state = ProtoState1});
{stop, Reason, ProtoState1} ->
stop(Reason, State#client_state{proto_state = ProtoState1})
end;
{error, Error} ->
?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)
end.
rate_limit(_Size, State = #client_state{rate_limit = undefined}) ->
run_socket(State);
rate_limit(Size, State = #client_state{rate_limit = Rl}) ->
case Rl:check(Size) of
{0, Rl1} ->
run_socket(State#client_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#client_state{conn_state = blocked, rate_limit = Rl1}
end.
run_socket(State = #client_state{conn_state = blocked}) ->
State;
run_socket(State = #client_state{await_recv = true}) ->
State;
run_socket(State = #client_state{connection = Conn}) ->
Conn:async_recv(0, infinity),
State#client_state{await_recv = true}.
with_proto(Fun, State = #client_state{proto_state = ProtoState}) ->
{ok, ProtoState1} = Fun(ProtoState),
{noreply, State#client_state{proto_state = ProtoState1}, hibernate}.
emit_stats(State = #client_state{proto_state = ProtoState}) ->
emit_stats(emqttd_protocol:clientid(ProtoState), State).
emit_stats(_ClientId, State = #client_state{enable_stats = false}) ->
State;
emit_stats(undefined, State) ->
State;
emit_stats(ClientId, State) ->
{reply, Stats, _, _} = handle_call(stats, undefined, State),
emqttd_stats:set_client_stats(ClientId, Stats),
State.
sock_stats(#client_state{connection = Conn}) ->
case Conn:getstat(?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end.
reply(Reply, State) ->
{reply, Reply, State, hibernate}.
shutdown(Reason, State) ->
stop({shutdown, Reason}, State).
stop(Reason, State) ->
{stop, Reason, State}.
gc(State = #client_state{connection = Conn}) ->
Cb = fun() -> Conn:gc(), emit_stats(State) end,
emqttd_gc:maybe_force_gc(#client_state.force_gc_count, State, Cb).

View File

@ -1,160 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc MQTT Client Manager
-module(emqttd_cm).
-behaviour(gen_server2).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_internal.hrl").
%% API Exports
-export([start_link/3]).
-export([lookup/1, lookup_proc/1, reg/1, unreg/1]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%% gen_server2 priorities
-export([prioritise_call/4, prioritise_cast/3, prioritise_info/3]).
-record(state, {pool, id, statsfun, monitors}).
-define(POOL, ?MODULE).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Start Client Manager
-spec(start_link(atom(), pos_integer(), fun()) -> {ok, pid()} | ignore | {error, term()}).
start_link(Pool, Id, StatsFun) ->
gen_server2:start_link(?MODULE, [Pool, Id, StatsFun], []).
%% @doc Lookup Client by ClientId
-spec(lookup(binary()) -> mqtt_client() | undefined).
lookup(ClientId) when is_binary(ClientId) ->
case ets:lookup(mqtt_client, ClientId) of [Client] -> Client; [] -> undefined end.
%% @doc Lookup client pid by clientId
-spec(lookup_proc(binary()) -> pid() | undefined).
lookup_proc(ClientId) when is_binary(ClientId) ->
try ets:lookup_element(mqtt_client, ClientId, #mqtt_client.client_pid)
catch
error:badarg -> undefined
end.
%% @doc Register ClientId with Pid.
-spec(reg(mqtt_client()) -> ok).
reg(Client = #mqtt_client{client_id = ClientId}) ->
gen_server2:call(pick(ClientId), {reg, Client}, 120000).
%% @doc Unregister clientId with pid.
-spec(unreg(binary()) -> ok).
unreg(ClientId) when is_binary(ClientId) ->
gen_server2:cast(pick(ClientId), {unreg, ClientId, self()}).
pick(ClientId) -> gproc_pool:pick_worker(?POOL, ClientId).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Pool, Id, StatsFun]) ->
?GPROC_POOL(join, Pool, Id),
{ok, #state{pool = Pool, id = Id, statsfun = StatsFun, monitors = dict:new()}}.
prioritise_call(Req, _From, _Len, _State) ->
case Req of {reg, _Client} -> 2; _ -> 1 end.
prioritise_cast(Msg, _Len, _State) ->
case Msg of {unreg, _ClientId, _Pid} -> 9; _ -> 1 end.
prioritise_info(_Msg, _Len, _State) ->
3.
handle_call({reg, Client = #mqtt_client{client_id = ClientId,
client_pid = Pid}}, _From, State) ->
case lookup_proc(ClientId) of
Pid ->
{reply, ok, State};
_ ->
ets:insert(mqtt_client, Client),
{reply, ok, setstats(monitor_client(ClientId, Pid, State))}
end;
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast({unreg, ClientId, Pid}, State) ->
case lookup_proc(ClientId) of
Pid ->
ets:delete(mqtt_client, ClientId),
{noreply, setstats(State)};
_ ->
{noreply, State}
end;
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) ->
case dict:find(MRef, State#state.monitors) of
{ok, {ClientId, DownPid}} ->
case lookup_proc(ClientId) of
DownPid ->
emqttd_stats:del_client_stats(ClientId),
ets:delete(mqtt_client, ClientId);
_ ->
ignore
end,
{noreply, setstats(erase_monitor(MRef, State))};
error ->
lager:error("MRef of client ~p not found", [DownPid]),
{noreply, State}
end;
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(_Reason, #state{pool = Pool, id = Id}) ->
?GPROC_POOL(leave, Pool, Id), ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
monitor_client(ClientId, Pid, State = #state{monitors = Monitors}) ->
MRef = erlang:monitor(process, Pid),
State#state{monitors = dict:store(MRef, {ClientId, Pid}, Monitors)}.
erase_monitor(MRef, State = #state{monitors = Monitors}) ->
erlang:demonitor(MRef, [flush]),
State#state{monitors = dict:erase(MRef, Monitors)}.
setstats(State = #state{statsfun = StatsFun}) ->
StatsFun(ets:info(mqtt_client, size)), State.

View File

@ -1,58 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Client Manager Supervisor.
-module(emqttd_cm_sup).
-behaviour(supervisor).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
%% API
-export([start_link/0]).
%% Supervisor callbacks
-export([init/1]).
-define(CM, emqttd_cm).
-define(TAB, mqtt_client).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
%% Create client table
create_client_tab(),
%% CM Pool Sup
MFA = {?CM, start_link, [emqttd_stats:statsfun('clients/count', 'clients/max')]},
PoolSup = emqttd_pool_sup:spec([?CM, hash, erlang:system_info(schedulers), MFA]),
{ok, {{one_for_all, 10, 3600}, [PoolSup]}}.
create_client_tab() ->
case ets:info(?TAB, name) of
undefined ->
ets:new(?TAB, [ordered_set, named_table, public,
{keypos, 2}, {write_concurrency, true}]);
_ ->
ok
end.

View File

@ -1,114 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Hot Configuration
%%
%% TODO: How to persist the configuration?
%%
%% 1. Store in mnesia database?
%% 2. Store in dets?
%% 3. Store in data/app.config?
%%
-module(emqttd_config).
-export([read/1, write/2, dump/2, reload/1, get/2, get/3, set/3]).
-type(env() :: {atom(), term()}).
%% @doc Read the configuration of an application.
-spec(read(atom()) -> {ok, list(env())} | {error, term()}).
read(App) ->
%% TODO:
%% 1. Read the app.conf from etc folder
%% 2. Cuttlefish to read the conf
%% 3. Return the terms and schema
% {error, unsupported}.
{ok, read_(App)}.
%% @doc Reload configuration of an application.
-spec(reload(atom()) -> ok | {error, term()}).
reload(_App) ->
%% TODO
%% 1. Read the app.conf from etc folder
%% 2. Cuttlefish to generate config terms.
%% 3. set/3 to apply the config
ok.
-spec(write(atom(), list(env())) -> ok | {error, term()}).
write(App, Terms) ->
Configs = lists:map(fun({Key, Val}) ->
{cuttlefish_variable:tokenize(binary_to_list(Key)), binary_to_list(Val)}
end, Terms),
Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]),
Schema = cuttlefish_schema:files([Path]),
case cuttlefish_generator:map(Schema, Configs) of
[{App, Configs1}] ->
emqttd_cli_config:write_config(App, Configs),
lists:foreach(fun({Key, Val}) -> application:set_env(App, Key, Val) end, Configs1);
_ ->
error
end.
-spec(dump(atom(), list(env())) -> ok | {error, term()}).
dump(_App, _Terms) ->
%% TODO
ok.
-spec(set(atom(), list(), list()) -> ok).
set(App, Par, Val) ->
emqttd_cli_config:run(["config",
"set",
lists:concat([Par, "=", Val]),
lists:concat(["--app=", App])]).
-spec(get(atom(), list()) -> undefined | {ok, term()}).
get(App, Par) ->
case emqttd_cli_config:get_cfg(App, Par) of
undefined -> undefined;
Val -> {ok, Val}
end.
-spec(get(atom(), list(), atom()) -> term()).
get(App, Par, Def) ->
emqttd_cli_config:get_cfg(App, Par, Def).
read_(App) ->
Configs = emqttd_cli_config:read_config(App),
Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]),
case filelib:is_file(Path) of
false ->
[];
true ->
{_, Mappings, _} = cuttlefish_schema:files([Path]),
OptionalCfg = lists:foldl(fun(Map, Acc) ->
Key = cuttlefish_mapping:variable(Map),
case proplists:get_value(Key, Configs) of
undefined ->
[{cuttlefish_variable:format(Key), "", cuttlefish_mapping:doc(Map), false} | Acc];
_ -> Acc
end
end, [], Mappings),
RequiredCfg = lists:foldl(fun({Key, Val}, Acc) ->
case lists:keyfind(Key, 2, Mappings) of
false -> Acc;
Map ->
[{cuttlefish_variable:format(Key), Val, cuttlefish_mapping:doc(Map), true} | Acc]
end
end, [], Configs),
RequiredCfg ++ OptionalCfg
end.

View File

@ -1,176 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_ctl).
-behaviour(gen_server).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_cli.hrl").
-define(SERVER, ?MODULE).
%% API Function Exports
-export([start_link/0, register_cmd/2, register_cmd/3, unregister_cmd/1,
lookup/1, run/1]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {seq = 0}).
-define(CMD_TAB, mqttd_ctl_cmd).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @doc Register a command
-spec(register_cmd(atom(), {module(), atom()}) -> ok).
register_cmd(Cmd, MF) ->
register_cmd(Cmd, MF, []).
%% @doc Register a command with opts
-spec(register_cmd(atom(), {module(), atom()}, list()) -> ok).
register_cmd(Cmd, MF, Opts) ->
cast({register_cmd, Cmd, MF, Opts}).
%% @doc Unregister a command
-spec(unregister_cmd(atom()) -> ok).
unregister_cmd(Cmd) ->
cast({unregister_cmd, Cmd}).
cast(Msg) -> gen_server:cast(?SERVER, Msg).
%% @doc Run a command
-spec(run([string()]) -> any()).
run([]) -> usage(), ok;
run(["help"]) -> usage(), ok;
run(["set"] = CmdS) when length(CmdS) =:= 1 ->
emqttd_cli_config:set_usage(), ok;
run(["set" | _] = CmdS) ->
emqttd_cli_config:run(["config" | CmdS]), ok;
run(["show" | _] = CmdS) ->
emqttd_cli_config:run(["config" | CmdS]), ok;
run([CmdS|Args]) ->
case lookup(list_to_atom(CmdS)) of
[{Mod, Fun}] ->
try Mod:Fun(Args) of
_ -> ok
catch
_:Reason ->
io:format("Reason:~p, get_stacktrace:~p~n",
[Reason, erlang:get_stacktrace()]),
{error, Reason}
end;
[] ->
usage(),
{error, cmd_not_found}
end.
%% @doc Lookup a command
-spec(lookup(atom()) -> [{module(), atom()}]).
lookup(Cmd) ->
case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of
[El] -> El;
[] -> []
end.
%% @doc Usage
usage() ->
?PRINT("Usage: ~s~n", [?MODULE]),
[begin ?PRINT("~80..-s~n", [""]), Mod:Cmd(usage) end
|| {_, {Mod, Cmd}, _} <- ets:tab2list(?CMD_TAB)].
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([]) ->
ets:new(?CMD_TAB, [ordered_set, named_table, protected]),
{ok, #state{seq = 0}}.
handle_call(_Request, _From, State) ->
{reply, ok, State}.
handle_cast({register_cmd, Cmd, MF, Opts}, State = #state{seq = Seq}) ->
case ets:match(?CMD_TAB, {{'$1', Cmd}, '_', '_'}) of
[] ->
ets:insert(?CMD_TAB, {{Seq, Cmd}, MF, Opts});
[[OriginSeq] | _] ->
lager:warning("CLI: ~s is overidden by ~p", [Cmd, MF]),
ets:insert(?CMD_TAB, {{OriginSeq, Cmd}, MF, Opts})
end,
noreply(next_seq(State));
handle_cast({unregister_cmd, Cmd}, State) ->
ets:match_delete(?CMD_TAB, {{'_', Cmd}, '_', '_'}),
noreply(State);
handle_cast(_Msg, State) ->
noreply(State).
handle_info(_Info, State) ->
noreply(State).
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal Function Definitions
%%--------------------------------------------------------------------
noreply(State) ->
{noreply, State, hibernate}.
next_seq(State = #state{seq = Seq}) ->
State#state{seq = Seq + 1}.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
register_cmd_test_() ->
{setup,
fun() ->
{ok, InitState} = emqttd_ctl:init([]),
InitState
end,
fun(State) ->
ok = emqttd_ctl:terminate(shutdown, State)
end,
fun(State = #state{seq = Seq}) ->
emqttd_ctl:handle_cast({register_cmd, test0, {?MODULE, test0}, []}, State),
[?_assertMatch([{{0,test0},{?MODULE, test0}, []}], ets:lookup(?CMD_TAB, {Seq,test0}))]
end
}.
-endif.

View File

@ -1,50 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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.
%%--------------------------------------------------------------------
%% GC Utility functions.
-module(emqttd_gc).
-author("Feng Lee <feng@emqtt.io>").
-export([conn_max_gc_count/0, reset_conn_gc_count/2, maybe_force_gc/2,
maybe_force_gc/3]).
-spec(conn_max_gc_count() -> integer()).
conn_max_gc_count() ->
case emqttd:env(conn_force_gc_count) of
{ok, I} when I > 0 -> I + rand:uniform(I);
{ok, I} when I =< 0 -> undefined;
undefined -> undefined
end.
-spec(reset_conn_gc_count(pos_integer(), tuple()) -> tuple()).
reset_conn_gc_count(Pos, State) ->
case element(Pos, State) of
undefined -> State;
_I -> setelement(Pos, State, conn_max_gc_count())
end.
maybe_force_gc(Pos, State) ->
maybe_force_gc(Pos, State, fun() -> ok end).
maybe_force_gc(Pos, State, Cb) ->
case element(Pos, State) of
undefined -> State;
I when I =< 0 -> Cb(), garbage_collect(),
reset_conn_gc_count(Pos, State);
I -> setelement(Pos, State, I - 1)
end.

View File

@ -1,189 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_hooks).
-behaviour(gen_server).
-author("Feng Lee <feng@emqtt.io>").
%% Start
-export([start_link/0]).
%% Hooks API
-export([add/3, add/4, delete/2, run/2, run/3, lookup/1]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {}).
-type(hooktag() :: atom() | string() | binary()).
-export_type([hooktag/0]).
-record(callback, {tag :: hooktag(),
function :: function(),
init_args = [] :: list(any()),
priority = 0 :: integer()}).
-record(hook, {name :: atom(), callbacks = [] :: list(#callback{})}).
-define(HOOK_TAB, mqtt_hook).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% Hooks API
%%--------------------------------------------------------------------
-spec(add(atom(), function() | {hooktag(), function()}, list(any())) -> ok).
add(HookPoint, Function, InitArgs) when is_function(Function) ->
add(HookPoint, {undefined, Function}, InitArgs, 0);
add(HookPoint, {Tag, Function}, InitArgs) when is_function(Function) ->
add(HookPoint, {Tag, Function}, InitArgs, 0).
-spec(add(atom(), function() | {hooktag(), function()}, list(any()), integer()) -> ok).
add(HookPoint, Function, InitArgs, Priority) when is_function(Function) ->
add(HookPoint, {undefined, Function}, InitArgs, Priority);
add(HookPoint, {Tag, Function}, InitArgs, Priority) when is_function(Function) ->
gen_server:call(?MODULE, {add, HookPoint, {Tag, Function}, InitArgs, Priority}).
-spec(delete(atom(), function() | {hooktag(), function()}) -> ok).
delete(HookPoint, Function) when is_function(Function) ->
delete(HookPoint, {undefined, Function});
delete(HookPoint, {Tag, Function}) when is_function(Function) ->
gen_server:call(?MODULE, {delete, HookPoint, {Tag, Function}}).
%% @doc Run hooks without Acc.
-spec(run(atom(), list(Arg :: any())) -> ok | stop).
run(HookPoint, Args) ->
run_(lookup(HookPoint), Args).
-spec(run(atom(), list(Arg :: any()), any()) -> any()).
run(HookPoint, Args, Acc) ->
run_(lookup(HookPoint), Args, Acc).
%% @private
run_([#callback{function = Fun, init_args = InitArgs} | Callbacks], Args) ->
case apply(Fun, lists:append([Args, InitArgs])) of
ok -> run_(Callbacks, Args);
stop -> stop;
_Any -> run_(Callbacks, Args)
end;
run_([], _Args) ->
ok.
%% @private
run_([#callback{function = Fun, init_args = InitArgs} | Callbacks], Args, Acc) ->
case apply(Fun, lists:append([Args, [Acc], InitArgs])) of
ok -> run_(Callbacks, Args, Acc);
{ok, NewAcc} -> run_(Callbacks, Args, NewAcc);
stop -> {stop, Acc};
{stop, NewAcc} -> {stop, NewAcc};
_Any -> run_(Callbacks, Args, Acc)
end;
run_([], _Args, Acc) ->
{ok, Acc}.
-spec(lookup(atom()) -> [#callback{}]).
lookup(HookPoint) ->
case ets:lookup(?HOOK_TAB, HookPoint) of
[#hook{callbacks = Callbacks}] -> Callbacks;
[] -> []
end.
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([]) ->
ets:new(?HOOK_TAB, [set, protected, named_table, {keypos, #hook.name}]),
{ok, #state{}}.
handle_call({add, HookPoint, {Tag, Function}, InitArgs, Priority}, _From, State) ->
Callback = #callback{tag = Tag, function = Function,
init_args = InitArgs, priority = Priority},
{reply,
case ets:lookup(?HOOK_TAB, HookPoint) of
[#hook{callbacks = Callbacks}] ->
case contain_(Tag, Function, Callbacks) of
false ->
insert_hook_(HookPoint, add_callback_(Callback, Callbacks));
true ->
{error, already_hooked}
end;
[] ->
insert_hook_(HookPoint, [Callback])
end, State};
handle_call({delete, HookPoint, {Tag, Function}}, _From, State) ->
{reply,
case ets:lookup(?HOOK_TAB, HookPoint) of
[#hook{callbacks = Callbacks}] ->
case contain_(Tag, Function, Callbacks) of
true ->
insert_hook_(HookPoint, del_callback_(Tag, Function, Callbacks));
false ->
{error, not_found}
end;
[] ->
{error, not_found}
end, State};
handle_call(Req, _From, State) ->
{reply, {error, {unexpected_request, Req}}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
insert_hook_(HookPoint, Callbacks) ->
ets:insert(?HOOK_TAB, #hook{name = HookPoint, callbacks = Callbacks}), ok.
add_callback_(Callback, Callbacks) ->
lists:keymerge(#callback.priority, Callbacks, [Callback]).
del_callback_(Tag, Function, Callbacks) ->
lists:filter(
fun(#callback{tag = Tag1, function = Func1}) ->
not ((Tag =:= Tag1) andalso (Function =:= Func1))
end, Callbacks).
contain_(_Tag, _Function, []) ->
false;
contain_(Tag, Function, [#callback{tag = Tag, function = Function}|_Callbacks]) ->
true;
contain_(Tag, Function, [_Callback | Callbacks]) ->
contain_(Tag, Function, Callbacks).

View File

@ -1,235 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc HTTP publish API and websocket client.
-module(emqttd_http).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-import(proplists, [get_value/2, get_value/3]).
-export([http_handler/0, handle_request/2, http_api/0, inner_handle_request/2]).
-include("emqttd_internal.hrl").
-record(state, {dispatch}).
http_handler() ->
APIs = http_api(),
State = #state{dispatch = dispatcher(APIs)},
{?MODULE, handle_request, [State]}.
http_api() ->
Attr = emqttd_rest_api:module_info(attributes),
[{Regexp, Method, Function, Args} || {http_api, [{Regexp, Method, Function, Args}]} <- Attr].
%%--------------------------------------------------------------------
%% Handle HTTP Request
%%--------------------------------------------------------------------
handle_request(Req, State) ->
{Path, _, _} = mochiweb_util:urlsplit_path(Req:get(raw_path)),
case Path of
"/status" ->
handle_request("/status", Req, Req:get(method));
"/" ->
handle_request("/", Req, Req:get(method));
"/api/v2/auth" ->
handle_request(Path, Req, State);
_ ->
if_authorized(Req, fun() -> handle_request(Path, Req, State) end)
end.
inner_handle_request(Req, State) ->
{Path, _, _} = mochiweb_util:urlsplit_path(Req:get(raw_path)),
case Path of
"/api/v2/auth" -> handle_request(Path, Req, State);
_ -> if_authorized(Req, fun() -> handle_request(Path, Req, State) end)
end.
handle_request("/api/v2/" ++ Url, Req, #state{dispatch = Dispatch}) ->
Dispatch(Req, Url);
handle_request("/status", Req, Method) when Method =:= 'HEAD'; Method =:= 'GET' ->
{InternalStatus, _ProvidedStatus} = init:get_status(),
AppStatus = case lists:keysearch(emqttd, 1, application:which_applications()) of
false -> not_running;
{value, _Val} -> running
end,
Status = io_lib:format("Node ~s is ~s~nemqttd is ~s",
[node(), InternalStatus, AppStatus]),
Req:ok({"text/plain", iolist_to_binary(Status)});
handle_request("/", Req, Method) when Method =:= 'HEAD'; Method =:= 'GET' ->
respond(Req, 200, api_list());
handle_request(_, Req, #state{}) ->
respond(Req, 404, []).
dispatcher(APIs) ->
fun(Req, Url) ->
Method = Req:get(method),
case filter(APIs, Url, Method) of
[{Regexp, _Method, Function, FilterArgs}] ->
case params(Req) of
{error, Error1} ->
respond(Req, 200, Error1);
Params ->
case {check_params(Params, FilterArgs),
check_params_type(Params, FilterArgs)} of
{true, true} ->
{match, [MatchList0]} = re:run(Url, Regexp, [global, {capture, all_but_first, list}]),
MatchList = lists:map(fun mochiweb_util:unquote/1, MatchList0),
Args = lists:append([[Method, Params], MatchList]),
lager:debug("Mod:~p, Fun:~p, Args:~p", [emqttd_rest_api, Function, Args]),
case catch apply(emqttd_rest_api, Function, Args) of
{ok, Data} ->
respond(Req, 200, [{code, ?SUCCESS}, {result, Data}]);
{error, Error} ->
respond(Req, 200, Error);
{'EXIT', Reason} ->
lager:error("Execute API '~s' Error: ~p", [Url, Reason]),
respond(Req, 404, [])
end;
{false, _} ->
respond(Req, 200, [{code, ?ERROR7}, {message, <<"params error">>}]);
{_, false} ->
respond(Req, 200, [{code, ?ERROR8}, {message, <<"params type error">>}])
end
end;
_ ->
lager:error("No match Url:~p", [Url]),
respond(Req, 404, [])
end
end.
% %%--------------------------------------------------------------------
% %% Basic Authorization
% %%--------------------------------------------------------------------
if_authorized(Req, Fun) ->
case authorized(Req) of
true -> Fun();
false -> respond(Req, 401, [])
end.
authorized(Req) ->
case Req:get_header_value("Authorization") of
undefined ->
false;
"Basic " ++ BasicAuth ->
{Username, Password} = user_passwd(BasicAuth),
case emqttd_mgmt:check_user(Username, Password) of
ok ->
true;
{error, Reason} ->
lager:error("HTTP Auth failure: username=~s, reason=~p", [Username, Reason]),
false
end
end.
user_passwd(BasicAuth) ->
list_to_tuple(binary:split(base64:decode(BasicAuth), <<":">>)).
respond(Req, 401, Data) ->
Req:respond({401, [{"WWW-Authenticate", "Basic Realm=\"emqx control center\""}], Data});
respond(Req, 404, Data) ->
Req:respond({404, [{"Content-Type", "text/plain"}], Data});
respond(Req, 200, Data) ->
Req:respond({200, [{"Content-Type", "application/json"}], to_json(Data)});
respond(Req, Code, Data) ->
Req:respond({Code, [{"Content-Type", "text/plain"}], Data}).
filter(APIs, Url, Method) ->
lists:filter(fun({Regexp, Method1, _Function, _Args}) ->
case re:run(Url, Regexp, [global, {capture, all_but_first, list}]) of
{match, _} -> Method =:= Method1;
_ -> false
end
end, APIs).
params(Req) ->
Method = Req:get(method),
case Method of
'GET' ->
mochiweb_request:parse_qs(Req);
_ ->
case Req:recv_body() of
<<>> -> [];
undefined -> [];
Body ->
case jsx:is_json(Body) of
true -> jsx:decode(Body);
false ->
lager:error("Body:~p", [Body]),
{error, [{code, ?ERROR9}, {message, <<"Body not json">>}]}
end
end
end.
check_params(_Params, Args) when Args =:= [] ->
true;
check_params(Params, Args)->
not lists:any(fun({Item, _Type}) -> undefined =:= proplists:get_value(Item, Params) end, Args).
check_params_type(_Params, Args) when Args =:= [] ->
true;
check_params_type(Params, Args) ->
not lists:any(fun({Item, Type}) ->
Val = proplists:get_value(Item, Params),
case Type of
int -> not is_integer(Val);
binary -> not is_binary(Val);
bool -> not is_boolean(Val)
end
end, Args).
to_json([]) -> <<"[]">>;
to_json(Data) -> iolist_to_binary(mochijson2:encode(Data)).
api_list() ->
[{paths, [<<"api/v2/management/nodes">>,
<<"api/v2/management/nodes/{node_name}">>,
<<"api/v2/monitoring/nodes">>,
<<"api/v2/monitoring/nodes/{node_name}">>,
<<"api/v2/monitoring/listeners">>,
<<"api/v2/monitoring/listeners/{node_name}">>,
<<"api/v2/monitoring/metrics/">>,
<<"api/v2/monitoring/metrics/{node_name}">>,
<<"api/v2/monitoring/stats">>,
<<"api/v2/monitoring/stats/{node_name}">>,
<<"api/v2/nodes/{node_name}/clients">>,
<<"api/v2/nodes/{node_name}/clients/{clientid}">>,
<<"api/v2/clients/{clientid}">>,
<<"api/v2/clients/{clientid}/clean_acl_cache">>,
<<"api/v2/nodes/{node_name}/sessions">>,
<<"api/v2/nodes/{node_name}/sessions/{clientid}">>,
<<"api/v2/sessions/{clientid}">>,
<<"api/v2/nodes/{node_name}/subscriptions">>,
<<"api/v2/nodes/{node_name}/subscriptions/{clientid}">>,
<<"api/v2/subscriptions/{clientid}">>,
<<"api/v2/routes">>,
<<"api/v2/routes/{topic}">>,
<<"api/v2/mqtt/publish">>,
<<"api/v2/mqtt/subscribe">>,
<<"api/v2/mqtt/unsubscribe">>,
<<"api/v2/nodes/{node_name}/plugins">>,
<<"api/v2/nodes/{node_name}/plugins/{plugin_name}">>,
<<"api/v2/configs/{app}">>,
<<"api/v2/nodes/{node_name}/configs/{app}">>]}].

View File

@ -1,94 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Inflight Window that wraps the gb_trees.
-module(emqttd_inflight).
-author("Feng Lee <feng@emqtt.io>").
-export([new/1, contain/2, lookup/2, insert/3, update/3, delete/2, values/1,
to_list/1, size/1, max_size/1, is_full/1, is_empty/1, window/1]).
-type(inflight() :: {?MODULE, list()}).
-export_type([inflight/0]).
-spec(new(non_neg_integer()) -> inflight()).
new(MaxSize) when MaxSize >= 0 ->
{?MODULE, [MaxSize, gb_trees:empty()]}.
-spec(contain(Key :: any(), inflight()) -> boolean()).
contain(Key, {?MODULE, [_MaxSize, Tree]}) ->
gb_trees:is_defined(Key, Tree).
-spec(lookup(Key :: any(), inflight()) -> any()).
lookup(Key, {?MODULE, [_MaxSize, Tree]}) ->
gb_trees:get(Key, Tree).
-spec(insert(Key :: any(), Value :: any(), inflight()) -> inflight()).
insert(Key, Value, {?MODULE, [MaxSize, Tree]}) ->
{?MODULE, [MaxSize, gb_trees:insert(Key, Value, Tree)]}.
-spec(delete(Key :: any(), inflight()) -> inflight()).
delete(Key, {?MODULE, [MaxSize, Tree]}) ->
{?MODULE, [MaxSize, gb_trees:delete(Key, Tree)]}.
-spec(update(Key :: any(), Val :: any(), inflight()) -> inflight()).
update(Key, Val, {?MODULE, [MaxSize, Tree]}) ->
{?MODULE, [MaxSize, gb_trees:update(Key, Val, Tree)]}.
-spec(is_full(inflight()) -> boolean()).
is_full({?MODULE, [0, _Tree]}) ->
false;
is_full({?MODULE, [MaxSize, Tree]}) ->
MaxSize =< gb_trees:size(Tree).
-spec(is_empty(inflight()) -> boolean()).
is_empty({?MODULE, [_MaxSize, Tree]}) ->
gb_trees:is_empty(Tree).
-spec(smallest(inflight()) -> {K :: any(), V :: any()}).
smallest({?MODULE, [_MaxSize, Tree]}) ->
gb_trees:smallest(Tree).
-spec(largest(inflight()) -> {K :: any(), V :: any()}).
largest({?MODULE, [_MaxSize, Tree]}) ->
gb_trees:largest(Tree).
-spec(values(inflight()) -> list()).
values({?MODULE, [_MaxSize, Tree]}) ->
gb_trees:values(Tree).
-spec(to_list(inflight()) -> list({K :: any(), V :: any()})).
to_list({?MODULE, [_MaxSize, Tree]}) ->
gb_trees:to_list(Tree).
-spec(window(inflight()) -> list()).
window(Inflight = {?MODULE, [_MaxSize, Tree]}) ->
case gb_trees:is_empty(Tree) of
true -> [];
false -> [Key || {Key, _Val} <- [smallest(Inflight), largest(Inflight)]]
end.
-spec(size(inflight()) -> non_neg_integer()).
size({?MODULE, [_MaxSize, Tree]}) ->
gb_trees:size(Tree).
-spec(max_size(inflight()) -> non_neg_integer()).
max_size({?MODULE, [MaxSize, _Tree]}) ->
MaxSize.

View File

@ -1,158 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc MQTT Message Functions
-module(emqttd_message).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-export([make/3, make/4, from_packet/1, from_packet/2, from_packet/3,
to_packet/1]).
-export([set_flag/1, set_flag/2, unset_flag/1, unset_flag/2]).
-export([format/1]).
-type(msg_from() :: atom() | {binary(), undefined | binary()}).
%% @doc Make a message
-spec(make(msg_from(), binary(), binary()) -> mqtt_message()).
make(From, Topic, Payload) ->
make(From, ?QOS_0, Topic, Payload).
-spec(make(msg_from(), mqtt_qos(), binary(), binary()) -> mqtt_message()).
make(From, Qos, Topic, Payload) ->
#mqtt_message{id = msgid(),
from = From,
qos = ?QOS_I(Qos),
topic = Topic,
payload = Payload,
timestamp = os:timestamp()}.
%% @doc Message from Packet
-spec(from_packet(mqtt_packet()) -> mqtt_message()).
from_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
retain = Retain,
qos = Qos,
dup = Dup},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId},
payload = Payload}) ->
#mqtt_message{id = msgid(),
pktid = PacketId,
qos = Qos,
retain = Retain,
dup = Dup,
topic = Topic,
payload = Payload,
timestamp = os:timestamp()};
from_packet(#mqtt_packet_connect{will_flag = false}) ->
undefined;
from_packet(#mqtt_packet_connect{client_id = ClientId,
username = Username,
will_retain = Retain,
will_qos = Qos,
will_topic = Topic,
will_msg = Msg}) ->
#mqtt_message{id = msgid(),
topic = Topic,
from = {ClientId, Username},
retain = Retain,
qos = Qos,
dup = false,
payload = Msg,
timestamp = os:timestamp()}.
from_packet(ClientId, Packet) ->
Msg = from_packet(Packet),
Msg#mqtt_message{from = ClientId}.
from_packet(Username, ClientId, Packet) ->
Msg = from_packet(Packet),
Msg#mqtt_message{from = {ClientId, Username}}.
msgid() -> emqttd_guid:gen().
%% @doc Message to packet
-spec(to_packet(mqtt_message()) -> mqtt_packet()).
to_packet(#mqtt_message{pktid = PkgId,
qos = Qos,
retain = Retain,
dup = Dup,
topic = Topic,
payload = Payload}) ->
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = Qos,
retain = Retain,
dup = Dup},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = if
Qos =:= ?QOS_0 -> undefined;
true -> PkgId
end
},
payload = Payload}.
%% @doc set dup, retain flag
-spec(set_flag(mqtt_message()) -> mqtt_message()).
set_flag(Msg) ->
Msg#mqtt_message{dup = true, retain = true}.
-spec(set_flag(atom(), mqtt_message()) -> mqtt_message()).
set_flag(dup, Msg = #mqtt_message{dup = false}) ->
Msg#mqtt_message{dup = true};
set_flag(sys, Msg = #mqtt_message{sys = false}) ->
Msg#mqtt_message{sys = true};
set_flag(retain, Msg = #mqtt_message{retain = false}) ->
Msg#mqtt_message{retain = true};
set_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg.
%% @doc Unset dup, retain flag
-spec(unset_flag(mqtt_message()) -> mqtt_message()).
unset_flag(Msg) ->
Msg#mqtt_message{dup = false, retain = false}.
-spec(unset_flag(dup | retain | atom(), mqtt_message()) -> mqtt_message()).
unset_flag(dup, Msg = #mqtt_message{dup = true}) ->
Msg#mqtt_message{dup = false};
unset_flag(retain, Msg = #mqtt_message{retain = true}) ->
Msg#mqtt_message{retain = false};
unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg.
%% @doc Format MQTT Message
format(#mqtt_message{id = MsgId, pktid = PktId, from = {ClientId, Username},
qos = Qos, retain = Retain, dup = Dup, topic =Topic}) ->
io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s/~s, Topic=~s)",
[i(Qos), i(Retain), i(Dup), MsgId, PktId, Username, ClientId, Topic]);
%% TODO:...
format(#mqtt_message{id = MsgId, pktid = PktId, from = From,
qos = Qos, retain = Retain, dup = Dup, topic =Topic}) ->
io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s, Topic=~s)",
[i(Qos), i(Retain), i(Dup), MsgId, PktId, From, Topic]).
i(true) -> 1;
i(false) -> 0;
i(I) when is_integer(I) -> I.

View File

@ -1,295 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_metrics).
-behaviour(gen_server).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-define(SERVER, ?MODULE).
%% API Function Exports
-export([start_link/0]).
%% Received/Sent Metrics
-export([received/1, sent/1]).
-export([all/0, value/1, inc/1, inc/2, inc/3, dec/2, dec/3, set/2]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {tick}).
-define(METRIC_TAB, mqtt_metric).
%% Bytes sent and received of Broker
-define(SYSTOP_BYTES, [
{counter, 'bytes/received'}, % Total bytes received
{counter, 'bytes/sent'} % Total bytes sent
]).
%% Packets sent and received of Broker
-define(SYSTOP_PACKETS, [
{counter, 'packets/received'}, % All Packets received
{counter, 'packets/sent'}, % All Packets sent
{counter, 'packets/connect'}, % CONNECT Packets received
{counter, 'packets/connack'}, % CONNACK Packets sent
{counter, 'packets/publish/received'}, % PUBLISH packets received
{counter, 'packets/publish/sent'}, % PUBLISH packets sent
{counter, 'packets/puback/received'}, % PUBACK packets received
{counter, 'packets/puback/sent'}, % PUBACK packets sent
{counter, 'packets/puback/missed'}, % PUBACK packets missed
{counter, 'packets/pubrec/received'}, % PUBREC packets received
{counter, 'packets/pubrec/sent'}, % PUBREC packets sent
{counter, 'packets/pubrec/missed'}, % PUBREC packets missed
{counter, 'packets/pubrel/received'}, % PUBREL packets received
{counter, 'packets/pubrel/sent'}, % PUBREL packets sent
{counter, 'packets/pubrel/missed'}, % PUBREL packets missed
{counter, 'packets/pubcomp/received'}, % PUBCOMP packets received
{counter, 'packets/pubcomp/sent'}, % PUBCOMP packets sent
{counter, 'packets/pubcomp/missed'}, % PUBCOMP packets missed
{counter, 'packets/subscribe'}, % SUBSCRIBE Packets received
{counter, 'packets/suback'}, % SUBACK packets sent
{counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received
{counter, 'packets/unsuback'}, % UNSUBACK Packets sent
{counter, 'packets/pingreq'}, % PINGREQ packets received
{counter, 'packets/pingresp'}, % PINGRESP Packets sent
{counter, 'packets/disconnect'} % DISCONNECT Packets received
]).
%% Messages sent and received of broker
-define(SYSTOP_MESSAGES, [
{counter, 'messages/received'}, % All Messages received
{counter, 'messages/sent'}, % All Messages sent
{counter, 'messages/qos0/received'}, % QoS0 Messages received
{counter, 'messages/qos0/sent'}, % QoS0 Messages sent
{counter, 'messages/qos1/received'}, % QoS1 Messages received
{counter, 'messages/qos1/sent'}, % QoS1 Messages sent
{counter, 'messages/qos2/received'}, % QoS2 Messages received
{counter, 'messages/qos2/sent'}, % QoS2 Messages sent
{counter, 'messages/qos2/dropped'}, % QoS2 Messages dropped
{gauge, 'messages/retained'}, % Messagea retained
{counter, 'messages/dropped'} % Messages dropped
]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Start the metrics server
-spec(start_link() -> {ok, pid()} | ignore | {error, term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @doc Count packets received.
-spec(received(mqtt_packet()) -> ignore | non_neg_integer()).
received(Packet) ->
inc('packets/received'),
received1(Packet).
received1(?PUBLISH_PACKET(Qos, _PktId)) ->
inc('packets/publish/received'),
inc('messages/received'),
qos_received(Qos);
received1(?PACKET(Type)) ->
received2(Type).
received2(?CONNECT) ->
inc('packets/connect');
received2(?PUBACK) ->
inc('packets/puback/received');
received2(?PUBREC) ->
inc('packets/pubrec/received');
received2(?PUBREL) ->
inc('packets/pubrel/received');
received2(?PUBCOMP) ->
inc('packets/pubcomp/received');
received2(?SUBSCRIBE) ->
inc('packets/subscribe');
received2(?UNSUBSCRIBE) ->
inc('packets/unsubscribe');
received2(?PINGREQ) ->
inc('packets/pingreq');
received2(?DISCONNECT) ->
inc('packets/disconnect');
received2(_) ->
ignore.
qos_received(?QOS_0) ->
inc('messages/qos0/received');
qos_received(?QOS_1) ->
inc('messages/qos1/received');
qos_received(?QOS_2) ->
inc('messages/qos2/received').
%% @doc Count packets received. Will not count $SYS PUBLISH.
-spec(sent(mqtt_packet()) -> ignore | non_neg_integer()).
sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) ->
ignore;
sent(Packet) ->
inc('packets/sent'),
sent1(Packet).
sent1(?PUBLISH_PACKET(Qos, _PktId)) ->
inc('packets/publish/sent'),
inc('messages/sent'),
qos_sent(Qos);
sent1(?PACKET(Type)) ->
sent2(Type).
sent2(?CONNACK) ->
inc('packets/connack');
sent2(?PUBACK) ->
inc('packets/puback/sent');
sent2(?PUBREC) ->
inc('packets/pubrec/sent');
sent2(?PUBREL) ->
inc('packets/pubrel/sent');
sent2(?PUBCOMP) ->
inc('packets/pubcomp/sent');
sent2(?SUBACK) ->
inc('packets/suback');
sent2(?UNSUBACK) ->
inc('packets/unsuback');
sent2(?PINGRESP) ->
inc('packets/pingresp');
sent2(_Type) ->
ignore.
qos_sent(?QOS_0) ->
inc('messages/qos0/sent');
qos_sent(?QOS_1) ->
inc('messages/qos1/sent');
qos_sent(?QOS_2) ->
inc('messages/qos2/sent').
%% @doc Get all metrics
-spec(all() -> [{atom(), non_neg_integer()}]).
all() ->
maps:to_list(
ets:foldl(
fun({{Metric, _N}, Val}, Map) ->
case maps:find(Metric, Map) of
{ok, Count} -> maps:put(Metric, Count+Val, Map);
error -> maps:put(Metric, Val, Map)
end
end, #{}, ?METRIC_TAB)).
%% @doc Get metric value
-spec(value(atom()) -> non_neg_integer()).
value(Metric) ->
lists:sum(ets:select(?METRIC_TAB, [{{{Metric, '_'}, '$1'}, [], ['$1']}])).
%% @doc Increase counter
-spec(inc(atom()) -> non_neg_integer()).
inc(Metric) ->
inc(counter, Metric, 1).
%% @doc Increase metric value
-spec(inc({counter | gauge, atom()} | atom(), pos_integer()) -> non_neg_integer()).
inc({gauge, Metric}, Val) ->
inc(gauge, Metric, Val);
inc({counter, Metric}, Val) ->
inc(counter, Metric, Val);
inc(Metric, Val) when is_atom(Metric) ->
inc(counter, Metric, Val).
%% @doc Increase metric value
-spec(inc(counter | gauge, atom(), pos_integer()) -> pos_integer()).
inc(gauge, Metric, Val) ->
ets:update_counter(?METRIC_TAB, key(gauge, Metric), {2, Val});
inc(counter, Metric, Val) ->
ets:update_counter(?METRIC_TAB, key(counter, Metric), {2, Val}).
%% @doc Decrease metric value
-spec(dec(gauge, atom()) -> integer()).
dec(gauge, Metric) ->
dec(gauge, Metric, 1).
%% @doc Decrease metric value
-spec(dec(gauge, atom(), pos_integer()) -> integer()).
dec(gauge, Metric, Val) ->
ets:update_counter(?METRIC_TAB, key(gauge, Metric), {2, -Val}).
%% @doc Set metric value
set(Metric, Val) when is_atom(Metric) ->
set(gauge, Metric, Val).
set(gauge, Metric, Val) ->
ets:insert(?METRIC_TAB, {key(gauge, Metric), Val}).
%% @doc Metric Key
key(gauge, Metric) ->
{Metric, 0};
key(counter, Metric) ->
{Metric, erlang:system_info(scheduler_id)}.
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([]) ->
emqttd_time:seed(),
Metrics = ?SYSTOP_BYTES ++ ?SYSTOP_PACKETS ++ ?SYSTOP_MESSAGES,
% Create metrics table
ets:new(?METRIC_TAB, [set, public, named_table, {write_concurrency, true}]),
% Init metrics
[create_metric(Metric) || Metric <- Metrics],
% $SYS Topics for metrics
% [ok = emqttd:create(topic, metric_topic(Topic)) || {_, Topic} <- Metrics],
% Tick to publish metrics
{ok, #state{tick = emqttd_broker:start_tick(tick)}, hibernate}.
handle_call(_Req, _From, State) ->
{reply, error, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(tick, State) ->
% publish metric message
[publish(Metric, Val) || {Metric, Val} <- all()],
{noreply, State, hibernate};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{tick = TRef}) ->
emqttd_broker:stop_tick(TRef).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
publish(Metric, Val) ->
Msg = emqttd_message:make(metrics, metric_topic(Metric), bin(Val)),
emqttd:publish(emqttd_message:set_flag(sys, Msg)).
create_metric({gauge, Name}) ->
ets:insert(?METRIC_TAB, {{Name, 0}, 0});
create_metric({counter, Name}) ->
Schedulers = lists:seq(1, erlang:system_info(schedulers)),
ets:insert(?METRIC_TAB, [{{Name, I}, 0} || I <- Schedulers]).
metric_topic(Metric) ->
emqttd_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))).
bin(I) when is_integer(I) -> list_to_binary(integer_to_list(I)).

View File

@ -1,505 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_mgmt).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-include("emqttd_internal.hrl").
-include_lib("stdlib/include/qlc.hrl").
-record(mqtt_admin, {username, password, tags}).
-define(EMPTY_KEY(Key), ((Key == undefined) orelse (Key == <<>>))).
-import(proplists, [get_value/2]).
-export([brokers/0, broker/1, metrics/0, metrics/1, stats/1, stats/0,
plugins/0, plugins/1, listeners/0, listener/1, nodes_info/0, node_info/1]).
-export([plugin_list/1, plugin_unload/2, plugin_load/2]).
-export([client_list/4, session_list/4, route_list/3, subscription_list/4, alarm_list/0]).
-export([client/1, session/1, route/1, subscription/1]).
-export([query_table/4, lookup_table/3]).
-export([publish/1, subscribe/1, unsubscribe/1]).
-export([kick_client/1, kick_client/2, clean_acl_cache/2, clean_acl_cache/3]).
-export([modify_config/2, modify_config/3, modify_config/4, get_configs/0, get_config/1,
get_plugin_config/1, get_plugin_config/2, modify_plugin_config/2, modify_plugin_config/3]).
-export([add_user/3, check_user/2, user_list/0, lookup_user/1,
update_user/2, change_password/3, remove_user/1]).
-define(KB, 1024).
-define(MB, (1024*1024)).
-define(GB, (1024*1024*1024)).
brokers() ->
[{Node, broker(Node)} || Node <- ekka_mnesia:running_nodes()].
broker(Node) when Node =:= node() ->
emqttd_broker:info();
broker(Node) ->
rpc_call(Node, broker, [Node]).
metrics() ->
[{Node, metrics(Node)} || Node <- ekka_mnesia:running_nodes()].
metrics(Node) when Node =:= node() ->
emqttd_metrics:all();
metrics(Node) ->
rpc_call(Node, metrics, [Node]).
stats() ->
[{Node, stats(Node)} || Node <- ekka_mnesia:running_nodes()].
stats(Node) when Node =:= node() ->
emqttd_stats:getstats();
stats(Node) ->
rpc_call(Node, stats, [Node]).
plugins() ->
[{Node, plugins(Node)} || Node <- ekka_mnesia:running_nodes()].
plugins(Node) when Node =:= node() ->
emqttd_plugins:list(Node);
plugins(Node) ->
rpc_call(Node, plugins, [Node]).
listeners() ->
[{Node, listener(Node)} || Node <- ekka_mnesia:running_nodes()].
listener(Node) when Node =:= node() ->
lists:map(fun({{Protocol, ListenOn}, Pid}) ->
Info = [{acceptors, esockd:get_acceptors(Pid)},
{max_clients, esockd:get_max_clients(Pid)},
{current_clients,esockd:get_current_clients(Pid)},
{shutdown_count, esockd:get_shutdown_count(Pid)}],
{Protocol, ListenOn, Info}
end, esockd:listeners());
listener(Node) ->
rpc_call(Node, listener, [Node]).
nodes_info() ->
Running = mnesia:system_info(running_db_nodes),
Stopped = mnesia:system_info(db_nodes) -- Running,
DownNodes = lists:map(fun stop_node/1, Stopped),
[node_info(Node) || Node <- Running] ++ DownNodes.
node_info(Node) when Node =:= node() ->
CpuInfo = [{K, list_to_binary(V)} || {K, V} <- emqttd_vm:loads()],
Memory = emqttd_vm:get_memory(),
OtpRel = "R" ++ erlang:system_info(otp_release) ++ "/" ++ erlang:system_info(version),
[{name, node()},
{otp_release, list_to_binary(OtpRel)},
{memory_total, kmg(get_value(allocated, Memory))},
{memory_used, kmg(get_value(used, Memory))},
{process_available, erlang:system_info(process_limit)},
{process_used, erlang:system_info(process_count)},
{max_fds, get_value(max_fds, erlang:system_info(check_io))},
{clients, ets:info(mqtt_client, size)},
{node_status, 'Running'} | CpuInfo];
node_info(Node) ->
rpc_call(Node, node_info, [Node]).
stop_node(Node) ->
[{name, Node}, {node_status, 'Stopped'}].
%%--------------------------------------------------------
%% plugins
%%--------------------------------------------------------
plugin_list(Node) when Node =:= node() ->
emqttd_plugins:list();
plugin_list(Node) ->
rpc_call(Node, plugin_list, [Node]).
plugin_load(Node, PluginName) when Node =:= node() ->
emqttd_plugins:load(PluginName);
plugin_load(Node, PluginName) ->
rpc_call(Node, plugin_load, [Node, PluginName]).
plugin_unload(Node, PluginName) when Node =:= node() ->
emqttd_plugins:unload(PluginName);
plugin_unload(Node, PluginName) ->
rpc_call(Node, plugin_unload, [Node, PluginName]).
%%--------------------------------------------------------
%% client
%%--------------------------------------------------------
client_list(Node, Key, PageNo, PageSize) when Node =:= node() ->
client_list(Key, PageNo, PageSize);
client_list(Node, Key, PageNo, PageSize) ->
rpc_call(Node, client_list, [Node, Key, PageNo, PageSize]).
client(ClientId) ->
lists:flatten([client_list(Node, ClientId, 1, 20) || Node <- ekka_mnesia:running_nodes()]).
%%--------------------------------------------------------
%% session
%%--------------------------------------------------------
session_list(Node, Key, PageNo, PageSize) when Node =:= node() ->
session_list(Key, PageNo, PageSize);
session_list(Node, Key, PageNo, PageSize) ->
rpc_call(Node, session_list, [Node, Key, PageNo, PageSize]).
session(ClientId) ->
lists:flatten([session_list(Node, ClientId, 1, 20) || Node <- ekka_mnesia:running_nodes()]).
%%--------------------------------------------------------
%% subscription
%%--------------------------------------------------------
subscription_list(Node, Key, PageNo, PageSize) when Node =:= node() ->
subscription_list(Key, PageNo, PageSize);
subscription_list(Node, Key, PageNo, PageSize) ->
rpc_call(Node, subscription_list, [Node, Key, PageNo, PageSize]).
subscription(Key) ->
lists:flatten([subscription_list(Node, Key, 1, 20) || Node <- ekka_mnesia:running_nodes()]).
%%--------------------------------------------------------
%% Routes
%%--------------------------------------------------------
route(Key) -> route_list(Key, 1, 20).
%%--------------------------------------------------------
%% alarm
%%--------------------------------------------------------
alarm_list() ->
emqttd_alarm:get_alarms().
query_table(Qh, PageNo, PageSize, TotalNum) ->
Cursor = qlc:cursor(Qh),
case PageNo > 1 of
true -> qlc:next_answers(Cursor, (PageNo - 1) * PageSize);
false -> ok
end,
Rows = qlc:next_answers(Cursor, PageSize),
qlc:delete_cursor(Cursor),
[{totalNum, TotalNum},
{totalPage, total_page(TotalNum, PageSize)},
{result, Rows}].
total_page(TotalNum, PageSize) ->
case TotalNum rem PageSize of
0 -> TotalNum div PageSize;
_ -> (TotalNum div PageSize) + 1
end.
%%TODO: refactor later...
lookup_table(LookupFun, _PageNo, _PageSize) ->
Rows = LookupFun(),
Rows.
%%--------------------------------------------------------------------
%% mqtt
%%--------------------------------------------------------------------
publish({ClientId, Topic, Payload, Qos, Retain}) ->
case validate(topic, Topic) of
true ->
Msg = emqttd_message:make(ClientId, Qos, Topic, Payload),
emqttd:publish(Msg#mqtt_message{retain = Retain}),
ok;
false ->
{error, format_error(Topic, "validate topic: ${0} fail")}
end.
subscribe({ClientId, Topic, Qos}) ->
case validate(topic, Topic) of
true ->
case emqttd_sm:lookup_session(ClientId) of
undefined ->
{error, format_error(ClientId, "Clientid: ${0} not found")};
#mqtt_session{sess_pid = SessPid} ->
emqttd_session:subscribe(SessPid, [{Topic, [{qos, Qos}]}]),
ok
end;
false ->
{error, format_error(Topic, "validate topic: ${0} fail")}
end.
unsubscribe({ClientId, Topic}) ->
case validate(topic, Topic) of
true ->
case emqttd_sm:lookup_session(ClientId) of
undefined ->
{error, format_error(ClientId, "Clientid: ${0} not found")};
#mqtt_session{sess_pid = SessPid} ->
emqttd_session:unsubscribe(SessPid, [{Topic, []}]),
ok
end;
false ->
{error, format_error(Topic, "validate topic: ${0} fail")}
end.
%%--------------------------------------------------------------------
%% manager API
%%--------------------------------------------------------------------
kick_client(ClientId) ->
Result = [kick_client(Node, ClientId) || Node <- ekka_mnesia:running_nodes()],
lists:any(fun(Item) -> Item =:= ok end, Result).
kick_client(Node, ClientId) when Node =:= node() ->
case emqttd_cm:lookup(ClientId) of
undefined -> error;
#mqtt_client{client_pid = Pid}-> emqttd_client:kick(Pid)
end;
kick_client(Node, ClientId) ->
rpc_call(Node, kick_client, [Node, ClientId]).
clean_acl_cache(ClientId, Topic) ->
Result = [clean_acl_cache(Node, ClientId, Topic) || Node <- ekka_mnesia:running_nodes()],
lists:any(fun(Item) -> Item =:= ok end, Result).
clean_acl_cache(Node, ClientId, Topic) when Node =:= node() ->
case emqttd_cm:lookup(ClientId) of
undefined -> error;
#mqtt_client{client_pid = Pid}-> emqttd_client:clean_acl_cache(Pid, Topic)
end;
clean_acl_cache(Node, ClientId, Topic) ->
rpc_call(Node, clean_acl_cache, [Node, ClientId, Topic]).
%%--------------------------------------------------------------------
%% Config ENV
%%--------------------------------------------------------------------
modify_config(App, Terms) ->
emqttd_config:write(App, Terms).
modify_config(App, Key, Value) ->
Result = [modify_config(Node, App, Key, Value) || Node <- ekka_mnesia:running_nodes()],
lists:any(fun(Item) -> Item =:= ok end, Result).
modify_config(Node, App, Key, Value) when Node =:= node() ->
emqttd_config:set(App, Key, Value);
modify_config(Node, App, Key, Value) ->
rpc_call(Node, modify_config, [Node, App, Key, Value]).
get_configs() ->
[{Node, get_config(Node)} || Node <- ekka_mnesia:running_nodes()].
get_config(Node) when Node =:= node()->
emqttd_cli_config:all_cfgs();
get_config(Node) ->
rpc_call(Node, get_config, [Node]).
get_plugin_config(PluginName) ->
emqttd_config:read(PluginName).
get_plugin_config(Node, PluginName) ->
rpc_call(Node, get_plugin_config, [PluginName]).
modify_plugin_config(PluginName, Terms) ->
emqttd_config:write(PluginName, Terms).
modify_plugin_config(Node, PluginName, Terms) ->
rpc_call(Node, modify_plugin_config, [PluginName, Terms]).
%%--------------------------------------------------------------------
%% manager user API
%%--------------------------------------------------------------------
check_user(undefined, _) ->
{error, "Username undefined"};
check_user(_, undefined) ->
{error, "Password undefined"};
check_user(Username, Password) ->
case mnesia:dirty_read(mqtt_admin, Username) of
[#mqtt_admin{password = <<Salt:4/binary, Hash/binary>>}] ->
case Hash =:= md5_hash(Salt, Password) of
true -> ok;
false -> {error, "Password error"}
end;
[] ->
{error, "User not found"}
end.
add_user(Username, Password, Tag) ->
Admin = #mqtt_admin{username = Username,
password = hash(Password),
tags = Tag},
return(mnesia:transaction(fun add_user_/1, [Admin])).
add_user_(Admin = #mqtt_admin{username = Username}) ->
case mnesia:wread({mqtt_admin, Username}) of
[] -> mnesia:write(Admin);
[_] -> {error, [{code, ?ERROR13}, {message, <<"User already exist">>}]}
end.
user_list() ->
[row(Admin) || Admin <- ets:tab2list(mqtt_admin)].
lookup_user(Username) ->
Admin = mnesia:dirty_read(mqtt_admin, Username),
row(Admin).
update_user(Username, Params) ->
case mnesia:dirty_read({mqtt_admin, Username}) of
[] ->
{error, [{code, ?ERROR5}, {message, <<"User not found">>}]};
[User] ->
Admin = case proplists:get_value(<<"tags">>, Params) of
undefined -> User;
Tag -> User#mqtt_admin{tags = Tag}
end,
return(mnesia:transaction(fun() -> mnesia:write(Admin) end))
end.
remove_user(Username) ->
Trans = fun() ->
case lookup_user(Username) of
[] -> {error, [{code, ?ERROR5}, {message, <<"User not found">>}]};
_ -> mnesia:delete({mqtt_admin, Username})
end
end,
return(mnesia:transaction(Trans)).
change_password(Username, OldPwd, NewPwd) ->
Trans = fun() ->
case mnesia:wread({mqtt_admin, Username}) of
[Admin = #mqtt_admin{password = <<Salt:4/binary, Hash/binary>>}] ->
case Hash =:= md5_hash(Salt, OldPwd) of
true ->
mnesia:write(Admin#mqtt_admin{password = hash(NewPwd)});
false ->
{error, [{code, ?ERROR14}, {message, <<"OldPassword error">>}]}
end;
[] ->
{error, [{code, ?ERROR5}, {message, <<"User not found">>}]}
end
end,
return(mnesia:transaction(Trans)).
return({atomic, ok}) ->
ok;
return({atomic, Error}) ->
Error;
return({aborted, Reason}) ->
lager:error("Mnesia Transaction error:~p~n", [Reason]),
error.
row(#mqtt_admin{username = Username, tags = Tags}) ->
[{username, Username}, {tags, Tags}];
row([#mqtt_admin{username = Username, tags = Tags}]) ->
[{username, Username}, {tags, Tags}];
row([]) ->[].
%%--------------------------------------------------------------------
%% Internel Functions.
%%--------------------------------------------------------------------
rpc_call(Node, Fun, Args) ->
case rpc:call(Node, ?MODULE, Fun, Args) of
{badrpc, Reason} -> {error, Reason};
Res -> Res
end.
kmg(Byte) when Byte > ?GB ->
float(Byte / ?GB, "G");
kmg(Byte) when Byte > ?MB ->
float(Byte / ?MB, "M");
kmg(Byte) when Byte > ?KB ->
float(Byte / ?MB, "K");
kmg(Byte) ->
Byte.
float(F, S) ->
iolist_to_binary(io_lib:format("~.2f~s", [F, S])).
validate(qos, Qos) ->
(Qos >= ?QOS_0) and (Qos =< ?QOS_2);
validate(topic, Topic) ->
emqttd_topic:validate({name, Topic}).
client_list(ClientId, PageNo, PageSize) when ?EMPTY_KEY(ClientId) ->
TotalNum = ets:info(mqtt_client, size),
Qh = qlc:q([R || R <- ets:table(mqtt_client)]),
query_table(Qh, PageNo, PageSize, TotalNum);
client_list(ClientId, PageNo, PageSize) ->
Fun = fun() -> ets:lookup(mqtt_client, ClientId) end,
lookup_table(Fun, PageNo, PageSize).
session_list(ClientId, PageNo, PageSize) when ?EMPTY_KEY(ClientId) ->
TotalNum = lists:sum([ets:info(Tab, size) || Tab <- [mqtt_local_session]]),
Qh = qlc:append([qlc:q([E || E <- ets:table(Tab)]) || Tab <- [mqtt_local_session]]),
query_table(Qh, PageNo, PageSize, TotalNum);
session_list(ClientId, PageNo, PageSize) ->
MP = {ClientId, '_', '_', '_'},
Fun = fun() -> lists:append([ets:match_object(Tab, MP) || Tab <- [mqtt_local_session]]) end,
lookup_table(Fun, PageNo, PageSize).
subscription_list(Key, PageNo, PageSize) when ?EMPTY_KEY(Key) ->
TotalNum = ets:info(mqtt_subproperty, size),
Qh = qlc:q([E || E <- ets:table(mqtt_subproperty)]),
query_table(Qh, PageNo, PageSize, TotalNum);
subscription_list(Key, PageNo, PageSize) ->
Fun = fun() -> ets:match_object(mqtt_subproperty, {{'_', {Key, '_'}}, '_'}) end,
lookup_table(Fun, PageNo, PageSize).
route_list(Topic, PageNo, PageSize) when ?EMPTY_KEY(Topic) ->
Tables = [mqtt_route],
TotalNum = lists:sum([ets:info(Tab, size) || Tab <- [mqtt_route, mqtt_local_route]]),
Qh = qlc:append([qlc:q([E || E <- ets:table(Tab)]) || Tab <- Tables]),
Data = query_table(Qh, PageNo, PageSize, TotalNum),
Route = get_value(result, Data),
LocalRoute = local_route_list(Topic, PageNo, PageSize),
lists:keyreplace(result, 1, Data, {result, lists:append(Route, LocalRoute)});
route_list(Topic, PageNo, PageSize) ->
Tables = [mqtt_route],
Fun = fun() -> lists:append([ets:lookup(Tab, Topic) || Tab <- Tables]) end,
Route = lookup_table(Fun, PageNo, PageSize),
LocalRoute = local_route_list(Topic, PageNo, PageSize),
lists:append(Route, LocalRoute).
local_route_list(Topic, PageNo, PageSize) when ?EMPTY_KEY(Topic) ->
TotalNum = lists:sum([ets:info(Tab, size) || Tab <- [mqtt_local_route]]),
Qh = qlc:append([qlc:q([E || E <- ets:table(Tab)]) || Tab <- [mqtt_local_route]]),
Data = query_table(Qh, PageNo, PageSize, TotalNum),
lists:map(fun({Topic1, Node}) -> {<<"$local/", Topic1/binary>>, Node} end, get_value(result, Data));
local_route_list(Topic, PageNo, PageSize) ->
Fun = fun() -> lists:append([ets:lookup(Tab, Topic) || Tab <- [mqtt_local_route]]) end,
Data = lookup_table(Fun, PageNo, PageSize),
lists:map(fun({Topic1, Node}) -> {<<"$local/", Topic1/binary>>, Node} end, Data).
format_error(Val, Msg) ->
re:replace(Msg, <<"\\$\\{[^}]+\\}">>, Val, [global, {return, binary}]).
hash(Password) ->
SaltBin = salt(),
<<SaltBin/binary, (md5_hash(SaltBin, Password))/binary>>.
md5_hash(SaltBin, Password) ->
erlang:md5(<<SaltBin/binary, Password/binary>>).
salt() ->
seed(),
Salt = rand:uniform(16#ffffffff),
<<Salt:32>>.
seed() ->
rand:seed(exsplus, erlang:timestamp()).

View File

@ -1,65 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_misc).
-author("Feng Lee <feng@emqtt.io>").
-export([merge_opts/2, start_timer/2, start_timer/3, cancel_timer/1,
proc_stats/0, proc_stats/1]).
%% @doc Merge Options
merge_opts(Defaults, Options) ->
lists:foldl(
fun({Opt, Val}, Acc) ->
case lists:keymember(Opt, 1, Acc) of
true -> lists:keyreplace(Opt, 1, Acc, {Opt, Val});
false -> [{Opt, Val}|Acc]
end;
(Opt, Acc) ->
case lists:member(Opt, Acc) of
true -> Acc;
false -> [Opt | Acc]
end
end, Defaults, Options).
-spec(start_timer(integer(), term()) -> reference()).
start_timer(Interval, Msg) ->
start_timer(Interval, self(), Msg).
-spec(start_timer(integer(), pid() | atom(), term()) -> reference()).
start_timer(Interval, Dest, Msg) ->
erlang:start_timer(Interval, Dest, Msg).
-spec(cancel_timer(undefined | reference()) -> ok).
cancel_timer(undefined) ->
ok;
cancel_timer(Timer) ->
case catch erlang:cancel_timer(Timer) of
false -> receive {timeout, Timer, _} -> ok after 0 -> ok end;
_ -> ok
end.
-spec(proc_stats() -> list()).
proc_stats() ->
proc_stats(self()).
-spec(proc_stats(pid()) -> list()).
proc_stats(Pid) ->
Stats = process_info(Pid, [message_queue_len, heap_size, reductions]),
{value, {_, V}, Stats1} = lists:keytake(message_queue_len, 1, Stats),
[{mailbox_len, V} | Stats1].

View File

@ -1,227 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc A Simple in-memory message queue.
%%
%% Notice that MQTT is not an enterprise messaging queue. MQTT assume that client
%% should be online in most of the time.
%%
%% 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 ----------------->|
%% -----------------------------------------------
%% IN -> | Messages Queue | Inflight Window | -> Out
%% -----------------------------------------------
%% |<--- Win Size --->|
%%
%%
%% 1. Inflight Window to store the messages delivered and awaiting for puback.
%%
%% 2. Enqueue messages when the inflight window is full.
%%
%% 3. If the queue is full, dropped qos0 messages if store_qos0 is true,
%% otherwise dropped the oldest one.
%%
%% @end
-module(emqttd_mqueue).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-import(proplists, [get_value/3]).
-export([new/3, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1,
dropped/1, stats/1]).
-define(LOW_WM, 0.2).
-define(HIGH_WM, 0.6).
-define(PQUEUE, priority_queue).
-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(stat() :: {max_len, non_neg_integer()}
| {len, non_neg_integer()}
| {dropped, non_neg_integer()}).
-record(mqueue, {type :: simple | priority,
name, q :: queue:queue() | ?PQUEUE:q(),
%% priority table
pseq = 0, priorities = [],
%% len of simple queue
len = 0, max_len = 0,
low_wm = ?LOW_WM, high_wm = ?HIGH_WM,
qos0 = false, dropped = 0,
alarm_fun}).
-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) ->
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).
init_q(MQ = #mqueue{type = simple}, _Opts) ->
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_p([], MQ) ->
MQ;
init_p([{Topic, P} | L], MQ) ->
{_, MQ1} = insert_p(iolist_to_binary(Topic), P, MQ),
init_p(L, MQ1).
insert_p(Topic, P, MQ = #mqueue{priorities = Tab, pseq = Seq}) ->
<<PInt:48>> = <<P:8, (erlang:phash2(Topic)):32, Seq:8>>,
{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.
-spec(type(mqueue()) -> atom()).
type(#mqueue{type = Type}) ->
Type.
is_empty(#mqueue{type = simple, len = Len}) -> Len =:= 0;
is_empty(#mqueue{type = priority, q = Q}) -> ?PQUEUE:is_empty(Q).
len(#mqueue{type = simple, len = Len}) -> Len;
len(#mqueue{type = priority, q = Q}) -> ?PQUEUE:len(Q).
max_len(#mqueue{max_len = MaxLen}) -> MaxLen.
%% @doc Dropped of the mqueue
-spec(dropped(mqueue()) -> non_neg_integer()).
dropped(#mqueue{dropped = Dropped}) -> Dropped.
%% @doc Stats of the mqueue
-spec(stats(mqueue()) -> [stat()]).
stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped}) ->
[{len, case Type of
simple -> Len;
priority -> ?PQUEUE:len(Q)
end} | [{max_len, MaxLen}, {dropped, Dropped}]].
%% @doc Enqueue a message.
-spec(in(mqtt_message(), mqueue()) -> mqueue()).
in(#mqtt_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};
in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = MaxLen, dropped = Dropped})
when Len >= MaxLen ->
{{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});
in(Msg = #mqtt_message{topic = Topic}, MQ = #mqueue{type = priority, q = Q,
priorities = Priorities,
max_len = 0}) ->
case lists:keysearch(Topic, 1, Priorities) of
{value, {_, Pri}} ->
MQ#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)};
false ->
{Pri, MQ1} = insert_p(Topic, 0, MQ),
MQ1#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)}
end;
in(Msg = #mqtt_message{topic = Topic}, MQ = #mqueue{type = priority, q = Q,
priorities = Priorities,
max_len = MaxLen}) ->
case lists:keysearch(Topic, 1, Priorities) of
{value, {_, Pri}} ->
case ?PQUEUE:plen(Pri, Q) >= MaxLen of
true ->
{_, Q1} = ?PQUEUE:out(Pri, Q),
MQ#mqueue{q = ?PQUEUE:in(Msg, Pri, Q1)};
false ->
MQ#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)}
end;
false ->
{Pri, MQ1} = insert_p(Topic, 0, MQ),
MQ1#mqueue{q = ?PQUEUE:in(Msg, Pri, Q)}
end.
out(MQ = #mqueue{type = simple, len = 0}) ->
{empty, MQ};
out(MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) ->
{R, Q2} = queue:out(Q),
{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})};
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 = #mqtt_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.

View File

@ -1,232 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_net).
-include_lib("kernel/include/inet.hrl").
-export([tcp_name/3, tcp_host/1, getopts/2, setopts/2, getaddr/2,
port_to_listeners/1]).
-export([peername/1, sockname/1, format/2, format/1, ntoa/1,
connection_string/2]).
-define(FIRST_TEST_BIND_PORT, 10000).
%%--------------------------------------------------------------------
%% inet_parse:address takes care of ip string, like "0.0.0.0"
%% inet:getaddr returns immediately for ip tuple {0,0,0,0},
%% and runs 'inet_gethost' port process for dns lookups.
%% On Windows inet:getaddr runs dns resolver for ip string, which may fail.
getaddr(Host, Family) ->
case inet_parse:address(Host) of
{ok, IPAddress} -> [{IPAddress, resolve_family(IPAddress, Family)}];
{error, _} -> gethostaddr(Host, Family)
end.
gethostaddr(Host, auto) ->
Lookups = [{Family, inet:getaddr(Host, Family)} || Family <- [inet, inet6]],
case [{IP, Family} || {Family, {ok, IP}} <- Lookups] of
[] -> host_lookup_error(Host, Lookups);
IPs -> IPs
end;
gethostaddr(Host, Family) ->
case inet:getaddr(Host, Family) of
{ok, IPAddress} -> [{IPAddress, Family}];
{error, Reason} -> host_lookup_error(Host, Reason)
end.
host_lookup_error(Host, Reason) ->
error_logger:error_msg("invalid host ~p - ~p~n", [Host, Reason]),
throw({error, {invalid_host, Host, Reason}}).
resolve_family({_,_,_,_}, auto) -> inet;
resolve_family({_,_,_,_,_,_,_,_}, auto) -> inet6;
resolve_family(IP, auto) -> throw({error, {strange_family, IP}});
resolve_family(_, F) -> F.
%%--------------------------------------------------------------------
%% There are three kinds of machine (for our purposes).
%%
%% * Those which treat IPv4 addresses as a special kind of IPv6 address
%% ("Single stack")
%% - Linux by default, Windows Vista and later
%% - We also treat any (hypothetical?) IPv6-only machine the same way
%% * Those which consider IPv6 and IPv4 to be completely separate things
%% ("Dual stack")
%% - OpenBSD, Windows XP / 2003, Linux if so configured
%% * Those which do not support IPv6.
%% - Ancient/weird OSes, Linux if so configured
%%
%% How to reconfigure Linux to test this:
%% Single stack (default):
%% echo 0 > /proc/sys/net/ipv6/bindv6only
%% Dual stack:
%% echo 1 > /proc/sys/net/ipv6/bindv6only
%% IPv4 only:
%% add ipv6.disable=1 to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub then
%% sudo update-grub && sudo reboot
%%
%% This matters in (and only in) the case where the sysadmin (or the
%% app descriptor) has only supplied a port and we wish to bind to
%% "all addresses". This means different things depending on whether
%% we're single or dual stack. On single stack binding to "::"
%% implicitly includes all IPv4 addresses, and subsequently attempting
%% to bind to "0.0.0.0" will fail. On dual stack, binding to "::" will
%% only bind to IPv6 addresses, and we need another listener bound to
%% "0.0.0.0" for IPv4. Finally, on IPv4-only systems we of course only
%% want to bind to "0.0.0.0".
%%
%% Unfortunately it seems there is no way to detect single vs dual stack
%% apart from attempting to bind to the port.
port_to_listeners(Port) ->
IPv4 = {"0.0.0.0", Port, inet},
IPv6 = {"::", Port, inet6},
case ipv6_status(?FIRST_TEST_BIND_PORT) of
single_stack -> [IPv6];
ipv6_only -> [IPv6];
dual_stack -> [IPv6, IPv4];
ipv4_only -> [IPv4]
end.
ipv6_status(TestPort) ->
IPv4 = [inet, {ip, {0,0,0,0}}],
IPv6 = [inet6, {ip, {0,0,0,0,0,0,0,0}}],
case gen_tcp:listen(TestPort, IPv6) of
{ok, LSock6} ->
case gen_tcp:listen(TestPort, IPv4) of
{ok, LSock4} ->
%% Dual stack
gen_tcp:close(LSock6),
gen_tcp:close(LSock4),
dual_stack;
%% Checking the error here would only let us
%% distinguish single stack IPv6 / IPv4 vs IPv6 only,
%% which we figure out below anyway.
{error, _} ->
gen_tcp:close(LSock6),
case gen_tcp:listen(TestPort, IPv4) of
%% Single stack
{ok, LSock4} -> gen_tcp:close(LSock4),
single_stack;
%% IPv6-only machine. Welcome to the future.
{error, eafnosupport} -> ipv6_only; %% Linux
{error, eprotonosupport}-> ipv6_only; %% FreeBSD
%% Dual stack machine with something already
%% on IPv4.
{error, _} -> ipv6_status(TestPort + 1)
end
end;
%% IPv4-only machine. Welcome to the 90s.
{error, eafnosupport} -> %% Linux
ipv4_only;
{error, eprotonosupport} -> %% FreeBSD
ipv4_only;
%% Port in use
{error, _} ->
ipv6_status(TestPort + 1)
end.
tcp_name(Prefix, IPAddress, Port)
when is_atom(Prefix) andalso is_number(Port) ->
list_to_atom(
lists:flatten(
io_lib:format(
"~w_~s:~w", [Prefix, inet_parse:ntoa(IPAddress), Port]))).
connection_string(Sock, Direction) ->
case socket_ends(Sock, Direction) of
{ok, {FromAddress, FromPort, ToAddress, ToPort}} ->
{ok, lists:flatten(
io_lib:format(
"~s:~p -> ~s:~p",
[maybe_ntoab(FromAddress), FromPort,
maybe_ntoab(ToAddress), ToPort]))};
Error ->
Error
end.
socket_ends(Sock, Direction) ->
{From, To} = sock_funs(Direction),
case {From(Sock), To(Sock)} of
{{ok, {FromAddress, FromPort}}, {ok, {ToAddress, ToPort}}} ->
{ok, {rdns(FromAddress), FromPort,
rdns(ToAddress), ToPort}};
{{error, _Reason} = Error, _} ->
Error;
{_, {error, _Reason} = Error} ->
Error
end.
maybe_ntoab(Addr) when is_tuple(Addr) -> ntoab(Addr);
maybe_ntoab(Host) -> Host.
rdns(Addr) -> Addr.
sock_funs(inbound) -> {fun peername/1, fun sockname/1};
sock_funs(outbound) -> {fun sockname/1, fun peername/1}.
getopts(Sock, Options) when is_port(Sock) ->
inet:getopts(Sock, Options).
setopts(Sock, Options) when is_port(Sock) ->
inet:setopts(Sock, Options).
sockname(Sock) when is_port(Sock) -> inet:sockname(Sock).
peername(Sock) when is_port(Sock) -> inet:peername(Sock).
format(sockname, SockName) ->
format(SockName);
format(peername, PeerName) ->
format(PeerName).
format({Addr, Port}) ->
lists:flatten(io_lib:format("~s:~p", [maybe_ntoab(Addr), Port])).
ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
ntoa(IP) ->
inet_parse:ntoa(IP).
ntoab(IP) ->
Str = ntoa(IP),
case string:str(Str, ":") of
0 -> Str;
_ -> "[" ++ Str ++ "]"
end.
tcp_host({0,0,0,0}) ->
hostname();
tcp_host({0,0,0,0,0,0,0,0}) ->
hostname();
tcp_host(IPAddress) ->
case inet:gethostbyaddr(IPAddress) of
{ok, #hostent{h_name = Name}} -> Name;
{error, _Reason} -> ntoa(IPAddress)
end.
hostname() ->
{ok, Hostname} = inet:gethostname(),
case inet:gethostbyname(Hostname) of
{ok, #hostent{h_name = Name}} -> Name;
{error, _Reason} -> Hostname
end.

View File

@ -1,130 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_packet).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
%% API
-export([protocol_name/1, type_name/1, connack_name/1]).
-export([format/1]).
%% @doc Protocol name of version
-spec(protocol_name(mqtt_vsn()) -> binary()).
protocol_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>;
protocol_name(?MQTT_PROTO_V4) -> <<"MQTT">>;
protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>.
%% @doc Name of MQTT packet type
-spec(type_name(mqtt_packet_type()) -> atom()).
type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH ->
lists:nth(Type, ?TYPE_NAMES).
%% @doc Connack Name
-spec(connack_name(mqtt_connack()) -> atom()).
connack_name(?CONNACK_ACCEPT) -> 'CONNACK_ACCEPT';
connack_name(?CONNACK_PROTO_VER) -> 'CONNACK_PROTO_VER';
connack_name(?CONNACK_INVALID_ID) -> 'CONNACK_INVALID_ID';
connack_name(?CONNACK_SERVER) -> 'CONNACK_SERVER';
connack_name(?CONNACK_CREDENTIALS) -> 'CONNACK_CREDENTIALS';
connack_name(?CONNACK_AUTH) -> 'CONNACK_AUTH'.
%% @doc Format packet
-spec(format(mqtt_packet()) -> iolist()).
format(#mqtt_packet{header = Header, variable = Variable, payload = Payload}) ->
format_header(Header, format_variable(Variable, Payload)).
format_header(#mqtt_packet_header{type = Type,
dup = Dup,
qos = QoS,
retain = Retain}, S) ->
S1 = if
S == undefined -> <<>>;
true -> [", ", S]
end,
io_lib:format("~s(Q~p, R~p, D~p~s)", [type_name(Type), QoS, i(Retain), i(Dup), S1]).
format_variable(undefined, _) ->
undefined;
format_variable(Variable, undefined) ->
format_variable(Variable);
format_variable(Variable, Payload) ->
io_lib:format("~s, Payload=~p", [format_variable(Variable), Payload]).
format_variable(#mqtt_packet_connect{
proto_ver = ProtoVer,
proto_name = ProtoName,
will_retain = WillRetain,
will_qos = WillQoS,
will_flag = WillFlag,
clean_sess = CleanSess,
keep_alive = KeepAlive,
client_id = ClientId,
will_topic = WillTopic,
will_msg = WillMsg,
username = Username,
password = Password}) ->
Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanSess=~s, KeepAlive=~p, Username=~s, Password=~s",
Args = [ClientId, ProtoName, ProtoVer, CleanSess, KeepAlive, Username, format_password(Password)],
{Format1, Args1} = if
WillFlag -> { Format ++ ", Will(Q~p, R~p, Topic=~s, Msg=~s)",
Args ++ [WillQoS, i(WillRetain), WillTopic, WillMsg] };
true -> {Format, Args}
end,
io_lib:format(Format1, Args1);
format_variable(#mqtt_packet_connack{ack_flags = AckFlags,
return_code = ReturnCode}) ->
io_lib:format("AckFlags=~p, ReturnCode=~p", [AckFlags, ReturnCode]);
format_variable(#mqtt_packet_publish{topic_name = TopicName,
packet_id = PacketId}) ->
io_lib:format("Topic=~s, PacketId=~p", [TopicName, PacketId]);
format_variable(#mqtt_packet_puback{packet_id = PacketId}) ->
io_lib:format("PacketId=~p", [PacketId]);
format_variable(#mqtt_packet_subscribe{packet_id = PacketId,
topic_table = TopicTable}) ->
io_lib:format("PacketId=~p, TopicTable=~p", [PacketId, TopicTable]);
format_variable(#mqtt_packet_unsubscribe{packet_id = PacketId,
topics = Topics}) ->
io_lib:format("PacketId=~p, Topics=~p", [PacketId, Topics]);
format_variable(#mqtt_packet_suback{packet_id = PacketId,
qos_table = QosTable}) ->
io_lib:format("PacketId=~p, QosTable=~p", [PacketId, QosTable]);
format_variable(#mqtt_packet_unsuback{packet_id = PacketId}) ->
io_lib:format("PacketId=~p", [PacketId]);
format_variable(PacketId) when is_integer(PacketId) ->
io_lib:format("PacketId=~p", [PacketId]);
format_variable(undefined) -> undefined.
format_password(undefined) -> undefined;
format_password(_Password) -> '******'.
i(true) -> 1;
i(false) -> 0;
i(I) when is_integer(I) -> I.

View File

@ -1,231 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc MQTT Packet Parser
-module(emqttd_parser).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
%% API
-export([initial_state/0, initial_state/1, parse/2]).
-type(max_packet_size() :: 1..?MAX_PACKET_SIZE).
-spec(initial_state() -> {none, max_packet_size()}).
initial_state() ->
initial_state(?MAX_PACKET_SIZE).
%% @doc Initialize a parser
-spec(initial_state(max_packet_size()) -> {none, max_packet_size()}).
initial_state(MaxSize) ->
{none, MaxSize}.
%% @doc Parse MQTT Packet
-spec(parse(binary(), {none, pos_integer()} | fun())
-> {ok, mqtt_packet()} | {error, term()} | {more, fun()}).
parse(<<>>, {none, MaxLen}) ->
{more, fun(Bin) -> parse(Bin, {none, MaxLen}) end};
parse(<<Type:4, Dup:1, QoS:2, Retain:1, Rest/binary>>, {none, Limit}) ->
parse_remaining_len(Rest, #mqtt_packet_header{type = Type,
dup = bool(Dup),
qos = fixqos(Type, QoS),
retain = bool(Retain)}, Limit);
parse(Bin, Cont) -> Cont(Bin).
parse_remaining_len(<<>>, Header, Limit) ->
{more, fun(Bin) -> parse_remaining_len(Bin, Header, Limit) end};
parse_remaining_len(Rest, Header, Limit) ->
parse_remaining_len(Rest, Header, 1, 0, Limit).
parse_remaining_len(_Bin, _Header, _Multiplier, Length, MaxLen)
when Length > MaxLen ->
{error, invalid_mqtt_frame_len};
parse_remaining_len(<<>>, Header, Multiplier, Length, Limit) ->
{more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Limit) end};
%% optimize: match PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK...
parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, _Limit) ->
parse_frame(Rest, Header, 2);
%% optimize: match PINGREQ...
parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, _Limit) ->
parse_frame(Rest, Header, 0);
parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Limit) ->
parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Limit);
parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, MaxLen) ->
FrameLen = Value + Len * Multiplier,
if
FrameLen > MaxLen -> {error, invalid_mqtt_frame_len};
true -> parse_frame(Rest, Header, FrameLen)
end.
parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) ->
case {Type, Bin} of
{?CONNECT, <<FrameBin:Length/binary, Rest/binary>>} ->
{ProtoName, Rest1} = parse_utf(FrameBin),
%% Fix mosquitto bridge: 0x83, 0x84
<<BridgeTag:4, ProtoVersion:4, Rest2/binary>> = Rest1,
<<UsernameFlag : 1,
PasswordFlag : 1,
WillRetain : 1,
WillQos : 2,
WillFlag : 1,
CleanSess : 1,
_Reserved : 1,
KeepAlive : 16/big,
Rest3/binary>> = Rest2,
{ClientId, Rest4} = parse_utf(Rest3),
{WillTopic, Rest5} = parse_utf(Rest4, WillFlag),
{WillMsg, Rest6} = parse_msg(Rest5, WillFlag),
{UserName, Rest7} = parse_utf(Rest6, UsernameFlag),
{PasssWord, <<>>} = parse_utf(Rest7, PasswordFlag),
case protocol_name_approved(ProtoVersion, ProtoName) of
true ->
wrap(Header,
#mqtt_packet_connect{
proto_ver = ProtoVersion,
proto_name = ProtoName,
will_retain = bool(WillRetain),
will_qos = WillQos,
will_flag = bool(WillFlag),
clean_sess = bool(CleanSess),
keep_alive = KeepAlive,
client_id = ClientId,
will_topic = WillTopic,
will_msg = WillMsg,
username = UserName,
password = PasssWord,
is_bridge = (BridgeTag =:= 8)}, Rest);
false ->
{error, protocol_header_corrupt}
end;
%{?CONNACK, <<FrameBin:Length/binary, Rest/binary>>} ->
% <<_Reserved:7, SP:1, ReturnCode:8>> = FrameBin,
% wrap(Header, #mqtt_packet_connack{ack_flags = SP,
% return_code = ReturnCode }, Rest);
{?PUBLISH, <<FrameBin:Length/binary, Rest/binary>>} ->
{TopicName, Rest1} = parse_utf(FrameBin),
{PacketId, Payload} = case Qos of
0 -> {undefined, Rest1};
_ -> <<Id:16/big, R/binary>> = Rest1,
{Id, R}
end,
wrap(fixdup(Header), #mqtt_packet_publish{topic_name = TopicName,
packet_id = PacketId},
Payload, Rest);
{?PUBACK, <<FrameBin:Length/binary, Rest/binary>>} ->
<<PacketId:16/big>> = FrameBin,
wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest);
{?PUBREC, <<FrameBin:Length/binary, Rest/binary>>} ->
<<PacketId:16/big>> = FrameBin,
wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest);
{?PUBREL, <<FrameBin:Length/binary, Rest/binary>>} ->
%% 1 = Qos,
<<PacketId:16/big>> = FrameBin,
wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest);
{?PUBCOMP, <<FrameBin:Length/binary, Rest/binary>>} ->
<<PacketId:16/big>> = FrameBin,
wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest);
{?SUBSCRIBE, <<FrameBin:Length/binary, Rest/binary>>} ->
%% 1 = Qos,
<<PacketId:16/big, Rest1/binary>> = FrameBin,
TopicTable = parse_topics(?SUBSCRIBE, Rest1, []),
wrap(Header, #mqtt_packet_subscribe{packet_id = PacketId,
topic_table = TopicTable}, Rest);
%{?SUBACK, <<FrameBin:Length/binary, Rest/binary>>} ->
% <<PacketId:16/big, Rest1/binary>> = FrameBin,
% wrap(Header, #mqtt_packet_suback{packet_id = PacketId,
% qos_table = parse_qos(Rest1, []) }, Rest);
{?UNSUBSCRIBE, <<FrameBin:Length/binary, Rest/binary>>} ->
%% 1 = Qos,
<<PacketId:16/big, Rest1/binary>> = FrameBin,
Topics = parse_topics(?UNSUBSCRIBE, Rest1, []),
wrap(Header, #mqtt_packet_unsubscribe{packet_id = PacketId,
topics = Topics}, Rest);
%{?UNSUBACK, <<FrameBin:Length/binary, Rest/binary>>} ->
% <<PacketId:16/big>> = FrameBin,
% wrap(Header, #mqtt_packet_unsuback { packet_id = PacketId }, Rest);
{?PINGREQ, Rest} ->
Length = 0,
wrap(Header, Rest);
%{?PINGRESP, Rest} ->
% Length = 0,
% wrap(Header, Rest);
{?DISCONNECT, Rest} ->
Length = 0,
wrap(Header, Rest);
{_, TooShortBin} ->
{more, fun(BinMore) ->
parse_frame(<<TooShortBin/binary, BinMore/binary>>,
Header, Length)
end}
end.
wrap(Header, Variable, Payload, Rest) ->
{ok, #mqtt_packet{header = Header, variable = Variable, payload = Payload}, Rest}.
wrap(Header, Variable, Rest) ->
{ok, #mqtt_packet{header = Header, variable = Variable}, Rest}.
wrap(Header, Rest) ->
{ok, #mqtt_packet{header = Header}, Rest}.
%client function
%parse_qos(<<>>, Acc) ->
% lists:reverse(Acc);
%parse_qos(<<QoS:8/unsigned, Rest/binary>>, Acc) ->
% parse_qos(Rest, [QoS | Acc]).
parse_topics(_, <<>>, Topics) ->
lists:reverse(Topics);
parse_topics(?SUBSCRIBE = Sub, Bin, Topics) ->
{Name, <<_:6, QoS:2, Rest/binary>>} = parse_utf(Bin),
parse_topics(Sub, Rest, [{Name, QoS}| Topics]);
parse_topics(?UNSUBSCRIBE = Sub, Bin, Topics) ->
{Name, <<Rest/binary>>} = parse_utf(Bin),
parse_topics(Sub, Rest, [Name | Topics]).
parse_utf(Bin, 0) ->
{undefined, Bin};
parse_utf(Bin, _) ->
parse_utf(Bin).
parse_utf(<<Len:16/big, Str:Len/binary, Rest/binary>>) ->
{Str, Rest}.
parse_msg(Bin, 0) ->
{undefined, Bin};
parse_msg(<<Len:16/big, Msg:Len/binary, Rest/binary>>, _) ->
{Msg, Rest}.
bool(0) -> false;
bool(1) -> true.
protocol_name_approved(Ver, Name) ->
lists:member({Ver, Name}, ?PROTOCOL_NAMES).
%% Fix Issue#575
fixqos(?PUBREL, 0) -> 1;
fixqos(?SUBSCRIBE, 0) -> 1;
fixqos(?UNSUBSCRIBE, 0) -> 1;
fixqos(_Type, QoS) -> QoS.
%% Fix Issue#1319
fixdup(Header = #mqtt_packet_header{qos = ?QOS0, dup = true}) ->
Header#mqtt_packet_header{dup = false};
fixdup(Header = #mqtt_packet_header{qos = ?QOS2, dup = true}) ->
Header#mqtt_packet_header{dup = false};
fixdup(Header) -> Header.

View File

@ -1,53 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_pmon).
-author("Feng Lee <feng@emqtt.io>").
-export([new/0, monitor/2, demonitor/2, erase/2]).
-type(pmon() :: {?MODULE, map()}).
-export_type([pmon/0]).
new() ->
{?MODULE, [maps:new()]}.
-spec(monitor(pid(), pmon()) -> pmon()).
monitor(Pid, PM = {?MODULE, [M]}) ->
case maps:is_key(Pid, M) of
true ->
PM;
false ->
Ref = erlang:monitor(process, Pid),
{?MODULE, [maps:put(Pid, Ref, M)]}
end.
-spec(demonitor(pid(), pmon()) -> pmon()).
demonitor(Pid, PM = {?MODULE, [M]}) ->
case maps:find(Pid, M) of
{ok, Ref} ->
erlang:demonitor(Ref, [flush]),
{?MODULE, [maps:remove(Pid, M)]};
error ->
PM
end.
-spec(erase(pid(), pmon()) -> pmon()).
erase(Pid, {?MODULE, [M]}) ->
{?MODULE, [maps:remove(Pid, M)]}.

View File

@ -1,98 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc.
%%
%% 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(emqttd_pooler).
-behaviour(gen_server).
-include("emqttd_internal.hrl").
%% Start the pool supervisor
-export([start_link/0]).
%% API Exports
-export([start_link/2, submit/1, async_submit/1]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {pool, id}).
%% @doc Start Pooler Supervisor.
start_link() ->
emqttd_pool_sup:start_link(pooler, random, {?MODULE, start_link, []}).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
-spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}).
start_link(Pool, Id) ->
gen_server:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id], []).
%% @doc Submit work to pooler
submit(Fun) -> gen_server:call(worker(), {submit, Fun}, infinity).
%% @doc Submit work to pooler asynchronously
async_submit(Fun) ->
gen_server:cast(worker(), {async_submit, Fun}).
worker() ->
gproc_pool:pick_worker(pooler).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Pool, Id]) ->
?GPROC_POOL(join, Pool, Id),
{ok, #state{pool = Pool, id = Id}}.
handle_call({submit, Fun}, _From, State) ->
{reply, run(Fun), State};
handle_call(_Req, _From, State) ->
{reply, ok, State}.
handle_cast({async_submit, Fun}, State) ->
try run(Fun)
catch _:Error ->
lager:error("Pooler Error: ~p, ~p", [Error, erlang:get_stacktrace()])
end,
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{pool = Pool, id = Id}) ->
?GPROC_POOL(leave, Pool, Id), ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
run({M, F, A}) ->
erlang:apply(M, F, A);
run(Fun) when is_function(Fun) ->
Fun().

View File

@ -1,598 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_protocol).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-include("emqttd_internal.hrl").
-import(proplists, [get_value/2, get_value/3]).
%% API
-export([init/3, init/4, 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]).
-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.
-record(proto_state, {peername, sendfun, connected = false, client_id, client_pid,
clean_sess, 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}).
-type(proto_state() :: #proto_state{}).
-define(INFO_KEYS, [client_id, username, clean_sess, proto_ver, proto_name,
keepalive, will_msg, ws_initial_headers, mountpoint,
peercert_username, connected_at]).
-define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]).
-define(LOG(Level, Format, Args, State),
lager:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format,
[State#proto_state.client_id, esockd_net:format(State#proto_state.peername) | Args])).
%% @doc Init protocol
init(Peername, SendFun, Opts) ->
Backoff = get_value(keepalive_backoff, Opts, 1.25),
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,
client_pid = self(),
peercert_username = undefined,
ws_initial_headers = WsInitialHeaders,
keepalive_backoff = Backoff,
stats_data = #proto_stats{enable_stats = EnableStats}}.
init(Conn, Peername, SendFun, Opts) ->
enrich_opt(Conn:opts(), Conn, init(Peername, SendFun, Opts)).
enrich_opt([], _Conn, State) ->
State;
enrich_opt([{mountpoint, MountPoint} | ConnOpts], Conn, State) ->
enrich_opt(ConnOpts, Conn, State#proto_state{mountpoint = MountPoint});
enrich_opt([{peer_cert_as_username, N} | ConnOpts], Conn, State) ->
enrich_opt(ConnOpts, Conn, State#proto_state{peercert_username = peercert_username(N, Conn)});
enrich_opt([_ | ConnOpts], Conn, State) ->
enrich_opt(ConnOpts, Conn, State).
peercert_username(cn, Conn) ->
Conn:peer_cert_common_name();
peercert_username(dn, Conn) ->
Conn:peer_cert_subject().
repl_username_with_peercert(State = #proto_state{peercert_username = undefined}) ->
State;
repl_username_with_peercert(State = #proto_state{peercert_username = PeerCert}) ->
State#proto_state{username = PeerCert}.
info(ProtoState) ->
?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS).
stats(#proto_state{stats_data = Stats}) ->
tl(?record_to_proplist(proto_stats, Stats)).
clientid(#proto_state{client_id = ClientId}) ->
ClientId.
client(#proto_state{client_id = ClientId,
client_pid = ClientPid,
peername = Peername,
username = Username,
clean_sess = CleanSess,
proto_ver = ProtoVer,
keepalive = Keepalive,
will_msg = WillMsg,
ws_initial_headers = WsInitialHeaders,
mountpoint = MountPoint,
connected_at = Time}) ->
WillTopic = if
WillMsg =:= undefined -> undefined;
true -> WillMsg#mqtt_message.topic
end,
#mqtt_client{client_id = ClientId,
client_pid = ClientPid,
username = Username,
peername = Peername,
clean_sess = CleanSess,
proto_ver = ProtoVer,
keepalive = Keepalive,
will_topic = WillTopic,
ws_initial_headers = WsInitialHeaders,
mountpoint = MountPoint,
connected_at = Time}.
session(#proto_state{session = Session}) ->
Session.
%% CONNECT Client requests a connection to a Server
%% 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(?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 = ?PACKET(Type), State = #proto_state{stats_data = Stats}) ->
trace(recv, Packet, State), Stats1 = inc_stats(recv, Type, Stats),
case validate_packet(Packet) of
ok ->
process(Packet, State#proto_state{stats_data = Stats1});
{error, Reason} ->
{error, Reason, State}
end.
subscribe(RawTopicTable, ProtoState = #proto_state{client_id = ClientId,
username = Username,
session = Session,
mountpoint = MountPoint}) ->
TopicTable = parse_topic_table(RawTopicTable),
case emqttd_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of
{ok, TopicTable1} ->
emqttd_session:subscribe(Session, mount(MountPoint, TopicTable1));
{stop, _} ->
ok
end,
{ok, ProtoState}.
unsubscribe(RawTopics, ProtoState = #proto_state{client_id = ClientId,
username = Username,
session = Session,
mountpoint = MountPoint}) ->
case emqttd_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of
{ok, TopicTable} ->
emqttd_session:unsubscribe(Session, mount(MountPoint, 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_sess = CleanSess,
keep_alive = 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_sess = CleanSess,
keepalive = KeepAlive,
will_msg = willmsg(Var, State0),
is_bridge = IsBridge,
connected_at = os:timestamp()}),
{ReturnCode1, SessPresent, State3} =
case validate_connect(Var, State1) of
?CONNACK_ACCEPT ->
case authenticate(client(State1), Password) of
{ok, IsSuperuser} ->
%% Generate clientId if null
State2 = maybe_set_clientid(State1),
%% Start session
case emqttd_sm:start_session(CleanSess, {clientid(State2), Username}) of
{ok, Session, SP} ->
%% Register the client
emqttd_cm:reg(client(State2)),
%% Start keepalive
start_keepalive(KeepAlive, State2),
%% Emit Stats
self() ! emit_stats,
%% ACCEPT
{?CONNACK_ACCEPT, SP, State2#proto_state{session = Session, is_superuser = IsSuperuser}};
{error, Error} ->
?LOG(error, "Username '~s' login failed for ~p", [Username, Error], State2),
{?CONNACK_SERVER, false, State2}
end;
{error, Reason}->
?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], State1),
{?CONNACK_CREDENTIALS, false, State1}
end;
ReturnCode ->
{ReturnCode, false, State1}
end,
%% Run hooks
emqttd_hooks:run('client.connected', [ReturnCode1], client(State3)),
%% Send connack
send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3),
%% stop if authentication failure
stop_if_auth_failure(ReturnCode1, State3);
process(Packet = ?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload), State = #proto_state{is_superuser = IsSuper}) ->
case IsSuper orelse allow == check_acl(publish, Topic, client(State)) of
true -> publish(Packet, State);
false -> ?LOG(error, "Cannot publish to ~s for ACL Deny", [Topic], State)
end,
{ok, State};
process(?PUBACK_PACKET(?PUBACK, PacketId), State = #proto_state{session = Session}) ->
emqttd_session:puback(Session, PacketId),
{ok, State};
process(?PUBACK_PACKET(?PUBREC, PacketId), State = #proto_state{session = Session}) ->
emqttd_session:pubrec(Session, PacketId),
send(?PUBREL_PACKET(PacketId), State);
process(?PUBACK_PACKET(?PUBREL, PacketId), State = #proto_state{session = Session}) ->
emqttd_session:pubrel(Session, PacketId),
send(?PUBACK_PACKET(?PUBCOMP, PacketId), State);
process(?PUBACK_PACKET(?PUBCOMP, PacketId), State = #proto_state{session = Session})->
emqttd_session:pubcomp(Session, PacketId), {ok, State};
%% Protect from empty topic table
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),
AllowDenies = if
IsSuperuser -> [];
true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicTable]
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);
false ->
case emqttd_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of
{ok, TopicTable1} ->
emqttd_session:subscribe(Session, PacketId, mount(MountPoint, TopicTable1)),
{ok, State};
{stop, _} ->
{ok, State}
end
end;
%% Protect from empty topic list
process(?UNSUBSCRIBE_PACKET(PacketId, []), State) ->
send(?UNSUBACK_PACKET(PacketId), State);
process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics),
State = #proto_state{client_id = ClientId,
username = Username,
mountpoint = MountPoint,
session = Session}) ->
case emqttd_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of
{ok, TopicTable} ->
emqttd_session:unsubscribe(Session, mount(MountPoint, TopicTable));
{stop, _} ->
ok
end,
send(?UNSUBACK_PACKET(PacketId), State);
process(?PACKET(?PINGREQ), State) ->
send(?PACKET(?PINGRESP), State);
process(?PACKET(?DISCONNECT), State) ->
% Clean willmsg
{stop, normal, State#proto_state{will_msg = undefined}}.
publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId),
#proto_state{client_id = ClientId,
username = Username,
mountpoint = MountPoint,
session = Session}) ->
Msg = emqttd_message:from_packet(Username, ClientId, Packet),
emqttd_session:publish(Session, mount(MountPoint, Msg));
publish(Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) ->
with_puback(?PUBACK, Packet, State);
publish(Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) ->
with_puback(?PUBREC, Packet, State).
with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId),
State = #proto_state{client_id = ClientId,
username = Username,
mountpoint = MountPoint,
session = Session}) ->
Msg = emqttd_message:from_packet(Username, ClientId, Packet),
case emqttd_session:publish(Session, mount(MountPoint, Msg)) of
ok ->
send(?PUBACK_PACKET(Type, PacketId), State);
{error, Error} ->
?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State)
end.
-spec(send(mqtt_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, mqtt_message) ->
emqttd_hooks:run('message.delivered', [ClientId, Username], Msg),
send(emqttd_message:to_packet(unmount(MountPoint, clean_retain(IsBridge, Msg))), State);
send(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) ->
trace(send, Packet, State),
emqttd_metrics:sent(Packet),
SendFun(Packet),
{ok, State#proto_state{stats_data = inc_stats(send, Type, Stats)}}.
trace(recv, Packet, ProtoState) ->
?LOG(debug, "RECV ~s", [emqttd_packet:format(Packet)], ProtoState);
trace(send, Packet, ProtoState) ->
?LOG(debug, "SEND ~s", [emqttd_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.
stop_if_auth_failure(RC, State) when RC == ?CONNACK_CREDENTIALS; RC == ?CONNACK_AUTH ->
{stop, {shutdown, auth_failure}, State};
stop_if_auth_failure(_RC, State) ->
{ok, State}.
shutdown(_Error, #proto_state{client_id = undefined}) ->
ignore;
shutdown(conflict, _State) ->
%% let it down
ignore;
shutdown(mnesia_conflict, _State) ->
%% let it down
ignore;
shutdown(Error, State = #proto_state{will_msg = WillMsg}) ->
?LOG(debug, "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(State, WillMsg)
end,
emqttd_hooks:run('client.disconnected', [Error], Client),
%% let it down
%% emqttd_cm:unreg(ClientId).
ok.
willmsg(Packet, #proto_state{mountpoint = MountPoint}) when is_record(Packet, mqtt_packet_connect) ->
case emqttd_message:from_packet(Packet) of
undefined -> undefined;
Msg -> mount(MountPoint, Msg)
end.
%% Generate a client if if nulll
maybe_set_clientid(State = #proto_state{client_id = NullId})
when NullId =:= undefined orelse NullId =:= <<>> ->
{_, NPid, _} = emqttd_guid:new(),
ClientId = iolist_to_binary(["emqttd_", integer_to_list(NPid)]),
State#proto_state{client_id = ClientId};
maybe_set_clientid(State) ->
State.
send_willmsg(_State, undefined) ->
ignore;
send_willmsg(State = #proto_state{client_id = ClientId, username = Username, is_superuser = IsSuper},
WillMsg = #mqtt_message{topic = Topic}) ->
case IsSuper orelse allow == check_acl(publish, Topic, client(State)) of
true -> emqttd:publish(WillMsg#mqtt_message{from = {ClientId, Username}});
false -> ?LOG(error, "Cannot publish LWT message to ~s for ACL Deny", [Topic], State)
end.
start_keepalive(0, _State) -> ignore;
start_keepalive(Sec, #proto_state{keepalive_backoff = Backoff}) when Sec > 0 ->
self() ! {keepalive, start, round(Sec * Backoff)}.
%%--------------------------------------------------------------------
%% Validate Packets
%%--------------------------------------------------------------------
validate_connect(Connect = #mqtt_packet_connect{}, ProtoState) ->
case validate_protocol(Connect) of
true ->
case validate_clientid(Connect, ProtoState) of
true ->
?CONNACK_ACCEPT;
false ->
?CONNACK_INVALID_ID
end;
false ->
?CONNACK_PROTO_VER
end.
validate_protocol(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}) ->
lists:member({Ver, Name}, ?PROTOCOL_NAMES).
validate_clientid(#mqtt_packet_connect{client_id = ClientId},
#proto_state{max_clientid_len = MaxLen})
when (byte_size(ClientId) >= 1) andalso (byte_size(ClientId) =< MaxLen) ->
true;
%% Issue#599: Null clientId and clean_sess = false
validate_clientid(#mqtt_packet_connect{client_id = ClientId,
clean_sess = CleanSess}, _ProtoState)
when byte_size(ClientId) == 0 andalso (not CleanSess) ->
false;
%% MQTT3.1.1 allow null clientId.
validate_clientid(#mqtt_packet_connect{proto_ver =?MQTT_PROTO_V4,
client_id = ClientId}, _ProtoState)
when byte_size(ClientId) =:= 0 ->
true;
validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer,
clean_sess = CleanSess}, ProtoState) ->
?LOG(warning, "Invalid clientId. ProtoVer: ~p, CleanSess: ~s",
[ProtoVer, CleanSess], ProtoState),
false.
validate_packet(?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload)) ->
case emqttd_topic:validate({name, Topic}) of
true -> ok;
false -> {error, badtopic}
end;
validate_packet(?SUBSCRIBE_PACKET(_PacketId, TopicTable)) ->
validate_topics(filter, TopicTable);
validate_packet(?UNSUBSCRIBE_PACKET(_PacketId, Topics)) ->
validate_topics(filter, Topics);
validate_packet(_Packet) ->
ok.
validate_topics(_Type, []) ->
{error, empty_topics};
validate_topics(Type, TopicTable = [{_Topic, _Qos}|_])
when Type =:= name orelse Type =:= filter ->
Valid = fun(Topic, Qos) ->
emqttd_topic:validate({Type, Topic}) and validate_qos(Qos)
end,
case [Topic || {Topic, Qos} <- TopicTable, not Valid(Topic, Qos)] of
[] -> ok;
_ -> {error, badtopic}
end;
validate_topics(Type, Topics = [Topic0|_]) when is_binary(Topic0) ->
case [Topic || Topic <- Topics, not emqttd_topic:validate({Type, Topic})] of
[] -> ok;
_ -> {error, badtopic}
end.
validate_qos(undefined) ->
true;
validate_qos(Qos) when ?IS_QOS(Qos) ->
true;
validate_qos(_) ->
false.
parse_topic_table(TopicTable) ->
lists:map(fun({Topic0, Qos}) ->
{Topic, Opts} = emqttd_topic:parse(Topic0),
{Topic, [{qos, Qos}|Opts]}
end, TopicTable).
parse_topics(Topics) ->
[emqttd_topic:parse(Topic) || Topic <- Topics].
authenticate(Client, Password) ->
case emqttd_access_control:auth(Client, Password) of
ok -> {ok, false};
{ok, IsSuper} -> {ok, IsSuper};
{error, Error} -> {error, Error}
end.
%% PUBLISH ACL is cached in process dictionary.
check_acl(publish, Topic, Client) ->
IfCache = emqttd:env(cache_acl, true),
case {IfCache, get({acl, publish, Topic})} of
{true, undefined} ->
AllowDeny = emqttd_access_control:check_acl(Client, publish, Topic),
put({acl, publish, Topic}, AllowDeny),
AllowDeny;
{true, AllowDeny} ->
AllowDeny;
{false, _} ->
emqttd_access_control:check_acl(Client, publish, Topic)
end;
check_acl(subscribe, Topic, Client) ->
emqttd_access_control:check_acl(Client, subscribe, Topic).
sp(true) -> 1;
sp(false) -> 0.
%%--------------------------------------------------------------------
%% The retained flag should be propagated for bridge.
%%--------------------------------------------------------------------
clean_retain(false, Msg = #mqtt_message{retain = true, headers = Headers}) ->
case lists:member(retained, Headers) of
true -> Msg;
false -> Msg#mqtt_message{retain = false}
end;
clean_retain(_IsBridge, Msg) ->
Msg.
%%--------------------------------------------------------------------
%% Mount Point
%%--------------------------------------------------------------------
mount(undefined, Any) ->
Any;
mount(MountPoint, Msg = #mqtt_message{topic = Topic}) ->
Msg#mqtt_message{topic = <<MountPoint/binary, Topic/binary>>};
mount(MountPoint, TopicTable) when is_list(TopicTable) ->
[{<<MountPoint/binary, Topic/binary>>, Opts} || {Topic, Opts} <- TopicTable].
unmount(undefined, Any) ->
Any;
unmount(MountPoint, Msg = #mqtt_message{topic = Topic}) ->
case catch split_binary(Topic, byte_size(MountPoint)) of
{MountPoint, Topic0} -> Msg#mqtt_message{topic = Topic0};
_ -> Msg
end.

View File

@ -1,252 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_pubsub).
-behaviour(gen_server2).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_internal.hrl").
-export([start_link/3]).
%% PubSub API.
-export([subscribe/3, async_subscribe/3, publish/2, unsubscribe/3,
async_unsubscribe/3, subscribers/1]).
-export([dispatch/2]).
%% gen_server Callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {pool, id, env}).
-define(PUBSUB, ?MODULE).
-define(is_local(Options), lists:member(local, Options)).
%%--------------------------------------------------------------------
%% Start PubSub
%%--------------------------------------------------------------------
-spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, term()}).
start_link(Pool, Id, Env) ->
gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []).
%%--------------------------------------------------------------------
%% PubSub API
%%--------------------------------------------------------------------
%% @doc Subscribe to a Topic
-spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok).
subscribe(Topic, Subscriber, Options) ->
call(pick(Topic), {subscribe, Topic, Subscriber, Options}).
-spec(async_subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok).
async_subscribe(Topic, Subscriber, Options) ->
cast(pick(Topic), {subscribe, Topic, Subscriber, Options}).
%% @doc Publish MQTT Message to Topic.
-spec(publish(binary(), mqtt_message()) -> {ok, mqtt_delivery()} | ignore).
publish(Topic, Msg) ->
route(lists:append(emqttd_router:match(Topic),
emqttd_router:match_local(Topic)), delivery(Msg)).
route([], #mqtt_delivery{message = #mqtt_message{topic = Topic}}) ->
dropped(Topic), ignore;
%% Dispatch on the local node.
route([#mqtt_route{topic = To, node = Node}],
Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() ->
dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]});
%% Forward to other nodes
route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) ->
forward(Node, To, Delivery#mqtt_delivery{flows = [{route, Node, To}|Flows]});
route(Routes, Delivery) ->
{ok, lists:foldl(fun(Route, Acc) ->
{ok, Acc1} = route([Route], Acc), Acc1
end, Delivery, Routes)}.
delivery(Msg) -> #mqtt_delivery{sender = self(), message = Msg, flows = []}.
%% @doc Forward message to another node...
forward(Node, To, Delivery) ->
rpc:cast(Node, ?PUBSUB, dispatch, [To, Delivery]), {ok, Delivery}.
%% @doc Dispatch Message to Subscribers.
-spec(dispatch(binary(), mqtt_delivery()) -> mqtt_delivery()).
dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) ->
case subscribers(Topic) of
[] ->
dropped(Topic), {ok, Delivery};
[Sub] -> %% optimize?
dispatch(Sub, Topic, Msg),
{ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1}|Flows]}};
Subscribers ->
Flows1 = [{dispatch, Topic, length(Subscribers)} | Flows],
lists:foreach(fun(Sub) -> dispatch(Sub, Topic, Msg) end, Subscribers),
{ok, Delivery#mqtt_delivery{flows = Flows1}}
end.
%%TODO: Is SubPid aliving???
dispatch(SubPid, 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({{share, _Share}, [Sub]}, Topic, Msg) ->
dispatch(Sub, Topic, Msg);
dispatch({{share, _Share}, []}, _Topic, _Msg) ->
ok;
dispatch({{share, _Share}, Subs}, Topic, Msg) -> %% round-robbin?
dispatch(lists:nth(rand:uniform(length(Subs)), Subs), Topic, Msg).
subscribers(Topic) ->
group_by_share(try ets:lookup_element(mqtt_subscriber, Topic, 2) catch error:badarg -> [] end).
group_by_share([]) -> [];
group_by_share(Subscribers) ->
{Subs1, Shares1} =
lists:foldl(fun({share, Share, Sub}, {Subs, Shares}) ->
{Subs, dict:append({share, Share}, Sub, Shares)};
(Sub, {Subs, Shares}) ->
{[Sub|Subs], Shares}
end, {[], dict:new()}, Subscribers),
lists:append(Subs1, dict:to_list(Shares1)).
%% @private
%% @doc Ingore $SYS Messages.
dropped(<<"$SYS/", _/binary>>) ->
ok;
dropped(_Topic) ->
emqttd_metrics:inc('messages/dropped').
%% @doc Unsubscribe
-spec(unsubscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok).
unsubscribe(Topic, Subscriber, Options) ->
call(pick(Topic), {unsubscribe, Topic, Subscriber, Options}).
-spec(async_unsubscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok).
async_unsubscribe(Topic, Subscriber, Options) ->
cast(pick(Topic), {unsubscribe, Topic, Subscriber, Options}).
call(PubSub, Req) when is_pid(PubSub) ->
gen_server2:call(PubSub, Req, infinity).
cast(PubSub, Msg) when is_pid(PubSub) ->
gen_server2:cast(PubSub, Msg).
pick(Topic) ->
gproc_pool:pick_worker(pubsub, Topic).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([Pool, Id, Env]) ->
?GPROC_POOL(join, Pool, Id),
{ok, #state{pool = Pool, id = Id, env = Env},
hibernate, {backoff, 2000, 2000, 20000}}.
handle_call({subscribe, Topic, Subscriber, Options}, _From, State) ->
add_subscriber(Topic, Subscriber, Options),
reply(ok, setstats(State));
handle_call({unsubscribe, Topic, Subscriber, Options}, _From, State) ->
del_subscriber(Topic, Subscriber, Options),
reply(ok, setstats(State));
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast({subscribe, Topic, Subscriber, Options}, State) ->
add_subscriber(Topic, Subscriber, Options),
noreply(setstats(State));
handle_cast({unsubscribe, Topic, Subscriber, Options}, State) ->
del_subscriber(Topic, Subscriber, Options),
noreply(setstats(State));
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(_Reason, #state{pool = Pool, id = Id}) ->
?GPROC_POOL(leave, Pool, Id).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internel Functions
%%--------------------------------------------------------------------
add_subscriber(Topic, Subscriber, Options) ->
Share = proplists:get_value(share, Options),
case ?is_local(Options) of
false -> add_global_subscriber(Share, Topic, Subscriber);
true -> add_local_subscriber(Share, Topic, Subscriber)
end.
add_global_subscriber(Share, Topic, Subscriber) ->
case ets:member(mqtt_subscriber, Topic) and emqttd_router:has_route(Topic) of
true -> ok;
false -> emqttd_router:add_route(Topic)
end,
ets:insert(mqtt_subscriber, {Topic, shared(Share, Subscriber)}).
add_local_subscriber(Share, Topic, Subscriber) ->
(not ets:member(mqtt_subscriber, {local, Topic})) andalso emqttd_router:add_local_route(Topic),
ets:insert(mqtt_subscriber, {{local, Topic}, shared(Share, Subscriber)}).
del_subscriber(Topic, Subscriber, Options) ->
Share = proplists:get_value(share, Options),
case ?is_local(Options) of
false -> del_global_subscriber(Share, Topic, Subscriber);
true -> del_local_subscriber(Share, Topic, Subscriber)
end.
del_global_subscriber(Share, Topic, Subscriber) ->
ets:delete_object(mqtt_subscriber, {Topic, shared(Share, Subscriber)}),
(not ets:member(mqtt_subscriber, Topic)) andalso emqttd_router:del_route(Topic).
del_local_subscriber(Share, Topic, Subscriber) ->
ets:delete_object(mqtt_subscriber, {{local, Topic}, shared(Share, Subscriber)}),
(not ets:member(mqtt_subscriber, {local, Topic})) andalso emqttd_router:del_local_route(Topic).
shared(undefined, Subscriber) ->
Subscriber;
shared(Share, Subscriber) ->
{share, Share, Subscriber}.
setstats(State) ->
emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(mqtt_subscriber, size)),
State.
reply(Reply, State) ->
{reply, Reply, State, hibernate}.
noreply(State) ->
{noreply, State, hibernate}.

View File

@ -1,86 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc PubSub Supervisor.
-module(emqttd_pubsub_sup).
-author("Feng Lee <feng@emqtt.io>").
-behaviour(supervisor).
%% API
-export([start_link/0, pubsub_pool/0]).
%% Supervisor callbacks
-export([init/1]).
-define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
pubsub_pool() ->
hd([Pid || {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]).
%%--------------------------------------------------------------------
%% Supervisor Callbacks
%%--------------------------------------------------------------------
init([]) ->
{ok, Env} = emqttd:env(pubsub),
%% Create ETS Tables
[create_tab(Tab) || Tab <- [mqtt_subproperty, mqtt_subscriber, mqtt_subscription]],
{ok, { {one_for_all, 10, 3600}, [pool_sup(pubsub, Env), pool_sup(server, Env)]} }.
%%--------------------------------------------------------------------
%% Pool
%%--------------------------------------------------------------------
pool_size(Env) ->
Schedulers = erlang:system_info(schedulers),
proplists:get_value(pool_size, Env, Schedulers).
pool_sup(Name, Env) ->
Pool = list_to_atom(atom_to_list(Name) ++ "_pool"),
Mod = list_to_atom("emqttd_" ++ atom_to_list(Name)),
MFA = {Mod, start_link, [Env]},
emqttd_pool_sup:spec(Pool, [Name, hash, pool_size(Env), MFA]).
%%--------------------------------------------------------------------
%% Create PubSub Tables
%%--------------------------------------------------------------------
create_tab(mqtt_subproperty) ->
%% Subproperty: {Topic, Sub} -> [{qos, 1}]
ensure_tab(mqtt_subproperty, [public, named_table, set | ?CONCURRENCY_OPTS]);
create_tab(mqtt_subscriber) ->
%% Subscriber: Topic -> Sub1, Sub2, Sub3, ..., SubN
%% duplicate_bag: o(1) insert
ensure_tab(mqtt_subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]);
create_tab(mqtt_subscription) ->
%% Subscription: Sub -> Topic1, Topic2, Topic3, ..., TopicN
%% bag: o(n) insert
ensure_tab(mqtt_subscription, [public, named_table, bag | ?CONCURRENCY_OPTS]).
ensure_tab(Tab, Opts) ->
case ets:info(Tab, name) of undefined -> ets:new(Tab, Opts); _ -> ok end.

View File

@ -1,550 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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 (emqttd_rest_api).
-include("emqttd.hrl").
-include("emqttd_internal.hrl").
-http_api({"^nodes/(.+?)/alarms/?$", 'GET', alarm_list, []}).
-http_api({"^nodes/(.+?)/clients/?$", 'GET', client_list, []}).
-http_api({"^nodes/(.+?)/clients/(.+?)/?$", 'GET',client_list, []}).
-http_api({"^clients/(.+?)/?$", 'GET', client, []}).
-http_api({"^clients/(.+?)/?$", 'DELETE', kick_client, []}).
-http_api({"^clients/(.+?)/clean_acl_cache?$", 'PUT', clean_acl_cache, [{<<"topic">>, binary}]}).
-http_api({"^routes?$", 'GET', route_list, []}).
-http_api({"^routes/(.+?)/?$", 'GET', route, []}).
-http_api({"^nodes/(.+?)/sessions/?$", 'GET', session_list, []}).
-http_api({"^nodes/(.+?)/sessions/(.+?)/?$", 'GET', session_list, []}).
-http_api({"^sessions/(.+?)/?$", 'GET', session, []}).
-http_api({"^nodes/(.+?)/subscriptions/?$", 'GET', subscription_list, []}).
-http_api({"^nodes/(.+?)/subscriptions/(.+?)/?$", 'GET', subscription_list, []}).
-http_api({"^subscriptions/(.+?)/?$", 'GET', subscription, []}).
-http_api({"^mqtt/publish?$", 'POST', publish, [{<<"topic">>, binary}, {<<"payload">>, binary}]}).
-http_api({"^mqtt/subscribe?$", 'POST', subscribe, [{<<"client_id">>, binary},{<<"topic">>, binary}]}).
-http_api({"^mqtt/unsubscribe?$", 'POST', unsubscribe, [{<<"client_id">>, binary},{<<"topic">>, binary}]}).
-http_api({"^management/nodes/?$", 'GET', brokers, []}).
-http_api({"^management/nodes/(.+?)/?$", 'GET', broker, []}).
-http_api({"^monitoring/nodes/?$", 'GET', nodes, []}).
-http_api({"^monitoring/nodes/(.+?)/?$", 'GET', node, []}).
-http_api({"^monitoring/listeners/?$", 'GET', listeners, []}).
-http_api({"^monitoring/listeners/(.+?)/?$", 'GET', listener, []}).
-http_api({"^monitoring/metrics/?$", 'GET', metrics, []}).
-http_api({"^monitoring/metrics/(.+?)/?$", 'GET', metric, []}).
-http_api({"^monitoring/stats/?$", 'GET', stats, []}).
-http_api({"^monitoring/stats/(.+?)/?$", 'GET', stat, []}).
-http_api({"^nodes/(.+?)/plugins/?$", 'GET', plugin_list, []}).
-http_api({"^nodes/(.+?)/plugins/(.+?)/?$", 'PUT', enabled, [{<<"active">>, bool}]}).
-http_api({"^configs/(.+?)/?$", 'PUT', modify_config, [{<<"key">>, binary}, {<<"value">>, binary}]}).
-http_api({"^configs/?$", 'GET', config_list, []}).
-http_api({"^nodes/(.+?)/configs/(.+?)/?$", 'PUT', modify_config, [{<<"key">>, binary}, {<<"value">>, binary}]}).
-http_api({"^nodes/(.+?)/configs/?$", 'GET', config_list, []}).
-http_api({"^nodes/(.+?)/plugin_configs/(.+?)/?$", 'GET', plugin_config_list, []}).
-http_api({"^nodes/(.+?)/plugin_configs/(.+?)/?$", 'PUT', modify_plugin_config, []}).
-http_api({"^users/?$", 'GET', users, []}).
-http_api({"^users/?$", 'POST', users, [{<<"username">>, binary},
{<<"password">>, binary},
{<<"tags">>, binary}]}).
-http_api({"^users/(.+?)/?$", 'GET', users, []}).
-http_api({"^users/(.+?)/?$", 'PUT', users, [{<<"tags">>, binary}]}).
-http_api({"^users/(.+?)/?$", 'DELETE', users, []}).
-http_api({"^auth/?$", 'POST', auth, [{<<"username">>, binary}, {<<"password">>, binary}]}).
-http_api({"^change_pwd/(.+?)/?$", 'PUT', change_pwd, [{<<"old_pwd">>, binary},
{<<"new_pwd">>, binary}]}).
-import(proplists, [get_value/2, get_value/3]).
-export([alarm_list/3]).
-export([client/3, client_list/3, client_list/4, kick_client/3, clean_acl_cache/3]).
-export([route/3, route_list/2]).
-export([session/3, session_list/3, session_list/4]).
-export([subscription/3, subscription_list/3, subscription_list/4]).
-export([nodes/2, node/3, brokers/2, broker/3, listeners/2, listener/3, metrics/2, metric/3, stats/2, stat/3]).
-export([publish/2, subscribe/2, unsubscribe/2]).
-export([plugin_list/3, enabled/4]).
-export([modify_config/3, modify_config/4, config_list/2, config_list/3,
plugin_config_list/4, modify_plugin_config/4]).
-export([users/2,users/3, auth/2, change_pwd/3]).
%%--------------------------------------------------------------------------
%% alarm
%%--------------------------------------------------------------------------
alarm_list('GET', _Req, _Node) ->
Alarms = emqttd_mgmt:alarm_list(),
{ok, lists:map(fun alarm_row/1, Alarms)}.
alarm_row(#mqtt_alarm{id = AlarmId,
severity = Severity,
title = Title,
summary = Summary,
timestamp = Timestamp}) ->
[{id, AlarmId},
{severity, Severity},
{title, l2b(Title)},
{summary, l2b(Summary)},
{occurred_at, l2b(strftime(Timestamp))}].
%%--------------------------------------------------------------------------
%% client
%%--------------------------------------------------------------------------
client('GET', _Params, Key) ->
Data = emqttd_mgmt:client(l2b(Key)),
{ok, [{objects, [client_row(Row) || Row <- Data]}]}.
client_list('GET', Params, Node) ->
{PageNo, PageSize} = page_params(Params),
Data = emqttd_mgmt:client_list(l2a(Node), undefined, PageNo, PageSize),
Rows = get_value(result, Data),
TotalPage = get_value(totalPage, Data),
TotalNum = get_value(totalNum, Data),
{ok, [{current_page, PageNo},
{page_size, PageSize},
{total_num, TotalNum},
{total_page, TotalPage},
{objects, [client_row(Row) || Row <- Rows]}]}.
client_list('GET', Params, Node, Key) ->
{PageNo, PageSize} = page_params(Params),
Data = emqttd_mgmt:client_list(l2a(Node), l2b(Key), PageNo, PageSize),
{ok, [{objects, [client_row(Row) || Row <- Data]}]}.
kick_client('DELETE', _Params, Key) ->
case emqttd_mgmt:kick_client(l2b(Key)) of
true -> {ok, []};
false -> {error, [{code, ?ERROR12}]}
end.
clean_acl_cache('PUT', Params, Key0) ->
Topic = get_value(<<"topic">>, Params),
[Key | _] = string:tokens(Key0, "/"),
case emqttd_mgmt:clean_acl_cache(l2b(Key), Topic) of
true -> {ok, []};
false -> {error, [{code, ?ERROR12}]}
end.
client_row(#mqtt_client{client_id = ClientId,
peername = {IpAddr, Port},
username = Username,
clean_sess = CleanSess,
proto_ver = ProtoVer,
keepalive = KeepAlvie,
connected_at = ConnectedAt}) ->
[{client_id, ClientId},
{username, Username},
{ipaddress, l2b(ntoa(IpAddr))},
{port, Port},
{clean_sess, CleanSess},
{proto_ver, ProtoVer},
{keepalive, KeepAlvie},
{connected_at, l2b(strftime(ConnectedAt))}].
%%--------------------------------------------------------------------------
%% route
%%--------------------------------------------------------------------------
route('GET', _Params, Key) ->
Data = emqttd_mgmt:route(l2b(Key)),
{ok, [{objects, [route_row(Row) || Row <- Data]}]}.
route_list('GET', Params) ->
{PageNo, PageSize} = page_params(Params),
Data = emqttd_mgmt:route_list(undefined, PageNo, PageSize),
Rows = get_value(result, Data),
TotalPage = get_value(totalPage, Data),
TotalNum = get_value(totalNum, Data),
{ok, [{current_page, PageNo},
{page_size, PageSize},
{total_num, TotalNum},
{total_page, TotalPage},
{objects, [route_row(Row) || Row <- Rows]}]}.
route_row(Route) when is_record(Route, mqtt_route) ->
[{topic, Route#mqtt_route.topic}, {node, Route#mqtt_route.node}];
route_row({Topic, Node}) ->
[{topic, Topic}, {node, Node}].
%%--------------------------------------------------------------------------
%% session
%%--------------------------------------------------------------------------
session('GET', _Params, Key) ->
Data = emqttd_mgmt:session(l2b(Key)),
{ok, [{objects, [session_row(Row) || Row <- Data]}]}.
session_list('GET', Params, Node) ->
{PageNo, PageSize} = page_params(Params),
Data = emqttd_mgmt:session_list(l2a(Node), undefined, PageNo, PageSize),
Rows = get_value(result, Data),
TotalPage = get_value(totalPage, Data),
TotalNum = get_value(totalNum, Data),
{ok, [{current_page, PageNo},
{page_size, PageSize},
{total_num, TotalNum},
{total_page, TotalPage},
{objects, [session_row(Row) || Row <- Rows]}]}.
session_list('GET', Params, Node, ClientId) ->
{PageNo, PageSize} = page_params(Params),
Data = emqttd_mgmt:session_list(l2a(Node), l2b(ClientId), PageNo, PageSize),
{ok, [{objects, [session_row(Row) || Row <- Data]}]}.
session_row({ClientId, _Pid, _Persistent, Session}) ->
Data = lists:append(Session, emqttd_stats:get_session_stats(ClientId)),
InfoKeys = [clean_sess, subscriptions, max_inflight, inflight_len, mqueue_len,
mqueue_dropped, awaiting_rel_len, deliver_msg,enqueue_msg, created_at],
[{client_id, ClientId} | [{Key, format(Key, get_value(Key, Data))} || Key <- InfoKeys]].
%%--------------------------------------------------------------------------
%% subscription
%%--------------------------------------------------------------------------
subscription('GET', _Params, Key) ->
Data = emqttd_mgmt:subscription(l2b(Key)),
{ok, [{objects, [subscription_row(Row) || Row <- Data]}]}.
subscription_list('GET', Params, Node) ->
{PageNo, PageSize} = page_params(Params),
Data = emqttd_mgmt:subscription_list(l2a(Node), undefined, PageNo, PageSize),
Rows = get_value(result, Data),
TotalPage = get_value(totalPage, Data),
TotalNum = get_value(totalNum, Data),
{ok, [{current_page, PageNo},
{page_size, PageSize},
{total_num, TotalNum},
{total_page, TotalPage},
{objects, [subscription_row(Row) || Row <- Rows]}]}.
subscription_list('GET', Params, Node, Key) ->
{PageNo, PageSize} = page_params(Params),
Data = emqttd_mgmt:subscription_list(l2a(Node), l2b(Key), PageNo, PageSize),
{ok, [{objects, [subscription_row(Row) || Row <- Data]}]}.
subscription_row({{Topic, SubPid}, Options}) when is_pid(SubPid) ->
subscription_row({{Topic, {undefined, SubPid}}, Options});
subscription_row({{Topic, {SubId, SubPid}}, Options}) ->
Qos = proplists:get_value(qos, Options),
ClientId = case SubId of
undefined -> list_to_binary(pid_to_list(SubPid));
SubId -> SubId
end,
[{client_id, ClientId}, {topic, Topic}, {qos, Qos}].
%%--------------------------------------------------------------------------
%% management/monitoring
%%--------------------------------------------------------------------------
nodes('GET', _Params) ->
Data = emqttd_mgmt:nodes_info(),
{ok, Data}.
node('GET', _Params, Node) ->
Data = emqttd_mgmt:node_info(l2a(Node)),
{ok, Data}.
brokers('GET', _Params) ->
Data = emqttd_mgmt:brokers(),
{ok, [format_broker(Node, Broker) || {Node, Broker} <- Data]}.
broker('GET', _Params, Node) ->
Data = emqttd_mgmt:broker(l2a(Node)),
{ok, format_broker(Data)}.
listeners('GET', _Params) ->
Data = emqttd_mgmt:listeners(),
{ok, [[{Node, format_listeners(Listeners, [])} || {Node, Listeners} <- Data]]}.
listener('GET', _Params, Node) ->
Data = emqttd_mgmt:listener(l2a(Node)),
{ok, [format_listener(Listeners) || Listeners <- Data]}.
metrics('GET', _Params) ->
Data = emqttd_mgmt:metrics(),
{ok, [Data]}.
metric('GET', _Params, Node) ->
Data = emqttd_mgmt:metrics(l2a(Node)),
{ok, Data}.
stats('GET', _Params) ->
Data = emqttd_mgmt:stats(),
{ok, [Data]}.
stat('GET', _Params, Node) ->
Data = emqttd_mgmt:stats(l2a(Node)),
{ok, Data}.
format_broker(Node, Broker) ->
OtpRel = "R" ++ erlang:system_info(otp_release) ++ "/" ++ erlang:system_info(version),
[{name, Node},
{version, bin(get_value(version, Broker))},
{sysdescr, bin(get_value(sysdescr, Broker))},
{uptime, bin(get_value(uptime, Broker))},
{datetime, bin(get_value(datetime, Broker))},
{otp_release, l2b(OtpRel)},
{node_status, 'Running'}].
format_broker(Broker) ->
OtpRel = "R" ++ erlang:system_info(otp_release) ++ "/" ++ erlang:system_info(version),
[{version, bin(get_value(version, Broker))},
{sysdescr, bin(get_value(sysdescr, Broker))},
{uptime, bin(get_value(uptime, Broker))},
{datetime, bin(get_value(datetime, Broker))},
{otp_release, l2b(OtpRel)},
{node_status, 'Running'}].
format_listeners([], Acc) ->
Acc;
format_listeners([{Protocol, ListenOn, Info}| Listeners], Acc) ->
format_listeners(Listeners, [format_listener({Protocol, ListenOn, Info}) | Acc]).
format_listener({Protocol, ListenOn, Info}) ->
Listen = l2b(esockd:to_string(ListenOn)),
lists:append([{protocol, Protocol}, {listen, Listen}], Info).
%%--------------------------------------------------------------------------
%% mqtt
%%--------------------------------------------------------------------------
publish('POST', Params) ->
Topic = get_value(<<"topic">>, Params),
ClientId = get_value(<<"client_id">>, Params, http),
Payload = get_value(<<"payload">>, Params, <<>>),
Qos = get_value(<<"qos">>, Params, 0),
Retain = get_value(<<"retain">>, Params, false),
case emqttd_mgmt:publish({ClientId, Topic, Payload, Qos, Retain}) of
ok ->
{ok, []};
{error, Error} ->
{error, [{code, ?ERROR2}, {message, Error}]}
end.
subscribe('POST', Params) ->
ClientId = get_value(<<"client_id">>, Params),
Topic = get_value(<<"topic">>, Params),
Qos = get_value(<<"qos">>, Params, 0),
case emqttd_mgmt:subscribe({ClientId, Topic, Qos}) of
ok ->
{ok, []};
{error, Error} ->
{error, [{code, ?ERROR2}, {message, Error}]}
end.
unsubscribe('POST', Params) ->
ClientId = get_value(<<"client_id">>, Params),
Topic = get_value(<<"topic">>, Params),
case emqttd_mgmt:unsubscribe({ClientId, Topic})of
ok ->
{ok, []};
{error, Error} ->
{error, [{code, ?ERROR2}, {message, Error}]}
end.
%%--------------------------------------------------------------------------
%% plugins
%%--------------------------------------------------------------------------
plugin_list('GET', _Params, Node) ->
Plugins = lists:map(fun plugin/1, emqttd_mgmt:plugin_list(l2a(Node))),
{ok, Plugins}.
enabled('PUT', Params, Node, PluginName) ->
Active = get_value(<<"active">>, Params),
case Active of
true ->
return(emqttd_mgmt:plugin_load(l2a(Node), l2a(PluginName)));
false ->
return(emqttd_mgmt:plugin_unload(l2a(Node), l2a(PluginName)))
end.
return(Result) ->
case Result of
ok ->
{ok, []};
{ok, _} ->
{ok, []};
{error, already_started} ->
{error, [{code, ?ERROR10}, {message, <<"already_started">>}]};
{error, not_started} ->
{error, [{code, ?ERROR11}, {message, <<"not_started">>}]};
Error ->
lager:error("error:~p", [Error]),
{error, [{code, ?ERROR2}, {message, <<"unknown">>}]}
end.
plugin(#mqtt_plugin{name = Name, version = Ver, descr = Descr,
active = Active}) ->
[{name, Name},
{version, iolist_to_binary(Ver)},
{description, iolist_to_binary(Descr)},
{active, Active}].
%%--------------------------------------------------------------------------
%% modify config
%%--------------------------------------------------------------------------
modify_config('PUT', Params, App) ->
Key = get_value(<<"key">>, Params, <<"">>),
Value = get_value(<<"value">>, Params, <<"">>),
case emqttd_mgmt:modify_config(l2a(App), b2l(Key), b2l(Value)) of
true -> {ok, []};
false -> {error, [{code, ?ERROR2}]}
end.
modify_config('PUT', Params, Node, App) ->
Key = get_value(<<"key">>, Params, <<"">>),
Value = get_value(<<"value">>, Params, <<"">>),
case emqttd_mgmt:modify_config(l2a(Node), l2a(App), b2l(Key), b2l(Value)) of
ok -> {ok, []};
_ -> {error, [{code, ?ERROR2}]}
end.
config_list('GET', _Params) ->
Data = emqttd_mgmt:get_configs(),
{ok, [{Node, format_config(Config, [])} || {Node, Config} <- Data]}.
config_list('GET', _Params, Node) ->
Data = emqttd_mgmt:get_config(l2a(Node)),
{ok, [format_config(Config) || Config <- lists:reverse(Data)]}.
plugin_config_list('GET', _Params, Node, App) ->
{ok, Data} = emqttd_mgmt:get_plugin_config(l2a(Node), l2a(App)),
{ok, [format_plugin_config(Config) || Config <- lists:reverse(Data)]}.
modify_plugin_config('PUT', Params, Node, App) ->
PluginName = l2a(App),
case emqttd_mgmt:modify_plugin_config(l2a(Node), PluginName, Params) of
ok ->
Plugins = emqttd_plugins:list(),
{_, _, _, _, Status} = lists:keyfind(PluginName, 2, Plugins),
case Status of
true ->
emqttd_plugins:unload(PluginName),
timer:sleep(500),
emqttd_plugins:load(PluginName),
{ok, []};
false ->
{ok, []}
end;
_ ->
{error, [{code, ?ERROR2}]}
end.
format_config([], Acc) ->
Acc;
format_config([{Key, Value, Datatpye, App}| Configs], Acc) ->
format_config(Configs, [format_config({Key, Value, Datatpye, App}) | Acc]).
format_config({Key, Value, Datatpye, App}) ->
[{<<"key">>, l2b(Key)},
{<<"value">>, l2b(Value)},
{<<"datatpye">>, l2b(Datatpye)},
{<<"app">>, App}].
format_plugin_config({Key, Value, Desc, Required}) ->
[{<<"key">>, l2b(Key)},
{<<"value">>, l2b(Value)},
{<<"desc">>, l2b(Desc)},
{<<"required">>, Required}].
%%--------------------------------------------------------------------------
%% Admin
%%--------------------------------------------------------------------------
auth('POST', Params) ->
Username = get_value(<<"username">>, Params),
Password = get_value(<<"password">>, Params),
case emqttd_mgmt:check_user(Username, Password) of
ok ->
{ok, []};
{error, Reason} ->
{error, [{code, ?ERROR3}, {message, list_to_binary(Reason)}]}
end.
users('POST', Params) ->
Username = get_value(<<"username">>, Params),
Password = get_value(<<"password">>, Params),
Tag = get_value(<<"tags">>, Params),
code(emqttd_mgmt:add_user(Username, Password, Tag));
users('GET', _Params) ->
{ok, [Admin || Admin <- emqttd_mgmt:user_list()]}.
users('GET', _Params, Username) ->
{ok, emqttd_mgmt:lookup_user(list_to_binary(Username))};
users('PUT', Params, Username) ->
code(emqttd_mgmt:update_user(list_to_binary(Username), Params));
users('DELETE', _Params, "admin") ->
{error, [{code, ?ERROR6}, {message, <<"admin cannot be deleted">>}]};
users('DELETE', _Params, Username) ->
code(emqttd_mgmt:remove_user(list_to_binary(Username))).
change_pwd('PUT', Params, Username) ->
OldPwd = get_value(<<"old_pwd">>, Params),
NewPwd = get_value(<<"new_pwd">>, Params),
code(emqttd_mgmt:change_password(list_to_binary(Username), OldPwd, NewPwd)).
code(ok) -> {ok, []};
code(error) -> {error, [{code, ?ERROR2}]};
code({error, Error}) -> {error, Error}.
%%--------------------------------------------------------------------------
%% Inner function
%%--------------------------------------------------------------------------
format(created_at, Val) ->
l2b(strftime(Val));
format(_, Val) ->
Val.
ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
ntoa(IP) ->
inet_parse:ntoa(IP).
%%--------------------------------------------------------------------
%% Strftime
%%--------------------------------------------------------------------
strftime({MegaSecs, Secs, _MicroSecs}) ->
strftime(datetime(MegaSecs * 1000000 + Secs));
strftime({{Y,M,D}, {H,MM,S}}) ->
lists:flatten(
io_lib:format(
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])).
datetime(Timestamp) when is_integer(Timestamp) ->
Universal = calendar:gregorian_seconds_to_datetime(Timestamp +
calendar:datetime_to_gregorian_seconds({{1970,1,1}, {0,0,0}})),
calendar:universal_time_to_local_time(Universal).
bin(S) when is_list(S) -> l2b(S);
bin(A) when is_atom(A) -> bin(atom_to_list(A));
bin(B) when is_binary(B) -> B;
bin(undefined) -> <<>>.
int(L) -> list_to_integer(L).
l2a(L) -> list_to_atom(L).
l2b(L) -> list_to_binary(L).
b2l(B) -> binary_to_list(B).
page_params(Params) ->
PageNo = int(get_value("curr_page", Params, "1")),
PageSize = int(get_value("page_size", Params, "20")),
{PageNo, PageSize}.

View File

@ -1,290 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_router).
-author("Feng Lee <feng@emqtt.io>").
-behaviour(gen_server).
-include("emqttd.hrl").
%% Mnesia Bootstrap
-export([mnesia/1]).
-boot_mnesia({mnesia, [boot]}).
-copy_mnesia({mnesia, [copy]}).
-export([start_link/0, topics/0, local_topics/0]).
%% For eunit tests
-export([start/0, stop/0]).
%% Route APIs
-export([add_route/1, del_route/1, match/1, print/1, has_route/1]).
%% Local Route API
-export([get_local_routes/0, add_local_route/1, match_local/1,
del_local_route/1, clean_local_routes/0]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([dump/0]).
-record(state, {stats_timer}).
-define(ROUTER, ?MODULE).
-define(LOCK, {?ROUTER, clean_routes}).
%%--------------------------------------------------------------------
%% Mnesia Bootstrap
%%--------------------------------------------------------------------
mnesia(boot) ->
ok = ekka_mnesia:create_table(mqtt_route, [
{type, bag},
{ram_copies, [node()]},
{record_name, mqtt_route},
{attributes, record_info(fields, mqtt_route)}]);
mnesia(copy) ->
ok = ekka_mnesia:copy_table(mqtt_route, ram_copies).
%%--------------------------------------------------------------------
%% Start the Router
%%--------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?ROUTER}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% Topics
%%--------------------------------------------------------------------
-spec(topics() -> list(binary())).
topics() ->
mnesia:dirty_all_keys(mqtt_route).
-spec(local_topics() -> list(binary())).
local_topics() ->
ets:select(mqtt_local_route, [{{'$1', '_'}, [], ['$1']}]).
%%--------------------------------------------------------------------
%% Match API
%%--------------------------------------------------------------------
%% @doc Match Routes.
-spec(match(Topic:: binary()) -> [mqtt_route()]).
match(Topic) when is_binary(Topic) ->
%% Optimize: ets???
Matched = mnesia:ets(fun emqttd_trie:match/1, [Topic]),
%% Optimize: route table will be replicated to all nodes.
lists:append([ets:lookup(mqtt_route, To) || To <- [Topic | Matched]]).
%% @doc Print Routes.
-spec(print(Topic :: binary()) -> [ok]).
print(Topic) ->
[io:format("~s -> ~s~n", [To, Node]) ||
#mqtt_route{topic = To, node = Node} <- match(Topic)].
%%--------------------------------------------------------------------
%% Route Management API
%%--------------------------------------------------------------------
%% @doc Add Route.
-spec(add_route(binary() | mqtt_route()) -> ok | {error, Reason :: term()}).
add_route(Topic) when is_binary(Topic) ->
add_route(#mqtt_route{topic = Topic, node = node()});
add_route(Route = #mqtt_route{topic = Topic}) ->
case emqttd_topic:wildcard(Topic) of
true -> case mnesia:is_transaction() of
true -> add_trie_route(Route);
false -> trans(fun add_trie_route/1, [Route])
end;
false -> add_direct_route(Route)
end.
add_direct_route(Route) ->
mnesia:async_dirty(fun mnesia:write/1, [Route]).
add_trie_route(Route = #mqtt_route{topic = Topic}) ->
case mnesia:wread({mqtt_route, Topic}) of
[] -> emqttd_trie:insert(Topic);
_ -> ok
end,
mnesia:write(Route).
%% @doc Delete Route
-spec(del_route(binary() | mqtt_route()) -> ok | {error, Reason :: term()}).
del_route(Topic) when is_binary(Topic) ->
del_route(#mqtt_route{topic = Topic, node = node()});
del_route(Route = #mqtt_route{topic = Topic}) ->
case emqttd_topic:wildcard(Topic) of
true -> case mnesia:is_transaction() of
true -> del_trie_route(Route);
false -> trans(fun del_trie_route/1, [Route])
end;
false -> del_direct_route(Route)
end.
del_direct_route(Route) ->
mnesia:async_dirty(fun mnesia:delete_object/1, [Route]).
del_trie_route(Route = #mqtt_route{topic = Topic}) ->
case mnesia:wread({mqtt_route, Topic}) of
[Route] -> %% Remove route and trie
mnesia:delete_object(Route),
emqttd_trie:delete(Topic);
[_|_] -> %% Remove route only
mnesia:delete_object(Route);
[] -> ok
end.
%% @doc Has route?
-spec(has_route(binary()) -> boolean()).
has_route(Topic) when is_binary(Topic) ->
ets:member(mqtt_route, Topic).
%% @private
-spec(trans(function(), list(any())) -> ok | {error, term()}).
trans(Fun, Args) ->
case mnesia:transaction(Fun, Args) of
{atomic, _} -> ok;
{aborted, Error} -> {error, Error}
end.
%%--------------------------------------------------------------------
%% Local Route API
%%--------------------------------------------------------------------
-spec(get_local_routes() -> list({binary(), node()})).
get_local_routes() ->
ets:tab2list(mqtt_local_route).
-spec(add_local_route(binary()) -> ok).
add_local_route(Topic) ->
gen_server:call(?ROUTER, {add_local_route, Topic}).
-spec(del_local_route(binary()) -> ok).
del_local_route(Topic) ->
gen_server:call(?ROUTER, {del_local_route, Topic}).
-spec(match_local(binary()) -> [mqtt_route()]).
match_local(Name) ->
case ets:info(mqtt_local_route, size) of
0 -> [];
_ -> ets:foldl(
fun({Filter, Node}, Matched) ->
case emqttd_topic:match(Name, Filter) of
true -> [#mqtt_route{topic = {local, Filter}, node = Node} | Matched];
false -> Matched
end
end, [], mqtt_local_route)
end.
-spec(clean_local_routes() -> ok).
clean_local_routes() ->
gen_server:call(?ROUTER, clean_local_routes).
dump() ->
[{route, ets:tab2list(mqtt_route)}, {local_route, ets:tab2list(mqtt_local_route)}].
%% For unit test.
start() ->
gen_server:start({local, ?ROUTER}, ?MODULE, [], []).
stop() ->
gen_server:call(?ROUTER, stop).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([]) ->
ekka:monitor(membership),
ets:new(mqtt_local_route, [set, named_table, protected]),
{ok, TRef} = timer:send_interval(timer:seconds(1), stats),
{ok, #state{stats_timer = TRef}}.
handle_call({add_local_route, Topic}, _From, State) ->
%% why node()...?
ets:insert(mqtt_local_route, {Topic, node()}),
{reply, ok, State};
handle_call({del_local_route, Topic}, _From, State) ->
ets:delete(mqtt_local_route, Topic),
{reply, ok, State};
handle_call(clean_local_routes, _From, State) ->
ets:delete_all_objects(mqtt_local_route),
{reply, ok, State};
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(_Req, _From, State) ->
{reply, ignore, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({membership, {mnesia, down, Node}}, State) ->
global:trans({?LOCK, self()},
fun() ->
clean_routes_(Node),
update_stats_()
end),
{noreply, State, hibernate};
handle_info({membership, _Event}, State) ->
%% ignore
{noreply, State};
handle_info(stats, State) ->
update_stats_(),
{noreply, State, hibernate};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{stats_timer = TRef}) ->
timer:cancel(TRef),
ekka:unmonitor(membership).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal Functions
%%--------------------------------------------------------------------
%% Clean Routes on Node
clean_routes_(Node) ->
Pattern = #mqtt_route{_ = '_', node = Node},
Clean = fun() ->
[mnesia:delete_object(mqtt_route, R, write) ||
R <- mnesia:match_object(mqtt_route, Pattern, write)]
end,
mnesia:transaction(Clean).
update_stats_() ->
Size = mnesia:table_info(mqtt_route, size),
emqttd_stats:setstats('routes/count', 'routes/max', Size),
emqttd_stats:setstats('topics/count', 'topics/max', Size).

View File

@ -1,150 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc MQTT Packet Serializer
-module(emqttd_serializer).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
%% API
-export([serialize/1]).
%% @doc Serialise MQTT Packet
-spec(serialize(mqtt_packet()) -> iolist()).
serialize(#mqtt_packet{header = Header = #mqtt_packet_header{type = Type},
variable = Variable,
payload = Payload}) ->
serialize_header(Header,
serialize_variable(Type, Variable,
serialize_payload(Payload))).
serialize_header(#mqtt_packet_header{type = Type,
dup = Dup,
qos = Qos,
retain = Retain},
{VariableBin, PayloadBin})
when ?CONNECT =< Type andalso Type =< ?DISCONNECT ->
Len = byte_size(VariableBin) + byte_size(PayloadBin),
true = (Len =< ?MAX_PACKET_SIZE),
[<<Type:4, (opt(Dup)):1, (opt(Qos)):2, (opt(Retain)):1>>,
serialize_len(Len), VariableBin, PayloadBin].
serialize_variable(?CONNECT, #mqtt_packet_connect{client_id = ClientId,
proto_ver = ProtoVer,
proto_name = ProtoName,
will_retain = WillRetain,
will_qos = WillQos,
will_flag = WillFlag,
clean_sess = CleanSess,
keep_alive = KeepAlive,
will_topic = WillTopic,
will_msg = WillMsg,
username = Username,
password = Password}, undefined) ->
VariableBin = <<(byte_size(ProtoName)):16/big-unsigned-integer,
ProtoName/binary,
ProtoVer:8,
(opt(Username)):1,
(opt(Password)):1,
(opt(WillRetain)):1,
WillQos:2,
(opt(WillFlag)):1,
(opt(CleanSess)):1,
0:1,
KeepAlive:16/big-unsigned-integer>>,
PayloadBin = serialize_utf(ClientId),
PayloadBin1 = case WillFlag of
true -> <<PayloadBin/binary,
(serialize_utf(WillTopic))/binary,
(byte_size(WillMsg)):16/big-unsigned-integer,
WillMsg/binary>>;
false -> PayloadBin
end,
UserPasswd = << <<(serialize_utf(B))/binary>> || B <- [Username, Password], B =/= undefined >>,
{VariableBin, <<PayloadBin1/binary, UserPasswd/binary>>};
serialize_variable(?CONNACK, #mqtt_packet_connack{ack_flags = AckFlags,
return_code = ReturnCode}, undefined) ->
{<<AckFlags:8, ReturnCode:8>>, <<>>};
serialize_variable(?SUBSCRIBE, #mqtt_packet_subscribe{packet_id = PacketId,
topic_table = Topics }, undefined) ->
{<<PacketId:16/big>>, serialize_topics(Topics)};
serialize_variable(?SUBACK, #mqtt_packet_suback{packet_id = PacketId,
qos_table = QosTable}, undefined) ->
{<<PacketId:16/big>>, << <<Q:8>> || Q <- QosTable >>};
serialize_variable(?UNSUBSCRIBE, #mqtt_packet_unsubscribe{packet_id = PacketId,
topics = Topics }, undefined) ->
{<<PacketId:16/big>>, serialize_topics(Topics)};
serialize_variable(?UNSUBACK, #mqtt_packet_unsuback{packet_id = PacketId}, undefined) ->
{<<PacketId:16/big>>, <<>>};
serialize_variable(?PUBLISH, #mqtt_packet_publish{topic_name = TopicName,
packet_id = PacketId }, PayloadBin) ->
TopicBin = serialize_utf(TopicName),
PacketIdBin = if
PacketId =:= undefined -> <<>>;
true -> <<PacketId:16/big>>
end,
{<<TopicBin/binary, PacketIdBin/binary>>, PayloadBin};
serialize_variable(PubAck, #mqtt_packet_puback{packet_id = PacketId}, _Payload)
when PubAck =:= ?PUBACK; PubAck =:= ?PUBREC; PubAck =:= ?PUBREL; PubAck =:= ?PUBCOMP ->
{<<PacketId:16/big>>, <<>>};
serialize_variable(?PINGREQ, undefined, undefined) ->
{<<>>, <<>>};
serialize_variable(?PINGRESP, undefined, undefined) ->
{<<>>, <<>>};
serialize_variable(?DISCONNECT, undefined, undefined) ->
{<<>>, <<>>}.
serialize_payload(undefined) ->
undefined;
serialize_payload(Bin) when is_binary(Bin) ->
Bin.
serialize_topics([{_Topic, _Qos}|_] = Topics) ->
<< <<(serialize_utf(Topic))/binary, ?RESERVED:6, Qos:2>> || {Topic, Qos} <- Topics >>;
serialize_topics([H|_] = Topics) when is_binary(H) ->
<< <<(serialize_utf(Topic))/binary>> || Topic <- Topics >>.
serialize_utf(String) ->
StringBin = unicode:characters_to_binary(String),
Len = byte_size(StringBin),
true = (Len =< 16#ffff),
<<Len:16/big, StringBin/binary>>.
serialize_len(N) when N =< ?LOWBITS ->
<<0:1, N:7>>;
serialize_len(N) ->
<<1:1, (N rem ?HIGHBIT):7, (serialize_len(N div ?HIGHBIT))/binary>>.
opt(undefined) -> ?RESERVED;
opt(false) -> 0;
opt(true) -> 1;
opt(X) when is_integer(X) -> X;
opt(B) when is_binary(B) -> 1.

View File

@ -1,328 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_server).
-behaviour(gen_server2).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-include("emqttd_internal.hrl").
-export([start_link/3]).
%% PubSub API.
-export([subscribe/1, subscribe/2, subscribe/3, publish/1,
unsubscribe/1, unsubscribe/2]).
%% Async PubSub API.
-export([async_subscribe/1, async_subscribe/2, async_subscribe/3,
async_unsubscribe/1, async_unsubscribe/2]).
%% Management API.
-export([setqos/3, subscriptions/1, subscribers/1, subscribed/2]).
%% Debug API
-export([dump/0]).
%% gen_server Callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {pool, id, env, subids :: map(), submon :: emqttd_pmon:pmon()}).
%% @doc Start the server
-spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, term()}).
start_link(Pool, Id, Env) ->
gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []).
%%--------------------------------------------------------------------
%% PubSub API
%%--------------------------------------------------------------------
%% @doc Subscribe to a Topic.
-spec(subscribe(binary()) -> ok | {error, term()}).
subscribe(Topic) when is_binary(Topic) ->
subscribe(Topic, self()).
-spec(subscribe(binary(), emqttd:subscriber()) -> ok | {error, term()}).
subscribe(Topic, Subscriber) when is_binary(Topic) ->
subscribe(Topic, Subscriber, []).
-spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) ->
ok | {error, term()}).
subscribe(Topic, Subscriber, Options) when is_binary(Topic) ->
call(pick(Subscriber), {subscribe, Topic, with_subpid(Subscriber), Options}).
%% @doc Subscribe to a Topic asynchronously.
-spec(async_subscribe(binary()) -> ok).
async_subscribe(Topic) when is_binary(Topic) ->
async_subscribe(Topic, self()).
-spec(async_subscribe(binary(), emqttd:subscriber()) -> ok).
async_subscribe(Topic, Subscriber) when is_binary(Topic) ->
async_subscribe(Topic, Subscriber, []).
-spec(async_subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok).
async_subscribe(Topic, Subscriber, Options) when is_binary(Topic) ->
cast(pick(Subscriber), {subscribe, Topic, with_subpid(Subscriber), Options}).
%% @doc Publish message to Topic.
-spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore).
publish(Msg = #mqtt_message{from = From}) ->
trace(publish, From, Msg),
case emqttd_hooks:run('message.publish', [], Msg) of
{ok, Msg1 = #mqtt_message{topic = Topic}} ->
emqttd_pubsub:publish(Topic, Msg1);
{stop, Msg1} ->
lager:info("Stop publishing: ~s", [emqttd_message:format(Msg1)]),
ignore
end.
%% @private
trace(publish, From, _Msg) when is_atom(From) ->
%% Dont' trace '$SYS' publish
ignore;
trace(publish, {ClientId, Username}, #mqtt_message{topic = Topic, payload = Payload}) ->
lager:debug([{client, ClientId}, {topic, Topic}],
"~s/~s PUBLISH to ~s: ~p", [ClientId, Username, Topic, Payload]);
trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) ->
lager:debug([{client, From}, {topic, Topic}],
"~s PUBLISH to ~s: ~p", [From, Topic, Payload]).
%% @doc Unsubscribe
-spec(unsubscribe(binary()) -> ok | {error, term()}).
unsubscribe(Topic) when is_binary(Topic) ->
unsubscribe(Topic, self()).
%% @doc Unsubscribe
-spec(unsubscribe(binary(), emqttd:subscriber()) -> ok | {error, term()}).
unsubscribe(Topic, Subscriber) when is_binary(Topic) ->
call(pick(Subscriber), {unsubscribe, Topic, with_subpid(Subscriber)}).
%% @doc Async Unsubscribe
-spec(async_unsubscribe(binary()) -> ok).
async_unsubscribe(Topic) when is_binary(Topic) ->
async_unsubscribe(Topic, self()).
-spec(async_unsubscribe(binary(), emqttd:subscriber()) -> ok).
async_unsubscribe(Topic, Subscriber) when is_binary(Topic) ->
cast(pick(Subscriber), {unsubscribe, Topic, with_subpid(Subscriber)}).
-spec(setqos(binary(), emqttd:subscriber(), mqtt_qos()) -> ok).
setqos(Topic, Subscriber, Qos) when is_binary(Topic) ->
call(pick(Subscriber), {setqos, Topic, with_subpid(Subscriber), Qos}).
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}.
-spec(subscriptions(emqttd:subscriber()) -> [{emqttd:subscriber(), binary(), list(emqttd:suboption())}]).
subscriptions(SubPid) when is_pid(SubPid) ->
with_subproperty(ets:lookup(mqtt_subscription, SubPid));
subscriptions(SubId) when is_binary(SubId) ->
with_subproperty(ets:match_object(mqtt_subscription, {{SubId, '_'}, '_'}));
subscriptions({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) ->
with_subproperty(ets:lookup(mqtt_subscription, {SubId, SubPid})).
with_subproperty({Subscriber, {share, _Share, Topic}}) ->
with_subproperty({Subscriber, Topic});
with_subproperty({Subscriber, Topic}) ->
{Subscriber, Topic, ets:lookup_element(mqtt_subproperty, {Topic, Subscriber}, 2)};
with_subproperty(Subscriptions) when is_list(Subscriptions) ->
[with_subproperty(Subscription) || Subscription <- Subscriptions].
-spec(subscribers(binary()) -> list(emqttd:subscriber())).
subscribers(Topic) when is_binary(Topic) ->
emqttd_pubsub:subscribers(Topic).
-spec(subscribed(binary(), emqttd:subscriber()) -> boolean()).
subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) ->
ets:member(mqtt_subproperty, {Topic, SubPid});
subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) ->
length(ets:match_object(mqtt_subproperty, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1;
subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) ->
ets:member(mqtt_subproperty, {Topic, {SubId, SubPid}}).
call(Server, Req) ->
gen_server2:call(Server, Req, infinity).
cast(Server, Msg) when is_pid(Server) ->
gen_server2:cast(Server, Msg).
pick(SubPid) when is_pid(SubPid) ->
gproc_pool:pick_worker(server, SubPid);
pick(SubId) when is_binary(SubId) ->
gproc_pool:pick_worker(server, SubId);
pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) ->
pick(SubId).
dump() ->
[{Tab, ets:tab2list(Tab)} || Tab <- [mqtt_subproperty, mqtt_subscription, mqtt_subscriber]].
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([Pool, Id, Env]) ->
?GPROC_POOL(join, Pool, Id),
State = #state{pool = Pool, id = Id, env = Env,
subids = #{}, submon = emqttd_pmon:new()},
{ok, State, hibernate, {backoff, 2000, 2000, 20000}}.
handle_call({subscribe, Topic, Subscriber, Options}, _From, State) ->
case do_subscribe(Topic, Subscriber, Options, State) of
{ok, NewState} -> reply(ok, setstats(NewState));
{error, Error} -> reply({error, Error}, State)
end;
handle_call({unsubscribe, Topic, Subscriber}, _From, State) ->
case do_unsubscribe(Topic, Subscriber, State) of
{ok, NewState} -> reply(ok, setstats(NewState));
{error, Error} -> reply({error, Error}, State)
end;
handle_call({setqos, Topic, Subscriber, Qos}, _From, State) ->
Key = {Topic, Subscriber},
case ets:lookup(mqtt_subproperty, Key) of
[{_, Opts}] ->
Opts1 = lists:ukeymerge(1, [{qos, Qos}], Opts),
ets:insert(mqtt_subproperty, {Key, Opts1}),
reply(ok, State);
[] ->
reply({error, {subscription_not_found, Topic}}, State)
end;
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast({subscribe, Topic, Subscriber, Options}, State) ->
case do_subscribe(Topic, Subscriber, Options, State) of
{ok, NewState} -> noreply(setstats(NewState));
{error, _Error} -> noreply(State)
end;
handle_cast({unsubscribe, Topic, Subscriber}, State) ->
case do_unsubscribe(Topic, Subscriber, State) of
{ok, NewState} -> noreply(setstats(NewState));
{error, _Error} -> noreply(State)
end;
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{subids = SubIds}) ->
case maps:find(DownPid, SubIds) of
{ok, SubId} ->
clean_subscriber({SubId, DownPid});
error ->
clean_subscriber(DownPid)
end,
noreply(setstats(demonitor_subscriber(DownPid, State)));
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(_Reason, #state{pool = Pool, id = Id}) ->
?GPROC_POOL(leave, Pool, Id).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal Functions
%%--------------------------------------------------------------------
do_subscribe(Topic, Subscriber, Options, State) ->
case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of
[] ->
emqttd_pubsub:async_subscribe(Topic, Subscriber, Options),
Share = proplists:get_value(share, Options),
add_subscription(Share, Subscriber, Topic),
ets:insert(mqtt_subproperty, {{Topic, Subscriber}, Options}),
{ok, monitor_subscriber(Subscriber, State)};
[_] ->
{error, {already_subscribed, Topic}}
end.
add_subscription(undefined, Subscriber, Topic) ->
ets:insert(mqtt_subscription, {Subscriber, Topic});
add_subscription(Share, Subscriber, Topic) ->
ets:insert(mqtt_subscription, {Subscriber, {share, Share, Topic}}).
monitor_subscriber(SubPid, State = #state{submon = SubMon}) when is_pid(SubPid) ->
State#state{submon = SubMon:monitor(SubPid)};
monitor_subscriber({SubId, SubPid}, State = #state{subids = SubIds, submon = SubMon}) ->
State#state{subids = maps:put(SubPid, SubId, SubIds), submon = SubMon:monitor(SubPid)}.
do_unsubscribe(Topic, Subscriber, State) ->
case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of
[{_, Options}] ->
emqttd_pubsub:async_unsubscribe(Topic, Subscriber, Options),
Share = proplists:get_value(share, Options),
del_subscription(Share, Subscriber, Topic),
ets:delete(mqtt_subproperty, {Topic, Subscriber}),
{ok, State};
[] ->
{error, {subscription_not_found, Topic}}
end.
del_subscription(undefined, Subscriber, Topic) ->
ets:delete_object(mqtt_subscription, {Subscriber, Topic});
del_subscription(Share, Subscriber, Topic) ->
ets:delete_object(mqtt_subscription, {Subscriber, {share, Share, Topic}}).
clean_subscriber(Subscriber) ->
lists:foreach(fun({_, {share, Share, Topic}}) ->
clean_subscriber(Share, Subscriber, Topic);
({_, Topic}) ->
clean_subscriber(undefined, Subscriber, Topic)
end, ets:lookup(mqtt_subscription, Subscriber)),
ets:delete(mqtt_subscription, Subscriber).
clean_subscriber(Share, Subscriber, Topic) ->
case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of
[] ->
%% TODO:....???
Options = if Share == undefined -> []; true -> [{share, Share}] end,
emqttd_pubsub:async_unsubscribe(Topic, Subscriber, Options);
[{_, Options}] ->
emqttd_pubsub:async_unsubscribe(Topic, Subscriber, Options),
ets:delete(mqtt_subproperty, {Topic, Subscriber})
end.
demonitor_subscriber(SubPid, State = #state{subids = SubIds, submon = SubMon}) ->
State#state{subids = maps:remove(SubPid, SubIds), submon = SubMon:demonitor(SubPid)}.
setstats(State) ->
emqttd_stats:setstats('subscriptions/count', 'subscriptions/max',
ets:info(mqtt_subscription, size)), State.
reply(Reply, State) ->
{reply, Reply, State, hibernate}.
noreply(State) ->
{noreply, State, hibernate}.

View File

@ -1,856 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%%
%% @doc MQTT Session
%%
%% A stateful interaction between a Client and a Server. Some Sessions
%% last only as long as the Network Connection, others can span multiple
%% consecutive Network Connections between a Client and a Server.
%%
%% The Session state in the Server consists of:
%%
%% The existence of a Session, even if the rest of the Session state is empty.
%%
%% The Clients subscriptions.
%%
%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not
%% been completely acknowledged.
%%
%% QoS 1 and QoS 2 messages pending transmission to the Client.
%%
%% QoS 2 messages which have been received from the Client, but have not
%% been completely acknowledged.
%%
%% Optionally, QoS 0 messages pending transmission to the Client.
%%
%% If the session is currently disconnected, the time at which the Session state
%% will be deleted.
%%
%% @end
%%
-module(emqttd_session).
-behaviour(gen_server2).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-include("emqttd_internal.hrl").
-import(emqttd_misc, [start_timer/2]).
-import(proplists, [get_value/2, get_value/3]).
%% Session API
-export([start_link/3, resume/3, destroy/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]).
%% gen_server2 Message Priorities
-export([prioritise_call/4, prioritise_cast/3, prioritise_info/3,
handle_pre_hibernate/1]).
-define(MQueue, emqttd_mqueue).
-record(state,
{
%% Clean Session Flag
clean_sess = false :: boolean(),
%% Client Binding: local | remote
binding = local :: local | remote,
%% ClientId: Identifier of Session
client_id :: binary(),
%% Username
username :: binary() | undefined,
%% Client Pid binding with session
client_pid :: pid(),
%% Old Client Pid that has been kickout
old_client_pid :: pid(),
%% Next message id of the session
next_msg_id = 1 :: mqtt_packet_id(),
max_subscriptions :: non_neg_integer(),
%% Clients subscriptions.
subscriptions :: map(),
%% Upgrade Qos?
upgrade_qos = false :: boolean(),
%% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked.
inflight :: emqttd_inflight:inflight(),
%% Max Inflight Size
max_inflight = 32 :: non_neg_integer(),
%% Retry interval for redelivering QoS1/2 messages
retry_interval = 20000 :: timeout(),
%% Retry Timer
retry_timer :: reference() | undefined,
%% All QoS1, QoS2 messages published to when client is disconnected.
%% QoS 1 and QoS 2 messages pending transmission to the Client.
%%
%% Optionally, QoS 0 messages pending transmission to the Client.
mqueue :: ?MQueue:mqueue(),
%% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel.
awaiting_rel :: map(),
%% Max Packets that Awaiting PUBREL
max_awaiting_rel = 100 :: non_neg_integer(),
%% Awaiting PUBREL timeout
await_rel_timeout = 20000 :: timeout(),
%% Awaiting PUBREL timer
await_rel_timer :: reference() | undefined,
%% Session Expiry Interval
expiry_interval = 7200000 :: timeout(),
%% Expired Timer
expiry_timer :: reference() | undefined,
%% Enable Stats
enable_stats :: boolean(),
%% Force GC Count
force_gc_count :: undefined | integer(),
%% Ignore loop deliver?
ignore_loop_deliver = false :: boolean(),
created_at :: erlang:timestamp()
}).
-define(TIMEOUT, 60000).
-define(INFO_KEYS, [clean_sess, client_id, username, client_pid, binding, created_at]).
-define(STATE_KEYS, [clean_sess, 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,
await_rel_timeout, expiry_interval, enable_stats, force_gc_count,
created_at]).
-define(LOG(Level, Format, Args, State),
lager:Level([{client, State#state.client_id}],
"Session(~s): " ++ Format, [State#state.client_id | Args])).
%% @doc Start a Session
-spec(start_link(boolean(), {mqtt_client_id(), mqtt_username()}, pid()) -> {ok, pid()} | {error, term()}).
start_link(CleanSess, {ClientId, Username}, ClientPid) ->
gen_server2:start_link(?MODULE, [CleanSess, {ClientId, Username}, ClientPid], []).
%%--------------------------------------------------------------------
%% PubSub API
%%--------------------------------------------------------------------
%% @doc Subscribe topics
-spec(subscribe(pid(), [{binary(), [emqttd_topic:option()]}]) -> ok).
subscribe(Session, TopicTable) -> %%TODO: the ack function??...
gen_server2:cast(Session, {subscribe, self(), TopicTable, fun(_) -> ok end}).
-spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqttd_topic:option()]}]) -> ok).
subscribe(Session, PacketId, TopicTable) -> %%TODO: the ack function??...
From = self(),
AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end,
gen_server2:cast(Session, {subscribe, From, TopicTable, AckFun}).
%% @doc Publish Message
-spec(publish(pid(), mqtt_message()) -> ok | {error, term()}).
publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) ->
%% Publish QoS0 Directly
emqttd_server:publish(Msg), ok;
publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) ->
%% Publish QoS1 message directly for client will PubAck automatically
emqttd_server:publish(Msg), ok;
publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) ->
%% Publish QoS2 to Session
gen_server2:call(Session, {publish, Msg}, ?TIMEOUT).
%% @doc PubAck Message
-spec(puback(pid(), mqtt_packet_id()) -> ok).
puback(Session, PacketId) ->
gen_server2:cast(Session, {puback, PacketId}).
-spec(pubrec(pid(), mqtt_packet_id()) -> ok).
pubrec(Session, PacketId) ->
gen_server2:cast(Session, {pubrec, PacketId}).
-spec(pubrel(pid(), mqtt_packet_id()) -> ok).
pubrel(Session, PacketId) ->
gen_server2:cast(Session, {pubrel, PacketId}).
-spec(pubcomp(pid(), mqtt_packet_id()) -> ok).
pubcomp(Session, PacketId) ->
gen_server2:cast(Session, {pubcomp, PacketId}).
%% @doc Unsubscribe the topics
-spec(unsubscribe(pid(), [{binary(), [emqttd_topic:option()]}]) -> ok).
unsubscribe(Session, TopicTable) ->
gen_server2:cast(Session, {unsubscribe, self(), TopicTable}).
%% @doc Resume the session
-spec(resume(pid(), mqtt_client_id(), pid()) -> ok).
resume(Session, ClientId, ClientPid) ->
gen_server2:cast(Session, {resume, ClientId, ClientPid}).
%% @doc Get session state
state(Session) when is_pid(Session) ->
gen_server2:call(Session, state).
%% @doc Get session info
-spec(info(pid() | #state{}) -> list(tuple())).
info(Session) when is_pid(Session) ->
gen_server2:call(Session, 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(Session) when is_pid(Session) ->
gen_server2:call(Session, stats);
stats(#state{max_subscriptions = MaxSubscriptions,
subscriptions = Subscriptions,
inflight = Inflight,
max_inflight = MaxInflight,
mqueue = MQueue,
max_awaiting_rel = MaxAwaitingRel,
awaiting_rel = AwaitingRel}) ->
lists:append(emqttd_misc:proc_stats(),
[{max_subscriptions, MaxSubscriptions},
{subscriptions, maps:size(Subscriptions)},
{max_inflight, MaxInflight},
{inflight_len, Inflight:size()},
{max_mqueue, ?MQueue:max_len(MQueue)},
{mqueue_len, ?MQueue:len(MQueue)},
{mqueue_dropped, ?MQueue:dropped(MQueue)},
{max_awaiting_rel, MaxAwaitingRel},
{awaiting_rel_len, maps:size(AwaitingRel)},
{deliver_msg, get(deliver_msg)},
{enqueue_msg, get(enqueue_msg)}]).
%% @doc Destroy the session
-spec(destroy(pid(), mqtt_client_id()) -> ok).
destroy(Session, ClientId) ->
gen_server2:cast(Session, {destroy, ClientId}).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([CleanSess, {ClientId, Username}, ClientPid]) ->
process_flag(trap_exit, true),
true = link(ClientPid),
init_stats([deliver_msg, enqueue_msg]),
{ok, Env} = emqttd:env(session),
{ok, QEnv} = emqttd:env(mqueue),
MaxInflight = get_value(max_inflight, Env, 0),
EnableStats = get_value(enable_stats, Env, false),
ForceGcCount = emqttd_gc:conn_max_gc_count(),
IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false),
MQueue = ?MQueue:new(ClientId, QEnv, emqttd_alarm:alarm_fun()),
State = #state{clean_sess = CleanSess,
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_inflight = MaxInflight,
inflight = emqttd_inflight:new(MaxInflight),
mqueue = MQueue,
retry_interval = 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),
enable_stats = EnableStats,
force_gc_count = ForceGcCount,
created_at = os:timestamp(),
ignore_loop_deliver = IgnoreLoopDeliver},
emqttd_sm:register_session(ClientId, CleanSess, info(State)),
emqttd_hooks:run('session.created', [ClientId, Username]),
{ok, emit_stats(State), hibernate, {backoff, 1000, 1000, 10000}}.
init_stats(Keys) ->
lists:foreach(fun(K) -> put(K, 0) end, Keys).
binding(ClientPid) ->
case node(ClientPid) =:= node() of true -> local; false -> remote end.
prioritise_call(Msg, _From, _Len, _State) ->
case Msg of info -> 10; stats -> 10; state -> 10; _ -> 5 end.
prioritise_cast(Msg, _Len, _State) ->
case Msg of
{destroy, _} -> 10;
{resume, _, _} -> 9;
{pubrel, _} -> 8;
{pubcomp, _} -> 8;
{pubrec, _} -> 8;
{puback, _} -> 7;
{unsubscribe, _, _} -> 6;
{subscribe, _, _} -> 5;
_ -> 0
end.
prioritise_info(Msg, _Len, _State) ->
case Msg of
{'EXIT', _, _} -> 10;
{timeout, _, _} -> 5;
{dispatch, _, _} -> 1;
_ -> 0
end.
handle_pre_hibernate(State) ->
{hibernate, emqttd_gc:reset_conn_gc_count(#state.force_gc_count, emit_stats(State))}.
handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, pktid = PacketId}}, _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)};
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),
emqttd_metrics:inc('messages/qos2/dropped'),
reply({error, dropped}, State)
end;
handle_call(info, _From, State) ->
reply(info(State), 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(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast({subscribe, _From, TopicTable, AckFun},
State = #state{client_id = ClientId,
username = Username,
subscriptions = Subscriptions}) ->
?LOG(debug, "Subscribe ~p", [TopicTable], State),
{GrantedQos, Subscriptions1} =
lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) ->
NewQos = proplists:get_value(qos, Opts),
SubMap1 =
case maps:find(Topic, SubMap) of
{ok, NewQos} ->
emqttd_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}),
?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State),
SubMap;
{ok, OldQos} ->
emqttd:setqos(Topic, ClientId, NewQos),
emqttd_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 ->
emqttd:subscribe(Topic, ClientId, Opts),
emqttd_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}),
maps:put(Topic, NewQos, SubMap)
end,
{[NewQos|QosAcc], SubMap1}
end, {[], Subscriptions}, TopicTable),
AckFun(lists:reverse(GrantedQos)),
hibernate(emit_stats(State#state{subscriptions = Subscriptions1}));
handle_cast({unsubscribe, _From, TopicTable},
State = #state{client_id = ClientId,
username = Username,
subscriptions = Subscriptions}) ->
?LOG(debug, "Unsubscribe ~p", [TopicTable], State),
Subscriptions1 =
lists:foldl(fun({Topic, Opts}, SubMap) ->
case maps:find(Topic, SubMap) of
{ok, _Qos} ->
emqttd:unsubscribe(Topic, ClientId),
emqttd_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, Opts}),
maps:remove(Topic, SubMap);
error ->
SubMap
end
end, Subscriptions, TopicTable),
hibernate(emit_stats(State#state{subscriptions = Subscriptions1}));
%% PUBACK:
handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) ->
{noreply,
case Inflight:contain(PacketId) of
true ->
dequeue(acked(puback, PacketId, State));
false ->
?LOG(warning, "PUBACK ~p missed inflight: ~p",
[PacketId, Inflight:window()], State),
emqttd_metrics:inc('packets/puback/missed'),
State
end, hibernate};
%% PUBREC:
handle_cast({pubrec, PacketId}, State = #state{inflight = Inflight}) ->
{noreply,
case Inflight:contain(PacketId) of
true ->
acked(pubrec, PacketId, State);
false ->
?LOG(warning, "PUBREC ~p missed inflight: ~p",
[PacketId, Inflight:window()], State),
emqttd_metrics:inc('packets/pubrec/missed'),
State
end, hibernate};
%% PUBREL:
handle_cast({pubrel, PacketId}, State = #state{awaiting_rel = AwaitingRel}) ->
{noreply,
case maps:take(PacketId, AwaitingRel) of
{Msg, AwaitingRel1} ->
%% Implement Qos2 by method A [MQTT 4.33]
%% Dispatch to subscriber when received PUBREL
spawn(emqttd_server, publish, [Msg]), %%:)
gc(State#state{awaiting_rel = AwaitingRel1});
error ->
?LOG(warning, "Cannot find PUBREL: ~p", [PacketId], State),
emqttd_metrics:inc('packets/pubrel/missed'),
State
end, hibernate};
%% PUBCOMP:
handle_cast({pubcomp, PacketId}, State = #state{inflight = Inflight}) ->
{noreply,
case Inflight:contain(PacketId) of
true ->
dequeue(acked(pubcomp, PacketId, State));
false ->
?LOG(warning, "The PUBCOMP ~p is not inflight: ~p",
[PacketId, Inflight:window()], State),
emqttd_metrics:inc('packets/pubcomp/missed'),
State
end, hibernate};
%% RESUME:
handle_cast({resume, ClientId, ClientPid},
State = #state{client_id = ClientId,
client_pid = OldClientPid,
clean_sess = CleanSess,
retry_timer = RetryTimer,
await_rel_timer = AwaitTimer,
expiry_timer = ExpireTimer}) ->
?LOG(debug, "Resumed by ~p", [ClientPid], State),
%% Cancel Timers
lists:foreach(fun emqttd_misc:cancel_timer/1,
[RetryTimer, AwaitTimer, ExpireTimer]),
case kick(ClientId, OldClientPid, ClientPid) of
ok -> ?LOG(warning, "~p kickout ~p", [ClientPid, OldClientPid], State);
ignore -> ok
end,
true = link(ClientPid),
State1 = State#state{client_pid = ClientPid,
binding = binding(ClientPid),
old_client_pid = OldClientPid,
clean_sess = false,
retry_timer = undefined,
awaiting_rel = #{},
await_rel_timer = undefined,
expiry_timer = undefined},
%% Clean Session: true -> false?
if
CleanSess =:= true ->
?LOG(info, "CleanSess changed to false.", [], State1),
emqttd_sm:register_session(ClientId, false, info(State1));
CleanSess =:= false ->
ok
end,
%% Replay delivery and Dequeue pending messages
hibernate(emit_stats(dequeue(retry_delivery(true, State1))));
handle_cast({destroy, ClientId},
State = #state{client_id = ClientId, client_pid = undefined}) ->
?LOG(warning, "Destroyed", [], State),
shutdown(destroy, State);
handle_cast({destroy, ClientId},
State = #state{client_id = ClientId, client_pid = OldClientPid}) ->
?LOG(warning, "kickout ~p", [OldClientPid], State),
shutdown(conflict, State);
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
%% Ignore Messages delivered by self
handle_info({dispatch, _Topic, #mqtt_message{from = {ClientId, _}}},
State = #state{client_id = ClientId, ignore_loop_deliver = true}) ->
hibernate(State);
%% Dispatch Message
handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, mqtt_message) ->
hibernate(gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State)));
%% Do nothing if the client has been disconnected.
handle_info({timeout, _Timer, retry_delivery}, State = #state{client_pid = undefined}) ->
hibernate(emit_stats(State#state{retry_timer = undefined}));
handle_info({timeout, _Timer, retry_delivery}, State) ->
hibernate(emit_stats(retry_delivery(false, State#state{retry_timer = undefined})));
handle_info({timeout, _Timer, check_awaiting_rel}, State) ->
hibernate(expire_awaiting_rel(emit_stats(State#state{await_rel_timer = undefined})));
handle_info({timeout, _Timer, expired}, State) ->
?LOG(info, "Expired, shutdown now.", [], State),
shutdown(expired, State);
handle_info({'EXIT', ClientPid, _Reason},
State = #state{clean_sess = true, client_pid = ClientPid}) ->
{stop, normal, State};
handle_info({'EXIT', ClientPid, Reason},
State = #state{clean_sess = false,
client_pid = ClientPid,
expiry_interval = Interval}) ->
?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State),
ExpireTimer = start_timer(Interval, expired),
State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer},
hibernate(emit_stats(State1));
handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) ->
%%ignore
hibernate(State);
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),
hibernate(State);
handle_info(Info, Session) ->
?UNEXPECTED_INFO(Info, Session).
terminate(Reason, #state{client_id = ClientId, username = Username}) ->
%% Move to emqttd_sm to avoid race condition
%%emqttd_stats:del_session_stats(ClientId),
emqttd_hooks:run('session.terminated', [ClientId, Username, Reason]),
emqttd_sm:unregister_session(ClientId).
code_change(_OldVsn, Session, _Extra) ->
{ok, Session}.
%%--------------------------------------------------------------------
%% Kickout old client
%%--------------------------------------------------------------------
kick(_ClientId, undefined, _Pid) ->
ignore;
kick(_ClientId, Pid, Pid) ->
ignore;
kick(ClientId, OldPid, Pid) ->
unlink(OldPid),
OldPid ! {shutdown, conflict, {ClientId, Pid}},
%% Clean noproc
receive {'EXIT', OldPid, _} -> ok after 0 -> ok end.
%%--------------------------------------------------------------------
%% Replay or Retry Delivery
%%--------------------------------------------------------------------
%% Redeliver at once if Force is true
retry_delivery(Force, State = #state{inflight = Inflight}) ->
case Inflight:is_empty() of
true -> State;
false -> Msgs = lists:sort(sortfun(inflight), Inflight:values()),
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)};
retry_delivery(Force, [{Type, Msg, 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 = #mqtt_message{pktid = PacketId}} ->
redeliver(Msg, State),
Inflight1 = Inflight:update(PacketId, {publish, Msg, Now}),
retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1});
{pubrel, PacketId} ->
redeliver({pubrel, PacketId}, State),
Inflight1 = Inflight:update(PacketId, {pubrel, PacketId, Now}),
retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1})
end;
true ->
State#state{retry_timer = start_timer(Interval - Diff, retry_delivery)}
end.
%%--------------------------------------------------------------------
%% Expire Awaiting Rel
%%--------------------------------------------------------------------
expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) ->
case maps:size(AwaitingRel) of
0 -> State;
_ -> Msgs = lists:sort(sortfun(awaiting_rel), maps:to_list(AwaitingRel)),
expire_awaiting_rel(Msgs, os:timestamp(), State)
end.
expire_awaiting_rel([], _Now, State) ->
State#state{await_rel_timer = undefined};
expire_awaiting_rel([{PacketId, Msg = #mqtt_message{timestamp = TS}} | Msgs],
Now, State = #state{awaiting_rel = AwaitingRel,
await_rel_timeout = Timeout}) ->
case (timer:now_diff(Now, TS) div 1000) of
Diff when Diff >= Timeout ->
?LOG(warning, "Dropped Qos2 Message for await_rel_timeout: ~p", [Msg], State),
emqttd_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)}
end.
%%--------------------------------------------------------------------
%% Sort Inflight, AwaitingRel
%%--------------------------------------------------------------------
sortfun(inflight) ->
fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end;
sortfun(awaiting_rel) ->
fun({_, #mqtt_message{timestamp = Ts1}},
{_, #mqtt_message{timestamp = Ts2}}) ->
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_pid = undefined}) ->
enqueue_msg(Msg, State);
%% Deliver qos0 message directly to client
dispatch(Msg = #mqtt_message{qos = ?QOS0}, State) ->
deliver(Msg, State), State;
dispatch(Msg = #mqtt_message{qos = QoS},
State = #state{next_msg_id = MsgId, inflight = Inflight})
when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 ->
case Inflight:is_full() of
true ->
enqueue_msg(Msg, State);
false ->
Msg1 = Msg#mqtt_message{pktid = MsgId},
deliver(Msg1, State),
await(Msg1, next_msg_id(State))
end.
enqueue_msg(Msg, State = #state{mqueue = Q}) ->
inc_stats(enqueue_msg),
State#state{mqueue = ?MQueue:in(Msg, Q)}.
%%--------------------------------------------------------------------
%% Deliver
%%--------------------------------------------------------------------
redeliver(Msg = #mqtt_message{qos = QoS}, State) ->
deliver(Msg#mqtt_message{dup = if QoS =:= ?QOS0 -> false; true -> true end}, State);
redeliver({pubrel, PacketId}, #state{client_pid = Pid}) ->
Pid ! {redeliver, {?PUBREL, PacketId}}.
deliver(Msg, #state{client_pid = Pid}) ->
inc_stats(deliver_msg),
Pid ! {deliver, Msg}.
%%--------------------------------------------------------------------
%% Awaiting ACK for QoS1/QoS2 Messages
%%--------------------------------------------------------------------
await(Msg = #mqtt_message{pktid = PacketId},
State = #state{inflight = Inflight,
retry_timer = RetryTimer,
retry_interval = Interval}) ->
%% Start retry timer if the Inflight is still empty
State1 = ?IF(RetryTimer == undefined, State#state{retry_timer = start_timer(Interval, retry_delivery)}, State),
State1#state{inflight = Inflight:insert(PacketId, {publish, Msg, os:timestamp()})}.
acked(puback, PacketId, State = #state{client_id = ClientId,
username = Username,
inflight = Inflight}) ->
case Inflight:lookup(PacketId) of
{publish, Msg, _Ts} ->
emqttd_hooks:run('message.acked', [ClientId, Username], Msg),
State#state{inflight = Inflight:delete(PacketId)};
_ ->
?LOG(warning, "Duplicated PUBACK Packet: ~p", [PacketId], State),
State
end;
acked(pubrec, PacketId, State = #state{client_id = ClientId,
username = Username,
inflight = Inflight}) ->
case Inflight:lookup(PacketId) of
{publish, Msg, _Ts} ->
emqttd_hooks:run('message.acked', [ClientId, Username], Msg),
State#state{inflight = Inflight:update(PacketId, {pubrel, PacketId, os:timestamp()})};
{pubrel, PacketId, _Ts} ->
?LOG(warning, "Duplicated PUBREC Packet: ~p", [PacketId], State),
State
end;
acked(pubcomp, PacketId, State = #state{inflight = Inflight}) ->
State#state{inflight = Inflight:delete(PacketId)}.
%%--------------------------------------------------------------------
%% Dequeue
%%--------------------------------------------------------------------
%% Do nothing if client is disconnected
dequeue(State = #state{client_pid = undefined}) ->
State;
dequeue(State = #state{inflight = Inflight}) ->
case Inflight:is_full() of
true -> State;
false -> dequeue2(State)
end.
dequeue2(State = #state{mqueue = Q}) ->
case ?MQueue:out(Q) of
{empty, _Q} ->
State;
{{value, Msg}, Q1} ->
%% Dequeue more
dequeue(dispatch(Msg, State#state{mqueue = Q1}))
end.
%%--------------------------------------------------------------------
%% Tune QoS
%%--------------------------------------------------------------------
tune_qos(Topic, Msg = #mqtt_message{qos = PubQoS},
#state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) ->
case maps:find(Topic, SubMap) of
{ok, SubQoS} when UpgradeQoS andalso (SubQoS > PubQoS) ->
Msg#mqtt_message{qos = SubQoS};
{ok, SubQoS} when (not UpgradeQoS) andalso (SubQoS < PubQoS) ->
Msg#mqtt_message{qos = SubQoS};
{ok, _} ->
Msg;
error ->
Msg
end.
%%--------------------------------------------------------------------
%% Reset Dup
%%--------------------------------------------------------------------
reset_dup(Msg = #mqtt_message{dup = true}) ->
Msg#mqtt_message{dup = false};
reset_dup(Msg) -> Msg.
%%--------------------------------------------------------------------
%% Next Msg Id
%%--------------------------------------------------------------------
next_msg_id(State = #state{next_msg_id = 16#FFFF}) ->
State#state{next_msg_id = 1};
next_msg_id(State = #state{next_msg_id = Id}) ->
State#state{next_msg_id = Id + 1}.
%%--------------------------------------------------------------------
%% Emit session stats
%%--------------------------------------------------------------------
emit_stats(State = #state{enable_stats = false}) ->
State;
emit_stats(State = #state{client_id = ClientId}) ->
emqttd_stats:set_session_stats(ClientId, stats(State)),
State.
inc_stats(Key) -> put(Key, get(Key) + 1).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
reply(Reply, State) ->
{reply, Reply, State, hibernate}.
hibernate(State) ->
{noreply, State, hibernate}.
shutdown(Reason, State) ->
{stop, {shutdown, Reason}, State}.
gc(State) ->
emqttd_gc:maybe_force_gc(#state.force_gc_count, State).

View File

@ -1,45 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Session Supervisor.
-module(emqttd_session_sup).
-author("Feng Lee <feng@emqtt.io>").
-behavior(supervisor).
-export([start_link/0, start_session/3]).
-export([init/1]).
%% @doc Start session supervisor
-spec(start_link() -> {ok, pid()}).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% @doc Start a session
-spec(start_session(boolean(), {binary(), binary() | undefined} , pid()) -> {ok, pid()}).
start_session(CleanSess, {ClientId, Username}, ClientPid) ->
supervisor:start_child(?MODULE, [CleanSess, {ClientId, Username}, ClientPid]).
%%--------------------------------------------------------------------
%% Supervisor callbacks
%%--------------------------------------------------------------------
init([]) ->
{ok, {{simple_one_for_one, 0, 1},
[{session, {emqttd_session, start_link, []},
temporary, 5000, worker, [emqttd_session]}]}}.

View File

@ -1,309 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_sm).
-author("Feng Lee <feng@emqtt.io>").
-behaviour(gen_server2).
-include("emqttd.hrl").
-include("emqttd_internal.hrl").
%% Mnesia Callbacks
-export([mnesia/1]).
-boot_mnesia({mnesia, [boot]}).
-copy_mnesia({mnesia, [copy]}).
%% API Function Exports
-export([start_link/2]).
-export([start_session/2, lookup_session/1, register_session/3,
unregister_session/1, unregister_session/2]).
-export([dispatch/3]).
-export([local_sessions/0]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%% gen_server2 priorities
-export([prioritise_call/4, prioritise_cast/3, prioritise_info/3]).
-record(state, {pool, id, monitors}).
-define(POOL, ?MODULE).
-define(TIMEOUT, 120000).
-define(LOG(Level, Format, Args, Session),
lager:Level("SM(~s): " ++ Format, [Session#mqtt_session.client_id | Args])).
%%--------------------------------------------------------------------
%% Mnesia callbacks
%%--------------------------------------------------------------------
mnesia(boot) ->
%% Global Session Table
ok = ekka_mnesia:create_table(mqtt_session, [
{type, set},
{ram_copies, [node()]},
{record_name, mqtt_session},
{attributes, record_info(fields, mqtt_session)}]);
mnesia(copy) ->
ok = ekka_mnesia:copy_table(mqtt_session).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Start a session manager
-spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}).
start_link(Pool, Id) ->
gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id], []).
%% @doc Start a session
-spec(start_session(boolean(), {binary(), binary() | undefined}) -> {ok, pid(), boolean()} | {error, term()}).
start_session(CleanSess, {ClientId, Username}) ->
SM = gproc_pool:pick_worker(?POOL, ClientId),
call(SM, {start_session, CleanSess, {ClientId, Username}, self()}).
%% @doc Lookup a Session
-spec(lookup_session(binary()) -> mqtt_session() | undefined).
lookup_session(ClientId) ->
case mnesia:dirty_read(mqtt_session, ClientId) of
[Session] -> Session;
[] -> undefined
end.
%% @doc Register a session with info.
-spec(register_session(binary(), boolean(), [tuple()]) -> true).
register_session(ClientId, CleanSess, Properties) ->
ets:insert(mqtt_local_session, {ClientId, self(), CleanSess, Properties}).
%% @doc Unregister a session.
-spec(unregister_session(binary()) -> boolean()).
unregister_session(ClientId) ->
unregister_session(ClientId, self()).
unregister_session(ClientId, Pid) ->
case ets:lookup(mqtt_local_session, ClientId) of
[LocalSess = {_, Pid, _, _}] ->
emqttd_stats:del_session_stats(ClientId),
ets:delete_object(mqtt_local_session, LocalSess);
_ ->
false
end.
dispatch(ClientId, Topic, Msg) ->
try ets:lookup_element(mqtt_local_session, ClientId, 2) of
Pid -> Pid ! {dispatch, Topic, Msg}
catch
error:badarg -> ok %%FIXME Later.
end.
call(SM, Req) ->
gen_server2:call(SM, Req, ?TIMEOUT). %%infinity).
%% @doc for debug.
local_sessions() ->
ets:tab2list(mqtt_local_session).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([Pool, Id]) ->
?GPROC_POOL(join, Pool, Id),
{ok, #state{pool = Pool, id = Id, monitors = dict:new()}}.
prioritise_call(_Msg, _From, _Len, _State) ->
1.
prioritise_cast(_Msg, _Len, _State) ->
0.
prioritise_info(_Msg, _Len, _State) ->
2.
%% Persistent Session
handle_call({start_session, false, {ClientId, Username}, ClientPid}, _From, State) ->
case lookup_session(ClientId) of
undefined ->
%% Create session locally
create_session({false, {ClientId, Username}, ClientPid}, State);
Session ->
case resume_session(Session, ClientPid) of
{ok, SessPid} ->
{reply, {ok, SessPid, true}, State};
{error, Erorr} ->
{reply, {error, Erorr}, State}
end
end;
%% Transient Session
handle_call({start_session, true, {ClientId, Username}, ClientPid}, _From, State) ->
Client = {true, {ClientId, Username}, ClientPid},
case lookup_session(ClientId) of
undefined ->
create_session(Client, State);
Session ->
case destroy_session(Session) of
ok ->
create_session(Client, State);
{error, Error} ->
{reply, {error, Error}, State}
end
end;
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) ->
case dict:find(MRef, State#state.monitors) of
{ok, ClientId} ->
NewState =
case mnesia:dirty_read({mqtt_session, ClientId}) of
[] -> State;
[Sess = #mqtt_session{sess_pid = DownPid}] ->
mnesia:dirty_delete_object(Sess),
erase_monitor(MRef, State);
[_Sess] ->
State
end,
{noreply, NewState, hibernate};
error ->
lager:error("MRef of session ~p not found", [DownPid]),
{noreply, State}
end;
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(_Reason, #state{pool = Pool, id = Id}) ->
?GPROC_POOL(leave, Pool, Id).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
%% Create Session Locally
create_session({CleanSess, {ClientId, Username}, ClientPid}, State) ->
case create_session(CleanSess, {ClientId, Username}, ClientPid) of
{ok, SessPid} ->
{reply, {ok, SessPid, false}, monitor_session(ClientId, SessPid, State)};
{error, Error} ->
{reply, {error, Error}, State}
end.
create_session(CleanSess, {ClientId, Username}, ClientPid) ->
case emqttd_session_sup:start_session(CleanSess, {ClientId, Username}, ClientPid) of
{ok, SessPid} ->
Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid, clean_sess = CleanSess},
case insert_session(Session) of
{aborted, {conflict, ConflictPid}} ->
%% Conflict with othe node?
lager:error("SM(~s): Conflict with ~p", [ClientId, ConflictPid]),
{error, mnesia_conflict};
{atomic, ok} ->
{ok, SessPid}
end;
{error, Error} ->
{error, Error}
end.
insert_session(Session = #mqtt_session{client_id = ClientId}) ->
mnesia:transaction(
fun() ->
case mnesia:wread({mqtt_session, ClientId}) of
[] ->
mnesia:write(mqtt_session, Session, write);
[#mqtt_session{sess_pid = SessPid}] ->
mnesia:abort({conflict, SessPid})
end
end).
%% Local node
resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}, ClientPid)
when node(SessPid) =:= node() ->
case is_process_alive(SessPid) of
true ->
emqttd_session:resume(SessPid, ClientId, ClientPid),
{ok, SessPid};
false ->
?LOG(error, "Cannot resume ~p which seems already dead!", [SessPid], Session),
remove_session(Session),
{error, session_died}
end;
%% Remote node
resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}, ClientPid) ->
Node = node(SessPid),
case rpc:call(Node, emqttd_session, resume, [SessPid, ClientId, ClientPid]) of
ok ->
{ok, SessPid};
{badrpc, nodedown} ->
?LOG(error, "Session died for node '~s' down", [Node], Session),
remove_session(Session),
{error, session_nodedown};
{badrpc, Reason} ->
?LOG(error, "Failed to resume from node ~s for ~p", [Node, Reason], Session),
{error, Reason}
end.
%% Local node
destroy_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid})
when node(SessPid) =:= node() ->
emqttd_session:destroy(SessPid, ClientId),
remove_session(Session);
%% Remote node
destroy_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}) ->
Node = node(SessPid),
case rpc:call(Node, emqttd_session, destroy, [SessPid, ClientId]) of
ok ->
remove_session(Session);
{badrpc, nodedown} ->
?LOG(error, "Node '~s' down", [Node], Session),
remove_session(Session);
{badrpc, Reason} ->
?LOG(error, "Failed to destory ~p on remote node ~p for ~s",
[SessPid, Node, Reason], Session),
{error, Reason}
end.
remove_session(Session) ->
mnesia:dirty_delete_object(Session).
monitor_session(ClientId, SessPid, State = #state{monitors = Monitors}) ->
MRef = erlang:monitor(process, SessPid),
State#state{monitors = dict:store(MRef, ClientId, Monitors)}.
erase_monitor(MRef, State = #state{monitors = Monitors}) ->
erlang:demonitor(MRef, [flush]),
State#state{monitors = dict:erase(MRef, Monitors)}.

View File

@ -1,89 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Session Helper.
-module(emqttd_sm_helper).
-author("Feng Lee <feng@emqtt.io>").
-behaviour(gen_server).
-include("emqttd.hrl").
-include("emqttd_internal.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
%% API Function Exports
-export([start_link/1]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {stats_fun, ticker}).
-define(LOCK, {?MODULE, clean_sessions}).
%% @doc Start a session helper
-spec(start_link(fun()) -> {ok, pid()} | ignore | {error, term()}).
start_link(StatsFun) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [StatsFun], []).
init([StatsFun]) ->
ekka:monitor(membership),
{ok, TRef} = timer:send_interval(timer:seconds(1), tick),
{ok, #state{stats_fun = StatsFun, ticker = TRef}}.
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info({membership, {mnesia, down, Node}}, State) ->
Fun = fun() ->
ClientIds =
mnesia:select(mqtt_session, [{#mqtt_session{client_id = '$1', sess_pid = '$2', _ = '_'},
[{'==', {node, '$2'}, Node}], ['$1']}]),
lists:foreach(fun(ClientId) -> mnesia:delete({mqtt_session, ClientId}) end, ClientIds)
end,
global:trans({?LOCK, self()}, fun() -> mnesia:async_dirty(Fun) end),
{noreply, State, hibernate};
handle_info({membership, _Event}, State) ->
{noreply, State};
handle_info(tick, State) ->
{noreply, setstats(State), hibernate};
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(_Reason, _State = #state{ticker = TRef}) ->
timer:cancel(TRef),
ekka:unmonitor(membership).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
setstats(State = #state{stats_fun = StatsFun}) ->
StatsFun(ets:info(mqtt_local_session, size)), State.

View File

@ -1,52 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Session Manager Supervisor.
-module(emqttd_sm_sup).
-behaviour(supervisor).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-define(HELPER, emqttd_sm_helper).
%% API
-export([start_link/0]).
%% Supervisor callbacks
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
%% Create session tables
ets:new(mqtt_local_session, [public, ordered_set, named_table, {write_concurrency, true}]),
%% Helper
StatsFun = emqttd_stats:statsfun('sessions/count', 'sessions/max'),
Helper = {?HELPER, {?HELPER, start_link, [StatsFun]},
permanent, 5000, worker, [?HELPER]},
%% SM Pool Sup
MFA = {emqttd_sm, start_link, []},
PoolSup = emqttd_pool_sup:spec([emqttd_sm, hash, erlang:system_info(schedulers), MFA]),
{ok, {{one_for_all, 10, 3600}, [Helper, PoolSup]}}.

View File

@ -1,211 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_stats).
-behaviour(gen_server).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-export([start_link/0, stop/0]).
%% Client and Session Stats
-export([set_client_stats/2, get_client_stats/1, del_client_stats/1,
set_session_stats/2, get_session_stats/1, del_session_stats/1]).
%% Statistics API.
-export([statsfun/1, statsfun/2, getstats/0, getstat/1, setstat/2, setstats/3]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {tick}).
-type(stats() :: list({atom(), non_neg_integer()})).
-define(STATS_TAB, mqtt_stats).
-define(CLIENT_STATS_TAB, mqtt_client_stats).
-define(SESSION_STATS_TAB, mqtt_session_stats).
%% $SYS Topics for Clients
-define(SYSTOP_CLIENTS, [
'clients/count', % clients connected current
'clients/max' % max clients connected
]).
%% $SYS Topics for Sessions
-define(SYSTOP_SESSIONS, [
'sessions/count',
'sessions/max'
]).
%% $SYS Topics for Subscribers
-define(SYSTOP_PUBSUB, [
'topics/count', % ...
'topics/max', % ...
'subscribers/count', % ...
'subscribers/max', % ...
'subscriptions/count', % ...
'subscriptions/max', % ...
'routes/count', % ...
'routes/max' % ...
]).
%% $SYS Topic for retained
-define(SYSTOP_RETAINED, [
'retained/count',
'retained/max'
]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Start stats server
-spec(start_link() -> {ok, pid()} | ignore | {error, term()}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
stop() ->
gen_server:call(?MODULE, stop).
-spec(set_client_stats(binary(), stats()) -> true).
set_client_stats(ClientId, Stats) ->
ets:insert(?CLIENT_STATS_TAB, {ClientId, [{'$ts', emqttd_time:now_secs()}|Stats]}).
-spec(get_client_stats(binary()) -> stats()).
get_client_stats(ClientId) ->
case ets:lookup(?CLIENT_STATS_TAB, ClientId) of
[{_, Stats}] -> Stats;
[] -> []
end.
-spec(del_client_stats(binary()) -> true).
del_client_stats(ClientId) ->
ets:delete(?CLIENT_STATS_TAB, ClientId).
-spec(set_session_stats(binary(), stats()) -> true).
set_session_stats(ClientId, Stats) ->
ets:insert(?SESSION_STATS_TAB, {ClientId, [{'$ts', emqttd_time:now_secs()}|Stats]}).
-spec(get_session_stats(binary()) -> stats()).
get_session_stats(ClientId) ->
case ets:lookup(?SESSION_STATS_TAB, ClientId) of
[{_, Stats}] -> Stats;
[] -> []
end.
-spec(del_session_stats(binary()) -> true).
del_session_stats(ClientId) ->
ets:delete(?SESSION_STATS_TAB, ClientId).
%% @doc Generate stats fun
-spec(statsfun(Stat :: atom()) -> fun()).
statsfun(Stat) ->
fun(Val) -> setstat(Stat, Val) end.
-spec(statsfun(Stat :: atom(), MaxStat :: atom()) -> fun()).
statsfun(Stat, MaxStat) ->
fun(Val) -> setstats(Stat, MaxStat, Val) end.
%% @doc Get broker statistics
-spec(getstats() -> [{atom(), non_neg_integer()}]).
getstats() ->
lists:sort(ets:tab2list(?STATS_TAB)).
%% @doc Get stats by name
-spec(getstat(atom()) -> non_neg_integer() | undefined).
getstat(Name) ->
case ets:lookup(?STATS_TAB, Name) of
[{Name, Val}] -> Val;
[] -> undefined
end.
%% @doc Set broker stats
-spec(setstat(Stat :: atom(), Val :: pos_integer()) -> boolean()).
setstat(Stat, Val) ->
ets:update_element(?STATS_TAB, Stat, {2, Val}).
%% @doc Set stats with max
-spec(setstats(Stat :: atom(), MaxStat :: atom(), Val :: pos_integer()) -> ok).
setstats(Stat, MaxStat, Val) ->
gen_server:cast(?MODULE, {setstats, Stat, MaxStat, Val}).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([]) ->
emqttd_time:seed(),
lists:foreach(
fun(Tab) ->
Tab = ets:new(Tab, [set, public, named_table, {write_concurrency, true}])
end, [?STATS_TAB, ?CLIENT_STATS_TAB, ?SESSION_STATS_TAB]),
Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB ++ ?SYSTOP_RETAINED,
ets:insert(?STATS_TAB, [{Topic, 0} || Topic <- Topics]),
% Tick to publish stats
{ok, #state{tick = emqttd_broker:start_tick(tick)}, hibernate}.
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(_Request, _From, State) ->
{reply, error, State}.
%% atomic
handle_cast({setstats, Stat, MaxStat, Val}, State) ->
MaxVal = ets:lookup_element(?STATS_TAB, MaxStat, 2),
if
Val > MaxVal ->
ets:update_element(?STATS_TAB, MaxStat, {2, Val});
true -> ok
end,
ets:update_element(?STATS_TAB, Stat, {2, Val}),
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
%% Interval Tick.
handle_info(tick, State) ->
[publish(Stat, Val) || {Stat, Val} <- ets:tab2list(?STATS_TAB)],
{noreply, State, hibernate};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{tick = TRef}) ->
emqttd_broker:stop_tick(TRef).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
publish(Stat, Val) ->
Msg = emqttd_message:make(stats, stats_topic(Stat), bin(Val)),
emqttd:publish(emqttd_message:set_flag(sys, Msg)).
stats_topic(Stat) ->
emqttd_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))).
bin(I) when is_integer(I) -> list_to_binary(integer_to_list(I)).

View File

@ -1,55 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_sup).
-behaviour(supervisor).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
%% API
-export([start_link/0, start_child/1, start_child/2]).
%% Supervisor callbacks
-export([init/1]).
%% Helper macro for declaring children of supervisor
-define(CHILD(Mod, Type), {Mod, {Mod, start_link, []},
permanent, 5000, Type, [Mod]}).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
start_child(ChildSpec) when is_tuple(ChildSpec) ->
supervisor:start_child(?MODULE, ChildSpec).
-spec(start_child(Mod::atom(), Type :: worker | supervisor) -> {ok, pid()}).
start_child(Mod, Type) when is_atom(Mod) and is_atom(Type) ->
supervisor:start_child(?MODULE, ?CHILD(Mod, Type)).
%%--------------------------------------------------------------------
%% Supervisor callbacks
%%--------------------------------------------------------------------
init([]) ->
{ok, {{one_for_all, 0, 1}, []}}.

View File

@ -1,176 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc VM System Monitor
-module(emqttd_sysmon).
-author("Feng Lee <feng@emqtt.io>").
-behavior(gen_server).
-include("emqttd_internal.hrl").
-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {tickref, events = [], tracelog}).
-define(LOG_FMT, [{formatter_config, [time, " ", message, "\n"]}]).
-define(LOG(Msg, ProcInfo),
lager:warning([{sysmon, true}], "~s~n~p", [WarnMsg, ProcInfo])).
-define(LOG(Msg, ProcInfo, PortInfo),
lager:warning([{sysmon, true}], "~s~n~p~n~p", [WarnMsg, ProcInfo, PortInfo])).
%% @doc Start system monitor
-spec(start_link(Opts :: list(tuple())) ->
{ok, pid()} | ignore | {error, term()}).
start_link(Opts) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Opts]) ->
erlang:system_monitor(self(), parse_opt(Opts)),
{ok, TRef} = timer:send_interval(timer:seconds(1), reset),
%%TODO: don't trace for performance issue.
%%{ok, TraceLog} = start_tracelog(proplists:get_value(logfile, Opts)),
{ok, #state{tickref = TRef}}.
parse_opt(Opts) ->
parse_opt(Opts, []).
parse_opt([], Acc) ->
Acc;
parse_opt([{long_gc, false}|Opts], Acc) ->
parse_opt(Opts, Acc);
parse_opt([{long_gc, Ms}|Opts], Acc) when is_integer(Ms) ->
parse_opt(Opts, [{long_gc, Ms}|Acc]);
parse_opt([{long_schedule, false}|Opts], Acc) ->
parse_opt(Opts, Acc);
parse_opt([{long_schedule, Ms}|Opts], Acc) when is_integer(Ms) ->
parse_opt(Opts, [{long_schedule, Ms}|Acc]);
parse_opt([{large_heap, Size}|Opts], Acc) when is_integer(Size) ->
parse_opt(Opts, [{large_heap, Size}|Acc]);
parse_opt([{busy_port, true}|Opts], Acc) ->
parse_opt(Opts, [busy_port|Acc]);
parse_opt([{busy_port, false}|Opts], Acc) ->
parse_opt(Opts, Acc);
parse_opt([{busy_dist_port, true}|Opts], Acc) ->
parse_opt(Opts, [busy_dist_port|Acc]);
parse_opt([{busy_dist_port, false}|Opts], Acc) ->
parse_opt(Opts, Acc);
parse_opt([_Opt|Opts], Acc) ->
parse_opt(Opts, Acc).
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info({monitor, Pid, long_gc, Info}, State) ->
suppress({long_gc, Pid}, fun() ->
WarnMsg = io_lib:format("long_gc warning: pid = ~p, info: ~p", [Pid, Info]),
?LOG(WarnMsg, procinfo(Pid)),
publish(long_gc, WarnMsg)
end, State);
handle_info({monitor, Pid, long_schedule, Info}, State) when is_pid(Pid) ->
suppress({long_schedule, Pid}, fun() ->
WarnMsg = io_lib:format("long_schedule warning: pid = ~p, info: ~p", [Pid, Info]),
?LOG(WarnMsg, procinfo(Pid)),
publish(long_schedule, WarnMsg)
end, State);
handle_info({monitor, Port, long_schedule, Info}, State) when is_port(Port) ->
suppress({long_schedule, Port}, fun() ->
WarnMsg = io_lib:format("long_schedule warning: port = ~p, info: ~p", [Port, Info]),
?LOG(WarnMsg, erlang:port_info(Port)),
publish(long_schedule, WarnMsg)
end, State);
handle_info({monitor, Pid, large_heap, Info}, State) ->
suppress({large_heap, Pid}, fun() ->
WarnMsg = io_lib:format("large_heap warning: pid = ~p, info: ~p", [Pid, Info]),
?LOG(WarnMsg, procinfo(Pid)),
publish(large_heap, WarnMsg)
end, State);
handle_info({monitor, SusPid, busy_port, Port}, State) ->
suppress({busy_port, Port}, fun() ->
WarnMsg = io_lib:format("busy_port warning: suspid = ~p, port = ~p", [SusPid, Port]),
?LOG(WarnMsg, procinfo(SusPid), erlang:port_info(Port)),
publish(busy_port, WarnMsg)
end, State);
handle_info({monitor, SusPid, busy_dist_port, Port}, State) ->
suppress({busy_dist_port, Port}, fun() ->
WarnMsg = io_lib:format("busy_dist_port warning: suspid = ~p, port = ~p", [SusPid, Port]),
?LOG(WarnMsg, procinfo(SusPid), erlang:port_info(Port)),
publish(busy_dist_port, WarnMsg)
end, State);
handle_info(reset, State) ->
{noreply, State#state{events = []}, hibernate};
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(_Reason, #state{tickref = TRef, tracelog = TraceLog}) ->
timer:cancel(TRef),
cancel_tracelog(TraceLog).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
suppress(Key, SuccFun, State = #state{events = Events}) ->
case lists:member(Key, Events) of
true ->
{noreply, State};
false ->
SuccFun(),
{noreply, State#state{events = [Key|Events]}}
end.
procinfo(Pid) ->
case {emqttd_vm:get_process_info(Pid), emqttd_vm:get_process_gc(Pid)} of
{undefined, _} -> undefined;
{_, undefined} -> undefined;
{Info, GcInfo} -> Info ++ GcInfo
end.
publish(Sysmon, WarnMsg) ->
Msg = emqttd_message:make(sysmon, topic(Sysmon), iolist_to_binary(WarnMsg)),
emqttd:publish(emqttd_message:set_flag(sys, Msg)).
topic(Sysmon) ->
emqttd_topic:systop(list_to_binary(lists:concat(['sysmon/', Sysmon]))).
%% start_tracelog(undefined) ->
%% {ok, undefined};
%% start_tracelog(LogFile) ->
%% lager:trace_file(LogFile, [{sysmon, true}], info, ?LOG_FMT).
cancel_tracelog(undefined) ->
ok;
cancel_tracelog(TraceLog) ->
lager:stop_trace(TraceLog).

View File

@ -1,117 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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.
%%--------------------------------------------------------------------
%% @docTrace MQTT packets/messages by ClientID or Topic.
-module(emqttd_trace).
-behaviour(gen_server).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd_internal.hrl").
%% API Function Exports
-export([start_link/0]).
-export([start_trace/2, stop_trace/1, all_traces/0]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {level, traces}).
-type(trace_who() :: {client | topic, binary()}).
-define(TRACE_OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
-spec(start_link() -> {ok, pid()}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%% @doc Start to trace client or topic.
-spec(start_trace(trace_who(), string()) -> ok | {error, term()}).
start_trace({client, ClientId}, LogFile) ->
start_trace({start_trace, {client, ClientId}, LogFile});
start_trace({topic, Topic}, LogFile) ->
start_trace({start_trace, {topic, Topic}, LogFile}).
start_trace(Req) -> gen_server:call(?MODULE, Req, infinity).
%% @doc Stop tracing client or topic.
-spec(stop_trace(trace_who()) -> ok | {error, term()}).
stop_trace({client, ClientId}) ->
gen_server:call(?MODULE, {stop_trace, {client, ClientId}});
stop_trace({topic, Topic}) ->
gen_server:call(?MODULE, {stop_trace, {topic, Topic}}).
%% @doc Lookup all traces.
-spec(all_traces() -> [{Who :: trace_who(), LogFile :: string()}]).
all_traces() -> gen_server:call(?MODULE, all_traces).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([]) ->
{ok, #state{level = debug, traces = #{}}}.
handle_call({start_trace, Who, LogFile}, _From, State = #state{level = Level, traces = Traces}) ->
case lager:trace_file(LogFile, [Who], Level, ?TRACE_OPTIONS) of
{ok, exists} ->
{reply, {error, existed}, State};
{ok, Trace} ->
{reply, ok, State#state{traces = maps:put(Who, {Trace, LogFile}, Traces)}};
{error, Error} ->
{reply, {error, Error}, State}
end;
handle_call({stop_trace, Who}, _From, State = #state{traces = Traces}) ->
case maps:find(Who, Traces) of
{ok, {Trace, _LogFile}} ->
case lager:stop_trace(Trace) of
ok -> ok;
{error, Error} -> lager:error("Stop trace ~p error: ~p", [Who, Error])
end,
{reply, ok, State#state{traces = maps:remove(Who, Traces)}};
error ->
{reply, {error, not_found}, State}
end;
handle_call(all_traces, _From, State = #state{traces = Traces}) ->
{reply, [{Who, LogFile} || {Who, {_Trace, LogFile}}
<- maps:to_list(Traces)], State};
handle_call(Req, _From, State) ->
?UNEXPECTED_REQ(Req, State).
handle_cast(Msg, State) ->
?UNEXPECTED_MSG(Msg, State).
handle_info(Info, State) ->
?UNEXPECTED_INFO(Info, State).
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

View File

@ -1,120 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_ws).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd_protocol.hrl").
-import(proplists, [get_value/3]).
-export([handle_request/1, ws_loop/3]).
%% WebSocket Loop State
-record(wsocket_state, {peername, client_pid, max_packet_size, parser}).
-define(WSLOG(Level, Format, Args, State),
lager:Level("WsClient(~s): " ++ Format,
[esockd_net:format(State#wsocket_state.peername) | Args])).
handle_request(Req) ->
handle_request(Req:get(method), Req:get(path), Req).
%%--------------------------------------------------------------------
%% MQTT Over WebSocket
%%--------------------------------------------------------------------
handle_request('GET', "/mqtt", Req) ->
lager: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} = emqttd:env(protocol),
PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE),
Parser = emqttd_parser:initial_state(PacketSize),
%% Upgrade WebSocket.
{ReentryWs, ReplyChannel} = mochiweb_websocket:upgrade_connection(Req, fun ?MODULE:ws_loop/3),
{ok, ClientPid} = emqttd_ws_client_sup:start_client(self(), Req, ReplyChannel),
ReentryWs(#wsocket_state{peername = Peername,
parser = Parser,
max_packet_size = PacketSize,
client_pid = ClientPid});
{error, Reason} ->
lager:error("Get peername with error ~s", [Reason]),
Req:respond({400, [], <<"Bad Request">>})
end;
{false, _} ->
lager:error("Not WebSocket: Upgrade = ~s", [Upgrade]),
Req:respond({400, [], <<"Bad Request">>});
{_, Proto} ->
lager:error("WebSocket with error Protocol: ~s", [Proto]),
Req:respond({400, [], <<"Bad WebSocket Protocol">>})
end;
handle_request(Method, Path, Req) ->
lager:error("Unexpected WS Request: ~s ~s", [Method, Path]),
Req:not_found().
is_websocket(Upgrade) ->
Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket".
check_protocol_header(Req) ->
case emqttd:env(websocket_protocol_header, false) of
true -> get_protocol_header(Req);
false -> "mqtt-v3.1.1"
end.
get_protocol_header(Req) ->
case Req:get_header_value("EMQ-WebSocket-Protocol") of
undefined -> Req:get_header_value("Sec-WebSocket-Protocol");
Proto -> Proto
end.
%%--------------------------------------------------------------------
%% Receive Loop
%%--------------------------------------------------------------------
%% @doc WebSocket frame receive loop.
ws_loop(<<>>, State, _ReplyChannel) ->
State;
ws_loop([<<>>], State, _ReplyChannel) ->
State;
ws_loop(Data, State = #wsocket_state{client_pid = ClientPid, parser = Parser}, ReplyChannel) ->
?WSLOG(debug, "RECV ~p", [Data], State),
emqttd_metrics:inc('bytes/received', iolist_size(Data)),
case catch emqttd_parser:parse(iolist_to_binary(Data), Parser) of
{more, NewParser} ->
State#wsocket_state{parser = NewParser};
{ok, Packet, Rest} ->
gen_server:cast(ClientPid, {received, Packet}),
ws_loop(Rest, reset_parser(State), ReplyChannel);
{error, Error} ->
?WSLOG(error, "Frame error: ~p", [Error], State),
exit({shutdown, Error});
{'EXIT', Reason} ->
?WSLOG(error, "Frame error: ~p", [Reason], State),
?WSLOG(error, "Error data: ~p", [Data], State),
exit({shutdown, parser_error})
end.
reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) ->
State#wsocket_state{parser = emqttd_parser:initial_state(PacketSize)}.

View File

@ -1,327 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc MQTT WebSocket Connection.
-module(emqttd_ws_client).
-behaviour(gen_server2).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqttd_protocol.hrl").
-include("emqttd_internal.hrl").
-import(proplists, [get_value/3]).
%% API Exports
-export([start_link/4]).
%% Management and Monitor API
-export([info/1, stats/1, kick/1, clean_acl_cache/2]).
%% SUB/UNSUB Asynchronously
-export([subscribe/2, unsubscribe/2]).
%% Get the session proc?
-export([session/1]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%% gen_server2 Callbacks
-export([prioritise_call/4, prioritise_info/3, handle_pre_hibernate/1]).
%% WebSocket Client State
-record(wsclient_state, {ws_pid, peername, connection, proto_state, keepalive,
enable_stats, force_gc_count}).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]).
-define(WSLOG(Level, Format, Args, State),
lager:Level("WsClient(~s): " ++ Format,
[esockd_net:format(State#wsclient_state.peername) | Args])).
%% @doc Start WebSocket Client.
start_link(Env, WsPid, Req, ReplyChannel) ->
gen_server2:start_link(?MODULE, [Env, WsPid, Req, ReplyChannel],
[{spawn_opt, ?FULLSWEEP_OPTS}]). %% Tune GC.
info(CPid) ->
gen_server2:call(CPid, info).
stats(CPid) ->
gen_server2:call(CPid, stats).
kick(CPid) ->
gen_server2:call(CPid, kick).
subscribe(CPid, TopicTable) ->
CPid ! {subscribe, TopicTable}.
unsubscribe(CPid, Topics) ->
CPid ! {unsubscribe, Topics}.
session(CPid) ->
gen_server2:call(CPid, session).
clean_acl_cache(CPid, Topic) ->
gen_server2:call(CPid, {clean_acl_cache, Topic}).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
init([Env, WsPid, Req, ReplyChannel]) ->
process_flag(trap_exit, true),
Conn = Req:get(connection),
true = link(WsPid),
case Req:get(peername) of
{ok, Peername} ->
Headers = mochiweb_headers:to_list(
mochiweb_request:get(headers, Req)),
ProtoState = emqttd_protocol:init(Conn, 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 = emqttd_gc:conn_max_gc_count(),
{ok, #wsclient_state{connection = Conn,
ws_pid = WsPid,
peername = Peername,
proto_state = ProtoState,
enable_stats = EnableStats,
force_gc_count = ForceGcCount},
IdleTimeout, {backoff, 2000, 2000, 20000}, ?MODULE};
{error, enotconn} -> Conn:fast_close(),
exit(WsPid, normal),
exit(normal);
{error, Reason} -> Conn:fast_close(),
exit(WsPid, normal),
exit({shutdown, Reason})
end.
prioritise_call(Msg, _From, _Len, _State) ->
case Msg of info -> 10; stats -> 10; state -> 10; _ -> 5 end.
prioritise_info(Msg, _Len, _State) ->
case Msg of {redeliver, _} -> 5; _ -> 0 end.
handle_pre_hibernate(State = #wsclient_state{ws_pid = WsPid}) ->
erlang:garbage_collect(WsPid),
{hibernate, emqttd_gc:reset_conn_gc_count(#wsclient_state.force_gc_count, emit_stats(State))}.
handle_call(info, From, State = #wsclient_state{peername = Peername,
proto_state = ProtoState}) ->
Info = [{websocket, true}, {peername, Peername} | emqttd_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([emqttd_misc:proc_stats(),
wsock_stats(State),
emqttd_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(emqttd_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}, State = #wsclient_state{proto_state = ProtoState}) ->
emqttd_metrics:received(Packet),
case emqttd_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, hibernate}.
handle_info({subscribe, TopicTable}, State) ->
with_proto(
fun(ProtoState) ->
emqttd_protocol:subscribe(TopicTable, ProtoState)
end, State);
handle_info({unsubscribe, Topics}, State) ->
with_proto(
fun(ProtoState) ->
emqttd_protocol:unsubscribe(Topics, ProtoState)
end, State);
handle_info({suback, PacketId, GrantedQos}, State) ->
with_proto(
fun(ProtoState) ->
Packet = ?SUBACK_PACKET(PacketId, GrantedQos),
emqttd_protocol:send(Packet, ProtoState)
end, State);
handle_info({deliver, Message}, State) ->
with_proto(
fun(ProtoState) ->
emqttd_protocol:send(Message, ProtoState)
end, gc(State));
handle_info({redeliver, {?PUBREL, PacketId}}, State) ->
with_proto(
fun(ProtoState) ->
emqttd_protocol:pubrel(PacketId, ProtoState)
end, State);
handle_info(emit_stats, State) ->
{noreply, emit_stats(State), hibernate};
handle_info(timeout, State) ->
shutdown(idle_timeout, State);
handle_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State),
shutdown(conflict, State);
handle_info({shutdown, Reason}, State) ->
shutdown(Reason, State);
handle_info({keepalive, start, Interval}, State = #wsclient_state{connection = Conn}) ->
?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State),
case emqttd_keepalive:start(stat_fun(Conn), 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 emqttd_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 emqttd_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}) ->
emqttd_keepalive:cancel(KeepAlive),
case Reason of
{shutdown, Error} ->
emqttd_protocol:shutdown(Error, ProtoState);
_ ->
emqttd_protocol:shutdown(Reason, ProtoState)
end.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
send_fun(ReplyChannel) ->
Self = self(),
fun(Packet) ->
Data = emqttd_serializer:serialize(Packet),
emqttd_metrics:inc('bytes/sent', iolist_size(Data)),
case ReplyChannel({binary, Data}) of
ok -> ok;
{error, Reason} -> Self ! {shutdown, Reason}
end
end.
stat_fun(Conn) ->
fun() ->
case Conn:getstat([recv_oct]) of
{ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct};
{error, Error} -> {error, Error}
end
end.
emit_stats(State = #wsclient_state{proto_state = ProtoState}) ->
emit_stats(emqttd_protocol:clientid(ProtoState), State).
emit_stats(_ClientId, State = #wsclient_state{enable_stats = false}) ->
State;
emit_stats(undefined, State) ->
State;
emit_stats(ClientId, State) ->
{reply, Stats, _, _} = handle_call(stats, undefined, State),
emqttd_stats:set_client_stats(ClientId, Stats),
State.
wsock_stats(#wsclient_state{connection = Conn}) ->
case Conn:getstat(?SOCK_STATS) of
{ok, Ss} -> Ss;
{error, _} -> []
end.
with_proto(Fun, State = #wsclient_state{proto_state = ProtoState}) ->
{ok, ProtoState1} = Fun(ProtoState),
{noreply, State#wsclient_state{proto_state = ProtoState1}, hibernate}.
reply(Reply, State) ->
{reply, Reply, State, hibernate}.
shutdown(Reason, State) ->
stop({shutdown, Reason}, State).
stop(Reason, State) ->
{stop, Reason, State}.
gc(State) ->
Cb = fun() -> emit_stats(State) end,
emqttd_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb).

View File

@ -1,46 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%%
%% 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(emqttd_ws_client_sup).
-author("Feng Lee <feng@emqtt.io>").
-behavior(supervisor).
-export([start_link/0, start_client/3]).
-export([init/1]).
%% @doc Start websocket client supervisor
-spec(start_link() -> {ok, pid()}).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% @doc Start a WebSocket Connection.
-spec(start_client(pid(), mochiweb_request:request(), fun()) -> {ok, pid()}).
start_client(WsPid, Req, ReplyChannel) ->
supervisor:start_child(?MODULE, [WsPid, Req, ReplyChannel]).
%%--------------------------------------------------------------------
%% Supervisor callbacks
%%--------------------------------------------------------------------
init([]) ->
Env = lists:append(emqttd:env(client, []), emqttd:env(protocol, [])),
{ok, {{simple_one_for_one, 0, 1},
[{ws_client, {emqttd_ws_client, start_link, [Env]},
temporary, 5000, worker, [emqttd_ws_client]}]}}.

12
src/emqx.app.src Normal file
View File

@ -0,0 +1,12 @@
{application,emqx,
[{description,"EMQ X Broker"},
{vsn,"git"},
{modules,[]},
{registered,[emqx_sup]},
{applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd,
cowboy]},
{env,[]},
{mod,{emqx_app,[]}},
{maintainers,["Feng Lee <feng@emqx.io>"]},
{licenses,["Apache-2.0"]},
{links,[{"Github","https://github.com/emqx/emqx"}]}]}.

160
src/emqx.erl Normal file
View File

@ -0,0 +1,160 @@
%% 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).
-include("emqx.hrl").
%% Start/Stop the application
-export([start/0, is_running/1, stop/0]).
%% PubSub API
-export([subscribe/1, subscribe/2, subscribe/3]).
-export([publish/1]).
-export([unsubscribe/1]).
%% PubSub management API
-export([topics/0, subscriptions/1, subscribers/1, subscribed/2]).
%% Hooks API
-export([hook/2, hook/3, hook/4, unhook/2, run_hooks/2, run_hooks/3]).
%% Shutdown and reboot
-export([shutdown/0, shutdown/1, reboot/0]).
-define(APP, ?MODULE).
%%------------------------------------------------------------------------------
%% Bootstrap, is_running...
%%------------------------------------------------------------------------------
%% @doc Start emqx application
-spec(start() -> {ok, list(atom())} | {error, term()}).
start() ->
%% Check OS
%% Check VM
%% Check Mnesia
application:ensure_all_started(?APP).
%% @doc Stop emqx application.
-spec(stop() -> ok | {error, term()}).
stop() ->
application:stop(?APP).
%% @doc Is emqx running?
-spec(is_running(node()) -> boolean()).
is_running(Node) ->
case rpc:call(Node, erlang, whereis, [?APP]) of
{badrpc, _} -> false;
undefined -> false;
Pid when is_pid(Pid) -> true
end.
%%------------------------------------------------------------------------------
%% PubSub API
%%------------------------------------------------------------------------------
-spec(subscribe(emqx_topic:topic() | string()) -> ok).
subscribe(Topic) ->
emqx_broker:subscribe(iolist_to_binary(Topic)).
-spec(subscribe(emqx_topic:topic() | string(), emqx_types:subid() | emqx_types:subopts()) -> ok).
subscribe(Topic, SubId) when is_atom(SubId); is_binary(SubId)->
emqx_broker:subscribe(iolist_to_binary(Topic), SubId);
subscribe(Topic, SubOpts) when is_map(SubOpts) ->
emqx_broker:subscribe(iolist_to_binary(Topic), SubOpts).
-spec(subscribe(emqx_topic:topic() | string(),
emqx_types:subid() | pid(), emqx_types:subopts()) -> ok).
subscribe(Topic, SubId, SubOpts) when (is_atom(SubId) orelse is_binary(SubId)), is_map(SubOpts) ->
emqx_broker:subscribe(iolist_to_binary(Topic), SubId, SubOpts).
-spec(publish(emqx_types:message()) -> emqx_types:deliver_results()).
publish(Msg) ->
emqx_broker:publish(Msg).
-spec(unsubscribe(emqx_topic:topic() | string()) -> ok).
unsubscribe(Topic) ->
emqx_broker:unsubscribe(iolist_to_binary(Topic)).
%%------------------------------------------------------------------------------
%% PubSub management API
%%------------------------------------------------------------------------------
-spec(topics() -> list(emqx_topic:topic())).
topics() -> emqx_router:topics().
-spec(subscribers(emqx_topic:topic() | string()) -> list(emqx_types:subscriber())).
subscribers(Topic) ->
emqx_broker:subscribers(iolist_to_binary(Topic)).
-spec(subscriptions(pid()) -> [{emqx_topic:topic(), emqx_types:subopts()}]).
subscriptions(SubPid) when is_pid(SubPid) ->
emqx_broker:subscriptions(SubPid).
-spec(subscribed(pid() | emqx_types:subid(), emqx_topic:topic() | string()) -> boolean()).
subscribed(SubPid, Topic) when is_pid(SubPid) ->
emqx_broker:subscribed(SubPid, iolist_to_binary(Topic));
subscribed(SubId, Topic) when is_atom(SubId); is_binary(SubId) ->
emqx_broker:subscribed(SubId, iolist_to_binary(Topic)).
%%------------------------------------------------------------------------------
%% Hooks API
%%------------------------------------------------------------------------------
-spec(hook(emqx_hooks:hookpoint(), emqx_hooks:action()) -> ok | {error, already_exists}).
hook(HookPoint, Action) ->
emqx_hooks:add(HookPoint, Action).
-spec(hook(emqx_hooks:hookpoint(), emqx_hooks:action(), emqx_hooks:filter() | integer())
-> ok | {error, already_exists}).
hook(HookPoint, Action, Priority) when is_integer(Priority) ->
emqx_hooks:add(HookPoint, Action, Priority);
hook(HookPoint, Action, Filter) when is_function(Filter); is_tuple(Filter) ->
emqx_hooks:add(HookPoint, Action, Filter);
hook(HookPoint, Action, InitArgs) when is_list(InitArgs) ->
emqx_hooks:add(HookPoint, Action, InitArgs).
-spec(hook(emqx_hooks:hookpoint(), emqx_hooks:action(), emqx_hooks:filter(), integer())
-> ok | {error, already_exists}).
hook(HookPoint, Action, Filter, Priority) ->
emqx_hooks:add(HookPoint, Action, Filter, Priority).
-spec(unhook(emqx_hooks:hookpoint(), emqx_hooks:action()) -> ok).
unhook(HookPoint, Action) ->
emqx_hooks:del(HookPoint, Action).
-spec(run_hooks(emqx_hooks:hookpoint(), list(any())) -> ok | stop).
run_hooks(HookPoint, Args) ->
emqx_hooks:run(HookPoint, Args).
-spec(run_hooks(emqx_hooks:hookpoint(), list(any()), any()) -> {ok | stop, any()}).
run_hooks(HookPoint, Args, Acc) ->
emqx_hooks:run(HookPoint, Args, Acc).
%%------------------------------------------------------------------------------
%% Shutdown and reboot
%%------------------------------------------------------------------------------
shutdown() ->
shutdown(normal).
shutdown(Reason) ->
emqx_logger:error("emqx shutdown for ~s", [Reason]),
emqx_plugins:unload(),
lists:foreach(fun application:stop/1, [emqx, ekka, cowboy, ranch, esockd, gproc]).
reboot() ->
lists:foreach(fun application:start/1, [gproc, esockd, ranch, cowboy, ekka, emqx]).

209
src/emqx_access_control.erl Normal file
View File

@ -0,0 +1,209 @@
%% 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_access_control).
-behaviour(gen_server).
-include("emqx.hrl").
-export([start_link/0]).
-export([authenticate/2]).
-export([check_acl/3, reload_acl/0]).
-export([register_mod/3, register_mod/4, unregister_mod/2]).
-export([lookup_mods/1]).
-export([stop/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-define(TAB, ?MODULE).
-define(SERVER, ?MODULE).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
%% @doc Start access control server.
-spec(start_link() -> {ok, pid()} | {error, term()}).
start_link() ->
start_with(fun register_default_acl/0).
start_with(Fun) ->
case gen_server:start_link({local, ?SERVER}, ?MODULE, [], []) of
{ok, Pid} ->
Fun(), {ok, Pid};
{error, Reason} ->
{error, Reason}
end.
register_default_acl() ->
case emqx_config:get_env(acl_file) of
undefined -> ok;
File -> register_mod(acl, emqx_acl_internal, [File])
end.
-spec(authenticate(emqx_types:credentials(), emqx_types:password())
-> ok | {ok, map()} | {continue, map()} | {error, term()}).
authenticate(Credentials, Password) ->
authenticate(Credentials, Password, lookup_mods(auth)).
authenticate(Credentials, _Password, []) ->
Zone = maps:get(zone, Credentials, undefined),
case emqx_zone:get_env(Zone, allow_anonymous, false) of
true -> ok;
false -> {error, auth_modules_not_found}
end;
authenticate(Credentials, Password, [{Mod, State, _Seq} | Mods]) ->
case catch Mod:check(Credentials, Password, State) of
ok -> ok;
{ok, IsSuper} when is_boolean(IsSuper) ->
{ok, #{is_superuser => IsSuper}};
{ok, Result} when is_map(Result) ->
{ok, Result};
{continue, Result} when is_map(Result) ->
{continue, Result};
ignore ->
authenticate(Credentials, Password, Mods);
{error, Reason} ->
{error, Reason};
{'EXIT', Error} ->
{error, Error}
end.
%% @doc Check ACL
-spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny).
check_acl(Credentials, PubSub, Topic) when PubSub =:= publish; PubSub =:= subscribe ->
check_acl(Credentials, PubSub, Topic, lookup_mods(acl), emqx_acl_cache:is_enabled()).
check_acl(Credentials, PubSub, Topic, AclMods, false) ->
do_check_acl(Credentials, PubSub, Topic, AclMods);
check_acl(Credentials, PubSub, Topic, AclMods, true) ->
case emqx_acl_cache:get_acl_cache(PubSub, Topic) of
not_found ->
AclResult = do_check_acl(Credentials, PubSub, Topic, AclMods),
emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult),
AclResult;
AclResult ->
AclResult
end.
do_check_acl(#{zone := Zone}, _PubSub, _Topic, []) ->
emqx_zone:get_env(Zone, acl_nomatch, deny);
do_check_acl(Credentials, PubSub, Topic, [{Mod, State, _Seq}|AclMods]) ->
case Mod:check_acl({Credentials, PubSub, Topic}, State) of
allow -> allow;
deny -> deny;
ignore -> do_check_acl(Credentials, PubSub, Topic, AclMods)
end.
-spec(reload_acl() -> list(ok | {error, term()})).
reload_acl() ->
[Mod:reload_acl(State) || {Mod, State, _Seq} <- lookup_mods(acl)].
%% @doc Register an Auth/ACL module.
-spec(register_mod(auth | acl, module(), list()) -> ok | {error, term()}).
register_mod(Type, Mod, Opts) when Type =:= auth; Type =:= acl ->
register_mod(Type, Mod, Opts, 0).
-spec(register_mod(auth | acl, module(), list(), non_neg_integer())
-> ok | {error, term()}).
register_mod(Type, Mod, Opts, Seq) when Type =:= auth; Type =:= acl->
gen_server:call(?SERVER, {register_mod, Type, Mod, Opts, Seq}).
%% @doc Unregister an Auth/ACL module.
-spec(unregister_mod(auth | acl, module()) -> ok | {error, not_found | term()}).
unregister_mod(Type, Mod) when Type =:= auth; Type =:= acl ->
gen_server:call(?SERVER, {unregister_mod, Type, Mod}).
%% @doc Lookup all Auth/ACL modules.
-spec(lookup_mods(auth | acl) -> list()).
lookup_mods(Type) ->
case ets:lookup(?TAB, tab_key(Type)) of
[] -> [];
[{_, Mods}] -> Mods
end.
tab_key(auth) -> auth_modules;
tab_key(acl) -> acl_modules.
stop() ->
gen_server:stop(?SERVER, normal, infinity).
%%-----------------------------------------------------------------------------
%% gen_server callbacks
%%-----------------------------------------------------------------------------
init([]) ->
ok = emqx_tables:new(?TAB, [set, protected, {read_concurrency, true}]),
{ok, #{}}.
handle_call({register_mod, Type, Mod, Opts, Seq}, _From, State) ->
Mods = lookup_mods(Type),
reply(case lists:keymember(Mod, 1, Mods) of
true -> {error, already_exists};
false ->
try Mod:init(Opts) of
{ok, ModState} ->
NewMods = lists:sort(fun({_, _, Seq1}, {_, _, Seq2}) ->
Seq1 >= Seq2
end, [{Mod, ModState, Seq} | Mods]),
ets:insert(?TAB, {tab_key(Type), NewMods}),
ok
catch
_:Error ->
emqx_logger:error("[AccessControl] Failed to init ~s: ~p", [Mod, Error]),
{error, Error}
end
end, State);
handle_call({unregister_mod, Type, Mod}, _From, State) ->
Mods = lookup_mods(Type),
reply(case lists:keyfind(Mod, 1, Mods) of
false ->
{error, not_found};
{Mod, _ModState, _Seq} ->
ets:insert(?TAB, {tab_key(Type), lists:keydelete(Mod, 1, Mods)}), ok
end, State);
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(Req, _From, State) ->
emqx_logger:error("[AccessControl] unexpected request: ~p", [Req]),
{reply, ignored, State}.
handle_cast(Msg, State) ->
emqx_logger:error("[AccessControl] unexpected msg: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
emqx_logger:error("[AccessControl] unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
reply(Reply, State) ->
{reply, Reply, State}.

151
src/emqx_access_rule.erl Normal file
View File

@ -0,0 +1,151 @@
%% 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_access_rule).
-include("emqx.hrl").
-type(who() :: all | binary() |
{client, binary()} |
{user, binary()} |
{ipaddr, esockd_cidr:cidr_string()}).
-type(access() :: subscribe | publish | pubsub).
-type(rule() :: {allow, all} |
{allow, who(), access(), list(emqx_topic:topic())} |
{deny, all} |
{deny, who(), access(), list(emqx_topic:topic())}).
-export_type([rule/0]).
-export([compile/1]).
-export([match/3]).
-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))).
-define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= pubsub))).
%% @doc Compile Access Rule.
compile({A, all}) when ?ALLOW_DENY(A) ->
{A, all};
compile({A, Who, Access, Topic}) when ?ALLOW_DENY(A), ?PUBSUB(Access), is_binary(Topic) ->
{A, compile(who, Who), Access, [compile(topic, Topic)]};
compile({A, Who, Access, TopicFilters}) when ?ALLOW_DENY(A), ?PUBSUB(Access) ->
{A, compile(who, Who), Access, [compile(topic, Topic) || Topic <- TopicFilters]}.
compile(who, all) ->
all;
compile(who, {ipaddr, CIDR}) ->
{ipaddr, esockd_cidr:parse(CIDR, true)};
compile(who, {client, all}) ->
{client, all};
compile(who, {client, ClientId}) ->
{client, bin(ClientId)};
compile(who, {user, all}) ->
{user, all};
compile(who, {user, Username}) ->
{user, bin(Username)};
compile(who, {'and', Conds}) when is_list(Conds) ->
{'and', [compile(who, Cond) || Cond <- Conds]};
compile(who, {'or', Conds}) when is_list(Conds) ->
{'or', [compile(who, Cond) || Cond <- Conds]};
compile(topic, {eq, Topic}) ->
{eq, emqx_topic:words(bin(Topic))};
compile(topic, Topic) ->
Words = emqx_topic:words(bin(Topic)),
case 'pattern?'(Words) of
true -> {pattern, Words};
false -> Words
end.
'pattern?'(Words) ->
lists:member(<<"%u">>, Words)
orelse lists:member(<<"%c">>, Words).
bin(L) when is_list(L) ->
list_to_binary(L);
bin(B) when is_binary(B) ->
B.
%% @doc Match access rule
-spec(match(emqx_types:credentials(), emqx_types:topic(), rule())
-> {matched, allow} | {matched, deny} | nomatch).
match(_Credentials, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) ->
{matched, AllowDeny};
match(Credentials, Topic, {AllowDeny, Who, _PubSub, TopicFilters})
when ?ALLOW_DENY(AllowDeny) ->
case match_who(Credentials, Who)
andalso match_topics(Credentials, Topic, TopicFilters) of
true -> {matched, AllowDeny};
false -> nomatch
end.
match_who(_Credentials, all) ->
true;
match_who(_Credentials, {user, all}) ->
true;
match_who(_Credentials, {client, all}) ->
true;
match_who(#{client_id := ClientId}, {client, ClientId}) ->
true;
match_who(#{username := Username}, {user, Username}) ->
true;
match_who(#{peername := undefined}, {ipaddr, _Tup}) ->
false;
match_who(#{peername := {IP, _}}, {ipaddr, CIDR}) ->
esockd_cidr:match(IP, CIDR);
match_who(Credentials, {'and', Conds}) when is_list(Conds) ->
lists:foldl(fun(Who, Allow) ->
match_who(Credentials, Who) andalso Allow
end, true, Conds);
match_who(Credentials, {'or', Conds}) when is_list(Conds) ->
lists:foldl(fun(Who, Allow) ->
match_who(Credentials, Who) orelse Allow
end, false, Conds);
match_who(_Credentials, _Who) ->
false.
match_topics(_Credentials, _Topic, []) ->
false;
match_topics(Credentials, Topic, [{pattern, PatternFilter}|Filters]) ->
TopicFilter = feed_var(Credentials, PatternFilter),
match_topic(emqx_topic:words(Topic), TopicFilter)
orelse match_topics(Credentials, Topic, Filters);
match_topics(Credentials, Topic, [TopicFilter|Filters]) ->
match_topic(emqx_topic:words(Topic), TopicFilter)
orelse match_topics(Credentials, Topic, Filters).
match_topic(Topic, {eq, TopicFilter}) ->
Topic == TopicFilter;
match_topic(Topic, TopicFilter) ->
emqx_topic:match(Topic, TopicFilter).
feed_var(Credentials, Pattern) ->
feed_var(Credentials, Pattern, []).
feed_var(_Credentials, [], Acc) ->
lists:reverse(Acc);
feed_var(Credentials = #{client_id := undefined}, [<<"%c">>|Words], Acc) ->
feed_var(Credentials, Words, [<<"%c">>|Acc]);
feed_var(Credentials = #{client_id := ClientId}, [<<"%c">>|Words], Acc) ->
feed_var(Credentials, Words, [ClientId |Acc]);
feed_var(Credentials = #{username := undefined}, [<<"%u">>|Words], Acc) ->
feed_var(Credentials, Words, [<<"%u">>|Acc]);
feed_var(Credentials = #{username := Username}, [<<"%u">>|Words], Acc) ->
feed_var(Credentials, Words, [Username|Acc]);
feed_var(Credentials, [W|Words], Acc) ->
feed_var(Credentials, Words, [W|Acc]).

221
src/emqx_acl_cache.erl Normal file
View File

@ -0,0 +1,221 @@
%% 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_acl_cache).
-include("emqx.hrl").
-export([ get_acl_cache/2
, put_acl_cache/3
, cleanup_acl_cache/0
, empty_acl_cache/0
, dump_acl_cache/0
, get_cache_size/0
, get_cache_max_size/0
, get_newest_key/0
, get_oldest_key/0
, cache_k/2
, cache_v/1
, is_enabled/0
]).
-type(acl_result() :: allow | deny).
%% Wrappers for key and value
cache_k(PubSub, Topic)-> {PubSub, Topic}.
cache_v(AclResult)-> {AclResult, time_now()}.
-spec(is_enabled() -> boolean()).
is_enabled() ->
application:get_env(emqx, enable_acl_cache, true).
%% We'll cleanup the cache before repalcing an expired acl.
-spec(get_acl_cache(publish | subscribe, emqx_topic:topic()) -> (acl_result() | not_found)).
get_acl_cache(PubSub, Topic) ->
case erlang:get(cache_k(PubSub, Topic)) of
undefined -> not_found;
{AclResult, CachedAt} ->
if_expired(CachedAt,
fun(false) ->
AclResult;
(true) ->
cleanup_acl_cache(),
not_found
end)
end.
%% If the cache get full, and also the latest one
%% is expired, then delete all the cache entries
-spec(put_acl_cache(publish | subscribe, emqx_topic:topic(), acl_result()) -> ok).
put_acl_cache(PubSub, Topic, AclResult) ->
MaxSize = get_cache_max_size(), true = (MaxSize =/= 0),
Size = get_cache_size(),
if
Size < MaxSize ->
add_acl(PubSub, Topic, AclResult);
Size =:= MaxSize ->
NewestK = get_newest_key(),
{_AclResult, CachedAt} = erlang:get(NewestK),
if_expired(CachedAt,
fun(true) ->
% all cache expired, cleanup first
empty_acl_cache(),
add_acl(PubSub, Topic, AclResult);
(false) ->
% cache full, perform cache replacement
evict_acl_cache(),
add_acl(PubSub, Topic, AclResult)
end)
end.
%% delete all the acl entries
-spec(empty_acl_cache() -> ok).
empty_acl_cache() ->
map_acl_cache(fun({CacheK, _CacheV}) ->
erlang:erase(CacheK)
end),
set_cache_size(0),
keys_queue_set(queue:new()).
%% delete the oldest acl entry
-spec(evict_acl_cache() -> ok).
evict_acl_cache() ->
OldestK = keys_queue_out(),
erlang:erase(OldestK),
decr_cache_size().
%% cleanup all the exipired cache entries
-spec(cleanup_acl_cache() -> ok).
cleanup_acl_cache() ->
keys_queue_set(
cleanup_acl(keys_queue_get())).
get_oldest_key() ->
keys_queue_pick(queue_front()).
get_newest_key() ->
keys_queue_pick(queue_rear()).
get_cache_max_size() ->
application:get_env(emqx, acl_cache_max_size, 32).
get_cache_size() ->
case erlang:get(acl_cache_size) of
undefined -> 0;
Size -> Size
end.
dump_acl_cache() ->
map_acl_cache(fun(Cache) -> Cache end).
map_acl_cache(Fun) ->
[Fun(R) || R = {{SubPub, _T}, _Acl} <- get(), SubPub =:= publish
orelse SubPub =:= subscribe].
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
add_acl(PubSub, Topic, AclResult) ->
K = cache_k(PubSub, Topic),
V = cache_v(AclResult),
case erlang:get(K) of
undefined -> add_new_acl(K, V);
{_AclResult, _CachedAt} ->
update_acl(K, V)
end.
add_new_acl(K, V) ->
erlang:put(K, V),
keys_queue_in(K),
incr_cache_size().
update_acl(K, V) ->
erlang:put(K, V),
keys_queue_update(K).
cleanup_acl(KeysQ) ->
case queue:out(KeysQ) of
{{value, OldestK}, KeysQ2} ->
{_AclResult, CachedAt} = erlang:get(OldestK),
if_expired(CachedAt,
fun(false) -> KeysQ;
(true) ->
erlang:erase(OldestK),
decr_cache_size(),
cleanup_acl(KeysQ2)
end);
{empty, KeysQ} -> KeysQ
end.
incr_cache_size() ->
erlang:put(acl_cache_size, get_cache_size() + 1), ok.
decr_cache_size() ->
Size = get_cache_size(),
if Size > 1 ->
erlang:put(acl_cache_size, Size-1);
Size =< 1 ->
erlang:put(acl_cache_size, 0)
end, ok.
set_cache_size(N) ->
erlang:put(acl_cache_size, N), ok.
%%% Ordered Keys Q %%%
keys_queue_in(Key) ->
%% delete the key first if exists
KeysQ = keys_queue_get(),
keys_queue_set(queue:in(Key, KeysQ)).
keys_queue_out() ->
case queue:out(keys_queue_get()) of
{{value, OldestK}, Q2} ->
keys_queue_set(Q2), OldestK;
{empty, _Q} ->
undefined
end.
keys_queue_update(Key) ->
NewKeysQ = keys_queue_remove(Key, keys_queue_get()),
keys_queue_set(queue:in(Key, NewKeysQ)).
keys_queue_pick(Pick) ->
KeysQ = keys_queue_get(),
case queue:is_empty(KeysQ) of
true -> undefined;
false -> Pick(KeysQ)
end.
keys_queue_remove(Key, KeysQ) ->
queue:filter(fun
(K) when K =:= Key -> false; (_) -> true
end, KeysQ).
keys_queue_set(KeysQ) ->
erlang:put(acl_keys_q, KeysQ), ok.
keys_queue_get() ->
case erlang:get(acl_keys_q) of
undefined -> queue:new();
KeysQ -> KeysQ
end.
queue_front() -> fun queue:get/1.
queue_rear() -> fun queue:get_r/1.
time_now() -> erlang:system_time(millisecond).
if_expired(CachedAt, Fun) ->
TTL = application:get_env(emqx, acl_cache_ttl, 60000),
Now = time_now(),
if (CachedAt + TTL) =< Now ->
Fun(true);
true ->
Fun(false)
end.

121
src/emqx_acl_internal.erl Normal file
View File

@ -0,0 +1,121 @@
%% 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_acl_internal).
-behaviour(emqx_acl_mod).
-include("emqx.hrl").
-export([all_rules/0]).
%% ACL mod callbacks
-export([init/1, check_acl/2, reload_acl/1, description/0]).
-define(ACL_RULE_TAB, emqx_acl_rule).
-type(state() :: #{acl_file := string()}).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
%% @doc Read all rules
-spec(all_rules() -> list(emqx_access_rule:rule())).
all_rules() ->
case ets:lookup(?ACL_RULE_TAB, all_rules) of
[] -> [];
[{_, Rules}] -> Rules
end.
%%------------------------------------------------------------------------------
%% ACL callbacks
%%------------------------------------------------------------------------------
-spec(init([File :: string()]) -> {ok, #{}}).
init([File]) ->
_ = emqx_tables:new(?ACL_RULE_TAB, [set, public, {read_concurrency, true}]),
ok = load_rules_from_file(File),
{ok, #{acl_file => File}}.
load_rules_from_file(AclFile) ->
case file:consult(AclFile) of
{ok, Terms} ->
Rules = [emqx_access_rule:compile(Term) || Term <- Terms],
lists:foreach(fun(PubSub) ->
ets:insert(?ACL_RULE_TAB, {PubSub,
lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)})
end, [publish, subscribe]),
ets:insert(?ACL_RULE_TAB, {all_rules, Terms}),
ok;
{error, Reason} ->
emqx_logger:error("[ACL_INTERNAL] Failed to read ~s: ~p", [AclFile, Reason]),
{error, Reason}
end.
filter(_PubSub, {allow, all}) ->
true;
filter(_PubSub, {deny, all}) ->
true;
filter(publish, {_AllowDeny, _Who, publish, _Topics}) ->
true;
filter(_PubSub, {_AllowDeny, _Who, pubsub, _Topics}) ->
true;
filter(subscribe, {_AllowDeny, _Who, subscribe, _Topics}) ->
true;
filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) ->
false.
%% @doc Check ACL
-spec(check_acl({emqx_types:credentials(), emqx_types:pubsub(), emqx_topic:topic()}, #{})
-> allow | deny | ignore).
check_acl({Credentials, PubSub, Topic}, _State) ->
case match(Credentials, Topic, lookup(PubSub)) of
{matched, allow} -> allow;
{matched, deny} -> deny;
nomatch -> ignore
end.
lookup(PubSub) ->
case ets:lookup(?ACL_RULE_TAB, PubSub) of
[] -> [];
[{PubSub, Rules}] -> Rules
end.
match(_Credentials, _Topic, []) ->
nomatch;
match(Credentials, Topic, [Rule|Rules]) ->
case emqx_access_rule:match(Credentials, Topic, Rule) of
nomatch ->
match(Credentials, Topic, Rules);
{matched, AllowDeny} ->
{matched, AllowDeny}
end.
-spec(reload_acl(state()) -> ok | {error, term()}).
reload_acl(#{acl_file := AclFile}) ->
case catch load_rules_from_file(AclFile) of
ok ->
emqx_logger:info("Reload acl_file ~s successfully", [AclFile]),
ok;
{error, Error} ->
{error, Error};
{'EXIT', Error} ->
{error, Error}
end.
-spec(description() -> string()).
description() ->
"Internal ACL with etc/acl.conf".

View File

@ -1,5 +1,4 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@ -12,13 +11,10 @@
%% 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(emqttd_acl_mod).
-module(emqx_acl_mod).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-include("emqx.hrl").
%%--------------------------------------------------------------------
%% ACL behavihour
@ -26,13 +22,12 @@
-ifdef(use_specs).
-callback(init(AclOpts :: list()) -> {ok, State :: any()}).
-callback(init(AclOpts :: list()) -> {ok, State :: term()}).
-callback(check_acl({Client :: mqtt_client(),
PubSub :: pubsub(),
Topic :: binary()}, State :: any()) -> allow | deny | ignore).
-callback(check_acl({credentials(), pubsub(), topic()}, State :: term())
-> allow | deny | ignore).
-callback(reload_acl(State :: any()) -> ok | {error, term()}).
-callback(reload_acl(State :: term()) -> ok | {error, term()}).
-callback(description() -> string()).

143
src/emqx_alarm_mgr.erl Normal file
View File

@ -0,0 +1,143 @@
%% 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_alarm_mgr).
-behaviour(gen_event).
-include("emqx.hrl").
-export([start_link/0]).
-export([alarm_fun/0, get_alarms/0, set_alarm/1, clear_alarm/1]).
-export([add_alarm_handler/1, add_alarm_handler/2, delete_alarm_handler/1]).
%% gen_event callbacks
-export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2,
code_change/3]).
-define(ALARM_MGR, ?MODULE).
start_link() ->
start_with(
fun(Pid) ->
gen_event:add_handler(Pid, ?MODULE, [])
end).
start_with(Fun) ->
case gen_event:start_link({local, ?ALARM_MGR}) of
{ok, Pid} -> Fun(Pid), {ok, Pid};
Error -> Error
end.
alarm_fun() -> alarm_fun(false).
alarm_fun(Bool) ->
fun(alert, _Alarm) when Bool =:= true -> alarm_fun(true);
(alert, Alarm) when Bool =:= false -> set_alarm(Alarm), alarm_fun(true);
(clear, AlarmId) when Bool =:= true -> clear_alarm(AlarmId), alarm_fun(false);
(clear, _AlarmId) when Bool =:= false -> alarm_fun(false)
end.
-spec(set_alarm(emqx_types:alarm()) -> ok).
set_alarm(Alarm) when is_record(Alarm, alarm) ->
gen_event:notify(?ALARM_MGR, {set_alarm, Alarm}).
-spec(clear_alarm(any()) -> ok).
clear_alarm(AlarmId) when is_binary(AlarmId) ->
gen_event:notify(?ALARM_MGR, {clear_alarm, AlarmId}).
-spec(get_alarms() -> list(emqx_types:alarm())).
get_alarms() ->
gen_event:call(?ALARM_MGR, ?MODULE, get_alarms).
add_alarm_handler(Module) when is_atom(Module) ->
gen_event:add_handler(?ALARM_MGR, Module, []).
add_alarm_handler(Module, Args) when is_atom(Module) ->
gen_event:add_handler(?ALARM_MGR, Module, Args).
delete_alarm_handler(Module) when is_atom(Module) ->
gen_event:delete_handler(?ALARM_MGR, Module, []).
%%------------------------------------------------------------------------------
%% Default Alarm handler
%%------------------------------------------------------------------------------
init(_) -> {ok, #{alarms => []}}.
handle_event({set_alarm, Alarm = #alarm{timestamp = undefined}}, State)->
handle_event({set_alarm, Alarm#alarm{timestamp = os:timestamp()}}, State);
handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #{alarms := Alarms}) ->
case encode_alarm(Alarm) of
{ok, Json} ->
emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json));
{error, Reason} ->
emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason])
end,
{ok, State#{alarms := [Alarm|Alarms]}};
handle_event({clear_alarm, AlarmId}, State = #{alarms := Alarms}) ->
case emqx_json:safe_encode([{id, AlarmId}, {ts, os:system_time(second)}]) of
{ok, Json} ->
emqx_broker:safe_publish(alarm_msg(clear, AlarmId, Json));
{error, Reason} ->
emqx_logger:error("[AlarmMgr] Failed to encode clear: ~p", [Reason])
end,
{ok, State#{alarms := lists:keydelete(AlarmId, 2, Alarms)}, hibernate};
handle_event(Event, State)->
emqx_logger:error("[AlarmMgr] unexpected event: ~p", [Event]),
{ok, State}.
handle_info(Info, State) ->
emqx_logger:error("[AlarmMgr] unexpected info: ~p", [Info]),
{ok, State}.
handle_call(get_alarms, State = #{alarms := Alarms}) ->
{ok, Alarms, State};
handle_call(Req, State) ->
emqx_logger:error("[AlarmMgr] unexpected call: ~p", [Req]),
{ok, ignored, State}.
terminate(swap, State) ->
{?MODULE, State};
terminate(_, _) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
encode_alarm(#alarm{id = AlarmId, severity = Severity, title = Title,
summary = Summary, timestamp = Ts}) ->
emqx_json:safe_encode([{id, AlarmId}, {severity, Severity},
{title, iolist_to_binary(Title)},
{summary, iolist_to_binary(Summary)},
{ts, emqx_time:now_secs(Ts)}]).
alarm_msg(Type, AlarmId, Json) ->
Msg = emqx_message:make(?ALARM_MGR, topic(Type, AlarmId), Json),
emqx_message:set_headers( #{'Content-Type' => <<"application/json">>},
emqx_message:set_flag(sys, Msg)).
topic(alert, AlarmId) ->
emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>);
topic(clear, AlarmId) ->
emqx_topic:systop(<<"alarms/", AlarmId/binary, "/clear">>).

71
src/emqx_app.erl Normal file
View File

@ -0,0 +1,71 @@
%% 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_app).
-behaviour(application).
-export([start/2, stop/1]).
-define(APP, emqx).
%%--------------------------------------------------------------------
%% Application callbacks
%%--------------------------------------------------------------------
start(_Type, _Args) ->
%% We'd like to configure the primary logger level here, rather than set the
%% kernel config `logger_level` before starting the erlang vm.
%% This is because the latter approach an annoying debug msg will be printed out:
%% "[debug] got_unexpected_message {'EXIT',<0.1198.0>,normal}"
logger:set_primary_config(level, application:get_env(emqx, primary_log_level, error)),
print_banner(),
ekka:start(),
{ok, Sup} = emqx_sup:start_link(),
emqx_modules:load(),
emqx_plugins:init(),
emqx_plugins:load(),
emqx_listeners:start(),
start_autocluster(),
register(emqx, self()),
print_vsn(),
{ok, Sup}.
-spec(stop(State :: term()) -> term()).
stop(_State) ->
emqx_listeners:stop(),
emqx_modules:unload().
%%--------------------------------------------------------------------
%% Print Banner
%%--------------------------------------------------------------------
print_banner() ->
io:format("Starting ~s on node ~s~n", [?APP, node()]).
print_vsn() ->
{ok, Descr} = application:get_key(description),
{ok, Vsn} = application:get_key(vsn),
io:format("~s ~s is running now!~n", [Descr, Vsn]).
%%--------------------------------------------------------------------
%% Autocluster
%%--------------------------------------------------------------------
start_autocluster() ->
ekka:callback(prepare, fun emqx:shutdown/1),
ekka:callback(reboot, fun emqx:reboot/0),
ekka:autocluster(?APP).

View File

@ -1,5 +1,4 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@ -12,41 +11,31 @@
%% 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_auth_mod).
-include("emqx.hrl").
%%--------------------------------------------------------------------
%% Authentication behavihour
%%--------------------------------------------------------------------
-module(emqttd_cli_SUITE).
-ifdef(use_specs).
-compile(export_all).
-callback(init(AuthOpts :: list()) -> {ok, State :: term()}).
-include("emqttd.hrl").
-callback(check(credentials(), password(), State :: term())
-> ok | {ok, boolean()} | {ok, map()} |
{continue, map()} | ignore | {error, term()}).
-callback(description() -> string()).
-include_lib("eunit/include/eunit.hrl").
-else.
all() ->
[{group, subscriptions}].
-export([behaviour_info/1]).
groups() ->
[{subscriptions, [sequence],
[t_subsciptions_list,
t_subsciptions_show,
t_subsciptions_add,
t_subsciptions_del]}].
init_per_suite(Config) ->
Config.
end_per_suite(_Config) ->
todo.
t_subsciptions_list(_) ->
todo.
t_subsciptions_show(_) ->
todo.
t_subsciptions_add(_) ->
todo.
t_subsciptions_del(_) ->
todo.
behaviour_info(callbacks) ->
[{init, 1}, {check, 3}, {description, 0}];
behaviour_info(_Other) ->
undefined.
-endif.

119
src/emqx_banned.erl Normal file
View File

@ -0,0 +1,119 @@
%% 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_banned).
-behaviour(gen_server).
-include("emqx.hrl").
%% Mnesia bootstrap
-export([mnesia/1]).
-boot_mnesia({mnesia, [boot]}).
-copy_mnesia({mnesia, [copy]}).
-export([start_link/0]).
-export([check/1]).
-export([add/1, delete/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-define(TAB, ?MODULE).
%%------------------------------------------------------------------------------
%% Mnesia bootstrap
%%------------------------------------------------------------------------------
mnesia(boot) ->
ok = ekka_mnesia:create_table(?TAB, [
{type, set},
{disc_copies, [node()]},
{record_name, banned},
{attributes, record_info(fields, banned)},
{storage_properties, [{ets, [{read_concurrency, true}]}]}]);
mnesia(copy) ->
ok = ekka_mnesia:copy_table(?TAB).
%% @doc Start the banned server.
-spec(start_link() -> emqx_types:startlink_ret()).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec(check(emqx_types:credentials()) -> boolean()).
check(#{client_id := ClientId, username := Username, peername := {IPAddr, _}}) ->
ets:member(?TAB, {client_id, ClientId})
orelse ets:member(?TAB, {username, Username})
orelse ets:member(?TAB, {ipaddr, IPAddr}).
-spec(add(#banned{}) -> ok).
add(Banned) when is_record(Banned, banned) ->
mnesia:dirty_write(?TAB, Banned).
-spec(delete({client_id, emqx_types:client_id()}
| {username, emqx_types:username()}
| {peername, emqx_types:peername()}) -> ok).
delete(Key) ->
mnesia:dirty_delete(?TAB, Key).
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([]) ->
{ok, ensure_expiry_timer(#{expiry_timer => undefined})}.
handle_call(Req, _From, State) ->
emqx_logger:error("[Banned] unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast(Msg, State) ->
emqx_logger:error("[Banned] unexpected msg: ~p", [Msg]),
{noreply, State}.
handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) ->
mnesia:async_dirty(fun expire_banned_items/1, [erlang:system_time(second)]),
{noreply, ensure_expiry_timer(State), hibernate};
handle_info(Info, State) ->
emqx_logger:error("[Banned] unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #{expiry_timer := TRef}) ->
emqx_misc:cancel_timer(TRef).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
-ifdef(TEST).
ensure_expiry_timer(State) ->
State#{expiry_timer := emqx_misc:start_timer(timer:seconds(1), expire)}.
-else.
ensure_expiry_timer(State) ->
State#{expiry_timer := emqx_misc:start_timer(timer:minutes(1), expire)}.
-endif.
expire_banned_items(Now) ->
mnesia:foldl(
fun(B = #banned{until = Until}, _Acc) when Until < Now ->
mnesia:delete_object(?TAB, B, sticky_write);
(_, _Acc) -> ok
end, ok, ?TAB).

112
src/emqx_base62.erl Normal file
View File

@ -0,0 +1,112 @@
%% 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_base62).
-export([encode/1,
encode/2,
decode/1,
decode/2]).
%% @doc Encode any data to base62 binary
-spec encode(string()
| integer()
| binary()) -> binary().
encode(I) when is_integer(I) ->
encode(integer_to_binary(I));
encode(S) when is_list(S)->
encode(list_to_binary(S));
encode(B) when is_binary(B) ->
encode(B, <<>>).
%% encode(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, <<>>).
%%====================================================================
%% Internal functions
%%====================================================================
encode(D, string) ->
binary_to_list(encode(D));
encode(<<Index1:6, Index2:6, Index3:6, Index4:6, Rest/binary>>, Acc) ->
CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3), encode_char(Index4)],
NewAcc = <<Acc/binary,(iolist_to_binary(CharList))/binary>>,
encode(Rest, NewAcc);
encode(<<Index1:6, Index2:6, Index3:4>>, Acc) ->
CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3)],
NewAcc = <<Acc/binary,(iolist_to_binary(CharList))/binary>>,
encode(<<>>, NewAcc);
encode(<<Index1:6, Index2:2>>, Acc) ->
CharList = [encode_char(Index1), encode_char(Index2)],
NewAcc = <<Acc/binary,(iolist_to_binary(CharList))/binary>>,
encode(<<>>, NewAcc);
encode(<<>>, Acc) ->
Acc.
decode(D, integer) ->
binary_to_integer(decode(D));
decode(D, string) ->
binary_to_list(decode(D));
decode(<<Head:8, Rest/binary>>, Acc)
when bit_size(Rest) >= 8->
case Head == $9 of
true ->
<<Head1:8, Rest1/binary>> = Rest,
DecodeChar = decode_char(9, Head1),
<<_:2, RestBit:6>> = <<DecodeChar>>,
NewAcc = <<Acc/bitstring, RestBit:6>>,
decode(Rest1, NewAcc);
false ->
DecodeChar = decode_char(Head),
<<_:2, RestBit:6>> = <<DecodeChar>>,
NewAcc = <<Acc/bitstring, RestBit:6>>,
decode(Rest, NewAcc)
end;
decode(<<Head:8, Rest/binary>>, Acc) ->
DecodeChar = decode_char(Head),
LeftBitSize = bit_size(Acc) rem 8,
RightBitSize = 8 - LeftBitSize,
<<_:LeftBitSize, RestBit:RightBitSize>> = <<DecodeChar>>,
NewAcc = <<Acc/bitstring, RestBit:RightBitSize>>,
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.

74
src/emqx_batch.erl Normal file
View File

@ -0,0 +1,74 @@
%% 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_batch).
-export([init/1, push/2, commit/1]).
-export([size/1, items/1]).
-type(options() :: #{
batch_size => non_neg_integer(),
linger_ms => pos_integer(),
commit_fun := function()
}).
-export_type([options/0]).
-record(batch, {
batch_size :: non_neg_integer(),
batch_q :: list(any()),
linger_ms :: pos_integer(),
linger_timer :: reference() | undefined,
commit_fun :: function()
}).
-type(batch() :: #batch{}).
-export_type([batch/0]).
-spec(init(options()) -> batch()).
init(Opts) when is_map(Opts) ->
#batch{batch_size = maps:get(batch_size, Opts, 1000),
batch_q = [],
linger_ms = maps:get(linger_ms, Opts, 1000),
commit_fun = maps:get(commit_fun, Opts)}.
-spec(push(any(), batch()) -> batch()).
push(El, Batch = #batch{batch_q = Q, linger_ms = Ms, linger_timer = undefined}) when length(Q) == 0 ->
Batch#batch{batch_q = [El], linger_timer = erlang:send_after(Ms, self(), batch_linger_expired)};
%% no limit.
push(El, Batch = #batch{batch_size = 0, batch_q = Q}) ->
Batch#batch{batch_q = [El|Q]};
push(El, Batch = #batch{batch_size = MaxSize, batch_q = Q}) when length(Q) >= MaxSize ->
commit(Batch#batch{batch_q = [El|Q]});
push(El, Batch = #batch{batch_q = Q}) ->
Batch#batch{batch_q = [El|Q]}.
-spec(commit(batch()) -> batch()).
commit(Batch = #batch{batch_q = Q, commit_fun = Commit}) ->
_ = Commit(lists:reverse(Q)),
reset(Batch).
reset(Batch = #batch{linger_timer = TRef}) ->
_ = emqx_misc:cancel_timer(TRef),
Batch#batch{batch_q = [], linger_timer = undefined}.
-spec(size(batch()) -> non_neg_integer()).
size(#batch{batch_q = Q}) ->
length(Q).
-spec(items(batch()) -> list(any())).
items(#batch{batch_q = Q}) ->
lists:reverse(Q).

334
src/emqx_bridge.erl Normal file
View File

@ -0,0 +1,334 @@
%% 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_bridge).
-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([show_forwards/1, add_forward/2, del_forward/2]).
-export([show_subscriptions/1, add_subscription/3, del_subscription/2]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-record(state, {client_pid, options, reconnect_interval,
mountpoint, queue, mqueue_type, max_pending_messages,
forwards = [], subscriptions = []}).
-record(mqtt_msg, {qos = ?QOS_0, 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).
-spec(show_forwards(atom()) -> list()).
show_forwards(Name) ->
gen_server:call(name(Name), show_forwards).
-spec(add_forward(atom(), binary()) -> ok | {error, already_exists | validate_fail}).
add_forward(Name, Topic) ->
case catch emqx_topic:validate({filter, Topic}) of
true ->
gen_server:call(name(Name), {add_forward, Topic});
{'EXIT', _Reason} ->
{error, validate_fail}
end.
-spec(del_forward(atom(), binary()) -> ok | {error, validate_fail}).
del_forward(Name, Topic) ->
case catch emqx_topic:validate({filter, Topic}) of
true ->
gen_server:call(name(Name), {del_forward, Topic});
_ ->
{error, validate_fail}
end.
-spec(show_subscriptions(atom()) -> list()).
show_subscriptions(Name) ->
gen_server:call(name(Name), show_subscriptions).
-spec(add_subscription(atom(), binary(), integer()) -> ok | {error, already_exists | validate_fail}).
add_subscription(Name, Topic, QoS) ->
case catch emqx_topic:validate({filter, Topic}) of
true ->
gen_server:call(name(Name), {add_subscription, Topic, QoS});
{'EXIT', _Reason} ->
{error, validate_fail}
end.
-spec(del_subscription(atom(), binary()) -> ok | {error, validate_fail}).
del_subscription(Name, Topic) ->
case catch emqx_topic:validate({filter, Topic}) of
true ->
gen_server:call(name(Name), {del_subscription, Topic});
_ ->
{error, validate_fail}
end.
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,
ReconnectInterval = get_value(reconnect_interval, Options, 30000),
MaxPendingMsg = get_value(max_pending_messages, Options, 10000),
Mountpoint = format_mountpoint(get_value(mountpoint, Options)),
MqueueType = get_value(mqueue_type, Options, memory),
Queue = [],
{ok, #state{mountpoint = Mountpoint,
queue = Queue,
mqueue_type = MqueueType,
options = Options,
reconnect_interval = ReconnectInterval,
max_pending_messages = MaxPendingMsg}}.
handle_call(start_bridge, _From, State = #state{client_pid = undefined}) ->
{noreply, NewState} = handle_info(start, State),
{reply, #{msg => <<"start bridge successfully">>}, NewState};
handle_call(start_bridge, _From, State) ->
{reply, #{msg => <<"bridge already started">>}, State};
handle_call(stop_bridge, _From, State = #state{client_pid = undefined}) ->
{reply, #{msg => <<"bridge not started">>}, State};
handle_call(stop_bridge, _From, State = #state{client_pid = Pid}) ->
emqx_client:disconnect(Pid),
{reply, #{msg => <<"stop bridge successfully">>}, State};
handle_call(status, _From, State = #state{client_pid = undefined}) ->
{reply, #{status => <<"Stopped">>}, State};
handle_call(status, _From, State = #state{client_pid = _Pid})->
{reply, #{status => <<"Running">>}, State};
handle_call(show_forwards, _From, State = #state{forwards = Forwards}) ->
{reply, Forwards, State};
handle_call({add_forward, Topic}, _From, State = #state{forwards = Forwards}) ->
case not lists:member(Topic, Forwards) of
true ->
emqx_broker:subscribe(Topic),
{reply, ok, State#state{forwards = [Topic | Forwards]}};
false ->
{reply, {error, already_exists}, State}
end;
handle_call({del_forward, Topic}, _From, State = #state{forwards = Forwards}) ->
case lists:member(Topic, Forwards) of
true ->
emqx_broker:unsubscribe(Topic),
{reply, ok, State#state{forwards = lists:delete(Topic, Forwards)}};
false ->
{reply, ok, State}
end;
handle_call(show_subscriptions, _From, State = #state{subscriptions = Subscriptions}) ->
{reply, Subscriptions, State};
handle_call({add_subscription, Topic, Qos}, _From, State = #state{subscriptions = Subscriptions, client_pid = ClientPid}) ->
case not lists:keymember(Topic, 1, Subscriptions) of
true ->
emqx_client:subscribe(ClientPid, {Topic, Qos}),
{reply, ok, State#state{subscriptions = [{Topic, Qos} | Subscriptions]}};
false ->
{reply, {error, already_exists}, State}
end;
handle_call({del_subscription, Topic}, _From, State = #state{subscriptions = Subscriptions, client_pid = ClientPid}) ->
case lists:keymember(Topic, 1, Subscriptions) of
true ->
emqx_client:unsubscribe(ClientPid, Topic),
{reply, ok, State#state{subscriptions = lists:keydelete(Topic, 1, Subscriptions)}};
false ->
{reply, ok, State}
end;
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}.
%%----------------------------------------------------------------
%% start message bridge
%%----------------------------------------------------------------
handle_info(start, State = #state{options = Options,
client_pid = undefined}) ->
case emqx_client:start_link([{owner, self()}|options(Options)]) of
{ok, ClientPid} ->
case emqx_client:connect(ClientPid) of
{ok, _} ->
emqx_logger:info("[Bridge] connected to remote sucessfully"),
Subs = subscribe_remote_topics(ClientPid, get_value(subscriptions, Options, [])),
Forwards = subscribe_local_topics(get_value(forwards, Options, [])),
{noreply, State#state{client_pid = ClientPid,
subscriptions = Subs,
forwards = Forwards}};
{error, Reason} ->
emqx_logger:error("[Bridge] connect to remote failed! error: ~p", [Reason]),
{noreply, State#state{client_pid = ClientPid}}
end;
{error, Reason} ->
emqx_logger:error("[Bridge] start failed! error: ~p", [Reason]),
{noreply, State}
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,
mqueue_type = MqueueType, 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(MqueueType, {PkgId, Msg}, Queue, MaxPendingMsg)}};
{error, Reason} ->
emqx_logger:error("[Bridge] 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, mqueue_type = MqueueType}) ->
% lists:keydelete(PkgId, 1, Queue)
{noreply, State#state{queue = delete(MqueueType, PkgId, Queue)}};
handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) ->
emqx_logger:warning("[Bridge] stop ~p", [normal]),
{noreply, State#state{client_pid = undefined}};
handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid,
reconnect_interval = ReconnectInterval}) ->
emqx_logger:error("[Bridge] stop ~p", [Reason]),
erlang:send_after(ReconnectInterval, self(), start),
{noreply, State#state{client_pid = undefined}};
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}.
subscribe_remote_topics(ClientPid, Subscriptions) ->
[begin emqx_client:subscribe(ClientPid, {bin(Topic), Qos}), {bin(Topic), Qos} end
|| {Topic, Qos} <- Subscriptions, emqx_topic:validate({filter, bin(Topic)})].
subscribe_local_topics(Topics) ->
[begin emqx_broker:subscribe(bin(Topic)), bin(Topic) end
|| Topic <- Topics, emqx_topic:validate({filter, bin(Topic)})].
proto_ver(mqttv3) -> v3;
proto_ver(mqttv4) -> v4;
proto_ver(mqttv5) -> 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([{ssl, Ssl}| Options], Acc) ->
options(Options, [{ssl, Ssl}|Acc]);
options([{ssl_opts, SslOpts}| Options], Acc) ->
options(Options, [{ssl_opts, SslOpts}|Acc]);
options([_Option | Options], Acc) ->
options(Options, Acc).
name(Id) ->
list_to_atom(lists:concat([?MODULE, "_", Id])).
bin(L) -> iolist_to_binary(L).
mountpoint(undefined, Topic) ->
Topic;
mountpoint(Prefix, Topic) ->
<<Prefix/binary, Topic/binary>>.
format_mountpoint(undefined) ->
undefined;
format_mountpoint(Prefix) ->
binary:replace(bin(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)).
store(memory, Data, Queue, MaxPendingMsg) when length(Queue) =< MaxPendingMsg ->
[Data | Queue];
store(memory, _Data, Queue, _MaxPendingMsg) ->
logger: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).

45
src/emqx_bridge_sup.erl Normal file
View File

@ -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_bridge_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(), map()}]).
bridges() ->
[{Name, emqx_bridge: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_bridge, start_link, [Id, Options]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [emqx_bridge]}.

445
src/emqx_broker.erl Normal file
View File

@ -0,0 +1,445 @@
%% 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_broker).
-behaviour(gen_server).
-include("emqx.hrl").
-export([start_link/2]).
-export([subscribe/1, subscribe/2, subscribe/3]).
-export([unsubscribe/1]).
-export([subscriber_down/1]).
-export([publish/1, safe_publish/1]).
-export([dispatch/2]).
-export([subscriptions/1, subscribers/1, subscribed/2]).
-export([get_subopts/2, set_subopts/2]).
-export([topics/0]).
%% Stats fun
-export([stats_fun/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-import(emqx_tables, [lookup_value/2, lookup_value/3]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
-define(BROKER, ?MODULE).
%% ETS tables for PubSub
-define(SUBOPTION, emqx_suboption).
-define(SUBSCRIBER, emqx_subscriber).
-define(SUBSCRIPTION, emqx_subscription).
%% Guards
-define(is_subid(Id), (is_binary(Id) orelse is_atom(Id))).
-spec(start_link(atom(), pos_integer()) -> emqx_types:startlink_ret()).
start_link(Pool, Id) ->
ok = create_tabs(),
gen_server:start_link({local, emqx_misc:proc_name(?BROKER, Id)},
?MODULE, [Pool, Id], []).
%%------------------------------------------------------------------------------
%% Create tabs
%%------------------------------------------------------------------------------
-spec(create_tabs() -> ok).
create_tabs() ->
TabOpts = [public, {read_concurrency, true}, {write_concurrency, true}],
%% SubOption: {SubPid, Topic} -> SubOption
ok = emqx_tables:new(?SUBOPTION, [set | TabOpts]),
%% Subscription: SubPid -> Topic1, Topic2, Topic3, ...
%% duplicate_bag: o(1) insert
ok = emqx_tables:new(?SUBSCRIPTION, [duplicate_bag | TabOpts]),
%% Subscriber: Topic -> SubPid1, SubPid2, SubPid3, ...
%% bag: o(n) insert:(
ok = emqx_tables:new(?SUBSCRIBER, [bag | TabOpts]).
%%------------------------------------------------------------------------------
%% Subscribe API
%%------------------------------------------------------------------------------
-spec(subscribe(emqx_topic:topic()) -> ok).
subscribe(Topic) when is_binary(Topic) ->
subscribe(Topic, undefined).
-spec(subscribe(emqx_topic:topic(), emqx_types:subid() | emqx_types:subopts()) -> ok).
subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) ->
subscribe(Topic, SubId, #{qos => 0});
subscribe(Topic, SubOpts) when is_binary(Topic), is_map(SubOpts) ->
subscribe(Topic, undefined, SubOpts).
-spec(subscribe(emqx_topic:topic(), emqx_types:subid(), emqx_types:subopts()) -> ok).
subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) ->
SubPid = self(),
case ets:member(?SUBOPTION, {SubPid, Topic}) of
false ->
ok = emqx_broker_helper:register_sub(SubPid, SubId),
do_subscribe(Topic, SubPid, with_subid(SubId, SubOpts));
true -> ok
end.
with_subid(undefined, SubOpts) ->
SubOpts;
with_subid(SubId, SubOpts) ->
maps:put(subid, SubId, SubOpts).
%% @private
do_subscribe(Topic, SubPid, SubOpts) ->
true = ets:insert(?SUBSCRIPTION, {SubPid, Topic}),
Group = maps:get(share, SubOpts, undefined),
do_subscribe(Group, Topic, SubPid, SubOpts).
do_subscribe(undefined, Topic, SubPid, SubOpts) ->
case emqx_broker_helper:get_sub_shard(SubPid, Topic) of
0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}),
true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}),
call(pick(Topic), {subscribe, Topic});
I -> true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}),
true = ets:insert(?SUBOPTION, {{SubPid, Topic}, maps:put(shard, I, SubOpts)}),
call(pick({Topic, I}), {subscribe, Topic, I})
end;
%% Shared subscription
do_subscribe(Group, Topic, SubPid, SubOpts) ->
true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}),
emqx_shared_sub:subscribe(Group, Topic, SubPid).
%%------------------------------------------------------------------------------
%% Unsubscribe API
%%------------------------------------------------------------------------------
-spec(unsubscribe(emqx_topic:topic()) -> ok).
unsubscribe(Topic) when is_binary(Topic) ->
SubPid = self(),
case ets:lookup(?SUBOPTION, {SubPid, Topic}) of
[{_, SubOpts}] ->
_ = emqx_broker_helper:reclaim_seq(Topic),
do_unsubscribe(Topic, SubPid, SubOpts);
[] -> ok
end.
do_unsubscribe(Topic, SubPid, SubOpts) ->
true = ets:delete(?SUBOPTION, {SubPid, Topic}),
true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}),
Group = maps:get(share, SubOpts, undefined),
do_unsubscribe(Group, Topic, SubPid, SubOpts).
do_unsubscribe(undefined, Topic, SubPid, SubOpts) ->
case maps:get(shard, SubOpts, 0) of
0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}),
cast(pick(Topic), {unsubscribed, Topic});
I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}),
cast(pick({Topic, I}), {unsubscribed, Topic, I})
end;
do_unsubscribe(Group, Topic, SubPid, _SubOpts) ->
emqx_shared_sub:unsubscribe(Group, Topic, SubPid).
%%------------------------------------------------------------------------------
%% Publish
%%------------------------------------------------------------------------------
-spec(publish(emqx_types:message()) -> emqx_types:deliver_results()).
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}} ->
Delivery = route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)),
Delivery#delivery.results;
{stop, _} ->
emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg)]),
[]
end.
%% Called internally
-spec(safe_publish(emqx_types:message()) -> ok).
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.
delivery(Msg) ->
#delivery{sender = self(), message = Msg, results = []}.
%%------------------------------------------------------------------------------
%% Route
%%------------------------------------------------------------------------------
route([], Delivery = #delivery{message = Msg}) ->
emqx_hooks:run('message.dropped', [#{node => node()}, Msg]),
inc_dropped_cnt(Msg#message.topic), Delivery;
route([{To, Node}], Delivery) when Node =:= node() ->
dispatch(To, Delivery);
route([{To, Node}], Delivery = #delivery{results = Results}) when is_atom(Node) ->
forward(Node, To, Delivery#delivery{results = [{route, Node, To}|Results]});
route([{To, Group}], Delivery) when is_tuple(Group); is_binary(Group) ->
emqx_shared_sub:dispatch(Group, To, Delivery);
route(Routes, Delivery) ->
lists:foldl(fun(Route, Acc) -> route([Route], Acc) end, Delivery, Routes).
aggre([]) ->
[];
aggre([#route{topic = To, dest = Node}]) when is_atom(Node) ->
[{To, Node}];
aggre([#route{topic = To, dest = {Group, _Node}}]) ->
[{To, Group}];
aggre(Routes) ->
lists:foldl(
fun(#route{topic = To, dest = Node}, Acc) when is_atom(Node) ->
[{To, Node} | Acc];
(#route{topic = To, dest = {Group, _Node}}, Acc) ->
lists:usort([{To, Group} | Acc])
end, [], Routes).
%% @doc Forward message to another node.
forward(Node, To, Delivery) ->
%% rpc:call to ensure the delivery, but the latency:(
case emqx_rpc:call(Node, ?BROKER, dispatch, [To, Delivery]) of
{badrpc, Reason} ->
emqx_logger:error("[Broker] Failed to forward msg to ~s: ~p", [Node, Reason]),
Delivery;
Delivery1 -> Delivery1
end.
-spec(dispatch(emqx_topic:topic(), emqx_types:delivery()) -> emqx_types:delivery()).
dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) ->
case subscribers(Topic) of
[] ->
emqx_hooks:run('message.dropped', [#{node => node()}, Msg]),
inc_dropped_cnt(Topic),
Delivery;
[Sub] -> %% optimize?
Cnt = dispatch(Sub, Topic, Msg),
Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]};
Subs ->
Cnt = lists:foldl(
fun(Sub, Acc) ->
dispatch(Sub, Topic, Msg) + Acc
end, 0, Subs),
Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]}
end.
dispatch(SubPid, Topic, Msg) when is_pid(SubPid) ->
case erlang:is_process_alive(SubPid) of
true ->
SubPid ! {dispatch, Topic, Msg},
1;
false -> 0
end;
dispatch({shard, I}, Topic, Msg) ->
lists:foldl(
fun(SubPid, Cnt) ->
dispatch(SubPid, Topic, Msg) + Cnt
end, 0, subscribers({shard, Topic, I})).
inc_dropped_cnt(<<"$SYS/", _/binary>>) ->
ok;
inc_dropped_cnt(_Topic) ->
emqx_metrics:inc('messages/dropped').
-spec(subscribers(emqx_topic:topic()) -> [pid()]).
subscribers(Topic) when is_binary(Topic) ->
lookup_value(?SUBSCRIBER, Topic, []);
subscribers(Shard = {shard, _Topic, _I}) ->
lookup_value(?SUBSCRIBER, Shard, []).
%%------------------------------------------------------------------------------
%% Subscriber is down
%%------------------------------------------------------------------------------
-spec(subscriber_down(pid()) -> true).
subscriber_down(SubPid) ->
lists:foreach(
fun(Topic) ->
case lookup_value(?SUBOPTION, {SubPid, Topic}) of
SubOpts when is_map(SubOpts) ->
_ = emqx_broker_helper:reclaim_seq(Topic),
true = ets:delete(?SUBOPTION, {SubPid, Topic}),
case maps:get(shard, SubOpts, 0) of
0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}),
ok = cast(pick(Topic), {unsubscribed, Topic});
I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}),
ok = cast(pick({Topic, I}), {unsubscribed, Topic, I})
end;
undefined -> ok
end
end, lookup_value(?SUBSCRIPTION, SubPid, [])),
ets:delete(?SUBSCRIPTION, SubPid).
%%------------------------------------------------------------------------------
%% Management APIs
%%------------------------------------------------------------------------------
-spec(subscriptions(pid() | emqx_types:subid())
-> [{emqx_topic:topic(), emqx_types:subopts()}]).
subscriptions(SubPid) when is_pid(SubPid) ->
[{Topic, lookup_value(?SUBOPTION, {SubPid, Topic}, #{})}
|| Topic <- lookup_value(?SUBSCRIPTION, SubPid, [])];
subscriptions(SubId) ->
case emqx_broker_helper:lookup_subpid(SubId) of
SubPid when is_pid(SubPid) ->
subscriptions(SubPid);
undefined -> []
end.
-spec(subscribed(pid(), emqx_topic:topic()) -> boolean()).
subscribed(SubPid, Topic) when is_pid(SubPid) ->
ets:member(?SUBOPTION, {SubPid, Topic});
subscribed(SubId, Topic) when ?is_subid(SubId) ->
SubPid = emqx_broker_helper:lookup_subpid(SubId),
ets:member(?SUBOPTION, {SubPid, Topic}).
-spec(get_subopts(pid(), emqx_topic:topic()) -> emqx_types:subopts() | undefined).
get_subopts(SubPid, Topic) when is_pid(SubPid), is_binary(Topic) ->
lookup_value(?SUBOPTION, {SubPid, Topic});
get_subopts(SubId, Topic) when ?is_subid(SubId) ->
case emqx_broker_helper:lookup_subpid(SubId) of
SubPid when is_pid(SubPid) ->
get_subopts(SubPid, Topic);
undefined -> undefined
end.
-spec(set_subopts(emqx_topic:topic(), emqx_types:subopts()) -> boolean()).
set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) ->
Sub = {self(), Topic},
case ets:lookup(?SUBOPTION, Sub) of
[{_, OldOpts}] ->
ets:insert(?SUBOPTION, {Sub, maps:merge(OldOpts, NewOpts)});
[] -> false
end.
-spec(topics() -> [emqx_topic:topic()]).
topics() ->
emqx_router:topics().
%%------------------------------------------------------------------------------
%% Stats fun
%%------------------------------------------------------------------------------
stats_fun() ->
safe_update_stats(?SUBSCRIBER, 'subscribers/count', 'subscribers/max'),
safe_update_stats(?SUBSCRIPTION, 'subscriptions/count', 'subscriptions/max'),
safe_update_stats(?SUBOPTION, 'suboptions/count', 'suboptions/max').
safe_update_stats(Tab, Stat, MaxStat) ->
case ets:info(Tab, size) of
undefined -> ok;
Size -> emqx_stats:setstat(Stat, MaxStat, Size)
end.
%%------------------------------------------------------------------------------
%% call, cast, pick
%%------------------------------------------------------------------------------
call(Broker, Req) ->
gen_server:call(Broker, Req).
cast(Broker, Msg) ->
gen_server:cast(Broker, Msg).
%% Pick a broker
pick(Topic) ->
gproc_pool:pick_worker(broker_pool, Topic).
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([Pool, Id]) ->
true = gproc_pool:connect_worker(Pool, {Pool, Id}),
{ok, #{pool => Pool, id => Id}}.
handle_call({subscribe, Topic}, _From, State) ->
Ok = emqx_router:do_add_route(Topic),
{reply, Ok, State};
handle_call({subscribe, Topic, I}, _From, State) ->
Ok = case get(Shard = {Topic, I}) of
undefined ->
_ = put(Shard, true),
true = ets:insert(?SUBSCRIBER, {Topic, {shard, I}}),
cast(pick(Topic), {subscribe, Topic});
true -> ok
end,
{reply, Ok, State};
handle_call(Req, _From, State) ->
emqx_logger:error("[Broker] unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast({subscribe, Topic}, State) ->
case emqx_router:do_add_route(Topic) of
ok -> ok;
{error, Reason} ->
emqx_logger:error("[Broker] Failed to add route: ~p", [Reason])
end,
{noreply, State};
handle_cast({unsubscribed, Topic}, State) ->
case ets:member(?SUBSCRIBER, Topic) of
false ->
_ = emqx_router:do_delete_route(Topic);
true -> ok
end,
{noreply, State};
handle_cast({unsubscribed, Topic, I}, State) ->
case ets:member(?SUBSCRIBER, {shard, Topic, I}) of
false ->
_ = erase({Topic, I}),
true = ets:delete_object(?SUBSCRIBER, {Topic, {shard, I}}),
cast(pick(Topic), {unsubscribed, Topic});
true -> ok
end,
{noreply, State};
handle_cast(Msg, State) ->
emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
emqx_logger:error("[Broker] unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #{pool := Pool, id := Id}) ->
gproc_pool:disconnect_worker(Pool, {Pool, Id}).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------

142
src/emqx_broker_helper.erl Normal file
View File

@ -0,0 +1,142 @@
%% 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_broker_helper).
-behaviour(gen_server).
-export([start_link/0]).
-export([register_sub/2]).
-export([lookup_subid/1, lookup_subpid/1]).
-export([get_sub_shard/2]).
-export([create_seq/1, reclaim_seq/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-define(HELPER, ?MODULE).
-define(SUBID, emqx_subid).
-define(SUBMON, emqx_submon).
-define(SUBSEQ, emqx_subseq).
-define(SHARD, 1024).
-define(BATCH_SIZE, 100000).
-spec(start_link() -> emqx_types:startlink_ret()).
start_link() ->
gen_server:start_link({local, ?HELPER}, ?MODULE, [], []).
-spec(register_sub(pid(), emqx_types:subid()) -> ok).
register_sub(SubPid, SubId) when is_pid(SubPid) ->
case ets:lookup(?SUBMON, SubPid) of
[] ->
gen_server:cast(?HELPER, {register_sub, SubPid, SubId});
[{_, SubId}] ->
ok;
_Other ->
error(subid_conflict)
end.
-spec(lookup_subid(pid()) -> emqx_types:subid() | undefined).
lookup_subid(SubPid) when is_pid(SubPid) ->
emqx_tables:lookup_value(?SUBMON, SubPid).
-spec(lookup_subpid(emqx_types:subid()) -> pid()).
lookup_subpid(SubId) ->
emqx_tables:lookup_value(?SUBID, SubId).
-spec(get_sub_shard(pid(), emqx_topic:topic()) -> non_neg_integer()).
get_sub_shard(SubPid, Topic) ->
case create_seq(Topic) of
Seq when Seq =< ?SHARD -> 0;
_ -> erlang:phash2(SubPid, shards_num()) + 1
end.
-spec(shards_num() -> pos_integer()).
shards_num() ->
%% Dynamic sharding later...
ets:lookup_element(?HELPER, shards, 2).
-spec(create_seq(emqx_topic:topic()) -> emqx_sequence:seqid()).
create_seq(Topic) ->
emqx_sequence:nextval(?SUBSEQ, Topic).
-spec(reclaim_seq(emqx_topic:topic()) -> emqx_sequence:seqid()).
reclaim_seq(Topic) ->
emqx_sequence:reclaim(?SUBSEQ, Topic).
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([]) ->
%% Helper table
ok = emqx_tables:new(?HELPER, [{read_concurrency, true}]),
%% Shards: CPU * 32
true = ets:insert(?HELPER, {shards, emqx_vm:schedulers() * 32}),
%% SubSeq: Topic -> SeqId
ok = emqx_sequence:create(?SUBSEQ),
%% SubId: SubId -> SubPid
ok = emqx_tables:new(?SUBID, [public, {read_concurrency, true}, {write_concurrency, true}]),
%% SubMon: SubPid -> SubId
ok = emqx_tables:new(?SUBMON, [public, {read_concurrency, true}, {write_concurrency, true}]),
%% Stats timer
ok = emqx_stats:update_interval(broker_stats, fun emqx_broker:stats_fun/0),
{ok, #{pmon => emqx_pmon:new()}}.
handle_call(Req, _From, State) ->
emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast({register_sub, SubPid, SubId}, State = #{pmon := PMon}) ->
true = (SubId =:= undefined) orelse ets:insert(?SUBID, {SubId, SubPid}),
true = ets:insert(?SUBMON, {SubPid, SubId}),
{noreply, State#{pmon := emqx_pmon:monitor(SubPid, PMon)}};
handle_cast(Msg, State) ->
emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #{pmon := PMon}) ->
SubPids = [SubPid | emqx_misc:drain_down(?BATCH_SIZE)],
ok = emqx_pool:async_submit(
fun lists:foreach/2, [fun clean_down/1, SubPids]),
{_, PMon1} = emqx_pmon:erase_all(SubPids, PMon),
{noreply, State#{pmon := PMon1}};
handle_info(Info, State) ->
emqx_logger:error("[BrokerHelper] unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
true = emqx_sequence:delete(?SUBSEQ),
emqx_stats:cancel_update(broker_stats).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
clean_down(SubPid) ->
case ets:lookup(?SUBMON, SubPid) of
[{_, SubId}] ->
true = ets:delete(?SUBMON, SubPid),
true = (SubId =:= undefined)
orelse ets:delete_object(?SUBID, {SubId, SubPid}),
emqx_broker:subscriber_down(SubPid);
[] -> ok
end.

53
src/emqx_broker_sup.erl Normal file
View File

@ -0,0 +1,53 @@
%% 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_broker_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%%------------------------------------------------------------------------------
%% Supervisor callbacks
%%------------------------------------------------------------------------------
init([]) ->
%% Broker pool
PoolSize = emqx_vm:schedulers() * 2,
BrokerPool = emqx_pool_sup:spec([broker_pool, hash, PoolSize,
{emqx_broker, start_link, []}]),
%% Shared subscription
SharedSub = #{id => shared_sub,
start => {emqx_shared_sub, start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => [emqx_shared_sub]},
%% Broker helper
Helper = #{id => helper,
start => {emqx_broker_helper, start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => [emqx_broker_helper]},
{ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}.

33
src/emqx_cli.erl Normal file
View File

@ -0,0 +1,33 @@
%% 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_cli).
-export([print/1, print/2, usage/1, usage/2]).
print(Msg) ->
io:format(Msg), lists:flatten(io_lib:format("~p", [Msg])).
print(Format, Args) ->
io:format(Format, Args), lists:flatten(io_lib:format(Format, Args)).
usage(CmdList) ->
lists:map(
fun({Cmd, Descr}) ->
io:format("~-48s# ~s~n", [Cmd, Descr]),
lists:flatten(io_lib:format("~-48s# ~s~n", [Cmd, Descr]))
end, CmdList).
usage(Format, Args) ->
usage([{Format, Args}]).

Some files were not shown because too many files have changed in this diff Show More