Merge branch 'emqx30'
This commit is contained in:
commit
ff9fccdb07
|
|
@ -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
|
||||||
|
|
@ -17,17 +17,24 @@ log/
|
||||||
*.so
|
*.so
|
||||||
.erlang.mk/
|
.erlang.mk/
|
||||||
cover/
|
cover/
|
||||||
emqttd.d
|
emqx.d
|
||||||
eunit.coverdata
|
eunit.coverdata
|
||||||
test/ct.cover.spec
|
test/ct.cover.spec
|
||||||
logs
|
logs
|
||||||
ct.coverdata
|
ct.coverdata
|
||||||
.idea/
|
.idea/
|
||||||
emqttd.iml
|
emqx.iml
|
||||||
_rel/
|
_rel/
|
||||||
data/
|
data/
|
||||||
_build
|
_build
|
||||||
.rebar3
|
.rebar3
|
||||||
rebar3.crashdump
|
rebar3.crashdump
|
||||||
.DS_Store
|
.DS_Store
|
||||||
rebar.config
|
emqx.iml
|
||||||
|
bbmustache/
|
||||||
|
etc/gen.emqx.conf
|
||||||
|
compile_commands.json
|
||||||
|
cuttlefish
|
||||||
|
rebar.lock
|
||||||
|
xrefr
|
||||||
|
erlang.mk
|
||||||
|
|
|
||||||
15
.travis.yml
15
.travis.yml
|
|
@ -1,9 +1,20 @@
|
||||||
language: erlang
|
language: erlang
|
||||||
|
|
||||||
otp_release:
|
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:
|
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
|
sudo: false
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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
149
Makefile
|
|
@ -1,57 +1,136 @@
|
||||||
PROJECT = emqttd
|
.PHONY: plugins tests
|
||||||
PROJECT_DESCRIPTION = Erlang MQTT Broker
|
|
||||||
PROJECT_VERSION = 2.3.11
|
|
||||||
|
|
||||||
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
|
DEPS = jsx gproc gen_rpc ekka esockd cowboy
|
||||||
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
|
|
||||||
|
|
||||||
ERLC_OPTS += +debug_info
|
dep_jsx = hex-emqx 2.9.0
|
||||||
ERLC_OPTS += +'{parse_transform, lager_transform}'
|
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
|
NO_AUTOPATCH = cuttlefish
|
||||||
|
|
||||||
|
ERLC_OPTS += +debug_info -DAPPLICATION=emqx
|
||||||
|
|
||||||
BUILD_DEPS = cuttlefish
|
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
|
#TEST_DEPS = emqx_ct_helplers
|
||||||
dep_emqttc = git https://github.com/emqtt/emqttc
|
#dep_emqx_ct_helplers = git git@github.com:emqx/emqx-ct-helpers
|
||||||
dep_emq_dashboard = git https://github.com/emqtt/emq_dashboard develop
|
|
||||||
|
|
||||||
TEST_ERLC_OPTS += +debug_info
|
TEST_ERLC_OPTS += +debug_info -DAPPLICATION=emqx
|
||||||
TEST_ERLC_OPTS += +'{parse_transform, lager_transform}'
|
|
||||||
|
|
||||||
EUNIT_OPTS = verbose
|
EUNIT_OPTS = verbose
|
||||||
# EUNIT_ERL_OPTS =
|
|
||||||
|
|
||||||
CT_SUITES = emqttd emqttd_access emqttd_lib emqttd_inflight emqttd_mod \
|
# CT_SUITES = emqx_frame
|
||||||
emqttd_net emqttd_mqueue emqttd_protocol emqttd_topic \
|
## emqx_trie emqx_router emqx_frame emqx_mqtt_compat
|
||||||
emqttd_router emqttd_trie emqttd_vm emqttd_config
|
|
||||||
|
|
||||||
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
|
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_DIRS := ebin/
|
||||||
DIALYZER_OPTS := --verbose --statistics -Werror_handling \
|
DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns
|
||||||
-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
|
include erlang.mk
|
||||||
|
|
||||||
app:: rebar.config
|
clean:: gen-clean
|
||||||
|
|
||||||
app.config::
|
.PHONY: gen-clean
|
||||||
./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/emq.conf -i priv/emq.schema -d data/
|
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
146
README.md
|
|
@ -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.
|
||||||
|
|
||||||
[](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
|
## 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
|
## 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
|
# Check Status
|
||||||
-----------------------------------------------------------------------|--------------------------------------
|
./bin/emqx_ctl status
|
||||||
[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
|
|
||||||
|
|
||||||
## Supports
|
# Stop emqx
|
||||||
|
./bin/emqx stop
|
||||||
|
|
||||||
* Twitter: [@emqtt](https://twitter.com/emqtt)
|
To view the dashboard after running, use your browser to open: http://localhost:18083
|
||||||
* 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
|
|
||||||
|
|
||||||
## Test Servers
|
|
||||||
|
|
||||||
The **q.emqtt.com** hosts a public Four-Node *EMQ* cluster on [QingCloud](https://qingcloud.com):
|
## Roadmap
|
||||||
|
|
||||||

|
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
|
## 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.
11
doc/README
11
doc/README
|
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
http://emqttd.io/docs/v2/
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
http://docs.emqtt.com/
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
http://emqttd-docs.rtfd.org
|
|
||||||
|
|
||||||
Binary file not shown.
|
|
@ -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() |
|
%% -type who() :: all | binary() |
|
||||||
%% {ipaddr, esockd_access:cidr()} |
|
%% {ipaddr, esockd_access:cidr()} |
|
||||||
|
|
@ -24,3 +24,4 @@
|
||||||
|
|
||||||
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
|
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
|
||||||
|
|
||||||
|
{allow, all}.
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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{}).
|
|
||||||
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -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}}).
|
|
||||||
|
|
||||||
|
|
@ -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()
|
|
||||||
}).
|
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
1509
priv/emq.schema
1509
priv/emq.schema
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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]}.
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
36
rebar.lock
36
rebar.lock
|
|
@ -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}].
|
|
||||||
|
|
@ -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"}]}]}.
|
|
||||||
181
src/emqttd.erl
181
src/emqttd.erl
|
|
@ -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()]).
|
|
||||||
|
|
||||||
|
|
@ -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}.
|
|
||||||
|
|
||||||
|
|
@ -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]).
|
|
||||||
|
|
||||||
|
|
@ -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".
|
|
||||||
|
|
||||||
|
|
@ -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">>).
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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])).
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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)].
|
|
||||||
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
||||||
|
|
@ -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,"].
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
||||||
|
|
@ -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}">>]}].
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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)).
|
|
||||||
|
|
||||||
|
|
@ -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()).
|
|
||||||
|
|
@ -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].
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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)]}.
|
|
||||||
|
|
||||||
|
|
@ -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().
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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}.
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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}.
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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}.
|
|
||||||
|
|
||||||
|
|
@ -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 Client’s subscriptions.
|
|
||||||
%%
|
|
||||||
%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not
|
|
||||||
%% been completely acknowledged.
|
|
||||||
%%
|
|
||||||
%% QoS 1 and QoS 2 messages pending transmission to the Client.
|
|
||||||
%%
|
|
||||||
%% QoS 2 messages which have been received from the Client, but have not
|
|
||||||
%% been completely acknowledged.
|
|
||||||
%%
|
|
||||||
%% Optionally, QoS 0 messages pending transmission to the Client.
|
|
||||||
%%
|
|
||||||
%% If the session is currently disconnected, the time at which the Session state
|
|
||||||
%% will be deleted.
|
|
||||||
%%
|
|
||||||
%% @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(),
|
|
||||||
|
|
||||||
%% Client’s 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).
|
|
||||||
|
|
@ -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]}]}}.
|
|
||||||
|
|
@ -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)}.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -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]}}.
|
|
||||||
|
|
||||||
|
|
@ -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)).
|
|
||||||
|
|
||||||
|
|
@ -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}, []}}.
|
|
||||||
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
||||||
|
|
@ -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}.
|
|
||||||
|
|
||||||
|
|
@ -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)}.
|
|
||||||
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
||||||
|
|
@ -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]}]}}.
|
|
||||||
|
|
||||||
|
|
@ -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"}]}]}.
|
||||||
|
|
@ -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]).
|
||||||
|
|
||||||
|
|
@ -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}.
|
||||||
|
|
||||||
|
|
@ -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]).
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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".
|
||||||
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
%%--------------------------------------------------------------------
|
%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
|
|
||||||
%%
|
%%
|
||||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
%% you may not use this file except in compliance with 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.
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
%% See the License for the specific language governing permissions and
|
%% See the License for the specific language governing permissions and
|
||||||
%% limitations under the License.
|
%% limitations under the License.
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
-module(emqttd_acl_mod).
|
-module(emqx_acl_mod).
|
||||||
|
|
||||||
-author("Feng Lee <feng@emqtt.io>").
|
-include("emqx.hrl").
|
||||||
|
|
||||||
-include("emqttd.hrl").
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% ACL behavihour
|
%% ACL behavihour
|
||||||
|
|
@ -26,13 +22,12 @@
|
||||||
|
|
||||||
-ifdef(use_specs).
|
-ifdef(use_specs).
|
||||||
|
|
||||||
-callback(init(AclOpts :: list()) -> {ok, State :: any()}).
|
-callback(init(AclOpts :: list()) -> {ok, State :: term()}).
|
||||||
|
|
||||||
-callback(check_acl({Client :: mqtt_client(),
|
-callback(check_acl({credentials(), pubsub(), topic()}, State :: term())
|
||||||
PubSub :: pubsub(),
|
-> allow | deny | ignore).
|
||||||
Topic :: binary()}, State :: any()) -> allow | deny | ignore).
|
|
||||||
|
|
||||||
-callback(reload_acl(State :: any()) -> ok | {error, term()}).
|
-callback(reload_acl(State :: term()) -> ok | {error, term()}).
|
||||||
|
|
||||||
-callback(description() -> string()).
|
-callback(description() -> string()).
|
||||||
|
|
||||||
|
|
@ -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">>).
|
||||||
|
|
||||||
|
|
@ -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).
|
||||||
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
%%--------------------------------------------------------------------
|
%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
%% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
|
|
||||||
%%
|
%%
|
||||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
%% you may not use this file except in compliance with 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.
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
%% See the License for the specific language governing permissions and
|
%% See the License for the specific language governing permissions and
|
||||||
%% limitations under the License.
|
%% 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() ->
|
-export([behaviour_info/1]).
|
||||||
[{group, subscriptions}].
|
|
||||||
|
|
||||||
groups() ->
|
behaviour_info(callbacks) ->
|
||||||
[{subscriptions, [sequence],
|
[{init, 1}, {check, 3}, {description, 0}];
|
||||||
[t_subsciptions_list,
|
behaviour_info(_Other) ->
|
||||||
t_subsciptions_show,
|
undefined.
|
||||||
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.
|
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
@ -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).
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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).
|
||||||
|
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -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]}.
|
||||||
|
|
@ -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
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
@ -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]}}.
|
||||||
|
|
||||||
|
|
@ -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}]).
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue