Merge branch 'master' into EMQX-782
This commit is contained in:
commit
f20d5b5395
|
|
@ -36,9 +36,14 @@ emqx_test(){
|
||||||
"zip")
|
"zip")
|
||||||
packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.zip)
|
packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.zip)
|
||||||
unzip -q "${PACKAGE_PATH}/${packagename}"
|
unzip -q "${PACKAGE_PATH}/${packagename}"
|
||||||
export EMQX_ZONES__DEFAULT__MQTT__SERVER_KEEPALIVE=60 \
|
export EMQX_ZONES__DEFAULT__MQTT__SERVER_KEEPALIVE=60
|
||||||
EMQX_MQTT__MAX_TOPIC_ALIAS=10
|
export EMQX_MQTT__MAX_TOPIC_ALIAS=10
|
||||||
[[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_LISTENERS__QUIC__DEFAULT__ENABLED=false
|
export EMQX_LOG__CONSOLE_HANDLER__LEVEL=debug
|
||||||
|
export EMQX_LOG__FILE_HANDLERS__DEFAULT__LEVEL=debug
|
||||||
|
if [[ $(arch) == *arm* || $(arch) == aarch64 ]]; then
|
||||||
|
export EMQX_LISTENERS__QUIC__DEFAULT__ENABLED=false
|
||||||
|
export WAIT_FOR_ERLANG_STOP=120
|
||||||
|
fi
|
||||||
# sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins
|
# sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins
|
||||||
|
|
||||||
echo "running ${packagename} start"
|
echo "running ${packagename} start"
|
||||||
|
|
@ -58,7 +63,11 @@ emqx_test(){
|
||||||
IDLE_TIME=$((IDLE_TIME+1))
|
IDLE_TIME=$((IDLE_TIME+1))
|
||||||
done
|
done
|
||||||
pytest -v /paho-mqtt-testing/interoperability/test_client/V5/test_connect.py::test_basic
|
pytest -v /paho-mqtt-testing/interoperability/test_client/V5/test_connect.py::test_basic
|
||||||
"${PACKAGE_PATH}"/emqx/bin/emqx stop
|
if ! "${PACKAGE_PATH}"/emqx/bin/emqx stop; then
|
||||||
|
cat "${PACKAGE_PATH}"/emqx/log/erlang.log.1 || true
|
||||||
|
cat "${PACKAGE_PATH}"/emqx/log/emqx.log.1 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "running ${packagename} stop"
|
echo "running ${packagename} stop"
|
||||||
rm -rf "${PACKAGE_PATH}"/emqx
|
rm -rf "${PACKAGE_PATH}"/emqx
|
||||||
;;
|
;;
|
||||||
|
|
@ -133,6 +142,7 @@ EOF
|
||||||
## for ARM, due to CI env issue, skip start of quic listener for the moment
|
## for ARM, due to CI env issue, skip start of quic listener for the moment
|
||||||
[[ $(arch) == *arm* || $(arch) == aarch64 ]] && tee -a "$emqx_env_vars" <<EOF
|
[[ $(arch) == *arm* || $(arch) == aarch64 ]] && tee -a "$emqx_env_vars" <<EOF
|
||||||
export EMQX_LISTENERS__QUIC__DEFAULT__ENABLED=false
|
export EMQX_LISTENERS__QUIC__DEFAULT__ENABLED=false
|
||||||
|
export WAIT_FOR_ERLANG_STOP=120
|
||||||
EOF
|
EOF
|
||||||
else
|
else
|
||||||
echo "Error: cannot locate emqx_vars"
|
echo "Error: cannot locate emqx_vars"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -386,7 +386,7 @@ jobs:
|
||||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
- uses: docker/build-push-action@v2
|
- uses: docker/build-push-action@v2
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release' && github.event.release.prerelease
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
pull: true
|
pull: true
|
||||||
|
|
@ -400,6 +400,23 @@ jobs:
|
||||||
EMQX_NAME=${{ matrix.profile }}
|
EMQX_NAME=${{ matrix.profile }}
|
||||||
file: source/deploy/docker/Dockerfile
|
file: source/deploy/docker/Dockerfile
|
||||||
context: source
|
context: source
|
||||||
|
- uses: docker/build-push-action@v2
|
||||||
|
if: github.event_name == 'release' && !github.event.release.prerelease
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
no-cache: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
emqx/${{ matrix.profile }}:latest
|
||||||
|
emqx/${{ matrix.profile }}:${{ steps.version.outputs.version }}
|
||||||
|
build-args: |
|
||||||
|
PKG_VSN=${{ steps.version.outputs.version }}
|
||||||
|
BUILD_FROM=ghcr.io/emqx/emqx-builder-helper/5.0:${{ matrix.otp }}-alpine3.14
|
||||||
|
RUN_FROM=alpine:3.14
|
||||||
|
EMQX_NAME=${{ matrix.profile }}
|
||||||
|
file: source/deploy/docker/Dockerfile
|
||||||
|
context: source
|
||||||
|
|
||||||
delete-artifact:
|
delete-artifact:
|
||||||
|
|
||||||
|
|
@ -487,15 +504,6 @@ jobs:
|
||||||
-X POST \
|
-X POST \
|
||||||
-d "{\"repo\":\"emqx/emqx\", \"tag\": \"${{ env.version }}\" }" \
|
-d "{\"repo\":\"emqx/emqx\", \"tag\": \"${{ env.version }}\" }" \
|
||||||
${{ secrets.EMQX_IO_RELEASE_API }}
|
${{ secrets.EMQX_IO_RELEASE_API }}
|
||||||
- name: push docker image to docker hub
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
run: |
|
|
||||||
set -e -x -u
|
|
||||||
sudo make docker-prepare
|
|
||||||
cd _packages/${{ matrix.profile }} && for var in $(ls |grep docker |grep -v sha256); do unzip $var; sudo docker load < ${var%.*}; rm -f ${var%.*}; done && cd -
|
|
||||||
echo ${{ secrets.DOCKER_HUB_TOKEN }} |sudo docker login -u ${{ secrets.DOCKER_HUB_USER }} --password-stdin
|
|
||||||
sudo TARGET=emqx/${{ matrix.profile }} make docker-push
|
|
||||||
sudo TARGET=emqx/${{ matrix.profile }} make docker-manifest-list
|
|
||||||
- name: update repo.emqx.io
|
- name: update repo.emqx.io
|
||||||
if: github.event_name == 'release' && endsWith(github.repository, 'enterprise') && matrix.profile == 'emqx-ee'
|
if: github.event_name == 'release' && endsWith(github.repository, 'enterprise') && matrix.profile == 'emqx-ee'
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -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,10 +45,19 @@ 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
|
||||||
|
- api_stats
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
repository: emqx/emqx-fvt
|
repository: emqx/emqx-fvt
|
||||||
|
ref: v1.2.0
|
||||||
path: .
|
path: .
|
||||||
- uses: actions/setup-java@v1
|
- uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
|
|
@ -74,7 +83,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.13
|
export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.16
|
||||||
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/).
|
||||||
|
|
|
||||||
|
|
@ -194,12 +194,17 @@ listeners.ssl.default {
|
||||||
mountpoint = ""
|
mountpoint = ""
|
||||||
|
|
||||||
## SSL options
|
## SSL options
|
||||||
## See ${example_common_ssl_options} for more information
|
|
||||||
ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
|
||||||
ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem"
|
ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem"
|
||||||
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"]
|
||||||
|
# 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"
|
||||||
|
# PSK: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA"
|
||||||
|
# NOTE: If PSK cipher-suites are intended, tlsv1.3 should not be enabled in 'versions' config
|
||||||
|
# ssl.ciphers = ""
|
||||||
|
|
||||||
## TCP options
|
## TCP options
|
||||||
## See ${example_common_tcp_options} for more information
|
## See ${example_common_tcp_options} for more information
|
||||||
tcp.backlog = 1024
|
tcp.backlog = 1024
|
||||||
|
|
@ -1345,12 +1350,13 @@ example_common_ssl_options {
|
||||||
## Default: true
|
## Default: true
|
||||||
ssl.honor_cipher_order = true
|
ssl.honor_cipher_order = true
|
||||||
|
|
||||||
## TLS versions only to protect from POODLE attack.
|
# 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"
|
||||||
## @doc listeners.<name>.ssl.versions
|
# 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"
|
||||||
## ValueType: Array<TLSVersion>
|
# PSK: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA"
|
||||||
## Default: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
# NOTE: If PSK cipher-suites are intended, tlsv1.3 should not be enabled in 'versions' config
|
||||||
ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"]
|
# NOTE: by default, ALL ciphers are enabled
|
||||||
|
# ssl.ciphers = ""
|
||||||
|
|
||||||
## TLS Handshake timeout.
|
## TLS Handshake timeout.
|
||||||
##
|
##
|
||||||
|
|
@ -1446,27 +1452,6 @@ example_common_ssl_options {
|
||||||
## Default: true
|
## Default: true
|
||||||
ssl.fail_if_no_peer_cert = false
|
ssl.fail_if_no_peer_cert = false
|
||||||
|
|
||||||
## This is the single most important configuration option of an Erlang SSL
|
|
||||||
## application. Ciphers (and their ordering) define the way the client and
|
|
||||||
## server encrypt information over the wire, from the initial Diffie-Helman
|
|
||||||
## key exchange, the session key encryption ## algorithm and the message
|
|
||||||
## digest algorithm. Selecting a good cipher suite is critical for the
|
|
||||||
## application’s data security, confidentiality and performance.
|
|
||||||
##
|
|
||||||
## The cipher list above offers:
|
|
||||||
##
|
|
||||||
## A good balance between compatibility with older browsers.
|
|
||||||
## It can get stricter for Machine-To-Machine scenarios.
|
|
||||||
## Perfect Forward Secrecy.
|
|
||||||
## No old/insecure encryption and HMAC algorithms
|
|
||||||
##
|
|
||||||
## Most of it was copied from Mozilla’s Server Side TLS article
|
|
||||||
##
|
|
||||||
## @doc listeners.<name>.ssl.ciphers
|
|
||||||
## ValueType: Array<Cipher>
|
|
||||||
## 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,PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA]
|
|
||||||
ssl.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-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA]
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## Socket options for websocket connections
|
## Socket options for websocket connections
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,18 @@
|
||||||
-ifndef(EMQ_X_HRL).
|
-ifndef(EMQ_X_HRL).
|
||||||
-define(EMQ_X_HRL, true).
|
-define(EMQ_X_HRL, true).
|
||||||
|
|
||||||
|
%% Shard
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Common
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
-define(Otherwise, true).
|
|
||||||
|
|
||||||
-define(COMMON_SHARD, emqx_common_shard).
|
-define(COMMON_SHARD, emqx_common_shard).
|
||||||
-define(SHARED_SUB_SHARD, emqx_shared_sub_shard).
|
-define(SHARED_SUB_SHARD, emqx_shared_sub_shard).
|
||||||
-define(MOD_DELAYED_SHARD, emqx_delayed_shard).
|
|
||||||
-define(CM_SHARD, emqx_cm_shard).
|
-define(CM_SHARD, emqx_cm_shard).
|
||||||
|
-define(ROUTE_SHARD, route_shard).
|
||||||
|
|
||||||
|
-define(BOOT_SHARDS, [ ?ROUTE_SHARD
|
||||||
|
, ?COMMON_SHARD
|
||||||
|
, ?SHARED_SUB_SHARD
|
||||||
|
]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Banner
|
%% Banner
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -71,7 +71,9 @@
|
||||||
%% Message Payload
|
%% Message Payload
|
||||||
payload :: emqx_types:payload(),
|
payload :: emqx_types:payload(),
|
||||||
%% Timestamp (Unit: millisecond)
|
%% Timestamp (Unit: millisecond)
|
||||||
timestamp :: integer()
|
timestamp :: integer(),
|
||||||
|
%% not used so far, for future extension
|
||||||
|
extra = [] :: term()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-record(delivery, {
|
-record(delivery, {
|
||||||
|
|
@ -83,11 +85,6 @@
|
||||||
%% Route
|
%% Route
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-define(ROUTE_SHARD, route_shard).
|
|
||||||
|
|
||||||
|
|
||||||
-define(RULE_ENGINE_SHARD, emqx_rule_engine_shard).
|
|
||||||
|
|
||||||
-record(route, {
|
-record(route, {
|
||||||
topic :: binary(),
|
topic :: binary(),
|
||||||
dest :: node() | {binary(), node()}
|
dest :: node() | {binary(), node()}
|
||||||
|
|
@ -133,8 +130,6 @@
|
||||||
until :: integer()
|
until :: integer()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-endif.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Authentication
|
%% Authentication
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -149,4 +144,6 @@
|
||||||
-record(chain,
|
-record(chain,
|
||||||
{ name :: atom()
|
{ name :: atom()
|
||||||
, authenticators :: [#authenticator{}]
|
, authenticators :: [#authenticator{}]
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
|
||||||
|
|
@ -542,4 +542,9 @@
|
||||||
-define(SHARE(Group, Topic), emqx_topic:join([<<?SHARE>>, Group, Topic])).
|
-define(SHARE(Group, Topic), emqx_topic:join([<<?SHARE>>, Group, Topic])).
|
||||||
-define(IS_SHARE(Topic), case Topic of <<?SHARE, _/binary>> -> true; _ -> false end).
|
-define(IS_SHARE(Topic), case Topic of <<?SHARE, _/binary>> -> true; _ -> false end).
|
||||||
|
|
||||||
|
-define(FRAME_PARSE_ERROR(Reason), {frame_parse_error, Reason}).
|
||||||
|
-define(FRAME_SERIALIZE_ERROR(Reason), {frame_serialize_error, Reason}).
|
||||||
|
-define(THROW_FRAME_ERROR(Reason), erlang:throw(?FRAME_PARSE_ERROR(Reason))).
|
||||||
|
-define(THROW_SERIALIZE_ERROR(Reason), erlang:throw(?FRAME_SERIALIZE_ERROR(Reason))).
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
-ifndef(EMQX_ENTERPRISE).
|
-ifndef(EMQX_ENTERPRISE).
|
||||||
|
|
||||||
-define(EMQX_RELEASE, {opensource, "5.0-alpha.6"}).
|
-define(EMQX_RELEASE, {opensource, "5.0-beta.1"}).
|
||||||
|
|
||||||
-else.
|
-else.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
|
|
||||||
-define(LOG(Level, Format), ?LOG(Level, Format, [])).
|
-define(LOG(Level, Format), ?LOG(Level, Format, [])).
|
||||||
|
|
||||||
|
%% deprecated
|
||||||
-define(LOG(Level, Format, Args, Meta),
|
-define(LOG(Level, Format, Args, Meta),
|
||||||
%% check 'allow' here so we do not have to pass an anonymous function
|
%% check 'allow' here so we do not have to pass an anonymous function
|
||||||
%% down to logger which may cause `badfun` exception during upgrade
|
%% down to logger which may cause `badfun` exception during upgrade
|
||||||
|
|
@ -58,8 +59,15 @@
|
||||||
|
|
||||||
%% structured logging
|
%% structured logging
|
||||||
-define(SLOG(Level, Data),
|
-define(SLOG(Level, Data),
|
||||||
logger:log(Level, Data, #{ mfa => {?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY}
|
%% check 'allow' here, only evaluate Data when necessary
|
||||||
, line => ?LINE})).
|
case logger:allow(Level, ?MODULE) of
|
||||||
|
true ->
|
||||||
|
logger:log(Level, (Data), #{ mfa => {?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY}
|
||||||
|
, line => ?LINE
|
||||||
|
});
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end).
|
||||||
|
|
||||||
%% print to 'user' group leader
|
%% print to 'user' group leader
|
||||||
-define(ULOG(Fmt, Args), io:format(user, Fmt, Args)).
|
-define(ULOG(Fmt, Args), io:format(user, Fmt, Args)).
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@
|
||||||
%% `git_subdir` dependency in other projects.
|
%% `git_subdir` dependency in other projects.
|
||||||
{deps,
|
{deps,
|
||||||
[ {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
|
[ {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
|
||||||
, {typerefl, {git, "https://github.com/k32/typerefl", {tag, "0.8.4"}}}
|
, {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.0"}}}
|
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.5"}}}
|
||||||
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
|
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
|
||||||
, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}
|
, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}
|
||||||
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}}
|
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}}
|
||||||
|
|
|
||||||
|
|
@ -239,11 +239,11 @@ handle_call({get_alarms, deactivated}, _From, State) ->
|
||||||
{reply, Alarms, State};
|
{reply, Alarms, State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected msg: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({timeout, _TRef, delete_expired_deactivated_alarm},
|
handle_info({timeout, _TRef, delete_expired_deactivated_alarm},
|
||||||
|
|
@ -253,11 +253,11 @@ handle_info({timeout, _TRef, delete_expired_deactivated_alarm},
|
||||||
{noreply, State#state{timer = ensure_timer(TRef, Period)}};
|
{noreply, State#state{timer = ensure_timer(TRef, Period)}};
|
||||||
|
|
||||||
handle_info({update_timer, Period}, #state{timer = TRef} = State) ->
|
handle_info({update_timer, Period}, #state{timer = TRef} = State) ->
|
||||||
?LOG(warning, "update the 'validity_period' timer to ~p", [Period]),
|
?SLOG(warning, #{msg => "validity_timer_updated", period => Period}),
|
||||||
{noreply, State#state{timer = ensure_timer(TRef, Period)}};
|
{noreply, State#state{timer = ensure_timer(TRef, Period)}};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
@ -323,8 +323,11 @@ deactivate_all_alarms() ->
|
||||||
clear_table(TableName) ->
|
clear_table(TableName) ->
|
||||||
case ekka_mnesia:clear_table(TableName) of
|
case ekka_mnesia:clear_table(TableName) of
|
||||||
{aborted, Reason} ->
|
{aborted, Reason} ->
|
||||||
?LOG(warning, "Faile to clear table ~p reason: ~p",
|
?SLOG(warning, #{
|
||||||
[TableName, Reason]);
|
msg => "fail_to_clear_table",
|
||||||
|
table_name => TableName,
|
||||||
|
reason => Reason
|
||||||
|
});
|
||||||
{atomic, ok} ->
|
{atomic, ok} ->
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
@ -354,10 +357,17 @@ delete_expired_deactivated_alarms(ActivatedAt, Checkpoint) ->
|
||||||
do_actions(_, _, []) ->
|
do_actions(_, _, []) ->
|
||||||
ok;
|
ok;
|
||||||
do_actions(activate, Alarm = #activated_alarm{name = Name, message = Message}, [log | More]) ->
|
do_actions(activate, Alarm = #activated_alarm{name = Name, message = Message}, [log | More]) ->
|
||||||
?LOG(warning, "Alarm ~s is activated, ~s", [Name, Message]),
|
?SLOG(warning, #{
|
||||||
|
msg => "alarm_is_activated",
|
||||||
|
name => Name,
|
||||||
|
message => Message
|
||||||
|
}),
|
||||||
do_actions(activate, Alarm, More);
|
do_actions(activate, Alarm, More);
|
||||||
do_actions(deactivate, Alarm = #deactivated_alarm{name = Name}, [log | More]) ->
|
do_actions(deactivate, Alarm = #deactivated_alarm{name = Name}, [log | More]) ->
|
||||||
?LOG(warning, "Alarm ~s is deactivated", [Name]),
|
?SLOG(warning, #{
|
||||||
|
msg => "alarm_is_deactivated",
|
||||||
|
name => Name
|
||||||
|
}),
|
||||||
do_actions(deactivate, Alarm, More);
|
do_actions(deactivate, Alarm, More);
|
||||||
do_actions(Operation, Alarm, [publish | More]) ->
|
do_actions(Operation, Alarm, [publish | More]) ->
|
||||||
Topic = topic(Operation),
|
Topic = topic(Operation),
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
, get_description/0
|
, get_description/0
|
||||||
, get_release/0
|
, get_release/0
|
||||||
, set_init_config_load_done/0
|
, set_init_config_load_done/0
|
||||||
|
, get_init_config_load_done/0
|
||||||
, set_override_conf_file/1
|
, set_override_conf_file/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
|
@ -33,25 +34,18 @@
|
||||||
|
|
||||||
-define(APP, emqx).
|
-define(APP, emqx).
|
||||||
|
|
||||||
-define(EMQX_SHARDS, [ ?ROUTE_SHARD
|
|
||||||
, ?COMMON_SHARD
|
|
||||||
, ?SHARED_SUB_SHARD
|
|
||||||
, ?RULE_ENGINE_SHARD
|
|
||||||
, ?MOD_DELAYED_SHARD
|
|
||||||
]).
|
|
||||||
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Application callbacks
|
%% Application callbacks
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
start(_Type, _Args) ->
|
start(_Type, _Args) ->
|
||||||
ok = maybe_load_config(),
|
ok = maybe_load_config(),
|
||||||
|
|
||||||
%% Load application first for ekka_mnesia scanner
|
%% Load application first for ekka_mnesia scanner
|
||||||
mnesia:change_table_copy_type(schema, node(), disc_copies),
|
%% mnesia:change_table_copy_type(schema, node(), disc_copies),
|
||||||
ekka:start(),
|
|
||||||
ok = ekka_rlog:wait_for_shards(?EMQX_SHARDS, infinity),
|
|
||||||
ok = maybe_start_quicer(),
|
ok = maybe_start_quicer(),
|
||||||
|
ensure_ekka_started(),
|
||||||
{ok, Sup} = emqx_sup:start_link(),
|
{ok, Sup} = emqx_sup:start_link(),
|
||||||
ok = maybe_start_listeners(),
|
ok = maybe_start_listeners(),
|
||||||
ok = emqx_alarm_handler:load(),
|
ok = emqx_alarm_handler:load(),
|
||||||
|
|
@ -65,21 +59,28 @@ prep_stop(_State) ->
|
||||||
|
|
||||||
stop(_State) -> ok.
|
stop(_State) -> ok.
|
||||||
|
|
||||||
|
ensure_ekka_started() ->
|
||||||
|
ekka:start(),
|
||||||
|
ok = ekka_rlog:wait_for_shards(?BOOT_SHARDS, infinity).
|
||||||
|
|
||||||
%% @doc Call this function to make emqx boot without loading config,
|
%% @doc Call this function to make emqx boot without loading config,
|
||||||
%% in case we want to delegate the config load to a higher level app
|
%% in case we want to delegate the config load to a higher level app
|
||||||
%% which manages emqx app.
|
%% which manages emqx app.
|
||||||
set_init_config_load_done() ->
|
set_init_config_load_done() ->
|
||||||
application:set_env(emqx, init_config_load_done, true).
|
application:set_env(emqx, init_config_load_done, true).
|
||||||
|
|
||||||
|
get_init_config_load_done() ->
|
||||||
|
application:get_env(emqx, init_config_load_done, false).
|
||||||
|
|
||||||
%% @doc This API is mostly for testing.
|
%% @doc This API is mostly for testing.
|
||||||
%% The override config file is typically located in the 'data' dir when
|
%% The override config file is typically located in the 'data' dir when
|
||||||
%% it is a emqx release, but emqx app should not have to konw where the
|
%% it is a emqx release, but emqx app should not have to know where the
|
||||||
%% 'data' dir is located.
|
%% 'data' dir is located.
|
||||||
set_override_conf_file(File) ->
|
set_override_conf_file(File) ->
|
||||||
application:set_env(emqx, override_conf_file, File).
|
application:set_env(emqx, override_conf_file, File).
|
||||||
|
|
||||||
maybe_load_config() ->
|
maybe_load_config() ->
|
||||||
case application:get_env(emqx, init_config_load_done, false) of
|
case get_init_config_load_done() of
|
||||||
true ->
|
true ->
|
||||||
ok;
|
ok;
|
||||||
false ->
|
false ->
|
||||||
|
|
|
||||||
|
|
@ -289,22 +289,16 @@ check_config(Config) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) ->
|
authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) ->
|
||||||
case ets:lookup(?CHAINS_TAB, Listener) of
|
Authenticators = get_authenticators(Listener, global_chain(Protocol)),
|
||||||
[#chain{authenticators = Authenticators}] when Authenticators =/= [] ->
|
case get_enabled(Authenticators) of
|
||||||
do_authenticate(Authenticators, Credential);
|
[] -> ignore;
|
||||||
_ ->
|
NAuthenticators -> do_authenticate(NAuthenticators, Credential)
|
||||||
case ets:lookup(?CHAINS_TAB, global_chain(Protocol)) of
|
|
||||||
[#chain{authenticators = Authenticators}] when Authenticators =/= [] ->
|
|
||||||
do_authenticate(Authenticators, Credential);
|
|
||||||
_ ->
|
|
||||||
ignore
|
|
||||||
end
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_authenticate([], _) ->
|
do_authenticate([], _) ->
|
||||||
{stop, {error, not_authorized}};
|
{stop, {error, not_authorized}};
|
||||||
do_authenticate([#authenticator{provider = Provider, state = State} | More], Credential) ->
|
do_authenticate([#authenticator{id = ID, provider = Provider, state = State} | More], Credential) ->
|
||||||
case Provider:authenticate(Credential, State) of
|
try Provider:authenticate(Credential, State) of
|
||||||
ignore ->
|
ignore ->
|
||||||
do_authenticate(More, Credential);
|
do_authenticate(More, Credential);
|
||||||
Result ->
|
Result ->
|
||||||
|
|
@ -314,8 +308,32 @@ do_authenticate([#authenticator{provider = Provider, state = State} | More], Cre
|
||||||
%% {continue, AuthData, AuthCache}
|
%% {continue, AuthData, AuthCache}
|
||||||
%% {error, Reason}
|
%% {error, Reason}
|
||||||
{stop, Result}
|
{stop, Result}
|
||||||
|
catch
|
||||||
|
Class:Reason:Stacktrace ->
|
||||||
|
?SLOG(warning, #{msg => "unexpected_error_in_authentication",
|
||||||
|
exception => Class,
|
||||||
|
reason => Reason,
|
||||||
|
stacktrace => Stacktrace,
|
||||||
|
authenticator => ID}),
|
||||||
|
do_authenticate(More, Credential)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
get_authenticators(Listener, Global) ->
|
||||||
|
case ets:lookup(?CHAINS_TAB, Listener) of
|
||||||
|
[#chain{authenticators = Authenticators}] ->
|
||||||
|
Authenticators;
|
||||||
|
_ ->
|
||||||
|
case ets:lookup(?CHAINS_TAB, Global) of
|
||||||
|
[#chain{authenticators = Authenticators}] ->
|
||||||
|
Authenticators;
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
get_enabled(Authenticators) ->
|
||||||
|
[Authenticator || Authenticator <- Authenticators, Authenticator#authenticator.enable =:= true].
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
@ -331,7 +349,11 @@ initialize_authentication(ChainName, AuthenticatorsConfig) ->
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
ok;
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Failed to create authenticator '~s': ~p", [generate_id(AuthenticatorConfig), Reason])
|
?SLOG(error, #{
|
||||||
|
msg => "failed_to_create_authenticator",
|
||||||
|
authenticator => generate_id(AuthenticatorConfig),
|
||||||
|
reason => Reason
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end, CheckedConfig).
|
end, CheckedConfig).
|
||||||
|
|
||||||
|
|
@ -536,7 +558,7 @@ handle_call({create_authenticator, ChainName, Config}, _From, #{providers := Pro
|
||||||
false ->
|
false ->
|
||||||
case do_create_authenticator(ChainName, AuthenticatorID, Config, Providers) of
|
case do_create_authenticator(ChainName, AuthenticatorID, Config, Providers) of
|
||||||
{ok, Authenticator} ->
|
{ok, Authenticator} ->
|
||||||
NAuthenticators = Authenticators ++ [Authenticator],
|
NAuthenticators = Authenticators ++ [Authenticator#authenticator{enable = maps:get(enable, Config)}],
|
||||||
true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}),
|
true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}),
|
||||||
{ok, serialize_authenticator(Authenticator)};
|
{ok, serialize_authenticator(Authenticator)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
@ -575,7 +597,8 @@ handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, S
|
||||||
Unique = unique(ChainName, AuthenticatorID, Version),
|
Unique = unique(ChainName, AuthenticatorID, Version),
|
||||||
case Provider:update(Config#{'_unique' => Unique}, ST) of
|
case Provider:update(Config#{'_unique' => Unique}, ST) of
|
||||||
{ok, NewST} ->
|
{ok, NewST} ->
|
||||||
NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST)},
|
NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST),
|
||||||
|
enable = maps:get(enable, Config)},
|
||||||
NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
|
NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
|
||||||
true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NewAuthenticators}),
|
true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NewAuthenticators}),
|
||||||
{ok, serialize_authenticator(NewAuthenticator)};
|
{ok, serialize_authenticator(NewAuthenticator)};
|
||||||
|
|
@ -629,15 +652,15 @@ handle_call({list_users, ChainName, AuthenticatorID}, _From, State) ->
|
||||||
reply(Reply, State);
|
reply(Reply, State);
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Req, State) ->
|
handle_cast(Req, State) ->
|
||||||
?LOG(error, "Unexpected case: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Req}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -187,11 +187,11 @@ init([]) ->
|
||||||
{ok, ensure_expiry_timer(#{expiry_timer => undefined})}.
|
{ok, ensure_expiry_timer(#{expiry_timer => undefined})}.
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "unexpected msg: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_msg", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) ->
|
handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) ->
|
||||||
|
|
@ -199,7 +199,7 @@ handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) ->
|
||||||
{noreply, ensure_expiry_timer(State), hibernate};
|
{noreply, ensure_expiry_timer(State), hibernate};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #{expiry_timer := TRef}) ->
|
terminate(_Reason, #{expiry_timer := TRef}) ->
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,8 @@ publish(Msg) when is_record(Msg, message) ->
|
||||||
emqx_message:is_sys(Msg) orelse emqx_metrics:inc('messages.publish'),
|
emqx_message:is_sys(Msg) orelse emqx_metrics:inc('messages.publish'),
|
||||||
case emqx_hooks:run_fold('message.publish', [], emqx_message:clean_dup(Msg)) of
|
case emqx_hooks:run_fold('message.publish', [], emqx_message:clean_dup(Msg)) of
|
||||||
#message{headers = #{allow_publish := false}} ->
|
#message{headers = #{allow_publish := false}} ->
|
||||||
?LOG(notice, "Stop publishing: ~s", [emqx_message:format(Msg)]),
|
?SLOG(debug, #{msg => "message_not_published",
|
||||||
|
payload => emqx_message:to_log_map(Msg)}),
|
||||||
[];
|
[];
|
||||||
Msg1 = #message{topic = Topic} ->
|
Msg1 = #message{topic = Topic} ->
|
||||||
route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1))
|
route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1))
|
||||||
|
|
@ -214,9 +215,14 @@ safe_publish(Msg) when is_record(Msg, message) ->
|
||||||
try
|
try
|
||||||
publish(Msg)
|
publish(Msg)
|
||||||
catch
|
catch
|
||||||
_:Error:Stk->
|
Error : Reason : Stk->
|
||||||
?LOG(error, "Publish error: ~0p~n~s~n~0p",
|
?SLOG(error,#{
|
||||||
[Error, emqx_message:format(Msg), Stk]),
|
msg => "publishing_error",
|
||||||
|
exception => Error,
|
||||||
|
reason => Reason,
|
||||||
|
payload => emqx_message:to_log_map(Msg),
|
||||||
|
stacktrace => Stk
|
||||||
|
}),
|
||||||
[]
|
[]
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -266,14 +272,22 @@ forward(Node, To, Delivery, async) ->
|
||||||
case emqx_rpc:cast(To, Node, ?BROKER, dispatch, [To, Delivery]) of
|
case emqx_rpc:cast(To, Node, ?BROKER, dispatch, [To, Delivery]) of
|
||||||
true -> emqx_metrics:inc('messages.forward');
|
true -> emqx_metrics:inc('messages.forward');
|
||||||
{badrpc, Reason} ->
|
{badrpc, Reason} ->
|
||||||
?LOG(error, "Ansync forward msg to ~s failed due to ~p", [Node, Reason]),
|
?SLOG(error, #{
|
||||||
|
msg => "async_forward_msg_to_node_failed",
|
||||||
|
node => Node,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{error, badrpc}
|
{error, badrpc}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
forward(Node, To, Delivery, sync) ->
|
forward(Node, To, Delivery, sync) ->
|
||||||
case emqx_rpc:call(To, Node, ?BROKER, dispatch, [To, Delivery]) of
|
case emqx_rpc:call(To, Node, ?BROKER, dispatch, [To, Delivery]) of
|
||||||
{badrpc, Reason} ->
|
{badrpc, Reason} ->
|
||||||
?LOG(error, "Sync forward msg to ~s failed due to ~p", [Node, Reason]),
|
?SLOG(error, #{
|
||||||
|
msg => "sync_forward_msg_to_node_failed",
|
||||||
|
node => Node,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{error, badrpc};
|
{error, badrpc};
|
||||||
Result ->
|
Result ->
|
||||||
emqx_metrics:inc('messages.forward'), Result
|
emqx_metrics:inc('messages.forward'), Result
|
||||||
|
|
@ -450,14 +464,14 @@ handle_call({subscribe, Topic, I}, _From, State) ->
|
||||||
{reply, Ok, State};
|
{reply, Ok, State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast({subscribe, Topic}, State) ->
|
handle_cast({subscribe, Topic}, State) ->
|
||||||
case emqx_router:do_add_route(Topic) of
|
case emqx_router:do_add_route(Topic) of
|
||||||
ok -> ok;
|
ok -> ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Failed to add route: ~p", [Reason])
|
?SLOG(error, #{msg => "failed_to_add_route", reason => Reason})
|
||||||
end,
|
end,
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
|
|
@ -481,11 +495,11 @@ handle_cast({unsubscribed, Topic, I}, State) ->
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #{pool := Pool, id := Id}) ->
|
terminate(_Reason, #{pool := Pool, id := Id}) ->
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ init([]) ->
|
||||||
{ok, #{pmon => emqx_pmon:new()}}.
|
{ok, #{pmon => emqx_pmon:new()}}.
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast({register_sub, SubPid, SubId}, State = #{pmon := PMon}) ->
|
handle_cast({register_sub, SubPid, SubId}, State = #{pmon := PMon}) ->
|
||||||
|
|
@ -127,7 +127,7 @@ handle_cast({register_sub, SubPid, SubId}, State = #{pmon := PMon}) ->
|
||||||
{noreply, State#{pmon := emqx_pmon:monitor(SubPid, PMon)}};
|
{noreply, State#{pmon := emqx_pmon:monitor(SubPid, PMon)}};
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #{pmon := PMon}) ->
|
handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #{pmon := PMon}) ->
|
||||||
|
|
@ -138,7 +138,7 @@ handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #{pmon := PMon})
|
||||||
{noreply, State#{pmon := PMon1}};
|
{noreply, State#{pmon := PMon1}};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -373,11 +373,11 @@ handle_in(?PUBACK_PACKET(PacketId, _ReasonCode, Properties), Channel
|
||||||
ok = after_message_acked(ClientInfo, Msg, Properties),
|
ok = after_message_acked(ClientInfo, Msg, Properties),
|
||||||
handle_out(publish, Publishes, Channel#channel{session = NSession});
|
handle_out(publish, Publishes, Channel#channel{session = NSession});
|
||||||
{error, ?RC_PACKET_IDENTIFIER_IN_USE} ->
|
{error, ?RC_PACKET_IDENTIFIER_IN_USE} ->
|
||||||
?LOG(warning, "The PUBACK PacketId ~w is inuse.", [PacketId]),
|
?SLOG(warning, #{msg => "puback_packetId_inuse", packetId => PacketId}),
|
||||||
ok = emqx_metrics:inc('packets.puback.inuse'),
|
ok = emqx_metrics:inc('packets.puback.inuse'),
|
||||||
{ok, Channel};
|
{ok, Channel};
|
||||||
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
|
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
|
||||||
?LOG(warning, "The PUBACK PacketId ~w is not found.", [PacketId]),
|
?SLOG(warning, #{msg => "puback_packetId_not_found", packetId => PacketId}),
|
||||||
ok = emqx_metrics:inc('packets.puback.missed'),
|
ok = emqx_metrics:inc('packets.puback.missed'),
|
||||||
{ok, Channel}
|
{ok, Channel}
|
||||||
end;
|
end;
|
||||||
|
|
@ -390,11 +390,11 @@ handle_in(?PUBREC_PACKET(PacketId, _ReasonCode, Properties), Channel
|
||||||
NChannel = Channel#channel{session = NSession},
|
NChannel = Channel#channel{session = NSession},
|
||||||
handle_out(pubrel, {PacketId, ?RC_SUCCESS}, NChannel);
|
handle_out(pubrel, {PacketId, ?RC_SUCCESS}, NChannel);
|
||||||
{error, RC = ?RC_PACKET_IDENTIFIER_IN_USE} ->
|
{error, RC = ?RC_PACKET_IDENTIFIER_IN_USE} ->
|
||||||
?LOG(warning, "The PUBREC PacketId ~w is inuse.", [PacketId]),
|
?SLOG(warning, #{msg => "pubrec_packetId_inuse", packetId => PacketId}),
|
||||||
ok = emqx_metrics:inc('packets.pubrec.inuse'),
|
ok = emqx_metrics:inc('packets.pubrec.inuse'),
|
||||||
handle_out(pubrel, {PacketId, RC}, Channel);
|
handle_out(pubrel, {PacketId, RC}, Channel);
|
||||||
{error, RC = ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
|
{error, RC = ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
|
||||||
?LOG(warning, "The PUBREC ~w is not found.", [PacketId]),
|
?SLOG(warning, #{msg => "pubrec_packetId_not_found", packetId => PacketId}),
|
||||||
ok = emqx_metrics:inc('packets.pubrec.missed'),
|
ok = emqx_metrics:inc('packets.pubrec.missed'),
|
||||||
handle_out(pubrel, {PacketId, RC}, Channel)
|
handle_out(pubrel, {PacketId, RC}, Channel)
|
||||||
end;
|
end;
|
||||||
|
|
@ -405,7 +405,7 @@ handle_in(?PUBREL_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Se
|
||||||
NChannel = Channel#channel{session = NSession},
|
NChannel = Channel#channel{session = NSession},
|
||||||
handle_out(pubcomp, {PacketId, ?RC_SUCCESS}, NChannel);
|
handle_out(pubcomp, {PacketId, ?RC_SUCCESS}, NChannel);
|
||||||
{error, RC = ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
|
{error, RC = ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
|
||||||
?LOG(warning, "The PUBREL PacketId ~w is not found.", [PacketId]),
|
?SLOG(warning, #{msg => "pubrec_packetId_not_found", packetId => PacketId}),
|
||||||
ok = emqx_metrics:inc('packets.pubrel.missed'),
|
ok = emqx_metrics:inc('packets.pubrel.missed'),
|
||||||
handle_out(pubcomp, {PacketId, RC}, Channel)
|
handle_out(pubcomp, {PacketId, RC}, Channel)
|
||||||
end;
|
end;
|
||||||
|
|
@ -420,7 +420,7 @@ handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = S
|
||||||
ok = emqx_metrics:inc('packets.pubcomp.inuse'),
|
ok = emqx_metrics:inc('packets.pubcomp.inuse'),
|
||||||
{ok, Channel};
|
{ok, Channel};
|
||||||
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
|
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
|
||||||
?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId]),
|
?SLOG(warning, #{msg => "pubcomp_packetId_not_found", packetId => PacketId}),
|
||||||
ok = emqx_metrics:inc('packets.pubcomp.missed'),
|
ok = emqx_metrics:inc('packets.pubcomp.missed'),
|
||||||
{ok, Channel}
|
{ok, Channel}
|
||||||
end;
|
end;
|
||||||
|
|
@ -501,11 +501,11 @@ handle_in({frame_error, Reason}, Channel = #channel{conn_state = ConnState})
|
||||||
handle_out(disconnect, {?RC_MALFORMED_PACKET, Reason}, Channel);
|
handle_out(disconnect, {?RC_MALFORMED_PACKET, Reason}, Channel);
|
||||||
|
|
||||||
handle_in({frame_error, Reason}, Channel = #channel{conn_state = disconnected}) ->
|
handle_in({frame_error, Reason}, Channel = #channel{conn_state = disconnected}) ->
|
||||||
?LOG(error, "Unexpected frame error: ~p", [Reason]),
|
?SLOG(error, #{msg => "malformed_mqtt_message", reason => Reason}),
|
||||||
{ok, Channel};
|
{ok, Channel};
|
||||||
|
|
||||||
handle_in(Packet, Channel) ->
|
handle_in(Packet, Channel) ->
|
||||||
?LOG(error, "Unexpected incoming: ~p", [Packet]),
|
?SLOG(error, #{msg => "disconnecting_due_to_unexpected_message", packet => Packet}),
|
||||||
handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel).
|
handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -529,7 +529,7 @@ process_connect(AckProps, Channel = #channel{conninfo = ConnInfo,
|
||||||
{error, client_id_unavailable} ->
|
{error, client_id_unavailable} ->
|
||||||
handle_out(connack, ?RC_CLIENT_IDENTIFIER_NOT_VALID, Channel);
|
handle_out(connack, ?RC_CLIENT_IDENTIFIER_NOT_VALID, Channel);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Failed to open session due to ~p", [Reason]),
|
?SLOG(error, #{msg => "failed_to_open_session", reason => Reason}),
|
||||||
handle_out(connack, ?RC_UNSPECIFIED_ERROR, Channel)
|
handle_out(connack, ?RC_UNSPECIFIED_ERROR, Channel)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -548,8 +548,11 @@ process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel) ->
|
||||||
Msg = packet_to_message(NPacket, NChannel),
|
Msg = packet_to_message(NPacket, NChannel),
|
||||||
do_publish(PacketId, Msg, NChannel);
|
do_publish(PacketId, Msg, NChannel);
|
||||||
{error, Rc = ?RC_NOT_AUTHORIZED, NChannel} ->
|
{error, Rc = ?RC_NOT_AUTHORIZED, NChannel} ->
|
||||||
?LOG(warning, "Cannot publish message to ~s due to ~s.",
|
?SLOG(warning, #{
|
||||||
[Topic, emqx_reason_codes:text(Rc)]),
|
msg => "cannot_publish_to_topic",
|
||||||
|
topic => Topic,
|
||||||
|
reason => emqx_reason_codes:name(Rc)
|
||||||
|
}),
|
||||||
case emqx:get_config([authorization, deny_action], ignore) of
|
case emqx:get_config([authorization, deny_action], ignore) of
|
||||||
ignore ->
|
ignore ->
|
||||||
case QoS of
|
case QoS of
|
||||||
|
|
@ -563,8 +566,11 @@ process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel) ->
|
||||||
handle_out(disconnect, Rc, NChannel)
|
handle_out(disconnect, Rc, NChannel)
|
||||||
end;
|
end;
|
||||||
{error, Rc = ?RC_QUOTA_EXCEEDED, NChannel} ->
|
{error, Rc = ?RC_QUOTA_EXCEEDED, NChannel} ->
|
||||||
?LOG(warning, "Cannot publish messages to ~s due to ~s.",
|
?SLOG(warning, #{
|
||||||
[Topic, emqx_reason_codes:text(Rc)]),
|
msg => "cannot_publish_to_topic",
|
||||||
|
topic => Topic,
|
||||||
|
reason => emqx_reason_codes:name(Rc)
|
||||||
|
}),
|
||||||
case QoS of
|
case QoS of
|
||||||
?QOS_0 ->
|
?QOS_0 ->
|
||||||
ok = emqx_metrics:inc('packets.publish.dropped'),
|
ok = emqx_metrics:inc('packets.publish.dropped'),
|
||||||
|
|
@ -575,8 +581,11 @@ process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel) ->
|
||||||
handle_out(pubrec, {PacketId, Rc}, NChannel)
|
handle_out(pubrec, {PacketId, Rc}, NChannel)
|
||||||
end;
|
end;
|
||||||
{error, Rc, NChannel} ->
|
{error, Rc, NChannel} ->
|
||||||
?LOG(warning, "Cannot publish message to ~s due to ~s.",
|
?SLOG(warning, #{
|
||||||
[Topic, emqx_reason_codes:text(Rc)]),
|
msg => "cannot_publish_to_topic",
|
||||||
|
topic => Topic,
|
||||||
|
reason => emqx_reason_codes:name(Rc)
|
||||||
|
}),
|
||||||
handle_out(disconnect, Rc, NChannel)
|
handle_out(disconnect, Rc, NChannel)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -621,8 +630,11 @@ do_publish(PacketId, Msg = #message{qos = ?QOS_2},
|
||||||
ok = emqx_metrics:inc('packets.publish.inuse'),
|
ok = emqx_metrics:inc('packets.publish.inuse'),
|
||||||
handle_out(pubrec, {PacketId, RC}, Channel);
|
handle_out(pubrec, {PacketId, RC}, Channel);
|
||||||
{error, RC = ?RC_RECEIVE_MAXIMUM_EXCEEDED} ->
|
{error, RC = ?RC_RECEIVE_MAXIMUM_EXCEEDED} ->
|
||||||
?LOG(warning, "Dropped the qos2 packet ~w "
|
?SLOG(warning, #{
|
||||||
"due to awaiting_rel is full.", [PacketId]),
|
msg => "dropped_qos2_packet",
|
||||||
|
reason => emqx_reason_codes:name(RC),
|
||||||
|
packetId => PacketId
|
||||||
|
}),
|
||||||
ok = emqx_metrics:inc('packets.publish.dropped'),
|
ok = emqx_metrics:inc('packets.publish.dropped'),
|
||||||
handle_out(pubrec, {PacketId, RC}, Channel)
|
handle_out(pubrec, {PacketId, RC}, Channel)
|
||||||
end.
|
end.
|
||||||
|
|
@ -671,8 +683,10 @@ process_subscribe([Topic = {TopicFilter, SubOpts}|More], SubProps, Channel, Acc)
|
||||||
Channel),
|
Channel),
|
||||||
process_subscribe(More, SubProps, NChannel, [{Topic, ReasonCode} | Acc]);
|
process_subscribe(More, SubProps, NChannel, [{Topic, ReasonCode} | Acc]);
|
||||||
{error, ReasonCode} ->
|
{error, ReasonCode} ->
|
||||||
?LOG(warning, "Cannot subscribe ~s due to ~s.",
|
?SLOG(warning, #{
|
||||||
[TopicFilter, emqx_reason_codes:text(ReasonCode)]),
|
msg => "cannot_subscribe_topic_filter",
|
||||||
|
reason => emqx_reason_codes:name(ReasonCode)
|
||||||
|
}),
|
||||||
process_subscribe(More, SubProps, Channel, [{Topic, ReasonCode} | Acc])
|
process_subscribe(More, SubProps, Channel, [{Topic, ReasonCode} | Acc])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -685,8 +699,10 @@ do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, Channel =
|
||||||
{ok, NSession} ->
|
{ok, NSession} ->
|
||||||
{QoS, Channel#channel{session = NSession}};
|
{QoS, Channel#channel{session = NSession}};
|
||||||
{error, RC} ->
|
{error, RC} ->
|
||||||
?LOG(warning, "Cannot subscribe ~s due to ~s.",
|
?SLOG(warning, #{
|
||||||
[TopicFilter, emqx_reason_codes:text(RC)]),
|
msg => "cannot_subscribe_topic_filter",
|
||||||
|
reason => emqx_reason_codes:text(RC)
|
||||||
|
}),
|
||||||
{RC, Channel}
|
{RC, Channel}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -869,7 +885,7 @@ handle_out(auth, {ReasonCode, Properties}, Channel) ->
|
||||||
{ok, ?AUTH_PACKET(ReasonCode, Properties), Channel};
|
{ok, ?AUTH_PACKET(ReasonCode, Properties), Channel};
|
||||||
|
|
||||||
handle_out(Type, Data, Channel) ->
|
handle_out(Type, Data, Channel) ->
|
||||||
?LOG(error, "Unexpected outgoing: ~s, ~p", [Type, Data]),
|
?SLOG(error, #{msg => "unexpected_outgoing", type => Type, data => Data}),
|
||||||
{ok, Channel}.
|
{ok, Channel}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -964,7 +980,7 @@ handle_call({quota, Policy}, Channel) ->
|
||||||
reply(ok, Channel#channel{quota = Quota});
|
reply(ok, Channel#channel{quota = Quota});
|
||||||
|
|
||||||
handle_call(Req, Channel) ->
|
handle_call(Req, Channel) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
reply(ignored, Channel).
|
reply(ignored, Channel).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -1004,7 +1020,7 @@ handle_info({sock_closed, Reason}, Channel =
|
||||||
end;
|
end;
|
||||||
|
|
||||||
handle_info({sock_closed, Reason}, Channel = #channel{conn_state = disconnected}) ->
|
handle_info({sock_closed, Reason}, Channel = #channel{conn_state = disconnected}) ->
|
||||||
?LOG(error, "Unexpected sock_closed: ~p", [Reason]),
|
?SLOG(error, #{msg => "unexpected_sock_close", reason => Reason}),
|
||||||
{ok, Channel};
|
{ok, Channel};
|
||||||
|
|
||||||
handle_info(clean_authz_cache, Channel) ->
|
handle_info(clean_authz_cache, Channel) ->
|
||||||
|
|
@ -1012,7 +1028,7 @@ handle_info(clean_authz_cache, Channel) ->
|
||||||
{ok, Channel};
|
{ok, Channel};
|
||||||
|
|
||||||
handle_info(Info, Channel) ->
|
handle_info(Info, Channel) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{ok, Channel}.
|
{ok, Channel}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -1075,7 +1091,7 @@ handle_timeout(_TRef, expire_quota_limit, Channel) ->
|
||||||
{ok, clean_timer(quota_timer, Channel)};
|
{ok, clean_timer(quota_timer, Channel)};
|
||||||
|
|
||||||
handle_timeout(_TRef, Msg, Channel) ->
|
handle_timeout(_TRef, Msg, Channel) ->
|
||||||
?LOG(error, "Unexpected timeout: ~p~n", [Msg]),
|
?SLOG(error, #{msg => "unexpected_timeout", timeout_message => Msg}),
|
||||||
{ok, Channel}.
|
{ok, Channel}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -266,9 +266,8 @@ get_mqtt_conf(Zone, Key) ->
|
||||||
emqx_config:get_zone_conf(Zone, [mqtt, Key]).
|
emqx_config:get_zone_conf(Zone, [mqtt, Key]).
|
||||||
|
|
||||||
%% @doc Try to takeover a session.
|
%% @doc Try to takeover a session.
|
||||||
-spec(takeover_session(emqx_types:clientid())
|
-spec(takeover_session(emqx_types:clientid()) ->
|
||||||
-> {error, term()}
|
{error, term()} | {ok, atom(), pid(), emqx_session:session()}).
|
||||||
| {ok, atom(), pid(), emqx_session:session()}).
|
|
||||||
takeover_session(ClientId) ->
|
takeover_session(ClientId) ->
|
||||||
case lookup_channels(ClientId) of
|
case lookup_channels(ClientId) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
|
|
@ -276,7 +275,7 @@ takeover_session(ClientId) ->
|
||||||
takeover_session(ClientId, ChanPid);
|
takeover_session(ClientId, ChanPid);
|
||||||
ChanPids ->
|
ChanPids ->
|
||||||
[ChanPid|StalePids] = lists:reverse(ChanPids),
|
[ChanPid|StalePids] = lists:reverse(ChanPids),
|
||||||
?LOG(error, "More than one channel found: ~p", [ChanPids]),
|
?SLOG(warning, #{msg => "more_than_one_channel_found", chan_pids => ChanPids}),
|
||||||
lists:foreach(fun(StalePid) ->
|
lists:foreach(fun(StalePid) ->
|
||||||
catch discard_session(ClientId, StalePid)
|
catch discard_session(ClientId, StalePid)
|
||||||
end, StalePids),
|
end, StalePids),
|
||||||
|
|
@ -341,7 +340,7 @@ kick_session(ClientId) ->
|
||||||
kick_session(ClientId, ChanPid);
|
kick_session(ClientId, ChanPid);
|
||||||
ChanPids ->
|
ChanPids ->
|
||||||
[ChanPid|StalePids] = lists:reverse(ChanPids),
|
[ChanPid|StalePids] = lists:reverse(ChanPids),
|
||||||
?LOG(error, "More than one channel found: ~p", [ChanPids]),
|
?SLOG(warning, #{msg => "more_than_one_channel_found", chan_pids => ChanPids}),
|
||||||
lists:foreach(fun(StalePid) ->
|
lists:foreach(fun(StalePid) ->
|
||||||
catch discard_session(ClientId, StalePid)
|
catch discard_session(ClientId, StalePid)
|
||||||
end, StalePids),
|
end, StalePids),
|
||||||
|
|
@ -409,14 +408,14 @@ cast(Msg) -> gen_server:cast(?CM, Msg).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
TabOpts = [public, {write_concurrency, true}],
|
TabOpts = [public, {write_concurrency, true}],
|
||||||
ok = emqx_tables:new(?CHAN_TAB, [bag, {read_concurrency, true}|TabOpts]),
|
ok = emqx_tables:new(?CHAN_TAB, [bag, {read_concurrency, true} | TabOpts]),
|
||||||
ok = emqx_tables:new(?CHAN_CONN_TAB, [bag | TabOpts]),
|
ok = emqx_tables:new(?CHAN_CONN_TAB, [bag | TabOpts]),
|
||||||
ok = emqx_tables:new(?CHAN_INFO_TAB, [set, compressed | TabOpts]),
|
ok = emqx_tables:new(?CHAN_INFO_TAB, [set, compressed | TabOpts]),
|
||||||
ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0),
|
ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0),
|
||||||
{ok, #{chan_pmon => emqx_pmon:new()}}.
|
{ok, #{chan_pmon => emqx_pmon:new()}}.
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast({registered, {ClientId, ChanPid}}, State = #{chan_pmon := PMon}) ->
|
handle_cast({registered, {ClientId, ChanPid}}, State = #{chan_pmon := PMon}) ->
|
||||||
|
|
@ -424,7 +423,7 @@ handle_cast({registered, {ClientId, ChanPid}}, State = #{chan_pmon := PMon}) ->
|
||||||
{noreply, State#{chan_pmon := PMon1}};
|
{noreply, State#{chan_pmon := PMon1}};
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) ->
|
handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) ->
|
||||||
|
|
@ -434,7 +433,8 @@ handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}
|
||||||
{noreply, State#{chan_pmon := PMon1}};
|
{noreply, State#{chan_pmon := PMon1}};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
|
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,11 @@ init([]) ->
|
||||||
{ok, #{}}.
|
{ok, #{}}.
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({membership, {mnesia, down, Node}}, State) ->
|
handle_info({membership, {mnesia, down, Node}}, State) ->
|
||||||
|
|
@ -132,7 +132,7 @@ handle_info({membership, _Event}, State) ->
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@
|
||||||
, find_listener_conf/3
|
, find_listener_conf/3
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-include("logger.hrl").
|
||||||
|
|
||||||
-define(CONF, conf).
|
-define(CONF, conf).
|
||||||
-define(RAW_CONF, raw_conf).
|
-define(RAW_CONF, raw_conf).
|
||||||
-define(PERSIS_SCHEMA_MODS, {?MODULE, schema_mods}).
|
-define(PERSIS_SCHEMA_MODS, {?MODULE, schema_mods}).
|
||||||
|
|
@ -250,7 +252,7 @@ init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) ->
|
||||||
{ok, RawRichConf} ->
|
{ok, RawRichConf} ->
|
||||||
init_load(SchemaMod, RawRichConf);
|
init_load(SchemaMod, RawRichConf);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:error(#{msg => failed_to_load_hocon_conf,
|
?SLOG(error, #{msg => failed_to_load_hocon_conf,
|
||||||
reason => Reason
|
reason => Reason
|
||||||
}),
|
}),
|
||||||
error(failed_to_load_hocon_conf)
|
error(failed_to_load_hocon_conf)
|
||||||
|
|
@ -294,7 +296,8 @@ fill_defaults(RawConf) ->
|
||||||
-spec fill_defaults(module(), raw_config()) -> map().
|
-spec fill_defaults(module(), raw_config()) -> map().
|
||||||
fill_defaults(SchemaMod, RawConf) ->
|
fill_defaults(SchemaMod, RawConf) ->
|
||||||
hocon_schema:check_plain(SchemaMod, RawConf,
|
hocon_schema:check_plain(SchemaMod, RawConf,
|
||||||
#{nullable => true, no_conversion => true}, root_names_from_conf(RawConf)).
|
#{nullable => true, only_fill_defaults => true},
|
||||||
|
root_names_from_conf(RawConf)).
|
||||||
|
|
||||||
-spec read_override_conf() -> raw_config().
|
-spec read_override_conf() -> raw_config().
|
||||||
read_override_conf() ->
|
read_override_conf() ->
|
||||||
|
|
@ -358,7 +361,9 @@ save_to_override_conf(RawConf) ->
|
||||||
case file:write_file(FileName, hocon_pp:do(RawConf, #{})) of
|
case file:write_file(FileName, hocon_pp:do(RawConf, #{})) of
|
||||||
ok -> ok;
|
ok -> ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:error("write to ~s failed, ~p", [FileName, Reason]),
|
?SLOG(error, #{msg => failed_to_write_override_file,
|
||||||
|
filename => FileName,
|
||||||
|
reason => Reason}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -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) ->
|
||||||
|
|
@ -117,7 +117,12 @@ handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From,
|
||||||
{error, Result}
|
{error, Result}
|
||||||
end
|
end
|
||||||
catch Error:Reason:ST ->
|
catch Error:Reason:ST ->
|
||||||
?LOG(error, "change_config failed: ~p", [{Error, Reason, ST}]),
|
?SLOG(error, #{
|
||||||
|
msg => "change_config_failed",
|
||||||
|
exception => Error,
|
||||||
|
reason => Reason,
|
||||||
|
stacktrace => ST
|
||||||
|
}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end,
|
end,
|
||||||
{reply, Reply, State};
|
{reply, Reply, State};
|
||||||
|
|
|
||||||
|
|
@ -417,14 +417,14 @@ handle_msg({'$gen_cast', Req}, State) ->
|
||||||
{ok, NewState};
|
{ok, NewState};
|
||||||
|
|
||||||
handle_msg({Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl ->
|
handle_msg({Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl ->
|
||||||
?LOG(debug, "RECV ~0p", [Data]),
|
?SLOG(debug, #{msg => "RECV_data", data => Data, transport => Inet}),
|
||||||
Oct = iolist_size(Data),
|
Oct = iolist_size(Data),
|
||||||
inc_counter(incoming_bytes, Oct),
|
inc_counter(incoming_bytes, Oct),
|
||||||
ok = emqx_metrics:inc('bytes.received', Oct),
|
ok = emqx_metrics:inc('bytes.received', Oct),
|
||||||
parse_incoming(Data, State);
|
parse_incoming(Data, State);
|
||||||
|
|
||||||
handle_msg({quic, Data, _Sock, _, _, _}, State) ->
|
handle_msg({quic, Data, _Sock, _, _, _}, State) ->
|
||||||
?LOG(debug, "RECV ~0p", [Data]),
|
?SLOG(debug, #{msg => "RECV_data", data => Data, transport => quic}),
|
||||||
Oct = iolist_size(Data),
|
Oct = iolist_size(Data),
|
||||||
inc_counter(incoming_bytes, Oct),
|
inc_counter(incoming_bytes, Oct),
|
||||||
ok = emqx_metrics:inc('bytes.received', Oct),
|
ok = emqx_metrics:inc('bytes.received', Oct),
|
||||||
|
|
@ -489,7 +489,7 @@ handle_msg({connack, ConnAck}, State) ->
|
||||||
handle_outgoing(ConnAck, State);
|
handle_outgoing(ConnAck, State);
|
||||||
|
|
||||||
handle_msg({close, Reason}, State) ->
|
handle_msg({close, Reason}, State) ->
|
||||||
?LOG(debug, "Force to close the socket due to ~p", [Reason]),
|
?SLOG(debug, #{msg => "force_socket_close", reason => Reason}),
|
||||||
handle_info({sock_closed, Reason}, close_socket(State));
|
handle_info({sock_closed, Reason}, close_socket(State));
|
||||||
|
|
||||||
handle_msg({event, connected}, State = #state{channel = Channel}) ->
|
handle_msg({event, connected}, State = #state{channel = Channel}) ->
|
||||||
|
|
@ -644,10 +644,21 @@ parse_incoming(Data, Packets, State = #state{parse_state = ParseState}) ->
|
||||||
NState = State#state{parse_state = NParseState},
|
NState = State#state{parse_state = NParseState},
|
||||||
parse_incoming(Rest, [Packet|Packets], NState)
|
parse_incoming(Rest, [Packet|Packets], NState)
|
||||||
catch
|
catch
|
||||||
error:Reason:Stk ->
|
throw : ?FRAME_PARSE_ERROR(Reason) ->
|
||||||
?LOG(error, "~nParse failed for ~0p~n~0p~nFrame data:~0p",
|
?SLOG(info, #{ reason => Reason
|
||||||
[Reason, Stk, Data]),
|
, at_state => emqx_frame:describe_state(ParseState)
|
||||||
{[{frame_error, Reason}|Packets], State}
|
, input_bytes => Data
|
||||||
|
, parsed_packets => Packets
|
||||||
|
}),
|
||||||
|
{[{frame_error, Reason} | Packets], State};
|
||||||
|
error : Reason : Stacktrace ->
|
||||||
|
?SLOG(error, #{ at_state => emqx_frame:describe_state(ParseState)
|
||||||
|
, input_bytes => Data
|
||||||
|
, parsed_packets => Packets
|
||||||
|
, reason => Reason
|
||||||
|
, stacktrace => Stacktrace
|
||||||
|
}),
|
||||||
|
{[{frame_error, Reason} | Packets], State}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-compile({inline, [next_incoming_msgs/1]}).
|
-compile({inline, [next_incoming_msgs/1]}).
|
||||||
|
|
@ -661,7 +672,7 @@ next_incoming_msgs(Packets) ->
|
||||||
|
|
||||||
handle_incoming(Packet, State) when is_record(Packet, mqtt_packet) ->
|
handle_incoming(Packet, State) when is_record(Packet, mqtt_packet) ->
|
||||||
ok = inc_incoming_stats(Packet),
|
ok = inc_incoming_stats(Packet),
|
||||||
?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]),
|
?SLOG(debug, #{msg => "RECV_packet", packet => Packet}),
|
||||||
with_channel(handle_in, [Packet], State);
|
with_channel(handle_in, [Packet], State);
|
||||||
|
|
||||||
handle_incoming(FrameError, State) ->
|
handle_incoming(FrameError, State) ->
|
||||||
|
|
@ -696,15 +707,32 @@ handle_outgoing(Packet, State) ->
|
||||||
|
|
||||||
serialize_and_inc_stats_fun(#state{serialize = Serialize}) ->
|
serialize_and_inc_stats_fun(#state{serialize = Serialize}) ->
|
||||||
fun(Packet) ->
|
fun(Packet) ->
|
||||||
case emqx_frame:serialize_pkt(Packet, Serialize) of
|
try emqx_frame:serialize_pkt(Packet, Serialize) of
|
||||||
<<>> -> ?LOG(warning, "~s is discarded due to the frame is too large!",
|
<<>> -> ?SLOG(warning, #{
|
||||||
[emqx_packet:format(Packet)]),
|
msg => "packet_is_discarded",
|
||||||
|
reason => "frame_is_too_large",
|
||||||
|
packet => emqx_packet:format(Packet)
|
||||||
|
}),
|
||||||
ok = emqx_metrics:inc('delivery.dropped.too_large'),
|
ok = emqx_metrics:inc('delivery.dropped.too_large'),
|
||||||
ok = emqx_metrics:inc('delivery.dropped'),
|
ok = emqx_metrics:inc('delivery.dropped'),
|
||||||
<<>>;
|
<<>>;
|
||||||
Data -> ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]),
|
Data -> ?SLOG(debug, #{
|
||||||
|
msg => "SEND_packet",
|
||||||
|
packet => emqx_packet:format(Packet)
|
||||||
|
}),
|
||||||
ok = inc_outgoing_stats(Packet),
|
ok = inc_outgoing_stats(Packet),
|
||||||
Data
|
Data
|
||||||
|
catch
|
||||||
|
%% Maybe Never happen.
|
||||||
|
throw : ?FRAME_SERIALIZE_ERROR(Reason) ->
|
||||||
|
?SLOG(info, #{ reason => Reason
|
||||||
|
, input_packet => Packet}),
|
||||||
|
erlang:error(?FRAME_SERIALIZE_ERROR(Reason));
|
||||||
|
error : Reason : Stacktrace ->
|
||||||
|
?SLOG(error, #{ input_packet => Packet
|
||||||
|
, exception => Reason
|
||||||
|
, stacktrace => Stacktrace}),
|
||||||
|
erlang:error(frame_serialize_error)
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -741,7 +769,7 @@ handle_info(activate_socket, State = #state{sockstate = OldSst}) ->
|
||||||
|
|
||||||
handle_info({sock_error, Reason}, State) ->
|
handle_info({sock_error, Reason}, State) ->
|
||||||
case Reason =/= closed andalso Reason =/= einval of
|
case Reason =/= closed andalso Reason =/= einval of
|
||||||
true -> ?LOG(warning, "socket_error: ~p", [Reason]);
|
true -> ?SLOG(warning, #{msg => "socket_error", reason => Reason});
|
||||||
false -> ok
|
false -> ok
|
||||||
end,
|
end,
|
||||||
handle_info({sock_closed, Reason}, close_socket(State));
|
handle_info({sock_closed, Reason}, close_socket(State));
|
||||||
|
|
@ -783,7 +811,7 @@ ensure_rate_limit(Stats, State = #state{limiter = Limiter}) ->
|
||||||
{ok, Limiter1} ->
|
{ok, Limiter1} ->
|
||||||
State#state{limiter = Limiter1};
|
State#state{limiter = Limiter1};
|
||||||
{pause, Time, Limiter1} ->
|
{pause, Time, Limiter1} ->
|
||||||
?LOG(warning, "Pause ~pms due to rate limit", [Time]),
|
?SLOG(warning, #{msg => "pause_time_due_to_rate_limit", time_in_ms => Time}),
|
||||||
TRef = start_timer(Time, limit_timeout),
|
TRef = start_timer(Time, limit_timeout),
|
||||||
State#state{sockstate = blocked,
|
State#state{sockstate = blocked,
|
||||||
limiter = Limiter1,
|
limiter = Limiter1,
|
||||||
|
|
|
||||||
|
|
@ -185,13 +185,13 @@ handle_call({register_command, Cmd, MF, Opts}, _From, State = #state{seq = Seq})
|
||||||
case ets:match(?CMD_TAB, {{'$1', Cmd}, '_', '_'}) of
|
case ets:match(?CMD_TAB, {{'$1', Cmd}, '_', '_'}) of
|
||||||
[] -> ets:insert(?CMD_TAB, {{Seq, Cmd}, MF, Opts});
|
[] -> ets:insert(?CMD_TAB, {{Seq, Cmd}, MF, Opts});
|
||||||
[[OriginSeq] | _] ->
|
[[OriginSeq] | _] ->
|
||||||
?LOG(warning, "CMD ~s is overidden by ~p", [Cmd, MF]),
|
?SLOG(warning, #{msg => "CMD_overidden", cmd => Cmd, mf => MF}),
|
||||||
true = ets:insert(?CMD_TAB, {{OriginSeq, Cmd}, MF, Opts})
|
true = ets:insert(?CMD_TAB, {{OriginSeq, Cmd}, MF, Opts})
|
||||||
end,
|
end,
|
||||||
{reply, ok, next_seq(State)};
|
{reply, ok, next_seq(State)};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast({unregister_command, Cmd}, State) ->
|
handle_cast({unregister_command, Cmd}, State) ->
|
||||||
|
|
@ -199,11 +199,11 @@ handle_cast({unregister_command, Cmd}, State) ->
|
||||||
noreply(State);
|
noreply(State);
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
noreply(State).
|
noreply(State).
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
noreply(State).
|
noreply(State).
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ init([]) ->
|
||||||
{ok, #{}, hibernate}.
|
{ok, #{}, hibernate}.
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast({detected, #flapping{clientid = ClientId,
|
handle_cast({detected, #flapping{clientid = ClientId,
|
||||||
|
|
@ -116,8 +116,13 @@ handle_cast({detected, #flapping{clientid = ClientId,
|
||||||
#{window_time := WindTime, ban_time := Interval}}, State) ->
|
#{window_time := WindTime, ban_time := Interval}}, State) ->
|
||||||
case now_diff(StartedAt) < WindTime of
|
case now_diff(StartedAt) < WindTime of
|
||||||
true -> %% Flapping happened:(
|
true -> %% Flapping happened:(
|
||||||
?LOG(error, "Flapping detected: ~s(~s) disconnected ~w times in ~wms",
|
?SLOG(warning, #{
|
||||||
[ClientId, inet:ntoa(PeerHost), DetectCnt, WindTime]),
|
msg => "flapping_detected",
|
||||||
|
client_id => ClientId,
|
||||||
|
peer_host => fmt_host(PeerHost),
|
||||||
|
detect_cnt => DetectCnt,
|
||||||
|
wind_time_in_ms => WindTime
|
||||||
|
}),
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Banned = #banned{who = {clientid, ClientId},
|
Banned = #banned{who = {clientid, ClientId},
|
||||||
by = <<"flapping detector">>,
|
by = <<"flapping detector">>,
|
||||||
|
|
@ -126,13 +131,18 @@ handle_cast({detected, #flapping{clientid = ClientId,
|
||||||
until = Now + (Interval div 1000)},
|
until = Now + (Interval div 1000)},
|
||||||
emqx_banned:create(Banned);
|
emqx_banned:create(Banned);
|
||||||
false ->
|
false ->
|
||||||
?LOG(warning, "~s(~s) disconnected ~w times in ~wms",
|
?SLOG(warning, #{
|
||||||
[ClientId, inet:ntoa(PeerHost), DetectCnt, Interval])
|
msg => "client_disconnected",
|
||||||
|
client_id => ClientId,
|
||||||
|
peer_host => fmt_host(PeerHost),
|
||||||
|
detect_cnt => DetectCnt,
|
||||||
|
interval => Interval
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({timeout, _TRef, {garbage_collect, Zone}}, State) ->
|
handle_info({timeout, _TRef, {garbage_collect, Zone}}, State) ->
|
||||||
|
|
@ -144,7 +154,7 @@ handle_info({timeout, _TRef, {garbage_collect, Zone}}, State) ->
|
||||||
{noreply, State, hibernate};
|
{noreply, State, hibernate};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
@ -161,3 +171,8 @@ start_timers() ->
|
||||||
lists:foreach(fun({Zone, _ZoneConf}) ->
|
lists:foreach(fun({Zone, _ZoneConf}) ->
|
||||||
start_timer(Zone)
|
start_timer(Zone)
|
||||||
end, maps:to_list(emqx:get_config([zones], #{}))).
|
end, maps:to_list(emqx:get_config([zones], #{}))).
|
||||||
|
|
||||||
|
fmt_host(PeerHost) ->
|
||||||
|
try inet:ntoa(PeerHost)
|
||||||
|
catch _:_ -> PeerHost
|
||||||
|
end.
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@
|
||||||
, serialize/2
|
, serialize/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
|
||||||
|
-export([ describe_state/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export_type([ options/0
|
-export_type([ options/0
|
||||||
, parse_state/0
|
, parse_state/0
|
||||||
, parse_result/0
|
, parse_result/0
|
||||||
|
|
@ -47,7 +51,9 @@
|
||||||
version => emqx_types:proto_ver()
|
version => emqx_types:proto_ver()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-type(parse_state() :: {none, options()} | {cont_state(), options()}).
|
-define(NONE(Options), {none, Options}).
|
||||||
|
|
||||||
|
-type(parse_state() :: ?NONE(options()) | {cont_state(), options()}).
|
||||||
|
|
||||||
-type(parse_result() :: {more, parse_state()}
|
-type(parse_result() :: {more, parse_state()}
|
||||||
| {ok, emqx_types:packet(), binary(), parse_state()}).
|
| {ok, emqx_types:packet(), binary(), parse_state()}).
|
||||||
|
|
@ -61,27 +67,45 @@
|
||||||
|
|
||||||
-type(serialize_opts() :: options()).
|
-type(serialize_opts() :: options()).
|
||||||
|
|
||||||
-define(none(Options), {none, Options}).
|
|
||||||
|
|
||||||
-define(DEFAULT_OPTIONS,
|
-define(DEFAULT_OPTIONS,
|
||||||
#{strict_mode => false,
|
#{strict_mode => false,
|
||||||
max_size => ?MAX_PACKET_SIZE,
|
max_size => ?MAX_PACKET_SIZE,
|
||||||
version => ?MQTT_PROTO_V4
|
version => ?MQTT_PROTO_V4
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-define(PARSE_ERR(Reason), ?THROW_FRAME_ERROR(Reason)).
|
||||||
|
-define(SERIALIZE_ERR(Reason), ?THROW_SERIALIZE_ERROR(Reason)).
|
||||||
|
|
||||||
|
-define(MULTIPLIER_MAX, 16#200000).
|
||||||
|
|
||||||
-dialyzer({no_match, [serialize_utf8_string/2]}).
|
-dialyzer({no_match, [serialize_utf8_string/2]}).
|
||||||
|
|
||||||
|
%% @doc Describe state for logging.
|
||||||
|
describe_state(?NONE(_Opts)) -> <<"clean">>;
|
||||||
|
describe_state({{len, _}, _Opts}) -> <<"parsing_varint_length">>;
|
||||||
|
describe_state({{body, State}, _Opts}) ->
|
||||||
|
#{ hdr := Hdr
|
||||||
|
, len := Len
|
||||||
|
} = State,
|
||||||
|
Desc = #{ parsed_header => Hdr
|
||||||
|
, expected_bytes => Len
|
||||||
|
},
|
||||||
|
case maps:get(rest, State, undefined) of
|
||||||
|
undefined -> Desc;
|
||||||
|
Body -> Desc#{received_bytes => body_bytes(Body)}
|
||||||
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Init Parse State
|
%% Init Parse State
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-spec(initial_parse_state() -> {none, options()}).
|
-spec(initial_parse_state() -> ?NONE(options())).
|
||||||
initial_parse_state() ->
|
initial_parse_state() ->
|
||||||
initial_parse_state(#{}).
|
initial_parse_state(#{}).
|
||||||
|
|
||||||
-spec(initial_parse_state(options()) -> {none, options()}).
|
-spec(initial_parse_state(options()) -> ?NONE(options())).
|
||||||
initial_parse_state(Options) when is_map(Options) ->
|
initial_parse_state(Options) when is_map(Options) ->
|
||||||
?none(maps:merge(?DEFAULT_OPTIONS, Options)).
|
?NONE(maps:merge(?DEFAULT_OPTIONS, Options)).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Parse MQTT Frame
|
%% Parse MQTT Frame
|
||||||
|
|
@ -92,10 +116,10 @@ parse(Bin) ->
|
||||||
parse(Bin, initial_parse_state()).
|
parse(Bin, initial_parse_state()).
|
||||||
|
|
||||||
-spec(parse(binary(), parse_state()) -> parse_result()).
|
-spec(parse(binary(), parse_state()) -> parse_result()).
|
||||||
parse(<<>>, {none, Options}) ->
|
parse(<<>>, ?NONE(Options)) ->
|
||||||
{more, {none, Options}};
|
{more, ?NONE(Options)};
|
||||||
parse(<<Type:4, Dup:1, QoS:2, Retain:1, Rest/binary>>,
|
parse(<<Type:4, Dup:1, QoS:2, Retain:1, Rest/binary>>,
|
||||||
{none, Options = #{strict_mode := StrictMode}}) ->
|
?NONE(Options = #{strict_mode := StrictMode})) ->
|
||||||
%% Validate header if strict mode.
|
%% Validate header if strict mode.
|
||||||
StrictMode andalso validate_header(Type, Dup, QoS, Retain),
|
StrictMode andalso validate_header(Type, Dup, QoS, Retain),
|
||||||
Header = #mqtt_packet_header{type = Type,
|
Header = #mqtt_packet_header{type = Type,
|
||||||
|
|
@ -123,14 +147,14 @@ parse_remaining_len(Rest, Header, Options) ->
|
||||||
|
|
||||||
parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{max_size := MaxSize})
|
parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{max_size := MaxSize})
|
||||||
when Length > MaxSize ->
|
when Length > MaxSize ->
|
||||||
error(frame_too_large);
|
?PARSE_ERR(frame_too_large);
|
||||||
parse_remaining_len(<<>>, Header, Multiplier, Length, Options) ->
|
parse_remaining_len(<<>>, Header, Multiplier, Length, Options) ->
|
||||||
{more, {{len, #{hdr => Header, len => {Multiplier, Length}}}, Options}};
|
{more, {{len, #{hdr => Header, len => {Multiplier, Length}}}, Options}};
|
||||||
%% Match DISCONNECT without payload
|
%% Match DISCONNECT without payload
|
||||||
parse_remaining_len(<<0:8, Rest/binary>>,
|
parse_remaining_len(<<0:8, Rest/binary>>,
|
||||||
Header = #mqtt_packet_header{type = ?DISCONNECT}, 1, 0, Options) ->
|
Header = #mqtt_packet_header{type = ?DISCONNECT}, 1, 0, Options) ->
|
||||||
Packet = packet(Header, #mqtt_packet_disconnect{reason_code = ?RC_SUCCESS}),
|
Packet = packet(Header, #mqtt_packet_disconnect{reason_code = ?RC_SUCCESS}),
|
||||||
{ok, Packet, Rest, ?none(Options)};
|
{ok, Packet, Rest, ?NONE(Options)};
|
||||||
%% Match PINGREQ.
|
%% Match PINGREQ.
|
||||||
parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) ->
|
parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) ->
|
||||||
parse_frame(Rest, Header, 0, Options);
|
parse_frame(Rest, Header, 0, Options);
|
||||||
|
|
@ -138,21 +162,22 @@ parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) ->
|
||||||
parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, Options) ->
|
parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, Options) ->
|
||||||
parse_frame(Rest, Header, 2, Options);
|
parse_frame(Rest, Header, 2, Options);
|
||||||
parse_remaining_len(<<1:1, _Len:7, _Rest/binary>>, _Header, Multiplier, _Value, _Options)
|
parse_remaining_len(<<1:1, _Len:7, _Rest/binary>>, _Header, Multiplier, _Value, _Options)
|
||||||
when Multiplier > 2097152 ->
|
when Multiplier > ?MULTIPLIER_MAX ->
|
||||||
error(malformed_variable_byte_integer);
|
?PARSE_ERR(malformed_variable_byte_integer);
|
||||||
parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Options) ->
|
parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Options) ->
|
||||||
parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Options);
|
parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Options);
|
||||||
parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value,
|
parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value,
|
||||||
Options = #{max_size := MaxSize}) ->
|
Options = #{max_size := MaxSize}) ->
|
||||||
FrameLen = Value + Len * Multiplier,
|
FrameLen = Value + Len * Multiplier,
|
||||||
case FrameLen > MaxSize of
|
case FrameLen > MaxSize of
|
||||||
true -> error(frame_too_large);
|
true -> ?PARSE_ERR(frame_too_large);
|
||||||
false -> parse_frame(Rest, Header, FrameLen, Options)
|
false -> parse_frame(Rest, Header, FrameLen, Options)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
body_bytes(B) when is_binary(B) -> size(B);
|
body_bytes(B) when is_binary(B) -> size(B);
|
||||||
body_bytes(?Q(Bytes, _)) -> Bytes.
|
body_bytes(?Q(Bytes, _)) -> Bytes.
|
||||||
|
|
||||||
|
append_body(H, <<>>) -> H;
|
||||||
append_body(H, T) when is_binary(H) andalso size(H) < 1024 ->
|
append_body(H, T) when is_binary(H) andalso size(H) < 1024 ->
|
||||||
<<H/binary, T/binary>>;
|
<<H/binary, T/binary>>;
|
||||||
append_body(H, T) when is_binary(H) ->
|
append_body(H, T) when is_binary(H) ->
|
||||||
|
|
@ -165,18 +190,18 @@ flatten_body(Body) when is_binary(Body) -> Body;
|
||||||
flatten_body(?Q(_, Q)) -> iolist_to_binary(queue:to_list(Q)).
|
flatten_body(?Q(_, Q)) -> iolist_to_binary(queue:to_list(Q)).
|
||||||
|
|
||||||
parse_frame(Body, Header, 0, Options) ->
|
parse_frame(Body, Header, 0, Options) ->
|
||||||
{ok, packet(Header), flatten_body(Body), ?none(Options)};
|
{ok, packet(Header), flatten_body(Body), ?NONE(Options)};
|
||||||
parse_frame(Body, Header, Length, Options) ->
|
parse_frame(Body, Header, Length, Options) ->
|
||||||
case body_bytes(Body) >= Length of
|
case body_bytes(Body) >= Length of
|
||||||
true ->
|
true ->
|
||||||
<<FrameBin:Length/binary, Rest/binary>> = flatten_body(Body),
|
<<FrameBin:Length/binary, Rest/binary>> = flatten_body(Body),
|
||||||
case parse_packet(Header, FrameBin, Options) of
|
case parse_packet(Header, FrameBin, Options) of
|
||||||
{Variable, Payload} ->
|
{Variable, Payload} ->
|
||||||
{ok, packet(Header, Variable, Payload), Rest, ?none(Options)};
|
{ok, packet(Header, Variable, Payload), Rest, ?NONE(Options)};
|
||||||
Variable = #mqtt_packet_connect{proto_ver = Ver} ->
|
Variable = #mqtt_packet_connect{proto_ver = Ver} ->
|
||||||
{ok, packet(Header, Variable), Rest, ?none(Options#{version := Ver})};
|
{ok, packet(Header, Variable), Rest, ?NONE(Options#{version := Ver})};
|
||||||
Variable ->
|
Variable ->
|
||||||
{ok, packet(Header, Variable), Rest, ?none(Options)}
|
{ok, packet(Header, Variable), Rest, ?NONE(Options)}
|
||||||
end;
|
end;
|
||||||
false ->
|
false ->
|
||||||
{more, {{body, #{hdr => Header,
|
{more, {{body, #{hdr => Header,
|
||||||
|
|
@ -420,10 +445,16 @@ parse_property(<<16#28, Val, Bin/binary>>, Props) ->
|
||||||
parse_property(<<16#29, Val, Bin/binary>>, Props) ->
|
parse_property(<<16#29, Val, Bin/binary>>, Props) ->
|
||||||
parse_property(Bin, Props#{'Subscription-Identifier-Available' => Val});
|
parse_property(Bin, Props#{'Subscription-Identifier-Available' => Val});
|
||||||
parse_property(<<16#2A, Val, Bin/binary>>, Props) ->
|
parse_property(<<16#2A, Val, Bin/binary>>, Props) ->
|
||||||
parse_property(Bin, Props#{'Shared-Subscription-Available' => Val}).
|
parse_property(Bin, Props#{'Shared-Subscription-Available' => Val});
|
||||||
|
parse_property(<<Property:8, _Rest/binary>>, _Props) ->
|
||||||
|
?PARSE_ERR(#{invalid_property_code => Property}).
|
||||||
|
%% TODO: invalid property in specific packet.
|
||||||
|
|
||||||
parse_variable_byte_integer(Bin) ->
|
parse_variable_byte_integer(Bin) ->
|
||||||
parse_variable_byte_integer(Bin, 1, 0).
|
parse_variable_byte_integer(Bin, 1, 0).
|
||||||
|
parse_variable_byte_integer(<<1:1, _Len:7, _Rest/binary>>, Multiplier, _Value)
|
||||||
|
when Multiplier > ?MULTIPLIER_MAX ->
|
||||||
|
?PARSE_ERR(malformed_variable_byte_integer);
|
||||||
parse_variable_byte_integer(<<1:1, Len:7, Rest/binary>>, Multiplier, Value) ->
|
parse_variable_byte_integer(<<1:1, Len:7, Rest/binary>>, Multiplier, Value) ->
|
||||||
parse_variable_byte_integer(Rest, Multiplier * ?HIGHBIT, Value + Len * Multiplier);
|
parse_variable_byte_integer(Rest, Multiplier * ?HIGHBIT, Value + Len * Multiplier);
|
||||||
parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) ->
|
parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) ->
|
||||||
|
|
@ -441,7 +472,23 @@ parse_reason_codes(Bin) ->
|
||||||
|
|
||||||
parse_utf8_pair(<<Len1:16/big, Key:Len1/binary,
|
parse_utf8_pair(<<Len1:16/big, Key:Len1/binary,
|
||||||
Len2:16/big, Val:Len2/binary, Rest/binary>>) ->
|
Len2:16/big, Val:Len2/binary, Rest/binary>>) ->
|
||||||
{{Key, Val}, Rest}.
|
{{Key, Val}, Rest};
|
||||||
|
parse_utf8_pair(<<LenK:16/big, Rest/binary>>)
|
||||||
|
when LenK > byte_size(Rest) ->
|
||||||
|
?PARSE_ERR(#{ hint => user_property_not_enough_bytes
|
||||||
|
, parsed_key_length => LenK
|
||||||
|
, remaining_bytes_length => byte_size(Rest)});
|
||||||
|
parse_utf8_pair(<<LenK:16/big, _Key:LenK/binary, %% key maybe malformed
|
||||||
|
LenV:16/big, Rest/binary>>)
|
||||||
|
when LenV > byte_size(Rest) ->
|
||||||
|
?PARSE_ERR(#{ hint => malformed_user_property_value
|
||||||
|
, parsed_key_length => LenK
|
||||||
|
, parsed_value_length => LenV
|
||||||
|
, remaining_bytes_length => byte_size(Rest)});
|
||||||
|
parse_utf8_pair(Bin)
|
||||||
|
when 4 > byte_size(Bin) ->
|
||||||
|
?PARSE_ERR(#{ hint => user_property_not_enough_bytes
|
||||||
|
, total_bytes => byte_size(Bin)}).
|
||||||
|
|
||||||
parse_utf8_string(Bin, false) ->
|
parse_utf8_string(Bin, false) ->
|
||||||
{undefined, Bin};
|
{undefined, Bin};
|
||||||
|
|
@ -449,10 +496,26 @@ parse_utf8_string(Bin, true) ->
|
||||||
parse_utf8_string(Bin).
|
parse_utf8_string(Bin).
|
||||||
|
|
||||||
parse_utf8_string(<<Len:16/big, Str:Len/binary, Rest/binary>>) ->
|
parse_utf8_string(<<Len:16/big, Str:Len/binary, Rest/binary>>) ->
|
||||||
{Str, Rest}.
|
{Str, Rest};
|
||||||
|
parse_utf8_string(<<Len:16/big, Rest/binary>>)
|
||||||
|
when Len > byte_size(Rest) ->
|
||||||
|
?PARSE_ERR(#{ hint => malformed_utf8_string
|
||||||
|
, parsed_length => Len
|
||||||
|
, remaining_bytes_length => byte_size(Rest)});
|
||||||
|
parse_utf8_string(Bin)
|
||||||
|
when 2 > byte_size(Bin) ->
|
||||||
|
?PARSE_ERR(malformed_utf8_string_length).
|
||||||
|
|
||||||
parse_binary_data(<<Len:16/big, Data:Len/binary, Rest/binary>>) ->
|
parse_binary_data(<<Len:16/big, Data:Len/binary, Rest/binary>>) ->
|
||||||
{Data, Rest}.
|
{Data, Rest};
|
||||||
|
parse_binary_data(<<Len:16/big, Rest/binary>>)
|
||||||
|
when Len > byte_size(Rest) ->
|
||||||
|
?PARSE_ERR(#{ hint => malformed_binary_data
|
||||||
|
, parsed_length => Len
|
||||||
|
, remaining_bytes_length => byte_size(Rest)});
|
||||||
|
parse_binary_data(Bin)
|
||||||
|
when 2 > byte_size(Bin) ->
|
||||||
|
?PARSE_ERR(malformed_binary_data_length).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Serialize MQTT Packet
|
%% Serialize MQTT Packet
|
||||||
|
|
@ -719,7 +782,7 @@ serialize_binary_data(Bin) ->
|
||||||
[<<(byte_size(Bin)):16/big-unsigned-integer>>, Bin].
|
[<<(byte_size(Bin)):16/big-unsigned-integer>>, Bin].
|
||||||
|
|
||||||
serialize_utf8_string(undefined, false) ->
|
serialize_utf8_string(undefined, false) ->
|
||||||
error(utf8_string_undefined);
|
?SERIALIZE_ERR(utf8_string_undefined);
|
||||||
serialize_utf8_string(undefined, true) ->
|
serialize_utf8_string(undefined, true) ->
|
||||||
<<>>;
|
<<>>;
|
||||||
serialize_utf8_string(String, _AllowNull) ->
|
serialize_utf8_string(String, _AllowNull) ->
|
||||||
|
|
@ -767,13 +830,13 @@ validate_header(?PINGREQ, 0, 0, 0) -> ok;
|
||||||
validate_header(?PINGRESP, 0, 0, 0) -> ok;
|
validate_header(?PINGRESP, 0, 0, 0) -> ok;
|
||||||
validate_header(?DISCONNECT, 0, 0, 0) -> ok;
|
validate_header(?DISCONNECT, 0, 0, 0) -> ok;
|
||||||
validate_header(?AUTH, 0, 0, 0) -> ok;
|
validate_header(?AUTH, 0, 0, 0) -> ok;
|
||||||
validate_header(_Type, _Dup, _QoS, _Rt) -> error(bad_frame_header).
|
validate_header(_Type, _Dup, _QoS, _Rt) -> ?PARSE_ERR(bad_frame_header).
|
||||||
|
|
||||||
-compile({inline, [validate_packet_id/1]}).
|
-compile({inline, [validate_packet_id/1]}).
|
||||||
validate_packet_id(0) -> error(bad_packet_id);
|
validate_packet_id(0) -> ?PARSE_ERR(bad_packet_id);
|
||||||
validate_packet_id(_) -> ok.
|
validate_packet_id(_) -> ok.
|
||||||
|
|
||||||
validate_subqos([3|_]) -> error(bad_subqos);
|
validate_subqos([3|_]) -> ?PARSE_ERR(bad_subqos);
|
||||||
validate_subqos([_|T]) -> validate_subqos(T);
|
validate_subqos([_|T]) -> validate_subqos(T);
|
||||||
validate_subqos([]) -> ok.
|
validate_subqos([]) -> ok.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
||||||
|
|
@ -206,8 +206,13 @@ safe_execute({M, F, A}, Args) ->
|
||||||
Result -> Result
|
Result -> Result
|
||||||
catch
|
catch
|
||||||
Error:Reason:Stacktrace ->
|
Error:Reason:Stacktrace ->
|
||||||
?LOG(error, "Failed to execute ~0p: ~0p", [{M, F, A}, {Error, Reason, Stacktrace}]),
|
?SLOG(error, #{
|
||||||
ok
|
msg => "failed_to_execute",
|
||||||
|
exception => Error,
|
||||||
|
reason => Reason,
|
||||||
|
stacktrace => Stacktrace,
|
||||||
|
failed_call => {M, F, A}
|
||||||
|
})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc execute a function.
|
%% @doc execute a function.
|
||||||
|
|
@ -246,7 +251,7 @@ handle_call({put, HookPoint, Callback = #callback{action = {M, F, _}}}, _From, S
|
||||||
{reply, Reply, State};
|
{reply, Reply, State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", req => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast({del, HookPoint, Action}, State) ->
|
handle_cast({del, HookPoint, Action}, State) ->
|
||||||
|
|
@ -259,11 +264,11 @@ handle_cast({del, HookPoint, Action}, State) ->
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected msg: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", req => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
-export([post_config_update/4]).
|
-export([post_config_update/4]).
|
||||||
|
|
||||||
-define(CONF_KEY_PATH, [listeners]).
|
-define(CONF_KEY_PATH, [listeners]).
|
||||||
|
-define(TYPES_STRING, ["tcp","ssl","ws","wss","quic"]).
|
||||||
|
|
||||||
%% @doc List configured listeners.
|
%% @doc List configured listeners.
|
||||||
-spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]).
|
-spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]).
|
||||||
|
|
@ -349,11 +350,10 @@ listener_id(Type, ListenerName) ->
|
||||||
list_to_atom(lists:append([str(Type), ":", str(ListenerName)])).
|
list_to_atom(lists:append([str(Type), ":", str(ListenerName)])).
|
||||||
|
|
||||||
parse_listener_id(Id) ->
|
parse_listener_id(Id) ->
|
||||||
try
|
[Type, Name] = string:split(str(Id), ":", leading),
|
||||||
[Type, Name] = string:split(str(Id), ":", leading),
|
case lists:member(Type, ?TYPES_STRING) of
|
||||||
{list_to_existing_atom(Type), list_to_atom(Name)}
|
true -> {list_to_existing_atom(Type), list_to_atom(Name)};
|
||||||
catch
|
false -> {error, {invalid_listener_id, Id}}
|
||||||
_ : _ -> error({invalid_listener_id, Id})
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
zone(Opts) ->
|
zone(Opts) ->
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
|
|
||||||
-export([ to_packet/2
|
-export([ to_packet/2
|
||||||
, to_map/1
|
, to_map/1
|
||||||
|
, to_log_map/1
|
||||||
, to_list/1
|
, to_list/1
|
||||||
, from_map/1
|
, from_map/1
|
||||||
]).
|
]).
|
||||||
|
|
@ -79,11 +80,10 @@
|
||||||
headers := emqx_types:headers(),
|
headers := emqx_types:headers(),
|
||||||
topic := emqx_types:topic(),
|
topic := emqx_types:topic(),
|
||||||
payload := emqx_types:payload(),
|
payload := emqx_types:payload(),
|
||||||
timestamp := integer()}
|
timestamp := integer(),
|
||||||
|
extra := _}
|
||||||
).
|
).
|
||||||
|
|
||||||
-export([format/1]).
|
|
||||||
|
|
||||||
-elvis([{elvis_style, god_modules, disable}]).
|
-elvis([{elvis_style, god_modules, disable}]).
|
||||||
|
|
||||||
-spec(make(emqx_types:topic(), emqx_types:payload()) -> emqx_types:message()).
|
-spec(make(emqx_types:topic(), emqx_types:payload()) -> emqx_types:message()).
|
||||||
|
|
@ -292,7 +292,8 @@ to_map(#message{
|
||||||
headers = Headers,
|
headers = Headers,
|
||||||
topic = Topic,
|
topic = Topic,
|
||||||
payload = Payload,
|
payload = Payload,
|
||||||
timestamp = Timestamp
|
timestamp = Timestamp,
|
||||||
|
extra = Extra
|
||||||
}) ->
|
}) ->
|
||||||
#{id => Id,
|
#{id => Id,
|
||||||
qos => QoS,
|
qos => QoS,
|
||||||
|
|
@ -301,9 +302,13 @@ to_map(#message{
|
||||||
headers => Headers,
|
headers => Headers,
|
||||||
topic => Topic,
|
topic => Topic,
|
||||||
payload => Payload,
|
payload => Payload,
|
||||||
timestamp => Timestamp
|
timestamp => Timestamp,
|
||||||
|
extra => Extra
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
%% @doc To map for logging, with payload dropped.
|
||||||
|
to_log_map(Msg) -> maps:without([payload], to_map(Msg)).
|
||||||
|
|
||||||
%% @doc Message to tuple list
|
%% @doc Message to tuple list
|
||||||
-spec(to_list(emqx_types:message()) -> list()).
|
-spec(to_list(emqx_types:message()) -> list()).
|
||||||
to_list(Msg) ->
|
to_list(Msg) ->
|
||||||
|
|
@ -318,7 +323,8 @@ from_map(#{id := Id,
|
||||||
headers := Headers,
|
headers := Headers,
|
||||||
topic := Topic,
|
topic := Topic,
|
||||||
payload := Payload,
|
payload := Payload,
|
||||||
timestamp := Timestamp
|
timestamp := Timestamp,
|
||||||
|
extra := Extra
|
||||||
}) ->
|
}) ->
|
||||||
#message{
|
#message{
|
||||||
id = Id,
|
id = Id,
|
||||||
|
|
@ -328,24 +334,10 @@ from_map(#{id := Id,
|
||||||
headers = Headers,
|
headers = Headers,
|
||||||
topic = Topic,
|
topic = Topic,
|
||||||
payload = Payload,
|
payload = Payload,
|
||||||
timestamp = Timestamp
|
timestamp = Timestamp,
|
||||||
|
extra = Extra
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%% MilliSeconds
|
%% MilliSeconds
|
||||||
elapsed(Since) ->
|
elapsed(Since) ->
|
||||||
max(0, erlang:system_time(millisecond) - Since).
|
max(0, erlang:system_time(millisecond) - Since).
|
||||||
|
|
||||||
format(#message{id = Id,
|
|
||||||
qos = QoS,
|
|
||||||
topic = Topic,
|
|
||||||
from = From,
|
|
||||||
flags = Flags,
|
|
||||||
headers = Headers}) ->
|
|
||||||
io_lib:format("Message(Id=~s, QoS=~w, Topic=~s, From=~p, Flags=~s, Headers=~s)",
|
|
||||||
[Id, QoS, Topic, From, format(flags, Flags), format(headers, Headers)]).
|
|
||||||
|
|
||||||
format(flags, Flags) ->
|
|
||||||
io_lib:format("~p", [[Flag || {Flag, true} <- maps:to_list(Flags)]]);
|
|
||||||
format(headers, Headers) ->
|
|
||||||
io_lib:format("~p", [Headers]).
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -442,13 +442,17 @@ init([]) ->
|
||||||
{ok, #state{next_idx = ?RESERVED_IDX + 1}, hibernate}.
|
{ok, #state{next_idx = ?RESERVED_IDX + 1}, hibernate}.
|
||||||
|
|
||||||
handle_call({create, Type, Name}, _From, State = #state{next_idx = ?MAX_SIZE}) ->
|
handle_call({create, Type, Name}, _From, State = #state{next_idx = ?MAX_SIZE}) ->
|
||||||
?LOG(error, "Failed to create ~s:~s for index exceeded.", [Type, Name]),
|
?SLOG(error, #{
|
||||||
|
msg => "failed_to_create_type_name_for_index_exceeded",
|
||||||
|
type => Type,
|
||||||
|
name => Name
|
||||||
|
}),
|
||||||
{reply, {error, metric_index_exceeded}, State};
|
{reply, {error, metric_index_exceeded}, State};
|
||||||
|
|
||||||
handle_call({create, Type, Name}, _From, State = #state{next_idx = NextIdx}) ->
|
handle_call({create, Type, Name}, _From, State = #state{next_idx = NextIdx}) ->
|
||||||
case ets:lookup(?TAB, Name) of
|
case ets:lookup(?TAB, Name) of
|
||||||
[#metric{idx = Idx}] ->
|
[#metric{idx = Idx}] ->
|
||||||
?LOG(info, "~s already exists.", [Name]),
|
?SLOG(info, #{msg => "name_already_exists", name => Name}),
|
||||||
{reply, {ok, Idx}, State};
|
{reply, {ok, Idx}, State};
|
||||||
[] ->
|
[] ->
|
||||||
Metric = #metric{name = Name, type = Type, idx = NextIdx},
|
Metric = #metric{name = Name, type = Type, idx = NextIdx},
|
||||||
|
|
@ -464,15 +468,15 @@ handle_call({set_type_to_counter, Keys}, _From, State) ->
|
||||||
{reply, ok, State};
|
{reply, ok, State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", req => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", req => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ handle_call(Req, _From, State) ->
|
||||||
{reply, {error, {unexpected_call, Req}}, State}.
|
{reply, {error, {unexpected_call, Req}}, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "unexpected_cast_discarded: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast=> Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({timeout, _Timer, check}, State) ->
|
handle_info({timeout, _Timer, check}, State) ->
|
||||||
|
|
@ -109,7 +109,7 @@ handle_info({timeout, _Timer, check}, State) ->
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(info, "unexpected_info_discarded: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@
|
||||||
, check_pass/2
|
, check_pass/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-include("logger.hrl").
|
||||||
|
|
||||||
-type(hash_type() :: plain | md5 | sha | sha256 | pbkdf2 | bcrypt).
|
-type(hash_type() :: plain | md5 | sha | sha256 | pbkdf2 | bcrypt).
|
||||||
|
|
||||||
-export_type([hash_type/0]).
|
-export_type([hash_type/0]).
|
||||||
|
|
@ -67,8 +69,8 @@ hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) ->
|
||||||
case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of
|
case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of
|
||||||
{ok, Hexstring} ->
|
{ok, Hexstring} ->
|
||||||
pbkdf2:to_hex(Hexstring);
|
pbkdf2:to_hex(Hexstring);
|
||||||
{error, Error} ->
|
{error, Reason} ->
|
||||||
error_logger:error_msg("pbkdf2 hash error:~p", [Error]),
|
?SLOG(error, #{msg => "pbkdf2_hash_error", reason => Reason}),
|
||||||
<<>>
|
<<>>
|
||||||
end;
|
end;
|
||||||
hash(bcrypt, {Salt, Password}) ->
|
hash(bcrypt, {Salt, Password}) ->
|
||||||
|
|
@ -76,8 +78,8 @@ hash(bcrypt, {Salt, Password}) ->
|
||||||
case bcrypt:hashpw(Password, Salt) of
|
case bcrypt:hashpw(Password, Salt) of
|
||||||
{ok, HashPasswd} ->
|
{ok, HashPasswd} ->
|
||||||
list_to_binary(HashPasswd);
|
list_to_binary(HashPasswd);
|
||||||
{error, Error}->
|
{error, Reason}->
|
||||||
error_logger:error_msg("bcrypt hash error:~p", [Error]),
|
?SLOG(error, #{msg => "bcrypt_hash_error", reason => Reason}),
|
||||||
<<>>
|
<<>>
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@
|
||||||
, find_plugin/1
|
, find_plugin/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([funlog/2]).
|
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
|
|
@ -50,10 +48,14 @@ load() ->
|
||||||
load(PluginName) when is_atom(PluginName) ->
|
load(PluginName) when is_atom(PluginName) ->
|
||||||
case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of
|
case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of
|
||||||
{false, _} ->
|
{false, _} ->
|
||||||
?LOG(alert, "Plugin ~s not found, cannot load it", [PluginName]),
|
?SLOG(alert, #{msg => "failed_to_load_plugin",
|
||||||
|
plugin_name => PluginName,
|
||||||
|
reason => not_found}),
|
||||||
{error, not_found};
|
{error, not_found};
|
||||||
{_, true} ->
|
{_, true} ->
|
||||||
?LOG(notice, "Plugin ~s is already started", [PluginName]),
|
?SLOG(notice, #{msg => "plugin_already_loaded",
|
||||||
|
plugin_name => PluginName,
|
||||||
|
reason => already_loaded}),
|
||||||
{error, already_started};
|
{error, already_started};
|
||||||
{_, false} ->
|
{_, false} ->
|
||||||
load_plugin(PluginName)
|
load_plugin(PluginName)
|
||||||
|
|
@ -69,10 +71,14 @@ unload() ->
|
||||||
unload(PluginName) when is_atom(PluginName) ->
|
unload(PluginName) when is_atom(PluginName) ->
|
||||||
case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of
|
case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of
|
||||||
{false, _} ->
|
{false, _} ->
|
||||||
?LOG(error, "Plugin ~s is not found, cannot unload it", [PluginName]),
|
?SLOG(error, #{msg => "fialed_to_unload_plugin",
|
||||||
|
plugin_name => PluginName,
|
||||||
|
reason => not_found}),
|
||||||
{error, not_found};
|
{error, not_found};
|
||||||
{_, false} ->
|
{_, false} ->
|
||||||
?LOG(error, "Plugin ~s is not started", [PluginName]),
|
?SLOG(error, #{msg => "failed_to_unload_plugin",
|
||||||
|
plugin_name => PluginName,
|
||||||
|
reason => not_loaded}),
|
||||||
{error, not_started};
|
{error, not_started};
|
||||||
{_, _} ->
|
{_, _} ->
|
||||||
unload_plugin(PluginName)
|
unload_plugin(PluginName)
|
||||||
|
|
@ -81,7 +87,9 @@ unload(PluginName) when is_atom(PluginName) ->
|
||||||
reload(PluginName) when is_atom(PluginName)->
|
reload(PluginName) when is_atom(PluginName)->
|
||||||
case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of
|
case {lists:member(PluginName, names(plugin)), lists:member(PluginName, names(started_app))} of
|
||||||
{false, _} ->
|
{false, _} ->
|
||||||
?LOG(error, "Plugin ~s is not found, cannot reload it", [PluginName]),
|
?SLOG(error, #{msg => "failed_to_reload_plugin",
|
||||||
|
plugin_name => PluginName,
|
||||||
|
reason => not_found}),
|
||||||
{error, not_found};
|
{error, not_found};
|
||||||
{_, false} ->
|
{_, false} ->
|
||||||
load(PluginName);
|
load(PluginName);
|
||||||
|
|
@ -127,14 +135,14 @@ load_ext_plugins(Dir) ->
|
||||||
end, filelib:wildcard("*", Dir)).
|
end, filelib:wildcard("*", Dir)).
|
||||||
|
|
||||||
load_ext_plugin(PluginDir) ->
|
load_ext_plugin(PluginDir) ->
|
||||||
?LOG(debug, "loading_extra_plugin: ~s", [PluginDir]),
|
?SLOG(debug, #{msg => "loading_extra_plugin", plugin_dir => PluginDir}),
|
||||||
Ebin = filename:join([PluginDir, "ebin"]),
|
Ebin = filename:join([PluginDir, "ebin"]),
|
||||||
AppFile = filename:join([Ebin, "*.app"]),
|
AppFile = filename:join([Ebin, "*.app"]),
|
||||||
AppName = case filelib:wildcard(AppFile) of
|
AppName = case filelib:wildcard(AppFile) of
|
||||||
[App] ->
|
[App] ->
|
||||||
list_to_atom(filename:basename(App, ".app"));
|
list_to_atom(filename:basename(App, ".app"));
|
||||||
[] ->
|
[] ->
|
||||||
?LOG(alert, "plugin_app_file_not_found: ~s", [AppFile]),
|
?SLOG(alert, #{msg => "plugin_app_file_not_found", app_file => AppFile}),
|
||||||
error({plugin_app_file_not_found, AppFile})
|
error({plugin_app_file_not_found, AppFile})
|
||||||
end,
|
end,
|
||||||
ok = load_plugin_app(AppName, Ebin).
|
ok = load_plugin_app(AppName, Ebin).
|
||||||
|
|
@ -184,8 +192,14 @@ load_plugin(Name) ->
|
||||||
{error, Error0} ->
|
{error, Error0} ->
|
||||||
{error, Error0}
|
{error, Error0}
|
||||||
end
|
end
|
||||||
catch _ : Error : Stacktrace ->
|
catch Error : Reason : Stacktrace ->
|
||||||
?LOG(alert, "Plugin ~s load failed with ~p", [Name, {Error, Stacktrace}]),
|
?SLOG(alert, #{
|
||||||
|
msg => "plugin_load_failed",
|
||||||
|
name => Name,
|
||||||
|
exception => Error,
|
||||||
|
reason => Reason,
|
||||||
|
stacktrace => Stacktrace
|
||||||
|
}),
|
||||||
{error, parse_config_file_failed}
|
{error, parse_config_file_failed}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -202,12 +216,19 @@ load_app(App) ->
|
||||||
start_app(App) ->
|
start_app(App) ->
|
||||||
case application:ensure_all_started(App) of
|
case application:ensure_all_started(App) of
|
||||||
{ok, Started} ->
|
{ok, Started} ->
|
||||||
?LOG(info, "Started plugins: ~p", [Started]),
|
case Started =/= [] of
|
||||||
?LOG(info, "Load plugin ~s successfully", [App]),
|
true -> ?SLOG(info, #{msg => "started_plugin_dependency_apps", apps => Started});
|
||||||
|
false -> ok
|
||||||
|
end,
|
||||||
|
?SLOG(info, #{msg => "started_plugin_app", app => App}),
|
||||||
ok;
|
ok;
|
||||||
{error, {ErrApp, Reason}} ->
|
{error, {ErrApp, Reason}} ->
|
||||||
?LOG(error, "Load plugin ~s failed, cannot start plugin ~s for ~0p", [App, ErrApp, Reason]),
|
?SLOG(error, #{msg => failed_to_start_plugin_app,
|
||||||
{error, {ErrApp, Reason}}
|
app => App,
|
||||||
|
err_app => ErrApp,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
{error, failed_to_start_plugin_app}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
unload_plugin(App) ->
|
unload_plugin(App) ->
|
||||||
|
|
@ -221,11 +242,17 @@ unload_plugin(App) ->
|
||||||
stop_app(App) ->
|
stop_app(App) ->
|
||||||
case application:stop(App) of
|
case application:stop(App) of
|
||||||
ok ->
|
ok ->
|
||||||
?LOG(info, "Stop plugin ~s successfully", [App]), ok;
|
?SLOG(info, #{msg => "stop_plugin_successfully", app => App}),
|
||||||
|
ok;
|
||||||
{error, {not_started, App}} ->
|
{error, {not_started, App}} ->
|
||||||
?LOG(error, "Plugin ~s is not started", [App]), ok;
|
?SLOG(info, #{msg => "plugin_not_started", app => App}),
|
||||||
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Stop plugin ~s error: ~p", [App]), {error, Reason}
|
?SLOG(error, #{msg => "failed_to_stop_plugin_app",
|
||||||
|
app => App,
|
||||||
|
error => Reason
|
||||||
|
}),
|
||||||
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
names(plugin) ->
|
names(plugin) ->
|
||||||
|
|
@ -236,6 +263,3 @@ names(started_app) ->
|
||||||
|
|
||||||
names(Plugins) ->
|
names(Plugins) ->
|
||||||
[Name || #plugin{name = Name} <- Plugins].
|
[Name || #plugin{name = Name} <- Plugins].
|
||||||
|
|
||||||
funlog(Key, Value) ->
|
|
||||||
?LOG(info, "~s = ~p", [string:join(Key, "."), Value]).
|
|
||||||
|
|
|
||||||
|
|
@ -100,22 +100,26 @@ handle_call({submit, Task}, _From, State) ->
|
||||||
{reply, catch run(Task), State};
|
{reply, catch run(Task), State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast({async_submit, Task}, State) ->
|
handle_cast({async_submit, Task}, State) ->
|
||||||
try run(Task)
|
try run(Task)
|
||||||
catch _:Error:Stacktrace ->
|
catch Error:Reason:Stacktrace ->
|
||||||
?LOG(error, "Error: ~0p, ~0p", [Error, Stacktrace])
|
?SLOG(error, #{msg => "async_submit_error",
|
||||||
|
exception => Error,
|
||||||
|
reason => Reason,
|
||||||
|
stacktrace => Stacktrace
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #{pool := Pool, id := Id}) ->
|
terminate(_Reason, #{pool := Pool, id := Id}) ->
|
||||||
|
|
|
||||||
|
|
@ -203,15 +203,15 @@ handle_call({delete_route, Topic, Dest}, _From, State) ->
|
||||||
{reply, Ok, State};
|
{reply, Ok, State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #{pool := Pool, id := Id}) ->
|
terminate(_Reason, #{pool := Pool, id := Id}) ->
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,11 @@ init([]) ->
|
||||||
{ok, #{nodes => Nodes}, hibernate}.
|
{ok, #{nodes => Nodes}, hibernate}.
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({mnesia_table_event, {write, {?ROUTING_NODE, Node, _}, _}},
|
handle_info({mnesia_table_event, {write, {?ROUTING_NODE, Node, _}, _}},
|
||||||
|
|
@ -130,7 +130,7 @@ handle_info({mnesia_table_event, {delete, {?ROUTING_NODE, _Node}, _}}, State) ->
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_info({mnesia_table_event, Event}, State) ->
|
handle_info({mnesia_table_event, Event}, State) ->
|
||||||
?LOG(error, "Unexpected mnesia_table_event: ~p", [Event]),
|
?SLOG(error,#{msg => "unexpected_mnesia_table_event", event => Event}),
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_info({nodedown, Node}, State = #{nodes := Nodes}) ->
|
handle_info({nodedown, Node}, State = #{nodes := Nodes}) ->
|
||||||
|
|
@ -148,7 +148,7 @@ handle_info({membership, _Event}, State) ->
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
|
|
||||||
-export([namespace/0, roots/0, roots/1, fields/1]).
|
-export([namespace/0, roots/0, roots/1, fields/1]).
|
||||||
-export([conf_get/2, conf_get/3, keys/2, filter/1]).
|
-export([conf_get/2, conf_get/3, keys/2, filter/1]).
|
||||||
-export([ssl/1]).
|
-export([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1, default_ciphers/1]).
|
||||||
|
|
||||||
namespace() -> undefined.
|
namespace() -> undefined.
|
||||||
|
|
||||||
|
|
@ -87,23 +87,26 @@ roots(high) ->
|
||||||
}
|
}
|
||||||
, {"zones",
|
, {"zones",
|
||||||
sc(map("name", ref("zone")),
|
sc(map("name", ref("zone")),
|
||||||
#{ desc => "A zone is a set of configs grouped by the zone <code>name</code>. <br>"
|
#{ desc =>
|
||||||
"For flexible configuration mapping, the <code>name</code> "
|
"""A zone is a set of configs grouped by the zone <code>name</code>.<br>
|
||||||
"can be set to a listener's <code>zone</code> config.<br>"
|
For flexible configuration mapping, the <code>name</code>
|
||||||
"NOTE: A builtin zone named <code>default</code> is auto created "
|
can be set to a listener's <code>zone</code> config.<br>
|
||||||
"and can not be deleted."
|
NOTE: A builtin zone named <code>default</code> is auto created
|
||||||
|
and can not be deleted."""
|
||||||
})}
|
})}
|
||||||
, {"mqtt",
|
, {"mqtt",
|
||||||
sc(ref("mqtt"),
|
sc(ref("mqtt"),
|
||||||
#{ desc => "Global MQTT configuration.<br>"
|
#{ desc =>
|
||||||
"The configs here work as default values which can be overriden "
|
"""Global MQTT configuration.<br>
|
||||||
"in <code>zone</code> configs"
|
The configs here work as default values which can be overriden
|
||||||
|
in <code>zone</code> configs"""
|
||||||
})}
|
})}
|
||||||
, {"authentication",
|
, {"authentication",
|
||||||
sc(hoconsc:lazy(hoconsc:array(map())),
|
sc(hoconsc:lazy(hoconsc:array(map())),
|
||||||
#{ desc => "Default authentication configs for all MQTT listeners.<br>"
|
#{ desc =>
|
||||||
"For per-listener overrides see <code>authentication</code> "
|
"""Default authentication configs for all MQTT listeners.<br>
|
||||||
"in listener configs"
|
For per-listener overrides see <code>authentication</code>
|
||||||
|
in listener configs"""
|
||||||
})}
|
})}
|
||||||
, {"authorization",
|
, {"authorization",
|
||||||
sc(ref("authorization"),
|
sc(ref("authorization"),
|
||||||
|
|
@ -156,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",
|
||||||
|
|
@ -294,7 +297,7 @@ fields("mqtt") ->
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"mqueue_default_priority",
|
, {"mqueue_default_priority",
|
||||||
sc(union(highest, lowest),
|
sc(hoconsc:enum([highest, lowest]),
|
||||||
#{ default => lowest
|
#{ default => lowest
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -309,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
|
||||||
})}
|
})}
|
||||||
];
|
];
|
||||||
|
|
@ -483,7 +486,7 @@ fields("mqtt_wss_listener") ->
|
||||||
#{})
|
#{})
|
||||||
}
|
}
|
||||||
, {"ssl",
|
, {"ssl",
|
||||||
sc(ref("listener_ssl_opts"),
|
sc(ref("listener_wss_opts"),
|
||||||
#{})
|
#{})
|
||||||
}
|
}
|
||||||
, {"websocket",
|
, {"websocket",
|
||||||
|
|
@ -498,6 +501,7 @@ fields("mqtt_quic_listener") ->
|
||||||
#{ default => true
|
#{ default => true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
%% TODO: ensure cacertfile is configurable
|
||||||
, {"certfile",
|
, {"certfile",
|
||||||
sc(string(),
|
sc(string(),
|
||||||
#{})
|
#{})
|
||||||
|
|
@ -506,11 +510,7 @@ fields("mqtt_quic_listener") ->
|
||||||
sc(string(),
|
sc(string(),
|
||||||
#{})
|
#{})
|
||||||
}
|
}
|
||||||
, {"ciphers",
|
, {"ciphers", ciphers_schema(quic)}
|
||||||
sc(comma_separated_list(),
|
|
||||||
#{ default => "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,"
|
|
||||||
"TLS_CHACHA20_POLY1305_SHA256"
|
|
||||||
})}
|
|
||||||
, {"idle_timeout",
|
, {"idle_timeout",
|
||||||
sc(duration(),
|
sc(duration(),
|
||||||
#{ default => "15s"
|
#{ default => "15s"
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -634,16 +634,26 @@ fields("tcp_opts") ->
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("listener_ssl_opts") ->
|
fields("listener_ssl_opts") ->
|
||||||
ssl(#{handshake_timeout => "15s"
|
server_ssl_opts_schema(
|
||||||
, depth => 10
|
#{ depth => 10
|
||||||
, reuse_sessions => true
|
, reuse_sessions => true
|
||||||
, versions => default_tls_vsns()
|
, versions => tls_all_available
|
||||||
, ciphers => default_ciphers()
|
, ciphers => tls_all_available
|
||||||
});
|
}, false);
|
||||||
|
|
||||||
|
fields("listener_wss_opts") ->
|
||||||
|
server_ssl_opts_schema(
|
||||||
|
#{ depth => 10
|
||||||
|
, reuse_sessions => true
|
||||||
|
, versions => tls_all_available
|
||||||
|
, ciphers => tls_all_available
|
||||||
|
}, true);
|
||||||
|
fields(ssl_client_opts) ->
|
||||||
|
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",
|
||||||
|
|
@ -652,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",
|
||||||
|
|
@ -699,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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -726,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",
|
||||||
|
|
@ -902,7 +912,10 @@ conf_get(Key, Conf, Default) ->
|
||||||
filter(Opts) ->
|
filter(Opts) ->
|
||||||
[{K, V} || {K, V} <- Opts, V =/= undefined].
|
[{K, V} || {K, V} <- Opts, V =/= undefined].
|
||||||
|
|
||||||
ssl(Defaults) ->
|
%% @private This function defines the SSL opts which are commonly used by
|
||||||
|
%% SSL listener and client.
|
||||||
|
-spec common_ssl_opts_schema(map()) -> hocon_schema:field_schema().
|
||||||
|
common_ssl_opts_schema(Defaults) ->
|
||||||
D = fun (Field) -> maps:get(to_atom(Field), Defaults, undefined) end,
|
D = fun (Field) -> maps:get(to_atom(Field), Defaults, undefined) end,
|
||||||
Df = fun (Field, Default) -> maps:get(to_atom(Field), Defaults, Default) end,
|
Df = fun (Field, Default) -> maps:get(to_atom(Field), Defaults, Default) end,
|
||||||
[ {"enable",
|
[ {"enable",
|
||||||
|
|
@ -913,69 +926,51 @@ ssl(Defaults) ->
|
||||||
, {"cacertfile",
|
, {"cacertfile",
|
||||||
sc(string(),
|
sc(string(),
|
||||||
#{ default => D("cacertfile")
|
#{ default => D("cacertfile")
|
||||||
|
, nullable => true
|
||||||
|
, desc =>
|
||||||
|
"""Trusted PEM format CA certificates bundle file.<br>
|
||||||
|
The certificates in this file are used to verify the TLS peer's certificates.
|
||||||
|
Append new certificates to the file if new CAs are to be trusted.
|
||||||
|
There is no need to restart EMQ X to have the updated file loaded, because
|
||||||
|
the system regularly checks if file has been updated (and reload).<br>
|
||||||
|
NOTE: invalidating (deleting) a certificate from the file will not affect
|
||||||
|
already established connections.
|
||||||
|
"""
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"certfile",
|
, {"certfile",
|
||||||
sc(string(),
|
sc(string(),
|
||||||
#{ default => D("certfile")
|
#{ default => D("certfile")
|
||||||
|
, nullable => true
|
||||||
|
, desc =>
|
||||||
|
"""PEM format certificates chain file.<br>
|
||||||
|
The certificates in this file should be in reversed order of the certificate
|
||||||
|
issue chain. That is, the host's certificate should be placed in the beginning
|
||||||
|
of the file, followed by the immediate issuer certificate and so on.
|
||||||
|
Although the root CA certificate is optional, it should placed at the end of
|
||||||
|
the file if it is to be added.
|
||||||
|
"""
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"keyfile",
|
, {"keyfile",
|
||||||
sc(string(),
|
sc(string(),
|
||||||
#{ default => D("keyfile")
|
#{ default => D("keyfile")
|
||||||
|
, nullable => true
|
||||||
|
, desc =>
|
||||||
|
"""PEM format private key file.<br>
|
||||||
|
"""
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"fail_if_no_peer_cert",
|
|
||||||
sc(boolean(),
|
|
||||||
#{ default => Df("fail_if_no_peer_cert", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
, {"secure_renegotiate",
|
|
||||||
sc(boolean(),
|
|
||||||
#{ default => Df("secure_renegotiate", true)
|
|
||||||
, desc => """
|
|
||||||
SSL parameter renegotiation is a feature that allows a client and a server
|
|
||||||
to renegotiate the parameters of the SSL connection on the fly.
|
|
||||||
RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation,
|
|
||||||
you drop support for the insecure renegotiation, prone to MitM attacks.
|
|
||||||
"""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
, {"client_renegotiation",
|
|
||||||
sc(boolean(),
|
|
||||||
#{ default => Df("client_renegotiation", true)
|
|
||||||
, desc => """
|
|
||||||
In protocols that support client-initiated renegotiation,
|
|
||||||
the cost of resources of such an operation is higher for the server than the client.
|
|
||||||
This can act as a vector for denial of service attacks.
|
|
||||||
The SSL application already takes measures to counter-act such attempts,
|
|
||||||
but client-initiated renegotiation can be strictly disabled by setting this option to false.
|
|
||||||
The default value is true. Note that disabling renegotiation can result in
|
|
||||||
long-lived connections becoming unusable due to limits on
|
|
||||||
the number of messages the underlying cipher suite can encipher.
|
|
||||||
"""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
, {"reuse_sessions",
|
, {"reuse_sessions",
|
||||||
sc(boolean(),
|
sc(boolean(),
|
||||||
#{ default => Df("reuse_sessions", true)
|
#{ default => Df("reuse_sessions", true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"honor_cipher_order",
|
|
||||||
sc(boolean(),
|
|
||||||
#{ default => Df("honor_cipher_order", true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
, {"handshake_timeout",
|
|
||||||
sc(duration(),
|
|
||||||
#{ default => Df("handshake_timeout", "15s")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
, {"depth",
|
, {"depth",
|
||||||
sc(integer(),
|
sc(integer(),
|
||||||
#{default => Df("depth", 10)
|
#{default => Df("depth", 10)
|
||||||
|
|
@ -983,74 +978,196 @@ the number of messages the underlying cipher suite can encipher.
|
||||||
}
|
}
|
||||||
, {"password",
|
, {"password",
|
||||||
sc(string(),
|
sc(string(),
|
||||||
#{ default => D("key_password")
|
#{ sensitive => true
|
||||||
, sensitive => true
|
, nullable => true
|
||||||
})
|
, desc =>
|
||||||
}
|
"""String containing the user's password. Only used if the private
|
||||||
, {"dhfile",
|
keyfile is password-protected."""
|
||||||
sc(string(),
|
|
||||||
#{ default => D("dhfile")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
, {"server_name_indication",
|
|
||||||
sc(hoconsc:union([disable, string()]),
|
|
||||||
#{ default => D("server_name_indication")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"versions",
|
, {"versions",
|
||||||
sc(typerefl:alias("string", list(atom())),
|
sc(hoconsc:array(typerefl:atom()),
|
||||||
#{ default => maps:get(versions, Defaults, default_tls_vsns())
|
#{ default => default_tls_vsns(maps:get(versions, Defaults, tls_all_available))
|
||||||
, converter => fun (Vsns) -> [tls_vsn(iolist_to_binary(V)) || V <- Vsns] end
|
, desc =>
|
||||||
|
"""All TLS/DTLS versions to be supported.<br>
|
||||||
|
NOTE: PSK ciphers are suppresed by 'tlsv1.3' version config<br>
|
||||||
|
In case PSK cipher suites are intended, make sure to configured
|
||||||
|
<code>['tlsv1.2', 'tlsv1.1']</code> here.
|
||||||
|
"""
|
||||||
|
, validator => fun validate_tls_versions/1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
, {"ciphers",
|
, {"ciphers", ciphers_schema(D("ciphers"))}
|
||||||
sc(hoconsc:array(string()),
|
, {user_lookup_fun,
|
||||||
#{ default => D("ciphers")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
, {"user_lookup_fun",
|
|
||||||
sc(typerefl:alias("string", any()),
|
sc(typerefl:alias("string", any()),
|
||||||
#{ default => "emqx_psk:lookup"
|
#{ default => "emqx_tls_psk:lookup"
|
||||||
, converter => fun ?MODULE:parse_user_lookup_fun/1
|
, converter => fun ?MODULE:parse_user_lookup_fun/1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
, {"secure_renegotiate",
|
||||||
|
sc(boolean(),
|
||||||
|
#{ default => Df("secure_renegotiate", true)
|
||||||
|
, desc => """
|
||||||
|
SSL parameter renegotiation is a feature that allows a client and a server
|
||||||
|
to renegotiate the parameters of the SSL connection on the fly.
|
||||||
|
RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation,
|
||||||
|
you drop support for the insecure renegotiation, prone to MitM attacks.
|
||||||
|
"""
|
||||||
|
})
|
||||||
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
%% on erl23.2.7.2-emqx-2, sufficient_crypto_support('tlsv1.3') -> false
|
%% @doc Make schema for SSL listener options.
|
||||||
default_tls_vsns() -> [<<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>].
|
%% When it's for ranch listener, an extra field `handshake_timeout' is added.
|
||||||
|
-spec server_ssl_opts_schema(map(), boolean()) -> hocon_schema:field_schema().
|
||||||
tls_vsn(<<"tlsv1.3">>) -> 'tlsv1.3';
|
server_ssl_opts_schema(Defaults, IsRanchListener) ->
|
||||||
tls_vsn(<<"tlsv1.2">>) -> 'tlsv1.2';
|
D = fun (Field) -> maps:get(to_atom(Field), Defaults, undefined) end,
|
||||||
tls_vsn(<<"tlsv1.1">>) -> 'tlsv1.1';
|
Df = fun (Field, Default) -> maps:get(to_atom(Field), Defaults, Default) end,
|
||||||
tls_vsn(<<"tlsv1">>) -> 'tlsv1'.
|
common_ssl_opts_schema(Defaults) ++
|
||||||
|
[ {"dhfile",
|
||||||
default_ciphers() -> [
|
sc(string(),
|
||||||
"TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256", "TLS_CHACHA20_POLY1305_SHA256",
|
#{ default => D("dhfile")
|
||||||
"TLS_AES_128_CCM_SHA256", "TLS_AES_128_CCM_8_SHA256", "ECDHE-ECDSA-AES256-GCM-SHA384",
|
, nullable => true
|
||||||
"ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-AES256-SHA384", "ECDHE-RSA-AES256-SHA384",
|
, desc =>
|
||||||
"ECDHE-ECDSA-DES-CBC3-SHA", "ECDH-ECDSA-AES256-GCM-SHA384", "ECDH-RSA-AES256-GCM-SHA384",
|
"""Path to a file containing PEM-encoded Diffie Hellman parameters
|
||||||
"ECDH-ECDSA-AES256-SHA384", "ECDH-RSA-AES256-SHA384", "DHE-DSS-AES256-GCM-SHA384",
|
to be used by the server if a cipher suite using Diffie Hellman
|
||||||
"DHE-DSS-AES256-SHA256", "AES256-GCM-SHA384", "AES256-SHA256",
|
key exchange is negotiated. If not specified, default parameters
|
||||||
"ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256",
|
are used.<br>
|
||||||
"ECDHE-ECDSA-AES128-SHA256", "ECDHE-RSA-AES128-SHA256", "ECDH-ECDSA-AES128-GCM-SHA256",
|
NOTE: The dhfile option is not supported by TLS 1.3."""
|
||||||
"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",
|
, {"fail_if_no_peer_cert",
|
||||||
"ECDH-ECDSA-AES256-SHA", "ECDH-RSA-AES256-SHA", "AES256-SHA", "ECDHE-ECDSA-AES128-SHA",
|
sc(boolean(),
|
||||||
"ECDHE-RSA-AES128-SHA", "DHE-DSS-AES128-SHA", "ECDH-ECDSA-AES128-SHA",
|
#{ default => Df("fail_if_no_peer_cert", false)
|
||||||
"ECDH-RSA-AES128-SHA", "AES128-SHA"
|
, desc =>
|
||||||
] ++ psk_ciphers().
|
"""
|
||||||
|
Used together with {verify, verify_peer} by an TLS/DTLS server.
|
||||||
psk_ciphers() -> [
|
If set to true, the server fails if the client does not have a
|
||||||
"PSK-AES128-CBC-SHA", "PSK-AES256-CBC-SHA", "PSK-3DES-EDE-CBC-SHA", "PSK-RC4-SHA"
|
certificate to send, that is, sends an empty certificate.
|
||||||
|
If set to false, it fails only if the client sends an invalid
|
||||||
|
certificate (an empty certificate is considered valid).
|
||||||
|
"""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
, {"honor_cipher_order",
|
||||||
|
sc(boolean(),
|
||||||
|
#{ default => Df("honor_cipher_order", true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
, {"client_renegotiation",
|
||||||
|
sc(boolean(),
|
||||||
|
#{ default => Df("client_renegotiation", true)
|
||||||
|
, desc => """
|
||||||
|
In protocols that support client-initiated renegotiation,
|
||||||
|
the cost of resources of such an operation is higher for the server than the client.
|
||||||
|
This can act as a vector for denial of service attacks.
|
||||||
|
The SSL application already takes measures to counter-act such attempts,
|
||||||
|
but client-initiated renegotiation can be strictly disabled by setting this option to false.
|
||||||
|
The default value is true. Note that disabling renegotiation can result in
|
||||||
|
long-lived connections becoming unusable due to limits on
|
||||||
|
the number of messages the underlying cipher suite can encipher.
|
||||||
|
"""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
| [ {"handshake_timeout",
|
||||||
|
sc(duration(),
|
||||||
|
#{ default => Df("handshake_timeout", "15s")
|
||||||
|
, desc => "Maximum time duration allowed for the handshake to complete"
|
||||||
|
})}
|
||||||
|
|| IsRanchListener]
|
||||||
].
|
].
|
||||||
|
|
||||||
|
%% @doc Make schema for SSL client.
|
||||||
|
-spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema().
|
||||||
|
client_ssl_opts_schema(Defaults) ->
|
||||||
|
common_ssl_opts_schema(Defaults) ++
|
||||||
|
[ { "server_name_indication",
|
||||||
|
sc(hoconsc:union([disable, string()]),
|
||||||
|
#{ default => disable
|
||||||
|
, desc =>
|
||||||
|
"""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
|
||||||
|
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
|
||||||
|
or when the host has multiple resolvable DNS records <br>
|
||||||
|
If not specified, it will default to the host name string which is used
|
||||||
|
to establish the connection, unless it is IP addressed used.<br>
|
||||||
|
The host name is then also used in the host name verification of the peer
|
||||||
|
certificate.<br> The special value 'disable' prevents the Server Name
|
||||||
|
Indication extension from being sent and disables the hostname
|
||||||
|
verification check."""
|
||||||
|
})}
|
||||||
|
].
|
||||||
|
|
||||||
|
|
||||||
|
default_tls_vsns(dtls_all_available) ->
|
||||||
|
proplists:get_value(available_dtls, ssl:versions());
|
||||||
|
default_tls_vsns(tls_all_available) ->
|
||||||
|
emqx_tls_lib:default_versions().
|
||||||
|
|
||||||
|
-spec ciphers_schema(quic | dtls_all_available | tls_all_available | undefined) -> hocon_schema:field_schema().
|
||||||
|
ciphers_schema(Default) ->
|
||||||
|
sc(hoconsc:array(string()),
|
||||||
|
#{ default => default_ciphers(Default)
|
||||||
|
, converter => fun(Ciphers) when is_binary(Ciphers) ->
|
||||||
|
binary:split(Ciphers, <<",">>, [global]);
|
||||||
|
(Ciphers) when is_list(Ciphers) ->
|
||||||
|
Ciphers
|
||||||
|
end
|
||||||
|
, validator => case Default =:= quic of
|
||||||
|
true -> undefined; %% quic has openssl statically linked
|
||||||
|
false -> fun validate_ciphers/1
|
||||||
|
end
|
||||||
|
, desc =>
|
||||||
|
"""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].
|
||||||
|
<br>
|
||||||
|
Ciphers (and their ordering) define the way in which the
|
||||||
|
client and server encrypts information over the wire.
|
||||||
|
Selecting a good cipher suite is critical for the
|
||||||
|
application's data security, confidentiality and performance.
|
||||||
|
The names should be in OpenSSL sting format (not RFC format).
|
||||||
|
Default values and examples proveded by EMQ X config
|
||||||
|
documentation are all in OpenSSL format.<br>
|
||||||
|
|
||||||
|
NOTE: Certain cipher suites are only compatible with
|
||||||
|
specific TLS <code>versions</code> ('tlsv1.1', 'tlsv1.2' or 'tlsv1.3')
|
||||||
|
incompatible cipher suites will be silently dropped.
|
||||||
|
For instance, if only 'tlsv1.3' is given in the <code>versions</code>,
|
||||||
|
configuring cipher suites for other versions will have no effect.
|
||||||
|
<br>
|
||||||
|
|
||||||
|
NOTE: PSK ciphers are suppresed by 'tlsv1.3' version config<br>
|
||||||
|
If PSK cipher suites are intended, 'tlsv1.3' should be disabled from <code>versions</code>.<br>
|
||||||
|
PSK cipher suites: <code>\"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\"</code><br>
|
||||||
|
""" ++ case Default of
|
||||||
|
quic -> "NOTE: QUIC listener supports only 'tlsv1.3' ciphers<br>";
|
||||||
|
_ -> ""
|
||||||
|
end}).
|
||||||
|
|
||||||
|
default_ciphers(undefined) ->
|
||||||
|
default_ciphers(tls_all_available);
|
||||||
|
default_ciphers(quic) -> [
|
||||||
|
"TLS_AES_256_GCM_SHA384",
|
||||||
|
"TLS_AES_128_GCM_SHA256",
|
||||||
|
"TLS_CHACHA20_POLY1305_SHA256"
|
||||||
|
];
|
||||||
|
default_ciphers(dtls_all_available) ->
|
||||||
|
%% as of now, dtls does not support tlsv1.3 ciphers
|
||||||
|
emqx_tls_lib:selected_ciphers(['dtlsv1.2', 'dtlsv1']);
|
||||||
|
default_ciphers(tls_all_available) ->
|
||||||
|
emqx_tls_lib:default_ciphers().
|
||||||
|
|
||||||
%% @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
|
||||||
|
|
@ -1069,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};
|
||||||
|
|
@ -1077,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.
|
||||||
|
|
||||||
|
|
@ -1159,4 +1287,19 @@ parse_user_lookup_fun(StrConf) ->
|
||||||
[ModStr, FunStr] = string:tokens(StrConf, ":"),
|
[ModStr, FunStr] = string:tokens(StrConf, ":"),
|
||||||
Mod = list_to_atom(ModStr),
|
Mod = list_to_atom(ModStr),
|
||||||
Fun = list_to_atom(FunStr),
|
Fun = list_to_atom(FunStr),
|
||||||
{fun Mod:Fun/3, <<>>}.
|
{fun Mod:Fun/3, undefined}.
|
||||||
|
|
||||||
|
validate_ciphers(Ciphers) ->
|
||||||
|
All = emqx_tls_lib:all_ciphers(),
|
||||||
|
case lists:filter(fun(Cipher) -> not lists:member(Cipher, All) end, Ciphers) of
|
||||||
|
[] -> ok;
|
||||||
|
Bad -> {error, {bad_ciphers, Bad}}
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -479,11 +479,12 @@ log_dropped(Msg = #message{qos = QoS}, #session{mqueue = Q}) ->
|
||||||
case (QoS == ?QOS_0) andalso (not emqx_mqueue:info(store_qos0, Q)) of
|
case (QoS == ?QOS_0) andalso (not emqx_mqueue:info(store_qos0, Q)) of
|
||||||
true ->
|
true ->
|
||||||
ok = emqx_metrics:inc('delivery.dropped.qos0_msg'),
|
ok = emqx_metrics:inc('delivery.dropped.qos0_msg'),
|
||||||
?LOG(warning, "Dropped qos0 msg: ~s", [emqx_message:format(Msg)]);
|
?SLOG(warning, #{msg => "dropped_qos0_msg",
|
||||||
|
payload => emqx_message:to_log_map(Msg)});
|
||||||
false ->
|
false ->
|
||||||
ok = emqx_metrics:inc('delivery.dropped.queue_full'),
|
ok = emqx_metrics:inc('delivery.dropped.queue_full'),
|
||||||
?LOG(warning, "Dropped msg due to mqueue is full: ~s",
|
?SLOG(warning, #{msg => "dropped_msg_due_to_mqueue_is_full",
|
||||||
[emqx_message:format(Msg)])
|
payload => emqx_message:to_log_map(Msg)})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
enrich_fun(Session = #session{subscriptions = Subs}) ->
|
enrich_fun(Session = #session{subscriptions = Subs}) ->
|
||||||
|
|
|
||||||
|
|
@ -325,11 +325,11 @@ handle_call({unsubscribe, Group, Topic, SubPid}, _From, State) ->
|
||||||
{reply, ok, State};
|
{reply, ok, State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", req => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", req => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) ->
|
handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) ->
|
||||||
|
|
@ -347,8 +347,8 @@ handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = P
|
||||||
handle_info({mnesia_table_event, _Event}, State) ->
|
handle_info({mnesia_table_event, _Event}, State) ->
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) ->
|
handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #state{pmon = PMon}) ->
|
||||||
?LOG(info, "Shared subscriber down: ~p", [SubPid]),
|
?SLOG(info, #{msg => "shared_subscriber_down", sub_pid => SubPid, reason => Reason}),
|
||||||
cleanup_down(SubPid),
|
cleanup_down(SubPid),
|
||||||
{noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})};
|
{noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ handle_call(stop, _From, State) ->
|
||||||
{stop, normal, ok, State};
|
{stop, normal, ok, State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast({setstat, Stat, MaxStat, Val}, State) ->
|
handle_cast({setstat, Stat, MaxStat, Val}, State) ->
|
||||||
|
|
@ -221,7 +221,9 @@ handle_cast({update_interval, Update = #update{name = Name}},
|
||||||
State = #state{updates = Updates}) ->
|
State = #state{updates = Updates}) ->
|
||||||
NState = case lists:keyfind(Name, #update.name, Updates) of
|
NState = case lists:keyfind(Name, #update.name, Updates) of
|
||||||
#update{} ->
|
#update{} ->
|
||||||
?LOG(warning, "Duplicated update: ~s", [Name]),
|
?SLOG(warning, #{msg => "duplicated_update",
|
||||||
|
name => Name
|
||||||
|
}),
|
||||||
State;
|
State;
|
||||||
false -> State#state{updates = [Update|Updates]}
|
false -> State#state{updates = [Update|Updates]}
|
||||||
end,
|
end,
|
||||||
|
|
@ -232,7 +234,7 @@ handle_cast({cancel_update, Name}, State = #state{updates = Updates}) ->
|
||||||
{noreply, State#state{updates = Updates1}};
|
{noreply, State#state{updates = Updates1}};
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({timeout, TRef, tick}, State = #state{timer = TRef, updates = Updates}) ->
|
handle_info({timeout, TRef, tick}, State = #state{timer = TRef, updates = Updates}) ->
|
||||||
|
|
@ -241,8 +243,13 @@ handle_info({timeout, TRef, tick}, State = #state{timer = TRef, updates = Update
|
||||||
func = UpFun}, Acc) when C =< 0 ->
|
func = UpFun}, Acc) when C =< 0 ->
|
||||||
try UpFun()
|
try UpFun()
|
||||||
catch
|
catch
|
||||||
_:Error ->
|
Error : Reason : Stacktrace ->
|
||||||
?LOG(error, "Update ~s failed: ~0p", [Name, Error])
|
?SLOG(error, #{msg => "update_name_failed",
|
||||||
|
name => Name,
|
||||||
|
exception => Error,
|
||||||
|
reason => Reason,
|
||||||
|
stacktrace => Stacktrace
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
[Update#update{countdown = I} | Acc];
|
[Update#update{countdown = I} | Acc];
|
||||||
(Update = #update{countdown = C}, Acc) ->
|
(Update = #update{countdown = C}, Acc) ->
|
||||||
|
|
@ -251,7 +258,7 @@ handle_info({timeout, TRef, tick}, State = #state{timer = TRef, updates = Update
|
||||||
{noreply, start_timer(State#state{updates = Updates1}), hibernate};
|
{noreply, start_timer(State#state{updates = Updates1}), hibernate};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #state{timer = TRef}) ->
|
terminate(_Reason, #state{timer = TRef}) ->
|
||||||
|
|
@ -271,6 +278,9 @@ safe_update_element(Key, Val) ->
|
||||||
true -> true
|
true -> true
|
||||||
catch
|
catch
|
||||||
error:badarg ->
|
error:badarg ->
|
||||||
?LOG(warning, "Failed to update ~0p to ~0p", [Key, Val])
|
?SLOG(warning, #{
|
||||||
|
msg => "failed_to_update",
|
||||||
|
key => Key,
|
||||||
|
val => Val
|
||||||
|
})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,11 +134,11 @@ handle_call(uptime, _From, State) ->
|
||||||
{reply, uptime(State), State};
|
{reply, uptime(State), State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({timeout, TRef, heartbeat}, State = #state{heartbeat = TRef}) ->
|
handle_info({timeout, TRef, heartbeat}, State = #state{heartbeat = TRef}) ->
|
||||||
|
|
@ -156,7 +156,7 @@ handle_info({timeout, TRef, tick},
|
||||||
{noreply, tick(State), hibernate};
|
{noreply, tick(State), hibernate};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #state{heartbeat = TRef1, ticker = TRef2}) ->
|
terminate(_Reason, #state{heartbeat = TRef1, ticker = TRef2}) ->
|
||||||
|
|
|
||||||
|
|
@ -83,42 +83,51 @@ sysm_opts([_Opt|Opts], Acc) ->
|
||||||
sysm_opts(Opts, Acc).
|
sysm_opts(Opts, Acc).
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", req => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", req => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({monitor, Pid, long_gc, Info}, State) ->
|
handle_info({monitor, Pid, long_gc, Info}, State) ->
|
||||||
suppress({long_gc, Pid},
|
suppress({long_gc, Pid},
|
||||||
fun() ->
|
fun() ->
|
||||||
WarnMsg = io_lib:format("long_gc warning: pid = ~p, info: ~p", [Pid, Info]),
|
WarnMsg = io_lib:format("long_gc warning: pid = ~p", [Pid]),
|
||||||
?LOG(warning, "~s~n~p", [WarnMsg, procinfo(Pid)]),
|
?SLOG(warning, #{msg => long_gc,
|
||||||
|
info => Info,
|
||||||
|
porcinfo => procinfo(Pid)
|
||||||
|
}),
|
||||||
safe_publish(long_gc, WarnMsg)
|
safe_publish(long_gc, WarnMsg)
|
||||||
end, State);
|
end, State);
|
||||||
|
|
||||||
handle_info({monitor, Pid, long_schedule, Info}, State) when is_pid(Pid) ->
|
handle_info({monitor, Pid, long_schedule, Info}, State) when is_pid(Pid) ->
|
||||||
suppress({long_schedule, Pid},
|
suppress({long_schedule, Pid},
|
||||||
fun() ->
|
fun() ->
|
||||||
WarnMsg = io_lib:format("long_schedule warning: pid = ~p, info: ~p", [Pid, Info]),
|
WarnMsg = io_lib:format("long_schedule warning: pid = ~p", [Pid]),
|
||||||
?LOG(warning, "~s~n~p", [WarnMsg, procinfo(Pid)]),
|
?SLOG(warning, #{msg => long_schedule,
|
||||||
|
info => Info,
|
||||||
|
procinfo => procinfo(Pid)}),
|
||||||
safe_publish(long_schedule, WarnMsg)
|
safe_publish(long_schedule, WarnMsg)
|
||||||
end, State);
|
end, State);
|
||||||
|
|
||||||
handle_info({monitor, Port, long_schedule, Info}, State) when is_port(Port) ->
|
handle_info({monitor, Port, long_schedule, Info}, State) when is_port(Port) ->
|
||||||
suppress({long_schedule, Port},
|
suppress({long_schedule, Port},
|
||||||
fun() ->
|
fun() ->
|
||||||
WarnMsg = io_lib:format("long_schedule warning: port = ~p, info: ~p", [Port, Info]),
|
WarnMsg = io_lib:format("long_schedule warning: port = ~p", [Port]),
|
||||||
?LOG(warning, "~s~n~p", [WarnMsg, erlang:port_info(Port)]),
|
?SLOG(warning, #{msg => long_schedule,
|
||||||
|
info => Info,
|
||||||
|
portinfo => portinfo(Port)}),
|
||||||
safe_publish(long_schedule, WarnMsg)
|
safe_publish(long_schedule, WarnMsg)
|
||||||
end, State);
|
end, State);
|
||||||
|
|
||||||
handle_info({monitor, Pid, large_heap, Info}, State) ->
|
handle_info({monitor, Pid, large_heap, Info}, State) ->
|
||||||
suppress({large_heap, Pid},
|
suppress({large_heap, Pid},
|
||||||
fun() ->
|
fun() ->
|
||||||
WarnMsg = io_lib:format("large_heap warning: pid = ~p, info: ~p", [Pid, Info]),
|
WarnMsg = io_lib:format("large_heap warning: pid = ~p", [Pid]),
|
||||||
?LOG(warning, "~s~n~p", [WarnMsg, procinfo(Pid)]),
|
?SLOG(warning, #{msg => large_heap,
|
||||||
|
info => Info,
|
||||||
|
procinfo => procinfo(Pid)}),
|
||||||
safe_publish(large_heap, WarnMsg)
|
safe_publish(large_heap, WarnMsg)
|
||||||
end, State);
|
end, State);
|
||||||
|
|
||||||
|
|
@ -126,7 +135,10 @@ handle_info({monitor, SusPid, busy_port, Port}, State) ->
|
||||||
suppress({busy_port, Port},
|
suppress({busy_port, Port},
|
||||||
fun() ->
|
fun() ->
|
||||||
WarnMsg = io_lib:format("busy_port warning: suspid = ~p, port = ~p", [SusPid, Port]),
|
WarnMsg = io_lib:format("busy_port warning: suspid = ~p, port = ~p", [SusPid, Port]),
|
||||||
?LOG(warning, "~s~n~p~n~p", [WarnMsg, procinfo(SusPid), erlang:port_info(Port)]),
|
?SLOG(warning, #{msg => busy_port,
|
||||||
|
portinfo => portinfo(Port),
|
||||||
|
procinfo => procinfo(SusPid)
|
||||||
|
}),
|
||||||
safe_publish(busy_port, WarnMsg)
|
safe_publish(busy_port, WarnMsg)
|
||||||
end, State);
|
end, State);
|
||||||
|
|
||||||
|
|
@ -134,7 +146,9 @@ handle_info({monitor, SusPid, busy_dist_port, Port}, State) ->
|
||||||
suppress({busy_dist_port, Port},
|
suppress({busy_dist_port, Port},
|
||||||
fun() ->
|
fun() ->
|
||||||
WarnMsg = io_lib:format("busy_dist_port warning: suspid = ~p, port = ~p", [SusPid, Port]),
|
WarnMsg = io_lib:format("busy_dist_port warning: suspid = ~p, port = ~p", [SusPid, Port]),
|
||||||
?LOG(warning, "~s~n~p~n~p", [WarnMsg, procinfo(SusPid), erlang:port_info(Port)]),
|
?SLOG(warning, #{msg => busy_dist_port,
|
||||||
|
portinfo => portinfo(Port),
|
||||||
|
procinfo => procinfo(SusPid)}),
|
||||||
safe_publish(busy_dist_port, WarnMsg)
|
safe_publish(busy_dist_port, WarnMsg)
|
||||||
end, State);
|
end, State);
|
||||||
|
|
||||||
|
|
@ -142,7 +156,7 @@ handle_info({timeout, _Ref, reset}, State) ->
|
||||||
{noreply, State#{events := []}, hibernate};
|
{noreply, State#{events := []}, hibernate};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "Unexpected Info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #{timer := TRef}) ->
|
terminate(_Reason, #{timer := TRef}) ->
|
||||||
|
|
@ -170,11 +184,14 @@ suppress(Key, SuccFun, State = #{events := Events}) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
procinfo(Pid) ->
|
procinfo(Pid) ->
|
||||||
case {emqx_vm:get_process_info(Pid), emqx_vm:get_process_gc_info(Pid)} of
|
[{pid, Pid} | procinfo_l(emqx_vm:get_process_gc_info(Pid))] ++
|
||||||
{undefined, _} -> undefined;
|
procinfo_l(emqx_vm:get_process_info(Pid)).
|
||||||
{_, undefined} -> undefined;
|
|
||||||
{Info, GcInfo} -> Info ++ GcInfo
|
procinfo_l(undefined) -> [];
|
||||||
end.
|
procinfo_l(List) -> List.
|
||||||
|
|
||||||
|
portinfo(Port) ->
|
||||||
|
[{port, Port} | erlang:port_info(Port)].
|
||||||
|
|
||||||
safe_publish(Event, WarnMsg) ->
|
safe_publish(Event, WarnMsg) ->
|
||||||
Topic = emqx_topic:systop(lists:concat(['sysmon/', Event])),
|
Topic = emqx_topic:systop(lists:concat(['sysmon/', Event])),
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,10 @@
|
||||||
-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
|
||||||
|
, all_ciphers/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% non-empty string
|
%% non-empty string
|
||||||
|
|
@ -31,9 +32,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 +50,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 +60,63 @@ integral_versions(Desired) ->
|
||||||
Filtered
|
Filtered
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Return a list of default (openssl string format) cipher suites.
|
%% @doc Return a list of all supported ciphers.
|
||||||
-spec default_ciphers() -> [string()].
|
all_ciphers() -> all_ciphers(default_versions()).
|
||||||
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",
|
||||||
|
"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", "ECDHE-ECDSA-AES128-SHA",
|
||||||
|
"ECDHE-RSA-AES128-SHA", "DHE-DSS-AES128-SHA", "ECDH-ECDSA-AES128-SHA",
|
||||||
|
"ECDH-RSA-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"
|
||||||
|
].
|
||||||
|
|
||||||
%% @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 +130,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 +138,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([]) -> [];
|
||||||
|
|
@ -178,7 +219,7 @@ drop_tls13(SslOpts0) ->
|
||||||
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),
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,10 @@
|
||||||
%% limitations under the License.
|
%% limitations under the License.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-module(emqx_psk).
|
-module(emqx_tls_psk).
|
||||||
|
|
||||||
-include("logger.hrl").
|
-include("logger.hrl").
|
||||||
|
|
||||||
|
|
||||||
%% SSL PSK Callbacks
|
%% SSL PSK Callbacks
|
||||||
-export([lookup/3]).
|
-export([lookup/3]).
|
||||||
|
|
||||||
|
|
@ -26,14 +25,24 @@
|
||||||
-type psk_user_state() :: term().
|
-type psk_user_state() :: term().
|
||||||
|
|
||||||
-spec lookup(psk, psk_identity(), psk_user_state()) -> {ok, SharedSecret :: binary()} | error.
|
-spec lookup(psk, psk_identity(), psk_user_state()) -> {ok, SharedSecret :: binary()} | error.
|
||||||
lookup(psk, ClientPSKID, _UserState) ->
|
lookup(psk, PSKIdentity, _UserState) ->
|
||||||
try emqx_hooks:run_fold('tls_handshake.psk_lookup', [ClientPSKID], not_found) of
|
try emqx_hooks:run_fold('tls_handshake.psk_lookup', [PSKIdentity], normal) of
|
||||||
SharedSecret when is_binary(SharedSecret) -> {ok, SharedSecret};
|
{ok, SharedSecret} when is_binary(SharedSecret) ->
|
||||||
Error ->
|
{ok, SharedSecret};
|
||||||
?LOG(error, "Look PSK for PSKID ~p error: ~p", [ClientPSKID, Error]),
|
normal ->
|
||||||
|
?SLOG(info, #{msg => "psk_identity_not_found",
|
||||||
|
psk_identity => PSKIdentity}),
|
||||||
|
error;
|
||||||
|
{error, Reason} ->
|
||||||
|
?SLOG(warning, #{msg => "psk_identity_not_found",
|
||||||
|
psk_identity => PSKIdentity,
|
||||||
|
reason => Reason}),
|
||||||
error
|
error
|
||||||
catch
|
catch
|
||||||
Except:Error:Stacktrace ->
|
Class:Reason:Stacktrace ->
|
||||||
?LOG(error, "Lookup PSK failed, ~0p: ~0p", [{Except,Error}, Stacktrace]),
|
?SLOG(error, #{msg => "lookup_psk_failed",
|
||||||
|
class => Class,
|
||||||
|
reason => Reason,
|
||||||
|
stacktrace => Stacktrace}),
|
||||||
error
|
error
|
||||||
end.
|
end.
|
||||||
|
|
@ -115,18 +115,18 @@ install_trace_handler(Who, Level, LogFile) ->
|
||||||
{fun filter_by_meta_key/2, Who}}]})
|
{fun filter_by_meta_key/2, Who}}]})
|
||||||
of
|
of
|
||||||
ok ->
|
ok ->
|
||||||
?LOG(info, "Start trace for ~p", [Who]);
|
?SLOG(info, #{msg => "start_trace", who => Who});
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Start trace for ~p failed, error: ~p", [Who, Reason]),
|
?SLOG(error, #{msg => "failed_to_trace", who => Who, reason => Reason}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
uninstall_trance_handler(Who) ->
|
uninstall_trance_handler(Who) ->
|
||||||
case logger:remove_handler(handler_id(Who)) of
|
case logger:remove_handler(handler_id(Who)) of
|
||||||
ok ->
|
ok ->
|
||||||
?LOG(info, "Stop trace for ~p", [Who]);
|
?SLOG(info, #{msg => "stop_trace", who => Who});
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Stop trace for ~p failed, error: ~p", [Who, Reason]),
|
?SLOG(error, #{msg => "failed_to_stop_trace", who => Who, reason => Reason}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,11 @@ init([]) ->
|
||||||
{ok, #{}}.
|
{ok, #{}}.
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?LOG(error, "[VM_MON] Unexpected call: ~p", [Req]),
|
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?LOG(error, "[VM_MON] Unexpected cast: ~p", [Msg]),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
handle_info({timeout, _Timer, check}, State) ->
|
handle_info({timeout, _Timer, check}, State) ->
|
||||||
|
|
@ -75,7 +75,7 @@ handle_info({timeout, _Timer, check}, State) ->
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?LOG(error, "[VM_MON] Unexpected info: ~p", [Info]),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, _State) ->
|
terminate(_Reason, _State) ->
|
||||||
|
|
|
||||||
|
|
@ -181,10 +181,11 @@ init(Req, #{listener := {Type, Listener}} = Opts) ->
|
||||||
idle_timeout => get_ws_opts(Type, Listener, idle_timeout)
|
idle_timeout => get_ws_opts(Type, Listener, idle_timeout)
|
||||||
},
|
},
|
||||||
case check_origin_header(Req, Opts) of
|
case check_origin_header(Req, Opts) of
|
||||||
{error, Message} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Invalid Origin Header ~p~n", [Message]),
|
?SLOG(error, #{msg => "invalid_origin_header", reason => Reason}),
|
||||||
{ok, cowboy_req:reply(403, Req), WsOpts};
|
{ok, cowboy_req:reply(403, Req), WsOpts};
|
||||||
ok -> parse_sec_websocket_protocol(Req, Opts, WsOpts)
|
ok ->
|
||||||
|
parse_sec_websocket_protocol(Req, Opts, WsOpts)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_sec_websocket_protocol(Req, #{listener := {Type, Listener}} = Opts, WsOpts) ->
|
parse_sec_websocket_protocol(Req, #{listener := {Type, Listener}} = Opts, WsOpts) ->
|
||||||
|
|
@ -231,7 +232,7 @@ parse_header_fun_origin(Req, #{listener := {Type, Listener}}) ->
|
||||||
Value ->
|
Value ->
|
||||||
case lists:member(Value, get_ws_opts(Type, Listener, check_origins)) of
|
case lists:member(Value, get_ws_opts(Type, Listener, check_origins)) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {origin_not_allowed, Value}
|
false -> {error, #{bad_origin => Value}}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -263,11 +264,12 @@ websocket_init([Req, #{zone := Zone, listener := {Type, Listener}} = Opts]) ->
|
||||||
WsCookie = try cowboy_req:parse_cookies(Req)
|
WsCookie = try cowboy_req:parse_cookies(Req)
|
||||||
catch
|
catch
|
||||||
error:badarg ->
|
error:badarg ->
|
||||||
?LOG(error, "Illegal cookie"),
|
?SLOG(error, #{msg => "bad_cookie"}),
|
||||||
undefined;
|
undefined;
|
||||||
Error:Reason ->
|
Error:Reason ->
|
||||||
?LOG(error, "Failed to parse cookie, Error: ~0p, Reason ~0p",
|
?SLOG(error, #{msg => "failed_to_parse_cookie",
|
||||||
[Error, Reason]),
|
exception => Error,
|
||||||
|
reason => Reason}),
|
||||||
undefined
|
undefined
|
||||||
end,
|
end,
|
||||||
ConnInfo = #{socktype => ws,
|
ConnInfo = #{socktype => ws,
|
||||||
|
|
@ -324,7 +326,7 @@ websocket_handle({binary, Data}, State) when is_list(Data) ->
|
||||||
websocket_handle({binary, iolist_to_binary(Data)}, State);
|
websocket_handle({binary, iolist_to_binary(Data)}, State);
|
||||||
|
|
||||||
websocket_handle({binary, Data}, State) ->
|
websocket_handle({binary, Data}, State) ->
|
||||||
?LOG(debug, "RECV ~0p", [Data]),
|
?SLOG(debug, #{msg => "RECV_data", data => Data, transport => websocket}),
|
||||||
ok = inc_recv_stats(1, iolist_size(Data)),
|
ok = inc_recv_stats(1, iolist_size(Data)),
|
||||||
NState = ensure_stats_timer(State),
|
NState = ensure_stats_timer(State),
|
||||||
return(parse_incoming(Data, NState));
|
return(parse_incoming(Data, NState));
|
||||||
|
|
@ -339,7 +341,7 @@ websocket_handle({Frame, _}, State) when Frame =:= ping; Frame =:= pong ->
|
||||||
|
|
||||||
websocket_handle({Frame, _}, State) ->
|
websocket_handle({Frame, _}, State) ->
|
||||||
%% TODO: should not close the ws connection
|
%% TODO: should not close the ws connection
|
||||||
?LOG(error, "Unexpected frame - ~p", [Frame]),
|
?SLOG(error, #{msg => "unexpected_frame", frame => Frame}),
|
||||||
shutdown(unexpected_ws_frame, State).
|
shutdown(unexpected_ws_frame, State).
|
||||||
|
|
||||||
websocket_info({call, From, Req}, State) ->
|
websocket_info({call, From, Req}, State) ->
|
||||||
|
|
@ -397,11 +399,11 @@ websocket_info(Info, State) ->
|
||||||
websocket_close({_, ReasonCode, _Payload}, State) when is_integer(ReasonCode) ->
|
websocket_close({_, ReasonCode, _Payload}, State) when is_integer(ReasonCode) ->
|
||||||
websocket_close(ReasonCode, State);
|
websocket_close(ReasonCode, State);
|
||||||
websocket_close(Reason, State) ->
|
websocket_close(Reason, State) ->
|
||||||
?LOG(debug, "Websocket closed due to ~p~n", [Reason]),
|
?SLOG(debug, #{msg => "websocket_closed", reason => Reason}),
|
||||||
handle_info({sock_closed, Reason}, State).
|
handle_info({sock_closed, Reason}, State).
|
||||||
|
|
||||||
terminate(Reason, _Req, #state{channel = Channel}) ->
|
terminate(Reason, _Req, #state{channel = Channel}) ->
|
||||||
?LOG(debug, "Terminated due to ~p", [Reason]),
|
?SLOG(debug, #{msg => "terminated", reason => Reason}),
|
||||||
emqx_channel:terminate(Reason, Channel);
|
emqx_channel:terminate(Reason, Channel);
|
||||||
|
|
||||||
terminate(_Reason, _Req, _UnExpectedState) ->
|
terminate(_Reason, _Req, _UnExpectedState) ->
|
||||||
|
|
@ -446,7 +448,7 @@ handle_info({connack, ConnAck}, State) ->
|
||||||
return(enqueue(ConnAck, State));
|
return(enqueue(ConnAck, State));
|
||||||
|
|
||||||
handle_info({close, Reason}, State) ->
|
handle_info({close, Reason}, State) ->
|
||||||
?LOG(debug, "Force to close the socket due to ~p", [Reason]),
|
?SLOG(debug, #{msg => "force_socket_close", reason => Reason}),
|
||||||
return(enqueue({close, Reason}, State));
|
return(enqueue({close, Reason}, State));
|
||||||
|
|
||||||
handle_info({event, connected}, State = #state{channel = Channel}) ->
|
handle_info({event, connected}, State = #state{channel = Channel}) ->
|
||||||
|
|
@ -499,7 +501,7 @@ ensure_rate_limit(Stats, State = #state{limiter = Limiter}) ->
|
||||||
{ok, Limiter1} ->
|
{ok, Limiter1} ->
|
||||||
State#state{limiter = Limiter1};
|
State#state{limiter = Limiter1};
|
||||||
{pause, Time, Limiter1} ->
|
{pause, Time, Limiter1} ->
|
||||||
?LOG(warning, "Pause ~pms due to rate limit", [Time]),
|
?SLOG(warning, #{msg => "pause_due_to_rate_limit", time => Time}),
|
||||||
TRef = start_timer(Time, limit_timeout),
|
TRef = start_timer(Time, limit_timeout),
|
||||||
NState = State#state{sockstate = blocked,
|
NState = State#state{sockstate = blocked,
|
||||||
limiter = Limiter1,
|
limiter = Limiter1,
|
||||||
|
|
@ -547,9 +549,19 @@ parse_incoming(Data, State = #state{parse_state = ParseState}) ->
|
||||||
NState = State#state{parse_state = NParseState},
|
NState = State#state{parse_state = NParseState},
|
||||||
parse_incoming(Rest, postpone({incoming, Packet}, NState))
|
parse_incoming(Rest, postpone({incoming, Packet}, NState))
|
||||||
catch
|
catch
|
||||||
error:Reason:Stk ->
|
throw : ?FRAME_PARSE_ERROR(Reason) ->
|
||||||
?LOG(error, "~nParse failed for ~0p~n~0p~nFrame data: ~0p",
|
?SLOG(info, #{ reason => Reason
|
||||||
[Reason, Stk, Data]),
|
, at_state => emqx_frame:describe_state(ParseState)
|
||||||
|
, input_bytes => Data
|
||||||
|
}),
|
||||||
|
FrameError = {frame_error, Reason},
|
||||||
|
postpone({incoming, FrameError}, State);
|
||||||
|
error : Reason : Stacktrace ->
|
||||||
|
?SLOG(error, #{ at_state => emqx_frame:describe_state(ParseState)
|
||||||
|
, input_bytes => Data
|
||||||
|
, exception => Reason
|
||||||
|
, stacktrace => Stacktrace
|
||||||
|
}),
|
||||||
FrameError = {frame_error, Reason},
|
FrameError = {frame_error, Reason},
|
||||||
postpone({incoming, FrameError}, State)
|
postpone({incoming, FrameError}, State)
|
||||||
end.
|
end.
|
||||||
|
|
@ -560,7 +572,7 @@ parse_incoming(Data, State = #state{parse_state = ParseState}) ->
|
||||||
|
|
||||||
handle_incoming(Packet, State = #state{listener = {Type, Listener}})
|
handle_incoming(Packet, State = #state{listener = {Type, Listener}})
|
||||||
when is_record(Packet, mqtt_packet) ->
|
when is_record(Packet, mqtt_packet) ->
|
||||||
?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]),
|
?SLOG(debug, #{msg => "RECV", packet => emqx_packet:format(Packet)}),
|
||||||
ok = inc_incoming_stats(Packet),
|
ok = inc_incoming_stats(Packet),
|
||||||
NState = case emqx_pd:get_counter(incoming_pubs) >
|
NState = case emqx_pd:get_counter(incoming_pubs) >
|
||||||
get_active_n(Type, Listener) of
|
get_active_n(Type, Listener) of
|
||||||
|
|
@ -617,15 +629,27 @@ handle_outgoing(Packets, State = #state{mqtt_piggyback = MQTTPiggyback,
|
||||||
|
|
||||||
serialize_and_inc_stats_fun(#state{serialize = Serialize}) ->
|
serialize_and_inc_stats_fun(#state{serialize = Serialize}) ->
|
||||||
fun(Packet) ->
|
fun(Packet) ->
|
||||||
case emqx_frame:serialize_pkt(Packet, Serialize) of
|
try emqx_frame:serialize_pkt(Packet, Serialize) of
|
||||||
<<>> -> ?LOG(warning, "~s is discarded due to the frame is too large.",
|
<<>> -> ?SLOG(warning, #{msg => "packet_discarded",
|
||||||
[emqx_packet:format(Packet)]),
|
reason => "frame_too_large",
|
||||||
|
packet => emqx_packet:format(Packet)}),
|
||||||
ok = emqx_metrics:inc('delivery.dropped.too_large'),
|
ok = emqx_metrics:inc('delivery.dropped.too_large'),
|
||||||
ok = emqx_metrics:inc('delivery.dropped'),
|
ok = emqx_metrics:inc('delivery.dropped'),
|
||||||
<<>>;
|
<<>>;
|
||||||
Data -> ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]),
|
Data -> ?SLOG(debug, #{msg => "SEND", packet => Packet}),
|
||||||
ok = inc_outgoing_stats(Packet),
|
ok = inc_outgoing_stats(Packet),
|
||||||
Data
|
Data
|
||||||
|
catch
|
||||||
|
%% Maybe Never happen.
|
||||||
|
throw : ?FRAME_SERIALIZE_ERROR(Reason) ->
|
||||||
|
?SLOG(info, #{ reason => Reason
|
||||||
|
, input_packet => Packet}),
|
||||||
|
erlang:error(?FRAME_SERIALIZE_ERROR(Reason));
|
||||||
|
error : Reason : Stacktrace ->
|
||||||
|
?SLOG(error, #{ input_packet => Packet
|
||||||
|
, exception => Reason
|
||||||
|
, stacktrace => Stacktrace}),
|
||||||
|
erlang:error(frame_serialize_error)
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -791,4 +815,4 @@ get_ws_opts(Type, Listener, Key) ->
|
||||||
emqx_config:get_listener_conf(Type, Listener, [websocket, Key]).
|
emqx_config:get_listener_conf(Type, Listener, [websocket, Key]).
|
||||||
|
|
||||||
get_active_n(Type, Listener) ->
|
get_active_n(Type, Listener) ->
|
||||||
emqx_config:get_listener_conf(Type, Listener, [tcp, active_n]).
|
emqx_config:get_listener_conf(Type, Listener, [tcp, active_n]).
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ t_validity_period(_) ->
|
||||||
ok = emqx_alarm:activate(a),
|
ok = emqx_alarm:activate(a),
|
||||||
ok = emqx_alarm:deactivate(a),
|
ok = emqx_alarm:deactivate(a),
|
||||||
?assertNotEqual({error, not_found}, get_alarm(a, emqx_alarm:get_alarms(deactivated))),
|
?assertNotEqual({error, not_found}, get_alarm(a, emqx_alarm:get_alarms(deactivated))),
|
||||||
ct:sleep(2000),
|
ct:sleep(3000),
|
||||||
?assertEqual({error, not_found}, get_alarm(a, emqx_alarm:get_alarms(deactivated))).
|
?assertEqual({error, not_found}, get_alarm(a, emqx_alarm:get_alarms(deactivated))).
|
||||||
|
|
||||||
get_alarm(Name, [Alarm = #{name := Name} | _More]) ->
|
get_alarm(Name, [Alarm = #{name := Name} | _More]) ->
|
||||||
|
|
|
||||||
|
|
@ -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})),
|
||||||
|
|
|
||||||
|
|
@ -37,38 +37,82 @@ init_per_suite(Config) ->
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_ct_helpers:stop_apps([]).
|
emqx_ct_helpers:stop_apps([]).
|
||||||
|
|
||||||
|
init_per_testcase(Case, Config) ->
|
||||||
|
?MODULE:Case({init, Config}).
|
||||||
|
|
||||||
|
end_per_testcase(Case, Config) ->
|
||||||
|
?MODULE:Case({'end', Config}).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% PubSub Test
|
%% PubSub Test
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
t_stats_fun(_) ->
|
t_stats_fun({init, Config}) ->
|
||||||
Subscribers = emqx_stats:getstat('subscribers.count'),
|
Parent = self(),
|
||||||
Subscriptions = emqx_stats:getstat('subscriptions.count'),
|
F = fun Loop() ->
|
||||||
Subopts = emqx_stats:getstat('suboptions.count'),
|
N1 = emqx_stats:getstat('subscribers.count'),
|
||||||
|
N2 = emqx_stats:getstat('subscriptions.count'),
|
||||||
|
N3 = emqx_stats:getstat('suboptions.count'),
|
||||||
|
case N1 + N2 + N3 =:= 0 of
|
||||||
|
true ->
|
||||||
|
Parent ! {ready, self()},
|
||||||
|
exit(normal);
|
||||||
|
false ->
|
||||||
|
receive
|
||||||
|
stop ->
|
||||||
|
exit(normal)
|
||||||
|
after
|
||||||
|
100 ->
|
||||||
|
Loop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Pid = spawn_link(F),
|
||||||
|
receive
|
||||||
|
{ready, P} when P =:= Pid->
|
||||||
|
Config
|
||||||
|
after
|
||||||
|
5000 ->
|
||||||
|
Pid ! stop,
|
||||||
|
ct:fail("timedout_waiting_for_sub_stats_to_reach_zero")
|
||||||
|
end;
|
||||||
|
t_stats_fun(Config) when is_list(Config) ->
|
||||||
ok = emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
|
ok = emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
|
||||||
ok = emqx_broker:subscribe(<<"topic2">>, <<"clientid">>),
|
ok = emqx_broker:subscribe(<<"topic2">>, <<"clientid">>),
|
||||||
|
%% ensure stats refreshed
|
||||||
emqx_broker:stats_fun(),
|
emqx_broker:stats_fun(),
|
||||||
ct:sleep(10),
|
%% emqx_stats:set_stat is a gen_server cast
|
||||||
?assertEqual(Subscribers + 2, emqx_stats:getstat('subscribers.count')),
|
%% make a synced call sync
|
||||||
?assertEqual(Subscribers + 2, emqx_stats:getstat('subscribers.max')),
|
ignored = gen_server:call(emqx_stats, call, infinity),
|
||||||
?assertEqual(Subscriptions + 2, emqx_stats:getstat('subscriptions.count')),
|
?assertEqual(2, emqx_stats:getstat('subscribers.count')),
|
||||||
?assertEqual(Subscriptions + 2, emqx_stats:getstat('subscriptions.max')),
|
?assertEqual(2, emqx_stats:getstat('subscribers.max')),
|
||||||
?assertEqual(Subopts + 2, emqx_stats:getstat('suboptions.count')),
|
?assertEqual(2, emqx_stats:getstat('subscriptions.count')),
|
||||||
?assertEqual(Subopts + 2, emqx_stats:getstat('suboptions.max')).
|
?assertEqual(2, emqx_stats:getstat('subscriptions.max')),
|
||||||
|
?assertEqual(2, emqx_stats:getstat('suboptions.count')),
|
||||||
|
?assertEqual(2, emqx_stats:getstat('suboptions.max'));
|
||||||
|
t_stats_fun({'end', _Config}) ->
|
||||||
|
ok = emqx_broker:unsubscribe(<<"topic">>),
|
||||||
|
ok = emqx_broker:unsubscribe(<<"topic2">>).
|
||||||
|
|
||||||
t_subscribed(_) ->
|
t_subscribed({init, Config}) ->
|
||||||
emqx_broker:subscribe(<<"topic">>),
|
emqx_broker:subscribe(<<"topic">>),
|
||||||
|
Config;
|
||||||
|
t_subscribed(Config) when is_list(Config) ->
|
||||||
?assertEqual(false, emqx_broker:subscribed(undefined, <<"topic">>)),
|
?assertEqual(false, emqx_broker:subscribed(undefined, <<"topic">>)),
|
||||||
?assertEqual(true, emqx_broker:subscribed(self(), <<"topic">>)),
|
?assertEqual(true, emqx_broker:subscribed(self(), <<"topic">>));
|
||||||
|
t_subscribed({'end', _Config}) ->
|
||||||
emqx_broker:unsubscribe(<<"topic">>).
|
emqx_broker:unsubscribe(<<"topic">>).
|
||||||
|
|
||||||
t_subscribed_2(_) ->
|
t_subscribed_2({init, Config}) ->
|
||||||
emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
|
emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
|
||||||
%?assertEqual(true, emqx_broker:subscribed(<<"clientid">>, <<"topic">>)),
|
Config;
|
||||||
?assertEqual(true, emqx_broker:subscribed(self(), <<"topic">>)),
|
t_subscribed_2(Config) when is_list(Config) ->
|
||||||
|
?assertEqual(true, emqx_broker:subscribed(self(), <<"topic">>));
|
||||||
|
t_subscribed_2({'end', _Config}) ->
|
||||||
emqx_broker:unsubscribe(<<"topic">>).
|
emqx_broker:unsubscribe(<<"topic">>).
|
||||||
|
|
||||||
t_subopts(_) ->
|
t_subopts({init, Config}) -> Config;
|
||||||
|
t_subopts(Config) when is_list(Config) ->
|
||||||
?assertEqual(false, emqx_broker:set_subopts(<<"topic">>, #{qos => 1})),
|
?assertEqual(false, emqx_broker:set_subopts(<<"topic">>, #{qos => 1})),
|
||||||
?assertEqual(undefined, emqx_broker:get_subopts(self(), <<"topic">>)),
|
?assertEqual(undefined, emqx_broker:get_subopts(self(), <<"topic">>)),
|
||||||
?assertEqual(undefined, emqx_broker:get_subopts(<<"clientid">>, <<"topic">>)),
|
?assertEqual(undefined, emqx_broker:get_subopts(<<"clientid">>, <<"topic">>)),
|
||||||
|
|
@ -85,42 +129,54 @@ t_subopts(_) ->
|
||||||
|
|
||||||
?assertEqual(true, emqx_broker:set_subopts(<<"topic">>, #{qos => 0})),
|
?assertEqual(true, emqx_broker:set_subopts(<<"topic">>, #{qos => 0})),
|
||||||
?assertEqual(#{nl => 0, qos => 0, rap => 0, rh => 0, subid => <<"clientid">>},
|
?assertEqual(#{nl => 0, qos => 0, rap => 0, rh => 0, subid => <<"clientid">>},
|
||||||
emqx_broker:get_subopts(self(), <<"topic">>)),
|
emqx_broker:get_subopts(self(), <<"topic">>));
|
||||||
|
t_subopts({'end', _Config}) ->
|
||||||
emqx_broker:unsubscribe(<<"topic">>).
|
emqx_broker:unsubscribe(<<"topic">>).
|
||||||
|
|
||||||
t_topics(_) ->
|
t_topics({init, Config}) ->
|
||||||
Topics = [<<"topic">>, <<"topic/1">>, <<"topic/2">>],
|
Topics = [<<"topic">>, <<"topic/1">>, <<"topic/2">>],
|
||||||
ok = emqx_broker:subscribe(lists:nth(1, Topics), <<"clientId">>),
|
[{topics, Topics} | Config];
|
||||||
ok = emqx_broker:subscribe(lists:nth(2, Topics), <<"clientId">>),
|
t_topics(Config) when is_list(Config) ->
|
||||||
ok = emqx_broker:subscribe(lists:nth(3, Topics), <<"clientId">>),
|
Topics = [T1, T2, T3] = proplists:get_value(topics, Config),
|
||||||
|
ok = emqx_broker:subscribe(T1, <<"clientId">>),
|
||||||
|
ok = emqx_broker:subscribe(T2, <<"clientId">>),
|
||||||
|
ok = emqx_broker:subscribe(T3, <<"clientId">>),
|
||||||
Topics1 = emqx_broker:topics(),
|
Topics1 = emqx_broker:topics(),
|
||||||
?assertEqual(true, lists:foldl(fun(Topic, Acc) ->
|
?assertEqual(true, lists:foldl(fun(Topic, Acc) ->
|
||||||
case lists:member(Topic, Topics1) of
|
case lists:member(Topic, Topics1) of
|
||||||
true -> Acc;
|
true -> Acc;
|
||||||
false -> false
|
false -> false
|
||||||
end
|
end
|
||||||
end, true, Topics)),
|
end, true, Topics));
|
||||||
emqx_broker:unsubscribe(lists:nth(1, Topics)),
|
t_topics({'end', Config}) ->
|
||||||
emqx_broker:unsubscribe(lists:nth(2, Topics)),
|
Topics = proplists:get_value(topics, Config),
|
||||||
emqx_broker:unsubscribe(lists:nth(3, Topics)).
|
lists:foreach(fun(T) -> emqx_broker:unsubscribe(T) end, Topics).
|
||||||
|
|
||||||
t_subscribers(_) ->
|
t_subscribers({init, Config}) ->
|
||||||
emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
|
emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
|
||||||
?assertEqual([self()], emqx_broker:subscribers(<<"topic">>)),
|
Config;
|
||||||
|
t_subscribers(Config) when is_list(Config) ->
|
||||||
|
?assertEqual([self()], emqx_broker:subscribers(<<"topic">>));
|
||||||
|
t_subscribers({'end', _Config}) ->
|
||||||
emqx_broker:unsubscribe(<<"topic">>).
|
emqx_broker:unsubscribe(<<"topic">>).
|
||||||
|
|
||||||
t_subscriptions(_) ->
|
t_subscriptions({init, Config}) ->
|
||||||
emqx_broker:subscribe(<<"topic">>, <<"clientid">>, #{qos => 1}),
|
emqx_broker:subscribe(<<"topic">>, <<"clientid">>, #{qos => 1}),
|
||||||
ok = timer:sleep(100),
|
Config;
|
||||||
|
t_subscriptions(Config) when is_list(Config) ->
|
||||||
|
ct:sleep(100),
|
||||||
?assertEqual(#{nl => 0, qos => 1, rap => 0, rh => 0, subid => <<"clientid">>},
|
?assertEqual(#{nl => 0, qos => 1, rap => 0, rh => 0, subid => <<"clientid">>},
|
||||||
proplists:get_value(<<"topic">>, emqx_broker:subscriptions(self()))),
|
proplists:get_value(<<"topic">>, emqx_broker:subscriptions(self()))),
|
||||||
?assertEqual(#{nl => 0, qos => 1, rap => 0, rh => 0, subid => <<"clientid">>},
|
?assertEqual(#{nl => 0, qos => 1, rap => 0, rh => 0, subid => <<"clientid">>},
|
||||||
proplists:get_value(<<"topic">>, emqx_broker:subscriptions(<<"clientid">>))),
|
proplists:get_value(<<"topic">>, emqx_broker:subscriptions(<<"clientid">>)));
|
||||||
|
t_subscriptions({'end', _Config}) ->
|
||||||
emqx_broker:unsubscribe(<<"topic">>).
|
emqx_broker:unsubscribe(<<"topic">>).
|
||||||
|
|
||||||
t_sub_pub(_) ->
|
t_sub_pub({init, Config}) ->
|
||||||
ok = emqx_broker:subscribe(<<"topic">>),
|
ok = emqx_broker:subscribe(<<"topic">>),
|
||||||
ct:sleep(10),
|
Config;
|
||||||
|
t_sub_pub(Config) when is_list(Config) ->
|
||||||
|
ct:sleep(100),
|
||||||
emqx_broker:safe_publish(emqx_message:make(ct, <<"topic">>, <<"hello">>)),
|
emqx_broker:safe_publish(emqx_message:make(ct, <<"topic">>, <<"hello">>)),
|
||||||
?assert(
|
?assert(
|
||||||
receive
|
receive
|
||||||
|
|
@ -130,16 +186,22 @@ t_sub_pub(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end).
|
end);
|
||||||
|
t_sub_pub({'end', _Config}) ->
|
||||||
|
ok = emqx_broker:unsubscribe(<<"topic">>).
|
||||||
|
|
||||||
t_nosub_pub(_) ->
|
t_nosub_pub({init, Config}) -> Config;
|
||||||
|
t_nosub_pub({'end', _Config}) -> ok;
|
||||||
|
t_nosub_pub(Config) when is_list(Config) ->
|
||||||
?assertEqual(0, emqx_metrics:val('messages.dropped')),
|
?assertEqual(0, emqx_metrics:val('messages.dropped')),
|
||||||
emqx_broker:publish(emqx_message:make(ct, <<"topic">>, <<"hello">>)),
|
emqx_broker:publish(emqx_message:make(ct, <<"topic">>, <<"hello">>)),
|
||||||
?assertEqual(1, emqx_metrics:val('messages.dropped')).
|
?assertEqual(1, emqx_metrics:val('messages.dropped')).
|
||||||
|
|
||||||
t_shared_subscribe(_) ->
|
t_shared_subscribe({init, Config}) ->
|
||||||
emqx_broker:subscribe(<<"topic">>, <<"clientid">>, #{share => <<"group">>}),
|
emqx_broker:subscribe(<<"topic">>, <<"clientid">>, #{share => <<"group">>}),
|
||||||
ct:sleep(10),
|
ct:sleep(100),
|
||||||
|
Config;
|
||||||
|
t_shared_subscribe(Config) when is_list(Config) ->
|
||||||
emqx_broker:safe_publish(emqx_message:make(ct, <<"topic">>, <<"hello">>)),
|
emqx_broker:safe_publish(emqx_message:make(ct, <<"topic">>, <<"hello">>)),
|
||||||
?assert(receive
|
?assert(receive
|
||||||
{deliver, <<"topic">>, #message{payload = <<"hello">>}} ->
|
{deliver, <<"topic">>, #message{payload = <<"hello">>}} ->
|
||||||
|
|
@ -149,9 +211,12 @@ t_shared_subscribe(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end);
|
||||||
|
t_shared_subscribe({'end', _Config}) ->
|
||||||
emqx_broker:unsubscribe(<<"$share/group/topic">>).
|
emqx_broker:unsubscribe(<<"$share/group/topic">>).
|
||||||
|
|
||||||
|
t_shared_subscribe_2({init, Config}) -> Config;
|
||||||
|
t_shared_subscribe_2({'end', _Config}) -> ok;
|
||||||
t_shared_subscribe_2(_) ->
|
t_shared_subscribe_2(_) ->
|
||||||
{ok, ConnPid} = emqtt:start_link([{clean_start, true}, {clientid, <<"clientid">>}]),
|
{ok, ConnPid} = emqtt:start_link([{clean_start, true}, {clientid, <<"clientid">>}]),
|
||||||
{ok, _} = emqtt:connect(ConnPid),
|
{ok, _} = emqtt:connect(ConnPid),
|
||||||
|
|
@ -173,6 +238,8 @@ t_shared_subscribe_2(_) ->
|
||||||
emqtt:disconnect(ConnPid),
|
emqtt:disconnect(ConnPid),
|
||||||
emqtt:disconnect(ConnPid2).
|
emqtt:disconnect(ConnPid2).
|
||||||
|
|
||||||
|
t_shared_subscribe_3({init, Config}) -> Config;
|
||||||
|
t_shared_subscribe_3({'end', _Config}) -> ok;
|
||||||
t_shared_subscribe_3(_) ->
|
t_shared_subscribe_3(_) ->
|
||||||
{ok, ConnPid} = emqtt:start_link([{clean_start, true}, {clientid, <<"clientid">>}]),
|
{ok, ConnPid} = emqtt:start_link([{clean_start, true}, {clientid, <<"clientid">>}]),
|
||||||
{ok, _} = emqtt:connect(ConnPid),
|
{ok, _} = emqtt:connect(ConnPid),
|
||||||
|
|
@ -189,11 +256,13 @@ t_shared_subscribe_3(_) ->
|
||||||
emqtt:disconnect(ConnPid),
|
emqtt:disconnect(ConnPid),
|
||||||
emqtt:disconnect(ConnPid2).
|
emqtt:disconnect(ConnPid2).
|
||||||
|
|
||||||
t_shard(_) ->
|
t_shard({init, Config}) ->
|
||||||
ok = meck:new(emqx_broker_helper, [passthrough, no_history]),
|
ok = meck:new(emqx_broker_helper, [passthrough, no_history]),
|
||||||
ok = meck:expect(emqx_broker_helper, get_sub_shard, fun(_, _) -> 1 end),
|
ok = meck:expect(emqx_broker_helper, get_sub_shard, fun(_, _) -> 1 end),
|
||||||
emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
|
emqx_broker:subscribe(<<"topic">>, <<"clientid">>),
|
||||||
ct:sleep(10),
|
Config;
|
||||||
|
t_shard(Config) when is_list(Config) ->
|
||||||
|
ct:sleep(100),
|
||||||
emqx_broker:safe_publish(emqx_message:make(ct, <<"topic">>, <<"hello">>)),
|
emqx_broker:safe_publish(emqx_message:make(ct, <<"topic">>, <<"hello">>)),
|
||||||
?assert(
|
?assert(
|
||||||
receive
|
receive
|
||||||
|
|
@ -203,7 +272,9 @@ t_shard(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end);
|
||||||
|
t_shard({'end', _Config}) ->
|
||||||
|
emqx_broker:unsubscribe(<<"topic">>),
|
||||||
ok = meck:unload(emqx_broker_helper).
|
ok = meck:unload(emqx_broker_helper).
|
||||||
|
|
||||||
recv_msgs(Count) ->
|
recv_msgs(Count) ->
|
||||||
|
|
|
||||||
|
|
@ -141,11 +141,8 @@ t_open_session_race_condition(_) ->
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
N = 1000,
|
N = 1000,
|
||||||
[spawn(
|
Pids = lists:flatten([[spawn_monitor(OpenASession), spawn_monitor(OpenASession)] ||
|
||||||
fun() ->
|
_ <- lists:seq(1, N)]),
|
||||||
spawn(OpenASession),
|
|
||||||
spawn(OpenASession)
|
|
||||||
end) || _ <- lists:seq(1, N)],
|
|
||||||
|
|
||||||
WaitingRecv = fun _Wr(N1, N2, 0) ->
|
WaitingRecv = fun _Wr(N1, N2, 0) ->
|
||||||
{N1, N2};
|
{N1, N2};
|
||||||
|
|
@ -158,14 +155,29 @@ t_open_session_race_condition(_) ->
|
||||||
|
|
||||||
{Succeeded, Failed} = WaitingRecv(0, 0, 2 * N),
|
{Succeeded, Failed} = WaitingRecv(0, 0, 2 * N),
|
||||||
ct:pal("Race condition status: succeeded=~p failed=~p~n", [Succeeded, Failed]),
|
ct:pal("Race condition status: succeeded=~p failed=~p~n", [Succeeded, Failed]),
|
||||||
|
?assertEqual(2 * N, length(Pids)),
|
||||||
|
WaitForDowns =
|
||||||
|
fun _Wd([{Pid, _Ref}]) -> Pid;
|
||||||
|
_Wd(Pids0) ->
|
||||||
|
receive
|
||||||
|
{'DOWN', DownRef, process, DownPid, _} ->
|
||||||
|
?assert(lists:member({DownPid, DownRef}, Pids0)),
|
||||||
|
_Wd(lists:delete({DownPid, DownRef}, Pids0))
|
||||||
|
after
|
||||||
|
10000 ->
|
||||||
|
exit(timeout)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Winner = WaitForDowns(Pids),
|
||||||
|
|
||||||
?assertMatch([_], ets:lookup(emqx_channel, ClientId)),
|
?assertMatch([_], ets:lookup(emqx_channel, ClientId)),
|
||||||
[Pid] = emqx_cm:lookup_channels(ClientId),
|
?assertEqual([Winner], emqx_cm:lookup_channels(ClientId)),
|
||||||
?assertMatch([_], ets:lookup(emqx_channel_conn, {ClientId, Pid})),
|
?assertMatch([_], ets:lookup(emqx_channel_conn, {ClientId, Winner})),
|
||||||
?assertMatch([_], ets:lookup(emqx_channel_registry, ClientId)),
|
?assertMatch([_], ets:lookup(emqx_channel_registry, ClientId)),
|
||||||
|
|
||||||
exit(Pid, kill),
|
exit(Winner, kill),
|
||||||
timer:sleep(100), %% TODO deterministic
|
receive {'DOWN', _, process, Winner, _} -> ok end,
|
||||||
|
ignored = gen_server:call(emqx_cm, ignore, infinity), %% sync
|
||||||
?assertEqual([], emqx_cm:lookup_channels(ClientId)).
|
?assertEqual([], emqx_cm:lookup_channels(ClientId)).
|
||||||
|
|
||||||
t_discard_session(_) ->
|
t_discard_session(_) ->
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
-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").
|
||||||
-include_lib("emqx_ct_helpers/include/emqx_ct.hrl").
|
|
||||||
|
-define(ASSERT_FRAME_THROW(Reason, Expr),
|
||||||
|
?assertThrow(?FRAME_PARSE_ERROR(Reason), Expr)).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
[{group, parse},
|
[{group, parse},
|
||||||
|
|
@ -113,7 +115,7 @@ init_per_group(_Group, Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_group(_Group, _Config) ->
|
end_per_group(_Group, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_parse_cont(_) ->
|
t_parse_cont(_) ->
|
||||||
Packet = ?CONNECT_PACKET(#mqtt_packet_connect{}),
|
Packet = ?CONNECT_PACKET(#mqtt_packet_connect{}),
|
||||||
|
|
@ -127,15 +129,15 @@ t_parse_cont(_) ->
|
||||||
|
|
||||||
t_parse_frame_too_large(_) ->
|
t_parse_frame_too_large(_) ->
|
||||||
Packet = ?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, payload(1000)),
|
Packet = ?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, payload(1000)),
|
||||||
?catch_error(frame_too_large, parse_serialize(Packet, #{max_size => 256})),
|
?ASSERT_FRAME_THROW(frame_too_large, parse_serialize(Packet, #{max_size => 256})),
|
||||||
?catch_error(frame_too_large, parse_serialize(Packet, #{max_size => 512})),
|
?ASSERT_FRAME_THROW(frame_too_large, parse_serialize(Packet, #{max_size => 512})),
|
||||||
?assertEqual(Packet, parse_serialize(Packet, #{max_size => 2048, version => ?MQTT_PROTO_V4})).
|
?assertEqual(Packet, parse_serialize(Packet, #{max_size => 2048, version => ?MQTT_PROTO_V4})).
|
||||||
|
|
||||||
t_parse_frame_malformed_variable_byte_integer(_) ->
|
t_parse_frame_malformed_variable_byte_integer(_) ->
|
||||||
MalformedPayload = << <<16#80>> || _ <- lists:seq(1, 4) >>,
|
MalformedPayload = << <<16#80>> || _ <- lists:seq(1, 6) >>,
|
||||||
ParseState = emqx_frame:initial_parse_state(#{}),
|
ParseState = emqx_frame:initial_parse_state(#{}),
|
||||||
?catch_error(malformed_variable_byte_integer,
|
?ASSERT_FRAME_THROW(malformed_variable_byte_integer,
|
||||||
emqx_frame:parse(MalformedPayload, ParseState)).
|
emqx_frame:parse(MalformedPayload, ParseState)).
|
||||||
|
|
||||||
t_serialize_parse_v3_connect(_) ->
|
t_serialize_parse_v3_connect(_) ->
|
||||||
Bin = <<16,37,0,6,77,81,73,115,100,112,3,2,0,60,0,23,109,111,115,
|
Bin = <<16,37,0,6,77,81,73,115,100,112,3,2,0,60,0,23,109,111,115,
|
||||||
|
|
@ -329,7 +331,7 @@ t_serialize_parse_qos1_publish(_) ->
|
||||||
?assertEqual(Bin, serialize_to_binary(Packet)),
|
?assertEqual(Bin, serialize_to_binary(Packet)),
|
||||||
?assertMatch(Packet, parse_to_packet(Bin, #{strict_mode => true})),
|
?assertMatch(Packet, parse_to_packet(Bin, #{strict_mode => true})),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 0, <<>>))),
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 0, <<>>))),
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
_ = parse_serialize(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 0, <<>>), #{strict_mode => false}).
|
_ = parse_serialize(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 0, <<>>), #{strict_mode => false}).
|
||||||
|
|
||||||
|
|
@ -340,7 +342,7 @@ t_serialize_parse_qos2_publish(_) ->
|
||||||
?assertEqual(Bin, serialize_to_binary(Packet)),
|
?assertEqual(Bin, serialize_to_binary(Packet)),
|
||||||
?assertMatch(Packet, parse_to_packet(Bin, #{strict_mode => true})),
|
?assertMatch(Packet, parse_to_packet(Bin, #{strict_mode => true})),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 0, <<>>))),
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 0, <<>>))),
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
_ = parse_serialize(?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 0, <<>>), #{strict_mode => false}).
|
_ = parse_serialize(?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 0, <<>>), #{strict_mode => false}).
|
||||||
|
|
||||||
|
|
@ -360,7 +362,7 @@ t_serialize_parse_puback(_) ->
|
||||||
?assertEqual(<<64,2,0,1>>, serialize_to_binary(Packet)),
|
?assertEqual(<<64,2,0,1>>, serialize_to_binary(Packet)),
|
||||||
?assertEqual(Packet, parse_serialize(Packet)),
|
?assertEqual(Packet, parse_serialize(Packet)),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?PUBACK_PACKET(0))),
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?PUBACK_PACKET(0))),
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
?PUBACK_PACKET(0) = parse_serialize(?PUBACK_PACKET(0), #{strict_mode => false}).
|
?PUBACK_PACKET(0) = parse_serialize(?PUBACK_PACKET(0), #{strict_mode => false}).
|
||||||
|
|
||||||
|
|
@ -381,7 +383,7 @@ t_serialize_parse_pubrec(_) ->
|
||||||
?assertEqual(<<5:4,0:4,2,0,1>>, serialize_to_binary(Packet)),
|
?assertEqual(<<5:4,0:4,2,0,1>>, serialize_to_binary(Packet)),
|
||||||
?assertEqual(Packet, parse_serialize(Packet)),
|
?assertEqual(Packet, parse_serialize(Packet)),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?PUBREC_PACKET(0))),
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?PUBREC_PACKET(0))),
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
?PUBREC_PACKET(0) = parse_serialize(?PUBREC_PACKET(0), #{strict_mode => false}).
|
?PUBREC_PACKET(0) = parse_serialize(?PUBREC_PACKET(0), #{strict_mode => false}).
|
||||||
|
|
||||||
|
|
@ -397,11 +399,11 @@ t_serialize_parse_pubrel(_) ->
|
||||||
%% PUBREL with bad qos 0
|
%% PUBREL with bad qos 0
|
||||||
Bin0 = <<6:4,0:4,2,0,1>>,
|
Bin0 = <<6:4,0:4,2,0,1>>,
|
||||||
?assertMatch(Packet, parse_to_packet(Bin0, #{strict_mode => false})),
|
?assertMatch(Packet, parse_to_packet(Bin0, #{strict_mode => false})),
|
||||||
?catch_error(bad_frame_header, parse_to_packet(Bin0, #{strict_mode => true})),
|
?ASSERT_FRAME_THROW(bad_frame_header, parse_to_packet(Bin0, #{strict_mode => true})),
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
?PUBREL_PACKET(0) = parse_serialize(?PUBREL_PACKET(0), #{strict_mode => false}),
|
?PUBREL_PACKET(0) = parse_serialize(?PUBREL_PACKET(0), #{strict_mode => false}),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?PUBREL_PACKET(0))).
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?PUBREL_PACKET(0))).
|
||||||
|
|
||||||
t_serialize_parse_pubrel_v5(_) ->
|
t_serialize_parse_pubrel_v5(_) ->
|
||||||
Packet = ?PUBREL_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}),
|
Packet = ?PUBREL_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}),
|
||||||
|
|
@ -415,7 +417,7 @@ t_serialize_parse_pubcomp(_) ->
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
?PUBCOMP_PACKET(0) = parse_serialize(?PUBCOMP_PACKET(0), #{strict_mode => false}),
|
?PUBCOMP_PACKET(0) = parse_serialize(?PUBCOMP_PACKET(0), #{strict_mode => false}),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?PUBCOMP_PACKET(0))).
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?PUBCOMP_PACKET(0))).
|
||||||
|
|
||||||
t_serialize_parse_pubcomp_v5(_) ->
|
t_serialize_parse_pubcomp_v5(_) ->
|
||||||
Packet = ?PUBCOMP_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}),
|
Packet = ?PUBCOMP_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}),
|
||||||
|
|
@ -434,12 +436,12 @@ t_serialize_parse_subscribe(_) ->
|
||||||
?assertMatch(Packet, parse_to_packet(Bin0, #{strict_mode => false})),
|
?assertMatch(Packet, parse_to_packet(Bin0, #{strict_mode => false})),
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
_ = parse_to_packet(Bin0, #{strict_mode => false}),
|
_ = parse_to_packet(Bin0, #{strict_mode => false}),
|
||||||
?catch_error(bad_frame_header, parse_to_packet(Bin0, #{strict_mode => true})),
|
?ASSERT_FRAME_THROW(bad_frame_header, parse_to_packet(Bin0, #{strict_mode => true})),
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
_ = parse_serialize(?SUBSCRIBE_PACKET(0, TopicFilters), #{strict_mode => false}),
|
_ = parse_serialize(?SUBSCRIBE_PACKET(0, TopicFilters), #{strict_mode => false}),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?SUBSCRIBE_PACKET(0, TopicFilters))),
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?SUBSCRIBE_PACKET(0, TopicFilters))),
|
||||||
?catch_error(bad_subqos, parse_serialize(?SUBSCRIBE_PACKET(1, [{<<"t">>, #{qos => 3}}]))).
|
?ASSERT_FRAME_THROW(bad_subqos, parse_serialize(?SUBSCRIBE_PACKET(1, [{<<"t">>, #{qos => 3}}]))).
|
||||||
|
|
||||||
t_serialize_parse_subscribe_v5(_) ->
|
t_serialize_parse_subscribe_v5(_) ->
|
||||||
TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0}},
|
TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0}},
|
||||||
|
|
@ -453,7 +455,7 @@ t_serialize_parse_suback(_) ->
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
_ = parse_serialize(?SUBACK_PACKET(0, [?QOS_0]), #{strict_mode => false}),
|
_ = parse_serialize(?SUBACK_PACKET(0, [?QOS_0]), #{strict_mode => false}),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?SUBACK_PACKET(0, [?QOS_0]))).
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?SUBACK_PACKET(0, [?QOS_0]))).
|
||||||
|
|
||||||
t_serialize_parse_suback_v5(_) ->
|
t_serialize_parse_suback_v5(_) ->
|
||||||
Packet = ?SUBACK_PACKET(1, #{'Reason-String' => <<"success">>,
|
Packet = ?SUBACK_PACKET(1, #{'Reason-String' => <<"success">>,
|
||||||
|
|
@ -471,11 +473,11 @@ t_serialize_parse_unsubscribe(_) ->
|
||||||
%% UNSUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[<<"TopicA">>])
|
%% UNSUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[<<"TopicA">>])
|
||||||
Bin0 = <<?UNSUBSCRIBE:4,0:4,10,0,2,0,6,84,111,112,105,99,65>>,
|
Bin0 = <<?UNSUBSCRIBE:4,0:4,10,0,2,0,6,84,111,112,105,99,65>>,
|
||||||
?assertMatch(Packet, parse_to_packet(Bin0, #{strict_mode => false})),
|
?assertMatch(Packet, parse_to_packet(Bin0, #{strict_mode => false})),
|
||||||
?catch_error(bad_frame_header, parse_to_packet(Bin0, #{strict_mode => true})),
|
?ASSERT_FRAME_THROW(bad_frame_header, parse_to_packet(Bin0, #{strict_mode => true})),
|
||||||
%% strict_mode = false
|
%% strict_mode = false
|
||||||
_ = parse_serialize(?UNSUBSCRIBE_PACKET(0, [<<"TopicA">>]), #{strict_mode => false}),
|
_ = parse_serialize(?UNSUBSCRIBE_PACKET(0, [<<"TopicA">>]), #{strict_mode => false}),
|
||||||
%% strict_mode = true
|
%% strict_mode = true
|
||||||
?catch_error(bad_packet_id, parse_serialize(?UNSUBSCRIBE_PACKET(0, [<<"TopicA">>]))).
|
?ASSERT_FRAME_THROW(bad_packet_id, parse_serialize(?UNSUBSCRIBE_PACKET(0, [<<"TopicA">>]))).
|
||||||
|
|
||||||
t_serialize_parse_unsubscribe_v5(_) ->
|
t_serialize_parse_unsubscribe_v5(_) ->
|
||||||
Props = #{'User-Property' => [{<<"key">>, <<"val">>}]},
|
Props = #{'User-Property' => [{<<"key">>, <<"val">>}]},
|
||||||
|
|
@ -550,4 +552,3 @@ parse_to_packet(Bin, Opts) ->
|
||||||
Packet.
|
Packet.
|
||||||
|
|
||||||
payload(Len) -> iolist_to_binary(lists:duplicate(Len, 1)).
|
payload(Len) -> iolist_to_binary(lists:duplicate(Len, 1)).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,9 @@
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("emqx_ct_helpers/include/emqx_ct.hrl").
|
|
||||||
|
|
||||||
all() -> emqx_ct:all(?MODULE).
|
all() -> emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
t_contain(_) ->
|
t_contain(_) ->
|
||||||
Inflight = emqx_inflight:insert(k, v, emqx_inflight:new()),
|
Inflight = emqx_inflight:insert(k, v, emqx_inflight:new()),
|
||||||
?assert(emqx_inflight:contain(k, Inflight)),
|
?assert(emqx_inflight:contain(k, Inflight)),
|
||||||
|
|
@ -41,12 +40,12 @@ t_insert(_) ->
|
||||||
?assertEqual(2, emqx_inflight:size(Inflight)),
|
?assertEqual(2, emqx_inflight:size(Inflight)),
|
||||||
?assertEqual({value, 1}, emqx_inflight:lookup(a, Inflight)),
|
?assertEqual({value, 1}, emqx_inflight:lookup(a, Inflight)),
|
||||||
?assertEqual({value, 2}, emqx_inflight:lookup(b, Inflight)),
|
?assertEqual({value, 2}, emqx_inflight:lookup(b, Inflight)),
|
||||||
?catch_error({key_exists, a}, emqx_inflight:insert(a, 1, Inflight)).
|
?assertError({key_exists, a}, emqx_inflight:insert(a, 1, Inflight)).
|
||||||
|
|
||||||
t_update(_) ->
|
t_update(_) ->
|
||||||
Inflight = emqx_inflight:insert(k, v, emqx_inflight:new()),
|
Inflight = emqx_inflight:insert(k, v, emqx_inflight:new()),
|
||||||
?assertEqual(Inflight, emqx_inflight:update(k, v, Inflight)),
|
?assertEqual(Inflight, emqx_inflight:update(k, v, Inflight)),
|
||||||
?catch_error(function_clause, emqx_inflight:update(badkey, v, Inflight)).
|
?assertError(function_clause, emqx_inflight:update(badkey, v, Inflight)).
|
||||||
|
|
||||||
t_resize(_) ->
|
t_resize(_) ->
|
||||||
Inflight = emqx_inflight:insert(k, v, emqx_inflight:new(2)),
|
Inflight = emqx_inflight:insert(k, v, emqx_inflight:new(2)),
|
||||||
|
|
|
||||||
|
|
@ -141,13 +141,6 @@ t_undefined_headers(_) ->
|
||||||
Msg2 = emqx_message:set_header(c, 3, Msg),
|
Msg2 = emqx_message:set_header(c, 3, Msg),
|
||||||
?assertEqual(3, emqx_message:get_header(c, Msg2)).
|
?assertEqual(3, emqx_message:get_header(c, Msg2)).
|
||||||
|
|
||||||
t_format(_) ->
|
|
||||||
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
|
|
||||||
io:format("~s~n", [emqx_message:format(Msg)]),
|
|
||||||
Msg1 = emqx_message:set_header(properties, #{'Subscription-Identifier' => 1},
|
|
||||||
emqx_message:set_flag(dup, Msg)),
|
|
||||||
io:format("~s~n", [emqx_message:format(Msg1)]).
|
|
||||||
|
|
||||||
t_is_expired(_) ->
|
t_is_expired(_) ->
|
||||||
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
|
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
|
||||||
?assertNot(emqx_message:is_expired(Msg)),
|
?assertNot(emqx_message:is_expired(Msg)),
|
||||||
|
|
@ -206,7 +199,9 @@ t_to_map(_) ->
|
||||||
{headers, #{}},
|
{headers, #{}},
|
||||||
{topic, <<"topic">>},
|
{topic, <<"topic">>},
|
||||||
{payload, <<"payload">>},
|
{payload, <<"payload">>},
|
||||||
{timestamp, emqx_message:timestamp(Msg)}],
|
{timestamp, emqx_message:timestamp(Msg)},
|
||||||
|
{extra, []}
|
||||||
|
],
|
||||||
?assertEqual(List, emqx_message:to_list(Msg)),
|
?assertEqual(List, emqx_message:to_list(Msg)),
|
||||||
?assertEqual(maps:from_list(List), emqx_message:to_map(Msg)).
|
?assertEqual(maps:from_list(List), emqx_message:to_map(Msg)).
|
||||||
|
|
||||||
|
|
@ -219,6 +214,8 @@ t_from_map(_) ->
|
||||||
headers => #{},
|
headers => #{},
|
||||||
topic => <<"topic">>,
|
topic => <<"topic">>,
|
||||||
payload => <<"payload">>,
|
payload => <<"payload">>,
|
||||||
timestamp => emqx_message:timestamp(Msg)},
|
timestamp => emqx_message:timestamp(Msg),
|
||||||
|
extra => []
|
||||||
|
},
|
||||||
?assertEqual(Map, emqx_message:to_map(Msg)),
|
?assertEqual(Map, emqx_message:to_map(Msg)),
|
||||||
?assertEqual(Msg, emqx_message:from_map(emqx_message:to_map(Msg))).
|
?assertEqual(Msg, emqx_message:from_map(emqx_message:to_map(Msg))).
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@
|
||||||
|
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("emqx_ct_helpers/include/emqx_ct.hrl").
|
|
||||||
|
|
||||||
all() -> emqx_ct:all(?MODULE).
|
all() -> emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
|
@ -30,14 +29,14 @@ t_id(_) ->
|
||||||
fun({Id, Prop}) ->
|
fun({Id, Prop}) ->
|
||||||
?assertEqual(Id, emqx_mqtt_props:id(element(1, Prop)))
|
?assertEqual(Id, emqx_mqtt_props:id(element(1, Prop)))
|
||||||
end),
|
end),
|
||||||
?catch_error({bad_property, 'Bad-Property'}, emqx_mqtt_props:id('Bad-Property')).
|
?assertError({bad_property, 'Bad-Property'}, emqx_mqtt_props:id('Bad-Property')).
|
||||||
|
|
||||||
t_name(_) ->
|
t_name(_) ->
|
||||||
foreach_prop(
|
foreach_prop(
|
||||||
fun({Id, Prop}) ->
|
fun({Id, Prop}) ->
|
||||||
?assertEqual(emqx_mqtt_props:name(Id), element(1, Prop))
|
?assertEqual(emqx_mqtt_props:name(Id), element(1, Prop))
|
||||||
end),
|
end),
|
||||||
?catch_error({unsupported_property, 16#FF}, emqx_mqtt_props:name(16#FF)).
|
?assertError({unsupported_property, 16#FF}, emqx_mqtt_props:name(16#FF)).
|
||||||
|
|
||||||
t_filter(_) ->
|
t_filter(_) ->
|
||||||
ConnProps = #{'Session-Expiry-Interval' => 1,
|
ConnProps = #{'Session-Expiry-Interval' => 1,
|
||||||
|
|
@ -60,7 +59,7 @@ t_validate(_) ->
|
||||||
},
|
},
|
||||||
ok = emqx_mqtt_props:validate(ConnProps),
|
ok = emqx_mqtt_props:validate(ConnProps),
|
||||||
BadProps = #{'Unknown-Property' => 10},
|
BadProps = #{'Unknown-Property' => 10},
|
||||||
?catch_error({bad_property,'Unknown-Property'},
|
?assertError({bad_property,'Unknown-Property'},
|
||||||
emqx_mqtt_props:validate(BadProps)).
|
emqx_mqtt_props:validate(BadProps)).
|
||||||
|
|
||||||
t_validate_value(_) ->
|
t_validate_value(_) ->
|
||||||
|
|
@ -68,11 +67,11 @@ t_validate_value(_) ->
|
||||||
ok = emqx_mqtt_props:validate(#{'Reason-String' => <<"Unknown Reason">>}),
|
ok = emqx_mqtt_props:validate(#{'Reason-String' => <<"Unknown Reason">>}),
|
||||||
ok = emqx_mqtt_props:validate(#{'User-Property' => {<<"Prop">>, <<"Val">>}}),
|
ok = emqx_mqtt_props:validate(#{'User-Property' => {<<"Prop">>, <<"Val">>}}),
|
||||||
ok = emqx_mqtt_props:validate(#{'User-Property' => [{<<"Prop">>, <<"Val">>}]}),
|
ok = emqx_mqtt_props:validate(#{'User-Property' => [{<<"Prop">>, <<"Val">>}]}),
|
||||||
?catch_error({bad_property_value, {'Payload-Format-Indicator', 16#FFFF}},
|
?assertError({bad_property_value, {'Payload-Format-Indicator', 16#FFFF}},
|
||||||
emqx_mqtt_props:validate(#{'Payload-Format-Indicator' => 16#FFFF})),
|
emqx_mqtt_props:validate(#{'Payload-Format-Indicator' => 16#FFFF})),
|
||||||
?catch_error({bad_property_value, {'Server-Keep-Alive', 16#FFFFFF}},
|
?assertError({bad_property_value, {'Server-Keep-Alive', 16#FFFFFF}},
|
||||||
emqx_mqtt_props:validate(#{'Server-Keep-Alive' => 16#FFFFFF})),
|
emqx_mqtt_props:validate(#{'Server-Keep-Alive' => 16#FFFFFF})),
|
||||||
?catch_error({bad_property_value, {'Will-Delay-Interval', -16#FF}},
|
?assertError({bad_property_value, {'Will-Delay-Interval', -16#FF}},
|
||||||
emqx_mqtt_props:validate(#{'Will-Delay-Interval' => -16#FF})).
|
emqx_mqtt_props:validate(#{'Will-Delay-Interval' => -16#FF})).
|
||||||
|
|
||||||
foreach_prop(Fun) ->
|
foreach_prop(Fun) ->
|
||||||
|
|
@ -86,4 +85,4 @@ foreach_prop(Fun) ->
|
||||||
% error('TODO').
|
% error('TODO').
|
||||||
|
|
||||||
% t_get(_) ->
|
% t_get(_) ->
|
||||||
% error('TODO').
|
% error('TODO').
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,483 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2019 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% The proper types extension for EMQ X
|
||||||
|
|
||||||
|
-module(emqx_proper_types).
|
||||||
|
|
||||||
|
-include_lib("proper/include/proper.hrl").
|
||||||
|
-include("emqx.hrl").
|
||||||
|
|
||||||
|
%% High level Types
|
||||||
|
-export([ conninfo/0
|
||||||
|
, clientinfo/0
|
||||||
|
, sessioninfo/0
|
||||||
|
, connack_return_code/0
|
||||||
|
, message/0
|
||||||
|
, topictab/0
|
||||||
|
, topic/0
|
||||||
|
, systopic/0
|
||||||
|
, subopts/0
|
||||||
|
, nodename/0
|
||||||
|
, normal_topic/0
|
||||||
|
, normal_topic_filter/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Basic Types
|
||||||
|
-export([ url/0
|
||||||
|
, ip/0
|
||||||
|
, port/0
|
||||||
|
, limited_atom/0
|
||||||
|
, limited_latin_atom/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Iterators
|
||||||
|
-export([ nof/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Types High level
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% Type defined emqx_types.erl - conninfo()
|
||||||
|
conninfo() ->
|
||||||
|
Keys = [{socktype, socktype()},
|
||||||
|
{sockname, peername()},
|
||||||
|
{peername, peername()},
|
||||||
|
{peercert, peercert()},
|
||||||
|
{conn_mod, conn_mod()},
|
||||||
|
{proto_name, proto_name()},
|
||||||
|
{proto_ver, non_neg_integer()},
|
||||||
|
{clean_start, boolean()},
|
||||||
|
{clientid, clientid()},
|
||||||
|
{username, username()},
|
||||||
|
{conn_props, properties()},
|
||||||
|
{connected, boolean()},
|
||||||
|
{connected_at, timestamp()},
|
||||||
|
{keepalive, range(0, 16#ffff)},
|
||||||
|
{receive_maximum, non_neg_integer()},
|
||||||
|
{expiry_interval, non_neg_integer()}],
|
||||||
|
?LET({Ks, M}, {Keys, map(limited_atom(), limited_any_term())},
|
||||||
|
begin
|
||||||
|
maps:merge(maps:from_list(Ks), M)
|
||||||
|
end).
|
||||||
|
|
||||||
|
clientinfo() ->
|
||||||
|
Keys = [{zone, zone()},
|
||||||
|
{protocol, protocol()},
|
||||||
|
{peerhost, ip()},
|
||||||
|
{sockport, port()},
|
||||||
|
{clientid, clientid()},
|
||||||
|
{username, username()},
|
||||||
|
{is_bridge, boolean()},
|
||||||
|
{is_supuser, boolean()},
|
||||||
|
{mountpoint, maybe(utf8())},
|
||||||
|
{ws_cookie, maybe(list())}
|
||||||
|
% password,
|
||||||
|
% auth_result,
|
||||||
|
% anonymous,
|
||||||
|
% cn,
|
||||||
|
% dn,
|
||||||
|
],
|
||||||
|
?LET({Ks, M}, {Keys, map(limited_atom(), limited_any_term())},
|
||||||
|
begin
|
||||||
|
maps:merge(maps:from_list(Ks), M)
|
||||||
|
end).
|
||||||
|
|
||||||
|
%% See emqx_session:session() type define
|
||||||
|
sessioninfo() ->
|
||||||
|
?LET(Session, {session,
|
||||||
|
subscriptions(), % subscriptions
|
||||||
|
non_neg_integer(), % max_subscriptions
|
||||||
|
boolean(), % upgrade_qos
|
||||||
|
inflight(), % emqx_inflight:inflight()
|
||||||
|
mqueue(), % emqx_mqueue:mqueue()
|
||||||
|
packet_id(), % next_pkt_id
|
||||||
|
safty_timeout(), % retry_interval
|
||||||
|
awaiting_rel(), % awaiting_rel
|
||||||
|
non_neg_integer(), % max_awaiting_rel
|
||||||
|
safty_timeout(), % await_rel_timeout
|
||||||
|
timestamp() % created_at
|
||||||
|
},
|
||||||
|
emqx_session:info(Session)).
|
||||||
|
|
||||||
|
subscriptions() ->
|
||||||
|
?LET(L, list({topic(), subopts()}), maps:from_list(L)).
|
||||||
|
|
||||||
|
inflight() ->
|
||||||
|
?LET(MaxLen, non_neg_integer(),
|
||||||
|
begin
|
||||||
|
?LET(Msgs, limited_list(MaxLen, {packet_id(), message(), timestamp()}),
|
||||||
|
begin
|
||||||
|
lists:foldl(fun({PktId, Msg, Ts}, Ift) ->
|
||||||
|
try
|
||||||
|
emqx_inflight:insert(PktId, {Msg, Ts}, Ift)
|
||||||
|
catch _:_ ->
|
||||||
|
Ift
|
||||||
|
end
|
||||||
|
end, emqx_inflight:new(MaxLen), Msgs)
|
||||||
|
end)
|
||||||
|
end).
|
||||||
|
|
||||||
|
mqueue() ->
|
||||||
|
?LET({MaxLen, IsStoreQos0}, {non_neg_integer(), boolean()},
|
||||||
|
begin
|
||||||
|
?LET(Msgs, limited_list(MaxLen, message()),
|
||||||
|
begin
|
||||||
|
Q = emqx_mqueue:init(#{max_len => MaxLen, store_qos0 => IsStoreQos0}),
|
||||||
|
lists:foldl(fun(Msg, Acc) ->
|
||||||
|
{_Dropped, NQ} = emqx_mqueue:in(Msg, Acc),
|
||||||
|
NQ
|
||||||
|
end, Q, Msgs)
|
||||||
|
end)
|
||||||
|
end).
|
||||||
|
|
||||||
|
message() ->
|
||||||
|
#message{
|
||||||
|
id = emqx_guid:gen(),
|
||||||
|
qos = qos(),
|
||||||
|
from = from(),
|
||||||
|
flags = flags(),
|
||||||
|
headers = map(limited_latin_atom(), limited_any_term()), %% headers
|
||||||
|
topic = topic(),
|
||||||
|
payload = payload(),
|
||||||
|
timestamp = timestamp(),
|
||||||
|
extra = []
|
||||||
|
}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
flags() ->
|
||||||
|
?LET({Dup, Retain}, {boolean(), boolean()}, #{dup => Dup, retain => Retain}).
|
||||||
|
|
||||||
|
packet_id() ->
|
||||||
|
range(1, 16#ffff).
|
||||||
|
|
||||||
|
awaiting_rel() ->
|
||||||
|
?LET(L, list({packet_id(), timestamp()}), maps:from_list(L)).
|
||||||
|
|
||||||
|
connack_return_code() ->
|
||||||
|
oneof([ success
|
||||||
|
, protocol_error
|
||||||
|
, client_identifier_not_valid
|
||||||
|
, bad_username_or_password
|
||||||
|
, bad_clientid_or_password
|
||||||
|
, username_or_password_undefined
|
||||||
|
, password_error
|
||||||
|
, not_authorized
|
||||||
|
, server_unavailable
|
||||||
|
, server_busy
|
||||||
|
, banned
|
||||||
|
, bad_authentication_method
|
||||||
|
]).
|
||||||
|
|
||||||
|
topictab() ->
|
||||||
|
non_empty(list({topic(), subopts()})).
|
||||||
|
|
||||||
|
topic() ->
|
||||||
|
oneof([normal_topic(),
|
||||||
|
normal_topic_filter(),
|
||||||
|
systopic_broker(), systopic_present(), systopic_stats(),
|
||||||
|
systopic_metrics(), systopic_alarms(), systopic_mon(),
|
||||||
|
sharetopic()]).
|
||||||
|
|
||||||
|
subopts() ->
|
||||||
|
?LET({Nl, Qos, Rap, Rh},
|
||||||
|
{range(0, 1), qos(),
|
||||||
|
range(0, 1), range(0, 1)},
|
||||||
|
#{nl => Nl, qos => Qos, rap => Rap, rh => Rh}).
|
||||||
|
|
||||||
|
qos() ->
|
||||||
|
range(0, 2).
|
||||||
|
|
||||||
|
from() ->
|
||||||
|
oneof([limited_latin_atom()]).
|
||||||
|
|
||||||
|
payload() ->
|
||||||
|
binary().
|
||||||
|
|
||||||
|
safty_timeout() ->
|
||||||
|
non_neg_integer().
|
||||||
|
|
||||||
|
nodename() ->
|
||||||
|
?LET({Name, Ip}, {non_empty(list(latin_char())), ip()},
|
||||||
|
begin
|
||||||
|
binary_to_atom(iolist_to_binary([Name, "@", inet:ntoa(Ip)]), utf8)
|
||||||
|
end).
|
||||||
|
|
||||||
|
systopic() ->
|
||||||
|
oneof(
|
||||||
|
[systopic_broker(), systopic_present(), systopic_stats(),
|
||||||
|
systopic_metrics(), systopic_alarms(), systopic_mon()]).
|
||||||
|
|
||||||
|
systopic_broker() ->
|
||||||
|
Topics = [<<"">>, <<"version">>, <<"uptime">>, <<"datetime">>, <<"sysdescr">>],
|
||||||
|
?LET({Nodename, T},
|
||||||
|
{nodename(), oneof(Topics)},
|
||||||
|
begin
|
||||||
|
case byte_size(T) of
|
||||||
|
0 -> <<"$SYS/brokers">>;
|
||||||
|
_ ->
|
||||||
|
<<"$SYS/brokers/", (ensure_bin(Nodename))/binary, "/", T/binary>>
|
||||||
|
end
|
||||||
|
end).
|
||||||
|
|
||||||
|
systopic_present() ->
|
||||||
|
?LET({Nodename, ClientId, T},
|
||||||
|
{nodename(), clientid(), oneof([<<"connected">>, <<"disconnected">>])},
|
||||||
|
begin
|
||||||
|
<<"$SYS/brokers/", (ensure_bin(Nodename))/binary, "/clients/", (ensure_bin(ClientId))/binary, "/", T/binary>>
|
||||||
|
end).
|
||||||
|
|
||||||
|
systopic_stats() ->
|
||||||
|
Topics = [<<"connections/max">>, <<"connections/count">>,
|
||||||
|
<<"suboptions/max">>, <<"suboptions/count">>,
|
||||||
|
<<"subscribers/max">>, <<"subscribers/count">>,
|
||||||
|
<<"subscriptions/max">>, <<"subscriptions/count">>,
|
||||||
|
<<"subscriptions/shared/max">>, <<"subscriptions/shared/count">>,
|
||||||
|
<<"topics/max">>, <<"topics/count">>,
|
||||||
|
<<"routes/max">>, <<"routes/count">>
|
||||||
|
],
|
||||||
|
?LET({Nodename, T},
|
||||||
|
{nodename(), oneof(Topics)},
|
||||||
|
<<"$SYS/brokers/", (ensure_bin(Nodename))/binary, "/stats/", T/binary>>).
|
||||||
|
|
||||||
|
systopic_metrics() ->
|
||||||
|
Topics = [<<"bytes/received">>, <<"bytes/sent">>,
|
||||||
|
<<"packets/received">>, <<"packets/sent">>,
|
||||||
|
<<"packets/connect/received">>, <<"packets/connack/sent">>,
|
||||||
|
<<"packets/publish/received">>, <<"packets/publish/sent">>,
|
||||||
|
<<"packets/publish/error">>, <<"packets/publish/auth_error">>,
|
||||||
|
<<"packets/publish/dropped">>,
|
||||||
|
<<"packets/puback/received">>, <<"packets/puback/sent">>,
|
||||||
|
<<"packets/puback/inuse">>, <<"packets/puback/missed">>,
|
||||||
|
<<"packets/pubrec/received">>, <<"packets/pubrec/sent">>,
|
||||||
|
<<"packets/pubrec/inuse">>, <<"packets/pubrec/missed">>,
|
||||||
|
<<"packets/pubrel/received">>, <<"packets/pubrel/sent">>,
|
||||||
|
<<"packets/pubrel/missed">>,
|
||||||
|
<<"packets/pubcomp/received">>, <<"packets/pubcomp/sent">>,
|
||||||
|
<<"packets/pubcomp/inuse">>, <<"packets/pubcomp/missed">>,
|
||||||
|
<<"packets/subscribe/received">>, <<"packets/subscribe/error">>,
|
||||||
|
<<"packets/subscribe/auth_error">>, <<"packets/suback/sent">>,
|
||||||
|
<<"packets/unsubscribe/received">>, <<"packets/unsuback/sent">>,
|
||||||
|
<<"packets/pingreq/received">>, <<"packets/pingresp/sent">>,
|
||||||
|
<<"packets/disconnect/received">>, <<"packets/disconnect/sent">>,
|
||||||
|
<<"packets/auth/received">>, <<"packets/auth/sent">>,
|
||||||
|
<<"messages/received">>, <<"messages/sent">>,
|
||||||
|
<<"messages/qos0/received">>, <<"messages/qos0/sent">>,
|
||||||
|
<<"messages/qos1/received">>, <<"messages/qos1/sent">>,
|
||||||
|
<<"messages/qos2/received">>, <<"messages/qos2/sent">>,
|
||||||
|
<<"messages/publish">>, <<"messages/dropped">>,
|
||||||
|
<<"messages/dropped/expired">>, <<"messages/dropped/no_subscribers">>,
|
||||||
|
<<"messages/forward">>, <<"messages/retained">>,
|
||||||
|
<<"messages/delayed">>, <<"messages/delivered">>,
|
||||||
|
<<"messages/acked">>],
|
||||||
|
?LET({Nodename, T},
|
||||||
|
{nodename(), oneof(Topics)},
|
||||||
|
<<"$SYS/brokers/", (ensure_bin(Nodename))/binary, "/metrics/", T/binary>>).
|
||||||
|
|
||||||
|
systopic_alarms() ->
|
||||||
|
?LET({Nodename, T},
|
||||||
|
{nodename(), oneof([<<"alert">>, <<"clear">>])},
|
||||||
|
<<"$SYS/brokers/", (ensure_bin(Nodename))/binary, "/alarms/", T/binary>>).
|
||||||
|
|
||||||
|
systopic_mon() ->
|
||||||
|
Topics = [<<"long_gc">>, <<"long_schedule">>,
|
||||||
|
<<"large_heap">>, <<"busy_port">>, <<"busy_dist_port">>],
|
||||||
|
?LET({Nodename, T},
|
||||||
|
{nodename(), oneof(Topics)},
|
||||||
|
<<"$SYS/brokers/", (ensure_bin(Nodename))/binary, "/sysmon/", T/binary>>).
|
||||||
|
|
||||||
|
sharetopic() ->
|
||||||
|
?LET({Type, Grp, T},
|
||||||
|
{oneof([<<"$queue">>, <<"$share">>]), list(latin_char()), normal_topic()},
|
||||||
|
<<Type/binary, "/", (iolist_to_binary(Grp))/binary, "/", T/binary>>).
|
||||||
|
|
||||||
|
normal_topic() ->
|
||||||
|
?LET(L, list(frequency([{3, latin_char()}, {1, $/}])),
|
||||||
|
list_to_binary(L)).
|
||||||
|
|
||||||
|
normal_topic_filter() ->
|
||||||
|
?LET({L, Wild}, {list(list(latin_char())), oneof(['#', '+'])},
|
||||||
|
begin
|
||||||
|
case Wild of
|
||||||
|
'#' ->
|
||||||
|
case L of
|
||||||
|
[] -> <<"#">>;
|
||||||
|
_ -> iolist_to_binary([lists:join("/", L), "/#"])
|
||||||
|
end;
|
||||||
|
'+' ->
|
||||||
|
case L of
|
||||||
|
[] -> <<"+">>;
|
||||||
|
_ ->
|
||||||
|
L1 = [case rand:uniform(3) == 1 of
|
||||||
|
true -> "+";
|
||||||
|
_ -> E
|
||||||
|
end || E <- L],
|
||||||
|
iolist_to_binary(lists:join("/", L1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Basic Types
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
maybe(T) ->
|
||||||
|
oneof([undefined, T]).
|
||||||
|
|
||||||
|
socktype() ->
|
||||||
|
oneof([tcp, udp, ssl, proxy]).
|
||||||
|
|
||||||
|
peername() ->
|
||||||
|
{ip(), port()}.
|
||||||
|
|
||||||
|
peercert() ->
|
||||||
|
%% TODO: cert?
|
||||||
|
oneof([nossl, undefined]).
|
||||||
|
|
||||||
|
conn_mod() ->
|
||||||
|
oneof([emqx_connection, emqx_ws_connection, emqx_coap_mqtt_adapter,
|
||||||
|
emqx_sn_gateway, emqx_lwm2m_protocol, emqx_gbt32960_conn,
|
||||||
|
emqx_jt808_connection, emqx_tcp_connection]).
|
||||||
|
|
||||||
|
proto_name() ->
|
||||||
|
oneof([<<"MQTT">>, <<"MQTT-SN">>, <<"CoAP">>, <<"LwM2M">>, utf8()]).
|
||||||
|
|
||||||
|
clientid() ->
|
||||||
|
utf8().
|
||||||
|
|
||||||
|
username() ->
|
||||||
|
maybe(utf8()).
|
||||||
|
|
||||||
|
properties() ->
|
||||||
|
map(limited_latin_atom(), binary()).
|
||||||
|
|
||||||
|
%% millisecond
|
||||||
|
timestamp() ->
|
||||||
|
%% 12h <- Now -> 12h
|
||||||
|
?LET(Offset, range(-43200, 43200), erlang:system_time(millisecond) + Offset).
|
||||||
|
|
||||||
|
zone() ->
|
||||||
|
oneof([external, internal, limited_latin_atom()]).
|
||||||
|
|
||||||
|
protocol() ->
|
||||||
|
oneof([mqtt, 'mqtt-sn', coap, lwm2m, limited_latin_atom()]).
|
||||||
|
|
||||||
|
url() ->
|
||||||
|
?LET({Schema, IP, Port, Path}, {oneof(["http://", "https://"]), ip(), port(), http_path()},
|
||||||
|
begin
|
||||||
|
IP1 = case tuple_size(IP) == 8 of
|
||||||
|
true -> "[" ++ inet:ntoa(IP) ++ "]";
|
||||||
|
false -> inet:ntoa(IP)
|
||||||
|
end,
|
||||||
|
lists:concat([Schema, IP1, ":", integer_to_list(Port), "/", Path])
|
||||||
|
end).
|
||||||
|
|
||||||
|
ip() ->
|
||||||
|
oneof([ipv4(), ipv6(), ipv6_from_v4()]).
|
||||||
|
|
||||||
|
ipv4() ->
|
||||||
|
?LET(IP, {range(1, 16#ff), range(0, 16#ff),
|
||||||
|
range(0, 16#ff), range(0, 16#ff)}, IP).
|
||||||
|
|
||||||
|
ipv6() ->
|
||||||
|
?LET(IP, {range(0, 16#ff), range(0, 16#ff),
|
||||||
|
range(0, 16#ff), range(0, 16#ff),
|
||||||
|
range(0, 16#ff), range(0, 16#ff),
|
||||||
|
range(0, 16#ff), range(0, 16#ff)}, IP).
|
||||||
|
|
||||||
|
ipv6_from_v4() ->
|
||||||
|
?LET(IP, {range(1, 16#ff), range(0, 16#ff),
|
||||||
|
range(0, 16#ff), range(0, 16#ff)},
|
||||||
|
inet:ipv4_mapped_ipv6_address(IP)).
|
||||||
|
|
||||||
|
port() ->
|
||||||
|
?LET(Port, range(1, 16#ffff), Port).
|
||||||
|
|
||||||
|
http_path() ->
|
||||||
|
list(frequency([{3, latin_char()},
|
||||||
|
{1, $/}])).
|
||||||
|
|
||||||
|
latin_char() ->
|
||||||
|
oneof([integer($0, $9), integer($A, $Z), integer($a, $z)]).
|
||||||
|
|
||||||
|
limited_latin_atom() ->
|
||||||
|
oneof([ 'abc_atom'
|
||||||
|
, '0123456789'
|
||||||
|
, 'ABC-ATOM'
|
||||||
|
, 'abc123ABC'
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Avoid generating a lot of atom and causing atom table overflows
|
||||||
|
limited_atom() ->
|
||||||
|
oneof([ 'a_normal_atom'
|
||||||
|
, '10123_num_prefixed_atom'
|
||||||
|
, '___dash_prefixed_atom'
|
||||||
|
, '123'
|
||||||
|
, binary_to_atom(<<"你好_utf8_atom"/utf8>>)
|
||||||
|
, '_', ' ', '""', '#$%^&*'
|
||||||
|
%% The longest atom with 255 chars
|
||||||
|
, list_to_atom(
|
||||||
|
lists:append([ "so"
|
||||||
|
, [ $o || _ <- lists:seq(1, 243)]
|
||||||
|
, "-long-atom"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]).
|
||||||
|
|
||||||
|
limited_any_term() ->
|
||||||
|
oneof([binary(), number(), string()]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Iterators
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
nof(Ls) when is_list(Ls) ->
|
||||||
|
Len = length(Ls),
|
||||||
|
?LET(N, range(0, Len),
|
||||||
|
begin
|
||||||
|
Ns = rand_nl(N, Len, []),
|
||||||
|
[lists:nth(I, Ls) || I <- Ns]
|
||||||
|
end).
|
||||||
|
|
||||||
|
limited_list(0, T) ->
|
||||||
|
list(T);
|
||||||
|
|
||||||
|
limited_list(N, T) ->
|
||||||
|
?LET(N2, range(0, N),
|
||||||
|
begin
|
||||||
|
[T || _ <- lists:seq(1, N2)]
|
||||||
|
end).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal funcs
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-compile({inline, rand_nl/3}).
|
||||||
|
|
||||||
|
rand_nl(0, _, Acc) ->
|
||||||
|
Acc;
|
||||||
|
rand_nl(N, L, Acc) ->
|
||||||
|
R = rand:uniform(L),
|
||||||
|
case lists:member(R, Acc) of
|
||||||
|
true -> rand_nl(N, L, Acc);
|
||||||
|
_ -> rand_nl(N-1, L, [R|Acc])
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_bin(A) when is_atom(A) ->
|
||||||
|
atom_to_binary(A, utf8);
|
||||||
|
ensure_bin(B) when is_binary(B) ->
|
||||||
|
B.
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_schema_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
ssl_opts_dtls_test() ->
|
||||||
|
Sc = emqx_schema:server_ssl_opts_schema(#{versions => dtls_all_available,
|
||||||
|
ciphers => dtls_all_available}, false),
|
||||||
|
Checked = validate(Sc, #{<<"versions">> => [<<"dtlsv1.2">>, <<"dtlsv1">>]}),
|
||||||
|
?assertMatch(#{versions := ['dtlsv1.2', 'dtlsv1'],
|
||||||
|
ciphers := ["ECDHE-ECDSA-AES256-GCM-SHA384" | _]
|
||||||
|
}, Checked).
|
||||||
|
|
||||||
|
ssl_opts_tls_1_3_test() ->
|
||||||
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
||||||
|
Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>]}),
|
||||||
|
?assertNot(maps:is_key(handshake_timeout, Checked)),
|
||||||
|
?assertMatch(#{versions := ['tlsv1.3'],
|
||||||
|
ciphers := [_ | _]
|
||||||
|
}, Checked).
|
||||||
|
|
||||||
|
ssl_opts_tls_for_ranch_test() ->
|
||||||
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, true),
|
||||||
|
Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>]}),
|
||||||
|
?assertMatch(#{versions := ['tlsv1.3'],
|
||||||
|
ciphers := [_ | _],
|
||||||
|
handshake_timeout := _
|
||||||
|
}, Checked).
|
||||||
|
|
||||||
|
ssl_opts_cipher_array_test() ->
|
||||||
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
||||||
|
Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>],
|
||||||
|
<<"ciphers">> => [<<"TLS_AES_256_GCM_SHA384">>,
|
||||||
|
<<"ECDHE-ECDSA-AES256-GCM-SHA384">>]}),
|
||||||
|
?assertMatch(#{versions := ['tlsv1.3'],
|
||||||
|
ciphers := ["TLS_AES_256_GCM_SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384"]
|
||||||
|
}, Checked).
|
||||||
|
|
||||||
|
ssl_opts_cipher_comma_separated_string_test() ->
|
||||||
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
||||||
|
Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>],
|
||||||
|
<<"ciphers">> => <<"TLS_AES_256_GCM_SHA384,ECDHE-ECDSA-AES256-GCM-SHA384">>}),
|
||||||
|
?assertMatch(#{versions := ['tlsv1.3'],
|
||||||
|
ciphers := ["TLS_AES_256_GCM_SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384"]
|
||||||
|
}, Checked).
|
||||||
|
|
||||||
|
ssl_opts_tls_psk_test() ->
|
||||||
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
||||||
|
Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>]}),
|
||||||
|
?assertMatch(#{versions := ['tlsv1.2']}, Checked).
|
||||||
|
|
||||||
|
bad_cipher_test() ->
|
||||||
|
Sc = emqx_schema:server_ssl_opts_schema(#{}, false),
|
||||||
|
Reason = {bad_ciphers, ["foo"]},
|
||||||
|
?assertThrow({_Sc, [{validation_error, #{reason := Reason}}]},
|
||||||
|
validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>],
|
||||||
|
<<"ciphers">> => [<<"foo">>]})),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
validate(Schema, Data0) ->
|
||||||
|
Sc = #{ roots => [ssl_opts]
|
||||||
|
, fields => #{ssl_opts => Schema}
|
||||||
|
},
|
||||||
|
Data = Data0#{ cacertfile => <<"cacertfile">>
|
||||||
|
, certfile => <<"certfile">>
|
||||||
|
, keyfile => <<"keyfile">>
|
||||||
|
},
|
||||||
|
#{ssl_opts := Checked} =
|
||||||
|
hocon_schema:check_plain(Sc, #{<<"ssl_opts">> => Data},
|
||||||
|
#{atom_key => true}),
|
||||||
|
Checked.
|
||||||
|
|
||||||
|
ciperhs_schema_test() ->
|
||||||
|
Sc = emqx_schema:ciphers_schema(undefined),
|
||||||
|
WSc = #{roots => [{ciphers, Sc}]},
|
||||||
|
?assertThrow({_, [{validation_error, _}]},
|
||||||
|
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.
|
||||||
|
|
@ -24,21 +24,22 @@
|
||||||
|
|
||||||
-define(SYSMON, emqx_sys_mon).
|
-define(SYSMON, emqx_sys_mon).
|
||||||
|
|
||||||
|
-define(FAKE_PORT, hd(erlang:ports())).
|
||||||
|
-define(FAKE_INFO, [{timeout, 100}, {in, foo}, {out, {?MODULE, bar, 1}}]).
|
||||||
-define(INPUTINFO, [{self(), long_gc,
|
-define(INPUTINFO, [{self(), long_gc,
|
||||||
concat_str("long_gc warning: pid = ~p, info: ~p", self(), "hello"), "hello"},
|
fmt("long_gc warning: pid = ~p", [self()]), ?FAKE_INFO},
|
||||||
{self(), long_schedule,
|
{self(), long_schedule,
|
||||||
concat_str("long_schedule warning: pid = ~p, info: ~p", self(), "hello"), "hello"},
|
fmt("long_schedule warning: pid = ~p", [self()]), ?FAKE_INFO},
|
||||||
{self(), large_heap,
|
{self(), large_heap,
|
||||||
concat_str("large_heap warning: pid = ~p, info: ~p", self(), "hello"), "hello"},
|
fmt("large_heap warning: pid = ~p", [self()]), ?FAKE_INFO},
|
||||||
{self(), busy_port,
|
{self(), busy_port,
|
||||||
concat_str("busy_port warning: suspid = ~p, port = ~p",
|
fmt("busy_port warning: suspid = ~p, port = ~p",
|
||||||
self(), list_to_port("#Port<0.4>")), list_to_port("#Port<0.4>")},
|
[self(), ?FAKE_PORT]), ?FAKE_PORT},
|
||||||
{self(), busy_dist_port,
|
{self(), busy_dist_port,
|
||||||
concat_str("busy_dist_port warning: suspid = ~p, port = ~p",
|
fmt("busy_dist_port warning: suspid = ~p, port = ~p",
|
||||||
self(), list_to_port("#Port<0.4>")),list_to_port("#Port<0.4>")},
|
[self(), ?FAKE_PORT]), ?FAKE_PORT},
|
||||||
{list_to_port("#Port<0.4>"), long_schedule,
|
{?FAKE_PORT, long_schedule,
|
||||||
concat_str("long_schedule warning: port = ~p, info: ~p",
|
fmt("long_schedule warning: port = ~p", [?FAKE_PORT]), ?FAKE_INFO}
|
||||||
list_to_port("#Port<0.4>"), "hello"), "hello"}
|
|
||||||
]).
|
]).
|
||||||
|
|
||||||
all() -> emqx_ct:all(?MODULE).
|
all() -> emqx_ct:all(?MODULE).
|
||||||
|
|
@ -82,16 +83,16 @@ t_procinfo(_) ->
|
||||||
ok = meck:new(emqx_vm, [passthrough, no_history]),
|
ok = meck:new(emqx_vm, [passthrough, no_history]),
|
||||||
ok = meck:expect(emqx_vm, get_process_info, fun(_) -> [] end),
|
ok = meck:expect(emqx_vm, get_process_info, fun(_) -> [] end),
|
||||||
ok = meck:expect(emqx_vm, get_process_gc_info, fun(_) -> [] end),
|
ok = meck:expect(emqx_vm, get_process_gc_info, fun(_) -> [] end),
|
||||||
?assertEqual([], emqx_sys_mon:procinfo([])),
|
?assertEqual([{pid, undefined}], emqx_sys_mon:procinfo(undefined)),
|
||||||
ok = meck:expect(emqx_vm, get_process_info, fun(_) -> ok end),
|
ok = meck:expect(emqx_vm, get_process_info, fun(_) -> [] end),
|
||||||
ok = meck:expect(emqx_vm, get_process_gc_info, fun(_) -> undefined end),
|
ok = meck:expect(emqx_vm, get_process_gc_info, fun(_) -> undefined end),
|
||||||
?assertEqual(undefined, emqx_sys_mon:procinfo([])),
|
?assertEqual([{pid, self()}], emqx_sys_mon:procinfo(self())),
|
||||||
ok = meck:unload(emqx_vm).
|
ok = meck:unload(emqx_vm).
|
||||||
|
|
||||||
t_sys_mon(_Config) ->
|
t_sys_mon(_Config) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun({PidOrPort, SysMonName,ValidateInfo, InfoOrPort}) ->
|
fun({PidOrPort, SysMonName, ValidateInfo, InfoOrPort}) ->
|
||||||
validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort)
|
validate_sys_mon_info(PidOrPort, SysMonName, ValidateInfo, InfoOrPort)
|
||||||
end, ?INPUTINFO).
|
end, ?INPUTINFO).
|
||||||
|
|
||||||
t_sys_mon2(_Config) ->
|
t_sys_mon2(_Config) ->
|
||||||
|
|
@ -101,7 +102,7 @@ t_sys_mon2(_Config) ->
|
||||||
?assertEqual(ok, gen_server:cast(?SYSMON, ignored)),
|
?assertEqual(ok, gen_server:cast(?SYSMON, ignored)),
|
||||||
gen_server:stop(?SYSMON).
|
gen_server:stop(?SYSMON).
|
||||||
|
|
||||||
validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) ->
|
validate_sys_mon_info(PidOrPort, SysMonName, ValidateInfo, InfoOrPort) ->
|
||||||
{ok, C} = emqtt:start_link([{host, "localhost"}]),
|
{ok, C} = emqtt:start_link([{host, "localhost"}]),
|
||||||
{ok, _} = emqtt:connect(C),
|
{ok, _} = emqtt:connect(C),
|
||||||
emqtt:subscribe(C, emqx_topic:systop(lists:concat(['sysmon/', SysMonName])), qos1),
|
emqtt:subscribe(C, emqx_topic:systop(lists:concat(['sysmon/', SysMonName])), qos1),
|
||||||
|
|
@ -117,6 +118,4 @@ validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) ->
|
||||||
end,
|
end,
|
||||||
emqtt:stop(C).
|
emqtt:stop(C).
|
||||||
|
|
||||||
concat_str(ValidateInfo, InfoOrPort, Info) ->
|
fmt(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)).
|
||||||
WarnInfo = io_lib:format(ValidateInfo, [InfoOrPort, Info]),
|
|
||||||
lists:flatten(WarnInfo).
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("emqx_ct_helpers/include/emqx_ct.hrl").
|
|
||||||
|
|
||||||
-import(emqx_topic,
|
-import(emqx_topic,
|
||||||
[ wildcard/1
|
[ wildcard/1
|
||||||
|
|
@ -126,21 +125,21 @@ t_validate(_) ->
|
||||||
true = validate({filter, <<"abc/#">>}),
|
true = validate({filter, <<"abc/#">>}),
|
||||||
true = validate({filter, <<"x">>}),
|
true = validate({filter, <<"x">>}),
|
||||||
true = validate({name, <<"x//y">>}),
|
true = validate({name, <<"x//y">>}),
|
||||||
true = validate({filter, <<"sport/tennis/#">>}),
|
true = validate({filter, <<"sport/tennis/#">>}),
|
||||||
ok = ?catch_error(empty_topic, validate({name, <<>>})),
|
?assertError(empty_topic, validate({name, <<>>})),
|
||||||
ok = ?catch_error(topic_name_error, validate({name, <<"abc/#">>})),
|
?assertError(topic_name_error, validate({name, <<"abc/#">>})),
|
||||||
ok = ?catch_error(topic_too_long, validate({name, long_topic()})),
|
?assertError(topic_too_long, validate({name, long_topic()})),
|
||||||
ok = ?catch_error('topic_invalid_#', validate({filter, <<"abc/#/1">>})),
|
?assertError('topic_invalid_#', validate({filter, <<"abc/#/1">>})),
|
||||||
ok = ?catch_error(topic_invalid_char, validate({filter, <<"abc/#xzy/+">>})),
|
?assertError(topic_invalid_char, validate({filter, <<"abc/#xzy/+">>})),
|
||||||
ok = ?catch_error(topic_invalid_char, validate({filter, <<"abc/xzy/+9827">>})),
|
?assertError(topic_invalid_char, validate({filter, <<"abc/xzy/+9827">>})),
|
||||||
ok = ?catch_error(topic_invalid_char, validate({filter, <<"sport/tennis#">>})),
|
?assertError(topic_invalid_char, validate({filter, <<"sport/tennis#">>})),
|
||||||
ok = ?catch_error('topic_invalid_#', validate({filter, <<"sport/tennis/#/ranking">>})).
|
?assertError('topic_invalid_#', validate({filter, <<"sport/tennis/#/ranking">>})).
|
||||||
|
|
||||||
t_sigle_level_validate(_) ->
|
t_sigle_level_validate(_) ->
|
||||||
true = validate({filter, <<"+">>}),
|
true = validate({filter, <<"+">>}),
|
||||||
true = validate({filter, <<"+/tennis/#">>}),
|
true = validate({filter, <<"+/tennis/#">>}),
|
||||||
true = validate({filter, <<"sport/+/player1">>}),
|
true = validate({filter, <<"sport/+/player1">>}),
|
||||||
ok = ?catch_error(topic_invalid_char, validate({filter, <<"sport+">>})).
|
?assertError(topic_invalid_char, validate({filter, <<"sport+">>})).
|
||||||
|
|
||||||
t_prepend(_) ->
|
t_prepend(_) ->
|
||||||
?assertEqual(<<"ab">>, prepend(undefined, <<"ab">>)),
|
?assertEqual(<<"ab">>, prepend(undefined, <<"ab">>)),
|
||||||
|
|
@ -192,14 +191,14 @@ long_topic() ->
|
||||||
iolist_to_binary([[integer_to_list(I), "/"] || I <- lists:seq(0, 66666)]).
|
iolist_to_binary([[integer_to_list(I), "/"] || I <- lists:seq(0, 66666)]).
|
||||||
|
|
||||||
t_parse(_) ->
|
t_parse(_) ->
|
||||||
ok = ?catch_error({invalid_topic_filter, <<"$queue/t">>},
|
?assertError({invalid_topic_filter, <<"$queue/t">>},
|
||||||
parse(<<"$queue/t">>, #{share => <<"g">>})),
|
parse(<<"$queue/t">>, #{share => <<"g">>})),
|
||||||
ok = ?catch_error({invalid_topic_filter, <<"$share/g/t">>},
|
?assertError({invalid_topic_filter, <<"$share/g/t">>},
|
||||||
parse(<<"$share/g/t">>, #{share => <<"g">>})),
|
parse(<<"$share/g/t">>, #{share => <<"g">>})),
|
||||||
ok = ?catch_error({invalid_topic_filter, <<"$share/t">>},
|
?assertError({invalid_topic_filter, <<"$share/t">>},
|
||||||
parse(<<"$share/t">>)),
|
parse(<<"$share/t">>)),
|
||||||
ok = ?catch_error({invalid_topic_filter, <<"$share/+/t">>},
|
?assertError({invalid_topic_filter, <<"$share/+/t">>},
|
||||||
parse(<<"$share/+/t">>)),
|
parse(<<"$share/+/t">>)),
|
||||||
?assertEqual({<<"a/b/+/#">>, #{}}, parse(<<"a/b/+/#">>)),
|
?assertEqual({<<"a/b/+/#">>, #{}}, parse(<<"a/b/+/#">>)),
|
||||||
?assertEqual({<<"a/b/+/#">>, #{qos => 1}}, parse({<<"a/b/+/#">>, #{qos => 1}})),
|
?assertEqual({<<"a/b/+/#">>, #{qos => 1}}, parse({<<"a/b/+/#">>, #{qos => 1}})),
|
||||||
?assertEqual({<<"topic">>, #{share => <<"$queue">>}}, parse(<<"$queue/topic">>)),
|
?assertEqual({<<"topic">>, #{share => <<"$queue">>}}, parse(<<"$queue/topic">>)),
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ t_ws_check_origin(_) ->
|
||||||
?assertMatch({gun_upgrade, _},
|
?assertMatch({gun_upgrade, _},
|
||||||
start_ws_client(#{protocols => [<<"mqtt">>],
|
start_ws_client(#{protocols => [<<"mqtt">>],
|
||||||
headers => [{<<"origin">>, <<"http://localhost:18083">>}]})),
|
headers => [{<<"origin">>, <<"http://localhost:18083">>}]})),
|
||||||
?assertMatch({gun_response, {_, 500, _}},
|
?assertMatch({gun_response, {_, 403, _}},
|
||||||
start_ws_client(#{protocols => [<<"mqtt">>],
|
start_ws_client(#{protocols => [<<"mqtt">>],
|
||||||
headers => [{<<"origin">>, <<"http://localhost:18080">>}]})).
|
headers => [{<<"origin">>, <<"http://localhost:18080">>}]})).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ json_basic() ->
|
||||||
oneof([true, false, null, number(), json_string()]).
|
oneof([true, false, null, number(), json_string()]).
|
||||||
|
|
||||||
latin_atom() ->
|
latin_atom() ->
|
||||||
emqx_ct_proper_types:limited_latin_atom().
|
emqx_proper_types:limited_latin_atom().
|
||||||
|
|
||||||
json_string() -> utf8().
|
json_string() -> utf8().
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ prop_lookup() ->
|
||||||
?ALL({ClientPSKID, UserState},
|
?ALL({ClientPSKID, UserState},
|
||||||
{client_pskid(), user_state()},
|
{client_pskid(), user_state()},
|
||||||
begin
|
begin
|
||||||
case emqx_psk:lookup(psk, ClientPSKID, UserState) of
|
case emqx_tls_psk:lookup(psk, ClientPSKID, UserState) of
|
||||||
{ok, _Result} -> true;
|
{ok, _Result} -> true;
|
||||||
error -> true;
|
error -> true;
|
||||||
_Other -> false
|
_Other -> false
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -1970,8 +1970,9 @@ find_config(AuthenticatorID, AuthenticatorsConfig) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
fill_defaults(Config) ->
|
fill_defaults(Config) ->
|
||||||
#{<<"authentication">> := CheckedConfig} = hocon_schema:check_plain(
|
#{<<"authentication">> := CheckedConfig} =
|
||||||
?AUTHN, #{<<"authentication">> => Config}, #{no_conversion => true}),
|
hocon_schema:check_plain(?AUTHN, #{<<"authentication">> => Config},
|
||||||
|
#{only_fill_defaults => true}),
|
||||||
CheckedConfig.
|
CheckedConfig.
|
||||||
|
|
||||||
convert_certs(#{<<"ssl">> := SSLOpts} = Config) ->
|
convert_certs(#{<<"ssl">> := SSLOpts} = Config) ->
|
||||||
|
|
@ -2070,4 +2071,4 @@ to_list(L) when is_list(L) ->
|
||||||
to_atom(B) when is_binary(B) ->
|
to_atom(B) when is_binary(B) ->
|
||||||
binary_to_atom(B);
|
binary_to_atom(B);
|
||||||
to_atom(A) when is_atom(A) ->
|
to_atom(A) when is_atom(A) ->
|
||||||
A.
|
A.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
-export([ replace_placeholders/2
|
-export([ replace_placeholders/2
|
||||||
, replace_placeholder/2
|
, replace_placeholder/2
|
||||||
|
, check_password/3
|
||||||
|
, is_superuser/1
|
||||||
, hash/4
|
, hash/4
|
||||||
, gen_salt/0
|
, gen_salt/0
|
||||||
, bin/1
|
, bin/1
|
||||||
|
|
@ -55,6 +57,28 @@ replace_placeholder(<<"${cert-common-name}">>, Credential) ->
|
||||||
replace_placeholder(Constant, _) ->
|
replace_placeholder(Constant, _) ->
|
||||||
Constant.
|
Constant.
|
||||||
|
|
||||||
|
check_password(undefined, _Selected, _State) ->
|
||||||
|
{error, bad_username_or_password};
|
||||||
|
check_password(Password,
|
||||||
|
#{<<"password_hash">> := Hash},
|
||||||
|
#{password_hash_algorithm := bcrypt}) ->
|
||||||
|
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
||||||
|
true -> ok;
|
||||||
|
false -> {error, bad_username_or_password}
|
||||||
|
end;
|
||||||
|
check_password(Password,
|
||||||
|
#{<<"password_hash">> := Hash} = Selected,
|
||||||
|
#{password_hash_algorithm := Algorithm,
|
||||||
|
salt_position := SaltPosition}) ->
|
||||||
|
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
||||||
|
case Hash =:= hash(Algorithm, Password, Salt, SaltPosition) of
|
||||||
|
true -> ok;
|
||||||
|
false -> {error, bad_username_or_password}
|
||||||
|
end.
|
||||||
|
|
||||||
|
is_superuser(Selected) ->
|
||||||
|
#{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}.
|
||||||
|
|
||||||
hash(Algorithm, Password, Salt, prefix) ->
|
hash(Algorithm, Password, Salt, prefix) ->
|
||||||
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
|
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
|
||||||
hash(Algorithm, Password, Salt, suffix) ->
|
hash(Algorithm, Password, Salt, suffix) ->
|
||||||
|
|
@ -75,4 +99,4 @@ bin(X) -> X.
|
||||||
convert_to_sql_param(undefined) ->
|
convert_to_sql_param(undefined) ->
|
||||||
null;
|
null;
|
||||||
convert_to_sql_param(V) ->
|
convert_to_sql_param(V) ->
|
||||||
bin(V).
|
bin(V).
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
@ -156,26 +156,23 @@ authenticate(#{auth_method := _}, _) ->
|
||||||
authenticate(Credential, #{'_unique' := Unique,
|
authenticate(Credential, #{'_unique' := Unique,
|
||||||
method := Method,
|
method := Method,
|
||||||
request_timeout := RequestTimeout} = State) ->
|
request_timeout := RequestTimeout} = State) ->
|
||||||
try
|
Request = generate_request(Credential, State),
|
||||||
Request = generate_request(Credential, State),
|
case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of
|
||||||
case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of
|
{ok, 204, _Headers} -> {ok, #{is_superuser => false}};
|
||||||
{ok, 204, _Headers} -> {ok, #{is_superuser => false}};
|
{ok, 200, Headers, Body} ->
|
||||||
{ok, 200, Headers, Body} ->
|
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
||||||
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
case safely_parse_body(ContentType, Body) of
|
||||||
case safely_parse_body(ContentType, Body) of
|
{ok, NBody} ->
|
||||||
{ok, NBody} ->
|
%% TODO: Return by user property
|
||||||
%% TODO: Return by user property
|
{ok, #{is_superuser => maps:get(<<"is_superuser">>, NBody, false),
|
||||||
{ok, #{is_superuser => maps:get(<<"is_superuser">>, NBody, false),
|
user_property => NBody}};
|
||||||
user_property => NBody}};
|
{error, _Reason} ->
|
||||||
{error, _Reason} ->
|
{ok, #{is_superuser => false}}
|
||||||
{ok, #{is_superuser => false}}
|
end;
|
||||||
end;
|
{error, Reason} ->
|
||||||
{error, _Reason} ->
|
?SLOG(error, #{msg => "http_server_query_failed",
|
||||||
ignore
|
resource => Unique,
|
||||||
end
|
reason => Reason}),
|
||||||
catch
|
|
||||||
error:Reason ->
|
|
||||||
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]),
|
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -194,9 +191,9 @@ check_url(URL) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_body(Body) ->
|
check_body(Body) ->
|
||||||
lists:any(fun({_, V}) ->
|
maps:fold(fun(_K, _V, false) -> false;
|
||||||
not is_binary(V)
|
(_K, V, true) -> is_binary(V)
|
||||||
end, maps:to_list(Body)).
|
end, true, Body).
|
||||||
|
|
||||||
default_headers() ->
|
default_headers() ->
|
||||||
maps:put(<<"content-type">>,
|
maps:put(<<"content-type">>,
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,9 @@ handle_info({http, {RequestID, Result}},
|
||||||
State1 = State0#{request_id := undefined},
|
State1 = State0#{request_id := undefined},
|
||||||
case Result of
|
case Result of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Failed to request jwks endpoint(~s): ~p", [Endpoint, Reason]),
|
?SLOG(warning, #{msg => "failed_to_request_jwks_endpoint",
|
||||||
|
endpoint => Endpoint,
|
||||||
|
reason => Reason}),
|
||||||
State1;
|
State1;
|
||||||
{_StatusLine, _Headers, Body} ->
|
{_StatusLine, _Headers, Body} ->
|
||||||
try
|
try
|
||||||
|
|
@ -102,7 +104,9 @@ handle_info({http, {RequestID, Result}},
|
||||||
{_, JWKs} = JWKS#jose_jwk.keys,
|
{_, JWKs} = JWKS#jose_jwk.keys,
|
||||||
State1#{jwks := JWKs}
|
State1#{jwks := JWKs}
|
||||||
catch _:_ ->
|
catch _:_ ->
|
||||||
?LOG(error, "Invalid jwks returned from jwks endpoint(~s): ~p~n", [Endpoint, Body]),
|
?SLOG(warning, #{msg => "invalid_jwks_returned",
|
||||||
|
endpoint => Endpoint,
|
||||||
|
body => Body}),
|
||||||
State1
|
State1
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
|
|
@ -136,11 +140,16 @@ handle_options(#{endpoint := Endpoint,
|
||||||
|
|
||||||
refresh_jwks(#{endpoint := Endpoint,
|
refresh_jwks(#{endpoint := Endpoint,
|
||||||
ssl_opts := SSLOpts} = State) ->
|
ssl_opts := SSLOpts} = State) ->
|
||||||
HTTPOpts = [{timeout, 5000}, {connect_timeout, 5000}, {ssl, SSLOpts}],
|
HTTPOpts = [ {timeout, 5000}
|
||||||
|
, {connect_timeout, 5000}
|
||||||
|
, {ssl, SSLOpts}
|
||||||
|
],
|
||||||
NState = case httpc:request(get, {Endpoint, [{"Accept", "application/json"}]}, HTTPOpts,
|
NState = case httpc:request(get, {Endpoint, [{"Accept", "application/json"}]}, HTTPOpts,
|
||||||
[{body_format, binary}, {sync, false}, {receiver, self()}]) of
|
[{body_format, binary}, {sync, false}, {receiver, self()}]) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "Failed to request jwks endpoint(~s): ~p", [Endpoint, Reason]),
|
?SLOG(warning, #{msg => "failed_to_request_jwks_endpoint",
|
||||||
|
endpoint => Endpoint,
|
||||||
|
reason => Reason}),
|
||||||
State;
|
State;
|
||||||
{ok, RequestID} ->
|
{ok, RequestID} ->
|
||||||
State#{request_id := RequestID}
|
State#{request_id := RequestID}
|
||||||
|
|
|
||||||
|
|
@ -141,29 +141,27 @@ authenticate(#{password := Password} = Credential,
|
||||||
, selector := Selector0
|
, selector := Selector0
|
||||||
, '_unique' := Unique
|
, '_unique' := Unique
|
||||||
} = State) ->
|
} = State) ->
|
||||||
try
|
Selector1 = replace_placeholders(Selector0, Credential),
|
||||||
Selector1 = replace_placeholders(Selector0, Credential),
|
Selector2 = normalize_selector(Selector1),
|
||||||
Selector2 = normalize_selector(Selector1),
|
case emqx_resource:query(Unique, {find_one, Collection, Selector2, #{}}) of
|
||||||
case emqx_resource:query(Unique, {find_one, Collection, Selector2, #{}}) of
|
undefined -> ignore;
|
||||||
undefined -> ignore;
|
{error, Reason} ->
|
||||||
{error, Reason} ->
|
?SLOG(error, #{msg => "mongodb_query_failed",
|
||||||
?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
|
resource => Unique,
|
||||||
ignore;
|
reason => Reason}),
|
||||||
Doc ->
|
ignore;
|
||||||
case check_password(Password, Doc, State) of
|
Doc ->
|
||||||
ok ->
|
case check_password(Password, Doc, State) of
|
||||||
{ok, #{is_superuser => is_superuser(Doc, State)}};
|
ok ->
|
||||||
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
{ok, #{is_superuser => is_superuser(Doc, State)}};
|
||||||
?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]),
|
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
||||||
{error, bad_username_or_password};
|
?SLOG(error, #{msg => "cannot_find_password_hash_field",
|
||||||
{error, Reason} ->
|
resource => Unique,
|
||||||
{error, Reason}
|
password_hash_field => PasswordHashField}),
|
||||||
end
|
ignore;
|
||||||
end
|
{error, Reason} ->
|
||||||
catch
|
{error, Reason}
|
||||||
error:Error ->
|
end
|
||||||
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
|
|
||||||
ignore
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
destroy(#{'_unique' := Unique}) ->
|
destroy(#{'_unique' := Unique}) ->
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
@ -114,24 +114,21 @@ authenticate(#{password := Password} = Credential,
|
||||||
query := Query,
|
query := Query,
|
||||||
query_timeout := Timeout,
|
query_timeout := Timeout,
|
||||||
'_unique' := Unique} = State) ->
|
'_unique' := Unique} = State) ->
|
||||||
try
|
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
||||||
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of
|
||||||
case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of
|
{ok, _Columns, []} -> ignore;
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, Columns, Rows} ->
|
||||||
{ok, Columns, Rows} ->
|
Selected = maps:from_list(lists:zip(Columns, Rows)),
|
||||||
Selected = maps:from_list(lists:zip(Columns, Rows)),
|
case emqx_authn_utils:check_password(Password, Selected, State) of
|
||||||
case check_password(Password, Selected, State) of
|
ok ->
|
||||||
ok ->
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}};
|
{error, Reason} ->
|
||||||
{error, Reason} ->
|
{error, Reason}
|
||||||
{error, Reason}
|
end;
|
||||||
end;
|
{error, Reason} ->
|
||||||
{error, _Reason} ->
|
?SLOG(error, #{msg => "mysql_query_failed",
|
||||||
ignore
|
resource => Unique,
|
||||||
end
|
reason => Reason}),
|
||||||
catch
|
|
||||||
error:Error ->
|
|
||||||
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
|
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -143,25 +140,6 @@ destroy(#{'_unique' := Unique}) ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
check_password(undefined, _Selected, _State) ->
|
|
||||||
{error, bad_username_or_password};
|
|
||||||
check_password(Password,
|
|
||||||
#{<<"password_hash">> := Hash},
|
|
||||||
#{password_hash_algorithm := bcrypt}) ->
|
|
||||||
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, bad_username_or_password}
|
|
||||||
end;
|
|
||||||
check_password(Password,
|
|
||||||
#{<<"password_hash">> := Hash} = Selected,
|
|
||||||
#{password_hash_algorithm := Algorithm,
|
|
||||||
salt_position := SaltPosition}) ->
|
|
||||||
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
|
||||||
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, bad_username_or_password}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% TODO: Support prepare
|
%% TODO: Support prepare
|
||||||
parse_query(Query) ->
|
parse_query(Query) ->
|
||||||
case re:run(Query, ?RE_PLACEHOLDER, [global, {capture, all, binary}]) of
|
case re:run(Query, ?RE_PLACEHOLDER, [global, {capture, all, binary}]) of
|
||||||
|
|
|
||||||
|
|
@ -103,25 +103,22 @@ authenticate(#{password := Password} = Credential,
|
||||||
#{query := Query,
|
#{query := Query,
|
||||||
placeholders := PlaceHolders,
|
placeholders := PlaceHolders,
|
||||||
'_unique' := Unique} = State) ->
|
'_unique' := Unique} = State) ->
|
||||||
try
|
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
||||||
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
case emqx_resource:query(Unique, {sql, Query, Params}) of
|
||||||
case emqx_resource:query(Unique, {sql, Query, Params}) of
|
{ok, _Columns, []} -> ignore;
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, Columns, Rows} ->
|
||||||
{ok, Columns, Rows} ->
|
NColumns = [Name || #column{name = Name} <- Columns],
|
||||||
NColumns = [Name || #column{name = Name} <- Columns],
|
Selected = maps:from_list(lists:zip(NColumns, Rows)),
|
||||||
Selected = maps:from_list(lists:zip(NColumns, Rows)),
|
case emqx_authn_utils:check_password(Password, Selected, State) of
|
||||||
case check_password(Password, Selected, State) of
|
ok ->
|
||||||
ok ->
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}};
|
{error, Reason} ->
|
||||||
{error, Reason} ->
|
{error, Reason}
|
||||||
{error, Reason}
|
end;
|
||||||
end;
|
{error, Reason} ->
|
||||||
{error, _Reason} ->
|
?SLOG(error, #{msg => "postgresql_query_failed",
|
||||||
ignore
|
resource => Unique,
|
||||||
end
|
reason => Reason}),
|
||||||
catch
|
|
||||||
error:Error ->
|
|
||||||
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
|
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -133,30 +130,11 @@ destroy(#{'_unique' := Unique}) ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
check_password(undefined, _Selected, _State) ->
|
|
||||||
{error, bad_username_or_password};
|
|
||||||
check_password(Password,
|
|
||||||
#{<<"password_hash">> := Hash},
|
|
||||||
#{password_hash_algorithm := bcrypt}) ->
|
|
||||||
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, bad_username_or_password}
|
|
||||||
end;
|
|
||||||
check_password(Password,
|
|
||||||
#{<<"password_hash">> := Hash} = Selected,
|
|
||||||
#{password_hash_algorithm := Algorithm,
|
|
||||||
salt_position := SaltPosition}) ->
|
|
||||||
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
|
||||||
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, bad_username_or_password}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% TODO: Support prepare
|
%% TODO: Support prepare
|
||||||
parse_query(Query) ->
|
parse_query(Query) ->
|
||||||
case re:run(Query, ?RE_PLACEHOLDER, [global, {capture, all, binary}]) of
|
case re:run(Query, ?RE_PLACEHOLDER, [global, {capture, all, binary}]) of
|
||||||
{match, Captured} ->
|
{match, Captured} ->
|
||||||
PlaceHolders = [PlaceHolder || PlaceHolder <- Captured],
|
PlaceHolders = [PlaceHolder || [PlaceHolder] <- Captured],
|
||||||
Replacements = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Captured))],
|
Replacements = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Captured))],
|
||||||
NQuery = lists:foldl(fun({PlaceHolder, Replacement}, Query0) ->
|
NQuery = lists:foldl(fun({PlaceHolder, Replacement}, Query0) ->
|
||||||
re:replace(Query0, <<"'\\", PlaceHolder/binary, "'">>, Replacement, [{return, binary}])
|
re:replace(Query0, <<"'\\", PlaceHolder/binary, "'">>, Replacement, [{return, binary}])
|
||||||
|
|
|
||||||
|
|
@ -127,24 +127,26 @@ authenticate(#{password := Password} = Credential,
|
||||||
#{ query := {Command, Key, Fields}
|
#{ query := {Command, Key, Fields}
|
||||||
, '_unique' := Unique
|
, '_unique' := Unique
|
||||||
} = State) ->
|
} = State) ->
|
||||||
try
|
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
|
||||||
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
|
case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of
|
||||||
case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of
|
{ok, Values} ->
|
||||||
{ok, Values} ->
|
case merge(Fields, Values) of
|
||||||
Selected = merge(Fields, Values),
|
#{<<"password_hash">> := _} = Selected ->
|
||||||
case check_password(Password, Selected, State) of
|
case emqx_authn_utils:check_password(Password, Selected, State) of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, #{is_superuser => maps:get("is_superuser", Selected, false)}};
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
_ ->
|
||||||
?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
|
?SLOG(error, #{msg => "cannot_find_password_hash_field",
|
||||||
ignore
|
resource => Unique}),
|
||||||
end
|
ignore
|
||||||
catch
|
end;
|
||||||
error:{cannot_get_variable, Placeholder} ->
|
{error, Reason} ->
|
||||||
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, {cannot_get_variable, Placeholder}]),
|
?SLOG(error, #{msg => "redis_query_failed",
|
||||||
|
resource => Unique,
|
||||||
|
reason => Reason}),
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -209,27 +211,5 @@ merge(Fields, Value) when not is_list(Value) ->
|
||||||
merge(Fields, [Value]);
|
merge(Fields, [Value]);
|
||||||
merge(Fields, Values) ->
|
merge(Fields, Values) ->
|
||||||
maps:from_list(
|
maps:from_list(
|
||||||
lists:filter(fun({_, V}) ->
|
[{list_to_binary(K), V}
|
||||||
V =/= undefined
|
|| {K, V} <- lists:zip(Fields, Values), V =/= undefined]).
|
||||||
end, lists:zip(Fields, Values))).
|
|
||||||
|
|
||||||
check_password(undefined, _Selected, _State) ->
|
|
||||||
{error, bad_username_or_password};
|
|
||||||
check_password(Password,
|
|
||||||
#{"password_hash" := PasswordHash},
|
|
||||||
#{password_hash_algorithm := bcrypt}) ->
|
|
||||||
case {ok, PasswordHash} =:= bcrypt:hashpw(Password, PasswordHash) of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, bad_username_or_password}
|
|
||||||
end;
|
|
||||||
check_password(Password,
|
|
||||||
#{"password_hash" := PasswordHash} = Selected,
|
|
||||||
#{password_hash_algorithm := Algorithm,
|
|
||||||
salt_position := SaltPosition}) ->
|
|
||||||
Salt = maps:get("salt", Selected, <<>>),
|
|
||||||
case PasswordHash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
|
|
||||||
true -> ok;
|
|
||||||
false -> {error, bad_username_or_password}
|
|
||||||
end;
|
|
||||||
check_password(_Password, _Selected, _State) ->
|
|
||||||
ignore.
|
|
||||||
|
|
|
||||||
|
|
@ -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/#"]}.
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,67 @@
|
||||||
authorization {
|
authorization {
|
||||||
# sources = [
|
sources = [
|
||||||
# # {
|
# {
|
||||||
# # type: http
|
# type: http
|
||||||
# # url: "https://emqx.com"
|
# url: "https://emqx.com"
|
||||||
# # headers: {
|
# headers: {
|
||||||
# # Accept: "application/json"
|
# Accept: "application/json"
|
||||||
# # Content-Type: "application/json"
|
# Content-Type: "application/json"
|
||||||
# # }
|
# }
|
||||||
# # },
|
# },
|
||||||
# # {
|
# {
|
||||||
# # type: mysql
|
# type: mysql
|
||||||
# # server: "127.0.0.1:3306"
|
# server: "127.0.0.1:3306"
|
||||||
# # database: mqtt
|
# database: mqtt
|
||||||
# # pool_size: 1
|
# pool_size: 1
|
||||||
# # username: root
|
# username: root
|
||||||
# # password: public
|
# password: public
|
||||||
# # auto_reconnect: true
|
# auto_reconnect: true
|
||||||
# # ssl: {
|
# ssl: {
|
||||||
# # enable: true
|
# enable: true
|
||||||
# # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem"
|
# cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem"
|
||||||
# # certfile: "{{ platform_etc_dir }}/certs/client-cert.pem"
|
# certfile: "{{ platform_etc_dir }}/certs/client-cert.pem"
|
||||||
# # keyfile: "{{ platform_etc_dir }}/certs/client-key.pem"
|
# keyfile: "{{ platform_etc_dir }}/certs/client-key.pem"
|
||||||
# # }
|
# }
|
||||||
# # query: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or clientid = '%c'"
|
# query: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or clientid = '%c'"
|
||||||
# # },
|
# },
|
||||||
# # {
|
# {
|
||||||
# # type: postgresql
|
# type: postgresql
|
||||||
# # server: "127.0.0.1:5432"
|
# server: "127.0.0.1:5432"
|
||||||
# # database: mqtt
|
# database: mqtt
|
||||||
# # pool_size: 1
|
# pool_size: 1
|
||||||
# # username: root
|
# username: root
|
||||||
# # password: public
|
# password: public
|
||||||
# # auto_reconnect: true
|
# auto_reconnect: true
|
||||||
# # ssl: {enable: false}
|
# ssl: {enable: false}
|
||||||
# # query: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"
|
# query: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"
|
||||||
# # },
|
# },
|
||||||
# # {
|
# {
|
||||||
# # type: redis
|
# type: redis
|
||||||
# # server: "127.0.0.1:6379"
|
# server: "127.0.0.1:6379"
|
||||||
# # database: 0
|
# database: 0
|
||||||
# # pool_size: 1
|
# pool_size: 1
|
||||||
# # password: public
|
# password: public
|
||||||
# # auto_reconnect: true
|
# auto_reconnect: true
|
||||||
# # ssl: {enable: false}
|
# ssl: {enable: false}
|
||||||
# # cmd: "HGETALL mqtt_authz:%u"
|
# cmd: "HGETALL mqtt_authz:%u"
|
||||||
# # },
|
# },
|
||||||
# # {
|
# {
|
||||||
# # type: mongodb
|
# type: mongodb
|
||||||
# # mongo_type: single
|
# mongo_type: single
|
||||||
# # server: "127.0.0.1:27017"
|
# server: "127.0.0.1:27017"
|
||||||
# # pool_size: 1
|
# pool_size: 1
|
||||||
# # database: mqtt
|
# database: mqtt
|
||||||
# # ssl: {enable: false}
|
# ssl: {enable: false}
|
||||||
# # collection: mqtt_authz
|
# collection: mqtt_authz
|
||||||
# # selector: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] }
|
# selector: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] }
|
||||||
# # },
|
# },
|
||||||
# {
|
{
|
||||||
# type: file
|
type: built-in-database
|
||||||
# path: "{{ platform_etc_dir }}/acl.conf"
|
},
|
||||||
# }
|
{
|
||||||
# ]
|
type: file
|
||||||
|
# file is loaded into cache
|
||||||
|
path: "{{ platform_etc_dir }}/acl.conf"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,37 @@
|
||||||
(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(CMD_MOVE_TOP, <<"top">>).
|
||||||
|
-define(CMD_MOVE_BOTTOM, <<"bottom">>).
|
||||||
|
-define(CMD_MOVE_BEFORE(Before), {<<"before">>, Before}).
|
||||||
|
-define(CMD_MOVE_AFTER(After), {<<"after">>, After}).
|
||||||
|
|
||||||
-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,153 @@ 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), ?CMD_MOVE_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), ?CMD_MOVE_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, ?CMD_MOVE_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, ?CMD_MOVE_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, ?CMD_MOVE_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, ?CMD_MOVE_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(#{enable := false}) -> ok;
|
||||||
|
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, Sources).
|
||||||
|
|
||||||
|
init_source(#{enable := false} = Source) -> Source;
|
||||||
|
init_source(#{type := file,
|
||||||
path := Path
|
path := Path
|
||||||
} = Source) ->
|
} = Source) ->
|
||||||
Rules = case file:consult(Path) of
|
Rules = case file:consult(Path) of
|
||||||
|
|
@ -288,8 +212,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 +220,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 +242,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 +296,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 +331,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 +343,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,659 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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
|
||||||
|
, users/2
|
||||||
|
, user/2
|
||||||
|
, clients/2
|
||||||
|
, client/2
|
||||||
|
, all/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
api_spec() ->
|
||||||
|
{[ purge_api()
|
||||||
|
, users_api()
|
||||||
|
, user_api()
|
||||||
|
, clients_api()
|
||||||
|
, client_api()
|
||||||
|
, all_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}
|
||||||
|
].
|
||||||
|
|
||||||
|
users_api() ->
|
||||||
|
Metadata = #{
|
||||||
|
get => #{
|
||||||
|
description => "Show the list of record for username",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
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 => minirest:ref(<<"username">>)
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_USERNAME])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
post => #{
|
||||||
|
description => "Add new records for username",
|
||||||
|
requestBody => #{
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => #{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
oneOf => [ minirest:ref(<<"username">>)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_USERNAME])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"Created">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"/authorization/sources/built-in-database/username", Metadata, users}.
|
||||||
|
|
||||||
|
clients_api() ->
|
||||||
|
Metadata = #{
|
||||||
|
get => #{
|
||||||
|
description => "Show the list of record for clientid",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
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 => minirest:ref(<<"clientid">>)
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_CLIENTID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
post => #{
|
||||||
|
description => "Add new records for clientid",
|
||||||
|
requestBody => #{
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => #{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
oneOf => [ minirest:ref(<<"clientid">>)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
examples => #{
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode([?EXAMPLE_CLIENTID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"Created">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"/authorization/sources/built-in-database/clientid", Metadata, clients}.
|
||||||
|
|
||||||
|
user_api() ->
|
||||||
|
Metadata = #{
|
||||||
|
get => #{
|
||||||
|
description => "Get record info for username",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => username,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses => #{
|
||||||
|
<<"200">> => #{
|
||||||
|
description => <<"OK">>,
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => minirest:ref(<<"username">>),
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_USERNAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
<<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
put => #{
|
||||||
|
description => "Set record for username",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => username,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requestBody => #{
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => minirest:ref(<<"username">>),
|
||||||
|
examples => #{
|
||||||
|
username => #{
|
||||||
|
summary => <<"Username">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_USERNAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"Updated">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete => #{
|
||||||
|
description => "Delete one record for username",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => username,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"No Content">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"/authorization/sources/built-in-database/username/:username", Metadata, user}.
|
||||||
|
|
||||||
|
client_api() ->
|
||||||
|
Metadata = #{
|
||||||
|
get => #{
|
||||||
|
description => "Get record info for clientid",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => clientid,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses => #{
|
||||||
|
<<"200">> => #{
|
||||||
|
description => <<"OK">>,
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => minirest:ref(<<"clientid">>),
|
||||||
|
examples => #{
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_CLIENTID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
<<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
put => #{
|
||||||
|
description => "Set record for clientid",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => clientid,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requestBody => #{
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => minirest:ref(<<"clientid">>),
|
||||||
|
examples => #{
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"Clientid">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_CLIENTID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"Updated">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete => #{
|
||||||
|
description => "Delete one record for clientid",
|
||||||
|
parameters => [
|
||||||
|
#{
|
||||||
|
name => clientid,
|
||||||
|
in => path,
|
||||||
|
schema => #{
|
||||||
|
type => string
|
||||||
|
},
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"No Content">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"/authorization/sources/built-in-database/clientid/:clientid", Metadata, client}.
|
||||||
|
|
||||||
|
all_api() ->
|
||||||
|
Metadata = #{
|
||||||
|
get => #{
|
||||||
|
description => "Show the list of rules for all",
|
||||||
|
responses => #{
|
||||||
|
<<"200">> => #{
|
||||||
|
description => <<"OK">>,
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => minirest:ref(<<"clientid">>),
|
||||||
|
examples => #{
|
||||||
|
clientid => #{
|
||||||
|
summary => <<"All">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_ALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
put => #{
|
||||||
|
description => "Set the list of rules for all",
|
||||||
|
requestBody => #{
|
||||||
|
content => #{
|
||||||
|
'application/json' => #{
|
||||||
|
schema => minirest:ref(<<"all">>),
|
||||||
|
examples => #{
|
||||||
|
all => #{
|
||||||
|
summary => <<"All">>,
|
||||||
|
value => jsx:encode(?EXAMPLE_ALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses => #{
|
||||||
|
<<"204">> => #{description => <<"Created">>},
|
||||||
|
<<"400">> => emqx_mgmt_util:bad_request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"/authorization/sources/built-in-database/all", Metadata, 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}.
|
||||||
|
|
||||||
|
users(get, #{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;
|
||||||
|
users(post, #{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}.
|
||||||
|
|
||||||
|
clients(get, #{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;
|
||||||
|
clients(post, #{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}.
|
||||||
|
|
||||||
|
user(get, #{bindings := #{username := Username}}) ->
|
||||||
|
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}) 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;
|
||||||
|
user(put, #{bindings := #{username := Username},
|
||||||
|
body := #{<<"username">> := Username, <<"rules">> := Rules}}) ->
|
||||||
|
ekka_mnesia:dirty_write(#emqx_acl{
|
||||||
|
who = {?ACL_TABLE_USERNAME, Username},
|
||||||
|
rules = format_rules(Rules)
|
||||||
|
}),
|
||||||
|
{204};
|
||||||
|
user(delete, #{bindings := #{username := Username}}) ->
|
||||||
|
ekka_mnesia:dirty_delete({?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}}),
|
||||||
|
{204}.
|
||||||
|
|
||||||
|
client(get, #{bindings := #{clientid := Clientid}}) ->
|
||||||
|
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}) 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;
|
||||||
|
client(put, #{bindings := #{clientid := Clientid},
|
||||||
|
body := #{<<"clientid">> := Clientid, <<"rules">> := Rules}}) ->
|
||||||
|
ekka_mnesia:dirty_write(#emqx_acl{
|
||||||
|
who = {?ACL_TABLE_CLIENTID, Clientid},
|
||||||
|
rules = format_rules(Rules)
|
||||||
|
}),
|
||||||
|
{204};
|
||||||
|
client(delete, #{bindings := #{clientid := Clientid}}) ->
|
||||||
|
ekka_mnesia:dirty_delete({?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}}),
|
||||||
|
{204}.
|
||||||
|
|
||||||
|
all(get, _) ->
|
||||||
|
case mnesia:dirty_read(?ACL_TABLE, ?ACL_TABLE_ALL) of
|
||||||
|
[] ->
|
||||||
|
{200, #{rules => []}};
|
||||||
|
[#emqx_acl{who = ?ACL_TABLE_ALL, rules = Rules}] ->
|
||||||
|
{200, #{rules => [ #{topic => Topic,
|
||||||
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
} || {Permission, Action, Topic} <- Rules]}
|
||||||
|
}
|
||||||
|
end;
|
||||||
|
all(put, #{body := #{<<"rules">> := Rules}}) ->
|
||||||
|
ekka_mnesia:dirty_write(#emqx_acl{
|
||||||
|
who = ?ACL_TABLE_ALL,
|
||||||
|
rules = format_rules(Rules)
|
||||||
|
}),
|
||||||
|
{204}.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -19,29 +19,9 @@
|
||||||
-export([definitions/0]).
|
-export([definitions/0]).
|
||||||
|
|
||||||
definitions() ->
|
definitions() ->
|
||||||
RetruenedSources = #{
|
|
||||||
allOf => [ #{type => object,
|
|
||||||
properties => #{
|
|
||||||
annotations => #{
|
|
||||||
type => object,
|
|
||||||
required => [status],
|
|
||||||
properties => #{
|
|
||||||
id => #{
|
|
||||||
type => string
|
|
||||||
},
|
|
||||||
status => #{
|
|
||||||
type => string,
|
|
||||||
example => <<"healthy">>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, minirest:ref(<<"sources">>)
|
|
||||||
]
|
|
||||||
},
|
|
||||||
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">>)
|
||||||
|
|
@ -100,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">>],
|
||||||
|
|
@ -154,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},
|
||||||
|
|
@ -212,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},
|
||||||
|
|
@ -268,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},
|
||||||
|
|
@ -467,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],
|
||||||
|
|
@ -493,10 +488,10 @@ definitions() ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[ #{<<"returned_sources">> => RetruenedSources}
|
[ #{<<"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}
|
||||||
|
|
|
||||||
|
|
@ -35,19 +35,16 @@
|
||||||
rules => <<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
|
rules => <<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-define(EXAMPLE_RETURNED_REDIS,
|
|
||||||
maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS)
|
|
||||||
).
|
|
||||||
-define(EXAMPLE_RETURNED_FILE,
|
|
||||||
maps:put(annotations, #{status => healthy}, ?EXAMPLE_FILE)
|
|
||||||
).
|
|
||||||
|
|
||||||
-define(EXAMPLE_RETURNED,
|
-define(EXAMPLE_RETURNED,
|
||||||
#{sources => [ ?EXAMPLE_RETURNED_REDIS
|
#{sources => [ ?EXAMPLE_REDIS
|
||||||
, ?EXAMPLE_RETURNED_FILE
|
, ?EXAMPLE_FILE
|
||||||
]
|
]
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-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
|
||||||
|
|
@ -76,7 +73,7 @@ sources_api() ->
|
||||||
required => [sources],
|
required => [sources],
|
||||||
properties => #{sources => #{
|
properties => #{sources => #{
|
||||||
type => array,
|
type => array,
|
||||||
items => minirest:ref(<<"returned_sources">>)
|
items => minirest:ref(<<"sources">>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -122,7 +119,7 @@ sources_api() ->
|
||||||
'application/json' => #{
|
'application/json' => #{
|
||||||
schema => #{
|
schema => #{
|
||||||
type => array,
|
type => array,
|
||||||
items => minirest:ref(<<"returned_sources">>)
|
items => minirest:ref(<<"sources">>)
|
||||||
},
|
},
|
||||||
examples => #{
|
examples => #{
|
||||||
redis => #{
|
redis => #{
|
||||||
|
|
@ -154,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
|
||||||
}
|
}
|
||||||
|
|
@ -164,15 +169,15 @@ source_api() ->
|
||||||
description => <<"OK">>,
|
description => <<"OK">>,
|
||||||
content => #{
|
content => #{
|
||||||
'application/json' => #{
|
'application/json' => #{
|
||||||
schema => minirest:ref(<<"returned_sources">>),
|
schema => minirest:ref(<<"sources">>),
|
||||||
examples => #{
|
examples => #{
|
||||||
redis => #{
|
redis => #{
|
||||||
summary => <<"Redis">>,
|
summary => <<"Redis">>,
|
||||||
value => jsx:encode(?EXAMPLE_RETURNED_REDIS)
|
value => jsx:encode(?EXAMPLE_REDIS)
|
||||||
},
|
},
|
||||||
file => #{
|
file => #{
|
||||||
summary => <<"File">>,
|
summary => <<"File">>,
|
||||||
value => jsx:encode(?EXAMPLE_RETURNED_FILE)
|
value => jsx:encode(?EXAMPLE_FILE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,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
|
||||||
}
|
}
|
||||||
|
|
@ -223,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
|
||||||
}
|
}
|
||||||
|
|
@ -245,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
|
||||||
}
|
}
|
||||||
|
|
@ -297,109 +326,69 @@ 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,
|
||||||
enable => Enable,
|
enable => Enable,
|
||||||
rules => Rules,
|
rules => Rules
|
||||||
annotations => #{status => healthy}
|
|
||||||
}]);
|
}]);
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
lists:append(AccIn, [#{type => file,
|
lists:append(AccIn, [#{type => file,
|
||||||
enable => Enable,
|
enable => Enable,
|
||||||
rules => <<"">>,
|
rules => <<"">>
|
||||||
annotations => #{status => unhealthy}
|
|
||||||
}])
|
}])
|
||||||
end;
|
end;
|
||||||
(#{enable := false} = Source, AccIn) ->
|
|
||||||
lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]);
|
|
||||||
(#{type := _Type, annotations := #{id := Id}} = Source, AccIn) ->
|
|
||||||
NSource0 = case maps:get(server, Source, undefined) of
|
|
||||||
undefined -> Source;
|
|
||||||
Server ->
|
|
||||||
Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}
|
|
||||||
end,
|
|
||||||
NSource1 = case maps:get(servers, Source, undefined) of
|
|
||||||
undefined -> NSource0;
|
|
||||||
Servers ->
|
|
||||||
NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}
|
|
||||||
end,
|
|
||||||
NSource2 = case emqx_resource:health_check(Id) of
|
|
||||||
ok ->
|
|
||||||
NSource1#{annotations => #{status => healthy}};
|
|
||||||
_ ->
|
|
||||||
NSource1#{annotations => #{status => unhealthy}}
|
|
||||||
end,
|
|
||||||
lists:append(AccIn, [read_cert(NSource2)]);
|
|
||||||
(Source, AccIn) ->
|
(Source, AccIn) ->
|
||||||
lists:append(AccIn, [Source#{annotations => #{status => healthy}}])
|
lists:append(AccIn, [read_cert(Source)])
|
||||||
end, [], emqx_authz:lookup()),
|
end, [], get_raw_sources()),
|
||||||
{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(?CMD_PREPEND, [#{<<"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(?CMD_PREPEND, [write_cert(Body)]);
|
||||||
sources(put, #{body := Body}) when is_list(Body) ->
|
sources(put, #{body := Body}) when is_list(Body) ->
|
||||||
NBody = [ begin
|
NBody = [ begin
|
||||||
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 emqx_authz:lookup(Type) of
|
case get_raw_source(Type) of
|
||||||
{error, Reason} -> {404, #{message => atom_to_binary(Reason)}};
|
[] -> {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,
|
||||||
enable => Enable,
|
enable => Enable,
|
||||||
rules => Rules,
|
rules => Rules
|
||||||
annotations => #{status => healthy}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{code => <<"BAD_REQUEST">>,
|
||||||
message => atom_to_binary(Reason)}}
|
message => bin(Reason)}}
|
||||||
end;
|
end;
|
||||||
#{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}};
|
[Source] ->
|
||||||
#{annotations := #{id := Id}} = Source ->
|
{200, read_cert(Source)}
|
||||||
NSource0 = case maps:get(server, Source, undefined) of
|
|
||||||
undefined -> Source;
|
|
||||||
Server ->
|
|
||||||
Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}
|
|
||||||
end,
|
|
||||||
NSource1 = case maps:get(servers, Source, undefined) of
|
|
||||||
undefined -> NSource0;
|
|
||||||
Servers ->
|
|
||||||
NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}
|
|
||||||
end,
|
|
||||||
NSource2 = case emqx_resource:health_check(Id) of
|
|
||||||
ok ->
|
|
||||||
NSource1#{annotations => #{status => healthy}};
|
|
||||||
_ ->
|
|
||||||
NSource1#{annotations => #{status => unhealthy}}
|
|
||||||
end,
|
|
||||||
{200, read_cert(NSource2)}
|
|
||||||
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
|
||||||
|
|
@ -409,40 +398,52 @@ 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() ->
|
||||||
|
RawSources = emqx:get_raw_config([authorization, sources]),
|
||||||
|
Schema = #{roots => emqx_authz_schema:fields("authorization"), fields => #{}},
|
||||||
|
Conf = #{<<"sources">> => RawSources},
|
||||||
|
#{<<"sources">> := Sources} = hocon_schema:check_plain(Schema, Conf, #{only_fill_defaults => true}),
|
||||||
|
Sources.
|
||||||
|
|
||||||
|
get_raw_source(Type) ->
|
||||||
|
lists:filter(fun (#{<<"type">> := T}) ->
|
||||||
|
T =:= Type
|
||||||
|
end, get_raw_sources()).
|
||||||
|
|
||||||
update_config(Cmd, Sources) ->
|
update_config(Cmd, Sources) ->
|
||||||
case emqx_authz:update(Cmd, Sources) of
|
case emqx_authz:update(Cmd, Sources) of
|
||||||
{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;
|
||||||
_ -> ""
|
_ -> ""
|
||||||
end,
|
end,
|
||||||
Cert = case file:read_file(maps:get(certfile, SSL, "")) of
|
Cert = case file:read_file(maps:get(<<"certfile">>, SSL, "")) of
|
||||||
{ok, Cert0} -> Cert0;
|
{ok, Cert0} -> Cert0;
|
||||||
_ -> ""
|
_ -> ""
|
||||||
end,
|
end,
|
||||||
Key = case file:read_file(maps:get(keyfile, SSL, "")) of
|
Key = case file:read_file(maps:get(<<"keyfile">>, SSL, "")) of
|
||||||
{ok, Key0} -> Key0;
|
{ok, Key0} -> Key0;
|
||||||
_ -> ""
|
_ -> ""
|
||||||
end,
|
end,
|
||||||
Source#{ssl => SSL#{cacertfile => CaCert,
|
Source#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert,
|
||||||
certfile => Cert,
|
<<"certfile">> => Cert,
|
||||||
keyfile => Key
|
<<"keyfile">> => Key
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
read_cert(Source) -> Source.
|
read_cert(Source) -> Source.
|
||||||
|
|
||||||
|
|
@ -494,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,
|
||||||
|
|
@ -96,7 +96,8 @@
|
||||||
}).
|
}).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_ct:all(?MODULE).
|
[]. %% Todo: Waiting for @terry-xiaoyu to fix the config_not_found error
|
||||||
|
% emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
groups() ->
|
groups() ->
|
||||||
[].
|
[].
|
||||||
|
|
|
||||||
|
|
@ -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,54 @@ 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_channels(_InstId, _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 +280,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) ->
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue