feat(gateway): The prototype for emqx-gateway application
* feat(gateway): add gateway application * chore(gateway): add normalize confs function * refactor: move emqx-stomp to emqx-gateway subdir * chore(gateway): fix some bad function defination * chore(gateway): rename type to gwid * chore(gw-stomp): upgrade the implementation to suppport gateway instance * feat(gw-stomp): add reconnect mechanism * refactor(stomp): upgrade connection&channel module to latest * refactor(stomp): more details for handle_in/out * refactor(stomp): get it up and running * chore(gw): load some modules by default * refactor: upgrade the emqx-gateway schema module * test(stomp): fix testcases for stomp gateway * chore(gw): remove needless lines * chore(gateway): correct a lot of specs * chore(gw): add a draft for metrics * chore(gw): add metrics process * fix(gw): fix cm process monitor * test(gw): add test cases for gateway-regitry * feat(gw): add metrics/cli for gateway * fix(gw): fix xref errors * chore(gw): pretty gateway metrics print format * chore(gw-stomp): generate clientid by default * chore(gw): more reliable * chore(gw): rename gwid -> type * chore(gw): impl the update logic * chore(gw): some format improvement * chore(gw): adapts the hocon configs * fix(gw): fix xref errors * test(gw): update configurations for tests * chore(gw): ignore diaylzer warnings * fix(gw): fix bad function call * chore(gw): remove needless comments
This commit is contained in:
parent
4313e37994
commit
56cdd469ff
|
@ -71,6 +71,7 @@ includes() ->
|
||||||
, "emqx_bridge_mqtt"
|
, "emqx_bridge_mqtt"
|
||||||
, "emqx_modules"
|
, "emqx_modules"
|
||||||
, "emqx_management"
|
, "emqx_management"
|
||||||
|
, "emqx_gateway"
|
||||||
].
|
].
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ init_hooks_cnter() ->
|
||||||
try
|
try
|
||||||
_ = ets:new(?CNTER, [named_table, public]), ok
|
_ = ets:new(?CNTER, [named_table, public]), ok
|
||||||
catch
|
catch
|
||||||
exit:badarg:_ ->
|
error:badarg:_ ->
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
.rebar3
|
||||||
|
_*
|
||||||
|
.eunit
|
||||||
|
*.o
|
||||||
|
*.beam
|
||||||
|
*.plt
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.erlang.cookie
|
||||||
|
ebin
|
||||||
|
log
|
||||||
|
erl_crash.dump
|
||||||
|
.rebar
|
||||||
|
logs
|
||||||
|
_build
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
rebar3.crashdump
|
||||||
|
*~
|
||||||
|
rebar.lock
|
|
@ -0,0 +1,191 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2021, JianBo He <heeejianbo@163.com>.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
## shallow clone for speed
|
||||||
|
|
||||||
|
REBAR_GIT_CLONE_OPTIONS += --depth 1
|
||||||
|
export REBAR_GIT_CLONE_OPTIONS
|
||||||
|
|
||||||
|
REBAR = rebar3
|
||||||
|
all: compile
|
||||||
|
|
||||||
|
compile:
|
||||||
|
$(REBAR) compile
|
||||||
|
|
||||||
|
clean: distclean
|
||||||
|
|
||||||
|
ct:
|
||||||
|
$(REBAR) as test ct -v
|
||||||
|
|
||||||
|
eunit:
|
||||||
|
$(REBAR) as test eunit
|
||||||
|
|
||||||
|
xref:
|
||||||
|
$(REBAR) xref
|
||||||
|
|
||||||
|
cover:
|
||||||
|
$(REBAR) cover
|
||||||
|
|
||||||
|
distclean:
|
||||||
|
@rm -rf _build
|
||||||
|
@rm -f data/app.*.config data/vm.*.args rebar.lock
|
|
@ -0,0 +1,309 @@
|
||||||
|
# emqx_gateway
|
||||||
|
|
||||||
|
***This is a very early prototype application*** for Gateway in EMQ X Broker 5.0
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
EMQ X Gateway Managment
|
||||||
|
- Gateway-Registry (or Gateway Type)
|
||||||
|
- *Load
|
||||||
|
- *UnLoad
|
||||||
|
- *List
|
||||||
|
|
||||||
|
- Gateway
|
||||||
|
- *Create
|
||||||
|
- *Delete
|
||||||
|
- *Update
|
||||||
|
- *Stop-And-Start
|
||||||
|
- *Hot-Upgrade
|
||||||
|
- *Satrt/Enable
|
||||||
|
- *Stop/Disable
|
||||||
|
- Listener
|
||||||
|
|
||||||
|
## ROADMAP
|
||||||
|
|
||||||
|
Gateway v0.1: Management support
|
||||||
|
|
||||||
|
Gateway v0.2: Conn/Frame/Protocol Template
|
||||||
|
|
||||||
|
### Compatible with EMQ X
|
||||||
|
|
||||||
|
> Why we need to compatible
|
||||||
|
|
||||||
|
1. Authentication
|
||||||
|
2. Hooks/Event system
|
||||||
|
3. Messages Mode & Rule Engine
|
||||||
|
4. Cluster registration
|
||||||
|
5. Metrics & Statistic
|
||||||
|
|
||||||
|
> How to do it
|
||||||
|
|
||||||
|
>
|
||||||
|
|
||||||
|
### User Interface
|
||||||
|
|
||||||
|
#### Configurations
|
||||||
|
|
||||||
|
```hocon
|
||||||
|
gateway {
|
||||||
|
|
||||||
|
## ... some confs for top scope
|
||||||
|
..
|
||||||
|
## End.
|
||||||
|
|
||||||
|
## Gateway Instances
|
||||||
|
|
||||||
|
lwm2m[.name] {
|
||||||
|
|
||||||
|
## variable support
|
||||||
|
mountpoint: lwm2m/%e/
|
||||||
|
|
||||||
|
lifetime_min: 1s
|
||||||
|
lifetime_max: 86400s
|
||||||
|
#qmode_time_window: 22
|
||||||
|
#auto_observe: off
|
||||||
|
|
||||||
|
#update_msg_publish_condition: contains_object_list
|
||||||
|
|
||||||
|
xml_dir: {{ platform_etc_dir }}/lwm2m_xml
|
||||||
|
|
||||||
|
clientinfo_override: {
|
||||||
|
username: ${register.opts.uname}
|
||||||
|
password: ${register.opts.passwd}
|
||||||
|
clientid: ${epn}
|
||||||
|
}
|
||||||
|
|
||||||
|
#authenticator: allow_anonymous
|
||||||
|
authenticator: [
|
||||||
|
{
|
||||||
|
type: auth-http
|
||||||
|
method: post
|
||||||
|
//?? how to generate clientinfo ??
|
||||||
|
params: $client.credential
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
translator: {
|
||||||
|
downlink: "dn/#"
|
||||||
|
uplink: {
|
||||||
|
notify: "up/notify"
|
||||||
|
response: "up/resp"
|
||||||
|
register: "up/resp"
|
||||||
|
update: "up/reps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
%% ?? listener.$type.name ??
|
||||||
|
listener.udp[.name] {
|
||||||
|
listen_on: 0.0.0.0:5683
|
||||||
|
max_connections: 1024000
|
||||||
|
max_conn_rate: 1000
|
||||||
|
## ?? udp keepalive in socket level ???
|
||||||
|
#keepalive:
|
||||||
|
## ?? udp proxy-protocol in socket level ???
|
||||||
|
#proxy_protocol: on
|
||||||
|
#proxy_timeout: 30s
|
||||||
|
recbuf: 2KB
|
||||||
|
sndbuf: 2KB
|
||||||
|
buffer: 2KB
|
||||||
|
tune_buffer: off
|
||||||
|
#access: allow all
|
||||||
|
read_packets: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.dtls[.name] {
|
||||||
|
listen_on: 0.0.0.0:5684
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## The CoAP Gateway
|
||||||
|
coap[.name] {
|
||||||
|
|
||||||
|
#enable_stats: on
|
||||||
|
|
||||||
|
authenticator: [
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
listener.udp[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.dtls[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## The Stomp Gateway
|
||||||
|
stomp[.name] {
|
||||||
|
|
||||||
|
allow_anonymous: true
|
||||||
|
|
||||||
|
default_user.login: guest
|
||||||
|
default_user.passcode: guest
|
||||||
|
|
||||||
|
frame.max_headers: 10
|
||||||
|
frame.max_header_length: 1024
|
||||||
|
frame.max_body_length: 8192
|
||||||
|
|
||||||
|
listener.tcp[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.ssl[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exproto[.name] {
|
||||||
|
|
||||||
|
proto_name: DL-648
|
||||||
|
|
||||||
|
authenticators: [...]
|
||||||
|
|
||||||
|
adapter: {
|
||||||
|
type: grpc
|
||||||
|
options: {
|
||||||
|
listen_on: 9100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler: {
|
||||||
|
type: grpc
|
||||||
|
options: {
|
||||||
|
url: <http://127.0.0.1:9001>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.tcp[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## ============================ Enterpise gateways
|
||||||
|
|
||||||
|
## The JT/T 808 Gateway
|
||||||
|
jtt808[.name] {
|
||||||
|
|
||||||
|
idle_timeout: 30s
|
||||||
|
enable_stats: on
|
||||||
|
max_packet_size: 8192
|
||||||
|
|
||||||
|
clientinfo_override: {
|
||||||
|
clientid: $phone
|
||||||
|
username: xxx
|
||||||
|
password: xxx
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticator: [
|
||||||
|
{
|
||||||
|
type: auth-http
|
||||||
|
method: post
|
||||||
|
params: $clientinfo.credential
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
translator: {
|
||||||
|
subscribe: [jt808/%c/dn]
|
||||||
|
publish: [jt808/%c/up]
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.tcp[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.ssl[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gbt32960[.name] {
|
||||||
|
|
||||||
|
frame.max_length: 8192
|
||||||
|
retx_interval: 8s
|
||||||
|
retx_max_times: 3
|
||||||
|
message_queue_len: 10
|
||||||
|
|
||||||
|
authenticators: [...]
|
||||||
|
|
||||||
|
translator: {
|
||||||
|
## upstream
|
||||||
|
login: gbt32960/${vin}/upstream/vlogin
|
||||||
|
logout: gbt32960/${vin}/upstream/vlogout
|
||||||
|
informing: gbt32960/${vin}/upstream/info
|
||||||
|
reinforming: gbt32960/${vin}/upstream/reinfo
|
||||||
|
## downstream
|
||||||
|
downstream: gbt32960/${vin}/dnstream
|
||||||
|
response: gbt32960/${vin}/upstream/response
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.tcp[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.ssl[.name] {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
privtcp[.name] {
|
||||||
|
|
||||||
|
max_packet_size: 65535
|
||||||
|
idle_timeout: 15s
|
||||||
|
|
||||||
|
enable_stats: on
|
||||||
|
|
||||||
|
force_gc_policy: 1000|1MB
|
||||||
|
force_shutdown_policy: 8000|800MB
|
||||||
|
|
||||||
|
translator: {
|
||||||
|
up_topic: tcp/%c/up
|
||||||
|
dn_topic: tcp/%c/dn
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.tcp[.name]: {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CLI
|
||||||
|
|
||||||
|
##### Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## List all started gateway and gateway-instance
|
||||||
|
emqx_ctl gateway list
|
||||||
|
emqx_ctl gateway lookup <GatewayId>
|
||||||
|
emqx_ctl gateway stop <GatewayId>
|
||||||
|
emqx_ctl gateway start <GatewayId>
|
||||||
|
|
||||||
|
emqx_ctl gateway-registry re-searching
|
||||||
|
emqx_ctl gateway-registry list
|
||||||
|
|
||||||
|
emqx_ctl gateway-clients list <Type>
|
||||||
|
emqx_ctl gateway-clients show <Type> <ClientId>
|
||||||
|
emqx_ctl gateway-clients kick <Type> <ClientId>
|
||||||
|
|
||||||
|
## Banned ??
|
||||||
|
emqx_ctl gateway-banned
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
emqx_ctl gateway-metrics [<GatewayId>]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mangement by HTTP-API/Dashboard/
|
||||||
|
|
||||||
|
#### How to integrate a protocol to your platform
|
||||||
|
|
||||||
|
### Develop your protocol gateway
|
||||||
|
|
||||||
|
There are 3 way to create your protocol gateway for EMQ X 5.0:
|
||||||
|
|
||||||
|
1. Use Erlang to create a new emqx plugin to handle all of protocol packets (same as v5.0 before)
|
||||||
|
|
||||||
|
2. Based on the emqx-gateway-impl-bhvr and emqx-gateway
|
||||||
|
|
||||||
|
3. Use the gRPC Gateway
|
|
@ -0,0 +1,30 @@
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## EMQ X Gateway configurations
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
|
||||||
|
## TODO:
|
||||||
|
|
||||||
|
emqx_gateway: {
|
||||||
|
stomp.1: {
|
||||||
|
frame: {
|
||||||
|
max_headers: 10
|
||||||
|
max_headers_length: 1024
|
||||||
|
max_body_length: 8192
|
||||||
|
}
|
||||||
|
|
||||||
|
clientinfo_override: {
|
||||||
|
username: "${Packet.headers.login}"
|
||||||
|
password: "${Packet.headers.passcode}"
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticator: allow_anonymous
|
||||||
|
|
||||||
|
listener.tcp.1: {
|
||||||
|
bind: 61613
|
||||||
|
acceptors: 16
|
||||||
|
max_connections: 1024000
|
||||||
|
max_conn_rate: 1000
|
||||||
|
active_n: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2017-2021 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(EMQX_GATEWAY_HRL).
|
||||||
|
-define(EMQX_GATEWAY_HRL, 1).
|
||||||
|
|
||||||
|
-type instance_id() :: atom().
|
||||||
|
-type gateway_type() :: atom().
|
||||||
|
|
||||||
|
%% @doc The Gateway Instace defination
|
||||||
|
-type instance() ::
|
||||||
|
#{ id := instance_id()
|
||||||
|
, type := gateway_type()
|
||||||
|
, name := binary()
|
||||||
|
, descr => binary() | undefined
|
||||||
|
%% Appears only in creating or detailed info
|
||||||
|
, rawconf => map()
|
||||||
|
%% Appears only in getting instance status/info
|
||||||
|
, status => stopped | running
|
||||||
|
}.
|
||||||
|
|
||||||
|
-endif.
|
|
@ -0,0 +1,7 @@
|
||||||
|
{erl_opts, [debug_info]}.
|
||||||
|
{deps, []}.
|
||||||
|
|
||||||
|
{shell, [
|
||||||
|
% {config, "config/sys.config"},
|
||||||
|
{apps, [emqx_gateway]}
|
||||||
|
]}.
|
|
@ -0,0 +1,22 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The behavior abstrat for TCP based gateway conn
|
||||||
|
%%
|
||||||
|
-module(emqx_gateway_conn).
|
||||||
|
|
||||||
|
%% TODO: Gateway v0.2
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 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_gateway_impl).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
-type state() :: map().
|
||||||
|
-type reason() :: any().
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
-callback init(Options :: list()) -> {error, reason()} | {ok, GwState :: state()}.
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
-callback on_insta_create(Insta :: instance(),
|
||||||
|
Ctx :: emqx_gateway_ctx:context(),
|
||||||
|
GwState :: state()
|
||||||
|
)
|
||||||
|
-> {error, reason()}
|
||||||
|
| {ok, [GwInstaPid :: pid()], GwInstaState :: state()}
|
||||||
|
| {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()}.
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
-callback on_insta_update(NewInsta :: instance(),
|
||||||
|
OldInsta :: instance(),
|
||||||
|
GwInstaState :: state(),
|
||||||
|
GwState :: state())
|
||||||
|
-> ok
|
||||||
|
| {ok, [GwInstaPid :: pid()], GwInstaState :: state()}
|
||||||
|
| {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()}
|
||||||
|
| {error, reason()}.
|
||||||
|
|
||||||
|
%% @doc
|
||||||
|
-callback on_insta_destroy(Insta :: instance(),
|
||||||
|
GwInstaState :: state(),
|
||||||
|
GwState :: state()) -> ok.
|
|
@ -0,0 +1,11 @@
|
||||||
|
{application, emqx_gateway,
|
||||||
|
[{description, "The Gateway management application"},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{registered, []},
|
||||||
|
{mod, {emqx_gateway_app, []}},
|
||||||
|
{applications, [kernel, stdlib]},
|
||||||
|
{env, []},
|
||||||
|
{modules, []},
|
||||||
|
{licenses, ["Apache 2.0"]},
|
||||||
|
{links, []}
|
||||||
|
]}.
|
|
@ -0,0 +1,84 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 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_gateway).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
%% APIs
|
||||||
|
-export([ registered_gateway/0
|
||||||
|
, create/4
|
||||||
|
, remove/1
|
||||||
|
, lookup/1
|
||||||
|
, update/1
|
||||||
|
, start/1
|
||||||
|
, stop/1
|
||||||
|
, list/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-spec registered_gateway() ->
|
||||||
|
[{gateway_type(), emqx_gateway_registry:descriptor()}].
|
||||||
|
registered_gateway() ->
|
||||||
|
emqx_gateway_registry:list().
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Gateway Instace APIs
|
||||||
|
|
||||||
|
-spec list() -> [instance()].
|
||||||
|
list() ->
|
||||||
|
lists:append(lists:map(
|
||||||
|
fun({_, Insta}) -> Insta end,
|
||||||
|
emqx_gateway_sup:list_gateway_insta()
|
||||||
|
)).
|
||||||
|
|
||||||
|
-spec create(gateway_type(), binary(), binary(), map())
|
||||||
|
-> {ok, pid()}
|
||||||
|
| {error, any()}.
|
||||||
|
create(Type, Name, Descr, RawConf) ->
|
||||||
|
Insta = #{ id => clacu_insta_id(Type, Name)
|
||||||
|
, type => Type
|
||||||
|
, name => Name
|
||||||
|
, descr => Descr
|
||||||
|
, rawconf => RawConf
|
||||||
|
},
|
||||||
|
emqx_gateway_sup:create_gateway_insta(Insta).
|
||||||
|
|
||||||
|
-spec remove(instance_id()) -> ok | {error, any()}.
|
||||||
|
remove(InstaId) ->
|
||||||
|
emqx_gateway_sup:remove_gateway_insta(InstaId).
|
||||||
|
|
||||||
|
-spec lookup(instance_id()) -> instance() | undefined.
|
||||||
|
lookup(InstaId) ->
|
||||||
|
emqx_gateway_sup:lookup_gateway_insta(InstaId).
|
||||||
|
|
||||||
|
-spec update(instance()) -> ok | {error, any()}.
|
||||||
|
update(NewInsta) ->
|
||||||
|
emqx_gateway_sup:update_gateway_insta(NewInsta).
|
||||||
|
|
||||||
|
-spec start(instance_id()) -> ok | {error, any()}.
|
||||||
|
start(InstaId) ->
|
||||||
|
emqx_gateway_sup:start_gateway_insta(InstaId).
|
||||||
|
|
||||||
|
-spec stop(instance_id()) -> ok | {error, any()}.
|
||||||
|
stop(InstaId) ->
|
||||||
|
emqx_gateway_sup:stop_gateway_insta(InstaId).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
clacu_insta_id(Type, Name) when is_binary(Name) ->
|
||||||
|
list_to_atom(lists:concat([Type, "#", binary_to_list(Name)])).
|
|
@ -0,0 +1,93 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 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_gateway_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-emqx_plugin(?MODULE).
|
||||||
|
|
||||||
|
-logger_header("[Gateway]").
|
||||||
|
|
||||||
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
|
start(_StartType, _StartArgs) ->
|
||||||
|
{ok, Sup} = emqx_gateway_sup:start_link(),
|
||||||
|
emqx_gateway_cli:load(),
|
||||||
|
load_default_gateway_applications(),
|
||||||
|
create_gateway_by_default(),
|
||||||
|
{ok, Sup}.
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
emqx_gateway_cli:unload(),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
|
||||||
|
load_default_gateway_applications() ->
|
||||||
|
Apps = gateway_type_searching(),
|
||||||
|
?LOG(info, "Starting the default gateway types: ~p", [Apps]),
|
||||||
|
lists:foreach(fun load/1, Apps).
|
||||||
|
|
||||||
|
gateway_type_searching() ->
|
||||||
|
%% FIXME: Hardcoded apps
|
||||||
|
[emqx_stomp_impl].
|
||||||
|
|
||||||
|
load(Mod) ->
|
||||||
|
try
|
||||||
|
Mod:load(),
|
||||||
|
?LOG(info, "Load ~s gateway application successfully!", [Mod])
|
||||||
|
catch
|
||||||
|
Class : Reason ->
|
||||||
|
?LOG(error, "Load ~s gateway application failed: {~p, ~p}",
|
||||||
|
[Mod, Class, Reason])
|
||||||
|
end.
|
||||||
|
|
||||||
|
create_gateway_by_default() ->
|
||||||
|
create_gateway_by_default(zipped_confs()).
|
||||||
|
|
||||||
|
create_gateway_by_default([]) ->
|
||||||
|
ok;
|
||||||
|
create_gateway_by_default([{Type, Name, Confs}|More]) ->
|
||||||
|
case emqx_gateway_registry:lookup(Type) of
|
||||||
|
undefined ->
|
||||||
|
?LOG(error, "Skip to start ~p#~p: not_registred_type",
|
||||||
|
[Type, Name]);
|
||||||
|
_ ->
|
||||||
|
case emqx_gateway:create(Type,
|
||||||
|
atom_to_binary(Name, utf8),
|
||||||
|
<<>>,
|
||||||
|
Confs) of
|
||||||
|
{ok, _} ->
|
||||||
|
?LOG(debug, "Start ~p#~p successfully!", [Type, Name]);
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "Start ~p#~p failed: ~0p",
|
||||||
|
[Type, Name, Reason])
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
create_gateway_by_default(More).
|
||||||
|
|
||||||
|
zipped_confs() ->
|
||||||
|
All = maps:to_list(emqx_config:get([emqx_gateway])),
|
||||||
|
lists:append(lists:foldr(
|
||||||
|
fun({Type, Gws}, Acc) ->
|
||||||
|
{Names, Confs} = lists:unzip(maps:to_list(Gws)),
|
||||||
|
Types = [ Type || _ <- lists:seq(1, length(Names))],
|
||||||
|
[lists:zip3(Types, Names, Confs) | Acc]
|
||||||
|
end, [], All)).
|
|
@ -0,0 +1,201 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The Command-Line-Interface module for Gateway Application
|
||||||
|
-module(emqx_gateway_cli).
|
||||||
|
|
||||||
|
-export([ load/0
|
||||||
|
, unload/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ gateway/1
|
||||||
|
, 'gateway-registry'/1
|
||||||
|
, 'gateway-clients'/1
|
||||||
|
, 'gateway-metrics'/1
|
||||||
|
%, 'gateway-banned'/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-spec load() -> ok.
|
||||||
|
load() ->
|
||||||
|
Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)],
|
||||||
|
lists:foreach(fun(Cmd) -> emqx_ctl:register_command(Cmd, {?MODULE, Cmd}, []) end, Cmds).
|
||||||
|
|
||||||
|
-spec unload() -> ok.
|
||||||
|
unload() ->
|
||||||
|
Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)],
|
||||||
|
lists:foreach(fun(Cmd) -> emqx_ctl:unregister_command(Cmd) end, Cmds).
|
||||||
|
|
||||||
|
is_cmd(Fun) ->
|
||||||
|
not lists:member(Fun, [init, load, module_info]).
|
||||||
|
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Cmds
|
||||||
|
|
||||||
|
gateway(["list"]) ->
|
||||||
|
lists:foreach(fun(#{id := InstaId, name := Name, type := Type}) ->
|
||||||
|
%% FIXME: Get the real running status
|
||||||
|
emqx_ctl:print("Gateway(~s, name=~s, type=~s, status=running~n",
|
||||||
|
[InstaId, Name, Type])
|
||||||
|
end, emqx_gateway:list());
|
||||||
|
|
||||||
|
gateway(["lookup", GatewayInstaId]) ->
|
||||||
|
case emqx_gateway:lookup(GatewayInstaId) of
|
||||||
|
undefined ->
|
||||||
|
emqx_ctl:print("undefined");
|
||||||
|
Info ->
|
||||||
|
emqx_ctl:print("~p~n", [Info])
|
||||||
|
end;
|
||||||
|
|
||||||
|
gateway(["stop", GatewayInstaId]) ->
|
||||||
|
case emqx_gateway:stop(GatewayInstaId) of
|
||||||
|
ok ->
|
||||||
|
emqx_ctl:print("ok");
|
||||||
|
{error, Reason} ->
|
||||||
|
emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
gateway(["start", GatewayInstaId]) ->
|
||||||
|
case emqx_gateway:start(GatewayInstaId) of
|
||||||
|
ok ->
|
||||||
|
emqx_ctl:print("ok");
|
||||||
|
{error, Reason} ->
|
||||||
|
emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
gateway(_) ->
|
||||||
|
%% TODO: create/remove APIs
|
||||||
|
emqx_ctl:usage([ {"gateway list",
|
||||||
|
"List all created gateway instances"}
|
||||||
|
, {"gateway lookup <GatewayId>",
|
||||||
|
"Looup a gateway detailed informations"}
|
||||||
|
, {"gateway stop <GatewayId>",
|
||||||
|
"Stop a gateway instance and release all resources"}
|
||||||
|
, {"gateway start <GatewayId>",
|
||||||
|
"Start a gateway instance"}
|
||||||
|
]).
|
||||||
|
|
||||||
|
'gateway-registry'(["list"]) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun({GwType, #{cbkmod := CbMod}}) ->
|
||||||
|
emqx_ctl:print("Registered Type: ~s, Callback Module: ~s~n", [GwType, CbMod])
|
||||||
|
end,
|
||||||
|
emqx_gateway_registry:list());
|
||||||
|
|
||||||
|
'gateway-registry'(_) ->
|
||||||
|
emqx_ctl:usage([ {"gateway-registry list",
|
||||||
|
"List all registered gateway types"}
|
||||||
|
]).
|
||||||
|
|
||||||
|
'gateway-clients'(["list", Type]) ->
|
||||||
|
InfoTab = emqx_gateway_cm:tabname(info, Type),
|
||||||
|
dump(InfoTab, client);
|
||||||
|
|
||||||
|
'gateway-clients'(["lookup", Type, ClientId]) ->
|
||||||
|
ChanTab = emqx_gateway_cm:tabname(chan, Type),
|
||||||
|
case ets:lookup(ChanTab, bin(ClientId)) of
|
||||||
|
[] -> emqx_ctl:print("Not Found.~n");
|
||||||
|
[Chann] ->
|
||||||
|
InfoTab = emqx_gateway_cm:tabname(info, Type),
|
||||||
|
[ChannInfo] = ets:lookup(InfoTab, Chann),
|
||||||
|
print({client, ChannInfo})
|
||||||
|
end;
|
||||||
|
|
||||||
|
'gateway-clients'(["kick", Type, ClientId]) ->
|
||||||
|
case emqx_gateway_cm:kick_session(Type, bin(ClientId)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
_ -> emqx_ctl:print("Not Found.~n")
|
||||||
|
end;
|
||||||
|
|
||||||
|
'gateway-clients'(_) ->
|
||||||
|
emqx_ctl:usage([ {"gateway-clients list <Type>",
|
||||||
|
"List all clients for a type of gateway"}
|
||||||
|
, {"gateway-clients lookup <Type> <ClientId>",
|
||||||
|
"Lookup the Client Info for specified client"}
|
||||||
|
, {"gateway-clients kick <Type> <ClientId>",
|
||||||
|
"Kick out a client"}
|
||||||
|
]).
|
||||||
|
|
||||||
|
'gateway-metrics'([GatewayType]) ->
|
||||||
|
Tab = emqx_gateway_metrics:tabname(GatewayType),
|
||||||
|
case ets:info(Tab) of
|
||||||
|
undefined ->
|
||||||
|
emqx_ctl:print("Bad Gateway Tyep.~n");
|
||||||
|
_ ->
|
||||||
|
lists:foreach(
|
||||||
|
fun({K, V}) ->
|
||||||
|
emqx_ctl:print("~-30s: ~w~n", [K, V])
|
||||||
|
end, lists:sort(ets:tab2list(Tab)))
|
||||||
|
end;
|
||||||
|
|
||||||
|
'gateway-metrics'(_) ->
|
||||||
|
emqx_ctl:usage([ {"gateway-metrics <Type>",
|
||||||
|
"List all metrics for a type of gateway"}
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
bin(S) -> iolist_to_binary(S).
|
||||||
|
|
||||||
|
dump(Table, Tag) ->
|
||||||
|
dump(Table, Tag, ets:first(Table), []).
|
||||||
|
|
||||||
|
dump(_Table, _, '$end_of_table', Result) ->
|
||||||
|
lists:reverse(Result);
|
||||||
|
|
||||||
|
dump(Table, Tag, Key, Result) ->
|
||||||
|
PrintValue = [print({Tag, Record}) || Record <- ets:lookup(Table, Key)],
|
||||||
|
dump(Table, Tag, ets:next(Table, Key), [PrintValue | Result]).
|
||||||
|
|
||||||
|
print({client, {_, Infos, Stats}}) ->
|
||||||
|
ClientInfo = maps:get(clientinfo, Infos, #{}),
|
||||||
|
ConnInfo = maps:get(conninfo, Infos, #{}),
|
||||||
|
_Session = maps:get(session, Infos, #{}),
|
||||||
|
SafeGet = fun(K, M) -> maps:get(K, M, undefined) end,
|
||||||
|
StatsGet = fun(K) -> proplists:get_value(K, Stats, 0) end,
|
||||||
|
|
||||||
|
ConnectedAt = SafeGet(connected_at, ConnInfo),
|
||||||
|
InfoKeys = [clientid, username, peername, clean_start, keepalive,
|
||||||
|
subscriptions_cnt, send_msg, connected, created_at, connected_at],
|
||||||
|
Info = #{ clientid => SafeGet(clientid, ClientInfo),
|
||||||
|
username => SafeGet(username, ClientInfo),
|
||||||
|
peername => SafeGet(peername, ConnInfo),
|
||||||
|
clean_start => SafeGet(clean_start, ConnInfo),
|
||||||
|
keepalive => SafeGet(keepalive, ConnInfo),
|
||||||
|
subscriptions_cnt => StatsGet(subscriptions_cnt),
|
||||||
|
send_msg => StatsGet(send_msg),
|
||||||
|
connected => SafeGet(conn_state, ClientInfo) == connected,
|
||||||
|
created_at => ConnectedAt,
|
||||||
|
connected_at => ConnectedAt
|
||||||
|
},
|
||||||
|
|
||||||
|
emqx_ctl:print("Client(~s, username=~s, peername=~s, "
|
||||||
|
"clean_start=~s, keepalive=~w, "
|
||||||
|
"subscriptions=~w, delivered_msgs=~w, "
|
||||||
|
"connected=~s, created_at=~w, connected_at=~w)~n",
|
||||||
|
[format(K, maps:get(K, Info)) || K <- InfoKeys]).
|
||||||
|
|
||||||
|
format(_, undefined) ->
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
format(peername, {IPAddr, Port}) ->
|
||||||
|
IPStr = emqx_mgmt_util:ntoa(IPAddr),
|
||||||
|
io_lib:format("~s:~p", [IPStr, Port]);
|
||||||
|
|
||||||
|
format(_, Val) ->
|
||||||
|
Val.
|
|
@ -0,0 +1,447 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The gateway connection management
|
||||||
|
-module(emqx_gateway_cm).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-logger_header("[PGW-CM]").
|
||||||
|
|
||||||
|
%% APIs
|
||||||
|
-export([start_link/1]).
|
||||||
|
|
||||||
|
-export([ open_session/5
|
||||||
|
, kick_session/2
|
||||||
|
, kick_session/3
|
||||||
|
, register_channel/4
|
||||||
|
, unregister_channel/2
|
||||||
|
, insert_channel_info/4
|
||||||
|
, set_chan_info/3
|
||||||
|
, set_chan_info/4
|
||||||
|
, get_chan_info/2
|
||||||
|
, get_chan_info/3
|
||||||
|
, set_chan_stats/3
|
||||||
|
, set_chan_stats/4
|
||||||
|
, get_chan_stats/2
|
||||||
|
, get_chan_stats/3
|
||||||
|
, connection_closed/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Internal funcs for getting tabname by GatewayId
|
||||||
|
-export([cmtabs/1, tabname/2]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([ init/1
|
||||||
|
, handle_call/3
|
||||||
|
, handle_cast/2
|
||||||
|
, handle_info/2
|
||||||
|
, terminate/2
|
||||||
|
, code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
type :: atom(), %% Gateway Id
|
||||||
|
locker :: pid(), %% ClientId Locker for CM
|
||||||
|
registry :: pid(), %% ClientId Registry server
|
||||||
|
chan_pmon :: emqx_pmon:pmon()
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(T_TAKEOVER, 15000).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% XXX: Options for cm process
|
||||||
|
start_link(Options) ->
|
||||||
|
Type = proplists:get_value(type, Options),
|
||||||
|
gen_server:start_link({local, procname(Type)}, ?MODULE, Options, []).
|
||||||
|
|
||||||
|
procname(Type) ->
|
||||||
|
list_to_atom(lists:concat([emqx_gateway_, Type, '_cm'])).
|
||||||
|
|
||||||
|
-spec cmtabs(Type :: atom()) -> {ChanTab :: atom(),
|
||||||
|
ConnTab :: atom(),
|
||||||
|
ChannInfoTab :: atom()}.
|
||||||
|
cmtabs(Type) ->
|
||||||
|
{ tabname(chan, Type) %% Client Tabname; Record: {ClientId, Pid}
|
||||||
|
, tabname(conn, Type) %% Client ConnMod; Recrod: {{ClientId, Pid}, ConnMod}
|
||||||
|
, tabname(info, Type) %% ClientInfo Tabname; Record: {{ClientId, Pid}, ClientInfo, ClientStats}
|
||||||
|
}.
|
||||||
|
|
||||||
|
tabname(chan, Type) ->
|
||||||
|
list_to_atom(lists:concat([emqx_gateway_, Type, '_channel']));
|
||||||
|
tabname(conn, Type) ->
|
||||||
|
list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_conn']));
|
||||||
|
tabname(info, Type) ->
|
||||||
|
list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_info'])).
|
||||||
|
|
||||||
|
lockername(Type) ->
|
||||||
|
list_to_atom(lists:concat([emqx_gateway_, Type, '_locker'])).
|
||||||
|
|
||||||
|
-spec register_channel(atom(), binary(), pid(), emqx_types:conninfo()) -> ok.
|
||||||
|
register_channel(Type, ClientId, ChanPid, #{conn_mod := ConnMod}) when is_pid(ChanPid) ->
|
||||||
|
Chan = {ClientId, ChanPid},
|
||||||
|
true = ets:insert(tabname(chan, Type), Chan),
|
||||||
|
true = ets:insert(tabname(conn, Type), {Chan, ConnMod}),
|
||||||
|
ok = emqx_gateway_cm_registry:register_channel(Type, Chan),
|
||||||
|
cast(procname(Type), {registered, Chan}).
|
||||||
|
|
||||||
|
%% @doc Unregister a channel.
|
||||||
|
-spec unregister_channel(atom(), emqx_types:clientid()) -> ok.
|
||||||
|
unregister_channel(Type, ClientId) when is_binary(ClientId) ->
|
||||||
|
true = do_unregister_channel(Type, {ClientId, self()}, cmtabs(Type)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc Insert/Update the channel info and stats
|
||||||
|
-spec insert_channel_info(atom(),
|
||||||
|
emqx_types:clientid(),
|
||||||
|
emqx_types:infos(),
|
||||||
|
emqx_types:stats()) -> ok.
|
||||||
|
insert_channel_info(Type, ClientId, Info, Stats) ->
|
||||||
|
Chan = {ClientId, self()},
|
||||||
|
true = ets:insert(tabname(info, Type), {Chan, Info, Stats}),
|
||||||
|
%%?tp(debug, insert_channel_info, #{client_id => ClientId}),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc Get info of a channel.
|
||||||
|
-spec get_chan_info(gateway_type(), emqx_types:clientid())
|
||||||
|
-> emqx_types:infos() | undefined.
|
||||||
|
get_chan_info(Type, ClientId) ->
|
||||||
|
with_channel(Type, ClientId,
|
||||||
|
fun(ChanPid) ->
|
||||||
|
get_chan_info(Type, ClientId, ChanPid)
|
||||||
|
end).
|
||||||
|
|
||||||
|
-spec get_chan_info(gateway_type(), emqx_types:clientid(), pid())
|
||||||
|
-> emqx_types:infos() | undefined.
|
||||||
|
get_chan_info(Type, ClientId, ChanPid) when node(ChanPid) == node() ->
|
||||||
|
Chan = {ClientId, ChanPid},
|
||||||
|
try ets:lookup_element(tabname(info, Type), Chan, 2)
|
||||||
|
catch
|
||||||
|
error:badarg -> undefined
|
||||||
|
end;
|
||||||
|
get_chan_info(Type, ClientId, ChanPid) ->
|
||||||
|
rpc_call(node(ChanPid), get_chan_info, [Type, ClientId, ChanPid]).
|
||||||
|
|
||||||
|
%% @doc Update infos of the channel.
|
||||||
|
-spec set_chan_info(gateway_type(),
|
||||||
|
emqx_types:clientid(),
|
||||||
|
emqx_types:infos()) -> boolean().
|
||||||
|
set_chan_info(Type, ClientId, Infos) ->
|
||||||
|
set_chan_info(Type, ClientId, self(), Infos).
|
||||||
|
|
||||||
|
-spec set_chan_info(gateway_type(),
|
||||||
|
emqx_types:clientid(),
|
||||||
|
pid(),
|
||||||
|
emqx_types:infos()) -> boolean().
|
||||||
|
set_chan_info(Type, ClientId, ChanPid, Infos) when node(ChanPid) == node() ->
|
||||||
|
Chan = {ClientId, ChanPid},
|
||||||
|
try ets:update_element(tabname(info, Type), Chan, {2, Infos})
|
||||||
|
catch
|
||||||
|
error:badarg -> false
|
||||||
|
end;
|
||||||
|
set_chan_info(Type, ClientId, ChanPid, Infos) ->
|
||||||
|
rpc_call(node(ChanPid), set_chan_info, [Type, ClientId, ChanPid, Infos]).
|
||||||
|
|
||||||
|
%% @doc Get channel's stats.
|
||||||
|
-spec get_chan_stats(gateway_type(), emqx_types:clientid())
|
||||||
|
-> emqx_types:stats() | undefined.
|
||||||
|
get_chan_stats(Type, ClientId) ->
|
||||||
|
with_channel(Type, ClientId,
|
||||||
|
fun(ChanPid) ->
|
||||||
|
get_chan_stats(Type, ClientId, ChanPid)
|
||||||
|
end).
|
||||||
|
|
||||||
|
-spec get_chan_stats(gateway_type(), emqx_types:clientid(), pid())
|
||||||
|
-> emqx_types:stats() | undefined.
|
||||||
|
get_chan_stats(Type, ClientId, ChanPid) when node(ChanPid) == node() ->
|
||||||
|
Chan = {ClientId, ChanPid},
|
||||||
|
try ets:lookup_element(tabname(info, Type), Chan, 3)
|
||||||
|
catch
|
||||||
|
error:badarg -> undefined
|
||||||
|
end;
|
||||||
|
get_chan_stats(Type, ClientId, ChanPid) ->
|
||||||
|
rpc_call(node(ChanPid), get_chan_stats, [Type, ClientId, ChanPid]).
|
||||||
|
|
||||||
|
-spec set_chan_stats(gateway_type(),
|
||||||
|
emqx_types:clientid(),
|
||||||
|
emqx_types:stats()) -> boolean().
|
||||||
|
set_chan_stats(Type, ClientId, Stats) ->
|
||||||
|
set_chan_stats(Type, ClientId, self(), Stats).
|
||||||
|
|
||||||
|
-spec set_chan_stats(gateway_type(),
|
||||||
|
emqx_types:clientid(),
|
||||||
|
pid(),
|
||||||
|
emqx_types:stats()) -> boolean().
|
||||||
|
set_chan_stats(Type, ClientId, ChanPid, Stats) when node(ChanPid) == node() ->
|
||||||
|
Chan = {ClientId, self()},
|
||||||
|
try ets:update_element(tabname(info, Type), Chan, {3, Stats})
|
||||||
|
catch
|
||||||
|
error:badarg -> false
|
||||||
|
end;
|
||||||
|
set_chan_stats(Type, ClientId, ChanPid, Stats) ->
|
||||||
|
rpc_call(node(ChanPid), set_chan_stats, [Type, ClientId, ChanPid, Stats]).
|
||||||
|
|
||||||
|
-spec connection_closed(gateway_type(), emqx_types:clientid()) -> true.
|
||||||
|
connection_closed(Type, ClientId) ->
|
||||||
|
%% XXX: Why we need to delete conn_mod tab ???
|
||||||
|
Chan = {ClientId, self()},
|
||||||
|
ets:delete_object(tabname(conn, Type), Chan).
|
||||||
|
|
||||||
|
-spec open_session(Type :: atom(), CleanStart :: boolean(),
|
||||||
|
ClientInfo :: emqx_types:clientinfo(),
|
||||||
|
ConnInfo :: emqx_types:conninfo(),
|
||||||
|
CreateSessionFun :: function())
|
||||||
|
-> {ok, #{session := map(),
|
||||||
|
present := boolean(),
|
||||||
|
pendings => list()
|
||||||
|
}}
|
||||||
|
| {error, any()}.
|
||||||
|
|
||||||
|
open_session(Type, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun) ->
|
||||||
|
Self = self(),
|
||||||
|
ClientId = maps:get(clientid, ClientInfo),
|
||||||
|
Fun = fun(_) ->
|
||||||
|
ok = discard_session(Type, ClientId),
|
||||||
|
Session = create_session(Type,
|
||||||
|
ClientInfo,
|
||||||
|
ConnInfo,
|
||||||
|
CreateSessionFun
|
||||||
|
),
|
||||||
|
register_channel(Type, ClientId, Self, ConnInfo),
|
||||||
|
{ok, #{session => Session, present => false}}
|
||||||
|
end,
|
||||||
|
locker_trans(Type, ClientId, Fun);
|
||||||
|
|
||||||
|
open_session(_Type, false = _CleanStart,
|
||||||
|
_ClientInfo, _ConnInfo, _CreateSessionFun) ->
|
||||||
|
{error, not_supported_now}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
create_session(_Type, ClientInfo, ConnInfo, CreateSessionFun) ->
|
||||||
|
try
|
||||||
|
Session = emqx_gateway_utils:apply(
|
||||||
|
CreateSessionFun,
|
||||||
|
[ClientInfo, ConnInfo]
|
||||||
|
),
|
||||||
|
%% TODO: v0.2 session metrics & hooks
|
||||||
|
%ok = emqx_metrics:inc('session.created'),
|
||||||
|
%ok = emqx_hooks:run('session.created', [ClientInfo, emqx_session:info(Session)]),
|
||||||
|
Session
|
||||||
|
catch
|
||||||
|
Class : Reason : Stk ->
|
||||||
|
?LOG(error, "Failed to create a session: ~p, ~p "
|
||||||
|
"Stacktrace:~0p", [Class, Reason, Stk]),
|
||||||
|
throw(Reason)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Discard all the sessions identified by the ClientId.
|
||||||
|
-spec discard_session(Type :: atom(), binary()) -> ok.
|
||||||
|
discard_session(Type, ClientId) when is_binary(ClientId) ->
|
||||||
|
case lookup_channels(Type, ClientId) of
|
||||||
|
[] -> ok;
|
||||||
|
ChanPids -> lists:foreach(fun(Pid) -> do_discard_session(Type, ClientId, Pid) end, ChanPids)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
do_discard_session(Type, ClientId, Pid) ->
|
||||||
|
try
|
||||||
|
discard_session(Type, ClientId, Pid)
|
||||||
|
catch
|
||||||
|
_ : noproc -> % emqx_ws_connection: call
|
||||||
|
%?tp(debug, "session_already_gone", #{pid => Pid}),
|
||||||
|
ok;
|
||||||
|
_ : {noproc, _} -> % emqx_connection: gen_server:call
|
||||||
|
%?tp(debug, "session_already_gone", #{pid => Pid}),
|
||||||
|
ok;
|
||||||
|
_ : {{shutdown, _}, _} ->
|
||||||
|
%?tp(debug, "session_already_shutdown", #{pid => Pid}),
|
||||||
|
ok;
|
||||||
|
_ : _Error : _St ->
|
||||||
|
%?tp(error, "failed_to_discard_session",
|
||||||
|
% #{pid => Pid, reason => Error, stacktrace=>St})
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
discard_session(Type, ClientId, ChanPid) when node(ChanPid) == node() ->
|
||||||
|
case get_chann_conn_mod(Type, ClientId, ChanPid) of
|
||||||
|
undefined -> ok;
|
||||||
|
ConnMod when is_atom(ConnMod) ->
|
||||||
|
ConnMod:call(ChanPid, discard, ?T_TAKEOVER)
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
discard_session(Type, ClientId, ChanPid) ->
|
||||||
|
rpc_call(node(ChanPid), discard_session, [Type, ClientId, ChanPid]).
|
||||||
|
|
||||||
|
-spec kick_session(gateway_type(), emqx_types:clientid())
|
||||||
|
-> {error, any()}
|
||||||
|
| ok.
|
||||||
|
kick_session(Type, ClientId) ->
|
||||||
|
case lookup_channels(Type, ClientId) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[ChanPid] ->
|
||||||
|
kick_session(Type, ClientId, ChanPid);
|
||||||
|
ChanPids ->
|
||||||
|
[ChanPid|StalePids] = lists:reverse(ChanPids),
|
||||||
|
?LOG(error, "More than one channel found: ~p", [ChanPids]),
|
||||||
|
lists:foreach(fun(StalePid) ->
|
||||||
|
catch discard_session(Type, ClientId, StalePid)
|
||||||
|
end, StalePids),
|
||||||
|
kick_session(Type, ClientId, ChanPid)
|
||||||
|
end.
|
||||||
|
|
||||||
|
kick_session(Type, ClientId, ChanPid) when node(ChanPid) == node() ->
|
||||||
|
case get_chan_info(Type, ClientId, ChanPid) of
|
||||||
|
#{conninfo := #{conn_mod := ConnMod}} ->
|
||||||
|
ConnMod:call(ChanPid, kick, ?T_TAKEOVER);
|
||||||
|
undefined ->
|
||||||
|
{error, not_found}
|
||||||
|
end;
|
||||||
|
|
||||||
|
kick_session(Type, ClientId, ChanPid) ->
|
||||||
|
rpc_call(node(ChanPid), kick_session, [Type, ClientId, ChanPid]).
|
||||||
|
|
||||||
|
with_channel(Type, ClientId, Fun) ->
|
||||||
|
case lookup_channels(Type, ClientId) of
|
||||||
|
[] -> undefined;
|
||||||
|
[Pid] -> Fun(Pid);
|
||||||
|
Pids -> Fun(lists:last(Pids))
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Lookup channels.
|
||||||
|
-spec(lookup_channels(atom(), emqx_types:clientid()) -> list(pid())).
|
||||||
|
lookup_channels(Type, ClientId) ->
|
||||||
|
emqx_gateway_cm_registry:lookup_channels(Type, ClientId).
|
||||||
|
|
||||||
|
get_chann_conn_mod(Type, ClientId, ChanPid) when node(ChanPid) == node() ->
|
||||||
|
Chan = {ClientId, ChanPid},
|
||||||
|
try [ConnMod] = ets:lookup_element(tabname(conn, Type), Chan, 2), ConnMod
|
||||||
|
catch
|
||||||
|
error:badarg -> undefined
|
||||||
|
end;
|
||||||
|
get_chann_conn_mod(Type, ClientId, ChanPid) ->
|
||||||
|
rpc_call(node(ChanPid), get_chann_conn_mod, [Type, ClientId, ChanPid]).
|
||||||
|
|
||||||
|
%% Locker
|
||||||
|
|
||||||
|
locker_trans(_Type, undefined, Fun) ->
|
||||||
|
Fun([]);
|
||||||
|
locker_trans(Type, ClientId, Fun) ->
|
||||||
|
Locker = lockername(Type),
|
||||||
|
case locker_lock(Locker, ClientId) of
|
||||||
|
{true, Nodes} ->
|
||||||
|
try Fun(Nodes) after locker_unlock(Locker, ClientId) end;
|
||||||
|
{false, _Nodes} ->
|
||||||
|
{error, client_id_unavailable}
|
||||||
|
end.
|
||||||
|
|
||||||
|
locker_lock(Locker, ClientId) ->
|
||||||
|
ekka_locker:acquire(Locker, ClientId, quorum).
|
||||||
|
|
||||||
|
locker_unlock(Locker, ClientId) ->
|
||||||
|
ekka_locker:release(Locker, ClientId, quorum).
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
rpc_call(Node, Fun, Args) ->
|
||||||
|
case rpc:call(Node, ?MODULE, Fun, Args) of
|
||||||
|
{badrpc, Reason} -> error(Reason);
|
||||||
|
Res -> Res
|
||||||
|
end.
|
||||||
|
|
||||||
|
cast(Name, Msg) ->
|
||||||
|
gen_server:cast(Name, Msg).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% gen_server callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init(Options) ->
|
||||||
|
Type = proplists:get_value(type, Options),
|
||||||
|
|
||||||
|
TabOpts = [public, {write_concurrency, true}],
|
||||||
|
|
||||||
|
{ChanTab, ConnTab, InfoTab} = cmtabs(Type),
|
||||||
|
ok = emqx_tables:new(ChanTab, [bag, {read_concurrency, true}|TabOpts]),
|
||||||
|
ok = emqx_tables:new(ConnTab, [bag | TabOpts]),
|
||||||
|
ok = emqx_tables:new(InfoTab, [set, compressed | TabOpts]),
|
||||||
|
|
||||||
|
%% Start link cm-registry process
|
||||||
|
%% XXX: Should I hang it under a higher level supervisor?
|
||||||
|
{ok, Registry} = emqx_gateway_cm_registry:start_link(Type),
|
||||||
|
|
||||||
|
%% Start locker process
|
||||||
|
{ok, Locker} = ekka_locker:start_link(lockername(Type)),
|
||||||
|
|
||||||
|
%% Interval update stats
|
||||||
|
%% TODO: v0.2
|
||||||
|
%ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0),
|
||||||
|
|
||||||
|
{ok, #state{type = Type,
|
||||||
|
locker = Locker,
|
||||||
|
registry = Registry,
|
||||||
|
chan_pmon = emqx_pmon:new()}}.
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
Reply = ok,
|
||||||
|
{reply, Reply, State}.
|
||||||
|
|
||||||
|
handle_cast({registered, {ClientId, ChanPid}}, State = #state{chan_pmon = PMon}) ->
|
||||||
|
PMon1 = emqx_pmon:monitor(ChanPid, ClientId, PMon),
|
||||||
|
{noreply, State#state{chan_pmon = PMon1}};
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info({'DOWN', _MRef, process, Pid, _Reason},
|
||||||
|
State = #state{type = Type, chan_pmon = PMon}) ->
|
||||||
|
ChanPids = [Pid | emqx_misc:drain_down(10000)], %% XXX: Fixed BATCH_SIZE
|
||||||
|
{Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon),
|
||||||
|
|
||||||
|
CmTabs = cmtabs(Type),
|
||||||
|
ok = emqx_pool:async_submit(fun do_unregister_channel_task/3, [Items, Type, CmTabs]),
|
||||||
|
{noreply, State#state{chan_pmon = PMon1}};
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
do_unregister_channel_task(Items, Type, CmTabs) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun({ChanPid, ClientId}) ->
|
||||||
|
do_unregister_channel(Type, {ClientId, ChanPid}, CmTabs)
|
||||||
|
end, Items).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
do_unregister_channel(Type, Chan, {ChanTab, ConnTab, InfoTab}) ->
|
||||||
|
ok = emqx_gateway_cm_registry:unregister_channel(Type, Chan),
|
||||||
|
true = ets:delete(ConnTab, Chan),
|
||||||
|
true = ets:delete(InfoTab, Chan),
|
||||||
|
ets:delete_object(ChanTab, Chan).
|
|
@ -0,0 +1,141 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The gateway connection registry
|
||||||
|
-module(emqx_gateway_cm_registry).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-logger_header("[PGW-CM-Registy]").
|
||||||
|
|
||||||
|
-export([start_link/1]).
|
||||||
|
|
||||||
|
%% XXX: needless
|
||||||
|
%-export([is_enabled/0]).
|
||||||
|
|
||||||
|
-export([ register_channel/2
|
||||||
|
, unregister_channel/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([lookup_channels/2]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([ init/1
|
||||||
|
, handle_call/3
|
||||||
|
, handle_cast/2
|
||||||
|
, handle_info/2
|
||||||
|
, terminate/2
|
||||||
|
, code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(LOCK, {?MODULE, cleanup_down}).
|
||||||
|
|
||||||
|
-record(channel, {chid, pid}).
|
||||||
|
|
||||||
|
%% @doc Start the global channel registry.
|
||||||
|
-spec(start_link(atom()) -> gen_server:startlink_ret()).
|
||||||
|
start_link(Type) ->
|
||||||
|
gen_server:start_link(?MODULE, [Type], []).
|
||||||
|
|
||||||
|
-spec tabname(atom()) -> atom().
|
||||||
|
tabname(Type) ->
|
||||||
|
list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_registry'])).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Register a global channel.
|
||||||
|
-spec register_channel(atom(), binary() | {binary(), pid()}) -> ok.
|
||||||
|
register_channel(Type, ClientId) when is_binary(ClientId) ->
|
||||||
|
register_channel(Type, {ClientId, self()});
|
||||||
|
|
||||||
|
register_channel(Type, {ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) ->
|
||||||
|
ekka_mnesia:dirty_write(tabname(Type), record(ClientId, ChanPid)).
|
||||||
|
|
||||||
|
%% @doc Unregister a global channel.
|
||||||
|
-spec unregister_channel(atom(), binary() | {binary(), pid()}) -> ok.
|
||||||
|
unregister_channel(Type, ClientId) when is_binary(ClientId) ->
|
||||||
|
unregister_channel(Type, {ClientId, self()});
|
||||||
|
|
||||||
|
unregister_channel(Type, {ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) ->
|
||||||
|
ekka_mnesia:dirty_delete_object(tabname(Type), record(ClientId, ChanPid)).
|
||||||
|
|
||||||
|
%% @doc Lookup the global channels.
|
||||||
|
-spec lookup_channels(atom(), binary()) -> list(pid()).
|
||||||
|
lookup_channels(Type, ClientId) ->
|
||||||
|
[ChanPid || #channel{pid = ChanPid} <- mnesia:dirty_read(tabname(Type), ClientId)].
|
||||||
|
|
||||||
|
record(ClientId, ChanPid) ->
|
||||||
|
#channel{chid = ClientId, pid = ChanPid}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% gen_server callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([Type]) ->
|
||||||
|
Tab = tabname(Type),
|
||||||
|
ok = ekka_mnesia:create_table(Tab, [
|
||||||
|
{type, bag},
|
||||||
|
{ram_copies, [node()]},
|
||||||
|
{record_name, channel},
|
||||||
|
{attributes, record_info(fields, channel)},
|
||||||
|
{storage_properties, [{ets, [{read_concurrency, true},
|
||||||
|
{write_concurrency, true}]}]}]),
|
||||||
|
ok = ekka_mnesia:copy_table(Tab, ram_copies),
|
||||||
|
%%ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity),
|
||||||
|
ok = ekka:monitor(membership),
|
||||||
|
{ok, #{type => Type}}.
|
||||||
|
|
||||||
|
handle_call(Req, _From, State) ->
|
||||||
|
logger:error("Unexpected call: ~p", [Req]),
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast(Msg, State) ->
|
||||||
|
logger:error("Unexpected cast: ~p", [Msg]),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info({membership, {mnesia, down, Node}}, State = #{type := Type}) ->
|
||||||
|
Tab = tabname(Type),
|
||||||
|
global:trans({?LOCK, self()},
|
||||||
|
fun() ->
|
||||||
|
%% FIXME: The shard name should be fixed later
|
||||||
|
ekka_mnesia:transaction(?MODULE, fun cleanup_channels/2, [Node, Tab])
|
||||||
|
end),
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_info({membership, _Event}, State) ->
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_info(Info, State) ->
|
||||||
|
logger:error("Unexpected info: ~p", [Info]),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
cleanup_channels(Node, Tab) ->
|
||||||
|
Pat = [{#channel{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}],
|
||||||
|
lists:foreach(fun(Chan) ->
|
||||||
|
mnesia:delete_object(Tab, Chan, write)
|
||||||
|
end, mnesia:select(Tab, Pat, write)).
|
|
@ -0,0 +1,148 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The gateway instance context
|
||||||
|
-module(emqx_gateway_ctx).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
-logger_header(["PGW-Ctx"]).
|
||||||
|
|
||||||
|
%% @doc The running context for a Connection/Channel process.
|
||||||
|
%%
|
||||||
|
%% The `Context` encapsulates a complex structure of contextual information.
|
||||||
|
%% It is convenient to use it directly in Channel/Connection to read
|
||||||
|
%% configuration, register devices and other common operations.
|
||||||
|
%%
|
||||||
|
-type context() ::
|
||||||
|
#{ %% Gateway Instance ID
|
||||||
|
instid := instance_id()
|
||||||
|
%% Gateway ID
|
||||||
|
, type := gateway_type()
|
||||||
|
%% Autenticator
|
||||||
|
, auth := allow_anonymous | emqx_authentication:chain_id()
|
||||||
|
%% The ConnectionManager PID
|
||||||
|
, cm := pid()
|
||||||
|
}.
|
||||||
|
|
||||||
|
%% Authentication circle
|
||||||
|
-export([ authenticate/2
|
||||||
|
, open_session/5
|
||||||
|
, insert_channel_info/4
|
||||||
|
, set_chan_info/3
|
||||||
|
, set_chan_stats/3
|
||||||
|
, connection_closed/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Message circle
|
||||||
|
-export([ authorize/4
|
||||||
|
% Needless for pub/sub
|
||||||
|
%, publish/3
|
||||||
|
%, subscribe/4
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Metrics & Stats
|
||||||
|
-export([ metrics_inc/2
|
||||||
|
, metrics_inc/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Authentication circle
|
||||||
|
|
||||||
|
%% @doc Authenticate whether the client has access to the Broker.
|
||||||
|
-spec authenticate(context(), emqx_types:clientinfo())
|
||||||
|
-> {ok, emqx_types:clientinfo()}
|
||||||
|
| {error, any()}.
|
||||||
|
authenticate(_Ctx = #{auth := allow_anonymous}, ClientInfo) ->
|
||||||
|
{ok, ClientInfo#{anonymous => true}};
|
||||||
|
authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) ->
|
||||||
|
ClientInfo = ClientInfo0#{
|
||||||
|
zone => undefined,
|
||||||
|
chain_id => ChainId
|
||||||
|
},
|
||||||
|
case emqx_access_control:authenticate(ClientInfo) of
|
||||||
|
{ok, AuthResult} ->
|
||||||
|
{ok, mountpoint(maps:merge(ClientInfo, AuthResult))};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Register the session to the cluster.
|
||||||
|
%%
|
||||||
|
%% This function should be called after the client has authenticated
|
||||||
|
%% successfully so that the client can be managed in the cluster.
|
||||||
|
-spec open_session(context(), boolean(), emqx_types:clientinfo(),
|
||||||
|
emqx_types:conninfo(), function())
|
||||||
|
-> {ok, #{session := any(),
|
||||||
|
present := boolean(),
|
||||||
|
pendings => list()
|
||||||
|
}}
|
||||||
|
| {error, any()}.
|
||||||
|
open_session(Ctx, false, ClientInfo, ConnInfo, CreateSessionFun) ->
|
||||||
|
logger:warning("clean_start=false is not supported now, "
|
||||||
|
"fallback to clean_start mode"),
|
||||||
|
open_session(Ctx, true, ClientInfo, ConnInfo, CreateSessionFun);
|
||||||
|
|
||||||
|
open_session(_Ctx = #{type := Type},
|
||||||
|
CleanStart, ClientInfo, ConnInfo, CreateSessionFun) ->
|
||||||
|
emqx_gateway_cm:open_session(Type, CleanStart,
|
||||||
|
ClientInfo, ConnInfo, CreateSessionFun).
|
||||||
|
|
||||||
|
-spec insert_channel_info(context(),
|
||||||
|
emqx_types:clientid(),
|
||||||
|
emqx_types:infos(),
|
||||||
|
emqx_types:stats()) -> ok.
|
||||||
|
insert_channel_info(_Ctx = #{type := Type}, ClientId, Infos, Stats) ->
|
||||||
|
emqx_gateway_cm:insert_channel_info(Type, ClientId, Infos, Stats).
|
||||||
|
|
||||||
|
%% @doc Set the Channel Info to the ConnectionManager for this client
|
||||||
|
-spec set_chan_info(context(),
|
||||||
|
emqx_types:clientid(),
|
||||||
|
emqx_types:infos()) -> boolean().
|
||||||
|
set_chan_info(_Ctx = #{type := Type}, ClientId, Infos) ->
|
||||||
|
emqx_gateway_cm:set_chan_info(Type, ClientId, Infos).
|
||||||
|
|
||||||
|
-spec set_chan_stats(context(),
|
||||||
|
emqx_types:clientid(),
|
||||||
|
emqx_types:stats()) -> boolean().
|
||||||
|
set_chan_stats(_Ctx = #{type := Type}, ClientId, Stats) ->
|
||||||
|
emqx_gateway_cm:set_chan_stats(Type, ClientId, Stats).
|
||||||
|
|
||||||
|
-spec connection_closed(context(), emqx_types:clientid()) -> boolean().
|
||||||
|
connection_closed(_Ctx = #{type := Type}, ClientId) ->
|
||||||
|
emqx_gateway_cm:connection_closed(Type, ClientId).
|
||||||
|
|
||||||
|
-spec authorize(context(), emqx_types:clientinfo(),
|
||||||
|
emqx_types:pubsub(), emqx_types:topic())
|
||||||
|
-> allow | deny.
|
||||||
|
authorize(_Ctx, ClientInfo, PubSub, Topic) ->
|
||||||
|
emqx_access_control:authorize(ClientInfo, PubSub, Topic).
|
||||||
|
|
||||||
|
metrics_inc(_Ctx = #{type := Type}, Name) ->
|
||||||
|
emqx_gateway_metrics:inc(Type, Name).
|
||||||
|
|
||||||
|
metrics_inc(_Ctx = #{type := Type}, Name, Oct) ->
|
||||||
|
emqx_gateway_metrics:inc(Type, Name, Oct).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
mountpoint(ClientInfo = #{mountpoint := undefined}) ->
|
||||||
|
ClientInfo;
|
||||||
|
mountpoint(ClientInfo = #{mountpoint := MountPoint}) ->
|
||||||
|
MountPoint1 = emqx_mountpoint:replvar(MountPoint, ClientInfo),
|
||||||
|
ClientInfo#{mountpoint := MountPoint1}.
|
|
@ -0,0 +1,135 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The Gateway Top supervisor.
|
||||||
|
-module(emqx_gateway_gw_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
-export([start_link/1]).
|
||||||
|
|
||||||
|
-export([ create_insta/3
|
||||||
|
, remove_insta/2
|
||||||
|
, update_insta/2
|
||||||
|
, start_insta/2
|
||||||
|
, stop_insta/2
|
||||||
|
, list_insta/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Supervisor callbacks
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_link(Type) ->
|
||||||
|
supervisor:start_link({local, Type}, ?MODULE, [Type]).
|
||||||
|
|
||||||
|
-spec create_insta(pid(), instance(), map()) -> {ok, GwInstaPid :: pid()} | {error, any()}.
|
||||||
|
create_insta(Sup, Insta = #{id := InstaId}, GwDscrptr) ->
|
||||||
|
case emqx_gateway_utils:find_sup_child(Sup, InstaId) of
|
||||||
|
{ok, _GwInstaPid} -> {error, alredy_existed};
|
||||||
|
false ->
|
||||||
|
%% XXX: More instances options to it?
|
||||||
|
%%
|
||||||
|
Ctx = ctx(Sup, InstaId),
|
||||||
|
%%
|
||||||
|
ChildSpec = emqx_gateway_utils:childspec(
|
||||||
|
InstaId,
|
||||||
|
worker,
|
||||||
|
emqx_gateway_insta_sup,
|
||||||
|
[Insta, Ctx, GwDscrptr]
|
||||||
|
),
|
||||||
|
emqx_gateway_utils:supervisor_ret(
|
||||||
|
supervisor:start_child(Sup, ChildSpec)
|
||||||
|
)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec remove_insta(pid(), InstaId :: atom()) -> ok | {error, any()}.
|
||||||
|
remove_insta(Sup, InstaId) ->
|
||||||
|
case emqx_gateway_utils:find_sup_child(Sup, InstaId) of
|
||||||
|
false -> ok;
|
||||||
|
{ok, _GwInstaPid} ->
|
||||||
|
ok = supervisor:terminate_child(Sup, InstaId),
|
||||||
|
ok = supervisor:delete_child(Sup, InstaId)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec update_insta(pid(), NewInsta :: instance()) -> ok | {error, any()}.
|
||||||
|
update_insta(Sup, NewInsta = #{id := InstaId}) ->
|
||||||
|
case emqx_gateway_utils:find_sup_child(Sup, InstaId) of
|
||||||
|
false -> {error, not_found};
|
||||||
|
{ok, GwInstaPid} ->
|
||||||
|
emqx_gateway_insta_sup:update(GwInstaPid, NewInsta)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec start_insta(pid(), atom()) -> ok | {error, any()}.
|
||||||
|
start_insta(Sup, InstaId) ->
|
||||||
|
case emqx_gateway_utils:find_sup_child(Sup, InstaId) of
|
||||||
|
false -> {error, not_found};
|
||||||
|
{ok, GwInstaPid} ->
|
||||||
|
emqx_gateway_insta_sup:enable(GwInstaPid)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec stop_insta(pid(), atom()) -> ok | {error, any()}.
|
||||||
|
stop_insta(Sup, InstaId) ->
|
||||||
|
case emqx_gateway_utils:find_sup_child(Sup, InstaId) of
|
||||||
|
false -> {error, not_found};
|
||||||
|
{ok, GwInstaPid} ->
|
||||||
|
emqx_gateway_insta_sup:disable(GwInstaPid)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec list_insta(pid()) -> [instance()].
|
||||||
|
list_insta(Sup) ->
|
||||||
|
lists:filtermap(
|
||||||
|
fun({InstaId, GwInstaPid, _Type, _Mods}) ->
|
||||||
|
is_gateway_insta_id(InstaId)
|
||||||
|
andalso {true, emqx_gateway_insta_sup:info(GwInstaPid)}
|
||||||
|
end, supervisor:which_children(Sup)).
|
||||||
|
|
||||||
|
%% Supervisor callback
|
||||||
|
|
||||||
|
%% @doc Initialize Top Supervisor for a Protocol
|
||||||
|
init([Type]) ->
|
||||||
|
SupFlags = #{ strategy => one_for_one
|
||||||
|
, intensity => 10
|
||||||
|
, period => 60
|
||||||
|
},
|
||||||
|
CmOpts = [{type, Type}],
|
||||||
|
CM = emqx_gateway_utils:childspec(worker, emqx_gateway_cm, [CmOpts]),
|
||||||
|
Metrics = emqx_gateway_utils:childspec(worker, emqx_gateway_metrics, [Type]),
|
||||||
|
{ok, {SupFlags, [CM, Metrics]}}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
ctx(Sup, InstaId) ->
|
||||||
|
{_, Type} = erlang:process_info(Sup, registered_name),
|
||||||
|
{ok, CM} = emqx_gateway_utils:find_sup_child(Sup, emqx_gateway_cm),
|
||||||
|
#{ instid => InstaId
|
||||||
|
, type => Type
|
||||||
|
, cm => CM
|
||||||
|
}.
|
||||||
|
|
||||||
|
is_gateway_insta_id(emqx_gateway_cm) ->
|
||||||
|
false;
|
||||||
|
is_gateway_insta_id(emqx_gateway_metrics) ->
|
||||||
|
false;
|
||||||
|
is_gateway_insta_id(_Id) ->
|
||||||
|
true.
|
|
@ -0,0 +1,312 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The gateway instance management
|
||||||
|
-module(emqx_gateway_insta_sup).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
-logger_header("[PGW-Insta-Sup]").
|
||||||
|
|
||||||
|
%% APIs
|
||||||
|
-export([ start_link/3
|
||||||
|
, info/1
|
||||||
|
, disable/1
|
||||||
|
, enable/1
|
||||||
|
, update/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([ init/1
|
||||||
|
, handle_call/3
|
||||||
|
, handle_cast/2
|
||||||
|
, handle_info/2
|
||||||
|
, terminate/2
|
||||||
|
, code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
insta :: instance(),
|
||||||
|
ctx :: emqx_gateway_ctx:context(),
|
||||||
|
status :: stopped | running,
|
||||||
|
child_pids :: [pid()],
|
||||||
|
insta_state :: emqx_gateway_impl:state() | undefined
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_link(Insta, Ctx, GwDscrptr) ->
|
||||||
|
gen_server:start_link(
|
||||||
|
{local, ?MODULE},
|
||||||
|
?MODULE,
|
||||||
|
[Insta, Ctx, GwDscrptr],
|
||||||
|
[]
|
||||||
|
).
|
||||||
|
|
||||||
|
-spec info(pid()) -> instance().
|
||||||
|
info(Pid) ->
|
||||||
|
gen_server:call(Pid, info).
|
||||||
|
|
||||||
|
%% @doc Stop instance
|
||||||
|
-spec disable(pid()) -> ok | {error, any()}.
|
||||||
|
disable(Pid) ->
|
||||||
|
call(Pid, disable).
|
||||||
|
|
||||||
|
%% @doc Start instance
|
||||||
|
-spec enable(pid()) -> ok | {error, any()}.
|
||||||
|
enable(Pid) ->
|
||||||
|
call(Pid, enable).
|
||||||
|
|
||||||
|
%% @doc Update the gateway instance configurations
|
||||||
|
-spec update(pid(), instance()) -> ok | {error, any()}.
|
||||||
|
update(Pid, NewInsta) ->
|
||||||
|
call(Pid, {update, NewInsta}).
|
||||||
|
|
||||||
|
call(Pid, Req) ->
|
||||||
|
gen_server:call(Pid, Req, 5000).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% gen_server callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([Insta, Ctx0, _GwDscrptr]) ->
|
||||||
|
process_flag(trap_exit, true),
|
||||||
|
#{rawconf := RawConf} = Insta,
|
||||||
|
Ctx = do_init_context(RawConf, Ctx0),
|
||||||
|
State = #state{
|
||||||
|
insta = Insta,
|
||||||
|
ctx = Ctx,
|
||||||
|
child_pids = [],
|
||||||
|
status = stopped
|
||||||
|
},
|
||||||
|
case cb_insta_create(State) of
|
||||||
|
{error, _Reason} ->
|
||||||
|
do_deinit_context(Ctx),
|
||||||
|
%% XXX: Return Reason??
|
||||||
|
{stop, create_gateway_instance_failed};
|
||||||
|
{ok, NState} ->
|
||||||
|
{ok, NState}
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_init_context(RawConf, Ctx) ->
|
||||||
|
Auth = case maps:get(authenticator, RawConf, allow_anonymous) of
|
||||||
|
allow_anonymous -> allow_anonymous;
|
||||||
|
Funcs when is_list(Funcs) ->
|
||||||
|
create_authenticator_for_gateway_insta(Funcs)
|
||||||
|
end,
|
||||||
|
Ctx#{auth => Auth}.
|
||||||
|
|
||||||
|
do_deinit_context(Ctx) ->
|
||||||
|
cleanup_authenticator_for_gateway_insta(maps:get(auth, Ctx)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
handle_call(info, _From, State = #state{insta = Insta}) ->
|
||||||
|
{reply, Insta, State};
|
||||||
|
|
||||||
|
handle_call(disable, _From, State = #state{status = Status}) ->
|
||||||
|
case Status of
|
||||||
|
running ->
|
||||||
|
case cb_insta_destroy(State) of
|
||||||
|
{ok, NState} ->
|
||||||
|
{reply, ok, NState};
|
||||||
|
{error, Reason} ->
|
||||||
|
{reply, {error, Reason}, State}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{reply, {error, already_stopped}, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call(enable, _From, State = #state{status = Status}) ->
|
||||||
|
case Status of
|
||||||
|
stopped ->
|
||||||
|
case cb_insta_create(State) of
|
||||||
|
{error, Reason} ->
|
||||||
|
{reply, {error, Reason}, State};
|
||||||
|
{ok, NState} ->
|
||||||
|
{reply, ok, NState}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{reply, {error, already_started}, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% Stopped -> update
|
||||||
|
handle_call({update, NewInsta}, _From, State = #state{insta = Insta,
|
||||||
|
status = stopped}) ->
|
||||||
|
case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of
|
||||||
|
true ->
|
||||||
|
{reply, ok, State#state{insta = NewInsta}};
|
||||||
|
false ->
|
||||||
|
{reply, {error, bad_instan_id}, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% Running -> update
|
||||||
|
handle_call({update, NewInsta}, _From, State = #state{insta = Insta,
|
||||||
|
status = running}) ->
|
||||||
|
case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of
|
||||||
|
true ->
|
||||||
|
case cb_insta_update(NewInsta, State) of
|
||||||
|
{ok, NState} ->
|
||||||
|
{reply, ok, NState};
|
||||||
|
{error, Reason} ->
|
||||||
|
{reply, {error, Reason}, State}
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
{reply, {error, bad_instan_id}, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
Reply = ok,
|
||||||
|
{reply, Reply, State}.
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info({'EXIT', Pid, Reason}, State = #state{child_pids = Pids}) ->
|
||||||
|
case lists:member(Pid, Pids) of
|
||||||
|
true ->
|
||||||
|
logger:error("Child process ~p exited: ~0p.", [Pid, Reason]),
|
||||||
|
case Pids -- [Pid]of
|
||||||
|
[] ->
|
||||||
|
logger:error("All child process exited!"),
|
||||||
|
{noreply, State#state{status = stopped,
|
||||||
|
child_pids = [],
|
||||||
|
insta_state = undefined}};
|
||||||
|
RemainPids ->
|
||||||
|
{noreply, State#state{child_pids = RemainPids}}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
logger:error("Unknown process exited ~p:~0p", [Pid, Reason]),
|
||||||
|
{noreply, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_info(Info, State) ->
|
||||||
|
logger:warning("Unexcepted info: ~p", [Info]),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, State = #state{ctx = Ctx, child_pids = Pids}) ->
|
||||||
|
%% Cleanup instances
|
||||||
|
%% Step1. Destory instance
|
||||||
|
Pids /= [] andalso (_ = cb_insta_destroy(State)),
|
||||||
|
%% Step2. Delete authenticator resources
|
||||||
|
_ = do_deinit_context(Ctx),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
create_authenticator_for_gateway_insta(_Funcs) ->
|
||||||
|
todo.
|
||||||
|
|
||||||
|
cleanup_authenticator_for_gateway_insta(allow_anonymouse) ->
|
||||||
|
ok;
|
||||||
|
cleanup_authenticator_for_gateway_insta(_ChainId) ->
|
||||||
|
todo.
|
||||||
|
|
||||||
|
cb_insta_destroy(State = #state{insta = Insta = #{type := Type},
|
||||||
|
insta_state = InstaState}) ->
|
||||||
|
try
|
||||||
|
#{cbkmod := CbMod,
|
||||||
|
state := GwState} = emqx_gateway_registry:lookup(Type),
|
||||||
|
CbMod:on_insta_destroy(Insta, InstaState, GwState),
|
||||||
|
{ok, State#state{child_pids = [],
|
||||||
|
insta_state = undefined,
|
||||||
|
status = stopped}}
|
||||||
|
catch
|
||||||
|
Class : Reason : Stk ->
|
||||||
|
logger:error("Destroy instance (~0p, ~0p, _) crashed: "
|
||||||
|
"{~p, ~p}, stacktrace: ~0p",
|
||||||
|
[Insta, InstaState,
|
||||||
|
Class, Reason, Stk]),
|
||||||
|
{error, {Class, Reason, Stk}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
cb_insta_create(State = #state{insta = Insta = #{type := Type},
|
||||||
|
ctx = Ctx}) ->
|
||||||
|
try
|
||||||
|
#{cbkmod := CbMod,
|
||||||
|
state := GwState} = emqx_gateway_registry:lookup(Type),
|
||||||
|
case CbMod:on_insta_create(Insta, Ctx, GwState) of
|
||||||
|
{error, Reason} -> throw({callback_return_error, Reason});
|
||||||
|
{ok, InstaPidOrSpecs, InstaState} ->
|
||||||
|
ChildPids = start_child_process(InstaPidOrSpecs),
|
||||||
|
{ok, State#state{
|
||||||
|
status = running,
|
||||||
|
child_pids = ChildPids,
|
||||||
|
insta_state = InstaState
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
Class : Reason1 : Stk ->
|
||||||
|
logger:error("Create instance (~0p, ~0p, _) crashed: "
|
||||||
|
"{~p, ~p}, stacktrace: ~0p",
|
||||||
|
[Insta, Ctx,
|
||||||
|
Class, Reason1, Stk]),
|
||||||
|
{error, {Class, Reason1, Stk}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
cb_insta_update(NewInsta,
|
||||||
|
State = #state{insta = Insta = #{type := Type},
|
||||||
|
ctx = Ctx,
|
||||||
|
insta_state = GwInstaState}) ->
|
||||||
|
try
|
||||||
|
#{cbkmod := CbMod,
|
||||||
|
state := GwState} = emqx_gateway_registry:lookup(Type),
|
||||||
|
case CbMod:on_insta_update(NewInsta, Insta, GwInstaState, GwState) of
|
||||||
|
{error, Reason} -> throw({callback_return_error, Reason});
|
||||||
|
{ok, InstaPidOrSpecs, InstaState} ->
|
||||||
|
%% XXX: Hot-upgrade ???
|
||||||
|
ChildPids = start_child_process(InstaPidOrSpecs),
|
||||||
|
{ok, State#state{
|
||||||
|
status = running,
|
||||||
|
child_pids = ChildPids,
|
||||||
|
insta_state = InstaState
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
Class : Reason1 : Stk ->
|
||||||
|
logger:error("Update instance (~0p, ~0p, ~0p, _) crashed: "
|
||||||
|
"{~p, ~p}, stacktrace: ~0p",
|
||||||
|
[NewInsta, Insta, Ctx,
|
||||||
|
Class, Reason1, Stk]),
|
||||||
|
{error, {Class, Reason1, Stk}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
start_child_process([Indictor|_] = InstaPidOrSpecs) ->
|
||||||
|
case erlang:is_pid(Indictor) of
|
||||||
|
true ->
|
||||||
|
InstaPidOrSpecs;
|
||||||
|
_ ->
|
||||||
|
do_start_child_process(InstaPidOrSpecs)
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_start_child_process(ChildSpecs) when is_list(ChildSpecs) ->
|
||||||
|
lists:map(fun do_start_child_process/1, ChildSpecs);
|
||||||
|
|
||||||
|
do_start_child_process(_ChildSpec = #{start := {M, F, A}}) ->
|
||||||
|
case erlang:apply(M, F, A) of
|
||||||
|
{ok, Pid} ->
|
||||||
|
Pid;
|
||||||
|
{error, Reason} ->
|
||||||
|
throw({start_child_process, Reason})
|
||||||
|
end.
|
|
@ -0,0 +1,101 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 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_gateway_metrics).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
-logger_header("[PGW-Metrics]").
|
||||||
|
|
||||||
|
%% APIs
|
||||||
|
-export([start_link/1]).
|
||||||
|
|
||||||
|
-export([ inc/2
|
||||||
|
, inc/3
|
||||||
|
, dec/2
|
||||||
|
, dec/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([ init/1
|
||||||
|
, handle_call/3
|
||||||
|
, handle_cast/2
|
||||||
|
, handle_info/2
|
||||||
|
, terminate/2
|
||||||
|
, code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([tabname/1]).
|
||||||
|
|
||||||
|
-record(state, {}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_link(Type) ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [Type], []).
|
||||||
|
|
||||||
|
-spec inc(gateway_type(), atom()) -> ok.
|
||||||
|
inc(Type, Name) ->
|
||||||
|
inc(Type, Name, 1).
|
||||||
|
|
||||||
|
-spec inc(gateway_type(), atom(), integer()) -> ok.
|
||||||
|
inc(Type, Name, Oct) ->
|
||||||
|
ets:update_counter(tabname(Type), Name, {2, Oct}, {Name, 0}),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
-spec dec(gateway_type(), atom()) -> ok.
|
||||||
|
dec(Type, Name) ->
|
||||||
|
inc(Type, Name, -1).
|
||||||
|
|
||||||
|
-spec dec(gateway_type(), atom(), non_neg_integer()) -> ok.
|
||||||
|
dec(Type, Name, Oct) ->
|
||||||
|
inc(Type, Name, -Oct).
|
||||||
|
|
||||||
|
tabname(Type) ->
|
||||||
|
list_to_atom(lists:concat([emqx_gateway_, Type, '_metrics'])).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% gen_server callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([Type]) ->
|
||||||
|
TabOpts = [public, {write_concurrency, true}],
|
||||||
|
ok = emqx_tables:new(tabname(Type), [set|TabOpts]),
|
||||||
|
{ok, #state{}}.
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
Reply = ok,
|
||||||
|
{reply, Reply, 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 funcs
|
||||||
|
%%--------------------------------------------------------------------
|
|
@ -0,0 +1,163 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The Registry Centre of Gateway Type
|
||||||
|
-module(emqx_gateway_registry).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
-logger_header("[PGW-Registry]").
|
||||||
|
|
||||||
|
-behavior(gen_server).
|
||||||
|
|
||||||
|
%% APIs for Impl.
|
||||||
|
-export([ load/3
|
||||||
|
, unload/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ list/0
|
||||||
|
, lookup/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% APIs
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([ init/1
|
||||||
|
, handle_call/3
|
||||||
|
, handle_cast/2
|
||||||
|
, handle_info/2
|
||||||
|
, terminate/2
|
||||||
|
, code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
loaded = #{} :: #{ gateway_type() => descriptor() }
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Mgmt
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-type registry_options() :: [registry_option()].
|
||||||
|
|
||||||
|
-type registry_option() :: {cbkmod, atom()}.
|
||||||
|
|
||||||
|
-type gateway_options() :: list().
|
||||||
|
|
||||||
|
-type descriptor() :: #{ cbkmod := atom()
|
||||||
|
, rgopts := registry_options()
|
||||||
|
, gwopts := gateway_options()
|
||||||
|
, state => any()
|
||||||
|
}.
|
||||||
|
|
||||||
|
-spec load(gateway_type(), registry_options(), gateway_options()) -> ok | {error, any()}.
|
||||||
|
load(Type, RgOpts, GwOpts) ->
|
||||||
|
CbMod = proplists:get_value(cbkmod, RgOpts, Type),
|
||||||
|
Dscrptr = #{ cbkmod => CbMod
|
||||||
|
, rgopts => RgOpts
|
||||||
|
, gwopts => GwOpts
|
||||||
|
},
|
||||||
|
call({load, Type, Dscrptr}).
|
||||||
|
|
||||||
|
-spec unload(gateway_type()) -> ok | {error, any()}.
|
||||||
|
unload(Type) ->
|
||||||
|
%% TODO: Checking ALL INSTACE HAS STOPPED
|
||||||
|
call({unload, Type}).
|
||||||
|
|
||||||
|
%% TODO:
|
||||||
|
%unload(Type, Force) ->
|
||||||
|
% call({unload, Type, Froce}).
|
||||||
|
|
||||||
|
%% @doc Return all registered protocol gateway implementation
|
||||||
|
-spec list() -> [{gateway_type(), descriptor()}].
|
||||||
|
list() ->
|
||||||
|
call(all).
|
||||||
|
|
||||||
|
-spec lookup(gateway_type()) -> undefined | descriptor().
|
||||||
|
lookup(Type) ->
|
||||||
|
call({lookup, Type}).
|
||||||
|
|
||||||
|
call(Req) ->
|
||||||
|
gen_server:call(?MODULE, Req, 5000).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% gen_server callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
%% TODO: Metrics ???
|
||||||
|
process_flag(trap_exit, true),
|
||||||
|
{ok, #state{loaded = #{}}}.
|
||||||
|
|
||||||
|
handle_call({load, Type, Dscrptr}, _From, State = #state{loaded = Gateways}) ->
|
||||||
|
case maps:get(Type, Gateways, notfound) of
|
||||||
|
notfound ->
|
||||||
|
try
|
||||||
|
GwOpts = maps:get(gwopts, Dscrptr),
|
||||||
|
CbMod = maps:get(cbkmod, Dscrptr),
|
||||||
|
{ok, GwState} = CbMod:init(GwOpts),
|
||||||
|
NDscrptr = maps:put(state, GwState, Dscrptr),
|
||||||
|
NGateways = maps:put(Type, NDscrptr, Gateways),
|
||||||
|
{reply, ok, State#state{loaded = NGateways}}
|
||||||
|
catch
|
||||||
|
Class : Reason : Stk ->
|
||||||
|
logger:error("Load ~s crashed {~p, ~p}; stacktrace: ~0p",
|
||||||
|
[Type, Class, Reason, Stk]),
|
||||||
|
{reply, {error, {Class, Reason}}, State}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{reply, {error, already_existed}, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({unload, Type}, _From, State = #state{loaded = Gateways}) ->
|
||||||
|
case maps:get(Type, Gateways, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{reply, ok, State};
|
||||||
|
_ ->
|
||||||
|
emqx_gateway_sup:stop_all_suptree(Type),
|
||||||
|
{reply, ok, State#state{loaded = maps:remove(Type, Gateways)}}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call(all, _From, State = #state{loaded = Gateways}) ->
|
||||||
|
{reply, maps:to_list(Gateways), State};
|
||||||
|
|
||||||
|
handle_call({lookup, Type}, _From, State = #state{loaded = Gateways}) ->
|
||||||
|
Reply = maps:get(Type, Gateways, undefined),
|
||||||
|
{reply, Reply, State};
|
||||||
|
|
||||||
|
handle_call(Req, _From, State) ->
|
||||||
|
logger:error("Unexpected call: ~0p", [Req]),
|
||||||
|
{reply, ok, State}.
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
|
@ -0,0 +1,178 @@
|
||||||
|
-module(emqx_gateway_schema).
|
||||||
|
|
||||||
|
-dialyzer(no_return).
|
||||||
|
-dialyzer(no_match).
|
||||||
|
-dialyzer(no_contracts).
|
||||||
|
-dialyzer(no_unused).
|
||||||
|
-dialyzer(no_fail_call).
|
||||||
|
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
|
-type flag() :: true | false.
|
||||||
|
-type duration() :: integer().
|
||||||
|
-type bytesize() :: integer().
|
||||||
|
-type comma_separated_list() :: list().
|
||||||
|
-type ip_port() :: tuple().
|
||||||
|
|
||||||
|
-typerefl_from_string({flag/0, emqx_schema, to_flag}).
|
||||||
|
-typerefl_from_string({duration/0, emqx_schema, to_duration}).
|
||||||
|
-typerefl_from_string({bytesize/0, emqx_schema, to_bytesize}).
|
||||||
|
-typerefl_from_string({comma_separated_list/0, emqx_schema, to_comma_separated_list}).
|
||||||
|
-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}).
|
||||||
|
|
||||||
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
|
-reflect_type([ flag/0
|
||||||
|
, duration/0
|
||||||
|
, bytesize/0
|
||||||
|
, comma_separated_list/0
|
||||||
|
, ip_port/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([structs/0 , fields/1]).
|
||||||
|
-export([t/1, t/3, t/4, ref/1]).
|
||||||
|
|
||||||
|
structs() -> ["emqx_gateway"].
|
||||||
|
|
||||||
|
fields("emqx_gateway") ->
|
||||||
|
[{stomp, t(ref(stomp))}];
|
||||||
|
|
||||||
|
fields(stomp) ->
|
||||||
|
[{"$id", t(ref(stomp_structs))}];
|
||||||
|
|
||||||
|
fields(stomp_structs) ->
|
||||||
|
[ {frame, t(ref(stomp_frame))}
|
||||||
|
, {clientinfo_override, t(ref(clientinfo_override))}
|
||||||
|
, {authenticator, t(union([allow_anonymous]))}
|
||||||
|
, {listener, t(ref(listener))}
|
||||||
|
];
|
||||||
|
|
||||||
|
fields(stomp_frame) ->
|
||||||
|
[ {max_headers, t(integer(), undefined, 10)}
|
||||||
|
, {max_headers_length, t(integer(), undefined, 1024)}
|
||||||
|
, {max_body_length, t(integer(), undefined, 8192)}
|
||||||
|
];
|
||||||
|
|
||||||
|
fields(clientinfo_override) ->
|
||||||
|
[ {username, t(string())}
|
||||||
|
, {password, t(string())}
|
||||||
|
, {clientid, t(string())}
|
||||||
|
];
|
||||||
|
|
||||||
|
fields(listener) ->
|
||||||
|
[ {tcp, t(ref(tcp_listener))}
|
||||||
|
, {ssl, t(ref(ssl_listener))}
|
||||||
|
];
|
||||||
|
|
||||||
|
fields(tcp_listener) ->
|
||||||
|
[ {"$name", t(ref(tcp_listener_settings))}];
|
||||||
|
|
||||||
|
fields(ssl_listener) ->
|
||||||
|
[ {"$name", t(ref(ssl_listener_settings))}];
|
||||||
|
|
||||||
|
fields(listener_settings) ->
|
||||||
|
%[ {"bind", t(union(ip_port(), integer()))}
|
||||||
|
[ {bind, t(integer())}
|
||||||
|
, {acceptors, t(integer(), undefined, 8)}
|
||||||
|
, {max_connections, t(integer(), undefined, 1024)}
|
||||||
|
, {max_conn_rate, t(integer())}
|
||||||
|
, {active_n, t(integer(), undefined, 100)}
|
||||||
|
%, {zone, t(string())}
|
||||||
|
%, {rate_limit, t(comma_separated_list())}
|
||||||
|
, {access, t(ref(access))}
|
||||||
|
, {proxy_protocol, t(flag())}
|
||||||
|
, {proxy_protocol_timeout, t(duration())}
|
||||||
|
, {backlog, t(integer(), undefined, 1024)}
|
||||||
|
, {send_timeout, t(duration(), undefined, "15s")}
|
||||||
|
, {send_timeout_close, t(flag(), undefined, true)}
|
||||||
|
, {recbuf, t(bytesize())}
|
||||||
|
, {sndbuf, t(bytesize())}
|
||||||
|
, {buffer, t(bytesize())}
|
||||||
|
, {high_watermark, t(bytesize(), undefined, "1MB")}
|
||||||
|
, {tune_buffer, t(flag())}
|
||||||
|
, {nodelay, t(boolean())}
|
||||||
|
, {reuseaddr, t(boolean())}
|
||||||
|
];
|
||||||
|
|
||||||
|
fields(tcp_listener_settings) ->
|
||||||
|
[
|
||||||
|
%% some special confs for tcp listener
|
||||||
|
] ++ fields(listener_settings);
|
||||||
|
|
||||||
|
fields(ssl_listener_settings) ->
|
||||||
|
[
|
||||||
|
%% some special confs for ssl listener
|
||||||
|
] ++
|
||||||
|
ssl(undefined, #{handshake_timeout => "15s"
|
||||||
|
, depth => 10
|
||||||
|
, reuse_sessions => true}) ++ fields(listener_settings);
|
||||||
|
|
||||||
|
fields(access) ->
|
||||||
|
[ {"$id", #{type => string(),
|
||||||
|
nullable => true}}];
|
||||||
|
|
||||||
|
fields(ExtraField) ->
|
||||||
|
Mod = list_to_atom(ExtraField++"_schema"),
|
||||||
|
Mod:fields(ExtraField).
|
||||||
|
|
||||||
|
%translations() -> [].
|
||||||
|
%
|
||||||
|
%translations(_) -> [].
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Helpers
|
||||||
|
|
||||||
|
%% types
|
||||||
|
|
||||||
|
t(Type) -> #{type => Type}.
|
||||||
|
|
||||||
|
t(Type, Mapping, Default) ->
|
||||||
|
hoconsc:t(Type, #{mapping => Mapping, default => Default}).
|
||||||
|
|
||||||
|
t(Type, Mapping, Default, OverrideEnv) ->
|
||||||
|
hoconsc:t(Type, #{ mapping => Mapping
|
||||||
|
, default => Default
|
||||||
|
, override_env => OverrideEnv
|
||||||
|
}).
|
||||||
|
|
||||||
|
ref(Field) ->
|
||||||
|
hoconsc:ref(?MODULE, Field).
|
||||||
|
|
||||||
|
%% utils
|
||||||
|
|
||||||
|
%% generate a ssl field.
|
||||||
|
%% ssl("emqx", #{"verify" => verify_peer}) will return
|
||||||
|
%% [ {"cacertfile", t(string(), "emqx.cacertfile", undefined)}
|
||||||
|
%% , {"certfile", t(string(), "emqx.certfile", undefined)}
|
||||||
|
%% , {"keyfile", t(string(), "emqx.keyfile", undefined)}
|
||||||
|
%% , {"verify", t(union(verify_peer, verify_none), "emqx.verify", verify_peer)}
|
||||||
|
%% , {"server_name_indication", "emqx.server_name_indication", undefined)}
|
||||||
|
%% ...
|
||||||
|
ssl(Mapping, Defaults) ->
|
||||||
|
M = fun (Field) ->
|
||||||
|
case (Mapping) of
|
||||||
|
undefined -> undefined;
|
||||||
|
_ -> Mapping ++ "." ++ Field
|
||||||
|
end end,
|
||||||
|
D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end,
|
||||||
|
[ {"enable", t(flag(), M("enable"), D("enable"))}
|
||||||
|
, {"cacertfile", t(string(), M("cacertfile"), D("cacertfile"))}
|
||||||
|
, {"certfile", t(string(), M("certfile"), D("certfile"))}
|
||||||
|
, {"keyfile", t(string(), M("keyfile"), D("keyfile"))}
|
||||||
|
, {"verify", t(union(verify_peer, verify_none), M("verify"), D("verify"))}
|
||||||
|
, {"fail_if_no_peer_cert", t(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))}
|
||||||
|
, {"secure_renegotiate", t(flag(), M("secure_renegotiate"), D("secure_renegotiate"))}
|
||||||
|
, {"reuse_sessions", t(flag(), M("reuse_sessions"), D("reuse_sessions"))}
|
||||||
|
, {"honor_cipher_order", t(flag(), M("honor_cipher_order"), D("honor_cipher_order"))}
|
||||||
|
, {"handshake_timeout", t(duration(), M("handshake_timeout"), D("handshake_timeout"))}
|
||||||
|
, {"depth", t(integer(), M("depth"), D("depth"))}
|
||||||
|
, {"password", hoconsc:t(string(), #{mapping => M("key_password"),
|
||||||
|
default => D("key_password"),
|
||||||
|
sensitive => true
|
||||||
|
})}
|
||||||
|
, {"dhfile", t(string(), M("dhfile"), D("dhfile"))}
|
||||||
|
, {"server_name_indication", t(union(disable, string()), M("server_name_indication"),
|
||||||
|
D("server_name_indication"))}
|
||||||
|
, {"tls_versions", t(comma_separated_list(), M("tls_versions"), D("tls_versions"))}
|
||||||
|
, {"ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))}
|
||||||
|
, {"psk_ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))}].
|
|
@ -0,0 +1,194 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 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_gateway_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
%% Gateway Instance APIs
|
||||||
|
-export([ create_gateway_insta/1
|
||||||
|
, remove_gateway_insta/1
|
||||||
|
, lookup_gateway_insta/1
|
||||||
|
, update_gateway_insta/1
|
||||||
|
, start_gateway_insta/1
|
||||||
|
, stop_gateway_insta/1
|
||||||
|
, list_gateway_insta/1
|
||||||
|
, list_gateway_insta/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Gateway APs
|
||||||
|
-export([ list_started_gateway/0
|
||||||
|
, stop_all_suptree/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% supervisor callbacks
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
-spec create_gateway_insta(instance()) -> {ok, pid()} | {error, any()}.
|
||||||
|
create_gateway_insta(Insta = #{type := Type}) ->
|
||||||
|
case emqx_gateway_registry:lookup(Type) of
|
||||||
|
undefined -> {error, {unknown_gateway_id, Type}};
|
||||||
|
GwDscrptr ->
|
||||||
|
{ok, GwSup} = ensure_gateway_suptree_ready(gatewayid(Type)),
|
||||||
|
emqx_gateway_gw_sup:create_insta(GwSup, Insta, GwDscrptr)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec remove_gateway_insta(instance_id()) -> ok | {error, any()}.
|
||||||
|
remove_gateway_insta(InstaId) ->
|
||||||
|
case search_gateway_insta_proc(InstaId) of
|
||||||
|
{ok, {GwSup, _}} ->
|
||||||
|
emqx_gateway_gw_sup:remove_insta(GwSup, InstaId);
|
||||||
|
_ ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec lookup_gateway_insta(instance_id()) -> instance() | undefined.
|
||||||
|
lookup_gateway_insta(InstaId) ->
|
||||||
|
case search_gateway_insta_proc(InstaId) of
|
||||||
|
{ok, {_, GwInstaPid}} ->
|
||||||
|
emqx_gateway_insta_sup:info(GwInstaPid);
|
||||||
|
_ ->
|
||||||
|
undefined
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec update_gateway_insta(instance())
|
||||||
|
-> ok
|
||||||
|
| {error, any()}.
|
||||||
|
update_gateway_insta(NewInsta = #{type := Type}) ->
|
||||||
|
case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of
|
||||||
|
{ok, GwSup} ->
|
||||||
|
emqx_gateway_gw_sup:update_insta(GwSup, NewInsta);
|
||||||
|
_ -> {error, not_found}
|
||||||
|
end.
|
||||||
|
|
||||||
|
start_gateway_insta(InstaId) ->
|
||||||
|
case search_gateway_insta_proc(InstaId) of
|
||||||
|
{ok, {GwSup, _}} ->
|
||||||
|
emqx_gateway_gw_sup:start_insta(GwSup, InstaId);
|
||||||
|
_ -> {error, not_found}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec stop_gateway_insta(instance_id()) -> ok | {error, any()}.
|
||||||
|
stop_gateway_insta(InstaId) ->
|
||||||
|
case search_gateway_insta_proc(InstaId) of
|
||||||
|
{ok, {GwSup, _}} ->
|
||||||
|
emqx_gateway_gw_sup:stop_insta(GwSup, InstaId);
|
||||||
|
_ -> {error, not_found}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec list_gateway_insta(gateway_type()) -> {ok, [instance()]} | {error, any()}.
|
||||||
|
list_gateway_insta(Type) ->
|
||||||
|
case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of
|
||||||
|
{ok, GwSup} ->
|
||||||
|
{ok, emqx_gateway_gw_sup:list_insta(GwSup)};
|
||||||
|
_ -> {error, not_found}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec list_gateway_insta() -> [{gateway_type(), instance()}].
|
||||||
|
list_gateway_insta() ->
|
||||||
|
lists:map(
|
||||||
|
fun(SupId) ->
|
||||||
|
Instas = emqx_gateway_gw_sup:list_insta(SupId),
|
||||||
|
{SupId, Instas}
|
||||||
|
end, list_started_gateway()).
|
||||||
|
|
||||||
|
-spec list_started_gateway() -> [gateway_type()].
|
||||||
|
list_started_gateway() ->
|
||||||
|
started_gateway_type().
|
||||||
|
|
||||||
|
-spec stop_all_suptree(atom()) -> ok.
|
||||||
|
stop_all_suptree(Type) ->
|
||||||
|
case lists:keyfind(Type, 1, supervisor:which_children(?MODULE)) of
|
||||||
|
false -> ok;
|
||||||
|
_ ->
|
||||||
|
_ = supervisor:terminate_child(?MODULE, Type),
|
||||||
|
_ = supervisor:delete_child(?MODULE, Type),
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Supervisor callback
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
SupFlags = #{ strategy => one_for_one
|
||||||
|
, intensity => 10
|
||||||
|
, period => 60
|
||||||
|
},
|
||||||
|
ChildSpecs = [ emqx_gateway_utils:childspec(worker, emqx_gateway_registry)
|
||||||
|
],
|
||||||
|
{ok, {SupFlags, ChildSpecs}}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
gatewayid(Type) ->
|
||||||
|
list_to_atom(lists:concat([Type])).
|
||||||
|
|
||||||
|
ensure_gateway_suptree_ready(Type) ->
|
||||||
|
case lists:keyfind(Type, 1, supervisor:which_children(?MODULE)) of
|
||||||
|
false ->
|
||||||
|
ChildSpec = emqx_gateway_utils:childspec(
|
||||||
|
Type,
|
||||||
|
supervisor,
|
||||||
|
emqx_gateway_gw_sup,
|
||||||
|
[Type]
|
||||||
|
),
|
||||||
|
emqx_gateway_utils:supervisor_ret(
|
||||||
|
supervisor:start_child(?MODULE, ChildSpec)
|
||||||
|
);
|
||||||
|
{_Id, Pid, _Type, _Mods} ->
|
||||||
|
{ok, Pid}
|
||||||
|
end.
|
||||||
|
|
||||||
|
search_gateway_insta_proc(InstaId) ->
|
||||||
|
search_gateway_insta_proc(InstaId, started_gateway_pid()).
|
||||||
|
|
||||||
|
search_gateway_insta_proc(_InstaId, []) ->
|
||||||
|
{error, not_found};
|
||||||
|
search_gateway_insta_proc(InstaId, [SupPid|More]) ->
|
||||||
|
case emqx_gateway_utils:find_sup_child(SupPid, InstaId) of
|
||||||
|
{ok, InstaPid} -> {ok, {SupPid, InstaPid}};
|
||||||
|
_ ->
|
||||||
|
search_gateway_insta_proc(InstaId, More)
|
||||||
|
end.
|
||||||
|
|
||||||
|
started_gateway_type() ->
|
||||||
|
lists:filtermap(
|
||||||
|
fun({Id, _, _, _}) ->
|
||||||
|
is_a_gateway_id(Id) andalso {true, Id}
|
||||||
|
end, supervisor:which_children(?MODULE)).
|
||||||
|
|
||||||
|
started_gateway_pid() ->
|
||||||
|
lists:filtermap(
|
||||||
|
fun({Id, Pid, _, _}) ->
|
||||||
|
is_a_gateway_id(Id) andalso {true, Pid}
|
||||||
|
end, supervisor:which_children(?MODULE)).
|
||||||
|
|
||||||
|
is_a_gateway_id(Id) ->
|
||||||
|
Id /= emqx_gateway_registry.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Utils funcs for emqx-gateway
|
||||||
|
-module(emqx_gateway_utils).
|
||||||
|
|
||||||
|
-export([ childspec/2
|
||||||
|
, childspec/3
|
||||||
|
, childspec/4
|
||||||
|
, supervisor_ret/1
|
||||||
|
, find_sup_child/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ apply/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ normalize_rawconf/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Common Envs
|
||||||
|
-export([ active_n/1
|
||||||
|
, ratelimit/1
|
||||||
|
, frame_options/1
|
||||||
|
, init_gc_state/1
|
||||||
|
, stats_timer/1
|
||||||
|
, idle_timeout/1
|
||||||
|
, oom_policy/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(ACTIVE_N, 100).
|
||||||
|
-define(DEFAULT_IDLE_TIMEOUT, 30000).
|
||||||
|
|
||||||
|
-spec childspec(supervisor:worker(), Mod :: atom())
|
||||||
|
-> supervisor:child_spec().
|
||||||
|
childspec(Type, Mod) ->
|
||||||
|
childspec(Mod, Type, Mod, []).
|
||||||
|
|
||||||
|
-spec childspec(supervisor:worker(), Mod :: atom(), Args :: list())
|
||||||
|
-> supervisor:child_spec().
|
||||||
|
childspec(Type, Mod, Args) ->
|
||||||
|
childspec(Mod, Type, Mod, Args).
|
||||||
|
|
||||||
|
-spec childspec(atom(), supervisor:worker(), Mod :: atom(), Args :: list())
|
||||||
|
-> supervisor:child_spec().
|
||||||
|
childspec(Id, Type, Mod, Args) ->
|
||||||
|
#{ id => Id
|
||||||
|
, start => {Mod, start_link, Args}
|
||||||
|
, type => Type
|
||||||
|
}.
|
||||||
|
|
||||||
|
-spec supervisor_ret(supervisor:startchild_ret())
|
||||||
|
-> {ok, pid()}
|
||||||
|
| {error, supervisor:startchild_err()}.
|
||||||
|
supervisor_ret({ok, Pid, _Info}) -> {ok, Pid};
|
||||||
|
supervisor_ret(Ret) -> Ret.
|
||||||
|
|
||||||
|
-spec find_sup_child(Sup :: pid() | atom(), ChildId :: supervisor:child_id())
|
||||||
|
-> false
|
||||||
|
| {ok, pid()}.
|
||||||
|
find_sup_child(Sup, ChildId) ->
|
||||||
|
case lists:keyfind(ChildId, 1, supervisor:which_children(Sup)) of
|
||||||
|
false -> false;
|
||||||
|
{_Id, Pid, _Type, _Mods} -> {ok, Pid}
|
||||||
|
end.
|
||||||
|
|
||||||
|
apply({M, F, A}, A2) when is_atom(M),
|
||||||
|
is_atom(M),
|
||||||
|
is_list(A),
|
||||||
|
is_list(A2) ->
|
||||||
|
erlang:apply(M, F, A ++ A2);
|
||||||
|
apply({F, A}, A2) when is_function(F),
|
||||||
|
is_list(A),
|
||||||
|
is_list(A2) ->
|
||||||
|
erlang:apply(F, A ++ A2);
|
||||||
|
apply(F, A2) when is_function(F),
|
||||||
|
is_list(A2) ->
|
||||||
|
erlang:apply(F, A2).
|
||||||
|
|
||||||
|
-type listener() :: #{}.
|
||||||
|
|
||||||
|
-type rawconf() ::
|
||||||
|
#{ clientinfo_override => #{}
|
||||||
|
, authenticators := #{}
|
||||||
|
, listeners => listener()
|
||||||
|
, atom() => any()
|
||||||
|
}.
|
||||||
|
|
||||||
|
-spec normalize_rawconf(rawconf())
|
||||||
|
-> list({ Type :: udp | tcp | ssl | dtls
|
||||||
|
, ListenOn :: esockd:listen_on()
|
||||||
|
, SocketOpts :: esockd:option()
|
||||||
|
, Cfg :: map()
|
||||||
|
}).
|
||||||
|
normalize_rawconf(RawConf = #{listener := LisMap}) ->
|
||||||
|
Cfg0 = maps:without([listener], RawConf),
|
||||||
|
lists:append(maps:fold(fun(Type, Liss, AccIn1) ->
|
||||||
|
Listeners =
|
||||||
|
maps:fold(fun(_Name, Confs, AccIn2) ->
|
||||||
|
ListenOn = maps:get(bind, Confs),
|
||||||
|
SocketOpts = esockd:parse_opt(maps:to_list(Confs)),
|
||||||
|
RemainCfgs = maps:without(
|
||||||
|
[bind] ++ proplists:get_keys(SocketOpts),
|
||||||
|
Confs),
|
||||||
|
Cfg = maps:merge(Cfg0, RemainCfgs),
|
||||||
|
[{Type, ListenOn, SocketOpts, Cfg}|AccIn2]
|
||||||
|
end, [], Liss),
|
||||||
|
[Listeners|AccIn1]
|
||||||
|
end, [], LisMap)).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Envs
|
||||||
|
|
||||||
|
active_n(Options) ->
|
||||||
|
maps:get(
|
||||||
|
active_n,
|
||||||
|
maps:get(listener, Options, #{active_n => ?ACTIVE_N}),
|
||||||
|
?ACTIVE_N
|
||||||
|
).
|
||||||
|
|
||||||
|
-spec idle_timeout(map()) -> pos_integer().
|
||||||
|
idle_timeout(Options) ->
|
||||||
|
maps:get(idle_timeout, Options, ?DEFAULT_IDLE_TIMEOUT).
|
||||||
|
|
||||||
|
-spec ratelimit(map()) -> esockd_rate_limit:config() | undefined.
|
||||||
|
ratelimit(Options) ->
|
||||||
|
maps:get(ratelimit, Options, undefined).
|
||||||
|
|
||||||
|
-spec frame_options(map()) -> map().
|
||||||
|
frame_options(Options) ->
|
||||||
|
maps:get(frame, Options, #{}).
|
||||||
|
|
||||||
|
-spec init_gc_state(map()) -> emqx_gc:gc_state() | undefined.
|
||||||
|
init_gc_state(Options) ->
|
||||||
|
emqx_misc:maybe_apply(fun emqx_gc:init/1, force_gc_policy(Options)).
|
||||||
|
|
||||||
|
-spec force_gc_policy(map()) -> emqx_gc:opts() | undefined.
|
||||||
|
force_gc_policy(Options) ->
|
||||||
|
maps:get(force_gc_policy, Options, undefined).
|
||||||
|
|
||||||
|
-spec oom_policy(map()) -> emqx_types:oom_policy().
|
||||||
|
oom_policy(Options) ->
|
||||||
|
maps:get(force_shutdown_policy, Options).
|
||||||
|
|
||||||
|
-spec stats_timer(map()) -> undefined | disabled.
|
||||||
|
stats_timer(Options) ->
|
||||||
|
case enable_stats(Options) of true -> undefined; false -> disabled end.
|
||||||
|
|
||||||
|
-spec enable_stats(map()) -> boolean().
|
||||||
|
enable_stats(Options) ->
|
||||||
|
maps:get(enable_stats, Options, true).
|
|
@ -1,13 +1,12 @@
|
||||||
|
|
||||||
emqx-stomp
|
# emqx-stomp
|
||||||
==========
|
|
||||||
|
|
||||||
The plugin adds STOMP 1.0/1.1/1.2 protocol supports to the EMQ X broker.
|
The plugin adds STOMP 1.0/1.1/1.2 protocol supports to the EMQ X broker.
|
||||||
|
|
||||||
The STOMP clients could PubSub to the MQTT clients.
|
The STOMP clients could PubSub to the MQTT clients.
|
||||||
|
|
||||||
Configuration
|
## Configuration
|
||||||
-------------
|
|
||||||
|
|
||||||
etc/emqx_stomp.conf
|
etc/emqx_stomp.conf
|
||||||
|
|
||||||
|
@ -58,20 +57,17 @@ stomp.frame.max_header_length = 1024
|
||||||
stomp.frame.max_body_length = 8192
|
stomp.frame.max_body_length = 8192
|
||||||
```
|
```
|
||||||
|
|
||||||
Load the Plugin
|
## Load the Plugin
|
||||||
---------------
|
|
||||||
|
|
||||||
```
|
```
|
||||||
./bin/emqx_ctl plugins load emqx_stomp
|
./bin/emqx_ctl plugins load emqx_stomp
|
||||||
```
|
```
|
||||||
|
|
||||||
License
|
## License
|
||||||
-------
|
|
||||||
|
|
||||||
Apache License Version 2.0
|
Apache License Version 2.0
|
||||||
|
|
||||||
Author
|
## Author
|
||||||
------
|
|
||||||
|
|
||||||
EMQ X Team.
|
EMQ X Team.
|
||||||
|
|
|
@ -0,0 +1,978 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020-2021 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_stomp_channel).
|
||||||
|
|
||||||
|
-include("src/stomp/include/emqx_stomp.hrl").
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-logger_header("[Stomp-Proto]").
|
||||||
|
|
||||||
|
-import(proplists, [get_value/2, get_value/3]).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([ info/1
|
||||||
|
, info/2
|
||||||
|
, stats/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ init/2
|
||||||
|
, handle_in/2
|
||||||
|
, handle_out/3
|
||||||
|
, handle_deliver/2
|
||||||
|
, handle_timeout/3
|
||||||
|
, terminate/2
|
||||||
|
, set_conn_state/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ handle_call/2
|
||||||
|
, handle_info/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% for trans callback
|
||||||
|
-export([ handle_recv_send_frame/2
|
||||||
|
, handle_recv_ack_frame/2
|
||||||
|
, handle_recv_nack_frame/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(channel, {
|
||||||
|
%% Context
|
||||||
|
ctx :: emqx_gateway_ctx:context(),
|
||||||
|
%% Stomp Connection Info
|
||||||
|
conninfo :: emqx_types:conninfo(),
|
||||||
|
%% Stomp Client Info
|
||||||
|
clientinfo :: emqx_types:clientinfo(),
|
||||||
|
%% ClientInfo override specs
|
||||||
|
clientinfo_override :: map(),
|
||||||
|
%% Connection Channel
|
||||||
|
conn_state :: conn_state(),
|
||||||
|
%% Heartbeat
|
||||||
|
heartbeat :: emqx_stomp_heartbeat:heartbeat(),
|
||||||
|
%% Subscriptions
|
||||||
|
subscriptions = [],
|
||||||
|
%% Timer
|
||||||
|
timers :: #{atom() => disable | undefined | reference()},
|
||||||
|
%% Transaction
|
||||||
|
transaction :: #{binary() => list()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-type(channel() :: #channel{}).
|
||||||
|
|
||||||
|
-type(conn_state() :: idle | connecting | connected | disconnected).
|
||||||
|
|
||||||
|
-type(reply() :: {outgoing, stomp_frame()}
|
||||||
|
| {outgoing, [stomp_frame()]}
|
||||||
|
| {event, conn_state()|updated}
|
||||||
|
| {close, Reason :: atom()}).
|
||||||
|
|
||||||
|
-type(replies() :: emqx_stomp_frame:packet() | reply() | [reply()]).
|
||||||
|
|
||||||
|
-define(TIMER_TABLE, #{
|
||||||
|
incoming_timer => incoming,
|
||||||
|
outgoing_timer => outgoing,
|
||||||
|
clean_trans_timer => clean_trans
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(TRANS_TIMEOUT, 60000).
|
||||||
|
|
||||||
|
-define(DEFAULT_OVERRIDE,
|
||||||
|
#{ clientid => <<"">> %% Generate clientid by default
|
||||||
|
, username => <<"${Packet.headers.login}">>
|
||||||
|
, password => <<"${Packet.headers.passcode}">>
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session, will_msg]).
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, [init/2,enrich_conninfo/2,ensure_connected/1,
|
||||||
|
process_connect/1,handle_in/2,handle_info/2,
|
||||||
|
ensure_disconnected/2,reverse_heartbeats/1,
|
||||||
|
negotiate_version/2]}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Init the channel
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Init protocol
|
||||||
|
init(ConnInfo = #{peername := {PeerHost, _},
|
||||||
|
sockname := {_, SockPort}}, Option) ->
|
||||||
|
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||||
|
Mountpoint = maps:get(mountpoint, Option, undefined),
|
||||||
|
ClientInfo = setting_peercert_infos(
|
||||||
|
Peercert,
|
||||||
|
#{ zone => undefined
|
||||||
|
, protocol => stomp
|
||||||
|
, peerhost => PeerHost
|
||||||
|
, sockport => SockPort
|
||||||
|
, clientid => undefined
|
||||||
|
, username => undefined
|
||||||
|
, is_bridge => false
|
||||||
|
, is_superuser => false
|
||||||
|
, mountpoint => Mountpoint
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
Ctx = maps:get(ctx, Option),
|
||||||
|
Override = maps:merge(?DEFAULT_OVERRIDE,
|
||||||
|
maps:get(clientinfo_override, Option, #{})
|
||||||
|
),
|
||||||
|
#channel{ ctx = Ctx
|
||||||
|
, conninfo = ConnInfo
|
||||||
|
, clientinfo = ClientInfo
|
||||||
|
, clientinfo_override = Override
|
||||||
|
, timers = #{}
|
||||||
|
, transaction = #{}
|
||||||
|
}.
|
||||||
|
|
||||||
|
setting_peercert_infos(NoSSL, ClientInfo)
|
||||||
|
when NoSSL =:= nossl;
|
||||||
|
NoSSL =:= undefined ->
|
||||||
|
ClientInfo;
|
||||||
|
setting_peercert_infos(Peercert, ClientInfo) ->
|
||||||
|
{DN, CN} = {esockd_peercert:subject(Peercert),
|
||||||
|
esockd_peercert:common_name(Peercert)},
|
||||||
|
ClientInfo#{dn => DN, cn => CN}.
|
||||||
|
|
||||||
|
-spec info(channel()) -> emqx_types:infos().
|
||||||
|
info(Channel) ->
|
||||||
|
maps:from_list(info(?INFO_KEYS, Channel)).
|
||||||
|
|
||||||
|
-spec(info(list(atom())|atom(), channel()) -> term()).
|
||||||
|
info(Keys, Channel) when is_list(Keys) ->
|
||||||
|
[{Key, info(Key, Channel)} || Key <- Keys];
|
||||||
|
|
||||||
|
info(conninfo, #channel{conninfo = ConnInfo}) ->
|
||||||
|
ConnInfo;
|
||||||
|
info(conn_state, #channel{conn_state = ConnState}) ->
|
||||||
|
ConnState;
|
||||||
|
info(clientinfo, #channel{clientinfo = ClientInfo}) ->
|
||||||
|
ClientInfo;
|
||||||
|
info(session, _) ->
|
||||||
|
#{};
|
||||||
|
info(will_msg, _) ->
|
||||||
|
undefined;
|
||||||
|
info(clientid, #channel{clientinfo = #{clientid := ClientId}}) ->
|
||||||
|
ClientId;
|
||||||
|
info(ctx, #channel{ctx = Ctx}) ->
|
||||||
|
Ctx.
|
||||||
|
|
||||||
|
stats(_Channel) ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
set_conn_state(ConnState, Channel) ->
|
||||||
|
Channel#channel{conn_state = ConnState}.
|
||||||
|
|
||||||
|
enrich_conninfo(_Packet,
|
||||||
|
Channel = #channel{conninfo = ConnInfo}) ->
|
||||||
|
%% XXX: How enrich more infos?
|
||||||
|
NConnInfo = ConnInfo#{ proto_name => <<"STOMP">>
|
||||||
|
, proto_ver => undefined
|
||||||
|
, clean_start => true
|
||||||
|
, keepalive => 0
|
||||||
|
, expiry_interval => 0
|
||||||
|
},
|
||||||
|
{ok, Channel#channel{conninfo = NConnInfo}}.
|
||||||
|
|
||||||
|
run_conn_hooks(Packet, Channel = #channel{ctx = Ctx,
|
||||||
|
conninfo = ConnInfo}) ->
|
||||||
|
%% XXX: Assign headers of Packet to ConnProps
|
||||||
|
ConnProps = #{},
|
||||||
|
case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of
|
||||||
|
Error = {error, _Reason} -> Error;
|
||||||
|
_NConnProps ->
|
||||||
|
{ok, Packet, Channel}
|
||||||
|
end.
|
||||||
|
|
||||||
|
negotiate_version(#stomp_frame{headers = Headers},
|
||||||
|
Channel = #channel{conninfo = ConnInfo}) ->
|
||||||
|
%% XXX:
|
||||||
|
case do_negotiate_version(header(<<"accept-version">>, Headers)) of
|
||||||
|
{ok, Version} ->
|
||||||
|
{ok, Channel#channel{conninfo = ConnInfo#{proto_ver => Version}}};
|
||||||
|
{error, Reason}->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
enrich_clientinfo(Packet,
|
||||||
|
Channel = #channel{
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo0,
|
||||||
|
clientinfo_override = Override}) ->
|
||||||
|
ClientInfo = write_clientinfo(
|
||||||
|
feedvar(Override, Packet, ConnInfo, ClientInfo0),
|
||||||
|
ClientInfo0
|
||||||
|
),
|
||||||
|
{ok, NPacket, NClientInfo} = emqx_misc:pipeline(
|
||||||
|
[ fun maybe_assign_clientid/2
|
||||||
|
, fun parse_heartbeat/2
|
||||||
|
%% FIXME: CALL After authentication successfully
|
||||||
|
, fun fix_mountpoint/2
|
||||||
|
], Packet, ClientInfo
|
||||||
|
),
|
||||||
|
{ok, NPacket, Channel#channel{clientinfo = NClientInfo}}.
|
||||||
|
|
||||||
|
feedvar(Override, Packet, ConnInfo, ClientInfo) ->
|
||||||
|
Envs = #{ 'ConnInfo' => ConnInfo
|
||||||
|
, 'ClientInfo' => ClientInfo
|
||||||
|
, 'Packet' => connect_packet_to_map(Packet)
|
||||||
|
},
|
||||||
|
maps:map(fun(_K, V) ->
|
||||||
|
Tokens = emqx_rule_utils:preproc_tmpl(V),
|
||||||
|
emqx_rule_utils:proc_tmpl(Tokens, Envs)
|
||||||
|
end, Override).
|
||||||
|
|
||||||
|
connect_packet_to_map(#stomp_frame{headers = Headers}) ->
|
||||||
|
#{headers => maps:from_list(Headers)}.
|
||||||
|
|
||||||
|
write_clientinfo(Override, ClientInfo) ->
|
||||||
|
Override1 = maps:with([username, password, clientid], Override),
|
||||||
|
maps:merge(ClientInfo, Override1).
|
||||||
|
|
||||||
|
maybe_assign_clientid(_Packet, ClientInfo = #{clientid := ClientId})
|
||||||
|
when ClientId == undefined;
|
||||||
|
ClientId == <<>> ->
|
||||||
|
{ok, ClientInfo#{clientid => emqx_guid:to_base62(emqx_guid:gen())}};
|
||||||
|
|
||||||
|
maybe_assign_clientid(_Packet, ClientInfo) ->
|
||||||
|
{ok, ClientInfo}.
|
||||||
|
|
||||||
|
parse_heartbeat(#stomp_frame{headers = Headers}, ClientInfo) ->
|
||||||
|
Heartbeat0 = header(<<"heart-beat">>, Headers, <<"0,0">>),
|
||||||
|
CxCy = re:split(Heartbeat0, <<",">>, [{return, list}]),
|
||||||
|
Heartbeat = list_to_tuple([list_to_integer(S) || S <- CxCy]),
|
||||||
|
{ok, ClientInfo#{heartbeat => Heartbeat}}.
|
||||||
|
|
||||||
|
fix_mountpoint(_Packet, #{mountpoint := undefined}) -> ok;
|
||||||
|
fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) ->
|
||||||
|
%% TODO: Enrich the varibale replacement????
|
||||||
|
%% i.e: ${ClientInfo.auth_result.productKey}
|
||||||
|
Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo),
|
||||||
|
{ok, ClientInfo#{mountpoint := Mountpoint1}}.
|
||||||
|
|
||||||
|
set_log_meta(_Packet, #channel{clientinfo = #{clientid := ClientId}}) ->
|
||||||
|
emqx_logger:set_metadata_clientid(ClientId),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
auth_connect(_Packet, Channel = #channel{ctx = Ctx,
|
||||||
|
clientinfo = ClientInfo}) ->
|
||||||
|
#{clientid := ClientId,
|
||||||
|
username := Username} = ClientInfo,
|
||||||
|
case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of
|
||||||
|
{ok, NClientInfo} ->
|
||||||
|
{ok, Channel#channel{clientinfo = NClientInfo}};
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p",
|
||||||
|
[ClientId, Username, Reason]),
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_connected(Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo}) ->
|
||||||
|
NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)},
|
||||||
|
ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]),
|
||||||
|
Channel#channel{conninfo = NConnInfo,
|
||||||
|
conn_state = connected
|
||||||
|
}.
|
||||||
|
|
||||||
|
process_connect(Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo
|
||||||
|
}) ->
|
||||||
|
SessFun = fun(_,_) -> #{} end,
|
||||||
|
case emqx_gateway_ctx:open_session(
|
||||||
|
Ctx,
|
||||||
|
true,
|
||||||
|
ClientInfo,
|
||||||
|
ConnInfo,
|
||||||
|
SessFun
|
||||||
|
) of
|
||||||
|
{ok, _Sess} -> %% The stomp protocol doesn't have session
|
||||||
|
#{proto_ver := Version} = ConnInfo,
|
||||||
|
#{heartbeat := Heartbeat} = ClientInfo,
|
||||||
|
Headers = [{<<"version">>, Version},
|
||||||
|
{<<"heart-beat">>, reverse_heartbeats(Heartbeat)}],
|
||||||
|
handle_out(connected, Headers, Channel);
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "Failed to open session du to ~p", [Reason]),
|
||||||
|
Headers = [{<<"version">>, <<"1.0,1.1,1.2">>},
|
||||||
|
{<<"content-type">>, <<"text/plain">>}],
|
||||||
|
handle_out(connerr, {Headers, undefined, <<"Not Authenticated">>}, Channel)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle incoming packet
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec handle_in(emqx_types:packet(), channel())
|
||||||
|
-> {ok, channel()}
|
||||||
|
| {ok, replies(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), replies(), channel()}.
|
||||||
|
|
||||||
|
handle_in(Frame = ?PACKET(?CMD_STOMP), Channel) ->
|
||||||
|
handle_in(Frame#stomp_frame{command = <<"CONNECT">>}, Channel);
|
||||||
|
|
||||||
|
handle_in(?PACKET(?CMD_CONNECT),
|
||||||
|
Channel = #channel{conn_state = connected}) ->
|
||||||
|
{error, unexpected_connect, Channel};
|
||||||
|
|
||||||
|
handle_in(Packet = ?PACKET(?CMD_CONNECT), Channel) ->
|
||||||
|
case emqx_misc:pipeline(
|
||||||
|
[ fun enrich_conninfo/2
|
||||||
|
, fun run_conn_hooks/2
|
||||||
|
, fun negotiate_version/2
|
||||||
|
, fun enrich_clientinfo/2
|
||||||
|
, fun set_log_meta/2
|
||||||
|
%% TODO: How to implement the banned in the gateway instance?
|
||||||
|
%, fun check_banned/2
|
||||||
|
, fun auth_connect/2
|
||||||
|
], Packet, Channel#channel{conn_state = connecting}) of
|
||||||
|
{ok, _NPacket, NChannel} ->
|
||||||
|
process_connect(ensure_connected(NChannel));
|
||||||
|
{error, ReasonCode, NChannel} ->
|
||||||
|
ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]),
|
||||||
|
handle_out(connerr, {[], undefined, ErrMsg}, NChannel)
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_in(Frame = ?PACKET(?CMD_SEND, Headers),
|
||||||
|
Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
clientinfo = ClientInfo
|
||||||
|
}) ->
|
||||||
|
Topic = header(<<"destination">>, Headers),
|
||||||
|
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of
|
||||||
|
deny ->
|
||||||
|
handle_out(error, {receipt_id(Headers), "ACL Deny"}, Channel);
|
||||||
|
allow ->
|
||||||
|
case header(<<"transaction">>, Headers) of
|
||||||
|
undefined ->
|
||||||
|
handle_recv_send_frame(Frame, Channel);
|
||||||
|
TxId ->
|
||||||
|
add_action(TxId, {fun ?MODULE:handle_recv_send_frame/2, [Frame]}, receipt_id(Headers), Channel)
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_in(?PACKET(?CMD_SUBSCRIBE, Headers),
|
||||||
|
Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
subscriptions = Subs,
|
||||||
|
clientinfo = ClientInfo = #{mountpoint := Mountpoint}
|
||||||
|
}) ->
|
||||||
|
SubId = header(<<"id">>, Headers),
|
||||||
|
Topic = header(<<"destination">>, Headers),
|
||||||
|
Ack = header(<<"ack">>, Headers, <<"auto">>),
|
||||||
|
|
||||||
|
MountedTopic = emqx_mountpoint:mount(Mountpoint, Topic),
|
||||||
|
|
||||||
|
case lists:keyfind(SubId, 1, Subs) of
|
||||||
|
{SubId, MountedTopic, Ack} ->
|
||||||
|
maybe_outgoing_receipt(receipt_id(Headers), Channel);
|
||||||
|
{SubId, _OtherTopic, _OtherAck} ->
|
||||||
|
%% FIXME:
|
||||||
|
?LOG(error, "Conflicts with subscribed topics ~s, id: ~s",
|
||||||
|
[_OtherTopic, SubId]),
|
||||||
|
ErrMsg = "Conflict subscribe id ",
|
||||||
|
handle_out(error, {receipt_id(Headers), ErrMsg}, Channel);
|
||||||
|
false ->
|
||||||
|
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic) of
|
||||||
|
deny ->
|
||||||
|
handle_out(error, {receipt_id(Headers), "ACL Deny"}, Channel);
|
||||||
|
allow ->
|
||||||
|
_ = emqx_broker:subscribe(MountedTopic),
|
||||||
|
maybe_outgoing_receipt(
|
||||||
|
receipt_id(Headers),
|
||||||
|
Channel#channel{subscriptions = [{SubId, MountedTopic, Ack} | Subs]}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_in(?PACKET(?CMD_UNSUBSCRIBE, Headers),
|
||||||
|
Channel = #channel{subscriptions = Subs}) ->
|
||||||
|
SubId = header(<<"id">>, Headers),
|
||||||
|
{ok, NChannel} = case lists:keyfind(SubId, 1, Subs) of
|
||||||
|
{SubId, Topic, _Ack} ->
|
||||||
|
ok = emqx_broker:unsubscribe(Topic),
|
||||||
|
{ok, Channel#channel{subscriptions = lists:keydelete(SubId, 1, Subs)}};
|
||||||
|
false ->
|
||||||
|
{ok, Channel}
|
||||||
|
end,
|
||||||
|
handle_out(receipt, receipt_id(Headers), NChannel);
|
||||||
|
|
||||||
|
%% XXX: How to ack a frame ???
|
||||||
|
handle_in(Frame = ?PACKET(?CMD_ACK, Headers), Channel) ->
|
||||||
|
case header(<<"transaction">>, Headers) of
|
||||||
|
undefined -> handle_recv_ack_frame(Frame, Channel);
|
||||||
|
TxId -> add_action(TxId, {fun ?MODULE:handle_recv_ack_frame/2, [Frame]}, receipt_id(Headers), Channel)
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% NACK
|
||||||
|
%% id:12345
|
||||||
|
%% transaction:tx1
|
||||||
|
%%
|
||||||
|
%% ^@
|
||||||
|
handle_in(Frame = ?PACKET(?CMD_NACK, Headers), Channel) ->
|
||||||
|
case header(<<"transaction">>, Headers) of
|
||||||
|
undefined -> handle_recv_nack_frame(Frame, Channel);
|
||||||
|
TxId -> add_action(TxId, {fun ?MODULE:handle_recv_nack_frame/2, [Frame]}, receipt_id(Headers), Channel)
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% The transaction header is REQUIRED, and the transaction identifier
|
||||||
|
%% will be used for SEND, COMMIT, ABORT, ACK, and NACK frames to bind
|
||||||
|
%% them to the named transaction.
|
||||||
|
%%
|
||||||
|
%% BEGIN
|
||||||
|
%% transaction:tx1
|
||||||
|
%%
|
||||||
|
%% ^@
|
||||||
|
handle_in(?PACKET(?CMD_BEGIN, Headers),
|
||||||
|
Channel = #channel{transaction = Trans}) ->
|
||||||
|
TxId = header(<<"transaction">>, Headers),
|
||||||
|
case maps:get(TxId, Trans, undefined) of
|
||||||
|
undefined ->
|
||||||
|
StartedAt = erlang:system_time(millisecond),
|
||||||
|
NChannel = ensure_clean_trans_timer(
|
||||||
|
Channel#channel{
|
||||||
|
transaction = Trans#{TxId => {StartedAt, []}}}
|
||||||
|
),
|
||||||
|
handle_out(receipt, receipt_id(Headers), NChannel);
|
||||||
|
_ ->
|
||||||
|
ErrMsg = ["Transaction ", TxId, " already started"],
|
||||||
|
handle_out(error, {receipt_id(Headers), ErrMsg}, Channel)
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% COMMIT
|
||||||
|
%% transaction:tx1
|
||||||
|
%%
|
||||||
|
%% ^@
|
||||||
|
handle_in(?PACKET(?CMD_COMMIT, Headers), Channel) ->
|
||||||
|
with_transaction(Headers, Channel, fun(TxId, Actions) ->
|
||||||
|
Chann0 = remove_trans(TxId, Channel),
|
||||||
|
case trans_pipeline(lists:reverse(Actions), [], Chann0) of
|
||||||
|
{ok, Outgoings, Chann1} ->
|
||||||
|
maybe_outgoing_receipt(receipt_id(Headers), Outgoings, Chann1);
|
||||||
|
{error, Reason} ->
|
||||||
|
%% FIXME: atomic for transaction ??
|
||||||
|
ErrMsg = io_lib:format("Execute transaction ~s falied: ~0p",
|
||||||
|
[TxId, Reason]
|
||||||
|
),
|
||||||
|
handle_out(error, {receipt_id(Headers), ErrMsg}, Chann0)
|
||||||
|
end
|
||||||
|
end);
|
||||||
|
|
||||||
|
%% ABORT
|
||||||
|
%% transaction:tx1
|
||||||
|
%%
|
||||||
|
%% ^@
|
||||||
|
handle_in(?PACKET(?CMD_ABORT, Headers),
|
||||||
|
Channel = #channel{transaction = Trans}) ->
|
||||||
|
with_transaction(Headers, Channel, fun(Id, _Actions) ->
|
||||||
|
NChannel = Channel#channel{transaction = maps:remove(Id, Trans)},
|
||||||
|
handle_out(receipt, receipt_id(Headers), NChannel)
|
||||||
|
end);
|
||||||
|
|
||||||
|
handle_in(?PACKET(?CMD_DISCONNECT, Headers), Channel) ->
|
||||||
|
shutdown_with_recepit(normal, receipt_id(Headers), Channel);
|
||||||
|
|
||||||
|
handle_in({frame_error, Reason}, Channel = #channel{conn_state = _ConnState}) ->
|
||||||
|
?LOG(error, "Unexpected frame error: ~p", [Reason]),
|
||||||
|
shutdown(Reason, Channel).
|
||||||
|
|
||||||
|
with_transaction(Headers, Channel = #channel{transaction = Trans}, Fun) ->
|
||||||
|
Id = header(<<"transaction">>, Headers),
|
||||||
|
ReceiptId = receipt_id(Headers),
|
||||||
|
case maps:get(Id, Trans, undefined) of
|
||||||
|
{_, Actions} ->
|
||||||
|
Fun(Id, Actions);
|
||||||
|
_ ->
|
||||||
|
ErrMsg = ["Transaction ", Id, " not found"],
|
||||||
|
handle_out(error, {ReceiptId, ErrMsg}, Channel)
|
||||||
|
end.
|
||||||
|
|
||||||
|
remove_trans(Id, Channel = #channel{transaction = Trans}) ->
|
||||||
|
Channel#channel{transaction = maps:remove(Id, Trans)}.
|
||||||
|
|
||||||
|
trans_pipeline([], Outgoings, Channel) ->
|
||||||
|
{ok, Outgoings, Channel};
|
||||||
|
|
||||||
|
trans_pipeline([{Func, Args}|More], Outgoings, Channel) ->
|
||||||
|
case erlang:apply(Func, Args ++ [Channel]) of
|
||||||
|
{ok, NChannel} ->
|
||||||
|
trans_pipeline(More, Outgoings, NChannel);
|
||||||
|
{ok, Outgoings1, NChannel} ->
|
||||||
|
trans_pipeline(More, Outgoings ++ Outgoings1, NChannel);
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason, Channel}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle outgoing packet
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(handle_out(atom(), term(), channel())
|
||||||
|
-> {ok, channel()}
|
||||||
|
| {ok, replies(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), replies(), channel()}).
|
||||||
|
|
||||||
|
handle_out(connerr, {Headers, ReceiptId, ErrMsg}, Channel) ->
|
||||||
|
Frame = error_frame(Headers, ReceiptId, ErrMsg),
|
||||||
|
shutdown(ErrMsg, Frame, Channel);
|
||||||
|
|
||||||
|
handle_out(error, {ReceiptId, ErrMsg}, Channel) ->
|
||||||
|
Frame = error_frame(ReceiptId, ErrMsg),
|
||||||
|
{ok, Frame, Channel};
|
||||||
|
|
||||||
|
handle_out(connected, Headers, Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
conninfo = ConnInfo
|
||||||
|
}) ->
|
||||||
|
%% XXX: connection_accepted is not defined by stomp protocol
|
||||||
|
_ = run_hooks(Ctx, 'client.connack', [ConnInfo, connection_accepted, []]),
|
||||||
|
Replies = [{outgoing, connected_frame(Headers)},
|
||||||
|
{event, connected}
|
||||||
|
],
|
||||||
|
{ok, Replies, ensure_heartbeart_timer(Channel)};
|
||||||
|
|
||||||
|
handle_out(receipt, undefined, Channel) ->
|
||||||
|
{ok, Channel};
|
||||||
|
handle_out(receipt, ReceiptId, Channel) ->
|
||||||
|
Frame = receipt_frame(ReceiptId),
|
||||||
|
{ok, Frame, Channel}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle call
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(handle_call(Req :: term(), channel())
|
||||||
|
-> {reply, Reply :: term(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), Reply :: term(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), Reply :: term(), emqx_types:packet(), channel()}).
|
||||||
|
handle_call(kick, Channel) ->
|
||||||
|
NChannel = ensure_disconnected(kicked, Channel),
|
||||||
|
shutdown_and_reply(kicked, ok, NChannel);
|
||||||
|
|
||||||
|
handle_call(discard, Channel) ->
|
||||||
|
shutdown_and_reply(discarded, ok, Channel);
|
||||||
|
|
||||||
|
%% XXX: No Session Takeover
|
||||||
|
%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) ->
|
||||||
|
% reply(Session, Channel#channel{takeover = true});
|
||||||
|
%
|
||||||
|
%handle_call({takeover, 'end'}, Channel = #channel{session = Session,
|
||||||
|
% pendings = Pendings}) ->
|
||||||
|
% ok = emqx_session:takeover(Session),
|
||||||
|
% %% TODO: Should not drain deliver here (side effect)
|
||||||
|
% Delivers = emqx_misc:drain_deliver(),
|
||||||
|
% AllPendings = lists:append(Delivers, Pendings),
|
||||||
|
% shutdown_and_reply(takeovered, AllPendings, Channel);
|
||||||
|
|
||||||
|
handle_call(list_acl_cache, Channel) ->
|
||||||
|
{reply, emqx_acl_cache:list_acl_cache(), Channel};
|
||||||
|
|
||||||
|
%% XXX: No Quota Now
|
||||||
|
% handle_call({quota, Policy}, Channel) ->
|
||||||
|
% Zone = info(zone, Channel),
|
||||||
|
% Quota = emqx_limiter:init(Zone, Policy),
|
||||||
|
% reply(ok, Channel#channel{quota = Quota});
|
||||||
|
|
||||||
|
handle_call(Req, Channel) ->
|
||||||
|
?LOG(error, "Unexpected call: ~p", [Req]),
|
||||||
|
reply(ignored, Channel).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle Info
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(handle_info(Info :: term(), channel())
|
||||||
|
-> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}).
|
||||||
|
|
||||||
|
%% XXX: Received from the emqx-management ???
|
||||||
|
%handle_info({subscribe, TopicFilters}, Channel ) ->
|
||||||
|
% {_, NChannel} = lists:foldl(
|
||||||
|
% fun({TopicFilter, SubOpts}, {_, ChannelAcc}) ->
|
||||||
|
% do_subscribe(TopicFilter, SubOpts, ChannelAcc)
|
||||||
|
% end, {[], Channel}, parse_topic_filters(TopicFilters)),
|
||||||
|
% {ok, NChannel};
|
||||||
|
%
|
||||||
|
%handle_info({unsubscribe, TopicFilters}, Channel) ->
|
||||||
|
% {_RC, NChannel} = process_unsubscribe(TopicFilters, #{}, Channel),
|
||||||
|
% {ok, NChannel};
|
||||||
|
|
||||||
|
handle_info({sock_closed, Reason},
|
||||||
|
Channel = #channel{conn_state = idle}) ->
|
||||||
|
shutdown(Reason, Channel);
|
||||||
|
|
||||||
|
handle_info({sock_closed, Reason},
|
||||||
|
Channel = #channel{conn_state = connecting}) ->
|
||||||
|
shutdown(Reason, Channel);
|
||||||
|
|
||||||
|
handle_info({sock_closed, Reason},
|
||||||
|
Channel = #channel{conn_state = connected,
|
||||||
|
clientinfo = _ClientInfo}) ->
|
||||||
|
%% XXX: Flapping detect ???
|
||||||
|
%% How to get the flapping detect policy ???
|
||||||
|
%emqx_zone:enable_flapping_detect(Zone)
|
||||||
|
% andalso emqx_flapping:detect(ClientInfo),
|
||||||
|
NChannel = ensure_disconnected(Reason, Channel),
|
||||||
|
%% XXX: Session keepper detect here
|
||||||
|
shutdown(Reason, NChannel);
|
||||||
|
|
||||||
|
handle_info({sock_closed, Reason},
|
||||||
|
Channel = #channel{conn_state = disconnected}) ->
|
||||||
|
?LOG(error, "Unexpected sock_closed: ~p", [Reason]),
|
||||||
|
{ok, Channel};
|
||||||
|
|
||||||
|
handle_info(clean_acl_cache, Channel) ->
|
||||||
|
ok = emqx_acl_cache:empty_acl_cache(),
|
||||||
|
{ok, Channel};
|
||||||
|
|
||||||
|
handle_info(Info, Channel) ->
|
||||||
|
?LOG(error, "Unexpected info: ~p", [Info]),
|
||||||
|
{ok, Channel}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Ensure disconnected
|
||||||
|
|
||||||
|
ensure_disconnected(Reason, Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo}) ->
|
||||||
|
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
|
||||||
|
ok = run_hooks(Ctx, 'client.disconnected',
|
||||||
|
[ClientInfo, Reason, NConnInfo]),
|
||||||
|
Channel#channel{conninfo = NConnInfo, conn_state = disconnected}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle Delivers from broker to client
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(handle_deliver(list(emqx_types:deliver()), channel())
|
||||||
|
-> {ok, channel()}
|
||||||
|
| {ok, replies(), channel()}).
|
||||||
|
|
||||||
|
handle_deliver(Delivers,
|
||||||
|
Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
clientinfo = ClientInfo,
|
||||||
|
subscriptions = Subs
|
||||||
|
}) ->
|
||||||
|
|
||||||
|
%% TODO: Re-deliver ???
|
||||||
|
%% Shared-subscription support ???
|
||||||
|
|
||||||
|
Frames0 = lists:foldl(fun({_, _, Message}, Acc) ->
|
||||||
|
Topic0 = emqx_message:topic(Message),
|
||||||
|
case lists:keyfind(Topic0, 2, Subs) of
|
||||||
|
{Id, Topic, Ack} ->
|
||||||
|
%% XXX: refactor later
|
||||||
|
metrics_inc('messages.delivered', Channel),
|
||||||
|
NMessage = run_hooks_without_metrics(
|
||||||
|
Ctx,
|
||||||
|
'message.delivered',
|
||||||
|
[ClientInfo],
|
||||||
|
Message
|
||||||
|
),
|
||||||
|
Topic = emqx_message:topic(NMessage),
|
||||||
|
Headers = emqx_message:get_headers(NMessage),
|
||||||
|
Payload = emqx_message:payload(NMessage),
|
||||||
|
Headers0 = [{<<"subscription">>, Id},
|
||||||
|
{<<"message-id">>, next_msgid()},
|
||||||
|
{<<"destination">>, Topic},
|
||||||
|
{<<"content-type">>, <<"text/plain">>}],
|
||||||
|
Headers1 = case Ack of
|
||||||
|
_ when Ack =:= <<"client">>;
|
||||||
|
Ack =:= <<"client-individual">> ->
|
||||||
|
Headers0 ++ [{<<"ack">>, next_ackid()}];
|
||||||
|
_ ->
|
||||||
|
Headers0
|
||||||
|
end,
|
||||||
|
Frame = #stomp_frame{command = <<"MESSAGE">>,
|
||||||
|
headers = Headers1 ++ maps:get(stomp_headers, Headers, []),
|
||||||
|
body = Payload
|
||||||
|
},
|
||||||
|
[Frame|Acc];
|
||||||
|
false ->
|
||||||
|
?LOG(error, "Dropped message ~0p due to not found "
|
||||||
|
"subscription id for ~s",
|
||||||
|
[Message, emqx_message:topic(Message)]),
|
||||||
|
metrics_inc('delivery.dropped', Channel),
|
||||||
|
metrics_inc('delivery.dropped.no_subid', Channel),
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end, [], Delivers),
|
||||||
|
{ok, [{outgoing, lists:reverse(Frames0)}], Channel}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle timeout
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(handle_timeout(reference(), Msg :: term(), channel())
|
||||||
|
-> {ok, channel()}
|
||||||
|
| {ok, replies(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), channel()}).
|
||||||
|
|
||||||
|
handle_timeout(_TRef, {incoming, NewVal},
|
||||||
|
Channel = #channel{heartbeat = HrtBt}) ->
|
||||||
|
case emqx_stomp_heartbeat:check(incoming, NewVal, HrtBt) of
|
||||||
|
{error, timeout} ->
|
||||||
|
shutdown(heartbeat_timeout, Channel);
|
||||||
|
{ok, NHrtBt} ->
|
||||||
|
{ok, reset_timer(incoming_timer,
|
||||||
|
Channel#channel{heartbeat = NHrtBt}
|
||||||
|
)}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_timeout(_TRef, {outgoing, NewVal},
|
||||||
|
Channel = #channel{heartbeat = HrtBt}) ->
|
||||||
|
case emqx_stomp_heartbeat:check(outgoing, NewVal, HrtBt) of
|
||||||
|
{error, timeout} ->
|
||||||
|
NHrtBt = emqx_stomp_heartbeat:reset(outgoing, NewVal, HrtBt),
|
||||||
|
NChannel = Channel#channel{heartbeat = NHrtBt},
|
||||||
|
{ok, emqx_stomp_frame:make(heartbeat),
|
||||||
|
reset_timer(outgoing_timer, NChannel)};
|
||||||
|
{ok, NHrtBt} ->
|
||||||
|
{ok, reset_timer(outgoing_timer,
|
||||||
|
Channel#channel{heartbeat = NHrtBt}
|
||||||
|
)}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_timeout(_TRef, clean_trans, Channel = #channel{transaction = Trans}) ->
|
||||||
|
Now = erlang:system_time(millisecond),
|
||||||
|
NTrans = maps:filter(fun(_, {StartedAt, _}) ->
|
||||||
|
StartedAt + ?TRANS_TIMEOUT < Now
|
||||||
|
end, Trans),
|
||||||
|
{ok, ensure_clean_trans_timer(Channel#channel{transaction = NTrans})}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Terminate
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
terminate(_Reason, _Channel) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
reply(Reply, Channel) ->
|
||||||
|
{reply, Reply, Channel}.
|
||||||
|
|
||||||
|
shutdown(Reason, Channel) ->
|
||||||
|
{shutdown, Reason, Channel}.
|
||||||
|
|
||||||
|
shutdown_with_recepit(Reason, ReceiptId, Channel) ->
|
||||||
|
case ReceiptId of
|
||||||
|
undefined ->
|
||||||
|
{shutdown, Reason, Channel};
|
||||||
|
_ ->
|
||||||
|
{shutdown, Reason, receipt_frame(ReceiptId), Channel}
|
||||||
|
end.
|
||||||
|
|
||||||
|
shutdown(Reason, AckFrame, Channel) ->
|
||||||
|
{shutdown, Reason, AckFrame, Channel}.
|
||||||
|
|
||||||
|
shutdown_and_reply(Reason, Reply, Channel) ->
|
||||||
|
{shutdown, Reason, Reply, Channel}.
|
||||||
|
|
||||||
|
do_negotiate_version(undefined) ->
|
||||||
|
{ok, <<"1.0">>};
|
||||||
|
|
||||||
|
do_negotiate_version(Accepts) ->
|
||||||
|
do_negotiate_version(
|
||||||
|
?STOMP_VER,
|
||||||
|
lists:reverse(lists:sort(binary:split(Accepts, <<",">>, [global])))
|
||||||
|
).
|
||||||
|
|
||||||
|
do_negotiate_version(Ver, []) ->
|
||||||
|
{error, <<"Supported protocol versions < ", Ver/binary>>};
|
||||||
|
do_negotiate_version(Ver, [AcceptVer|_]) when Ver >= AcceptVer ->
|
||||||
|
{ok, AcceptVer};
|
||||||
|
do_negotiate_version(Ver, [_|T]) ->
|
||||||
|
do_negotiate_version(Ver, T).
|
||||||
|
|
||||||
|
header(Name, Headers) ->
|
||||||
|
get_value(Name, Headers).
|
||||||
|
header(Name, Headers, Val) ->
|
||||||
|
get_value(Name, Headers, Val).
|
||||||
|
|
||||||
|
connected_frame(Headers) ->
|
||||||
|
emqx_stomp_frame:make(<<"CONNECTED">>, Headers).
|
||||||
|
|
||||||
|
receipt_frame(ReceiptId) ->
|
||||||
|
emqx_stomp_frame:make(<<"RECEIPT">>, [{<<"receipt-id">>, ReceiptId}]).
|
||||||
|
|
||||||
|
error_frame(ReceiptId, Msg) ->
|
||||||
|
error_frame([{<<"content-type">>, <<"text/plain">>}], ReceiptId, Msg).
|
||||||
|
|
||||||
|
error_frame(Headers, undefined, Msg) ->
|
||||||
|
emqx_stomp_frame:make(<<"ERROR">>, Headers, Msg);
|
||||||
|
error_frame(Headers, ReceiptId, Msg) ->
|
||||||
|
emqx_stomp_frame:make(<<"ERROR">>, [{<<"receipt-id">>, ReceiptId} | Headers], Msg).
|
||||||
|
|
||||||
|
next_msgid() ->
|
||||||
|
MsgId = case get(msgid) of
|
||||||
|
undefined -> 1;
|
||||||
|
I -> I
|
||||||
|
end,
|
||||||
|
put(msgid, MsgId + 1),
|
||||||
|
MsgId.
|
||||||
|
|
||||||
|
next_ackid() ->
|
||||||
|
AckId = case get(ackid) of
|
||||||
|
undefined -> 1;
|
||||||
|
I -> I
|
||||||
|
end,
|
||||||
|
put(ackid, AckId + 1),
|
||||||
|
AckId.
|
||||||
|
|
||||||
|
frame2message(?PACKET(?CMD_SEND, Headers, Body),
|
||||||
|
#channel{
|
||||||
|
conninfo = #{proto_ver := ProtoVer},
|
||||||
|
clientinfo = #{
|
||||||
|
protocol := Protocol,
|
||||||
|
clientid := ClientId,
|
||||||
|
username := Username,
|
||||||
|
peerhost := PeerHost,
|
||||||
|
mountpoint := Mountpoint
|
||||||
|
}}) ->
|
||||||
|
Topic = header(<<"destination">>, Headers),
|
||||||
|
Msg = emqx_message:make(ClientId, Topic, Body),
|
||||||
|
StompHeaders = lists:foldl(
|
||||||
|
fun(Key, Headers0) ->
|
||||||
|
proplists:delete(Key, Headers0)
|
||||||
|
end, Headers,
|
||||||
|
[<<"destination">>,
|
||||||
|
<<"content-length">>,
|
||||||
|
<<"content-type">>,
|
||||||
|
<<"transaction">>,
|
||||||
|
<<"receipt">>
|
||||||
|
]),
|
||||||
|
%% Pass-through of custom headers on the sending side
|
||||||
|
NMsg = emqx_message:set_headers(#{proto_ver => ProtoVer,
|
||||||
|
protocol => Protocol,
|
||||||
|
username => Username,
|
||||||
|
peerhost => PeerHost,
|
||||||
|
stomp_headers => StompHeaders
|
||||||
|
}, Msg),
|
||||||
|
emqx_mountpoint:mount(Mountpoint, NMsg).
|
||||||
|
|
||||||
|
receipt_id(Headers) ->
|
||||||
|
header(<<"receipt">>, Headers).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Trans
|
||||||
|
|
||||||
|
add_action(TxId, Action, ReceiptId, Channel = #channel{transaction = Trans}) ->
|
||||||
|
case maps:get(TxId, Trans, undefined) of
|
||||||
|
{_StartedAt, Actions} ->
|
||||||
|
NTrans = Trans#{TxId => {_StartedAt, [Action|Actions]}},
|
||||||
|
{ok, Channel#channel{transaction = NTrans}};
|
||||||
|
_ ->
|
||||||
|
{ok, error_frame(ReceiptId, ["Transaction ", TxId, " not found"]), Channel}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Transaction Handle
|
||||||
|
|
||||||
|
handle_recv_send_frame(Frame = ?PACKET(?CMD_SEND, Headers), Channel) ->
|
||||||
|
Msg = frame2message(Frame, Channel),
|
||||||
|
_ = emqx_broker:publish(Msg),
|
||||||
|
maybe_outgoing_receipt(receipt_id(Headers), Channel).
|
||||||
|
|
||||||
|
handle_recv_ack_frame(?PACKET(?CMD_ACK, Headers), Channel) ->
|
||||||
|
maybe_outgoing_receipt(receipt_id(Headers), Channel).
|
||||||
|
|
||||||
|
handle_recv_nack_frame(?PACKET(?CMD_NACK, Headers), Channel) ->
|
||||||
|
maybe_outgoing_receipt(receipt_id(Headers), Channel).
|
||||||
|
|
||||||
|
maybe_outgoing_receipt(undefined, Channel) ->
|
||||||
|
{ok, [], Channel};
|
||||||
|
maybe_outgoing_receipt(ReceiptId, Channel) ->
|
||||||
|
{ok, [{outgoing, receipt_frame(ReceiptId)}], Channel}.
|
||||||
|
|
||||||
|
maybe_outgoing_receipt(undefined, Outgoings, Channel) ->
|
||||||
|
{ok, Outgoings, Channel};
|
||||||
|
maybe_outgoing_receipt(ReceiptId, Outgoings, Channel) ->
|
||||||
|
{ok, lists:reverse([receipt_frame(ReceiptId)|Outgoings]), Channel}.
|
||||||
|
|
||||||
|
ensure_clean_trans_timer(Channel = #channel{transaction = Trans}) ->
|
||||||
|
case maps:size(Trans) of
|
||||||
|
0 -> Channel;
|
||||||
|
_ -> ensure_timer(clean_trans_timer, Channel)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Heartbeat
|
||||||
|
|
||||||
|
reverse_heartbeats({Cx, Cy}) ->
|
||||||
|
iolist_to_binary(io_lib:format("~w,~w", [Cy, Cx])).
|
||||||
|
|
||||||
|
ensure_heartbeart_timer(Channel = #channel{clientinfo = ClientInfo}) ->
|
||||||
|
Heartbeat = maps:get(heartbeat, ClientInfo),
|
||||||
|
ensure_timer(
|
||||||
|
[incoming_timer, outgoing_timer],
|
||||||
|
Channel#channel{heartbeat = emqx_stomp_heartbeat:init(Heartbeat)}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Timer
|
||||||
|
|
||||||
|
ensure_timer([Name], Channel) ->
|
||||||
|
ensure_timer(Name, Channel);
|
||||||
|
ensure_timer([Name | Rest], Channel) ->
|
||||||
|
ensure_timer(Rest, ensure_timer(Name, Channel));
|
||||||
|
|
||||||
|
ensure_timer(Name, Channel = #channel{timers = Timers}) ->
|
||||||
|
TRef = maps:get(Name, Timers, undefined),
|
||||||
|
Time = interval(Name, Channel),
|
||||||
|
case TRef == undefined andalso is_integer(Time) andalso Time > 0 of
|
||||||
|
true -> ensure_timer(Name, Time, Channel);
|
||||||
|
false -> Channel %% Timer disabled or exists
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_timer(Name, Time, Channel = #channel{timers = Timers}) ->
|
||||||
|
Msg = maps:get(Name, ?TIMER_TABLE),
|
||||||
|
TRef = emqx_misc:start_timer(Time, Msg),
|
||||||
|
Channel#channel{timers = Timers#{Name => TRef}}.
|
||||||
|
|
||||||
|
reset_timer(Name, Channel) ->
|
||||||
|
ensure_timer(Name, clean_timer(Name, Channel)).
|
||||||
|
|
||||||
|
clean_timer(Name, Channel = #channel{timers = Timers}) ->
|
||||||
|
Channel#channel{timers = maps:remove(Name, Timers)}.
|
||||||
|
|
||||||
|
interval(incoming_timer, #channel{heartbeat = HrtBt}) ->
|
||||||
|
emqx_stomp_heartbeat:interval(incoming, HrtBt);
|
||||||
|
interval(outgoing_timer, #channel{heartbeat = HrtBt}) ->
|
||||||
|
emqx_stomp_heartbeat:interval(outgoing, HrtBt);
|
||||||
|
interval(clean_trans_timer, _) ->
|
||||||
|
?TRANS_TIMEOUT.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Helper functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
run_hooks(Ctx, Name, Args) ->
|
||||||
|
emqx_gateway_ctx:metrics_inc(Ctx, Name),
|
||||||
|
emqx_hooks:run(Name, Args).
|
||||||
|
|
||||||
|
run_hooks(Ctx, Name, Args, Acc) ->
|
||||||
|
emqx_gateway_ctx:metrics_inc(Ctx, Name),
|
||||||
|
emqx_hooks:run_fold(Name, Args, Acc).
|
||||||
|
|
||||||
|
run_hooks_without_metrics(_Ctx, Name, Args, Acc) ->
|
||||||
|
emqx_hooks:run_fold(Name, Args, Acc).
|
||||||
|
|
||||||
|
metrics_inc(Name, #channel{ctx = Ctx}) ->
|
||||||
|
emqx_gateway_ctx:metrics_inc(Ctx, Name).
|
|
@ -0,0 +1,908 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020-2021 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_stomp_connection).
|
||||||
|
|
||||||
|
-include("src/stomp/include/emqx_stomp.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
|
-logger_header("[Stomp-Conn]").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([ start_link/3
|
||||||
|
, stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ info/1
|
||||||
|
, stats/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ async_set_keepalive/3
|
||||||
|
, async_set_keepalive/4
|
||||||
|
, async_set_socket_options/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ call/2
|
||||||
|
, call/3
|
||||||
|
, cast/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Callback
|
||||||
|
-export([init/4]).
|
||||||
|
|
||||||
|
%% Sys callbacks
|
||||||
|
-export([ system_continue/3
|
||||||
|
, system_terminate/4
|
||||||
|
, system_code_change/4
|
||||||
|
, system_get_state/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Internal callback
|
||||||
|
-export([wakeup_from_hib/2, recvloop/2, get_state/1]).
|
||||||
|
|
||||||
|
%% Export for CT
|
||||||
|
-export([set_field/3]).
|
||||||
|
|
||||||
|
-import(emqx_misc,
|
||||||
|
[ maybe_apply/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
%% TCP/TLS Transport
|
||||||
|
transport :: esockd:transport(),
|
||||||
|
%% TCP/TLS Socket
|
||||||
|
socket :: esockd:socket(),
|
||||||
|
%% Peername of the connection
|
||||||
|
peername :: emqx_types:peername(),
|
||||||
|
%% Sockname of the connection
|
||||||
|
sockname :: emqx_types:peername(),
|
||||||
|
%% Sock State
|
||||||
|
sockstate :: emqx_types:sockstate(),
|
||||||
|
%% The {active, N} option
|
||||||
|
active_n :: pos_integer(),
|
||||||
|
%% Limiter
|
||||||
|
limiter :: emqx_limiter:limiter() | undefined,
|
||||||
|
%% Limit Timer
|
||||||
|
limit_timer :: reference() | undefined,
|
||||||
|
%% Parse State
|
||||||
|
parse_state :: emqx_stomp_frame:parse_state(),
|
||||||
|
%% Serialize options
|
||||||
|
serialize :: emqx_stomp_frame:serialize_opts(),
|
||||||
|
%% Channel State
|
||||||
|
channel :: emqx_stomp_channel:channel(),
|
||||||
|
%% GC State
|
||||||
|
gc_state :: emqx_gc:gc_state() | undefined,
|
||||||
|
%% Stats Timer
|
||||||
|
stats_timer :: disabled | reference(),
|
||||||
|
%% Idle Timeout
|
||||||
|
idle_timeout :: integer(),
|
||||||
|
%% Idle Timer
|
||||||
|
idle_timer :: reference() | undefined
|
||||||
|
}).
|
||||||
|
|
||||||
|
-type(state() :: #state{}).
|
||||||
|
|
||||||
|
-define(ACTIVE_N, 100).
|
||||||
|
-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]).
|
||||||
|
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
|
||||||
|
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]).
|
||||||
|
|
||||||
|
-define(ENABLED(X), (X =/= undefined)).
|
||||||
|
|
||||||
|
%-define(ALARM_TCP_CONGEST(Channel),
|
||||||
|
% list_to_binary(io_lib:format("mqtt_conn/congested/~s/~s",
|
||||||
|
% [emqx_stomp_channel:info(clientid, Channel),
|
||||||
|
% emqx_stomp_channel:info(username, Channel)]))).
|
||||||
|
%-define(ALARM_CONN_INFO_KEYS, [
|
||||||
|
% socktype, sockname, peername,
|
||||||
|
% clientid, username, proto_name, proto_ver, connected_at
|
||||||
|
%]).
|
||||||
|
%-define(ALARM_SOCK_STATS_KEYS, [send_pend, recv_cnt, recv_oct, send_cnt, send_oct]).
|
||||||
|
%-define(ALARM_SOCK_OPTS_KEYS, [high_watermark, high_msgq_watermark, sndbuf, recbuf, buffer]).
|
||||||
|
|
||||||
|
-dialyzer({no_match, [info/2]}).
|
||||||
|
-dialyzer({nowarn_function, [ init/4
|
||||||
|
, init_state/3
|
||||||
|
, run_loop/2
|
||||||
|
, system_terminate/4
|
||||||
|
, system_code_change/4
|
||||||
|
]}).
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, [ensure_stats_timer/2,cancel_stats_timer/1,
|
||||||
|
terminate/2,handle_call/3,handle_timeout/3,
|
||||||
|
parse_incoming/3,serialize_and_inc_stats_fun/1,
|
||||||
|
check_oom/1,inc_incoming_stats/1,
|
||||||
|
inc_outgoing_stats/1]}).
|
||||||
|
|
||||||
|
-spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist())
|
||||||
|
-> {ok, pid()}).
|
||||||
|
start_link(Transport, Socket, Options) ->
|
||||||
|
Args = [self(), Transport, Socket, Options],
|
||||||
|
CPid = proc_lib:spawn_link(?MODULE, init, Args),
|
||||||
|
{ok, CPid}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% API
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Get infos of the connection/channel.
|
||||||
|
-spec(info(pid()|state()) -> emqx_types:infos()).
|
||||||
|
info(CPid) when is_pid(CPid) ->
|
||||||
|
call(CPid, info);
|
||||||
|
info(State = #state{channel = Channel}) ->
|
||||||
|
ChanInfo = emqx_stomp_channel:info(Channel),
|
||||||
|
SockInfo = maps:from_list(
|
||||||
|
info(?INFO_KEYS, State)),
|
||||||
|
ChanInfo#{sockinfo => SockInfo}.
|
||||||
|
|
||||||
|
info(Keys, State) when is_list(Keys) ->
|
||||||
|
[{Key, info(Key, State)} || Key <- Keys];
|
||||||
|
info(socktype, #state{transport = Transport, socket = Socket}) ->
|
||||||
|
Transport:type(Socket);
|
||||||
|
info(peername, #state{peername = Peername}) ->
|
||||||
|
Peername;
|
||||||
|
info(sockname, #state{sockname = Sockname}) ->
|
||||||
|
Sockname;
|
||||||
|
info(sockstate, #state{sockstate = SockSt}) ->
|
||||||
|
SockSt;
|
||||||
|
info(active_n, #state{active_n = ActiveN}) ->
|
||||||
|
ActiveN;
|
||||||
|
info(stats_timer, #state{stats_timer = StatsTimer}) ->
|
||||||
|
StatsTimer;
|
||||||
|
info(limit_timer, #state{limit_timer = LimitTimer}) ->
|
||||||
|
LimitTimer;
|
||||||
|
info(limiter, #state{limiter = Limiter}) ->
|
||||||
|
maybe_apply(fun emqx_limiter:info/1, Limiter).
|
||||||
|
|
||||||
|
%% @doc Get stats of the connection/channel.
|
||||||
|
-spec(stats(pid()|state()) -> emqx_types:stats()).
|
||||||
|
stats(CPid) when is_pid(CPid) ->
|
||||||
|
call(CPid, stats);
|
||||||
|
stats(#state{transport = Transport,
|
||||||
|
socket = Socket,
|
||||||
|
channel = Channel}) ->
|
||||||
|
SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of
|
||||||
|
{ok, Ss} -> Ss;
|
||||||
|
{error, _} -> []
|
||||||
|
end,
|
||||||
|
ConnStats = emqx_pd:get_counters(?CONN_STATS),
|
||||||
|
ChanStats = emqx_stomp_channel:stats(Channel),
|
||||||
|
ProcStats = emqx_misc:proc_stats(),
|
||||||
|
lists:append([SockStats, ConnStats, ChanStats, ProcStats]).
|
||||||
|
|
||||||
|
%% @doc Set TCP keepalive socket options to override system defaults.
|
||||||
|
%% Idle: The number of seconds a connection needs to be idle before
|
||||||
|
%% TCP begins sending out keep-alive probes (Linux default 7200).
|
||||||
|
%% Interval: The number of seconds between TCP keep-alive probes
|
||||||
|
%% (Linux default 75).
|
||||||
|
%% Probes: The maximum number of TCP keep-alive probes to send before
|
||||||
|
%% giving up and killing the connection if no response is
|
||||||
|
%% obtained from the other end (Linux default 9).
|
||||||
|
%%
|
||||||
|
%% NOTE: This API sets TCP socket options, which has nothing to do with
|
||||||
|
%% the MQTT layer's keepalive (PINGREQ and PINGRESP).
|
||||||
|
async_set_keepalive(Idle, Interval, Probes) ->
|
||||||
|
async_set_keepalive(self(), Idle, Interval, Probes).
|
||||||
|
|
||||||
|
async_set_keepalive(Pid, Idle, Interval, Probes) ->
|
||||||
|
Options = [ {keepalive, true}
|
||||||
|
, {raw, 6, 4, <<Idle:32/native>>}
|
||||||
|
, {raw, 6, 5, <<Interval:32/native>>}
|
||||||
|
, {raw, 6, 6, <<Probes:32/native>>}
|
||||||
|
],
|
||||||
|
async_set_socket_options(Pid, Options).
|
||||||
|
|
||||||
|
%% @doc Set custom socket options.
|
||||||
|
%% This API is made async because the call might be originated from
|
||||||
|
%% a hookpoint callback (otherwise deadlock).
|
||||||
|
%% If failed to set, the error message is logged.
|
||||||
|
async_set_socket_options(Pid, Options) ->
|
||||||
|
cast(Pid, {async_set_socket_options, Options}).
|
||||||
|
|
||||||
|
cast(Pid, Req) ->
|
||||||
|
gen_server:cast(Pid, Req).
|
||||||
|
|
||||||
|
call(Pid, Req) ->
|
||||||
|
call(Pid, Req, infinity).
|
||||||
|
call(Pid, Req, Timeout) ->
|
||||||
|
gen_server:call(Pid, Req, Timeout).
|
||||||
|
|
||||||
|
stop(Pid) ->
|
||||||
|
gen_server:stop(Pid).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init(Parent, Transport, RawSocket, Options) ->
|
||||||
|
case Transport:wait(RawSocket) of
|
||||||
|
{ok, Socket} ->
|
||||||
|
run_loop(Parent, init_state(Transport, Socket, Options));
|
||||||
|
{error, Reason} ->
|
||||||
|
ok = Transport:fast_close(RawSocket),
|
||||||
|
exit_on_sock_error(Reason)
|
||||||
|
end.
|
||||||
|
|
||||||
|
init_state(Transport, Socket, Options) ->
|
||||||
|
{ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]),
|
||||||
|
{ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]),
|
||||||
|
Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]),
|
||||||
|
ConnInfo = #{socktype => Transport:type(Socket),
|
||||||
|
peername => Peername,
|
||||||
|
sockname => Sockname,
|
||||||
|
peercert => Peercert,
|
||||||
|
conn_mod => ?MODULE
|
||||||
|
},
|
||||||
|
ActiveN = emqx_gateway_utils:active_n(Options),
|
||||||
|
%% TODO: RateLimit ? How ?
|
||||||
|
Limiter = undefined,
|
||||||
|
%RateLimit = emqx_gateway_utils:ratelimit(Options),
|
||||||
|
%%Limiter = emqx_limiter:init(Zone, RateLimit),
|
||||||
|
FrameOpts = emqx_gateway_utils:frame_options(Options),
|
||||||
|
ParseState = emqx_stomp_frame:initial_parse_state(FrameOpts),
|
||||||
|
Serialize = emqx_stomp_frame:serialize_opts(),
|
||||||
|
Channel = emqx_stomp_channel:init(ConnInfo, Options),
|
||||||
|
GcState = emqx_gateway_utils:init_gc_state(Options),
|
||||||
|
StatsTimer = emqx_gateway_utils:stats_timer(Options),
|
||||||
|
IdleTimeout = emqx_gateway_utils:idle_timeout(Options),
|
||||||
|
IdleTimer = emqx_misc:start_timer(IdleTimeout, idle_timeout),
|
||||||
|
#state{transport = Transport,
|
||||||
|
socket = Socket,
|
||||||
|
peername = Peername,
|
||||||
|
sockname = Sockname,
|
||||||
|
sockstate = idle,
|
||||||
|
active_n = ActiveN,
|
||||||
|
limiter = Limiter,
|
||||||
|
parse_state = ParseState,
|
||||||
|
serialize = Serialize,
|
||||||
|
channel = Channel,
|
||||||
|
gc_state = GcState,
|
||||||
|
stats_timer = StatsTimer,
|
||||||
|
idle_timeout = IdleTimeout,
|
||||||
|
idle_timer = IdleTimer
|
||||||
|
}.
|
||||||
|
|
||||||
|
run_loop(Parent, State = #state{transport = Transport,
|
||||||
|
socket = Socket,
|
||||||
|
peername = Peername,
|
||||||
|
channel = _Channel}) ->
|
||||||
|
emqx_logger:set_metadata_peername(esockd:format(Peername)),
|
||||||
|
% TODO: How yo get oom_policy ???
|
||||||
|
%emqx_misc:tune_heap_size(emqx_gateway_utils:oom_policy(
|
||||||
|
% emqx_stomp_channel:info(zone, Channel))),
|
||||||
|
case activate_socket(State) of
|
||||||
|
{ok, NState} -> hibernate(Parent, NState);
|
||||||
|
{error, Reason} ->
|
||||||
|
ok = Transport:fast_close(Socket),
|
||||||
|
exit_on_sock_error(Reason)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec exit_on_sock_error(any()) -> no_return().
|
||||||
|
exit_on_sock_error(Reason) when Reason =:= einval;
|
||||||
|
Reason =:= enotconn;
|
||||||
|
Reason =:= closed ->
|
||||||
|
erlang:exit(normal);
|
||||||
|
exit_on_sock_error(timeout) ->
|
||||||
|
erlang:exit({shutdown, ssl_upgrade_timeout});
|
||||||
|
exit_on_sock_error(Reason) ->
|
||||||
|
erlang:exit({shutdown, Reason}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Recv Loop
|
||||||
|
|
||||||
|
recvloop(Parent, State = #state{idle_timeout = IdleTimeout}) ->
|
||||||
|
receive
|
||||||
|
Msg ->
|
||||||
|
handle_recv(Msg, Parent, State)
|
||||||
|
after
|
||||||
|
IdleTimeout + 100 ->
|
||||||
|
hibernate(Parent, cancel_stats_timer(State))
|
||||||
|
end.
|
||||||
|
|
||||||
|
handle_recv({system, From, Request}, Parent, State) ->
|
||||||
|
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State);
|
||||||
|
handle_recv({'EXIT', Parent, Reason}, Parent, State) ->
|
||||||
|
%% FIXME: it's not trapping exit, should never receive an EXIT
|
||||||
|
terminate(Reason, State);
|
||||||
|
handle_recv(Msg, Parent, State = #state{idle_timeout = IdleTimeout}) ->
|
||||||
|
case process_msg([Msg], ensure_stats_timer(IdleTimeout, State)) of
|
||||||
|
{ok, NewState} ->
|
||||||
|
?MODULE:recvloop(Parent, NewState);
|
||||||
|
{stop, Reason, NewSate} ->
|
||||||
|
terminate(Reason, NewSate)
|
||||||
|
end.
|
||||||
|
|
||||||
|
hibernate(Parent, State) ->
|
||||||
|
proc_lib:hibernate(?MODULE, wakeup_from_hib, [Parent, State]).
|
||||||
|
|
||||||
|
%% Maybe do something here later.
|
||||||
|
wakeup_from_hib(Parent, State) ->
|
||||||
|
?MODULE:recvloop(Parent, State).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Ensure/cancel stats timer
|
||||||
|
|
||||||
|
ensure_stats_timer(Timeout, State = #state{stats_timer = undefined}) ->
|
||||||
|
State#state{stats_timer = emqx_misc:start_timer(Timeout, emit_stats)};
|
||||||
|
ensure_stats_timer(_Timeout, State) -> State.
|
||||||
|
|
||||||
|
cancel_stats_timer(State = #state{stats_timer = TRef})
|
||||||
|
when is_reference(TRef) ->
|
||||||
|
?tp(debug, cancel_stats_timer, #{}),
|
||||||
|
ok = emqx_misc:cancel_timer(TRef),
|
||||||
|
State#state{stats_timer = undefined};
|
||||||
|
cancel_stats_timer(State) -> State.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Process next Msg
|
||||||
|
|
||||||
|
process_msg([], State) ->
|
||||||
|
{ok, State};
|
||||||
|
process_msg([Msg|More], State) ->
|
||||||
|
try
|
||||||
|
case handle_msg(Msg, State) of
|
||||||
|
ok ->
|
||||||
|
process_msg(More, State);
|
||||||
|
{ok, NState} ->
|
||||||
|
process_msg(More, NState);
|
||||||
|
{ok, Msgs, NState} ->
|
||||||
|
process_msg(append_msg(More, Msgs), NState);
|
||||||
|
{stop, Reason, NState} ->
|
||||||
|
{stop, Reason, NState}
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
exit : normal ->
|
||||||
|
{stop, normal, State};
|
||||||
|
exit : shutdown ->
|
||||||
|
{stop, shutdown, State};
|
||||||
|
exit : {shutdown, _} = Shutdown ->
|
||||||
|
{stop, Shutdown, State};
|
||||||
|
Exception : Context : Stack ->
|
||||||
|
{stop, #{exception => Exception,
|
||||||
|
context => Context,
|
||||||
|
stacktrace => Stack}, State}
|
||||||
|
end.
|
||||||
|
|
||||||
|
append_msg([], Msgs) when is_list(Msgs) ->
|
||||||
|
Msgs;
|
||||||
|
append_msg([], Msg) -> [Msg];
|
||||||
|
append_msg(Q, Msgs) when is_list(Msgs) ->
|
||||||
|
lists:append(Q, Msgs);
|
||||||
|
append_msg(Q, Msg) ->
|
||||||
|
lists:append(Q, [Msg]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle a Msg
|
||||||
|
|
||||||
|
handle_msg({'$gen_call', From, Req}, State) ->
|
||||||
|
case handle_call(From, Req, State) of
|
||||||
|
{reply, Reply, NState} ->
|
||||||
|
gen_server:reply(From, Reply),
|
||||||
|
{ok, NState};
|
||||||
|
{stop, Reason, Reply, NState} ->
|
||||||
|
gen_server:reply(From, Reply),
|
||||||
|
stop(Reason, NState)
|
||||||
|
end;
|
||||||
|
handle_msg({'$gen_cast', Req}, State) ->
|
||||||
|
NewState = handle_cast(Req, State),
|
||||||
|
{ok, NewState};
|
||||||
|
|
||||||
|
handle_msg({Inet, _Sock, Data}, State = #state{channel = Channel})
|
||||||
|
when Inet == tcp;
|
||||||
|
Inet == ssl ->
|
||||||
|
?LOG(debug, "RECV ~0p", [Data]),
|
||||||
|
Oct = iolist_size(Data),
|
||||||
|
inc_counter(incoming_bytes, Oct),
|
||||||
|
Ctx = emqx_stomp_channel:info(ctx, Channel),
|
||||||
|
ok = emqx_gateway_ctx:metrics_inc(Ctx, 'bytes.received', Oct),
|
||||||
|
parse_incoming(Data, State);
|
||||||
|
|
||||||
|
handle_msg({incoming, Packet}, State = #state{idle_timer = undefined}) ->
|
||||||
|
handle_incoming(Packet, State);
|
||||||
|
|
||||||
|
handle_msg({incoming, Packet},
|
||||||
|
State = #state{idle_timer = IdleTimer}) ->
|
||||||
|
ok = emqx_misc:cancel_timer(IdleTimer),
|
||||||
|
%% XXX: Serialize with inpunt packets
|
||||||
|
%%Serialize = emqx_stomp_frame:serialize_opts(),
|
||||||
|
NState = State#state{idle_timer = undefined},
|
||||||
|
handle_incoming(Packet, NState);
|
||||||
|
|
||||||
|
handle_msg({outgoing, Packets}, State) ->
|
||||||
|
handle_outgoing(Packets, State);
|
||||||
|
|
||||||
|
handle_msg({Error, _Sock, Reason}, State)
|
||||||
|
when Error == tcp_error; Error == ssl_error ->
|
||||||
|
handle_info({sock_error, Reason}, State);
|
||||||
|
|
||||||
|
handle_msg({Closed, _Sock}, State)
|
||||||
|
when Closed == tcp_closed; Closed == ssl_closed ->
|
||||||
|
handle_info({sock_closed, Closed}, close_socket(State));
|
||||||
|
|
||||||
|
handle_msg({Passive, _Sock}, State)
|
||||||
|
when Passive == tcp_passive; Passive == ssl_passive ->
|
||||||
|
%% In Stats
|
||||||
|
Pubs = emqx_pd:reset_counter(incoming_pubs),
|
||||||
|
Bytes = emqx_pd:reset_counter(incoming_bytes),
|
||||||
|
InStats = #{cnt => Pubs, oct => Bytes},
|
||||||
|
%% Ensure Rate Limit
|
||||||
|
NState = ensure_rate_limit(InStats, State),
|
||||||
|
%% Run GC and Check OOM
|
||||||
|
NState1 = check_oom(run_gc(InStats, NState)),
|
||||||
|
handle_info(activate_socket, NState1);
|
||||||
|
|
||||||
|
handle_msg(Deliver = {deliver, _Topic, _Msg},
|
||||||
|
#state{active_n = ActiveN} = State) ->
|
||||||
|
Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)],
|
||||||
|
with_channel(handle_deliver, [Delivers], State);
|
||||||
|
|
||||||
|
%% Something sent
|
||||||
|
handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) ->
|
||||||
|
case emqx_pd:get_counter(outgoing_pubs) > ActiveN of
|
||||||
|
true ->
|
||||||
|
Pubs = emqx_pd:reset_counter(outgoing_pubs),
|
||||||
|
Bytes = emqx_pd:reset_counter(outgoing_bytes),
|
||||||
|
OutStats = #{cnt => Pubs, oct => Bytes},
|
||||||
|
{ok, run_gc(OutStats, State)};
|
||||||
|
%% FIXME: check oom ???
|
||||||
|
%%{ok, check_oom(run_gc(OutStats, State))};
|
||||||
|
false -> ok
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_msg({inet_reply, _Sock, {error, Reason}}, State) ->
|
||||||
|
handle_info({sock_error, Reason}, State);
|
||||||
|
|
||||||
|
handle_msg({connack, ConnAck}, State) ->
|
||||||
|
handle_outgoing(ConnAck, State);
|
||||||
|
|
||||||
|
handle_msg({close, Reason}, State) ->
|
||||||
|
?LOG(debug, "Force to close the socket due to ~p", [Reason]),
|
||||||
|
handle_info({sock_closed, Reason}, close_socket(State));
|
||||||
|
|
||||||
|
handle_msg({event, connected}, State = #state{channel = Channel}) ->
|
||||||
|
Ctx = emqx_stomp_channel:info(ctx, Channel),
|
||||||
|
ClientId = emqx_stomp_channel:info(clientid, Channel),
|
||||||
|
emqx_gateway_ctx:insert_channel_info(
|
||||||
|
Ctx,
|
||||||
|
ClientId,
|
||||||
|
info(State),
|
||||||
|
stats(State)
|
||||||
|
);
|
||||||
|
|
||||||
|
handle_msg({event, disconnected}, State = #state{channel = Channel}) ->
|
||||||
|
Ctx = emqx_stomp_channel:info(ctx, Channel),
|
||||||
|
ClientId = emqx_stomp_channel:info(clientid, Channel),
|
||||||
|
emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)),
|
||||||
|
emqx_gateway_ctx:connection_closed(Ctx, ClientId),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
handle_msg({event, _Other}, State = #state{channel = Channel}) ->
|
||||||
|
Ctx = emqx_stomp_channel:info(ctx, Channel),
|
||||||
|
ClientId = emqx_stomp_channel:info(clientid, Channel),
|
||||||
|
emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)),
|
||||||
|
emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
handle_msg({timeout, TRef, TMsg}, State) ->
|
||||||
|
handle_timeout(TRef, TMsg, State);
|
||||||
|
|
||||||
|
handle_msg(Shutdown = {shutdown, _Reason}, State) ->
|
||||||
|
stop(Shutdown, State);
|
||||||
|
|
||||||
|
handle_msg(Msg, State) ->
|
||||||
|
handle_info(Msg, State).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Terminate
|
||||||
|
|
||||||
|
-spec terminate(any(), state()) -> no_return().
|
||||||
|
terminate(Reason, State = #state{channel = Channel, transport = _Transport,
|
||||||
|
socket = _Socket}) ->
|
||||||
|
try
|
||||||
|
Channel1 = emqx_stomp_channel:set_conn_state(disconnected, Channel),
|
||||||
|
%emqx_congestion:cancel_alarms(Socket, Transport, Channel1),
|
||||||
|
emqx_stomp_channel:terminate(Reason, Channel1),
|
||||||
|
close_socket_ok(State)
|
||||||
|
catch
|
||||||
|
E : C : S ->
|
||||||
|
?tp(warning, unclean_terminate, #{exception => E, context => C, stacktrace => S})
|
||||||
|
end,
|
||||||
|
?tp(info, terminate, #{reason => Reason}),
|
||||||
|
maybe_raise_excption(Reason).
|
||||||
|
|
||||||
|
%% close socket, discard new state, always return ok.
|
||||||
|
close_socket_ok(State) ->
|
||||||
|
_ = close_socket(State),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% tell truth about the original exception
|
||||||
|
maybe_raise_excption(#{exception := Exception,
|
||||||
|
context := Context,
|
||||||
|
stacktrace := Stacktrace
|
||||||
|
}) ->
|
||||||
|
erlang:raise(Exception, Context, Stacktrace);
|
||||||
|
maybe_raise_excption(Reason) ->
|
||||||
|
exit(Reason).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Sys callbacks
|
||||||
|
|
||||||
|
system_continue(Parent, _Debug, State) ->
|
||||||
|
?MODULE:recvloop(Parent, State).
|
||||||
|
|
||||||
|
system_terminate(Reason, _Parent, _Debug, State) ->
|
||||||
|
terminate(Reason, State).
|
||||||
|
|
||||||
|
system_code_change(State, _Mod, _OldVsn, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
system_get_state(State) -> {ok, State}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle call
|
||||||
|
|
||||||
|
handle_call(_From, info, State) ->
|
||||||
|
{reply, info(State), State};
|
||||||
|
|
||||||
|
handle_call(_From, stats, State) ->
|
||||||
|
{reply, stats(State), State};
|
||||||
|
|
||||||
|
%% TODO: How to set ratelimit ???
|
||||||
|
%%handle_call(_From, {ratelimit, Policy}, State = #state{channel = Channel}) ->
|
||||||
|
%% Zone = emqx_stomp_channel:info(zone, Channel),
|
||||||
|
%% Limiter = emqx_limiter:init(Zone, Policy),
|
||||||
|
%% {reply, ok, State#state{limiter = Limiter}};
|
||||||
|
|
||||||
|
handle_call(_From, Req, State = #state{channel = Channel}) ->
|
||||||
|
case emqx_stomp_channel:handle_call(Req, Channel) of
|
||||||
|
{reply, Reply, NChannel} ->
|
||||||
|
{reply, Reply, State#state{channel = NChannel}};
|
||||||
|
{shutdown, Reason, Reply, NChannel} ->
|
||||||
|
shutdown(Reason, Reply, State#state{channel = NChannel});
|
||||||
|
{shutdown, Reason, Reply, OutPacket, NChannel} ->
|
||||||
|
NState = State#state{channel = NChannel},
|
||||||
|
ok = handle_outgoing(OutPacket, NState),
|
||||||
|
shutdown(Reason, Reply, NState)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle timeout
|
||||||
|
|
||||||
|
handle_timeout(_TRef, idle_timeout, State) ->
|
||||||
|
shutdown(idle_timeout, State);
|
||||||
|
|
||||||
|
handle_timeout(_TRef, limit_timeout, State) ->
|
||||||
|
NState = State#state{sockstate = idle,
|
||||||
|
limit_timer = undefined
|
||||||
|
},
|
||||||
|
handle_info(activate_socket, NState);
|
||||||
|
|
||||||
|
handle_timeout(_TRef, emit_stats, State = #state{channel = Channel,
|
||||||
|
transport = _Transport,
|
||||||
|
socket = _Socket}) ->
|
||||||
|
%emqx_congestion:maybe_alarm_conn_congestion(Socket, Transport, Channel),
|
||||||
|
Ctx = emqx_stomp_channel:info(ctx, Channel),
|
||||||
|
ClientId = emqx_stomp_channel:info(clientid, Channel),
|
||||||
|
emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)),
|
||||||
|
{ok, State#state{stats_timer = undefined}};
|
||||||
|
|
||||||
|
%% Abstraction ???
|
||||||
|
%handle_timeout(TRef, keepalive, State = #state{transport = Transport,
|
||||||
|
% socket = Socket,
|
||||||
|
% channel = Channel})->
|
||||||
|
% case emqx_stomp_channel:info(conn_state, Channel) of
|
||||||
|
% disconnected -> {ok, State};
|
||||||
|
% _ ->
|
||||||
|
% case Transport:getstat(Socket, [recv_oct]) of
|
||||||
|
% {ok, [{recv_oct, RecvOct}]} ->
|
||||||
|
% handle_timeout(TRef, {keepalive, RecvOct}, State);
|
||||||
|
% {error, Reason} ->
|
||||||
|
% handle_info({sock_error, Reason}, State)
|
||||||
|
% end
|
||||||
|
% end;
|
||||||
|
|
||||||
|
handle_timeout(TRef, TMsg, State = #state{transport = Transport,
|
||||||
|
socket = Socket,
|
||||||
|
channel = Channel
|
||||||
|
})
|
||||||
|
when TMsg =:= incoming;
|
||||||
|
TMsg =:= outgoing ->
|
||||||
|
Stat = case TMsg of incoming -> recv_oct; _ -> send_oct end,
|
||||||
|
case emqx_stomp_channel:info(conn_state, Channel) of
|
||||||
|
disconnected -> {ok, State};
|
||||||
|
_ ->
|
||||||
|
case Transport:getstat(Socket, [Stat]) of
|
||||||
|
{ok, [{recv_oct, RecvOct}]} ->
|
||||||
|
handle_timeout(TRef, {incoming, RecvOct}, State);
|
||||||
|
{ok, [{send_oct, SendOct}]} ->
|
||||||
|
handle_timeout(TRef, {outgoing, SendOct}, State);
|
||||||
|
{error, Reason} ->
|
||||||
|
handle_info({sock_error, Reason}, State)
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_timeout(TRef, Msg, State) ->
|
||||||
|
with_channel(handle_timeout, [TRef, Msg], State).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Parse incoming data
|
||||||
|
|
||||||
|
parse_incoming(Data, State) ->
|
||||||
|
{Packets, NState} = parse_incoming(Data, [], State),
|
||||||
|
{ok, next_incoming_msgs(Packets), NState}.
|
||||||
|
|
||||||
|
parse_incoming(<<>>, Packets, State) ->
|
||||||
|
{Packets, State};
|
||||||
|
|
||||||
|
parse_incoming(Data, Packets, State = #state{parse_state = ParseState}) ->
|
||||||
|
try emqx_stomp_frame:parse(Data, ParseState) of
|
||||||
|
{more, NParseState} ->
|
||||||
|
{Packets, State#state{parse_state = NParseState}};
|
||||||
|
{ok, Packet, Rest, NParseState} ->
|
||||||
|
NState = State#state{parse_state = NParseState},
|
||||||
|
parse_incoming(Rest, [Packet|Packets], NState)
|
||||||
|
catch
|
||||||
|
error:Reason:Stk ->
|
||||||
|
?LOG(error, "~nParse failed for ~0p~n~0p~nFrame data:~0p",
|
||||||
|
[Reason, Stk, Data]),
|
||||||
|
{[{frame_error, Reason}|Packets], State}
|
||||||
|
end.
|
||||||
|
|
||||||
|
next_incoming_msgs([Packet]) ->
|
||||||
|
{incoming, Packet};
|
||||||
|
next_incoming_msgs(Packets) ->
|
||||||
|
[{incoming, Packet} || Packet <- lists:reverse(Packets)].
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle incoming packet
|
||||||
|
|
||||||
|
handle_incoming(Packet, State) when is_record(Packet, stomp_frame) ->
|
||||||
|
ok = inc_incoming_stats(Packet),
|
||||||
|
?LOG(debug, "RECV ~s", [emqx_stomp_frame:format(Packet)]),
|
||||||
|
with_channel(handle_in, [Packet], State);
|
||||||
|
|
||||||
|
handle_incoming(FrameError, State) ->
|
||||||
|
with_channel(handle_in, [FrameError], State).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% With Channel
|
||||||
|
|
||||||
|
with_channel(Fun, Args, State = #state{channel = Channel}) ->
|
||||||
|
case erlang:apply(emqx_stomp_channel, Fun, Args ++ [Channel]) of
|
||||||
|
ok -> {ok, State};
|
||||||
|
{ok, NChannel} ->
|
||||||
|
{ok, State#state{channel = NChannel}};
|
||||||
|
{ok, Replies, NChannel} ->
|
||||||
|
{ok, next_msgs(Replies), State#state{channel = NChannel}};
|
||||||
|
{shutdown, Reason, NChannel} ->
|
||||||
|
shutdown(Reason, State#state{channel = NChannel});
|
||||||
|
{shutdown, Reason, Packet, NChannel} ->
|
||||||
|
NState = State#state{channel = NChannel},
|
||||||
|
ok = handle_outgoing(Packet, NState),
|
||||||
|
shutdown(Reason, NState)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle outgoing packets
|
||||||
|
|
||||||
|
handle_outgoing(Packets, State) when is_list(Packets) ->
|
||||||
|
send(lists:map(serialize_and_inc_stats_fun(State), Packets), State);
|
||||||
|
|
||||||
|
handle_outgoing(Packet, State) ->
|
||||||
|
send((serialize_and_inc_stats_fun(State))(Packet), State).
|
||||||
|
|
||||||
|
serialize_and_inc_stats_fun(#state{serialize = Serialize, channel = Channel}) ->
|
||||||
|
Ctx = emqx_stomp_channel:info(ctx, Channel),
|
||||||
|
fun(Packet) ->
|
||||||
|
case emqx_stomp_frame:serialize_pkt(Packet, Serialize) of
|
||||||
|
<<>> -> ?LOG(warning, "~s is discarded due to the frame is too large!",
|
||||||
|
[emqx_stomp_frame:format(Packet)]),
|
||||||
|
ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped.too_large'),
|
||||||
|
ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped'),
|
||||||
|
<<>>;
|
||||||
|
Data -> ?LOG(debug, "SEND ~s", [emqx_stomp_frame:format(Packet)]),
|
||||||
|
ok = inc_outgoing_stats(Packet),
|
||||||
|
Data
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Send data
|
||||||
|
|
||||||
|
-spec(send(iodata(), state()) -> ok).
|
||||||
|
send(IoData, #state{transport = Transport, socket = Socket, channel = Channel}) ->
|
||||||
|
Ctx = emqx_stomp_channel:info(ctx, Channel),
|
||||||
|
Oct = iolist_size(IoData),
|
||||||
|
ok = emqx_gateway_ctx:metrics_inc(Ctx, 'bytes.sent', Oct),
|
||||||
|
inc_counter(outgoing_bytes, Oct),
|
||||||
|
%emqx_congestion:maybe_alarm_conn_congestion(Socket, Transport, Channel),
|
||||||
|
case Transport:async_send(Socket, IoData, [nosuspend]) of
|
||||||
|
ok -> ok;
|
||||||
|
Error = {error, _Reason} ->
|
||||||
|
%% Send an inet_reply to postpone handling the error
|
||||||
|
self() ! {inet_reply, Socket, Error},
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle Info
|
||||||
|
|
||||||
|
handle_info(activate_socket, State = #state{sockstate = OldSst}) ->
|
||||||
|
case activate_socket(State) of
|
||||||
|
{ok, NState = #state{sockstate = NewSst}} ->
|
||||||
|
case OldSst =/= NewSst of
|
||||||
|
true -> {ok, {event, NewSst}, NState};
|
||||||
|
false -> {ok, NState}
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
handle_info({sock_error, Reason}, State)
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_info({sock_error, Reason}, State) ->
|
||||||
|
case Reason =/= closed andalso Reason =/= einval of
|
||||||
|
true -> ?LOG(warning, "socket_error: ~p", [Reason]);
|
||||||
|
false -> ok
|
||||||
|
end,
|
||||||
|
handle_info({sock_closed, Reason}, close_socket(State));
|
||||||
|
|
||||||
|
handle_info(Info, State) ->
|
||||||
|
with_channel(handle_info, [Info], State).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle Info
|
||||||
|
|
||||||
|
handle_cast({async_set_socket_options, Opts},
|
||||||
|
State = #state{transport = Transport,
|
||||||
|
socket = Socket
|
||||||
|
}) ->
|
||||||
|
case Transport:setopts(Socket, Opts) of
|
||||||
|
ok -> ?tp(info, "custom_socket_options_successfully", #{opts => Opts});
|
||||||
|
Err -> ?tp(error, "failed_to_set_custom_socket_optionn", #{reason => Err})
|
||||||
|
end,
|
||||||
|
State;
|
||||||
|
handle_cast(Req, State) ->
|
||||||
|
?tp(error, "received_unknown_cast", #{cast => Req}),
|
||||||
|
State.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Ensure rate limit
|
||||||
|
|
||||||
|
ensure_rate_limit(Stats, State = #state{limiter = Limiter}) ->
|
||||||
|
case ?ENABLED(Limiter) andalso emqx_limiter:check(Stats, Limiter) of
|
||||||
|
false -> State;
|
||||||
|
{ok, Limiter1} ->
|
||||||
|
State#state{limiter = Limiter1};
|
||||||
|
{pause, Time, Limiter1} ->
|
||||||
|
?LOG(warning, "Pause ~pms due to rate limit", [Time]),
|
||||||
|
TRef = emqx_misc:start_timer(Time, limit_timeout),
|
||||||
|
State#state{sockstate = blocked,
|
||||||
|
limiter = Limiter1,
|
||||||
|
limit_timer = TRef
|
||||||
|
}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Run GC and Check OOM
|
||||||
|
|
||||||
|
run_gc(Stats, State = #state{gc_state = GcSt}) ->
|
||||||
|
case ?ENABLED(GcSt) andalso emqx_gc:run(Stats, GcSt) of
|
||||||
|
false -> State;
|
||||||
|
{_IsGC, GcSt1} ->
|
||||||
|
State#state{gc_state = GcSt1}
|
||||||
|
end.
|
||||||
|
|
||||||
|
check_oom(State = #state{channel = Channel}) ->
|
||||||
|
Zone = emqx_stomp_channel:info(zone, Channel),
|
||||||
|
OomPolicy = emqx_gateway_utils:oom_policy(Zone),
|
||||||
|
?tp(debug, check_oom, #{policy => OomPolicy}),
|
||||||
|
case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of
|
||||||
|
{shutdown, Reason} ->
|
||||||
|
%% triggers terminate/2 callback immediately
|
||||||
|
erlang:exit({shutdown, Reason});
|
||||||
|
_Other ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
State.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Activate Socket
|
||||||
|
|
||||||
|
-compile({inline, [activate_socket/1]}).
|
||||||
|
activate_socket(State = #state{sockstate = closed}) ->
|
||||||
|
{ok, State};
|
||||||
|
activate_socket(State = #state{sockstate = blocked}) ->
|
||||||
|
{ok, State};
|
||||||
|
activate_socket(State = #state{transport = Transport,
|
||||||
|
socket = Socket,
|
||||||
|
active_n = N}) ->
|
||||||
|
case Transport:setopts(Socket, [{active, N}]) of
|
||||||
|
ok -> {ok, State#state{sockstate = running}};
|
||||||
|
Error -> Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Close Socket
|
||||||
|
|
||||||
|
close_socket(State = #state{sockstate = closed}) -> State;
|
||||||
|
close_socket(State = #state{transport = Transport, socket = Socket}) ->
|
||||||
|
ok = Transport:fast_close(Socket),
|
||||||
|
State#state{sockstate = closed}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Inc incoming/outgoing stats
|
||||||
|
|
||||||
|
%% XXX: Other packet type?
|
||||||
|
inc_incoming_stats(Packet = ?PACKET(Type)) ->
|
||||||
|
inc_counter(recv_pkt, 1),
|
||||||
|
case Type =:= ?CMD_SEND of
|
||||||
|
true ->
|
||||||
|
inc_counter(recv_msg, 1),
|
||||||
|
inc_counter(incoming_pubs, 1);
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
emqx_metrics:inc_recv(Packet).
|
||||||
|
|
||||||
|
inc_outgoing_stats(Packet = ?PACKET(Type)) ->
|
||||||
|
inc_counter(send_pkt, 1),
|
||||||
|
case Type =:= ?CMD_MESSAGE of
|
||||||
|
true ->
|
||||||
|
inc_counter(send_msg, 1),
|
||||||
|
inc_counter(outgoing_pubs, 1);
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
emqx_metrics:inc_sent(Packet).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Helper functions
|
||||||
|
|
||||||
|
next_msgs(Packet) when is_record(Packet, stomp_frame) ->
|
||||||
|
{outgoing, Packet};
|
||||||
|
next_msgs(Event) when is_tuple(Event) ->
|
||||||
|
Event;
|
||||||
|
next_msgs(More) when is_list(More) ->
|
||||||
|
More.
|
||||||
|
|
||||||
|
shutdown(Reason, State) ->
|
||||||
|
stop({shutdown, Reason}, State).
|
||||||
|
|
||||||
|
shutdown(Reason, Reply, State) ->
|
||||||
|
stop({shutdown, Reason}, Reply, State).
|
||||||
|
|
||||||
|
stop(Reason, State) ->
|
||||||
|
{stop, Reason, State}.
|
||||||
|
|
||||||
|
stop(Reason, Reply, State) ->
|
||||||
|
{stop, Reason, Reply, State}.
|
||||||
|
|
||||||
|
inc_counter(Key, Inc) ->
|
||||||
|
_ = emqx_pd:inc_counter(Key, Inc),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% For CT tests
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
set_field(Name, Value, State) ->
|
||||||
|
Pos = emqx_misc:index_of(Name, record_info(fields, state)),
|
||||||
|
setelement(Pos+1, State, Value).
|
||||||
|
|
||||||
|
get_state(Pid) ->
|
||||||
|
State = sys:get_state(Pid),
|
||||||
|
maps:from_list(lists:zip(record_info(fields, state),
|
||||||
|
tl(tuple_to_list(State)))).
|
|
@ -68,14 +68,16 @@
|
||||||
|
|
||||||
-module(emqx_stomp_frame).
|
-module(emqx_stomp_frame).
|
||||||
|
|
||||||
-include("emqx_stomp.hrl").
|
-include("src/stomp/include/emqx_stomp.hrl").
|
||||||
|
|
||||||
-export([ init_parer_state/1
|
-export([ initial_parse_state/1
|
||||||
, parse/2
|
, parse/2
|
||||||
, serialize/1
|
, serialize_opts/0
|
||||||
|
, serialize_pkt/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([ make/2
|
-export([ make/1
|
||||||
|
, make/2
|
||||||
, make/3
|
, make/3
|
||||||
, format/1
|
, format/1
|
||||||
]).
|
]).
|
||||||
|
@ -96,28 +98,33 @@
|
||||||
|
|
||||||
-record(frame_limit, {max_header_num, max_header_length, max_body_length}).
|
-record(frame_limit, {max_header_num, max_header_length, max_body_length}).
|
||||||
|
|
||||||
-type(result() :: {ok, stomp_frame(), binary()}
|
-type(parse_result() :: {ok, stomp_frame(), binary()}
|
||||||
| {more, parser()}
|
| {more, parse_state()}).
|
||||||
| {error, any()}).
|
|
||||||
|
|
||||||
-type(parser() :: #{phase := none | command | headers | hdname | hdvalue | body,
|
-type(parse_state() ::
|
||||||
|
#{phase := none | command | headers | hdname | hdvalue | body,
|
||||||
pre => binary(),
|
pre => binary(),
|
||||||
state := #parser_state{}}).
|
state := #parser_state{}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, [serialize_pkt/2,make/1]}).
|
||||||
|
|
||||||
%% @doc Initialize a parser
|
%% @doc Initialize a parser
|
||||||
-spec init_parer_state([proplists:property()]) -> parser().
|
-spec initial_parse_state(map()) -> parse_state().
|
||||||
init_parer_state(Opts) ->
|
initial_parse_state(Opts) ->
|
||||||
#{phase => none, state => #parser_state{limit = limit(Opts)}}.
|
#{phase => none, state => #parser_state{limit = limit(Opts)}}.
|
||||||
|
|
||||||
limit(Opts) ->
|
limit(Opts) ->
|
||||||
#frame_limit{max_header_num = g(max_header_num, Opts, ?MAX_HEADER_NUM),
|
#frame_limit{
|
||||||
|
max_header_num = g(max_header_num, Opts, ?MAX_HEADER_NUM),
|
||||||
max_header_length = g(max_header_length, Opts, ?MAX_BODY_LENGTH),
|
max_header_length = g(max_header_length, Opts, ?MAX_BODY_LENGTH),
|
||||||
max_body_length = g(max_body_length, Opts, ?MAX_BODY_LENGTH)}.
|
max_body_length = g(max_body_length, Opts, ?MAX_BODY_LENGTH)
|
||||||
|
}.
|
||||||
|
|
||||||
g(Key, Opts, Val) ->
|
g(Key, Opts, Val) ->
|
||||||
proplists:get_value(Key, Opts, Val).
|
maps:get(Key, Opts, Val).
|
||||||
|
|
||||||
-spec parse(binary(), parser()) -> result().
|
-spec parse(binary(), parse_state()) -> parse_result().
|
||||||
parse(<<>>, Parser) ->
|
parse(<<>>, Parser) ->
|
||||||
{more, Parser};
|
{more, Parser};
|
||||||
|
|
||||||
|
@ -131,11 +138,14 @@ parse(<<?CR, ?LF, Rest/binary>>, #{phase := Phase, state := State}) ->
|
||||||
parse(<<?CR>>, Parser) ->
|
parse(<<?CR>>, Parser) ->
|
||||||
{more, Parser#{pre => <<?CR>>}};
|
{more, Parser#{pre => <<?CR>>}};
|
||||||
parse(<<?CR, _Ch:8, _Rest/binary>>, _Parser) ->
|
parse(<<?CR, _Ch:8, _Rest/binary>>, _Parser) ->
|
||||||
{error, linefeed_expected};
|
error(linefeed_expected);
|
||||||
|
|
||||||
parse(<<?BSL>>, Parser = #{phase := Phase}) when Phase =:= hdname; Phase =:= hdvalue ->
|
parse(<<?BSL>>, Parser = #{phase := Phase}) when Phase =:= hdname;
|
||||||
|
Phase =:= hdvalue ->
|
||||||
{more, Parser#{pre => <<?BSL>>}};
|
{more, Parser#{pre => <<?BSL>>}};
|
||||||
parse(<<?BSL, Ch:8, Rest/binary>>, #{phase := Phase, state := State}) when Phase =:= hdname; Phase =:= hdvalue ->
|
parse(<<?BSL, Ch:8, Rest/binary>>,
|
||||||
|
#{phase := Phase, state := State}) when Phase =:= hdname;
|
||||||
|
Phase =:= hdvalue ->
|
||||||
parse(Phase, Rest, acc(unescape(Ch), State));
|
parse(Phase, Rest, acc(unescape(Ch), State));
|
||||||
|
|
||||||
parse(Bytes, #{phase := none, state := State}) ->
|
parse(Bytes, #{phase := none, state := State}) ->
|
||||||
|
@ -153,14 +163,19 @@ parse(headers, Bin, State) ->
|
||||||
parse(hdname, Bin, State);
|
parse(hdname, Bin, State);
|
||||||
|
|
||||||
parse(hdname, <<?LF, _Rest/binary>>, _State) ->
|
parse(hdname, <<?LF, _Rest/binary>>, _State) ->
|
||||||
{error, unexpected_linefeed};
|
error(unexpected_linefeed);
|
||||||
parse(hdname, <<?COLON, Rest/binary>>, State = #parser_state{acc = Acc}) ->
|
parse(hdname, <<?COLON, Rest/binary>>, State = #parser_state{acc = Acc}) ->
|
||||||
parse(hdvalue, Rest, State#parser_state{hdname = Acc, acc = <<>>});
|
parse(hdvalue, Rest, State#parser_state{hdname = Acc, acc = <<>>});
|
||||||
parse(hdname, <<Ch:8, Rest/binary>>, State) ->
|
parse(hdname, <<Ch:8, Rest/binary>>, State) ->
|
||||||
parse(hdname, Rest, acc(Ch, State));
|
parse(hdname, Rest, acc(Ch, State));
|
||||||
|
|
||||||
parse(hdvalue, <<?LF, Rest/binary>>, State = #parser_state{headers = Headers, hdname = Name, acc = Acc}) ->
|
parse(hdvalue, <<?LF, Rest/binary>>,
|
||||||
parse(headers, Rest, State#parser_state{headers = add_header(Name, Acc, Headers), hdname = undefined, acc = <<>>});
|
State = #parser_state{headers = Headers, hdname = Name, acc = Acc}) ->
|
||||||
|
NState = State#parser_state{headers = add_header(Name, Acc, Headers),
|
||||||
|
hdname = undefined,
|
||||||
|
acc = <<>>
|
||||||
|
},
|
||||||
|
parse(headers, Rest, NState);
|
||||||
parse(hdvalue, <<Ch:8, Rest/binary>>, State) ->
|
parse(hdvalue, <<Ch:8, Rest/binary>>, State) ->
|
||||||
parse(hdvalue, Rest, acc(Ch, State)).
|
parse(hdvalue, Rest, acc(Ch, State)).
|
||||||
|
|
||||||
|
@ -170,15 +185,19 @@ parse(body, <<>>, State, Length) ->
|
||||||
parse(body, Bin, State, none) ->
|
parse(body, Bin, State, none) ->
|
||||||
case binary:split(Bin, <<?NULL>>) of
|
case binary:split(Bin, <<?NULL>>) of
|
||||||
[Chunk, Rest] ->
|
[Chunk, Rest] ->
|
||||||
{ok, new_frame(acc(Chunk, State)), Rest};
|
{ok, new_frame(acc(Chunk, State)), Rest, new_state(State)};
|
||||||
[Chunk] ->
|
[Chunk] ->
|
||||||
{more, #{phase => body, length => none, state => acc(Chunk, State)}}
|
{more, #{phase => body,
|
||||||
|
length => none,
|
||||||
|
state => acc(Chunk, State)}}
|
||||||
end;
|
end;
|
||||||
parse(body, Bin, State, Len) when byte_size(Bin) >= (Len+1) ->
|
parse(body, Bin, State, Len) when byte_size(Bin) >= (Len+1) ->
|
||||||
<<Chunk:Len/binary, ?NULL, Rest/binary>> = Bin,
|
<<Chunk:Len/binary, ?NULL, Rest/binary>> = Bin,
|
||||||
{ok, new_frame(acc(Chunk, State)), Rest};
|
{ok, new_frame(acc(Chunk, State)), Rest, new_state(State)};
|
||||||
parse(body, Bin, State, Len) ->
|
parse(body, Bin, State, Len) ->
|
||||||
{more, #{phase => body, length => Len - byte_size(Bin), state => acc(Bin, State)}}.
|
{more, #{phase => body,
|
||||||
|
length => Len - byte_size(Bin),
|
||||||
|
state => acc(Bin, State)}}.
|
||||||
|
|
||||||
add_header(Name, Value, Headers) ->
|
add_header(Name, Value, Headers) ->
|
||||||
case lists:keyfind(Name, 1, Headers) of
|
case lists:keyfind(Name, 1, Headers) of
|
||||||
|
@ -208,20 +227,33 @@ unescape($r) -> ?CR;
|
||||||
unescape($n) -> ?LF;
|
unescape($n) -> ?LF;
|
||||||
unescape($c) -> ?COLON;
|
unescape($c) -> ?COLON;
|
||||||
unescape($\\) -> ?BSL;
|
unescape($\\) -> ?BSL;
|
||||||
unescape(_Ch) -> {error, cannnot_unescape}.
|
unescape(_Ch) -> error(cannnot_unescape).
|
||||||
|
|
||||||
serialize(#stomp_frame{command = Cmd, headers = Headers, body = Body}) ->
|
%%--------------------------------------------------------------------
|
||||||
|
%% Serialize funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
serialize_opts() ->
|
||||||
|
#{}.
|
||||||
|
|
||||||
|
serialize_pkt(#stomp_frame{command = heartbeat}, _SerializeOpts) ->
|
||||||
|
<<$\n>>;
|
||||||
|
|
||||||
|
serialize_pkt(#stomp_frame{command = Cmd, headers = Headers, body = Body},
|
||||||
|
_SerializeOpts) ->
|
||||||
Headers1 = lists:keydelete(<<"content-length">>, 1, Headers),
|
Headers1 = lists:keydelete(<<"content-length">>, 1, Headers),
|
||||||
Headers2 =
|
Headers2 =
|
||||||
case iolist_size(Body) of
|
case iolist_size(Body) of
|
||||||
0 -> Headers1;
|
0 -> Headers1;
|
||||||
Len -> Headers1 ++ [{<<"content-length">>, Len}]
|
Len -> Headers1 ++ [{<<"content-length">>, Len}]
|
||||||
end,
|
end,
|
||||||
[Cmd, ?LF, [serialize(header, Header) || Header <- Headers2], ?LF, Body, 0].
|
[Cmd,
|
||||||
|
?LF, [serialize_pkt(header, Header) || Header <- Headers2],
|
||||||
|
?LF, Body, 0];
|
||||||
|
|
||||||
serialize(header, {Name, Val}) when is_integer(Val) ->
|
serialize_pkt(header, {Name, Val}) when is_integer(Val) ->
|
||||||
[escape(Name), ?COLON, integer_to_list(Val), ?LF];
|
[escape(Name), ?COLON, integer_to_list(Val), ?LF];
|
||||||
serialize(header, {Name, Val}) ->
|
serialize_pkt(header, {Name, Val}) ->
|
||||||
[escape(Name), ?COLON, escape(Val), ?LF].
|
[escape(Name), ?COLON, escape(Val), ?LF].
|
||||||
|
|
||||||
escape(Bin) when is_binary(Bin) ->
|
escape(Bin) when is_binary(Bin) ->
|
||||||
|
@ -232,8 +264,18 @@ escape(?BSL) -> <<?BSL, ?BSL>>;
|
||||||
escape(?COLON) -> <<?BSL, $c>>;
|
escape(?COLON) -> <<?BSL, $c>>;
|
||||||
escape(Ch) -> <<Ch>>.
|
escape(Ch) -> <<Ch>>.
|
||||||
|
|
||||||
|
new_state(#parser_state{limit = Limit}) ->
|
||||||
|
#{phase => none, state => #parser_state{limit = Limit}}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% ???
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
%% @doc Make a frame
|
%% @doc Make a frame
|
||||||
|
|
||||||
|
make(heartbeat) ->
|
||||||
|
#stomp_frame{command = heartbeat}.
|
||||||
|
|
||||||
make(<<"CONNECTED">>, Headers) ->
|
make(<<"CONNECTED">>, Headers) ->
|
||||||
#stomp_frame{command = <<"CONNECTED">>,
|
#stomp_frame{command = <<"CONNECTED">>,
|
||||||
headers = [{<<"server">>, ?STOMP_SERVER} | Headers]};
|
headers = [{<<"server">>, ?STOMP_SERVER} | Headers]};
|
||||||
|
@ -245,5 +287,4 @@ make(Command, Headers, Body) ->
|
||||||
#stomp_frame{command = Command, headers = Headers, body = Body}.
|
#stomp_frame{command = Command, headers = Headers, body = Body}.
|
||||||
|
|
||||||
%% @doc Format a frame
|
%% @doc Format a frame
|
||||||
format(Frame) -> serialize(Frame).
|
format(Frame) -> serialize_pkt(Frame, #{}).
|
||||||
|
|
|
@ -17,10 +17,11 @@
|
||||||
%% @doc Stomp heartbeat.
|
%% @doc Stomp heartbeat.
|
||||||
-module(emqx_stomp_heartbeat).
|
-module(emqx_stomp_heartbeat).
|
||||||
|
|
||||||
-include("emqx_stomp.hrl").
|
-include("src/stomp/include/emqx_stomp.hrl").
|
||||||
|
|
||||||
-export([ init/1
|
-export([ init/1
|
||||||
, check/3
|
, check/3
|
||||||
|
, reset/3
|
||||||
, info/1
|
, info/1
|
||||||
, interval/2
|
, interval/2
|
||||||
]).
|
]).
|
||||||
|
@ -33,7 +34,6 @@
|
||||||
outgoing => #heartbeater{}
|
outgoing => #heartbeater{}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -77,6 +77,15 @@ check(NewVal, HrtBter = #heartbeater{statval = OldVal,
|
||||||
true -> {error, timeout}
|
true -> {error, timeout}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-spec reset(name(), pos_integer(), heartbeat())
|
||||||
|
-> heartbeat().
|
||||||
|
reset(Name, NewVal, HrtBt) ->
|
||||||
|
HrtBter = maps:get(Name, HrtBt),
|
||||||
|
HrtBt#{Name => reset(NewVal, HrtBter)}.
|
||||||
|
|
||||||
|
reset(NewVal, HrtBter) ->
|
||||||
|
HrtBter#heartbeater{statval = NewVal, repeat = 1}.
|
||||||
|
|
||||||
-spec info(heartbeat()) -> map().
|
-spec info(heartbeat()) -> map().
|
||||||
info(HrtBt) ->
|
info(HrtBt) ->
|
||||||
maps:map(fun(_, #heartbeater{interval = Intv,
|
maps:map(fun(_, #heartbeater{interval = Intv,
|
|
@ -0,0 +1,153 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2017-2021 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_stomp_impl).
|
||||||
|
|
||||||
|
-include_lib("emqx_gateway/include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
-behavior(emqx_gateway_impl).
|
||||||
|
|
||||||
|
%% APIs
|
||||||
|
-export([ load/0
|
||||||
|
, unload/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ init/1
|
||||||
|
, on_insta_create/3
|
||||||
|
, on_insta_update/4
|
||||||
|
, on_insta_destroy/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(TCP_OPTS, [binary, {packet, raw}, {reuseaddr, true}, {nodelay, true}]).
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, [load/0]}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
load() ->
|
||||||
|
RegistryOptions = [ {cbkmod, ?MODULE}
|
||||||
|
, {schema, emqx_stomp_schema}
|
||||||
|
],
|
||||||
|
|
||||||
|
YourOptions = [param1, param2],
|
||||||
|
emqx_gateway_registry:load(stomp, RegistryOptions, YourOptions).
|
||||||
|
|
||||||
|
unload() ->
|
||||||
|
emqx_gateway_registry:unload(stomp).
|
||||||
|
|
||||||
|
init([param1, param2]) ->
|
||||||
|
GwState = #{},
|
||||||
|
{ok, GwState}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% emqx_gateway_registry callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
on_insta_create(_Insta = #{ id := InstaId,
|
||||||
|
rawconf := RawConf
|
||||||
|
}, Ctx, _GwState) ->
|
||||||
|
%% Step1. Fold the rawconfs to listeners
|
||||||
|
Listeners = emqx_gateway_utils:normalize_rawconf(RawConf),
|
||||||
|
%% Step2. Start listeners or escokd:specs
|
||||||
|
ListenerPids = lists:map(fun(Lis) ->
|
||||||
|
start_listener(InstaId, Ctx, Lis)
|
||||||
|
end, Listeners),
|
||||||
|
%% FIXME: How to throw an exception to interrupt the restart logic ?
|
||||||
|
%% FIXME: Assign ctx to InstaState
|
||||||
|
{ok, ListenerPids, _InstaState = #{ctx => Ctx}}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
on_insta_update(NewInsta, OldInstace, GwInstaState = #{ctx := Ctx}, GwState) ->
|
||||||
|
InstaId = maps:get(id, NewInsta),
|
||||||
|
try
|
||||||
|
%% XXX: 1. How hot-upgrade the changes ???
|
||||||
|
%% XXX: 2. Check the New confs first before destroy old instance ???
|
||||||
|
on_insta_destroy(OldInstace, GwInstaState, GwState),
|
||||||
|
on_insta_create(NewInsta, Ctx, GwState)
|
||||||
|
catch
|
||||||
|
Class : Reason : Stk ->
|
||||||
|
logger:error("Failed to update stomp instance ~s; "
|
||||||
|
"reason: {~0p, ~0p} stacktrace: ~0p",
|
||||||
|
[InstaId, Class, Reason, Stk]),
|
||||||
|
{error, {Class, Reason}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
on_insta_destroy(_Insta = #{ id := InstaId,
|
||||||
|
rawconf := RawConf
|
||||||
|
}, _GwInstaState, _GwState) ->
|
||||||
|
Listeners = emqx_gateway_utils:normalize_rawconf(RawConf),
|
||||||
|
lists:foreach(fun(Lis) ->
|
||||||
|
stop_listener(InstaId, Lis)
|
||||||
|
end, Listeners).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) ->
|
||||||
|
case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of
|
||||||
|
{ok, Pid} ->
|
||||||
|
io:format("Start stomp ~s:~s listener on ~s successfully.~n",
|
||||||
|
[InstaId, Type, format(ListenOn)]),
|
||||||
|
Pid;
|
||||||
|
{error, Reason} ->
|
||||||
|
io:format(standard_error,
|
||||||
|
"Failed to start stomp ~s:~s listener on ~s: ~0p~n",
|
||||||
|
[InstaId, Type, format(ListenOn), Reason]),
|
||||||
|
throw({badconf, Reason})
|
||||||
|
end.
|
||||||
|
|
||||||
|
start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) ->
|
||||||
|
Name = name(InstaId, Type),
|
||||||
|
esockd:open(Name, ListenOn, merge_default(SocketOpts),
|
||||||
|
{emqx_stomp_connection, start_link, [Cfg#{ctx => Ctx}]}).
|
||||||
|
|
||||||
|
name(InstaId, Type) ->
|
||||||
|
list_to_atom(lists:concat([InstaId, ":", Type])).
|
||||||
|
|
||||||
|
merge_default(Options) ->
|
||||||
|
case lists:keytake(tcp_options, 1, Options) of
|
||||||
|
{value, {tcp_options, TcpOpts}, Options1} ->
|
||||||
|
[{tcp_options, emqx_misc:merge_opts(?TCP_OPTS, TcpOpts)} | Options1];
|
||||||
|
false ->
|
||||||
|
[{tcp_options, ?TCP_OPTS} | Options]
|
||||||
|
end.
|
||||||
|
|
||||||
|
format(Port) when is_integer(Port) ->
|
||||||
|
io_lib:format("0.0.0.0:~w", [Port]);
|
||||||
|
format({Addr, Port}) when is_list(Addr) ->
|
||||||
|
io_lib:format("~s:~w", [Addr, Port]);
|
||||||
|
format({Addr, Port}) when is_tuple(Addr) ->
|
||||||
|
io_lib:format("~s:~w", [inet:ntoa(Addr), Port]).
|
||||||
|
|
||||||
|
stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) ->
|
||||||
|
StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg),
|
||||||
|
case StopRet of
|
||||||
|
ok -> io:format("Stop stomp ~s:~s listener on ~s successfully.~n",
|
||||||
|
[InstaId, Type, format(ListenOn)]);
|
||||||
|
{error, Reason} ->
|
||||||
|
io:format(standard_error,
|
||||||
|
"Failed to stop stomp ~s:~s listener on ~s: ~0p~n",
|
||||||
|
[InstaId, Type, format(ListenOn), Reason]
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
StopRet.
|
||||||
|
|
||||||
|
stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) ->
|
||||||
|
Name = name(InstaId, Type),
|
||||||
|
esockd:close(Name, ListenOn).
|
|
@ -0,0 +1,92 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020-2021 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(EMQX_STOMP_HRL).
|
||||||
|
-define(EMQX_STOMP_HRL, true).
|
||||||
|
|
||||||
|
-define(STOMP_VER, <<"1.2">>).
|
||||||
|
|
||||||
|
-define(STOMP_SERVER, <<"emqx-stomp/1.2">>).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% STOMP Frame
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% client command
|
||||||
|
-define(CMD_STOMP, <<"STOMP">>).
|
||||||
|
-define(CMD_CONNECT, <<"CONNECT">>).
|
||||||
|
-define(CMD_SEND, <<"SEND">>).
|
||||||
|
-define(CMD_SUBSCRIBE, <<"SUBSCRIBE">>).
|
||||||
|
-define(CMD_UNSUBSCRIBE, <<"UNSUBSCRIBE">>).
|
||||||
|
-define(CMD_BEGIN, <<"BEGIN">>).
|
||||||
|
-define(CMD_COMMIT, <<"COMMIT">>).
|
||||||
|
-define(CMD_ABORT, <<"ABORT">>).
|
||||||
|
-define(CMD_ACK, <<"ACK">>).
|
||||||
|
-define(CMD_NACK, <<"NACK">>).
|
||||||
|
-define(CMD_DISCONNECT, <<"DISCONNECT">>).
|
||||||
|
|
||||||
|
%% server command
|
||||||
|
-define(CMD_CONNECTED, <<"CONNECTED">>).
|
||||||
|
-define(CMD_MESSAGE, <<"MESSAGE">>).
|
||||||
|
-define(CMD_RECEIPT, <<"RECEIPT">>).
|
||||||
|
-define(CMD_ERROR, <<"ERROR">>).
|
||||||
|
|
||||||
|
-type client_command() :: binary().
|
||||||
|
%-type client_command() :: ?CMD_SEND | ?CMD_SUBSCRIBE | ?CMD_UNSUBSCRIBE
|
||||||
|
% | ?CMD_BEGIN | ?CMD_COMMIT | ?CMD_ABORT | ?CMD_ACK
|
||||||
|
% | ?CMD_NACK | ?CMD_DISCONNECT | ?CMD_CONNECT
|
||||||
|
% | ?CMD_STOMP.
|
||||||
|
%
|
||||||
|
-type server_command() :: binary().
|
||||||
|
%-type server_command() :: ?CMD_CONNECTED | ?CMD_MESSAGE | ?CMD_RECEIPT
|
||||||
|
% | ?CMD_ERROR.
|
||||||
|
|
||||||
|
-record(stomp_frame, {
|
||||||
|
command :: client_command() | server_command(),
|
||||||
|
headers = [],
|
||||||
|
body = <<>> :: iodata()}
|
||||||
|
).
|
||||||
|
|
||||||
|
-type stomp_frame() :: #stomp_frame{}.
|
||||||
|
|
||||||
|
-define(PACKET(CMD), #stomp_frame{command = CMD}).
|
||||||
|
|
||||||
|
-define(PACKET(CMD, Headers), #stomp_frame{command = CMD, headers = Headers}).
|
||||||
|
|
||||||
|
-define(PACKET(CMD, Headers, Body), #stomp_frame{command = CMD,
|
||||||
|
headers = Headers,
|
||||||
|
body = Body
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Frame Size Limits
|
||||||
|
%%
|
||||||
|
%% To prevent malicious clients from exploiting memory allocation in a server,
|
||||||
|
%% servers MAY place maximum limits on:
|
||||||
|
%%
|
||||||
|
%% the number of frame headers allowed in a single frame
|
||||||
|
%% the maximum length of header lines
|
||||||
|
%% the maximum size of a frame body
|
||||||
|
%%
|
||||||
|
%% If these limits are exceeded the server SHOULD send the client an ERROR frame
|
||||||
|
%% and then close the connection.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(MAX_HEADER_NUM, 10).
|
||||||
|
-define(MAX_HEADER_LENGTH, 1024).
|
||||||
|
-define(MAX_BODY_LENGTH, 65536).
|
||||||
|
|
||||||
|
-endif.
|
|
@ -0,0 +1,87 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 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_gateway_registry_SUITE).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
all() -> emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Setups
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init_per_suite(Cfg) ->
|
||||||
|
emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_configs/1),
|
||||||
|
Cfg.
|
||||||
|
|
||||||
|
end_per_suite(_Cfg) ->
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_gateway]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
set_special_configs(emqx_gateway) ->
|
||||||
|
emqx_config:put(
|
||||||
|
[emqx_gateway],
|
||||||
|
#{stomp =>
|
||||||
|
#{'1' =>
|
||||||
|
#{authenticator => allow_anonymous,
|
||||||
|
clientinfo_override =>
|
||||||
|
#{password => "${Packet.headers.passcode}",
|
||||||
|
username => "${Packet.headers.login}"},
|
||||||
|
frame =>
|
||||||
|
#{max_body_length => 8192,
|
||||||
|
max_headers => 10,
|
||||||
|
max_headers_length => 1024},
|
||||||
|
listener =>
|
||||||
|
#{tcp =>
|
||||||
|
#{'1' =>
|
||||||
|
#{acceptors => 16,active_n => 100,backlog => 1024,
|
||||||
|
bind => 61613,high_watermark => 1048576,
|
||||||
|
max_conn_rate => 1000,max_connections => 1024000,
|
||||||
|
send_timeout => 15000,send_timeout_close => true}}}}}}),
|
||||||
|
ok;
|
||||||
|
set_special_configs(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Test cases
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_load_unload(_) ->
|
||||||
|
OldCnt = length(emqx_gateway_registry:list()),
|
||||||
|
RgOpts = [{cbkmod, ?MODULE}],
|
||||||
|
GwOpts = [paramsin],
|
||||||
|
ok = emqx_gateway_registry:load(test, RgOpts, GwOpts),
|
||||||
|
?assertEqual(OldCnt+1, length(emqx_gateway_registry:list())),
|
||||||
|
|
||||||
|
#{cbkmod := ?MODULE,
|
||||||
|
rgopts := RgOpts,
|
||||||
|
gwopts := GwOpts,
|
||||||
|
state := #{gwstate := 1}} = emqx_gateway_registry:lookup(test),
|
||||||
|
|
||||||
|
{error, already_existed} = emqx_gateway_registry:load(test, [{cbkmod, ?MODULE}], GwOpts),
|
||||||
|
|
||||||
|
ok = emqx_gateway_registry:unload(test),
|
||||||
|
undefined = emqx_gateway_registry:lookup(test),
|
||||||
|
OldCnt = length(emqx_gateway_registry:list()),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
init([paramsin]) ->
|
||||||
|
{ok, _GwState = #{gwstate => 1}}.
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
-module(emqx_stomp_SUITE).
|
-module(emqx_stomp_SUITE).
|
||||||
|
|
||||||
-include_lib("emqx_stomp/include/emqx_stomp.hrl").
|
-include_lib("emqx_gateway/src/stomp/include/emqx_stomp.hrl").
|
||||||
|
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
|
@ -29,12 +29,37 @@ all() -> emqx_ct:all(?MODULE).
|
||||||
%% Setups
|
%% Setups
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Cfg) ->
|
||||||
emqx_ct_helpers:start_apps([emqx_stomp]),
|
emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_configs/1),
|
||||||
Config.
|
Cfg.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Cfg) ->
|
||||||
emqx_ct_helpers:stop_apps([emqx_stomp]).
|
emqx_ct_helpers:stop_apps([emqx_gateway]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
set_special_configs(emqx_gateway) ->
|
||||||
|
emqx_config:put(
|
||||||
|
[emqx_gateway],
|
||||||
|
#{stomp =>
|
||||||
|
#{'1' =>
|
||||||
|
#{authenticator => allow_anonymous,
|
||||||
|
clientinfo_override =>
|
||||||
|
#{password => "${Packet.headers.passcode}",
|
||||||
|
username => "${Packet.headers.login}"},
|
||||||
|
frame =>
|
||||||
|
#{max_body_length => 8192,
|
||||||
|
max_headers => 10,
|
||||||
|
max_headers_length => 1024},
|
||||||
|
listener =>
|
||||||
|
#{tcp =>
|
||||||
|
#{'1' =>
|
||||||
|
#{acceptors => 16,active_n => 100,backlog => 1024,
|
||||||
|
bind => 61613,high_watermark => 1048576,
|
||||||
|
max_conn_rate => 1000,max_connections => 1024000,
|
||||||
|
send_timeout => 15000,send_timeout_close => true}}}}}}),
|
||||||
|
ok;
|
||||||
|
set_special_configs(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Test Cases
|
%% Test Cases
|
||||||
|
@ -52,7 +77,7 @@ t_connect(_) ->
|
||||||
{ok, Data} = gen_tcp:recv(Sock, 0),
|
{ok, Data} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, Frame = #stomp_frame{command = <<"CONNECTED">>,
|
{ok, Frame = #stomp_frame{command = <<"CONNECTED">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = _}, _} = parse(Data),
|
body = _}, _, _} = parse(Data),
|
||||||
<<"2000,1000">> = proplists:get_value(<<"heart-beat">>, Frame#stomp_frame.headers),
|
<<"2000,1000">> = proplists:get_value(<<"heart-beat">>, Frame#stomp_frame.headers),
|
||||||
|
|
||||||
gen_tcp:send(Sock, serialize(<<"DISCONNECT">>,
|
gen_tcp:send(Sock, serialize(<<"DISCONNECT">>,
|
||||||
|
@ -61,22 +86,23 @@ t_connect(_) ->
|
||||||
{ok, Data1} = gen_tcp:recv(Sock, 0),
|
{ok, Data1} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
||||||
headers = [{<<"receipt-id">>, <<"12345">>}],
|
headers = [{<<"receipt-id">>, <<"12345">>}],
|
||||||
body = _}, _} = parse(Data1)
|
body = _}, _, _} = parse(Data1)
|
||||||
end),
|
end),
|
||||||
|
|
||||||
%% Connect will be failed, because of bad login or passcode
|
%% Connect will be failed, because of bad login or passcode
|
||||||
with_connection(fun(Sock) ->
|
%% FIXME: Waiting for authentication works
|
||||||
gen_tcp:send(Sock, serialize(<<"CONNECT">>,
|
%with_connection(fun(Sock) ->
|
||||||
[{<<"accept-version">>, ?STOMP_VER},
|
% gen_tcp:send(Sock, serialize(<<"CONNECT">>,
|
||||||
{<<"host">>, <<"127.0.0.1:61613">>},
|
% [{<<"accept-version">>, ?STOMP_VER},
|
||||||
{<<"login">>, <<"admin">>},
|
% {<<"host">>, <<"127.0.0.1:61613">>},
|
||||||
{<<"passcode">>, <<"admin">>},
|
% {<<"login">>, <<"admin">>},
|
||||||
{<<"heart-beat">>, <<"1000,2000">>}])),
|
% {<<"passcode">>, <<"admin">>},
|
||||||
{ok, Data} = gen_tcp:recv(Sock, 0),
|
% {<<"heart-beat">>, <<"1000,2000">>}])),
|
||||||
{ok, #stomp_frame{command = <<"ERROR">>,
|
% {ok, Data} = gen_tcp:recv(Sock, 0),
|
||||||
headers = _,
|
% {ok, #stomp_frame{command = <<"ERROR">>,
|
||||||
body = <<"Login or passcode error!">>}, _} = parse(Data)
|
% headers = _,
|
||||||
end),
|
% body = <<"Login or passcode error!">>}, _, _} = parse(Data)
|
||||||
|
% end),
|
||||||
|
|
||||||
%% Connect will be failed, because of bad version
|
%% Connect will be failed, because of bad version
|
||||||
with_connection(fun(Sock) ->
|
with_connection(fun(Sock) ->
|
||||||
|
@ -89,7 +115,7 @@ t_connect(_) ->
|
||||||
{ok, Data} = gen_tcp:recv(Sock, 0),
|
{ok, Data} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"ERROR">>,
|
{ok, #stomp_frame{command = <<"ERROR">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = <<"Supported protocol versions < 1.2">>}, _} = parse(Data)
|
body = <<"Login Failed: Supported protocol versions < 1.2">>}, _, _} = parse(Data)
|
||||||
end).
|
end).
|
||||||
|
|
||||||
t_heartbeat(_) ->
|
t_heartbeat(_) ->
|
||||||
|
@ -104,7 +130,7 @@ t_heartbeat(_) ->
|
||||||
{ok, Data} = gen_tcp:recv(Sock, 0),
|
{ok, Data} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = _}, _} = parse(Data),
|
body = _}, _, _} = parse(Data),
|
||||||
|
|
||||||
{ok, ?HEARTBEAT} = gen_tcp:recv(Sock, 0),
|
{ok, ?HEARTBEAT} = gen_tcp:recv(Sock, 0),
|
||||||
%% Server will close the connection because never receive the heart beat from client
|
%% Server will close the connection because never receive the heart beat from client
|
||||||
|
@ -122,7 +148,7 @@ t_subscribe(_) ->
|
||||||
{ok, Data} = gen_tcp:recv(Sock, 0),
|
{ok, Data} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = _}, _} = parse(Data),
|
body = _}, _, _} = parse(Data),
|
||||||
|
|
||||||
%% Subscribe
|
%% Subscribe
|
||||||
gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>,
|
gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>,
|
||||||
|
@ -139,7 +165,7 @@ t_subscribe(_) ->
|
||||||
{ok, Data1} = gen_tcp:recv(Sock, 0, 1000),
|
{ok, Data1} = gen_tcp:recv(Sock, 0, 1000),
|
||||||
{ok, Frame = #stomp_frame{command = <<"MESSAGE">>,
|
{ok, Frame = #stomp_frame{command = <<"MESSAGE">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = <<"hello">>}, _} = parse(Data1),
|
body = <<"hello">>}, _, _} = parse(Data1),
|
||||||
lists:foreach(fun({Key, Val}) ->
|
lists:foreach(fun({Key, Val}) ->
|
||||||
Val = proplists:get_value(Key, Frame#stomp_frame.headers)
|
Val = proplists:get_value(Key, Frame#stomp_frame.headers)
|
||||||
end, [{<<"destination">>, <<"/queue/foo">>},
|
end, [{<<"destination">>, <<"/queue/foo">>},
|
||||||
|
@ -155,7 +181,7 @@ t_subscribe(_) ->
|
||||||
|
|
||||||
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
||||||
headers = [{<<"receipt-id">>, <<"12345">>}],
|
headers = [{<<"receipt-id">>, <<"12345">>}],
|
||||||
body = _}, _} = parse(Data2),
|
body = _}, _, _} = parse(Data2),
|
||||||
|
|
||||||
gen_tcp:send(Sock, serialize(<<"SEND">>,
|
gen_tcp:send(Sock, serialize(<<"SEND">>,
|
||||||
[{<<"destination">>, <<"/queue/foo">>}],
|
[{<<"destination">>, <<"/queue/foo">>}],
|
||||||
|
@ -175,7 +201,7 @@ t_transaction(_) ->
|
||||||
{ok, Data} = gen_tcp:recv(Sock, 0),
|
{ok, Data} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = _}, _} = parse(Data),
|
body = _}, _, _} = parse(Data),
|
||||||
|
|
||||||
%% Subscribe
|
%% Subscribe
|
||||||
gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>,
|
gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>,
|
||||||
|
@ -208,12 +234,12 @@ t_transaction(_) ->
|
||||||
|
|
||||||
{ok, #stomp_frame{command = <<"MESSAGE">>,
|
{ok, #stomp_frame{command = <<"MESSAGE">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = <<"hello">>}, Rest1} = parse(Data1),
|
body = <<"hello">>}, Rest1, _} = parse(Data1),
|
||||||
|
|
||||||
%{ok, Data2} = gen_tcp:recv(Sock, 0, 500),
|
%{ok, Data2} = gen_tcp:recv(Sock, 0, 500),
|
||||||
{ok, #stomp_frame{command = <<"MESSAGE">>,
|
{ok, #stomp_frame{command = <<"MESSAGE">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = <<"hello again">>}, _Rest2} = parse(Rest1),
|
body = <<"hello again">>}, _Rest2, _} = parse(Rest1),
|
||||||
|
|
||||||
%% Transaction: tx2
|
%% Transaction: tx2
|
||||||
gen_tcp:send(Sock, serialize(<<"BEGIN">>,
|
gen_tcp:send(Sock, serialize(<<"BEGIN">>,
|
||||||
|
@ -236,7 +262,7 @@ t_transaction(_) ->
|
||||||
{ok, Data3} = gen_tcp:recv(Sock, 0),
|
{ok, Data3} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
||||||
headers = [{<<"receipt-id">>, <<"12345">>}],
|
headers = [{<<"receipt-id">>, <<"12345">>}],
|
||||||
body = _}, _} = parse(Data3)
|
body = _}, _, _} = parse(Data3)
|
||||||
end).
|
end).
|
||||||
|
|
||||||
t_receipt_in_error(_) ->
|
t_receipt_in_error(_) ->
|
||||||
|
@ -250,7 +276,7 @@ t_receipt_in_error(_) ->
|
||||||
{ok, Data} = gen_tcp:recv(Sock, 0),
|
{ok, Data} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = _}, _} = parse(Data),
|
body = _}, _, _} = parse(Data),
|
||||||
|
|
||||||
gen_tcp:send(Sock, serialize(<<"ABORT">>,
|
gen_tcp:send(Sock, serialize(<<"ABORT">>,
|
||||||
[{<<"transaction">>, <<"tx1">>},
|
[{<<"transaction">>, <<"tx1">>},
|
||||||
|
@ -259,7 +285,7 @@ t_receipt_in_error(_) ->
|
||||||
{ok, Data1} = gen_tcp:recv(Sock, 0),
|
{ok, Data1} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, Frame = #stomp_frame{command = <<"ERROR">>,
|
{ok, Frame = #stomp_frame{command = <<"ERROR">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = <<"Transaction tx1 not found">>}, _} = parse(Data1),
|
body = <<"Transaction tx1 not found">>}, _, _} = parse(Data1),
|
||||||
|
|
||||||
<<"12345">> = proplists:get_value(<<"receipt-id">>, Frame#stomp_frame.headers)
|
<<"12345">> = proplists:get_value(<<"receipt-id">>, Frame#stomp_frame.headers)
|
||||||
end).
|
end).
|
||||||
|
@ -275,7 +301,7 @@ t_ack(_) ->
|
||||||
{ok, Data} = gen_tcp:recv(Sock, 0),
|
{ok, Data} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
{ok, #stomp_frame{command = <<"CONNECTED">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = _}, _} = parse(Data),
|
body = _}, _, _} = parse(Data),
|
||||||
|
|
||||||
%% Subscribe
|
%% Subscribe
|
||||||
gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>,
|
gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>,
|
||||||
|
@ -290,7 +316,7 @@ t_ack(_) ->
|
||||||
{ok, Data1} = gen_tcp:recv(Sock, 0),
|
{ok, Data1} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, Frame = #stomp_frame{command = <<"MESSAGE">>,
|
{ok, Frame = #stomp_frame{command = <<"MESSAGE">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = <<"ack test">>}, _} = parse(Data1),
|
body = <<"ack test">>}, _, _} = parse(Data1),
|
||||||
|
|
||||||
AckId = proplists:get_value(<<"ack">>, Frame#stomp_frame.headers),
|
AckId = proplists:get_value(<<"ack">>, Frame#stomp_frame.headers),
|
||||||
|
|
||||||
|
@ -301,7 +327,7 @@ t_ack(_) ->
|
||||||
{ok, Data2} = gen_tcp:recv(Sock, 0),
|
{ok, Data2} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
||||||
headers = [{<<"receipt-id">>, <<"12345">>}],
|
headers = [{<<"receipt-id">>, <<"12345">>}],
|
||||||
body = _}, _} = parse(Data2),
|
body = _}, _, _} = parse(Data2),
|
||||||
|
|
||||||
gen_tcp:send(Sock, serialize(<<"SEND">>,
|
gen_tcp:send(Sock, serialize(<<"SEND">>,
|
||||||
[{<<"destination">>, <<"/queue/foo">>}],
|
[{<<"destination">>, <<"/queue/foo">>}],
|
||||||
|
@ -310,7 +336,7 @@ t_ack(_) ->
|
||||||
{ok, Data3} = gen_tcp:recv(Sock, 0),
|
{ok, Data3} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, Frame1 = #stomp_frame{command = <<"MESSAGE">>,
|
{ok, Frame1 = #stomp_frame{command = <<"MESSAGE">>,
|
||||||
headers = _,
|
headers = _,
|
||||||
body = <<"nack test">>}, _} = parse(Data3),
|
body = <<"nack test">>}, _, _} = parse(Data3),
|
||||||
|
|
||||||
AckId1 = proplists:get_value(<<"ack">>, Frame1#stomp_frame.headers),
|
AckId1 = proplists:get_value(<<"ack">>, Frame1#stomp_frame.headers),
|
||||||
|
|
||||||
|
@ -321,9 +347,16 @@ t_ack(_) ->
|
||||||
{ok, Data4} = gen_tcp:recv(Sock, 0),
|
{ok, Data4} = gen_tcp:recv(Sock, 0),
|
||||||
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
{ok, #stomp_frame{command = <<"RECEIPT">>,
|
||||||
headers = [{<<"receipt-id">>, <<"12345">>}],
|
headers = [{<<"receipt-id">>, <<"12345">>}],
|
||||||
body = _}, _} = parse(Data4)
|
body = _}, _, _} = parse(Data4)
|
||||||
end).
|
end).
|
||||||
|
|
||||||
|
%% TODO: Mountpoint, AuthChain, ACL + Mountpoint, ClientInfoOverride,
|
||||||
|
%% Listeners, Metrics, Stats, ClientInfo
|
||||||
|
%%
|
||||||
|
%% TODO: Start/Stop, List Instace
|
||||||
|
%%
|
||||||
|
%% TODO: RateLimit, OOM,
|
||||||
|
|
||||||
with_connection(DoFun) ->
|
with_connection(DoFun) ->
|
||||||
{ok, Sock} = gen_tcp:connect({127, 0, 0, 1},
|
{ok, Sock} = gen_tcp:connect({127, 0, 0, 1},
|
||||||
61613,
|
61613,
|
||||||
|
@ -336,14 +369,15 @@ with_connection(DoFun) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
serialize(Command, Headers) ->
|
serialize(Command, Headers) ->
|
||||||
emqx_stomp_frame:serialize(emqx_stomp_frame:make(Command, Headers)).
|
emqx_stomp_frame:serialize_pkt(emqx_stomp_frame:make(Command, Headers), #{}).
|
||||||
|
|
||||||
serialize(Command, Headers, Body) ->
|
serialize(Command, Headers, Body) ->
|
||||||
emqx_stomp_frame:serialize(emqx_stomp_frame:make(Command, Headers, Body)).
|
emqx_stomp_frame:serialize_pkt(emqx_stomp_frame:make(Command, Headers, Body), #{}).
|
||||||
|
|
||||||
parse(Data) ->
|
parse(Data) ->
|
||||||
ProtoEnv = [{max_headers, 10},
|
ProtoEnv = #{max_headers => 10,
|
||||||
{max_header_length, 1024},
|
max_header_length => 1024,
|
||||||
{max_body_length, 8192}],
|
max_body_length => 8192
|
||||||
Parser = emqx_stomp_frame:init_parer_state(ProtoEnv),
|
},
|
||||||
|
Parser = emqx_stomp_frame:initial_parse_state(ProtoEnv),
|
||||||
emqx_stomp_frame:parse(Data, Parser).
|
emqx_stomp_frame:parse(Data, Parser).
|
|
@ -1,25 +0,0 @@
|
||||||
.eunit
|
|
||||||
deps
|
|
||||||
*.o
|
|
||||||
*.beam
|
|
||||||
*.plt
|
|
||||||
erl_crash.dump
|
|
||||||
ebin
|
|
||||||
rel/example_project
|
|
||||||
.concrete/DEV_MODE
|
|
||||||
.rebar
|
|
||||||
.erlang.mk/
|
|
||||||
emq_stomp.d
|
|
||||||
ct.coverdata
|
|
||||||
logs/
|
|
||||||
test/ct.cover.spec
|
|
||||||
data/
|
|
||||||
.DS_Store
|
|
||||||
emqx_stomp.d
|
|
||||||
_build/
|
|
||||||
rebar.lock
|
|
||||||
erlang.mk
|
|
||||||
rebar3.crashdump
|
|
||||||
etc/emqx_stomp.conf.rendered
|
|
||||||
.rebar3/
|
|
||||||
*.swp
|
|
|
@ -1,124 +0,0 @@
|
||||||
##--------------------------------------------------------------------
|
|
||||||
## Stomp Plugin
|
|
||||||
##--------------------------------------------------------------------
|
|
||||||
|
|
||||||
##--------------------------------------------------------------------
|
|
||||||
## Stomp listener
|
|
||||||
|
|
||||||
## The Port that stomp listener will bind.
|
|
||||||
##
|
|
||||||
## Value: Port
|
|
||||||
stomp.listener.port = 61613
|
|
||||||
|
|
||||||
## The acceptor pool for stomp listener.
|
|
||||||
##
|
|
||||||
## Value: Number
|
|
||||||
stomp.listener.acceptors = 4
|
|
||||||
|
|
||||||
## Maximum number of concurrent stomp connections.
|
|
||||||
##
|
|
||||||
## Value: Number
|
|
||||||
stomp.listener.max_connections = 512
|
|
||||||
|
|
||||||
## Whether to enable SSL.
|
|
||||||
##
|
|
||||||
## Value: on | off
|
|
||||||
## stomp.listener.ssl = off
|
|
||||||
|
|
||||||
## Path to the file containing the user's private PEM-encoded key.
|
|
||||||
##
|
|
||||||
## Value: File
|
|
||||||
## stomp.listener.keyfile = "etc/certs/key.pem"
|
|
||||||
|
|
||||||
## Path to a file containing the user certificate.
|
|
||||||
##
|
|
||||||
## Value: File
|
|
||||||
## stomp.listener.certfile = "etc/certs/cert.pem"
|
|
||||||
|
|
||||||
## Path to the file containing PEM-encoded CA certificates.
|
|
||||||
##
|
|
||||||
## Value: File
|
|
||||||
## stomp.listener.cacertfile = "etc/certs/cacert.pem"
|
|
||||||
|
|
||||||
## See: 'listener.ssl.<name>.dhfile' in emq.conf
|
|
||||||
##
|
|
||||||
## Value: File
|
|
||||||
## stomp.listener.dhfile = "etc/certs/dh-params.pem"
|
|
||||||
|
|
||||||
## See: 'listener.ssl.<name>.verify' in emq.conf
|
|
||||||
##
|
|
||||||
## Value: verify_peer | verify_none
|
|
||||||
## stomp.listener.verify = verify_peer
|
|
||||||
|
|
||||||
## See: 'listener.ssl.<name>.fail_if_no_peer_cert' in emq.conf
|
|
||||||
##
|
|
||||||
## Value: false | true
|
|
||||||
## stomp.listener.fail_if_no_peer_cert = true
|
|
||||||
|
|
||||||
## TLS versions only to protect from POODLE attack.
|
|
||||||
##
|
|
||||||
## Value: String, seperated by ','
|
|
||||||
## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier
|
|
||||||
## stomp.listener.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1"
|
|
||||||
|
|
||||||
## SSL Handshake timeout.
|
|
||||||
##
|
|
||||||
## Value: Duration
|
|
||||||
## stomp.listener.handshake_timeout = 15s
|
|
||||||
|
|
||||||
## See: 'listener.ssl.<name>.ciphers' in emq.conf
|
|
||||||
##
|
|
||||||
## Value: Ciphers
|
|
||||||
## stomp.listener.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA"
|
|
||||||
|
|
||||||
## See: 'listener.ssl.<name>.secure_renegotiate' in emq.conf
|
|
||||||
##
|
|
||||||
## Value: on | off
|
|
||||||
## stomp.listener.secure_renegotiate = off
|
|
||||||
|
|
||||||
## See: 'listener.ssl.<name>.reuse_sessions' in emq.conf
|
|
||||||
##
|
|
||||||
## Value: on | off
|
|
||||||
## stomp.listener.reuse_sessions = on
|
|
||||||
|
|
||||||
## See: 'listener.ssl.<name>.honor_cipher_order' in emq.conf
|
|
||||||
##
|
|
||||||
## Value: on | off
|
|
||||||
## stomp.listener.honor_cipher_order = on
|
|
||||||
|
|
||||||
##--------------------------------------------------------------------
|
|
||||||
## Stomp login user and password
|
|
||||||
|
|
||||||
## Default login user
|
|
||||||
##
|
|
||||||
## Value: String
|
|
||||||
stomp.default_user.login = guest
|
|
||||||
|
|
||||||
## Default login password
|
|
||||||
##
|
|
||||||
## Value: String
|
|
||||||
stomp.default_user.passcode = guest
|
|
||||||
|
|
||||||
## Allow anonymous authentication.
|
|
||||||
##
|
|
||||||
## Value: true | false
|
|
||||||
stomp.allow_anonymous = true
|
|
||||||
|
|
||||||
##--------------------------------------------------------------------
|
|
||||||
## Stomp frame
|
|
||||||
|
|
||||||
## Maximum numbers of frame headers.
|
|
||||||
##
|
|
||||||
## Value: Number
|
|
||||||
stomp.frame.max_headers = 10
|
|
||||||
|
|
||||||
## Maximum length of frame header.
|
|
||||||
##
|
|
||||||
## Value: Number
|
|
||||||
stomp.frame.max_header_length = 1024
|
|
||||||
|
|
||||||
## Maximum body length of frame.
|
|
||||||
##
|
|
||||||
## Value: Number
|
|
||||||
stomp.frame.max_body_length = 8192
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
||||||
%%
|
|
||||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
%% you may not use this file except in compliance with the License.
|
|
||||||
%% You may obtain a copy of the License at
|
|
||||||
%%
|
|
||||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
%%
|
|
||||||
%% Unless required by applicable law or agreed to in writing, software
|
|
||||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
%% See the License for the specific language governing permissions and
|
|
||||||
%% limitations under the License.
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
%% @doc Stomp Frame Header.
|
|
||||||
|
|
||||||
-define(STOMP_VER, <<"1.2">>).
|
|
||||||
|
|
||||||
-define(STOMP_SERVER, <<"emqx-stomp/1.2">>).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% STOMP Frame
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
-record(stomp_frame, {command, headers = [], body = <<>> :: iodata()}).
|
|
||||||
|
|
||||||
-type(stomp_frame() :: #stomp_frame{}).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Frame Size Limits
|
|
||||||
%%
|
|
||||||
%% To prevent malicious clients from exploiting memory allocation in a server,
|
|
||||||
%% servers MAY place maximum limits on:
|
|
||||||
%%
|
|
||||||
%% the number of frame headers allowed in a single frame
|
|
||||||
%% the maximum length of header lines
|
|
||||||
%% the maximum size of a frame body
|
|
||||||
%%
|
|
||||||
%% If these limits are exceeded the server SHOULD send the client an ERROR frame
|
|
||||||
%% and then close the connection.
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
-define(MAX_HEADER_NUM, 10).
|
|
||||||
-define(MAX_HEADER_LENGTH, 1024).
|
|
||||||
-define(MAX_BODY_LENGTH, 65536).
|
|
||||||
|
|
|
@ -1,149 +0,0 @@
|
||||||
%%-*- mode: erlang -*-
|
|
||||||
%% emqx_stomp config mapping
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.port", "emqx_stomp.listener", [
|
|
||||||
{default, 61613},
|
|
||||||
{datatype, [integer, ip]}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.acceptors", "emqx_stomp.listener", [
|
|
||||||
{default, 4},
|
|
||||||
{datatype, integer}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.max_connections", "emqx_stomp.listener", [
|
|
||||||
{default, 512},
|
|
||||||
{datatype, integer}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.ssl", "emqx_stomp.listener", [
|
|
||||||
{datatype, flag},
|
|
||||||
{default, off}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.tls_versions", "emqx_stomp.listener", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.handshake_timeout", "emqx_stomp.listener", [
|
|
||||||
{default, "15s"},
|
|
||||||
{datatype, {duration, ms}}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.dhfile", "emqx_stomp.listener", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.keyfile", "emqx_stomp.listener", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.certfile", "emqx_stomp.listener", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.cacertfile", "emqx_stomp.listener", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.verify", "emqx_stomp.listener", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.fail_if_no_peer_cert", "emqx_stomp.listener", [
|
|
||||||
{datatype, {enum, [true, false]}}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.ciphers", "emqx_stomp.listener", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.secure_renegotiate", "emqx_stomp.listener", [
|
|
||||||
{datatype, flag}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.reuse_sessions", "emqx_stomp.listener", [
|
|
||||||
{default, on},
|
|
||||||
{datatype, flag}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.listener.honor_cipher_order", "emqx_stomp.listener", [
|
|
||||||
{datatype, flag}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{translation, "emqx_stomp.listener", fun(Conf) ->
|
|
||||||
Port = cuttlefish:conf_get("stomp.listener.port", Conf),
|
|
||||||
Acceptors = cuttlefish:conf_get("stomp.listener.acceptors", Conf),
|
|
||||||
MaxConnections = cuttlefish:conf_get("stomp.listener.max_connections", Conf),
|
|
||||||
Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
|
|
||||||
SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end,
|
|
||||||
SslOpts = fun(Prefix) ->
|
|
||||||
Versions = case SplitFun(cuttlefish:conf_get(Prefix ++ ".tls_versions", Conf, undefined)) of
|
|
||||||
undefined -> undefined;
|
|
||||||
L -> [list_to_atom(V) || V <- L]
|
|
||||||
end,
|
|
||||||
Filter([{versions, Versions},
|
|
||||||
{ciphers, SplitFun(cuttlefish:conf_get(Prefix ++ ".ciphers", Conf, undefined))},
|
|
||||||
{handshake_timeout, cuttlefish:conf_get(Prefix ++ ".handshake_timeout", Conf, undefined)},
|
|
||||||
{dhfile, cuttlefish:conf_get(Prefix ++ ".dhfile", Conf, undefined)},
|
|
||||||
{keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)},
|
|
||||||
{certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)},
|
|
||||||
{cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)},
|
|
||||||
{verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)},
|
|
||||||
{fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)},
|
|
||||||
{secure_renegotiate, cuttlefish:conf_get(Prefix ++ ".secure_renegotiate", Conf, undefined)},
|
|
||||||
{reuse_sessions, cuttlefish:conf_get(Prefix ++ ".reuse_sessions", Conf, undefined)},
|
|
||||||
{honor_cipher_order, cuttlefish:conf_get(Prefix ++ ".honor_cipher_order", Conf, undefined)}])
|
|
||||||
end,
|
|
||||||
Opts = [{acceptors, Acceptors}, {max_connections, MaxConnections}],
|
|
||||||
{Port, case cuttlefish:conf_get("stomp.listener.ssl", Conf) of
|
|
||||||
true ->
|
|
||||||
[{sslopts, SslOpts("stomp.listener")} | Opts];
|
|
||||||
false ->
|
|
||||||
Opts
|
|
||||||
end}
|
|
||||||
end}.
|
|
||||||
|
|
||||||
{mapping, "stomp.default_user.login", "emqx_stomp.default_user", [
|
|
||||||
{default, "guest"},
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.default_user.passcode", "emqx_stomp.default_user", [
|
|
||||||
{default, "guest"},
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{translation, "emqx_stomp.default_user", fun(Conf) ->
|
|
||||||
Login = cuttlefish:conf_get("stomp.default_user.login", Conf),
|
|
||||||
Passcode = cuttlefish:conf_get("stomp.default_user.passcode", Conf),
|
|
||||||
[{login, Login}, {passcode, Passcode}]
|
|
||||||
end}.
|
|
||||||
|
|
||||||
{mapping, "stomp.allow_anonymous", "emqx_stomp.allow_anonymous", [
|
|
||||||
{default, true},
|
|
||||||
{datatype, {enum, [true, false]}}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.frame.max_headers", "emqx_stomp.frame", [
|
|
||||||
{default, 10},
|
|
||||||
{datatype, integer}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.frame.max_header_length", "emqx_stomp.frame", [
|
|
||||||
{default, 1024},
|
|
||||||
{datatype, integer}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "stomp.frame.max_body_length", "emqx_stomp.frame", [
|
|
||||||
{default, 8192},
|
|
||||||
{datatype, integer}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{translation, "emqx_stomp.frame", fun(Conf) ->
|
|
||||||
MaxHeaders = cuttlefish:conf_get("stomp.frame.max_headers", Conf),
|
|
||||||
MaxHeaderLength = cuttlefish:conf_get("stomp.frame.max_header_length", Conf),
|
|
||||||
MaxBodyLength = cuttlefish:conf_get("stomp.frame.max_body_length", Conf),
|
|
||||||
[{max_headers, MaxHeaders}, {max_header_length, MaxHeaderLength}, {max_body_length, MaxBodyLength}]
|
|
||||||
end}.
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
{deps, []}.
|
|
||||||
|
|
||||||
{edoc_opts, [{preprocess, true}]}.
|
|
||||||
{erl_opts, [warn_unused_vars,
|
|
||||||
warn_shadow_vars,
|
|
||||||
warn_unused_import,
|
|
||||||
warn_obsolete_guard,
|
|
||||||
debug_info,
|
|
||||||
{parse_transform}]}.
|
|
||||||
|
|
||||||
{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}.
|
|
|
@ -1,14 +0,0 @@
|
||||||
{application, emqx_stomp,
|
|
||||||
[{description, "EMQ X Stomp Protocol Plugin"},
|
|
||||||
{vsn, "4.4.0"}, % strict semver, bump manually!
|
|
||||||
{modules, []},
|
|
||||||
{registered, [emqx_stomp_sup]},
|
|
||||||
{applications, [kernel,stdlib]},
|
|
||||||
{mod, {emqx_stomp,[]}},
|
|
||||||
{env, []},
|
|
||||||
{licenses, ["Apache-2.0"]},
|
|
||||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
|
||||||
{links, [{"Homepage", "https://emqx.io/"},
|
|
||||||
{"Github", "https://github.com/emqx/emqx-stomp"}
|
|
||||||
]}
|
|
||||||
]}.
|
|
|
@ -1,142 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020-2021 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_stomp).
|
|
||||||
|
|
||||||
-behaviour(application).
|
|
||||||
-behaviour(supervisor).
|
|
||||||
|
|
||||||
-emqx_plugin(protocol).
|
|
||||||
|
|
||||||
-export([ start/2
|
|
||||||
, stop/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([ start_listeners/0
|
|
||||||
, start_listener/1
|
|
||||||
, start_listener/3
|
|
||||||
, stop_listeners/0
|
|
||||||
, stop_listener/1
|
|
||||||
, stop_listener/3
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([init/1]).
|
|
||||||
|
|
||||||
-define(APP, ?MODULE).
|
|
||||||
-define(TCP_OPTS, [binary, {packet, raw}, {reuseaddr, true}, {nodelay, true}]).
|
|
||||||
|
|
||||||
-type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Application callbacks
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
|
||||||
{ok, Sup} = supervisor:start_link({local, emqx_stomp_sup}, ?MODULE, []),
|
|
||||||
start_listeners(),
|
|
||||||
{ok, Sup}.
|
|
||||||
|
|
||||||
stop(_State) ->
|
|
||||||
stop_listeners().
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Supervisor callbacks
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
init([]) ->
|
|
||||||
{ok, {{one_for_all, 10, 100}, []}}.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Start/Stop listeners
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
-spec(start_listeners() -> ok).
|
|
||||||
start_listeners() ->
|
|
||||||
lists:foreach(fun start_listener/1, listeners_confs()).
|
|
||||||
|
|
||||||
-spec(start_listener(listener()) -> ok).
|
|
||||||
start_listener({Proto, ListenOn, Options}) ->
|
|
||||||
case start_listener(Proto, ListenOn, Options) of
|
|
||||||
{ok, _} -> io:format("Start stomp:~s listener on ~s successfully.~n",
|
|
||||||
[Proto, format(ListenOn)]);
|
|
||||||
{error, Reason} ->
|
|
||||||
io:format(standard_error, "Failed to start stomp:~s listener on ~s: ~0p~n",
|
|
||||||
[Proto, format(ListenOn), Reason]),
|
|
||||||
error(Reason)
|
|
||||||
end.
|
|
||||||
|
|
||||||
-spec(start_listener(esockd:proto(), esockd:listen_on(), [esockd:option()])
|
|
||||||
-> {ok, pid()} | {error, term()}).
|
|
||||||
start_listener(tcp, ListenOn, Options) ->
|
|
||||||
start_stomp_listener('stomp:tcp', ListenOn, Options);
|
|
||||||
start_listener(ssl, ListenOn, Options) ->
|
|
||||||
start_stomp_listener('stomp:ssl', ListenOn, Options).
|
|
||||||
|
|
||||||
%% @private
|
|
||||||
start_stomp_listener(Name, ListenOn, Options) ->
|
|
||||||
SockOpts = esockd:parse_opt(Options),
|
|
||||||
esockd:open(Name, ListenOn, merge_default(SockOpts),
|
|
||||||
{emqx_stomp_connection, start_link, [Options -- SockOpts]}).
|
|
||||||
|
|
||||||
-spec(stop_listeners() -> ok).
|
|
||||||
stop_listeners() ->
|
|
||||||
lists:foreach(fun stop_listener/1, listeners_confs()).
|
|
||||||
|
|
||||||
-spec(stop_listener(listener()) -> ok | {error, term()}).
|
|
||||||
stop_listener({Proto, ListenOn, Opts}) ->
|
|
||||||
StopRet = stop_listener(Proto, ListenOn, Opts),
|
|
||||||
case StopRet of
|
|
||||||
ok -> io:format("Stop stomp:~s listener on ~s successfully.~n",
|
|
||||||
[Proto, format(ListenOn)]);
|
|
||||||
{error, Reason} ->
|
|
||||||
io:format(standard_error, "Failed to stop stomp:~s listener on ~s: ~0p~n",
|
|
||||||
[Proto, format(ListenOn), Reason])
|
|
||||||
end,
|
|
||||||
StopRet.
|
|
||||||
|
|
||||||
-spec(stop_listener(esockd:proto(), esockd:listen_on(), [esockd:option()])
|
|
||||||
-> ok | {error, term()}).
|
|
||||||
stop_listener(tcp, ListenOn, _Opts) ->
|
|
||||||
esockd:close('stomp:tcp', ListenOn);
|
|
||||||
stop_listener(ssl, ListenOn, _Opts) ->
|
|
||||||
esockd:close('stomp:ssl', ListenOn).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Internal funcs
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
listeners_confs() ->
|
|
||||||
{ok, {Port, Opts}} = application:get_env(?APP, listener),
|
|
||||||
Options = application:get_env(?APP, frame, []),
|
|
||||||
Anonymous = application:get_env(emqx_stomp, allow_anonymous, false),
|
|
||||||
{ok, DefaultUser} = application:get_env(emqx_stomp, default_user),
|
|
||||||
[{tcp, Port, [{allow_anonymous, Anonymous},
|
|
||||||
{default_user, DefaultUser} | Options ++ Opts]}].
|
|
||||||
|
|
||||||
merge_default(Options) ->
|
|
||||||
case lists:keytake(tcp_options, 1, Options) of
|
|
||||||
{value, {tcp_options, TcpOpts}, Options1} ->
|
|
||||||
[{tcp_options, emqx_misc:merge_opts(?TCP_OPTS, TcpOpts)} | Options1];
|
|
||||||
false ->
|
|
||||||
[{tcp_options, ?TCP_OPTS} | Options]
|
|
||||||
end.
|
|
||||||
|
|
||||||
format(Port) when is_integer(Port) ->
|
|
||||||
io_lib:format("0.0.0.0:~w", [Port]);
|
|
||||||
format({Addr, Port}) when is_list(Addr) ->
|
|
||||||
io_lib:format("~s:~w", [Addr, Port]);
|
|
||||||
format({Addr, Port}) when is_tuple(Addr) ->
|
|
||||||
io_lib:format("~s:~w", [inet:ntoa(Addr), Port]).
|
|
|
@ -1,274 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020-2021 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_stomp_connection).
|
|
||||||
|
|
||||||
-behaviour(gen_server).
|
|
||||||
|
|
||||||
-include("emqx_stomp.hrl").
|
|
||||||
-include_lib("emqx/include/logger.hrl").
|
|
||||||
|
|
||||||
-logger_header("[Stomp-Conn]").
|
|
||||||
|
|
||||||
-export([ start_link/3
|
|
||||||
, info/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
%% gen_server Function Exports
|
|
||||||
-export([ init/1
|
|
||||||
, handle_call/3
|
|
||||||
, handle_cast/2
|
|
||||||
, handle_info/2
|
|
||||||
, code_change/3
|
|
||||||
, terminate/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
%% for protocol
|
|
||||||
-export([send/4, heartbeat/2]).
|
|
||||||
|
|
||||||
-record(state, {transport, socket, peername, conn_name, conn_state,
|
|
||||||
await_recv, rate_limit, parser, pstate,
|
|
||||||
proto_env, heartbeat}).
|
|
||||||
|
|
||||||
-define(INFO_KEYS, [peername, await_recv, conn_state]).
|
|
||||||
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]).
|
|
||||||
|
|
||||||
start_link(Transport, Sock, ProtoEnv) ->
|
|
||||||
{ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, ProtoEnv]])}.
|
|
||||||
|
|
||||||
info(CPid) ->
|
|
||||||
gen_server:call(CPid, info, infinity).
|
|
||||||
|
|
||||||
init([Transport, Sock, ProtoEnv]) ->
|
|
||||||
process_flag(trap_exit, true),
|
|
||||||
case Transport:wait(Sock) of
|
|
||||||
{ok, NewSock} ->
|
|
||||||
{ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]),
|
|
||||||
ConnName = esockd:format(Peername),
|
|
||||||
SendFun = {fun ?MODULE:send/4, [Transport, Sock, self()]},
|
|
||||||
HrtBtFun = {fun ?MODULE:heartbeat/2, [Transport, Sock]},
|
|
||||||
Parser = emqx_stomp_frame:init_parer_state(ProtoEnv),
|
|
||||||
PState = emqx_stomp_protocol:init(#{peername => Peername,
|
|
||||||
sendfun => SendFun,
|
|
||||||
heartfun => HrtBtFun}, ProtoEnv),
|
|
||||||
RateLimit = init_rate_limit(proplists:get_value(rate_limit, ProtoEnv)),
|
|
||||||
State = run_socket(#state{transport = Transport,
|
|
||||||
socket = NewSock,
|
|
||||||
peername = Peername,
|
|
||||||
conn_name = ConnName,
|
|
||||||
conn_state = running,
|
|
||||||
await_recv = false,
|
|
||||||
rate_limit = RateLimit,
|
|
||||||
parser = Parser,
|
|
||||||
proto_env = ProtoEnv,
|
|
||||||
pstate = PState}),
|
|
||||||
emqx_logger:set_metadata_peername(esockd:format(Peername)),
|
|
||||||
gen_server:enter_loop(?MODULE, [{hibernate_after, 5000}], State, 20000);
|
|
||||||
{error, Reason} ->
|
|
||||||
{stop, Reason}
|
|
||||||
end.
|
|
||||||
|
|
||||||
init_rate_limit(undefined) ->
|
|
||||||
undefined;
|
|
||||||
init_rate_limit({Rate, Burst}) ->
|
|
||||||
esockd_rate_limit:new(Rate, Burst).
|
|
||||||
|
|
||||||
send(Data, Transport, Sock, ConnPid) ->
|
|
||||||
try Transport:async_send(Sock, Data) of
|
|
||||||
ok -> ok;
|
|
||||||
{error, Reason} -> ConnPid ! {shutdown, Reason}
|
|
||||||
catch
|
|
||||||
error:Error -> ConnPid ! {shutdown, Error}
|
|
||||||
end.
|
|
||||||
|
|
||||||
heartbeat(Transport, Sock) ->
|
|
||||||
Transport:send(Sock, <<$\n>>).
|
|
||||||
|
|
||||||
handle_call(info, _From, State = #state{transport = Transport,
|
|
||||||
socket = Sock,
|
|
||||||
peername = Peername,
|
|
||||||
await_recv = AwaitRecv,
|
|
||||||
conn_state = ConnState,
|
|
||||||
pstate = PState}) ->
|
|
||||||
ClientInfo = [{peername, Peername}, {await_recv, AwaitRecv},
|
|
||||||
{conn_state, ConnState}],
|
|
||||||
ProtoInfo = emqx_stomp_protocol:info(PState),
|
|
||||||
case Transport:getstat(Sock, ?SOCK_STATS) of
|
|
||||||
{ok, SockStats} ->
|
|
||||||
{reply, lists:append([ClientInfo, ProtoInfo, SockStats]), State};
|
|
||||||
{error, Reason} ->
|
|
||||||
{stop, Reason, lists:append([ClientInfo, ProtoInfo]), State}
|
|
||||||
end;
|
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
|
||||||
?LOG(error, "unexpected request: ~p", [Req]),
|
|
||||||
{reply, ignored, State}.
|
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
|
||||||
?LOG(error, "unexpected msg: ~p", [Msg]),
|
|
||||||
noreply(State).
|
|
||||||
|
|
||||||
handle_info(timeout, State) ->
|
|
||||||
shutdown(idle_timeout, State);
|
|
||||||
|
|
||||||
handle_info({shutdown, Reason}, State) ->
|
|
||||||
shutdown(Reason, State);
|
|
||||||
|
|
||||||
handle_info({timeout, TRef, TMsg}, State) when TMsg =:= incoming;
|
|
||||||
TMsg =:= outgoing ->
|
|
||||||
|
|
||||||
Stat = case TMsg of
|
|
||||||
incoming -> recv_oct;
|
|
||||||
_ -> send_oct
|
|
||||||
end,
|
|
||||||
case getstat(Stat, State) of
|
|
||||||
{ok, Val} ->
|
|
||||||
with_proto(timeout, [TRef, {TMsg, Val}], State);
|
|
||||||
{error, Reason} ->
|
|
||||||
shutdown({sock_error, Reason}, State)
|
|
||||||
end;
|
|
||||||
|
|
||||||
handle_info({timeout, TRef, TMsg}, State) ->
|
|
||||||
with_proto(timeout, [TRef, TMsg], State);
|
|
||||||
|
|
||||||
handle_info({'EXIT', HbProc, Error}, State = #state{heartbeat = HbProc}) ->
|
|
||||||
stop(Error, State);
|
|
||||||
|
|
||||||
handle_info(activate_sock, State) ->
|
|
||||||
noreply(run_socket(State#state{conn_state = running}));
|
|
||||||
|
|
||||||
handle_info({inet_async, _Sock, _Ref, {ok, Bytes}}, State) ->
|
|
||||||
?LOG(debug, "RECV ~p", [Bytes]),
|
|
||||||
received(Bytes, rate_limit(size(Bytes), State#state{await_recv = false}));
|
|
||||||
|
|
||||||
handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) ->
|
|
||||||
shutdown(Reason, State);
|
|
||||||
|
|
||||||
handle_info({inet_reply, _Ref, ok}, State) ->
|
|
||||||
noreply(State);
|
|
||||||
|
|
||||||
handle_info({inet_reply, _Sock, {error, Reason}}, State) ->
|
|
||||||
shutdown(Reason, State);
|
|
||||||
|
|
||||||
handle_info({deliver, _Topic, Msg}, State = #state{pstate = PState}) ->
|
|
||||||
noreply(State#state{pstate = case emqx_stomp_protocol:send(Msg, PState) of
|
|
||||||
{ok, PState1} ->
|
|
||||||
PState1;
|
|
||||||
{error, dropped, PState1} ->
|
|
||||||
PState1
|
|
||||||
end});
|
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
|
||||||
noreply(State).
|
|
||||||
|
|
||||||
terminate(Reason, #state{transport = Transport,
|
|
||||||
socket = Sock,
|
|
||||||
pstate = PState}) ->
|
|
||||||
?LOG(info, "terminated for ~p", [Reason]),
|
|
||||||
Transport:fast_close(Sock),
|
|
||||||
case {PState, Reason} of
|
|
||||||
{undefined, _} -> ok;
|
|
||||||
{_, {shutdown, Error}} ->
|
|
||||||
emqx_stomp_protocol:shutdown(Error, PState);
|
|
||||||
{_, Reason} ->
|
|
||||||
emqx_stomp_protocol:shutdown(Reason, PState)
|
|
||||||
end.
|
|
||||||
|
|
||||||
code_change(_OldVsn, State, _Extra) ->
|
|
||||||
{ok, State}.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Receive and Parse data
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
with_proto(Fun, Args, State = #state{pstate = PState}) ->
|
|
||||||
case erlang:apply(emqx_stomp_protocol, Fun, Args ++ [PState]) of
|
|
||||||
{ok, NPState} ->
|
|
||||||
noreply(State#state{pstate = NPState});
|
|
||||||
{F, Reason, NPState} when F == stop;
|
|
||||||
F == error;
|
|
||||||
F == shutdown ->
|
|
||||||
shutdown(Reason, State#state{pstate = NPState})
|
|
||||||
end.
|
|
||||||
|
|
||||||
received(<<>>, State) ->
|
|
||||||
noreply(State);
|
|
||||||
|
|
||||||
received(Bytes, State = #state{parser = Parser,
|
|
||||||
pstate = PState}) ->
|
|
||||||
try emqx_stomp_frame:parse(Bytes, Parser) of
|
|
||||||
{more, NewParser} ->
|
|
||||||
noreply(State#state{parser = NewParser});
|
|
||||||
{ok, Frame, Rest} ->
|
|
||||||
?LOG(info, "RECV Frame: ~s", [emqx_stomp_frame:format(Frame)]),
|
|
||||||
case emqx_stomp_protocol:received(Frame, PState) of
|
|
||||||
{ok, PState1} ->
|
|
||||||
received(Rest, reset_parser(State#state{pstate = PState1}));
|
|
||||||
{error, Error, PState1} ->
|
|
||||||
shutdown(Error, State#state{pstate = PState1});
|
|
||||||
{stop, Reason, PState1} ->
|
|
||||||
stop(Reason, State#state{pstate = PState1})
|
|
||||||
end;
|
|
||||||
{error, Error} ->
|
|
||||||
?LOG(error, "Framing error - ~s", [Error]),
|
|
||||||
?LOG(error, "Bytes: ~p", [Bytes]),
|
|
||||||
shutdown(frame_error, State)
|
|
||||||
catch
|
|
||||||
_Error:Reason ->
|
|
||||||
?LOG(error, "Parser failed for ~p", [Reason]),
|
|
||||||
?LOG(error, "Error data: ~p", [Bytes]),
|
|
||||||
shutdown(parse_error, State)
|
|
||||||
end.
|
|
||||||
|
|
||||||
reset_parser(State = #state{proto_env = ProtoEnv}) ->
|
|
||||||
State#state{parser = emqx_stomp_frame:init_parer_state(ProtoEnv)}.
|
|
||||||
|
|
||||||
rate_limit(_Size, State = #state{rate_limit = undefined}) ->
|
|
||||||
run_socket(State);
|
|
||||||
rate_limit(Size, State = #state{rate_limit = Rl}) ->
|
|
||||||
case esockd_rate_limit:check(Size, Rl) of
|
|
||||||
{0, Rl1} ->
|
|
||||||
run_socket(State#state{conn_state = running, rate_limit = Rl1});
|
|
||||||
{Pause, Rl1} ->
|
|
||||||
?LOG(error, "Rate limiter pause for ~p", [Pause]),
|
|
||||||
erlang:send_after(Pause, self(), activate_sock),
|
|
||||||
State#state{conn_state = blocked, rate_limit = Rl1}
|
|
||||||
end.
|
|
||||||
|
|
||||||
run_socket(State = #state{conn_state = blocked}) ->
|
|
||||||
State;
|
|
||||||
run_socket(State = #state{await_recv = true}) ->
|
|
||||||
State;
|
|
||||||
run_socket(State = #state{transport = Transport, socket = Sock}) ->
|
|
||||||
Transport:async_recv(Sock, 0, infinity),
|
|
||||||
State#state{await_recv = true}.
|
|
||||||
|
|
||||||
getstat(Stat, #state{transport = Transport, socket = Sock}) ->
|
|
||||||
case Transport:getstat(Sock, [Stat]) of
|
|
||||||
{ok, [{Stat, Val}]} -> {ok, Val};
|
|
||||||
{error, Error} -> {error, Error}
|
|
||||||
end.
|
|
||||||
|
|
||||||
noreply(State) ->
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
stop(Reason, State) ->
|
|
||||||
{stop, Reason, State}.
|
|
||||||
|
|
||||||
shutdown(Reason, State) ->
|
|
||||||
stop({shutdown, Reason}, State).
|
|
||||||
|
|
|
@ -1,468 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
||||||
%%
|
|
||||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
%% you may not use this file except in compliance with the License.
|
|
||||||
%% You may obtain a copy of the License at
|
|
||||||
%%
|
|
||||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
%%
|
|
||||||
%% Unless required by applicable law or agreed to in writing, software
|
|
||||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
%% See the License for the specific language governing permissions and
|
|
||||||
%% limitations under the License.
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
%% @doc Stomp Protocol Processor.
|
|
||||||
-module(emqx_stomp_protocol).
|
|
||||||
|
|
||||||
-include("emqx_stomp.hrl").
|
|
||||||
|
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
|
||||||
-include_lib("emqx/include/logger.hrl").
|
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
|
||||||
|
|
||||||
-logger_header("[Stomp-Proto]").
|
|
||||||
|
|
||||||
-import(proplists, [get_value/2, get_value/3]).
|
|
||||||
|
|
||||||
%% API
|
|
||||||
-export([ init/2
|
|
||||||
, info/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([ received/2
|
|
||||||
, send/2
|
|
||||||
, shutdown/2
|
|
||||||
, timeout/3
|
|
||||||
]).
|
|
||||||
|
|
||||||
%% for trans callback
|
|
||||||
-export([ handle_recv_send_frame/2
|
|
||||||
, handle_recv_ack_frame/2
|
|
||||||
, handle_recv_nack_frame/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
-record(pstate, {
|
|
||||||
peername,
|
|
||||||
heartfun,
|
|
||||||
sendfun,
|
|
||||||
connected = false,
|
|
||||||
proto_ver,
|
|
||||||
proto_name,
|
|
||||||
heart_beats,
|
|
||||||
login,
|
|
||||||
allow_anonymous,
|
|
||||||
default_user,
|
|
||||||
subscriptions = [],
|
|
||||||
timers :: #{atom() => disable | undefined | reference()},
|
|
||||||
transaction :: #{binary() => list()}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-define(TIMER_TABLE, #{
|
|
||||||
incoming_timer => incoming,
|
|
||||||
outgoing_timer => outgoing,
|
|
||||||
clean_trans_timer => clean_trans
|
|
||||||
}).
|
|
||||||
|
|
||||||
-define(TRANS_TIMEOUT, 60000).
|
|
||||||
|
|
||||||
-type(pstate() :: #pstate{}).
|
|
||||||
|
|
||||||
%% @doc Init protocol
|
|
||||||
init(#{peername := Peername,
|
|
||||||
sendfun := SendFun,
|
|
||||||
heartfun := HeartFun}, Env) ->
|
|
||||||
AllowAnonymous = get_value(allow_anonymous, Env, false),
|
|
||||||
DefaultUser = get_value(default_user, Env),
|
|
||||||
#pstate{peername = Peername,
|
|
||||||
heartfun = HeartFun,
|
|
||||||
sendfun = SendFun,
|
|
||||||
timers = #{},
|
|
||||||
transaction = #{},
|
|
||||||
allow_anonymous = AllowAnonymous,
|
|
||||||
default_user = DefaultUser}.
|
|
||||||
|
|
||||||
info(#pstate{connected = Connected,
|
|
||||||
proto_ver = ProtoVer,
|
|
||||||
proto_name = ProtoName,
|
|
||||||
heart_beats = Heartbeats,
|
|
||||||
login = Login,
|
|
||||||
subscriptions = Subscriptions}) ->
|
|
||||||
[{connected, Connected},
|
|
||||||
{proto_ver, ProtoVer},
|
|
||||||
{proto_name, ProtoName},
|
|
||||||
{heart_beats, Heartbeats},
|
|
||||||
{login, Login},
|
|
||||||
{subscriptions, Subscriptions}].
|
|
||||||
|
|
||||||
-spec(received(stomp_frame(), pstate())
|
|
||||||
-> {ok, pstate()}
|
|
||||||
| {error, any(), pstate()}
|
|
||||||
| {stop, any(), pstate()}).
|
|
||||||
received(Frame = #stomp_frame{command = <<"STOMP">>}, State) ->
|
|
||||||
received(Frame#stomp_frame{command = <<"CONNECT">>}, State);
|
|
||||||
|
|
||||||
received(#stomp_frame{command = <<"CONNECT">>, headers = Headers},
|
|
||||||
State = #pstate{connected = false, allow_anonymous = AllowAnonymous, default_user = DefaultUser}) ->
|
|
||||||
case negotiate_version(header(<<"accept-version">>, Headers)) of
|
|
||||||
{ok, Version} ->
|
|
||||||
Login = header(<<"login">>, Headers),
|
|
||||||
Passc = header(<<"passcode">>, Headers),
|
|
||||||
case check_login(Login, Passc, AllowAnonymous, DefaultUser) of
|
|
||||||
true ->
|
|
||||||
emqx_logger:set_metadata_clientid(Login),
|
|
||||||
|
|
||||||
Heartbeats = parse_heartbeats(header(<<"heart-beat">>, Headers, <<"0,0">>)),
|
|
||||||
NState = start_heartbeart_timer(Heartbeats, State#pstate{connected = true,
|
|
||||||
proto_ver = Version, login = Login}),
|
|
||||||
send(connected_frame([{<<"version">>, Version},
|
|
||||||
{<<"heart-beat">>, reverse_heartbeats(Heartbeats)}]), NState);
|
|
||||||
false ->
|
|
||||||
_ = send(error_frame(undefined, <<"Login or passcode error!">>), State),
|
|
||||||
{error, login_or_passcode_error, State}
|
|
||||||
end;
|
|
||||||
{error, Msg} ->
|
|
||||||
_ = send(error_frame([{<<"version">>, <<"1.0,1.1,1.2">>},
|
|
||||||
{<<"content-type">>, <<"text/plain">>}], undefined, Msg), State),
|
|
||||||
{error, unsupported_version, State}
|
|
||||||
end;
|
|
||||||
|
|
||||||
received(#stomp_frame{command = <<"CONNECT">>}, State = #pstate{connected = true}) ->
|
|
||||||
{error, unexpected_connect, State};
|
|
||||||
|
|
||||||
received(Frame = #stomp_frame{command = <<"SEND">>, headers = Headers}, State) ->
|
|
||||||
case header(<<"transaction">>, Headers) of
|
|
||||||
undefined -> {ok, handle_recv_send_frame(Frame, State)};
|
|
||||||
TransactionId -> add_action(TransactionId, {fun ?MODULE:handle_recv_send_frame/2, [Frame]}, receipt_id(Headers), State)
|
|
||||||
end;
|
|
||||||
|
|
||||||
received(#stomp_frame{command = <<"SUBSCRIBE">>, headers = Headers},
|
|
||||||
State = #pstate{subscriptions = Subscriptions}) ->
|
|
||||||
Id = header(<<"id">>, Headers),
|
|
||||||
Topic = header(<<"destination">>, Headers),
|
|
||||||
Ack = header(<<"ack">>, Headers, <<"auto">>),
|
|
||||||
{ok, State1} = case lists:keyfind(Id, 1, Subscriptions) of
|
|
||||||
{Id, Topic, Ack} ->
|
|
||||||
{ok, State};
|
|
||||||
false ->
|
|
||||||
emqx_broker:subscribe(Topic),
|
|
||||||
{ok, State#pstate{subscriptions = [{Id, Topic, Ack}|Subscriptions]}}
|
|
||||||
end,
|
|
||||||
maybe_send_receipt(receipt_id(Headers), State1);
|
|
||||||
|
|
||||||
received(#stomp_frame{command = <<"UNSUBSCRIBE">>, headers = Headers},
|
|
||||||
State = #pstate{subscriptions = Subscriptions}) ->
|
|
||||||
Id = header(<<"id">>, Headers),
|
|
||||||
|
|
||||||
{ok, State1} = case lists:keyfind(Id, 1, Subscriptions) of
|
|
||||||
{Id, Topic, _Ack} ->
|
|
||||||
ok = emqx_broker:unsubscribe(Topic),
|
|
||||||
{ok, State#pstate{subscriptions = lists:keydelete(Id, 1, Subscriptions)}};
|
|
||||||
false ->
|
|
||||||
{ok, State}
|
|
||||||
end,
|
|
||||||
maybe_send_receipt(receipt_id(Headers), State1);
|
|
||||||
|
|
||||||
%% ACK
|
|
||||||
%% id:12345
|
|
||||||
%% transaction:tx1
|
|
||||||
%%
|
|
||||||
%% ^@
|
|
||||||
received(Frame = #stomp_frame{command = <<"ACK">>, headers = Headers}, State) ->
|
|
||||||
case header(<<"transaction">>, Headers) of
|
|
||||||
undefined -> {ok, handle_recv_ack_frame(Frame, State)};
|
|
||||||
TransactionId -> add_action(TransactionId, {fun ?MODULE:handle_recv_ack_frame/2, [Frame]}, receipt_id(Headers), State)
|
|
||||||
end;
|
|
||||||
|
|
||||||
%% NACK
|
|
||||||
%% id:12345
|
|
||||||
%% transaction:tx1
|
|
||||||
%%
|
|
||||||
%% ^@
|
|
||||||
received(Frame = #stomp_frame{command = <<"NACK">>, headers = Headers}, State) ->
|
|
||||||
case header(<<"transaction">>, Headers) of
|
|
||||||
undefined -> {ok, handle_recv_nack_frame(Frame, State)};
|
|
||||||
TransactionId -> add_action(TransactionId, {fun ?MODULE:handle_recv_nack_frame/2, [Frame]}, receipt_id(Headers), State)
|
|
||||||
end;
|
|
||||||
|
|
||||||
%% BEGIN
|
|
||||||
%% transaction:tx1
|
|
||||||
%%
|
|
||||||
%% ^@
|
|
||||||
received(#stomp_frame{command = <<"BEGIN">>, headers = Headers},
|
|
||||||
State = #pstate{transaction = Trans}) ->
|
|
||||||
Id = header(<<"transaction">>, Headers),
|
|
||||||
case maps:get(Id, Trans, undefined) of
|
|
||||||
undefined ->
|
|
||||||
Ts = erlang:system_time(millisecond),
|
|
||||||
NState = ensure_clean_trans_timer(State#pstate{transaction = Trans#{Id => {Ts, []}}}),
|
|
||||||
maybe_send_receipt(receipt_id(Headers), NState);
|
|
||||||
_ ->
|
|
||||||
send(error_frame(receipt_id(Headers), ["Transaction ", Id, " already started"]), State)
|
|
||||||
end;
|
|
||||||
|
|
||||||
%% COMMIT
|
|
||||||
%% transaction:tx1
|
|
||||||
%%
|
|
||||||
%% ^@
|
|
||||||
received(#stomp_frame{command = <<"COMMIT">>, headers = Headers},
|
|
||||||
State = #pstate{transaction = Trans}) ->
|
|
||||||
Id = header(<<"transaction">>, Headers),
|
|
||||||
case maps:get(Id, Trans, undefined) of
|
|
||||||
{_, Actions} ->
|
|
||||||
NState = lists:foldr(fun({Func, Args}, S) ->
|
|
||||||
erlang:apply(Func, Args ++ [S])
|
|
||||||
end, State#pstate{transaction = maps:remove(Id, Trans)}, Actions),
|
|
||||||
maybe_send_receipt(receipt_id(Headers), NState);
|
|
||||||
_ ->
|
|
||||||
send(error_frame(receipt_id(Headers), ["Transaction ", Id, " not found"]), State)
|
|
||||||
end;
|
|
||||||
|
|
||||||
%% ABORT
|
|
||||||
%% transaction:tx1
|
|
||||||
%%
|
|
||||||
%% ^@
|
|
||||||
received(#stomp_frame{command = <<"ABORT">>, headers = Headers},
|
|
||||||
State = #pstate{transaction = Trans}) ->
|
|
||||||
Id = header(<<"transaction">>, Headers),
|
|
||||||
case maps:get(Id, Trans, undefined) of
|
|
||||||
{_, _Actions} ->
|
|
||||||
NState = State#pstate{transaction = maps:remove(Id, Trans)},
|
|
||||||
maybe_send_receipt(receipt_id(Headers), NState);
|
|
||||||
_ ->
|
|
||||||
send(error_frame(receipt_id(Headers), ["Transaction ", Id, " not found"]), State)
|
|
||||||
end;
|
|
||||||
|
|
||||||
received(#stomp_frame{command = <<"DISCONNECT">>, headers = Headers}, State) ->
|
|
||||||
_ = maybe_send_receipt(receipt_id(Headers), State),
|
|
||||||
{stop, normal, State}.
|
|
||||||
|
|
||||||
send(Msg = #message{topic = Topic, headers = Headers, payload = Payload},
|
|
||||||
State = #pstate{subscriptions = Subscriptions}) ->
|
|
||||||
case lists:keyfind(Topic, 2, Subscriptions) of
|
|
||||||
{Id, Topic, Ack} ->
|
|
||||||
Headers0 = [{<<"subscription">>, Id},
|
|
||||||
{<<"message-id">>, next_msgid()},
|
|
||||||
{<<"destination">>, Topic},
|
|
||||||
{<<"content-type">>, <<"text/plain">>}],
|
|
||||||
Headers1 = case Ack of
|
|
||||||
_ when Ack =:= <<"client">> orelse Ack =:= <<"client-individual">> ->
|
|
||||||
Headers0 ++ [{<<"ack">>, next_ackid()}];
|
|
||||||
_ ->
|
|
||||||
Headers0
|
|
||||||
end,
|
|
||||||
Frame = #stomp_frame{command = <<"MESSAGE">>,
|
|
||||||
headers = Headers1 ++ maps:get(stomp_headers, Headers, []),
|
|
||||||
body = Payload},
|
|
||||||
send(Frame, State);
|
|
||||||
false ->
|
|
||||||
?LOG(error, "Stomp dropped: ~p", [Msg]),
|
|
||||||
{error, dropped, State}
|
|
||||||
end;
|
|
||||||
|
|
||||||
send(Frame, State = #pstate{sendfun = {Fun, Args}}) ->
|
|
||||||
?LOG(info, "SEND Frame: ~s", [emqx_stomp_frame:format(Frame)]),
|
|
||||||
Data = emqx_stomp_frame:serialize(Frame),
|
|
||||||
?LOG(debug, "SEND ~p", [Data]),
|
|
||||||
erlang:apply(Fun, [Data] ++ Args),
|
|
||||||
{ok, State}.
|
|
||||||
|
|
||||||
shutdown(_Reason, _State) ->
|
|
||||||
ok.
|
|
||||||
|
|
||||||
timeout(_TRef, {incoming, NewVal},
|
|
||||||
State = #pstate{heart_beats = HrtBt}) ->
|
|
||||||
case emqx_stomp_heartbeat:check(incoming, NewVal, HrtBt) of
|
|
||||||
{error, timeout} ->
|
|
||||||
{shutdown, heartbeat_timeout, State};
|
|
||||||
{ok, NHrtBt} ->
|
|
||||||
{ok, reset_timer(incoming_timer, State#pstate{heart_beats = NHrtBt})}
|
|
||||||
end;
|
|
||||||
|
|
||||||
timeout(_TRef, {outgoing, NewVal},
|
|
||||||
State = #pstate{heart_beats = HrtBt,
|
|
||||||
heartfun = {Fun, Args}}) ->
|
|
||||||
case emqx_stomp_heartbeat:check(outgoing, NewVal, HrtBt) of
|
|
||||||
{error, timeout} ->
|
|
||||||
_ = erlang:apply(Fun, Args),
|
|
||||||
{ok, State};
|
|
||||||
{ok, NHrtBt} ->
|
|
||||||
{ok, reset_timer(outgoing_timer, State#pstate{heart_beats = NHrtBt})}
|
|
||||||
end;
|
|
||||||
|
|
||||||
timeout(_TRef, clean_trans, State = #pstate{transaction = Trans}) ->
|
|
||||||
Now = erlang:system_time(millisecond),
|
|
||||||
NTrans = maps:filter(fun(_, {Ts, _}) -> Ts + ?TRANS_TIMEOUT < Now end, Trans),
|
|
||||||
{ok, ensure_clean_trans_timer(State#pstate{transaction = NTrans})}.
|
|
||||||
|
|
||||||
negotiate_version(undefined) ->
|
|
||||||
{ok, <<"1.0">>};
|
|
||||||
negotiate_version(Accepts) ->
|
|
||||||
negotiate_version(?STOMP_VER,
|
|
||||||
lists:reverse(
|
|
||||||
lists:sort(
|
|
||||||
binary:split(Accepts, <<",">>, [global])))).
|
|
||||||
|
|
||||||
negotiate_version(Ver, []) ->
|
|
||||||
{error, <<"Supported protocol versions < ", Ver/binary>>};
|
|
||||||
negotiate_version(Ver, [AcceptVer|_]) when Ver >= AcceptVer ->
|
|
||||||
{ok, AcceptVer};
|
|
||||||
negotiate_version(Ver, [_|T]) ->
|
|
||||||
negotiate_version(Ver, T).
|
|
||||||
|
|
||||||
check_login(undefined, _, AllowAnonymous, _) ->
|
|
||||||
AllowAnonymous;
|
|
||||||
check_login(_, _, _, undefined) ->
|
|
||||||
false;
|
|
||||||
check_login(Login, Passcode, _, DefaultUser) ->
|
|
||||||
case {list_to_binary(get_value(login, DefaultUser)),
|
|
||||||
list_to_binary(get_value(passcode, DefaultUser))} of
|
|
||||||
{Login, Passcode} -> true;
|
|
||||||
{_, _ } -> false
|
|
||||||
end.
|
|
||||||
|
|
||||||
add_action(Id, Action, ReceiptId, State = #pstate{transaction = Trans}) ->
|
|
||||||
case maps:get(Id, Trans, undefined) of
|
|
||||||
{Ts, Actions} ->
|
|
||||||
NTrans = Trans#{Id => {Ts, [Action|Actions]}},
|
|
||||||
{ok, State#pstate{transaction = NTrans}};
|
|
||||||
_ ->
|
|
||||||
send(error_frame(ReceiptId, ["Transaction ", Id, " not found"]), State)
|
|
||||||
end.
|
|
||||||
|
|
||||||
maybe_send_receipt(undefined, State) ->
|
|
||||||
{ok, State};
|
|
||||||
maybe_send_receipt(ReceiptId, State) ->
|
|
||||||
send(receipt_frame(ReceiptId), State).
|
|
||||||
|
|
||||||
ack(_Id, State) ->
|
|
||||||
State.
|
|
||||||
|
|
||||||
nack(_Id, State) -> State.
|
|
||||||
|
|
||||||
header(Name, Headers) ->
|
|
||||||
get_value(Name, Headers).
|
|
||||||
header(Name, Headers, Val) ->
|
|
||||||
get_value(Name, Headers, Val).
|
|
||||||
|
|
||||||
connected_frame(Headers) ->
|
|
||||||
emqx_stomp_frame:make(<<"CONNECTED">>, Headers).
|
|
||||||
|
|
||||||
receipt_frame(ReceiptId) ->
|
|
||||||
emqx_stomp_frame:make(<<"RECEIPT">>, [{<<"receipt-id">>, ReceiptId}]).
|
|
||||||
|
|
||||||
error_frame(ReceiptId, Msg) ->
|
|
||||||
error_frame([{<<"content-type">>, <<"text/plain">>}], ReceiptId, Msg).
|
|
||||||
|
|
||||||
error_frame(Headers, undefined, Msg) ->
|
|
||||||
emqx_stomp_frame:make(<<"ERROR">>, Headers, Msg);
|
|
||||||
error_frame(Headers, ReceiptId, Msg) ->
|
|
||||||
emqx_stomp_frame:make(<<"ERROR">>, [{<<"receipt-id">>, ReceiptId} | Headers], Msg).
|
|
||||||
|
|
||||||
next_msgid() ->
|
|
||||||
MsgId = case get(msgid) of
|
|
||||||
undefined -> 1;
|
|
||||||
I -> I
|
|
||||||
end,
|
|
||||||
put(msgid, MsgId + 1),
|
|
||||||
MsgId.
|
|
||||||
|
|
||||||
next_ackid() ->
|
|
||||||
AckId = case get(ackid) of
|
|
||||||
undefined -> 1;
|
|
||||||
I -> I
|
|
||||||
end,
|
|
||||||
put(ackid, AckId + 1),
|
|
||||||
AckId.
|
|
||||||
|
|
||||||
make_mqtt_message(Topic, Headers, Body) ->
|
|
||||||
Msg = emqx_message:make(stomp, Topic, Body),
|
|
||||||
Headers1 = lists:foldl(fun(Key, Headers0) ->
|
|
||||||
proplists:delete(Key, Headers0)
|
|
||||||
end, Headers, [<<"destination">>,
|
|
||||||
<<"content-length">>,
|
|
||||||
<<"content-type">>,
|
|
||||||
<<"transaction">>,
|
|
||||||
<<"receipt">>]),
|
|
||||||
emqx_message:set_headers(#{stomp_headers => Headers1}, Msg).
|
|
||||||
|
|
||||||
receipt_id(Headers) ->
|
|
||||||
header(<<"receipt">>, Headers).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Transaction Handle
|
|
||||||
|
|
||||||
handle_recv_send_frame(#stomp_frame{command = <<"SEND">>, headers = Headers, body = Body}, State) ->
|
|
||||||
Topic = header(<<"destination">>, Headers),
|
|
||||||
_ = maybe_send_receipt(receipt_id(Headers), State),
|
|
||||||
_ = emqx_broker:publish(
|
|
||||||
make_mqtt_message(Topic, Headers, iolist_to_binary(Body))
|
|
||||||
),
|
|
||||||
State.
|
|
||||||
|
|
||||||
handle_recv_ack_frame(#stomp_frame{command = <<"ACK">>, headers = Headers}, State) ->
|
|
||||||
Id = header(<<"id">>, Headers),
|
|
||||||
_ = maybe_send_receipt(receipt_id(Headers), State),
|
|
||||||
ack(Id, State).
|
|
||||||
|
|
||||||
handle_recv_nack_frame(#stomp_frame{command = <<"NACK">>, headers = Headers}, State) ->
|
|
||||||
Id = header(<<"id">>, Headers),
|
|
||||||
_ = maybe_send_receipt(receipt_id(Headers), State),
|
|
||||||
nack(Id, State).
|
|
||||||
|
|
||||||
ensure_clean_trans_timer(State = #pstate{transaction = Trans}) ->
|
|
||||||
case maps:size(Trans) of
|
|
||||||
0 -> State;
|
|
||||||
_ -> ensure_timer(clean_trans_timer, State)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Heartbeat
|
|
||||||
|
|
||||||
parse_heartbeats(Heartbeats) ->
|
|
||||||
CxCy = re:split(Heartbeats, <<",">>, [{return, list}]),
|
|
||||||
list_to_tuple([list_to_integer(S) || S <- CxCy]).
|
|
||||||
|
|
||||||
reverse_heartbeats({Cx, Cy}) ->
|
|
||||||
iolist_to_binary(io_lib:format("~w,~w", [Cy, Cx])).
|
|
||||||
|
|
||||||
start_heartbeart_timer(Heartbeats, State) ->
|
|
||||||
ensure_timer(
|
|
||||||
[incoming_timer, outgoing_timer],
|
|
||||||
State#pstate{heart_beats = emqx_stomp_heartbeat:init(Heartbeats)}).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Timer
|
|
||||||
|
|
||||||
ensure_timer([Name], State) ->
|
|
||||||
ensure_timer(Name, State);
|
|
||||||
ensure_timer([Name | Rest], State) ->
|
|
||||||
ensure_timer(Rest, ensure_timer(Name, State));
|
|
||||||
|
|
||||||
ensure_timer(Name, State = #pstate{timers = Timers}) ->
|
|
||||||
TRef = maps:get(Name, Timers, undefined),
|
|
||||||
Time = interval(Name, State),
|
|
||||||
case TRef == undefined andalso is_integer(Time) andalso Time > 0 of
|
|
||||||
true -> ensure_timer(Name, Time, State);
|
|
||||||
false -> State %% Timer disabled or exists
|
|
||||||
end.
|
|
||||||
|
|
||||||
ensure_timer(Name, Time, State = #pstate{timers = Timers}) ->
|
|
||||||
Msg = maps:get(Name, ?TIMER_TABLE),
|
|
||||||
TRef = emqx_misc:start_timer(Time, Msg),
|
|
||||||
State#pstate{timers = Timers#{Name => TRef}}.
|
|
||||||
|
|
||||||
reset_timer(Name, State) ->
|
|
||||||
ensure_timer(Name, clean_timer(Name, State)).
|
|
||||||
|
|
||||||
clean_timer(Name, State = #pstate{timers = Timers}) ->
|
|
||||||
State#pstate{timers = maps:remove(Name, Timers)}.
|
|
||||||
|
|
||||||
interval(incoming_timer, #pstate{heart_beats = HrtBt}) ->
|
|
||||||
emqx_stomp_heartbeat:interval(incoming, HrtBt);
|
|
||||||
interval(outgoing_timer, #pstate{heart_beats = HrtBt}) ->
|
|
||||||
emqx_stomp_heartbeat:interval(outgoing, HrtBt);
|
|
||||||
interval(clean_trans_timer, _) ->
|
|
||||||
?TRANS_TIMEOUT.
|
|
|
@ -1,19 +0,0 @@
|
||||||
from stompest.config import StompConfig
|
|
||||||
from stompest.protocol import StompSpec
|
|
||||||
from stompest.sync import Stomp
|
|
||||||
|
|
||||||
CONFIG = StompConfig('tcp://localhost:61613', version=StompSpec.VERSION_1_1)
|
|
||||||
QUEUE = '/queue/test'
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
client = Stomp(CONFIG)
|
|
||||||
client.connect(heartBeats=(0, 10000))
|
|
||||||
client.subscribe(QUEUE, {StompSpec.ID_HEADER: 1, StompSpec.ACK_HEADER: StompSpec.ACK_CLIENT_INDIVIDUAL})
|
|
||||||
client.send(QUEUE, 'test message 1')
|
|
||||||
client.send(QUEUE, 'test message 2')
|
|
||||||
while True:
|
|
||||||
frame = client.receiveFrame()
|
|
||||||
print 'Got %s' % frame.info()
|
|
||||||
client.ack(frame)
|
|
||||||
client.disconnect()
|
|
||||||
|
|
|
@ -253,6 +253,7 @@ relx_apps(ReleaseType) ->
|
||||||
, emqx_connector
|
, emqx_connector
|
||||||
, emqx_authn
|
, emqx_authn
|
||||||
, emqx_authz
|
, emqx_authz
|
||||||
|
, emqx_gateway
|
||||||
, emqx_data_bridge
|
, emqx_data_bridge
|
||||||
, emqx_rule_engine
|
, emqx_rule_engine
|
||||||
, emqx_rule_actions
|
, emqx_rule_actions
|
||||||
|
|
Loading…
Reference in New Issue