Merge branch 'master' into fix/authn2
This commit is contained in:
commit
79685a77ba
|
|
@ -1,5 +1,6 @@
|
||||||
* text=auto
|
* text=auto
|
||||||
*.* text eol=lf
|
*.* text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
*.jpg -text
|
*.jpg -text
|
||||||
*.png -text
|
*.png -text
|
||||||
*.pdf -text
|
*.pdf -text
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,6 @@ jobs:
|
||||||
path: source/_packages/${{ matrix.profile }}/.
|
path: source/_packages/${{ matrix.profile }}/.
|
||||||
|
|
||||||
mac:
|
mac:
|
||||||
runs-on: macos-10.15
|
|
||||||
|
|
||||||
needs: prepare
|
needs: prepare
|
||||||
|
|
||||||
|
|
@ -148,11 +147,16 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
profile: ${{fromJSON(needs.prepare.outputs.profiles)}}
|
profile: ${{fromJSON(needs.prepare.outputs.profiles)}}
|
||||||
|
macos:
|
||||||
|
- macos-11
|
||||||
|
- macos-10.15
|
||||||
otp:
|
otp:
|
||||||
- 24.0.5-emqx-1
|
- 24.0.5-emqx-1
|
||||||
exclude:
|
exclude:
|
||||||
- profile: emqx-edge
|
- profile: emqx-edge
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.macos }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
|
@ -170,16 +174,12 @@ jobs:
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ~/.kerl
|
path: ~/.kerl
|
||||||
key: erl${{ matrix.otp }}-macos10.15
|
key: otp-${{ matrix.otp }}-${{ matrix.macos }}
|
||||||
- name: build erlang
|
- name: build erlang
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
|
||||||
KERL_BUILD_BACKEND: git
|
|
||||||
OTP_GITHUB_URL: https://github.com/emqx/otp
|
|
||||||
run: |
|
run: |
|
||||||
kerl update releases
|
kerl build git https://github.com/emqx/otp.git OTP-${{ matrix.otp }} ${{ matrix.otp }}
|
||||||
kerl build ${{ matrix.otp }}
|
|
||||||
kerl install ${{ matrix.otp }} $HOME/.kerl/${{ matrix.otp }}
|
kerl install ${{ matrix.otp }} $HOME/.kerl/${{ matrix.otp }}
|
||||||
- name: build
|
- name: build
|
||||||
working-directory: source
|
working-directory: source
|
||||||
|
|
@ -191,8 +191,8 @@ jobs:
|
||||||
- name: test
|
- name: test
|
||||||
working-directory: source
|
working-directory: source
|
||||||
run: |
|
run: |
|
||||||
pkg_name=$(basename _packages/${{ matrix.profile }}/${{ matrix.profile }}-*.zip)
|
pkg_name=$(find _packages/${{ matrix.profile }} -mindepth 1 -maxdepth 1 -iname \*.zip | head)
|
||||||
unzip -q _packages/${{ matrix.profile }}/$pkg_name
|
unzip -q $pkg_name
|
||||||
# gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
|
# gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
|
||||||
./emqx/bin/emqx start || cat emqx/log/erlang.log.1
|
./emqx/bin/emqx start || cat emqx/log/erlang.log.1
|
||||||
ready='no'
|
ready='no'
|
||||||
|
|
@ -211,7 +211,7 @@ jobs:
|
||||||
./emqx/bin/emqx_ctl status
|
./emqx/bin/emqx_ctl status
|
||||||
./emqx/bin/emqx stop
|
./emqx/bin/emqx stop
|
||||||
rm -rf emqx
|
rm -rf emqx
|
||||||
openssl dgst -sha256 ./_packages/${{ matrix.profile }}/$pkg_name | awk '{print $2}' > ./_packages/${{ matrix.profile }}/$pkg_name.sha256
|
openssl dgst -sha256 $pkg_name | awk '{print $2}' > $pkg_name.sha256
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v1
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ jobs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
otp:
|
otp:
|
||||||
- 24.0.5-emqx-1
|
- 24.0.5-emqx-1
|
||||||
|
|
@ -53,13 +54,18 @@ jobs:
|
||||||
path: _packages/**/*.zip
|
path: _packages/**/*.zip
|
||||||
|
|
||||||
mac:
|
mac:
|
||||||
runs-on: macos-10.15
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
macos:
|
||||||
|
- macos-11
|
||||||
|
- macos-10.15
|
||||||
otp:
|
otp:
|
||||||
- 24.0.5-emqx-1
|
- 24.0.5-emqx-1
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.macos }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: prepare
|
- name: prepare
|
||||||
|
|
@ -82,16 +88,12 @@ jobs:
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ~/.kerl
|
path: ~/.kerl
|
||||||
key: erl${{ matrix.otp }}-macos10.15
|
key: otp-${{ matrix.otp }}-${{ matrix.macos }}
|
||||||
- name: build erlang
|
- name: build erlang
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
|
||||||
KERL_BUILD_BACKEND: git
|
|
||||||
OTP_GITHUB_URL: https://github.com/emqx/otp
|
|
||||||
run: |
|
run: |
|
||||||
kerl update releases
|
kerl build git https://github.com/emqx/otp.git OTP-${{ matrix.otp }} ${{ matrix.otp }}
|
||||||
kerl build ${{ matrix.otp }}
|
|
||||||
kerl install ${{ matrix.otp }} $HOME/.kerl/${{ matrix.otp }}
|
kerl install ${{ matrix.otp }} $HOME/.kerl/${{ matrix.otp }}
|
||||||
- name: build
|
- name: build
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -106,8 +108,7 @@ jobs:
|
||||||
path: ./rebar3.crashdump
|
path: ./rebar3.crashdump
|
||||||
- name: test
|
- name: test
|
||||||
run: |
|
run: |
|
||||||
pkg_name=$(basename _packages/${EMQX_NAME}/emqx-*.zip)
|
unzip -q $(find _packages/${EMQX_NAME} -mindepth 1 -maxdepth 1 -iname \*.zip | head)
|
||||||
unzip -q _packages/${EMQX_NAME}/$pkg_name
|
|
||||||
# gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
|
# gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
|
||||||
./emqx/bin/emqx start || cat emqx/log/erlang.log.1
|
./emqx/bin/emqx start || cat emqx/log/erlang.log.1
|
||||||
ready='no'
|
ready='no'
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,13 @@ jobs:
|
||||||
- api_login
|
- api_login
|
||||||
- api_banned
|
- api_banned
|
||||||
- api_alarms
|
- api_alarms
|
||||||
|
- api_nodes
|
||||||
|
- api_topic_metrics
|
||||||
|
- api_retainer
|
||||||
|
- api_auto_subscribe
|
||||||
|
- api_delayed_publish
|
||||||
|
- api_topic_rewrite
|
||||||
|
- api_event_message
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
|
@ -74,7 +81,7 @@ jobs:
|
||||||
cd /tmp && tar -xvf apache-jmeter.tgz
|
cd /tmp && tar -xvf apache-jmeter.tgz
|
||||||
echo "jmeter.save.saveservice.output_format=xml" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties
|
echo "jmeter.save.saveservice.output_format=xml" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties
|
||||||
echo "jmeter.save.saveservice.response_data.on_error=true" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties
|
echo "jmeter.save.saveservice.response_data.on_error=true" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties
|
||||||
wget --no-verbose -O /tmp/apache-jmeter-$JMETER_VERSION/lib/ext/mqtt-xmeter-2.0.2-jar-with-dependencies.jar https://raw.githubusercontent.com/xmeter-net/mqtt-jmeter/master/Download/v2.0.2/mqtt-xmeter-2.0.2-jar-with-dependencies.jar
|
wget --no-verbose -O /tmp/apache-jmeter-$JMETER_VERSION/lib/ext/mqtt-xmeter-fuse-2.0.2-jar-with-dependencies.jar https://raw.githubusercontent.com/xmeter-net/mqtt-jmeter/master/Download/v2.0.2/mqtt-xmeter-fuse-2.0.2-jar-with-dependencies.jar
|
||||||
ln -s /tmp/apache-jmeter-$JMETER_VERSION /opt/jmeter
|
ln -s /tmp/apache-jmeter-$JMETER_VERSION /opt/jmeter
|
||||||
- name: run ${{ matrix.script_name }}
|
- name: run ${{ matrix.script_name }}
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,5 @@ _upgrade_base/
|
||||||
TAGS
|
TAGS
|
||||||
erlang_ls.config
|
erlang_ls.config
|
||||||
.els_cache/
|
.els_cache/
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build
|
||||||
SCRIPTS = $(CURDIR)/scripts
|
SCRIPTS = $(CURDIR)/scripts
|
||||||
export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh)
|
export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh)
|
||||||
export EMQX_DESC ?= EMQ X
|
export EMQX_DESC ?= EMQ X
|
||||||
export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.14
|
export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.15
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
export REBAR_COLOR=none
|
export REBAR_COLOR=none
|
||||||
endif
|
endif
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ English | [简体中文](./README-CN.md) | [日本語](./README-JP.md) | [рус
|
||||||
|
|
||||||
*EMQ X* broker is a fully open source, highly scalable, highly available distributed MQTT messaging broker for IoT, M2M and Mobile applications that can handle tens of millions of concurrent clients.
|
*EMQ X* broker is a fully open source, highly scalable, highly available distributed MQTT messaging broker for IoT, M2M and Mobile applications that can handle tens of millions of concurrent clients.
|
||||||
|
|
||||||
Starting from 3.0 release, *EMQ X* broker fully supports MQTT V5.0 protocol specifications and backward compatible with MQTT V3.1 and V3.1.1, as well as other communication protocols such as MQTT-SN, CoAP, LwM2M, WebSocket and STOMP. The 3.0 release of the *EMQ X* broker can scaled to 10+ million concurrent MQTT connections on one cluster.
|
Starting from 3.0 release, *EMQ X* broker fully supports MQTT V5.0 protocol specifications and backward compatible with MQTT V3.1 and V3.1.1, as well as other communication protocols such as MQTT-SN, CoAP, LwM2M, WebSocket and STOMP. The 3.0 release of the *EMQ X* broker can scale to 10+ million concurrent MQTT connections on one cluster.
|
||||||
|
|
||||||
- For full list of new features, please read [EMQ X Release Notes](https://github.com/emqx/emqx/releases).
|
- For full list of new features, please read [EMQ X Release Notes](https://github.com/emqx/emqx/releases).
|
||||||
- For more information, please visit [EMQ X homepage](https://www.emqx.io/).
|
- For more information, please visit [EMQ X homepage](https://www.emqx.io/).
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ listeners.ssl.default {
|
||||||
ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem"
|
ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem"
|
||||||
ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
||||||
|
|
||||||
ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
# ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
||||||
# TLS 1.3: "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"
|
# TLS 1.3: "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"
|
||||||
# TLS 1-1.2 "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"
|
# TLS 1-1.2 "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"
|
||||||
# PSK: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA"
|
# PSK: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA"
|
||||||
|
|
@ -1350,7 +1350,7 @@ example_common_ssl_options {
|
||||||
## Default: true
|
## Default: true
|
||||||
ssl.honor_cipher_order = true
|
ssl.honor_cipher_order = true
|
||||||
|
|
||||||
ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
# ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
||||||
# TLS 1.3: "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"
|
# TLS 1.3: "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"
|
||||||
# TLS 1-1.2 "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"
|
# TLS 1-1.2 "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"
|
||||||
# PSK: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA"
|
# PSK: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
, {typerefl, {git, "https://github.com/k32/typerefl", {tag, "0.8.5"}}}
|
, {typerefl, {git, "https://github.com/k32/typerefl", {tag, "0.8.5"}}}
|
||||||
, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
|
, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
|
||||||
, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.3"}}}
|
, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.3"}}}
|
||||||
, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}}
|
, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.3"}}}
|
||||||
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}}
|
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}}
|
||||||
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}}
|
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}}
|
||||||
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.5"}}}
|
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.5"}}}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ stop() ->
|
||||||
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
|
||||||
update_config(SchemaModule, ConfKeyPath, UpdateArgs) ->
|
update_config(SchemaModule, ConfKeyPath, UpdateArgs) ->
|
||||||
?ATOM_CONF_PATH(ConfKeyPath, gen_server:call(?MODULE, {change_config, SchemaModule,
|
?ATOM_CONF_PATH(ConfKeyPath, gen_server:call(?MODULE, {change_config, SchemaModule,
|
||||||
AtomKeyPath, UpdateArgs}), {error, ConfKeyPath}).
|
AtomKeyPath, UpdateArgs}), {error, {not_found, ConfKeyPath}}).
|
||||||
|
|
||||||
-spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok.
|
-spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok.
|
||||||
add_handler(ConfKeyPath, HandlerName) ->
|
add_handler(ConfKeyPath, HandlerName) ->
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
%% - The execution order is the adding order of callbacks if they have
|
%% - The execution order is the adding order of callbacks if they have
|
||||||
%% equal priority values.
|
%% equal priority values.
|
||||||
|
|
||||||
-type(hookpoint() :: atom()).
|
-type(hookpoint() :: atom() | binary()).
|
||||||
-type(action() :: {module(), atom(), [term()] | undefined}).
|
-type(action() :: {module(), atom(), [term()] | undefined}).
|
||||||
-type(filter() :: {module(), atom(), [term()] | undefined}).
|
-type(filter() :: {module(), atom(), [term()] | undefined}).
|
||||||
|
|
||||||
|
|
@ -158,12 +158,12 @@ del(HookPoint, Action) ->
|
||||||
gen_server:cast(?SERVER, {del, HookPoint, Action}).
|
gen_server:cast(?SERVER, {del, HookPoint, Action}).
|
||||||
|
|
||||||
%% @doc Run hooks.
|
%% @doc Run hooks.
|
||||||
-spec(run(atom(), list(Arg::term())) -> ok).
|
-spec(run(hookpoint(), list(Arg::term())) -> ok).
|
||||||
run(HookPoint, Args) ->
|
run(HookPoint, Args) ->
|
||||||
do_run(lookup(HookPoint), Args).
|
do_run(lookup(HookPoint), Args).
|
||||||
|
|
||||||
%% @doc Run hooks with Accumulator.
|
%% @doc Run hooks with Accumulator.
|
||||||
-spec(run_fold(atom(), list(Arg::term()), Acc::term()) -> Acc::term()).
|
-spec(run_fold(hookpoint(), list(Arg::term()), Acc::term()) -> Acc::term()).
|
||||||
run_fold(HookPoint, Args, Acc) ->
|
run_fold(HookPoint, Args, Acc) ->
|
||||||
do_run_fold(lookup(HookPoint), Args, Acc).
|
do_run_fold(lookup(HookPoint), Args, Acc).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
-module(emqx_rule_actions_trans).
|
|
||||||
|
|
||||||
-include_lib("syntax_tools/include/merl.hrl").
|
|
||||||
|
|
||||||
-export([parse_transform/2]).
|
|
||||||
|
|
||||||
parse_transform(Forms, _Options) ->
|
|
||||||
trans(Forms, []).
|
|
||||||
|
|
||||||
trans([], ResAST) ->
|
|
||||||
lists:reverse(ResAST);
|
|
||||||
trans([{eof, L} | AST], ResAST) ->
|
|
||||||
lists:reverse([{eof, L} | ResAST]) ++ AST;
|
|
||||||
trans([{function, LineNo, FuncName, Arity, Clauses} | AST], ResAST) ->
|
|
||||||
NewClauses = trans_func_clauses(atom_to_list(FuncName), Clauses),
|
|
||||||
trans(AST, [{function, LineNo, FuncName, Arity, NewClauses} | ResAST]);
|
|
||||||
trans([Form | AST], ResAST) ->
|
|
||||||
trans(AST, [Form | ResAST]).
|
|
||||||
|
|
||||||
trans_func_clauses("on_action_create_" ++ _ = _FuncName , Clauses) ->
|
|
||||||
NewClauses = [
|
|
||||||
begin
|
|
||||||
Bindings = lists:flatten(get_vars(Args) ++ get_vars(Body, lefth)),
|
|
||||||
Body2 = append_to_result(Bindings, Body),
|
|
||||||
{clause, LineNo, Args, Guards, Body2}
|
|
||||||
end || {clause, LineNo, Args, Guards, Body} <- Clauses],
|
|
||||||
NewClauses;
|
|
||||||
trans_func_clauses(_FuncName, Clauses) ->
|
|
||||||
Clauses.
|
|
||||||
|
|
||||||
get_vars(Exprs) ->
|
|
||||||
get_vars(Exprs, all).
|
|
||||||
get_vars(Exprs, Type) ->
|
|
||||||
do_get_vars(Exprs, [], Type).
|
|
||||||
|
|
||||||
do_get_vars([], Vars, _Type) -> Vars;
|
|
||||||
do_get_vars([Line | Expr], Vars, all) ->
|
|
||||||
do_get_vars(Expr, [syntax_vars(erl_syntax:form_list([Line])) | Vars], all);
|
|
||||||
do_get_vars([Line | Expr], Vars, lefth) ->
|
|
||||||
do_get_vars(Expr,
|
|
||||||
case (Line) of
|
|
||||||
?Q("_@LeftV = _@@_") -> Vars ++ syntax_vars(LeftV);
|
|
||||||
_ -> Vars
|
|
||||||
end, lefth).
|
|
||||||
|
|
||||||
syntax_vars(Line) ->
|
|
||||||
sets:to_list(erl_syntax_lib:variables(Line)).
|
|
||||||
|
|
||||||
%% append bindings to the return value as the first tuple element.
|
|
||||||
%% e.g. if the original result is R, then the new result will be {[binding()], R}.
|
|
||||||
append_to_result(Bindings, Exprs) ->
|
|
||||||
erl_syntax:revert_forms(do_append_to_result(to_keyword(Bindings), Exprs, [])).
|
|
||||||
|
|
||||||
do_append_to_result(KeyWordVars, [Line], Res) ->
|
|
||||||
case Line of
|
|
||||||
?Q("_@LeftV = _@RightV") ->
|
|
||||||
lists:reverse([?Q("{[_@KeyWordVars], _@LeftV}"), Line | Res]);
|
|
||||||
_ ->
|
|
||||||
lists:reverse([?Q("{[_@KeyWordVars], _@Line}") | Res])
|
|
||||||
end;
|
|
||||||
do_append_to_result(KeyWordVars, [Line | Exprs], Res) ->
|
|
||||||
do_append_to_result(KeyWordVars, Exprs, [Line | Res]).
|
|
||||||
|
|
||||||
to_keyword(Vars) ->
|
|
||||||
[erl_syntax:tuple([erl_syntax:atom(Var), merl:var(Var)])
|
|
||||||
|| Var <- Vars].
|
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
|
|
||||||
% workaround: prevent being recognized as unused functions
|
% workaround: prevent being recognized as unused functions
|
||||||
-export([to_duration/1, to_duration_s/1, to_duration_ms/1,
|
-export([to_duration/1, to_duration_s/1, to_duration_ms/1,
|
||||||
to_bytesize/1, to_wordsize/1,
|
mk_duration/2, to_bytesize/1, to_wordsize/1,
|
||||||
to_percent/1, to_comma_separated_list/1,
|
to_percent/1, to_comma_separated_list/1,
|
||||||
to_bar_separated_list/1, to_ip_port/1,
|
to_bar_separated_list/1, to_ip_port/1,
|
||||||
to_erl_cipher_suite/1,
|
to_erl_cipher_suite/1,
|
||||||
|
|
@ -159,11 +159,11 @@ fields("stats") ->
|
||||||
|
|
||||||
fields("authorization") ->
|
fields("authorization") ->
|
||||||
[ {"no_match",
|
[ {"no_match",
|
||||||
sc(hoconsc:union([allow, deny]),
|
sc(hoconsc:enum([allow, deny]),
|
||||||
#{ default => allow
|
#{ default => allow
|
||||||
})}
|
})}
|
||||||
, {"deny_action",
|
, {"deny_action",
|
||||||
sc(hoconsc:union([ignore, disconnect]),
|
sc(hoconsc:enum([ignore, disconnect]),
|
||||||
#{ default => ignore
|
#{ default => ignore
|
||||||
})}
|
})}
|
||||||
, {"cache",
|
, {"cache",
|
||||||
|
|
@ -297,7 +297,7 @@ fields("mqtt") ->
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"mqueue_default_priority",
|
, {"mqueue_default_priority",
|
||||||
sc(union(highest, lowest),
|
sc(hoconsc:enum([highest, lowest]),
|
||||||
#{ default => lowest
|
#{ default => lowest
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -312,11 +312,11 @@ fields("mqtt") ->
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"peer_cert_as_username",
|
, {"peer_cert_as_username",
|
||||||
sc(hoconsc:union([disabled, cn, dn, crt, pem, md5]),
|
sc(hoconsc:enum([disabled, cn, dn, crt, pem, md5]),
|
||||||
#{ default => disabled
|
#{ default => disabled
|
||||||
})}
|
})}
|
||||||
, {"peer_cert_as_clientid",
|
, {"peer_cert_as_clientid",
|
||||||
sc(hoconsc:union([disabled, cn, dn, crt, pem, md5]),
|
sc(hoconsc:enum([disabled, cn, dn, crt, pem, md5]),
|
||||||
#{ default => disabled
|
#{ default => disabled
|
||||||
})}
|
})}
|
||||||
];
|
];
|
||||||
|
|
@ -525,7 +525,7 @@ fields("ws_opts") ->
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"mqtt_piggyback",
|
, {"mqtt_piggyback",
|
||||||
sc(hoconsc:union([single, multiple]),
|
sc(hoconsc:enum([single, multiple]),
|
||||||
#{ default => multiple
|
#{ default => multiple
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -637,23 +637,23 @@ fields("listener_ssl_opts") ->
|
||||||
server_ssl_opts_schema(
|
server_ssl_opts_schema(
|
||||||
#{ depth => 10
|
#{ depth => 10
|
||||||
, reuse_sessions => true
|
, reuse_sessions => true
|
||||||
, versions => tcp
|
, versions => tls_all_available
|
||||||
, ciphers => tcp_all
|
, ciphers => tls_all_available
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
fields("listener_wss_opts") ->
|
fields("listener_wss_opts") ->
|
||||||
server_ssl_opts_schema(
|
server_ssl_opts_schema(
|
||||||
#{ depth => 10
|
#{ depth => 10
|
||||||
, reuse_sessions => true
|
, reuse_sessions => true
|
||||||
, versions => tcp
|
, versions => tls_all_available
|
||||||
, ciphers => tcp_all
|
, ciphers => tls_all_available
|
||||||
}, true);
|
}, true);
|
||||||
fields(ssl_client_opts) ->
|
fields(ssl_client_opts) ->
|
||||||
client_ssl_opts_schema(#{});
|
client_ssl_opts_schema(#{});
|
||||||
|
|
||||||
fields("deflate_opts") ->
|
fields("deflate_opts") ->
|
||||||
[ {"level",
|
[ {"level",
|
||||||
sc(hoconsc:union([none, default, best_compression, best_speed]),
|
sc(hoconsc:enum([none, default, best_compression, best_speed]),
|
||||||
#{})
|
#{})
|
||||||
}
|
}
|
||||||
, {"mem_level",
|
, {"mem_level",
|
||||||
|
|
@ -662,15 +662,15 @@ fields("deflate_opts") ->
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"strategy",
|
, {"strategy",
|
||||||
sc(hoconsc:union([default, filtered, huffman_only, rle]),
|
sc(hoconsc:enum([default, filtered, huffman_only, rle]),
|
||||||
#{})
|
#{})
|
||||||
}
|
}
|
||||||
, {"server_context_takeover",
|
, {"server_context_takeover",
|
||||||
sc(hoconsc:union([takeover, no_takeover]),
|
sc(hoconsc:enum([takeover, no_takeover]),
|
||||||
#{})
|
#{})
|
||||||
}
|
}
|
||||||
, {"client_context_takeover",
|
, {"client_context_takeover",
|
||||||
sc(hoconsc:union([takeover, no_takeover]),
|
sc(hoconsc:enum([takeover, no_takeover]),
|
||||||
#{})
|
#{})
|
||||||
}
|
}
|
||||||
, {"server_max_window_bits",
|
, {"server_max_window_bits",
|
||||||
|
|
@ -709,12 +709,12 @@ fields("broker") ->
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"session_locking_strategy",
|
, {"session_locking_strategy",
|
||||||
sc(hoconsc:union([local, leader, quorum, all]),
|
sc(hoconsc:enum([local, leader, quorum, all]),
|
||||||
#{ default => quorum
|
#{ default => quorum
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"shared_subscription_strategy",
|
, {"shared_subscription_strategy",
|
||||||
sc(hoconsc:union([random, round_robin]),
|
sc(hoconsc:enum([random, round_robin]),
|
||||||
#{ default => round_robin
|
#{ default => round_robin
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -736,7 +736,7 @@ fields("broker") ->
|
||||||
|
|
||||||
fields("broker_perf") ->
|
fields("broker_perf") ->
|
||||||
[ {"route_lock_type",
|
[ {"route_lock_type",
|
||||||
sc(hoconsc:union([key, tab, global]),
|
sc(hoconsc:enum([key, tab, global]),
|
||||||
#{ default => key
|
#{ default => key
|
||||||
})}
|
})}
|
||||||
, {"trie_compaction",
|
, {"trie_compaction",
|
||||||
|
|
@ -962,7 +962,7 @@ the file if it is to be added.
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"verify",
|
, {"verify",
|
||||||
sc(hoconsc:union([verify_peer, verify_none]),
|
sc(hoconsc:enum([verify_peer, verify_none]),
|
||||||
#{ default => Df("verify", verify_none)
|
#{ default => Df("verify", verify_none)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -987,13 +987,14 @@ keyfile is password-protected."""
|
||||||
}
|
}
|
||||||
, {"versions",
|
, {"versions",
|
||||||
sc(hoconsc:array(typerefl:atom()),
|
sc(hoconsc:array(typerefl:atom()),
|
||||||
#{ default => default_tls_vsns(maps:get(versions, Defaults, tcp))
|
#{ default => default_tls_vsns(maps:get(versions, Defaults, tls_all_available))
|
||||||
, desc =>
|
, desc =>
|
||||||
"""All TLS/DTLS versions to be supported.<br>
|
"""All TLS/DTLS versions to be supported.<br>
|
||||||
NOTE: PSK ciphers are suppresed by 'tlsv1.3' version config<br>
|
NOTE: PSK ciphers are suppresed by 'tlsv1.3' version config<br>
|
||||||
In case PSK cipher suites are intended, make sure to configured
|
In case PSK cipher suites are intended, make sure to configured
|
||||||
<code>['tlsv1.2', 'tlsv1.1']</code> here.
|
<code>['tlsv1.2', 'tlsv1.1']</code> here.
|
||||||
"""
|
"""
|
||||||
|
, validator => fun validate_tls_versions/1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"ciphers", ciphers_schema(D("ciphers"))}
|
, {"ciphers", ciphers_schema(D("ciphers"))}
|
||||||
|
|
@ -1086,7 +1087,7 @@ client_ssl_opts_schema(Defaults) ->
|
||||||
, desc =>
|
, desc =>
|
||||||
"""Specify the host name to be used in TLS Server Name Indication extension.<br>
|
"""Specify the host name to be used in TLS Server Name Indication extension.<br>
|
||||||
For instance, when connecting to \"server.example.net\", the genuine server
|
For instance, when connecting to \"server.example.net\", the genuine server
|
||||||
which accedpts the connection and performs TSL handshake may differ from the
|
which accedpts the connection and performs TLS handshake may differ from the
|
||||||
host the TLS client initially connects to, e.g. when connecting to an IP address
|
host the TLS client initially connects to, e.g. when connecting to an IP address
|
||||||
or when the host has multiple resolvable DNS records <br>
|
or when the host has multiple resolvable DNS records <br>
|
||||||
If not specified, it will default to the host name string which is used
|
If not specified, it will default to the host name string which is used
|
||||||
|
|
@ -1099,12 +1100,12 @@ verification check."""
|
||||||
].
|
].
|
||||||
|
|
||||||
|
|
||||||
default_tls_vsns(dtls) ->
|
default_tls_vsns(dtls_all_available) ->
|
||||||
[<<"dtlsv1.2">>, <<"dtlsv1">>];
|
proplists:get_value(available_dtls, ssl:versions());
|
||||||
default_tls_vsns(tcp) ->
|
default_tls_vsns(tls_all_available) ->
|
||||||
[<<"tlsv1.3">>, <<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>].
|
emqx_tls_lib:default_versions().
|
||||||
|
|
||||||
-spec ciphers_schema(quic | dtls | tcp_all | undefined) -> hocon_schema:field_schema().
|
-spec ciphers_schema(quic | dtls_all_available | tls_all_available | undefined) -> hocon_schema:field_schema().
|
||||||
ciphers_schema(Default) ->
|
ciphers_schema(Default) ->
|
||||||
sc(hoconsc:array(string()),
|
sc(hoconsc:array(string()),
|
||||||
#{ default => default_ciphers(Default)
|
#{ default => default_ciphers(Default)
|
||||||
|
|
@ -1113,7 +1114,10 @@ ciphers_schema(Default) ->
|
||||||
(Ciphers) when is_list(Ciphers) ->
|
(Ciphers) when is_list(Ciphers) ->
|
||||||
Ciphers
|
Ciphers
|
||||||
end
|
end
|
||||||
, validator => fun validate_ciphers/1
|
, validator => case Default =:= quic of
|
||||||
|
true -> undefined; %% quic has openssl statically linked
|
||||||
|
false -> fun validate_ciphers/1
|
||||||
|
end
|
||||||
, desc =>
|
, desc =>
|
||||||
"""TLS cipher suite names separated by comma, or as an array of strings
|
"""TLS cipher suite names separated by comma, or as an array of strings
|
||||||
<code>\"TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256\"</code> or
|
<code>\"TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256\"</code> or
|
||||||
|
|
@ -1146,52 +1150,24 @@ RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA\"</code><br>
|
||||||
end}).
|
end}).
|
||||||
|
|
||||||
default_ciphers(undefined) ->
|
default_ciphers(undefined) ->
|
||||||
default_ciphers(tcp_all);
|
default_ciphers(tls_all_available);
|
||||||
default_ciphers(quic) -> [
|
default_ciphers(quic) -> [
|
||||||
"TLS_AES_256_GCM_SHA384",
|
"TLS_AES_256_GCM_SHA384",
|
||||||
"TLS_AES_128_GCM_SHA256",
|
"TLS_AES_128_GCM_SHA256",
|
||||||
"TLS_CHACHA20_POLY1305_SHA256"
|
"TLS_CHACHA20_POLY1305_SHA256"
|
||||||
];
|
];
|
||||||
default_ciphers(tcp_all) ->
|
default_ciphers(dtls_all_available) ->
|
||||||
default_ciphers('tlsv1.3') ++
|
|
||||||
default_ciphers('tlsv1.2') ++
|
|
||||||
default_ciphers(psk);
|
|
||||||
default_ciphers(dtls) ->
|
|
||||||
%% as of now, dtls does not support tlsv1.3 ciphers
|
%% as of now, dtls does not support tlsv1.3 ciphers
|
||||||
default_ciphers('tlsv1.2') ++ default_ciphers('psk');
|
emqx_tls_lib:selected_ciphers(['dtlsv1.2', 'dtlsv1']);
|
||||||
default_ciphers('tlsv1.3') ->
|
default_ciphers(tls_all_available) ->
|
||||||
["TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256",
|
emqx_tls_lib:default_ciphers().
|
||||||
"TLS_CHACHA20_POLY1305_SHA256", "TLS_AES_128_CCM_SHA256",
|
|
||||||
"TLS_AES_128_CCM_8_SHA256"]
|
|
||||||
++ default_ciphers('tlsv1.2');
|
|
||||||
default_ciphers('tlsv1.2') -> [
|
|
||||||
"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"
|
|
||||||
];
|
|
||||||
default_ciphers(psk) ->
|
|
||||||
[ "RSA-PSK-AES256-GCM-SHA384","RSA-PSK-AES256-CBC-SHA384",
|
|
||||||
"RSA-PSK-AES128-GCM-SHA256","RSA-PSK-AES128-CBC-SHA256",
|
|
||||||
"RSA-PSK-AES256-CBC-SHA","RSA-PSK-AES128-CBC-SHA",
|
|
||||||
"RSA-PSK-DES-CBC3-SHA","RSA-PSK-RC4-SHA"
|
|
||||||
].
|
|
||||||
|
|
||||||
%% @private return a list of keys in a parent field
|
%% @private return a list of keys in a parent field
|
||||||
-spec(keys(string(), hocon:config()) -> [string()]).
|
-spec(keys(string(), hocon:config()) -> [string()]).
|
||||||
keys(Parent, Conf) ->
|
keys(Parent, Conf) ->
|
||||||
[binary_to_list(B) || B <- maps:keys(conf_get(Parent, Conf, #{}))].
|
[binary_to_list(B) || B <- maps:keys(conf_get(Parent, Conf, #{}))].
|
||||||
|
|
||||||
-spec ceiling(float()) -> integer().
|
-spec ceiling(number()) -> integer().
|
||||||
ceiling(X) ->
|
ceiling(X) ->
|
||||||
T = erlang:trunc(X),
|
T = erlang:trunc(X),
|
||||||
case (X - T) of
|
case (X - T) of
|
||||||
|
|
@ -1210,6 +1186,15 @@ ref(Field) -> hoconsc:ref(?MODULE, Field).
|
||||||
|
|
||||||
ref(Module, Field) -> hoconsc:ref(Module, Field).
|
ref(Module, Field) -> hoconsc:ref(Module, Field).
|
||||||
|
|
||||||
|
mk_duration(Desc, OverrideMeta) ->
|
||||||
|
DefaultMeta = #{desc => Desc ++ " Time span. A text string with number followed by time units:
|
||||||
|
`ms` for milli-seconds,
|
||||||
|
`s` for seconds,
|
||||||
|
`m` for minutes,
|
||||||
|
`h` for hours;
|
||||||
|
or combined representation like `1h5m0s`"},
|
||||||
|
hoconsc:mk(typerefl:alias("string", duration()), maps:merge(DefaultMeta, OverrideMeta)).
|
||||||
|
|
||||||
to_duration(Str) ->
|
to_duration(Str) ->
|
||||||
case hocon_postprocess:duration(Str) of
|
case hocon_postprocess:duration(Str) of
|
||||||
I when is_integer(I) -> {ok, I};
|
I when is_integer(I) -> {ok, I};
|
||||||
|
|
@ -1218,13 +1203,15 @@ to_duration(Str) ->
|
||||||
|
|
||||||
to_duration_s(Str) ->
|
to_duration_s(Str) ->
|
||||||
case hocon_postprocess:duration(Str) of
|
case hocon_postprocess:duration(Str) of
|
||||||
I when is_integer(I) -> {ok, ceiling(I / 1000)};
|
I when is_number(I) -> {ok, ceiling(I / 1000)};
|
||||||
_ -> {error, Str}
|
_ -> {error, Str}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-spec to_duration_ms(Input) -> {ok, integer()} | {error, Input}
|
||||||
|
when Input :: string() | binary().
|
||||||
to_duration_ms(Str) ->
|
to_duration_ms(Str) ->
|
||||||
case hocon_postprocess:duration(Str) of
|
case hocon_postprocess:duration(Str) of
|
||||||
I when is_integer(I) -> {ok, ceiling(I)};
|
I when is_number(I) -> {ok, ceiling(I)};
|
||||||
_ -> {error, Str}
|
_ -> {error, Str}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -1303,9 +1290,22 @@ parse_user_lookup_fun(StrConf) ->
|
||||||
{fun Mod:Fun/3, <<>>}.
|
{fun Mod:Fun/3, <<>>}.
|
||||||
|
|
||||||
validate_ciphers(Ciphers) ->
|
validate_ciphers(Ciphers) ->
|
||||||
All = ssl:cipher_suites(all, 'tlsv1.3', openssl) ++
|
All = case is_tlsv13_available() of
|
||||||
ssl:cipher_suites(all, 'tlsv1.2', openssl), %% includes older version ciphers
|
true -> ssl:cipher_suites(all, 'tlsv1.3', openssl);
|
||||||
|
false -> []
|
||||||
|
end ++ ssl:cipher_suites(all, 'tlsv1.2', openssl),
|
||||||
case lists:filter(fun(Cipher) -> not lists:member(Cipher, All) end, Ciphers) of
|
case lists:filter(fun(Cipher) -> not lists:member(Cipher, All) end, Ciphers) of
|
||||||
[] -> ok;
|
[] -> ok;
|
||||||
Bad -> {error, {bad_ciphers, Bad}}
|
Bad -> {error, {bad_ciphers, Bad}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
validate_tls_versions(Versions) ->
|
||||||
|
AvailableVersions = proplists:get_value(available, ssl:versions()) ++
|
||||||
|
proplists:get_value(available_dtls, ssl:versions()),
|
||||||
|
case lists:filter(fun(V) -> not lists:member(V, AvailableVersions) end, Versions) of
|
||||||
|
[] -> ok;
|
||||||
|
Vs -> {error, {unsupported_ssl_versions, Vs}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
is_tlsv13_available() ->
|
||||||
|
lists:member('tlsv1.3', proplists:get_value(available, ssl:versions())).
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
-export([ default_versions/0
|
-export([ default_versions/0
|
||||||
, integral_versions/1
|
, integral_versions/1
|
||||||
, default_ciphers/0
|
, default_ciphers/0
|
||||||
, default_ciphers/1
|
, selected_ciphers/1
|
||||||
, integral_ciphers/2
|
, integral_ciphers/2
|
||||||
, drop_tls13_for_old_otp/1
|
, drop_tls13_for_old_otp/1
|
||||||
]).
|
]).
|
||||||
|
|
@ -31,9 +31,7 @@
|
||||||
|
|
||||||
%% @doc Returns the default supported tls versions.
|
%% @doc Returns the default supported tls versions.
|
||||||
-spec default_versions() -> [atom()].
|
-spec default_versions() -> [atom()].
|
||||||
default_versions() ->
|
default_versions() -> available_versions().
|
||||||
OtpRelease = list_to_integer(erlang:system_info(otp_release)),
|
|
||||||
integral_versions(default_versions(OtpRelease)).
|
|
||||||
|
|
||||||
%% @doc Validate a given list of desired tls versions.
|
%% @doc Validate a given list of desired tls versions.
|
||||||
%% raise an error exception if non of them are available.
|
%% raise an error exception if non of them are available.
|
||||||
|
|
@ -51,7 +49,7 @@ integral_versions(Desired) when ?IS_STRING(Desired) ->
|
||||||
integral_versions(Desired) when is_binary(Desired) ->
|
integral_versions(Desired) when is_binary(Desired) ->
|
||||||
integral_versions(parse_versions(Desired));
|
integral_versions(parse_versions(Desired));
|
||||||
integral_versions(Desired) ->
|
integral_versions(Desired) ->
|
||||||
{_, Available} = lists:keyfind(available, 1, ssl:versions()),
|
Available = available_versions(),
|
||||||
case lists:filter(fun(V) -> lists:member(V, Available) end, Desired) of
|
case lists:filter(fun(V) -> lists:member(V, Available) end, Desired) of
|
||||||
[] -> erlang:error(#{ reason => no_available_tls_version
|
[] -> erlang:error(#{ reason => no_available_tls_version
|
||||||
, desired => Desired
|
, desired => Desired
|
||||||
|
|
@ -61,27 +59,61 @@ integral_versions(Desired) ->
|
||||||
Filtered
|
Filtered
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Return a list of default (openssl string format) cipher suites.
|
|
||||||
-spec default_ciphers() -> [string()].
|
|
||||||
default_ciphers() -> default_ciphers(default_versions()).
|
|
||||||
|
|
||||||
%% @doc Return a list of (openssl string format) cipher suites.
|
%% @doc Return a list of (openssl string format) cipher suites.
|
||||||
-spec default_ciphers([ssl:tls_version()]) -> [string()].
|
-spec all_ciphers([ssl:tls_version()]) -> [string()].
|
||||||
default_ciphers(['tlsv1.3']) ->
|
all_ciphers(['tlsv1.3']) ->
|
||||||
%% When it's only tlsv1.3 wanted, use 'exclusive' here
|
%% When it's only tlsv1.3 wanted, use 'exclusive' here
|
||||||
%% because 'all' returns legacy cipher suites too,
|
%% because 'all' returns legacy cipher suites too,
|
||||||
%% which does not make sense since tlsv1.3 can not use
|
%% which does not make sense since tlsv1.3 can not use
|
||||||
%% legacy cipher suites.
|
%% legacy cipher suites.
|
||||||
ssl:cipher_suites(exclusive, 'tlsv1.3', openssl);
|
ssl:cipher_suites(exclusive, 'tlsv1.3', openssl);
|
||||||
default_ciphers(Versions) ->
|
all_ciphers(Versions) ->
|
||||||
%% assert non-empty
|
%% assert non-empty
|
||||||
[_ | _] = dedup(lists:append([ssl:cipher_suites(all, V, openssl) || V <- Versions])).
|
[_ | _] = dedup(lists:append([ssl:cipher_suites(all, V, openssl) || V <- Versions])).
|
||||||
|
|
||||||
|
|
||||||
|
%% @doc All Pre-selected TLS ciphers.
|
||||||
|
default_ciphers() ->
|
||||||
|
selected_ciphers(available_versions()).
|
||||||
|
|
||||||
|
%% @doc Pre-selected TLS ciphers for given versions..
|
||||||
|
selected_ciphers(Vsns) ->
|
||||||
|
All = all_ciphers(Vsns),
|
||||||
|
dedup(lists:filter(fun(Cipher) -> lists:member(Cipher, All) end,
|
||||||
|
lists:flatmap(fun do_selected_ciphers/1, Vsns))).
|
||||||
|
|
||||||
|
do_selected_ciphers('tlsv1.3') ->
|
||||||
|
case lists:member('tlsv1.3', proplists:get_value(available, ssl:versions())) of
|
||||||
|
true -> ssl:cipher_suites(exclusive, 'tlsv1.3', openssl);
|
||||||
|
false -> []
|
||||||
|
end ++ do_selected_ciphers('tlsv1.2');
|
||||||
|
do_selected_ciphers(_) ->
|
||||||
|
[ "ECDHE-ECDSA-AES256-GCM-SHA384",
|
||||||
|
"ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-AES256-SHA384", "ECDHE-RSA-AES256-SHA384",
|
||||||
|
"ECDHE-ECDSA-DES-CBC3-SHA", "ECDH-ECDSA-AES256-GCM-SHA384", "ECDH-RSA-AES256-GCM-SHA384",
|
||||||
|
"ECDH-ECDSA-AES256-SHA384", "ECDH-RSA-AES256-SHA384", "DHE-DSS-AES256-GCM-SHA384",
|
||||||
|
"DHE-DSS-AES256-SHA256", "AES256-GCM-SHA384", "AES256-SHA256",
|
||||||
|
"ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
|
"ECDHE-ECDSA-AES128-SHA256", "ECDHE-RSA-AES128-SHA256", "ECDH-ECDSA-AES128-GCM-SHA256",
|
||||||
|
"ECDH-RSA-AES128-GCM-SHA256", "ECDH-ECDSA-AES128-SHA256", "ECDH-RSA-AES128-SHA256",
|
||||||
|
"DHE-DSS-AES128-GCM-SHA256", "DHE-DSS-AES128-SHA256", "AES128-GCM-SHA256", "AES128-SHA256",
|
||||||
|
"ECDHE-ECDSA-AES256-SHA", "ECDHE-RSA-AES256-SHA", "DHE-DSS-AES256-SHA",
|
||||||
|
"ECDH-ECDSA-AES256-SHA", "ECDH-RSA-AES256-SHA", "AES256-SHA", "ECDHE-ECDSA-AES128-SHA",
|
||||||
|
"ECDHE-RSA-AES128-SHA", "DHE-DSS-AES128-SHA", "ECDH-ECDSA-AES128-SHA",
|
||||||
|
"ECDH-RSA-AES128-SHA", "AES128-SHA",
|
||||||
|
|
||||||
|
%% psk
|
||||||
|
"RSA-PSK-AES256-GCM-SHA384","RSA-PSK-AES256-CBC-SHA384",
|
||||||
|
"RSA-PSK-AES128-GCM-SHA256","RSA-PSK-AES128-CBC-SHA256",
|
||||||
|
"RSA-PSK-AES256-CBC-SHA","RSA-PSK-AES128-CBC-SHA",
|
||||||
|
"RSA-PSK-DES-CBC3-SHA","RSA-PSK-RC4-SHA"
|
||||||
|
].
|
||||||
|
|
||||||
%% @doc Ensure version & cipher-suites integrity.
|
%% @doc Ensure version & cipher-suites integrity.
|
||||||
-spec integral_ciphers([ssl:tls_version()], binary() | string() | [string()]) -> [string()].
|
-spec integral_ciphers([ssl:tls_version()], binary() | string() | [string()]) -> [string()].
|
||||||
integral_ciphers(Versions, Ciphers) when Ciphers =:= [] orelse Ciphers =:= undefined ->
|
integral_ciphers(Versions, Ciphers) when Ciphers =:= [] orelse Ciphers =:= undefined ->
|
||||||
%% not configured
|
%% not configured
|
||||||
integral_ciphers(Versions, default_ciphers(Versions));
|
integral_ciphers(Versions, selected_ciphers(Versions));
|
||||||
integral_ciphers(Versions, Ciphers) when ?IS_STRING_LIST(Ciphers) ->
|
integral_ciphers(Versions, Ciphers) when ?IS_STRING_LIST(Ciphers) ->
|
||||||
%% ensure tlsv1.3 ciphers if none of them is found in Ciphers
|
%% ensure tlsv1.3 ciphers if none of them is found in Ciphers
|
||||||
dedup(ensure_tls13_cipher(lists:member('tlsv1.3', Versions), Ciphers));
|
dedup(ensure_tls13_cipher(lists:member('tlsv1.3', Versions), Ciphers));
|
||||||
|
|
@ -95,7 +127,7 @@ integral_ciphers(Versions, Ciphers) ->
|
||||||
%% In case tlsv1.3 is present, ensure tlsv1.3 cipher is added if user
|
%% In case tlsv1.3 is present, ensure tlsv1.3 cipher is added if user
|
||||||
%% did not provide it from config --- which is a common mistake
|
%% did not provide it from config --- which is a common mistake
|
||||||
ensure_tls13_cipher(true, Ciphers) ->
|
ensure_tls13_cipher(true, Ciphers) ->
|
||||||
Tls13Ciphers = default_ciphers(['tlsv1.3']),
|
Tls13Ciphers = selected_ciphers(['tlsv1.3']),
|
||||||
case lists:any(fun(C) -> lists:member(C, Tls13Ciphers) end, Ciphers) of
|
case lists:any(fun(C) -> lists:member(C, Tls13Ciphers) end, Ciphers) of
|
||||||
true -> Ciphers;
|
true -> Ciphers;
|
||||||
false -> Tls13Ciphers ++ Ciphers
|
false -> Tls13Ciphers ++ Ciphers
|
||||||
|
|
@ -103,11 +135,17 @@ ensure_tls13_cipher(true, Ciphers) ->
|
||||||
ensure_tls13_cipher(false, Ciphers) ->
|
ensure_tls13_cipher(false, Ciphers) ->
|
||||||
Ciphers.
|
Ciphers.
|
||||||
|
|
||||||
|
%% default ssl versions based on available versions.
|
||||||
|
-spec available_versions() -> [atom()].
|
||||||
|
available_versions() ->
|
||||||
|
OtpRelease = list_to_integer(erlang:system_info(otp_release)),
|
||||||
|
default_versions(OtpRelease).
|
||||||
|
|
||||||
%% tlsv1.3 is available from OTP-22 but we do not want to use until 23.
|
%% tlsv1.3 is available from OTP-22 but we do not want to use until 23.
|
||||||
default_versions(OtpRelease) when OtpRelease >= 23 ->
|
default_versions(OtpRelease) when OtpRelease >= 23 ->
|
||||||
['tlsv1.3' | default_versions(22)];
|
proplists:get_value(available, ssl:versions());
|
||||||
default_versions(_) ->
|
default_versions(_) ->
|
||||||
['tlsv1.2', 'tlsv1.1', tlsv1].
|
lists:delete('tlsv1.3', proplists:get_value(available, ssl:versions())).
|
||||||
|
|
||||||
%% Deduplicate a list without re-ordering the elements.
|
%% Deduplicate a list without re-ordering the elements.
|
||||||
dedup([]) -> [];
|
dedup([]) -> [];
|
||||||
|
|
@ -175,10 +213,12 @@ drop_tls13(SslOpts0) ->
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
all_ciphers() -> all_ciphers(default_versions()).
|
||||||
|
|
||||||
drop_tls13_test() ->
|
drop_tls13_test() ->
|
||||||
Versions = default_versions(),
|
Versions = default_versions(),
|
||||||
?assert(lists:member('tlsv1.3', Versions)),
|
?assert(lists:member('tlsv1.3', Versions)),
|
||||||
Ciphers = default_ciphers(),
|
Ciphers = all_ciphers(),
|
||||||
?assert(has_tlsv13_cipher(Ciphers)),
|
?assert(has_tlsv13_cipher(Ciphers)),
|
||||||
Opts0 = #{versions => Versions, ciphers => Ciphers, other => true},
|
Opts0 = #{versions => Versions, ciphers => Ciphers, other => true},
|
||||||
Opts = drop_tls13(Opts0),
|
Opts = drop_tls13(Opts0),
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,9 @@ t_update_config(Config) when is_list(Config) ->
|
||||||
?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID1})),
|
?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID1})),
|
||||||
?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(Global, ID1)),
|
?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(Global, ID1)),
|
||||||
|
|
||||||
|
?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID2})),
|
||||||
|
?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTHN:lookup_authenticator(Global, ID2)),
|
||||||
|
|
||||||
ListenerID = 'tcp:default',
|
ListenerID = 'tcp:default',
|
||||||
ConfKeyPath = [listeners, tcp, default, authentication],
|
ConfKeyPath = [listeners, tcp, default, authentication],
|
||||||
?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig1})),
|
?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig1})),
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
ssl_opts_dtls_test() ->
|
ssl_opts_dtls_test() ->
|
||||||
Sc = emqx_schema:server_ssl_opts_schema(#{versions => dtls,
|
Sc = emqx_schema:server_ssl_opts_schema(#{versions => dtls_all_available,
|
||||||
ciphers => dtls}, false),
|
ciphers => dtls_all_available}, false),
|
||||||
Checked = validate(Sc, #{<<"versions">> => [<<"dtlsv1.2">>, <<"dtlsv1">>]}),
|
Checked = validate(Sc, #{<<"versions">> => [<<"dtlsv1.2">>, <<"dtlsv1">>]}),
|
||||||
?assertMatch(#{versions := ['dtlsv1.2', 'dtlsv1'],
|
?assertMatch(#{versions := ['dtlsv1.2', 'dtlsv1'],
|
||||||
ciphers := ["ECDHE-ECDSA-AES256-GCM-SHA384" | _]
|
ciphers := ["ECDHE-ECDSA-AES256-GCM-SHA384" | _]
|
||||||
|
|
@ -62,19 +62,14 @@ ssl_opts_cipher_comma_separated_string_test() ->
|
||||||
ssl_opts_tls_psk_test() ->
|
ssl_opts_tls_psk_test() ->
|
||||||
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
||||||
Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>]}),
|
Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>]}),
|
||||||
?assertMatch(#{versions := ['tlsv1.2']}, Checked),
|
?assertMatch(#{versions := ['tlsv1.2']}, Checked).
|
||||||
#{ciphers := Ciphers} = Checked,
|
|
||||||
PskCiphers = emqx_schema:default_ciphers(psk),
|
|
||||||
lists:foreach(fun(Cipher) ->
|
|
||||||
?assert(lists:member(Cipher, Ciphers))
|
|
||||||
end, PskCiphers).
|
|
||||||
|
|
||||||
bad_cipher_test() ->
|
bad_cipher_test() ->
|
||||||
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
||||||
Reason = {bad_ciphers, ["foo"]},
|
Reason = {bad_ciphers, ["foo"]},
|
||||||
?assertThrow({_Sc, [{validation_error, #{reason := Reason}}]},
|
?assertThrow({_Sc, [{validation_error, #{reason := Reason}}]},
|
||||||
[validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>],
|
validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>],
|
||||||
<<"ciphers">> => [<<"foo">>]})]),
|
<<"ciphers">> => [<<"foo">>]})),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
validate(Schema, Data0) ->
|
validate(Schema, Data0) ->
|
||||||
|
|
@ -95,3 +90,10 @@ ciperhs_schema_test() ->
|
||||||
WSc = #{roots => [{ciphers, Sc}]},
|
WSc = #{roots => [{ciphers, Sc}]},
|
||||||
?assertThrow({_, [{validation_error, _}]},
|
?assertThrow({_, [{validation_error, _}]},
|
||||||
hocon_schema:check_plain(WSc, #{<<"ciphers">> => <<"foo,bar">>})).
|
hocon_schema:check_plain(WSc, #{<<"ciphers">> => <<"foo,bar">>})).
|
||||||
|
|
||||||
|
bad_tls_version_test() ->
|
||||||
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
||||||
|
Reason = {unsupported_ssl_versions, [foo]},
|
||||||
|
?assertThrow({_Sc, [{validation_error, #{reason := Reason}}]},
|
||||||
|
validate(Sc, #{<<"versions">> => [<<"foo">>]})),
|
||||||
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
enable => true})).
|
enable => true})).
|
||||||
|
|
||||||
-define(INSTANCE_EXAMPLE_2, maps:merge(?EXAMPLE_2, #{id => <<"password-based:http-server">>,
|
-define(INSTANCE_EXAMPLE_2, maps:merge(?EXAMPLE_2, #{id => <<"password-based:http-server">>,
|
||||||
connect_timeout => 5000,
|
connect_timeout => "5s",
|
||||||
enable_pipelining => true,
|
enable_pipelining => true,
|
||||||
headers => #{
|
headers => #{
|
||||||
<<"accept">> => <<"application/json">>,
|
<<"accept">> => <<"application/json">>,
|
||||||
|
|
@ -102,8 +102,8 @@
|
||||||
},
|
},
|
||||||
max_retries => 5,
|
max_retries => 5,
|
||||||
pool_size => 8,
|
pool_size => 8,
|
||||||
request_timeout => 5000,
|
request_timeout => "5s",
|
||||||
retry_interval => 1000,
|
retry_interval => "1s",
|
||||||
enable => true})).
|
enable => true})).
|
||||||
|
|
||||||
-define(INSTANCE_EXAMPLE_3, maps:merge(?EXAMPLE_3, #{id => <<"jwt">>,
|
-define(INSTANCE_EXAMPLE_3, maps:merge(?EXAMPLE_3, #{id => <<"jwt">>,
|
||||||
|
|
@ -1259,9 +1259,9 @@ definitions() ->
|
||||||
example => <<"SELECT password_hash FROM mqtt_user WHERE username = ${mqtt-username}">>
|
example => <<"SELECT password_hash FROM mqtt_user WHERE username = ${mqtt-username}">>
|
||||||
},
|
},
|
||||||
query_timeout => #{
|
query_timeout => #{
|
||||||
type => integer,
|
type => string,
|
||||||
description => <<"Query timeout, Unit: Milliseconds">>,
|
description => <<"Query timeout">>,
|
||||||
default => 5000
|
default => "5s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1528,16 +1528,16 @@ definitions() ->
|
||||||
type => object
|
type => object
|
||||||
},
|
},
|
||||||
connect_timeout => #{
|
connect_timeout => #{
|
||||||
type => integer,
|
type => string,
|
||||||
default => 5000
|
default => <<"5s">>
|
||||||
},
|
},
|
||||||
max_retries => #{
|
max_retries => #{
|
||||||
type => integer,
|
type => integer,
|
||||||
default => 5
|
default => 5
|
||||||
},
|
},
|
||||||
retry_interval => #{
|
retry_interval => #{
|
||||||
type => integer,
|
type => string,
|
||||||
default => 1000
|
default => <<"1s">>
|
||||||
},
|
},
|
||||||
request_timout => #{
|
request_timout => #{
|
||||||
type => integer,
|
type => integer,
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,8 @@ body(type) -> map();
|
||||||
body(validator) -> [fun check_body/1];
|
body(validator) -> [fun check_body/1];
|
||||||
body(_) -> undefined.
|
body(_) -> undefined.
|
||||||
|
|
||||||
request_timeout(type) -> non_neg_integer();
|
request_timeout(type) -> emqx_schema:duration_ms();
|
||||||
request_timeout(default) -> 5000;
|
request_timeout(default) -> "5s";
|
||||||
request_timeout(_) -> undefined.
|
request_timeout(_) -> undefined.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,8 @@ salt_position(_) -> undefined.
|
||||||
query(type) -> string();
|
query(type) -> string();
|
||||||
query(_) -> undefined.
|
query(_) -> undefined.
|
||||||
|
|
||||||
query_timeout(type) -> integer();
|
query_timeout(type) -> emqx_schema:duration_ms();
|
||||||
query_timeout(default) -> 5000;
|
query_timeout(default) -> "5s";
|
||||||
query_timeout(_) -> undefined.
|
query_timeout(_) -> undefined.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
%%
|
%%
|
||||||
%% -type(ipaddrs() :: {ipaddrs, string()}).
|
%% -type(ipaddrs() :: {ipaddrs, string()}).
|
||||||
%%
|
%%
|
||||||
%% -type(username() :: {username, regex()}).
|
%% -type(username() :: {user | username, string()} | {user | username, {re, regex()}}).
|
||||||
%%
|
%%
|
||||||
%% -type(clientid() :: {clientid, regex()}).
|
%% -type(clientid() :: {client | clientid, string()} | {client | clientid, {re, regex()}}).
|
||||||
%%
|
%%
|
||||||
%% -type(who() :: ipaddr() | ipaddrs() |username() | clientid() |
|
%% -type(who() :: ipaddr() | ipaddrs() |username() | clientid() |
|
||||||
%% {'and', [ipaddr() | ipaddrs()| username() | clientid()]} |
|
%% {'and', [ipaddr() | ipaddrs()| username() | clientid()]} |
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
%%
|
%%
|
||||||
%% -type(permission() :: allow | deny).
|
%% -type(permission() :: allow | deny).
|
||||||
%%
|
%%
|
||||||
%% -type(rule() :: {permission(), who(), access(), topics()}).
|
%% -type(rule() :: {permission(), who(), access(), topics()} | {permission(), all}).
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
{allow, {username, "^dashboard?"}, subscribe, ["$SYS/#"]}.
|
{allow, {username, "^dashboard?"}, subscribe, ["$SYS/#"]}.
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,12 @@ authorization {
|
||||||
# collection: mqtt_authz
|
# collection: mqtt_authz
|
||||||
# selector: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] }
|
# selector: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] }
|
||||||
# },
|
# },
|
||||||
|
{
|
||||||
|
type: built-in-database
|
||||||
|
}
|
||||||
{
|
{
|
||||||
type: file
|
type: file
|
||||||
|
# file is loaded into cache
|
||||||
path: "{{ platform_etc_dir }}/acl.conf"
|
path: "{{ platform_etc_dir }}/acl.conf"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,32 @@
|
||||||
(A =:= all) orelse (A =:= <<"all">>)
|
(A =:= all) orelse (A =:= <<"all">>)
|
||||||
)).
|
)).
|
||||||
|
|
||||||
|
-define(ACL_SHARDED, emqx_acl_sharded).
|
||||||
|
|
||||||
|
-define(ACL_TABLE, emqx_acl).
|
||||||
|
|
||||||
|
%% To save some space, use an integer for label, 0 for 'all', {1, Username} and {2, ClientId}.
|
||||||
|
-define(ACL_TABLE_ALL, 0).
|
||||||
|
-define(ACL_TABLE_USERNAME, 1).
|
||||||
|
-define(ACL_TABLE_CLIENTID, 2).
|
||||||
|
|
||||||
|
-record(emqx_acl, {
|
||||||
|
who :: ?ACL_TABLE_ALL| {?ACL_TABLE_USERNAME, binary()} | {?ACL_TABLE_CLIENTID, binary()},
|
||||||
|
rules :: [ {permission(), action(), emqx_topic:topic()} ]
|
||||||
|
}).
|
||||||
|
|
||||||
-record(authz_metrics, {
|
-record(authz_metrics, {
|
||||||
allow = 'client.authorize.allow',
|
allow = 'client.authorize.allow',
|
||||||
deny = 'client.authorize.deny',
|
deny = 'client.authorize.deny',
|
||||||
ignore = 'client.authorize.ignore'
|
ignore = 'client.authorize.ignore'
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-define(CMD_REPLCAE, replace).
|
||||||
|
-define(CMD_DELETE, delete).
|
||||||
|
-define(CMD_PREPEND, prepend).
|
||||||
|
-define(CMD_APPEND, append).
|
||||||
|
-define(CMD_MOVE, move).
|
||||||
|
|
||||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@
|
||||||
-export([post_config_update/4, pre_config_update/2]).
|
-export([post_config_update/4, pre_config_update/2]).
|
||||||
|
|
||||||
-define(CONF_KEY_PATH, [authorization, sources]).
|
-define(CONF_KEY_PATH, [authorization, sources]).
|
||||||
-define(SOURCE_TYPES, [file, http, mongodb, mysql, postgresql, redis]).
|
|
||||||
|
|
||||||
-spec(register_metrics() -> ok).
|
-spec(register_metrics() -> ok).
|
||||||
register_metrics() ->
|
register_metrics() ->
|
||||||
|
|
@ -50,228 +49,151 @@ init() ->
|
||||||
emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE),
|
emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE),
|
||||||
Sources = emqx:get_config(?CONF_KEY_PATH, []),
|
Sources = emqx:get_config(?CONF_KEY_PATH, []),
|
||||||
ok = check_dup_types(Sources),
|
ok = check_dup_types(Sources),
|
||||||
NSources = [init_source(Source) || Source <- Sources],
|
NSources = init_sources(Sources),
|
||||||
ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NSources]}, -1).
|
ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NSources]}, -1).
|
||||||
|
|
||||||
lookup() ->
|
lookup() ->
|
||||||
{_M, _F, [A]}= find_action_in_hooks(),
|
{_M, _F, [A]}= find_action_in_hooks(),
|
||||||
A.
|
A.
|
||||||
|
|
||||||
lookup(Type) ->
|
lookup(Type) ->
|
||||||
try find_source_by_type(atom(Type), lookup()) of
|
{Source, _Front, _Rear} = take(Type),
|
||||||
{_, Source} -> Source
|
Source.
|
||||||
catch
|
|
||||||
error:Reason -> {error, Reason}
|
|
||||||
end.
|
|
||||||
|
|
||||||
move(Type, Cmd) ->
|
move(Type, Cmd) ->
|
||||||
move(Type, Cmd, #{}).
|
move(Type, Cmd, #{}).
|
||||||
|
|
||||||
move(Type, #{<<"before">> := Before}, Opts) ->
|
move(Type, #{<<"before">> := Before}, Opts) ->
|
||||||
emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}, Opts);
|
emqx:update_config(?CONF_KEY_PATH, {?CMD_MOVE, type(Type), #{<<"before">> => type(Before)}}, Opts);
|
||||||
move(Type, #{<<"after">> := After}, Opts) ->
|
move(Type, #{<<"after">> := After}, Opts) ->
|
||||||
emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}, Opts);
|
emqx:update_config(?CONF_KEY_PATH, {?CMD_MOVE, type(Type), #{<<"after">> => type(After)}}, Opts);
|
||||||
move(Type, Position, Opts) ->
|
move(Type, Position, Opts) ->
|
||||||
emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}, Opts).
|
emqx:update_config(?CONF_KEY_PATH, {?CMD_MOVE, type(Type), Position}, Opts).
|
||||||
|
|
||||||
update(Cmd, Sources) ->
|
update(Cmd, Sources) ->
|
||||||
update(Cmd, Sources, #{}).
|
update(Cmd, Sources, #{}).
|
||||||
|
|
||||||
update({replace_once, Type}, Sources, Opts) ->
|
update({replace, Type}, Sources, Opts) ->
|
||||||
emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}, Opts);
|
emqx:update_config(?CONF_KEY_PATH, {{replace, type(Type)}, Sources}, Opts);
|
||||||
update({delete_once, Type}, Sources, Opts) ->
|
update({delete, Type}, Sources, Opts) ->
|
||||||
emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}, Opts);
|
emqx:update_config(?CONF_KEY_PATH, {{delete, type(Type)}, Sources}, Opts);
|
||||||
update(Cmd, Sources, Opts) ->
|
update(Cmd, Sources, Opts) ->
|
||||||
emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}, Opts).
|
emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}, Opts).
|
||||||
|
|
||||||
pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) ->
|
do_update({?CMD_MOVE, Type, <<"top">>}, Conf) when is_list(Conf) ->
|
||||||
{Index, _} = find_source_by_type(Type),
|
{Source, Front, Rear} = take(Type, Conf),
|
||||||
{List1, List2} = lists:split(Index, Conf),
|
[Source | Front] ++ Rear;
|
||||||
NConf = [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2,
|
do_update({?CMD_MOVE, Type, <<"bottom">>}, Conf) when is_list(Conf) ->
|
||||||
case check_dup_types(NConf) of
|
{Source, Front, Rear} = take(Type, Conf),
|
||||||
ok -> {ok, NConf};
|
Front ++ Rear ++ [Source];
|
||||||
Error -> Error
|
do_update({?CMD_MOVE, Type, #{<<"before">> := Before}}, Conf) when is_list(Conf) ->
|
||||||
end;
|
{S1, Front1, Rear1} = take(Type, Conf),
|
||||||
|
{S2, Front2, Rear2} = take(Before, Front1 ++ Rear1),
|
||||||
pre_config_update({move, Type, <<"bottom">>}, Conf) when is_list(Conf) ->
|
Front2 ++ [S1, S2] ++ Rear2;
|
||||||
{Index, _} = find_source_by_type(Type),
|
do_update({?CMD_MOVE, Type, #{<<"after">> := After}}, Conf) when is_list(Conf) ->
|
||||||
{List1, List2} = lists:split(Index, Conf),
|
{S1, Front1, Rear1} = take(Type, Conf),
|
||||||
NConf = lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)],
|
{S2, Front2, Rear2} = take(After, Front1 ++ Rear1),
|
||||||
case check_dup_types(NConf) of
|
Front2 ++ [S2, S1] ++ Rear2;
|
||||||
ok -> {ok, NConf};
|
do_update({?CMD_PREPEND, Sources}, Conf) when is_list(Sources), is_list(Conf) ->
|
||||||
Error -> Error
|
|
||||||
end;
|
|
||||||
|
|
||||||
pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Conf) ->
|
|
||||||
{Index1, _} = find_source_by_type(Type),
|
|
||||||
Conf1 = lists:nth(Index1, Conf),
|
|
||||||
{Index2, _} = find_source_by_type(Before),
|
|
||||||
Conf2 = lists:nth(Index2, Conf),
|
|
||||||
|
|
||||||
{List1, List2} = lists:split(Index2, Conf),
|
|
||||||
NConf = lists:delete(Conf1, lists:droplast(List1))
|
|
||||||
++ [Conf1] ++ [Conf2]
|
|
||||||
++ lists:delete(Conf1, List2),
|
|
||||||
case check_dup_types(NConf) of
|
|
||||||
ok -> {ok, NConf};
|
|
||||||
Error -> Error
|
|
||||||
end;
|
|
||||||
|
|
||||||
pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf) ->
|
|
||||||
{Index1, _} = find_source_by_type(Type),
|
|
||||||
Conf1 = lists:nth(Index1, Conf),
|
|
||||||
{Index2, _} = find_source_by_type(After),
|
|
||||||
|
|
||||||
{List1, List2} = lists:split(Index2, Conf),
|
|
||||||
NConf = lists:delete(Conf1, List1)
|
|
||||||
++ [Conf1]
|
|
||||||
++ lists:delete(Conf1, List2),
|
|
||||||
case check_dup_types(NConf) of
|
|
||||||
ok -> {ok, NConf};
|
|
||||||
Error -> Error
|
|
||||||
end;
|
|
||||||
|
|
||||||
pre_config_update({head, Sources}, Conf) when is_list(Sources), is_list(Conf) ->
|
|
||||||
NConf = Sources ++ Conf,
|
NConf = Sources ++ Conf,
|
||||||
case check_dup_types(NConf) of
|
ok = check_dup_types(NConf),
|
||||||
ok -> {ok, Sources ++ Conf};
|
NConf;
|
||||||
Error -> Error
|
do_update({?CMD_APPEND, Sources}, Conf) when is_list(Sources), is_list(Conf) ->
|
||||||
end;
|
|
||||||
pre_config_update({tail, Sources}, Conf) when is_list(Sources), is_list(Conf) ->
|
|
||||||
NConf = Conf ++ Sources,
|
NConf = Conf ++ Sources,
|
||||||
case check_dup_types(NConf) of
|
ok = check_dup_types(NConf),
|
||||||
ok -> {ok, Conf ++ Sources};
|
NConf;
|
||||||
Error -> Error
|
do_update({{replace, Type}, Source}, Conf) when is_map(Source), is_list(Conf) ->
|
||||||
end;
|
{_Old, Front, Rear} = take(Type, Conf),
|
||||||
pre_config_update({{replace_once, Type}, Source}, Conf) when is_map(Source), is_list(Conf) ->
|
NConf = Front ++ [Source | Rear],
|
||||||
{Index, _} = find_source_by_type(Type),
|
ok = check_dup_types(NConf),
|
||||||
{List1, List2} = lists:split(Index, Conf),
|
NConf;
|
||||||
NConf = lists:droplast(List1) ++ [Source] ++ List2,
|
do_update({{delete, Type}, _Source}, Conf) when is_list(Conf) ->
|
||||||
case check_dup_types(NConf) of
|
{_Old, Front, Rear} = take(Type, Conf),
|
||||||
ok -> {ok, NConf};
|
NConf = Front ++ Rear,
|
||||||
Error -> Error
|
NConf;
|
||||||
end;
|
do_update({_, Sources}, _Conf) when is_list(Sources)->
|
||||||
pre_config_update({{delete_once, Type}, _Source}, Conf) when is_list(Conf) ->
|
|
||||||
{Index, _} = find_source_by_type(Type),
|
|
||||||
{List1, List2} = lists:split(Index, Conf),
|
|
||||||
NConf = lists:droplast(List1) ++ List2,
|
|
||||||
case check_dup_types(NConf) of
|
|
||||||
ok -> {ok, NConf};
|
|
||||||
Error -> Error
|
|
||||||
end;
|
|
||||||
pre_config_update({_, Sources}, _Conf) when is_list(Sources)->
|
|
||||||
%% overwrite the entire config!
|
%% overwrite the entire config!
|
||||||
{ok, Sources}.
|
Sources.
|
||||||
|
|
||||||
|
pre_config_update(Cmd, Conf) ->
|
||||||
|
{ok, do_update(Cmd, Conf)}.
|
||||||
|
|
||||||
|
|
||||||
post_config_update(_, undefined, _Conf, _AppEnvs) ->
|
post_config_update(_, undefined, _Conf, _AppEnvs) ->
|
||||||
ok;
|
ok;
|
||||||
post_config_update({move, Type, <<"top">>}, _NewSources, _OldSources, _AppEnvs) ->
|
post_config_update(Cmd, NewSources, _OldSource, _AppEnvs) ->
|
||||||
InitedSources = lookup(),
|
ok = do_post_update(Cmd, NewSources),
|
||||||
{Index, Source} = find_source_by_type(Type, InitedSources),
|
|
||||||
{Sources1, Sources2 } = lists:split(Index, InitedSources),
|
|
||||||
Sources3 = [Source] ++ lists:droplast(Sources1) ++ Sources2,
|
|
||||||
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1),
|
|
||||||
ok = emqx_authz_cache:drain_cache();
|
|
||||||
post_config_update({move, Type, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) ->
|
|
||||||
InitedSources = lookup(),
|
|
||||||
{Index, Source} = find_source_by_type(Type, InitedSources),
|
|
||||||
{Sources1, Sources2 } = lists:split(Index, InitedSources),
|
|
||||||
Sources3 = lists:droplast(Sources1) ++ Sources2 ++ [Source],
|
|
||||||
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1),
|
|
||||||
ok = emqx_authz_cache:drain_cache();
|
|
||||||
post_config_update({move, Type, #{<<"before">> := Before}}, _NewSources, _OldSources, _AppEnvs) ->
|
|
||||||
InitedSources = lookup(),
|
|
||||||
{_, Source0} = find_source_by_type(Type, InitedSources),
|
|
||||||
{Index, Source1} = find_source_by_type(Before, InitedSources),
|
|
||||||
{Sources1, Sources2} = lists:split(Index, InitedSources),
|
|
||||||
Sources3 = lists:delete(Source0, lists:droplast(Sources1))
|
|
||||||
++ [Source0] ++ [Source1]
|
|
||||||
++ lists:delete(Source0, Sources2),
|
|
||||||
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1),
|
|
||||||
ok = emqx_authz_cache:drain_cache();
|
|
||||||
|
|
||||||
post_config_update({move, Type, #{<<"after">> := After}}, _NewSources, _OldSources, _AppEnvs) ->
|
|
||||||
InitedSources = lookup(),
|
|
||||||
{_, Source} = find_source_by_type(Type, InitedSources),
|
|
||||||
{Index, _} = find_source_by_type(After, InitedSources),
|
|
||||||
{Sources1, Sources2} = lists:split(Index, InitedSources),
|
|
||||||
Sources3 = lists:delete(Source, Sources1)
|
|
||||||
++ [Source]
|
|
||||||
++ lists:delete(Source, Sources2),
|
|
||||||
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1),
|
|
||||||
ok = emqx_authz_cache:drain_cache();
|
|
||||||
|
|
||||||
post_config_update({head, Sources}, _NewSources, _OldConf, _AppEnvs) ->
|
|
||||||
InitedSources = [init_source(R) || R <- check_sources(Sources)],
|
|
||||||
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedSources ++ lookup()]}, -1),
|
|
||||||
ok = emqx_authz_cache:drain_cache();
|
|
||||||
|
|
||||||
post_config_update({tail, Sources}, _NewSources, _OldConf, _AppEnvs) ->
|
|
||||||
InitedSources = [init_source(R) || R <- check_sources(Sources)],
|
|
||||||
emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedSources]}, -1),
|
|
||||||
ok = emqx_authz_cache:drain_cache();
|
|
||||||
|
|
||||||
post_config_update({{replace_once, Type}, #{type := Type} = Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) ->
|
|
||||||
OldInitedSources = lookup(),
|
|
||||||
{Index, OldSource} = find_source_by_type(Type, OldInitedSources),
|
|
||||||
case maps:get(type, OldSource, undefined) of
|
|
||||||
undefined -> ok;
|
|
||||||
file -> ok;
|
|
||||||
_ ->
|
|
||||||
#{annotations := #{id := Id}} = OldSource,
|
|
||||||
ok = emqx_resource:remove(Id)
|
|
||||||
end,
|
|
||||||
{OldSources1, OldSources2 } = lists:split(Index, OldInitedSources),
|
|
||||||
InitedSources = [init_source(R) || R <- check_sources([Source])],
|
|
||||||
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldSources1) ++ InitedSources ++ OldSources2]}, -1),
|
|
||||||
ok = emqx_authz_cache:drain_cache();
|
|
||||||
post_config_update({{delete_once, Type}, _Source}, _NewSources, _OldConf, _AppEnvs) ->
|
|
||||||
OldInitedSources = lookup(),
|
|
||||||
{_, OldSource} = find_source_by_type(Type, OldInitedSources),
|
|
||||||
case OldSource of
|
|
||||||
#{annotations := #{id := Id}} ->
|
|
||||||
ok = emqx_resource:remove(Id);
|
|
||||||
_ -> ok
|
|
||||||
end,
|
|
||||||
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:delete(OldSource, OldInitedSources)]}, -1),
|
|
||||||
ok = emqx_authz_cache:drain_cache();
|
|
||||||
post_config_update(_, NewSources, _OldConf, _AppEnvs) ->
|
|
||||||
%% overwrite the entire config!
|
|
||||||
OldInitedSources = lookup(),
|
|
||||||
InitedSources = [init_source(Source) || Source <- NewSources],
|
|
||||||
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedSources]}, -1),
|
|
||||||
lists:foreach(fun (#{type := _Type, enable := true, annotations := #{id := Id}}) ->
|
|
||||||
ok = emqx_resource:remove(Id);
|
|
||||||
(_) -> ok
|
|
||||||
end, OldInitedSources),
|
|
||||||
ok = emqx_authz_cache:drain_cache().
|
ok = emqx_authz_cache:drain_cache().
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
do_post_update({?CMD_MOVE, _Type, _Where} = Cmd, _NewSources) ->
|
||||||
%% Initialize source
|
InitedSources = lookup(),
|
||||||
%%--------------------------------------------------------------------
|
MovedSources = do_update(Cmd, InitedSources),
|
||||||
|
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [MovedSources]}, -1),
|
||||||
|
ok = emqx_authz_cache:drain_cache();
|
||||||
|
do_post_update({?CMD_PREPEND, Sources}, _NewSources) ->
|
||||||
|
InitedSources = init_sources(check_sources(Sources)),
|
||||||
|
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedSources ++ lookup()]}, -1),
|
||||||
|
ok = emqx_authz_cache:drain_cache();
|
||||||
|
do_post_update({?CMD_APPEND, Sources}, _NewSources) ->
|
||||||
|
InitedSources = init_sources(check_sources(Sources)),
|
||||||
|
emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedSources]}, -1),
|
||||||
|
ok = emqx_authz_cache:drain_cache();
|
||||||
|
do_post_update({{replace, Type}, #{type := Type} = Source}, _NewSources) when is_map(Source) ->
|
||||||
|
OldInitedSources = lookup(),
|
||||||
|
{OldSource, Front, Rear} = take(Type, OldInitedSources),
|
||||||
|
ok = ensure_resource_deleted(OldSource),
|
||||||
|
InitedSources = init_sources(check_sources([Source])),
|
||||||
|
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Front ++ InitedSources ++ Rear]}, -1),
|
||||||
|
ok = emqx_authz_cache:drain_cache();
|
||||||
|
do_post_update({{delete, Type}, _Source}, _NewSources) ->
|
||||||
|
OldInitedSources = lookup(),
|
||||||
|
{OldSource, Front, Rear} = take(Type, OldInitedSources),
|
||||||
|
ok = ensure_resource_deleted(OldSource),
|
||||||
|
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, Front ++ Rear}, -1),
|
||||||
|
ok = emqx_authz_cache:drain_cache();
|
||||||
|
do_post_update(_, NewSources) ->
|
||||||
|
%% overwrite the entire config!
|
||||||
|
OldInitedSources = lookup(),
|
||||||
|
InitedSources = init_sources(NewSources),
|
||||||
|
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedSources]}, -1),
|
||||||
|
lists:foreach(fun ensure_resource_deleted/1, OldInitedSources),
|
||||||
|
ok = emqx_authz_cache:drain_cache().
|
||||||
|
|
||||||
|
ensure_resource_deleted(#{type := file}) -> ok;
|
||||||
|
ensure_resource_deleted(#{type := 'built-in-database'}) -> ok;
|
||||||
|
ensure_resource_deleted(#{annotations := #{id := Id}}) -> ok = emqx_resource:remove(Id).
|
||||||
|
|
||||||
check_dup_types(Sources) ->
|
check_dup_types(Sources) ->
|
||||||
check_dup_types(Sources, ?SOURCE_TYPES).
|
check_dup_types(Sources, []).
|
||||||
check_dup_types(_Sources, []) -> ok;
|
|
||||||
check_dup_types(Sources, [T0 | Tail]) ->
|
check_dup_types([], _Checked) -> ok;
|
||||||
case lists:foldl(fun (#{type := T1}, AccIn) ->
|
check_dup_types([Source | Sources], Checked) ->
|
||||||
case T0 =:= T1 of
|
%% the input might be raw or type-checked result, so lookup both 'type' and <<"type">>
|
||||||
true -> AccIn + 1;
|
%% TODO: check: really?
|
||||||
false -> AccIn
|
Type = case maps:get(<<"type">>, Source, maps:get(type, Source, undefined)) of
|
||||||
end;
|
undefined ->
|
||||||
(#{<<"type">> := T1}, AccIn) ->
|
%% this should never happen if the value is type checked by honcon schema
|
||||||
case T0 =:= atom(T1) of
|
error({bad_source_input, Source});
|
||||||
true -> AccIn + 1;
|
Type0 ->
|
||||||
false -> AccIn
|
type(Type0)
|
||||||
end
|
end,
|
||||||
end, 0, Sources) > 1 of
|
case lists:member(Type, Checked) of
|
||||||
true ->
|
true ->
|
||||||
?LOG(error, "The type is duplicated in the Authorization source"),
|
%% we have made it clear not to support more than one authz instance for each type
|
||||||
{error, 'The type is duplicated in the Authorization source'};
|
error({duplicated_authz_source_type, Type});
|
||||||
false -> check_dup_types(Sources, Tail)
|
false ->
|
||||||
|
check_dup_types(Sources, [Type | Checked])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
init_source(#{enable := true,
|
init_sources(Sources) ->
|
||||||
type := file,
|
{Enabled, Disabled} = lists:partition(fun(#{enable := Enable}) -> Enable end, Sources),
|
||||||
|
case Disabled =/= [] of
|
||||||
|
true -> ?SLOG(info, #{msg => "disabled_sources_ignored", sources => Disabled});
|
||||||
|
false -> ok
|
||||||
|
end,
|
||||||
|
lists:map(fun init_source/1, Enabled).
|
||||||
|
|
||||||
|
init_source(#{type := file,
|
||||||
path := Path
|
path := Path
|
||||||
} = Source) ->
|
} = Source) ->
|
||||||
Rules = case file:consult(Path) of
|
Rules = case file:consult(Path) of
|
||||||
|
|
@ -288,8 +210,7 @@ init_source(#{enable := true,
|
||||||
error(Reason)
|
error(Reason)
|
||||||
end,
|
end,
|
||||||
Source#{annotations => #{rules => Rules}};
|
Source#{annotations => #{rules => Rules}};
|
||||||
init_source(#{enable := true,
|
init_source(#{type := http,
|
||||||
type := http,
|
|
||||||
url := Url
|
url := Url
|
||||||
} = Source) ->
|
} = Source) ->
|
||||||
NSource= maps:put(base_url, maps:remove(query, Url), Source),
|
NSource= maps:put(base_url, maps:remove(query, Url), Source),
|
||||||
|
|
@ -297,16 +218,17 @@ init_source(#{enable := true,
|
||||||
{error, Reason} -> error({load_config_error, Reason});
|
{error, Reason} -> error({load_config_error, Reason});
|
||||||
Id -> Source#{annotations => #{id => Id}}
|
Id -> Source#{annotations => #{id => Id}}
|
||||||
end;
|
end;
|
||||||
init_source(#{enable := true,
|
init_source(#{type := 'built-in-database'
|
||||||
type := DB
|
} = Source) ->
|
||||||
|
Source;
|
||||||
|
init_source(#{type := DB
|
||||||
} = Source) when DB =:= redis;
|
} = Source) when DB =:= redis;
|
||||||
DB =:= mongodb ->
|
DB =:= mongodb ->
|
||||||
case create_resource(Source) of
|
case create_resource(Source) of
|
||||||
{error, Reason} -> error({load_config_error, Reason});
|
{error, Reason} -> error({load_config_error, Reason});
|
||||||
Id -> Source#{annotations => #{id => Id}}
|
Id -> Source#{annotations => #{id => Id}}
|
||||||
end;
|
end;
|
||||||
init_source(#{enable := true,
|
init_source(#{type := DB,
|
||||||
type := DB,
|
|
||||||
query := SQL
|
query := SQL
|
||||||
} = Source) when DB =:= mysql;
|
} = Source) when DB =:= mysql;
|
||||||
DB =:= postgresql ->
|
DB =:= postgresql ->
|
||||||
|
|
@ -318,8 +240,7 @@ init_source(#{enable := true,
|
||||||
query => Mod:parse_query(SQL)
|
query => Mod:parse_query(SQL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end;
|
end.
|
||||||
init_source(#{enable := false} = Source) ->Source.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% AuthZ callbacks
|
%% AuthZ callbacks
|
||||||
|
|
@ -373,13 +294,17 @@ check_sources(RawSources) ->
|
||||||
#{sources := Sources} = hocon_schema:check_plain(Schema, Conf, #{atom_key => true}),
|
#{sources := Sources} = hocon_schema:check_plain(Schema, Conf, #{atom_key => true}),
|
||||||
Sources.
|
Sources.
|
||||||
|
|
||||||
find_source_by_type(Type) -> find_source_by_type(Type, lookup()).
|
take(Type) -> take(Type, lookup()).
|
||||||
find_source_by_type(Type, Sources) -> find_source_by_type(Type, Sources, 1).
|
|
||||||
find_source_by_type(_, [], _N) -> error(not_found_source);
|
%% Take the source of give type, the sources list is split into two parts
|
||||||
find_source_by_type(Type, [ Source = #{type := T} | Tail], N) ->
|
%% front part and rear part.
|
||||||
case Type =:= T of
|
take(Type, Sources) ->
|
||||||
true -> {N, Source};
|
{Front, Rear} = lists:splitwith(fun(T) -> type(T) =/= type(Type) end, Sources),
|
||||||
false -> find_source_by_type(Type, Tail, N + 1)
|
case Rear =:= [] of
|
||||||
|
true ->
|
||||||
|
error({authz_source_of_type_not_found, Type});
|
||||||
|
_ ->
|
||||||
|
{hd(Rear), Front, tl(Rear)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
find_action_in_hooks() ->
|
find_action_in_hooks() ->
|
||||||
|
|
@ -404,6 +329,8 @@ create_resource(#{type := DB} = Source) ->
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
authz_module('built-in-database') ->
|
||||||
|
emqx_authz_mnesia;
|
||||||
authz_module(Type) ->
|
authz_module(Type) ->
|
||||||
list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)).
|
list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)).
|
||||||
|
|
||||||
|
|
@ -414,9 +341,20 @@ connector_module(postgresql) ->
|
||||||
connector_module(Type) ->
|
connector_module(Type) ->
|
||||||
list_to_existing_atom("emqx_connector_" ++ atom_to_list(Type)).
|
list_to_existing_atom("emqx_connector_" ++ atom_to_list(Type)).
|
||||||
|
|
||||||
atom(B) when is_binary(B) ->
|
type(#{type := Type}) -> type(Type);
|
||||||
try binary_to_existing_atom(B, utf8)
|
type(#{<<"type">> := Type}) -> type(Type);
|
||||||
catch
|
type(file) -> file;
|
||||||
_ -> binary_to_atom(B)
|
type(<<"file">>) -> file;
|
||||||
end;
|
type(http) -> http;
|
||||||
atom(A) when is_atom(A) -> A.
|
type(<<"http">>) -> http;
|
||||||
|
type(mongodb) -> mongodb;
|
||||||
|
type(<<"mongodb">>) -> mongodb;
|
||||||
|
type(mysql) -> mysql;
|
||||||
|
type(<<"mysql">>) -> mysql;
|
||||||
|
type(redis) -> redis;
|
||||||
|
type(<<"redis">>) -> redis;
|
||||||
|
type(postgresql) -> postgresql;
|
||||||
|
type(<<"postgresql">>) -> postgresql;
|
||||||
|
type('built-in-database') -> 'built-in-database';
|
||||||
|
type(<<"built-in-database">>) -> 'built-in-database';
|
||||||
|
type(Unknown) -> error({unknown_authz_source_type, Unknown}). % should never happend if the input is type-checked by hocon schema
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,590 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_authz_api_mnesia).
|
||||||
|
|
||||||
|
-behavior(minirest_api).
|
||||||
|
|
||||||
|
-include("emqx_authz.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
|
|
||||||
|
-define(EXAMPLE_USERNAME, #{username => user1,
|
||||||
|
rules => [ #{topic => <<"test/toopic/1">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"publish">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"test/toopic/2">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"eq test/#">>,
|
||||||
|
permission => <<"deny">>,
|
||||||
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).
|
||||||
|
-define(EXAMPLE_CLIENTID, #{clientid => client1,
|
||||||
|
rules => [ #{topic => <<"test/toopic/1">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"publish">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"test/toopic/2">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"eq test/#">>,
|
||||||
|
permission => <<"deny">>,
|
||||||
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).
|
||||||
|
-define(EXAMPLE_ALL , #{rules => [ #{topic => <<"test/toopic/1">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"publish">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"test/toopic/2">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"eq test/#">>,
|
||||||
|
permission => <<"deny">>,
|
||||||
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).
|
||||||
|
|
||||||
|
-export([ api_spec/0
|
||||||
|
, purge/2
|
||||||
|
, records/2
|
||||||
|
, record/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
api_spec() ->
|
||||||
|
{[ purge_api()
|
||||||
|
, records_api()
|
||||||
|
, record_api()
|
||||||
|
], definitions()}.
|
||||||
|
|
||||||
|
definitions() ->
|
||||||
|
Rules = #{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
type => object,
|
||||||
|
required => [topic, permission, action],
|
||||||
|
properties => #{
|
||||||
|
topic => #{
|
||||||
|
type => string,
|
||||||
|
example => <<"test/topic/1">>
|
||||||
|
},
|
||||||
|
permission => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"allow">>, <<"deny">>],
|
||||||
|
example => <<"allow">>
|
||||||
|
},
|
||||||
|
action => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"publish">>, <<"subscribe">>, <<"all">>],
|
||||||
|
example => <<"publish">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Username = #{
|
||||||
|
type => object,
|
||||||
|
required => [username, rules],
|
||||||
|
properties => #{
|
||||||
|
username => #{
|
||||||
|
type => string,
|
||||||
|
example => <<"username">>
|
||||||
|
},
|
||||||
|
rules => minirest:ref(<<"rules">>)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Clientid = #{
|
||||||
|
type => object,
|
||||||
|
required => [clientid, rules],
|
||||||
|
properties => #{
|
||||||
|
clientid => #{
|
||||||
|
type => string,
|
||||||
|
example => <<"clientid">>
|
||||||
|
},
|
||||||
|
rules => minirest:ref(<<"rules">>)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ALL = #{
|
||||||
|
type => object,
|
||||||
|
required => [rules],
|
||||||
|
properties => #{
|
||||||
|
rules => minirest:ref(<<"rules">>)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ #{<<"rules">> => Rules}
|
||||||
|
, #{<<"username">> => Username}
|
||||||
|
, #{<<"clientid">> => Clientid}
|
||||||
|
, #{<<"all">> => ALL}
|
||||||
|
].
|
||||||
|
|
||||||
|
purge_api() ->
|
||||||
|
Metadata = #{
|
||||||
|
delete => #{
|
||||||
|
description => "Purge all records",
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"No Content">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"/authorization/sources/built-in-database/purge-all", Metadata, purge}.
|
||||||
|
|
||||||
|
records_api() ->
|
||||||
|
Metadata = #{
|
||||||
|
get => #{
|
||||||
|
description => "List records",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => type,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"username">>, <<"clientid">>, <<"all">>]
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
name => page,
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
description => <<"Page Index">>,
|
||||||
|
schema => #{type => integer}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
name => limit,
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
description => <<"Page limit">>,
|
||||||
|
schema => #{type => integer}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses => #{
|
||||||
|
<<"200">> => #{
|
||||||
|
description => <<"OK">>,
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => #{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
oneOf => [ minirest:ref(<<"username">>)
|
||||||
|
, minirest:ref(<<"clientid">>)
|
||||||
|
, minirest:ref(<<"all">>)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_USERNAME])
|
||||||
|
},
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_CLIENTID])
|
||||||
|
},
|
||||||
|
all => #{
|
||||||
|
summary => <<"All">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_ALL])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
post => #{
|
||||||
|
description => "Add new records",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => type,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"username">>, <<"clientid">>]
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requestBody => #{
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => #{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
oneOf => [ minirest:ref(<<"username">>)
|
||||||
|
, minirest:ref(<<"clientid">>)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_USERNAME])
|
||||||
|
},
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_CLIENTID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"Created">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
put => #{
|
||||||
|
description => "Set the list of rules for all",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => type,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"all">>]
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requestBody => #{
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => #{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
oneOf => [ minirest:ref(<<"username">>)
|
||||||
|
, minirest:ref(<<"clientid">>)
|
||||||
|
, minirest:ref(<<"all">>)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_USERNAME)
|
||||||
|
},
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_CLIENTID)
|
||||||
|
},
|
||||||
|
all => #{
|
||||||
|
summary => <<"All">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_ALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"Created">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"/authorization/sources/built-in-database/:type", Metadata, records}.
|
||||||
|
|
||||||
|
record_api() ->
|
||||||
|
Metadata = #{
|
||||||
|
get => #{
|
||||||
|
description => "Get record info",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => type,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"username">>, <<"clientid">>]
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
name => key,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses => #{
|
||||||
|
<<"200">> => #{
|
||||||
|
description => <<"OK">>,
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => #{
|
||||||
|
oneOf => [ minirest:ref(<<"username">>)
|
||||||
|
, minirest:ref(<<"clientid">>)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_USERNAME)
|
||||||
|
},
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_CLIENTID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
<<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
put => #{
|
||||||
|
description => "Update one record",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => type,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"username">>, <<"clientid">>]
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
name => key,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requestBody => #{
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => #{
|
||||||
|
oneOf => [ minirest:ref(<<"username">>)
|
||||||
|
, minirest:ref(<<"clientid">>)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_USERNAME)
|
||||||
|
},
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_CLIENTID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"Updated">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete => #{
|
||||||
|
description => "Delete one record",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => type,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"username">>, <<"clientid">>]
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
name => key,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"No Content">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"/authorization/sources/built-in-database/:type/:key", Metadata, record}.
|
||||||
|
|
||||||
|
purge(delete, _) ->
|
||||||
|
case emqx_authz_api_sources:get_raw_source(<<"built-in-database">>) of
|
||||||
|
[#{enable := false}] ->
|
||||||
|
ok = lists:foreach(fun(Key) ->
|
||||||
|
ok = ekka_mnesia:dirty_delete(?ACL_TABLE, Key)
|
||||||
|
end, mnesia:dirty_all_keys(?ACL_TABLE)),
|
||||||
|
{204};
|
||||||
|
_ ->
|
||||||
|
{400, #{code => <<"BAD_REQUEST">>,
|
||||||
|
message => <<"'built-in-database' type source must be disabled before purge.">>}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
records(get, #{bindings := #{type := <<"username">>},
|
||||||
|
query_string := Qs
|
||||||
|
}) ->
|
||||||
|
MatchSpec = ets:fun2ms(
|
||||||
|
fun({?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}, Rules}) ->
|
||||||
|
[{username, Username}, {rules, Rules}]
|
||||||
|
end),
|
||||||
|
Format = fun ([{username, Username}, {rules, Rules}]) ->
|
||||||
|
#{username => Username,
|
||||||
|
rules => [ #{topic => Topic,
|
||||||
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
} || {Permission, Action, Topic} <- Rules]
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
case Qs of
|
||||||
|
#{<<"limit">> := _, <<"page">> := _} = Page ->
|
||||||
|
{200, emqx_mgmt_api:paginate(?ACL_TABLE, MatchSpec, Page, Format)};
|
||||||
|
#{<<"limit">> := Limit} ->
|
||||||
|
case ets:select(?ACL_TABLE, MatchSpec, binary_to_integer(Limit)) of
|
||||||
|
{Rows, _Continuation} -> {200, [Format(Row) || Row <- Rows ]};
|
||||||
|
'$end_of_table' -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{200, [Format(Row) || Row <- ets:select(?ACL_TABLE, MatchSpec)]}
|
||||||
|
end;
|
||||||
|
|
||||||
|
records(get, #{bindings := #{type := <<"clientid">>},
|
||||||
|
query_string := Qs
|
||||||
|
}) ->
|
||||||
|
MatchSpec = ets:fun2ms(
|
||||||
|
fun({?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}, Rules}) ->
|
||||||
|
[{clientid, Clientid}, {rules, Rules}]
|
||||||
|
end),
|
||||||
|
Format = fun ([{clientid, Clientid}, {rules, Rules}]) ->
|
||||||
|
#{clientid => Clientid,
|
||||||
|
rules => [ #{topic => Topic,
|
||||||
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
} || {Permission, Action, Topic} <- Rules]
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
case Qs of
|
||||||
|
#{<<"limit">> := _, <<"page">> := _} = Page ->
|
||||||
|
{200, emqx_mgmt_api:paginate(?ACL_TABLE, MatchSpec, Page, Format)};
|
||||||
|
#{<<"limit">> := Limit} ->
|
||||||
|
case ets:select(?ACL_TABLE, MatchSpec, binary_to_integer(Limit)) of
|
||||||
|
{Rows, _Continuation} -> {200, [Format(Row) || Row <- Rows ]};
|
||||||
|
'$end_of_table' -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{200, [Format(Row) || Row <- ets:select(?ACL_TABLE, MatchSpec)]}
|
||||||
|
end;
|
||||||
|
records(get, #{bindings := #{type := <<"all">>}}) ->
|
||||||
|
MatchSpec = ets:fun2ms(
|
||||||
|
fun({?ACL_TABLE, ?ACL_TABLE_ALL, Rules}) ->
|
||||||
|
[{rules, Rules}]
|
||||||
|
end),
|
||||||
|
{200, [ #{rules => [ #{topic => Topic,
|
||||||
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
} || {Permission, Action, Topic} <- Rules]
|
||||||
|
} || [{rules, Rules}] <- ets:select(?ACL_TABLE, MatchSpec)]};
|
||||||
|
records(post, #{bindings := #{type := <<"username">>},
|
||||||
|
body := Body}) when is_list(Body) ->
|
||||||
|
lists:foreach(fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
|
||||||
|
ekka_mnesia:dirty_write(#emqx_acl{
|
||||||
|
who = {?ACL_TABLE_USERNAME, Username},
|
||||||
|
rules = format_rules(Rules)
|
||||||
|
})
|
||||||
|
end, Body),
|
||||||
|
{204};
|
||||||
|
records(post, #{bindings := #{type := <<"clientid">>},
|
||||||
|
body := Body}) when is_list(Body) ->
|
||||||
|
lists:foreach(fun(#{<<"clientid">> := Clientid, <<"rules">> := Rules}) ->
|
||||||
|
ekka_mnesia:dirty_write(#emqx_acl{
|
||||||
|
who = {?ACL_TABLE_CLIENTID, Clientid},
|
||||||
|
rules = format_rules(Rules)
|
||||||
|
})
|
||||||
|
end, Body),
|
||||||
|
{204};
|
||||||
|
records(put, #{bindings := #{type := <<"all">>},
|
||||||
|
body := #{<<"rules">> := Rules}}) ->
|
||||||
|
ekka_mnesia:dirty_write(#emqx_acl{
|
||||||
|
who = ?ACL_TABLE_ALL,
|
||||||
|
rules = format_rules(Rules)
|
||||||
|
}),
|
||||||
|
{204}.
|
||||||
|
|
||||||
|
record(get, #{bindings := #{type := <<"username">>, key := Key}}) ->
|
||||||
|
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Key}) of
|
||||||
|
[] -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
||||||
|
[#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}] ->
|
||||||
|
{200, #{username => Username,
|
||||||
|
rules => [ #{topic => Topic,
|
||||||
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
} || {Permission, Action, Topic} <- Rules]}
|
||||||
|
}
|
||||||
|
end;
|
||||||
|
record(get, #{bindings := #{type := <<"clientid">>, key := Key}}) ->
|
||||||
|
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Key}) of
|
||||||
|
[] -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
||||||
|
[#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}] ->
|
||||||
|
{200, #{clientid => Clientid,
|
||||||
|
rules => [ #{topic => Topic,
|
||||||
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
} || {Permission, Action, Topic} <- Rules]}
|
||||||
|
}
|
||||||
|
end;
|
||||||
|
record(put, #{bindings := #{type := <<"username">>, key := Username},
|
||||||
|
body := #{<<"username">> := Username, <<"rules">> := Rules}}) ->
|
||||||
|
ekka_mnesia:dirty_write(#emqx_acl{
|
||||||
|
who = {?ACL_TABLE_USERNAME, Username},
|
||||||
|
rules = format_rules(Rules)
|
||||||
|
}),
|
||||||
|
{204};
|
||||||
|
record(put, #{bindings := #{type := <<"clientid">>, key := Clientid},
|
||||||
|
body := #{<<"clientid">> := Clientid, <<"rules">> := Rules}}) ->
|
||||||
|
ekka_mnesia:dirty_write(#emqx_acl{
|
||||||
|
who = {?ACL_TABLE_CLIENTID, Clientid},
|
||||||
|
rules = format_rules(Rules)
|
||||||
|
}),
|
||||||
|
{204};
|
||||||
|
record(delete, #{bindings := #{type := <<"username">>, key := Key}}) ->
|
||||||
|
ekka_mnesia:dirty_delete({?ACL_TABLE, {?ACL_TABLE_USERNAME, Key}}),
|
||||||
|
{204};
|
||||||
|
record(delete, #{bindings := #{type := <<"clientid">>, key := Key}}) ->
|
||||||
|
ekka_mnesia:dirty_delete({?ACL_TABLE, {?ACL_TABLE_CLIENTID, Key}}),
|
||||||
|
{204}.
|
||||||
|
|
||||||
|
format_rules(Rules) when is_list(Rules) ->
|
||||||
|
lists:foldl(fun(#{<<"topic">> := Topic,
|
||||||
|
<<"action">> := Action,
|
||||||
|
<<"permission">> := Permission
|
||||||
|
}, AccIn) when ?PUBSUB(Action)
|
||||||
|
andalso ?ALLOW_DENY(Permission) ->
|
||||||
|
AccIn ++ [{ atom(Permission), atom(Action), Topic }]
|
||||||
|
end, [], Rules).
|
||||||
|
|
||||||
|
atom(B) when is_binary(B) ->
|
||||||
|
try binary_to_existing_atom(B, utf8)
|
||||||
|
catch
|
||||||
|
_ -> binary_to_atom(B)
|
||||||
|
end;
|
||||||
|
atom(A) when is_atom(A) -> A.
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
definitions() ->
|
definitions() ->
|
||||||
Sources = #{
|
Sources = #{
|
||||||
oneOf => [ minirest:ref(<<"http">>)
|
oneOf => [ minirest:ref(<<"http">>)
|
||||||
|
, minirest:ref(<<"built-in-database">>)
|
||||||
, minirest:ref(<<"mongo_single">>)
|
, minirest:ref(<<"mongo_single">>)
|
||||||
, minirest:ref(<<"mongo_rs">>)
|
, minirest:ref(<<"mongo_rs">>)
|
||||||
, minirest:ref(<<"mongo_sharded">>)
|
, minirest:ref(<<"mongo_sharded">>)
|
||||||
|
|
@ -79,9 +80,9 @@ definitions() ->
|
||||||
},
|
},
|
||||||
headers => #{type => object},
|
headers => #{type => object},
|
||||||
body => #{type => object},
|
body => #{type => object},
|
||||||
connect_timeout => #{type => integer},
|
connect_timeout => #{type => string},
|
||||||
max_retries => #{type => integer},
|
max_retries => #{type => integer},
|
||||||
retry_interval => #{type => integer},
|
retry_interval => #{type => string},
|
||||||
pool_type => #{
|
pool_type => #{
|
||||||
type => string,
|
type => string,
|
||||||
enum => [<<"random">>, <<"hash">>],
|
enum => [<<"random">>, <<"hash">>],
|
||||||
|
|
@ -133,8 +134,8 @@ definitions() ->
|
||||||
properties => #{
|
properties => #{
|
||||||
pool_size => #{type => integer},
|
pool_size => #{type => integer},
|
||||||
max_overflow => #{type => integer},
|
max_overflow => #{type => integer},
|
||||||
overflow_ttl => #{type => integer},
|
overflow_ttl => #{type => string},
|
||||||
overflow_check_period => #{type => integer},
|
overflow_check_period => #{type => string},
|
||||||
local_threshold_ms => #{type => integer},
|
local_threshold_ms => #{type => integer},
|
||||||
connect_timeout_ms => #{type => integer},
|
connect_timeout_ms => #{type => integer},
|
||||||
socket_timeout_ms => #{type => integer},
|
socket_timeout_ms => #{type => integer},
|
||||||
|
|
@ -191,8 +192,8 @@ definitions() ->
|
||||||
properties => #{
|
properties => #{
|
||||||
pool_size => #{type => integer},
|
pool_size => #{type => integer},
|
||||||
max_overflow => #{type => integer},
|
max_overflow => #{type => integer},
|
||||||
overflow_ttl => #{type => integer},
|
overflow_ttl => #{type => string},
|
||||||
overflow_check_period => #{type => integer},
|
overflow_check_period => #{type => string},
|
||||||
local_threshold_ms => #{type => integer},
|
local_threshold_ms => #{type => integer},
|
||||||
connect_timeout_ms => #{type => integer},
|
connect_timeout_ms => #{type => integer},
|
||||||
socket_timeout_ms => #{type => integer},
|
socket_timeout_ms => #{type => integer},
|
||||||
|
|
@ -247,8 +248,8 @@ definitions() ->
|
||||||
properties => #{
|
properties => #{
|
||||||
pool_size => #{type => integer},
|
pool_size => #{type => integer},
|
||||||
max_overflow => #{type => integer},
|
max_overflow => #{type => integer},
|
||||||
overflow_ttl => #{type => integer},
|
overflow_ttl => #{type => string},
|
||||||
overflow_check_period => #{type => integer},
|
overflow_check_period => #{type => string},
|
||||||
local_threshold_ms => #{type => integer},
|
local_threshold_ms => #{type => integer},
|
||||||
connect_timeout_ms => #{type => integer},
|
connect_timeout_ms => #{type => integer},
|
||||||
socket_timeout_ms => #{type => integer},
|
socket_timeout_ms => #{type => integer},
|
||||||
|
|
@ -446,6 +447,21 @@ definitions() ->
|
||||||
ssl => minirest:ref(<<"ssl">>)
|
ssl => minirest:ref(<<"ssl">>)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Mnesia = #{
|
||||||
|
type => object,
|
||||||
|
required => [type, enable],
|
||||||
|
properties => #{
|
||||||
|
type => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"redis">>],
|
||||||
|
example => <<"redis">>
|
||||||
|
},
|
||||||
|
enable => #{
|
||||||
|
type => boolean,
|
||||||
|
example => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
File = #{
|
File = #{
|
||||||
type => object,
|
type => object,
|
||||||
required => [type, enable, rules],
|
required => [type, enable, rules],
|
||||||
|
|
@ -475,6 +491,7 @@ definitions() ->
|
||||||
[ #{<<"sources">> => Sources}
|
[ #{<<"sources">> => Sources}
|
||||||
, #{<<"ssl">> => SSL}
|
, #{<<"ssl">> => SSL}
|
||||||
, #{<<"http">> => HTTP}
|
, #{<<"http">> => HTTP}
|
||||||
|
, #{<<"built-in-database">> => Mnesia}
|
||||||
, #{<<"mongo_single">> => MongoSingle}
|
, #{<<"mongo_single">> => MongoSingle}
|
||||||
, #{<<"mongo_rs">> => MongoRs}
|
, #{<<"mongo_rs">> => MongoRs}
|
||||||
, #{<<"mongo_sharded">> => MongoSharded}
|
, #{<<"mongo_sharded">> => MongoSharded}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@
|
||||||
]
|
]
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-export([ get_raw_sources/0
|
||||||
|
, get_raw_source/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([ api_spec/0
|
||||||
, sources/2
|
, sources/2
|
||||||
, source/2
|
, source/2
|
||||||
|
|
@ -147,7 +151,15 @@ source_api() ->
|
||||||
name => type,
|
name => type,
|
||||||
in => path,
|
in => path,
|
||||||
schema => #{
|
schema => #{
|
||||||
type => string
|
type => string,
|
||||||
|
enum => [ <<"file">>
|
||||||
|
, <<"http">>
|
||||||
|
, <<"mongodb">>
|
||||||
|
, <<"mysql">>
|
||||||
|
, <<"postgresql">>
|
||||||
|
, <<"redis">>
|
||||||
|
, <<"built-in-database">>
|
||||||
|
]
|
||||||
},
|
},
|
||||||
required => true
|
required => true
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +193,15 @@ source_api() ->
|
||||||
name => type,
|
name => type,
|
||||||
in => path,
|
in => path,
|
||||||
schema => #{
|
schema => #{
|
||||||
type => string
|
type => string,
|
||||||
|
enum => [ <<"file">>
|
||||||
|
, <<"http">>
|
||||||
|
, <<"mongodb">>
|
||||||
|
, <<"mysql">>
|
||||||
|
, <<"postgresql">>
|
||||||
|
, <<"redis">>
|
||||||
|
, <<"built-in-database">>
|
||||||
|
]
|
||||||
},
|
},
|
||||||
required => true
|
required => true
|
||||||
}
|
}
|
||||||
|
|
@ -216,7 +236,15 @@ source_api() ->
|
||||||
name => type,
|
name => type,
|
||||||
in => path,
|
in => path,
|
||||||
schema => #{
|
schema => #{
|
||||||
type => string
|
type => string,
|
||||||
|
enum => [ <<"file">>
|
||||||
|
, <<"http">>
|
||||||
|
, <<"mongodb">>
|
||||||
|
, <<"mysql">>
|
||||||
|
, <<"postgresql">>
|
||||||
|
, <<"redis">>
|
||||||
|
, <<"built-in-database">>
|
||||||
|
]
|
||||||
},
|
},
|
||||||
required => true
|
required => true
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +266,15 @@ move_source_api() ->
|
||||||
name => type,
|
name => type,
|
||||||
in => path,
|
in => path,
|
||||||
schema => #{
|
schema => #{
|
||||||
type => string
|
type => string,
|
||||||
|
enum => [ <<"file">>
|
||||||
|
, <<"http">>
|
||||||
|
, <<"mongodb">>
|
||||||
|
, <<"mysql">>
|
||||||
|
, <<"postgresql">>
|
||||||
|
, <<"redis">>
|
||||||
|
, <<"built-in-database">>
|
||||||
|
]
|
||||||
},
|
},
|
||||||
required => true
|
required => true
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +326,7 @@ move_source_api() ->
|
||||||
{"/authorization/sources/:type/move", Metadata, move_source}.
|
{"/authorization/sources/:type/move", Metadata, move_source}.
|
||||||
|
|
||||||
sources(get, _) ->
|
sources(get, _) ->
|
||||||
Sources = lists:foldl(fun (#{type := file, enable := Enable, path := Path}, AccIn) ->
|
Sources = lists:foldl(fun (#{<<"type">> := <<"file">>, <<"enable">> := Enable, <<"path">> := Path}, AccIn) ->
|
||||||
case file:read_file(Path) of
|
case file:read_file(Path) of
|
||||||
{ok, Rules} ->
|
{ok, Rules} ->
|
||||||
lists:append(AccIn, [#{type => file,
|
lists:append(AccIn, [#{type => file,
|
||||||
|
|
@ -309,7 +345,7 @@ sources(get, _) ->
|
||||||
{200, #{sources => Sources}};
|
{200, #{sources => Sources}};
|
||||||
sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules}}) ->
|
sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules}}) ->
|
||||||
{ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), Rules),
|
{ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), Rules),
|
||||||
update_config(head, [#{type => file, enable => true, path => Filename}]);
|
update_config(head, [#{<<"type">> => <<"file">>, <<"enable">> => true, <<"path">> => Filename}]);
|
||||||
sources(post, #{body := Body}) when is_map(Body) ->
|
sources(post, #{body := Body}) when is_map(Body) ->
|
||||||
update_config(head, [write_cert(Body)]);
|
update_config(head, [write_cert(Body)]);
|
||||||
sources(put, #{body := Body}) when is_list(Body) ->
|
sources(put, #{body := Body}) when is_list(Body) ->
|
||||||
|
|
@ -317,16 +353,16 @@ sources(put, #{body := Body}) when is_list(Body) ->
|
||||||
case Source of
|
case Source of
|
||||||
#{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} ->
|
#{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} ->
|
||||||
{ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), Rules),
|
{ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), Rules),
|
||||||
#{type => file, enable => Enable, path => Filename};
|
#{<<"type">> => <<"file">>, <<"enable">> => Enable, <<"path">> => Filename};
|
||||||
_ -> write_cert(Source)
|
_ -> write_cert(Source)
|
||||||
end
|
end
|
||||||
end || Source <- Body],
|
end || Source <- Body],
|
||||||
update_config(replace, NBody).
|
update_config(?CMD_REPLCAE, NBody).
|
||||||
|
|
||||||
source(get, #{bindings := #{type := Type}}) ->
|
source(get, #{bindings := #{type := Type}}) ->
|
||||||
case get_raw_source(Type) of
|
case get_raw_source(Type) of
|
||||||
[] -> {404, #{message => <<"Not found ", Type/binary>>}};
|
[] -> {404, #{message => <<"Not found ", Type/binary>>}};
|
||||||
[#{type := <<"file">>, enable := Enable, path := Path}] ->
|
[#{<<"type">> := <<"file">>, <<"enable">> := Enable, <<"path">> := Path}] ->
|
||||||
case file:read_file(Path) of
|
case file:read_file(Path) of
|
||||||
{ok, Rules} ->
|
{ok, Rules} ->
|
||||||
{200, #{type => file,
|
{200, #{type => file,
|
||||||
|
|
@ -336,23 +372,23 @@ source(get, #{bindings := #{type := Type}}) ->
|
||||||
};
|
};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{code => <<"BAD_REQUEST">>,
|
||||||
message => atom_to_binary(Reason)}}
|
message => bin(Reason)}}
|
||||||
end;
|
end;
|
||||||
[Source] ->
|
[Source] ->
|
||||||
{200, read_cert(Source)}
|
{200, read_cert(Source)}
|
||||||
end;
|
end;
|
||||||
source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) ->
|
source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) ->
|
||||||
{ok, Filename} = write_file(maps:get(path, emqx_authz:lookup(file), ""), Rules),
|
{ok, Filename} = write_file(maps:get(path, emqx_authz:lookup(file), ""), Rules),
|
||||||
case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Filename}) of
|
case emqx_authz:update({?CMD_REPLCAE, file}, #{<<"type">> => file, <<"enable">> => Enable, <<"path">> => Filename}) of
|
||||||
{ok, _} -> {204};
|
{ok, _} -> {204};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{code => <<"BAD_REQUEST">>,
|
||||||
message => atom_to_binary(Reason)}}
|
message => bin(Reason)}}
|
||||||
end;
|
end;
|
||||||
source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) ->
|
source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) ->
|
||||||
update_config({replace_once, Type}, write_cert(Body));
|
update_config({?CMD_REPLCAE, Type}, write_cert(Body));
|
||||||
source(delete, #{bindings := #{type := Type}}) ->
|
source(delete, #{bindings := #{type := Type}}) ->
|
||||||
update_config({delete_once, Type}, #{}).
|
update_config({?CMD_DELETE, Type}, #{}).
|
||||||
|
|
||||||
move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) ->
|
move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) ->
|
||||||
case emqx_authz:move(Type, Position) of
|
case emqx_authz:move(Type, Position) of
|
||||||
|
|
@ -362,18 +398,18 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos
|
||||||
message => <<"source ", Type/binary, " not found">>}};
|
message => <<"source ", Type/binary, " not found">>}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{code => <<"BAD_REQUEST">>,
|
||||||
message => atom_to_binary(Reason)}}
|
message => bin(Reason)}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_raw_sources() ->
|
get_raw_sources() ->
|
||||||
RawSources = emqx:get_raw_config([authorization, sources]),
|
RawSources = emqx:get_raw_config([authorization, sources]),
|
||||||
Schema = #{roots => emqx_authz_schema:fields("authorization"), fields => #{}},
|
Schema = #{roots => emqx_authz_schema:fields("authorization"), fields => #{}},
|
||||||
Conf = #{<<"sources">> => RawSources},
|
Conf = #{<<"sources">> => RawSources},
|
||||||
#{sources := Sources} = hocon_schema:check_plain(Schema, Conf, #{atom_key => true, no_conversion => true}),
|
#{<<"sources">> := Sources} = hocon_schema:check_plain(Schema, Conf, #{only_fill_defaults => true}),
|
||||||
Sources.
|
Sources.
|
||||||
|
|
||||||
get_raw_source(Type) ->
|
get_raw_source(Type) ->
|
||||||
lists:filter(fun (#{type := T}) ->
|
lists:filter(fun (#{<<"type">> := T}) ->
|
||||||
T =:= Type
|
T =:= Type
|
||||||
end, get_raw_sources()).
|
end, get_raw_sources()).
|
||||||
|
|
||||||
|
|
@ -382,16 +418,16 @@ update_config(Cmd, Sources) ->
|
||||||
{ok, _} -> {204};
|
{ok, _} -> {204};
|
||||||
{error, {pre_config_update, emqx_authz, Reason}} ->
|
{error, {pre_config_update, emqx_authz, Reason}} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{code => <<"BAD_REQUEST">>,
|
||||||
message => atom_to_binary(Reason)}};
|
message => bin(Reason)}};
|
||||||
{error, {post_config_update, emqx_authz, Reason}} ->
|
{error, {post_config_update, emqx_authz, Reason}} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{code => <<"BAD_REQUEST">>,
|
||||||
message => atom_to_binary(Reason)}};
|
message => bin(Reason)}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{code => <<"BAD_REQUEST">>,
|
||||||
message => atom_to_binary(Reason)}}
|
message => bin(Reason)}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
read_cert(#{ssl := #{enable := true} = SSL} = Source) ->
|
read_cert(#{<<"ssl">> := #{<<"enable">> := true} = SSL} = Source) ->
|
||||||
CaCert = case file:read_file(maps:get(cacertfile, SSL, "")) of
|
CaCert = case file:read_file(maps:get(cacertfile, SSL, "")) of
|
||||||
{ok, CaCert0} -> CaCert0;
|
{ok, CaCert0} -> CaCert0;
|
||||||
_ -> ""
|
_ -> ""
|
||||||
|
|
@ -459,3 +495,6 @@ do_write_file(Filename, Bytes) ->
|
||||||
?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]),
|
?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]),
|
||||||
error(Reason)
|
error(Reason)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
bin(Term) ->
|
||||||
|
erlang:iolist_to_binary(io_lib:format("~p", [Term])).
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@
|
||||||
|
|
||||||
-behaviour(application).
|
-behaviour(application).
|
||||||
|
|
||||||
|
-include("emqx_authz.hrl").
|
||||||
|
|
||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
|
ok = ekka_rlog:wait_for_shards([?ACL_SHARDED], infinity),
|
||||||
{ok, Sup} = emqx_authz_sup:start_link(),
|
{ok, Sup} = emqx_authz_sup:start_link(),
|
||||||
ok = emqx_authz:init(),
|
ok = emqx_authz:init(),
|
||||||
{ok, Sup}.
|
{ok, Sup}.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_authz_mnesia).
|
||||||
|
|
||||||
|
-include("emqx_authz.hrl").
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
%% AuthZ Callbacks
|
||||||
|
-export([ mnesia/1
|
||||||
|
, authorize/4
|
||||||
|
, description/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-boot_mnesia({mnesia, [boot]}).
|
||||||
|
-copy_mnesia({mnesia, [copy]}).
|
||||||
|
|
||||||
|
-spec(mnesia(boot | copy) -> ok).
|
||||||
|
mnesia(boot) ->
|
||||||
|
ok = ekka_mnesia:create_table(?ACL_TABLE, [
|
||||||
|
{type, ordered_set},
|
||||||
|
{rlog_shard, ?ACL_SHARDED},
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{attributes, record_info(fields, ?ACL_TABLE)},
|
||||||
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}]);
|
||||||
|
mnesia(copy) ->
|
||||||
|
ok = ekka_mnesia:copy_table(?ACL_TABLE, disc_copies).
|
||||||
|
|
||||||
|
description() ->
|
||||||
|
"AuthZ with Mnesia".
|
||||||
|
|
||||||
|
authorize(#{username := Username,
|
||||||
|
clientid := Clientid
|
||||||
|
} = Client, PubSub, Topic, #{type := 'built-in-database'}) ->
|
||||||
|
|
||||||
|
Rules = case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}) of
|
||||||
|
[] -> [];
|
||||||
|
[#emqx_acl{rules = Rules0}] when is_list(Rules0) -> Rules0
|
||||||
|
end
|
||||||
|
++ case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}) of
|
||||||
|
[] -> [];
|
||||||
|
[#emqx_acl{rules = Rules1}] when is_list(Rules1) -> Rules1
|
||||||
|
end
|
||||||
|
++ case mnesia:dirty_read(?ACL_TABLE, ?ACL_TABLE_ALL) of
|
||||||
|
[] -> [];
|
||||||
|
[#emqx_acl{rules = Rules2}] when is_list(Rules2) -> Rules2
|
||||||
|
end,
|
||||||
|
do_authorize(Client, PubSub, Topic, Rules).
|
||||||
|
|
||||||
|
do_authorize(_Client, _PubSub, _Topic, []) -> nomatch;
|
||||||
|
do_authorize(Client, PubSub, Topic, [ {Permission, Action, TopicFilter} | Tail]) ->
|
||||||
|
case emqx_authz_rule:match(Client, PubSub, Topic,
|
||||||
|
emqx_authz_rule:compile({Permission, all, Action, [TopicFilter]})
|
||||||
|
) of
|
||||||
|
{matched, Permission} -> {matched, Permission};
|
||||||
|
nomatch -> do_authorize(Client, PubSub, Topic, Tail)
|
||||||
|
end.
|
||||||
|
|
@ -58,9 +58,9 @@ do_authorize(Client, PubSub, Topic, [Rule | Tail]) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
replvar(Selector, #{clientid := Clientid,
|
replvar(Selector, #{clientid := Clientid,
|
||||||
username := Username,
|
username := Username,
|
||||||
peerhost := IpAddress
|
peerhost := IpAddress
|
||||||
}) ->
|
}) ->
|
||||||
Fun = fun
|
Fun = fun
|
||||||
_Fun(K, V, AccIn) when is_map(V) -> maps:put(K, maps:fold(_Fun, AccIn, V), AccIn);
|
_Fun(K, V, AccIn) when is_map(V) -> maps:put(K, maps:fold(_Fun, AccIn, V), AccIn);
|
||||||
_Fun(K, V, AccIn) when is_list(V) ->
|
_Fun(K, V, AccIn) when is_list(V) ->
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,6 @@ do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) ->
|
||||||
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail)
|
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
format_result(Columns, Row) ->
|
format_result(Columns, Row) ->
|
||||||
Permission = lists:nth(index(<<"permission">>, Columns), Row),
|
Permission = lists:nth(index(<<"permission">>, Columns), Row),
|
||||||
Action = lists:nth(index(<<"action">>, Columns), Row),
|
Action = lists:nth(index(<<"action">>, Columns), Row),
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,21 @@
|
||||||
|
|
||||||
-export_type([rule/0]).
|
-export_type([rule/0]).
|
||||||
|
|
||||||
|
compile({Permission, all}) when ?ALLOW_DENY(Permission) -> {Permission, all, all, [compile_topic(<<"#">>)]};
|
||||||
compile({Permission, Who, Action, TopicFilters}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) ->
|
compile({Permission, Who, Action, TopicFilters}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) ->
|
||||||
{atom(Permission), compile_who(Who), atom(Action), [compile_topic(Topic) || Topic <- TopicFilters]}.
|
{atom(Permission), compile_who(Who), atom(Action), [compile_topic(Topic) || Topic <- TopicFilters]}.
|
||||||
|
|
||||||
compile_who(all) -> all;
|
compile_who(all) -> all;
|
||||||
compile_who({username, Username}) ->
|
compile_who({user, Username}) -> compile_who({username, Username});
|
||||||
|
compile_who({username, {re, Username}}) ->
|
||||||
{ok, MP} = re:compile(bin(Username)),
|
{ok, MP} = re:compile(bin(Username)),
|
||||||
{username, MP};
|
{username, MP};
|
||||||
compile_who({clientid, Clientid}) ->
|
compile_who({username, Username}) -> {username, {eq, bin(Username)}};
|
||||||
|
compile_who({client, Clientid}) -> compile_who({clientid, Clientid});
|
||||||
|
compile_who({clientid, {re, Clientid}}) ->
|
||||||
{ok, MP} = re:compile(bin(Clientid)),
|
{ok, MP} = re:compile(bin(Clientid)),
|
||||||
{clientid, MP};
|
{clientid, MP};
|
||||||
|
compile_who({clientid, Clientid}) -> {clientid, {eq, bin(Clientid)}};
|
||||||
compile_who({ipaddr, CIDR}) ->
|
compile_who({ipaddr, CIDR}) ->
|
||||||
{ipaddr, esockd_cidr:parse(CIDR, true)};
|
{ipaddr, esockd_cidr:parse(CIDR, true)};
|
||||||
compile_who({ipaddrs, CIDRs}) ->
|
compile_who({ipaddrs, CIDRs}) ->
|
||||||
|
|
@ -102,14 +107,16 @@ match_action(_, all) -> true;
|
||||||
match_action(_, _) -> false.
|
match_action(_, _) -> false.
|
||||||
|
|
||||||
match_who(_, all) -> true;
|
match_who(_, all) -> true;
|
||||||
match_who(#{username := undefined}, {username, _MP}) ->
|
match_who(#{username := undefined}, {username, _}) ->
|
||||||
false;
|
false;
|
||||||
match_who(#{username := Username}, {username, MP}) ->
|
match_who(#{username := Username}, {username, {eq, Username}}) -> true;
|
||||||
|
match_who(#{username := Username}, {username, {re_pattern, _, _, _, _} = MP}) ->
|
||||||
case re:run(Username, MP) of
|
case re:run(Username, MP) of
|
||||||
{match, _} -> true;
|
{match, _} -> true;
|
||||||
_ -> false
|
_ -> false
|
||||||
end;
|
end;
|
||||||
match_who(#{clientid := Clientid}, {clientid, MP}) ->
|
match_who(#{clientid := Clientid}, {clientid, {eq, Clientid}}) -> true;
|
||||||
|
match_who(#{clientid := Clientid}, {clientid, {re_pattern, _, _, _, _} = MP}) ->
|
||||||
case re:run(Clientid, MP) of
|
case re:run(Clientid, MP) of
|
||||||
{match, _} -> true;
|
{match, _} -> true;
|
||||||
_ -> false
|
_ -> false
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
, fields/1
|
, fields/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-import(emqx_schema, [mk_duration/2]).
|
||||||
|
|
||||||
namespace() -> authz.
|
namespace() -> authz.
|
||||||
|
|
||||||
%% @doc authorization schema is not exported
|
%% @doc authorization schema is not exported
|
||||||
|
|
@ -29,6 +31,7 @@ fields("authorization") ->
|
||||||
[ hoconsc:ref(?MODULE, file)
|
[ hoconsc:ref(?MODULE, file)
|
||||||
, hoconsc:ref(?MODULE, http_get)
|
, hoconsc:ref(?MODULE, http_get)
|
||||||
, hoconsc:ref(?MODULE, http_post)
|
, hoconsc:ref(?MODULE, http_post)
|
||||||
|
, hoconsc:ref(?MODULE, mnesia)
|
||||||
, hoconsc:ref(?MODULE, mongo_single)
|
, hoconsc:ref(?MODULE, mongo_single)
|
||||||
, hoconsc:ref(?MODULE, mongo_rs)
|
, hoconsc:ref(?MODULE, mongo_rs)
|
||||||
, hoconsc:ref(?MODULE, mongo_sharded)
|
, hoconsc:ref(?MODULE, mongo_sharded)
|
||||||
|
|
@ -45,11 +48,7 @@ fields(file) ->
|
||||||
, {enable, #{type => boolean(),
|
, {enable, #{type => boolean(),
|
||||||
default => true}}
|
default => true}}
|
||||||
, {path, #{type => string(),
|
, {path, #{type => string(),
|
||||||
validator => fun(S) -> case filelib:is_file(S) of
|
desc => "Path to the file which contains the ACL rules."
|
||||||
true -> ok;
|
|
||||||
_ -> {error, "File does not exist"}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}}
|
}}
|
||||||
];
|
];
|
||||||
fields(http_get) ->
|
fields(http_get) ->
|
||||||
|
|
@ -77,7 +76,7 @@ fields(http_get) ->
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
, {request_timeout, #{type => timeout(), default => 30000 }}
|
, {request_timeout, mk_duration("request timeout", #{default => "30s"})}
|
||||||
] ++ proplists:delete(base_url, emqx_connector_http:fields(config));
|
] ++ proplists:delete(base_url, emqx_connector_http:fields(config));
|
||||||
fields(http_post) ->
|
fields(http_post) ->
|
||||||
[ {type, #{type => http}}
|
[ {type, #{type => http}}
|
||||||
|
|
@ -107,12 +106,17 @@ fields(http_post) ->
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
, {request_timeout, #{type => timeout(), default => 30000 }}
|
, {request_timeout, mk_duration("request timeout", #{default => "30s"})}
|
||||||
, {body, #{type => map(),
|
, {body, #{type => map(),
|
||||||
nullable => true
|
nullable => true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
] ++ proplists:delete(base_url, emqx_connector_http:fields(config));
|
] ++ proplists:delete(base_url, emqx_connector_http:fields(config));
|
||||||
|
fields(mnesia) ->
|
||||||
|
[ {type, #{type => 'built-in-database'}}
|
||||||
|
, {enable, #{type => boolean(),
|
||||||
|
default => true}}
|
||||||
|
];
|
||||||
fields(mongo_single) ->
|
fields(mongo_single) ->
|
||||||
[ {collection, #{type => atom()}}
|
[ {collection, #{type => atom()}}
|
||||||
, {selector, #{type => map()}}
|
, {selector, #{type => map()}}
|
||||||
|
|
|
||||||
|
|
@ -50,14 +50,14 @@ init_per_suite(Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
{ok, _} = emqx_authz:update(replace, []),
|
{ok, _} = emqx_authz:update(?CMD_REPLCAE, []),
|
||||||
emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]),
|
emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]),
|
||||||
meck:unload(emqx_resource),
|
meck:unload(emqx_resource),
|
||||||
meck:unload(emqx_schema),
|
meck:unload(emqx_schema),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
init_per_testcase(_, Config) ->
|
init_per_testcase(_, Config) ->
|
||||||
{ok, _} = emqx_authz:update(replace, []),
|
{ok, _} = emqx_authz:update(?CMD_REPLCAE, []),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
-define(SOURCE1, #{<<"type">> => <<"http">>,
|
-define(SOURCE1, #{<<"type">> => <<"http">>,
|
||||||
|
|
@ -120,12 +120,12 @@ init_per_testcase(_, Config) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_update_source(_) ->
|
t_update_source(_) ->
|
||||||
{ok, _} = emqx_authz:update(replace, [?SOURCE3]),
|
{ok, _} = emqx_authz:update(?CMD_REPLCAE, [?SOURCE3]),
|
||||||
{ok, _} = emqx_authz:update(head, [?SOURCE2]),
|
{ok, _} = emqx_authz:update(?CMD_PREPEND, [?SOURCE2]),
|
||||||
{ok, _} = emqx_authz:update(head, [?SOURCE1]),
|
{ok, _} = emqx_authz:update(?CMD_PREPEND, [?SOURCE1]),
|
||||||
{ok, _} = emqx_authz:update(tail, [?SOURCE4]),
|
{ok, _} = emqx_authz:update(?CMD_APPEND, [?SOURCE4]),
|
||||||
{ok, _} = emqx_authz:update(tail, [?SOURCE5]),
|
{ok, _} = emqx_authz:update(?CMD_APPEND, [?SOURCE5]),
|
||||||
{ok, _} = emqx_authz:update(tail, [?SOURCE6]),
|
{ok, _} = emqx_authz:update(?CMD_APPEND, [?SOURCE6]),
|
||||||
|
|
||||||
?assertMatch([ #{type := http, enable := true}
|
?assertMatch([ #{type := http, enable := true}
|
||||||
, #{type := mongodb, enable := true}
|
, #{type := mongodb, enable := true}
|
||||||
|
|
@ -135,12 +135,12 @@ t_update_source(_) ->
|
||||||
, #{type := file, enable := true}
|
, #{type := file, enable := true}
|
||||||
], emqx:get_config([authorization, sources], [])),
|
], emqx:get_config([authorization, sources], [])),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:update({replace_once, http}, ?SOURCE1#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLCAE, http}, ?SOURCE1#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({replace_once, mongodb}, ?SOURCE2#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLCAE, mongodb}, ?SOURCE2#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({replace_once, mysql}, ?SOURCE3#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLCAE, mysql}, ?SOURCE3#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({replace_once, postgresql}, ?SOURCE4#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLCAE, postgresql}, ?SOURCE4#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({replace_once, redis}, ?SOURCE5#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLCAE, redis}, ?SOURCE5#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({replace_once, file}, ?SOURCE6#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLCAE, file}, ?SOURCE6#{<<"enable">> := false}),
|
||||||
|
|
||||||
?assertMatch([ #{type := http, enable := false}
|
?assertMatch([ #{type := http, enable := false}
|
||||||
, #{type := mongodb, enable := false}
|
, #{type := mongodb, enable := false}
|
||||||
|
|
@ -150,10 +150,10 @@ t_update_source(_) ->
|
||||||
, #{type := file, enable := false}
|
, #{type := file, enable := false}
|
||||||
], emqx:get_config([authorization, sources], [])),
|
], emqx:get_config([authorization, sources], [])),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:update(replace, []).
|
{ok, _} = emqx_authz:update(?CMD_REPLCAE, []).
|
||||||
|
|
||||||
t_move_source(_) ->
|
t_move_source(_) ->
|
||||||
{ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]),
|
{ok, _} = emqx_authz:update(?CMD_REPLCAE, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]),
|
||||||
?assertMatch([ #{type := http}
|
?assertMatch([ #{type := http}
|
||||||
, #{type := mongodb}
|
, #{type := mongodb}
|
||||||
, #{type := mysql}
|
, #{type := mysql}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_authz_api_mnesia_SUITE).
|
||||||
|
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include("emqx_authz.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-define(CONF_DEFAULT, <<"authorization: {sources: []}">>).
|
||||||
|
|
||||||
|
-import(emqx_ct_http, [ request_api/3
|
||||||
|
, request_api/5
|
||||||
|
, get_http_data/1
|
||||||
|
, create_default_app/0
|
||||||
|
, delete_default_app/0
|
||||||
|
, default_auth_header/0
|
||||||
|
, auth_header/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(HOST, "http://127.0.0.1:18083/").
|
||||||
|
-define(API_VERSION, "v5").
|
||||||
|
-define(BASE_PATH, "api").
|
||||||
|
|
||||||
|
-define(EXAMPLE_USERNAME, #{username => user1,
|
||||||
|
rules => [ #{topic => <<"test/toopic/1">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"publish">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"test/toopic/2">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"eq test/#">>,
|
||||||
|
permission => <<"deny">>,
|
||||||
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).
|
||||||
|
-define(EXAMPLE_CLIENTID, #{clientid => client1,
|
||||||
|
rules => [ #{topic => <<"test/toopic/1">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"publish">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"test/toopic/2">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"eq test/#">>,
|
||||||
|
permission => <<"deny">>,
|
||||||
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).
|
||||||
|
-define(EXAMPLE_ALL , #{rules => [ #{topic => <<"test/toopic/1">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"publish">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"test/toopic/2">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
}
|
||||||
|
, #{topic => <<"eq test/#">>,
|
||||||
|
permission => <<"deny">>,
|
||||||
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[]. %% Todo: Waiting for @terry-xiaoyu to fix the config_not_found error
|
||||||
|
% emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
|
||||||
|
meck:expect(emqx_schema, fields, fun("authorization") ->
|
||||||
|
meck:passthrough(["authorization"]) ++
|
||||||
|
emqx_authz_schema:fields("authorization");
|
||||||
|
(F) -> meck:passthrough([F])
|
||||||
|
end),
|
||||||
|
|
||||||
|
ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT),
|
||||||
|
|
||||||
|
ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1),
|
||||||
|
{ok, _} = emqx:update_config([authorization, cache, enable], false),
|
||||||
|
{ok, _} = emqx:update_config([authorization, no_match], deny),
|
||||||
|
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
{ok, _} = emqx_authz:update(replace, []),
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_authz, emqx_dashboard]),
|
||||||
|
meck:unload(emqx_schema),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
set_special_configs(emqx_dashboard) ->
|
||||||
|
Config = #{
|
||||||
|
default_username => <<"admin">>,
|
||||||
|
default_password => <<"public">>,
|
||||||
|
listeners => [#{
|
||||||
|
protocol => http,
|
||||||
|
port => 18083
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
emqx_config:put([emqx_dashboard], Config),
|
||||||
|
ok;
|
||||||
|
set_special_configs(emqx_authz) ->
|
||||||
|
emqx_config:put([authorization], #{sources => [#{type => 'built-in-database',
|
||||||
|
enable => true}
|
||||||
|
]}),
|
||||||
|
ok;
|
||||||
|
set_special_configs(_App) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Testcases
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_api(_) ->
|
||||||
|
{ok, 204, _} = request(post, uri(["authorization", "sources", "built-in-database", "username"]), [?EXAMPLE_USERNAME]),
|
||||||
|
{ok, 200, Request1} = request(get, uri(["authorization", "sources", "built-in-database", "username"]), []),
|
||||||
|
{ok, 200, Request2} = request(get, uri(["authorization", "sources", "built-in-database", "username", "user1"]), []),
|
||||||
|
[#{<<"username">> := <<"user1">>, <<"rules">> := Rules1}] = jsx:decode(Request1),
|
||||||
|
#{<<"username">> := <<"user1">>, <<"rules">> := Rules1} = jsx:decode(Request2),
|
||||||
|
?assertEqual(3, length(Rules1)),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(put, uri(["authorization", "sources", "built-in-database", "username", "user1"]), ?EXAMPLE_USERNAME#{rules => []}),
|
||||||
|
{ok, 200, Request3} = request(get, uri(["authorization", "sources", "built-in-database", "username", "user1"]), []),
|
||||||
|
#{<<"username">> := <<"user1">>, <<"rules">> := Rules2} = jsx:decode(Request3),
|
||||||
|
?assertEqual(0, length(Rules2)),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(delete, uri(["authorization", "sources", "built-in-database", "username", "user1"]), []),
|
||||||
|
{ok, 404, _} = request(get, uri(["authorization", "sources", "built-in-database", "username", "user1"]), []),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(post, uri(["authorization", "sources", "built-in-database", "clientid"]), [?EXAMPLE_CLIENTID]),
|
||||||
|
{ok, 200, Request4} = request(get, uri(["authorization", "sources", "built-in-database", "clientid"]), []),
|
||||||
|
{ok, 200, Request5} = request(get, uri(["authorization", "sources", "built-in-database", "clientid", "client1"]), []),
|
||||||
|
[#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3}] = jsx:decode(Request4),
|
||||||
|
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3} = jsx:decode(Request5),
|
||||||
|
?assertEqual(3, length(Rules3)),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(put, uri(["authorization", "sources", "built-in-database", "clientid", "client1"]), ?EXAMPLE_CLIENTID#{rules => []}),
|
||||||
|
{ok, 200, Request6} = request(get, uri(["authorization", "sources", "built-in-database", "clientid", "client1"]), []),
|
||||||
|
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules4} = jsx:decode(Request6),
|
||||||
|
?assertEqual(0, length(Rules4)),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(delete, uri(["authorization", "sources", "built-in-database", "clientid", "client1"]), []),
|
||||||
|
{ok, 404, _} = request(get, uri(["authorization", "sources", "built-in-database", "clientid", "client1"]), []),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(put, uri(["authorization", "sources", "built-in-database", "all"]), ?EXAMPLE_ALL),
|
||||||
|
{ok, 200, Request7} = request(get, uri(["authorization", "sources", "built-in-database", "all"]), []),
|
||||||
|
[#{<<"rules">> := Rules5}] = jsx:decode(Request7),
|
||||||
|
?assertEqual(3, length(Rules5)),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(put, uri(["authorization", "sources", "built-in-database", "all"]), ?EXAMPLE_ALL#{rules => []}),
|
||||||
|
{ok, 200, Request8} = request(get, uri(["authorization", "sources", "built-in-database", "all"]), []),
|
||||||
|
[#{<<"rules">> := Rules6}] = jsx:decode(Request8),
|
||||||
|
?assertEqual(0, length(Rules6)),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(post, uri(["authorization", "sources", "built-in-database", "username"]), [ #{username => N, rules => []} || N <- lists:seq(1, 20) ]),
|
||||||
|
{ok, 200, Request9} = request(get, uri(["authorization", "sources", "built-in-database", "username?page=2&limit=5"]), []),
|
||||||
|
#{<<"data">> := Data1} = jsx:decode(Request9),
|
||||||
|
?assertEqual(5, length(Data1)),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(post, uri(["authorization", "sources", "built-in-database", "clientid"]), [ #{clientid => N, rules => []} || N <- lists:seq(1, 20) ]),
|
||||||
|
{ok, 200, Request10} = request(get, uri(["authorization", "sources", "built-in-database", "clientid?limit=5"]), []),
|
||||||
|
?assertEqual(5, length(jsx:decode(Request10))),
|
||||||
|
|
||||||
|
{ok, 400, _} = request(delete, uri(["authorization", "sources", "built-in-database", "purge-all"]), []),
|
||||||
|
{ok, 204, _} = request(put, uri(["authorization", "sources", "built-in-database"]), #{<<"enable">> => false}),
|
||||||
|
{ok, 204, _} = request(delete, uri(["authorization", "sources", "built-in-database", "purge-all"]), []),
|
||||||
|
?assertEqual([], mnesia:dirty_all_keys(?ACL_TABLE)),
|
||||||
|
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% HTTP Request
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
request(Method, Url, Body) ->
|
||||||
|
Request = case Body of
|
||||||
|
[] -> {Url, [auth_header_()]};
|
||||||
|
_ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)}
|
||||||
|
end,
|
||||||
|
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
|
||||||
|
case httpc:request(Method, Request, [], [{body_format, binary}]) of
|
||||||
|
{error, socket_closed_remotely} ->
|
||||||
|
{error, socket_closed_remotely};
|
||||||
|
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } ->
|
||||||
|
{ok, Code, Return};
|
||||||
|
{ok, {Reason, _, _}} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
uri() -> uri([]).
|
||||||
|
uri(Parts) when is_list(Parts) ->
|
||||||
|
NParts = [E || E <- Parts],
|
||||||
|
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
|
||||||
|
|
||||||
|
get_sources(Result) -> jsx:decode(Result).
|
||||||
|
|
||||||
|
auth_header_() ->
|
||||||
|
Username = <<"admin">>,
|
||||||
|
Password = <<"public">>,
|
||||||
|
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
|
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<<"url">> => <<"https://fake.com:443/">>,
|
<<"url">> => <<"https://fake.com:443/">>,
|
||||||
<<"headers">> => #{},
|
<<"headers">> => #{},
|
||||||
<<"method">> => <<"get">>,
|
<<"method">> => <<"get">>,
|
||||||
<<"request_timeout">> => 5000
|
<<"request_timeout">> => <<"5s">>
|
||||||
}).
|
}).
|
||||||
-define(SOURCE2, #{<<"type">> => <<"mongodb">>,
|
-define(SOURCE2, #{<<"type">> => <<"mongodb">>,
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_authz_mnesia_SUITE).
|
||||||
|
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include("emqx_authz.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-define(CONF_DEFAULT, <<"authorization: {sources: []}">>).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
|
||||||
|
meck:expect(emqx_schema, fields, fun("authorization") ->
|
||||||
|
meck:passthrough(["authorization"]) ++
|
||||||
|
emqx_authz_schema:fields("authorization");
|
||||||
|
(F) -> meck:passthrough([F])
|
||||||
|
end),
|
||||||
|
|
||||||
|
ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT),
|
||||||
|
ok = emqx_ct_helpers:start_apps([emqx_authz]),
|
||||||
|
|
||||||
|
{ok, _} = emqx:update_config([authorization, cache, enable], false),
|
||||||
|
{ok, _} = emqx:update_config([authorization, no_match], deny),
|
||||||
|
Rules = [#{<<"type">> => <<"built-in-database">>}],
|
||||||
|
{ok, _} = emqx_authz:update(replace, Rules),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
{ok, _} = emqx_authz:update(replace, []),
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_authz]),
|
||||||
|
meck:unload(emqx_schema),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
init_per_testcase(t_authz, Config) ->
|
||||||
|
mnesia:transaction(fun ekka_mnesia:dirty_write/1, [#emqx_acl{who = {?ACL_TABLE_USERNAME, <<"test_username">>},
|
||||||
|
rules = [{allow, publish, <<"test/%u">>},
|
||||||
|
{allow, subscribe, <<"eq #">>}
|
||||||
|
]
|
||||||
|
}]),
|
||||||
|
mnesia:transaction(fun ekka_mnesia:dirty_write/1, [#emqx_acl{who = {?ACL_TABLE_CLIENTID, <<"test_clientid">>},
|
||||||
|
rules = [{allow, publish, <<"test/%c">>},
|
||||||
|
{deny, subscribe, <<"eq #">>}
|
||||||
|
]
|
||||||
|
}]),
|
||||||
|
mnesia:transaction(fun ekka_mnesia:dirty_write/1, [#emqx_acl{who = ?ACL_TABLE_ALL,
|
||||||
|
rules = [{deny, all, <<"#">>}]
|
||||||
|
}]),
|
||||||
|
Config;
|
||||||
|
init_per_testcase(_, Config) -> Config.
|
||||||
|
|
||||||
|
end_per_testcase(t_authz, Config) ->
|
||||||
|
[ ekka_mnesia:dirty_delete(?ACL_TABLE, K) || K <- mnesia:dirty_all_keys(?ACL_TABLE)],
|
||||||
|
Config;
|
||||||
|
end_per_testcase(_, Config) -> Config.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Testcases
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_authz(_) ->
|
||||||
|
ClientInfo1 = #{clientid => <<"test">>,
|
||||||
|
username => <<"test">>,
|
||||||
|
peerhost => {127,0,0,1},
|
||||||
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
ClientInfo2 = #{clientid => <<"fake_clientid">>,
|
||||||
|
username => <<"test_username">>,
|
||||||
|
peerhost => {127,0,0,1},
|
||||||
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
ClientInfo3 = #{clientid => <<"test_clientid">>,
|
||||||
|
username => <<"fake_username">>,
|
||||||
|
peerhost => {127,0,0,1},
|
||||||
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
|
?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)),
|
||||||
|
?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)),
|
||||||
|
|
||||||
|
?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_username">>)),
|
||||||
|
?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"#">>)),
|
||||||
|
|
||||||
|
?assertEqual(allow, emqx_access_control:authorize(ClientInfo3, publish, <<"test/test_clientid">>)),
|
||||||
|
?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, subscribe, <<"#">>)),
|
||||||
|
|
||||||
|
ok.
|
||||||
|
|
||||||
|
|
@ -22,11 +22,11 @@
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
-define(SOURCE1, {deny, all, all, ["#"]}).
|
-define(SOURCE1, {deny, all}).
|
||||||
-define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}).
|
-define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}).
|
||||||
-define(SOURCE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, ["%c"]}).
|
-define(SOURCE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, ["%c"]}).
|
||||||
-define(SOURCE4, {allow, {'and', [{clientid, "^test?"}, {username, "^test?"}]}, publish, ["topic/test"]}).
|
-define(SOURCE4, {allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}).
|
||||||
-define(SOURCE5, {allow, {'or', [{username, "^test"}, {clientid, "test?"}]}, publish, ["%u", "%c"]}).
|
-define(SOURCE5, {allow, {'or', [{username, {re, "^test"}}, {clientid, {re, "test?"}}]}, publish, ["%u", "%c"]}).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_ct:all(?MODULE).
|
emqx_ct:all(?MODULE).
|
||||||
|
|
@ -52,7 +52,7 @@ t_compile(_) ->
|
||||||
}, emqx_authz_rule:compile(?SOURCE3)),
|
}, emqx_authz_rule:compile(?SOURCE3)),
|
||||||
|
|
||||||
?assertMatch({allow,
|
?assertMatch({allow,
|
||||||
{'and', [{clientid, {re_pattern, _, _, _, _}}, {username, {re_pattern, _, _, _, _}}]},
|
{'and', [{clientid, {eq, <<"test">>}}, {username, {eq, <<"test">>}}]},
|
||||||
publish,
|
publish,
|
||||||
[[<<"topic">>, <<"test">>]]
|
[[<<"topic">>, <<"test">>]]
|
||||||
}, emqx_authz_rule:compile(?SOURCE4)),
|
}, emqx_authz_rule:compile(?SOURCE4)),
|
||||||
|
|
|
||||||
|
|
@ -45,3 +45,30 @@
|
||||||
# retain = false
|
# retain = false
|
||||||
# }
|
# }
|
||||||
#}
|
#}
|
||||||
|
#
|
||||||
|
#bridges.http.my_http_bridge {
|
||||||
|
# base_url: "http://localhost:9901"
|
||||||
|
# connect_timeout: "30s"
|
||||||
|
# max_retries: 3
|
||||||
|
# retry_interval = "10s"
|
||||||
|
# pool_type = "random"
|
||||||
|
# pool_size = 4
|
||||||
|
# enable_pipelining = true
|
||||||
|
# ssl {
|
||||||
|
# enable = false
|
||||||
|
# keyfile = "{{ platform_etc_dir }}/certs/client-key.pem"
|
||||||
|
# certfile = "{{ platform_etc_dir }}/certs/client-cert.pem"
|
||||||
|
# cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
||||||
|
# }
|
||||||
|
# egress_channels.post_messages {
|
||||||
|
# subscribe_local_topic = "emqx_http/#"
|
||||||
|
# request_timeout: "30s"
|
||||||
|
# ## following config entries can use placehodler variables
|
||||||
|
# method = post
|
||||||
|
# path = "/messages/${topic}"
|
||||||
|
# body = "${payload}"
|
||||||
|
# headers {
|
||||||
|
# "content-type": "application/json"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,15 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_bridge).
|
-module(emqx_bridge).
|
||||||
-behaviour(emqx_config_handler).
|
-behaviour(emqx_config_handler).
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([post_config_update/4]).
|
-export([post_config_update/4]).
|
||||||
|
|
||||||
|
-export([reload_hook/0, unload_hook/0]).
|
||||||
|
|
||||||
|
-export([on_message_publish/1]).
|
||||||
|
|
||||||
-export([ load_bridges/0
|
-export([ load_bridges/0
|
||||||
, get_bridge/2
|
, get_bridge/2
|
||||||
, get_bridge/3
|
, get_bridge/3
|
||||||
|
|
@ -28,6 +34,7 @@
|
||||||
, start_bridge/2
|
, start_bridge/2
|
||||||
, stop_bridge/2
|
, stop_bridge/2
|
||||||
, restart_bridge/2
|
, restart_bridge/2
|
||||||
|
, send_message/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([ config_key_path/0
|
-export([ config_key_path/0
|
||||||
|
|
@ -38,24 +45,57 @@
|
||||||
, resource_id/1
|
, resource_id/1
|
||||||
, resource_id/2
|
, resource_id/2
|
||||||
, parse_bridge_id/1
|
, parse_bridge_id/1
|
||||||
|
, channel_id/4
|
||||||
|
, parse_channel_id/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
reload_hook() ->
|
||||||
|
unload_hook(),
|
||||||
|
Bridges = emqx:get_config([bridges], #{}),
|
||||||
|
lists:foreach(fun({_Type, Bridge}) ->
|
||||||
|
lists:foreach(fun({_Name, BridgeConf}) ->
|
||||||
|
load_hook(BridgeConf)
|
||||||
|
end, maps:to_list(Bridge))
|
||||||
|
end, maps:to_list(Bridges)).
|
||||||
|
|
||||||
|
load_hook(#{egress_channels := Channels}) ->
|
||||||
|
case has_subscribe_local_topic(Channels) of
|
||||||
|
true -> ok;
|
||||||
|
false -> emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []})
|
||||||
|
end;
|
||||||
|
load_hook(_Conf) -> ok.
|
||||||
|
|
||||||
|
unload_hook() ->
|
||||||
|
ok = emqx_hooks:del('message.publish', {?MODULE, on_message_publish}).
|
||||||
|
|
||||||
|
on_message_publish(Message = #message{topic = Topic, flags = Flags}) ->
|
||||||
|
case maps:get(sys, Flags, false) of
|
||||||
|
false ->
|
||||||
|
ChannelIds = get_matched_channels(Topic),
|
||||||
|
lists:foreach(fun(ChannelId) ->
|
||||||
|
send_message(ChannelId, emqx_message:to_map(Message))
|
||||||
|
end, ChannelIds);
|
||||||
|
true -> ok
|
||||||
|
end,
|
||||||
|
{ok, Message}.
|
||||||
|
|
||||||
|
%% TODO: remove this clause, treat mqtt bridges the same as other bridges
|
||||||
|
send_message(ChannelId, Message) ->
|
||||||
|
{BridgeType, BridgeName, _, _} = parse_channel_id(ChannelId),
|
||||||
|
ResId = emqx_bridge:resource_id(BridgeType, BridgeName),
|
||||||
|
do_send_message(ResId, ChannelId, Message).
|
||||||
|
|
||||||
|
do_send_message(ResId, ChannelId, Message) ->
|
||||||
|
emqx_resource:query(ResId, {send_message, ChannelId, Message}).
|
||||||
|
|
||||||
config_key_path() ->
|
config_key_path() ->
|
||||||
[bridges].
|
[bridges].
|
||||||
|
|
||||||
resource_type(mqtt) -> emqx_connector_mqtt;
|
resource_type(mqtt) -> emqx_connector_mqtt;
|
||||||
resource_type(mysql) -> emqx_connector_mysql;
|
resource_type(http) -> emqx_connector_http.
|
||||||
resource_type(pgsql) -> emqx_connector_pgsql;
|
|
||||||
resource_type(mongo) -> emqx_connector_mongo;
|
|
||||||
resource_type(redis) -> emqx_connector_redis;
|
|
||||||
resource_type(ldap) -> emqx_connector_ldap.
|
|
||||||
|
|
||||||
bridge_type(emqx_connector_mqtt) -> mqtt;
|
bridge_type(emqx_connector_mqtt) -> mqtt;
|
||||||
bridge_type(emqx_connector_mysql) -> mysql;
|
bridge_type(emqx_connector_http) -> http.
|
||||||
bridge_type(emqx_connector_pgsql) -> pgsql;
|
|
||||||
bridge_type(emqx_connector_mongo) -> mongo;
|
|
||||||
bridge_type(emqx_connector_redis) -> redis;
|
|
||||||
bridge_type(emqx_connector_ldap) -> ldap.
|
|
||||||
|
|
||||||
post_config_update(_Req, NewConf, OldConf, _AppEnv) ->
|
post_config_update(_Req, NewConf, OldConf, _AppEnv) ->
|
||||||
#{added := Added, removed := Removed, changed := Updated}
|
#{added := Added, removed := Removed, changed := Updated}
|
||||||
|
|
@ -100,11 +140,23 @@ bridge_id(BridgeType, BridgeName) ->
|
||||||
<<Type/binary, ":", Name/binary>>.
|
<<Type/binary, ":", Name/binary>>.
|
||||||
|
|
||||||
parse_bridge_id(BridgeId) ->
|
parse_bridge_id(BridgeId) ->
|
||||||
try
|
case string:split(bin(BridgeId), ":", all) of
|
||||||
[Type, Name] = string:split(str(BridgeId), ":", leading),
|
[Type, Name] -> {binary_to_atom(Type, utf8), binary_to_atom(Name, utf8)};
|
||||||
{list_to_existing_atom(Type), list_to_atom(Name)}
|
_ -> error({invalid_bridge_id, BridgeId})
|
||||||
catch
|
end.
|
||||||
_ : _ -> error({invalid_bridge_id, BridgeId})
|
|
||||||
|
channel_id(BridgeType, BridgeName, ChannelType, ChannelName) ->
|
||||||
|
BType = bin(BridgeType),
|
||||||
|
BName = bin(BridgeName),
|
||||||
|
CType = bin(ChannelType),
|
||||||
|
CName = bin(ChannelName),
|
||||||
|
<<BType/binary, ":", BName/binary, ":", CType/binary, ":", CName/binary>>.
|
||||||
|
|
||||||
|
parse_channel_id(ChannelId) ->
|
||||||
|
case string:split(bin(ChannelId), ":", all) of
|
||||||
|
[BridgeType, BridgeName, ChannelType, ChannelName] ->
|
||||||
|
{BridgeType, BridgeName, ChannelType, ChannelName};
|
||||||
|
_ -> error({invalid_bridge_id, ChannelId})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
list_bridges() ->
|
list_bridges() ->
|
||||||
|
|
@ -137,7 +189,8 @@ restart_bridge(Type, Name) ->
|
||||||
emqx_resource:restart(resource_id(Type, Name)).
|
emqx_resource:restart(resource_id(Type, Name)).
|
||||||
|
|
||||||
create_bridge(Type, Name, Conf) ->
|
create_bridge(Type, Name, Conf) ->
|
||||||
logger:info("create ~p bridge ~p use config: ~p", [Type, Name, Conf]),
|
?SLOG(info, #{msg => "create bridge", type => Type, name => Name,
|
||||||
|
config => Conf}),
|
||||||
ResId = resource_id(Type, Name),
|
ResId = resource_id(Type, Name),
|
||||||
case emqx_resource:create(ResId,
|
case emqx_resource:create(ResId,
|
||||||
emqx_bridge:resource_type(Type), Conf) of
|
emqx_bridge:resource_type(Type), Conf) of
|
||||||
|
|
@ -158,12 +211,13 @@ update_bridge(Type, Name, {_OldConf, Conf}) ->
|
||||||
%% `egress_channels` are changed, then we should not restart the bridge, we only restart/start
|
%% `egress_channels` are changed, then we should not restart the bridge, we only restart/start
|
||||||
%% the channels.
|
%% the channels.
|
||||||
%%
|
%%
|
||||||
logger:info("update ~p bridge ~p use config: ~p", [Type, Name, Conf]),
|
?SLOG(info, #{msg => "update bridge", type => Type, name => Name,
|
||||||
|
config => Conf}),
|
||||||
emqx_resource:recreate(resource_id(Type, Name),
|
emqx_resource:recreate(resource_id(Type, Name),
|
||||||
emqx_bridge:resource_type(Type), Conf, []).
|
emqx_bridge:resource_type(Type), Conf, []).
|
||||||
|
|
||||||
remove_bridge(Type, Name, _Conf) ->
|
remove_bridge(Type, Name, _Conf) ->
|
||||||
logger:info("remove ~p bridge ~p", [Type, Name]),
|
?SLOG(info, #{msg => "remove bridge", type => Type, name => Name}),
|
||||||
case emqx_resource:remove(resource_id(Type, Name)) of
|
case emqx_resource:remove(resource_id(Type, Name)) of
|
||||||
ok -> ok;
|
ok -> ok;
|
||||||
{error, not_found} -> ok;
|
{error, not_found} -> ok;
|
||||||
|
|
@ -184,13 +238,35 @@ flatten_confs(Conf0) ->
|
||||||
do_flatten_confs(Type, Conf0) ->
|
do_flatten_confs(Type, Conf0) ->
|
||||||
[{{Type, Name}, Conf} || {Name, Conf} <- maps:to_list(Conf0)].
|
[{{Type, Name}, Conf} || {Name, Conf} <- maps:to_list(Conf0)].
|
||||||
|
|
||||||
|
has_subscribe_local_topic(Channels) ->
|
||||||
|
lists:any(fun (#{subscribe_local_topic := _}) -> true;
|
||||||
|
(_) -> false
|
||||||
|
end, maps:to_list(Channels)).
|
||||||
|
|
||||||
|
get_matched_channels(Topic) ->
|
||||||
|
Bridges = emqx:get_config([bridges], #{}),
|
||||||
|
maps:fold(fun
|
||||||
|
%% TODO: also trigger 'message.publish' for mqtt bridges.
|
||||||
|
(mqtt, _Conf, Acc0) -> Acc0;
|
||||||
|
(BType, Conf, Acc0) ->
|
||||||
|
maps:fold(fun
|
||||||
|
(BName, #{egress_channels := Channels}, Acc1) ->
|
||||||
|
do_get_matched_channels(Topic, Channels, BType, BName, egress_channels)
|
||||||
|
++ Acc1;
|
||||||
|
(_Name, _BridgeConf, Acc1) -> Acc1
|
||||||
|
end, Acc0, Conf)
|
||||||
|
end, [], Bridges).
|
||||||
|
|
||||||
|
do_get_matched_channels(Topic, Channels, BType, BName, CType) ->
|
||||||
|
maps:fold(fun
|
||||||
|
(ChannName, #{subscribe_local_topic := Filter}, Acc) ->
|
||||||
|
case emqx_topic:match(Topic, Filter) of
|
||||||
|
true -> [channel_id(BType, BName, CType, ChannName) | Acc];
|
||||||
|
false -> Acc
|
||||||
|
end;
|
||||||
|
(_ChannName, _ChannConf, Acc) -> Acc
|
||||||
|
end, [], Channels).
|
||||||
|
|
||||||
bin(Bin) when is_binary(Bin) -> Bin;
|
bin(Bin) when is_binary(Bin) -> Bin;
|
||||||
bin(Str) when is_list(Str) -> list_to_binary(Str);
|
bin(Str) when is_list(Str) -> list_to_binary(Str);
|
||||||
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
|
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
|
||||||
|
|
||||||
str(A) when is_atom(A) ->
|
|
||||||
atom_to_list(A);
|
|
||||||
str(B) when is_binary(B) ->
|
|
||||||
binary_to_list(B);
|
|
||||||
str(S) when is_list(S) ->
|
|
||||||
S.
|
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,12 @@
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
{ok, Sup} = emqx_bridge_sup:start_link(),
|
{ok, Sup} = emqx_bridge_sup:start_link(),
|
||||||
ok = emqx_bridge:load_bridges(),
|
ok = emqx_bridge:load_bridges(),
|
||||||
|
ok = emqx_bridge:reload_hook(),
|
||||||
emqx_config_handler:add_handler(emqx_bridge:config_key_path(), emqx_bridge),
|
emqx_config_handler:add_handler(emqx_bridge:config_key_path(), emqx_bridge),
|
||||||
{ok, Sup}.
|
{ok, Sup}.
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
|
ok = emqx_bridge:unload_hook(),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%% internal functions
|
%% internal functions
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
-module(emqx_bridge_schema).
|
-module(emqx_bridge_schema).
|
||||||
|
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-export([roots/0, fields/1]).
|
-export([roots/0, fields/1]).
|
||||||
|
|
||||||
%%======================================================================================
|
%%======================================================================================
|
||||||
|
|
@ -8,7 +10,16 @@
|
||||||
roots() -> [bridges].
|
roots() -> [bridges].
|
||||||
|
|
||||||
fields(bridges) ->
|
fields(bridges) ->
|
||||||
[{mqtt, hoconsc:mk(hoconsc:map(name, hoconsc:ref(?MODULE, "mqtt_bridge")))}];
|
[ {mqtt, hoconsc:mk(hoconsc:map(name, hoconsc:ref(?MODULE, "mqtt_bridge")))}
|
||||||
|
, {http, hoconsc:mk(hoconsc:map(name, hoconsc:ref(?MODULE, "http_bridge")))}
|
||||||
|
];
|
||||||
|
|
||||||
fields("mqtt_bridge") ->
|
fields("mqtt_bridge") ->
|
||||||
emqx_connector_mqtt:fields("config").
|
emqx_connector_mqtt:fields("config");
|
||||||
|
|
||||||
|
fields("http_bridge") ->
|
||||||
|
emqx_connector_http:fields(config) ++ http_channels().
|
||||||
|
|
||||||
|
http_channels() ->
|
||||||
|
[{egress_channels, hoconsc:mk(hoconsc:map(id,
|
||||||
|
hoconsc:ref(emqx_connector_http, "http_request")))}].
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([ on_start/2
|
||||||
, on_stop/2
|
, on_stop/2
|
||||||
|
|
@ -38,7 +40,7 @@
|
||||||
|
|
||||||
-export([ check_ssl_opts/2 ]).
|
-export([ check_ssl_opts/2 ]).
|
||||||
|
|
||||||
-type connect_timeout() :: non_neg_integer() | infinity.
|
-type connect_timeout() :: emqx_schema:duration() | infinity.
|
||||||
-type pool_type() :: random | hash.
|
-type pool_type() :: random | hash.
|
||||||
|
|
||||||
-reflect_type([ connect_timeout/0
|
-reflect_type([ connect_timeout/0
|
||||||
|
|
@ -50,6 +52,22 @@
|
||||||
roots() ->
|
roots() ->
|
||||||
[{config, #{type => hoconsc:ref(?MODULE, config)}}].
|
[{config, #{type => hoconsc:ref(?MODULE, config)}}].
|
||||||
|
|
||||||
|
fields("http_request") ->
|
||||||
|
[ {subscribe_local_topic, hoconsc:mk(binary())}
|
||||||
|
, {method, hoconsc:mk(method(), #{default => post})}
|
||||||
|
, {path, hoconsc:mk(binary(), #{default => <<"">>})}
|
||||||
|
, {headers, hoconsc:mk(map(),
|
||||||
|
#{default => #{
|
||||||
|
<<"accept">> => <<"application/json">>,
|
||||||
|
<<"cache-control">> => <<"no-cache">>,
|
||||||
|
<<"connection">> => <<"keep-alive">>,
|
||||||
|
<<"content-type">> => <<"application/json">>,
|
||||||
|
<<"keep-alive">> => <<"timeout=5">>}})
|
||||||
|
}
|
||||||
|
, {body, hoconsc:mk(binary(), #{default => <<"${payload}">>})}
|
||||||
|
, {request_timeout, hoconsc:mk(emqx_schema:duration_ms(), #{default => <<"30s">>})}
|
||||||
|
];
|
||||||
|
|
||||||
fields(config) ->
|
fields(config) ->
|
||||||
[ {base_url, fun base_url/1}
|
[ {base_url, fun base_url/1}
|
||||||
, {connect_timeout, fun connect_timeout/1}
|
, {connect_timeout, fun connect_timeout/1}
|
||||||
|
|
@ -60,6 +78,13 @@ fields(config) ->
|
||||||
, {enable_pipelining, fun enable_pipelining/1}
|
, {enable_pipelining, fun enable_pipelining/1}
|
||||||
] ++ emqx_connector_schema_lib:ssl_fields().
|
] ++ emqx_connector_schema_lib:ssl_fields().
|
||||||
|
|
||||||
|
method() ->
|
||||||
|
hoconsc:union([ typerefl:atom(post)
|
||||||
|
, typerefl:atom(put)
|
||||||
|
, typerefl:atom(get)
|
||||||
|
, typerefl:atom(delete)
|
||||||
|
]).
|
||||||
|
|
||||||
validations() ->
|
validations() ->
|
||||||
[ {check_ssl_opts, fun check_ssl_opts/1} ].
|
[ {check_ssl_opts, fun check_ssl_opts/1} ].
|
||||||
|
|
||||||
|
|
@ -71,16 +96,16 @@ base_url(validator) -> fun(#{query := _Query}) ->
|
||||||
end;
|
end;
|
||||||
base_url(_) -> undefined.
|
base_url(_) -> undefined.
|
||||||
|
|
||||||
connect_timeout(type) -> connect_timeout();
|
connect_timeout(type) -> emqx_schema:duration_ms();
|
||||||
connect_timeout(default) -> 5000;
|
connect_timeout(default) -> "5s";
|
||||||
connect_timeout(_) -> undefined.
|
connect_timeout(_) -> undefined.
|
||||||
|
|
||||||
max_retries(type) -> non_neg_integer();
|
max_retries(type) -> non_neg_integer();
|
||||||
max_retries(default) -> 5;
|
max_retries(default) -> 5;
|
||||||
max_retries(_) -> undefined.
|
max_retries(_) -> undefined.
|
||||||
|
|
||||||
retry_interval(type) -> non_neg_integer();
|
retry_interval(type) -> emqx_schema:duration();
|
||||||
retry_interval(default) -> 1000;
|
retry_interval(default) -> "1s";
|
||||||
retry_interval(_) -> undefined.
|
retry_interval(_) -> undefined.
|
||||||
|
|
||||||
pool_type(type) -> pool_type();
|
pool_type(type) -> pool_type();
|
||||||
|
|
@ -105,13 +130,14 @@ on_start(InstId, #{base_url := #{scheme := Scheme,
|
||||||
retry_interval := RetryInterval,
|
retry_interval := RetryInterval,
|
||||||
pool_type := PoolType,
|
pool_type := PoolType,
|
||||||
pool_size := PoolSize} = Config) ->
|
pool_size := PoolSize} = Config) ->
|
||||||
logger:info("starting http connector: ~p, config: ~p", [InstId, Config]),
|
?SLOG(info, #{msg => "starting http connector",
|
||||||
|
connector => InstId, config => Config}),
|
||||||
{Transport, TransportOpts} = case Scheme of
|
{Transport, TransportOpts} = case Scheme of
|
||||||
http ->
|
http ->
|
||||||
{tcp, []};
|
{tcp, []};
|
||||||
https ->
|
https ->
|
||||||
SSLOpts = emqx_plugin_libs_ssl:save_files_return_opts(
|
SSLOpts = emqx_plugin_libs_ssl:save_files_return_opts(
|
||||||
maps:get(ssl_opts, Config), "connectors", InstId),
|
maps:get(ssl, Config), "connectors", InstId),
|
||||||
{tls, SSLOpts}
|
{tls, SSLOpts}
|
||||||
end,
|
end,
|
||||||
NTransportOpts = emqx_misc:ipv6_probe(TransportOpts),
|
NTransportOpts = emqx_misc:ipv6_probe(TransportOpts),
|
||||||
|
|
@ -126,30 +152,51 @@ on_start(InstId, #{base_url := #{scheme := Scheme,
|
||||||
, {transport, Transport}
|
, {transport, Transport}
|
||||||
, {transport_opts, NTransportOpts}],
|
, {transport_opts, NTransportOpts}],
|
||||||
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
||||||
{ok, _} = ehttpc_sup:start_pool(PoolName, PoolOpts),
|
State = #{
|
||||||
{ok, #{pool_name => PoolName,
|
pool_name => PoolName,
|
||||||
host => Host,
|
host => Host,
|
||||||
port => Port,
|
port => Port,
|
||||||
base_path => BasePath}}.
|
base_path => BasePath,
|
||||||
|
channels => preproc_channels(InstId, Config)
|
||||||
|
},
|
||||||
|
case ehttpc_sup:start_pool(PoolName, PoolOpts) of
|
||||||
|
{ok, _} -> {ok, State};
|
||||||
|
{error, {already_started, _}} -> {ok, State};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
on_stop(InstId, #{pool_name := PoolName}) ->
|
on_stop(InstId, #{pool_name := PoolName}) ->
|
||||||
logger:info("stopping http connector: ~p", [InstId]),
|
?SLOG(info, #{msg => "stopping http connector",
|
||||||
|
connector => InstId}),
|
||||||
ehttpc_sup:stop_pool(PoolName).
|
ehttpc_sup:stop_pool(PoolName).
|
||||||
|
|
||||||
|
on_query(InstId, {send_message, ChannelId, Msg}, AfterQuery, #{channels := Channels} = State) ->
|
||||||
|
case maps:find(ChannelId, Channels) of
|
||||||
|
error -> ?SLOG(error, #{msg => "channel not found", channel_id => ChannelId});
|
||||||
|
{ok, ChannConf} ->
|
||||||
|
#{method := Method, path := Path, body := Body, headers := Headers,
|
||||||
|
request_timeout := Timeout} = proc_channel_conf(ChannConf, Msg),
|
||||||
|
on_query(InstId, {Method, {Path, Headers, Body}, Timeout}, AfterQuery, State)
|
||||||
|
end;
|
||||||
on_query(InstId, {Method, Request}, AfterQuery, State) ->
|
on_query(InstId, {Method, Request}, AfterQuery, State) ->
|
||||||
on_query(InstId, {undefined, Method, Request, 5000}, AfterQuery, State);
|
on_query(InstId, {undefined, Method, Request, 5000}, AfterQuery, State);
|
||||||
on_query(InstId, {Method, Request, Timeout}, AfterQuery, State) ->
|
on_query(InstId, {Method, Request, Timeout}, AfterQuery, State) ->
|
||||||
on_query(InstId, {undefined, Method, Request, Timeout}, AfterQuery, State);
|
on_query(InstId, {undefined, Method, Request, Timeout}, AfterQuery, State);
|
||||||
on_query(InstId, {KeyOrNum, Method, Request, Timeout}, AfterQuery, #{pool_name := PoolName,
|
on_query(InstId, {KeyOrNum, Method, Request, Timeout}, AfterQuery,
|
||||||
base_path := BasePath} = State) ->
|
#{pool_name := PoolName, base_path := BasePath} = State) ->
|
||||||
logger:debug("http connector ~p received request: ~p, at state: ~p", [InstId, Request, State]),
|
?SLOG(debug, #{msg => "http connector received request",
|
||||||
|
request => Request, connector => InstId,
|
||||||
|
state => State}),
|
||||||
NRequest = update_path(BasePath, Request),
|
NRequest = update_path(BasePath, Request),
|
||||||
case Result = ehttpc:request(case KeyOrNum of
|
case Result = ehttpc:request(case KeyOrNum of
|
||||||
undefined -> PoolName;
|
undefined -> PoolName;
|
||||||
_ -> {PoolName, KeyOrNum}
|
_ -> {PoolName, KeyOrNum}
|
||||||
end, Method, NRequest, Timeout) of
|
end, Method, NRequest, Timeout) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:debug("http connector ~p do reqeust failed, sql: ~p, reason: ~p", [InstId, NRequest, Reason]),
|
?SLOG(error, #{msg => "http connector do reqeust failed",
|
||||||
|
request => NRequest, reason => Reason,
|
||||||
|
connector => InstId}),
|
||||||
emqx_resource:query_failed(AfterQuery);
|
emqx_resource:query_failed(AfterQuery);
|
||||||
_ ->
|
_ ->
|
||||||
emqx_resource:query_success(AfterQuery)
|
emqx_resource:query_success(AfterQuery)
|
||||||
|
|
@ -169,6 +216,52 @@ on_health_check(_InstId, #{host := Host, port := Port} = State) ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
preproc_channels(<<"bridge:", BridgeId/binary>>, Config) ->
|
||||||
|
{BridgeType, BridgeName} = emqx_bridge:parse_bridge_id(BridgeId),
|
||||||
|
maps:fold(fun(ChannName, ChannConf, Acc) ->
|
||||||
|
Acc#{emqx_bridge:channel_id(BridgeType, BridgeName, egress_channels, ChannName) =>
|
||||||
|
preproc_channel_conf(ChannConf)}
|
||||||
|
end, #{}, maps:get(egress_channels, Config, #{})).
|
||||||
|
|
||||||
|
preproc_channel_conf(#{
|
||||||
|
method := Method,
|
||||||
|
path := Path,
|
||||||
|
body := Body,
|
||||||
|
headers := Headers} = Conf) ->
|
||||||
|
Conf#{ method => emqx_plugin_libs_rule:preproc_tmpl(bin(Method))
|
||||||
|
, path => emqx_plugin_libs_rule:preproc_tmpl(Path)
|
||||||
|
, body => emqx_plugin_libs_rule:preproc_tmpl(Body)
|
||||||
|
, headers => preproc_headers(Headers)
|
||||||
|
}.
|
||||||
|
|
||||||
|
preproc_headers(Headers) ->
|
||||||
|
maps:fold(fun(K, V, Acc) ->
|
||||||
|
Acc#{emqx_plugin_libs_rule:preproc_tmpl(bin(K)) =>
|
||||||
|
emqx_plugin_libs_rule:preproc_tmpl(bin(V))}
|
||||||
|
end, #{}, Headers).
|
||||||
|
|
||||||
|
proc_channel_conf(#{
|
||||||
|
method := MethodTks,
|
||||||
|
path := PathTks,
|
||||||
|
body := BodyTks,
|
||||||
|
headers := HeadersTks} = Conf, Msg) ->
|
||||||
|
Conf#{ method => make_method(emqx_plugin_libs_rule:proc_tmpl(MethodTks, Msg))
|
||||||
|
, path => emqx_plugin_libs_rule:proc_tmpl(PathTks, Msg)
|
||||||
|
, body => emqx_plugin_libs_rule:proc_tmpl(BodyTks, Msg)
|
||||||
|
, headers => maps:to_list(proc_headers(HeadersTks, Msg))
|
||||||
|
}.
|
||||||
|
|
||||||
|
proc_headers(HeaderTks, Msg) ->
|
||||||
|
maps:fold(fun(K, V, Acc) ->
|
||||||
|
Acc#{emqx_plugin_libs_rule:proc_tmpl(K, Msg) =>
|
||||||
|
emqx_plugin_libs_rule:proc_tmpl(V, Msg)}
|
||||||
|
end, #{}, HeaderTks).
|
||||||
|
|
||||||
|
make_method(M) when M == <<"POST">>; M == <<"post">> -> post;
|
||||||
|
make_method(M) when M == <<"PUT">>; M == <<"put">> -> put;
|
||||||
|
make_method(M) when M == <<"GET">>; M == <<"get">> -> get;
|
||||||
|
make_method(M) when M == <<"DELETE">>; M == <<"delete">> -> delete.
|
||||||
|
|
||||||
check_ssl_opts(Conf) ->
|
check_ssl_opts(Conf) ->
|
||||||
check_ssl_opts("base_url", Conf).
|
check_ssl_opts("base_url", Conf).
|
||||||
|
|
||||||
|
|
@ -185,3 +278,10 @@ update_path(BasePath, {Path, Headers}) ->
|
||||||
{filename:join(BasePath, Path), Headers};
|
{filename:join(BasePath, Path), Headers};
|
||||||
update_path(BasePath, {Path, Headers, Body}) ->
|
update_path(BasePath, {Path, Headers, Body}) ->
|
||||||
{filename:join(BasePath, Path), Headers, Body}.
|
{filename:join(BasePath, Path), Headers, Body}.
|
||||||
|
|
||||||
|
bin(Bin) when is_binary(Bin) ->
|
||||||
|
Bin;
|
||||||
|
bin(Str) when is_list(Str) ->
|
||||||
|
list_to_binary(Str);
|
||||||
|
bin(Atom) when is_atom(Atom) ->
|
||||||
|
atom_to_binary(Atom, utf8).
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
-include("emqx_connector.hrl").
|
-include("emqx_connector.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([roots/0, fields/1]).
|
-export([roots/0, fields/1]).
|
||||||
|
|
||||||
|
|
@ -53,7 +54,8 @@ on_start(InstId, #{servers := Servers0,
|
||||||
pool_size := PoolSize,
|
pool_size := PoolSize,
|
||||||
auto_reconnect := AutoReconn,
|
auto_reconnect := AutoReconn,
|
||||||
ssl := SSL} = Config) ->
|
ssl := SSL} = Config) ->
|
||||||
logger:info("starting ldap connector: ~p, config: ~p", [InstId, Config]),
|
?SLOG(info, #{msg => "starting ldap connector",
|
||||||
|
connector => InstId, config => Config}),
|
||||||
Servers = [begin proplists:get_value(host, S) end || S <- Servers0],
|
Servers = [begin proplists:get_value(host, S) end || S <- Servers0],
|
||||||
SslOpts = case maps:get(enable, SSL) of
|
SslOpts = case maps:get(enable, SSL) of
|
||||||
true ->
|
true ->
|
||||||
|
|
@ -75,14 +77,20 @@ on_start(InstId, #{servers := Servers0,
|
||||||
{ok, #{poolname => PoolName}}.
|
{ok, #{poolname => PoolName}}.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
logger:info("stopping ldap connector: ~p", [InstId]),
|
?SLOG(info, #{msg => "stopping ldap connector",
|
||||||
|
connector => InstId}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := PoolName} = State) ->
|
on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := PoolName} = State) ->
|
||||||
logger:debug("ldap connector ~p received request: ~p, at state: ~p", [InstId, {Base, Filter, Attributes}, State]),
|
Request = {Base, Filter, Attributes},
|
||||||
|
?SLOG(debug, #{msg => "ldap connector received request",
|
||||||
|
request => Request, connector => InstId,
|
||||||
|
state => State}),
|
||||||
case Result = ecpool:pick_and_do(PoolName, {?MODULE, search, [Base, Filter, Attributes]}, no_handover) of
|
case Result = ecpool:pick_and_do(PoolName, {?MODULE, search, [Base, Filter, Attributes]}, no_handover) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:debug("ldap connector ~p do request failed, request: ~p, reason: ~p", [InstId, {Base, Filter, Attributes}, Reason]),
|
?SLOG(error, #{msg => "ldap connector do request failed",
|
||||||
|
request => Request, connector => InstId,
|
||||||
|
reason => Reason}),
|
||||||
emqx_resource:query_failed(AfterQuery);
|
emqx_resource:query_failed(AfterQuery);
|
||||||
_ ->
|
_ ->
|
||||||
emqx_resource:query_success(AfterQuery)
|
emqx_resource:query_success(AfterQuery)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
-include("emqx_connector.hrl").
|
-include("emqx_connector.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-type server() :: emqx_schema:ip_port().
|
-type server() :: emqx_schema:ip_port().
|
||||||
-reflect_type([server/0]).
|
-reflect_type([server/0]).
|
||||||
|
|
@ -93,7 +94,8 @@ on_jsonify(Config) ->
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
on_start(InstId, Config = #{server := Server,
|
on_start(InstId, Config = #{server := Server,
|
||||||
mongo_type := single}) ->
|
mongo_type := single}) ->
|
||||||
logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]),
|
?SLOG(info, #{msg => "starting mongodb single connector",
|
||||||
|
connector => InstId, config => Config}),
|
||||||
Opts = [{type, single},
|
Opts = [{type, single},
|
||||||
{hosts, [emqx_connector_schema_lib:ip_port_to_string(Server)]}
|
{hosts, [emqx_connector_schema_lib:ip_port_to_string(Server)]}
|
||||||
],
|
],
|
||||||
|
|
@ -102,7 +104,8 @@ on_start(InstId, Config = #{server := Server,
|
||||||
on_start(InstId, Config = #{servers := Servers,
|
on_start(InstId, Config = #{servers := Servers,
|
||||||
mongo_type := rs,
|
mongo_type := rs,
|
||||||
replica_set_name := RsName}) ->
|
replica_set_name := RsName}) ->
|
||||||
logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]),
|
?SLOG(info, #{msg => "starting mongodb rs connector",
|
||||||
|
connector => InstId, config => Config}),
|
||||||
Opts = [{type, {rs, RsName}},
|
Opts = [{type, {rs, RsName}},
|
||||||
{hosts, [emqx_connector_schema_lib:ip_port_to_string(S)
|
{hosts, [emqx_connector_schema_lib:ip_port_to_string(S)
|
||||||
|| S <- Servers]}
|
|| S <- Servers]}
|
||||||
|
|
@ -111,7 +114,8 @@ on_start(InstId, Config = #{servers := Servers,
|
||||||
|
|
||||||
on_start(InstId, Config = #{servers := Servers,
|
on_start(InstId, Config = #{servers := Servers,
|
||||||
mongo_type := sharded}) ->
|
mongo_type := sharded}) ->
|
||||||
logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]),
|
?SLOG(info, #{msg => "starting mongodb sharded connector",
|
||||||
|
connector => InstId, config => Config}),
|
||||||
Opts = [{type, sharded},
|
Opts = [{type, sharded},
|
||||||
{hosts, [emqx_connector_schema_lib:ip_port_to_string(S)
|
{hosts, [emqx_connector_schema_lib:ip_port_to_string(S)
|
||||||
|| S <- Servers]}
|
|| S <- Servers]}
|
||||||
|
|
@ -119,14 +123,20 @@ on_start(InstId, Config = #{servers := Servers,
|
||||||
do_start(InstId, Opts, Config).
|
do_start(InstId, Opts, Config).
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
logger:info("stopping mongodb connector: ~p", [InstId]),
|
?SLOG(info, #{msg => "stopping mongodb connector",
|
||||||
|
connector => InstId}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {Action, Collection, Selector, Docs}, AfterQuery, #{poolname := PoolName} = State) ->
|
on_query(InstId, {Action, Collection, Selector, Docs}, AfterQuery, #{poolname := PoolName} = State) ->
|
||||||
logger:debug("mongodb connector ~p received request: ~p, at state: ~p", [InstId, {Action, Collection, Selector, Docs}, State]),
|
Request = {Action, Collection, Selector, Docs},
|
||||||
|
?SLOG(debug, #{msg => "mongodb connector received request",
|
||||||
|
request => Request, connector => InstId,
|
||||||
|
state => State}),
|
||||||
case ecpool:pick_and_do(PoolName, {?MODULE, mongo_query, [Action, Collection, Selector, Docs]}, no_handover) of
|
case ecpool:pick_and_do(PoolName, {?MODULE, mongo_query, [Action, Collection, Selector, Docs]}, no_handover) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:debug("mongodb connector ~p do sql query failed, request: ~p, reason: ~p", [InstId, {Action, Collection, Selector, Docs}, Reason]),
|
?SLOG(error, #{msg => "mongodb connector do query failed",
|
||||||
|
request => Request, reason => Reason,
|
||||||
|
connector => InstId}),
|
||||||
emqx_resource:query_failed(AfterQuery),
|
emqx_resource:query_failed(AfterQuery),
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
{ok, Cursor} when is_pid(Cursor) ->
|
{ok, Cursor} when is_pid(Cursor) ->
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-behaviour(supervisor).
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
|
@ -88,13 +89,15 @@ drop_bridge(Name) ->
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
%% When use this bridge as a data source, ?MODULE:on_message_received/2 will be called
|
%% When use this bridge as a data source, ?MODULE:on_message_received/2 will be called
|
||||||
%% if the bridge received msgs from the remote broker.
|
%% if the bridge received msgs from the remote broker.
|
||||||
on_message_received(Msg, ChannelName) ->
|
on_message_received(Msg, ChannId) ->
|
||||||
emqx:run_hook(ChannelName, [Msg]).
|
Name = atom_to_binary(ChannId, utf8),
|
||||||
|
emqx:run_hook(<<"$bridges/", Name/binary>>, [Msg]).
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
on_start(InstId, Conf) ->
|
on_start(InstId, Conf) ->
|
||||||
logger:info("starting mqtt connector: ~p, ~p", [InstId, Conf]),
|
?SLOG(info, #{msg => "starting mqtt connector",
|
||||||
NamePrefix = binary_to_list(InstId),
|
connector => InstId, config => Conf}),
|
||||||
|
"bridge:" ++ NamePrefix = binary_to_list(InstId),
|
||||||
BasicConf = basic_config(Conf),
|
BasicConf = basic_config(Conf),
|
||||||
InitRes = {ok, #{name_prefix => NamePrefix, baisc_conf => BasicConf, channels => []}},
|
InitRes = {ok, #{name_prefix => NamePrefix, baisc_conf => BasicConf, channels => []}},
|
||||||
InOutConfigs = taged_map_list(ingress_channels, maps:get(ingress_channels, Conf, #{}))
|
InOutConfigs = taged_map_list(ingress_channels, maps:get(ingress_channels, Conf, #{}))
|
||||||
|
|
@ -110,7 +113,8 @@ on_start(InstId, Conf) ->
|
||||||
end, InitRes, InOutConfigs).
|
end, InitRes, InOutConfigs).
|
||||||
|
|
||||||
on_stop(InstId, #{channels := NameList}) ->
|
on_stop(InstId, #{channels := NameList}) ->
|
||||||
logger:info("stopping mqtt connector: ~p", [InstId]),
|
?SLOG(info, #{msg => "stopping mqtt connector",
|
||||||
|
connector => InstId}),
|
||||||
lists:foreach(fun(Name) ->
|
lists:foreach(fun(Name) ->
|
||||||
remove_channel(Name)
|
remove_channel(Name)
|
||||||
end, NameList).
|
end, NameList).
|
||||||
|
|
@ -120,9 +124,10 @@ on_stop(InstId, #{channels := NameList}) ->
|
||||||
on_query(_InstId, {create_channel, Conf}, _AfterQuery, #{name_prefix := Prefix,
|
on_query(_InstId, {create_channel, Conf}, _AfterQuery, #{name_prefix := Prefix,
|
||||||
baisc_conf := BasicConf}) ->
|
baisc_conf := BasicConf}) ->
|
||||||
create_channel(Conf, Prefix, BasicConf);
|
create_channel(Conf, Prefix, BasicConf);
|
||||||
on_query(_InstId, {send_to_remote, ChannelName, Msg}, _AfterQuery, _State) ->
|
on_query(_InstId, {send_message, ChannelId, Msg}, _AfterQuery, _State) ->
|
||||||
logger:debug("send msg to remote node on channel: ~p, msg: ~p", [ChannelName, Msg]),
|
?SLOG(debug, #{msg => "send msg to remote node", message => Msg,
|
||||||
emqx_connector_mqtt_worker:send_to_remote(ChannelName, Msg).
|
channel_id => ChannelId}),
|
||||||
|
emqx_connector_mqtt_worker:send_to_remote(ChannelId, Msg).
|
||||||
|
|
||||||
on_health_check(_InstId, #{channels := NameList} = State) ->
|
on_health_check(_InstId, #{channels := NameList} = State) ->
|
||||||
Results = [{Name, emqx_connector_mqtt_worker:ping(Name)} || Name <- NameList],
|
Results = [{Name, emqx_connector_mqtt_worker:ping(Name)} || Name <- NameList],
|
||||||
|
|
@ -134,35 +139,43 @@ on_health_check(_InstId, #{channels := NameList} = State) ->
|
||||||
create_channel({{ingress_channels, Id}, #{subscribe_remote_topic := RemoteT} = Conf},
|
create_channel({{ingress_channels, Id}, #{subscribe_remote_topic := RemoteT} = Conf},
|
||||||
NamePrefix, BasicConf) ->
|
NamePrefix, BasicConf) ->
|
||||||
LocalT = maps:get(local_topic, Conf, undefined),
|
LocalT = maps:get(local_topic, Conf, undefined),
|
||||||
Name = ingress_channel_name(NamePrefix, Id),
|
ChannId = ingress_channel_id(NamePrefix, Id),
|
||||||
logger:info("creating ingress channel ~p, remote ~s -> local ~s", [Name, RemoteT, LocalT]),
|
?SLOG(info, #{msg => "creating ingress channel",
|
||||||
|
remote_topic => RemoteT,
|
||||||
|
local_topic => LocalT,
|
||||||
|
channel_id => ChannId}),
|
||||||
do_create_channel(BasicConf#{
|
do_create_channel(BasicConf#{
|
||||||
name => Name,
|
name => ChannId,
|
||||||
clientid => clientid(Name),
|
clientid => clientid(ChannId),
|
||||||
subscriptions => Conf#{
|
subscriptions => Conf#{
|
||||||
local_topic => LocalT,
|
local_topic => LocalT,
|
||||||
on_message_received => {fun ?MODULE:on_message_received/2, [Name]}
|
on_message_received => {fun ?MODULE:on_message_received/2, [ChannId]}
|
||||||
},
|
},
|
||||||
forwards => undefined});
|
forwards => undefined});
|
||||||
|
|
||||||
create_channel({{egress_channels, Id}, #{remote_topic := RemoteT} = Conf},
|
create_channel({{egress_channels, Id}, #{remote_topic := RemoteT} = Conf},
|
||||||
NamePrefix, BasicConf) ->
|
NamePrefix, BasicConf) ->
|
||||||
LocalT = maps:get(subscribe_local_topic, Conf, undefined),
|
LocalT = maps:get(subscribe_local_topic, Conf, undefined),
|
||||||
Name = egress_channel_name(NamePrefix, Id),
|
ChannId = egress_channel_id(NamePrefix, Id),
|
||||||
logger:info("creating egress channel ~p, local ~s -> remote ~s", [Name, LocalT, RemoteT]),
|
?SLOG(info, #{msg => "creating egress channel",
|
||||||
|
remote_topic => RemoteT,
|
||||||
|
local_topic => LocalT,
|
||||||
|
channel_id => ChannId}),
|
||||||
do_create_channel(BasicConf#{
|
do_create_channel(BasicConf#{
|
||||||
name => Name,
|
name => ChannId,
|
||||||
clientid => clientid(Name),
|
clientid => clientid(ChannId),
|
||||||
subscriptions => undefined,
|
subscriptions => undefined,
|
||||||
forwards => Conf#{subscribe_local_topic => LocalT}}).
|
forwards => Conf#{subscribe_local_topic => LocalT}}).
|
||||||
|
|
||||||
remove_channel(ChannelName) ->
|
remove_channel(ChannId) ->
|
||||||
logger:info("removing channel ~p", [ChannelName]),
|
?SLOG(info, #{msg => "removing channel",
|
||||||
case ?MODULE:drop_bridge(ChannelName) of
|
channel_id => ChannId}),
|
||||||
|
case ?MODULE:drop_bridge(ChannId) of
|
||||||
ok -> ok;
|
ok -> ok;
|
||||||
{error, not_found} -> ok;
|
{error, not_found} -> ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:error("stop channel ~p failed, error: ~p", [ChannelName, Reason])
|
?SLOG(error, #{msg => "stop channel failed",
|
||||||
|
channel_id => ChannId, reason => Reason})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_create_channel(#{name := Name} = Conf) ->
|
do_create_channel(#{name := Name} = Conf) ->
|
||||||
|
|
@ -215,9 +228,9 @@ basic_config(#{
|
||||||
taged_map_list(Tag, Map) ->
|
taged_map_list(Tag, Map) ->
|
||||||
[{{Tag, K}, V} || {K, V} <- maps:to_list(Map)].
|
[{{Tag, K}, V} || {K, V} <- maps:to_list(Map)].
|
||||||
|
|
||||||
ingress_channel_name(Prefix, Id) ->
|
ingress_channel_id(Prefix, Id) ->
|
||||||
channel_name("ingress_channels", Prefix, Id).
|
channel_name("ingress_channels", Prefix, Id).
|
||||||
egress_channel_name(Prefix, Id) ->
|
egress_channel_id(Prefix, Id) ->
|
||||||
channel_name("egress_channels", Prefix, Id).
|
channel_name("egress_channels", Prefix, Id).
|
||||||
|
|
||||||
channel_name(Type, Prefix, Id) ->
|
channel_name(Type, Prefix, Id) ->
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([ on_start/2
|
||||||
|
|
@ -54,7 +55,8 @@ on_start(InstId, #{server := {Host, Port},
|
||||||
auto_reconnect := AutoReconn,
|
auto_reconnect := AutoReconn,
|
||||||
pool_size := PoolSize,
|
pool_size := PoolSize,
|
||||||
ssl := SSL } = Config) ->
|
ssl := SSL } = Config) ->
|
||||||
logger:info("starting mysql connector: ~p, config: ~p", [InstId, Config]),
|
?SLOG(info, #{msg => "starting mysql connector",
|
||||||
|
connector => InstId, config => Config}),
|
||||||
SslOpts = case maps:get(enable, SSL) of
|
SslOpts = case maps:get(enable, SSL) of
|
||||||
true ->
|
true ->
|
||||||
[{ssl, [{server_name_indication, disable} |
|
[{ssl, [{server_name_indication, disable} |
|
||||||
|
|
@ -73,7 +75,8 @@ on_start(InstId, #{server := {Host, Port},
|
||||||
{ok, #{poolname => PoolName}}.
|
{ok, #{poolname => PoolName}}.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
logger:info("stopping mysql connector: ~p", [InstId]),
|
?SLOG(info, #{msg => "stopping mysql connector",
|
||||||
|
connector => InstId}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {sql, SQL}, AfterQuery, #{poolname := _PoolName} = State) ->
|
on_query(InstId, {sql, SQL}, AfterQuery, #{poolname := _PoolName} = State) ->
|
||||||
|
|
@ -81,10 +84,12 @@ on_query(InstId, {sql, SQL}, AfterQuery, #{poolname := _PoolName} = State) ->
|
||||||
on_query(InstId, {sql, SQL, Params}, AfterQuery, #{poolname := _PoolName} = State) ->
|
on_query(InstId, {sql, SQL, Params}, AfterQuery, #{poolname := _PoolName} = State) ->
|
||||||
on_query(InstId, {sql, SQL, Params, default_timeout}, AfterQuery, State);
|
on_query(InstId, {sql, SQL, Params, default_timeout}, AfterQuery, State);
|
||||||
on_query(InstId, {sql, SQL, Params, Timeout}, AfterQuery, #{poolname := PoolName} = State) ->
|
on_query(InstId, {sql, SQL, Params, Timeout}, AfterQuery, #{poolname := PoolName} = State) ->
|
||||||
logger:debug("mysql connector ~p received sql query: ~p, at state: ~p", [InstId, SQL, State]),
|
?SLOG(debug, #{msg => "mysql connector received sql query",
|
||||||
|
connector => InstId, sql => SQL, state => State}),
|
||||||
case Result = ecpool:pick_and_do(PoolName, {mysql, query, [SQL, Params, Timeout]}, no_handover) of
|
case Result = ecpool:pick_and_do(PoolName, {mysql, query, [SQL, Params, Timeout]}, no_handover) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:debug("mysql connector ~p do sql query failed, sql: ~p, reason: ~p", [InstId, SQL, Reason]),
|
?SLOG(error, #{msg => "mysql connector do sql query failed",
|
||||||
|
connector => InstId, sql => SQL, reason => Reason}),
|
||||||
emqx_resource:query_failed(AfterQuery);
|
emqx_resource:query_failed(AfterQuery);
|
||||||
_ ->
|
_ ->
|
||||||
emqx_resource:query_success(AfterQuery)
|
emqx_resource:query_success(AfterQuery)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([roots/0, fields/1]).
|
-export([roots/0, fields/1]).
|
||||||
|
|
||||||
|
|
@ -54,7 +55,8 @@ on_start(InstId, #{server := {Host, Port},
|
||||||
auto_reconnect := AutoReconn,
|
auto_reconnect := AutoReconn,
|
||||||
pool_size := PoolSize,
|
pool_size := PoolSize,
|
||||||
ssl := SSL } = Config) ->
|
ssl := SSL } = Config) ->
|
||||||
logger:info("starting postgresql connector: ~p, config: ~p", [InstId, Config]),
|
?SLOG(info, #{msg => "starting postgresql connector",
|
||||||
|
connector => InstId, config => Config}),
|
||||||
SslOpts = case maps:get(enable, SSL) of
|
SslOpts = case maps:get(enable, SSL) of
|
||||||
true ->
|
true ->
|
||||||
[{ssl, [{server_name_indication, disable} |
|
[{ssl, [{server_name_indication, disable} |
|
||||||
|
|
@ -73,16 +75,20 @@ on_start(InstId, #{server := {Host, Port},
|
||||||
{ok, #{poolname => PoolName}}.
|
{ok, #{poolname => PoolName}}.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
logger:info("stopping postgresql connector: ~p", [InstId]),
|
?SLOG(info, #{msg => "stopping postgresql connector",
|
||||||
|
connector => InstId}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {sql, SQL}, AfterQuery, #{poolname := _PoolName} = State) ->
|
on_query(InstId, {sql, SQL}, AfterQuery, #{poolname := _PoolName} = State) ->
|
||||||
on_query(InstId, {sql, SQL, []}, AfterQuery, State);
|
on_query(InstId, {sql, SQL, []}, AfterQuery, State);
|
||||||
on_query(InstId, {sql, SQL, Params}, AfterQuery, #{poolname := PoolName} = State) ->
|
on_query(InstId, {sql, SQL, Params}, AfterQuery, #{poolname := PoolName} = State) ->
|
||||||
logger:debug("postgresql connector ~p received sql query: ~p, at state: ~p", [InstId, SQL, State]),
|
?SLOG(debug, #{msg => "postgresql connector received sql query",
|
||||||
|
connector => InstId, sql => SQL, state => State}),
|
||||||
case Result = ecpool:pick_and_do(PoolName, {?MODULE, query, [SQL, Params]}, no_handover) of
|
case Result = ecpool:pick_and_do(PoolName, {?MODULE, query, [SQL, Params]}, no_handover) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:debug("postgresql connector ~p do sql query failed, sql: ~p, reason: ~p", [InstId, SQL, Reason]),
|
?SLOG(error, #{
|
||||||
|
msg => "postgresql connector do sql query failed",
|
||||||
|
connector => InstId, sql => SQL, reason => Reason}),
|
||||||
emqx_resource:query_failed(AfterQuery);
|
emqx_resource:query_failed(AfterQuery);
|
||||||
_ ->
|
_ ->
|
||||||
emqx_resource:query_success(AfterQuery)
|
emqx_resource:query_success(AfterQuery)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
-include("emqx_connector.hrl").
|
-include("emqx_connector.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-type server() :: tuple().
|
-type server() :: tuple().
|
||||||
|
|
||||||
|
|
@ -85,7 +86,8 @@ on_start(InstId, #{redis_type := Type,
|
||||||
pool_size := PoolSize,
|
pool_size := PoolSize,
|
||||||
auto_reconnect := AutoReconn,
|
auto_reconnect := AutoReconn,
|
||||||
ssl := SSL } = Config) ->
|
ssl := SSL } = Config) ->
|
||||||
logger:info("starting redis connector: ~p, config: ~p", [InstId, Config]),
|
?SLOG(info, #{msg => "starting redis connector",
|
||||||
|
connector => InstId, config => Config}),
|
||||||
Servers = case Type of
|
Servers = case Type of
|
||||||
single -> [{servers, [maps:get(server, Config)]}];
|
single -> [{servers, [maps:get(server, Config)]}];
|
||||||
_ ->[{servers, maps:get(servers, Config)}]
|
_ ->[{servers, maps:get(servers, Config)}]
|
||||||
|
|
@ -116,18 +118,21 @@ on_start(InstId, #{redis_type := Type,
|
||||||
{ok, #{poolname => PoolName, type => Type}}.
|
{ok, #{poolname => PoolName, type => Type}}.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
logger:info("stopping redis connector: ~p", [InstId]),
|
?SLOG(info, #{msg => "stopping redis connector",
|
||||||
|
connector => InstId}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := Type} = State) ->
|
on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := Type} = State) ->
|
||||||
logger:debug("redis connector ~p received cmd query: ~p, at state: ~p", [InstId, Command, State]),
|
?SLOG(debug, #{msg => "redis connector received cmd query",
|
||||||
|
connector => InstId, sql => Command, state => State}),
|
||||||
Result = case Type of
|
Result = case Type of
|
||||||
cluster -> eredis_cluster:q(PoolName, Command);
|
cluster -> eredis_cluster:q(PoolName, Command);
|
||||||
_ -> ecpool:pick_and_do(PoolName, {?MODULE, cmd, [Type, Command]}, no_handover)
|
_ -> ecpool:pick_and_do(PoolName, {?MODULE, cmd, [Type, Command]}, no_handover)
|
||||||
end,
|
end,
|
||||||
case Result of
|
case Result of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:debug("redis connector ~p do cmd query failed, cmd: ~p, reason: ~p", [InstId, Command, Reason]),
|
?SLOG(error, #{msg => "redis connector do cmd query failed",
|
||||||
|
connector => InstId, sql => Command, reason => Reason}),
|
||||||
emqx_resource:query_failed(AfterCommand);
|
emqx_resource:query_failed(AfterCommand);
|
||||||
_ ->
|
_ ->
|
||||||
emqx_resource:query_success(AfterCommand)
|
emqx_resource:query_success(AfterCommand)
|
||||||
|
|
|
||||||
|
|
@ -155,14 +155,18 @@ handle_puback(#{packet_id := PktId, reason_code := RC}, Parent)
|
||||||
RC =:= ?RC_NO_MATCHING_SUBSCRIBERS ->
|
RC =:= ?RC_NO_MATCHING_SUBSCRIBERS ->
|
||||||
Parent ! {batch_ack, PktId}, ok;
|
Parent ! {batch_ack, PktId}, ok;
|
||||||
handle_puback(#{packet_id := PktId, reason_code := RC}, _Parent) ->
|
handle_puback(#{packet_id := PktId, reason_code := RC}, _Parent) ->
|
||||||
?LOG(warning, "publish ~p to remote node falied, reason_code: ~p", [PktId, RC]).
|
?SLOG(warning, #{msg => "publish to remote node falied",
|
||||||
|
packet_id => PktId, reason_code => RC}).
|
||||||
|
|
||||||
handle_publish(Msg, undefined) ->
|
handle_publish(Msg, undefined) ->
|
||||||
?LOG(error, "cannot publish to local broker as 'bridge.mqtt.<name>.in' not configured, msg: ~p", [Msg]);
|
?SLOG(error, #{msg => "cannot publish to local broker as"
|
||||||
|
" ingress_channles' is not configured",
|
||||||
|
message => Msg});
|
||||||
handle_publish(Msg, #{on_message_received := {OnMsgRcvdFunc, Args}} = Vars) ->
|
handle_publish(Msg, #{on_message_received := {OnMsgRcvdFunc, Args}} = Vars) ->
|
||||||
?LOG(debug, "publish to local broker, msg: ~p, vars: ~p", [Msg, Vars]),
|
?SLOG(debug, #{msg => "publish to local broker",
|
||||||
|
message => Msg, vars => Vars}),
|
||||||
emqx_metrics:inc('bridge.mqtt.message_received_from_remote', 1),
|
emqx_metrics:inc('bridge.mqtt.message_received_from_remote', 1),
|
||||||
_ = erlang:apply(OnMsgRcvdFunc, [Msg, Args]),
|
_ = erlang:apply(OnMsgRcvdFunc, [Msg | Args]),
|
||||||
case maps:get(local_topic, Vars, undefined) of
|
case maps:get(local_topic, Vars, undefined) of
|
||||||
undefined -> ok;
|
undefined -> ok;
|
||||||
_Topic ->
|
_Topic ->
|
||||||
|
|
|
||||||
|
|
@ -23,19 +23,21 @@
|
||||||
-export([ roots/0
|
-export([ roots/0
|
||||||
, fields/1]).
|
, fields/1]).
|
||||||
|
|
||||||
|
-import(emqx_schema, [mk_duration/2]).
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[{config, #{type => hoconsc:ref(?MODULE, "config")}}].
|
[{config, #{type => hoconsc:ref(?MODULE, "config")}}].
|
||||||
|
|
||||||
fields("config") ->
|
fields("config") ->
|
||||||
[ {server, hoconsc:mk(emqx_schema:ip_port(), #{default => "127.0.0.1:1883"})}
|
[ {server, hoconsc:mk(emqx_schema:ip_port(), #{default => "127.0.0.1:1883"})}
|
||||||
, {reconnect_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})}
|
, {reconnect_interval, mk_duration("reconnect interval", #{default => "30s"})}
|
||||||
, {proto_ver, fun proto_ver/1}
|
, {proto_ver, fun proto_ver/1}
|
||||||
, {bridge_mode, hoconsc:mk(boolean(), #{default => true})}
|
, {bridge_mode, hoconsc:mk(boolean(), #{default => true})}
|
||||||
, {username, hoconsc:mk(string())}
|
, {username, hoconsc:mk(string())}
|
||||||
, {password, hoconsc:mk(string())}
|
, {password, hoconsc:mk(string())}
|
||||||
, {clean_start, hoconsc:mk(boolean(), #{default => true})}
|
, {clean_start, hoconsc:mk(boolean(), #{default => true})}
|
||||||
, {keepalive, hoconsc:mk(integer(), #{default => 300})}
|
, {keepalive, mk_duration("keepalive", #{default => "300s"})}
|
||||||
, {retry_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})}
|
, {retry_interval, mk_duration("retry interval", #{default => "30s"})}
|
||||||
, {max_inflight, hoconsc:mk(integer(), #{default => 32})}
|
, {max_inflight, hoconsc:mk(integer(), #{default => 32})}
|
||||||
, {replayq, hoconsc:mk(hoconsc:ref(?MODULE, "replayq"))}
|
, {replayq, hoconsc:mk(hoconsc:ref(?MODULE, "replayq"))}
|
||||||
, {ingress_channels, hoconsc:mk(hoconsc:map(id, hoconsc:ref(?MODULE, "ingress_channels")), #{default => []})}
|
, {ingress_channels, hoconsc:mk(hoconsc:map(id, hoconsc:ref(?MODULE, "ingress_channels")), #{default => []})}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
-behaviour(gen_statem).
|
-behaviour(gen_statem).
|
||||||
|
|
||||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
%% APIs
|
%% APIs
|
||||||
-export([ start_link/1
|
-export([ start_link/1
|
||||||
|
|
@ -189,7 +190,8 @@ callback_mode() -> [state_functions].
|
||||||
|
|
||||||
%% @doc Config should be a map().
|
%% @doc Config should be a map().
|
||||||
init(#{name := Name} = ConnectOpts) ->
|
init(#{name := Name} = ConnectOpts) ->
|
||||||
?LOG(debug, "starting bridge worker for ~p", [Name]),
|
?SLOG(debug, #{msg => "starting bridge worker",
|
||||||
|
name => Name}),
|
||||||
erlang:process_flag(trap_exit, true),
|
erlang:process_flag(trap_exit, true),
|
||||||
Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})),
|
Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})),
|
||||||
State = init_state(ConnectOpts),
|
State = init_state(ConnectOpts),
|
||||||
|
|
@ -335,8 +337,9 @@ common(_StateName, cast, {send_to_remote, Msg}, #{replayq := Q} = State) ->
|
||||||
NewQ = replayq:append(Q, [Msg]),
|
NewQ = replayq:append(Q, [Msg]),
|
||||||
{keep_state, State#{replayq => NewQ}, {next_event, internal, maybe_send}};
|
{keep_state, State#{replayq => NewQ}, {next_event, internal, maybe_send}};
|
||||||
common(StateName, Type, Content, #{name := Name} = State) ->
|
common(StateName, Type, Content, #{name := Name} = State) ->
|
||||||
?LOG(notice, "Bridge ~p discarded ~p type event at state ~p:~p",
|
?SLOG(notice, #{msg => "Bridge discarded event",
|
||||||
[Name, Type, StateName, Content]),
|
name => Name, type => Type, state_name => StateName,
|
||||||
|
content => Content}),
|
||||||
{keep_state, State}.
|
{keep_state, State}.
|
||||||
|
|
||||||
do_connect(#{connect_opts := ConnectOpts = #{forwards := Forwards},
|
do_connect(#{connect_opts := ConnectOpts = #{forwards := Forwards},
|
||||||
|
|
@ -352,8 +355,8 @@ do_connect(#{connect_opts := ConnectOpts = #{forwards := Forwards},
|
||||||
{ok, State#{connection => Conn}};
|
{ok, State#{connection => Conn}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
ConnectOpts1 = obfuscate(ConnectOpts),
|
ConnectOpts1 = obfuscate(ConnectOpts),
|
||||||
?LOG(error, "Failed to connect \n"
|
?SLOG(error, #{msg => "Failed to connect",
|
||||||
"config=~p\nreason:~p", [ConnectOpts1, Reason]),
|
config => ConnectOpts1, reason => Reason}),
|
||||||
{error, Reason, State}
|
{error, Reason, State}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -399,7 +402,9 @@ pop_and_send_loop(#{replayq := Q} = State, N) ->
|
||||||
|
|
||||||
%% Assert non-empty batch because we have a is_empty check earlier.
|
%% Assert non-empty batch because we have a is_empty check earlier.
|
||||||
do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Batch) ->
|
do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Batch) ->
|
||||||
?LOG(error, "cannot forward messages to remote broker as 'bridge.mqtt.<name>.in' not configured, msg: ~p", [Batch]);
|
?SLOG(error, #{msg => "cannot forward messages to remote broker"
|
||||||
|
" as egress_channel is not configured",
|
||||||
|
messages => Batch});
|
||||||
do_send(#{inflight := Inflight,
|
do_send(#{inflight := Inflight,
|
||||||
connection := Connection,
|
connection := Connection,
|
||||||
mountpoint := Mountpoint,
|
mountpoint := Mountpoint,
|
||||||
|
|
@ -409,14 +414,16 @@ do_send(#{inflight := Inflight,
|
||||||
emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'),
|
emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'),
|
||||||
emqx_connector_mqtt_msg:to_remote_msg(Message, Vars)
|
emqx_connector_mqtt_msg:to_remote_msg(Message, Vars)
|
||||||
end,
|
end,
|
||||||
?LOG(debug, "publish to remote broker, msg: ~p, vars: ~p", [Batch, Vars]),
|
?SLOG(debug, #{msg => "publish to remote broker",
|
||||||
|
message => Batch, vars => Vars}),
|
||||||
case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(M) || M <- Batch]) of
|
case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(M) || M <- Batch]) of
|
||||||
{ok, Refs} ->
|
{ok, Refs} ->
|
||||||
{ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef,
|
{ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef,
|
||||||
send_ack_ref => map_set(Refs),
|
send_ack_ref => map_set(Refs),
|
||||||
batch => Batch}]}};
|
batch => Batch}]}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(info, "mqtt_bridge_produce_failed ~p", [Reason]),
|
?SLOG(info, #{msg => "mqtt_bridge_produce_failed",
|
||||||
|
reason => Reason}),
|
||||||
{error, State}
|
{error, State}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -436,7 +443,8 @@ handle_batch_ack(#{inflight := Inflight0, replayq := Q} = State, Ref) ->
|
||||||
State#{inflight := Inflight}.
|
State#{inflight := Inflight}.
|
||||||
|
|
||||||
do_ack([], Ref) ->
|
do_ack([], Ref) ->
|
||||||
?LOG(debug, "stale_batch_ack_reference ~p", [Ref]),
|
?SLOG(debug, #{msg => "stale_batch_ack_reference",
|
||||||
|
ref => Ref}),
|
||||||
[];
|
[];
|
||||||
do_ack([#{send_ack_ref := Refs} = First | Rest], Ref) ->
|
do_ack([#{send_ack_ref := Refs} = First | Rest], Ref) ->
|
||||||
case maps:is_key(Ref, Refs) of
|
case maps:is_key(Ref, Refs) of
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,10 @@ update_pwd(Username, Fun) ->
|
||||||
|
|
||||||
|
|
||||||
-spec(lookup_user(binary()) -> [mqtt_admin()]).
|
-spec(lookup_user(binary()) -> [mqtt_admin()]).
|
||||||
lookup_user(Username) when is_binary(Username) -> mnesia:dirty_read(mqtt_admin, Username).
|
lookup_user(Username) when is_binary(Username) ->
|
||||||
|
Fun = fun() -> mnesia:read(mqtt_admin, Username) end,
|
||||||
|
{atomic, User} = ekka_mnesia:ro_transaction(?DASHBOARD_SHARD, Fun),
|
||||||
|
User.
|
||||||
|
|
||||||
-spec(all_users() -> [#mqtt_admin{}]).
|
-spec(all_users() -> [#mqtt_admin{}]).
|
||||||
all_users() -> ets:tab2list(mqtt_admin).
|
all_users() -> ets:tab2list(mqtt_admin).
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,8 @@ flush({Connection, Route, Subscription}, {Received0, Sent0, Dropped0}) ->
|
||||||
diff(Sent, Sent0),
|
diff(Sent, Sent0),
|
||||||
diff(Dropped, Dropped0)},
|
diff(Dropped, Dropped0)},
|
||||||
Ts = get_local_time(),
|
Ts = get_local_time(),
|
||||||
_ = mnesia:dirty_write(emqx_collect, #mqtt_collect{timestamp = Ts, collect = Collect}),
|
ekka_mnesia:transaction(ekka_mnesia:local_content_shard(),
|
||||||
|
fun mnesia:write/1, [#mqtt_collect{timestamp = Ts, collect = Collect}]),
|
||||||
{Received, Sent, Dropped}.
|
{Received, Sent, Dropped}.
|
||||||
|
|
||||||
avg(Items) ->
|
avg(Items) ->
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@
|
||||||
%% API
|
%% API
|
||||||
-export([spec/1, spec/2]).
|
-export([spec/1, spec/2]).
|
||||||
-export([translate_req/2]).
|
-export([translate_req/2]).
|
||||||
|
-export([namespace/0, fields/1]).
|
||||||
|
-export([error_codes/1, error_codes/2]).
|
||||||
|
-define(MAX_ROW_LIMIT, 100).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
@ -22,7 +27,8 @@
|
||||||
-define(INIT_SCHEMA, #{fields => #{}, translations => #{}, validations => [], namespace => undefined}).
|
-define(INIT_SCHEMA, #{fields => #{}, translations => #{}, validations => [], namespace => undefined}).
|
||||||
|
|
||||||
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
||||||
-define(TO_COMPONENTS(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>, ?TO_REF(namespace(_M_), _F_)])).
|
-define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>, ?TO_REF(namespace(_M_), _F_)])).
|
||||||
|
-define(TO_COMPONENTS_PARAM(_M_, _F_), iolist_to_binary([<<"#/components/parameters/">>, ?TO_REF(namespace(_M_), _F_)])).
|
||||||
|
|
||||||
%% @equiv spec(Module, #{check_schema => false})
|
%% @equiv spec(Module, #{check_schema => false})
|
||||||
-spec(spec(module()) ->
|
-spec(spec(module()) ->
|
||||||
|
|
@ -54,7 +60,6 @@ spec(Module, Options) ->
|
||||||
end, {[], []}, Paths),
|
end, {[], []}, Paths),
|
||||||
{ApiSpec, components(lists:usort(AllRefs))}.
|
{ApiSpec, components(lists:usort(AllRefs))}.
|
||||||
|
|
||||||
|
|
||||||
-spec(translate_req(#{binding => list(), query_string => list(), body => map()},
|
-spec(translate_req(#{binding => list(), query_string => list(), body => map()},
|
||||||
#{module => module(), path => string(), method => atom()}) ->
|
#{module => module(), path => string(), method => atom()}) ->
|
||||||
{ok, #{binding => list(), query_string => list(), body => map()}}|
|
{ok, #{binding => list(), query_string => list(), body => map()}}|
|
||||||
|
|
@ -64,7 +69,7 @@ translate_req(Request, #{module := Module, path := Path, method := Method}) ->
|
||||||
try
|
try
|
||||||
Params = maps:get(parameters, Spec, []),
|
Params = maps:get(parameters, Spec, []),
|
||||||
Body = maps:get(requestBody, Spec, []),
|
Body = maps:get(requestBody, Spec, []),
|
||||||
{Bindings, QueryStr} = check_parameters(Request, Params),
|
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
||||||
NewBody = check_requestBody(Request, Body, Module, hoconsc:is_schema(Body)),
|
NewBody = check_requestBody(Request, Body, Module, hoconsc:is_schema(Body)),
|
||||||
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
||||||
catch throw:Error ->
|
catch throw:Error ->
|
||||||
|
|
@ -73,6 +78,30 @@ translate_req(Request, #{module := Module, path := Path, method := Method}) ->
|
||||||
{400, 'BAD_REQUEST', iolist_to_binary(io_lib:format("~s : ~p", [Key, Reason]))}
|
{400, 'BAD_REQUEST', iolist_to_binary(io_lib:format("~s : ~p", [Key, Reason]))}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
namespace() -> "public".
|
||||||
|
|
||||||
|
fields(page) ->
|
||||||
|
Desc = <<"Page number of the results to fetch.">>,
|
||||||
|
Meta = #{in => query, desc => Desc, default => 1, example => 1},
|
||||||
|
[{page, hoconsc:mk(integer(), Meta)}];
|
||||||
|
fields(limit) ->
|
||||||
|
Desc = iolist_to_binary([<<"Results per page(max ">>,
|
||||||
|
integer_to_binary(?MAX_ROW_LIMIT), <<")">>]),
|
||||||
|
Meta = #{in => query, desc => Desc, default => ?MAX_ROW_LIMIT, example => 50},
|
||||||
|
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}].
|
||||||
|
|
||||||
|
error_codes(Codes) ->
|
||||||
|
error_codes(Codes, <<"Error code to troubleshoot problems.">>).
|
||||||
|
|
||||||
|
error_codes(Codes = [_ | _], MsgExample) ->
|
||||||
|
[
|
||||||
|
{code, hoconsc:mk(hoconsc:enum(Codes))},
|
||||||
|
{message, hoconsc:mk(string(), #{
|
||||||
|
desc => <<"Details description of the error.">>,
|
||||||
|
example => MsgExample
|
||||||
|
})}
|
||||||
|
].
|
||||||
|
|
||||||
support_check_schema(#{check_schema := true}) -> ?DEFAULT_FILTER;
|
support_check_schema(#{check_schema := true}) -> ?DEFAULT_FILTER;
|
||||||
support_check_schema(#{check_schema := Func})when is_function(Func, 2) -> #{filter => Func};
|
support_check_schema(#{check_schema := Func})when is_function(Func, 2) -> #{filter => Func};
|
||||||
support_check_schema(_) -> #{filter => undefined}.
|
support_check_schema(_) -> #{filter => undefined}.
|
||||||
|
|
@ -93,23 +122,28 @@ parse_spec_ref(Module, Path) ->
|
||||||
maps:without([operationId], Schema)),
|
maps:without([operationId], Schema)),
|
||||||
{maps:get(operationId, Schema), Specs, Refs}.
|
{maps:get(operationId, Schema), Specs, Refs}.
|
||||||
|
|
||||||
check_parameters(Request, Spec) ->
|
check_parameters(Request, Spec, Module) ->
|
||||||
#{bindings := Bindings, query_string := QueryStr} = Request,
|
#{bindings := Bindings, query_string := QueryStr} = Request,
|
||||||
BindingsBin = maps:fold(fun(Key, Value, Acc) -> Acc#{atom_to_binary(Key) => Value} end, #{}, Bindings),
|
BindingsBin = maps:fold(fun(Key, Value, Acc) -> Acc#{atom_to_binary(Key) => Value} end, #{}, Bindings),
|
||||||
check_parameter(Spec, BindingsBin, QueryStr, #{}, #{}).
|
check_parameter(Spec, BindingsBin, QueryStr, Module, #{}, #{}).
|
||||||
|
|
||||||
check_parameter([], _Bindings, _QueryStr, NewBindings, NewQueryStr) -> {NewBindings, NewQueryStr};
|
check_parameter([?REF(Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
|
||||||
check_parameter([{Name, Type} | Spec], Bindings, QueryStr, BindingsAcc, QueryStrAcc) ->
|
check_parameter([?R_REF(LocalMod, Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
|
||||||
|
check_parameter([?R_REF(Module, Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
|
||||||
|
Params = apply(Module, fields, [Fields]),
|
||||||
|
check_parameter(Params ++ Spec, Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
|
||||||
|
check_parameter([], _Bindings, _QueryStr, _Module, NewBindings, NewQueryStr) -> {NewBindings, NewQueryStr};
|
||||||
|
check_parameter([{Name, Type} | Spec], Bindings, QueryStr, Module, BindingsAcc, QueryStrAcc) ->
|
||||||
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
||||||
case hocon_schema:field_schema(Type, in) of
|
case hocon_schema:field_schema(Type, in) of
|
||||||
path ->
|
path ->
|
||||||
NewBindings = hocon_schema:check_plain(Schema, Bindings, #{atom_key => true, override_env => false}),
|
NewBindings = hocon_schema:check_plain(Schema, Bindings, #{atom_key => true, override_env => false}),
|
||||||
NewBindingsAcc = maps:merge(BindingsAcc, NewBindings),
|
NewBindingsAcc = maps:merge(BindingsAcc, NewBindings),
|
||||||
check_parameter(Spec, Bindings, QueryStr, NewBindingsAcc, QueryStrAcc);
|
check_parameter(Spec, Bindings, QueryStr, Module, NewBindingsAcc, QueryStrAcc);
|
||||||
query ->
|
query ->
|
||||||
NewQueryStr = hocon_schema:check_plain(Schema, QueryStr, #{override_env => false}),
|
NewQueryStr = hocon_schema:check_plain(Schema, QueryStr, #{override_env => false}),
|
||||||
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
|
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
|
||||||
check_parameter(Spec, Bindings, QueryStr, BindingsAcc, NewQueryStrAcc)
|
check_parameter(Spec, Bindings, QueryStr, Module, BindingsAcc, NewQueryStrAcc)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_requestBody(#{body := Body}, Schema, Module, true) ->
|
check_requestBody(#{body := Body}, Schema, Module, true) ->
|
||||||
|
|
@ -154,19 +188,28 @@ to_spec(Meta, Params, RequestBody, Responses) ->
|
||||||
|
|
||||||
parameters(Params, Module) ->
|
parameters(Params, Module) ->
|
||||||
{SpecList, AllRefs} =
|
{SpecList, AllRefs} =
|
||||||
lists:foldl(fun({Name, Type}, {Acc, RefsAcc}) ->
|
lists:foldl(fun(Param, {Acc, RefsAcc}) ->
|
||||||
In = hocon_schema:field_schema(Type, in),
|
case Param of
|
||||||
In =:= undefined andalso throw({error, <<"missing in:path/query field in parameters">>}),
|
?REF(StructName) ->
|
||||||
Nullable = hocon_schema:field_schema(Type, nullable),
|
{[#{<<"$ref">> => ?TO_COMPONENTS_PARAM(Module, StructName)} |Acc],
|
||||||
Default = hocon_schema:field_schema(Type, default),
|
[{Module, StructName, parameter}|RefsAcc]};
|
||||||
HoconType = hocon_schema:field_schema(Type, type),
|
?R_REF(RModule, StructName) ->
|
||||||
Meta = init_meta(Nullable, Default),
|
{[#{<<"$ref">> => ?TO_COMPONENTS_PARAM(RModule, StructName)} |Acc],
|
||||||
{ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
|
[{RModule, StructName, parameter}|RefsAcc]};
|
||||||
Spec0 = init_prop([required | ?DEFAULT_FIELDS],
|
{Name, Type} ->
|
||||||
#{schema => maps:merge(ParamType, Meta), name => Name, in => In}, Type),
|
In = hocon_schema:field_schema(Type, in),
|
||||||
Spec1 = trans_required(Spec0, Nullable, In),
|
In =:= undefined andalso throw({error, <<"missing in:path/query field in parameters">>}),
|
||||||
Spec2 = trans_desc(Spec1, Type),
|
Nullable = hocon_schema:field_schema(Type, nullable),
|
||||||
{[Spec2 | Acc], Refs ++ RefsAcc}
|
Default = hocon_schema:field_schema(Type, default),
|
||||||
|
HoconType = hocon_schema:field_schema(Type, type),
|
||||||
|
Meta = init_meta(Nullable, Default),
|
||||||
|
{ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
|
||||||
|
Spec0 = init_prop([required | ?DEFAULT_FIELDS],
|
||||||
|
#{schema => maps:merge(ParamType, Meta), name => Name, in => In}, Type),
|
||||||
|
Spec1 = trans_required(Spec0, Nullable, In),
|
||||||
|
Spec2 = trans_desc(Spec1, Type),
|
||||||
|
{[Spec2 | Acc], Refs ++ RefsAcc}
|
||||||
|
end
|
||||||
end, {[], []}, Params),
|
end, {[], []}, Params),
|
||||||
{lists:reverse(SpecList), AllRefs}.
|
{lists:reverse(SpecList), AllRefs}.
|
||||||
|
|
||||||
|
|
@ -196,7 +239,7 @@ trans_required(Spec, _, _) -> Spec.
|
||||||
trans_desc(Spec, Hocon) ->
|
trans_desc(Spec, Hocon) ->
|
||||||
case hocon_schema:field_schema(Hocon, desc) of
|
case hocon_schema:field_schema(Hocon, desc) of
|
||||||
undefined -> Spec;
|
undefined -> Spec;
|
||||||
Desc -> Spec#{description => Desc}
|
Desc -> Spec#{description => to_bin(Desc)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
requestBody([], _Module) -> {[], []};
|
requestBody([], _Module) -> {[], []};
|
||||||
|
|
@ -248,6 +291,13 @@ components([{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
|
||||||
Namespace = namespace(Module),
|
Namespace = namespace(Module),
|
||||||
{Object, SubRefs} = parse_object(Props, Module),
|
{Object, SubRefs} = parse_object(Props, Module),
|
||||||
NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Object},
|
NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Object},
|
||||||
|
components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc);
|
||||||
|
%% parameters in ref only have one value, not array
|
||||||
|
components([{Module, Field, parameter} | Refs], SpecAcc, SubRefsAcc) ->
|
||||||
|
Props = apply(Module, fields, [Field]),
|
||||||
|
{[Param], SubRefs} = parameters(Props, Module),
|
||||||
|
Namespace = namespace(Module),
|
||||||
|
NewSpecAcc = SpecAcc#{?TO_REF(Namespace, Field) => Param},
|
||||||
components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc).
|
components(Refs, NewSpecAcc, SubRefs ++ SubRefsAcc).
|
||||||
|
|
||||||
namespace(Module) ->
|
namespace(Module) ->
|
||||||
|
|
@ -257,10 +307,10 @@ namespace(Module) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
|
||||||
{#{<<"$ref">> => ?TO_COMPONENTS(Module, StructName)},
|
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
|
||||||
[{Module, StructName}]};
|
[{Module, StructName}]};
|
||||||
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
|
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
|
||||||
{#{<<"$ref">> => ?TO_COMPONENTS(LocalModule, StructName)},
|
{#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
|
||||||
[{LocalModule, StructName}]};
|
[{LocalModule, StructName}]};
|
||||||
hocon_schema_to_spec(Type, _LocalModule) when ?IS_TYPEREFL(Type) ->
|
hocon_schema_to_spec(Type, _LocalModule) when ?IS_TYPEREFL(Type) ->
|
||||||
{typename_to_spec(typerefl:name(Type)), []};
|
{typename_to_spec(typerefl:name(Type)), []};
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ do_sign(Username, Password) ->
|
||||||
},
|
},
|
||||||
Signed = jose_jwt:sign(JWK, JWS, JWT),
|
Signed = jose_jwt:sign(JWK, JWS, JWT),
|
||||||
{_, Token} = jose_jws:compact(Signed),
|
{_, Token} = jose_jws:compact(Signed),
|
||||||
ok = ekka_mnesia:dirty_write(format(Token, Username, ExpTime)),
|
JWTRec = format(Token, Username, ExpTime),
|
||||||
|
ekka_mnesia:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]),
|
||||||
{ok, Token}.
|
{ok, Token}.
|
||||||
|
|
||||||
do_verify(Token)->
|
do_verify(Token)->
|
||||||
|
|
@ -111,8 +112,9 @@ do_verify(Token)->
|
||||||
{ok, JWT = #mqtt_admin_jwt{exptime = ExpTime}} ->
|
{ok, JWT = #mqtt_admin_jwt{exptime = ExpTime}} ->
|
||||||
case ExpTime > erlang:system_time(millisecond) of
|
case ExpTime > erlang:system_time(millisecond) of
|
||||||
true ->
|
true ->
|
||||||
ekka_mnesia:dirty_write(JWT#mqtt_admin_jwt{exptime = jwt_expiration_time()}),
|
NewJWT = JWT#mqtt_admin_jwt{exptime = jwt_expiration_time()},
|
||||||
ok;
|
{atomic, Res} = ekka_mnesia:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [NewJWT]),
|
||||||
|
Res;
|
||||||
_ ->
|
_ ->
|
||||||
{error, token_timeout}
|
{error, token_timeout}
|
||||||
end;
|
end;
|
||||||
|
|
@ -132,14 +134,18 @@ do_destroy_by_username(Username) ->
|
||||||
%% jwt internal util function
|
%% jwt internal util function
|
||||||
-spec(lookup(Token :: binary()) -> {ok, #mqtt_admin_jwt{}} | {error, not_found}).
|
-spec(lookup(Token :: binary()) -> {ok, #mqtt_admin_jwt{}} | {error, not_found}).
|
||||||
lookup(Token) ->
|
lookup(Token) ->
|
||||||
case mnesia:dirty_read(?TAB, Token) of
|
Fun = fun() -> mnesia:read(?TAB, Token) end,
|
||||||
[JWT] -> {ok, JWT};
|
case ekka_mnesia:ro_transaction(?DASHBOARD_SHARD, Fun) of
|
||||||
[] -> {error, not_found}
|
{atomic, [JWT]} -> {ok, JWT};
|
||||||
|
{atomic, []} -> {error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
lookup_by_username(Username) ->
|
lookup_by_username(Username) ->
|
||||||
Spec = [{{mqtt_admin_jwt, '_', Username, '_'}, [], ['$_']}],
|
Spec = [{{mqtt_admin_jwt, '_', Username, '_'}, [], ['$_']}],
|
||||||
mnesia:dirty_select(?TAB, Spec).
|
Fun = fun() -> mnesia:select(?TAB, Spec) end,
|
||||||
|
{atomic, List} = ekka_mnesia:ro_transaction(?DASHBOARD_SHARD, Fun),
|
||||||
|
List.
|
||||||
|
|
||||||
|
|
||||||
jwk(Username, Password, Salt) ->
|
jwk(Username, Password, Salt) ->
|
||||||
Key = erlang:md5(<<Salt/binary, Username/binary, Password/binary>>),
|
Key = erlang:md5(<<Salt/binary, Username/binary, Password/binary>>),
|
||||||
|
|
@ -187,7 +193,8 @@ handle_info(clean_jwt, State) ->
|
||||||
timer_clean(self()),
|
timer_clean(self()),
|
||||||
Now = erlang:system_time(millisecond),
|
Now = erlang:system_time(millisecond),
|
||||||
Spec = [{{mqtt_admin_jwt, '_', '_', '$1'}, [{'<', '$1', Now}], ['$_']}],
|
Spec = [{{mqtt_admin_jwt, '_', '_', '$1'}, [{'<', '$1', Now}], ['$_']}],
|
||||||
JWTList = mnesia:dirty_select(?TAB, Spec),
|
{atomic, JWTList} = ekka_mnesia:ro_transaction(?DASHBOARD_SHARD,
|
||||||
|
fun() -> mnesia:select(?TAB, Spec) end),
|
||||||
destroy(JWTList),
|
destroy(JWTList),
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
handle_info(_Info, State) ->
|
handle_info(_Info, State) ->
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
%%% @author zhongwen
|
|
||||||
%%% @copyright (C) 2021, <COMPANY>
|
|
||||||
%%% @doc
|
|
||||||
%%%
|
|
||||||
%%% @end
|
|
||||||
%%% Created : 22. 9月 2021 13:38
|
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
-module(emqx_swagger_util).
|
|
||||||
-author("zhongwen").
|
|
||||||
|
|
||||||
%% API
|
|
||||||
-export([]).
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([paths/0, api_spec/0, schema/1]).
|
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
||||||
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1]).
|
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1, t_ref/1, t_public_ref/1]).
|
||||||
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
|
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
|
||||||
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1]).
|
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1, t_ref_trans/1]).
|
||||||
-export([t_in_path_trans_error/1, t_in_query_trans_error/1, t_in_mix_trans_error/1]).
|
-export([t_in_path_trans_error/1, t_in_query_trans_error/1, t_in_mix_trans_error/1]).
|
||||||
-export([all/0, suite/0, groups/0]).
|
-export([all/0, suite/0, groups/0]).
|
||||||
|
|
||||||
|
|
@ -20,9 +20,9 @@
|
||||||
all() -> [{group, spec}, {group, validation}].
|
all() -> [{group, spec}, {group, validation}].
|
||||||
suite() -> [{timetrap, {minutes, 1}}].
|
suite() -> [{timetrap, {minutes, 1}}].
|
||||||
groups() -> [
|
groups() -> [
|
||||||
{spec, [parallel], [t_api_spec, t_in_path, t_in_query, t_in_mix,
|
{spec, [parallel], [t_api_spec, t_in_path, t_ref, t_in_query, t_in_mix,
|
||||||
t_without_in, t_require, t_nullable, t_method]},
|
t_without_in, t_require, t_nullable, t_method, t_public_ref]},
|
||||||
{validation, [parallel], [t_in_path_trans, t_in_query_trans, t_in_mix_trans,
|
{validation, [parallel], [t_in_path_trans, t_ref_trans, t_in_query_trans, t_in_mix_trans,
|
||||||
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
|
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
|
@ -44,6 +44,41 @@ t_in_query(_Config) ->
|
||||||
validate("/test/in/query", Expect),
|
validate("/test/in/query", Expect),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_ref(_Config) ->
|
||||||
|
LocalPath = "/test/in/ref/local",
|
||||||
|
Path = "/test/in/ref",
|
||||||
|
Expect = [#{<<"$ref">> => <<"#/components/parameters/emqx_swagger_parameter_SUITE.page">>}],
|
||||||
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
||||||
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, LocalPath),
|
||||||
|
?assertEqual(test, OperationId),
|
||||||
|
Params = maps:get(parameters, maps:get(post, Spec)),
|
||||||
|
?assertEqual(Expect, Params),
|
||||||
|
?assertEqual([{?MODULE, page, parameter}], Refs),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_public_ref(_Config) ->
|
||||||
|
Path = "/test/in/ref/public",
|
||||||
|
Expect = [
|
||||||
|
#{<<"$ref">> => <<"#/components/parameters/public.page">>},
|
||||||
|
#{<<"$ref">> => <<"#/components/parameters/public.limit">>}
|
||||||
|
],
|
||||||
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
||||||
|
?assertEqual(test, OperationId),
|
||||||
|
Params = maps:get(parameters, maps:get(post, Spec)),
|
||||||
|
?assertEqual(Expect, Params),
|
||||||
|
?assertEqual([
|
||||||
|
{emqx_dashboard_swagger, limit, parameter},
|
||||||
|
{emqx_dashboard_swagger, page, parameter}
|
||||||
|
], Refs),
|
||||||
|
ExpectRefs = [
|
||||||
|
#{<<"public.limit">> => #{description => <<"Results per page(max 100)">>, example => 50,in => query,name => limit,
|
||||||
|
schema => #{default => 100,example => 1,maximum => 100, minimum => 1,type => integer}}},
|
||||||
|
#{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>,
|
||||||
|
example => 1,in => query,name => page,
|
||||||
|
schema => #{default => 1,example => 100,type => integer}}}],
|
||||||
|
?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs)),
|
||||||
|
ok.
|
||||||
|
|
||||||
t_in_mix(_Config) ->
|
t_in_mix(_Config) ->
|
||||||
Expect =
|
Expect =
|
||||||
[#{description => <<"Indicates which sorts of issues to return">>,
|
[#{description => <<"Indicates which sorts of issues to return">>,
|
||||||
|
|
@ -115,6 +150,18 @@ t_in_query_trans(_Config) ->
|
||||||
?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100})),
|
?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100})),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_ref_trans(_Config) ->
|
||||||
|
LocalPath = "/test/in/ref/local",
|
||||||
|
Path = "/test/in/ref",
|
||||||
|
Expect = {ok, #{bindings => #{},body => #{},
|
||||||
|
query_string => #{<<"per_page">> => 100}}},
|
||||||
|
?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100})),
|
||||||
|
?assertEqual(Expect, trans_parameters(LocalPath, #{}, #{<<"per_page">> => 100})),
|
||||||
|
{400,'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 1010}),
|
||||||
|
?assertNotEqual(nomatch, binary:match(Reason, [<<"per_page">>])),
|
||||||
|
{400,'BAD_REQUEST', Reason} = trans_parameters(LocalPath, #{}, #{<<"per_page">> => 1010}),
|
||||||
|
ok.
|
||||||
|
|
||||||
t_in_mix_trans(_Config) ->
|
t_in_mix_trans(_Config) ->
|
||||||
Path = "/test/in/mix/:state",
|
Path = "/test/in/mix/:state",
|
||||||
Bindings = #{
|
Bindings = #{
|
||||||
|
|
@ -186,7 +233,7 @@ trans_parameters(Path, Bindings, QueryStr) ->
|
||||||
|
|
||||||
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
||||||
|
|
||||||
paths() -> ["/test/in/:filter", "/test/in/query", "/test/in/mix/:state",
|
paths() -> ["/test/in/:filter", "/test/in/query", "/test/in/mix/:state", "/test/in/ref",
|
||||||
"/required/false", "/nullable/false", "/nullable/true", "/method/ok"].
|
"/required/false", "/nullable/false", "/nullable/true", "/method/ok"].
|
||||||
|
|
||||||
schema("/test/in/:filter") ->
|
schema("/test/in/:filter") ->
|
||||||
|
|
@ -213,6 +260,33 @@ schema("/test/in/query") ->
|
||||||
responses => #{200 => <<"ok">>}
|
responses => #{200 => <<"ok">>}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
schema("/test/in/ref/local") ->
|
||||||
|
#{
|
||||||
|
operationId => test,
|
||||||
|
post => #{
|
||||||
|
parameters => [hoconsc:ref(page)],
|
||||||
|
responses => #{200 => <<"ok">>}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
schema("/test/in/ref") ->
|
||||||
|
#{
|
||||||
|
operationId => test,
|
||||||
|
post => #{
|
||||||
|
parameters => [hoconsc:ref(?MODULE, page)],
|
||||||
|
responses => #{200 => <<"ok">>}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
schema("/test/in/ref/public") ->
|
||||||
|
#{
|
||||||
|
operationId => test,
|
||||||
|
post => #{
|
||||||
|
parameters => [
|
||||||
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||||
|
hoconsc:ref(emqx_dashboard_swagger, limit)
|
||||||
|
],
|
||||||
|
responses => #{200 => <<"ok">>}
|
||||||
|
}
|
||||||
|
};
|
||||||
schema("/test/in/mix/:state") ->
|
schema("/test/in/mix/:state") ->
|
||||||
#{
|
#{
|
||||||
operationId => test,
|
operationId => test,
|
||||||
|
|
@ -257,6 +331,13 @@ schema("/method/ok") ->
|
||||||
#{operationId => test}, ?METHODS);
|
#{operationId => test}, ?METHODS);
|
||||||
schema("/method/error") ->
|
schema("/method/error") ->
|
||||||
#{operationId => test, bar => #{200 => <<"ok">>}}.
|
#{operationId => test, bar => #{200 => <<"ok">>}}.
|
||||||
|
|
||||||
|
fields(page) ->
|
||||||
|
[
|
||||||
|
{per_page,
|
||||||
|
mk(range(1, 100),
|
||||||
|
#{in => query, desc => <<"results per page (max 100)">>, example => 1})}
|
||||||
|
].
|
||||||
to_schema(Params) ->
|
to_schema(Params) ->
|
||||||
#{
|
#{
|
||||||
operationId => test,
|
operationId => test,
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ t_remote_ref(_Config) ->
|
||||||
{<<"another_ref">>, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}], <<"type">> => object}},
|
{<<"another_ref">>, #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}], <<"type">> => object}},
|
||||||
#{<<"emqx_swagger_remote_schema.ref3">> => #{<<"properties">> => [
|
#{<<"emqx_swagger_remote_schema.ref3">> => #{<<"properties">> => [
|
||||||
{<<"ip">>, #{description => <<"IP:Port">>, example => <<"127.0.0.1:80">>,type => string}},
|
{<<"ip">>, #{description => <<"IP:Port">>, example => <<"127.0.0.1:80">>,type => string}},
|
||||||
{<<"version">>, #{description => "a good version", example => <<"1.0.0">>,type => string}}],
|
{<<"version">>, #{description => <<"a good version">>, example => <<"1.0.0">>,type => string}}],
|
||||||
<<"type">> => object}}],
|
<<"type">> => object}}],
|
||||||
?assertEqual(ExpectComponents, Components),
|
?assertEqual(ExpectComponents, Components),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -116,7 +116,7 @@ t_nest_ref(_Config) ->
|
||||||
ExpectComponents = lists:sort([
|
ExpectComponents = lists:sort([
|
||||||
#{<<"emqx_swagger_requestBody_SUITE.nest_ref">> => #{<<"properties">> => [
|
#{<<"emqx_swagger_requestBody_SUITE.nest_ref">> => #{<<"properties">> => [
|
||||||
{<<"env">>, #{enum => [test,dev,prod],type => string}},
|
{<<"env">>, #{enum => [test,dev,prod],type => string}},
|
||||||
{<<"another_ref">>, #{description => "nest ref", <<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
|
{<<"another_ref">>, #{description => <<"nest ref">>, <<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
|
||||||
<<"type">> => object}},
|
<<"type">> => object}},
|
||||||
#{<<"emqx_swagger_requestBody_SUITE.good_ref">> => #{<<"properties">> => [
|
#{<<"emqx_swagger_requestBody_SUITE.good_ref">> => #{<<"properties">> => [
|
||||||
{<<"webhook-host">>, #{default => <<"127.0.0.1:80">>, example => <<"127.0.0.1:80">>,type => string}},
|
{<<"webhook-host">>, #{default => <<"127.0.0.1:80">>, example => <<"127.0.0.1:80">>,type => string}},
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
-export([all/0, suite/0, groups/0]).
|
-export([all/0, suite/0, groups/0]).
|
||||||
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
||||||
-export([t_simple_binary/1, t_object/1, t_nest_object/1, t_empty/1,
|
-export([t_simple_binary/1, t_object/1, t_nest_object/1, t_empty/1, t_error/1,
|
||||||
t_raw_local_ref/1, t_raw_remote_ref/1, t_hocon_schema_function/1,
|
t_raw_local_ref/1, t_raw_remote_ref/1, t_hocon_schema_function/1,
|
||||||
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1,
|
t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1,
|
||||||
t_ref_array_with_key/1, t_ref_array_without_key/1, t_api_spec/1]).
|
t_ref_array_with_key/1, t_ref_array_without_key/1, t_api_spec/1]).
|
||||||
|
|
@ -21,7 +21,7 @@ all() -> [{group, spec}].
|
||||||
suite() -> [{timetrap, {minutes, 1}}].
|
suite() -> [{timetrap, {minutes, 1}}].
|
||||||
groups() -> [
|
groups() -> [
|
||||||
{spec, [parallel], [
|
{spec, [parallel], [
|
||||||
t_api_spec, t_simple_binary, t_object, t_nest_object,
|
t_api_spec, t_simple_binary, t_object, t_nest_object, t_error,
|
||||||
t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function,
|
t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function,
|
||||||
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
|
t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
|
||||||
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]}
|
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]}
|
||||||
|
|
@ -48,6 +48,33 @@ t_object(_config) ->
|
||||||
validate(Path, Object, ExpectRefs),
|
validate(Path, Object, ExpectRefs),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_error(_Config) ->
|
||||||
|
Path = "/error",
|
||||||
|
Error400 = #{<<"content">> =>
|
||||||
|
#{<<"application/json">> => #{<<"schema">> => #{<<"type">> => object,
|
||||||
|
<<"properties">> =>
|
||||||
|
[
|
||||||
|
{<<"code">>, #{enum => ['Bad1','Bad2'], type => string}},
|
||||||
|
{<<"message">>, #{description => <<"Details description of the error.">>,
|
||||||
|
example => <<"Bad request desc">>, type => string}}]
|
||||||
|
}}}},
|
||||||
|
Error404 = #{<<"content">> =>
|
||||||
|
#{<<"application/json">> => #{<<"schema">> => #{<<"type">> => object,
|
||||||
|
<<"properties">> =>
|
||||||
|
[
|
||||||
|
{<<"code">>, #{enum => ['Not-Found'], type => string}},
|
||||||
|
{<<"message">>, #{description => <<"Details description of the error.">>,
|
||||||
|
example => <<"Error code to troubleshoot problems.">>, type => string}}]
|
||||||
|
}}}},
|
||||||
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
||||||
|
?assertEqual(test, OperationId),
|
||||||
|
Response = maps:get(responses, maps:get(get, Spec)),
|
||||||
|
?assertEqual(Error400, maps:get(<<"400">>, Response)),
|
||||||
|
?assertEqual(Error404, maps:get(<<"404">>, Response)),
|
||||||
|
?assertEqual(#{}, maps:without([<<"400">>, <<"404">>], Response)),
|
||||||
|
?assertEqual([], Refs),
|
||||||
|
ok.
|
||||||
|
|
||||||
t_nest_object(_Config) ->
|
t_nest_object(_Config) ->
|
||||||
Path = "/nest/object",
|
Path = "/nest/object",
|
||||||
Object =
|
Object =
|
||||||
|
|
@ -175,7 +202,7 @@ t_hocon_schema_function(_Config) ->
|
||||||
#{<<"emqx_swagger_remote_schema.ref3">> => #{<<"type">> => object,
|
#{<<"emqx_swagger_remote_schema.ref3">> => #{<<"type">> => object,
|
||||||
<<"properties">> => [
|
<<"properties">> => [
|
||||||
{<<"ip">>, #{description => <<"IP:Port">>, example => <<"127.0.0.1:80">>,type => string}},
|
{<<"ip">>, #{description => <<"IP:Port">>, example => <<"127.0.0.1:80">>,type => string}},
|
||||||
{<<"version">>, #{description => "a good version", example => <<"1.0.0">>, type => string}}]
|
{<<"version">>, #{description => <<"a good version">>, example => <<"1.0.0">>, type => string}}]
|
||||||
}},
|
}},
|
||||||
#{<<"emqx_swagger_remote_schema.root">> => #{required => [<<"default_password">>, <<"default_username">>],
|
#{<<"emqx_swagger_remote_schema.root">> => #{required => [<<"default_password">>, <<"default_username">>],
|
||||||
<<"properties">> => [{<<"listeners">>, #{items =>
|
<<"properties">> => [{<<"listeners">>, #{items =>
|
||||||
|
|
@ -255,7 +282,15 @@ schema("/ref/array/with/key") ->
|
||||||
schema("/ref/array/without/key") ->
|
schema("/ref/array/without/key") ->
|
||||||
to_schema(mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{}));
|
to_schema(mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{}));
|
||||||
schema("/ref/hocon/schema/function") ->
|
schema("/ref/hocon/schema/function") ->
|
||||||
to_schema(mk(hoconsc:ref(emqx_swagger_remote_schema, "root"), #{})).
|
to_schema(mk(hoconsc:ref(emqx_swagger_remote_schema, "root"), #{}));
|
||||||
|
schema("/error") ->
|
||||||
|
#{
|
||||||
|
operationId => test,
|
||||||
|
get => #{responses => #{
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(['Bad1', 'Bad2'], <<"Bad request desc">>),
|
||||||
|
404 => emqx_dashboard_swagger:error_codes(['Not-Found'])
|
||||||
|
}}
|
||||||
|
}.
|
||||||
|
|
||||||
validate(Path, ExpectObject, ExpectRefs) ->
|
validate(Path, ExpectObject, ExpectRefs) ->
|
||||||
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
||||||
|
|
|
||||||
|
|
@ -5,296 +5,345 @@
|
||||||
## TODO: These configuration options are temporary example here.
|
## TODO: These configuration options are temporary example here.
|
||||||
## In the final version, it will be commented out.
|
## In the final version, it will be commented out.
|
||||||
|
|
||||||
gateway.stomp {
|
#gateway.stomp {
|
||||||
|
#
|
||||||
## How long time the connection will be disconnected if the
|
# ## How long time the connection will be disconnected if the
|
||||||
## connection is established but no bytes received
|
# ## connection is established but no bytes received
|
||||||
idle_timeout = 30s
|
# idle_timeout = 30s
|
||||||
|
#
|
||||||
## To control whether write statistics data into ETS table
|
# ## To control whether write statistics data into ETS table
|
||||||
## for dashbord to read.
|
# ## for dashbord to read.
|
||||||
enable_stats = true
|
# enable_stats = true
|
||||||
|
#
|
||||||
## When publishing or subscribing, prefix all topics with a mountpoint string.
|
# ## When publishing or subscribing, prefix all topics with a mountpoint string.
|
||||||
mountpoint = ""
|
# mountpoint = ""
|
||||||
|
#
|
||||||
frame {
|
# frame {
|
||||||
max_headers = 10
|
# max_headers = 10
|
||||||
max_headers_length = 1024
|
# max_headers_length = 1024
|
||||||
max_body_length = 8192
|
# max_body_length = 8192
|
||||||
}
|
# }
|
||||||
|
#
|
||||||
clientinfo_override {
|
# clientinfo_override {
|
||||||
username = "${Packet.headers.login}"
|
# username = "${Packet.headers.login}"
|
||||||
password = "${Packet.headers.passcode}"
|
# password = "${Packet.headers.passcode}"
|
||||||
}
|
# }
|
||||||
|
#
|
||||||
authentication: [
|
# authentication: {
|
||||||
# {
|
# mechanism = password-based
|
||||||
# name = "authenticator1"
|
# backend = built-in-database
|
||||||
# type = "password-based:built-in-database"
|
# user_id_type = clientid
|
||||||
# user_id_type = clientid
|
# }
|
||||||
# }
|
#
|
||||||
]
|
# listeners.tcp.default {
|
||||||
|
# bind = 61613
|
||||||
listeners.tcp.default {
|
# acceptors = 16
|
||||||
bind = 61613
|
# max_connections = 1024000
|
||||||
acceptors = 16
|
# max_conn_rate = 1000
|
||||||
max_connections = 1024000
|
#
|
||||||
max_conn_rate = 1000
|
# access_rules = [
|
||||||
|
# "allow all"
|
||||||
access_rules = [
|
# ]
|
||||||
"allow all"
|
#
|
||||||
]
|
# authentication: {
|
||||||
|
# mechanism = password-based
|
||||||
## TCP options
|
# backend = built-in-database
|
||||||
## See ${example_common_tcp_options} for more information
|
# user_id_type = username
|
||||||
tcp.active_n = 100
|
# }
|
||||||
tcp.backlog = 1024
|
#
|
||||||
tcp.buffer = 4KB
|
# ## TCP options
|
||||||
}
|
# ## See ${example_common_tcp_options} for more information
|
||||||
|
# tcp.active_n = 100
|
||||||
listeners.ssl.default {
|
# tcp.backlog = 1024
|
||||||
bind = 61614
|
# tcp.buffer = 4KB
|
||||||
acceptors = 16
|
# }
|
||||||
max_connections = 1024000
|
#
|
||||||
max_conn_rate = 1000
|
# listeners.ssl.default {
|
||||||
|
# bind = 61614
|
||||||
## TCP options
|
# acceptors = 16
|
||||||
## See ${example_common_tcp_options} for more information
|
# max_connections = 1024000
|
||||||
tcp.active_n = 100
|
# max_conn_rate = 1000
|
||||||
tcp.backlog = 1024
|
#
|
||||||
tcp.buffer = 4KB
|
# ## TCP options
|
||||||
|
# ## See ${example_common_tcp_options} for more information
|
||||||
## SSL options
|
# tcp.active_n = 100
|
||||||
## See ${example_common_ssl_options} for more information
|
# tcp.backlog = 1024
|
||||||
ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
# tcp.buffer = 4KB
|
||||||
ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem"
|
#
|
||||||
ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem"
|
# ## SSL options
|
||||||
ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
# ## See ${example_common_ssl_options} for more information
|
||||||
#ssl.verify = verify_none
|
# ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
||||||
#ssl.fail_if_no_peer_cert = false
|
# ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem"
|
||||||
#ssl.server_name_indication = disable
|
# ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem"
|
||||||
#ssl.secure_renegotiate = false
|
# ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
||||||
#ssl.reuse_sessions = false
|
# #ssl.verify = verify_none
|
||||||
#ssl.honor_cipher_order = false
|
# #ssl.fail_if_no_peer_cert = false
|
||||||
#ssl.handshake_timeout = 15s
|
# #ssl.server_name_indication = disable
|
||||||
#ssl.depth = 10
|
# #ssl.secure_renegotiate = false
|
||||||
#ssl.password = foo
|
# #ssl.reuse_sessions = false
|
||||||
#ssl.dhfile = path-to-your-file
|
# #ssl.honor_cipher_order = false
|
||||||
}
|
# #ssl.handshake_timeout = 15s
|
||||||
}
|
# #ssl.depth = 10
|
||||||
|
# #ssl.password = foo
|
||||||
gateway.coap {
|
# #ssl.dhfile = path-to-your-file
|
||||||
|
# }
|
||||||
## How long time the connection will be disconnected if the
|
#}
|
||||||
## connection is established but no bytes received
|
#
|
||||||
idle_timeout = 30s
|
#gateway.coap {
|
||||||
|
#
|
||||||
## To control whether write statistics data into ETS table
|
# ## How long time the connection will be disconnected if the
|
||||||
## for dashbord to read.
|
# ## connection is established but no bytes received
|
||||||
enable_stats = true
|
# idle_timeout = 30s
|
||||||
|
#
|
||||||
## When publishing or subscribing, prefix all topics with a mountpoint string.
|
# ## To control whether write statistics data into ETS table
|
||||||
mountpoint = ""
|
# ## for dashbord to read.
|
||||||
|
# enable_stats = true
|
||||||
notify_type = qos
|
#
|
||||||
|
# ## When publishing or subscribing, prefix all topics with a mountpoint string.
|
||||||
## if true, you need to establish a connection before use
|
# mountpoint = ""
|
||||||
connection_required = false
|
#
|
||||||
subscribe_qos = qos0
|
# ## Enable or disable connection mode
|
||||||
publish_qos = qos1
|
# ## If true, you need to establish a connection before send any publish/subscribe
|
||||||
|
# ## requests
|
||||||
listeners.udp.default {
|
# ##
|
||||||
bind = 5683
|
# ## Default: false
|
||||||
acceptors = 4
|
# #connection_required = false
|
||||||
max_connections = 102400
|
#
|
||||||
max_conn_rate = 1000
|
# ## The Notification Message Type.
|
||||||
|
# ## The notification message will be delivered to the CoAP client if a new
|
||||||
## UDP Options
|
# ## message received on an observed topic.
|
||||||
## See ${example_common_udp_options} for more information
|
# ## The type of delivered coap message can be set to:
|
||||||
udp.active_n = 100
|
# ## - non: Non-confirmable
|
||||||
udp.buffer = 16KB
|
# ## - con: Confirmable
|
||||||
}
|
# ## - qos: Mapping from QoS type of the recevied message.
|
||||||
listeners.dtls.default {
|
# ## QoS0 -> non, QoS1,2 -> con.
|
||||||
bind = 5684
|
# ##
|
||||||
acceptors = 4
|
# ## Enum: non | con | qos
|
||||||
max_connections = 102400
|
# ## Default: qos
|
||||||
max_conn_rate = 1000
|
# #notify_type = qos
|
||||||
|
#
|
||||||
## UDP Options
|
# ## The *Default QoS Level* indicator for subscribe request.
|
||||||
## See ${example_common_udp_options} for more information
|
# ## This option specifies the QoS level for the CoAP Client when establishing
|
||||||
udp.active_n = 100
|
# ## a subscription membership, if the subscribe request is not carried `qos`
|
||||||
udp.buffer = 16KB
|
# ## option.
|
||||||
|
# ## The indicator can be set to:
|
||||||
## DTLS Options
|
# ## - qos0, qos1, qos2: Fixed default QoS level
|
||||||
## See #{example_common_dtls_options} for more information
|
# ## - coap: Dynamic QoS level by the message type of subscribe request
|
||||||
dtls.versions = ["dtlsv1.2", "dtlsv1"]
|
# ## * qos0: If the subscribe request is non-confirmable
|
||||||
dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem"
|
# ## * qos1: If the subscribe request is confirmable
|
||||||
dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem"
|
# ##
|
||||||
dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
# ## Enum: qos0 | qos1 | qos2 | coap
|
||||||
}
|
# ## Default: coap
|
||||||
}
|
# #subscribe_qos = coap
|
||||||
|
#
|
||||||
gateway.mqttsn {
|
# ## The *Default QoS Level* indicator for publish request.
|
||||||
|
# ## This option specifies the QoS level for the CoAP Client when publishing a
|
||||||
## How long time the connection will be disconnected if the
|
# ## message to EMQ X PUB/SUB system, if the publish request is not carried `qos`
|
||||||
## connection is established but no bytes received
|
# ## option.
|
||||||
idle_timeout = 30s
|
# ## The indicator can be set to:
|
||||||
|
# ## - qos0, qos1, qos2: Fixed default QoS level
|
||||||
## To control whether write statistics data into ETS table
|
# ## - coap: Dynamic QoS level by the message type of publish request
|
||||||
## for dashbord to read.
|
# ## * qos0: If the publish request is non-confirmable
|
||||||
enable_stats = true
|
# ## * qos1: If the publish request is confirmable
|
||||||
|
# ##
|
||||||
## When publishing or subscribing, prefix all topics with a mountpoint string.
|
# ## Enum: qos0 | qos1 | qos2 | coap
|
||||||
mountpoint = ""
|
# #publish_qos = coap
|
||||||
|
#
|
||||||
## The MQTT-SN Gateway ID in ADVERTISE message.
|
# listeners.udp.default {
|
||||||
gateway_id = 1
|
# bind = 5683
|
||||||
|
# max_connections = 102400
|
||||||
## Enable broadcast this gateway to WLAN
|
# max_conn_rate = 1000
|
||||||
broadcast = true
|
#
|
||||||
|
# ## UDP Options
|
||||||
## To control whether accept and process the received
|
# ## See ${example_common_udp_options} for more information
|
||||||
## publish message with qos=-1.
|
# udp.active_n = 100
|
||||||
enable_qos3 = true
|
# udp.buffer = 16KB
|
||||||
|
# }
|
||||||
## The pre-defined topic name corresponding to the pre-defined topic
|
# listeners.dtls.default {
|
||||||
## id of N.
|
# bind = 5684
|
||||||
## Note that the pre-defined topic id of 0 is reserved.
|
# acceptors = 4
|
||||||
predefined = [
|
# max_connections = 102400
|
||||||
{ id = 1
|
# max_conn_rate = 1000
|
||||||
topic = "/predefined/topic/name/hello"
|
#
|
||||||
},
|
# ## UDP Options
|
||||||
{ id = 2
|
# ## See ${example_common_udp_options} for more information
|
||||||
topic = "/predefined/topic/name/nice"
|
# udp.active_n = 100
|
||||||
}
|
# udp.buffer = 16KB
|
||||||
]
|
#
|
||||||
|
# ## DTLS Options
|
||||||
### ClientInfo override
|
# ## See #{example_common_dtls_options} for more information
|
||||||
clientinfo_override {
|
# dtls.versions = ["dtlsv1.2", "dtlsv1"]
|
||||||
username = "mqtt_sn_user"
|
# dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem"
|
||||||
password = "abc"
|
# dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem"
|
||||||
}
|
# dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
||||||
|
# dtls.handshake_timeout = 15s
|
||||||
listeners.udp.default {
|
# }
|
||||||
bind = 1884
|
#}
|
||||||
max_connections = 10240000
|
#
|
||||||
max_conn_rate = 1000
|
#gateway.mqttsn {
|
||||||
}
|
#
|
||||||
|
# ## How long time the connection will be disconnected if the
|
||||||
listeners.dtls.default {
|
# ## connection is established but no bytes received
|
||||||
bind = 1885
|
# idle_timeout = 30s
|
||||||
acceptors = 4
|
#
|
||||||
max_connections = 102400
|
# ## To control whether write statistics data into ETS table
|
||||||
max_conn_rate = 1000
|
# ## for dashbord to read.
|
||||||
|
# enable_stats = true
|
||||||
## UDP Options
|
#
|
||||||
## See ${example_common_udp_options} for more information
|
# ## When publishing or subscribing, prefix all topics with a mountpoint string.
|
||||||
udp.active_n = 100
|
# mountpoint = ""
|
||||||
udp.buffer = 16KB
|
#
|
||||||
|
# ## The MQTT-SN Gateway ID in ADVERTISE message.
|
||||||
## DTLS Options
|
# gateway_id = 1
|
||||||
## See #{example_common_dtls_options} for more information
|
#
|
||||||
dtls.versions = ["dtlsv1.2", "dtlsv1"]
|
# ## Enable broadcast this gateway to WLAN
|
||||||
dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem"
|
# broadcast = true
|
||||||
dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem"
|
#
|
||||||
dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
# ## To control whether accept and process the received
|
||||||
}
|
# ## publish message with qos=-1.
|
||||||
|
# enable_qos3 = true
|
||||||
}
|
#
|
||||||
|
# ## The pre-defined topic name corresponding to the pre-defined topic
|
||||||
gateway.lwm2m {
|
# ## id of N.
|
||||||
|
# ## Note that the pre-defined topic id of 0 is reserved.
|
||||||
## How long time the connection will be disconnected if the
|
# predefined = [
|
||||||
## connection is established but no bytes received
|
# { id = 1
|
||||||
idle_timeout = 30s
|
# topic = "/predefined/topic/name/hello"
|
||||||
|
# },
|
||||||
## To control whether write statistics data into ETS table
|
# { id = 2
|
||||||
## for dashbord to read.
|
# topic = "/predefined/topic/name/nice"
|
||||||
enable_stats = true
|
# }
|
||||||
|
# ]
|
||||||
## When publishing or subscribing, prefix all topics with a mountpoint string.
|
#
|
||||||
mountpoint = "lwm2m/%u"
|
# ### ClientInfo override
|
||||||
|
# clientinfo_override {
|
||||||
xml_dir = "{{ platform_etc_dir }}/lwm2m_xml"
|
# username = "mqtt_sn_user"
|
||||||
|
# password = "abc"
|
||||||
lifetime_min = 1s
|
# }
|
||||||
lifetime_max = 86400s
|
#
|
||||||
qmode_time_windonw = 22
|
# listeners.udp.default {
|
||||||
auto_observe = false
|
# bind = 1884
|
||||||
|
# max_connections = 10240000
|
||||||
## always | contains_object_list
|
# max_conn_rate = 1000
|
||||||
update_msg_publish_condition = contains_object_list
|
# }
|
||||||
|
#
|
||||||
|
# listeners.dtls.default {
|
||||||
translators {
|
# bind = 1885
|
||||||
command {
|
# acceptors = 4
|
||||||
topic = "/dn/#"
|
# max_connections = 102400
|
||||||
qos = 0
|
# max_conn_rate = 1000
|
||||||
}
|
#
|
||||||
|
# ## UDP Options
|
||||||
response {
|
# ## See ${example_common_udp_options} for more information
|
||||||
topic = "/up/resp"
|
# udp.active_n = 100
|
||||||
qos = 0
|
# udp.buffer = 16KB
|
||||||
}
|
#
|
||||||
|
# ## DTLS Options
|
||||||
notify {
|
# ## See #{example_common_dtls_options} for more information
|
||||||
topic = "/up/notify"
|
# dtls.versions = ["dtlsv1.2", "dtlsv1"]
|
||||||
qos = 0
|
# dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem"
|
||||||
}
|
# dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem"
|
||||||
|
# dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
|
||||||
register {
|
# }
|
||||||
topic = "/up/resp"
|
#
|
||||||
qos = 0
|
#}
|
||||||
}
|
#
|
||||||
|
#gateway.lwm2m {
|
||||||
update {
|
#
|
||||||
topic = "/up/resp"
|
# ## How long time the connection will be disconnected if the
|
||||||
qos = 0
|
# ## connection is established but no bytes received
|
||||||
}
|
# idle_timeout = 30s
|
||||||
}
|
#
|
||||||
|
# ## To control whether write statistics data into ETS table
|
||||||
listeners.udp.default {
|
# ## for dashbord to read.
|
||||||
bind = 5783
|
# enable_stats = true
|
||||||
}
|
#
|
||||||
}
|
# ## When publishing or subscribing, prefix all topics with a mountpoint string.
|
||||||
|
# mountpoint = "lwm2m/%u"
|
||||||
gateway.exproto {
|
#
|
||||||
|
# xml_dir = "{{ platform_etc_dir }}/lwm2m_xml"
|
||||||
## How long time the connection will be disconnected if the
|
#
|
||||||
## connection is established but no bytes received
|
# ##
|
||||||
idle_timeout = 30s
|
# ##
|
||||||
|
# lifetime_min = 1s
|
||||||
## To control whether write statistics data into ETS table
|
#
|
||||||
## for dashbord to read.
|
# lifetime_max = 86400s
|
||||||
enable_stats = true
|
#
|
||||||
|
# qmode_time_window = 22
|
||||||
## When publishing or subscribing, prefix all topics with a mountpoint string.
|
#
|
||||||
mountpoint = ""
|
# auto_observe = false
|
||||||
|
#
|
||||||
## The gRPC server to accept requests
|
# ## always | contains_object_list
|
||||||
server {
|
# update_msg_publish_condition = contains_object_list
|
||||||
bind = 9100
|
#
|
||||||
#ssl.keyfile:
|
#
|
||||||
#ssl.certfile:
|
# translators {
|
||||||
#ssl.cacertfile:
|
# command {
|
||||||
}
|
# topic = "/dn/#"
|
||||||
|
# qos = 0
|
||||||
handler {
|
# }
|
||||||
address = "http://127.0.0.1:9001"
|
#
|
||||||
#ssl.keyfile:
|
# response {
|
||||||
#ssl.certfile:
|
# topic = "/up/resp"
|
||||||
#ssl.cacertfile:
|
# qos = 0
|
||||||
}
|
# }
|
||||||
|
#
|
||||||
listeners.tcp.default {
|
# notify {
|
||||||
bind = 7993
|
# topic = "/up/notify"
|
||||||
acceptors = 8
|
# qos = 0
|
||||||
max_connections = 10240
|
# }
|
||||||
max_conn_rate = 1000
|
#
|
||||||
}
|
# register {
|
||||||
#listeners.ssl.default: {}
|
# topic = "/up/resp"
|
||||||
#listeners.udp.default: {}
|
# qos = 0
|
||||||
#listeners.dtls.default: {}
|
# }
|
||||||
}
|
#
|
||||||
|
# update {
|
||||||
|
# topic = "/up/resp"
|
||||||
|
# qos = 0
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# listeners.udp.default {
|
||||||
|
# bind = 5783
|
||||||
|
# }
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#gateway.exproto {
|
||||||
|
#
|
||||||
|
# ## How long time the connection will be disconnected if the
|
||||||
|
# ## connection is established but no bytes received
|
||||||
|
# idle_timeout = 30s
|
||||||
|
#
|
||||||
|
# ## To control whether write statistics data into ETS table
|
||||||
|
# ## for dashbord to read.
|
||||||
|
# enable_stats = true
|
||||||
|
#
|
||||||
|
# ## When publishing or subscribing, prefix all topics with a mountpoint string.
|
||||||
|
# mountpoint = ""
|
||||||
|
#
|
||||||
|
# ## The gRPC server to accept requests
|
||||||
|
# server {
|
||||||
|
# bind = 9100
|
||||||
|
# #ssl.keyfile:
|
||||||
|
# #ssl.certfile:
|
||||||
|
# #ssl.cacertfile:
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# handler {
|
||||||
|
# address = "http://127.0.0.1:9001"
|
||||||
|
# #ssl.keyfile:
|
||||||
|
# #ssl.certfile:
|
||||||
|
# #ssl.cacertfile:
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# listeners.tcp.default {
|
||||||
|
# bind = 7993
|
||||||
|
# acceptors = 8
|
||||||
|
# max_connections = 10240
|
||||||
|
# max_conn_rate = 1000
|
||||||
|
# }
|
||||||
|
# #listeners.ssl.default: {}
|
||||||
|
# #listeners.udp.default: {}
|
||||||
|
# #listeners.dtls.default: {}
|
||||||
|
#}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@
|
||||||
|
|
||||||
-type gateway_name() :: atom().
|
-type gateway_name() :: atom().
|
||||||
|
|
||||||
-type listener() :: #{}.
|
|
||||||
|
|
||||||
%% @doc The Gateway defination
|
%% @doc The Gateway defination
|
||||||
-type gateway() ::
|
-type gateway() ::
|
||||||
#{ name := gateway_name()
|
#{ name := gateway_name()
|
||||||
|
|
|
||||||
|
|
@ -81,10 +81,13 @@
|
||||||
%% Frame Module
|
%% Frame Module
|
||||||
frame_mod :: atom(),
|
frame_mod :: atom(),
|
||||||
%% Channel Module
|
%% Channel Module
|
||||||
chann_mod :: atom()
|
chann_mod :: atom(),
|
||||||
|
%% Listener Tag
|
||||||
|
listener :: listener() | undefined
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-type(state() :: #state{}).
|
-type listener() :: {GwName :: atom(), LisType :: atom(), LisName :: atom()}.
|
||||||
|
-type state() :: #state{}.
|
||||||
|
|
||||||
-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]).
|
-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]).
|
||||||
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
|
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
|
||||||
|
|
@ -279,7 +282,8 @@ init_state(WrappedSock, Peername, Options, FrameMod, ChannMod) ->
|
||||||
idle_timer = IdleTimer,
|
idle_timer = IdleTimer,
|
||||||
oom_policy = OomPolicy,
|
oom_policy = OomPolicy,
|
||||||
frame_mod = FrameMod,
|
frame_mod = FrameMod,
|
||||||
chann_mod = ChannMod
|
chann_mod = ChannMod,
|
||||||
|
listener = maps:get(listener, Options, undefined)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
run_loop(Parent, State = #state{socket = Socket,
|
run_loop(Parent, State = #state{socket = Socket,
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ request(post, #{body := Body, bindings := Bindings}) ->
|
||||||
CT = maps:get(<<"content_type">>, Body, <<"text/plain">>),
|
CT = maps:get(<<"content_type">>, Body, <<"text/plain">>),
|
||||||
Token = maps:get(<<"token">>, Body, <<>>),
|
Token = maps:get(<<"token">>, Body, <<>>),
|
||||||
Payload = maps:get(<<"payload">>, Body, <<>>),
|
Payload = maps:get(<<"payload">>, Body, <<>>),
|
||||||
WaitTime = maps:get(<<"timeout">>, Body, ?DEF_WAIT_TIME),
|
BinWaitTime = maps:get(<<"timeout">>, Body, <<"10s">>),
|
||||||
|
{ok, WaitTime} = emqx_schema:to_duration_ms(BinWaitTime),
|
||||||
Payload2 = parse_payload(CT, Payload),
|
Payload2 = parse_payload(CT, Payload),
|
||||||
ReqType = erlang:binary_to_atom(Method),
|
ReqType = erlang:binary_to_atom(Method),
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ request_parameters() ->
|
||||||
request_properties() ->
|
request_properties() ->
|
||||||
properties([ {token, string, "message token, can be empty"}
|
properties([ {token, string, "message token, can be empty"}
|
||||||
, {method, string, "request method type", ["get", "put", "post", "delete"]}
|
, {method, string, "request method type", ["get", "put", "post", "delete"]}
|
||||||
, {timeout, integer, "timespan for response"}
|
, {timeout, string, "timespan for response", "10s"}
|
||||||
, {content_type, string, "payload type",
|
, {content_type, string, "payload type",
|
||||||
[<<"text/plain">>, <<"application/json">>, <<"application/octet-stream">>]}
|
[<<"text/plain">>, <<"application/json">>, <<"application/octet-stream">>]}
|
||||||
, {payload, string, "payload"}]).
|
, {payload, string, "payload"}]).
|
||||||
|
|
|
||||||
|
|
@ -103,9 +103,15 @@ init(ConnInfo = #{peername := {PeerHost, _},
|
||||||
#{ctx := Ctx} = Config) ->
|
#{ctx := Ctx} = Config) ->
|
||||||
Peercert = maps:get(peercert, ConnInfo, undefined),
|
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||||
Mountpoint = maps:get(mountpoint, Config, <<>>),
|
Mountpoint = maps:get(mountpoint, Config, <<>>),
|
||||||
|
ListenerId = case maps:get(listener, Config, undefined) of
|
||||||
|
undefined -> undefined;
|
||||||
|
{GwName, Type, LisName} ->
|
||||||
|
emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||||
|
end,
|
||||||
ClientInfo = set_peercert_infos(
|
ClientInfo = set_peercert_infos(
|
||||||
Peercert,
|
Peercert,
|
||||||
#{ zone => default
|
#{ zone => default
|
||||||
|
, listener => ListenerId
|
||||||
, protocol => 'coap'
|
, protocol => 'coap'
|
||||||
, peerhost => PeerHost
|
, peerhost => PeerHost
|
||||||
, sockport => SockPort
|
, sockport => SockPort
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,8 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
||||||
|
|
||||||
start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
||||||
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
NCfg = Cfg#{
|
NCfg = Cfg#{ctx => Ctx,
|
||||||
ctx => Ctx,
|
listener => {GwName, Type, LisName},
|
||||||
frame_mod => emqx_coap_frame,
|
frame_mod => emqx_coap_frame,
|
||||||
chann_mod => emqx_coap_channel
|
chann_mod => emqx_coap_channel
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,6 @@
|
||||||
|
|
||||||
-include("include/emqx_gateway.hrl").
|
-include("include/emqx_gateway.hrl").
|
||||||
|
|
||||||
%% callbacks for emqx_config_handler
|
|
||||||
-export([ pre_config_update/2
|
|
||||||
, post_config_update/4
|
|
||||||
]).
|
|
||||||
|
|
||||||
%% Gateway APIs
|
%% Gateway APIs
|
||||||
-export([ registered_gateway/0
|
-export([ registered_gateway/0
|
||||||
, load/2
|
, load/2
|
||||||
|
|
@ -36,8 +31,6 @@
|
||||||
, list/0
|
, list/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([update_rawconf/2]).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -84,37 +77,6 @@ start(Name) ->
|
||||||
stop(Name) ->
|
stop(Name) ->
|
||||||
emqx_gateway_sup:stop_gateway_insta(Name).
|
emqx_gateway_sup:stop_gateway_insta(Name).
|
||||||
|
|
||||||
-spec update_rawconf(binary(), emqx_config:raw_config())
|
|
||||||
-> ok
|
|
||||||
| {error, any()}.
|
|
||||||
update_rawconf(RawName, RawConfDiff) ->
|
|
||||||
case emqx:update_config([gateway], {RawName, RawConfDiff}) of
|
|
||||||
{ok, _Result} -> ok;
|
|
||||||
{error, Reason} -> {error, Reason}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Config Handler
|
|
||||||
|
|
||||||
-spec pre_config_update(emqx_config:update_request(),
|
|
||||||
emqx_config:raw_config()) ->
|
|
||||||
{ok, emqx_config:update_request()} | {error, term()}.
|
|
||||||
pre_config_update({RawName, RawConfDiff}, RawConf) ->
|
|
||||||
{ok, emqx_map_lib:deep_merge(RawConf, #{RawName => RawConfDiff})}.
|
|
||||||
|
|
||||||
-spec post_config_update(emqx_config:update_request(), emqx_config:config(),
|
|
||||||
emqx_config:config(), emqx_config:app_envs())
|
|
||||||
-> ok | {ok, Result::any()} | {error, Reason::term()}.
|
|
||||||
post_config_update({RawName, _}, NewConfig, OldConfig, _AppEnvs) ->
|
|
||||||
GwName = binary_to_existing_atom(RawName),
|
|
||||||
SubConf = maps:get(GwName, NewConfig),
|
|
||||||
case maps:get(GwName, OldConfig, undefined) of
|
|
||||||
undefined ->
|
|
||||||
emqx_gateway:load(GwName, SubConf);
|
|
||||||
_ ->
|
|
||||||
emqx_gateway:update(GwName, SubConf)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Internal funcs
|
%% Internal funcs
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ apis() ->
|
||||||
, {"/gateway/:name", gateway_insta}
|
, {"/gateway/:name", gateway_insta}
|
||||||
, {"/gateway/:name/stats", gateway_insta_stats}
|
, {"/gateway/:name/stats", gateway_insta_stats}
|
||||||
].
|
].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% http handlers
|
%% http handlers
|
||||||
|
|
||||||
|
|
@ -57,7 +58,29 @@ gateway(get, Request) ->
|
||||||
undefined -> all;
|
undefined -> all;
|
||||||
S0 -> binary_to_existing_atom(S0, utf8)
|
S0 -> binary_to_existing_atom(S0, utf8)
|
||||||
end,
|
end,
|
||||||
{200, emqx_gateway_http:gateways(Status)}.
|
{200, emqx_gateway_http:gateways(Status)};
|
||||||
|
gateway(post, Request) ->
|
||||||
|
Body = maps:get(body, Request, #{}),
|
||||||
|
try
|
||||||
|
Name0 = maps:get(<<"name">>, Body),
|
||||||
|
GwName = binary_to_existing_atom(Name0),
|
||||||
|
case emqx_gateway_registry:lookup(GwName) of
|
||||||
|
undefined -> error(badarg);
|
||||||
|
_ ->
|
||||||
|
GwConf = maps:without([<<"name">>], Body),
|
||||||
|
case emqx_gateway_conf:load_gateway(GwName, GwConf) of
|
||||||
|
ok ->
|
||||||
|
{204};
|
||||||
|
{error, Reason} ->
|
||||||
|
return_http_error(500, Reason)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
error : {badkey, K} ->
|
||||||
|
return_http_error(400, [K, " is required"]);
|
||||||
|
error : badarg ->
|
||||||
|
return_http_error(404, "Bad gateway name")
|
||||||
|
end.
|
||||||
|
|
||||||
gateway_insta(delete, #{bindings := #{name := Name0}}) ->
|
gateway_insta(delete, #{bindings := #{name := Name0}}) ->
|
||||||
with_gateway(Name0, fun(GwName, _) ->
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
|
@ -69,18 +92,16 @@ gateway_insta(get, #{bindings := #{name := Name0}}) ->
|
||||||
GwConf = filled_raw_confs([<<"gateway">>, Name0]),
|
GwConf = filled_raw_confs([<<"gateway">>, Name0]),
|
||||||
LisConf = maps:get(<<"listeners">>, GwConf, #{}),
|
LisConf = maps:get(<<"listeners">>, GwConf, #{}),
|
||||||
NLisConf = emqx_gateway_http:mapping_listener_m2l(Name0, LisConf),
|
NLisConf = emqx_gateway_http:mapping_listener_m2l(Name0, LisConf),
|
||||||
{200, GwConf#{<<"listeners">> => NLisConf}}
|
{200, GwConf#{<<"name">> => Name0, <<"listeners">> => NLisConf}}
|
||||||
end);
|
end);
|
||||||
gateway_insta(put, #{body := GwConf0,
|
gateway_insta(put, #{body := GwConf0,
|
||||||
bindings := #{name := Name0}
|
bindings := #{name := Name0}
|
||||||
}) ->
|
}) ->
|
||||||
with_gateway(Name0, fun(_, _) ->
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
GwConf = maps:without([<<"authentication">>, <<"listeners">>], GwConf0),
|
GwConf = maps:without([<<"authentication">>, <<"listeners">>], GwConf0),
|
||||||
case emqx_gateway:update_rawconf(Name0, GwConf) of
|
case emqx_gateway_conf:update_gateway(GwName, GwConf) of
|
||||||
ok ->
|
ok ->
|
||||||
{200};
|
{200};
|
||||||
{error, not_found} ->
|
|
||||||
return_http_error(404, "Gateway not found");
|
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
return_http_error(500, Reason)
|
return_http_error(500, Reason)
|
||||||
end
|
end
|
||||||
|
|
@ -122,6 +143,16 @@ swagger("/gateway", get) ->
|
||||||
, responses =>
|
, responses =>
|
||||||
#{ <<"200">> => schema_gateway_overview_list() }
|
#{ <<"200">> => schema_gateway_overview_list() }
|
||||||
};
|
};
|
||||||
|
swagger("/gateway", post) ->
|
||||||
|
#{ description => <<"Load a gateway">>
|
||||||
|
, requestBody => schema_gateway_conf()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"204">> => schema_no_content()
|
||||||
|
}
|
||||||
|
};
|
||||||
swagger("/gateway/:name", get) ->
|
swagger("/gateway/:name", get) ->
|
||||||
#{ description => <<"Get the gateway configurations">>
|
#{ description => <<"Get the gateway configurations">>
|
||||||
, parameters => params_gateway_name_in_path()
|
, parameters => params_gateway_name_in_path()
|
||||||
|
|
@ -189,7 +220,7 @@ schema_gateway_overview_list() ->
|
||||||
#{ type => object
|
#{ type => object
|
||||||
, properties => properties_gateway_overview()
|
, properties => properties_gateway_overview()
|
||||||
},
|
},
|
||||||
<<"Gateway Overview list">>
|
<<"Gateway list">>
|
||||||
).
|
).
|
||||||
|
|
||||||
%% XXX: This is whole confs for all type gateways. It is used to fill the
|
%% XXX: This is whole confs for all type gateways. It is used to fill the
|
||||||
|
|
@ -202,6 +233,7 @@ schema_gateway_overview_list() ->
|
||||||
<<"name">> => <<"authenticator1">>,
|
<<"name">> => <<"authenticator1">>,
|
||||||
<<"server_type">> => <<"built-in-database">>,
|
<<"server_type">> => <<"built-in-database">>,
|
||||||
<<"user_id_type">> => <<"clientid">>},
|
<<"user_id_type">> => <<"clientid">>},
|
||||||
|
<<"name">> => <<"coap">>,
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
<<"enable_stats">> => true,<<"heartbeat">> => <<"30s">>,
|
<<"enable_stats">> => true,<<"heartbeat">> => <<"30s">>,
|
||||||
<<"idle_timeout">> => <<"30s">>,
|
<<"idle_timeout">> => <<"30s">>,
|
||||||
|
|
@ -219,6 +251,7 @@ schema_gateway_overview_list() ->
|
||||||
|
|
||||||
-define(EXPROTO_GATEWAY_CONFS,
|
-define(EXPROTO_GATEWAY_CONFS,
|
||||||
#{<<"enable">> => true,
|
#{<<"enable">> => true,
|
||||||
|
<<"name">> => <<"exproto">>,
|
||||||
<<"enable_stats">> => true,
|
<<"enable_stats">> => true,
|
||||||
<<"handler">> =>
|
<<"handler">> =>
|
||||||
#{<<"address">> => <<"http://127.0.0.1:9001">>},
|
#{<<"address">> => <<"http://127.0.0.1:9001">>},
|
||||||
|
|
@ -236,6 +269,7 @@ schema_gateway_overview_list() ->
|
||||||
|
|
||||||
-define(LWM2M_GATEWAY_CONFS,
|
-define(LWM2M_GATEWAY_CONFS,
|
||||||
#{<<"auto_observe">> => false,
|
#{<<"auto_observe">> => false,
|
||||||
|
<<"name">> => <<"lwm2m">>,
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
<<"enable_stats">> => true,
|
<<"enable_stats">> => true,
|
||||||
<<"idle_timeout">> => <<"30s">>,
|
<<"idle_timeout">> => <<"30s">>,
|
||||||
|
|
@ -264,6 +298,7 @@ schema_gateway_overview_list() ->
|
||||||
#{<<"password">> => <<"abc">>,
|
#{<<"password">> => <<"abc">>,
|
||||||
<<"username">> => <<"mqtt_sn_user">>},
|
<<"username">> => <<"mqtt_sn_user">>},
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
|
<<"name">> => <<"mqtt-sn">>,
|
||||||
<<"enable_qos3">> => true,<<"enable_stats">> => true,
|
<<"enable_qos3">> => true,<<"enable_stats">> => true,
|
||||||
<<"gateway_id">> => 1,<<"idle_timeout">> => <<"30s">>,
|
<<"gateway_id">> => 1,<<"idle_timeout">> => <<"30s">>,
|
||||||
<<"listeners">> => [
|
<<"listeners">> => [
|
||||||
|
|
@ -290,6 +325,7 @@ schema_gateway_overview_list() ->
|
||||||
#{<<"password">> => <<"${Packet.headers.passcode}">>,
|
#{<<"password">> => <<"${Packet.headers.passcode}">>,
|
||||||
<<"username">> => <<"${Packet.headers.login}">>},
|
<<"username">> => <<"${Packet.headers.login}">>},
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
|
<<"name">> => <<"stomp">>,
|
||||||
<<"enable_stats">> => true,
|
<<"enable_stats">> => true,
|
||||||
<<"frame">> =>
|
<<"frame">> =>
|
||||||
#{<<"max_body_length">> => 8192,<<"max_headers">> => 10,
|
#{<<"max_body_length">> => 8192,<<"max_headers">> => 10,
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,144 @@
|
||||||
|
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
|
-import(emqx_gateway_http,
|
||||||
|
[ return_http_error/2
|
||||||
|
, schema_bad_request/0
|
||||||
|
, schema_not_found/0
|
||||||
|
, schema_internal_error/0
|
||||||
|
, schema_no_content/0
|
||||||
|
, with_gateway/2
|
||||||
|
, checks/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% minirest behaviour callbacks
|
%% minirest behaviour callbacks
|
||||||
-export([api_spec/0]).
|
-export([api_spec/0]).
|
||||||
|
|
||||||
|
%% http handlers
|
||||||
|
-export([authn/2]).
|
||||||
|
|
||||||
|
%% internal export for emqx_gateway_api_listeners module
|
||||||
|
-export([schema_authn/0]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% minirest behaviour callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
{[], []}.
|
{metadata(apis()), []}.
|
||||||
|
|
||||||
|
apis() ->
|
||||||
|
[ {"/gateway/:name/authentication", authn}
|
||||||
|
].
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% http handlers
|
||||||
|
|
||||||
|
authn(get, #{bindings := #{name := Name0}}) ->
|
||||||
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
{200, emqx_gateway_http:authn(GwName)}
|
||||||
|
end);
|
||||||
|
|
||||||
|
authn(put, #{bindings := #{name := Name0},
|
||||||
|
body := Body}) ->
|
||||||
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
ok = emqx_gateway_http:update_authn(GwName, Body),
|
||||||
|
{204}
|
||||||
|
end);
|
||||||
|
|
||||||
|
authn(post, #{bindings := #{name := Name0},
|
||||||
|
body := Body}) ->
|
||||||
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
ok = emqx_gateway_http:add_authn(GwName, Body),
|
||||||
|
{204}
|
||||||
|
end);
|
||||||
|
|
||||||
|
authn(delete, #{bindings := #{name := Name0}}) ->
|
||||||
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
ok = emqx_gateway_http:remove_authn(GwName),
|
||||||
|
{204}
|
||||||
|
end).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Swagger defines
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
metadata(APIs) ->
|
||||||
|
metadata(APIs, []).
|
||||||
|
metadata([], APIAcc) ->
|
||||||
|
lists:reverse(APIAcc);
|
||||||
|
metadata([{Path, Fun}|More], APIAcc) ->
|
||||||
|
Methods = [get, post, put, delete, patch],
|
||||||
|
Mds = lists:foldl(fun(M, Acc) ->
|
||||||
|
try
|
||||||
|
Acc#{M => swagger(Path, M)}
|
||||||
|
catch
|
||||||
|
error : function_clause ->
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end, #{}, Methods),
|
||||||
|
metadata(More, [{Path, Mds, Fun} | APIAcc]).
|
||||||
|
|
||||||
|
swagger("/gateway/:name/authentication", get) ->
|
||||||
|
#{ description => <<"Get the gateway authentication">>
|
||||||
|
, parameters => params_gateway_name_in_path()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"200">> => schema_authn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
swagger("/gateway/:name/authentication", put) ->
|
||||||
|
#{ description => <<"Create the gateway authentication">>
|
||||||
|
, parameters => params_gateway_name_in_path()
|
||||||
|
, requestBody => schema_authn()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"204">> => schema_no_content()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
swagger("/gateway/:name/authentication", post) ->
|
||||||
|
#{ description => <<"Add authentication for the gateway">>
|
||||||
|
, parameters => params_gateway_name_in_path()
|
||||||
|
, requestBody => schema_authn()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"204">> => schema_no_content()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
swagger("/gateway/:name/authentication", delete) ->
|
||||||
|
#{ description => <<"Remove the gateway authentication">>
|
||||||
|
, parameters => params_gateway_name_in_path()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"204">> => schema_no_content()
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% params defines
|
||||||
|
|
||||||
|
params_gateway_name_in_path() ->
|
||||||
|
[#{ name => name
|
||||||
|
, in => path
|
||||||
|
, schema => #{type => string}
|
||||||
|
, required => true
|
||||||
|
}].
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% schemas
|
||||||
|
|
||||||
|
schema_authn() ->
|
||||||
|
#{ description => <<"OK">>
|
||||||
|
, content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => minirest:ref(<<"AuthenticatorInstance">>)
|
||||||
|
}}
|
||||||
|
}.
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,23 @@
|
||||||
|
|
||||||
-import(emqx_gateway_http,
|
-import(emqx_gateway_http,
|
||||||
[ return_http_error/2
|
[ return_http_error/2
|
||||||
, with_gateway/2
|
|
||||||
, checks/2
|
|
||||||
, schema_bad_request/0
|
, schema_bad_request/0
|
||||||
, schema_not_found/0
|
, schema_not_found/0
|
||||||
, schema_internal_error/0
|
, schema_internal_error/0
|
||||||
, schema_no_content/0
|
, schema_no_content/0
|
||||||
|
, with_gateway/2
|
||||||
|
, checks/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-import(emqx_gateway_api_authn, [schema_authn/0]).
|
||||||
|
|
||||||
%% minirest behaviour callbacks
|
%% minirest behaviour callbacks
|
||||||
-export([api_spec/0]).
|
-export([api_spec/0]).
|
||||||
|
|
||||||
%% http handlers
|
%% http handlers
|
||||||
-export([ listeners/2
|
-export([ listeners/2
|
||||||
, listeners_insta/2
|
, listeners_insta/2
|
||||||
|
, listeners_insta_authn/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -46,7 +49,9 @@ api_spec() ->
|
||||||
apis() ->
|
apis() ->
|
||||||
[ {"/gateway/:name/listeners", listeners}
|
[ {"/gateway/:name/listeners", listeners}
|
||||||
, {"/gateway/:name/listeners/:id", listeners_insta}
|
, {"/gateway/:name/listeners/:id", listeners_insta}
|
||||||
|
, {"/gateway/:name/listeners/:id/authentication", listeners_insta_authn}
|
||||||
].
|
].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% http handlers
|
%% http handlers
|
||||||
|
|
||||||
|
|
@ -69,13 +74,8 @@ listeners(post, #{bindings := #{name := Name0}, body := LConf}) ->
|
||||||
undefined ->
|
undefined ->
|
||||||
ListenerId = emqx_gateway_utils:listener_id(
|
ListenerId = emqx_gateway_utils:listener_id(
|
||||||
GwName, Type, LName),
|
GwName, Type, LName),
|
||||||
case emqx_gateway_http:update_listener(
|
ok = emqx_gateway_http:add_listener(ListenerId, LConf),
|
||||||
ListenerId, LConf) of
|
{204};
|
||||||
ok ->
|
|
||||||
{204};
|
|
||||||
{error, Reason} ->
|
|
||||||
return_http_error(500, Reason)
|
|
||||||
end;
|
|
||||||
_ ->
|
_ ->
|
||||||
return_http_error(400, "Listener name has occupied")
|
return_http_error(400, "Listener name has occupied")
|
||||||
end
|
end
|
||||||
|
|
@ -84,12 +84,8 @@ listeners(post, #{bindings := #{name := Name0}, body := LConf}) ->
|
||||||
listeners_insta(delete, #{bindings := #{name := Name0, id := ListenerId0}}) ->
|
listeners_insta(delete, #{bindings := #{name := Name0, id := ListenerId0}}) ->
|
||||||
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
||||||
with_gateway(Name0, fun(_GwName, _) ->
|
with_gateway(Name0, fun(_GwName, _) ->
|
||||||
case emqx_gateway_http:remove_listener(ListenerId) of
|
ok = emqx_gateway_http:remove_listener(ListenerId),
|
||||||
ok -> {204};
|
{204}
|
||||||
{error, not_found} -> {204};
|
|
||||||
{error, Reason} ->
|
|
||||||
return_http_error(500, Reason)
|
|
||||||
end
|
|
||||||
end);
|
end);
|
||||||
listeners_insta(get, #{bindings := #{name := Name0, id := ListenerId0}}) ->
|
listeners_insta(get, #{bindings := #{name := Name0, id := ListenerId0}}) ->
|
||||||
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
||||||
|
|
@ -108,12 +104,38 @@ listeners_insta(put, #{body := LConf,
|
||||||
}) ->
|
}) ->
|
||||||
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
||||||
with_gateway(Name0, fun(_GwName, _) ->
|
with_gateway(Name0, fun(_GwName, _) ->
|
||||||
case emqx_gateway_http:update_listener(ListenerId, LConf) of
|
ok = emqx_gateway_http:update_listener(ListenerId, LConf),
|
||||||
ok ->
|
{204}
|
||||||
{204};
|
end).
|
||||||
{error, Reason} ->
|
|
||||||
return_http_error(500, Reason)
|
listeners_insta_authn(get, #{bindings := #{name := Name0,
|
||||||
end
|
id := ListenerId0}}) ->
|
||||||
|
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
||||||
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
{200, emqx_gateway_http:authn(GwName, ListenerId)}
|
||||||
|
end);
|
||||||
|
listeners_insta_authn(post, #{body := Conf,
|
||||||
|
bindings := #{name := Name0,
|
||||||
|
id := ListenerId0}}) ->
|
||||||
|
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
||||||
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
ok = emqx_gateway_http:add_authn(GwName, ListenerId, Conf),
|
||||||
|
{204}
|
||||||
|
end);
|
||||||
|
listeners_insta_authn(put, #{body := Conf,
|
||||||
|
bindings := #{name := Name0,
|
||||||
|
id := ListenerId0}}) ->
|
||||||
|
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
||||||
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
ok = emqx_gateway_http:update_authn(GwName, ListenerId, Conf),
|
||||||
|
{204}
|
||||||
|
end);
|
||||||
|
listeners_insta_authn(delete, #{bindings := #{name := Name0,
|
||||||
|
id := ListenerId0}}) ->
|
||||||
|
ListenerId = emqx_mgmt_util:urldecode(ListenerId0),
|
||||||
|
with_gateway(Name0, fun(GwName, _) ->
|
||||||
|
ok = emqx_gateway_http:remove_authn(GwName, ListenerId),
|
||||||
|
{204}
|
||||||
end).
|
end).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -190,6 +212,52 @@ swagger("/gateway/:name/listeners/:id", put) ->
|
||||||
, <<"500">> => schema_internal_error()
|
, <<"500">> => schema_internal_error()
|
||||||
, <<"200">> => schema_no_content()
|
, <<"200">> => schema_no_content()
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
swagger("/gateway/:name/listeners/:id/authentication", get) ->
|
||||||
|
#{ description => <<"Get the listener's authentication info">>
|
||||||
|
, parameters => params_gateway_name_in_path()
|
||||||
|
++ params_listener_id_in_path()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"200">> => schema_authn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
swagger("/gateway/:name/listeners/:id/authentication", post) ->
|
||||||
|
#{ description => <<"Add authentication for the listener">>
|
||||||
|
, parameters => params_gateway_name_in_path()
|
||||||
|
++ params_listener_id_in_path()
|
||||||
|
, requestBody => schema_authn()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"204">> => schema_no_content()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
swagger("/gateway/:name/listeners/:id/authentication", put) ->
|
||||||
|
#{ description => <<"Update authentication for the listener">>
|
||||||
|
, parameters => params_gateway_name_in_path()
|
||||||
|
++ params_listener_id_in_path()
|
||||||
|
, requestBody => schema_authn()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"204">> => schema_no_content()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
swagger("/gateway/:name/listeners/:id/authentication", delete) ->
|
||||||
|
#{ description => <<"Remove authentication for the listener">>
|
||||||
|
, parameters => params_gateway_name_in_path()
|
||||||
|
++ params_listener_id_in_path()
|
||||||
|
, responses =>
|
||||||
|
#{ <<"400">> => schema_bad_request()
|
||||||
|
, <<"404">> => schema_not_found()
|
||||||
|
, <<"500">> => schema_internal_error()
|
||||||
|
, <<"204">> => schema_no_content()
|
||||||
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -301,7 +369,6 @@ raw_properties_common_listener() ->
|
||||||
<<"Listener type. Enum: tcp, udp, ssl, dtls">>,
|
<<"Listener type. Enum: tcp, udp, ssl, dtls">>,
|
||||||
[<<"tcp">>, <<"ssl">>, <<"udp">>, <<"dtls">>]}
|
[<<"tcp">>, <<"ssl">>, <<"udp">>, <<"dtls">>]}
|
||||||
, {running, boolean, <<"Listener running status">>}
|
, {running, boolean, <<"Listener running status">>}
|
||||||
%% FIXME:
|
|
||||||
, {bind, string, <<"Listener bind address or port">>}
|
, {bind, string, <<"Listener bind address or port">>}
|
||||||
, {acceptors, integer, <<"Listener acceptors number">>}
|
, {acceptors, integer, <<"Listener acceptors number">>}
|
||||||
, {access_rules, {array, string}, <<"Listener Access rules for client">>}
|
, {access_rules, {array, string}, <<"Listener Access rules for client">>}
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,17 @@
|
||||||
|
|
||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
-define(CONF_CALLBACK_MODULE, emqx_gateway).
|
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
{ok, Sup} = emqx_gateway_sup:start_link(),
|
{ok, Sup} = emqx_gateway_sup:start_link(),
|
||||||
emqx_gateway_cli:load(),
|
emqx_gateway_cli:load(),
|
||||||
load_default_gateway_applications(),
|
load_default_gateway_applications(),
|
||||||
load_gateway_by_default(),
|
load_gateway_by_default(),
|
||||||
emqx_config_handler:add_handler([gateway], ?CONF_CALLBACK_MODULE),
|
emqx_gateway_conf:load(),
|
||||||
{ok, Sup}.
|
{ok, Sup}.
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
|
emqx_gateway_conf:unload(),
|
||||||
emqx_gateway_cli:unload(),
|
emqx_gateway_cli:unload(),
|
||||||
%% XXX: No api now
|
|
||||||
%emqx_config_handler:remove_handler([gateway], ?MODULE),
|
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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 configuration management module
|
||||||
|
-module(emqx_gateway_conf).
|
||||||
|
|
||||||
|
%% Load/Unload
|
||||||
|
-export([ load/0
|
||||||
|
, unload/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% APIs
|
||||||
|
-export([ load_gateway/2
|
||||||
|
, update_gateway/2
|
||||||
|
, remove_gateway/1
|
||||||
|
, add_listener/3
|
||||||
|
, update_listener/3
|
||||||
|
, remove_listener/2
|
||||||
|
, add_authn/2
|
||||||
|
, add_authn/3
|
||||||
|
, update_authn/2
|
||||||
|
, update_authn/3
|
||||||
|
, remove_authn/1
|
||||||
|
, remove_authn/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% callbacks for emqx_config_handler
|
||||||
|
-export([ pre_config_update/2
|
||||||
|
, post_config_update/4
|
||||||
|
]).
|
||||||
|
|
||||||
|
-type atom_or_bin() :: atom() | binary().
|
||||||
|
-type ok_or_err() :: ok_or_err().
|
||||||
|
-type listener_ref() :: {ListenerType :: atom_or_bin(),
|
||||||
|
ListenerName :: atom_or_bin()}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Load/Unload
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec load() -> ok.
|
||||||
|
load() ->
|
||||||
|
emqx_config_handler:add_handler([gateway], ?MODULE).
|
||||||
|
|
||||||
|
-spec unload() -> ok.
|
||||||
|
unload() ->
|
||||||
|
emqx_config_handler:remove_handler([gateway]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
|
||||||
|
-spec load_gateway(atom_or_bin(), map()) -> ok_or_err().
|
||||||
|
load_gateway(GwName, Conf) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), Conf}).
|
||||||
|
|
||||||
|
-spec update_gateway(atom_or_bin(), map()) -> ok_or_err().
|
||||||
|
update_gateway(GwName, Conf) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), Conf}).
|
||||||
|
|
||||||
|
-spec remove_gateway(atom_or_bin()) -> ok_or_err().
|
||||||
|
remove_gateway(GwName) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName)}).
|
||||||
|
|
||||||
|
-spec add_listener(atom_or_bin(), listener_ref(), map()) -> ok_or_err().
|
||||||
|
add_listener(GwName, ListenerRef, Conf) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf}).
|
||||||
|
|
||||||
|
-spec update_listener(atom_or_bin(), listener_ref(), map()) -> ok_or_err().
|
||||||
|
update_listener(GwName, ListenerRef, Conf) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf}).
|
||||||
|
|
||||||
|
-spec remove_listener(atom_or_bin(), listener_ref()) -> ok_or_err().
|
||||||
|
remove_listener(GwName, ListenerRef) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef)}).
|
||||||
|
|
||||||
|
-spec add_authn(atom_or_bin(), map()) -> ok_or_err().
|
||||||
|
add_authn(GwName, Conf) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), Conf}).
|
||||||
|
|
||||||
|
-spec add_authn(atom_or_bin(), listener_ref(), map()) -> ok_or_err().
|
||||||
|
add_authn(GwName, ListenerRef, Conf) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf}).
|
||||||
|
|
||||||
|
-spec update_authn(atom_or_bin(), map()) -> ok_or_err().
|
||||||
|
update_authn(GwName, Conf) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), Conf}).
|
||||||
|
|
||||||
|
-spec update_authn(atom_or_bin(), listener_ref(), map()) -> ok_or_err().
|
||||||
|
update_authn(GwName, ListenerRef, Conf) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef), Conf}).
|
||||||
|
|
||||||
|
-spec remove_authn(atom_or_bin()) -> ok_or_err().
|
||||||
|
remove_authn(GwName) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName)}).
|
||||||
|
|
||||||
|
-spec remove_authn(atom_or_bin(), listener_ref()) -> ok_or_err().
|
||||||
|
remove_authn(GwName, ListenerRef) ->
|
||||||
|
update({?FUNCTION_NAME, bin(GwName), bin(ListenerRef)}).
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
update(Req) ->
|
||||||
|
res(emqx:update_config([gateway], Req)).
|
||||||
|
|
||||||
|
res({ok, _Result}) -> ok;
|
||||||
|
res({error, {pre_config_update,emqx_gateway_conf,Reason}}) -> {error, Reason};
|
||||||
|
res({error, Reason}) -> {error, Reason}.
|
||||||
|
|
||||||
|
bin({LType, LName}) ->
|
||||||
|
{bin(LType), bin(LName)};
|
||||||
|
bin(A) when is_atom(A) ->
|
||||||
|
atom_to_binary(A);
|
||||||
|
bin(B) when is_binary(B) ->
|
||||||
|
B.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Config Handler
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec pre_config_update(emqx_config:update_request(),
|
||||||
|
emqx_config:raw_config()) ->
|
||||||
|
{ok, emqx_config:update_request()} | {error, term()}.
|
||||||
|
pre_config_update({load_gateway, GwName, Conf}, RawConf) ->
|
||||||
|
case maps:get(GwName, RawConf, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{ok, emqx_map_lib:deep_merge(RawConf, #{GwName => Conf})};
|
||||||
|
_ ->
|
||||||
|
{error, already_exist}
|
||||||
|
end;
|
||||||
|
pre_config_update({update_gateway, GwName, Conf}, RawConf) ->
|
||||||
|
case maps:get(GwName, RawConf, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{error, not_found};
|
||||||
|
_ ->
|
||||||
|
NConf = maps:without([<<"listeners">>,
|
||||||
|
<<"authentication">>], Conf),
|
||||||
|
{ok, emqx_map_lib:deep_merge(RawConf, #{GwName => NConf})}
|
||||||
|
end;
|
||||||
|
pre_config_update({remove_gateway, GwName}, RawConf) ->
|
||||||
|
{ok, maps:remove(GwName, RawConf)};
|
||||||
|
|
||||||
|
pre_config_update({add_listener, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||||
|
case emqx_map_lib:deep_get(
|
||||||
|
[GwName, <<"listeners">>, LType, LName], RawConf, undefined) of
|
||||||
|
undefined ->
|
||||||
|
NListener = #{LType => #{LName => Conf}},
|
||||||
|
{ok, emqx_map_lib:deep_merge(
|
||||||
|
RawConf,
|
||||||
|
#{GwName => #{<<"listeners">> => NListener}})};
|
||||||
|
_ ->
|
||||||
|
{error, already_exist}
|
||||||
|
end;
|
||||||
|
pre_config_update({update_listener, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||||
|
case emqx_map_lib:deep_get(
|
||||||
|
[GwName, <<"listeners">>, LType, LName], RawConf, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{error, not_found};
|
||||||
|
_OldConf ->
|
||||||
|
NListener = #{LType => #{LName => Conf}},
|
||||||
|
{ok, emqx_map_lib:deep_merge(
|
||||||
|
RawConf,
|
||||||
|
#{GwName => #{<<"listeners">> => NListener}})}
|
||||||
|
|
||||||
|
end;
|
||||||
|
pre_config_update({remove_listener, GwName, {LType, LName}}, RawConf) ->
|
||||||
|
{ok, emqx_map_lib:deep_remove(
|
||||||
|
[GwName, <<"listeners">>, LType, LName], RawConf)};
|
||||||
|
|
||||||
|
pre_config_update({add_authn, GwName, Conf}, RawConf) ->
|
||||||
|
case emqx_map_lib:deep_get(
|
||||||
|
[GwName, <<"authentication">>], RawConf, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{ok, emqx_map_lib:deep_merge(
|
||||||
|
RawConf,
|
||||||
|
#{GwName => #{<<"authentication">> => Conf}})};
|
||||||
|
_ ->
|
||||||
|
{error, already_exist}
|
||||||
|
end;
|
||||||
|
pre_config_update({add_authn, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||||
|
case emqx_map_lib:deep_get(
|
||||||
|
[GwName, <<"listeners">>, LType, LName],
|
||||||
|
RawConf, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{error, not_found};
|
||||||
|
Listener ->
|
||||||
|
case maps:get(<<"authentication">>, Listener, undefined) of
|
||||||
|
undefined ->
|
||||||
|
NListener = maps:put(<<"authentication">>, Conf, Listener),
|
||||||
|
NGateway = #{GwName =>
|
||||||
|
#{<<"listeners">> =>
|
||||||
|
#{LType => #{LName => NListener}}}},
|
||||||
|
{ok, emqx_map_lib:deep_merge(RawConf, NGateway)};
|
||||||
|
_ ->
|
||||||
|
{error, already_exist}
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
pre_config_update({update_authn, GwName, Conf}, RawConf) ->
|
||||||
|
case emqx_map_lib:deep_get(
|
||||||
|
[GwName, <<"authentication">>], RawConf, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{error, not_found};
|
||||||
|
_ ->
|
||||||
|
{ok, emqx_map_lib:deep_merge(
|
||||||
|
RawConf,
|
||||||
|
#{GwName => #{<<"authentication">> => Conf}})}
|
||||||
|
end;
|
||||||
|
pre_config_update({update_authn, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||||
|
case emqx_map_lib:deep_get(
|
||||||
|
[GwName, <<"listeners">>, LType, LName],
|
||||||
|
RawConf, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{error, not_found};
|
||||||
|
Listener ->
|
||||||
|
case maps:get(<<"authentication">>, Listener, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{error, not_found};
|
||||||
|
Auth ->
|
||||||
|
NListener = maps:put(
|
||||||
|
<<"authentication">>,
|
||||||
|
emqx_map_lib:deep_merge(Auth, Conf),
|
||||||
|
Listener
|
||||||
|
),
|
||||||
|
NGateway = #{GwName =>
|
||||||
|
#{<<"listeners">> =>
|
||||||
|
#{LType => #{LName => NListener}}}},
|
||||||
|
{ok, emqx_map_lib:deep_merge(RawConf, NGateway)}
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
pre_config_update({remove_authn, GwName}, RawConf) ->
|
||||||
|
{ok, emqx_map_lib:deep_remove(
|
||||||
|
[GwName, <<"authentication">>], RawConf)};
|
||||||
|
pre_config_update({remove_authn, GwName, {LType, LName}}, RawConf) ->
|
||||||
|
Path = [GwName, <<"listeners">>, LType, LName, <<"authentication">>],
|
||||||
|
{ok, emqx_map_lib:deep_remove(Path, RawConf)};
|
||||||
|
|
||||||
|
pre_config_update(UnknownReq, _RawConf) ->
|
||||||
|
logger:error("Unknown configuration update request: ~0p", [UnknownReq]),
|
||||||
|
{error, badreq}.
|
||||||
|
|
||||||
|
-spec post_config_update(emqx_config:update_request(), emqx_config:config(),
|
||||||
|
emqx_config:config(), emqx_config:app_envs())
|
||||||
|
-> ok | {ok, Result::any()} | {error, Reason::term()}.
|
||||||
|
|
||||||
|
post_config_update(Req, NewConfig, OldConfig, _AppEnvs) ->
|
||||||
|
[_Tag, GwName0|_] = tuple_to_list(Req),
|
||||||
|
GwName = binary_to_existing_atom(GwName0),
|
||||||
|
|
||||||
|
case {maps:get(GwName, NewConfig, undefined),
|
||||||
|
maps:get(GwName, OldConfig, undefined)} of
|
||||||
|
{undefined, undefined} ->
|
||||||
|
ok; %% nothing to change
|
||||||
|
{undefined, Old} when is_map(Old) ->
|
||||||
|
emqx_gateway:unload(GwName);
|
||||||
|
{New, undefined} when is_map(New) ->
|
||||||
|
emqx_gateway:load(GwName, New);
|
||||||
|
{New, Old} when is_map(New), is_map(Old) ->
|
||||||
|
emqx_gateway:update(GwName, New)
|
||||||
|
end.
|
||||||
|
|
@ -29,8 +29,8 @@
|
||||||
-type context() ::
|
-type context() ::
|
||||||
#{ %% Gateway Name
|
#{ %% Gateway Name
|
||||||
gwname := gateway_name()
|
gwname := gateway_name()
|
||||||
%% Autenticator
|
%% Authentication chains
|
||||||
, auth := emqx_authn:chain_id() | undefined
|
, auth := [emqx_authentication:chain_name()] | undefined
|
||||||
%% The ConnectionManager PID
|
%% The ConnectionManager PID
|
||||||
, cm := pid()
|
, cm := pid()
|
||||||
}.
|
}.
|
||||||
|
|
@ -66,12 +66,8 @@
|
||||||
| {error, any()}.
|
| {error, any()}.
|
||||||
authenticate(_Ctx = #{auth := undefined}, ClientInfo) ->
|
authenticate(_Ctx = #{auth := undefined}, ClientInfo) ->
|
||||||
{ok, mountpoint(ClientInfo)};
|
{ok, mountpoint(ClientInfo)};
|
||||||
authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) ->
|
authenticate(_Ctx = #{auth := _ChainName}, ClientInfo0) ->
|
||||||
ClientInfo = ClientInfo0#{
|
ClientInfo = ClientInfo0#{zone => default},
|
||||||
zone => default,
|
|
||||||
listener => {tcp, default},
|
|
||||||
chain_id => ChainId
|
|
||||||
},
|
|
||||||
case emqx_access_control:authenticate(ClientInfo) of
|
case emqx_access_control:authenticate(ClientInfo) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{ok, mountpoint(ClientInfo)};
|
{ok, mountpoint(ClientInfo)};
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,22 @@
|
||||||
%% Mgmt APIs - listeners
|
%% Mgmt APIs - listeners
|
||||||
-export([ listeners/1
|
-export([ listeners/1
|
||||||
, listener/1
|
, listener/1
|
||||||
|
, add_listener/2
|
||||||
, remove_listener/1
|
, remove_listener/1
|
||||||
, update_listener/2
|
, update_listener/2
|
||||||
, mapping_listener_m2l/2
|
, mapping_listener_m2l/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-export([ authn/1
|
||||||
|
, authn/2
|
||||||
|
, add_authn/2
|
||||||
|
, add_authn/3
|
||||||
|
, update_authn/2
|
||||||
|
, update_authn/3
|
||||||
|
, remove_authn/1
|
||||||
|
, remove_authn/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% Mgmt APIs - clients
|
%% Mgmt APIs - clients
|
||||||
-export([ lookup_client/3
|
-export([ lookup_client/3
|
||||||
, lookup_client/4
|
, lookup_client/4
|
||||||
|
|
@ -171,12 +182,13 @@ listener(GwName, Type, Conf) ->
|
||||||
[begin
|
[begin
|
||||||
ListenerId = emqx_gateway_utils:listener_id(GwName, Type, LName),
|
ListenerId = emqx_gateway_utils:listener_id(GwName, Type, LName),
|
||||||
Running = is_running(ListenerId, LConf),
|
Running = is_running(ListenerId, LConf),
|
||||||
LConf#{
|
bind2str(
|
||||||
id => ListenerId,
|
LConf#{
|
||||||
type => Type,
|
id => ListenerId,
|
||||||
name => LName,
|
type => Type,
|
||||||
running => Running
|
name => LName,
|
||||||
}
|
running => Running
|
||||||
|
})
|
||||||
end || {LName, LConf} <- Conf, is_map(LConf)].
|
end || {LName, LConf} <- Conf, is_map(LConf)].
|
||||||
|
|
||||||
is_running(ListenerId, #{<<"bind">> := ListenOn0}) ->
|
is_running(ListenerId, #{<<"bind">> := ListenOn0}) ->
|
||||||
|
|
@ -188,27 +200,78 @@ is_running(ListenerId, #{<<"bind">> := ListenOn0}) ->
|
||||||
false
|
false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec remove_listener(binary()) -> ok | {error, not_found} | {error, any()}.
|
bind2str(LConf = #{bind := Bind}) when is_integer(Bind) ->
|
||||||
remove_listener(ListenerId) ->
|
maps:put(bind, integer_to_binary(Bind), LConf);
|
||||||
{GwName, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
bind2str(LConf = #{<<"bind">> := Bind}) when is_integer(Bind) ->
|
||||||
LConf = emqx:get_raw_config(
|
maps:put(<<"bind">>, integer_to_binary(Bind), LConf);
|
||||||
[<<"gateway">>, GwName, <<"listeners">>, Type]
|
bind2str(LConf = #{bind := Bind}) when is_binary(Bind) ->
|
||||||
),
|
LConf;
|
||||||
NLConf = maps:remove(Name, LConf),
|
bind2str(LConf = #{<<"bind">> := Bind}) when is_binary(Bind) ->
|
||||||
emqx_gateway:update_rawconf(
|
LConf.
|
||||||
GwName,
|
|
||||||
#{<<"listeners">> => #{Type => NLConf}}
|
|
||||||
).
|
|
||||||
|
|
||||||
-spec update_listener(atom() | binary(), map()) -> ok | {error, any()}.
|
-spec add_listener(atom() | binary(), map()) -> ok.
|
||||||
update_listener(ListenerId, NewConf0) ->
|
add_listener(ListenerId, NewConf0) ->
|
||||||
{GwName, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
{GwName, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||||
NewConf = maps:without([<<"id">>, <<"name">>,
|
NewConf = maps:without([<<"id">>, <<"name">>,
|
||||||
<<"type">>, <<"running">>], NewConf0),
|
<<"type">>, <<"running">>], NewConf0),
|
||||||
emqx_gateway:update_rawconf(
|
confexp(emqx_gateway_conf:add_listener(GwName, {Type, Name}, NewConf)).
|
||||||
GwName,
|
|
||||||
#{<<"listeners">> => #{Type => #{Name => NewConf}}
|
-spec update_listener(atom() | binary(), map()) -> ok.
|
||||||
}).
|
update_listener(ListenerId, NewConf0) ->
|
||||||
|
{GwName, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||||
|
|
||||||
|
NewConf = maps:without([<<"id">>, <<"name">>,
|
||||||
|
<<"type">>, <<"running">>], NewConf0),
|
||||||
|
confexp(emqx_gateway_conf:update_listener(GwName, {Type, Name}, NewConf)).
|
||||||
|
|
||||||
|
-spec remove_listener(binary()) -> ok.
|
||||||
|
remove_listener(ListenerId) ->
|
||||||
|
{GwName, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||||
|
confexp(emqx_gateway_conf:remove_listener(GwName, {Type, Name})).
|
||||||
|
|
||||||
|
-spec authn(gateway_name()) -> map().
|
||||||
|
authn(GwName) ->
|
||||||
|
Path = [gateway, GwName, authentication],
|
||||||
|
emqx_map_lib:jsonable_map(emqx:get_config(Path)).
|
||||||
|
|
||||||
|
-spec authn(gateway_name(), binary()) -> map().
|
||||||
|
authn(GwName, ListenerId) ->
|
||||||
|
{_, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||||
|
Path = [gateway, GwName, listeners, Type, Name, authentication],
|
||||||
|
emqx_map_lib:jsonable_map(emqx:get_config(Path)).
|
||||||
|
|
||||||
|
-spec add_authn(gateway_name(), map()) -> ok.
|
||||||
|
add_authn(GwName, AuthConf) ->
|
||||||
|
confexp(emqx_gateway_conf:add_authn(GwName, AuthConf)).
|
||||||
|
|
||||||
|
-spec add_authn(gateway_name(), binary(), map()) -> ok.
|
||||||
|
add_authn(GwName, ListenerId, AuthConf) ->
|
||||||
|
{_, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||||
|
confexp(emqx_gateway_conf:add_authn(GwName, {Type, Name}, AuthConf)).
|
||||||
|
|
||||||
|
-spec update_authn(gateway_name(), map()) -> ok.
|
||||||
|
update_authn(GwName, AuthConf) ->
|
||||||
|
confexp(emqx_gateway_conf:update_authn(GwName, AuthConf)).
|
||||||
|
|
||||||
|
-spec update_authn(gateway_name(), binary(), map()) -> ok.
|
||||||
|
update_authn(GwName, ListenerId, AuthConf) ->
|
||||||
|
{_, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||||
|
confexp(emqx_gateway_conf:update_authn(GwName, {Type, Name}, AuthConf)).
|
||||||
|
|
||||||
|
-spec remove_authn(gateway_name()) -> ok.
|
||||||
|
remove_authn(GwName) ->
|
||||||
|
confexp(emqx_gateway_conf:remove_authn(GwName)).
|
||||||
|
|
||||||
|
-spec remove_authn(gateway_name(), binary()) -> ok.
|
||||||
|
remove_authn(GwName, ListenerId) ->
|
||||||
|
{_, Type, Name} = emqx_gateway_utils:parse_listener_id(ListenerId),
|
||||||
|
confexp(emqx_gateway_conf:remove_authn(GwName, {Type, Name})).
|
||||||
|
|
||||||
|
confexp(ok) -> ok;
|
||||||
|
confexp({error, not_found}) ->
|
||||||
|
error({update_conf_error, not_found});
|
||||||
|
confexp({error, already_exist}) ->
|
||||||
|
error({update_conf_error, already_exist}).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Mgmt APIs - clients
|
%% Mgmt APIs - clients
|
||||||
|
|
@ -328,10 +391,22 @@ with_gateway(GwName0, Fun) ->
|
||||||
catch
|
catch
|
||||||
error : badname ->
|
error : badname ->
|
||||||
return_http_error(404, "Bad gateway name");
|
return_http_error(404, "Bad gateway name");
|
||||||
|
%% Exceptions from: checks/2
|
||||||
error : {miss_param, K} ->
|
error : {miss_param, K} ->
|
||||||
return_http_error(400, [K, " is required"]);
|
return_http_error(400, [K, " is required"]);
|
||||||
|
%% Exceptions from emqx_gateway_utils:parse_listener_id/1
|
||||||
error : {invalid_listener_id, Id} ->
|
error : {invalid_listener_id, Id} ->
|
||||||
return_http_error(400, ["invalid listener id: ", Id]);
|
return_http_error(400, ["invalid listener id: ", Id]);
|
||||||
|
%% Exceptions from: emqx:get_config/1
|
||||||
|
error : {config_not_found, Path0} ->
|
||||||
|
Path = lists:concat(
|
||||||
|
lists:join(".", lists:map(fun to_list/1, Path0))),
|
||||||
|
return_http_error(404, "Resource not found. path: " ++ Path);
|
||||||
|
%% Exceptions from: confexp/1
|
||||||
|
error : {update_conf_error, not_found} ->
|
||||||
|
return_http_error(404, "Resource not found");
|
||||||
|
error : {update_conf_error, already_exist} ->
|
||||||
|
return_http_error(400, "Resource already exist");
|
||||||
Class : Reason : Stk ->
|
Class : Reason : Stk ->
|
||||||
?LOG(error, "Uncatched error: {~p, ~p}, stacktrace: ~0p",
|
?LOG(error, "Uncatched error: {~p, ~p}, stacktrace: ~0p",
|
||||||
[Class, Reason, Stk]),
|
[Class, Reason, Stk]),
|
||||||
|
|
@ -348,6 +423,11 @@ checks([K|Ks], Map) ->
|
||||||
error({miss_param, K})
|
error({miss_param, K})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
to_list(A) when is_atom(A) ->
|
||||||
|
atom_to_list(A);
|
||||||
|
to_list(B) when is_binary(B) ->
|
||||||
|
binary_to_list(B).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% common schemas
|
%% common schemas
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
name :: gateway_name(),
|
name :: gateway_name(),
|
||||||
config :: emqx_config:config(),
|
config :: emqx_config:config(),
|
||||||
ctx :: emqx_gateway_ctx:context(),
|
ctx :: emqx_gateway_ctx:context(),
|
||||||
|
authns :: [emqx_authentication:chain_name()],
|
||||||
status :: stopped | running,
|
status :: stopped | running,
|
||||||
child_pids :: [pid()],
|
child_pids :: [pid()],
|
||||||
gw_state :: emqx_gateway_impl:state() | undefined,
|
gw_state :: emqx_gateway_impl:state() | undefined,
|
||||||
|
|
@ -94,16 +95,23 @@ init([Gateway, Ctx, _GwDscrptr]) ->
|
||||||
State = #state{
|
State = #state{
|
||||||
ctx = Ctx,
|
ctx = Ctx,
|
||||||
name = GwName,
|
name = GwName,
|
||||||
|
authns = [],
|
||||||
config = Config,
|
config = Config,
|
||||||
child_pids = [],
|
child_pids = [],
|
||||||
status = stopped,
|
status = stopped,
|
||||||
created_at = erlang:system_time(millisecond)
|
created_at = erlang:system_time(millisecond)
|
||||||
},
|
},
|
||||||
case cb_gateway_load(State) of
|
case maps:get(enable, Config, true) of
|
||||||
{error, Reason} ->
|
false ->
|
||||||
{stop, {load_gateway_failure, Reason}};
|
?LOG(info, "Skipp to start ~s gateway due to disabled", [GwName]),
|
||||||
{ok, NState} ->
|
{ok, State};
|
||||||
{ok, NState}
|
true ->
|
||||||
|
case cb_gateway_load(State) of
|
||||||
|
{error, Reason} ->
|
||||||
|
{stop, {load_gateway_failure, Reason}};
|
||||||
|
{ok, NState} ->
|
||||||
|
{ok, NState}
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
handle_call(info, _From, State) ->
|
handle_call(info, _From, State) ->
|
||||||
|
|
@ -174,9 +182,9 @@ handle_info(Info, State) ->
|
||||||
?LOG(warning, "Unexcepted info: ~p", [Info]),
|
?LOG(warning, "Unexcepted info: ~p", [Info]),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, State = #state{ctx = Ctx, child_pids = Pids}) ->
|
terminate(_Reason, State = #state{child_pids = Pids}) ->
|
||||||
Pids /= [] andalso (_ = cb_gateway_unload(State)),
|
Pids /= [] andalso (_ = cb_gateway_unload(State)),
|
||||||
_ = do_deinit_authn(maps:get(auth, Ctx, undefined)),
|
_ = do_deinit_authn(State#state.authns),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
code_change(_OldVsn, State, _Extra) ->
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
|
@ -197,52 +205,102 @@ detailed_gateway_info(State) ->
|
||||||
%% Internal funcs
|
%% Internal funcs
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
do_init_authn(GwName, Config) ->
|
%% same with emqx_authentication:global_chain/1
|
||||||
case maps:get(authentication, Config, #{enable => false}) of
|
global_chain(mqtt) ->
|
||||||
#{enable := false} -> undefined;
|
'mqtt:global';
|
||||||
AuthCfg when is_map(AuthCfg) ->
|
global_chain('mqtt-sn') ->
|
||||||
case maps:get(enable, AuthCfg, true) of
|
'mqtt-sn:global';
|
||||||
false ->
|
global_chain(coap) ->
|
||||||
undefined;
|
'coap:global';
|
||||||
_ ->
|
global_chain(lwm2m) ->
|
||||||
%% TODO: Implement Authentication
|
'lwm2m:global';
|
||||||
GwName
|
global_chain(stomp) ->
|
||||||
%case emqx_authn:create_chain(#{id => ChainId}) of
|
'stomp:global';
|
||||||
% {ok, _ChainInfo} ->
|
global_chain(_) ->
|
||||||
% case emqx_authn:create_authenticator(ChainId, AuthCfg) of
|
'unknown:global'.
|
||||||
% {ok, _} -> ChainId;
|
|
||||||
% {error, Reason} ->
|
listener_chain(GwName, Type, LisName) ->
|
||||||
% ?LOG(error, "Failed to create authentication ~p", [Reason]),
|
emqx_gateway_utils:listener_id(GwName, Type, LisName).
|
||||||
% throw({bad_authentication, Reason})
|
|
||||||
% end;
|
%% There are two layer authentication configs
|
||||||
% {error, Reason} ->
|
%% stomp.authn
|
||||||
% ?LOG(error, "Failed to create authentication chain: ~p", [Reason]),
|
%% / \
|
||||||
% throw({bad_chain, {ChainId, Reason}})
|
%% listeners.tcp.defautl.authn *.ssl.default.authn
|
||||||
%end.
|
%%
|
||||||
end;
|
|
||||||
_ ->
|
init_authn(GwName, Config) ->
|
||||||
undefined
|
Authns = authns(GwName, Config),
|
||||||
|
try
|
||||||
|
do_init_authn(Authns, [])
|
||||||
|
catch
|
||||||
|
throw : Reason = {badauth, _} ->
|
||||||
|
do_deinit_authn(proplists:get_keys(Authns)),
|
||||||
|
throw(Reason)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_deinit_authn(undefined) ->
|
do_init_authn([], Names) ->
|
||||||
ok;
|
Names;
|
||||||
do_deinit_authn(AuthnRef) ->
|
do_init_authn([{_ChainName, _AuthConf = #{enable := false}}|More], Names) ->
|
||||||
%% TODO:
|
do_init_authn(More, Names);
|
||||||
?LOG(warning, "Failed to clean authn ~p, not suppported now", [AuthnRef]).
|
do_init_authn([{ChainName, AuthConf}|More], Names) when is_map(AuthConf) ->
|
||||||
%case emqx_authn:delete_chain(AuthnRef) of
|
_ = application:ensure_all_started(emqx_authn),
|
||||||
% ok -> ok;
|
do_create_authn_chain(ChainName, AuthConf),
|
||||||
% {error, {not_found, _}} ->
|
do_init_authn(More, [ChainName|Names]);
|
||||||
% ?LOG(warning, "Failed to clean authentication chain: ~s, "
|
do_init_authn([_BadConf|More], Names) ->
|
||||||
% "reason: not_found", [AuthnRef]);
|
do_init_authn(More, Names).
|
||||||
% {error, Reason} ->
|
|
||||||
% ?LOG(error, "Failed to clean authentication chain: ~s, "
|
authns(GwName, Config) ->
|
||||||
% "reason: ~p", [AuthnRef, Reason])
|
Listeners = maps:to_list(maps:get(listeners, Config, #{})),
|
||||||
%end.
|
lists:append(
|
||||||
|
[ [{listener_chain(GwName, LisType, LisName), authn_conf(Opts)}
|
||||||
|
|| {LisName, Opts} <- maps:to_list(LisNames) ]
|
||||||
|
|| {LisType, LisNames} <- Listeners])
|
||||||
|
++ [{global_chain(GwName), authn_conf(Config)}].
|
||||||
|
|
||||||
|
authn_conf(Conf) ->
|
||||||
|
maps:get(authentication, Conf, #{enable => false}).
|
||||||
|
|
||||||
|
do_create_authn_chain(ChainName, AuthConf) ->
|
||||||
|
case ensure_chain(ChainName) of
|
||||||
|
ok ->
|
||||||
|
case emqx_authentication:create_authenticator(ChainName, AuthConf) of
|
||||||
|
{ok, _} -> ok;
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "Failed to create authenticator chain ~s, "
|
||||||
|
"reason: ~p, config: ~p",
|
||||||
|
[ChainName, Reason, AuthConf]),
|
||||||
|
throw({badauth, Reason})
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "Falied to create authn chain ~s, reason ~p",
|
||||||
|
[ChainName, Reason]),
|
||||||
|
throw({badauth, Reason})
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_chain(ChainName) ->
|
||||||
|
case emqx_authentication:create_chain(ChainName) of
|
||||||
|
{ok, _ChainInfo} ->
|
||||||
|
ok;
|
||||||
|
{error, {already_exists, _}} ->
|
||||||
|
ok;
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_deinit_authn(Names) ->
|
||||||
|
lists:foreach(fun(ChainName) ->
|
||||||
|
case emqx_authentication:delete_chain(ChainName) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, {not_found, _}} -> ok;
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "Failed to clean authentication chain: ~s, "
|
||||||
|
"reason: ~p", [ChainName, Reason])
|
||||||
|
end
|
||||||
|
end, Names).
|
||||||
|
|
||||||
do_update_one_by_one(NCfg0, State = #state{
|
do_update_one_by_one(NCfg0, State = #state{
|
||||||
ctx = Ctx,
|
config = OCfg,
|
||||||
config = OCfg,
|
status = Status}) ->
|
||||||
status = Status}) ->
|
|
||||||
|
|
||||||
NCfg = emqx_map_lib:deep_merge(OCfg, NCfg0),
|
NCfg = emqx_map_lib:deep_merge(OCfg, NCfg0),
|
||||||
|
|
||||||
|
|
@ -263,14 +321,9 @@ do_update_one_by_one(NCfg0, State = #state{
|
||||||
true -> State;
|
true -> State;
|
||||||
false ->
|
false ->
|
||||||
%% Reset Authentication first
|
%% Reset Authentication first
|
||||||
_ = do_deinit_authn(maps:get(auth, Ctx, undefined)),
|
_ = do_deinit_authn(State#state.authns),
|
||||||
NCtx = Ctx#{
|
AuthnNames = init_authn(State#state.name, NCfg),
|
||||||
auth => do_init_authn(
|
State#state{authns = AuthnNames}
|
||||||
State#state.name,
|
|
||||||
NCfg
|
|
||||||
)
|
|
||||||
},
|
|
||||||
State#state{ctx = NCtx}
|
|
||||||
end,
|
end,
|
||||||
cb_gateway_update(NCfg, NState);
|
cb_gateway_update(NCfg, NState);
|
||||||
Status == running, NEnable == false ->
|
Status == running, NEnable == false ->
|
||||||
|
|
@ -289,6 +342,7 @@ cb_gateway_unload(State = #state{name = GwName,
|
||||||
#{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName),
|
#{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName),
|
||||||
CbMod:on_gateway_unload(Gateway, GwState),
|
CbMod:on_gateway_unload(Gateway, GwState),
|
||||||
{ok, State#state{child_pids = [],
|
{ok, State#state{child_pids = [],
|
||||||
|
authns = [],
|
||||||
status = stopped,
|
status = stopped,
|
||||||
gw_state = undefined,
|
gw_state = undefined,
|
||||||
started_at = undefined,
|
started_at = undefined,
|
||||||
|
|
@ -300,6 +354,8 @@ cb_gateway_unload(State = #state{name = GwName,
|
||||||
[GwName, GwState,
|
[GwName, GwState,
|
||||||
Class, Reason, Stk]),
|
Class, Reason, Stk]),
|
||||||
{error, {Class, Reason, Stk}}
|
{error, {Class, Reason, Stk}}
|
||||||
|
after
|
||||||
|
_ = do_deinit_authn(State#state.authns)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc 1. Create Authentcation Context
|
%% @doc 1. Create Authentcation Context
|
||||||
|
|
@ -311,38 +367,33 @@ cb_gateway_load(State = #state{name = GwName,
|
||||||
ctx = Ctx}) ->
|
ctx = Ctx}) ->
|
||||||
|
|
||||||
Gateway = detailed_gateway_info(State),
|
Gateway = detailed_gateway_info(State),
|
||||||
|
try
|
||||||
case maps:get(enable, Config, true) of
|
AuthnNames = init_authn(GwName, Config),
|
||||||
false ->
|
NCtx = Ctx#{auth => AuthnNames},
|
||||||
?LOG(info, "Skipp to start ~s gateway due to disabled", [GwName]);
|
#{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName),
|
||||||
true ->
|
case CbMod:on_gateway_load(Gateway, NCtx) of
|
||||||
try
|
{error, Reason} ->
|
||||||
AuthnRef = do_init_authn(GwName, Config),
|
do_deinit_authn(AuthnNames),
|
||||||
NCtx = Ctx#{auth => AuthnRef},
|
throw({callback_return_error, Reason});
|
||||||
#{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName),
|
{ok, ChildPidOrSpecs, GwState} ->
|
||||||
case CbMod:on_gateway_load(Gateway, NCtx) of
|
ChildPids = start_child_process(ChildPidOrSpecs),
|
||||||
{error, Reason} ->
|
{ok, State#state{
|
||||||
do_deinit_authn(AuthnRef),
|
ctx = NCtx,
|
||||||
throw({callback_return_error, Reason});
|
authns = AuthnNames,
|
||||||
{ok, ChildPidOrSpecs, GwState} ->
|
status = running,
|
||||||
ChildPids = start_child_process(ChildPidOrSpecs),
|
child_pids = ChildPids,
|
||||||
{ok, State#state{
|
gw_state = GwState,
|
||||||
ctx = NCtx,
|
stopped_at = undefined,
|
||||||
status = running,
|
started_at = erlang:system_time(millisecond)
|
||||||
child_pids = ChildPids,
|
}}
|
||||||
gw_state = GwState,
|
end
|
||||||
stopped_at = undefined,
|
catch
|
||||||
started_at = erlang:system_time(millisecond)
|
Class : Reason1 : Stk ->
|
||||||
}}
|
?LOG(error, "Failed to load ~s gateway (~0p, ~0p) "
|
||||||
end
|
"crashed: {~p, ~p}, stacktrace: ~0p",
|
||||||
catch
|
[GwName, Gateway, Ctx,
|
||||||
Class : Reason1 : Stk ->
|
Class, Reason1, Stk]),
|
||||||
?LOG(error, "Failed to load ~s gateway (~0p, ~0p) "
|
{error, {Class, Reason1, Stk}}
|
||||||
"crashed: {~p, ~p}, stacktrace: ~0p",
|
|
||||||
[GwName, Gateway, Ctx,
|
|
||||||
Class, Reason1, Stk]),
|
|
||||||
{error, {Class, Reason1, Stk}}
|
|
||||||
end
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
cb_gateway_update(Config,
|
cb_gateway_update(Config,
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,11 @@ namespace() -> gateway.
|
||||||
roots() -> [gateway].
|
roots() -> [gateway].
|
||||||
|
|
||||||
fields(gateway) ->
|
fields(gateway) ->
|
||||||
[{stomp, sc(ref(stomp))},
|
[{stomp, sc_meta(ref(stomp) , #{nullable => {true, recursively}})},
|
||||||
{mqttsn, sc(ref(mqttsn))},
|
{mqttsn, sc_meta(ref(mqttsn) , #{nullable => {true, recursively}})},
|
||||||
{coap, sc(ref(coap))},
|
{coap, sc_meta(ref(coap) , #{nullable => {true, recursively}})},
|
||||||
{lwm2m, sc(ref(lwm2m))},
|
{lwm2m, sc_meta(ref(lwm2m) , #{nullable => {true, recursively}})},
|
||||||
{exproto, sc(ref(exproto))}
|
{exproto, sc_meta(ref(exproto), #{nullable => {true, recursively}})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(stomp) ->
|
fields(stomp) ->
|
||||||
|
|
@ -92,10 +92,10 @@ fields(coap) ->
|
||||||
|
|
||||||
fields(lwm2m) ->
|
fields(lwm2m) ->
|
||||||
[ {xml_dir, sc(binary())}
|
[ {xml_dir, sc(binary())}
|
||||||
, {lifetime_min, sc(duration())}
|
, {lifetime_min, sc(duration(), "1s")}
|
||||||
, {lifetime_max, sc(duration())}
|
, {lifetime_max, sc(duration(), "86400s")}
|
||||||
, {qmode_time_windonw, sc(integer())}
|
, {qmode_time_window, sc(integer(), 22)}
|
||||||
, {auto_observe, sc(boolean())}
|
, {auto_observe, sc(boolean(), false)}
|
||||||
, {update_msg_publish_condition, sc(hoconsc:union([always, contains_object_list]))}
|
, {update_msg_publish_condition, sc(hoconsc:union([always, contains_object_list]))}
|
||||||
, {translators, sc(ref(translators))}
|
, {translators, sc(ref(translators))}
|
||||||
, {listeners, sc(ref(udp_listeners))}
|
, {listeners, sc(ref(udp_listeners))}
|
||||||
|
|
@ -154,8 +154,8 @@ fields(udp_tcp_listeners) ->
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(tcp_listener) ->
|
fields(tcp_listener) ->
|
||||||
[
|
[ %% some special confs for tcp listener
|
||||||
%% some special confs for tcp listener
|
{acceptors, sc(integer(), 16)}
|
||||||
] ++
|
] ++
|
||||||
tcp_opts() ++
|
tcp_opts() ++
|
||||||
proxy_protocol_opts() ++
|
proxy_protocol_opts() ++
|
||||||
|
|
@ -175,6 +175,8 @@ fields(udp_listener) ->
|
||||||
common_listener_opts();
|
common_listener_opts();
|
||||||
|
|
||||||
fields(dtls_listener) ->
|
fields(dtls_listener) ->
|
||||||
|
[ {acceptors, sc(integer(), 16)}
|
||||||
|
] ++
|
||||||
fields(udp_listener) ++
|
fields(udp_listener) ++
|
||||||
[{dtls, sc_meta(ref(dtls_opts),
|
[{dtls, sc_meta(ref(dtls_opts),
|
||||||
#{desc => "DTLS listener options"})}];
|
#{desc => "DTLS listener options"})}];
|
||||||
|
|
@ -191,29 +193,33 @@ fields(dtls_opts) ->
|
||||||
emqx_schema:server_ssl_opts_schema(
|
emqx_schema:server_ssl_opts_schema(
|
||||||
#{ depth => 10
|
#{ depth => 10
|
||||||
, reuse_sessions => true
|
, reuse_sessions => true
|
||||||
, versions => dtls
|
, versions => dtls_all_available
|
||||||
, ciphers => dtls
|
, ciphers => dtls_all_available
|
||||||
}, false).
|
}, false).
|
||||||
|
|
||||||
% authentication() ->
|
authentication() ->
|
||||||
% hoconsc:union(
|
sc_meta(hoconsc:union(
|
||||||
% [ undefined
|
[ hoconsc:ref(emqx_authn_mnesia, config)
|
||||||
% , hoconsc:ref(emqx_authn_mnesia, config)
|
, hoconsc:ref(emqx_authn_mysql, config)
|
||||||
% , hoconsc:ref(emqx_authn_mysql, config)
|
, hoconsc:ref(emqx_authn_pgsql, config)
|
||||||
% , hoconsc:ref(emqx_authn_pgsql, config)
|
, hoconsc:ref(emqx_authn_mongodb, standalone)
|
||||||
% , hoconsc:ref(emqx_authn_mongodb, standalone)
|
, hoconsc:ref(emqx_authn_mongodb, 'replica-set')
|
||||||
% , hoconsc:ref(emqx_authn_mongodb, 'replica-set')
|
, hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster')
|
||||||
% , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster')
|
, hoconsc:ref(emqx_authn_redis, standalone)
|
||||||
% , hoconsc:ref(emqx_authn_redis, standalone)
|
, hoconsc:ref(emqx_authn_redis, cluster)
|
||||||
% , hoconsc:ref(emqx_authn_redis, cluster)
|
, hoconsc:ref(emqx_authn_redis, sentinel)
|
||||||
% , hoconsc:ref(emqx_authn_redis, sentinel)
|
, hoconsc:ref(emqx_authn_http, get)
|
||||||
% , hoconsc:ref(emqx_authn_http, get)
|
, hoconsc:ref(emqx_authn_http, post)
|
||||||
% , hoconsc:ref(emqx_authn_http, post)
|
, hoconsc:ref(emqx_authn_jwt, 'hmac-based')
|
||||||
% , hoconsc:ref(emqx_authn_jwt, 'hmac-based')
|
, hoconsc:ref(emqx_authn_jwt, 'public-key')
|
||||||
% , hoconsc:ref(emqx_authn_jwt, 'public-key')
|
, hoconsc:ref(emqx_authn_jwt, 'jwks')
|
||||||
% , hoconsc:ref(emqx_authn_jwt, 'jwks')
|
, hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
|
||||||
% , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
|
]),
|
||||||
% ]).
|
#{nullable => {true, recursively},
|
||||||
|
desc =>
|
||||||
|
"""Default authentication configs for all of the gateway listeners.<br>
|
||||||
|
For per-listener overrides see <code>authentication</code>
|
||||||
|
in listener configs"""}).
|
||||||
|
|
||||||
gateway_common_options() ->
|
gateway_common_options() ->
|
||||||
[ {enable, sc(boolean(), true)}
|
[ {enable, sc(boolean(), true)}
|
||||||
|
|
@ -221,16 +227,15 @@ gateway_common_options() ->
|
||||||
, {idle_timeout, sc(duration(), <<"30s">>)}
|
, {idle_timeout, sc(duration(), <<"30s">>)}
|
||||||
, {mountpoint, sc(binary(), <<>>)}
|
, {mountpoint, sc(binary(), <<>>)}
|
||||||
, {clientinfo_override, sc(ref(clientinfo_override))}
|
, {clientinfo_override, sc(ref(clientinfo_override))}
|
||||||
, {authentication, sc(hoconsc:lazy(map()))}
|
, {authentication, authentication()}
|
||||||
].
|
].
|
||||||
|
|
||||||
common_listener_opts() ->
|
common_listener_opts() ->
|
||||||
[ {enable, sc(boolean(), true)}
|
[ {enable, sc(boolean(), true)}
|
||||||
, {bind, sc(union(ip_port(), integer()))}
|
, {bind, sc(union(ip_port(), integer()))}
|
||||||
, {acceptors, sc(integer(), 16)}
|
|
||||||
, {max_connections, sc(integer(), 1024)}
|
, {max_connections, sc(integer(), 1024)}
|
||||||
, {max_conn_rate, sc(integer())}
|
, {max_conn_rate, sc(integer())}
|
||||||
%, {rate_limit, sc(comma_separated_list())}
|
, {authentication, authentication()}
|
||||||
, {mountpoint, sc(binary(), undefined)}
|
, {mountpoint, sc(binary(), undefined)}
|
||||||
, {access_rules, sc(hoconsc:array(string()), [])}
|
, {access_rules, sc(hoconsc:array(string()), [])}
|
||||||
].
|
].
|
||||||
|
|
@ -242,8 +247,8 @@ udp_opts() ->
|
||||||
[{udp, sc_meta(ref(udp_opts), #{})}].
|
[{udp, sc_meta(ref(udp_opts), #{})}].
|
||||||
|
|
||||||
proxy_protocol_opts() ->
|
proxy_protocol_opts() ->
|
||||||
[ {proxy_protocol, sc(boolean())}
|
[ {proxy_protocol, sc(boolean(), false)}
|
||||||
, {proxy_protocol_timeout, sc(duration())}
|
, {proxy_protocol_timeout, sc(duration(), "15s")}
|
||||||
].
|
].
|
||||||
|
|
||||||
sc(Type) ->
|
sc(Type) ->
|
||||||
|
|
|
||||||
|
|
@ -117,13 +117,18 @@ format_listenon({Addr, Port}) when is_tuple(Addr) ->
|
||||||
|
|
||||||
parse_listenon(Port) when is_integer(Port) ->
|
parse_listenon(Port) when is_integer(Port) ->
|
||||||
Port;
|
Port;
|
||||||
|
parse_listenon(IpPort) when is_tuple(IpPort) ->
|
||||||
|
IpPort;
|
||||||
parse_listenon(Str) when is_binary(Str) ->
|
parse_listenon(Str) when is_binary(Str) ->
|
||||||
parse_listenon(binary_to_list(Str));
|
parse_listenon(binary_to_list(Str));
|
||||||
parse_listenon(Str) when is_list(Str) ->
|
parse_listenon(Str) when is_list(Str) ->
|
||||||
case emqx_schema:to_ip_port(Str) of
|
try list_to_integer(Str)
|
||||||
{ok, R} -> R;
|
catch _ : _ ->
|
||||||
{error, _} ->
|
case emqx_schema:to_ip_port(Str) of
|
||||||
error({invalid_listenon_name, Str})
|
{ok, R} -> R;
|
||||||
|
{error, _} ->
|
||||||
|
error({invalid_listenon_name, Str})
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
listener_id(GwName, Type, LisName) ->
|
listener_id(GwName, Type, LisName) ->
|
||||||
|
|
@ -226,11 +231,7 @@ sock_opts(Name, Opts) ->
|
||||||
%% Envs
|
%% Envs
|
||||||
|
|
||||||
active_n(Options) ->
|
active_n(Options) ->
|
||||||
maps:get(
|
maps:get(active_n, Options, ?ACTIVE_N).
|
||||||
active_n,
|
|
||||||
maps:get(listener, Options, #{active_n => ?ACTIVE_N}),
|
|
||||||
?ACTIVE_N
|
|
||||||
).
|
|
||||||
|
|
||||||
-spec idle_timeout(map()) -> pos_integer().
|
-spec idle_timeout(map()) -> pos_integer().
|
||||||
idle_timeout(Options) ->
|
idle_timeout(Options) ->
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,12 @@ init(ConnInfo = #{socktype := Socktype,
|
||||||
GRpcChann = maps:get(handler, Options),
|
GRpcChann = maps:get(handler, Options),
|
||||||
PoolName = maps:get(pool_name, Options),
|
PoolName = maps:get(pool_name, Options),
|
||||||
NConnInfo = default_conninfo(ConnInfo),
|
NConnInfo = default_conninfo(ConnInfo),
|
||||||
ClientInfo = default_clientinfo(ConnInfo),
|
ListenerId = case maps:get(listener, Options, undefined) of
|
||||||
|
undefined -> undefined;
|
||||||
|
{GwName, Type, LisName} ->
|
||||||
|
emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||||
|
end,
|
||||||
|
ClientInfo = maps:put(listener, ListenerId, default_clientinfo(ConnInfo)),
|
||||||
Channel = #channel{
|
Channel = #channel{
|
||||||
ctx = Ctx,
|
ctx = Ctx,
|
||||||
gcli = #{channel => GRpcChann, pool_name => PoolName},
|
gcli = #{channel => GRpcChann, pool_name => PoolName},
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
||||||
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
NCfg = Cfg#{
|
NCfg = Cfg#{
|
||||||
ctx => Ctx,
|
ctx => Ctx,
|
||||||
|
listener => {GwName, Type, LisName},
|
||||||
frame_mod => emqx_exproto_frame,
|
frame_mod => emqx_exproto_frame,
|
||||||
chann_mod => emqx_exproto_channel
|
chann_mod => emqx_exproto_channel
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,15 @@ init(ConnInfo = #{peername := {PeerHost, _},
|
||||||
#{ctx := Ctx} = Config) ->
|
#{ctx := Ctx} = Config) ->
|
||||||
Peercert = maps:get(peercert, ConnInfo, undefined),
|
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||||
Mountpoint = maps:get(mountpoint, Config, undefined),
|
Mountpoint = maps:get(mountpoint, Config, undefined),
|
||||||
|
ListenerId = case maps:get(listener, Config, undefined) of
|
||||||
|
undefined -> undefined;
|
||||||
|
{GwName, Type, LisName} ->
|
||||||
|
emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||||
|
end,
|
||||||
ClientInfo = set_peercert_infos(
|
ClientInfo = set_peercert_infos(
|
||||||
Peercert,
|
Peercert,
|
||||||
#{ zone => default
|
#{ zone => default
|
||||||
|
, listener => ListenerId
|
||||||
, protocol => lwm2m
|
, protocol => lwm2m
|
||||||
, peerhost => PeerHost
|
, peerhost => PeerHost
|
||||||
, sockport => SockPort
|
, sockport => SockPort
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
||||||
start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
||||||
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
NCfg = Cfg#{ ctx => Ctx
|
NCfg = Cfg#{ ctx => Ctx
|
||||||
|
, listener => {GwName, Type, LisName}
|
||||||
, frame_mod => emqx_coap_frame
|
, frame_mod => emqx_coap_frame
|
||||||
, chann_mod => emqx_lwm2m_channel
|
, chann_mod => emqx_lwm2m_channel
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -116,9 +116,15 @@ init(ConnInfo = #{peername := {PeerHost, _},
|
||||||
Registry = maps:get(registry, Option),
|
Registry = maps:get(registry, Option),
|
||||||
GwId = maps:get(gateway_id, Option),
|
GwId = maps:get(gateway_id, Option),
|
||||||
EnableQoS3 = maps:get(enable_qos3, Option, true),
|
EnableQoS3 = maps:get(enable_qos3, Option, true),
|
||||||
|
ListenerId = case maps:get(listener, Option, undefined) of
|
||||||
|
undefined -> undefined;
|
||||||
|
{GwName, Type, LisName} ->
|
||||||
|
emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||||
|
end,
|
||||||
ClientInfo = set_peercert_infos(
|
ClientInfo = set_peercert_infos(
|
||||||
Peercert,
|
Peercert,
|
||||||
#{ zone => default
|
#{ zone => default
|
||||||
|
, listener => ListenerId
|
||||||
, protocol => 'mqtt-sn'
|
, protocol => 'mqtt-sn'
|
||||||
, peerhost => PeerHost
|
, peerhost => PeerHost
|
||||||
, sockport => SockPort
|
, sockport => SockPort
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
||||||
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
NCfg = Cfg#{
|
NCfg = Cfg#{
|
||||||
ctx => Ctx,
|
ctx => Ctx,
|
||||||
|
listene => {GwName, Type, LisName},
|
||||||
frame_mod => emqx_sn_frame,
|
frame_mod => emqx_sn_frame,
|
||||||
chann_mod => emqx_sn_channel
|
chann_mod => emqx_sn_channel
|
||||||
},
|
},
|
||||||
|
|
@ -138,13 +139,13 @@ merge_default(Options) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
||||||
StopRet = stop_listener(GwName, LisName, Type, ListenOn, SocketOpts, Cfg),
|
StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg),
|
||||||
ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn),
|
ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn),
|
||||||
case StopRet of
|
case StopRet of
|
||||||
ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n",
|
ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n",
|
||||||
[GwName, Type, LisName, ListenOnStr]);
|
[GwName, Type, LisName, ListenOnStr]);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?ELOG("Failed to stop gatewat ~s:~s:~s on ~s: ~0p~n",
|
?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n",
|
||||||
[GwName, Type, LisName, ListenOnStr, Reason])
|
[GwName, Type, LisName, ListenOnStr, Reason])
|
||||||
end,
|
end,
|
||||||
StopRet.
|
StopRet.
|
||||||
|
|
|
||||||
|
|
@ -109,10 +109,15 @@ init(ConnInfo = #{peername := {PeerHost, _},
|
||||||
sockname := {_, SockPort}}, Option) ->
|
sockname := {_, SockPort}}, Option) ->
|
||||||
Peercert = maps:get(peercert, ConnInfo, undefined),
|
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||||
Mountpoint = maps:get(mountpoint, Option, undefined),
|
Mountpoint = maps:get(mountpoint, Option, undefined),
|
||||||
|
ListenerId = case maps:get(listener, Option, undefined) of
|
||||||
|
undefined -> undefined;
|
||||||
|
{GwName, Type, LisName} ->
|
||||||
|
emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||||
|
end,
|
||||||
ClientInfo = setting_peercert_infos(
|
ClientInfo = setting_peercert_infos(
|
||||||
Peercert,
|
Peercert,
|
||||||
#{ zone => default
|
#{ zone => default
|
||||||
, listener => {tcp, default}
|
, listener => ListenerId
|
||||||
, protocol => stomp
|
, protocol => stomp
|
||||||
, peerhost => PeerHost
|
, peerhost => PeerHost
|
||||||
, sockport => SockPort
|
, sockport => SockPort
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
||||||
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
Name = emqx_gateway_utils:listener_id(GwName, Type, LisName),
|
||||||
NCfg = Cfg#{
|
NCfg = Cfg#{
|
||||||
ctx => Ctx,
|
ctx => Ctx,
|
||||||
|
listener => {GwName, Type, LisName}, %% Used for authn
|
||||||
frame_mod => emqx_stomp_frame,
|
frame_mod => emqx_stomp_frame,
|
||||||
chann_mod => emqx_stomp_channel
|
chann_mod => emqx_stomp_channel
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ gateway.coap
|
||||||
connection_required = true
|
connection_required = true
|
||||||
subscribe_qos = qos1
|
subscribe_qos = qos1
|
||||||
publish_qos = qos1
|
publish_qos = qos1
|
||||||
authentication = undefined
|
|
||||||
|
|
||||||
listeners.udp.default
|
listeners.udp.default
|
||||||
{bind = 5683}
|
{bind = 5683}
|
||||||
|
|
@ -113,24 +112,24 @@ t_publish(_Config) ->
|
||||||
with_connection(Action).
|
with_connection(Action).
|
||||||
|
|
||||||
|
|
||||||
t_publish_authz_deny(_Config) ->
|
%t_publish_authz_deny(_Config) ->
|
||||||
Action = fun(Channel, Token) ->
|
% Action = fun(Channel, Token) ->
|
||||||
Topic = <<"/abc">>,
|
% Topic = <<"/abc">>,
|
||||||
Payload = <<"123">>,
|
% Payload = <<"123">>,
|
||||||
InvalidToken = lists:reverse(Token),
|
% InvalidToken = lists:reverse(Token),
|
||||||
|
%
|
||||||
TopicStr = binary_to_list(Topic),
|
% TopicStr = binary_to_list(Topic),
|
||||||
URI = ?PS_PREFIX ++ TopicStr ++ "?clientid=client1&token=" ++ InvalidToken,
|
% URI = ?PS_PREFIX ++ TopicStr ++ "?clientid=client1&token=" ++ InvalidToken,
|
||||||
|
%
|
||||||
%% Sub topic first
|
% %% Sub topic first
|
||||||
emqx:subscribe(Topic),
|
% emqx:subscribe(Topic),
|
||||||
|
%
|
||||||
Req = make_req(post, Payload),
|
% Req = make_req(post, Payload),
|
||||||
Result = do_request(Channel, URI, Req),
|
% Result = do_request(Channel, URI, Req),
|
||||||
?assertEqual({error, reset}, Result)
|
% ?assertEqual({error, reset}, Result)
|
||||||
end,
|
% end,
|
||||||
|
%
|
||||||
with_connection(Action).
|
% with_connection(Action).
|
||||||
|
|
||||||
t_subscribe(_Config) ->
|
t_subscribe(_Config) ->
|
||||||
Topic = <<"/abc">>,
|
Topic = <<"/abc">>,
|
||||||
|
|
|
||||||
|
|
@ -25,20 +25,18 @@
|
||||||
|
|
||||||
-define(CONF_DEFAULT, <<"
|
-define(CONF_DEFAULT, <<"
|
||||||
gateway.coap {
|
gateway.coap {
|
||||||
idle_timeout = 30s
|
idle_timeout = 30s
|
||||||
enable_stats = false
|
enable_stats = false
|
||||||
mountpoint = \"\"
|
mountpoint = \"\"
|
||||||
notify_type = qos
|
notify_type = qos
|
||||||
connection_required = true
|
connection_required = true
|
||||||
subscribe_qos = qos1
|
subscribe_qos = qos1
|
||||||
publish_qos = qos1
|
publish_qos = qos1
|
||||||
authentication = undefined
|
listeners.udp.default {
|
||||||
|
bind = 5683
|
||||||
listeners.udp.default {
|
}
|
||||||
bind = 5683
|
}
|
||||||
}
|
">>).
|
||||||
}
|
|
||||||
">>).
|
|
||||||
|
|
||||||
-define(HOST, "127.0.0.1").
|
-define(HOST, "127.0.0.1").
|
||||||
-define(PORT, 5683).
|
-define(PORT, 5683).
|
||||||
|
|
@ -73,7 +71,7 @@ t_send_request_api(_) ->
|
||||||
Payload = <<"simple echo this">>,
|
Payload = <<"simple echo this">>,
|
||||||
Req = #{token => Token,
|
Req = #{token => Token,
|
||||||
payload => Payload,
|
payload => Payload,
|
||||||
timeout => 10,
|
timeout => <<"10s">>,
|
||||||
content_type => <<"text/plain">>,
|
content_type => <<"text/plain">>,
|
||||||
method => <<"get">>},
|
method => <<"get">>},
|
||||||
Auth = emqx_mgmt_api_test_util:auth_header_(),
|
Auth = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_conf_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Setups
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Conf) ->
|
||||||
|
%% FIXME: Magic line. for saving gateway schema name for emqx_config
|
||||||
|
emqx_config:init_load(emqx_gateway_schema, <<"gateway {}">>),
|
||||||
|
emqx_ct_helpers:start_apps([emqx_gateway]),
|
||||||
|
Conf.
|
||||||
|
|
||||||
|
end_per_suite(_Conf) ->
|
||||||
|
emqx_ct_helpers:stop_apps([emqx_gateway]).
|
||||||
|
|
||||||
|
init_per_testcase(_CaseName, Conf) ->
|
||||||
|
_ = emqx_gateway_conf:remove_gateway(stomp),
|
||||||
|
Conf.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Cases
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(CONF_STOMP_BAISC_1,
|
||||||
|
#{ <<"idle_timeout">> => <<"10s">>,
|
||||||
|
<<"mountpoint">> => <<"t/">>,
|
||||||
|
<<"frame">> =>
|
||||||
|
#{ <<"max_headers">> => 20,
|
||||||
|
<<"max_headers_length">> => 2000,
|
||||||
|
<<"max_body_length">> => 2000
|
||||||
|
}
|
||||||
|
}).
|
||||||
|
-define(CONF_STOMP_BAISC_2,
|
||||||
|
#{ <<"idle_timeout">> => <<"20s">>,
|
||||||
|
<<"mountpoint">> => <<"t2/">>,
|
||||||
|
<<"frame">> =>
|
||||||
|
#{ <<"max_headers">> => 30,
|
||||||
|
<<"max_headers_length">> => 3000,
|
||||||
|
<<"max_body_length">> => 3000
|
||||||
|
}
|
||||||
|
}).
|
||||||
|
-define(CONF_STOMP_LISTENER_1,
|
||||||
|
#{ <<"bind">> => <<"61613">>
|
||||||
|
}).
|
||||||
|
-define(CONF_STOMP_LISTENER_2,
|
||||||
|
#{ <<"bind">> => <<"61614">>
|
||||||
|
}).
|
||||||
|
-define(CONF_STOMP_AUTHN_1,
|
||||||
|
#{ <<"mechanism">> => <<"password-based">>,
|
||||||
|
<<"backend">> => <<"built-in-database">>,
|
||||||
|
<<"user_id_type">> => <<"clientid">>
|
||||||
|
}).
|
||||||
|
-define(CONF_STOMP_AUTHN_2,
|
||||||
|
#{ <<"mechanism">> => <<"password-based">>,
|
||||||
|
<<"backend">> => <<"built-in-database">>,
|
||||||
|
<<"user_id_type">> => <<"username">>
|
||||||
|
}).
|
||||||
|
|
||||||
|
t_load_remove_gateway(_) ->
|
||||||
|
StompConf1 = compose(?CONF_STOMP_BAISC_1,
|
||||||
|
?CONF_STOMP_AUTHN_1,
|
||||||
|
?CONF_STOMP_LISTENER_1
|
||||||
|
),
|
||||||
|
StompConf2 = compose(?CONF_STOMP_BAISC_2,
|
||||||
|
?CONF_STOMP_AUTHN_1,
|
||||||
|
?CONF_STOMP_LISTENER_1),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:load_gateway(stomp, StompConf1),
|
||||||
|
{error, already_exist} =
|
||||||
|
emqx_gateway_conf:load_gateway(stomp, StompConf1),
|
||||||
|
assert_confs(StompConf1, emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:update_gateway(stomp, StompConf2),
|
||||||
|
assert_confs(StompConf2, emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:remove_gateway(stomp),
|
||||||
|
ok = emqx_gateway_conf:remove_gateway(stomp),
|
||||||
|
|
||||||
|
{error, not_found} =
|
||||||
|
emqx_gateway_conf:update_gateway(stomp, StompConf2),
|
||||||
|
|
||||||
|
?assertException(error, {config_not_found, [gateway, stomp]},
|
||||||
|
emqx:get_raw_config([gateway, stomp])),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_load_remove_authn(_) ->
|
||||||
|
StompConf = compose_listener(?CONF_STOMP_BAISC_1, ?CONF_STOMP_LISTENER_1),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:load_gateway(<<"stomp">>, StompConf),
|
||||||
|
assert_confs(StompConf, emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:add_authn(<<"stomp">>, ?CONF_STOMP_AUTHN_1),
|
||||||
|
assert_confs(
|
||||||
|
maps:put(<<"authentication">>, ?CONF_STOMP_AUTHN_1, StompConf),
|
||||||
|
emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:update_authn(<<"stomp">>, ?CONF_STOMP_AUTHN_2),
|
||||||
|
assert_confs(
|
||||||
|
maps:put(<<"authentication">>, ?CONF_STOMP_AUTHN_2, StompConf),
|
||||||
|
emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:remove_authn(<<"stomp">>),
|
||||||
|
|
||||||
|
{error, not_found} =
|
||||||
|
emqx_gateway_conf:update_authn(<<"stomp">>, ?CONF_STOMP_AUTHN_2),
|
||||||
|
|
||||||
|
?assertException(
|
||||||
|
error, {config_not_found, [gateway, stomp, authentication]},
|
||||||
|
emqx:get_raw_config([gateway, stomp, authentication])
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_load_remove_listeners(_) ->
|
||||||
|
StompConf = compose_authn(?CONF_STOMP_BAISC_1, ?CONF_STOMP_AUTHN_1),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:load_gateway(<<"stomp">>, StompConf),
|
||||||
|
assert_confs(StompConf, emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:add_listener(
|
||||||
|
<<"stomp">>, {<<"tcp">>, <<"default">>}, ?CONF_STOMP_LISTENER_1),
|
||||||
|
assert_confs(
|
||||||
|
maps:merge(StompConf, listener(?CONF_STOMP_LISTENER_1)),
|
||||||
|
emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:update_listener(
|
||||||
|
<<"stomp">>, {<<"tcp">>, <<"default">>}, ?CONF_STOMP_LISTENER_2),
|
||||||
|
assert_confs(
|
||||||
|
maps:merge(StompConf, listener(?CONF_STOMP_LISTENER_2)),
|
||||||
|
emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:remove_listener(
|
||||||
|
<<"stomp">>, {<<"tcp">>, <<"default">>}),
|
||||||
|
|
||||||
|
{error, not_found} =
|
||||||
|
emqx_gateway_conf:update_listener(
|
||||||
|
<<"stomp">>, {<<"tcp">>, <<"default">>}, ?CONF_STOMP_LISTENER_2),
|
||||||
|
|
||||||
|
?assertException(
|
||||||
|
error, {config_not_found, [gateway, stomp, listeners, tcp, default]},
|
||||||
|
emqx:get_raw_config([gateway, stomp, listeners, tcp, default])
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_load_remove_listener_authn(_) ->
|
||||||
|
StompConf = compose_listener(
|
||||||
|
?CONF_STOMP_BAISC_1,
|
||||||
|
?CONF_STOMP_LISTENER_1
|
||||||
|
),
|
||||||
|
StompConf1 = compose_listener_authn(
|
||||||
|
?CONF_STOMP_BAISC_1,
|
||||||
|
?CONF_STOMP_LISTENER_1,
|
||||||
|
?CONF_STOMP_AUTHN_1
|
||||||
|
),
|
||||||
|
StompConf2 = compose_listener_authn(
|
||||||
|
?CONF_STOMP_BAISC_1,
|
||||||
|
?CONF_STOMP_LISTENER_1,
|
||||||
|
?CONF_STOMP_AUTHN_2
|
||||||
|
),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:load_gateway(<<"stomp">>, StompConf),
|
||||||
|
assert_confs(StompConf, emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:add_authn(
|
||||||
|
<<"stomp">>, {<<"tcp">>, <<"default">>}, ?CONF_STOMP_AUTHN_1),
|
||||||
|
assert_confs(StompConf1, emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:update_authn(
|
||||||
|
<<"stomp">>, {<<"tcp">>, <<"default">>}, ?CONF_STOMP_AUTHN_2),
|
||||||
|
assert_confs(StompConf2, emqx:get_raw_config([gateway, stomp])),
|
||||||
|
|
||||||
|
ok = emqx_gateway_conf:remove_authn(
|
||||||
|
<<"stomp">>, {<<"tcp">>, <<"default">>}),
|
||||||
|
|
||||||
|
{error, not_found} =
|
||||||
|
emqx_gateway_conf:update_authn(
|
||||||
|
<<"stomp">>, {<<"tcp">>, <<"default">>}, ?CONF_STOMP_AUTHN_2),
|
||||||
|
|
||||||
|
Path = [gateway, stomp, listeners, tcp, default, authentication],
|
||||||
|
?assertException(
|
||||||
|
error, {config_not_found, Path},
|
||||||
|
emqx:get_raw_config(Path)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Utils
|
||||||
|
|
||||||
|
compose(Basic, Authn, Listener) ->
|
||||||
|
maps:merge(
|
||||||
|
maps:merge(Basic, #{<<"authentication">> => Authn}),
|
||||||
|
listener(Listener)).
|
||||||
|
|
||||||
|
compose_listener(Basic, Listener) ->
|
||||||
|
maps:merge(Basic, listener(Listener)).
|
||||||
|
|
||||||
|
compose_authn(Basic, Authn) ->
|
||||||
|
maps:merge(Basic, #{<<"authentication">> => Authn}).
|
||||||
|
|
||||||
|
compose_listener_authn(Basic, Listener, Authn) ->
|
||||||
|
maps:merge(
|
||||||
|
Basic,
|
||||||
|
listener(maps:put(<<"authentication">>, Authn, Listener))).
|
||||||
|
|
||||||
|
listener(L) ->
|
||||||
|
#{<<"listeners">> => #{<<"tcp">> => #{<<"default">> => L}}}.
|
||||||
|
|
||||||
|
assert_confs(Expected, Effected) ->
|
||||||
|
case do_assert_confs(Expected, Effected) of
|
||||||
|
false ->
|
||||||
|
io:format(standard_error, "Expected config: ~p,\n"
|
||||||
|
"Effected config: ~p",
|
||||||
|
[Expected, Effected]),
|
||||||
|
exit(conf_not_match);
|
||||||
|
true ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_assert_confs(Expected, Effected) when is_map(Expected),
|
||||||
|
is_map(Effected) ->
|
||||||
|
Ks1 = maps:keys(Expected),
|
||||||
|
lists:all(fun(K) ->
|
||||||
|
do_assert_confs(maps:get(K, Expected),
|
||||||
|
maps:get(K, Effected, undefined))
|
||||||
|
end, Ks1);
|
||||||
|
do_assert_confs(Expected, Effected) ->
|
||||||
|
Expected =:= Effected.
|
||||||
|
|
@ -33,7 +33,7 @@ gateway.lwm2m {
|
||||||
xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\"
|
xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\"
|
||||||
lifetime_min = 1s
|
lifetime_min = 1s
|
||||||
lifetime_max = 86400s
|
lifetime_max = 86400s
|
||||||
qmode_time_windonw = 22
|
qmode_time_window = 22
|
||||||
auto_observe = false
|
auto_observe = false
|
||||||
mountpoint = \"lwm2m/%u\"
|
mountpoint = \"lwm2m/%u\"
|
||||||
update_msg_publish_condition = contains_object_list
|
update_msg_publish_condition = contains_object_list
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ gateway.lwm2m {
|
||||||
xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\"
|
xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\"
|
||||||
lifetime_min = 1s
|
lifetime_min = 1s
|
||||||
lifetime_max = 86400s
|
lifetime_max = 86400s
|
||||||
qmode_time_windonw = 22
|
qmode_time_window = 22
|
||||||
auto_observe = false
|
auto_observe = false
|
||||||
mountpoint = \"lwm2m/%u\"
|
mountpoint = \"lwm2m/%u\"
|
||||||
update_msg_publish_condition = contains_object_list
|
update_msg_publish_condition = contains_object_list
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,12 @@
|
||||||
|
|
||||||
%% @doc EMQ X boot entrypoint.
|
%% @doc EMQ X boot entrypoint.
|
||||||
start() ->
|
start() ->
|
||||||
os:set_signal(sighup, ignore),
|
case os:type() of
|
||||||
os:set_signal(sigterm, handle), %% default is handle
|
{win32, nt} -> ok;
|
||||||
|
_nix ->
|
||||||
|
os:set_signal(sighup, ignore),
|
||||||
|
os:set_signal(sigterm, handle) %% default is handle
|
||||||
|
end,
|
||||||
ok = set_backtrace_depth(),
|
ok = set_backtrace_depth(),
|
||||||
ok = print_otp_version_warning(),
|
ok = print_otp_version_warning(),
|
||||||
|
|
||||||
|
|
@ -146,7 +150,6 @@ reboot_apps() ->
|
||||||
, emqx_management
|
, emqx_management
|
||||||
, emqx_retainer
|
, emqx_retainer
|
||||||
, emqx_exhook
|
, emqx_exhook
|
||||||
, emqx_rule_actions
|
|
||||||
, emqx_authn
|
, emqx_authn
|
||||||
, emqx_authz
|
, emqx_authz
|
||||||
].
|
].
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ fields("cluster") ->
|
||||||
, default => emqxcl
|
, default => emqxcl
|
||||||
})}
|
})}
|
||||||
, {"discovery_strategy",
|
, {"discovery_strategy",
|
||||||
sc(union([manual, static, mcast, dns, etcd, k8s]),
|
sc(hoconsc:enum([manual, static, mcast, dns, etcd, k8s]),
|
||||||
#{ default => manual
|
#{ default => manual
|
||||||
})}
|
})}
|
||||||
, {"autoclean",
|
, {"autoclean",
|
||||||
|
|
@ -122,7 +122,7 @@ fields("cluster") ->
|
||||||
sc(ref(cluster_mcast),
|
sc(ref(cluster_mcast),
|
||||||
#{})}
|
#{})}
|
||||||
, {"proto_dist",
|
, {"proto_dist",
|
||||||
sc(union([inet_tcp, inet6_tcp, inet_tls]),
|
sc(hoconsc:enum([inet_tcp, inet6_tcp, inet_tls]),
|
||||||
#{ mapping => "ekka.proto_dist"
|
#{ mapping => "ekka.proto_dist"
|
||||||
, default => inet_tcp
|
, default => inet_tcp
|
||||||
})}
|
})}
|
||||||
|
|
@ -136,7 +136,7 @@ fields("cluster") ->
|
||||||
sc(ref(cluster_k8s),
|
sc(ref(cluster_k8s),
|
||||||
#{})}
|
#{})}
|
||||||
, {"db_backend",
|
, {"db_backend",
|
||||||
sc(union([mnesia, rlog]),
|
sc(hoconsc:enum([mnesia, rlog]),
|
||||||
#{ mapping => "ekka.db_backend"
|
#{ mapping => "ekka.db_backend"
|
||||||
, default => mnesia
|
, default => mnesia
|
||||||
})}
|
})}
|
||||||
|
|
@ -224,7 +224,7 @@ fields(cluster_k8s) ->
|
||||||
#{ default => "emqx"
|
#{ default => "emqx"
|
||||||
})}
|
})}
|
||||||
, {"address_type",
|
, {"address_type",
|
||||||
sc(union([ip, dns, hostname]),
|
sc(hoconsc:enum([ip, dns, hostname]),
|
||||||
#{})}
|
#{})}
|
||||||
, {"app_name",
|
, {"app_name",
|
||||||
sc(string(),
|
sc(string(),
|
||||||
|
|
@ -242,7 +242,7 @@ fields(cluster_k8s) ->
|
||||||
|
|
||||||
fields("rlog") ->
|
fields("rlog") ->
|
||||||
[ {"role",
|
[ {"role",
|
||||||
sc(union([core, replicant]),
|
sc(hoconsc:enum([core, replicant]),
|
||||||
#{ mapping => "ekka.node_role"
|
#{ mapping => "ekka.node_role"
|
||||||
, default => core
|
, default => core
|
||||||
})}
|
})}
|
||||||
|
|
@ -334,7 +334,7 @@ fields("cluster_call") ->
|
||||||
|
|
||||||
fields("rpc") ->
|
fields("rpc") ->
|
||||||
[ {"mode",
|
[ {"mode",
|
||||||
sc(union(sync, async),
|
sc(hoconsc:enum([sync, async]),
|
||||||
#{ default => async
|
#{ default => async
|
||||||
})}
|
})}
|
||||||
, {"async_batch_size",
|
, {"async_batch_size",
|
||||||
|
|
@ -343,7 +343,7 @@ fields("rpc") ->
|
||||||
, default => 256
|
, default => 256
|
||||||
})}
|
})}
|
||||||
, {"port_discovery",
|
, {"port_discovery",
|
||||||
sc(union(manual, stateless),
|
sc(hoconsc:enum([manual, stateless]),
|
||||||
#{ mapping => "gen_rpc.port_discovery"
|
#{ mapping => "gen_rpc.port_discovery"
|
||||||
, default => stateless
|
, default => stateless
|
||||||
})}
|
})}
|
||||||
|
|
@ -434,7 +434,7 @@ fields("log_file_handler") ->
|
||||||
sc(ref("log_rotation"),
|
sc(ref("log_rotation"),
|
||||||
#{})}
|
#{})}
|
||||||
, {"max_size",
|
, {"max_size",
|
||||||
sc(union([infinity, emqx_schema:bytesize()]),
|
sc(hoconsc:union([infinity, emqx_schema:bytesize()]),
|
||||||
#{ default => "10MB"
|
#{ default => "10MB"
|
||||||
})}
|
})}
|
||||||
] ++ log_handler_common_confs();
|
] ++ log_handler_common_confs();
|
||||||
|
|
@ -464,7 +464,7 @@ fields("log_overload_kill") ->
|
||||||
#{ default => 20000
|
#{ default => 20000
|
||||||
})}
|
})}
|
||||||
, {"restart_after",
|
, {"restart_after",
|
||||||
sc(union(emqx_schema:duration(), infinity),
|
sc(hoconsc:union([emqx_schema:duration(), infinity]),
|
||||||
#{ default => "5s"
|
#{ default => "5s"
|
||||||
})}
|
})}
|
||||||
];
|
];
|
||||||
|
|
@ -582,7 +582,7 @@ log_handler_common_confs() ->
|
||||||
#{ default => unlimited
|
#{ default => unlimited
|
||||||
})}
|
})}
|
||||||
, {"formatter",
|
, {"formatter",
|
||||||
sc(union([text, json]),
|
sc(hoconsc:enum([text, json]),
|
||||||
#{ default => text
|
#{ default => text
|
||||||
})}
|
})}
|
||||||
, {"single_line",
|
, {"single_line",
|
||||||
|
|
@ -608,11 +608,11 @@ log_handler_common_confs() ->
|
||||||
sc(ref("log_burst_limit"),
|
sc(ref("log_burst_limit"),
|
||||||
#{})}
|
#{})}
|
||||||
, {"supervisor_reports",
|
, {"supervisor_reports",
|
||||||
sc(union([error, progress]),
|
sc(hoconsc:enum([error, progress]),
|
||||||
#{ default => error
|
#{ default => error
|
||||||
})}
|
})}
|
||||||
, {"max_depth",
|
, {"max_depth",
|
||||||
sc(union([unlimited, integer()]),
|
sc(hoconsc:union([unlimited, integer()]),
|
||||||
#{ default => 100
|
#{ default => 100
|
||||||
})}
|
})}
|
||||||
].
|
].
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ t_catch_up_status_handle_next_commit(_Config) ->
|
||||||
t_commit_ok_apply_fail_on_other_node_then_recover(_Config) ->
|
t_commit_ok_apply_fail_on_other_node_then_recover(_Config) ->
|
||||||
emqx_cluster_rpc:reset(),
|
emqx_cluster_rpc:reset(),
|
||||||
{atomic, []} = emqx_cluster_rpc:status(),
|
{atomic, []} = emqx_cluster_rpc:status(),
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(millisecond),
|
||||||
{M, F, A} = {?MODULE, failed_on_other_recover_after_5_second, [erlang:whereis(?NODE1), Now]},
|
{M, F, A} = {?MODULE, failed_on_other_recover_after_5_second, [erlang:whereis(?NODE1), Now]},
|
||||||
{ok, _, ok} = emqx_cluster_rpc:multicall(M, F, A, 1, 1000),
|
{ok, _, ok} = emqx_cluster_rpc:multicall(M, F, A, 1, 1000),
|
||||||
{ok, _, ok} = emqx_cluster_rpc:multicall(io, format, ["test"], 1, 1000),
|
{ok, _, ok} = emqx_cluster_rpc:multicall(io, format, ["test"], 1, 1000),
|
||||||
|
|
@ -132,10 +132,10 @@ t_commit_ok_apply_fail_on_other_node_then_recover(_Config) ->
|
||||||
?assertEqual([], L),
|
?assertEqual([], L),
|
||||||
?assertEqual({io, format, ["test"]}, maps:get(mfa, Status)),
|
?assertEqual({io, format, ["test"]}, maps:get(mfa, Status)),
|
||||||
?assertEqual(node(), maps:get(node, Status)),
|
?assertEqual(node(), maps:get(node, Status)),
|
||||||
sleep(3000),
|
sleep(2300),
|
||||||
{atomic, [Status1]} = emqx_cluster_rpc:status(),
|
{atomic, [Status1]} = emqx_cluster_rpc:status(),
|
||||||
?assertEqual(Status, Status1),
|
?assertEqual(Status, Status1),
|
||||||
sleep(2600),
|
sleep(3600),
|
||||||
{atomic, NewStatus} = emqx_cluster_rpc:status(),
|
{atomic, NewStatus} = emqx_cluster_rpc:status(),
|
||||||
?assertEqual(3, length(NewStatus)),
|
?assertEqual(3, length(NewStatus)),
|
||||||
Pid = self(),
|
Pid = self(),
|
||||||
|
|
@ -243,11 +243,11 @@ failed_on_node_by_odd(Pid) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
failed_on_other_recover_after_5_second(Pid, CreatedAt) ->
|
failed_on_other_recover_after_5_second(Pid, CreatedAt) ->
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(millisecond),
|
||||||
case Pid =:= self() of
|
case Pid =:= self() of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false ->
|
false ->
|
||||||
case Now < CreatedAt + 5 of
|
case Now < CreatedAt + 5001 of
|
||||||
true -> "MFA return not ok";
|
true -> "MFA return not ok";
|
||||||
false -> ok
|
false -> ok
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@
|
||||||
|
|
||||||
-include_lib("stdlib/include/qlc.hrl").
|
-include_lib("stdlib/include/qlc.hrl").
|
||||||
|
|
||||||
-export([paginate/3]).
|
-export([ paginate/3
|
||||||
|
, paginate/4
|
||||||
|
]).
|
||||||
|
|
||||||
%% first_next query APIs
|
%% first_next query APIs
|
||||||
-export([ params2qs/2
|
-export([ params2qs/2
|
||||||
|
|
@ -47,6 +49,23 @@ paginate(Tables, Params, RowFun) ->
|
||||||
#{meta => #{page => Page, limit => Limit, count => Count},
|
#{meta => #{page => Page, limit => Limit, count => Count},
|
||||||
data => [RowFun(Row) || Row <- Rows]}.
|
data => [RowFun(Row) || Row <- Rows]}.
|
||||||
|
|
||||||
|
paginate(Tables, MatchSpec, Params, RowFun) ->
|
||||||
|
Qh = query_handle(Tables, MatchSpec),
|
||||||
|
Count = count(Tables, MatchSpec),
|
||||||
|
Page = b2i(page(Params)),
|
||||||
|
Limit = b2i(limit(Params)),
|
||||||
|
Cursor = qlc:cursor(Qh),
|
||||||
|
case Page > 1 of
|
||||||
|
true ->
|
||||||
|
_ = qlc:next_answers(Cursor, (Page - 1) * Limit),
|
||||||
|
ok;
|
||||||
|
false -> ok
|
||||||
|
end,
|
||||||
|
Rows = qlc:next_answers(Cursor, Limit),
|
||||||
|
qlc:delete_cursor(Cursor),
|
||||||
|
#{meta => #{page => Page, limit => Limit, count => Count},
|
||||||
|
data => [RowFun(Row) || Row <- Rows]}.
|
||||||
|
|
||||||
query_handle(Table) when is_atom(Table) ->
|
query_handle(Table) when is_atom(Table) ->
|
||||||
qlc:q([R|| R <- ets:table(Table)]);
|
qlc:q([R|| R <- ets:table(Table)]);
|
||||||
query_handle([Table]) when is_atom(Table) ->
|
query_handle([Table]) when is_atom(Table) ->
|
||||||
|
|
@ -54,6 +73,16 @@ query_handle([Table]) when is_atom(Table) ->
|
||||||
query_handle(Tables) ->
|
query_handle(Tables) ->
|
||||||
qlc:append([qlc:q([E || E <- ets:table(T)]) || T <- Tables]).
|
qlc:append([qlc:q([E || E <- ets:table(T)]) || T <- Tables]).
|
||||||
|
|
||||||
|
query_handle(Table, MatchSpec) when is_atom(Table) ->
|
||||||
|
Options = {traverse, {select, MatchSpec}},
|
||||||
|
qlc:q([R|| R <- ets:table(Table, Options)]);
|
||||||
|
query_handle([Table], MatchSpec) when is_atom(Table) ->
|
||||||
|
Options = {traverse, {select, MatchSpec}},
|
||||||
|
qlc:q([R|| R <- ets:table(Table, Options)]);
|
||||||
|
query_handle(Tables, MatchSpec) ->
|
||||||
|
Options = {traverse, {select, MatchSpec}},
|
||||||
|
qlc:append([qlc:q([E || E <- ets:table(T, Options)]) || T <- Tables]).
|
||||||
|
|
||||||
count(Table) when is_atom(Table) ->
|
count(Table) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
count([Table]) when is_atom(Table) ->
|
count([Table]) when is_atom(Table) ->
|
||||||
|
|
@ -61,8 +90,16 @@ count([Table]) when is_atom(Table) ->
|
||||||
count(Tables) ->
|
count(Tables) ->
|
||||||
lists:sum([count(T) || T <- Tables]).
|
lists:sum([count(T) || T <- Tables]).
|
||||||
|
|
||||||
count(Table, Nodes) ->
|
count(Table, MatchSpec) when is_atom(Table) ->
|
||||||
lists:sum([rpc_call(Node, ets, info, [Table, size], 5000) || Node <- Nodes]).
|
[{MatchPattern, Where, _Re}] = MatchSpec,
|
||||||
|
NMatchSpec = [{MatchPattern, Where, [true]}],
|
||||||
|
ets:select_count(Table, NMatchSpec);
|
||||||
|
count([Table], MatchSpec) when is_atom(Table) ->
|
||||||
|
[{MatchPattern, Where, _Re}] = MatchSpec,
|
||||||
|
NMatchSpec = [{MatchPattern, Where, [true]}],
|
||||||
|
ets:select_count(Table, NMatchSpec);
|
||||||
|
count(Tables, MatchSpec) ->
|
||||||
|
lists:sum([count(T, MatchSpec) || T <- Tables]).
|
||||||
|
|
||||||
page(Params) when is_map(Params) ->
|
page(Params) when is_map(Params) ->
|
||||||
maps:get(<<"page">>, Params, 1);
|
maps:get(<<"page">>, Params, 1);
|
||||||
|
|
@ -122,7 +159,7 @@ cluster_query(Params, Tab, QsSchema, QueryFun) ->
|
||||||
Rows = do_cluster_query(Nodes, Tab, Qs, QueryFun, Start, Limit+1, []),
|
Rows = do_cluster_query(Nodes, Tab, Qs, QueryFun, Start, Limit+1, []),
|
||||||
Meta = #{page => Page, limit => Limit},
|
Meta = #{page => Page, limit => Limit},
|
||||||
NMeta = case CodCnt =:= 0 of
|
NMeta = case CodCnt =:= 0 of
|
||||||
true -> Meta#{count => count(Tab, Nodes)};
|
true -> Meta#{count => lists:sum([rpc_call(Node, ets, info, [Tab, size], 5000) || Node <- Nodes])};
|
||||||
_ -> Meta#{count => length(Rows)}
|
_ -> Meta#{count => length(Rows)}
|
||||||
end,
|
end,
|
||||||
#{meta => NMeta, data => lists:sublist(Rows, Limit)}.
|
#{meta => NMeta, data => lists:sublist(Rows, Limit)}.
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,19 @@
|
||||||
|
|
||||||
-behavior(minirest_api).
|
-behavior(minirest_api).
|
||||||
|
|
||||||
-import(emqx_mgmt_util, [ page_params/0
|
-include_lib("typerefl/include/types.hrl").
|
||||||
, schema/1
|
|
||||||
, schema/2
|
-import(hoconsc, [mk/2, ref/1, ref/2]).
|
||||||
, object_schema/2
|
|
||||||
, error_schema/2
|
|
||||||
, page_object_schema/1
|
|
||||||
, properties/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
-define(MAX_PAYLOAD_LENGTH, 2048).
|
-define(MAX_PAYLOAD_LENGTH, 2048).
|
||||||
-define(PAYLOAD_TOO_LARGE, 'PAYLOAD_TOO_LARGE').
|
-define(PAYLOAD_TOO_LARGE, 'PAYLOAD_TOO_LARGE').
|
||||||
|
|
||||||
-export([ status/2
|
-export([status/2
|
||||||
, delayed_messages/2
|
, delayed_messages/2
|
||||||
, delayed_message/2
|
, delayed_message/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-export([paths/0, fields/1, schema/1]).
|
||||||
|
|
||||||
%% for rpc
|
%% for rpc
|
||||||
-export([update_config_/1]).
|
-export([update_config_/1]).
|
||||||
|
|
@ -49,91 +46,94 @@
|
||||||
-define(MESSAGE_ID_SCHEMA_ERROR, 'MESSAGE_ID_SCHEMA_ERROR').
|
-define(MESSAGE_ID_SCHEMA_ERROR, 'MESSAGE_ID_SCHEMA_ERROR').
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
{
|
emqx_dashboard_swagger:spec(?MODULE).
|
||||||
[status_api(), delayed_messages_api(), delayed_message_api()],
|
|
||||||
[]
|
|
||||||
}.
|
|
||||||
|
|
||||||
conf_schema() ->
|
paths() -> ["/mqtt/delayed", "/mqtt/delayed/messages", "/mqtt/delayed/messages/:msgid"].
|
||||||
emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([delayed])).
|
|
||||||
properties() ->
|
|
||||||
PayloadDesc = io_lib:format("Payload, base64 encode. Payload will be ~p if length large than ~p",
|
|
||||||
[?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH]),
|
|
||||||
properties([
|
|
||||||
{msgid, integer, <<"Message Id">>},
|
|
||||||
{publish_at, string, <<"Client publish message time, rfc 3339">>},
|
|
||||||
{delayed_interval, integer, <<"Delayed interval, second">>},
|
|
||||||
{delayed_remaining, integer, <<"Delayed remaining, second">>},
|
|
||||||
{expected_at, string, <<"Expect publish time, rfc 3339">>},
|
|
||||||
{topic, string, <<"Topic">>},
|
|
||||||
{qos, string, <<"QoS">>},
|
|
||||||
{payload, string, iolist_to_binary(PayloadDesc)},
|
|
||||||
{from_clientid, string, <<"From ClientId">>},
|
|
||||||
{from_username, string, <<"From Username">>}
|
|
||||||
]).
|
|
||||||
|
|
||||||
parameters() ->
|
schema("/mqtt/delayed") ->
|
||||||
[#{
|
#{
|
||||||
name => msgid,
|
operationId => status,
|
||||||
in => path,
|
|
||||||
schema => #{type => string},
|
|
||||||
required => true
|
|
||||||
}].
|
|
||||||
|
|
||||||
status_api() ->
|
|
||||||
Metadata = #{
|
|
||||||
get => #{
|
get => #{
|
||||||
|
tags => [<<"mqtt">>],
|
||||||
description => <<"Get delayed status">>,
|
description => <<"Get delayed status">>,
|
||||||
|
summary => <<"Get delayed status">>,
|
||||||
responses => #{
|
responses => #{
|
||||||
<<"200">> => schema(conf_schema())}
|
200 => ref(emqx_modules_schema, "delayed")
|
||||||
},
|
}
|
||||||
|
},
|
||||||
put => #{
|
put => #{
|
||||||
|
tags => [<<"mqtt">>],
|
||||||
description => <<"Enable or disable delayed, set max delayed messages">>,
|
description => <<"Enable or disable delayed, set max delayed messages">>,
|
||||||
'requestBody' => schema(conf_schema()),
|
requestBody => ref(emqx_modules_schema, "delayed"),
|
||||||
responses => #{
|
responses => #{
|
||||||
<<"200">> =>
|
200 => mk(ref(emqx_modules_schema, "delayed"),
|
||||||
schema(conf_schema(), <<"Enable or disable delayed successfully">>),
|
#{desc => <<"Enable or disable delayed successfully">>}),
|
||||||
<<"400">> =>
|
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Max limit illegality">>)
|
||||||
error_schema(<<"Max limit illegality">>, [?BAD_REQUEST])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
{"/mqtt/delayed", Metadata, status}.
|
|
||||||
|
|
||||||
delayed_messages_api() ->
|
schema("/mqtt/delayed/messages/:msgid") ->
|
||||||
Metadata = #{
|
#{operationId => delayed_message,
|
||||||
get => #{
|
|
||||||
description => "List delayed messages",
|
|
||||||
parameters => page_params(),
|
|
||||||
responses => #{
|
|
||||||
<<"200">> => page_object_schema(properties())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{"/mqtt/delayed/messages", Metadata, delayed_messages}.
|
|
||||||
|
|
||||||
delayed_message_api() ->
|
|
||||||
Metadata = #{
|
|
||||||
get => #{
|
get => #{
|
||||||
|
tags => [<<"mqtt">>],
|
||||||
description => <<"Get delayed message">>,
|
description => <<"Get delayed message">>,
|
||||||
parameters => parameters(),
|
parameters => [{msgid, mk(binary(), #{in => path, desc => <<"delay message ID">>})}],
|
||||||
responses => #{
|
responses => #{
|
||||||
<<"400">> => error_schema(<<"Message ID Schema error">>, [?MESSAGE_ID_SCHEMA_ERROR]),
|
200 => ref("message_without_payload"),
|
||||||
<<"404">> => error_schema(<<"Message ID not found">>, [?MESSAGE_ID_NOT_FOUND]),
|
400 => emqx_dashboard_swagger:error_codes([?MESSAGE_ID_SCHEMA_ERROR], <<"Bad MsgId format">>),
|
||||||
<<"200">> => object_schema(maps:without([payload], properties()), <<"Get delayed message success">>)
|
404 => emqx_dashboard_swagger:error_codes([?MESSAGE_ID_NOT_FOUND], <<"MsgId not found">>)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
|
tags => [<<"mqtt">>],
|
||||||
description => <<"Delete delayed message">>,
|
description => <<"Delete delayed message">>,
|
||||||
parameters => parameters(),
|
parameters => [{msgid, mk(binary(), #{in => path, desc => <<"delay message ID">>})}],
|
||||||
responses => #{
|
responses => #{
|
||||||
<<"400">> => error_schema(<<"Message ID Schema error">>, [?MESSAGE_ID_SCHEMA_ERROR]),
|
200 => <<"Delete delayed message success">>,
|
||||||
<<"404">> => error_schema(<<"Message ID not found">>, [?MESSAGE_ID_NOT_FOUND]),
|
400 => emqx_dashboard_swagger:error_codes([?MESSAGE_ID_SCHEMA_ERROR], <<"Bad MsgId format">>),
|
||||||
<<"200">> => schema(<<"Delete delayed message success">>)
|
404 => emqx_dashboard_swagger:error_codes([?MESSAGE_ID_NOT_FOUND], <<"MsgId not found">>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
{"/mqtt/delayed/messages/:msgid", Metadata, delayed_message}.
|
schema("/mqtt/delayed/messages") ->
|
||||||
|
#{
|
||||||
|
operationId => delayed_messages,
|
||||||
|
get => #{
|
||||||
|
tags => [<<"mqtt">>],
|
||||||
|
description => <<"List delayed messages">>,
|
||||||
|
parameters => [ref(emqx_dashboard_swagger, page), ref(emqx_dashboard_swagger, limit)],
|
||||||
|
responses => #{
|
||||||
|
200 =>
|
||||||
|
[
|
||||||
|
{data, mk(hoconsc:array(ref("message")), #{})},
|
||||||
|
{meta, [
|
||||||
|
{page, mk(integer(), #{})},
|
||||||
|
{limit, mk(integer(), #{})},
|
||||||
|
{count, mk(integer(), #{})}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
fields("message_without_payload") ->
|
||||||
|
[
|
||||||
|
{msgid, mk(integer(), #{desc => <<"Message Id (MQTT message id hash)">>})},
|
||||||
|
{publish_at, mk(binary(), #{desc => <<"Client publish message time, rfc 3339">>})},
|
||||||
|
{delayed_interval, mk(integer(), #{desc => <<"Delayed interval, second">>})},
|
||||||
|
{delayed_remaining, mk(integer(), #{desc => <<"Delayed remaining, second">>})},
|
||||||
|
{expected_at, mk(binary(), #{desc => <<"Expect publish time, rfc 3339">>})},
|
||||||
|
{topic, mk(binary(), #{desc => <<"Topic">>, example => <<"/sys/#">>})},
|
||||||
|
{qos, mk(binary(), #{desc => <<"QoS">>})},
|
||||||
|
{from_clientid, mk(binary(), #{desc => <<"From ClientId">>})},
|
||||||
|
{from_username, mk(binary(), #{desc => <<"From Username">>})}
|
||||||
|
];
|
||||||
|
fields("message") ->
|
||||||
|
PayloadDesc = io_lib:format("Payload, base64 encode. Payload will be ~p if length large than ~p",
|
||||||
|
[?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH]),
|
||||||
|
fields("message_without_payload") ++
|
||||||
|
[{payload, mk(binary(), #{desc => iolist_to_binary(PayloadDesc)})}].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% HTTP API
|
%% HTTP API
|
||||||
|
|
@ -210,7 +210,7 @@ generate_max_delayed_messages(Config) ->
|
||||||
update_config_(Config) ->
|
update_config_(Config) ->
|
||||||
lists:foreach(fun(Node) ->
|
lists:foreach(fun(Node) ->
|
||||||
update_config_(Node, Config)
|
update_config_(Node, Config)
|
||||||
end, ekka_mnesia:running_nodes()).
|
end, ekka_mnesia:running_nodes()).
|
||||||
|
|
||||||
update_config_(Node, Config) when Node =:= node() ->
|
update_config_(Node, Config) when Node =:= node() ->
|
||||||
_ = emqx_delayed:update_config(Config),
|
_ = emqx_delayed:update_config(Config),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# emqx_rule_actions
|
|
||||||
|
|
||||||
This project contains a collection of rule actions/resources. It is mainly for
|
|
||||||
making unit test easier. Also it's easier for us to create utils that many
|
|
||||||
modules depends on it.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
-----
|
|
||||||
|
|
||||||
$ rebar3 compile
|
|
||||||
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{deps, []}.
|
|
||||||
|
|
||||||
{erl_opts, [warn_unused_vars,
|
|
||||||
warn_shadow_vars,
|
|
||||||
warn_unused_import,
|
|
||||||
warn_obsolete_guard,
|
|
||||||
no_debug_info,
|
|
||||||
compressed, %% for edge
|
|
||||||
{parse_transform}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{overrides, [{add, [{erl_opts, [no_debug_info, compressed]}]}]}.
|
|
||||||
|
|
||||||
{edoc_opts, [{preprocess, true}]}.
|
|
||||||
|
|
||||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
|
||||||
locals_not_used, deprecated_function_calls,
|
|
||||||
warnings_as_errors, deprecated_functions
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{cover_enabled, true}.
|
|
||||||
{cover_opts, [verbose]}.
|
|
||||||
{cover_export_enabled, true}.
|
|
||||||
|
|
||||||
{plugins, [rebar3_proper]}.
|
|
||||||
|
|
@ -1,576 +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 This module implements EMQX Bridge transport layer on top of MQTT protocol
|
|
||||||
|
|
||||||
-module(emqx_bridge_mqtt_actions).
|
|
||||||
|
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
|
||||||
-include_lib("emqx/include/logger.hrl").
|
|
||||||
-include_lib("emqx_rule_engine/include/rule_actions.hrl").
|
|
||||||
|
|
||||||
-import(emqx_plugin_libs_rule, [str/1]).
|
|
||||||
|
|
||||||
-export([ on_resource_create/2
|
|
||||||
, on_get_resource_status/2
|
|
||||||
, on_resource_destroy/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
%% Callbacks of ecpool Worker
|
|
||||||
-export([connect/1]).
|
|
||||||
|
|
||||||
-export([subscriptions/1]).
|
|
||||||
|
|
||||||
-export([ on_action_create_data_to_mqtt_broker/2
|
|
||||||
, on_action_data_to_mqtt_broker/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
-define(RESOURCE_TYPE_MQTT, 'bridge_mqtt').
|
|
||||||
-define(RESOURCE_TYPE_RPC, 'bridge_rpc').
|
|
||||||
|
|
||||||
-define(RESOURCE_CONFIG_SPEC_MQTT, #{
|
|
||||||
address => #{
|
|
||||||
order => 1,
|
|
||||||
type => string,
|
|
||||||
required => true,
|
|
||||||
default => <<"127.0.0.1:1883">>,
|
|
||||||
title => #{en => <<" Broker Address">>,
|
|
||||||
zh => <<"远程 broker 地址"/utf8>>},
|
|
||||||
description => #{en => <<"The MQTT Remote Address">>,
|
|
||||||
zh => <<"远程 MQTT Broker 的地址"/utf8>>}
|
|
||||||
},
|
|
||||||
pool_size => #{
|
|
||||||
order => 2,
|
|
||||||
type => number,
|
|
||||||
required => true,
|
|
||||||
default => 8,
|
|
||||||
title => #{en => <<"Pool Size">>,
|
|
||||||
zh => <<"连接池大小"/utf8>>},
|
|
||||||
description => #{en => <<"MQTT Connection Pool Size">>,
|
|
||||||
zh => <<"连接池大小"/utf8>>}
|
|
||||||
},
|
|
||||||
clientid => #{
|
|
||||||
order => 3,
|
|
||||||
type => string,
|
|
||||||
required => true,
|
|
||||||
default => <<"client">>,
|
|
||||||
title => #{en => <<"ClientId">>,
|
|
||||||
zh => <<"客户端 Id"/utf8>>},
|
|
||||||
description => #{en => <<"ClientId for connecting to remote MQTT broker">>,
|
|
||||||
zh => <<"连接远程 Broker 的 ClientId"/utf8>>}
|
|
||||||
},
|
|
||||||
append => #{
|
|
||||||
order => 4,
|
|
||||||
type => boolean,
|
|
||||||
required => false,
|
|
||||||
default => true,
|
|
||||||
title => #{en => <<"Append GUID">>,
|
|
||||||
zh => <<"附加 GUID"/utf8>>},
|
|
||||||
description => #{en => <<"Append GUID to MQTT ClientId?">>,
|
|
||||||
zh => <<"是否将GUID附加到 MQTT ClientId 后"/utf8>>}
|
|
||||||
},
|
|
||||||
username => #{
|
|
||||||
order => 5,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"">>,
|
|
||||||
title => #{en => <<"Username">>, zh => <<"用户名"/utf8>>},
|
|
||||||
description => #{en => <<"Username for connecting to remote MQTT Broker">>,
|
|
||||||
zh => <<"连接远程 Broker 的用户名"/utf8>>}
|
|
||||||
},
|
|
||||||
password => #{
|
|
||||||
order => 6,
|
|
||||||
type => password,
|
|
||||||
required => false,
|
|
||||||
default => <<"">>,
|
|
||||||
title => #{en => <<"Password">>,
|
|
||||||
zh => <<"密码"/utf8>>},
|
|
||||||
description => #{en => <<"Password for connecting to remote MQTT Broker">>,
|
|
||||||
zh => <<"连接远程 Broker 的密码"/utf8>>}
|
|
||||||
},
|
|
||||||
mountpoint => #{
|
|
||||||
order => 7,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"bridge/aws/${node}/">>,
|
|
||||||
title => #{en => <<"Bridge MountPoint">>,
|
|
||||||
zh => <<"桥接挂载点"/utf8>>},
|
|
||||||
description => #{
|
|
||||||
en => <<"MountPoint for bridge topic:<br/>"
|
|
||||||
"Example: The topic of messages sent to `topic1` on local node "
|
|
||||||
"will be transformed to `bridge/aws/${node}/topic1`">>,
|
|
||||||
zh => <<"桥接主题的挂载点:<br/>"
|
|
||||||
"示例: 本地节点向 `topic1` 发消息,远程桥接节点的主题"
|
|
||||||
"会变换为 `bridge/aws/${node}/topic1`"/utf8>>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
disk_cache => #{
|
|
||||||
order => 8,
|
|
||||||
type => boolean,
|
|
||||||
required => false,
|
|
||||||
default => false,
|
|
||||||
title => #{en => <<"Disk Cache">>,
|
|
||||||
zh => <<"磁盘缓存"/utf8>>},
|
|
||||||
description => #{en => <<"The flag which determines whether messages "
|
|
||||||
"can be cached on local disk when bridge is "
|
|
||||||
"disconnected">>,
|
|
||||||
zh => <<"当桥接断开时用于控制是否将消息缓存到本地磁"
|
|
||||||
"盘队列上"/utf8>>}
|
|
||||||
},
|
|
||||||
proto_ver => #{
|
|
||||||
order => 9,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"mqttv4">>,
|
|
||||||
enum => [<<"mqttv3">>, <<"mqttv4">>, <<"mqttv5">>],
|
|
||||||
title => #{en => <<"Protocol Version">>,
|
|
||||||
zh => <<"协议版本"/utf8>>},
|
|
||||||
description => #{en => <<"MQTTT Protocol version">>,
|
|
||||||
zh => <<"MQTT 协议版本"/utf8>>}
|
|
||||||
},
|
|
||||||
keepalive => #{
|
|
||||||
order => 10,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"60s">> ,
|
|
||||||
title => #{en => <<"Keepalive">>,
|
|
||||||
zh => <<"心跳间隔"/utf8>>},
|
|
||||||
description => #{en => <<"Keepalive">>,
|
|
||||||
zh => <<"心跳间隔"/utf8>>}
|
|
||||||
},
|
|
||||||
reconnect_interval => #{
|
|
||||||
order => 11,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"30s">>,
|
|
||||||
title => #{en => <<"Reconnect Interval">>,
|
|
||||||
zh => <<"重连间隔"/utf8>>},
|
|
||||||
description => #{en => <<"Reconnect interval of bridge:<br/>">>,
|
|
||||||
zh => <<"重连间隔"/utf8>>}
|
|
||||||
},
|
|
||||||
retry_interval => #{
|
|
||||||
order => 12,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"20s">>,
|
|
||||||
title => #{en => <<"Retry interval">>,
|
|
||||||
zh => <<"重传间隔"/utf8>>},
|
|
||||||
description => #{en => <<"Retry interval for bridge QoS1 message delivering">>,
|
|
||||||
zh => <<"消息重传间隔"/utf8>>}
|
|
||||||
},
|
|
||||||
bridge_mode => #{
|
|
||||||
order => 13,
|
|
||||||
type => boolean,
|
|
||||||
required => false,
|
|
||||||
default => false,
|
|
||||||
title => #{en => <<"Bridge Mode">>,
|
|
||||||
zh => <<"桥接模式"/utf8>>},
|
|
||||||
description => #{en => <<"Bridge mode for MQTT bridge connection">>,
|
|
||||||
zh => <<"MQTT 连接是否为桥接模式"/utf8>>}
|
|
||||||
},
|
|
||||||
ssl => #{
|
|
||||||
order => 14,
|
|
||||||
type => boolean,
|
|
||||||
default => false,
|
|
||||||
title => #{en => <<"Enable SSL">>,
|
|
||||||
zh => <<"开启SSL链接"/utf8>>},
|
|
||||||
description => #{en => <<"Enable SSL or not">>,
|
|
||||||
zh => <<"是否开启 SSL"/utf8>>}
|
|
||||||
},
|
|
||||||
cacertfile => #{
|
|
||||||
order => 15,
|
|
||||||
type => file,
|
|
||||||
required => false,
|
|
||||||
default => <<"etc/certs/cacert.pem">>,
|
|
||||||
title => #{en => <<"CA certificates">>,
|
|
||||||
zh => <<"CA 证书"/utf8>>},
|
|
||||||
description => #{en => <<"The file path of the CA certificates">>,
|
|
||||||
zh => <<"CA 证书路径"/utf8>>}
|
|
||||||
},
|
|
||||||
certfile => #{
|
|
||||||
order => 16,
|
|
||||||
type => file,
|
|
||||||
required => false,
|
|
||||||
default => <<"etc/certs/client-cert.pem">>,
|
|
||||||
title => #{en => <<"SSL Certfile">>,
|
|
||||||
zh => <<"SSL 客户端证书"/utf8>>},
|
|
||||||
description => #{en => <<"The file path of the client certfile">>,
|
|
||||||
zh => <<"客户端证书路径"/utf8>>}
|
|
||||||
},
|
|
||||||
keyfile => #{
|
|
||||||
order => 17,
|
|
||||||
type => file,
|
|
||||||
required => false,
|
|
||||||
default => <<"etc/certs/client-key.pem">>,
|
|
||||||
title => #{en => <<"SSL Keyfile">>,
|
|
||||||
zh => <<"SSL 密钥文件"/utf8>>},
|
|
||||||
description => #{en => <<"The file path of the client keyfile">>,
|
|
||||||
zh => <<"客户端密钥路径"/utf8>>}
|
|
||||||
},
|
|
||||||
ciphers => #{
|
|
||||||
order => 18,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"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">>,
|
|
||||||
title => #{en => <<"SSL Ciphers">>,
|
|
||||||
zh => <<"SSL 加密算法"/utf8>>},
|
|
||||||
description => #{en => <<"SSL Ciphers">>,
|
|
||||||
zh => <<"SSL 加密算法"/utf8>>}
|
|
||||||
}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-define(RESOURCE_CONFIG_SPEC_RPC, #{
|
|
||||||
address => #{
|
|
||||||
order => 1,
|
|
||||||
type => string,
|
|
||||||
required => true,
|
|
||||||
default => <<"emqx2@127.0.0.1">>,
|
|
||||||
title => #{en => <<"EMQ X Node Name">>,
|
|
||||||
zh => <<"EMQ X 节点名称"/utf8>>},
|
|
||||||
description => #{en => <<"EMQ X Remote Node Name">>,
|
|
||||||
zh => <<"远程 EMQ X 节点名称 "/utf8>>}
|
|
||||||
},
|
|
||||||
mountpoint => #{
|
|
||||||
order => 2,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"bridge/emqx/${node}/">>,
|
|
||||||
title => #{en => <<"Bridge MountPoint">>,
|
|
||||||
zh => <<"桥接挂载点"/utf8>>},
|
|
||||||
description => #{en => <<"MountPoint for bridge topic<br/>"
|
|
||||||
"Example: The topic of messages sent to `topic1` on local node "
|
|
||||||
"will be transformed to `bridge/aws/${node}/topic1`">>,
|
|
||||||
zh => <<"桥接主题的挂载点<br/>"
|
|
||||||
"示例: 本地节点向 `topic1` 发消息,远程桥接节点的主题"
|
|
||||||
"会变换为 `bridge/aws/${node}/topic1`"/utf8>>}
|
|
||||||
},
|
|
||||||
pool_size => #{
|
|
||||||
order => 3,
|
|
||||||
type => number,
|
|
||||||
required => true,
|
|
||||||
default => 8,
|
|
||||||
title => #{en => <<"Pool Size">>,
|
|
||||||
zh => <<"连接池大小"/utf8>>},
|
|
||||||
description => #{en => <<"MQTT/RPC Connection Pool Size">>,
|
|
||||||
zh => <<"连接池大小"/utf8>>}
|
|
||||||
},
|
|
||||||
reconnect_interval => #{
|
|
||||||
order => 4,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"30s">>,
|
|
||||||
title => #{en => <<"Reconnect Interval">>,
|
|
||||||
zh => <<"重连间隔"/utf8>>},
|
|
||||||
description => #{en => <<"Reconnect Interval of bridge">>,
|
|
||||||
zh => <<"重连间隔"/utf8>>}
|
|
||||||
},
|
|
||||||
batch_size => #{
|
|
||||||
order => 5,
|
|
||||||
type => number,
|
|
||||||
required => false,
|
|
||||||
default => 32,
|
|
||||||
title => #{en => <<"Batch Size">>,
|
|
||||||
zh => <<"批处理大小"/utf8>>},
|
|
||||||
description => #{en => <<"Batch Size">>,
|
|
||||||
zh => <<"批处理大小"/utf8>>}
|
|
||||||
},
|
|
||||||
disk_cache => #{
|
|
||||||
order => 6,
|
|
||||||
type => boolean,
|
|
||||||
required => false,
|
|
||||||
default => false,
|
|
||||||
title => #{en => <<"Disk Cache">>,
|
|
||||||
zh => <<"磁盘缓存"/utf8>>},
|
|
||||||
description => #{en => <<"The flag which determines whether messages "
|
|
||||||
"can be cached on local disk when bridge is "
|
|
||||||
"disconnected">>,
|
|
||||||
zh => <<"当桥接断开时用于控制是否将消息缓存到本地磁"
|
|
||||||
"盘队列上"/utf8>>}
|
|
||||||
}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-define(ACTION_PARAM_RESOURCE, #{
|
|
||||||
type => string,
|
|
||||||
required => true,
|
|
||||||
title => #{en => <<"Resource ID">>, zh => <<"资源 ID"/utf8>>},
|
|
||||||
description => #{en => <<"Bind a resource to this action">>,
|
|
||||||
zh => <<"给动作绑定一个资源"/utf8>>}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-resource_type(#{
|
|
||||||
name => ?RESOURCE_TYPE_MQTT,
|
|
||||||
create => on_resource_create,
|
|
||||||
status => on_get_resource_status,
|
|
||||||
destroy => on_resource_destroy,
|
|
||||||
params => ?RESOURCE_CONFIG_SPEC_MQTT,
|
|
||||||
title => #{en => <<"MQTT Bridge">>, zh => <<"MQTT Bridge"/utf8>>},
|
|
||||||
description => #{en => <<"MQTT Message Bridge">>, zh => <<"MQTT 消息桥接"/utf8>>}
|
|
||||||
}).
|
|
||||||
|
|
||||||
|
|
||||||
-resource_type(#{
|
|
||||||
name => ?RESOURCE_TYPE_RPC,
|
|
||||||
create => on_resource_create,
|
|
||||||
status => on_get_resource_status,
|
|
||||||
destroy => on_resource_destroy,
|
|
||||||
params => ?RESOURCE_CONFIG_SPEC_RPC,
|
|
||||||
title => #{en => <<"EMQX Bridge">>, zh => <<"EMQX Bridge"/utf8>>},
|
|
||||||
description => #{en => <<"EMQ X RPC Bridge">>, zh => <<"EMQ X RPC 消息桥接"/utf8>>}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rule_action(#{
|
|
||||||
name => data_to_mqtt_broker,
|
|
||||||
category => data_forward,
|
|
||||||
for => 'message.publish',
|
|
||||||
types => [?RESOURCE_TYPE_MQTT, ?RESOURCE_TYPE_RPC],
|
|
||||||
create => on_action_create_data_to_mqtt_broker,
|
|
||||||
params => #{'$resource' => ?ACTION_PARAM_RESOURCE,
|
|
||||||
forward_topic => #{
|
|
||||||
order => 1,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"">>,
|
|
||||||
title => #{en => <<"Forward Topic">>,
|
|
||||||
zh => <<"转发消息主题"/utf8>>},
|
|
||||||
description => #{en => <<"The topic used when forwarding the message. "
|
|
||||||
"Defaults to the topic of the bridge message if not provided.">>,
|
|
||||||
zh => <<"转发消息时使用的主题。如果未提供,则默认为桥接消息的主题。"/utf8>>}
|
|
||||||
},
|
|
||||||
payload_tmpl => #{
|
|
||||||
order => 2,
|
|
||||||
type => string,
|
|
||||||
input => textarea,
|
|
||||||
required => false,
|
|
||||||
default => <<"">>,
|
|
||||||
title => #{en => <<"Payload Template">>,
|
|
||||||
zh => <<"消息内容模板"/utf8>>},
|
|
||||||
description => #{en => <<"The payload template, variable interpolation is supported. "
|
|
||||||
"If using empty template (default), then the payload will be "
|
|
||||||
"all the available vars in JSON format">>,
|
|
||||||
zh => <<"消息内容模板,支持变量。"
|
|
||||||
"若使用空模板(默认),消息内容为 JSON 格式的所有字段"/utf8>>}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title => #{en => <<"Data bridge to MQTT Broker">>,
|
|
||||||
zh => <<"桥接数据到 MQTT Broker"/utf8>>},
|
|
||||||
description => #{en => <<"Bridge Data to MQTT Broker">>,
|
|
||||||
zh => <<"桥接数据到 MQTT Broker"/utf8>>}
|
|
||||||
}).
|
|
||||||
|
|
||||||
on_resource_create(ResId, Params) ->
|
|
||||||
?LOG(info, "Initiating Resource ~p, ResId: ~p", [?RESOURCE_TYPE_MQTT, ResId]),
|
|
||||||
{ok, _} = application:ensure_all_started(ecpool),
|
|
||||||
PoolName = pool_name(ResId),
|
|
||||||
Options = options(Params, PoolName, ResId),
|
|
||||||
start_resource(ResId, PoolName, Options),
|
|
||||||
case test_resource_status(PoolName) of
|
|
||||||
true -> ok;
|
|
||||||
false ->
|
|
||||||
on_resource_destroy(ResId, #{<<"pool">> => PoolName}),
|
|
||||||
error({{?RESOURCE_TYPE_MQTT, ResId}, connection_failed})
|
|
||||||
end,
|
|
||||||
#{<<"pool">> => PoolName}.
|
|
||||||
|
|
||||||
start_resource(ResId, PoolName, Options) ->
|
|
||||||
case ecpool:start_sup_pool(PoolName, ?MODULE, Options) of
|
|
||||||
{ok, _} ->
|
|
||||||
?LOG(info, "Initiated Resource ~p Successfully, ResId: ~p", [?RESOURCE_TYPE_MQTT, ResId]);
|
|
||||||
{error, {already_started, _Pid}} ->
|
|
||||||
on_resource_destroy(ResId, #{<<"pool">> => PoolName}),
|
|
||||||
start_resource(ResId, PoolName, Options);
|
|
||||||
{error, Reason} ->
|
|
||||||
?LOG(error, "Initiate Resource ~p failed, ResId: ~p, ~p", [?RESOURCE_TYPE_MQTT, ResId, Reason]),
|
|
||||||
on_resource_destroy(ResId, #{<<"pool">> => PoolName}),
|
|
||||||
error({{?RESOURCE_TYPE_MQTT, ResId}, create_failed})
|
|
||||||
end.
|
|
||||||
|
|
||||||
test_resource_status(PoolName) ->
|
|
||||||
IsConnected = fun(Worker) ->
|
|
||||||
case ecpool_worker:client(Worker) of
|
|
||||||
{ok, Bridge} ->
|
|
||||||
try emqx_connector_mqtt_worker:status(Bridge) of
|
|
||||||
connected -> true;
|
|
||||||
_ -> false
|
|
||||||
catch _Error:_Reason ->
|
|
||||||
false
|
|
||||||
end;
|
|
||||||
{error, _} ->
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
Status = [IsConnected(Worker) || {_WorkerName, Worker} <- ecpool:workers(PoolName)],
|
|
||||||
lists:any(fun(St) -> St =:= true end, Status).
|
|
||||||
|
|
||||||
-spec(on_get_resource_status(ResId::binary(), Params::map()) -> Status::map()).
|
|
||||||
on_get_resource_status(_ResId, #{<<"pool">> := PoolName}) ->
|
|
||||||
IsAlive = test_resource_status(PoolName),
|
|
||||||
#{is_alive => IsAlive}.
|
|
||||||
|
|
||||||
on_resource_destroy(ResId, #{<<"pool">> := PoolName}) ->
|
|
||||||
?LOG(info, "Destroying Resource ~p, ResId: ~p", [?RESOURCE_TYPE_MQTT, ResId]),
|
|
||||||
case ecpool:stop_sup_pool(PoolName) of
|
|
||||||
ok ->
|
|
||||||
?LOG(info, "Destroyed Resource ~p Successfully, ResId: ~p", [?RESOURCE_TYPE_MQTT, ResId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
?LOG(error, "Destroy Resource ~p failed, ResId: ~p, ~p", [?RESOURCE_TYPE_MQTT, ResId, Reason]),
|
|
||||||
error({{?RESOURCE_TYPE_MQTT, ResId}, destroy_failed})
|
|
||||||
end.
|
|
||||||
|
|
||||||
on_action_create_data_to_mqtt_broker(ActId, Opts = #{<<"pool">> := PoolName,
|
|
||||||
<<"forward_topic">> := ForwardTopic,
|
|
||||||
<<"payload_tmpl">> := PayloadTmpl}) ->
|
|
||||||
?LOG(info, "Initiating Action ~p.", [?FUNCTION_NAME]),
|
|
||||||
PayloadTks = emqx_plugin_libs_rule:preproc_tmpl(PayloadTmpl),
|
|
||||||
TopicTks = case ForwardTopic == <<"">> of
|
|
||||||
true -> undefined;
|
|
||||||
false -> emqx_plugin_libs_rule:preproc_tmpl(ForwardTopic)
|
|
||||||
end,
|
|
||||||
Opts.
|
|
||||||
|
|
||||||
on_action_data_to_mqtt_broker(Msg, _Env =
|
|
||||||
#{id := Id, clientid := From, flags := Flags,
|
|
||||||
topic := Topic, timestamp := TimeStamp, qos := QoS,
|
|
||||||
?BINDING_KEYS := #{
|
|
||||||
'ActId' := ActId,
|
|
||||||
'PoolName' := PoolName,
|
|
||||||
'TopicTks' := TopicTks,
|
|
||||||
'PayloadTks' := PayloadTks
|
|
||||||
}}) ->
|
|
||||||
Topic1 = case TopicTks =:= undefined of
|
|
||||||
true -> Topic;
|
|
||||||
false -> emqx_plugin_libs_rule:proc_tmpl(TopicTks, Msg)
|
|
||||||
end,
|
|
||||||
BrokerMsg = #message{id = Id,
|
|
||||||
qos = QoS,
|
|
||||||
from = From,
|
|
||||||
flags = Flags,
|
|
||||||
topic = Topic1,
|
|
||||||
payload = format_data(PayloadTks, Msg),
|
|
||||||
timestamp = TimeStamp},
|
|
||||||
ecpool:with_client(PoolName,
|
|
||||||
fun(BridgePid) ->
|
|
||||||
BridgePid ! {deliver, rule_engine, BrokerMsg}
|
|
||||||
end),
|
|
||||||
emqx_rule_metrics:inc_actions_success(ActId).
|
|
||||||
|
|
||||||
format_data([], Msg) ->
|
|
||||||
emqx_json:encode(Msg);
|
|
||||||
|
|
||||||
format_data(Tokens, Msg) ->
|
|
||||||
emqx_plugin_libs_rule:proc_tmpl(Tokens, Msg).
|
|
||||||
|
|
||||||
subscriptions(Subscriptions) ->
|
|
||||||
scan_binary(<<"[", Subscriptions/binary, "].">>).
|
|
||||||
|
|
||||||
is_node_addr(Addr0) ->
|
|
||||||
Addr = binary_to_list(Addr0),
|
|
||||||
case string:tokens(Addr, "@") of
|
|
||||||
[_NodeName, _Hostname] -> true;
|
|
||||||
_ -> false
|
|
||||||
end.
|
|
||||||
|
|
||||||
scan_binary(Bin) ->
|
|
||||||
TermString = binary_to_list(Bin),
|
|
||||||
scan_string(TermString).
|
|
||||||
|
|
||||||
scan_string(TermString) ->
|
|
||||||
{ok, Tokens, _} = erl_scan:string(TermString),
|
|
||||||
{ok, Term} = erl_parse:parse_term(Tokens),
|
|
||||||
Term.
|
|
||||||
|
|
||||||
connect(Options) when is_list(Options) ->
|
|
||||||
connect(maps:from_list(Options));
|
|
||||||
connect(Options = #{disk_cache := DiskCache, ecpool_worker_id := Id, pool_name := Pool}) ->
|
|
||||||
Options0 = case DiskCache of
|
|
||||||
true ->
|
|
||||||
DataDir = filename:join([emqx:get_config([node, data_dir]), replayq, Pool, integer_to_list(Id)]),
|
|
||||||
QueueOption = #{replayq_dir => DataDir},
|
|
||||||
Options#{queue => QueueOption};
|
|
||||||
false ->
|
|
||||||
Options
|
|
||||||
end,
|
|
||||||
Options1 = case maps:is_key(append, Options0) of
|
|
||||||
false -> Options0;
|
|
||||||
true ->
|
|
||||||
case maps:get(append, Options0, false) of
|
|
||||||
true ->
|
|
||||||
ClientId = lists:concat([str(maps:get(clientid, Options0)), "_", str(emqx_guid:to_hexstr(emqx_guid:gen()))]),
|
|
||||||
Options0#{clientid => ClientId};
|
|
||||||
false ->
|
|
||||||
Options0
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
Options2 = maps:without([ecpool_worker_id, pool_name, append], Options1),
|
|
||||||
emqx_connector_mqtt_worker:start_link(Options2#{name => name(Pool, Id)}).
|
|
||||||
name(Pool, Id) ->
|
|
||||||
list_to_atom(atom_to_list(Pool) ++ ":" ++ integer_to_list(Id)).
|
|
||||||
pool_name(ResId) ->
|
|
||||||
list_to_atom("bridge_mqtt:" ++ str(ResId)).
|
|
||||||
|
|
||||||
options(Options, PoolName, ResId) ->
|
|
||||||
GetD = fun(Key, Default) -> maps:get(Key, Options, Default) end,
|
|
||||||
Get = fun(Key) -> GetD(Key, undefined) end,
|
|
||||||
Address = Get(<<"address">>),
|
|
||||||
[{max_inflight_batches, 32},
|
|
||||||
{forward_mountpoint, str(Get(<<"mountpoint">>))},
|
|
||||||
{disk_cache, GetD(<<"disk_cache">>, false)},
|
|
||||||
{start_type, auto},
|
|
||||||
{reconnect_delay_ms, hocon_postprocess:duration(str(Get(<<"reconnect_interval">>)))},
|
|
||||||
{if_record_metrics, false},
|
|
||||||
{pool_size, GetD(<<"pool_size">>, 1)},
|
|
||||||
{pool_name, PoolName}
|
|
||||||
] ++ case is_node_addr(Address) of
|
|
||||||
true ->
|
|
||||||
[{address, binary_to_atom(Get(<<"address">>), utf8)},
|
|
||||||
{connect_module, emqx_bridge_rpc},
|
|
||||||
{batch_size, Get(<<"batch_size">>)}];
|
|
||||||
false ->
|
|
||||||
[{address, binary_to_list(Address)},
|
|
||||||
{bridge_mode, GetD(<<"bridge_mode">>, true)},
|
|
||||||
{clean_start, true},
|
|
||||||
{clientid, str(Get(<<"clientid">>))},
|
|
||||||
{append, Get(<<"append">>)},
|
|
||||||
{connect_module, emqx_bridge_mqtt},
|
|
||||||
{keepalive, hocon_postprocess:duration(str(Get(<<"keepalive">>))) div 1000},
|
|
||||||
{username, str(Get(<<"username">>))},
|
|
||||||
{password, str(Get(<<"password">>))},
|
|
||||||
{proto_ver, mqtt_ver(Get(<<"proto_ver">>))},
|
|
||||||
{retry_interval, hocon_postprocess:duration(str(GetD(<<"retry_interval">>, "30s"))) div 1000}
|
|
||||||
| maybe_ssl(Options, Get(<<"ssl">>), ResId)]
|
|
||||||
end.
|
|
||||||
|
|
||||||
maybe_ssl(_Options, false, _ResId) ->
|
|
||||||
[];
|
|
||||||
maybe_ssl(Options, true, ResId) ->
|
|
||||||
[{ssl, true}, {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(Options, "rules", ResId)}].
|
|
||||||
|
|
||||||
mqtt_ver(ProtoVer) ->
|
|
||||||
case ProtoVer of
|
|
||||||
<<"mqttv3">> -> v3;
|
|
||||||
<<"mqttv4">> -> v4;
|
|
||||||
<<"mqttv5">> -> v5;
|
|
||||||
_ -> v4
|
|
||||||
end.
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
%% -*- mode: erlang -*-
|
|
||||||
{application, emqx_rule_actions,
|
|
||||||
[{description, "Rule actions"},
|
|
||||||
{vsn, "5.0.0"},
|
|
||||||
{registered, []},
|
|
||||||
{applications,
|
|
||||||
[kernel,stdlib,emqx]},
|
|
||||||
{env,[]},
|
|
||||||
{modules, []},
|
|
||||||
{licenses, ["Apache 2.0"]},
|
|
||||||
{links, []}
|
|
||||||
]}.
|
|
||||||
|
|
@ -1,379 +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.
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
%% Define the default actions.
|
|
||||||
-module(emqx_web_hook_actions).
|
|
||||||
|
|
||||||
-export([ on_resource_create/2
|
|
||||||
, on_get_resource_status/2
|
|
||||||
, on_resource_destroy/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([ on_action_create_data_to_webserver/2
|
|
||||||
, on_action_data_to_webserver/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export_type([action_fun/0]).
|
|
||||||
|
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
|
||||||
-include_lib("emqx/include/logger.hrl").
|
|
||||||
-include_lib("emqx_rule_engine/include/rule_actions.hrl").
|
|
||||||
|
|
||||||
-type(action_fun() :: fun((Data :: map(), Envs :: map()) -> Result :: any())).
|
|
||||||
|
|
||||||
-type(url() :: binary()).
|
|
||||||
|
|
||||||
-define(RESOURCE_TYPE_WEBHOOK, 'web_hook').
|
|
||||||
-define(RESOURCE_CONFIG_SPEC, #{
|
|
||||||
url => #{order => 1,
|
|
||||||
type => string,
|
|
||||||
format => url,
|
|
||||||
required => true,
|
|
||||||
title => #{en => <<"Request URL">>,
|
|
||||||
zh => <<"请求 URL"/utf8>>},
|
|
||||||
description => #{en => <<"The URL of the server that will receive the Webhook requests.">>,
|
|
||||||
zh => <<"用于接收 Webhook 请求的服务器的 URL。"/utf8>>}},
|
|
||||||
connect_timeout => #{order => 2,
|
|
||||||
type => string,
|
|
||||||
default => <<"5s">>,
|
|
||||||
title => #{en => <<"Connect Timeout">>,
|
|
||||||
zh => <<"连接超时时间"/utf8>>},
|
|
||||||
description => #{en => <<"Connect Timeout In Seconds">>,
|
|
||||||
zh => <<"连接超时时间"/utf8>>}},
|
|
||||||
request_timeout => #{order => 3,
|
|
||||||
type => string,
|
|
||||||
default => <<"5s">>,
|
|
||||||
title => #{en => <<"Request Timeout">>,
|
|
||||||
zh => <<"请求超时时间时间"/utf8>>},
|
|
||||||
description => #{en => <<"Request Timeout In Seconds">>,
|
|
||||||
zh => <<"请求超时时间"/utf8>>}},
|
|
||||||
pool_size => #{order => 4,
|
|
||||||
type => number,
|
|
||||||
default => 8,
|
|
||||||
title => #{en => <<"Pool Size">>, zh => <<"连接池大小"/utf8>>},
|
|
||||||
description => #{en => <<"Connection Pool">>,
|
|
||||||
zh => <<"连接池大小"/utf8>>}
|
|
||||||
},
|
|
||||||
cacertfile => #{order => 5,
|
|
||||||
type => file,
|
|
||||||
default => <<"">>,
|
|
||||||
title => #{en => <<"CA Certificate File">>,
|
|
||||||
zh => <<"CA 证书文件"/utf8>>},
|
|
||||||
description => #{en => <<"CA Certificate file">>,
|
|
||||||
zh => <<"CA 证书文件"/utf8>>}},
|
|
||||||
keyfile => #{order => 6,
|
|
||||||
type => file,
|
|
||||||
default => <<"">>,
|
|
||||||
title =>#{en => <<"SSL Key">>,
|
|
||||||
zh => <<"SSL Key"/utf8>>},
|
|
||||||
description => #{en => <<"Your ssl keyfile">>,
|
|
||||||
zh => <<"SSL 私钥"/utf8>>}},
|
|
||||||
certfile => #{order => 7,
|
|
||||||
type => file,
|
|
||||||
default => <<"">>,
|
|
||||||
title => #{en => <<"SSL Cert">>,
|
|
||||||
zh => <<"SSL Cert"/utf8>>},
|
|
||||||
description => #{en => <<"Your ssl certfile">>,
|
|
||||||
zh => <<"SSL 证书"/utf8>>}},
|
|
||||||
verify => #{order => 8,
|
|
||||||
type => boolean,
|
|
||||||
default => false,
|
|
||||||
title => #{en => <<"Verify Server Certfile">>,
|
|
||||||
zh => <<"校验服务器证书"/utf8>>},
|
|
||||||
description => #{en => <<"Whether to verify the server certificate. By default, the client will not verify the server's certificate. If verification is required, please set it to true.">>,
|
|
||||||
zh => <<"是否校验服务器证书。 默认客户端不会去校验服务器的证书,如果需要校验,请设置成true。"/utf8>>}},
|
|
||||||
server_name_indication => #{order => 9,
|
|
||||||
type => string,
|
|
||||||
title => #{en => <<"Server Name Indication">>,
|
|
||||||
zh => <<"服务器名称指示"/utf8>>},
|
|
||||||
description => #{en => <<"Specify the hostname used for peer certificate verification, or set to disable to turn off this verification.">>,
|
|
||||||
zh => <<"指定用于对端证书验证时使用的主机名,或者设置为 disable 以关闭此项验证。"/utf8>>}}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-define(ACTION_PARAM_RESOURCE, #{
|
|
||||||
order => 0,
|
|
||||||
type => string,
|
|
||||||
required => true,
|
|
||||||
title => #{en => <<"Resource ID">>,
|
|
||||||
zh => <<"资源 ID"/utf8>>},
|
|
||||||
description => #{en => <<"Bind a resource to this action">>,
|
|
||||||
zh => <<"给动作绑定一个资源"/utf8>>}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-define(ACTION_DATA_SPEC, #{
|
|
||||||
'$resource' => ?ACTION_PARAM_RESOURCE,
|
|
||||||
method => #{
|
|
||||||
order => 1,
|
|
||||||
type => string,
|
|
||||||
enum => [<<"POST">>, <<"DELETE">>, <<"PUT">>, <<"GET">>],
|
|
||||||
default => <<"POST">>,
|
|
||||||
title => #{en => <<"Method">>,
|
|
||||||
zh => <<"Method"/utf8>>},
|
|
||||||
description => #{en => <<"HTTP Method.\n"
|
|
||||||
"Note that: the Body option in the Action will be discarded in case of GET or DELETE method.">>,
|
|
||||||
zh => <<"HTTP Method。\n"
|
|
||||||
"注意:当方法为 GET 或 DELETE 时,动作中的 Body 选项会被忽略。"/utf8>>}},
|
|
||||||
path => #{
|
|
||||||
order => 2,
|
|
||||||
type => string,
|
|
||||||
required => false,
|
|
||||||
default => <<"">>,
|
|
||||||
title => #{en => <<"Path">>,
|
|
||||||
zh => <<"Path"/utf8>>},
|
|
||||||
description => #{en => <<"The path part of the URL, support using ${Var} to get the field value output by the rule.">>,
|
|
||||||
zh => <<"URL 的路径部分,支持使用 ${Var} 获取规则输出的字段值。\n"/utf8>>}
|
|
||||||
},
|
|
||||||
headers => #{
|
|
||||||
order => 3,
|
|
||||||
type => object,
|
|
||||||
schema => #{},
|
|
||||||
default => #{<<"content-type">> => <<"application/json">>},
|
|
||||||
title => #{en => <<"Headers">>,
|
|
||||||
zh => <<"Headers"/utf8>>},
|
|
||||||
description => #{en => <<"HTTP headers.">>,
|
|
||||||
zh => <<"HTTP headers。"/utf8>>}},
|
|
||||||
body => #{
|
|
||||||
order => 4,
|
|
||||||
type => string,
|
|
||||||
input => textarea,
|
|
||||||
required => false,
|
|
||||||
default => <<"">>,
|
|
||||||
title => #{en => <<"Body">>,
|
|
||||||
zh => <<"Body"/utf8>>},
|
|
||||||
description => #{en => <<"The HTTP body supports the use of ${Var} to obtain the field value output by the rule.\n"
|
|
||||||
"The content of the default HTTP request body is a JSON string composed of the keys and values of all fields output by the rule.">>,
|
|
||||||
zh => <<"HTTP 请求体,支持使用 ${Var} 获取规则输出的字段值\n"
|
|
||||||
"默认 HTTP 请求体的内容为规则输出的所有字段的键和值构成的 JSON 字符串。"/utf8>>}}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-resource_type(
|
|
||||||
#{name => ?RESOURCE_TYPE_WEBHOOK,
|
|
||||||
create => on_resource_create,
|
|
||||||
status => on_get_resource_status,
|
|
||||||
destroy => on_resource_destroy,
|
|
||||||
params => ?RESOURCE_CONFIG_SPEC,
|
|
||||||
title => #{en => <<"WebHook">>,
|
|
||||||
zh => <<"WebHook"/utf8>>},
|
|
||||||
description => #{en => <<"WebHook">>,
|
|
||||||
zh => <<"WebHook"/utf8>>}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rule_action(#{name => data_to_webserver,
|
|
||||||
category => data_forward,
|
|
||||||
for => '$any',
|
|
||||||
create => on_action_create_data_to_webserver,
|
|
||||||
params => ?ACTION_DATA_SPEC,
|
|
||||||
types => [?RESOURCE_TYPE_WEBHOOK],
|
|
||||||
title => #{en => <<"Data to Web Server">>,
|
|
||||||
zh => <<"发送数据到 Web 服务"/utf8>>},
|
|
||||||
description => #{en => <<"Forward Messages to Web Server">>,
|
|
||||||
zh => <<"将数据转发给 Web 服务"/utf8>>}
|
|
||||||
}).
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Actions for web hook
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
-spec(on_resource_create(binary(), map()) -> map()).
|
|
||||||
on_resource_create(ResId, Conf) ->
|
|
||||||
{ok, _} = application:ensure_all_started(ehttpc),
|
|
||||||
Options = pool_opts(Conf, ResId),
|
|
||||||
PoolName = pool_name(ResId),
|
|
||||||
case test_http_connect(Conf) of
|
|
||||||
true -> ok;
|
|
||||||
false -> error({error, check_http_connectivity_failed})
|
|
||||||
end,
|
|
||||||
start_resource(ResId, PoolName, Options),
|
|
||||||
Conf#{<<"pool">> => PoolName, options => Options}.
|
|
||||||
|
|
||||||
start_resource(ResId, PoolName, Options) ->
|
|
||||||
case ehttpc_pool:start_pool(PoolName, Options) of
|
|
||||||
{ok, _} ->
|
|
||||||
?LOG(info, "Initiated Resource ~p Successfully, ResId: ~p",
|
|
||||||
[?RESOURCE_TYPE_WEBHOOK, ResId]);
|
|
||||||
{error, {already_started, _Pid}} ->
|
|
||||||
on_resource_destroy(ResId, #{<<"pool">> => PoolName}),
|
|
||||||
start_resource(ResId, PoolName, Options);
|
|
||||||
{error, Reason} ->
|
|
||||||
?LOG(error, "Initiate Resource ~p failed, ResId: ~p, ~0p",
|
|
||||||
[?RESOURCE_TYPE_WEBHOOK, ResId, Reason]),
|
|
||||||
error({{?RESOURCE_TYPE_WEBHOOK, ResId}, create_failed})
|
|
||||||
end.
|
|
||||||
|
|
||||||
-spec(on_get_resource_status(binary(), map()) -> map()).
|
|
||||||
on_get_resource_status(_ResId, Conf) ->
|
|
||||||
#{is_alive => test_http_connect(Conf)}.
|
|
||||||
|
|
||||||
-spec(on_resource_destroy(binary(), map()) -> ok | {error, Reason::term()}).
|
|
||||||
on_resource_destroy(ResId, #{<<"pool">> := PoolName}) ->
|
|
||||||
?LOG(info, "Destroying Resource ~p, ResId: ~p", [?RESOURCE_TYPE_WEBHOOK, ResId]),
|
|
||||||
case ehttpc_pool:stop_pool(PoolName) of
|
|
||||||
ok ->
|
|
||||||
?LOG(info, "Destroyed Resource ~p Successfully, ResId: ~p", [?RESOURCE_TYPE_WEBHOOK, ResId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
?LOG(error, "Destroy Resource ~p failed, ResId: ~p, ~p", [?RESOURCE_TYPE_WEBHOOK, ResId, Reason]),
|
|
||||||
error({{?RESOURCE_TYPE_WEBHOOK, ResId}, destroy_failed})
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% An action that forwards publish messages to a remote web server.
|
|
||||||
-spec(on_action_create_data_to_webserver(Id::binary(), #{url() := string()}) -> {bindings(), NewParams :: map()}).
|
|
||||||
on_action_create_data_to_webserver(Id, Params) ->
|
|
||||||
#{method := Method,
|
|
||||||
path := Path,
|
|
||||||
headers := Headers,
|
|
||||||
body := Body,
|
|
||||||
pool := Pool,
|
|
||||||
request_timeout := RequestTimeout} = parse_action_params(Params),
|
|
||||||
BodyTokens = emqx_plugin_libs_rule:preproc_tmpl(Body),
|
|
||||||
PathTokens = emqx_plugin_libs_rule:preproc_tmpl(Path),
|
|
||||||
Params.
|
|
||||||
|
|
||||||
on_action_data_to_webserver(Selected, _Envs =
|
|
||||||
#{?BINDING_KEYS := #{
|
|
||||||
'Id' := Id,
|
|
||||||
'Method' := Method,
|
|
||||||
'Headers' := Headers,
|
|
||||||
'PathTokens' := PathTokens,
|
|
||||||
'BodyTokens' := BodyTokens,
|
|
||||||
'Pool' := Pool,
|
|
||||||
'RequestTimeout' := RequestTimeout},
|
|
||||||
clientid := ClientID}) ->
|
|
||||||
NBody = format_msg(BodyTokens, Selected),
|
|
||||||
NPath = emqx_plugin_libs_rule:proc_tmpl(PathTokens, Selected),
|
|
||||||
Req = create_req(Method, NPath, Headers, NBody),
|
|
||||||
case ehttpc:request(ehttpc_pool:pick_worker(Pool, ClientID), Method, Req, RequestTimeout) of
|
|
||||||
{ok, StatusCode, _} when StatusCode >= 200 andalso StatusCode < 300 ->
|
|
||||||
emqx_rule_metrics:inc_actions_success(Id);
|
|
||||||
{ok, StatusCode, _, _} when StatusCode >= 200 andalso StatusCode < 300 ->
|
|
||||||
emqx_rule_metrics:inc_actions_success(Id);
|
|
||||||
{ok, StatusCode, _} ->
|
|
||||||
?LOG(warning, "[WebHook Action] HTTP request failed with status code: ~p", [StatusCode]),
|
|
||||||
emqx_rule_metrics:inc_actions_error(Id);
|
|
||||||
{ok, StatusCode, _, _} ->
|
|
||||||
?LOG(warning, "[WebHook Action] HTTP request failed with status code: ~p", [StatusCode]),
|
|
||||||
emqx_rule_metrics:inc_actions_error(Id);
|
|
||||||
{error, Reason} ->
|
|
||||||
?LOG(error, "[WebHook Action] HTTP request error: ~p", [Reason]),
|
|
||||||
emqx_rule_metrics:inc_actions_error(Id)
|
|
||||||
end.
|
|
||||||
|
|
||||||
format_msg([], Data) ->
|
|
||||||
emqx_json:encode(Data);
|
|
||||||
format_msg(Tokens, Data) ->
|
|
||||||
emqx_plugin_libs_rule:proc_tmpl(Tokens, Data).
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Internal functions
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
create_req(Method, Path, Headers, _Body)
|
|
||||||
when Method =:= get orelse Method =:= delete ->
|
|
||||||
{Path, Headers};
|
|
||||||
create_req(_, Path, Headers, Body) ->
|
|
||||||
{Path, Headers, Body}.
|
|
||||||
|
|
||||||
parse_action_params(Params = #{<<"url">> := URL}) ->
|
|
||||||
try
|
|
||||||
{ok, #{path := CommonPath}} = emqx_http_lib:uri_parse(URL),
|
|
||||||
Method = method(maps:get(<<"method">>, Params, <<"POST">>)),
|
|
||||||
Headers = headers(maps:get(<<"headers">>, Params, undefined)),
|
|
||||||
NHeaders = ensure_content_type_header(Headers, Method),
|
|
||||||
#{method => Method,
|
|
||||||
path => merge_path(CommonPath, maps:get(<<"path">>, Params, <<>>)),
|
|
||||||
headers => NHeaders,
|
|
||||||
body => maps:get(<<"body">>, Params, <<>>),
|
|
||||||
request_timeout => hocon_postprocess:duration(str(maps:get(<<"request_timeout">>, Params, <<"5s">>))),
|
|
||||||
pool => maps:get(<<"pool">>, Params)}
|
|
||||||
catch _:_ ->
|
|
||||||
throw({invalid_params, Params})
|
|
||||||
end.
|
|
||||||
|
|
||||||
ensure_content_type_header(Headers, Method) when Method =:= post orelse Method =:= put ->
|
|
||||||
Headers;
|
|
||||||
ensure_content_type_header(Headers, _Method) ->
|
|
||||||
lists:keydelete("content-type", 1, Headers).
|
|
||||||
|
|
||||||
merge_path(CommonPath, <<>>) ->
|
|
||||||
CommonPath;
|
|
||||||
merge_path(CommonPath, Path0) ->
|
|
||||||
case emqx_http_lib:uri_parse(Path0) of
|
|
||||||
{ok, #{path := Path1, 'query' := Query}} ->
|
|
||||||
Path2 = filename:join(CommonPath, Path1),
|
|
||||||
<<Path2/binary, "?", Query/binary>>;
|
|
||||||
{ok, #{path := Path1}} ->
|
|
||||||
filename:join(CommonPath, Path1)
|
|
||||||
end.
|
|
||||||
|
|
||||||
method(GET) when GET == <<"GET">>; GET == <<"get">> -> get;
|
|
||||||
method(POST) when POST == <<"POST">>; POST == <<"post">> -> post;
|
|
||||||
method(PUT) when PUT == <<"PUT">>; PUT == <<"put">> -> put;
|
|
||||||
method(DEL) when DEL == <<"DELETE">>; DEL == <<"delete">> -> delete.
|
|
||||||
|
|
||||||
headers(undefined) -> [];
|
|
||||||
headers(Headers) when is_map(Headers) ->
|
|
||||||
headers(maps:to_list(Headers));
|
|
||||||
headers(Headers) when is_list(Headers) ->
|
|
||||||
[{string:to_lower(str(K)), str(V)} || {K, V} <- Headers].
|
|
||||||
|
|
||||||
str(Str) when is_list(Str) -> Str;
|
|
||||||
str(Atom) when is_atom(Atom) -> atom_to_list(Atom);
|
|
||||||
str(Bin) when is_binary(Bin) -> binary_to_list(Bin).
|
|
||||||
|
|
||||||
pool_opts(Params = #{<<"url">> := URL}, ResId) ->
|
|
||||||
{ok, #{host := Host,
|
|
||||||
port := Port,
|
|
||||||
scheme := Scheme}} = emqx_http_lib:uri_parse(URL),
|
|
||||||
PoolSize = maps:get(<<"pool_size">>, Params, 32),
|
|
||||||
ConnectTimeout =
|
|
||||||
hocon_postprocess:duration(str(maps:get(<<"connect_timeout">>, Params, <<"5s">>))),
|
|
||||||
TransportOpts0 =
|
|
||||||
case Scheme =:= https of
|
|
||||||
true -> [get_ssl_opts(Params, ResId)];
|
|
||||||
false -> []
|
|
||||||
end,
|
|
||||||
TransportOpts = emqx_misc:ipv6_probe(TransportOpts0),
|
|
||||||
Opts = case Scheme =:= https of
|
|
||||||
true -> [{transport_opts, TransportOpts}, {transport, ssl}];
|
|
||||||
false -> [{transport_opts, TransportOpts}]
|
|
||||||
end,
|
|
||||||
[{host, Host},
|
|
||||||
{port, Port},
|
|
||||||
{pool_size, PoolSize},
|
|
||||||
{pool_type, hash},
|
|
||||||
{connect_timeout, ConnectTimeout},
|
|
||||||
{retry, 5},
|
|
||||||
{retry_timeout, 1000} | Opts].
|
|
||||||
|
|
||||||
pool_name(ResId) ->
|
|
||||||
list_to_atom("webhook:" ++ str(ResId)).
|
|
||||||
|
|
||||||
get_ssl_opts(Opts, ResId) ->
|
|
||||||
emqx_plugin_libs_ssl:save_files_return_opts(Opts, "rules", ResId).
|
|
||||||
|
|
||||||
test_http_connect(Conf) ->
|
|
||||||
Url = fun() -> maps:get(<<"url">>, Conf) end,
|
|
||||||
try
|
|
||||||
emqx_plugin_libs_rule:http_connectivity(Url())
|
|
||||||
of
|
|
||||||
ok -> true;
|
|
||||||
{error, _Reason} ->
|
|
||||||
?LOG(error, "check http_connectivity failed: ~p", [Url()]),
|
|
||||||
false
|
|
||||||
catch
|
|
||||||
Err:Reason:ST ->
|
|
||||||
?LOG(error, "check http_connectivity failed: ~p, ~0p", [Conf, {Err, Reason, ST}]),
|
|
||||||
false
|
|
||||||
end.
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
#Rule-Engine-APIs
|
|
||||||
|
|
||||||
## ENVs
|
|
||||||
|
|
||||||
APPSECRET="88ebdd6569afc:Mjg3MzUyNTI2Mjk2NTcyOTEwMDEwMDMzMTE2NTM1MTkzNjA"
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
### test sql
|
|
||||||
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules?test' -d \
|
|
||||||
'{"rawsql":"select * from \"message.publish\" where topic=\"t/a\"","ctx":{}}'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### create
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules' -d \
|
|
||||||
'{"rawsql":"select * from \"t/a\"","actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule"}'
|
|
||||||
|
|
||||||
{"code":0,"data":{"actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule","enabled":true,"id":"rule:bc987915","rawsql":"select * from \"t/a\""}}
|
|
||||||
|
|
||||||
## with a resource id in the action args
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules' -d \
|
|
||||||
'{"rawsql":"select * from \"t/a\"","actions":[{"name":"inspect","params":{"$resource":"resource:3a7b44a1"}}],"description":"test-rule"}'
|
|
||||||
|
|
||||||
{"code":0,"data":{"actions":[{"name":"inspect","params":{"$resource":"resource:3a7b44a1","a":1}}],"description":"test-rule","enabled":true,"id":"rule:6fce0ca9","rawsql":"select * from \"t/a\""}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### modify
|
|
||||||
|
|
||||||
```shell
|
|
||||||
## modify all of the params
|
|
||||||
$ curl -XPUT -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915' -d \
|
|
||||||
'{"rawsql":"select * from \"t/a\"","actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule"}'
|
|
||||||
|
|
||||||
## modify some of the params: disable it
|
|
||||||
$ curl -XPUT -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915' -d \
|
|
||||||
'{"enabled": false}'
|
|
||||||
|
|
||||||
## modify some of the params: add fallback actions
|
|
||||||
$ curl -XPUT -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915' -d \
|
|
||||||
'{"actions":[{"name":"inspect","params":{"a":1}, "fallbacks": [{"name":"donothing"}]}]}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### show
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915'
|
|
||||||
|
|
||||||
{"code":0,"data":{"actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule","enabled":true,"id":"rule:bc987915","rawsql":"select * from \"t/a\""}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### list
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k http://localhost:8081/api/v4/rules
|
|
||||||
|
|
||||||
{"code":0,"data":[{"actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule","enabled":true,"id":"rule:bc987915","rawsql":"select * from \"t/a\""},{"actions":[{"name":"inspect","params":{"$resource":"resource:3a7b44a1","a":1}}],"description":"test-rule","enabled":true,"id":"rule:6fce0ca9","rawsql":"select * from \"t/a\""}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
### delete
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -XDELETE -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915'
|
|
||||||
|
|
||||||
{"code":0}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Actions
|
|
||||||
|
|
||||||
### list
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k http://localhost:8081/api/v4/actions
|
|
||||||
|
|
||||||
{"code":0,"data":[{"app":"emqx_rule_engine","description":"Republish a MQTT message to a another topic","name":"republish","params":{...},"types":[]},{"app":"emqx_rule_engine","description":"Inspect the details of action params for debug purpose","name":"inspect","params":{},"types":[]},{"app":"emqx_web_hook","description":"Forward Messages to Web Server","name":"data_to_webserver","params":{...},"types":["web_hook"]}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
### show
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/actions/inspect'
|
|
||||||
|
|
||||||
{"code":0,"data":{"app":"emqx_rule_engine","description":"Debug Action","name":"inspect","params":{"$resource":"built_in"}}}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Resource Types
|
|
||||||
|
|
||||||
### list
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resource_types'
|
|
||||||
|
|
||||||
{"code":0,"data":[{"description":"Debug resource type","name":"built_in","params":{},"provider":"emqx_rule_engine"}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
### list all resources of a type
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resource_types/built_in/resources'
|
|
||||||
|
|
||||||
{"code":0,"data":[{"attrs":"undefined","config":{"a":1},"description":"test-rule","id":"resource:71df3086","type":"built_in"}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
### show
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resource_types/built_in'
|
|
||||||
|
|
||||||
{"code":0,"data":{"description":"Debug resource type","name":"built_in","params":{},"provider":"emqx_rule_engine"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
### create
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources' -d \
|
|
||||||
'{"type": "built_in", "config": {"a":1}, "description": "test-resource"}'
|
|
||||||
|
|
||||||
{"code":0,"data":{"attrs":"undefined","config":{"a":1},"description":"test-resource","id":"resource:71df3086","type":"built_in"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### start
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -XPOST -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources/resource:71df3086'
|
|
||||||
|
|
||||||
{"code":0}
|
|
||||||
```
|
|
||||||
|
|
||||||
### list
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources'
|
|
||||||
|
|
||||||
{"code":0,"data":[{"attrs":"undefined","config":{"a":1},"description":"test-resource","id":"resource:71df3086","type":"built_in"}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
### show
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources/resource:71df3086'
|
|
||||||
|
|
||||||
{"code":0,"data":{"attrs":"undefined","config":{"a":1},"description":"test-resource","id":"resource:71df3086","type":"built_in"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### get resource status
|
|
||||||
|
|
||||||
```shell
|
|
||||||
curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resource_status/resource:71df3086'
|
|
||||||
|
|
||||||
{"code":0,"data":{"is_alive":true}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### delete
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ curl -XDELETE -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources/resource:71df3086'
|
|
||||||
|
|
||||||
{"code":0}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rule example using webhook
|
|
||||||
|
|
||||||
``` shell
|
|
||||||
|
|
||||||
$ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources' -d \
|
|
||||||
'{"type": "web_hook", "config": {"url": "http://127.0.0.1:9910", "headers": {"token":"axfw34y235wrq234t4ersgw4t"}, "method": "POST"}, "description": "web hook resource-1"}'
|
|
||||||
|
|
||||||
{"code":0,"data":{"attrs":"undefined","config":{"headers":{"token":"axfw34y235wrq234t4ersgw4t"},"method":"POST","url":"http://127.0.0.1:9910"},"description":"web hook resource-1","id":"resource:8531a11f","type":"web_hook"}}
|
|
||||||
|
|
||||||
curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules' -d \
|
|
||||||
'{"rawsql":"SELECT clientid as c, username as u.name FROM \"#\"","actions":[{"name":"data_to_webserver","params":{"$resource": "resource:8531a11f"}}],"description":"Forward connected events to webhook"}'
|
|
||||||
|
|
||||||
{"code":0,"data":{"actions":[{"name":"data_to_webserver","params":{"$resource":"resource:8531a11f","headers":{"token":"axfw34y235wrq234t4ersgw4t"},"method":"POST","url":"http://127.0.0.1:9910"}}],"description":"Forward connected events to webhook","enabled":true,"id":"rule:4fe05936","rawsql":"select * from \"#\""}}
|
|
||||||
```
|
|
||||||
|
|
||||||
Start a `web server` using `nc`, and then connect to emqx broker using a mqtt client with username = 'Shawn':
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ echo -e "HTTP/1.1 200 OK\n\n $(date)" | nc -l 127.0.0.1 9910
|
|
||||||
|
|
||||||
POST / HTTP/1.1
|
|
||||||
content-type: application/json
|
|
||||||
content-length: 48
|
|
||||||
te:
|
|
||||||
host: 127.0.0.1:9910
|
|
||||||
connection: keep-alive
|
|
||||||
token: axfw34y235wrq234t4ersgw4t
|
|
||||||
|
|
||||||
{"c":"clientId-bP70ymeIyo","u":{"name":"Shawn"}
|
|
||||||
```
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue