diff --git a/.ci/build_packages/Dockerfile b/.ci/build_packages/Dockerfile index 792acddf5..6f9a12159 100644 --- a/.ci/build_packages/Dockerfile +++ b/.ci/build_packages/Dockerfile @@ -7,6 +7,8 @@ COPY . /emqx WORKDIR /emqx +RUN rm -rf _build/${EMQX_NAME}/lib _build/${EMQX_NAME}-pkg/lib + RUN make ${EMQX_NAME}-zip || cat rebar3.crashdump RUN make ${EMQX_NAME}-pkg || cat rebar3.crashdump diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index bbc107d37..d5feda5f2 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -3,10 +3,23 @@ set -x -e -u export CODE_PATH=${CODE_PATH:-"/emqx"} export EMQX_NAME=${EMQX_NAME:-"emqx"} export PACKAGE_PATH="${CODE_PATH}/_packages/${EMQX_NAME}" -export RELUP_PACKAGE_PATH="${CODE_PATH}/relup_packages/${EMQX_NAME}" +export RELUP_PACKAGE_PATH="${CODE_PATH}/_upgrade_base" # export EMQX_NODE_NAME="emqx-on-$(uname -m)@127.0.0.1" # export EMQX_NODE_COOKIE=$(date +%s%N) +case "$(uname -m)" in + x86_64) + ARCH='amd64' + ;; + aarch64) + ARCH='arm64' + ;; + arm*) + ARCH=arm + ;; +esac +export ARCH + emqx_prepare(){ mkdir -p "${PACKAGE_PATH}" @@ -136,19 +149,19 @@ running_test(){ } relup_test(){ - TARGET_VERSION="$1" + TARGET_VERSION="$("$CODE_PATH"/pkg-vsn.sh)" if [ -d "${RELUP_PACKAGE_PATH}" ];then - cd "${RELUP_PACKAGE_PATH }" + cd "${RELUP_PACKAGE_PATH}" - for var in "${EMQX_NAME}"-*-"$(uname -m)".zip;do + for var in "${EMQX_NAME}"-*-"${ARCH}".zip;do packagename=$(basename "${var}") unzip "$packagename" ./emqx/bin/emqx start || ( tail emqx/log/emqx.log.1 && exit 1 ) ./emqx/bin/emqx_ctl status ./emqx/bin/emqx versions - cp "${PACKAGE_PATH}/${EMQX_NAME}"-*-"${TARGET_VERSION}-$(uname -m)".zip ./emqx/releases + cp "${PACKAGE_PATH}/${EMQX_NAME}"-*-"${TARGET_VERSION}-${ARCH}".zip ./emqx/releases ./emqx/bin/emqx install "${TARGET_VERSION}" - [ "$(./emqx/bin/emqx versions |grep permanent | grep -oE "[0-9].[0-9].[0-9]")" = "${TARGET_VERSION}" ] || exit 1 + [ "$(./emqx/bin/emqx versions |grep permanent | awk '{print $2}')" = "${TARGET_VERSION}" ] || exit 1 ./emqx/bin/emqx_ctl status ./emqx/bin/emqx stop rm -rf emqx @@ -158,4 +171,4 @@ relup_test(){ emqx_prepare emqx_test -# relup_test +relup_test diff --git a/.ci/docker-compose-file/conf.env b/.ci/docker-compose-file/conf.env index 93dfecd2b..0b1b7c512 100644 --- a/.ci/docker-compose-file/conf.env +++ b/.ci/docker-compose-file/conf.env @@ -11,3 +11,4 @@ EMQX_AUTH__PGSQL__DATABASE=mqtt EMQX_AUTH__REDIS__SERVER=redis_server:6379 EMQX_AUTH__REDIS__PASSWORD=public CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ +HOCON_ENV_OVERRIDE_PREFIX=EMQX_ diff --git a/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml b/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml index 18e1bb6cc..6bc8e67e2 100644 --- a/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml +++ b/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml @@ -9,7 +9,7 @@ services: - emqx2 volumes: - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg - - ../../etc/certs:/usr/local/etc/haproxy/certs + - ../../apps/emqx/etc/certs:/usr/local/etc/haproxy/certs ports: - "18083:18083" # - "1883:1883" diff --git a/.ci/fvt_tests/relup.lux b/.ci/fvt_tests/relup.lux index cbecb9e14..bd37d241d 100644 --- a/.ci/fvt_tests/relup.lux +++ b/.ci/fvt_tests/relup.lux @@ -72,7 +72,7 @@ [shell bench] !cd $BENCH_PATH - !./emqtt_bench pub -c 10 -I 1000 -t t/%i -s 64 -L 600 + !./emqtt_bench pub -c 10 -I 1000 -t t/%i -s 64 -L 300 ???sent [shell emqx] @@ -109,7 +109,7 @@ ???publish complete ??SH-PROMPT: !curl http://127.0.0.1:8080/counter - ???{"data":600,"code":0} + ???{"data":300,"code":0} ?SH-PROMPT [shell emqx2] diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 70419bcc1..69ad6acf9 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -3,10 +3,6 @@ name: Cross build packages on: schedule: - cron: '0 */6 * * *' - push: - tags: - - v* - - e* release: types: - published @@ -19,25 +15,34 @@ jobs: outputs: profiles: ${{ steps.set_profile.outputs.profiles}} + old_vsns: ${{ steps.set_profile.outputs.old_vsns}} steps: - uses: actions/checkout@v2 with: path: source + fetch-depth: 0 - name: set profile id: set_profile shell: bash run: | - if make -C source emqx-ee --dry-run > /dev/null 2>&1; then + cd source + vsn="$(./pkg-vsn.sh)" + pre_vsn="$(echo $vsn | grep -oE '^[0-9]+.[0-9]')" + if make emqx-ee --dry-run > /dev/null 2>&1; then + old_vsns="$(git tag -l "e$pre_vsn.[0-9]" | xargs echo -n | sed "s/e$vsn//")" + echo "::set-output name=old_vsns::$old_vsns" echo "::set-output name=profiles::[\"emqx-ee\"]" else + old_vsns="$(git tag -l "v$pre_vsn.[0-9]" | xargs echo -n | sed "s/v$vsn//")" + echo "::set-output name=old_vsns::$old_vsns" echo "::set-output name=profiles::[\"emqx\", \"emqx-edge\"]" fi - name: get_all_deps if: endsWith(github.repository, 'emqx') run: | make -C source deps-all - zip -ryq source.zip source + zip -ryq source.zip source/* source/.[^.]* - name: get_all_deps if: endsWith(github.repository, 'enterprise') run: | @@ -45,7 +50,7 @@ jobs: git config --global credential.helper store echo "${{ secrets.CI_GIT_TOKEN }}" >> source/scripts/git-token make -C source deps-all - zip -ryq source.zip source + zip -ryq source.zip source/* source/.[^.]* - uses: actions/upload-artifact@v2 with: name: source @@ -95,6 +100,10 @@ jobs: if (Test-Path rebar.lock) { Remove-Item -Force -Path rebar.lock } + make ensure-rebar3 + copy rebar3 "${{ steps.install_erlang.outputs.erlpath }}\bin" + ls "${{ steps.install_erlang.outputs.erlpath }}\bin" + rebar3 --help make ${{ matrix.profile }} mkdir -p _packages/${{ matrix.profile }} Compress-Archive -Path _build/${{ matrix.profile }}/rel/emqx -DestinationPath _build/${{ matrix.profile }}/rel/$pkg_name @@ -124,7 +133,7 @@ jobs: matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} erl_otp: - - 23.2.7.2 + - erl23.2.7.2-emqx-2 exclude: - profile: emqx-edge @@ -155,6 +164,8 @@ jobs: - name: build run: | . $HOME/.kerl/${{ matrix.erl_otp }}/activate + make -C source ensure-rebar3 + sudo cp source/rebar3 /usr/local/bin/rebar3 make -C source ${{ matrix.profile }}-zip - name: test run: | @@ -208,7 +219,7 @@ jobs: - centos7 - centos6 - raspbian10 - - raspbian9 + # - raspbian9 exclude: - os: centos6 arch: arm64 @@ -245,35 +256,32 @@ jobs: path: . - name: unzip source code run: unzip -q source.zip - - name: downloads emqx zip packages + - name: downloads old emqx zip packages env: PROFILE: ${{ matrix.profile }} ARCH: ${{ matrix.arch }} SYSTEM: ${{ matrix.os }} + OLD_VSNS: ${{ needs.prepare.outputs.old_vsns }} run: | - set -e -u -x - cd source - if [ $PROFILE = "emqx" ];then broker="emqx-ce"; else broker="$PROFILE"; fi - if [ $PROFILE = "emqx-ee" ];then edition='enterprise'; else edition='opensource'; fi - - vsn="$(./pkg-vsn.sh)" - pre_vsn="$(echo $vsn | grep -oE '^[0-9]+.[0-9]')" - if [ $PROFILE = "emqx-ee" ]; then - old_vsns=($(git tag -l "e$pre_vsn.[0-9]" | sed "s/e$vsn//")) - else - old_vsns=($(git tag -l "v$pre_vsn.[0-9]" | sed "s/v$vsn//")) + set -e -x -u + broker=$PROFILE + if [ $PROFILE = "emqx" ];then + broker="emqx-ce" + fi + if [ ! -z "$(echo $SYSTEM | grep -oE 'raspbian')" ]; then + export ARCH="arm" fi - mkdir -p _upgrade_base - cd _upgrade_base - for tag in ${old_vsns[@]};do - if [ ! -z "$(echo $(curl -I -m 10 -o /dev/null -s -w %{http_code} https://s3-${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com/${{ secrets.AWS_S3_BUCKET }}/$broker/$tag/$PROFILE-$SYSTEM-${tag#[e|v]}-$ARCH.zip) | grep -oE "^[23]+")" ];then + mkdir -p source/_upgrade_base + cd source/_upgrade_base + old_vsns=($(echo $OLD_VSNS | tr ' ' ' ')) + for tag in ${old_vsns[@]}; do + if [ ! -z "$(echo $(curl -I -m 10 -o /dev/null -s -w %{http_code} https://s3-us-west-2.amazonaws.com/packages.emqx/$broker/$tag/$PROFILE-$SYSTEM-${tag#[e|v]}-$ARCH.zip) | grep -oE "^[23]+")" ];then wget --no-verbose https://s3-us-west-2.amazonaws.com/packages.emqx/$broker/$tag/$PROFILE-$SYSTEM-${tag#[e|v]}-$ARCH.zip wget --no-verbose https://s3-us-west-2.amazonaws.com/packages.emqx/$broker/$tag/$PROFILE-$SYSTEM-${tag#[e|v]}-$ARCH.zip.sha256 echo "$(cat $PROFILE-$SYSTEM-${tag#[e|v]}-$ARCH.zip.sha256) $PROFILE-$SYSTEM-${tag#[e|v]}-$ARCH.zip" | sha256sum -c || exit 1 fi done - cd - - name: build emqx packages env: ERL_OTP: erl23.2.7.2-emqx-2 diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 573b84e1b..08a35299c 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -53,7 +53,7 @@ jobs: strategy: matrix: erl_otp: - - 23.2.7.2 + - 23.2.7.2-emqx-2 steps: - uses: actions/checkout@v1 @@ -82,11 +82,14 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' timeout-minutes: 60 run: | + export OTP_GITHUB_URL="https://github.com/emqx/otp" kerl build ${{ matrix.erl_otp }} kerl install ${{ matrix.erl_otp }} $HOME/.kerl/${{ matrix.erl_otp }} - name: build run: | . $HOME/.kerl/${{ matrix.erl_otp }}/activate + make ensure-rebar3 + sudo cp rebar3 /usr/local/bin/rebar3 make ${EMQX_NAME}-zip - name: test run: | diff --git a/.github/workflows/git_sync.yaml b/.github/workflows/git_sync.yaml index 93411ac9f..50fa8c364 100644 --- a/.github/workflows/git_sync.yaml +++ b/.github/workflows/git_sync.yaml @@ -4,6 +4,7 @@ on: push: branches: - master + - main-v* jobs: sync_to_enterprise: @@ -22,11 +23,16 @@ jobs: id: create_pull_request run: | set -euo pipefail + if [ "$GITHUB_REF" = "refs/heads/master" ]; then + EE_REF="refs/heads/enterprise" + else + EE_REF="${GITHUB_REF}-enterprise" + fi R=$(curl --silent --show-error \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${{ secrets.CI_GIT_TOKEN }}" \ -X POST \ - -d '{"title": "Sync code into enterprise from opensource", "head": "master", "base":"enterprise"}' \ + -d "{\"title\": \"Sync code from opensource $GITHUB_REF to entperprise $EE_REF\", \"head\": \"$GITHUB_REF\", \"base\":\"$EE_REF\"}" \ https://api.github.com/repos/${{ github.repository_owner }}/emqx-enterprise/pulls) echo $R | jq echo "::set-output name=url::$(echo $R | jq '.url')" diff --git a/.github/workflows/run_cts_tests.yaml b/.github/workflows/run_cts_tests.yaml index c8c642af7..9126d97ac 100644 --- a/.github/workflows/run_cts_tests.yaml +++ b/.github/workflows/run_cts_tests.yaml @@ -5,9 +5,6 @@ on: tags: - v* - e* - release: - types: - - published pull_request: jobs: @@ -51,7 +48,7 @@ jobs: export CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ printenv > .env docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --dir apps/emqx_auth_ldap" + docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_ldap" docker exec --env-file .env -i erlang sh -c "./rebar3 ct --dir apps/emqx_auth_ldap" - uses: actions/upload-artifact@v1 if: failure() @@ -120,7 +117,7 @@ jobs: export CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ printenv > .env docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --dir apps/emqx_auth_mongo" + docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_mongo" docker exec --env-file .env -i erlang sh -c "./rebar3 ct --dir apps/emqx_auth_mongo" - uses: actions/upload-artifact@v1 if: failure() @@ -202,7 +199,7 @@ jobs: export CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ printenv > .env docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --dir apps/emqx_auth_mysql" + docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_mysql" docker exec --env-file .env -i erlang sh -c "./rebar3 ct --dir apps/emqx_auth_mysql" - uses: actions/upload-artifact@v1 if: failure() @@ -276,7 +273,7 @@ jobs: CUTTLEFISH_ENV_OVERRIDE_PREFIX=EMQX_ printenv > .env docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --dir apps/emqx_auth_pgsql" + docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_pgsql" docker exec --env-file .env -i erlang sh -c "./rebar3 ct --dir apps/emqx_auth_pgsql" - uses: actions/upload-artifact@v1 if: failure() @@ -397,7 +394,7 @@ jobs: export EMQX_AUTH__REIDS__PASSWORD=public printenv > .env docker exec -i erlang sh -c "make ensure-rebar3" - docker exec -i erlang sh -c "./rebar3 eunit --dir apps/emqx_auth_redis" + docker exec -i erlang sh -c "./rebar3 eunit --application=emqx_auth_redis" docker exec --env-file .env -i erlang sh -c "./rebar3 ct --dir apps/emqx_auth_redis" - uses: actions/upload-artifact@v1 if: failure() diff --git a/.github/workflows/run_emqx_app_tests.yaml b/.github/workflows/run_emqx_app_tests.yaml new file mode 100644 index 000000000..3fa30b484 --- /dev/null +++ b/.github/workflows/run_emqx_app_tests.yaml @@ -0,0 +1,26 @@ +name: Check emqx app standalone + +on: + push: + tags: + - v* + - e* + pull_request: + +jobs: + check_all: + runs-on: ubuntu-20.04 + container: emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04 + + steps: + - uses: actions/checkout@v2 + - name: run + run: | + make ensure-rebar3 + cp rebar3 apps/emqx/ + cd apps/emqx + ./rebar3 xref + ./rebar3 dialyzer + ./rebar3 eunit -v + ./rebar3 ct -v + ./rebar3 proper -d test/props diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index 12f9da95c..0bf57dc39 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -5,9 +5,6 @@ on: tags: - v* - e* - release: - types: - - published pull_request: jobs: diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index 07afdcde2..3fbe88c40 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -5,9 +5,6 @@ on: tags: - v* - e* - release: - types: - - published pull_request: jobs: @@ -130,7 +127,7 @@ jobs: docker exec --env-file .env -i erlang bash -c "make coveralls" - name: cat rebar.crashdump if: failure() - run: if [ -f 'rebar3.crashdump' ];then cat 'rebar3.crashdump' fi + run: if [ -f 'rebar3.crashdump' ];then cat 'rebar3.crashdump'; fi - uses: actions/upload-artifact@v1 if: failure() with: diff --git a/Makefile b/Makefile index 01fc40435..cc8cdb0db 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ $(shell $(CURDIR)/scripts/git-hooks-init.sh) -REBAR_VERSION = 3.14.3-emqx-7 +REBAR_VERSION = 3.14.3-emqx-8 REBAR = $(CURDIR)/rebar3 BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts diff --git a/README-CN.md b/README-CN.md index f6c5faf1e..7d2888327 100644 --- a/README-CN.md +++ b/README-CN.md @@ -7,6 +7,7 @@ [![Slack Invite]()](https://slack-invite.emqx.io) [![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow)](https://askemq.com) +[![YouTube](https://img.shields.io/badge/Subscribe-EMQ%20中文-FF0000?logo=youtube)](https://www.youtube.com/channel/UCir_r04HIsLjf2qqyZ4A8Cg) [![最棒的物联网 MQTT 开源团队期待您的加入](https://www.emqx.io/static/img/github_readme_cn_bg.png)](https://careers.emqx.cn/) diff --git a/README-JP.md b/README-JP.md index 0219eb25e..b7afe8195 100644 --- a/README-JP.md +++ b/README-JP.md @@ -6,6 +6,7 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) [![Slack Invite]()](https://slack-invite.emqx.io) [![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) +[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) [![The best IoT MQTT open source team looks forward to your joining](https://www.emqx.io/static/img/github_readme_en_bg.png)](https://www.emqx.io/careers) diff --git a/README-RU.md b/README-RU.md index 2dc5a6287..cddaba4a5 100644 --- a/README-RU.md +++ b/README-RU.md @@ -7,6 +7,7 @@ [![Slack Invite]()](https://slack-invite.emqx.io) [![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow?logo=github)](https://github.com/emqx/emqx/discussions) +[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) [![The best IoT MQTT open source team looks forward to your joining](https://www.emqx.io/static/img/github_readme_en_bg.png)](https://www.emqx.io/careers) diff --git a/README.md b/README.md index 34366c2ea..8d8ed8731 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) [![Slack Invite]()](https://slack-invite.emqx.io) [![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) -[![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow?logo=github)](https://github.com/emqx/emqx/discussions) +[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) [![The best IoT MQTT open source team looks forward to your joining](https://www.emqx.io/static/img/github_readme_en_bg.png)](https://www.emqx.io/careers) diff --git a/etc/acl.conf b/apps/emqx/etc/acl.conf similarity index 100% rename from etc/acl.conf rename to apps/emqx/etc/acl.conf diff --git a/etc/acl.conf.paho b/apps/emqx/etc/acl.conf.paho similarity index 100% rename from etc/acl.conf.paho rename to apps/emqx/etc/acl.conf.paho diff --git a/etc/certs/README b/apps/emqx/etc/certs/README similarity index 100% rename from etc/certs/README rename to apps/emqx/etc/certs/README diff --git a/etc/certs/cacert.pem b/apps/emqx/etc/certs/cacert.pem similarity index 100% rename from etc/certs/cacert.pem rename to apps/emqx/etc/certs/cacert.pem diff --git a/etc/certs/cert.pem b/apps/emqx/etc/certs/cert.pem similarity index 100% rename from etc/certs/cert.pem rename to apps/emqx/etc/certs/cert.pem diff --git a/etc/certs/client-cert.pem b/apps/emqx/etc/certs/client-cert.pem similarity index 100% rename from etc/certs/client-cert.pem rename to apps/emqx/etc/certs/client-cert.pem diff --git a/etc/certs/client-key.pem b/apps/emqx/etc/certs/client-key.pem similarity index 100% rename from etc/certs/client-key.pem rename to apps/emqx/etc/certs/client-key.pem diff --git a/etc/certs/key.pem b/apps/emqx/etc/certs/key.pem similarity index 100% rename from etc/certs/key.pem rename to apps/emqx/etc/certs/key.pem diff --git a/etc/emqx.conf b/apps/emqx/etc/emqx.conf similarity index 99% rename from etc/emqx.conf rename to apps/emqx/etc/emqx.conf index 65d8ec58b..f6627bf1c 100644 --- a/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -453,6 +453,13 @@ log.file = emqx.log ## Default: No Limit #log.chars_limit = 8192 +## Maximum depth for Erlang term log formatting +## and Erlang process message queue inspection. +## +## Value: Integer or 'unlimited' (without quotes) +## Default: 20 +#log.max_depth = 20 + ## Log formatter ## Value: text | json #log.formatter = text @@ -598,42 +605,42 @@ log.rotation.count = 5 ## Notice: Disable the option in production deployment! ## ## Value: true | false -allow_anonymous = true +acl.allow_anonymous = true ## Allow or deny if no ACL rules matched. ## ## Value: allow | deny -acl_nomatch = allow +acl.acl_nomatch = allow ## Default ACL File. ## ## Value: File Name -acl_file = "{{ platform_etc_dir }}/acl.conf" +acl.acl_file = "{{ platform_etc_dir }}/acl.conf" ## Whether to enable ACL cache. ## ## If enabled, ACLs roles for each client will be cached in the memory ## ## Value: on | off -enable_acl_cache = on +acl.enable_acl_cache = on ## The maximum count of ACL entries can be cached for a client. ## ## Value: Integer greater than 0 ## Default: 32 -acl_cache_max_size = 32 +acl.acl_cache_max_size = 32 ## The time after which an ACL cache entry will be deleted ## ## Value: Duration ## Default: 1 minute -acl_cache_ttl = 1m +acl.acl_cache_ttl = 1m ## The action when acl check reject current operation ## ## Value: ignore | disconnect ## Default: ignore -acl_deny_action = ignore +acl.acl_deny_action = ignore ## Specify the global flapping detect policy. ## The value is a string composed of flapping threshold, duration and banned interval. @@ -642,7 +649,7 @@ acl_deny_action = ignore ## 3. banned interval: the banned interval if a flapping is detected. ## ## Value: Integer,Duration,Duration -flapping_detect_policy = "30, 1m, 5m" +acl.flapping_detect_policy = "30, 1m, 5m" ##-------------------------------------------------------------------- ## MQTT Protocol @@ -2155,7 +2162,7 @@ listener.wss.external.check_origins = "https://localhost:8084, https://127.0.0.1 ## The file to store loaded module names. ## ## Value: File -modules.loaded_file = "{{ platform_data_dir }}/loaded_modules" +module.loaded_file = "{{ platform_data_dir }}/loaded_modules" ##-------------------------------------------------------------------- ## Presence Module @@ -2204,8 +2211,8 @@ module.presence.qos = 1 ## Rewrite Module ## {rewrite, Topic, Re, Dest} -## module.rewrite.pub.rule.1 = "x/# ^x/y/(.+)$ z/y/$1" -## module.rewrite.sub.rule.1 = "y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2" +## module.rewrite.pub_rule.1 = "x/# ^x/y/(.+)$ z/y/$1" +## module.rewrite.sub_rule.1 = "y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2" ## CONFIG_SECTION_END=modules ================================================== diff --git a/etc/emqx_cloud/vm.args b/apps/emqx/etc/emqx_cloud/vm.args similarity index 100% rename from etc/emqx_cloud/vm.args rename to apps/emqx/etc/emqx_cloud/vm.args diff --git a/etc/emqx_edge/vm.args b/apps/emqx/etc/emqx_edge/vm.args similarity index 100% rename from etc/emqx_edge/vm.args rename to apps/emqx/etc/emqx_edge/vm.args diff --git a/etc/ssl_dist.conf b/apps/emqx/etc/ssl_dist.conf similarity index 100% rename from etc/ssl_dist.conf rename to apps/emqx/etc/ssl_dist.conf diff --git a/include/emqx.hrl b/apps/emqx/include/emqx.hrl similarity index 100% rename from include/emqx.hrl rename to apps/emqx/include/emqx.hrl diff --git a/include/emqx_mqtt.hrl b/apps/emqx/include/emqx_mqtt.hrl similarity index 100% rename from include/emqx_mqtt.hrl rename to apps/emqx/include/emqx_mqtt.hrl diff --git a/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl similarity index 100% rename from include/emqx_release.hrl rename to apps/emqx/include/emqx_release.hrl diff --git a/include/logger.hrl b/apps/emqx/include/logger.hrl similarity index 100% rename from include/logger.hrl rename to apps/emqx/include/logger.hrl diff --git a/include/types.hrl b/apps/emqx/include/types.hrl similarity index 100% rename from include/types.hrl rename to apps/emqx/include/types.hrl diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config new file mode 100644 index 000000000..2c566d7f8 --- /dev/null +++ b/apps/emqx/rebar.config @@ -0,0 +1,47 @@ +{erl_opts, [warn_unused_vars,warn_shadow_vars,warn_unused_import, + warn_obsolete_guard,compressed]}. + +{xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, + deprecated_function_calls,warnings_as_errors,deprecated_functions]}. + +%% Deps here may duplicate with emqx.git root level rebar.config +%% but there not be any descrpancy. +%% This rebar.config is necessary because the app may be used as a +%% `git_subdir` dependency in other projects. +{deps, + [ {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} + , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} + , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} + , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.0"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.9.0"}}} + , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} + , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.0"}}} + , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} + , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} + , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} + ]}. + +{plugins, [rebar3_proper]}. +{extra_src_dirs, [{"etc", [recursive]}]}. +{profiles, [ + {test, + [{deps, + [ meck + , {bbmustache,"1.10.0"} + , {emqx_ct_helpers, {git,"https://github.com/zmstone/emqx-ct-helpers", {branch,"hocon"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3.1"}}} + ]}, + {extra_src_dirs, [{"test",[recursive]}]} + ]} +]}. + +{dialyzer, [ + {warnings, [unmatched_returns, error_handling, race_conditions]}, + {plt_location, "."}, + {plt_prefix, "emqx_dialyzer"}, + {plt_apps, all_apps}, + {plt_extra_apps, [hocon]}, + {statistics, true} + ] +}. diff --git a/apps/emqx/rebar.config.script b/apps/emqx/rebar.config.script new file mode 100644 index 000000000..eae18f106 --- /dev/null +++ b/apps/emqx/rebar.config.script @@ -0,0 +1,11 @@ +Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {branch, "0.6.0"}}}, +AddBcrypt = fun(C) -> + {deps, Deps0} = lists:keyfind(deps, 1, C), + Deps = [Bcrypt | Deps0], + lists:keystore(deps, 1, C, {deps, Deps}) +end, + +case os:type() of + {win32, _} -> CONFIG; + _ -> AddBcrypt(CONFIG) +end. diff --git a/src/emqx.app.src b/apps/emqx/src/emqx.app.src similarity index 89% rename from src/emqx.app.src rename to apps/emqx/src/emqx.app.src index b195d7a1b..e909702ae 100644 --- a/src/emqx.app.src +++ b/apps/emqx/src/emqx.app.src @@ -1,7 +1,7 @@ {application, emqx, [{id, "emqx"}, {description, "EMQ X"}, - {vsn, "4.3.2"}, % strict semver, bump manually! + {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon]}, diff --git a/apps/emqx/src/emqx.appup.src b/apps/emqx/src/emqx.appup.src new file mode 100644 index 000000000..b51a7f3b7 --- /dev/null +++ b/apps/emqx/src/emqx.appup.src @@ -0,0 +1,69 @@ +%% -*- mode: erlang -*- +{VSN, + [{"4.3.2", + [{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, + {load_module,emqx_app,brutal_purge,soft_purge,[]}, + {load_module,emqx_connection,brutal_purge,soft_purge,[]}]}, + {"4.3.1", + [{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, + {load_module,emqx_connection,brutal_purge,soft_purge,[]}, + {load_module,emqx_frame,brutal_purge,soft_purge,[]}, + {load_module,emqx_cm,brutal_purge,soft_purge,[]}, + {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, + {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, + {load_module,emqx_app,brutal_purge,soft_purge,[]}, + {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, + {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, + {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, + {"4.3.0", + [{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]}, + {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, + {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, + {load_module,emqx_connection,brutal_purge,soft_purge,[]}, + {load_module,emqx_frame,brutal_purge,soft_purge,[]}, + {load_module,emqx_trie,brutal_purge,soft_purge,[]}, + {load_module,emqx_cm,brutal_purge,soft_purge,[]}, + {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, + {load_module,emqx_app,brutal_purge,soft_purge,[]}, + {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, + {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, + {load_module,emqx_metrics,brutal_purge,soft_purge,[]}, + {apply,{emqx_metrics,upgrade_retained_delayed_counter_type,[]}}, + {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, + {<<".*">>,[]}], + [{"4.3.2", + [{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, + {load_module,emqx_app,brutal_purge,soft_purge,[]}, + {load_module,emqx_connection,brutal_purge,soft_purge,[]}]}, + {"4.3.1", + [{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, + {load_module,emqx_connection,brutal_purge,soft_purge,[]}, + {load_module,emqx_frame,brutal_purge,soft_purge,[]}, + {load_module,emqx_cm,brutal_purge,soft_purge,[]}, + {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, + {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, + {load_module,emqx_app,brutal_purge,soft_purge,[]}, + {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, + {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, + {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, + {"4.3.0", + [{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]}, + {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, + {load_module,emqx_connection,brutal_purge,soft_purge,[]}, + {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, + {load_module,emqx_frame,brutal_purge,soft_purge,[]}, + {load_module,emqx_trie,brutal_purge,soft_purge,[]}, + {load_module,emqx_cm,brutal_purge,soft_purge,[]}, + {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, + {load_module,emqx_channel,brutal_purge,soft_purge,[]}, + {load_module,emqx_app,brutal_purge,soft_purge,[]}, + {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, + {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, + {load_module,emqx_metrics,brutal_purge,soft_purge,[]}, + {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, + {<<".*">>,[]}]}. diff --git a/src/emqx.erl b/apps/emqx/src/emqx.erl similarity index 100% rename from src/emqx.erl rename to apps/emqx/src/emqx.erl diff --git a/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl similarity index 100% rename from src/emqx_access_control.erl rename to apps/emqx/src/emqx_access_control.erl diff --git a/src/emqx_access_rule.erl b/apps/emqx/src/emqx_access_rule.erl similarity index 100% rename from src/emqx_access_rule.erl rename to apps/emqx/src/emqx_access_rule.erl diff --git a/src/emqx_acl_cache.erl b/apps/emqx/src/emqx_acl_cache.erl similarity index 100% rename from src/emqx_acl_cache.erl rename to apps/emqx/src/emqx_acl_cache.erl diff --git a/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl similarity index 100% rename from src/emqx_alarm.erl rename to apps/emqx/src/emqx_alarm.erl diff --git a/src/emqx_alarm_handler.erl b/apps/emqx/src/emqx_alarm_handler.erl similarity index 100% rename from src/emqx_alarm_handler.erl rename to apps/emqx/src/emqx_alarm_handler.erl diff --git a/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl similarity index 100% rename from src/emqx_app.erl rename to apps/emqx/src/emqx_app.erl diff --git a/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl similarity index 100% rename from src/emqx_banned.erl rename to apps/emqx/src/emqx_banned.erl diff --git a/src/emqx_base62.erl b/apps/emqx/src/emqx_base62.erl similarity index 100% rename from src/emqx_base62.erl rename to apps/emqx/src/emqx_base62.erl diff --git a/src/emqx_batch.erl b/apps/emqx/src/emqx_batch.erl similarity index 100% rename from src/emqx_batch.erl rename to apps/emqx/src/emqx_batch.erl diff --git a/src/emqx_boot.erl b/apps/emqx/src/emqx_boot.erl similarity index 100% rename from src/emqx_boot.erl rename to apps/emqx/src/emqx_boot.erl diff --git a/src/emqx_broker.erl b/apps/emqx/src/emqx_broker.erl similarity index 100% rename from src/emqx_broker.erl rename to apps/emqx/src/emqx_broker.erl diff --git a/src/emqx_broker_bench.erl b/apps/emqx/src/emqx_broker_bench.erl similarity index 100% rename from src/emqx_broker_bench.erl rename to apps/emqx/src/emqx_broker_bench.erl diff --git a/src/emqx_broker_helper.erl b/apps/emqx/src/emqx_broker_helper.erl similarity index 100% rename from src/emqx_broker_helper.erl rename to apps/emqx/src/emqx_broker_helper.erl diff --git a/src/emqx_broker_sup.erl b/apps/emqx/src/emqx_broker_sup.erl similarity index 100% rename from src/emqx_broker_sup.erl rename to apps/emqx/src/emqx_broker_sup.erl diff --git a/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl similarity index 99% rename from src/emqx_channel.erl rename to apps/emqx/src/emqx_channel.erl index 3a818b9a7..e3cbff692 100644 --- a/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -407,7 +407,8 @@ handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), case emqx_packet:check(Packet) of ok -> TopicFilters0 = parse_topic_filters(TopicFilters), - TupleTopicFilters0 = check_sub_acls(TopicFilters0, Channel), + TopicFilters1 = put_subid_in_subopts(Properties, TopicFilters0), + TupleTopicFilters0 = check_sub_acls(TopicFilters1, Channel), case emqx_zone:get_env(Zone, acl_deny_action, ignore) =:= disconnect andalso lists:any(fun({_TopicFilter, ReasonCode}) -> ReasonCode =:= ?RC_NOT_AUTHORIZED @@ -419,8 +420,7 @@ handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), _Fun(lists:keyreplace(Key, 1, TupleList, Tuple), More); _Fun(TupleList, []) -> TupleList end, - TopicFilters1 = [ TopicFilter || {TopicFilter, 0} <- TupleTopicFilters0], - TopicFilters2 = put_subid_in_subopts(Properties, TopicFilters1), + TopicFilters2 = [ TopicFilter || {TopicFilter, 0} <- TupleTopicFilters0], TopicFilters3 = run_hooks('client.subscribe', [ClientInfo, Properties], TopicFilters2), diff --git a/src/emqx_cm.erl b/apps/emqx/src/emqx_cm.erl similarity index 100% rename from src/emqx_cm.erl rename to apps/emqx/src/emqx_cm.erl diff --git a/src/emqx_cm_locker.erl b/apps/emqx/src/emqx_cm_locker.erl similarity index 100% rename from src/emqx_cm_locker.erl rename to apps/emqx/src/emqx_cm_locker.erl diff --git a/src/emqx_cm_registry.erl b/apps/emqx/src/emqx_cm_registry.erl similarity index 100% rename from src/emqx_cm_registry.erl rename to apps/emqx/src/emqx_cm_registry.erl diff --git a/src/emqx_cm_sup.erl b/apps/emqx/src/emqx_cm_sup.erl similarity index 100% rename from src/emqx_cm_sup.erl rename to apps/emqx/src/emqx_cm_sup.erl diff --git a/src/emqx_congestion.erl b/apps/emqx/src/emqx_congestion.erl similarity index 100% rename from src/emqx_congestion.erl rename to apps/emqx/src/emqx_congestion.erl diff --git a/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl similarity index 99% rename from src/emqx_connection.erl rename to apps/emqx/src/emqx_connection.erl index 814fd9007..ab91c02b4 100644 --- a/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -41,7 +41,8 @@ , stats/1 ]). --export([ async_set_keepalive/4 +-export([ async_set_keepalive/3 + , async_set_keepalive/4 , async_set_socket_options/2 ]). @@ -200,6 +201,9 @@ stats(#state{transport = Transport, %% %% NOTE: This API sets TCP socket options, which has nothing to do with %% the MQTT layer's keepalive (PINGREQ and PINGRESP). +async_set_keepalive(Idle, Interval, Probes) -> + async_set_keepalive(self(), Idle, Interval, Probes). + async_set_keepalive(Pid, Idle, Interval, Probes) -> Options = [ {keepalive, true} , {raw, 6, 4, <>} @@ -345,6 +349,7 @@ ensure_stats_timer(_Timeout, State) -> State. -compile({inline, [cancel_stats_timer/1]}). cancel_stats_timer(State = #state{stats_timer = TRef}) when is_reference(TRef) -> + ?tp(debug, cancel_stats_timer, #{}), ok = emqx_misc:cancel_timer(TRef), State#state{stats_timer = undefined}; cancel_stats_timer(State) -> State. diff --git a/src/emqx_ctl.erl b/apps/emqx/src/emqx_ctl.erl similarity index 100% rename from src/emqx_ctl.erl rename to apps/emqx/src/emqx_ctl.erl diff --git a/src/emqx_flapping.erl b/apps/emqx/src/emqx_flapping.erl similarity index 100% rename from src/emqx_flapping.erl rename to apps/emqx/src/emqx_flapping.erl diff --git a/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl similarity index 97% rename from src/emqx_frame.erl rename to apps/emqx/src/emqx_frame.erl index c4a2f6ac0..37063c65f 100644 --- a/src/emqx_frame.erl +++ b/apps/emqx/src/emqx_frame.erl @@ -121,17 +121,8 @@ parse(Bin, {{body, #{hdr := Header, len := Length, rest := Body} }, Options}) when is_binary(Bin) -> - BodyBytes = body_bytes(Body), - {NewBodyPart, Tail} = split(BodyBytes + size(Bin) - Length, Bin), - NewBody = append_body(Body, NewBodyPart), - parse_frame(NewBody, Tail, Header, Length, Options). - -%% split given binary with the first N bytes -split(N, Bin) when N =< 0 -> - {Bin, <<>>}; -split(N, Bin) when N =< size(Bin) -> - <> = Bin, - {H, T}. + NewBody = append_body(Body, Bin), + parse_frame(NewBody, Header, Length, Options). parse_remaining_len(<<>>, Header, Options) -> {more, {{len, #{hdr => Header, len => {1, 0}}}, Options}}; @@ -178,19 +169,15 @@ append_body(H, T) when is_binary(H) -> append_body(?Q(Bytes, Q), T) -> ?Q(Bytes + iolist_size(T), queue:in(T, Q)). -flatten_body(Body, Tail) when is_binary(Body) -> <>; -flatten_body(?Q(_, Q), Tail) -> iolist_to_binary([queue:to_list(Q), Tail]). +flatten_body(Body) when is_binary(Body) -> Body; +flatten_body(?Q(_, Q)) -> iolist_to_binary(queue:to_list(Q)). +parse_frame(Body, Header, 0, Options) -> + {ok, packet(Header), flatten_body(Body), ?none(Options)}; parse_frame(Body, Header, Length, Options) -> - %% already appended - parse_frame(Body, _SplitTail = <<>>, Header, Length, Options). - -parse_frame(Body, Tail, Header, 0, Options) -> - {ok, packet(Header), flatten_body(Body, Tail), ?none(Options)}; -parse_frame(Body, Tail, Header, Length, Options) -> case body_bytes(Body) >= Length of true -> - <> = flatten_body(Body, Tail), + <> = flatten_body(Body), case parse_packet(Header, FrameBin, Options) of {Variable, Payload} -> {ok, packet(Header, Variable, Payload), Rest, ?none(Options)}; @@ -202,7 +189,7 @@ parse_frame(Body, Tail, Header, Length, Options) -> false -> {more, {{body, #{hdr => Header, len => Length, - rest => append_body(Body, Tail) + rest => Body }}, Options}} end. diff --git a/src/emqx_gc.erl b/apps/emqx/src/emqx_gc.erl similarity index 100% rename from src/emqx_gc.erl rename to apps/emqx/src/emqx_gc.erl diff --git a/src/emqx_gen_mod.erl b/apps/emqx/src/emqx_gen_mod.erl similarity index 100% rename from src/emqx_gen_mod.erl rename to apps/emqx/src/emqx_gen_mod.erl diff --git a/src/emqx_global_gc.erl b/apps/emqx/src/emqx_global_gc.erl similarity index 100% rename from src/emqx_global_gc.erl rename to apps/emqx/src/emqx_global_gc.erl diff --git a/src/emqx_guid.erl b/apps/emqx/src/emqx_guid.erl similarity index 100% rename from src/emqx_guid.erl rename to apps/emqx/src/emqx_guid.erl diff --git a/src/emqx_hooks.erl b/apps/emqx/src/emqx_hooks.erl similarity index 100% rename from src/emqx_hooks.erl rename to apps/emqx/src/emqx_hooks.erl diff --git a/src/emqx_inflight.erl b/apps/emqx/src/emqx_inflight.erl similarity index 100% rename from src/emqx_inflight.erl rename to apps/emqx/src/emqx_inflight.erl diff --git a/src/emqx_json.erl b/apps/emqx/src/emqx_json.erl similarity index 100% rename from src/emqx_json.erl rename to apps/emqx/src/emqx_json.erl diff --git a/src/emqx_keepalive.erl b/apps/emqx/src/emqx_keepalive.erl similarity index 100% rename from src/emqx_keepalive.erl rename to apps/emqx/src/emqx_keepalive.erl diff --git a/src/emqx_kernel_sup.erl b/apps/emqx/src/emqx_kernel_sup.erl similarity index 100% rename from src/emqx_kernel_sup.erl rename to apps/emqx/src/emqx_kernel_sup.erl diff --git a/src/emqx_limiter.erl b/apps/emqx/src/emqx_limiter.erl similarity index 100% rename from src/emqx_limiter.erl rename to apps/emqx/src/emqx_limiter.erl diff --git a/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl similarity index 100% rename from src/emqx_listeners.erl rename to apps/emqx/src/emqx_listeners.erl diff --git a/src/emqx_logger.erl b/apps/emqx/src/emqx_logger.erl similarity index 100% rename from src/emqx_logger.erl rename to apps/emqx/src/emqx_logger.erl diff --git a/src/emqx_logger_jsonfmt.erl b/apps/emqx/src/emqx_logger_jsonfmt.erl similarity index 99% rename from src/emqx_logger_jsonfmt.erl rename to apps/emqx/src/emqx_logger_jsonfmt.erl index 31e4c2fda..fdc1a8bb3 100644 --- a/src/emqx_logger_jsonfmt.erl +++ b/apps/emqx/src/emqx_logger_jsonfmt.erl @@ -32,6 +32,9 @@ -export([format/2]). -ifdef(TEST). +-include_lib("proper/include/proper.hrl"). +-include_lib("eunit/include/eunit.hrl"). + -export([report_cb_1/1, report_cb_2/2, report_cb_crash/2]). -endif. @@ -220,8 +223,6 @@ json_key(Term) -> end. -ifdef(TEST). --include_lib("proper/include/proper.hrl"). --include_lib("eunit/include/eunit.hrl"). no_crash_test_() -> Opts = [{numtests, 1000}, {to_file, user}], diff --git a/src/emqx_logger_textfmt.erl b/apps/emqx/src/emqx_logger_textfmt.erl similarity index 88% rename from src/emqx_logger_textfmt.erl rename to apps/emqx/src/emqx_logger_textfmt.erl index 3bc9f185a..291153d74 100644 --- a/src/emqx_logger_textfmt.erl +++ b/apps/emqx/src/emqx_logger_textfmt.erl @@ -35,15 +35,9 @@ format(#{msg := Msg0, meta := Meta} = Event, Config) -> logger_formatter:format(Event#{msg := Msg}, Config). maybe_merge({report, Report}, Meta) when is_map(Report) -> - {report, maps:merge(rename(Report), filter(Meta))}; + {report, maps:merge(Report, filter(Meta))}; maybe_merge(Report, _Meta) -> Report. filter(Meta) -> maps:without(?WITHOUT_MERGE, Meta). - -rename(#{'$kind' := Kind} = Meta0) -> % snabbkaffe - Meta = maps:remove('$kind', Meta0), - Meta#{msg => Kind}; -rename(Meta) -> - Meta. diff --git a/src/emqx_message.erl b/apps/emqx/src/emqx_message.erl similarity index 100% rename from src/emqx_message.erl rename to apps/emqx/src/emqx_message.erl diff --git a/src/emqx_metrics.erl b/apps/emqx/src/emqx_metrics.erl similarity index 100% rename from src/emqx_metrics.erl rename to apps/emqx/src/emqx_metrics.erl diff --git a/src/emqx_misc.erl b/apps/emqx/src/emqx_misc.erl similarity index 100% rename from src/emqx_misc.erl rename to apps/emqx/src/emqx_misc.erl diff --git a/src/emqx_mountpoint.erl b/apps/emqx/src/emqx_mountpoint.erl similarity index 100% rename from src/emqx_mountpoint.erl rename to apps/emqx/src/emqx_mountpoint.erl diff --git a/src/emqx_mqtt_caps.erl b/apps/emqx/src/emqx_mqtt_caps.erl similarity index 100% rename from src/emqx_mqtt_caps.erl rename to apps/emqx/src/emqx_mqtt_caps.erl diff --git a/src/emqx_mqtt_props.erl b/apps/emqx/src/emqx_mqtt_props.erl similarity index 100% rename from src/emqx_mqtt_props.erl rename to apps/emqx/src/emqx_mqtt_props.erl diff --git a/src/emqx_mqueue.erl b/apps/emqx/src/emqx_mqueue.erl similarity index 100% rename from src/emqx_mqueue.erl rename to apps/emqx/src/emqx_mqueue.erl diff --git a/src/emqx_node_dump.erl b/apps/emqx/src/emqx_node_dump.erl similarity index 98% rename from src/emqx_node_dump.erl rename to apps/emqx/src/emqx_node_dump.erl index 18189bb57..ff0895553 100644 --- a/src/emqx_node_dump.erl +++ b/apps/emqx/src/emqx_node_dump.erl @@ -52,7 +52,7 @@ censor([Key | _], Val) -> end. is_sensitive(Key) when is_atom(Key) -> - is_sensitive(atom_to_binary(Key)); + is_sensitive(atom_to_binary(Key, utf8)); is_sensitive(Key) when is_list(Key) -> try iolist_to_binary(Key) of Bin -> diff --git a/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl similarity index 100% rename from src/emqx_os_mon.erl rename to apps/emqx/src/emqx_os_mon.erl diff --git a/src/emqx_packet.erl b/apps/emqx/src/emqx_packet.erl similarity index 100% rename from src/emqx_packet.erl rename to apps/emqx/src/emqx_packet.erl diff --git a/src/emqx_passwd.erl b/apps/emqx/src/emqx_passwd.erl similarity index 100% rename from src/emqx_passwd.erl rename to apps/emqx/src/emqx_passwd.erl diff --git a/src/emqx_pd.erl b/apps/emqx/src/emqx_pd.erl similarity index 100% rename from src/emqx_pd.erl rename to apps/emqx/src/emqx_pd.erl diff --git a/src/emqx_plugins.erl b/apps/emqx/src/emqx_plugins.erl similarity index 95% rename from src/emqx_plugins.erl rename to apps/emqx/src/emqx_plugins.erl index f05da760e..6ef3d2d21 100644 --- a/src/emqx_plugins.erl +++ b/apps/emqx/src/emqx_plugins.erl @@ -34,6 +34,8 @@ , apply_configs/1 ]). +-export([funlog/2]). + -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). @@ -172,7 +174,14 @@ load_ext_plugin(PluginDir) -> error({plugin_app_file_not_found, AppFile}) end, ok = load_plugin_app(AppName, Ebin), - ok = load_plugin_conf(AppName, PluginDir). + try + ok = load_plugin_conf(AppName, PluginDir) + catch + throw : {conf_file_not_found, ConfFile} -> + %% this is maybe a dependency of an external plugin + ?LOG(debug, "config_load_error_ignored for app=~p, path=~s", [AppName, ConfFile]), + ok + end. load_plugin_app(AppName, Ebin) -> _ = code:add_patha(Ebin), @@ -180,8 +189,8 @@ load_plugin_app(AppName, Ebin) -> lists:foreach( fun(BeamFile) -> Module = list_to_atom(filename:basename(BeamFile, ".beam")), - case code:ensure_loaded(Module) of - {module, Module} -> ok; + case code:load_file(Module) of + {module, _} -> ok; {error, Reason} -> error({failed_to_load_plugin_beam, BeamFile, Reason}) end end, Modules), @@ -193,12 +202,12 @@ load_plugin_app(AppName, Ebin) -> load_plugin_conf(AppName, PluginDir) -> Priv = filename:join([PluginDir, "priv"]), Etc = filename:join([PluginDir, "etc"]), - Schema = filelib:wildcard(filename:join([Priv, "*.schema"])), ConfFile = filename:join([Etc, atom_to_list(AppName) ++ ".conf"]), Conf = case filelib:is_file(ConfFile) of true -> cuttlefish_conf:file(ConfFile); - false -> error({conf_file_not_found, ConfFile}) + false -> throw({conf_file_not_found, ConfFile}) end, + Schema = filelib:wildcard(filename:join([Priv, "*.schema"])), ?LOG(debug, "loading_extra_plugin_config conf=~s, schema=~s", [ConfFile, Schema]), AppsEnv = cuttlefish_generator:map(cuttlefish_schema:files(Schema), Conf), lists:foreach(fun({AppName1, Envs}) -> @@ -260,8 +269,7 @@ do_generate_configs(App) -> true -> Schema = cuttlefish_schema:files([SchemaFile]), Conf = cuttlefish_conf:file(ConfFile), - LogFun = fun(Key, Value) -> ?LOG(info, "~s = ~p", [string:join(Key, "."), Value]) end, - cuttlefish_generator:map(Schema, Conf, undefined, LogFun); + cuttlefish_generator:map(Schema, Conf, undefined, fun ?MODULE:funlog/2); false -> error({schema_not_found, SchemaFile}) end. @@ -404,3 +412,7 @@ plugin_type(protocol) -> protocol; plugin_type(backend) -> backend; plugin_type(bridge) -> bridge; plugin_type(_) -> feature. + + +funlog(Key, Value) -> + ?LOG(info, "~s = ~p", [string:join(Key, "."), Value]). diff --git a/src/emqx_pmon.erl b/apps/emqx/src/emqx_pmon.erl similarity index 100% rename from src/emqx_pmon.erl rename to apps/emqx/src/emqx_pmon.erl diff --git a/src/emqx_pool.erl b/apps/emqx/src/emqx_pool.erl similarity index 100% rename from src/emqx_pool.erl rename to apps/emqx/src/emqx_pool.erl diff --git a/src/emqx_pool_sup.erl b/apps/emqx/src/emqx_pool_sup.erl similarity index 100% rename from src/emqx_pool_sup.erl rename to apps/emqx/src/emqx_pool_sup.erl diff --git a/src/emqx_pqueue.erl b/apps/emqx/src/emqx_pqueue.erl similarity index 100% rename from src/emqx_pqueue.erl rename to apps/emqx/src/emqx_pqueue.erl diff --git a/src/emqx_psk.erl b/apps/emqx/src/emqx_psk.erl similarity index 100% rename from src/emqx_psk.erl rename to apps/emqx/src/emqx_psk.erl diff --git a/src/emqx_reason_codes.erl b/apps/emqx/src/emqx_reason_codes.erl similarity index 100% rename from src/emqx_reason_codes.erl rename to apps/emqx/src/emqx_reason_codes.erl diff --git a/src/emqx_router.erl b/apps/emqx/src/emqx_router.erl similarity index 100% rename from src/emqx_router.erl rename to apps/emqx/src/emqx_router.erl diff --git a/src/emqx_router_helper.erl b/apps/emqx/src/emqx_router_helper.erl similarity index 100% rename from src/emqx_router_helper.erl rename to apps/emqx/src/emqx_router_helper.erl diff --git a/src/emqx_router_sup.erl b/apps/emqx/src/emqx_router_sup.erl similarity index 100% rename from src/emqx_router_sup.erl rename to apps/emqx/src/emqx_router_sup.erl diff --git a/src/emqx_rpc.erl b/apps/emqx/src/emqx_rpc.erl similarity index 100% rename from src/emqx_rpc.erl rename to apps/emqx/src/emqx_rpc.erl diff --git a/src/emqx_rule_actions_trans.erl b/apps/emqx/src/emqx_rule_actions_trans.erl similarity index 100% rename from src/emqx_rule_actions_trans.erl rename to apps/emqx/src/emqx_rule_actions_trans.erl diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl new file mode 100644 index 000000000..bac2e5cfe --- /dev/null +++ b/apps/emqx/src/emqx_schema.erl @@ -0,0 +1,1230 @@ +-module(emqx_schema). + +% tmp +-dialyzer(no_return). +-dialyzer(no_match). +-dialyzer(no_contracts). +-dialyzer(no_unused). +-dialyzer(no_fail_call). + +-include_lib("typerefl/include/types.hrl"). + +-type log_level() :: debug | info | notice | warning | error | critical | alert | emergency | all. +-type flag() :: true | false. +-type duration() :: integer(). +-type duration_s() :: integer(). +-type bytesize() :: integer(). +-type percent() :: float(). +-type file() :: string(). +-type comma_separated_list() :: list(). +-type bar_separated_list() :: list(). +-type ip_port() :: tuple(). + +-typerefl_from_string({flag/0, emqx_schema, to_flag}). +-typerefl_from_string({duration/0, emqx_schema, to_duration}). +-typerefl_from_string({duration_s/0, emqx_schema, to_duration_s}). +-typerefl_from_string({bytesize/0, emqx_schema, to_bytesize}). +-typerefl_from_string({percent/0, emqx_schema, to_percent}). +-typerefl_from_string({comma_separated_list/0, emqx_schema, to_comma_separated_list}). +-typerefl_from_string({bar_separated_list/0, emqx_schema, to_bar_separated_list}). +-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}). + +% workaround: prevent being recognized as unused functions +-export([to_duration/1, to_duration_s/1, to_bytesize/1, + to_flag/1, to_percent/1, to_comma_separated_list/1, + to_bar_separated_list/1, to_ip_port/1]). + +-behaviour(hocon_schema). + +-reflect_type([ log_level/0, flag/0, duration/0, duration_s/0, + bytesize/0, percent/0, file/0, + comma_separated_list/0, bar_separated_list/0, ip_port/0]). + +-export([structs/0, fields/1, translations/0, translation/1]). +-export([t/1, t/3, t/4, ref/1]). +-export([conf_get/2, conf_get/3, keys/2, filter/1]). +-export([ssl/2, tr_ssl/2, tr_password_hash/2]). + +structs() -> ["cluster", "node", "rpc", "log", "lager", + "acl", "mqtt", "zone", "listener", "module", "broker", + "plugins", "sysmon", "os_mon", "vm_mon", "alarm", "telemetry"]. + +fields("cluster") -> + [ {"name", t(atom(), "ekka.cluster_name", emqxcl)} + , {"discovery", t(atom(), undefined, manual)} + , {"autoclean", t(duration(), "ekka.cluster_autoclean", undefined)} + , {"autoheal", t(flag(), "ekka.cluster_autoheal", false)} + , {"static", ref("static")} + , {"mcast", ref("mcast")} + , {"proto_dist", t(union([inet_tcp, inet6_tcp, inet_tls]), "ekka.proto_dist", inet_tcp)} + , {"dns", ref("dns")} + , {"etcd", ref("etcd")} + , {"k8s", ref("k8s")} + ]; + +fields("static") -> + [ {"seeds", t(comma_separated_list())}]; + +fields("mcast") -> + [ {"addr", t(string(), undefined, "239.192.0.1")} + , {"ports", t(comma_separated_list(), undefined, "4369")} + , {"iface", t(string(), undefined, "0.0.0.0")} + , {"ttl", t(integer(), undefined, 255)} + , {"loop", t(flag(), undefined, true)} + , {"sndbuf", t(bytesize(), undefined, "16KB")} + , {"recbuf", t(bytesize(), undefined, "16KB")} + , {"buffer", t(bytesize(), undefined, "32KB")} + ]; + +fields("dns") -> + [ {"app", t(string())}]; + +fields("etcd") -> + [ {"server", t(comma_separated_list())} + , {"prefix", t(string())} + , {"node_ttl", t(duration(), undefined, "1m")} + , {"ssl", ref("etcd_ssl")} + ]; + +fields("etcd_ssl") -> + ssl(undefined, #{}); + +fields("k8s") -> + [ {"apiserver", t(string())} + , {"service_name", t(string())} + , {"address_type", t(union([ip, dns, hostname]))} + , {"app_name", t(string())} + , {"namespace", t(string())} + , {"suffix", t(string(), undefined, "")} + ]; + +fields("node") -> + [ {"name", t(string(), "vm_args.-name", "emqx@127.0.0.1", "NODE_NAME")} + , {"ssl_dist_optfile", t(string(), "vm_args.-ssl_dist_optfile", undefined)} + , {"cookie", t(string(), "vm_args.-setcookie", "emqxsecretcookie", "NODE_COOKIE")} + , {"data_dir", t(string(), "emqx.data_dir", undefined)} + , {"heartbeat", t(flag(), undefined, false)} + , {"async_threads", t(range(1, 1024), "vm_args.+A", undefined)} + , {"process_limit", t(integer(), "vm_args.+P", undefined)} + , {"max_ports", t(range(1024, 134217727), "vm_args.+Q", undefined, "MAX_PORTS")} + , {"dist_buffer_size", fun node__dist_buffer_size/1} + , {"global_gc_interval", t(duration_s(), "emqx.global_gc_interval", undefined)} + , {"fullsweep_after", t(non_neg_integer(), + "vm_args.-env ERL_FULLSWEEP_AFTER", 1000)} + , {"max_ets_tables", t(integer(), "vm_args.+e", 256000)} + , {"crash_dump", t(file(), "vm_args.-env ERL_CRASH_DUMP", undefined)} + , {"dist_net_ticktime", t(integer(), "vm_args.-kernel net_ticktime", undefined)} + , {"dist_listen_min", t(integer(), "kernel.inet_dist_listen_min", undefined)} + , {"dist_listen_max", t(integer(), "kernel.inet_dist_listen_max", undefined)} + , {"backtrace_depth", t(integer(), "emqx.backtrace_depth", 16)} + ]; + +fields("rpc") -> + [ {"mode", t(union(sync, async), "emqx.rpc_mode", async)} + , {"async_batch_size", t(integer(), "gen_rpc.max_batch_size", 256)} + , {"port_discovery",t(union(manual, stateless), "gen_rpc.port_discovery", stateless)} + , {"tcp_server_port", t(integer(), "gen_rpc.tcp_server_port", 5369)} + , {"tcp_client_num", t(range(0, 255), undefined, 0)} + , {"connect_timeout", t(duration(), "gen_rpc.connect_timeout", "5s")} + , {"send_timeout", t(duration(), "gen_rpc.send_timeout", "5s")} + , {"authentication_timeout", t(duration(), "gen_rpc.authentication_timeout", "5s")} + , {"call_receive_timeout", t(duration(), "gen_rpc.call_receive_timeout", "15s")} + , {"socket_keepalive_idle", t(duration_s(), "gen_rpc.socket_keepalive_idle", "7200s")} + , {"socket_keepalive_interval", t(duration_s(), "gen_rpc.socket_keepalive_interval", "75s")} + , {"socket_keepalive_count", t(integer(), "gen_rpc.socket_keepalive_count", 9)} + , {"socket_sndbuf", t(bytesize(), "gen_rpc.socket_sndbuf", "1MB")} + , {"socket_recbuf", t(bytesize(), "gen_rpc.socket_recbuf", "1MB")} + , {"socket_buffer", t(bytesize(), "gen_rpc.socket_buffer", "1MB")} + ]; + +fields("log") -> + [ {"to", t(union([file, console, both]), undefined, file)} + , {"level", t(log_level(), undefined, warning)} + , {"time_offset", t(string(), undefined, "system")} + , {"primary_log_level", t(log_level(), undefined, warning)} + , {"dir", t(string(), undefined, "log")} + , {"file", t(file(), undefined, "emqx.log")} + , {"chars_limit", t(integer(), undefined, -1)} + , {"supervisor_reports", t(union([error, progress]), undefined, error)} + , {"max_depth", t(union([infinity, integer()]), + "kernel.error_logger_format_depth", 20)} + , {"formatter", t(union([text, json]), undefined, text)} + , {"single_line", t(boolean(), undefined, true)} + , {"rotation", ref("rotation")} + , {"size", t(union(bytesize(), infinity), undefined, infinity)} + , {"sync_mode_qlen", t(integer(), undefined, 100)} + , {"drop_mode_qlen", t(integer(), undefined, 3000)} + , {"flush_qlen", t(integer(), undefined, 8000)} + , {"overload_kill", t(flag(), undefined, true)} + , {"overload_kill_mem_size", t(bytesize(), undefined, "30MB")} + , {"overload_kill_qlen", t(integer(), undefined, 20000)} + , {"overload_kill_restart_after", t(union(duration(), infinity), undefined, "5s")} + , {"burst_limit", t(comma_separated_list(), undefined, "disabled")} + , {"error_logger", t(atom(), "kernel.error_logger", silent)} + , {"debug", ref("additional_log_file")} + , {"info", ref("additional_log_file")} + , {"notice", ref("additional_log_file")} + , {"warning", ref("additional_log_file")} + , {"error", ref("additional_log_file")} + , {"critical", ref("additional_log_file")} + , {"alert", ref("additional_log_file")} + , {"emergency", ref("additional_log_file")} + ]; + +fields("additional_log_file") -> + [ {"file", t(string())}]; + +fields("rotation") -> + [ {"enable", t(flag(), undefined, true)} + , {"size", t(bytesize(), undefined, "10MB")} + , {"count", t(integer(), undefined, 5)} + ]; + +fields("lager") -> + [ {"handlers", t(string(), "lager.handlers", "")} + , {"crash_log", t(flag(), "lager.crash_log", false)} + ]; + +fields("acl") -> + [ {"allow_anonymous", t(boolean(), "emqx.allow_anonymous", false)} + , {"acl_nomatch", t(union(allow, deny), "emqx.acl_nomatch", deny)} + , {"acl_file", t(string(), "emqx.acl_file", undefined)} + , {"enable_acl_cache", t(flag(), "emqx.enable_acl_cache", true)} + , {"acl_cache_ttl", t(duration(), "emqx.acl_cache_ttl", "1m")} + , {"acl_cache_max_size", t(range(1, inf), "emqx.acl_cache_max_size", 32)} + , {"acl_deny_action", t(union(ignore, disconnect), "emqx.acl_deny_action", ignore)} + , {"flapping_detect_policy", t(comma_separated_list(), undefined, "30,1m,5m")} + ]; + +fields("mqtt") -> + [ {"max_packet_size", t(bytesize(), "emqx.max_packet_size", "1MB", "MAX_PACKET_SIZE")} + , {"max_clientid_len", t(integer(), "emqx.max_clientid_len", 65535)} + , {"max_topic_levels", t(integer(), "emqx.max_topic_levels", 0)} + , {"max_qos_allowed", t(range(0, 2), "emqx.max_qos_allowed", 2)} + , {"max_topic_alias", t(integer(), "emqx.max_topic_alias", 65535)} + , {"retain_available", t(boolean(), "emqx.retain_available", true)} + , {"wildcard_subscription", t(boolean(), "emqx.wildcard_subscription", true)} + , {"shared_subscription", t(boolean(), "emqx.shared_subscription", true)} + , {"ignore_loop_deliver", t(boolean(), "emqx.ignore_loop_deliver", true)} + , {"strict_mode", t(boolean(), "emqx.strict_mode", false)} + , {"response_information", t(string(), "emqx.response_information", undefined)} + ]; + +fields("zone") -> + [ {"$name", ref("zone_settings")}]; + +fields("zone_settings") -> + [ {"idle_timeout", t(duration(), undefined, "15s")} + , {"allow_anonymous", t(boolean())} + , {"acl_nomatch", t(union(allow, deny))} + , {"enable_acl", t(flag(), undefined, false)} + , {"acl_deny_action", t(union(ignore, disconnect), undefined, ignore)} + , {"enable_ban", t(flag(), undefined, false)} + , {"enable_stats", t(flag(), undefined, false)} + , {"max_packet_size", t(bytesize())} + , {"max_clientid_len", t(integer())} + , {"max_topic_levels", t(integer())} + , {"max_qos_allowed", t(range(0, 2))} + , {"max_topic_alias", t(integer())} + , {"retain_available", t(boolean())} + , {"wildcard_subscription", t(boolean())} + , {"shared_subscription", t(boolean())} + , {"server_keepalive", t(integer())} + , {"keepalive_backoff", t(float(), undefined, 0.75)} + , {"max_subscriptions", t(integer(), undefined, 0)} + , {"upgrade_qos", t(flag(), undefined, false)} + , {"max_inflight", t(range(0, 65535))} + , {"retry_interval", t(duration_s(), undefined, "30s")} + , {"max_awaiting_rel", t(duration(), undefined, 0)} + , {"await_rel_timeout", t(duration_s(), undefined, "300s")} + , {"ignore_loop_deliver", t(boolean())} + , {"session_expiry_interval", t(duration_s(), undefined, "2h")} + , {"max_mqueue_len", t(integer(), undefined, 1000)} + , {"mqueue_priorities", t(comma_separated_list(), undefined, "none")} + , {"mqueue_default_priority", t(union(highest, lowest), undefined, lowest)} + , {"mqueue_store_qos0", t(boolean(), undefined, true)} + , {"enable_flapping_detect", t(flag(), undefined, false)} + , {"rate_limit", ref("rate_limit")} + , {"conn_congestion", ref("conn_congestion")} + , {"quota", ref("quota")} + , {"force_gc_policy", t(bar_separated_list())} + , {"force_shutdown_policy", t(bar_separated_list(), undefined, "default")} + , {"mountpoint", t(string())} + , {"use_username_as_clientid", t(boolean(), undefined, false)} + , {"strict_mode", t(boolean(), undefined, false)} + , {"response_information", t(string())} + , {"bypass_auth_plugins", t(boolean(), undefined, false)} + ]; + +fields("rate_limit") -> + [ {"conn_messages_in", t(comma_separated_list())} + , {"conn_bytes_in", t(comma_separated_list())} + ]; + +fields("conn_congestion") -> + [ {"alarm", t(flag(), undefined, false)} + , {"min_alarm_sustain_duration", t(duration(), undefined, "1m")} + ]; + +fields("quota") -> + [ {"conn_messages_routing", t(comma_separated_list())} + , {"overall_messages_routing", t(comma_separated_list())} + ]; + +fields("listener") -> + [ {"tcp", ref("tcp_listener")} + , {"ssl", ref("ssl_listener")} + , {"ws", ref("ws_listener")} + , {"wss", ref("wss_listener")} + ]; + +fields("tcp_listener") -> + [ {"$name", ref("tcp_listener_settings")}]; + +fields("ssl_listener") -> + [ {"$name", ref("ssl_listener_settings")}]; + +fields("ws_listener") -> + [ {"$name", ref("ws_listener_settings")}]; + +fields("wss_listener") -> + [ {"$name", ref("wss_listener_settings")}]; + +fields("listener_settings") -> + [ {"endpoint", t(union(ip_port(), integer()))} + , {"acceptors", t(integer(), undefined, 8)} + , {"max_connections", t(integer(), undefined, 1024)} + , {"max_conn_rate", t(integer())} + , {"active_n", t(integer(), undefined, 100)} + , {"zone", t(string())} + , {"rate_limit", t(comma_separated_list())} + , {"access", ref("access")} + , {"proxy_protocol", t(flag())} + , {"proxy_protocol_timeout", t(duration())} + , {"backlog", t(integer(), undefined, 1024)} + , {"send_timeout", t(duration(), undefined, "15s")} + , {"send_timeout_close", t(flag(), undefined, true)} + , {"recbuf", t(bytesize())} + , {"sndbuf", t(bytesize())} + , {"buffer", t(bytesize())} + , {"high_watermark", t(bytesize(), undefined, "1MB")} + , {"tune_buffer", t(flag())} + , {"nodelay", t(boolean())} + , {"reuseaddr", t(boolean())} + ]; + +fields("tcp_listener_settings") -> + [ {"peer_cert_as_username", t(cn)} + , {"peer_cert_as_clientid", t(cn)} + ] ++ fields("listener_settings"); + +fields("ssl_listener_settings") -> + [ {"peer_cert_as_username", t(union([cn, dn, crt, pem, md5]))} + , {"peer_cert_as_clientid", t(union([cn, dn, crt, pem, md5]))} + ] ++ + ssl(undefined, #{handshake_timeout => "15s" + , depth => 10 + , reuse_sessions => true}) ++ fields("listener_settings"); + +fields("ws_listener_settings") -> + [ {"mqtt_path", t(string(), undefined, "/mqtt")} + , {"fail_if_no_subprotocol", t(boolean(), undefined, true)} + , {"supported_subprotocols", t(string(), undefined, "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5")} + , {"proxy_address_header", t(string(), undefined, "x-forwarded-for")} + , {"proxy_port_header", t(string(), undefined, "x-forwarded-port")} + , {"compress", t(boolean())} + , {"deflate_opts", ref("deflate_opts")} + , {"idle_timeout", t(duration())} + , {"max_frame_size", t(integer())} + , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} + , {"check_origin_enable", t(boolean(), undefined, false)} + , {"allow_origin_absence", t(boolean(), undefined, true)} + , {"check_origins", t(comma_separated_list())} + % @fixme + ] ++ lists:keydelete("high_watermark", 1, fields("tcp_listener_settings")); + +fields("wss_listener_settings") -> + % @fixme + Ssl = ssl(undefined, #{depth => 10 + , reuse_sessions => true}) ++ fields("listener_settings"), + Settings = lists:ukeymerge(1, Ssl, fields("ws_listener_settings")), + lists:keydelete("high_watermark", 1, Settings); + +fields("access") -> + [ {"$id", t(string(), undefined, undefined)}]; + +fields("deflate_opts") -> + [ {"level", t(union([none, default, best_compression, best_speed]))} + , {"mem_level", t(range(1, 9))} + , {"strategy", t(union([default, filtered, huffman_only, rle]))} + , {"server_context_takeover", t(union(takeover, no_takeover))} + , {"client_context_takeover", t(union(takeover, no_takeover))} + , {"server_max_window_bits", t(integer())} + , {"client_max_window_bits", t(integer())} + ]; + +fields("module") -> + [ {"loaded_file", t(string(), "emqx.modules_loaded_file", undefined)} + , {"presence", ref("presence")} + , {"subscription", ref("subscription")} + , {"rewrite", ref("rewrite")} + ]; + +fields("presence") -> + [ {"qos", t(range(0, 2), undefined, 1)}]; + +fields("subscription") -> + [ {"$id", ref("subscription_settings")}]; + +fields("subscription_settings") -> + [ {"topic", t(string())} + , {"qos", t(range(0, 2), undefined, 1)} + , {"nl", t(range(0, 1), undefined, 0)} + , {"rap", t(range(0, 1), undefined, 0)} + , {"rh", t(range(0, 2), undefined, 0)} + ]; + + +fields("rewrite") -> + [ {"rule", ref("rule")} + , {"pub_rule", ref("rule")} + , {"sub_rule", ref("rule")} + ]; + +fields("rule") -> + [ {"$id", t(string())}]; + +fields("plugins") -> + [ {"etc_dir", t(string(), "emqx.plugins_etc_dir", undefined)} + , {"loaded_file", t(string(), "emqx.plugins_loaded_file", undefined)} + , {"expand_plugins_dir", t(string(), "emqx.expand_plugins_dir", undefined)} + ]; + +fields("broker") -> + [ {"sys_interval", t(duration(), "emqx.broker_sys_interval", "1m")} + , {"sys_heartbeat", t(duration(), "emqx.broker_sys_heartbeat", "30s")} + , {"enable_session_registry", t(flag(), "emqx.enable_session_registry", true)} + , {"session_locking_strategy", t(union([local, leader, quorum, all]), + "emqx.session_locking_strategy", quorum)} + , {"shared_subscription_strategy", t(union(random, round_robin), + "emqx.shared_subscription_strategy", round_robin)} + , {"shared_dispatch_ack_enabled", t(boolean(), "emqx.shared_dispatch_ack_enabled", false)} + , {"route_batch_clean", t(flag(), "emqx.route_batch_clean", true)} + , {"perf", ref("perf")} + ]; + +fields("perf") -> + [ {"route_lock_type", t(union([key, tab, global]), "emqx.route_lock_type", key)} + , {"trie_compaction", t(boolean(), "emqx.trie_compaction", true)} + ]; + +fields("sysmon") -> + [ {"long_gc", t(duration(), undefined, 0)} + , {"long_schedule", t(duration(), undefined, 240)} + , {"large_heap", t(bytesize(), undefined, "8MB")} + , {"busy_dist_port", t(boolean(), undefined, true)} + , {"busy_port", t(boolean(), undefined, false)} + ]; + +fields("os_mon") -> + [ {"cpu_check_interval", t(duration_s(), undefined, 60)} + , {"cpu_high_watermark", t(percent(), undefined, "80%")} + , {"cpu_low_watermark", t(percent(), undefined, "60%")} + , {"mem_check_interval", t(duration_s(), undefined, 60)} + , {"sysmem_high_watermark", t(percent(), undefined, "70%")} + , {"procmem_high_watermark", t(percent(), undefined, "5%")} + ]; + +fields("vm_mon") -> + [ {"check_interval", t(duration_s(), undefined, 30)} + , {"process_high_watermark", t(percent(), undefined, "80%")} + , {"process_low_watermark", t(percent(), undefined, "60%")} + ]; + +fields("alarm") -> + [ {"actions", t(comma_separated_list(), undefined, "log,publish")} + , {"size_limit", t(integer(), undefined, 1000)} + , {"validity_period", t(duration_s(), undefined, "24h")} + ]; + +fields("telemetry") -> + [ {"enabled", t(boolean(), undefined, false)} + , {"url", t(string(), undefined, "https://telemetry-emqx-io.bigpar.vercel.app/api/telemetry")} + , {"report_interval", t(duration_s(), undefined, "7d")} + ]. + + +translations() -> ["ekka", "vm_args", "gen_rpc", "kernel", "emqx"]. + +translation("ekka") -> + [ {"cluster_discovery", fun tr_cluster__discovery/1}]; + +translation("vm_args") -> + [ {"+zdbbl", fun tr_zdbbl/1} + , {"-heart", fun tr_heart/1}]; + +translation("gen_rpc") -> + [ {"tcp_client_num", fun tr_tcp_client_num/1} + , {"tcp_client_port", fun tr_tcp_client_port/1}]; + +translation("kernel") -> + [ {"logger_level", fun tr_logger_level/1} + , {"logger", fun tr_logger/1}]; + +translation("emqx") -> + [ {"flapping_detect_policy", fun tr_flapping_detect_policy/1} + , {"zones", fun tr_zones/1} + , {"listeners", fun tr_listeners/1} + , {"modules", fun tr_modules/1} + , {"sysmon", fun tr_sysmon/1} + , {"os_mon", fun tr_os_mon/1} + , {"vm_mon", fun tr_vm_mon/1} + , {"alarm", fun tr_alarm/1} + , {"telemetry", fun tr_telemetry/1} + ]. + +tr_cluster__discovery(Conf) -> + Strategy = conf_get("cluster.discovery", Conf), + {Strategy, filter(options(Strategy, Conf))}. + +tr_heart(Conf) -> + case conf_get("node.heartbeat", Conf) of + true -> ""; + "on" -> ""; + _ -> undefined + end. + +%% @doc http://www.erlang.org/doc/man/erl.html#%2bzdbbl +node__dist_buffer_size(type) -> bytesize(); +node__dist_buffer_size(validator) -> + fun(ZDBBL) -> + case ZDBBL >= 1024 andalso ZDBBL =< 2147482624 of + true -> + ok; + false -> + {error, "must be between 1KB and 2097151KB"} + end + end; +node__dist_buffer_size(_) -> undefined. + +tr_zdbbl(Conf) -> + case conf_get("node.dist_buffer_size", Conf) of + undefined -> undefined; + X when is_integer(X) -> ceiling(X / 1024); %% Bytes to Kilobytes; + _ -> undefined + end. + +%% Force client to use server listening port, because we do no provide +%% per-node listening port manual mapping from configs. +%% i.e. all nodes in the cluster should agree to the same +%% listening port number. +tr_tcp_client_num(Conf) -> + case conf_get("rpc.tcp_client_num", Conf) of + 0 -> max(1, erlang:system_info(schedulers) div 2); + V -> V + end. + +tr_tcp_client_port(Conf) -> + conf_get("rpc.tcp_server_port", Conf). + +tr_logger_level(Conf) -> conf_get("log.level", Conf). + +tr_logger(Conf) -> + LogTo = conf_get("log.to", Conf), + LogLevel = conf_get("log.level", Conf), + LogType = case conf_get("log.rotation.enable", Conf) of + true -> wrap; + _ -> halt + end, + CharsLimit = case conf_get("log.chars_limit", Conf) of + -1 -> unlimited; + V -> V + end, + SingleLine = conf_get("log.single_line", Conf), + FmtName = conf_get("log.formatter", Conf), + Formatter = formatter(FmtName, CharsLimit, SingleLine), + BurstLimit = conf_get("log.burst_limit", Conf), + {BustLimitOn, {MaxBurstCount, TimeWindow}} = burst_limit(BurstLimit), + FileConf = fun (Filename) -> + BasicConf = + #{type => LogType, + file => filename:join(conf_get("log.dir", Conf), Filename), + max_no_files => conf_get("log.rotation.count", Conf), + sync_mode_qlen => conf_get("log.sync_mode_qlen", Conf), + drop_mode_qlen => conf_get("log.drop_mode_qlen", Conf), + flush_qlen => conf_get("log.flush_qlen", Conf), + overload_kill_enable => conf_get("log.overload_kill", Conf), + overload_kill_qlen => conf_get("log.overload_kill_qlen", Conf), + overload_kill_mem_size => conf_get("log.overload_kill_mem_size", Conf), + overload_kill_restart_after => conf_get("log.overload_kill_restart_after", Conf), + burst_limit_enable => BustLimitOn, + burst_limit_max_count => MaxBurstCount, + burst_limit_window_time => TimeWindow + }, + MaxNoBytes = case LogType of + wrap -> conf_get("log.rotation.size", Conf); + halt -> conf_get("log.size", Conf) + end, + BasicConf#{max_no_bytes => MaxNoBytes} end, + + Filters = case conf_get("log.supervisor_reports", Conf) of + error -> [{drop_progress_reports, {fun logger_filters:progress/2, stop}}]; + progress -> [] + end, + + %% For the default logger that outputs to console + DefaultHandler = + if LogTo =:= console orelse LogTo =:= both -> + [{handler, console, logger_std_h, + #{level => LogLevel, + config => #{type => standard_io}, + formatter => Formatter, + filters => Filters + } + }]; + true -> + [{handler, default, undefined}] + end, + + %% For the file logger + FileHandler = + if LogTo =:= file orelse LogTo =:= both -> + [{handler, file, logger_disk_log_h, + #{level => LogLevel, + config => FileConf(conf_get("log.file", Conf)), + formatter => Formatter, + filesync_repeat_interval => no_repeat, + filters => Filters + }}]; + true -> [] + end, + + AdditionalLogFiles = additional_log_files(Conf), + AdditionalHandlers = + [{handler, list_to_atom("file_for_"++Level), logger_disk_log_h, + #{level => list_to_atom(Level), + config => FileConf(Filename), + formatter => Formatter, + filesync_repeat_interval => no_repeat}} + || {Level, Filename} <- AdditionalLogFiles], + + DefaultHandler ++ FileHandler ++ AdditionalHandlers. + +tr_flapping_detect_policy(Conf) -> + [Threshold, Duration, Interval] = conf_get("acl.flapping_detect_policy", Conf), + ParseDuration = fun(S, F) -> + case F(S) of + {ok, I} -> I; + {error, Reason} -> error({duration, Reason}) + end end, + #{threshold => list_to_integer(Threshold), + duration => ParseDuration(Duration, fun to_duration/1), + banned_interval => ParseDuration(Interval, fun to_duration_s/1) + }. + +tr_zones(Conf) -> + Names = lists:usort(keys("zone", Conf)), + lists:foldl( + fun(Name, Zones) -> + Zone = keys("zone." ++ Name, Conf), + Mapped = lists:flatten([map_zones(K, conf_get(["zone", Name, K], Conf)) || K <- Zone]), + [{list_to_atom(Name), lists:filter(fun ({K, []}) when K =:= ratelimit; K =:= quota -> false; + ({_, undefined}) -> false; + (_) -> true end, Mapped)} | Zones] + end, [], Names). + +tr_listeners(Conf) -> + Atom = fun(undefined) -> undefined; + (B) when is_binary(B)-> binary_to_atom(B); + (S) when is_list(S) -> list_to_atom(S) end, + + Access = fun(S) -> + [A, CIDR] = string:tokens(S, " "), + {list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end} + end, + + AccOpts = fun(Prefix) -> + case keys(Prefix ++ ".access", Conf) of + [] -> []; + Ids -> + [{access_rules, [Access(conf_get(Prefix ++ ".access." ++ Id, Conf)) || Id <- Ids]}] + end end, + + RateLimit = fun(undefined) -> + undefined; + ([L, D]) -> + Limit = case to_bytesize(L) of + {ok, I0} -> I0; + {error, R0} -> error({bytesize, R0}) + end, + Duration = case to_duration_s(D) of + {ok, I1} -> I1; + {error, R1} -> error({duration, R1}) + end, + {Limit, Duration} + end, + + CheckOrigin = fun(S) -> [ list_to_binary(string:trim(O)) || O <- S] end, + + WsOpts = fun(Prefix) -> + case conf_get(Prefix ++ ".check_origins", Conf) of + undefined -> undefined; + Rules -> lists:flatten(CheckOrigin(Rules)) + end + end, + + LisOpts = fun(Prefix) -> + filter([{acceptors, conf_get(Prefix ++ ".acceptors", Conf)}, + {mqtt_path, conf_get(Prefix ++ ".mqtt_path", Conf)}, + {max_connections, conf_get(Prefix ++ ".max_connections", Conf)}, + {max_conn_rate, conf_get(Prefix ++ ".max_conn_rate", Conf)}, + {active_n, conf_get(Prefix ++ ".active_n", Conf)}, + {tune_buffer, conf_get(Prefix ++ ".tune_buffer", Conf)}, + {zone, Atom(conf_get(Prefix ++ ".zone", Conf))}, + {rate_limit, RateLimit(conf_get(Prefix ++ ".rate_limit", Conf))}, + {proxy_protocol, conf_get(Prefix ++ ".proxy_protocol", Conf)}, + {proxy_address_header, list_to_binary(string:lowercase(conf_get(Prefix ++ ".proxy_address_header", Conf, <<"">>)))}, + {proxy_port_header, list_to_binary(string:lowercase(conf_get(Prefix ++ ".proxy_port_header", Conf, <<"">>)))}, + {proxy_protocol_timeout, conf_get(Prefix ++ ".proxy_protocol_timeout", Conf)}, + {fail_if_no_subprotocol, conf_get(Prefix ++ ".fail_if_no_subprotocol", Conf)}, + {supported_subprotocols, string:tokens(conf_get(Prefix ++ ".supported_subprotocols", Conf, ""), ", ")}, + {peer_cert_as_username, conf_get(Prefix ++ ".peer_cert_as_username", Conf)}, + {peer_cert_as_clientid, conf_get(Prefix ++ ".peer_cert_as_clientid", Conf)}, + {compress, conf_get(Prefix ++ ".compress", Conf)}, + {idle_timeout, conf_get(Prefix ++ ".idle_timeout", Conf)}, + {max_frame_size, conf_get(Prefix ++ ".max_frame_size", Conf)}, + {mqtt_piggyback, conf_get(Prefix ++ ".mqtt_piggyback", Conf)}, + {check_origin_enable, conf_get(Prefix ++ ".check_origin_enable", Conf)}, + {allow_origin_absence, conf_get(Prefix ++ ".allow_origin_absence", Conf)}, + {check_origins, WsOpts(Prefix)} | AccOpts(Prefix)]) + end, + DeflateOpts = fun(Prefix) -> + filter([{level, conf_get(Prefix ++ ".deflate_opts.level", Conf)}, + {mem_level, conf_get(Prefix ++ ".deflate_opts.mem_level", Conf)}, + {strategy, conf_get(Prefix ++ ".deflate_opts.strategy", Conf)}, + {server_context_takeover, conf_get(Prefix ++ ".deflate_opts.server_context_takeover", Conf)}, + {client_context_takeover, conf_get(Prefix ++ ".deflate_opts.client_context_takeover", Conf)}, + {server_max_windows_bits, conf_get(Prefix ++ ".deflate_opts.server_max_window_bits", Conf)}, + {client_max_windows_bits, conf_get(Prefix ++ ".deflate_opts.client_max_window_bits", Conf)}]) + end, + TcpOpts = fun(Prefix) -> + filter([{backlog, conf_get(Prefix ++ ".backlog", Conf)}, + {send_timeout, conf_get(Prefix ++ ".send_timeout", Conf)}, + {send_timeout_close, conf_get(Prefix ++ ".send_timeout_close", Conf)}, + {recbuf, conf_get(Prefix ++ ".recbuf", Conf)}, + {sndbuf, conf_get(Prefix ++ ".sndbuf", Conf)}, + {buffer, conf_get(Prefix ++ ".buffer", Conf)}, + {high_watermark, conf_get(Prefix ++ ".high_watermark", Conf)}, + {nodelay, conf_get(Prefix ++ ".nodelay", Conf, true)}, + {reuseaddr, conf_get(Prefix ++ ".reuseaddr", Conf)}]) + end, + + SslOpts = fun(Prefix) -> + Opts = tr_ssl(Prefix, Conf), + case lists:keyfind(ciphers, 1, Opts) of + false -> + error(Prefix ++ ".ciphers or " ++ Prefix ++ ".psk_ciphers is absent"); + _ -> + Opts + end end, + + TcpListeners = fun(Type, Name) -> + Prefix = string:join(["listener", Type, Name], "."), + ListenOnN = case conf_get(Prefix ++ ".endpoint", Conf) of + undefined -> []; + ListenOn -> ListenOn + end, + [#{ proto => Atom(Type) + , name => Name + , listen_on => ListenOnN + , opts => [ {deflate_options, DeflateOpts(Prefix)} + , {tcp_options, TcpOpts(Prefix)} + | LisOpts(Prefix) + ] + } + ] + end, + SslListeners = fun(Type, Name) -> + Prefix = string:join(["listener", Type, Name], "."), + case conf_get(Prefix ++ ".endpoint", Conf) of + undefined -> + []; + ListenOn -> + [#{ proto => Atom(Type) + , name => Name + , listen_on => ListenOn + , opts => [ {deflate_options, DeflateOpts(Prefix)} + , {tcp_options, TcpOpts(Prefix)} + , {ssl_options, SslOpts(Prefix)} + | LisOpts(Prefix) + ] + } + ] + end end, + + + lists:flatten([TcpListeners("tcp", Name) || Name <- keys("listener.tcp", Conf)] + ++ [TcpListeners("ws", Name) || Name <- keys("listener.ws", Conf)] + ++ [SslListeners("ssl", Name) || Name <- keys("listener.ssl", Conf)] + ++ [SslListeners("wss", Name) || Name <- keys("listener.wss", Conf)]). + +tr_modules(Conf) -> + Subscriptions = fun() -> + List = keys("module.subscription", Conf), + TopicList = [{N, conf_get(["module", "subscription", N, "topic"], Conf)}|| N <- List], + [{list_to_binary(T), #{ qos => conf_get("module.subscription." ++ N ++ ".qos", Conf, 0), + nl => conf_get("module.subscription." ++ N ++ ".nl", Conf, 0), + rap => conf_get("module.subscription." ++ N ++ ".rap", Conf, 0), + rh => conf_get("module.subscription." ++ N ++ ".rh", Conf, 0) + }} || {N, T} <- TopicList] + end, + Rewrites = fun() -> + Rules = keys("module.rewrite.rule", Conf), + PubRules = keys("module.rewrite.pub_rule", Conf), + SubRules = keys("module.rewrite.sub_rule", Conf), + TotalRules = + [ {["module", "rewrite", "pub", "rule", R], conf_get(["module.rewrite.rule", R], Conf)} || R <- Rules] ++ + [ {["module", "rewrite", "pub", "rule", R], conf_get(["module.rewrite.pub_rule", R], Conf)} || R <- PubRules] ++ + [ {["module", "rewrite", "sub", "rule", R], conf_get(["module.rewrite.rule", R], Conf)} || R <- Rules] ++ + [ {["module", "rewrite", "sub", "rule", R], conf_get(["module.rewrite.sub_rule", R], Conf)} || R <- SubRules], + lists:map(fun({[_, "rewrite", PubOrSub, "rule", _], Rule}) -> + [Topic, Re, Dest] = string:tokens(Rule, " "), + {rewrite, list_to_atom(PubOrSub), list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} + end, TotalRules) + end, + lists:append([ + [{emqx_mod_presence, [{qos, conf_get("module.presence.qos", Conf, 1)}]}], + [{emqx_mod_subscription, Subscriptions()}], + [{emqx_mod_rewrite, Rewrites()}], + [{emqx_mod_topic_metrics, []}], + [{emqx_mod_delayed, []}], + [{emqx_mod_acl_internal, [{acl_file, conf_get("acl.acl_file", Conf)}]}] + ]). + +tr_sysmon(Conf) -> + Keys = maps:to_list(conf_get("sysmon", Conf, #{})), + [{binary_to_atom(K), maps:get(value, V)} || {K, V} <- Keys]. + +tr_os_mon(Conf) -> + [{cpu_check_interval, conf_get("os_mon.cpu_check_interval", Conf)} + , {cpu_high_watermark, conf_get("os_mon.cpu_high_watermark", Conf) * 100} + , {cpu_low_watermark, conf_get("os_mon.cpu_low_watermark", Conf) * 100} + , {mem_check_interval, conf_get("os_mon.mem_check_interval", Conf)} + , {sysmem_high_watermark, conf_get("os_mon.sysmem_high_watermark", Conf) * 100} + , {procmem_high_watermark, conf_get("os_mon.procmem_high_watermark", Conf) * 100} + ]. + +tr_vm_mon(Conf) -> + [ {check_interval, conf_get("vm_mon.check_interval", Conf)} + , {process_high_watermark, conf_get("vm_mon.process_high_watermark", Conf) * 100} + , {process_low_watermark, conf_get("vm_mon.process_low_watermark", Conf) * 100} + ]. + +tr_alarm(Conf) -> + [ {actions, [list_to_atom(Action) || Action <- conf_get("alarm.actions", Conf)]} + , {size_limit, conf_get("alarm.size_limit", Conf)} + , {validity_period, conf_get("alarm.validity_period", Conf)} + ]. + +tr_telemetry(Conf) -> + [ {enabled, conf_get("telemetry.enabled", Conf)} + , {url, conf_get("telemetry.url", Conf)} + , {report_interval, conf_get("telemetry.report_interval", Conf)} + ]. + +%% helpers + +options(static, Conf) -> + [{seeds, [list_to_atom(S) || S <- conf_get("cluster.static.seeds", Conf, "")]}]; +options(mcast, Conf) -> + {ok, Addr} = inet:parse_address(conf_get("cluster.mcast.addr", Conf)), + {ok, Iface} = inet:parse_address(conf_get("cluster.mcast.iface", Conf)), + Ports = [list_to_integer(S) || S <- conf_get("cluster.mcast.ports", Conf)], + [{addr, Addr}, {ports, Ports}, {iface, Iface}, + {ttl, conf_get("cluster.mcast.ttl", Conf, 1)}, + {loop, conf_get("cluster.mcast.loop", Conf, true)}]; +options(dns, Conf) -> + [{name, conf_get("cluster.dns.name", Conf)}, + {app, conf_get("cluster.dns.app", Conf)}]; +options(etcd, Conf) -> + Namespace = "cluster.etcd.ssl", + SslOpts = fun(C) -> + Options = keys(Namespace, C), + lists:map(fun(Key) -> {list_to_atom(Key), conf_get([Namespace, Key], Conf)} end, Options) end, + [{server, conf_get("cluster.etcd.server", Conf)}, + {prefix, conf_get("cluster.etcd.prefix", Conf, "emqxcl")}, + {node_ttl, conf_get("cluster.etcd.node_ttl", Conf, 60)}, + {ssl_options, filter(SslOpts(Conf))}]; +options(k8s, Conf) -> + [{apiserver, conf_get("cluster.k8s.apiserver", Conf)}, + {service_name, conf_get("cluster.k8s.service_name", Conf)}, + {address_type, conf_get("cluster.k8s.address_type", Conf, ip)}, + {app_name, conf_get("cluster.k8s.app_name", Conf)}, + {namespace, conf_get("cluster.k8s.namespace", Conf)}, + {suffix, conf_get("cluster.k8s.suffix", Conf, "")}]; +options(manual, _Conf) -> + []. + +formatter(json, CharsLimit, SingleLine) -> + {emqx_logger_jsonfmt, + #{chars_limit => CharsLimit, + single_line => SingleLine + }}; +formatter(text, CharsLimit, SingleLine) -> + {emqx_logger_textfmt, + #{template => + [time," [",level,"] ", + {clientid, + [{peername, + [clientid,"@",peername," "], + [clientid, " "]}], + [{peername, + [peername," "], + []}]}, + msg,"\n"], + chars_limit => CharsLimit, + single_line => SingleLine + }}. + +burst_limit(["disabled"]) -> + {false, {20000, 1000}}; +burst_limit([Count, Window]) -> + {true, {list_to_integer(Count), + case to_duration(Window) of + {ok, I} -> I; + {error, R} -> error({duration, R}) + end}}. + +%% For creating additional log files for specific log levels. +additional_log_files(Conf) -> + LogLevel = ["debug", "info", "notice", "warning", + "error", "critical", "alert", "emergency"], + additional_log_files(Conf, LogLevel, []). + +additional_log_files(_Conf, [], Acc) -> + Acc; +additional_log_files(Conf, [L | More], Acc) -> + case conf_get(["log", L, "file"], Conf) of + undefined -> additional_log_files(Conf, More, Acc); + F -> additional_log_files(Conf, More, [{L, F} | Acc]) + end. + +rate_limit_byte_dur([L, D]) -> + Limit = case to_bytesize(L) of + {ok, I0} -> I0; + {error, R0} -> error({bytesize, R0}) + end, + Duration = case to_duration_s(D) of + {ok, I1} -> I1; + {error, R1} -> error({duration, R1}) + end, + {Limit, Duration}. + +rate_limit_num_dur([L, D]) -> + Limit = case string:to_integer(L) of + {Int, []} when is_integer(Int) -> Int; + _ -> error("failed to parse bytesize string") + end, + Duration = case to_duration_s(D) of + {ok, I} -> I; + {error, Reason} -> error(Reason) + end, + {Limit, Duration}. + +map_zones(_, undefined) -> + {undefined, undefined}; +map_zones("force_gc_policy", [Count, Bytes]) -> + GcPolicy = case to_bytesize(Bytes) of + {error, Reason} -> + error({bytesize, Reason}); + {ok, Bytes1} -> + #{bytes => Bytes1, + count => list_to_integer(Count)} + end, + {force_gc_policy, GcPolicy}; +map_zones("force_shutdown_policy", ["default"]) -> + WordSize = erlang:system_info(wordsize), + {DefaultLen, DefaultSize} = + case WordSize of + 8 -> % arch_64 + {10000, hocon_postprocess:bytesize("64MB")}; + 4 -> % arch_32 + {1000, hocon_postprocess:bytesize("32MB")} + end, + {force_shutdown_policy, #{message_queue_len => DefaultLen, + max_heap_size => DefaultSize div WordSize + }}; +map_zones("force_shutdown_policy", [Len, Siz]) -> + WordSize = erlang:system_info(wordsize), + MaxSiz = case WordSize of + 8 -> % arch_64 + (1 bsl 59) - 1; + 4 -> % arch_32 + (1 bsl 27) - 1 + end, + ShutdownPolicy = + case to_bytesize(Siz) of + {error, Reason} -> + error(Reason); + {ok, Siz1} when Siz1 > MaxSiz -> + error(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); + {ok, Siz1} -> + #{message_queue_len => list_to_integer(Len), + max_heap_size => Siz1 div WordSize} + end, + {force_shutdown_policy, ShutdownPolicy}; +map_zones("mqueue_priorities", Val) -> + case Val of + ["none"] -> {mqueue_priorities, none}; % NO_PRIORITY_TABLE + _ -> + MqueuePriorities = lists:foldl(fun(T, Acc) -> + %% NOTE: space in "= " is intended + [Topic, Prio] = string:tokens(T, "= "), + P = list_to_integer(Prio), + (P < 0 orelse P > 255) andalso error({bad_priority, Topic, Prio}), + maps:put(iolist_to_binary(Topic), P, Acc) + end, #{}, Val), + {mqueue_priorities, MqueuePriorities} + end; +map_zones("mountpoint", Val) -> + {mountpoint, iolist_to_binary(Val)}; +map_zones("response_information", Val) -> + {response_information, iolist_to_binary(Val)}; +map_zones("rate_limit", Conf) -> + Messages = case conf_get("conn_messages_in", #{value => Conf}) of + undefined -> + []; + M -> + [{conn_messages_in, rate_limit_num_dur(M)}] + end, + Bytes = case conf_get("conn_bytes_in", #{value => Conf}) of + undefined -> + []; + B -> + [{conn_bytes_in, rate_limit_byte_dur(B)}] + end, + {ratelimit, Messages ++ Bytes}; +map_zones("conn_congestion", Conf) -> + Alarm = case conf_get("alarm", #{value => Conf}) of + undefined -> + []; + A -> + [{conn_congestion_alarm_enabled, A}] + end, + MinAlarm = case conf_get("min_alarm_sustain_duration", #{value => Conf}) of + undefined -> + []; + M -> + [{conn_congestion_min_alarm_sustain_duration, M}] + end, + Alarm ++ MinAlarm; +map_zones("quota", Conf) -> + Conn = case conf_get("conn_messages_routing", #{value => Conf}) of + undefined -> + []; + C -> + [{conn_messages_routing, rate_limit_num_dur(C)}] + end, + Overall = case conf_get("overall_messages_routing", #{value => Conf}) of + undefined -> + []; + O -> + [{overall_messages_routing, rate_limit_num_dur(O)}] + end, + {quota, Conn ++ Overall}; +map_zones(Opt, Val) -> + {list_to_atom(Opt), Val}. + + +%% utils + +-spec(conf_get(string() | [string()], hocon:config()) -> term()). +conf_get(Key, Conf) -> + V = hocon_schema:deep_get(Key, Conf, value), + case is_binary(V) of + true -> + binary_to_list(V); + false -> + V + end. + +conf_get(Key, Conf, Default) -> + V = hocon_schema:deep_get(Key, Conf, value, Default), + case is_binary(V) of + true -> + binary_to_list(V); + false -> + V + end. + +filter(Opts) -> + [{K, V} || {K, V} <- Opts, V =/= undefined]. + +%% generate a ssl field. +%% ssl("emqx", #{"verify" => verify_peer}) will return +%% [ {"cacertfile", t(string(), "emqx.cacertfile", undefined)} +%% , {"certfile", t(string(), "emqx.certfile", undefined)} +%% , {"keyfile", t(string(), "emqx.keyfile", undefined)} +%% , {"verify", t(union(verify_peer, verify_none), "emqx.verify", verify_peer)} +%% , {"server_name_indication", "emqx.server_name_indication", undefined)} +%% ... +ssl(Mapping, Defaults) -> + M = fun (Field) -> + case (Mapping) of + undefined -> undefined; + _ -> Mapping ++ "." ++ Field + end end, + D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, + [ {"enable", t(flag(), M("enable"), D("enable"))} + , {"cacertfile", t(string(), M("cacertfile"), D("cacertfile"))} + , {"certfile", t(string(), M("certfile"), D("certfile"))} + , {"keyfile", t(string(), M("keyfile"), D("keyfile"))} + , {"verify", t(union(verify_peer, verify_none), M("verify"), D("verify"))} + , {"fail_if_no_peer_cert", t(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} + , {"secure_renegotiate", t(flag(), M("secure_renegotiate"), D("secure_renegotiate"))} + , {"reuse_sessions", t(flag(), M("reuse_sessions"), D("reuse_sessions"))} + , {"honor_cipher_order", t(flag(), M("honor_cipher_order"), D("honor_cipher_order"))} + , {"handshake_timeout", t(duration(), M("handshake_timeout"), D("handshake_timeout"))} + , {"depth", t(integer(), M("depth"), D("depth"))} + , {"password", t(string(), M("key_password"), D("key_password"))} + , {"dhfile", t(string(), M("dhfile"), D("dhfile"))} + , {"server_name_indication", t(union(disable, string()), M("server_name_indication"), + D("server_name_indication"))} + , {"tls_versions", t(comma_separated_list(), M("tls_versions"), D("tls_versions"))} + , {"ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))} + , {"psk_ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))}]. + +tr_ssl(Field, Conf) -> + Versions = case conf_get([Field, "tls_versions"], Conf) of + undefined -> undefined; + Vs -> [list_to_existing_atom(V) || V <- Vs] + end, + TLSCiphers = conf_get([Field, "ciphers"], Conf), + PSKCiphers = conf_get([Field, "psk_ciphers"], Conf), + Ciphers = ciphers(TLSCiphers, PSKCiphers, Field), + case emqx_schema:conf_get([Field, "enable"], Conf) of + X when X =:= true orelse X =:= undefined -> + filter([{versions, Versions}, + {ciphers, Ciphers}, + {user_lookup_fun, user_lookup_fun(PSKCiphers)}, + {handshake_timeout, conf_get([Field, "handshake_timeout"], Conf)}, + {depth, conf_get([Field, "depth"], Conf)}, + {password, conf_get([Field, "key_password"], Conf)}, + {dhfile, conf_get([Field, "dhfile"], Conf)}, + {keyfile, emqx_schema:conf_get([Field, "keyfile"], Conf)}, + {certfile, emqx_schema:conf_get([Field, "certfile"], Conf)}, + {cacertfile, emqx_schema:conf_get([Field, "cacertfile"], Conf)}, + {verify, emqx_schema:conf_get([Field, "verify"], Conf)}, + {fail_if_no_peer_cert, conf_get([Field, "fail_if_no_peer_cert"], Conf)}, + {secure_renegotiate, conf_get([Field, "secure_renegotiate"], Conf)}, + {reuse_sessions, conf_get([Field, "reuse_sessions"], Conf)}, + {honor_cipher_order, conf_get([Field, "honor_cipher_order"], Conf)}, + {server_name_indication, emqx_schema:conf_get([Field, "server_name_indication"], Conf)} + ]); + _ -> + [] + end. + +map_psk_ciphers(PSKCiphers) -> + lists:map( + fun("PSK-AES128-CBC-SHA") -> {psk, aes_128_cbc, sha}; + ("PSK-AES256-CBC-SHA") -> {psk, aes_256_cbc, sha}; + ("PSK-3DES-EDE-CBC-SHA") -> {psk, '3des_ede_cbc', sha}; + ("PSK-RC4-SHA") -> {psk, rc4_128, sha} + end, PSKCiphers). + +ciphers(undefined, undefined, _) -> + undefined; +ciphers(TLSCiphers, undefined, _) -> + TLSCiphers; +ciphers(undefined, PSKCiphers, _) -> + map_psk_ciphers(PSKCiphers); +ciphers(_, _, Field) -> + error(Field ++ ".ciphers and " ++ Field ++ ".psk_ciphers cannot be configured at the same time"). + +user_lookup_fun(undefined) -> + undefined; +user_lookup_fun(_PSKCiphers) -> + {fun emqx_psk:lookup/3, <<>>}. + +tr_password_hash(Field, Conf) -> + case emqx_schema:conf_get([Field, "password_hash"], Conf) of + [Hash] -> list_to_atom(Hash); + [Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)}; + [Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), + list_to_integer(Iterations), list_to_integer(Dklen)}; + _ -> plain + end. + + +%% @private return a list of keys in a parent field +-spec(keys(string(), hocon:config()) -> [string()]). +keys(Parent, Conf) -> + [binary_to_list(B) || B <- maps:keys(conf_get(Parent, Conf, #{}))]. + +-spec ceiling(float()) -> integer(). +ceiling(X) -> + T = erlang:trunc(X), + case (X - T) of + Neg when Neg < 0 -> T; + Pos when Pos > 0 -> T + 1; + _ -> T + end. + +%% types + +t(T) -> + fun (type) -> T; (_) -> undefined end. + +t(T, M, D) -> + fun (type) -> T; (mapping) -> M; (default) -> D; (_) -> undefined end. + +t(T, M, D, O) -> + fun (type) -> T; + (mapping) -> M; + (default) -> D; + (override_env) -> O; + (_) -> undefined end. + +ref(Field) -> + fun (type) -> Field; (_) -> undefined end. + +to_flag(Str) -> + {ok, hocon_postprocess:onoff(Str)}. + +to_duration(Str) -> + case hocon_postprocess:duration(Str) of + I when is_integer(I) -> {ok, I}; + _ -> {error, Str} + end. + +to_duration_s(Str) -> + case hocon_postprocess:duration(Str) of + I when is_integer(I) -> {ok, ceiling(I / 1000)}; + _ -> {error, Str} + end. + +to_bytesize(Str) -> + case hocon_postprocess:bytesize(Str) of + I when is_integer(I) -> {ok, I}; + _ -> {error, Str} + end. + +to_percent(Str) -> + {ok, hocon_postprocess:percent(Str)}. + +to_comma_separated_list(Str) -> + {ok, string:tokens(Str, ", ")}. + +to_bar_separated_list(Str) -> + {ok, string:tokens(Str, "| ")}. + +to_ip_port(Str) -> + case string:tokens(Str, ":") of + [Ip, Port] -> + case inet:parse_address(Ip) of + {ok, R} -> {ok, {R, list_to_integer(Port)}}; + _ -> {error, Str} + end; + _ -> {error, Str} + end. diff --git a/src/emqx_sequence.erl b/apps/emqx/src/emqx_sequence.erl similarity index 100% rename from src/emqx_sequence.erl rename to apps/emqx/src/emqx_sequence.erl diff --git a/src/emqx_session.erl b/apps/emqx/src/emqx_session.erl similarity index 100% rename from src/emqx_session.erl rename to apps/emqx/src/emqx_session.erl diff --git a/src/emqx_shared_sub.erl b/apps/emqx/src/emqx_shared_sub.erl similarity index 100% rename from src/emqx_shared_sub.erl rename to apps/emqx/src/emqx_shared_sub.erl diff --git a/src/emqx_stats.erl b/apps/emqx/src/emqx_stats.erl similarity index 100% rename from src/emqx_stats.erl rename to apps/emqx/src/emqx_stats.erl diff --git a/src/emqx_sup.erl b/apps/emqx/src/emqx_sup.erl similarity index 100% rename from src/emqx_sup.erl rename to apps/emqx/src/emqx_sup.erl diff --git a/src/emqx_sys.erl b/apps/emqx/src/emqx_sys.erl similarity index 100% rename from src/emqx_sys.erl rename to apps/emqx/src/emqx_sys.erl diff --git a/src/emqx_sys_mon.erl b/apps/emqx/src/emqx_sys_mon.erl similarity index 100% rename from src/emqx_sys_mon.erl rename to apps/emqx/src/emqx_sys_mon.erl diff --git a/src/emqx_sys_sup.erl b/apps/emqx/src/emqx_sys_sup.erl similarity index 100% rename from src/emqx_sys_sup.erl rename to apps/emqx/src/emqx_sys_sup.erl diff --git a/src/emqx_tables.erl b/apps/emqx/src/emqx_tables.erl similarity index 100% rename from src/emqx_tables.erl rename to apps/emqx/src/emqx_tables.erl diff --git a/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl similarity index 100% rename from src/emqx_tls_lib.erl rename to apps/emqx/src/emqx_tls_lib.erl diff --git a/src/emqx_topic.erl b/apps/emqx/src/emqx_topic.erl similarity index 100% rename from src/emqx_topic.erl rename to apps/emqx/src/emqx_topic.erl diff --git a/src/emqx_tracer.erl b/apps/emqx/src/emqx_tracer.erl similarity index 100% rename from src/emqx_tracer.erl rename to apps/emqx/src/emqx_tracer.erl diff --git a/src/emqx_trie.erl b/apps/emqx/src/emqx_trie.erl similarity index 100% rename from src/emqx_trie.erl rename to apps/emqx/src/emqx_trie.erl diff --git a/src/emqx_types.erl b/apps/emqx/src/emqx_types.erl similarity index 100% rename from src/emqx_types.erl rename to apps/emqx/src/emqx_types.erl diff --git a/src/emqx_vm.erl b/apps/emqx/src/emqx_vm.erl similarity index 100% rename from src/emqx_vm.erl rename to apps/emqx/src/emqx_vm.erl diff --git a/src/emqx_vm_mon.erl b/apps/emqx/src/emqx_vm_mon.erl similarity index 100% rename from src/emqx_vm_mon.erl rename to apps/emqx/src/emqx_vm_mon.erl diff --git a/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl similarity index 98% rename from src/emqx_ws_connection.erl rename to apps/emqx/src/emqx_ws_connection.erl index 389a81e7b..7bc68c271 100644 --- a/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -257,13 +257,16 @@ websocket_init([Req, Opts]) -> case proplists:get_bool(proxy_protocol, Opts) andalso maps:get(proxy_header, Req) of #{src_address := SrcAddr, src_port := SrcPort, ssl := SSL} -> - ProxyName = {SrcAddr, SrcPort}, + SourceName = {SrcAddr, SrcPort}, %% Notice: Only CN is available in Proxy Protocol V2 additional info - ProxySSL = case maps:get(cn, SSL, undefined) of + SourceSSL = case maps:get(cn, SSL, undefined) of undeined -> nossl; CN -> [{pp2_ssl_cn, CN}] end, - {ProxyName, ProxySSL}; + {SourceName, SourceSSL}; + #{src_address := SrcAddr, src_port := SrcPort} -> + SourceName = {SrcAddr, SrcPort}, + {SourceName , nossl}; _ -> {get_peer(Req, Opts), cowboy_req:cert(Req)} end, diff --git a/src/emqx_zone.erl b/apps/emqx/src/emqx_zone.erl similarity index 100% rename from src/emqx_zone.erl rename to apps/emqx/src/emqx_zone.erl diff --git a/test/emqx_SUITE.erl b/apps/emqx/test/emqx_SUITE.erl similarity index 100% rename from test/emqx_SUITE.erl rename to apps/emqx/test/emqx_SUITE.erl diff --git a/test/emqx_SUITE_data/acl.conf b/apps/emqx/test/emqx_SUITE_data/acl.conf similarity index 100% rename from test/emqx_SUITE_data/acl.conf rename to apps/emqx/test/emqx_SUITE_data/acl.conf diff --git a/test/emqx_SUITE_data/loaded_modules b/apps/emqx/test/emqx_SUITE_data/loaded_modules similarity index 100% rename from test/emqx_SUITE_data/loaded_modules rename to apps/emqx/test/emqx_SUITE_data/loaded_modules diff --git a/test/emqx_SUITE_data/loaded_plugins b/apps/emqx/test/emqx_SUITE_data/loaded_plugins similarity index 100% rename from test/emqx_SUITE_data/loaded_plugins rename to apps/emqx/test/emqx_SUITE_data/loaded_plugins diff --git a/test/emqx_access_SUITE_data/acl.conf b/apps/emqx/test/emqx_access_SUITE_data/acl.conf similarity index 100% rename from test/emqx_access_SUITE_data/acl.conf rename to apps/emqx/test/emqx_access_SUITE_data/acl.conf diff --git a/test/emqx_access_SUITE_data/acl_deny_action.conf b/apps/emqx/test/emqx_access_SUITE_data/acl_deny_action.conf similarity index 100% rename from test/emqx_access_SUITE_data/acl_deny_action.conf rename to apps/emqx/test/emqx_access_SUITE_data/acl_deny_action.conf diff --git a/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl similarity index 100% rename from test/emqx_access_control_SUITE.erl rename to apps/emqx/test/emqx_access_control_SUITE.erl diff --git a/test/emqx_access_rule_SUITE.erl b/apps/emqx/test/emqx_access_rule_SUITE.erl similarity index 100% rename from test/emqx_access_rule_SUITE.erl rename to apps/emqx/test/emqx_access_rule_SUITE.erl diff --git a/test/emqx_acl_cache_SUITE.erl b/apps/emqx/test/emqx_acl_cache_SUITE.erl similarity index 100% rename from test/emqx_acl_cache_SUITE.erl rename to apps/emqx/test/emqx_acl_cache_SUITE.erl diff --git a/test/emqx_acl_test_mod.erl b/apps/emqx/test/emqx_acl_test_mod.erl similarity index 100% rename from test/emqx_acl_test_mod.erl rename to apps/emqx/test/emqx_acl_test_mod.erl diff --git a/test/emqx_alarm_SUITE.erl b/apps/emqx/test/emqx_alarm_SUITE.erl similarity index 100% rename from test/emqx_alarm_SUITE.erl rename to apps/emqx/test/emqx_alarm_SUITE.erl diff --git a/test/emqx_banned_SUITE.erl b/apps/emqx/test/emqx_banned_SUITE.erl similarity index 100% rename from test/emqx_banned_SUITE.erl rename to apps/emqx/test/emqx_banned_SUITE.erl diff --git a/test/emqx_batch_SUITE.erl b/apps/emqx/test/emqx_batch_SUITE.erl similarity index 100% rename from test/emqx_batch_SUITE.erl rename to apps/emqx/test/emqx_batch_SUITE.erl diff --git a/test/emqx_boot_SUITE.erl b/apps/emqx/test/emqx_boot_SUITE.erl similarity index 100% rename from test/emqx_boot_SUITE.erl rename to apps/emqx/test/emqx_boot_SUITE.erl diff --git a/test/emqx_broker_SUITE.erl b/apps/emqx/test/emqx_broker_SUITE.erl similarity index 100% rename from test/emqx_broker_SUITE.erl rename to apps/emqx/test/emqx_broker_SUITE.erl diff --git a/test/emqx_broker_helper_SUITE.erl b/apps/emqx/test/emqx_broker_helper_SUITE.erl similarity index 100% rename from test/emqx_broker_helper_SUITE.erl rename to apps/emqx/test/emqx_broker_helper_SUITE.erl diff --git a/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl similarity index 100% rename from test/emqx_channel_SUITE.erl rename to apps/emqx/test/emqx_channel_SUITE.erl diff --git a/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl similarity index 100% rename from test/emqx_client_SUITE.erl rename to apps/emqx/test/emqx_client_SUITE.erl diff --git a/test/emqx_cm_SUITE.erl b/apps/emqx/test/emqx_cm_SUITE.erl similarity index 79% rename from test/emqx_cm_SUITE.erl rename to apps/emqx/test/emqx_cm_SUITE.erl index 0cfba4737..3f6950b3b 100644 --- a/test/emqx_cm_SUITE.erl +++ b/apps/emqx/test/emqx_cm_SUITE.erl @@ -109,9 +109,13 @@ t_open_session(_) -> emqx_cm:unregister_channel(<<"clientid">>), ok = meck:unload(emqx_connection). +rand_client_id() -> + list_to_binary("client-id-" ++ integer_to_list(erlang:system_time())). + t_open_session_race_condition(_) -> + ClientId = rand_client_id(), ClientInfo = #{zone => external, - clientid => <<"clientid">>, + clientid => ClientId, username => <<"username">>, peerhost => {127,0,0,1}}, ConnInfo = #{socktype => tcp, @@ -136,11 +140,12 @@ t_open_session_race_condition(_) -> exit(Reason) end end, + N = 1000, [spawn( fun() -> spawn(OpenASession), spawn(OpenASession) - end) || _ <- lists:seq(1, 1000)], + end) || _ <- lists:seq(1, N)], WaitingRecv = fun _Wr(N1, N2, 0) -> {N1, N2}; @@ -151,46 +156,54 @@ t_open_session_race_condition(_) -> end end, - ct:pal("Race condition status: ~p~n", [WaitingRecv(0, 0, 2000)]), + {Succeeded, Failed} = WaitingRecv(0, 0, 2 * N), + ct:pal("Race condition status: succeeded=~p failed=~p~n", [Succeeded, Failed]), - ?assertEqual(1, ets:info(emqx_channel, size)), - ?assertEqual(1, ets:info(emqx_channel_conn, size)), - ?assertEqual(1, ets:info(emqx_channel_registry, size)), + ?assertMatch([_], ets:lookup(emqx_channel, ClientId)), + [Pid] = emqx_cm:lookup_channels(ClientId), + ?assertMatch([_], ets:lookup(emqx_channel_conn, {ClientId, Pid})), + ?assertMatch([_], ets:lookup(emqx_channel_registry, ClientId)), - [Pid] = emqx_cm:lookup_channels(<<"clientid">>), - exit(Pid, kill), timer:sleep(100), - ?assertEqual([], emqx_cm:lookup_channels(<<"clientid">>)). + exit(Pid, kill), + timer:sleep(100), %% TODO deterministic + ?assertEqual([], emqx_cm:lookup_channels(ClientId)). t_discard_session(_) -> + ClientId = rand_client_id(), #{conninfo := ConnInfo} = ?ChanInfo, - ok = emqx_cm:register_channel(<<"clientid">>, self(), ConnInfo), + ok = emqx_cm:register_channel(ClientId, self(), ConnInfo), ok = meck:new(emqx_connection, [passthrough, no_history]), ok = meck:expect(emqx_connection, call, fun(_, _) -> ok end), ok = meck:expect(emqx_connection, call, fun(_, _, _) -> ok end), - ok = emqx_cm:discard_session(<<"clientid">>), - ok = emqx_cm:register_channel(<<"clientid">>, self(), ConnInfo), - ok = emqx_cm:discard_session(<<"clientid">>), - ok = emqx_cm:unregister_channel(<<"clientid">>), - ok = emqx_cm:register_channel(<<"clientid">>, self(), ConnInfo), - ok = emqx_cm:discard_session(<<"clientid">>), + ok = emqx_cm:discard_session(ClientId), + ok = emqx_cm:register_channel(ClientId, self(), ConnInfo), + ok = emqx_cm:discard_session(ClientId), + ok = emqx_cm:unregister_channel(ClientId), + ok = emqx_cm:register_channel(ClientId, self(), ConnInfo), + ok = emqx_cm:discard_session(ClientId), ok = meck:expect(emqx_connection, call, fun(_, _) -> error(testing) end), ok = meck:expect(emqx_connection, call, fun(_, _, _) -> error(testing) end), - ok = emqx_cm:discard_session(<<"clientid">>), - ok = emqx_cm:unregister_channel(<<"clientid">>), + ok = emqx_cm:discard_session(ClientId), + ok = emqx_cm:unregister_channel(ClientId), ok = meck:unload(emqx_connection). t_discard_session_race(_) -> - ok = snabbkaffe:start_trace(), - #{conninfo := ConnInfo0} = ?ChanInfo, - ConnInfo = ConnInfo0#{conn_mod := emqx_ws_connection}, - {Pid, Ref} = spawn_monitor(fun() -> receive stop -> exit(normal) end end), - ok = emqx_cm:register_channel(<<"clientid">>, Pid, ConnInfo), - Pid ! stop, - receive {'DOWN', Ref, process, Pid, normal} -> ok end, - ok = emqx_cm:discard_session(<<"clientid">>), - {ok, _} = ?block_until(#{?snk_kind := "session_already_gone", pid := Pid}, 1000), - snabbkaffe:stop(). + ClientId = rand_client_id(), + ?check_trace( + begin + #{conninfo := ConnInfo0} = ?ChanInfo, + ConnInfo = ConnInfo0#{conn_mod := emqx_ws_connection}, + {Pid, Ref} = spawn_monitor(fun() -> receive stop -> exit(normal) end end), + ok = emqx_cm:register_channel(ClientId, Pid, ConnInfo), + Pid ! stop, + receive {'DOWN', Ref, process, Pid, normal} -> ok end, + ok = emqx_cm:discard_session(ClientId), + {ok, _} = ?block_until(#{?snk_kind := "session_already_gone", pid := Pid}, 1000) + end, + fun(_, _) -> + true + end). t_takeover_session(_) -> #{conninfo := ConnInfo} = ?ChanInfo, @@ -231,10 +244,10 @@ t_all_channels(_) -> ?assertEqual(true, is_list(emqx_cm:all_channels())). t_lock_clientid(_) -> - {true, _Nodes} = emqx_cm_locker:lock(<<"clientid">>), - {true, _Nodes} = emqx_cm_locker:lock(<<"clientid">>), - {true, _Nodes} = emqx_cm_locker:unlock(<<"clientid">>), - {true, _Nodes} = emqx_cm_locker:unlock(<<"clientid">>). + {true, Nodes} = emqx_cm_locker:lock(<<"clientid">>), + ?assertEqual({true, Nodes}, emqx_cm_locker:lock(<<"clientid">>)), + ?assertEqual({true, Nodes}, emqx_cm_locker:unlock(<<"clientid">>)), + ?assertEqual({true, Nodes}, emqx_cm_locker:unlock(<<"clientid">>)). t_message(_) -> ?CM ! testing, diff --git a/test/emqx_cm_locker_SUITE.erl b/apps/emqx/test/emqx_cm_locker_SUITE.erl similarity index 100% rename from test/emqx_cm_locker_SUITE.erl rename to apps/emqx/test/emqx_cm_locker_SUITE.erl diff --git a/test/emqx_cm_registry_SUITE.erl b/apps/emqx/test/emqx_cm_registry_SUITE.erl similarity index 100% rename from test/emqx_cm_registry_SUITE.erl rename to apps/emqx/test/emqx_cm_registry_SUITE.erl diff --git a/test/emqx_connection_SUITE.erl b/apps/emqx/test/emqx_connection_SUITE.erl similarity index 100% rename from test/emqx_connection_SUITE.erl rename to apps/emqx/test/emqx_connection_SUITE.erl diff --git a/test/emqx_ctl_SUITE.erl b/apps/emqx/test/emqx_ctl_SUITE.erl similarity index 100% rename from test/emqx_ctl_SUITE.erl rename to apps/emqx/test/emqx_ctl_SUITE.erl diff --git a/test/emqx_flapping_SUITE.erl b/apps/emqx/test/emqx_flapping_SUITE.erl similarity index 100% rename from test/emqx_flapping_SUITE.erl rename to apps/emqx/test/emqx_flapping_SUITE.erl diff --git a/test/emqx_frame_SUITE.erl b/apps/emqx/test/emqx_frame_SUITE.erl similarity index 96% rename from test/emqx_frame_SUITE.erl rename to apps/emqx/test/emqx_frame_SUITE.erl index 09aa97c3e..09206cee1 100644 --- a/test/emqx_frame_SUITE.erl +++ b/apps/emqx/test/emqx_frame_SUITE.erl @@ -58,7 +58,8 @@ groups() -> t_serialize_parse_connack_v5 ]}, {publish, [parallel], - [t_serialize_parse_qos0_publish, + [t_parse_sticky_frames, + t_serialize_parse_qos0_publish, t_serialize_parse_qos1_publish, t_serialize_parse_qos2_publish, t_serialize_parse_publish_v5 @@ -286,6 +287,24 @@ t_serialize_parse_connack_v5(_) -> Packet = ?CONNACK_PACKET(?RC_SUCCESS, 0, Props), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). +t_parse_sticky_frames(_) -> + Payload = lists:duplicate(10, 0), + P = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + dup = false, + qos = ?QOS_0, + retain = false}, + variable = #mqtt_packet_publish{topic_name = <<"a/b">>, + packet_id = undefined}, + payload = iolist_to_binary(Payload) + }, + Bin = serialize_to_binary(P), + Size = size(Bin), + <> = Bin, + {more, PState1} = emqx_frame:parse(H), %% needs 2 more bytes + %% feed 3 bytes as if the next 1 byte belongs to the next packet. + {ok, _, <<42>>, PState2} = emqx_frame:parse(iolist_to_binary([TailTwoBytes, 42]), PState1), + ?assertMatch({none, _}, PState2). + t_serialize_parse_qos0_publish(_) -> Bin = <<48,14,0,7,120,120,120,47,121,121,121,104,101,108,108,111>>, Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, diff --git a/test/emqx_gc_SUITE.erl b/apps/emqx/test/emqx_gc_SUITE.erl similarity index 100% rename from test/emqx_gc_SUITE.erl rename to apps/emqx/test/emqx_gc_SUITE.erl diff --git a/test/emqx_global_gc_SUITE.erl b/apps/emqx/test/emqx_global_gc_SUITE.erl similarity index 100% rename from test/emqx_global_gc_SUITE.erl rename to apps/emqx/test/emqx_global_gc_SUITE.erl diff --git a/test/emqx_guid_SUITE.erl b/apps/emqx/test/emqx_guid_SUITE.erl similarity index 100% rename from test/emqx_guid_SUITE.erl rename to apps/emqx/test/emqx_guid_SUITE.erl diff --git a/test/emqx_hooks_SUITE.erl b/apps/emqx/test/emqx_hooks_SUITE.erl similarity index 100% rename from test/emqx_hooks_SUITE.erl rename to apps/emqx/test/emqx_hooks_SUITE.erl diff --git a/test/emqx_inflight_SUITE.erl b/apps/emqx/test/emqx_inflight_SUITE.erl similarity index 100% rename from test/emqx_inflight_SUITE.erl rename to apps/emqx/test/emqx_inflight_SUITE.erl diff --git a/test/emqx_json_SUITE.erl b/apps/emqx/test/emqx_json_SUITE.erl similarity index 100% rename from test/emqx_json_SUITE.erl rename to apps/emqx/test/emqx_json_SUITE.erl diff --git a/test/emqx_keepalive_SUITE.erl b/apps/emqx/test/emqx_keepalive_SUITE.erl similarity index 100% rename from test/emqx_keepalive_SUITE.erl rename to apps/emqx/test/emqx_keepalive_SUITE.erl diff --git a/test/emqx_limiter_SUITE.erl b/apps/emqx/test/emqx_limiter_SUITE.erl similarity index 100% rename from test/emqx_limiter_SUITE.erl rename to apps/emqx/test/emqx_limiter_SUITE.erl diff --git a/test/emqx_listeners_SUITE.erl b/apps/emqx/test/emqx_listeners_SUITE.erl similarity index 92% rename from test/emqx_listeners_SUITE.erl rename to apps/emqx/test/emqx_listeners_SUITE.erl index 673cc80e9..53f388dfa 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/apps/emqx/test/emqx_listeners_SUITE.erl @@ -48,7 +48,7 @@ t_restart_listeners(_) -> ok = emqx_listeners:stop(). render_config_file() -> - Path = local_path(["..", "..", "..", "..", "etc", "emqx.conf"]), + Path = local_path(["etc", "emqx.conf"]), {ok, Temp} = file:read_file(Path), Vars0 = mustache_vars(), Vars = [{atom_to_list(N), iolist_to_binary(V)} || {N, V} <- Vars0], @@ -65,10 +65,9 @@ mustache_vars() -> ]. generate_config() -> - Schema = cuttlefish_schema:files([local_path(["priv", "emqx.schema"])]), ConfFile = render_config_file(), - {ok, Conf} = hocon:load(ConfFile, #{format => proplists}), - cuttlefish_generator:map(Schema, Conf). + {ok, Conf} = hocon:load(ConfFile, #{format => richmap}), + hocon_schema:generate(emqx_schema, Conf). set_app_env({App, Lists}) -> lists:foreach(fun({acl_file, _Var}) -> diff --git a/test/emqx_logger_SUITE.erl b/apps/emqx/test/emqx_logger_SUITE.erl similarity index 100% rename from test/emqx_logger_SUITE.erl rename to apps/emqx/test/emqx_logger_SUITE.erl diff --git a/test/emqx_message_SUITE.erl b/apps/emqx/test/emqx_message_SUITE.erl similarity index 100% rename from test/emqx_message_SUITE.erl rename to apps/emqx/test/emqx_message_SUITE.erl diff --git a/test/emqx_metrics_SUITE.erl b/apps/emqx/test/emqx_metrics_SUITE.erl similarity index 100% rename from test/emqx_metrics_SUITE.erl rename to apps/emqx/test/emqx_metrics_SUITE.erl diff --git a/test/emqx_misc_SUITE.erl b/apps/emqx/test/emqx_misc_SUITE.erl similarity index 100% rename from test/emqx_misc_SUITE.erl rename to apps/emqx/test/emqx_misc_SUITE.erl diff --git a/test/emqx_mountpoint_SUITE.erl b/apps/emqx/test/emqx_mountpoint_SUITE.erl similarity index 100% rename from test/emqx_mountpoint_SUITE.erl rename to apps/emqx/test/emqx_mountpoint_SUITE.erl diff --git a/test/emqx_mqtt_SUITE.erl b/apps/emqx/test/emqx_mqtt_SUITE.erl similarity index 100% rename from test/emqx_mqtt_SUITE.erl rename to apps/emqx/test/emqx_mqtt_SUITE.erl diff --git a/test/emqx_mqtt_caps_SUITE.erl b/apps/emqx/test/emqx_mqtt_caps_SUITE.erl similarity index 100% rename from test/emqx_mqtt_caps_SUITE.erl rename to apps/emqx/test/emqx_mqtt_caps_SUITE.erl diff --git a/test/emqx_mqtt_props_SUITE.erl b/apps/emqx/test/emqx_mqtt_props_SUITE.erl similarity index 100% rename from test/emqx_mqtt_props_SUITE.erl rename to apps/emqx/test/emqx_mqtt_props_SUITE.erl diff --git a/test/mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl similarity index 96% rename from test/mqtt_protocol_v5_SUITE.erl rename to apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index 0956a07fc..8ce35b50c 100644 --- a/test/mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(mqtt_protocol_v5_SUITE). +-module(emqx_mqtt_protocol_v5_SUITE). -compile(export_all). -compile(nowarn_export_all). @@ -22,6 +22,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -import(lists, [nth/2]). @@ -37,6 +38,7 @@ init_per_suite(Config) -> %% Meck emqtt ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]), %% Start Apps + emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), Config. @@ -44,6 +46,19 @@ end_per_suite(_Config) -> ok = meck:unload(emqtt), emqx_ct_helpers:stop_apps([]). +init_per_testcase(TestCase, Config) -> + case erlang:function_exported(?MODULE, TestCase, 2) of + true -> ?MODULE:TestCase(init, Config); + _ -> Config + end. + +end_per_testcase(TestCase, Config) -> + case erlang:function_exported(?MODULE, TestCase, 2) of + true -> ?MODULE:TestCase('end', Config); + false -> ok + end, + Config. + %%-------------------------------------------------------------------- %% Helpers %%-------------------------------------------------------------------- @@ -273,21 +288,33 @@ t_connect_limit_timeout(_) -> emqx_zone:set_env(external, publish_limit, undefined), meck:unload(proplists). -t_connect_emit_stats_timeout(_) -> - IdleTimeout = 2000, - emqx_zone:set_env(external, idle_timeout, IdleTimeout), +t_connect_emit_stats_timeout(init, Config) -> + NewIdleTimeout = 1000, + OldIdleTimeout = emqx_zone:get_env(external, idle_timeout), + emqx_zone:set_env(external, idle_timeout, NewIdleTimeout), + ok = snabbkaffe:start_trace(), + [{idle_timeout, NewIdleTimeout}, {old_idle_timeout, OldIdleTimeout} | Config]; +t_connect_emit_stats_timeout('end', Config) -> + snabbkaffe:stop(), + {_, OldIdleTimeout} = lists:keyfind(old_idle_timeout, 1, Config), + emqx_zone:set_env(external, idle_timeout, OldIdleTimeout), + ok. +t_connect_emit_stats_timeout(Config) -> + {_, IdleTimeout} = lists:keyfind(idle_timeout, 1, Config), {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60}]), {ok, _} = emqtt:connect(Client), [ClientPid] = emqx_cm:lookup_channels(client_info(clientid, Client)), - ?assert(is_reference(emqx_connection:info(stats_timer, sys:get_state(ClientPid)))), - timer:sleep(IdleTimeout), + ?block_until(#{?snk_kind := cancel_stats_timer}, IdleTimeout * 2, _BackInTime = 0), ?assertEqual(undefined, emqx_connection:info(stats_timer, sys:get_state(ClientPid))), ok = emqtt:disconnect(Client). %% [MQTT-3.1.2-22] t_connect_keepalive_timeout(_) -> + %% Prevent the emqtt client bringing us down on the disconnect. + process_flag(trap_exit, true), + Keepalive = 2, {ok, Client} = emqtt:start_link([{proto_ver, v5}, diff --git a/test/emqx_mqueue_SUITE.erl b/apps/emqx/test/emqx_mqueue_SUITE.erl similarity index 100% rename from test/emqx_mqueue_SUITE.erl rename to apps/emqx/test/emqx_mqueue_SUITE.erl diff --git a/test/emqx_os_mon_SUITE.erl b/apps/emqx/test/emqx_os_mon_SUITE.erl similarity index 100% rename from test/emqx_os_mon_SUITE.erl rename to apps/emqx/test/emqx_os_mon_SUITE.erl diff --git a/test/emqx_packet_SUITE.erl b/apps/emqx/test/emqx_packet_SUITE.erl similarity index 100% rename from test/emqx_packet_SUITE.erl rename to apps/emqx/test/emqx_packet_SUITE.erl diff --git a/test/emqx_passwd_SUITE.erl b/apps/emqx/test/emqx_passwd_SUITE.erl similarity index 100% rename from test/emqx_passwd_SUITE.erl rename to apps/emqx/test/emqx_passwd_SUITE.erl diff --git a/test/emqx_pd_SUITE.erl b/apps/emqx/test/emqx_pd_SUITE.erl similarity index 100% rename from test/emqx_pd_SUITE.erl rename to apps/emqx/test/emqx_pd_SUITE.erl diff --git a/test/emqx_plugins_SUITE.erl b/apps/emqx/test/emqx_plugins_SUITE.erl similarity index 100% rename from test/emqx_plugins_SUITE.erl rename to apps/emqx/test/emqx_plugins_SUITE.erl diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile similarity index 100% rename from test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile rename to apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf similarity index 100% rename from test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf rename to apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema similarity index 100% rename from test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema rename to apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config similarity index 100% rename from test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config rename to apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src similarity index 100% rename from test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src rename to apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl similarity index 100% rename from test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl rename to apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl diff --git a/test/emqx_pmon_SUITE.erl b/apps/emqx/test/emqx_pmon_SUITE.erl similarity index 100% rename from test/emqx_pmon_SUITE.erl rename to apps/emqx/test/emqx_pmon_SUITE.erl diff --git a/test/emqx_pool_SUITE.erl b/apps/emqx/test/emqx_pool_SUITE.erl similarity index 100% rename from test/emqx_pool_SUITE.erl rename to apps/emqx/test/emqx_pool_SUITE.erl diff --git a/test/emqx_pqueue_SUITE.erl b/apps/emqx/test/emqx_pqueue_SUITE.erl similarity index 100% rename from test/emqx_pqueue_SUITE.erl rename to apps/emqx/test/emqx_pqueue_SUITE.erl diff --git a/test/emqx_reason_codes_SUITE.erl b/apps/emqx/test/emqx_reason_codes_SUITE.erl similarity index 100% rename from test/emqx_reason_codes_SUITE.erl rename to apps/emqx/test/emqx_reason_codes_SUITE.erl diff --git a/test/emqx_request_handler.erl b/apps/emqx/test/emqx_request_handler.erl similarity index 100% rename from test/emqx_request_handler.erl rename to apps/emqx/test/emqx_request_handler.erl diff --git a/test/emqx_request_responser_SUITE.erl b/apps/emqx/test/emqx_request_responser_SUITE.erl similarity index 100% rename from test/emqx_request_responser_SUITE.erl rename to apps/emqx/test/emqx_request_responser_SUITE.erl diff --git a/test/emqx_request_sender.erl b/apps/emqx/test/emqx_request_sender.erl similarity index 100% rename from test/emqx_request_sender.erl rename to apps/emqx/test/emqx_request_sender.erl diff --git a/test/emqx_router_SUITE.erl b/apps/emqx/test/emqx_router_SUITE.erl similarity index 100% rename from test/emqx_router_SUITE.erl rename to apps/emqx/test/emqx_router_SUITE.erl diff --git a/test/emqx_router_helper_SUITE.erl b/apps/emqx/test/emqx_router_helper_SUITE.erl similarity index 100% rename from test/emqx_router_helper_SUITE.erl rename to apps/emqx/test/emqx_router_helper_SUITE.erl diff --git a/test/emqx_sequence_SUITE.erl b/apps/emqx/test/emqx_sequence_SUITE.erl similarity index 100% rename from test/emqx_sequence_SUITE.erl rename to apps/emqx/test/emqx_sequence_SUITE.erl diff --git a/test/emqx_session_SUITE.erl b/apps/emqx/test/emqx_session_SUITE.erl similarity index 100% rename from test/emqx_session_SUITE.erl rename to apps/emqx/test/emqx_session_SUITE.erl diff --git a/test/emqx_shared_sub_SUITE.erl b/apps/emqx/test/emqx_shared_sub_SUITE.erl similarity index 100% rename from test/emqx_shared_sub_SUITE.erl rename to apps/emqx/test/emqx_shared_sub_SUITE.erl diff --git a/test/emqx_stats_SUITE.erl b/apps/emqx/test/emqx_stats_SUITE.erl similarity index 100% rename from test/emqx_stats_SUITE.erl rename to apps/emqx/test/emqx_stats_SUITE.erl diff --git a/test/emqx_sup_SUITE.erl b/apps/emqx/test/emqx_sup_SUITE.erl similarity index 100% rename from test/emqx_sup_SUITE.erl rename to apps/emqx/test/emqx_sup_SUITE.erl diff --git a/test/emqx_sys_SUITE.erl b/apps/emqx/test/emqx_sys_SUITE.erl similarity index 100% rename from test/emqx_sys_SUITE.erl rename to apps/emqx/test/emqx_sys_SUITE.erl diff --git a/test/emqx_sys_mon_SUITE.erl b/apps/emqx/test/emqx_sys_mon_SUITE.erl similarity index 100% rename from test/emqx_sys_mon_SUITE.erl rename to apps/emqx/test/emqx_sys_mon_SUITE.erl diff --git a/test/emqx_tables_SUITE.erl b/apps/emqx/test/emqx_tables_SUITE.erl similarity index 100% rename from test/emqx_tables_SUITE.erl rename to apps/emqx/test/emqx_tables_SUITE.erl diff --git a/test/emqx_takeover_SUITE.erl b/apps/emqx/test/emqx_takeover_SUITE.erl similarity index 100% rename from test/emqx_takeover_SUITE.erl rename to apps/emqx/test/emqx_takeover_SUITE.erl diff --git a/test/emqx_tls_lib_tests.erl b/apps/emqx/test/emqx_tls_lib_tests.erl similarity index 100% rename from test/emqx_tls_lib_tests.erl rename to apps/emqx/test/emqx_tls_lib_tests.erl diff --git a/test/emqx_topic_SUITE.erl b/apps/emqx/test/emqx_topic_SUITE.erl similarity index 100% rename from test/emqx_topic_SUITE.erl rename to apps/emqx/test/emqx_topic_SUITE.erl diff --git a/test/emqx_tracer_SUITE.erl b/apps/emqx/test/emqx_tracer_SUITE.erl similarity index 100% rename from test/emqx_tracer_SUITE.erl rename to apps/emqx/test/emqx_tracer_SUITE.erl diff --git a/test/emqx_trie_SUITE.erl b/apps/emqx/test/emqx_trie_SUITE.erl similarity index 100% rename from test/emqx_trie_SUITE.erl rename to apps/emqx/test/emqx_trie_SUITE.erl diff --git a/test/emqx_vm_SUITE.erl b/apps/emqx/test/emqx_vm_SUITE.erl similarity index 100% rename from test/emqx_vm_SUITE.erl rename to apps/emqx/test/emqx_vm_SUITE.erl diff --git a/test/emqx_vm_mon_SUITE.erl b/apps/emqx/test/emqx_vm_mon_SUITE.erl similarity index 100% rename from test/emqx_vm_mon_SUITE.erl rename to apps/emqx/test/emqx_vm_mon_SUITE.erl diff --git a/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl similarity index 100% rename from test/emqx_ws_connection_SUITE.erl rename to apps/emqx/test/emqx_ws_connection_SUITE.erl diff --git a/test/emqx_zone_SUITE.erl b/apps/emqx/test/emqx_zone_SUITE.erl similarity index 100% rename from test/emqx_zone_SUITE.erl rename to apps/emqx/test/emqx_zone_SUITE.erl diff --git a/test/props/prop_emqx_base62.erl b/apps/emqx/test/props/prop_emqx_base62.erl similarity index 100% rename from test/props/prop_emqx_base62.erl rename to apps/emqx/test/props/prop_emqx_base62.erl diff --git a/test/props/prop_emqx_frame.erl b/apps/emqx/test/props/prop_emqx_frame.erl similarity index 100% rename from test/props/prop_emqx_frame.erl rename to apps/emqx/test/props/prop_emqx_frame.erl diff --git a/test/props/prop_emqx_json.erl b/apps/emqx/test/props/prop_emqx_json.erl similarity index 100% rename from test/props/prop_emqx_json.erl rename to apps/emqx/test/props/prop_emqx_json.erl diff --git a/test/props/prop_emqx_psk.erl b/apps/emqx/test/props/prop_emqx_psk.erl similarity index 100% rename from test/props/prop_emqx_psk.erl rename to apps/emqx/test/props/prop_emqx_psk.erl diff --git a/test/props/prop_emqx_reason_codes.erl b/apps/emqx/test/props/prop_emqx_reason_codes.erl similarity index 100% rename from test/props/prop_emqx_reason_codes.erl rename to apps/emqx/test/props/prop_emqx_reason_codes.erl diff --git a/test/props/prop_emqx_rpc.erl b/apps/emqx/test/props/prop_emqx_rpc.erl similarity index 100% rename from test/props/prop_emqx_rpc.erl rename to apps/emqx/test/props/prop_emqx_rpc.erl diff --git a/test/props/prop_emqx_sys.erl b/apps/emqx/test/props/prop_emqx_sys.erl similarity index 100% rename from test/props/prop_emqx_sys.erl rename to apps/emqx/test/props/prop_emqx_sys.erl diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src b/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src index bf0ba78aa..7d784e3b2 100644 --- a/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src +++ b/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src @@ -1,6 +1,6 @@ {application, emqx_auth_jwt, [{description, "EMQ X Authentication with JWT"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_auth_jwt_sup]}, {applications, [kernel,stdlib,jose]}, diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt.appup.src b/apps/emqx_auth_jwt/src/emqx_auth_jwt.appup.src new file mode 100644 index 000000000..b9831bb6f --- /dev/null +++ b/apps/emqx_auth_jwt/src/emqx_auth_jwt.appup.src @@ -0,0 +1,15 @@ +%% -*-: erlang -*- +{VSN, + [ + {"4.3.0", [ + {load_module, emqx_auth_jwt_svr, brutal_purge, soft_purge, []} + ]}, + {<<".*">>, []} + ], + [ + {"4.3.0", [ + {load_module, emqx_auth_jwt_svr, brutal_purge, soft_purge, []} + ]}, + {<<".*">>, []} + ] +}. diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl b/apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl index 2d00903e3..b9d19bf57 100644 --- a/apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl +++ b/apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl @@ -140,7 +140,7 @@ handle_verify(JwsCompacted, State = #state{static = Static, remote = Remote}) -> try Jwks = case emqx_json:decode(jose_jws:peek_protected(JwsCompacted), [return_maps]) of - #{<<"kid">> := Kid} -> + #{<<"kid">> := Kid} when Remote /= undefined -> [J || J <- Remote, maps:get(<<"kid">>, J#jose_jwk.fields, undefined) =:= Kid]; _ -> Static end, @@ -150,7 +150,9 @@ handle_verify(JwsCompacted, {reply, do_verify(JwsCompacted, Jwks), State} end catch - _:_ -> + Class : Reason : Stk -> + ?LOG(error, "Handle JWK crashed: ~p, ~p, stacktrace: ~p~n", + [Class, Reason, Stk]), {reply, {error, invalid_signature}, State} end. @@ -186,8 +188,8 @@ do_verify(JwsCompacted, [Jwk|More]) -> {true, Payload, _Jws} -> Claims = emqx_json:decode(Payload, [return_maps]), case check_claims(Claims) of - false -> - {error, invalid_signature}; + {false, <<"exp">>} -> + {error, {invalid_signature, expired}}; NClaims -> {ok, NClaims} end; @@ -217,6 +219,6 @@ do_check_claim([{K, F}|More], Claims) -> {V, NClaims} -> case F(V) of true -> do_check_claim(More, NClaims); - _ -> false + _ -> {false, K} end end. diff --git a/apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl b/apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl index d0a4a34a0..d4f562b6f 100644 --- a/apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl +++ b/apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl @@ -33,6 +33,7 @@ groups() -> , t_check_claims , t_check_claims_clientid , t_check_claims_username + , t_check_claims_kid_in_header ]} ]. @@ -61,6 +62,12 @@ set_special_configs(emqx_auth_jwt) -> set_special_configs(_) -> ok. +sign(Payload, Header, Key) when is_map(Header) -> + Jwk = jose_jwk:from_oct(Key), + Jwt = emqx_json:encode(Payload), + {_, Token} = jose_jws:compact(jose_jwt:sign(Jwk, Header, Jwt)), + Token; + sign(Payload, Alg, Key) -> Jwk = jose_jwk:from_oct(Key), Jwt = emqx_json:encode(Payload), @@ -145,3 +152,15 @@ t_check_claims_username(_) -> Result3 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}), ct:pal("Auth result for the invalid jwt: ~p~n", [Result3]), ?assertEqual({error, invalid_signature}, Result3). + +t_check_claims_kid_in_header(_) -> + application:set_env(emqx_auth_jwt, verify_claims, []), + Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external}, + Jwt = sign([{clientid, <<"client23">>}, + {username, <<"plain">>}, + {exp, os:system_time(seconds) + 3}], + #{<<"alg">> => <<"HS256">>, + <<"kid">> => <<"a_kid_str">>}, <<"emqxsecret">>), + Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}), + ct:pal("Auth result: ~p~n", [Result0]), + ?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0). diff --git a/apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src b/apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src index 8635c4834..1b76c32c8 100644 --- a/apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src +++ b/apps/emqx_auth_ldap/src/emqx_auth_ldap.app.src @@ -1,6 +1,6 @@ {application, emqx_auth_ldap, [{description, "EMQ X Authentication/ACL with LDAP"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_auth_ldap_sup]}, {applications, [kernel,stdlib,eldap2,ecpool]}, diff --git a/apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src b/apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src index cc4e72ef3..ab0b4ff56 100644 --- a/apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src +++ b/apps/emqx_auth_mongo/src/emqx_auth_mongo.app.src @@ -1,6 +1,6 @@ {application, emqx_auth_mongo, [{description, "EMQ X Authentication/ACL with MongoDB"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_auth_mongo_sup]}, {applications, [kernel,stdlib,mongodb,ecpool]}, diff --git a/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src b/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src index 221061f03..8a0d116cc 100644 --- a/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src +++ b/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src @@ -1,6 +1,6 @@ {application, emqx_auth_mysql, [{description, "EMQ X Authentication/ACL with MySQL"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_auth_mysql_sup]}, {applications, [kernel,stdlib,mysql,ecpool]}, diff --git a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.app.src b/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.app.src index f70612262..e97487e21 100644 --- a/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.app.src +++ b/apps/emqx_auth_pgsql/src/emqx_auth_pgsql.app.src @@ -1,6 +1,6 @@ {application, emqx_auth_pgsql, [{description, "EMQ X Authentication/ACL with PostgreSQL"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_auth_pgsql_sup]}, {applications, [kernel,stdlib,epgsql,ecpool]}, diff --git a/apps/emqx_auth_redis/src/emqx_auth_redis.app.src b/apps/emqx_auth_redis/src/emqx_auth_redis.app.src index 6a38af2ce..419131566 100644 --- a/apps/emqx_auth_redis/src/emqx_auth_redis.app.src +++ b/apps/emqx_auth_redis/src/emqx_auth_redis.app.src @@ -1,6 +1,6 @@ {application, emqx_auth_redis, [{description, "EMQ X Authentication/ACL with Redis"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_auth_redis_sup]}, {applications, [kernel,stdlib,eredis,eredis_cluster,ecpool]}, diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src index 945abcdfc..83ce7a759 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src @@ -1,6 +1,6 @@ {application, emqx_bridge_mqtt, [{description, "EMQ X Bridge to MQTT Broker"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.3.1"}, % strict semver, bump manually! {modules, []}, {registered, []}, {applications, [kernel,stdlib,replayq,emqtt]}, diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src index 0c7b8ebf3..03e6119ae 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src @@ -2,9 +2,15 @@ {VSN, [ + {"4.3.0", [ + {load_module, emqx_bridge_worker, brutal_purge, soft_purge, []} + ]}, {<<".*">>, []} ], [ + {"4.3.0", [ + {load_module, emqx_bridge_worker, brutal_purge, soft_purge, []} + ]}, {<<".*">>, []} ] }. diff --git a/apps/emqx_coap/test/emqx_coap_SUITE.erl b/apps/emqx_coap/test/emqx_coap_SUITE.erl index 444bcc064..440b80ebd 100644 --- a/apps/emqx_coap/test/emqx_coap_SUITE.erl +++ b/apps/emqx_coap/test/emqx_coap_SUITE.erl @@ -120,7 +120,7 @@ t_observe_acl_deny(_Config) -> ok = meck:unload(emqx_access_control). t_observe_wildcard(_Config) -> - Topic = <<"+/b">>, TopicStr = http_uri:encode(binary_to_list(Topic)), + Topic = <<"+/b">>, TopicStr = emqx_http_lib:uri_encode(binary_to_list(Topic)), Payload = <<"123">>, Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri), @@ -143,7 +143,7 @@ t_observe_wildcard(_Config) -> [] = emqx:subscribers(Topic). t_observe_pub(_Config) -> - Topic = <<"+/b">>, TopicStr = http_uri:encode(binary_to_list(Topic)), + Topic = <<"+/b">>, TopicStr = emqx_http_lib:uri_encode(binary_to_list(Topic)), Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri), ?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]), @@ -152,7 +152,7 @@ t_observe_pub(_Config) -> ?assert(is_pid(SubPid)), Topic2 = <<"a/b">>, Payload2 = <<"UFO">>, - TopicStr2 = http_uri:encode(binary_to_list(Topic2)), + TopicStr2 = emqx_http_lib:uri_encode(binary_to_list(Topic2)), URI2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret", Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = Payload2}), @@ -164,7 +164,7 @@ t_observe_pub(_Config) -> ?assertEqual(Payload2, PayloadRecv2), Topic3 = <<"j/b">>, Payload3 = <<"ET629">>, - TopicStr3 = http_uri:encode(binary_to_list(Topic3)), + TopicStr3 = emqx_http_lib:uri_encode(binary_to_list(Topic3)), URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=mike&p=guess", Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}), {ok,changed, _} = Reply3, @@ -186,7 +186,7 @@ t_one_clientid_sub_2_topics(_Config) -> [SubPid] = emqx:subscribers(Topic1), ?assert(is_pid(SubPid)), - Topic2 = <<"x/y">>, TopicStr2 = http_uri:encode(binary_to_list(Topic2)), + Topic2 = <<"x/y">>, TopicStr2 = emqx_http_lib:uri_encode(binary_to_list(Topic2)), Payload2 = <<"456">>, Uri2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret", {ok, Pid2, N2, Code2, Content2} = er_coap_observer:observe(Uri2), @@ -217,7 +217,7 @@ t_invalid_parameter(_Config) -> %% "cid=client2" is invaid %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Topic3 = <<"a/b">>, Payload3 = <<"ET629">>, - TopicStr3 = http_uri:encode(binary_to_list(Topic3)), + TopicStr3 = emqx_http_lib:uri_encode(binary_to_list(Topic3)), URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?cid=client2&u=tom&p=simple", Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}), ?assertMatch({error,bad_request}, Reply3), diff --git a/apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl b/apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl index 1aaf6cb69..c018b9165 100644 --- a/apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl +++ b/apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl @@ -173,7 +173,7 @@ t_case01_publish_post(_Config) -> ?assertEqual(<<"42">>, CT2), %% post to publish message to topic maintopic/topic1 - FullTopicStr = http_uri:encode(binary_to_list(FullTopic)), + FullTopicStr = emqx_http_lib:uri_encode(binary_to_list(FullTopic)), URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret", PubPayload = <<"PUBLISH">>, @@ -286,7 +286,7 @@ t_case01_publish_put(_Config) -> ?assertEqual(<<"42">>, CT2), %% put to publish message to topic maintopic/topic1 - FullTopicStr = http_uri:encode(binary_to_list(FullTopic)), + FullTopicStr = emqx_http_lib:uri_encode(binary_to_list(FullTopic)), URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret", PubPayload = <<"PUBLISH">>, @@ -430,7 +430,7 @@ t_case01_subscribe(_Config) -> t_case02_subscribe(_Config) -> Topic = <<"a/b">>, TopicStr = binary_to_list(Topic), - PercentEncodedTopic = http_uri:encode(TopicStr), + PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), Payload = <<"payload">>, %% post to publish a new topic "a/b", and the topic is created @@ -477,7 +477,7 @@ t_case03_subscribe(_Config) -> %% Subscribe to the unexisted topic "a/b", got not_found Topic = <<"a/b">>, TopicStr = binary_to_list(Topic), - PercentEncodedTopic = http_uri:encode(TopicStr), + PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", {error, not_found} = er_coap_observer:observe(Uri), @@ -487,7 +487,7 @@ t_case04_subscribe(_Config) -> %% Subscribe to the wildcad topic "+/b", got bad_request Topic = <<"+/b">>, TopicStr = binary_to_list(Topic), - PercentEncodedTopic = http_uri:encode(TopicStr), + PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", {error, bad_request} = er_coap_observer:observe(Uri), @@ -582,7 +582,7 @@ t_case04_read(_Config) -> t_case05_read(_Config) -> Topic = <<"a/b">>, TopicStr = binary_to_list(Topic), - PercentEncodedTopic = http_uri:encode(TopicStr), + PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), Payload = <<"payload">>, %% post to publish a new topic "a/b", and the topic is created @@ -609,7 +609,7 @@ t_case05_read(_Config) -> t_case01_delete(_Config) -> TopicInPayload = <<"a/b">>, TopicStr = binary_to_list(TopicInPayload), - PercentEncodedTopic = http_uri:encode(TopicStr), + PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), Payload = list_to_binary("<"++PercentEncodedTopic++">;ct=42"), URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret", @@ -621,7 +621,7 @@ t_case01_delete(_Config) -> %% Client post to CREATE topic "a/b/c" TopicInPayload1 = <<"a/b/c">>, - PercentEncodedTopic1 = http_uri:encode(binary_to_list(TopicInPayload1)), + PercentEncodedTopic1 = emqx_http_lib:uri_encode(binary_to_list(TopicInPayload1)), Payload1 = list_to_binary("<"++PercentEncodedTopic1++">;ct=42"), Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload1}), ?LOGT("Reply =~p", [Reply1]), @@ -643,7 +643,7 @@ t_case01_delete(_Config) -> t_case02_delete(_Config) -> TopicInPayload = <<"a/b">>, TopicStr = binary_to_list(TopicInPayload), - PercentEncodedTopic = http_uri:encode(TopicStr), + PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), %% DELETE the unexisted topic "a/b" Uri1 = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", diff --git a/apps/emqx_exhook/priv/protos/exhook.proto b/apps/emqx_exhook/priv/protos/exhook.proto index 612b5151f..72ba26581 100644 --- a/apps/emqx_exhook/priv/protos/exhook.proto +++ b/apps/emqx_exhook/priv/protos/exhook.proto @@ -237,14 +237,14 @@ message EmptySuccess { } message ValuedResponse { // The responsed value type - // - ignore: Ignore the responsed value // - contiune: Use the responsed value and execute the next hook + // - ignore: Ignore the responsed value // - stop_and_return: Use the responsed value and stop the chain executing enum ResponsedType { - IGNORE = 0; + CONTINUE = 0; - CONTINUE = 1; + IGNORE = 1; STOP_AND_RETURN = 2; } diff --git a/apps/emqx_exhook/src/emqx_exhook.app.src b/apps/emqx_exhook/src/emqx_exhook.app.src index 555243107..452d2a742 100644 --- a/apps/emqx_exhook/src/emqx_exhook.app.src +++ b/apps/emqx_exhook/src/emqx_exhook.app.src @@ -1,6 +1,6 @@ {application, emqx_exhook, [{description, "EMQ X Extension for Hook"}, - {vsn, "4.3.0"}, + {vsn, "4.3.1"}, {modules, []}, {registered, []}, {mod, {emqx_exhook_app, []}}, diff --git a/apps/emqx_exhook/src/emqx_exhook.appup.src b/apps/emqx_exhook/src/emqx_exhook.appup.src new file mode 100644 index 000000000..2811c1554 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook.appup.src @@ -0,0 +1,15 @@ +%% -*-: erlang -*- +{VSN, + [ + {"4.3.0", [ + {load_module, emqx_exhook_pb, brutal_purge, soft_purge, []} + ]}, + {<<".*">>, []} + ], + [ + {"4.3.0", [ + {load_module, emqx_exhook_pb, brutal_purge, soft_purge, []} + ]}, + {<<".*">>, []} + ] +}. diff --git a/apps/emqx_exproto/src/emqx_exproto.app.src b/apps/emqx_exproto/src/emqx_exproto.app.src index 52ce96a54..9841f5bcb 100644 --- a/apps/emqx_exproto/src/emqx_exproto.app.src +++ b/apps/emqx_exproto/src/emqx_exproto.app.src @@ -1,6 +1,6 @@ {application, emqx_exproto, [{description, "EMQ X Extension for Protocol"}, - {vsn, "4.3.0"}, %% strict semver + {vsn, "4.4.0"}, %% strict semver {modules, []}, {registered, []}, {mod, {emqx_exproto_app, []}}, diff --git a/apps/emqx_lwm2m/test/emqx_lwm2m_SUITE.erl b/apps/emqx_lwm2m/test/emqx_lwm2m_SUITE.erl index 2cfb745bf..257502f2f 100644 --- a/apps/emqx_lwm2m/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_lwm2m/test/emqx_lwm2m_SUITE.erl @@ -1886,8 +1886,11 @@ std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic) -> timer:sleep(100). resolve_uri(Uri) -> - {ok, {Scheme, _UserInfo, Host, PortNo, Path, Query}} = - http_uri:parse(Uri, [{scheme_defaults, [{coap, ?DEFAULT_COAP_PORT}, {coaps, ?DEFAULT_COAPS_PORT}]}]), + {ok, #{scheme := Scheme, + host := Host, + port := PortNo, + path := Path} = URIMap} = emqx_http_lib:uri_parse(Uri), + Query = maps:get(query, URIMap, ""), {ok, PeerIP} = inet:getaddr(Host, inet), {Scheme, {PeerIP, PortNo}, split_path(Path), split_query(Query)}. @@ -1896,7 +1899,7 @@ split_path([$/]) -> []; split_path([$/ | Path]) -> split_segments(Path, $/, []). split_query([]) -> []; -split_query([$? | Path]) -> split_segments(Path, $&, []). +split_query(Path) -> split_segments(Path, $&, []). split_segments(Path, Char, Acc) -> case string:rchr(Path, Char) of @@ -1908,7 +1911,7 @@ split_segments(Path, Char, Acc) -> end. make_segment(Seg) -> - list_to_binary(http_uri:decode(Seg)). + list_to_binary(emqx_http_lib:uri_decode(Seg)). get_coap_path(Options) -> diff --git a/apps/emqx_management/src/emqx_management.app.src b/apps/emqx_management/src/emqx_management.app.src index 3604d3505..fe68fef44 100644 --- a/apps/emqx_management/src/emqx_management.app.src +++ b/apps/emqx_management/src/emqx_management.app.src @@ -1,6 +1,6 @@ {application, emqx_management, [{description, "EMQ X Management API and CLI"}, - {vsn, "4.3.2"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_management_sup]}, {applications, [kernel,stdlib,minirest]}, diff --git a/apps/emqx_management/src/emqx_management.appup.src b/apps/emqx_management/src/emqx_management.appup.src index 3206ce31b..06945afad 100644 --- a/apps/emqx_management/src/emqx_management.appup.src +++ b/apps/emqx_management/src/emqx_management.appup.src @@ -1,14 +1,13 @@ -%% -*-: erlang -*- -{"4.3.2", - [ {<<"4.3.[0-1]">>, - [ {load_module, emqx_mgmt_data_backup, brutal_purge, soft_purge, []} - , {load_module, emqx_mgmt_cli, brutal_purge, soft_purge, []} - ]} +%% -*- mode: erlang -*- +{VSN, + [ {<<"4.3.[0-2]">>, + [ {restart_application, emqx_management} + ]}, + {<<".*">>, []} ], - [ - {<<"4.3.[0-1]">>, - [ {load_module, emqx_mgmt_data_backup, brutal_purge, soft_purge, []} - , {load_module, emqx_mgmt_cli, brutal_purge, soft_purge, []} - ]} + [ {<<"4.3.[0-2]">>, + [ {restart_application, emqx_management} + ]}, + {<<".*">>, []} ] }. diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 8c196f682..bfba34603 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -139,7 +139,7 @@ node_info(Node) when Node =:= node() -> Info#{node => node(), otp_release => iolist_to_binary(otp_rel()), memory_total => proplists:get_value(allocated, Memory), - memory_used => proplists:get_value(used, Memory), + memory_used => proplists:get_value(total, Memory), process_available => erlang:system_info(process_limit), process_used => erlang:system_info(process_count), max_fds => proplists:get_value(max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))), diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 5dedc91be..e068c5384 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -239,22 +239,30 @@ pick_params_to_qs([{Key, Value}|Params], QsKits, Acc1, Acc2) -> end end. -qs(<<"_gte_", Key/binary>>, Value, Type) -> - {binary_to_existing_atom(Key, utf8), '>=', to_type(Value, Type)}; -qs(<<"_lte_", Key/binary>>, Value, Type) -> - {binary_to_existing_atom(Key, utf8), '=<', to_type(Value, Type)}; -qs(<<"_like_", Key/binary>>, Value, Type) -> - {binary_to_existing_atom(Key, utf8), like, to_type(Value, Type)}; -qs(<<"_match_", Key/binary>>, Value, Type) -> - {binary_to_existing_atom(Key, utf8), match, to_type(Value, Type)}; -qs(Key, Value, Type) -> - {binary_to_existing_atom(Key, utf8), '=:=', to_type(Value, Type)}. - qs(K1, V1, K2, V2, Type) -> {Key, Op1, NV1} = qs(K1, V1, Type), {Key, Op2, NV2} = qs(K2, V2, Type), {Key, Op1, NV1, Op2, NV2}. +qs(K, Value0, Type) -> + try + qs(K, to_type(Value0, Type)) + catch + throw : bad_value_type -> + throw({bad_value_type, {K, Type, Value0}}) + end. + +qs(<<"_gte_", Key/binary>>, Value) -> + {binary_to_existing_atom(Key, utf8), '>=', Value}; +qs(<<"_lte_", Key/binary>>, Value) -> + {binary_to_existing_atom(Key, utf8), '=<', Value}; +qs(<<"_like_", Key/binary>>, Value) -> + {binary_to_existing_atom(Key, utf8), like, Value}; +qs(<<"_match_", Key/binary>>, Value) -> + {binary_to_existing_atom(Key, utf8), match, Value}; +qs(Key, Value) -> + {binary_to_existing_atom(Key, utf8), '=:=', Value}. + is_fuzzy_key(<<"_like_", _/binary>>) -> true; is_fuzzy_key(<<"_match_", _/binary>>) -> @@ -265,11 +273,19 @@ is_fuzzy_key(_) -> %%-------------------------------------------------------------------- %% Types -to_type(V, atom) -> to_atom(V); -to_type(V, integer) -> to_integer(V); -to_type(V, timestamp) -> to_timestamp(V); -to_type(V, ip) -> aton(V); -to_type(V, _) -> V. +to_type(V, TargetType) -> + try + to_type_(V, TargetType) + catch + _ : _ -> + throw(bad_value_type) + end. + +to_type_(V, atom) -> to_atom(V); +to_type_(V, integer) -> to_integer(V); +to_type_(V, timestamp) -> to_timestamp(V); +to_type_(V, ip) -> aton(V); +to_type_(V, _) -> V. to_atom(A) when is_atom(A) -> A; diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 90f0d3466..2fe6a5ccb 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -140,10 +140,14 @@ -define(format_fun, {?MODULE, format_channel_info}). list(Bindings, Params) when map_size(Bindings) == 0 -> - minirest:return({ok, emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun)}); + fence(fun() -> + emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun) + end); list(#{node := Node}, Params) when Node =:= node() -> - minirest:return({ok, emqx_mgmt_api:node_query(Node, Params, ?CLIENT_QS_SCHEMA, ?query_fun)}); + fence(fun() -> + emqx_mgmt_api:node_query(Node, Params, ?CLIENT_QS_SCHEMA, ?query_fun) + end); list(Bindings = #{node := Node}, Params) -> case rpc:call(Node, ?MODULE, list, [Bindings, Params]) of @@ -151,6 +155,19 @@ list(Bindings = #{node := Node}, Params) -> Res -> Res end. +%% @private +fence(Func) -> + try + minirest:return({ok, Func()}) + catch + throw : {bad_value_type, {_Key, Type, Value}} -> + Reason = iolist_to_binary( + io_lib:format("Can't convert ~p to ~p type", + [Value, Type]) + ), + minirest:return({error, ?ERROR8, Reason}) + end. + lookup(#{node := Node, clientid := ClientId}, _Params) -> minirest:return({ok, emqx_mgmt:lookup_client(Node, {clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)}); diff --git a/apps/emqx_management/src/emqx_mgmt_api_data.erl b/apps/emqx_management/src/emqx_mgmt_api_data.erl index 83b8d0e28..e389a1313 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_data.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_data.erl @@ -110,7 +110,8 @@ get_list_exported() -> import(_Bindings, Params) -> case proplists:get_value(<<"filename">>, Params) of undefined -> - minirest:return({error, missing_required_params}); + Result = import_content(Params), + minirest:return(Result); Filename -> case proplists:get_value(<<"node">>, Params) of undefined -> @@ -127,11 +128,11 @@ import(_Bindings, Params) -> end. do_import(Filename) -> - FullFilename = filename:join([emqx:get_env(data_dir), Filename]), + FullFilename = fullname(Filename), emqx_mgmt_data_backup:import(FullFilename, "{}"). download(#{filename := Filename}, _Params) -> - FullFilename = filename:join([emqx:get_env(data_dir), Filename]), + FullFilename = fullname(Filename), case file:read_file(FullFilename) of {ok, Bin} -> {ok, #{filename => list_to_binary(Filename), @@ -145,7 +146,7 @@ upload(Bindings, Params) -> do_upload(_Bindings, #{<<"filename">> := Filename, <<"file">> := Bin}) -> - FullFilename = filename:join([emqx:get_env(data_dir), Filename]), + FullFilename = fullname(Filename), case file:write_file(FullFilename, Bin) of ok -> minirest:return({ok, [{node, node()}]}); @@ -153,18 +154,33 @@ do_upload(_Bindings, #{<<"filename">> := Filename, minirest:return({error, Reason}) end; do_upload(Bindings, Params = #{<<"file">> := _}) -> - Seconds = erlang:system_time(second), - {{Y, M, D}, {H, MM, S}} = emqx_mgmt_util:datetime(Seconds), - Filename = io_lib:format("emqx-export-~p-~p-~p-~p-~p-~p.json", [Y, M, D, H, MM, S]), - do_upload(Bindings, Params#{<<"filename">> => Filename}); + do_upload(Bindings, Params#{<<"filename">> => tmp_filename()}); do_upload(_Bindings, _Params) -> minirest:return({error, missing_required_params}). delete(#{filename := Filename}, _Params) -> - FullFilename = filename:join([emqx:get_env(data_dir), Filename]), + FullFilename = fullname(Filename), case file:delete(FullFilename) of ok -> minirest:return(); {error, Reason} -> minirest:return({error, Reason}) end. + +import_content(Content) -> + File = dump_to_tmp_file(Content), + do_import(File). + +dump_to_tmp_file(Content) -> + Bin = emqx_json:encode(Content), + Filename = tmp_filename(), + ok = file:write_file(fullname(Filename), Bin), + Filename. + +fullname(Name) -> + filename:join(emqx:get_env(data_dir), Name). + +tmp_filename() -> + Seconds = erlang:system_time(second), + {{Y, M, D}, {H, MM, S}} = emqx_mgmt_util:datetime(Seconds), + io_lib:format("emqx-export-~p-~p-~p-~p-~p-~p.json", [Y, M, D, H, MM, S]). diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index a037da37f..da5a75027 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -119,12 +119,18 @@ authorize_appid(Req) -> _ -> false end. +-ifdef(EMQX_ENTERPRISE). +filter(_) -> + true. +-else. filter(#{app := emqx_modules}) -> true; filter(#{app := App}) -> case emqx_plugins:find_plugin(App) of false -> false; Plugin -> Plugin#plugin.active end. +-endif. + format(Port) when is_integer(Port) -> io_lib:format("0.0.0.0:~w", [Port]); diff --git a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl index bf3727ff5..d4d284e69 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl @@ -553,6 +553,23 @@ t_data(_) -> application:stop(emqx_dahboard), ok. +t_data_import_content(_) -> + ok = emqx_rule_registry:mnesia(boot), + ok = emqx_dashboard_admin:mnesia(boot), + application:ensure_all_started(emqx_rule_engine), + application:ensure_all_started(emqx_dashboard), + {ok, Data} = request_api(post, api_path(["data","export"]), [], auth_header_(), [#{}]), + #{<<"filename">> := Filename} = emqx_ct_http:get_http_data(Data), + Dir = emqx:get_env(data_dir), + {ok, Bin} = file:read_file(filename:join(Dir, Filename)), + Content = emqx_json:decode(Bin), + %% TODO: enable when 5.0 if we are still using data export/import + %?assertMatch({ok, "{\"code\":0}"}, request_api(post, api_path(["data","import"]), [], auth_header_(), Content)), + ?assertMatch({ok, "{\"message\":\"5.0\",\"code\":\"unsupported_version\"}"}, + request_api(post, api_path(["data","import"]), [], auth_header_(), Content)), + application:stop(emqx_rule_engine), + application:stop(emqx_dahboard). + request_api(Method, Url, Auth) -> request_api(Method, Url, [], Auth, []). diff --git a/apps/emqx_recon/src/emqx_recon.app.src b/apps/emqx_recon/src/emqx_recon.app.src index 061ed8e93..e25e2bbc5 100644 --- a/apps/emqx_recon/src/emqx_recon.app.src +++ b/apps/emqx_recon/src/emqx_recon.app.src @@ -1,6 +1,6 @@ {application, emqx_recon, [{description, "EMQ X Recon Plugin"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, {applications, [kernel,stdlib,recon]}, diff --git a/apps/emqx_resource/rebar.config b/apps/emqx_resource/rebar.config index 305312b4e..66271ee56 100644 --- a/apps/emqx_resource/rebar.config +++ b/apps/emqx_resource/rebar.config @@ -12,7 +12,6 @@ {dialyzer, [{warnings, [unmatched_returns, error_handling]} ]}. -{deps, [ {hocon, {git, "https://github.com/emqx/hocon", {branch, "master"}}} - , {jsx, {git, "https://github.com/talentdeficit/jsx", {tag, "v3.1.0"}}} +{deps, [ {jsx, {git, "https://github.com/talentdeficit/jsx", {tag, "v3.1.0"}}} ]}. diff --git a/apps/emqx_resource/src/emqx_resource_instance.erl b/apps/emqx_resource/src/emqx_resource_instance.erl index 1e924e249..d2f4dbbfa 100644 --- a/apps/emqx_resource/src/emqx_resource_instance.erl +++ b/apps/emqx_resource/src/emqx_resource_instance.erl @@ -100,6 +100,16 @@ save_config_to_disk(InstId, ResourceType, Config) -> emqx_data_dir() -> "data". +save_config_to_disk(InstId, ResourceType, Config) -> + %% TODO: send an event to the config handler, and the hander (single process) + %% will dump configs for all instances (from an ETS table) to a file. + file:write_file(filename:join([emqx_data_dir(), binary_to_list(InstId) ++ ".conf"]), + jsx:encode(#{id => InstId, resource_type => ResourceType, + config => emqx_resource:call_jsonify(ResourceType, Config)})). + +emqx_data_dir() -> + "data". + %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ diff --git a/apps/emqx_resource/src/emqx_resource_transform.erl b/apps/emqx_resource/src/emqx_resource_transform.erl index 21327dd9f..e18bf1eab 100644 --- a/apps/emqx_resource/src/emqx_resource_transform.erl +++ b/apps/emqx_resource/src/emqx_resource_transform.erl @@ -29,7 +29,7 @@ parse_transform(Forms, _Opts) -> debug_print(Mod, Ts) -> {ok, Io} = file:open("./" ++ atom_to_list(Mod) ++ ".trans.erl", [write]), - do_debug_print(Io, Ts), + _ = do_debug_print(Io, Ts), file:close(Io). do_debug_print(Io, Ts) when is_list(Ts) -> diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index 2e82ef0cd..d8244c018 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -2553,33 +2553,20 @@ stop_apps() -> start_apps() -> [start_apps(App, SchemaFile, ConfigFile) || {App, SchemaFile, ConfigFile} - <- [{emqx, deps_path(emqx, "priv/emqx.schema"), - deps_path(emqx, "etc/emqx.conf.rendered")}, + <- [{emqx, emqx_schema, deps_path(emqx, "etc/emqx.conf")}, {emqx_rule_engine, local_path("priv/emqx_rule_engine.schema"), local_path("etc/emqx_rule_engine.conf")}]]. -start_apps(App, SchemaFile, ConfigFile) -> - read_schema_configs(App, SchemaFile, ConfigFile), - set_special_configs(App), - {ok, _} = application:ensure_all_started(App). - -read_schema_configs(App, SchemaFile, ConfigFile) -> - ct:pal("Read configs - SchemaFile: ~p, ConfigFile: ~p", [SchemaFile, ConfigFile]), - Schema = cuttlefish_schema:files([SchemaFile]), - {ok, Conf} = hocon:load(ConfigFile, #{format => proplists}), - NewConfig = cuttlefish_generator:map(Schema, Conf), - Vals = proplists:get_value(App, NewConfig, []), - [application:set_env(App, Par, Value) || {Par, Value} <- Vals]. +start_apps(App, Schema, ConfigFile) -> + emqx_ct_helpers:start_app(App, Schema, ConfigFile, fun set_special_configs/1). deps_path(App, RelativePath) -> - %% Note: not lib_dir because etc dir is not sym-link-ed to _build dir - %% but priv dir is - Path0 = code:priv_dir(App), + Path0 = code:lib_dir(App), Path = case file:read_link(Path0) of {ok, Resolved} -> Resolved; {error, _} -> Path0 end, - filename:join([Path, "..", RelativePath]). + filename:join([Path, RelativePath]). local_path(RelativePath) -> deps_path(emqx_rule_engine, RelativePath). diff --git a/apps/emqx_sn/src/emqx_sn.app.src b/apps/emqx_sn/src/emqx_sn.app.src index 3d7db1b02..0e4e53dc8 100644 --- a/apps/emqx_sn/src/emqx_sn.app.src +++ b/apps/emqx_sn/src/emqx_sn.app.src @@ -1,6 +1,6 @@ {application, emqx_sn, [{description, "EMQ X MQTT-SN Plugin"}, - {vsn, "4.3.1"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, {applications, [kernel,stdlib,esockd]}, diff --git a/apps/emqx_sn/src/emqx_sn.appup.src b/apps/emqx_sn/src/emqx_sn.appup.src index 499664fe1..a6a1a6c84 100644 --- a/apps/emqx_sn/src/emqx_sn.appup.src +++ b/apps/emqx_sn/src/emqx_sn.appup.src @@ -1,17 +1,13 @@ %% -*-: erlang -*- {VSN, [ - {"4.3.0", [ - {load_module, emqx_sn_asleep_timer, brutal_purge, soft_purge, []}, - {load_module, emqx_sn_gateway, brutal_purge, soft_purge, [emqx_sn_asleep_timer]} - ]}, - {<<".*">>, []} + {<<"4.3.[0-1]">>, [ + {restart_application, emqx_sn} + ]} ], [ - {"4.3.0", [ - {load_module, emqx_sn_asleep_timer, brutal_purge, soft_purge, []}, - {load_module, emqx_sn_gateway, brutal_purge, soft_purge, [emqx_sn_asleep_timer]} - ]}, - {<<".*">>, []} + {<<"4.3.[0-1]">>, [ + {restart_application, emqx_sn} + ]} ] }. diff --git a/apps/emqx_sn/src/emqx_sn_app.erl b/apps/emqx_sn/src/emqx_sn_app.erl index d39e8b34f..9575523f8 100644 --- a/apps/emqx_sn/src/emqx_sn_app.erl +++ b/apps/emqx_sn/src/emqx_sn_app.erl @@ -43,7 +43,8 @@ start(_Type, _Args) -> Addr = application:get_env(emqx_sn, port, 1884), GwId = application:get_env(emqx_sn, gateway_id, 1), - {ok, Sup} = emqx_sn_sup:start_link(Addr, GwId), + PredefTopics = application:get_env(emqx_sn, predefined, []), + {ok, Sup} = emqx_sn_sup:start_link(Addr, GwId, PredefTopics), start_listeners(), {ok, Sup}. @@ -57,13 +58,7 @@ stop(_State) -> -spec start_listeners() -> ok. start_listeners() -> - PredefTopics = application:get_env(emqx_sn, predefined, []), - ListenCfs = [begin - TabName = tabname(Proto, ListenOn), - {ok, RegistryPid} = emqx_sn_sup:start_registry_proc(emqx_sn_sup, TabName, PredefTopics), - {Proto, ListenOn, [{registry, {TabName, RegistryPid}} | Options]} - end || {Proto, ListenOn, Options} <- listeners_confs()], - lists:foreach(fun start_listener/1, ListenCfs). + lists:foreach(fun start_listener/1, listeners_confs()). -spec start_listener(listener()) -> ok. start_listener({Proto, ListenOn, Options}) -> @@ -151,7 +146,3 @@ format({Addr, Port}) when is_list(Addr) -> io_lib:format("~s:~w", [Addr, Port]); format({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). - -tabname(Proto, ListenOn) -> - list_to_atom(lists:flatten(["emqx_sn_registry__", atom_to_list(Proto), "_", format(ListenOn)])). - diff --git a/apps/emqx_sn/src/emqx_sn_gateway.erl b/apps/emqx_sn/src/emqx_sn_gateway.erl index 96f849974..2339961cf 100644 --- a/apps/emqx_sn/src/emqx_sn_gateway.erl +++ b/apps/emqx_sn/src/emqx_sn_gateway.erl @@ -82,7 +82,6 @@ sockname :: {inet:ip_address(), inet:port()}, peername :: {inet:ip_address(), inet:port()}, channel :: maybe(emqx_channel:channel()), - registry :: emqx_sn_registry:registry(), clientid :: maybe(binary()), username :: maybe(binary()), password :: maybe(binary()), @@ -147,7 +146,6 @@ kick(GwPid) -> init([{_, SockPid, Sock}, Peername, Options]) -> GwId = proplists:get_value(gateway_id, Options), - Registry = proplists:get_value(registry, Options), Username = proplists:get_value(username, Options, undefined), Password = proplists:get_value(password, Options, undefined), EnableQos3 = proplists:get_value(enable_qos3, Options, false), @@ -165,7 +163,6 @@ init([{_, SockPid, Sock}, Peername, Options]) -> sockname = Sockname, peername = Peername, channel = Channel, - registry = Registry, asleep_timer = emqx_sn_asleep_timer:init(), enable_stats = EnableStats, enable_qos3 = EnableQos3, @@ -205,9 +202,9 @@ idle(cast, {incoming, ?SN_PUBLISH_MSG(_Flag, _TopicId, _MsgId, _Data)}, State = idle(cast, {incoming, ?SN_PUBLISH_MSG(#mqtt_sn_flags{qos = ?QOS_NEG1, topic_id_type = TopicIdType }, TopicId, _MsgId, Data)}, - State = #state{clientid = ClientId, registry = Registry}) -> + State = #state{clientid = ClientId}) -> TopicName = case (TopicIdType =:= ?SN_SHORT_TOPIC) of - false -> emqx_sn_registry:lookup_topic(Registry, self(), TopicId); + false -> emqx_sn_registry:lookup_topic(ClientId, TopicId); true -> <> end, _ = case TopicName =/= undefined of @@ -292,9 +289,9 @@ wait_for_will_msg(EventType, EventContent, State) -> handle_event(EventType, EventContent, wait_for_will_msg, State). connected(cast, {incoming, ?SN_REGISTER_MSG(_TopicId, MsgId, TopicName)}, - State = #state{clientid = ClientId, registry = Registry}) -> + State = #state{clientid = ClientId}) -> State0 = - case emqx_sn_registry:register_topic(Registry, self(), TopicName) of + case emqx_sn_registry:register_topic(ClientId, TopicName) of TopicId when is_integer(TopicId) -> ?LOG(debug, "register ClientId=~p, TopicName=~p, TopicId=~p", [ClientId, TopicName, TopicId]), send_message(?SN_REGACK_MSG(TopicId, MsgId, ?SN_RC_ACCEPTED), State); @@ -580,13 +577,16 @@ handle_event(EventType, EventContent, StateName, State) -> [StateName, {EventType, EventContent}]), {keep_state, State}. -terminate(Reason, _StateName, #state{channel = Channel, - registry = Registry}) -> - emqx_sn_registry:unregister_topic(Registry, self()), - case Channel =:= undefined of - true -> ok; - false -> emqx_channel:terminate(Reason, Channel) - end. +terminate(Reason, _StateName, #state{channel = Channel}) -> + ClientId = emqx_channel:info(clientid, Channel), + case Reason of + {shutdown, takeovered} -> + ok; + _ -> + emqx_sn_registry:unregister_topic(ClientId) + end, + emqx_channel:terminate(Reason, Channel), + ok. code_change(_Vsn, StateName, State, _Extra) -> {ok, StateName, State}. @@ -719,11 +719,12 @@ mqtt2sn(?PUBCOMP_PACKET(MsgId), _State) -> mqtt2sn(?UNSUBACK_PACKET(MsgId), _State)-> ?SN_UNSUBACK_MSG(MsgId); -mqtt2sn(?PUBLISH_PACKET(QoS, Topic, PacketId, Payload), #state{registry = Registry}) -> +mqtt2sn(?PUBLISH_PACKET(QoS, Topic, PacketId, Payload), #state{channel = Channel}) -> NewPacketId = if QoS =:= ?QOS_0 -> 0; true -> PacketId end, - {TopicIdType, TopicContent} = case emqx_sn_registry:lookup_topic_id(Registry, self(), Topic) of + ClientId = emqx_channel:info(clientid, Channel), + {TopicIdType, TopicContent} = case emqx_sn_registry:lookup_topic_id(ClientId, Topic) of {predef, PredefTopicId} -> {?SN_PREDEFINED_TOPIC, PredefTopicId}; TopicId when is_integer(TopicId) -> @@ -844,14 +845,13 @@ do_connect(ClientId, CleanStart, WillFlag, Duration, State) -> do_2nd_connect(Flags, Duration, ClientId, State = #state{sockname = Sockname, peername = Peername, - registry = Registry, channel = Channel}) -> emqx_logger:set_metadata_clientid(ClientId), #mqtt_sn_flags{will = Will, clean_start = CleanStart} = Flags, NChannel = case CleanStart of true -> emqx_channel:terminate(normal, Channel), - emqx_sn_registry:unregister_topic(Registry, self()), + emqx_sn_registry:unregister_topic(ClientId), emqx_channel:init(#{socktype => udp, sockname => Sockname, peername => Peername, @@ -864,8 +864,9 @@ do_2nd_connect(Flags, Duration, ClientId, State = #state{sockname = Sockname, do_connect(ClientId, CleanStart, Will, Duration, NState). handle_subscribe(?SN_NORMAL_TOPIC, TopicName, QoS, MsgId, - State=#state{registry = Registry}) -> - case emqx_sn_registry:register_topic(Registry, self(), TopicName) of + State=#state{channel = Channel}) -> + ClientId = emqx_channel:info(clientid, Channel), + case emqx_sn_registry:register_topic(ClientId, TopicName) of {error, too_large} -> State0 = send_message(?SN_SUBACK_MSG(#mqtt_sn_flags{qos = QoS}, ?SN_INVALID_TOPIC_ID, @@ -879,8 +880,9 @@ handle_subscribe(?SN_NORMAL_TOPIC, TopicName, QoS, MsgId, end; handle_subscribe(?SN_PREDEFINED_TOPIC, TopicId, QoS, MsgId, - State = #state{registry = Registry}) -> - case emqx_sn_registry:lookup_topic(Registry, self(), TopicId) of + State = #state{channel = Channel}) -> + ClientId = emqx_channel:info(clientid, Channel), + case emqx_sn_registry:lookup_topic(ClientId, TopicId) of undefined -> State0 = send_message(?SN_SUBACK_MSG(#mqtt_sn_flags{qos = QoS}, TopicId, @@ -909,8 +911,9 @@ handle_unsubscribe(?SN_NORMAL_TOPIC, TopicId, MsgId, State) -> proto_unsubscribe(TopicId, MsgId, State); handle_unsubscribe(?SN_PREDEFINED_TOPIC, TopicId, MsgId, - State = #state{registry = Registry}) -> - case emqx_sn_registry:lookup_topic(Registry, self(), TopicId) of + State = #state{channel = Channel}) -> + ClientId = emqx_channel:info(clientid, Channel), + case emqx_sn_registry:lookup_topic(ClientId, TopicId) of undefined -> {keep_state, send_message(?SN_UNSUBACK_MSG(MsgId), State)}; PredefinedTopic -> @@ -932,10 +935,11 @@ do_publish(?SN_NORMAL_TOPIC, TopicName, Data, Flags, MsgId, State) -> <> = TopicName, do_publish(?SN_PREDEFINED_TOPIC, TopicId, Data, Flags, MsgId, State); do_publish(?SN_PREDEFINED_TOPIC, TopicId, Data, Flags, MsgId, - State=#state{registry = Registry}) -> + State=#state{channel = Channel}) -> #mqtt_sn_flags{qos = QoS, dup = Dup, retain = Retain} = Flags, NewQoS = get_corrected_qos(QoS), - case emqx_sn_registry:lookup_topic(Registry, self(), TopicId) of + ClientId = emqx_channel:info(clientid, Channel), + case emqx_sn_registry:lookup_topic(ClientId, TopicId) of undefined -> {keep_state, maybe_send_puback(NewQoS, TopicId, MsgId, ?SN_RC_INVALID_TOPIC_ID, State)}; @@ -946,7 +950,7 @@ do_publish(?SN_PREDEFINED_TOPIC, TopicId, Data, Flags, MsgId, do_publish(?SN_SHORT_TOPIC, STopicName, Data, Flags, MsgId, State) -> #mqtt_sn_flags{qos = QoS, dup = Dup, retain = Retain} = Flags, NewQoS = get_corrected_qos(QoS), - <> = STopicName , + <> = STopicName, case emqx_topic:wildcard(STopicName) of true -> {keep_state, maybe_send_puback(NewQoS, TopicId, MsgId, ?SN_RC_NOT_SUPPORTED, @@ -974,12 +978,13 @@ do_publish_will(#state{will_msg = WillMsg, clientid = ClientId}) -> ok. do_puback(TopicId, MsgId, ReturnCode, StateName, - State=#state{registry = Registry}) -> + State=#state{channel = Channel}) -> case ReturnCode of ?SN_RC_ACCEPTED -> handle_incoming(?PUBACK_PACKET(MsgId), StateName, State); ?SN_RC_INVALID_TOPIC_ID -> - case emqx_sn_registry:lookup_topic(Registry, self(), TopicId) of + ClientId = emqx_channel:info(clientid, Channel), + case emqx_sn_registry:lookup_topic(ClientId, TopicId) of undefined -> {keep_state, State}; TopicName -> %%notice that this TopicName maybe normal or predefined, @@ -1068,9 +1073,10 @@ handle_outgoing(Packets, State) when is_list(Packets) -> end, State, Packets); handle_outgoing(PubPkt = ?PUBLISH_PACKET(_, TopicName, _, _), - State = #state{registry = Registry}) -> + State = #state{channel = Channel}) -> ?LOG(debug, "Handle outgoing publish: ~0p", [PubPkt]), - TopicId = emqx_sn_registry:lookup_topic_id(Registry, self(), TopicName), + ClientId = emqx_channel:info(clientid, Channel), + TopicId = emqx_sn_registry:lookup_topic_id(ClientId, TopicName), case (TopicId == undefined) andalso (byte_size(TopicName) =/= 2) of true -> register_and_notify_client(PubPkt, State); false -> send_message(mqtt2sn(PubPkt, State), State) @@ -1094,10 +1100,11 @@ replay_no_reg_pending_publishes(TopicId, #state{pending_topic_ids = Pendings} = State#state{pending_topic_ids = maps:remove(TopicId, Pendings)}. register_and_notify_client(?PUBLISH_PACKET(QoS, TopicName, PacketId, Payload) = PubPkt, - State = #state{registry = Registry, pending_topic_ids = Pendings}) -> + State = #state{pending_topic_ids = Pendings, channel = Channel}) -> MsgId = message_id(PacketId), #mqtt_packet{header = #mqtt_packet_header{dup = Dup, retain = Retain}} = PubPkt, - TopicId = emqx_sn_registry:register_topic(Registry, self(), TopicName), + ClientId = emqx_channel:info(clientid, Channel), + TopicId = emqx_sn_registry:register_topic(ClientId, TopicName), ?LOG(debug, "Register TopicId=~p, TopicName=~p, Payload=~p, Dup=~p, QoS=~p, " "Retain=~p, MsgId=~p", [TopicId, TopicName, Payload, Dup, QoS, Retain, MsgId]), NewPendings = cache_no_reg_publish_message(Pendings, TopicId, PubPkt, State), diff --git a/apps/emqx_sn/src/emqx_sn_registry.erl b/apps/emqx_sn/src/emqx_sn_registry.erl index fa17ebc27..4a3b22585 100644 --- a/apps/emqx_sn/src/emqx_sn_registry.erl +++ b/apps/emqx_sn/src/emqx_sn_registry.erl @@ -23,16 +23,16 @@ -define(LOG(Level, Format, Args), emqx_logger:Level("MQTT-SN(registry): " ++ Format, Args)). --export([ start_link/2 - , stop/1 +-export([ start_link/1 + , stop/0 ]). --export([ register_topic/3 - , unregister_topic/2 +-export([ register_topic/2 + , unregister_topic/1 ]). --export([ lookup_topic/3 - , lookup_topic_id/3 +-export([ lookup_topic/2 + , lookup_topic_id/2 ]). %% gen_server callbacks @@ -46,25 +46,45 @@ -define(TAB, ?MODULE). --record(state, {tab, max_predef_topic_id = 0}). +-record(state, {max_predef_topic_id = 0}). --type(registry() :: {ets:tab(), pid()}). +-record(emqx_sn_registry, {key, value}). + +%% Mnesia bootstrap +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + + +%% @doc Create or replicate tables. +-spec(mnesia(boot | copy) -> ok). +mnesia(boot) -> + %% Optimize storage + StoreProps = [{ets, [{read_concurrency, true}]}], + ok = ekka_mnesia:create_table(?MODULE, [ + {attributes, record_info(fields, emqx_sn_registry)}, + {ram_copies, [node()]}, + {storage_properties, StoreProps}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?MODULE, ram_copies). %%----------------------------------------------------------------------------- --spec(start_link(atom(), list()) -> {ok, pid()} | ignore | {error, Reason :: term()}). -start_link(Tab, PredefTopics) -> - gen_server:start_link(?MODULE, [Tab, PredefTopics], []). +-spec(start_link(list()) -> {ok, pid()} | ignore | {error, Reason :: term()}). +start_link(PredefTopics) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [PredefTopics], []). --spec(stop(registry()) -> ok). -stop({_Tab, Pid}) -> - gen_server:stop(Pid, normal, infinity). +-spec(stop() -> ok). +stop() -> + gen_server:stop(?MODULE, normal, infinity). --spec(register_topic(registry(), pid(), binary()) -> integer() | {error, term()}). -register_topic({_, Pid}, ClientPid, TopicName) when is_binary(TopicName) -> +-spec(register_topic(binary(), binary()) -> integer() | {error, term()}). +register_topic(ClientId, TopicName) when is_binary(TopicName) -> case emqx_topic:wildcard(TopicName) of false -> - gen_server:call(Pid, {register, ClientPid, TopicName}); + gen_server:call(?MODULE, {register, ClientId, TopicName}); %% TopicId: in case of “accepted” the value that will be used as topic %% id by the gateway when sending PUBLISH messages to the client (not %% relevant in case of subscriptions to a short topic name or to a topic @@ -72,22 +92,22 @@ register_topic({_, Pid}, ClientPid, TopicName) when is_binary(TopicName) -> true -> {error, wildcard_topic} end. --spec(lookup_topic(registry(), pid(), pos_integer()) -> undefined | binary()). -lookup_topic({Tab, _Pid}, ClientPid, TopicId) when is_integer(TopicId) -> - case lookup_element(Tab, {predef, TopicId}, 2) of +-spec(lookup_topic(binary(), pos_integer()) -> undefined | binary()). +lookup_topic(ClientId, TopicId) when is_integer(TopicId) -> + case lookup_element(?TAB, {predef, TopicId}, 3) of undefined -> - lookup_element(Tab, {ClientPid, TopicId}, 2); + lookup_element(?TAB, {ClientId, TopicId}, 3); Topic -> Topic end. --spec(lookup_topic_id(registry(), pid(), binary()) +-spec(lookup_topic_id(binary(), binary()) -> undefined | pos_integer() | {predef, integer()}). -lookup_topic_id({Tab, _Pid}, ClientPid, TopicName) when is_binary(TopicName) -> - case lookup_element(Tab, {predef, TopicName}, 2) of +lookup_topic_id(ClientId, TopicName) when is_binary(TopicName) -> + case lookup_element(?TAB, {predef, TopicName}, 3) of undefined -> - lookup_element(Tab, {ClientPid, TopicName}, 2); + lookup_element(?TAB, {ClientId, TopicName}, 3); TopicId -> {predef, TopicId} end. @@ -96,47 +116,59 @@ lookup_topic_id({Tab, _Pid}, ClientPid, TopicName) when is_binary(TopicName) -> lookup_element(Tab, Key, Pos) -> try ets:lookup_element(Tab, Key, Pos) catch error:badarg -> undefined end. --spec(unregister_topic(registry(), pid()) -> ok). -unregister_topic({_Tab, Pid}, ClientPid) -> - gen_server:call(Pid, {unregister, ClientPid}). +-spec(unregister_topic(binary()) -> ok). +unregister_topic(ClientId) -> + gen_server:call(?MODULE, {unregister, ClientId}). %%----------------------------------------------------------------------------- -init([Tab, PredefTopics]) -> +init([PredefTopics]) -> %% {predef, TopicId} -> TopicName %% {predef, TopicName} -> TopicId - %% {ClientPid, TopicId} -> TopicName - %% {ClientPid, TopicName} -> TopicId - _ = ets:new(Tab, [set, public, named_table, {read_concurrency, true}]), + %% {ClientId, TopicId} -> TopicName + %% {ClientId, TopicName} -> TopicId MaxPredefId = lists:foldl( fun({TopicId, TopicName}, AccId) -> - _ = ets:insert(Tab, {{predef, TopicId}, TopicName}), - _ = ets:insert(Tab, {{predef, TopicName}, TopicId}), + mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicId}, + value = TopicName}), + mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicName}, + value = TopicId}), if TopicId > AccId -> TopicId; true -> AccId end end, 0, PredefTopics), - {ok, #state{tab = Tab, max_predef_topic_id = MaxPredefId}}. + {ok, #state{max_predef_topic_id = MaxPredefId}}. -handle_call({register, ClientPid, TopicName}, _From, - State = #state{tab = Tab, max_predef_topic_id = PredefId}) -> - case lookup_topic_id({Tab, self()}, ClientPid, TopicName) of +handle_call({register, ClientId, TopicName}, _From, + State = #state{max_predef_topic_id = PredefId}) -> + case lookup_topic_id(ClientId, TopicName) of {predef, PredefTopicId} when is_integer(PredefTopicId) -> {reply, PredefTopicId, State}; TopicId when is_integer(TopicId) -> {reply, TopicId, State}; undefined -> - case next_topic_id(Tab, PredefId, ClientPid) of + case next_topic_id(?TAB, PredefId, ClientId) of TopicId when TopicId >= 16#FFFF -> {reply, {error, too_large}, State}; TopicId -> - _ = ets:insert(Tab, {{ClientPid, next_topic_id}, TopicId + 1}), - _ = ets:insert(Tab, {{ClientPid, TopicName}, TopicId}), - _ = ets:insert(Tab, {{ClientPid, TopicId}, TopicName}), - {reply, TopicId, State} + Fun = fun() -> + mnesia:write(#emqx_sn_registry{key = {ClientId, next_topic_id}, + value = TopicId + 1}), + mnesia:write(#emqx_sn_registry{key = {ClientId, TopicName}, + value = TopicId}), + mnesia:write(#emqx_sn_registry{key = {ClientId, TopicId}, + value = TopicName}) + end, + case mnesia:transaction(Fun) of + {atomic, ok} -> + {reply, TopicId, State}; + {aborted, Error} -> + {reply, {error, Error}, State} + end end end; -handle_call({unregister, ClientPid}, _From, State = #state{tab = Tab}) -> - ets:match_delete(Tab, {{ClientPid, '_'}, '_'}), +handle_call({unregister, ClientId}, _From, State) -> + Registry = mnesia:dirty_match_object({?TAB, {ClientId, '_'}, '_'}), + lists:foreach(fun(R) -> mnesia:dirty_delete_object(R) end, Registry), {reply, ok, State}; handle_call(Req, _From, State) -> @@ -159,8 +191,8 @@ code_change(_OldVsn, State, _Extra) -> %%----------------------------------------------------------------------------- -next_topic_id(Tab, PredefId, ClientPid) -> - case ets:lookup(Tab, {ClientPid, next_topic_id}) of - [{_, Id}] -> Id; +next_topic_id(Tab, PredefId, ClientId) -> + case mnesia:dirty_read(Tab, {ClientId, next_topic_id}) of + [#emqx_sn_registry{value = Id}] -> Id; [] -> PredefId + 1 end. diff --git a/apps/emqx_sn/src/emqx_sn_sup.erl b/apps/emqx_sn/src/emqx_sn_sup.erl index 817aa4d06..3d4fe602f 100644 --- a/apps/emqx_sn/src/emqx_sn_sup.erl +++ b/apps/emqx_sn/src/emqx_sn_sup.erl @@ -18,32 +18,26 @@ -behaviour(supervisor). --export([ start_link/2 - , start_registry_proc/3 +-export([ start_link/3 , init/1 ]). -start_registry_proc(Sup, TabName, PredefTopics) -> - Registry = #{id => TabName, - start => {emqx_sn_registry, start_link, [TabName, PredefTopics]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_sn_registry]}, - handle_ret(supervisor:start_child(Sup, Registry)). +start_link(Addr, GwId, PredefTopics) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, [Addr, GwId, PredefTopics]). -start_link(Addr, GwId) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [Addr, GwId]). - -init([{_Ip, Port}, GwId]) -> +init([{_Ip, Port}, GwId, PredefTopics]) -> Broadcast = #{id => emqx_sn_broadcast, start => {emqx_sn_broadcast, start_link, [GwId, Port]}, restart => permanent, shutdown => brutal_kill, type => worker, modules => [emqx_sn_broadcast]}, - {ok, {{one_for_one, 10, 3600}, [Broadcast]}}. + Registry = #{id => emqx_sn_registry, + start => {emqx_sn_registry, start_link, [PredefTopics]}, + restart => permanent, + shutdown => brutal_kill, + type => worker, + modules => [emqx_sn_registry]}, + {ok, {{one_for_one, 10, 3600}, [Broadcast, Registry]}}. -handle_ret({ok, Pid, _Info}) -> {ok, Pid}; -handle_ret(Ret) -> Ret. diff --git a/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl index 2972571be..a953a45e7 100644 --- a/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl @@ -47,6 +47,9 @@ % FLAG NOT USED -define(FNU, 0). +%% erlang:system_time should be unique and random enough +-define(CLIENTID, iolist_to_binary([atom_to_list(?FUNCTION_NAME), "-", + integer_to_list(erlang:system_time())])). %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- @@ -55,6 +58,7 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> + logger:set_module_level(emqx_sn_gateway, debug), emqx_ct_helpers:start_apps([emqx_sn], fun set_special_confs/1), Config. @@ -96,7 +100,8 @@ t_connect(_) -> t_do_2nd_connect(_) -> {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"client_id_test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), timer:sleep(100), send_connect_msg(Socket, <<"client_id_other">>), @@ -116,7 +121,8 @@ t_subscribe(_) -> TopicId = ?MAX_PRED_TOPIC_ID + 1, ReturnCode = 0, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"client_id_test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), TopicName1 = <<"abcD">>, send_register_msg(Socket, TopicName1, MsgId), @@ -125,12 +131,12 @@ t_subscribe(_) -> ?assertEqual(<<8, ?SN_SUBACK, Dup:1, QoS:2, Retain:1, Will:1, CleanSession:1, ?SN_NORMAL_TOPIC:2, TopicId:16, MsgId:16, ReturnCode>>, receive_response(Socket)), - ?assertEqual([TopicName1], emqx_broker:topics()), + ?assert(lists:member(TopicName1, emqx_broker:topics())), send_unsubscribe_msg_normal_topic(Socket, TopicName1, MsgId), ?assertEqual(<<4, ?SN_UNSUBACK, MsgId:16>>, receive_response(Socket)), timer:sleep(100), - ?assertEqual([], emqx_broker:topics()), + ?assertNot(lists:member(TopicName1, emqx_broker:topics())), send_disconnect_msg(Socket, undefined), ?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)), @@ -146,7 +152,8 @@ t_subscribe_case01(_) -> TopicId = ?MAX_PRED_TOPIC_ID + 1, ReturnCode = 0, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"client_id_test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), TopicName1 = <<"abcD">>, @@ -176,7 +183,8 @@ t_subscribe_case02(_) -> ReturnCode = 0, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"client_id_test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ?CLIENTID), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), Topic1 = ?PREDEF_TOPIC_NAME1, @@ -206,7 +214,7 @@ t_subscribe_case03(_) -> ReturnCode = 0, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"ClientA">>, + ClientId = ?CLIENTID, send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), @@ -234,7 +242,8 @@ t_subscribe_case04(_) -> TopicId = ?PREDEF_TOPIC_ID1, %this TopicId is the predefined topic id corresponding to ?PREDEF_TOPIC_NAME1 ReturnCode = 0, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"client_id_test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), Topic1 = ?PREDEF_TOPIC_NAME1, send_register_msg(Socket, Topic1, MsgId), @@ -263,7 +272,7 @@ t_subscribe_case05(_) -> TopicId2 = ?MAX_PRED_TOPIC_ID + 2, ReturnCode = 0, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"testu">>, + ClientId = ?CLIENTID, send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), @@ -304,8 +313,9 @@ t_subscribe_case06(_) -> TopicId1 = ?MAX_PRED_TOPIC_ID + 1, TopicId2 = ?MAX_PRED_TOPIC_ID + 2, ReturnCode = 0, + ClientId = ?CLIENTID, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_register_msg(Socket, <<"abc">>, MsgId), @@ -340,7 +350,8 @@ t_subscribe_case07(_) -> TopicId1 = ?MAX_PRED_TOPIC_ID + 2, TopicId2 = ?MAX_PRED_TOPIC_ID + 3, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_predefined_topic(Socket, QoS, TopicId1, MsgId), @@ -362,7 +373,8 @@ t_subscribe_case08(_) -> MsgId = 1, TopicId2 = 2, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_reserved_topic(Socket, QoS, TopicId2, MsgId), @@ -383,7 +395,8 @@ t_publish_negqos_case09(_) -> MsgId = 1, TopicId1 = ?MAX_PRED_TOPIC_ID + 1, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), Topic = <<"abc">>, @@ -416,7 +429,8 @@ t_publish_qos0_case01(_) -> MsgId = 1, TopicId1 = ?MAX_PRED_TOPIC_ID + 1, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), Topic = <<"abc">>, @@ -447,7 +461,8 @@ t_publish_qos0_case02(_) -> MsgId = 1, PredefTopicId = ?PREDEF_TOPIC_ID1, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_predefined_topic(Socket, QoS, PredefTopicId, MsgId), @@ -476,7 +491,8 @@ t_publish_qos0_case3(_) -> MsgId = 1, TopicId = ?MAX_PRED_TOPIC_ID + 1, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), Topic = <<"/a/b/c">>, @@ -506,7 +522,8 @@ t_publish_qos0_case04(_) -> MsgId = 1, TopicId0 = 0, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_normal_topic(Socket, QoS, <<"#">>, MsgId), @@ -536,7 +553,8 @@ t_publish_qos0_case05(_) -> MsgId = 1, TopicId0 = 0, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_short_topic(Socket, QoS, <<"/#">>, MsgId), ?assertEqual(<<8, ?SN_SUBACK, Dup:1, QoS:2, Retain:1, Will:1, CleanSession:1, ?SN_NORMAL_TOPIC:2, TopicId0:16, MsgId:16, ?SN_RC_ACCEPTED>>, @@ -556,7 +574,8 @@ t_publish_qos0_case06(_) -> MsgId = 1, TopicId1 = ?MAX_PRED_TOPIC_ID + 1, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), Topic = <<"abc">>, @@ -587,7 +606,8 @@ t_publish_qos1_case01(_) -> TopicId1 = ?MAX_PRED_TOPIC_ID + 1, Topic = <<"abc">>, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_normal_topic(Socket, QoS, Topic, MsgId), ?assertEqual(<<8, ?SN_SUBACK, Dup:1, QoS:2, Retain:1, Will:1, CleanSession:1, @@ -613,7 +633,8 @@ t_publish_qos1_case02(_) -> MsgId = 1, PredefTopicId = ?PREDEF_TOPIC_ID1, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_predefined_topic(Socket, QoS, PredefTopicId, MsgId), @@ -633,7 +654,8 @@ t_publish_qos1_case03(_) -> MsgId = 1, TopicId5 = 5, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_publish_msg_predefined_topic(Socket, QoS, MsgId, tid(5), <<20, 21, 22, 23>>), ?assertEqual(<<7, ?SN_PUBACK, TopicId5:16, MsgId:16, ?SN_RC_INVALID_TOPIC_ID>>, receive_response(Socket)), @@ -651,7 +673,8 @@ t_publish_qos1_case04(_) -> MsgId = 7, TopicId0 = 0, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_short_topic(Socket, QoS, <<"ab">>, MsgId), ?assertEqual(<<8, ?SN_SUBACK, Dup:1, QoS:2, Retain:1, Will:1, CleanSession:1, @@ -677,7 +700,8 @@ t_publish_qos1_case05(_) -> MsgId = 7, TopicId1 = ?MAX_PRED_TOPIC_ID + 1, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_normal_topic(Socket, QoS, <<"ab">>, MsgId), @@ -702,7 +726,8 @@ t_publish_qos1_case06(_) -> MsgId = 7, TopicId1 = ?MAX_PRED_TOPIC_ID + 1, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_normal_topic(Socket, QoS, <<"ab">>, MsgId), @@ -728,7 +753,8 @@ t_publish_qos2_case01(_) -> TopicId1 = ?MAX_PRED_TOPIC_ID + 1, Topic = <<"/abc">>, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_normal_topic(Socket, QoS, Topic, MsgId), ?assertEqual(<<8, ?SN_SUBACK, ?FNU:1, QoS:2, ?FNU:5, TopicId1:16, MsgId:16, @@ -755,7 +781,8 @@ t_publish_qos2_case02(_) -> MsgId = 7, PredefTopicId = ?PREDEF_TOPIC_ID2, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_predefined_topic(Socket, QoS, PredefTopicId, MsgId), @@ -785,7 +812,8 @@ t_publish_qos2_case03(_) -> MsgId = 7, TopicId0 = 0, {ok, Socket} = gen_udp:open(0, [binary]), - send_connect_msg(Socket, <<"test">>), + ClientId = ?CLIENTID, + send_connect_msg(Socket, ClientId), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), send_subscribe_msg_normal_topic(Socket, QoS, <<"/#">>, MsgId), @@ -811,7 +839,7 @@ t_will_case01(_) -> WillMsg = <<10, 11, 12, 13, 14>>, WillTopic = <<"abc">>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, ok = emqx_broker:subscribe(WillTopic), @@ -846,7 +874,7 @@ t_will_test2(_) -> Duration = 1, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, <<"goodbye">>, QoS), @@ -870,7 +898,7 @@ t_will_test3(_) -> Duration = 1, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_empty_msg(Socket), @@ -892,7 +920,7 @@ t_will_test4(_) -> Duration = 1, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, <<"abc">>, QoS), @@ -920,7 +948,7 @@ t_will_test5(_) -> Duration = 5, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, <<"abc">>, QoS), @@ -947,7 +975,7 @@ t_will_case06(_) -> WillMsg = <<10, 11, 12, 13, 14>>, WillTopic = <<"abc">>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, ok = emqx_broker:subscribe(WillTopic), @@ -983,7 +1011,7 @@ t_asleep_test01_timeout(_) -> WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1007,7 +1035,7 @@ t_asleep_test02_to_awake_and_back(_) -> WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Keepalive_Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1022,13 +1050,13 @@ t_asleep_test02_to_awake_and_back(_) -> timer:sleep(4500), % goto awake state and back - send_pingreq_msg(Socket, <<"test">>), + send_pingreq_msg(Socket, ClientId), ?assertEqual(<<2, ?SN_PINGRESP>>, receive_response(Socket)), timer:sleep(4500), % goto awake state and back - send_pingreq_msg(Socket, <<"test">>), + send_pingreq_msg(Socket, ClientId), ?assertEqual(<<2, ?SN_PINGRESP>>, receive_response(Socket)), %% during above procedure, mqtt keepalive timer should not terminate mqtt-sn process @@ -1045,7 +1073,7 @@ t_asleep_test03_to_awake_qos1_dl_msg(_) -> WillPayload = <<10, 11, 12, 13, 14>>, MsgId = 1000, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1084,7 +1112,7 @@ t_asleep_test03_to_awake_qos1_dl_msg(_) -> {ok, C} = emqtt:start_link(), {ok, _} = emqtt:connect(C), {ok, _} = emqtt:publish(C, TopicName1, Payload1, QoS), - timer:sleep(500), + timer:sleep(100), ok = emqtt:disconnect(C), timer:sleep(50), @@ -1108,7 +1136,7 @@ t_asleep_test04_to_awake_qos1_dl_msg(_) -> WillTopic = <<"dead">>, WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1149,7 +1177,7 @@ t_asleep_test04_to_awake_qos1_dl_msg(_) -> timer:sleep(300), % goto awake state, receive downlink messages, and go back to asleep - send_pingreq_msg(Socket, <<"test">>), + send_pingreq_msg(Socket, ClientId), %% 1. get REGISTER first, since this topic has never been registered UdpData1 = receive_response(Socket), @@ -1183,7 +1211,7 @@ t_asleep_test05_to_awake_qos1_dl_msg(_) -> WillTopic = <<"dead">>, WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1228,7 +1256,7 @@ t_asleep_test05_to_awake_qos1_dl_msg(_) -> timer:sleep(50), % goto awake state, receive downlink messages, and go back to asleep - send_pingreq_msg(Socket, <<"test">>), + send_pingreq_msg(Socket, ClientId), UdpData_reg = receive_response(Socket), {TopicIdNew, MsgId_reg} = check_register_msg_on_udp(TopicName_test5, UdpData_reg), @@ -1261,7 +1289,7 @@ t_asleep_test06_to_awake_qos2_dl_msg(_) -> WillTopic = <<"dead">>, WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1278,6 +1306,7 @@ t_asleep_test06_to_awake_qos2_dl_msg(_) -> CleanSession = 0, ReturnCode = 0, send_register_msg(Socket, TopicName_tom, MsgId1), + timer:sleep(50), TopicId_tom = check_regack_msg_on_udp(MsgId1, receive_response(Socket)), send_subscribe_msg_predefined_topic(Socket, QoS, TopicId_tom, MsgId1), ?assertEqual(<<8, ?SN_SUBACK, Dup:1, QoS:2, Retain:1, WillBit:1, CleanSession:1, @@ -1303,7 +1332,7 @@ t_asleep_test06_to_awake_qos2_dl_msg(_) -> timer:sleep(300), % goto awake state, receive downlink messages, and go back to asleep - send_pingreq_msg(Socket, <<"test">>), + send_pingreq_msg(Socket, ClientId), UdpData = wrap_receive_response(Socket), MsgId_udp = check_publish_msg_on_udp({Dup, QoS, Retain, WillBit, CleanSession, ?SN_NORMAL_TOPIC, TopicId_tom, Payload1}, UdpData), @@ -1322,7 +1351,7 @@ t_asleep_test07_to_connected(_) -> WillTopic = <<"dead">>, WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Keepalive_Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1371,7 +1400,7 @@ t_asleep_test08_to_disconnected(_) -> WillTopic = <<"dead">>, WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Keepalive_Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1402,7 +1431,7 @@ t_asleep_test09_to_awake_again_qos1_dl_msg(_) -> WillTopic = <<"dead">>, WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1445,7 +1474,7 @@ t_asleep_test09_to_awake_again_qos1_dl_msg(_) -> ok = emqtt:disconnect(C), % goto awake state, receive downlink messages, and go back to asleep - send_pingreq_msg(Socket, <<"test">>), + send_pingreq_msg(Socket, ClientId), UdpData_reg = receive_response(Socket), {TopicIdNew, MsgId_reg} = check_register_msg_on_udp(TopicName_test9, UdpData_reg), @@ -1481,7 +1510,7 @@ t_asleep_test09_to_awake_again_qos1_dl_msg(_) -> ?assertEqual(<<2, ?SN_PINGRESP>>, receive_response(Socket)), %% send PINGREQ again to enter awake state - send_pingreq_msg(Socket, <<"test">>), + send_pingreq_msg(Socket, ClientId), %% will not receive any buffered PUBLISH messages buffered before last awake, only receive PINGRESP here ?assertEqual(<<2, ?SN_PINGRESP>>, receive_response(Socket)), @@ -1494,7 +1523,7 @@ t_awake_test01_to_connected(_) -> WillTopic = <<"dead">>, WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Keepalive_Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1528,7 +1557,7 @@ t_awake_test02_to_disconnected(_) -> WillTopic = <<"dead">>, WillPayload = <<10, 11, 12, 13, 14>>, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = <<"test">>, + ClientId = ?CLIENTID, send_connect_msg_with_will(Socket, Keepalive_Duration, ClientId), ?assertEqual(<<2, ?SN_WILLTOPICREQ>>, receive_response(Socket)), send_willtopic_msg(Socket, WillTopic, QoS), @@ -1544,7 +1573,7 @@ t_awake_test02_to_disconnected(_) -> timer:sleep(100), % goto awake state - send_pingreq_msg(Socket, <<"test">>), + send_pingreq_msg(Socket, ClientId), ?assertEqual(<<2, ?SN_PINGRESP>>, receive_response(Socket)), %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -1591,7 +1620,7 @@ send_connect_msg(Socket, ClientId) -> ok = gen_udp:send(Socket, ?HOST, ?PORT, Packet). send_connect_msg_with_will(Socket, Duration, ClientId) -> - Length = 10, + Length = 6 + byte_size(ClientId), Will = 1, CleanSession = 1, ProtocolId = 1, @@ -1600,7 +1629,7 @@ send_connect_msg_with_will(Socket, Duration, ClientId) -> ok = gen_udp:send(Socket, ?HOST, ?PORT, ConnectPacket). send_connect_msg_with_will1(Socket, Duration, ClientId) -> - Length = 10, + Length = 6 + byte_size(ClientId), Will = 1, CleanSession = 0, ProtocolId = 1, diff --git a/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl b/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl index 8cce4592a..8d320d8ed 100644 --- a/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl +++ b/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl @@ -16,12 +16,9 @@ -module(emqx_sn_registry_SUITE). --import(proplists, [get_value/2]). - -compile(export_all). -compile(nowarn_export_all). --include_lib("emqx_sn/include/emqx_sn.hrl"). -include_lib("eunit/include/eunit.hrl"). -define(REGISTRY, emqx_sn_registry). @@ -44,84 +41,81 @@ end_per_suite(_Config) -> ok. init_per_testcase(_TestCase, Config) -> + ekka_mnesia:start(), + emqx_sn_registry:mnesia(boot), + mnesia:clear_table(emqx_sn_registry), PredefTopics = application:get_env(emqx_sn, predefined, []), - TabName = emqx_sn_registry, - {ok, Pid} = ?REGISTRY:start_link(TabName, PredefTopics), - [{registray, {TabName, Pid}} | Config]. + {ok, _Pid} = ?REGISTRY:start_link(PredefTopics), + Config. end_per_testcase(_TestCase, Config) -> - ?REGISTRY:stop(get_value(registray, Config)), + ?REGISTRY:stop(), Config. %%-------------------------------------------------------------------- %% Test cases %%-------------------------------------------------------------------- -t_register(Config) -> - Registry = get_value(registray, Config), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"Topic1">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"Topic2">>)), - ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic1">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic2">>)), - emqx_sn_registry:unregister_topic(Registry, <<"ClientId">>), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic1">>)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic2">>)). +t_register(_Config) -> + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic1">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic2">>)), + ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic1">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic2">>)), + emqx_sn_registry:unregister_topic(<<"ClientId">>), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic1">>)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic2">>)). -t_register_case2(Config) -> - Registry = get_value(registray, Config), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"Topic1">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"Topic2">>)), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"Topic1">>)), - ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic1">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic2">>)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic3">>)), - ?REGISTRY:unregister_topic(Registry, <<"ClientId">>), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic1">>)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, <<"Topic2">>)). +t_register_case2(_Config) -> + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic1">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic2">>)), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic1">>)), + ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic1">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic2">>)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic3">>)), + ?REGISTRY:unregister_topic(<<"ClientId">>), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic1">>)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic2">>)). -t_reach_maximum(Config) -> - Registry = get_value(registray, Config), - register_a_lot(Registry, ?MAX_PREDEF_ID+1, 16#ffff), - ?assertEqual({error, too_large}, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"TopicABC">>)), +t_reach_maximum(_Config) -> + register_a_lot(?MAX_PREDEF_ID+1, 16#ffff), + ?assertEqual({error, too_large}, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicABC">>)), Topic1 = iolist_to_binary(io_lib:format("Topic~p", [?MAX_PREDEF_ID+1])), Topic2 = iolist_to_binary(io_lib:format("Topic~p", [?MAX_PREDEF_ID+2])), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, Topic1)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, Topic2)), - ?REGISTRY:unregister_topic(Registry, <<"ClientId">>), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(Registry, <<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, Topic1)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Registry, <<"ClientId">>, Topic2)). + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(<<"ClientId">>, Topic1)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(<<"ClientId">>, Topic2)), + ?REGISTRY:unregister_topic(<<"ClientId">>), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, Topic1)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, Topic2)). -t_register_case4(Config) -> - Registry = get_value(registray, Config), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"TopicA">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"TopicB">>)), - ?assertEqual(?MAX_PREDEF_ID+3, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"TopicC">>)), - ?REGISTRY:unregister_topic(Registry, <<"ClientId">>), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"TopicD">>)). +t_register_case4(_Config) -> + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicA">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicB">>)), + ?assertEqual(?MAX_PREDEF_ID+3, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicC">>)), + ?REGISTRY:unregister_topic(<<"ClientId">>), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicD">>)). -t_deny_wildcard_topic(Config) -> - Registry = get_value(registray, Config), - ?assertEqual({error, wildcard_topic}, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"/TopicA/#">>)), - ?assertEqual({error, wildcard_topic}, ?REGISTRY:register_topic(Registry, <<"ClientId">>, <<"/+/TopicB">>)). +t_deny_wildcard_topic(_Config) -> + ?assertEqual({error, wildcard_topic}, ?REGISTRY:register_topic(<<"ClientId">>, <<"/TopicA/#">>)), + ?assertEqual({error, wildcard_topic}, ?REGISTRY:register_topic(<<"ClientId">>, <<"/+/TopicB">>)). %%-------------------------------------------------------------------- %% Helper funcs %%-------------------------------------------------------------------- -register_a_lot(_, Max, Max) -> +register_a_lot(Max, Max) -> ok; -register_a_lot(Registry, N, Max) when N < Max -> +register_a_lot(N, Max) when N < Max -> Topic = iolist_to_binary(["Topic", integer_to_list(N)]), - ?assertEqual(N, ?REGISTRY:register_topic(Registry, <<"ClientId">>, Topic)), - register_a_lot(Registry, N+1, Max). + ?assertEqual(N, ?REGISTRY:register_topic(<<"ClientId">>, Topic)), + register_a_lot(N+1, Max). diff --git a/apps/emqx_stomp/src/emqx_stomp.app.src b/apps/emqx_stomp/src/emqx_stomp.app.src index b03abdac1..2e66734ec 100644 --- a/apps/emqx_stomp/src/emqx_stomp.app.src +++ b/apps/emqx_stomp/src/emqx_stomp.app.src @@ -1,6 +1,6 @@ {application, emqx_stomp, [{description, "EMQ X Stomp Protocol Plugin"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_stomp_sup]}, {applications, [kernel,stdlib]}, diff --git a/apps/emqx_web_hook/src/emqx_web_hook.app.src b/apps/emqx_web_hook/src/emqx_web_hook.app.src index b68816579..97624a900 100644 --- a/apps/emqx_web_hook/src/emqx_web_hook.app.src +++ b/apps/emqx_web_hook/src/emqx_web_hook.app.src @@ -1,6 +1,6 @@ {application, emqx_web_hook, [{description, "EMQ X WebHook Plugin"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.3.1"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_web_hook_sup]}, {applications, [kernel,stdlib,ehttpc]}, diff --git a/apps/emqx_web_hook/src/emqx_web_hook.appup.src b/apps/emqx_web_hook/src/emqx_web_hook.appup.src index 0c7b8ebf3..b92284b64 100644 --- a/apps/emqx_web_hook/src/emqx_web_hook.appup.src +++ b/apps/emqx_web_hook/src/emqx_web_hook.appup.src @@ -2,9 +2,15 @@ {VSN, [ + {"4.3.0", [ + {load_module, emqx_web_hook_actions, brutal_purge, soft_purge, []} + ]}, {<<".*">>, []} ], [ + {"4.3.0", [ + {load_module, emqx_web_hook_actions, brutal_purge, soft_purge, []} + ]}, {<<".*">>, []} ] }. diff --git a/apps/emqx_web_hook/src/emqx_web_hook_actions.erl b/apps/emqx_web_hook/src/emqx_web_hook_actions.erl index c88f8f39b..f8e4d8f08 100644 --- a/apps/emqx_web_hook/src/emqx_web_hook_actions.erl +++ b/apps/emqx_web_hook/src/emqx_web_hook_actions.erl @@ -353,7 +353,7 @@ pool_name(ResId) -> list_to_atom("webhook:" ++ str(ResId)). get_ssl_opts(Opts, ResId) -> - [{ssl, true}, {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(Opts, "rules", ResId)}]. + emqx_plugin_libs_ssl:save_files_return_opts(Opts, "rules", ResId). test_http_connect(Conf) -> Url = fun() -> maps:get(<<"url">>, Conf) end, diff --git a/bin/emqx b/bin/emqx index fd6986614..96f6ec42f 100755 --- a/bin/emqx +++ b/bin/emqx @@ -20,8 +20,8 @@ mkdir -p "$RUNNER_LOG_DIR" # Make sure data directory exists mkdir -p "$RUNNER_DATA_DIR" -# cuttlefish try to read environment variables starting with "EMQX_" -export CUTTLEFISH_ENV_OVERRIDE_PREFIX='EMQX_' +# hocon try to read environment variables starting with "EMQX_" +export HOCON_ENV_OVERRIDE_PREFIX='EMQX_' relx_usage() { command="$1" @@ -123,9 +123,6 @@ fi # Echo to stderr on errors echoerr() { echo "$@" 1>&2; } -# By default, use cuttlefish to generate app.config and vm.args -CUTTLEFISH="${USE_CUTTLEFISH:-yes}" - SED_REPLACE="sed -i " case $(sed --help 2>&1) in *GNU*) SED_REPLACE="sed -i ";; @@ -202,54 +199,46 @@ generate_config() { ## changing the config 'log.rotation.size' rm -rf "${RUNNER_LOG_DIR}"/*.siz - if [ "$CUTTLEFISH" != "yes" ]; then - # Note: we have added a parameter '-vm_args' to this. It - # appears redundant but it is not! the erlang vm allows us to - # access all arguments to the erl command EXCEPT '-args_file', - # so in order to get access to this file location from within - # the vm, we need to pass it in twice. - CONFIG_ARGS=" -config $RUNNER_ETC_DIR/app.config -args_file $RUNNER_ETC_DIR/vm.args -vm_args $RUNNER_ETC_DIR/vm.args " - else - EMQX_LICENSE_CONF_OPTION="" - if [ "${EMQX_LICENSE_CONF:-}" != "" ]; then - EMQX_LICENSE_CONF_OPTION="-i ${EMQX_LICENSE_CONF}" - fi - - set +e - # shellcheck disable=SC2086 - CUTTLEFISH_OUTPUT="$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/cuttlefish -v -i "$REL_DIR"/emqx.schema $EMQX_LICENSE_CONF_OPTION -c "$RUNNER_ETC_DIR"/emqx.conf -d "$RUNNER_DATA_DIR"/configs generate)" - # shellcheck disable=SC2181 - RESULT=$? - set -e - if [ $RESULT -gt 0 ]; then - echo "$CUTTLEFISH_OUTPUT" - exit $RESULT - fi - # print override from environment variables (EMQX_*) - echo "$CUTTLEFISH_OUTPUT" | sed -e '$d' - CONFIG_ARGS=$(echo "$CUTTLEFISH_OUTPUT" | tail -n 1) - - ## Merge cuttlefish generated *.args into the vm.args - CUTTLE_GEN_ARG_FILE=$(echo "$CONFIG_ARGS" | sed -n 's/^.*\(vm_args[[:space:]]\)//p' | awk '{print $1}') - TMP_ARG_FILE="$RUNNER_DATA_DIR/configs/vm.args.tmp" - cp "$RUNNER_ETC_DIR/vm.args" "$TMP_ARG_FILE" - echo "" >> "$TMP_ARG_FILE" - echo "-pa ${REL_DIR}/consolidated" >> "$TMP_ARG_FILE" - sed '/^#/d' "$CUTTLE_GEN_ARG_FILE" | sed '/^$/d' | while IFS='' read -r ARG_LINE || [ -n "$ARG_LINE" ]; do - ARG_KEY=$(echo "$ARG_LINE" | awk '{$NF="";print}') - ARG_VALUE=$(echo "$ARG_LINE" | awk '{print $NF}') - TMP_ARG_VALUE=$(grep "^$ARG_KEY" "$TMP_ARG_FILE" | awk '{print $NF}') - if [ "$ARG_VALUE" != "$TMP_ARG_VALUE" ] ; then - if [ -n "$TMP_ARG_VALUE" ]; then - sh -c "$SED_REPLACE 's/^$ARG_KEY.*$/$ARG_LINE/' $TMP_ARG_FILE" - else - echo "$ARG_LINE" >> "$TMP_ARG_FILE" - fi - fi - done - mv -f "$TMP_ARG_FILE" "$CUTTLE_GEN_ARG_FILE" + EMQX_LICENSE_CONF_OPTION="" + if [ "${EMQX_LICENSE_CONF:-}" != "" ]; then + EMQX_LICENSE_CONF_OPTION="-c ${EMQX_LICENSE_CONF}" fi + set +e + # disable shellcheck; let EMQX_LICENSE_CONF_OPTION split + # shellcheck disable=SC2086 + HOCON_OUTPUT="$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/hocon -s emqx_schema -c "$RUNNER_ETC_DIR"/emqx.conf $EMQX_LICENSE_CONF_OPTION -d "$RUNNER_DATA_DIR"/configs generate)" + # shellcheck disable=SC2181 + RESULT=$? + set -e + if [ $RESULT -gt 0 ]; then + echo "$HOCON_OUTPUT" + exit $RESULT + fi + # print override from environment variables (EMQX_*) + echo "$HOCON_OUTPUT" | sed -e '$d' + CONFIG_ARGS=$(echo "$HOCON_OUTPUT" | tail -n 1) + + ## Merge hocon generated *.args into the vm.args + HOCON_GEN_ARG_FILE=$(echo "$CONFIG_ARGS" | sed -n 's/^.*\(vm_args[[:space:]]\)//p' | awk '{print $1}') + TMP_ARG_FILE="$RUNNER_DATA_DIR/configs/vm.args.tmp" + cp "$RUNNER_ETC_DIR/vm.args" "$TMP_ARG_FILE" + echo "" >> "$TMP_ARG_FILE" + echo "-pa ${REL_DIR}/consolidated" >> "$TMP_ARG_FILE" + sed '/^#/d' "$HOCON_GEN_ARG_FILE" | sed '/^$/d' | while IFS='' read -r ARG_LINE || [ -n "$ARG_LINE" ]; do + ARG_KEY=$(echo "$ARG_LINE" | awk '{$NF="";print}') + ARG_VALUE=$(echo "$ARG_LINE" | awk '{print $NF}') + TMP_ARG_VALUE=$(grep "^$ARG_KEY" "$TMP_ARG_FILE" | awk '{print $NF}') + if [ "$ARG_VALUE" != "$TMP_ARG_VALUE" ] ; then + if [ -n "$TMP_ARG_VALUE" ]; then + sh -c "$SED_REPLACE 's/^$ARG_KEY.*$/$ARG_LINE/' $TMP_ARG_FILE" + else + echo "$ARG_LINE" >> "$TMP_ARG_FILE" + fi + fi + done + mv -f "$TMP_ARG_FILE" "$HOCON_GEN_ARG_FILE" + # shellcheck disable=SC2086 if ! relx_nodetool chkconfig $CONFIG_ARGS; then echoerr "Error reading $CONFIG_ARGS" @@ -303,7 +292,8 @@ if [ -z "$NAME_ARG" ]; then NODENAME="$(grep -E '^-name' "$LATEST_VM_ARGS" | awk '{print $2}')" else # for boot commands, inspect emqx.conf for node name - NODENAME=$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/cuttlefish -i "$REL_DIR"/emqx.schema -c "$RUNNER_ETC_DIR"/emqx.conf get node.name) + # todo: use get command from hocon escript + NODENAME="$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/hocon -s emqx_schema -c "$RUNNER_ETC_DIR"/emqx.conf get node.name | tr -d \")" fi fi if [ -z "$NODENAME" ]; then @@ -329,7 +319,7 @@ PIPE_DIR="${PIPE_DIR:-/$RUNNER_DATA_DIR/${WHOAMI}_erl_pipes/$NAME/}" COOKIE="${EMQX_NODE_COOKIE:-}" if [ -z "$COOKIE" ]; then if [ "$IS_BOOT_COMMAND" = 'yes' ]; then - COOKIE=$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/cuttlefish -i "$REL_DIR"/emqx.schema -c "$RUNNER_ETC_DIR"/emqx.conf get node.cookie) + COOKIE="$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/hocon -s emqx_schema -c "$RUNNER_ETC_DIR"/emqx.conf get node.cookie | tr -d \")" else # shellcheck disable=SC2012,SC2086 LATEST_VM_ARGS="$(ls -t $RUNNER_DATA_DIR/configs/vm.*.args | head -1)" diff --git a/bin/nodetool b/bin/nodetool index 0e89ac278..c3bfe5b3c 100755 --- a/bin/nodetool +++ b/bin/nodetool @@ -63,7 +63,6 @@ main(Args) -> %% a "pong" io:format("pong\n"); ["stop"] -> - rpc:call(TargetNode, emqx_plugins, unload, [], 60000), io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]); ["restart", "-config", ConfigFile | _RestArgs1] -> io:format("~p\n", [rpc:call(TargetNode, emqx, restart, [ConfigFile], 60000)]); diff --git a/etc/BUILT_ON b/data/BUILT_ON similarity index 100% rename from etc/BUILT_ON rename to data/BUILT_ON diff --git a/deploy/charts/emqx/README.md b/deploy/charts/emqx/README.md index 1ba1e3266..535ccb8e1 100644 --- a/deploy/charts/emqx/README.md +++ b/deploy/charts/emqx/README.md @@ -37,6 +37,7 @@ The following table lists the configurable parameters of the emqx chart and thei | `image.repository` | EMQ X Image name |emqx/emqx| | `image.pullPolicy` | The image pull policy |IfNotPresent| | `image.pullSecrets ` | The image pull secrets |`[]` (does not add image pull secrets to deployed pods)| +| `recreatePods` | Forces the recreation of pods during upgrades, which can be useful to always apply the most recent configuration. | false | | `persistence.enabled` | Enable EMQX persistence using PVC |false| | `persistence.storageClass` | Storage class of backing PVC |`nil` (uses alpha storage class annotation)| | `persistence.existingClaim` | EMQ X data Persistent Volume existing claim name, evaluated as a template |""| diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 3a385732a..4cad21569 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -47,6 +47,10 @@ spec: version: {{ .Chart.AppVersion }} app.kubernetes.io/name: {{ include "emqx.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} + {{- if .Values.recreatePods }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum | quote }} + {{- end }} spec: volumes: - name: emqx-loaded-plugins diff --git a/deploy/charts/emqx/templates/rbac.yaml b/deploy/charts/emqx/templates/rbac.yaml index f1536836b..87cd18178 100644 --- a/deploy/charts/emqx/templates/rbac.yaml +++ b/deploy/charts/emqx/templates/rbac.yaml @@ -5,7 +5,11 @@ metadata: name: {{ include "emqx.fullname" . }} --- kind: Role +{{- if semverCompare ">=1.17-0" .Capabilities.KubeVersion.GitVersion }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- else }} apiVersion: rbac.authorization.k8s.io/v1beta1 +{{- end }} metadata: namespace: {{ .Release.Namespace }} name: {{ include "emqx.fullname" . }} @@ -20,7 +24,11 @@ rules: - list --- kind: RoleBinding +{{- if semverCompare ">=1.17-0" .Capabilities.KubeVersion.GitVersion }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- else }} apiVersion: rbac.authorization.k8s.io/v1beta1 +{{- end }} metadata: namespace: {{ .Release.Namespace }} name: {{ include "emqx.fullname" . }} diff --git a/deploy/charts/emqx/values.yaml b/deploy/charts/emqx/values.yaml index 34302e09a..0e6f24a66 100644 --- a/deploy/charts/emqx/values.yaml +++ b/deploy/charts/emqx/values.yaml @@ -14,6 +14,9 @@ image: # pullSecrets: # - myRegistryKeySecretName +## Forces the recreation of pods during helm upgrades. This can be useful to update configuration values even if the container image did not change. +recreatePods: false + persistence: enabled: false size: 20Mi diff --git a/lib-ce/emqx_dashboard/src/emqx_dashboard.app.src b/lib-ce/emqx_dashboard/src/emqx_dashboard.app.src index 92bd59a7a..1604198dc 100644 --- a/lib-ce/emqx_dashboard/src/emqx_dashboard.app.src +++ b/lib-ce/emqx_dashboard/src/emqx_dashboard.app.src @@ -1,6 +1,6 @@ {application, emqx_dashboard, [{description, "EMQ X Web Dashboard"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.4.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_dashboard_sup]}, {applications, [kernel,stdlib,mnesia,minirest]}, diff --git a/lib-ce/emqx_modules/src/emqx_mod_api_topic_metrics.erl b/lib-ce/emqx_modules/src/emqx_mod_api_topic_metrics.erl index 150dcb151..5ccef4c6b 100644 --- a/lib-ce/emqx_modules/src/emqx_mod_api_topic_metrics.erl +++ b/lib-ce/emqx_modules/src/emqx_mod_api_topic_metrics.erl @@ -53,6 +53,12 @@ , unregister/2 ]). +-export([ get_topic_metrics/2 + , register_topic_metrics/2 + , unregister_topic_metrics/2 + , unregister_all_topic_metrics/1 + ]). + list(#{topic := Topic0}, _Params) -> execute_when_enabled(fun() -> Topic = emqx_mgmt_util:urldecode(Topic0), diff --git a/lib-ce/emqx_modules/src/emqx_modules.app.src b/lib-ce/emqx_modules/src/emqx_modules.app.src index 576316703..702652fc2 100644 --- a/lib-ce/emqx_modules/src/emqx_modules.app.src +++ b/lib-ce/emqx_modules/src/emqx_modules.app.src @@ -1,6 +1,6 @@ {application, emqx_modules, [{description, "EMQ X Module Management"}, - {vsn, "4.3.1"}, + {vsn, "4.3.2"}, {modules, []}, {applications, [kernel,stdlib]}, {mod, {emqx_modules_app, []}}, diff --git a/lib-ce/emqx_modules/src/emqx_modules.appup.src b/lib-ce/emqx_modules/src/emqx_modules.appup.src index b44a65c17..aa997c453 100644 --- a/lib-ce/emqx_modules/src/emqx_modules.appup.src +++ b/lib-ce/emqx_modules/src/emqx_modules.appup.src @@ -1,14 +1,22 @@ %% -*-: erlang -*- {VSN, [ + {"4.3.1", [ + {load_module, emqx_mod_api_topic_metrics, brutal_purge, soft_purge, []} + ]}, {"4.3.0", [ - {update, emqx_mod_delayed, {advanced, []}} + {update, emqx_mod_delayed, {advanced, []}}, + {load_module, emqx_mod_api_topic_metrics, brutal_purge, soft_purge, []} ]}, {<<".*">>, []} ], [ + {"4.3.1", [ + {load_module, emqx_mod_api_topic_metrics, brutal_purge, soft_purge, []} + ]}, {"4.3.0", [ - {update, emqx_mod_delayed, {advanced, []}} + {update, emqx_mod_delayed, {advanced, []}}, + {load_module, emqx_mod_api_topic_metrics, brutal_purge, soft_purge, []} ]}, {<<".*">>, []} ] diff --git a/lib-ce/emqx_telemetry/src/emqx_telemetry.app.src b/lib-ce/emqx_telemetry/src/emqx_telemetry.app.src index c14d00f73..e65678c37 100644 --- a/lib-ce/emqx_telemetry/src/emqx_telemetry.app.src +++ b/lib-ce/emqx_telemetry/src/emqx_telemetry.app.src @@ -1,6 +1,6 @@ {application, emqx_telemetry, [{description, "EMQ X Telemetry"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "4.3.1"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_telemetry_sup]}, {applications, [kernel,stdlib]}, diff --git a/lib-ce/emqx_telemetry/src/emqx_telemetry.appup.src b/lib-ce/emqx_telemetry/src/emqx_telemetry.appup.src new file mode 100644 index 000000000..27998f0d5 --- /dev/null +++ b/lib-ce/emqx_telemetry/src/emqx_telemetry.appup.src @@ -0,0 +1,15 @@ +%% -*- mode: erlang -*- +{VSN, + [ + {"4.3.0", [ + {load_module, emqx_telemetry, brutal_purge, soft_purge, []} + ]}, + {<<".*">>, []} + ], + [ + {"4.3.0", [ + {load_module, emqx_telemetry, brutal_purge, soft_purge, []} + ]}, + {<<".*">>, []} + ] +}. diff --git a/pkg-vsn.sh b/pkg-vsn.sh index 9da5a1075..72bdb2850 100755 --- a/pkg-vsn.sh +++ b/pkg-vsn.sh @@ -12,8 +12,10 @@ else EDITION='opensource' fi -RELEASE="$(grep -E "define.+EMQX_RELEASE.+${EDITION}" include/emqx_release.hrl | cut -d '"' -f2)" +## emqx_release.hrl is the single source of truth for release version +RELEASE="$(grep -E "define.+EMQX_RELEASE.+${EDITION}" apps/emqx/include/emqx_release.hrl | cut -d '"' -f2)" +## git commit hash is added as suffix in case the git tag and release version is not an exact match if [ -d .git ] && ! git describe --tags --match "[e|v]${RELEASE}" --exact >/dev/null 2>&1; then SUFFIX="-$(git rev-parse HEAD | cut -b1-8)" fi diff --git a/priv/emqx.schema b/priv/emqx.schema deleted file mode 100644 index 37679cf59..000000000 --- a/priv/emqx.schema +++ /dev/null @@ -1,2513 +0,0 @@ -%%-*- mode: erlang -*- -%% EMQ X R4.0 config mapping - -%%-------------------------------------------------------------------- -%% Cluster -%%-------------------------------------------------------------------- - -%% @doc Cluster name -{mapping, "cluster.name", "ekka.cluster_name", [ - {default, emqxcl}, - {datatype, atom} -]}. - -%% @doc Cluster discovery -{mapping, "cluster.discovery", "ekka.cluster_discovery", [ - {default, manual}, - {datatype, atom} -]}. - -%% @doc Clean down node from the cluster -{mapping, "cluster.autoclean", "ekka.cluster_autoclean", [ - {datatype, {duration, ms}} -]}. - -%% @doc Cluster autoheal -{mapping, "cluster.autoheal", "ekka.cluster_autoheal", [ - {datatype, flag}, - {default, off} -]}. - -%%-------------------------------------------------------------------- -%% Cluster by static node list - -{mapping, "cluster.static.seeds", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Cluster by UDP Multicast - -{mapping, "cluster.mcast.addr", "ekka.cluster_discovery", [ - {default, "239.192.0.1"}, - {datatype, string} -]}. - -{mapping, "cluster.mcast.ports", "ekka.cluster_discovery", [ - {default, "4369"}, - {datatype, string} -]}. - -{mapping, "cluster.mcast.iface", "ekka.cluster_discovery", [ - {datatype, string}, - {default, "0.0.0.0"} -]}. - -{mapping, "cluster.mcast.ttl", "ekka.cluster_discovery", [ - {datatype, integer}, - {default, 255} -]}. - -{mapping, "cluster.mcast.loop", "ekka.cluster_discovery", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "cluster.mcast.sndbuf", "ekka.cluster_discovery", [ - {datatype, bytesize}, - {default, "16KB"} -]}. - -{mapping, "cluster.mcast.recbuf", "ekka.cluster_discovery", [ - {datatype, bytesize}, - {default, "16KB"} -]}. - -{mapping, "cluster.mcast.buffer", "ekka.cluster_discovery", [ - {datatype, bytesize}, - {default, "32KB"} -]}. - -%%-------------------------------------------------------------------- -%% Cluster by DNS A Record - -{mapping, "cluster.dns.name", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -%% @doc The erlang distributed protocol -{mapping, "cluster.proto_dist", "ekka.proto_dist", [ - {default, "inet_tcp"}, - {datatype, {enum, [inet_tcp, inet6_tcp, inet_tls]}}, - hidden -]}. - -{mapping, "cluster.dns.app", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Cluster using etcd - -{mapping, "cluster.etcd.server", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.etcd.version", "ekka.cluster_discovery", [ - {datatype, {enum, [v2, v3]}} -]}. - -{mapping, "cluster.etcd.prefix", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.etcd.node_ttl", "ekka.cluster_discovery", [ - {datatype, {duration, ms}}, - {default, "1m"} -]}. - -{mapping, "cluster.etcd.ssl.keyfile", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.etcd.ssl.certfile", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.etcd.ssl.cacertfile", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Cluster on K8s - -{mapping, "cluster.k8s.apiserver", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.k8s.service_name", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.k8s.address_type", "ekka.cluster_discovery", [ - {datatype, {enum, [ip, dns, hostname]}} -]}. - -{mapping, "cluster.k8s.app_name", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.k8s.namespace", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.k8s.suffix", "ekka.cluster_discovery", [ - {datatype, string}, - {default, ""} - ]}. - -{translation, "ekka.cluster_discovery", fun(Conf) -> - Strategy = cuttlefish:conf_get("cluster.discovery", Conf), - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - IpPort = fun(S) -> - [Addr, Port] = string:tokens(S, ":"), - {ok, Ip} = inet:parse_address(Addr), - {Ip, Port} - end, - Options = fun(static) -> - [{seeds, [list_to_atom(S) || S <- string:tokens(cuttlefish:conf_get("cluster.static.seeds", Conf, ""), ",")]}]; - (mcast) -> - {ok, Addr} = inet:parse_address(cuttlefish:conf_get("cluster.mcast.addr", Conf)), - {ok, Iface} = inet:parse_address(cuttlefish:conf_get("cluster.mcast.iface", Conf)), - Ports = [list_to_integer(S) || S <- string:tokens(cuttlefish:conf_get("cluster.mcast.ports", Conf), ",")], - [{addr, Addr}, {ports, Ports}, {iface, Iface}, - {ttl, cuttlefish:conf_get("cluster.mcast.ttl", Conf, 1)}, - {loop, cuttlefish:conf_get("cluster.mcast.loop", Conf, true)}]; - (dns) -> - [{name, cuttlefish:conf_get("cluster.dns.name", Conf)}, - {app, cuttlefish:conf_get("cluster.dns.app", Conf)}]; - (etcd) -> - SslOpts = fun(Conf) -> - Options = cuttlefish_variable:filter_by_prefix("cluster.etcd.ssl", Conf), - lists:map(fun({["cluster", "etcd", "ssl", Name], Value}) -> - {list_to_atom(Name), Value} - end, Options) - end, - [{server, string:tokens(cuttlefish:conf_get("cluster.etcd.server", Conf), ",")}, - {version, cuttlefish:conf_get("cluster.etcd.version", Conf, v3)}, - {prefix, cuttlefish:conf_get("cluster.etcd.prefix", Conf, "emqcl")}, - {node_ttl, cuttlefish:conf_get("cluster.etcd.node_ttl", Conf, 60)}, - {ssl_options, SslOpts(Conf)}]; - (k8s) -> - [{apiserver, cuttlefish:conf_get("cluster.k8s.apiserver", Conf)}, - {service_name, cuttlefish:conf_get("cluster.k8s.service_name", Conf)}, - {address_type, cuttlefish:conf_get("cluster.k8s.address_type", Conf, ip)}, - {app_name, cuttlefish:conf_get("cluster.k8s.app_name", Conf)}, - {namespace, cuttlefish:conf_get("cluster.k8s.namespace", Conf)}, - {suffix, cuttlefish:conf_get("cluster.k8s.suffix", Conf, "")}]; - (manual) -> - [ ] - end, - {Strategy, Filter(Options(Strategy))} -end}. - -%%-------------------------------------------------------------------- -%% Node -%%-------------------------------------------------------------------- - -%% @doc Node name -{mapping, "node.name", "vm_args.-name", [ - {default, "emqx@127.0.0.1"}, - {override_env, "NODE_NAME"} -]}. - -%% @doc Specify SSL Options in the file if using SSL for erlang distribution -{mapping, "node.ssl_dist_optfile", "vm_args.-ssl_dist_optfile", [ - {datatype, string}, - hidden -]}. - -%% @doc Secret cookie for distributed erlang node -{mapping, "node.cookie", "vm_args.-setcookie", [ - {default, "emqxsecretcookie"}, - {override_env, "NODE_COOKIE"} -]}. - -{mapping, "node.data_dir", "emqx.data_dir", [ - {datatype, string} -]}. - -%% @doc http://erlang.org/doc/man/heart.html -{mapping, "node.heartbeat", "vm_args.-heart", [ - {datatype, flag}, - hidden -]}. - -{translation, "vm_args.-heart", fun(Conf) -> - case cuttlefish:conf_get("node.heartbeat", Conf) of - true -> ""; - false -> cuttlefish:invalid("should be 'on' or comment the line!") - end -end}. - -%% @doc More information at: http://erlang.org/doc/man/erl.html -{mapping, "node.async_threads", "vm_args.+A", [ - {datatype, integer}, - {validators, ["range:0-1024"]} -]}. - -%% @doc Erlang Process Limit -{mapping, "node.process_limit", "vm_args.+P", [ - {datatype, integer}, - hidden -]}. - -%% @doc The maximum number of concurrent ports/sockets. -%% Valid range is 1024-134217727 -{mapping, "node.max_ports", "vm_args.+Q", [ - {datatype, integer}, - {validators, ["range4ports"]}, - {override_env, "MAX_PORTS"} -]}. - -{validator, "range4ports", "must be 1024 to 134217727", - fun(X) -> X >= 1024 andalso X =< 134217727 end}. - -%% @doc http://www.erlang.org/doc/man/erl.html#%2bzdbbl -{mapping, "node.dist_buffer_size", "vm_args.+zdbbl", [ - {datatype, bytesize}, - {commented, "32MB"}, - hidden, - {validators, ["zdbbl_range"]} -]}. - -{translation, "vm_args.+zdbbl", - fun(Conf) -> - ZDBBL = cuttlefish:conf_get("node.dist_buffer_size", Conf, undefined), - case ZDBBL of - undefined -> undefined; - X when is_integer(X) -> cuttlefish_util:ceiling(X / 1024); %% Bytes to Kilobytes; - _ -> undefined - end - end}. - -{validator, "zdbbl_range", "must be between 1KB and 2097151KB", - fun(ZDBBL) -> - %% 2097151KB = 2147482624 - ZDBBL >= 1024 andalso ZDBBL =< 2147482624 - end -}. - -%% @doc Global GC Interval -{mapping, "node.global_gc_interval", "emqx.global_gc_interval", [ - {datatype, {duration, s}} -]}. - -%% @doc http://www.erlang.org/doc/man/erlang.html#system_flag-2 -{mapping, "node.fullsweep_after", "vm_args.-env ERL_FULLSWEEP_AFTER", [ - {default, 1000}, - {datatype, integer}, - hidden, - {validators, ["positive_integer"]} -]}. - -{validator, "positive_integer", "must be a positive integer", - fun(X) -> X >= 0 end}. - -%% Note: OTP R15 and earlier uses -env ERL_MAX_ETS_TABLES, -%% R16+ uses +e -%% @doc The ETS table limit -{mapping, "node.max_ets_tables", - cuttlefish:otp("R16", "vm_args.+e", "vm_args.-env ERL_MAX_ETS_TABLES"), [ - {default, 256000}, - {datatype, integer}, - hidden -]}. - -%% @doc Set the location of crash dumps -{mapping, "node.crash_dump", "vm_args.-env ERL_CRASH_DUMP", [ - {default, "{{crash_dump}}"}, - {datatype, file}, - hidden -]}. - -%% @doc http://www.erlang.org/doc/man/kernel_app.html#net_ticktime -{mapping, "node.dist_net_ticktime", "vm_args.-kernel net_ticktime", [ - {datatype, integer}, - hidden -]}. - -%% @doc http://www.erlang.org/doc/man/kernel_app.html -{mapping, "node.dist_listen_min", "kernel.inet_dist_listen_min", [ - {commented, 6369}, - {datatype, integer}, - hidden -]}. - -%% @see node.dist_listen_min -{mapping, "node.dist_listen_max", "kernel.inet_dist_listen_max", [ - {commented, 6369}, - {datatype, integer}, - hidden -]}. - -{mapping, "node.backtrace_depth", "emqx.backtrace_depth", [ - {default, 16}, - {datatype, integer} -]}. - -%%-------------------------------------------------------------------- -%% RPC -%%-------------------------------------------------------------------- - -%% RPC Mode. -{mapping, "rpc.mode", "emqx.rpc_mode", [ - {default, async}, - {datatype, {enum, [sync, async]}} -]}. - -{mapping, "rpc.async_batch_size", "gen_rpc.max_batch_size", [ - {default, 256}, - {datatype, integer} -]}. - -{mapping, "rpc.port_discovery", "gen_rpc.port_discovery", [ - {default, stateless}, - {datatype, {enum, [manual, stateless]}} -]}. - -%% RPC server port. -{mapping, "rpc.tcp_server_port", "gen_rpc.tcp_server_port", [ - {default, 5369}, - {datatype, integer} -]}. - -%% Number of tcp connections when connecting to RPC server -{mapping, "rpc.tcp_client_num", "gen_rpc.tcp_client_num", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:gt_0_lt_256"]} -]}. - -{translation, "gen_rpc.tcp_client_num", fun(Conf) -> - case cuttlefish:conf_get("rpc.tcp_client_num", Conf) of - 0 -> max(1, erlang:system_info(schedulers) div 2); - V -> V - end -end}. - -%% Client connect timeout -{mapping, "rpc.connect_timeout", "gen_rpc.connect_timeout", [ - {default, "5s"}, - {datatype, {duration, ms}} -]}. - -%% Client and Server send timeout -{mapping, "rpc.send_timeout", "gen_rpc.send_timeout", [ - {default, 5000}, - {datatype, {duration, ms}} -]}. - -%% Authentication timeout -{mapping, "rpc.authentication_timeout", "gen_rpc.authentication_timeout", [ - {default, 5000}, - {datatype, {duration, ms}} -]}. - -%% Default receive timeout for call() functions -{mapping, "rpc.call_receive_timeout", "gen_rpc.call_receive_timeout", [ - {default, 15000}, - {datatype, {duration, ms}} -]}. - -%% Socket keepalive configuration -{mapping, "rpc.socket_keepalive_idle", "gen_rpc.socket_keepalive_idle", [ - {default, 7200}, - {datatype, {duration, s}} -]}. - -%% Seconds between probes -{mapping, "rpc.socket_keepalive_interval", "gen_rpc.socket_keepalive_interval", [ - {default, 75}, - {datatype, {duration, s}} -]}. - -%% Probes lost to close the connection -{mapping, "rpc.socket_keepalive_count", "gen_rpc.socket_keepalive_count", [ - {default, 9}, - {datatype, integer} -]}. - -%% Size of TCP send buffer -{mapping, "rpc.socket_sndbuf", "gen_rpc.socket_sndbuf", [ - {default, "1MB"}, - {datatype, bytesize} -]}. - -%% Size of TCP receive buffer -{mapping, "rpc.socket_recbuf", "gen_rpc.socket_recbuf", [ - {default, "1MB"}, - {datatype, bytesize} -]}. - -%% Size of TCP receive buffer -{mapping, "rpc.socket_buffer", "gen_rpc.socket_buffer", [ - {default, "1MB"}, - {datatype, bytesize} -]}. - -{validator, "range:gt_0_lt_256", "must greater than 0 and less than 256", - fun(X) -> X >= 0 andalso X < 256 end -}. - -%% Force client to use server listening port, because we do no provide -%% per-node listening port manual mapping from configs. -%% i.e. all nodes in the cluster should agree to the same -%% listening port number. -{translation, "gen_rpc.tcp_client_port", fun(_, _, Conf) -> - cuttlefish:conf_get("rpc.tcp_server_port", Conf) -end}. - -%%-------------------------------------------------------------------- -%% Log -%%-------------------------------------------------------------------- - -{mapping, "log.to", "kernel.logger", [ - {default, file}, - {datatype, {enum, [file, console, both]}} -]}. - -{mapping, "log.level", "kernel.logger", [ - {default, warning}, - {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, all]}} -]}. - -%% @doc Timezone offset to display in logs, -%% "system" use system time zone -%% "utc" for Universal Coordinated Time (UTC) -%% "+hh:mm" or "-hh:mm" for a specified offset -{mapping, "log.time_offset", "kernel.logger", [ - {default, "system"}, - {datatype, string} -]}. - -{mapping, "log.primary_log_level", "kernel.logger_level", [ - {default, warning}, - {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, all]}} -]}. - -{mapping, "log.dir", "kernel.logger", [ - {default, "log"}, - {datatype, string} -]}. - -{mapping, "log.file", "kernel.logger", [ - {default, "emqx.log"}, - {datatype, file} -]}. - -{mapping, "log.chars_limit", "kernel.logger", [ - {default, -1}, - {datatype, integer} -]}. - -{mapping, "log.supervisor_reports", "kernel.logger", [ - {default, error}, - {datatype, {enum, [error, progress]}}, - hidden -]}. - -%% @doc Maximum depth in Erlang term log formatting -%% and message queue inspection. -{mapping, "log.max_depth", "kernel.error_logger_format_depth", [ - {default, 20}, - {datatype, [{enum, [unlimited]}, integer]} -]}. - -%% @doc format logs as JSON objects -{mapping, "log.formatter", "kernel.logger", [ - {default, text}, - {datatype, {enum, [text, json]}} -]}. - -%% @doc format logs in a single line. -{mapping, "log.single_line", "kernel.logger", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "log.rotation.enable", "kernel.logger", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "log.rotation.size", "kernel.logger", [ - {default, "10MB"}, - {datatype, bytesize} -]}. - -{mapping, "log.size", "kernel.logger", [ - {default, infinity}, - {datatype, [bytesize, atom]} -]}. - -{mapping, "log.rotation.count", "kernel.logger", [ - {default, 5}, - {datatype, integer} -]}. - -{mapping, "log.$level.file", "kernel.logger", [ - {datatype, file} -]}. - -{mapping, "log.sync_mode_qlen", "kernel.logger", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "log.drop_mode_qlen", "kernel.logger", [ - {default, 3000}, - {datatype, integer} -]}. - -{mapping, "log.flush_qlen", "kernel.logger", [ - {default, 8000}, - {datatype, integer} -]}. - -{mapping, "log.overload_kill", "kernel.logger", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "log.overload_kill_mem_size", "kernel.logger", [ - {default, "30MB"}, - {datatype, bytesize} -]}. - -{mapping, "log.overload_kill_qlen", "kernel.logger", [ - {default, 20000}, - {datatype, integer} -]}. - -{mapping, "log.overload_kill_restart_after", "kernel.logger", [ - {default, "5s"}, - {datatype, [{duration, ms}, atom]} -]}. - -{mapping, "log.burst_limit", "kernel.logger", [ - {default, "disabled"}, - {datatype, string} -]}. - -{mapping, "log.error_logger", "kernel.error_logger", [ - {default, silent}, - {datatype, {enum, [silent]}}, - hidden -]}. - -%% disable lager -{mapping, "lager.handlers", "lager.handlers", [ - {default, []}, - hidden -]}. -{mapping, "lager.crash_log", "lager.crash_log", [ - {default, off}, - {datatype, flag}, - hidden -]}. - -{translation, "kernel.logger_level", fun(_, _, Conf) -> - cuttlefish:conf_get("log.level", Conf) -end}. - -{translation, "kernel.logger", fun(Conf) -> - LogTo = cuttlefish:conf_get("log.to", Conf), - LogLevel = cuttlefish:conf_get("log.level", Conf), - LogTimeoffset = - case cuttlefish:conf_get("log.time_offset", Conf) of - "system" -> ""; - "utc" -> "0"; - [S, H1, H2, $:, M1, M2] = HHMM -> - (S =:= $+ orelse S =:= $-) andalso - try - begin - H = list_to_integer([H1, H2]), - M = list_to_integer([M1, M2]), - H >=0 andalso H =< 14 andalso - M >= 0 andalso M =< 59 - end - catch - _ : _ -> - error({"invalid_log_time_offset", HHMM}) - end andalso HHMM; - Other -> - error({"invalid_log_time_offset", Other}) - end, - LogType = case cuttlefish:conf_get("log.rotation.enable", Conf) of - true -> wrap; - false -> halt - end, - CharsLimit = case cuttlefish:conf_get("log.chars_limit", Conf) of - -1 -> unlimited; - V -> V - end, - SingleLine = cuttlefish:conf_get("log.single_line", Conf), - FmtName = cuttlefish:conf_get("log.formatter", Conf), - Formatter = - case FmtName of - json -> - {emqx_logger_jsonfmt, - #{chars_limit => CharsLimit, - single_line => SingleLine - }}; - text -> - {emqx_logger_textfmt, - #{template => - [time," [",level,"] ", - {clientid, - [{peername, - [clientid,"@",peername," "], - [clientid, " "]}], - [{peername, - [peername," "], - []}]}, - msg,"\n"], - chars_limit => CharsLimit, - single_line => SingleLine - }} - end, - {BustLimitOn, {MaxBurstCount, TimeWindow}} = - case string:tokens(cuttlefish:conf_get("log.burst_limit", Conf), ", ") of - ["disabled"] -> {false, {20000, 1000}}; - [Count, Window] -> - {true, {list_to_integer(Count), - case cuttlefish_duration:parse(Window, ms) of - Secs when is_integer(Secs) -> Secs; - {error, Reason1} -> error(Reason1) - end}} - end, - FileConf = fun(Filename) -> - BasicConf = - #{type => LogType, - file => filename:join(cuttlefish:conf_get("log.dir", Conf), Filename), - max_no_files => cuttlefish:conf_get("log.rotation.count", Conf), - sync_mode_qlen => cuttlefish:conf_get("log.sync_mode_qlen", Conf), - drop_mode_qlen => cuttlefish:conf_get("log.drop_mode_qlen", Conf), - flush_qlen => cuttlefish:conf_get("log.flush_qlen", Conf), - overload_kill_enable => cuttlefish:conf_get("log.overload_kill", Conf), - overload_kill_qlen => cuttlefish:conf_get("log.overload_kill_qlen", Conf), - overload_kill_mem_size => cuttlefish:conf_get("log.overload_kill_mem_size", Conf), - overload_kill_restart_after => cuttlefish:conf_get("log.overload_kill_restart_after", Conf), - burst_limit_enable => BustLimitOn, - burst_limit_max_count => MaxBurstCount, - burst_limit_window_time => TimeWindow - }, - MaxNoBytes = case LogType of - wrap -> cuttlefish:conf_get("log.rotation.size", Conf); - halt -> cuttlefish:conf_get("log.size", Conf) - end, - BasicConf#{max_no_bytes => MaxNoBytes} - end, - - Filters = case cuttlefish:conf_get("log.supervisor_reports", Conf) of - error -> [{drop_progress_reports, {fun logger_filters:progress/2, stop}}]; - progress -> [] - end, - - %% For the default logger that outputs to console - DefaultHandler = - if LogTo =:= console orelse LogTo =:= both -> - [{handler, console, logger_std_h, - #{level => LogLevel, - config => #{type => standard_io}, - formatter => Formatter, - filters => Filters - } - }]; - true -> - [{handler, default, undefined}] - end, - - %% For the file logger - FileHandler = - if LogTo =:= file orelse LogTo =:= both -> - [{handler, file, logger_disk_log_h, - #{level => LogLevel, - config => FileConf(cuttlefish:conf_get("log.file", Conf)), - formatter => Formatter, - filesync_repeat_interval => no_repeat, - filters => Filters - }}]; - true -> [] - end, - - %% For creating additional log files for specific log levels. - AdditionalLogFiles = - lists:foldl( - fun({[_, Level, _] = K, Filename}, Acc) when LogTo =:= file; LogTo =:= both -> - case cuttlefish_variable:is_fuzzy_match(K, ["log", "$level", "file"]) of - true -> [{Level, Filename} | Acc]; - false -> Acc - end; - ({_K, _V}, Acc) -> - Acc - end, [], Conf), - AdditionalHandlers = - [{handler, list_to_atom("file_for_"++Level), logger_disk_log_h, - #{level => list_to_atom(Level), - config => FileConf(Filename), - formatter => Formatter, - filesync_repeat_interval => no_repeat}} - || {Level, Filename} <- AdditionalLogFiles], - - DefaultHandler ++ FileHandler ++ AdditionalHandlers -end}. - -%%-------------------------------------------------------------------- -%% Authentication/ACL -%%-------------------------------------------------------------------- - -%% @doc Allow anonymous authentication. -{mapping, "allow_anonymous", "emqx.allow_anonymous", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc ACL nomatch. -{mapping, "acl_nomatch", "emqx.acl_nomatch", [ - {default, deny}, - {datatype, {enum, [allow, deny]}} -]}. - -%% @doc Default ACL file. -{mapping, "acl_file", "emqx.acl_file", [ - {datatype, string}, - hidden -]}. - -%% @doc Enable ACL cache for publish. -{mapping, "enable_acl_cache", "emqx.enable_acl_cache", [ - {default, on}, - {datatype, flag} -]}. - -%% @doc ACL cache time-to-live. -{mapping, "acl_cache_ttl", "emqx.acl_cache_ttl", [ - {default, "1m"}, - {datatype, {duration, ms}} -]}. - -%% @doc ACL cache size. -{mapping, "acl_cache_max_size", "emqx.acl_cache_max_size", [ - {default, 32}, - {datatype, integer}, - {validators, ["range:gt_0"]} -]}. - -%% @doc Action when acl check reject current operation -{mapping, "acl_deny_action", "emqx.acl_deny_action", [ - {default, ignore}, - {datatype, {enum, [ignore, disconnect]}} -]}. - -%% @doc Flapping detect policy -{mapping, "flapping_detect_policy", "emqx.flapping_detect_policy", [ - {datatype, string}, - {default, "30,1m,5m"} -]}. - -{translation, "emqx.flapping_detect_policy", fun(Conf) -> - Policy = cuttlefish:conf_get("flapping_detect_policy", Conf), - [Threshold, Duration, Interval] = string:tokens(Policy, ", "), - ParseDuration = fun(S, Dur) -> - case cuttlefish_duration:parse(S, Dur) of - I when is_integer(I) -> I; - {error, Reason} -> error(Reason) - end - end, - #{threshold => list_to_integer(Threshold), - duration => ParseDuration(Duration, ms), - banned_interval => ParseDuration(Interval, s) - } -end}. - -{validator, "range:gt_0", "must greater than 0", - fun(X) -> X > 0 end -}. - -%%-------------------------------------------------------------------- -%% MQTT Protocol -%%-------------------------------------------------------------------- - -%% @doc Max Packet Size Allowed, 1MB by default. -{mapping, "mqtt.max_packet_size", "emqx.max_packet_size", [ - {default, "1MB"}, - {datatype, bytesize}, - {override_env, "MAX_PACKET_SIZE"} -]}. - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "mqtt.max_clientid_len", "emqx.max_clientid_len", [ - {default, 65535}, - {datatype, integer} -]}. - -%% @doc Set the Maximum topic levels. -{mapping, "mqtt.max_topic_levels", "emqx.max_topic_levels", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Set the Maximum QoS allowed. -{mapping, "mqtt.max_qos_allowed", "emqx.max_qos_allowed", [ - {default, 2}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -%% @doc Set the Maximum Topic Alias. -{mapping, "mqtt.max_topic_alias", "emqx.max_topic_alias", [ - {default, 65535}, - {datatype, integer} -]}. - -%% @doc Whether the server supports MQTT retained messages. -{mapping, "mqtt.retain_available", "emqx.retain_available", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports MQTT Wildcard Subscriptions. -{mapping, "mqtt.wildcard_subscription", "emqx.wildcard_subscription", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports MQTT Shared Subscriptions. -{mapping, "mqtt.shared_subscription", "emqx.shared_subscription", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether to ignore loop delivery of messages.(for mqtt v3.1.1) -{mapping, "mqtt.ignore_loop_deliver", "emqx.ignore_loop_deliver", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether to parse the MQTT frame in strict mode -{mapping, "mqtt.strict_mode", "emqx.strict_mode", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Specify the response information returned to the client -{mapping, "mqtt.response_information", "emqx.response_information", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Zones -%%-------------------------------------------------------------------- - -%% @doc Idle timeout of the MQTT connection. -{mapping, "zone.$name.idle_timeout", "emqx.zones", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - -{mapping, "zone.$name.allow_anonymous", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -{mapping, "zone.$name.acl_nomatch", "emqx.zones", [ - {datatype, {enum, [allow, deny]}} -]}. - -%% @doc Enable ACL check. -{mapping, "zone.$name.enable_acl", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Action when acl check reject current operation -{mapping, "zone.$name.acl_deny_action", "emqx.zones", [ - {default, ignore}, - {datatype, {enum, [ignore, disconnect]}} -]}. - -%% @doc Enable Ban. -{mapping, "zone.$name.enable_ban", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Enable per connection statistics. -{mapping, "zone.$name.enable_stats", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Publish limit of the MQTT connections. -{mapping, "zone.$name.publish_limit", "emqx.zones", [ - {datatype, string} -]}. - -%% @doc Max Packet Size Allowed, 64K by default. -{mapping, "zone.$name.max_packet_size", "emqx.zones", [ - {datatype, bytesize} -]}. - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "zone.$name.max_clientid_len", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Set the Maximum topic levels. -{mapping, "zone.$name.max_topic_levels", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Set the Maximum QoS allowed. -{mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -%% @doc Set the Maximum topic alias. -{mapping, "zone.$name.max_topic_alias", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Whether the server supports retained messages. -{mapping, "zone.$name.retain_available", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports Wildcard Subscriptions. -{mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports Shared Subscriptions. -{mapping, "zone.$name.shared_subscription", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Server Keepalive -{mapping, "zone.$name.server_keepalive", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Keepalive backoff -{mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ - {default, 0.75}, - {datatype, float} -]}. - -%% @doc Max Number of Subscriptions Allowed. -{mapping, "zone.$name.max_subscriptions", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Upgrade QoS according to subscription? -{mapping, "zone.$name.upgrade_qos", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. -%% 0 is equivalent to maximum allowed -{mapping, "zone.$name.max_inflight", "emqx.zones", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:1-65535"]} -]}. - -%% @doc Retry interval for redelivering QoS1/2 messages. -{mapping, "zone.$name.retry_interval", "emqx.zones", [ - {default, "30s"}, - {datatype, {duration, s}} -]}. - -%% @doc Max Packets that Awaiting PUBREL, 0 means no limit -{mapping, "zone.$name.max_awaiting_rel", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Awaiting PUBREL timeout -{mapping, "zone.$name.await_rel_timeout", "emqx.zones", [ - {default, "300s"}, - {datatype, {duration, s}} -]}. - -%% @doc Ignore loop delivery of messages -{mapping, "zone.$name.ignore_loop_deliver", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Session Expiry Interval -{mapping, "zone.$name.session_expiry_interval", "emqx.zones", [ - {default, "2h"}, - {datatype, {duration, s}} -]}. - -%% @doc Max queue length. Enqueued messages when persistent client -%% disconnected, or inflight window is full. 0 means no limit. -{mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ - {default, 1000}, - {datatype, integer} -]}. - -%% @doc Topic Priorities, comma separated topic=priority pairs, -%% where priority should be integer in range 1-255 (inclusive) -%% 1 being the lowest and 255 being the highest. -%% default value `none` to indicate no priority table, hence all -%% messages are treated equal, which means either highest ('infinity'), -%% or lowest (0) depending on mqueue_default_priority config. -{mapping, "zone.$name.mqueue_priorities", "emqx.zones", [ - {default, "none"}, - {datatype, string} -]}. - -%% @doc Default priority for topics not in priority table. -{mapping, "zone.$name.mqueue_default_priority", "emqx.zones", [ - {default, lowest}, - {datatype, {enum, [highest, lowest]}} -]}. - -%% @doc Queue Qos0 messages? -{mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "zone.$name.enable_flapping_detect", "emqx.zones", [ - {datatype, flag}, - {default, off} -]}. - -{mapping, "zone.$name.rate_limit.conn_messages_in", "emqx.zones", [ - {datatype, string} -]}. - -{mapping, "zone.$name.rate_limit.conn_bytes_in", "emqx.zones", [ - {datatype, string} -]}. - -{mapping, "zone.$name.conn_congestion.alarm", "emqx.zones", [ - {datatype, flag}, - {default, off} -]}. - -{mapping, "zone.$name.conn_congestion.min_alarm_sustain_duration", "emqx.zones", [ - {default, "1m"}, - {datatype, {duration, ms}} -]}. - -{mapping, "zone.$name.quota.conn_messages_routing", "emqx.zones", [ - {datatype, string} -]}. - -{mapping, "zone.$name.quota.overall_messages_routing", "emqx.zones", [ - {datatype, string} -]}. - -%% @doc Force connection/session process GC after this number of -%% messages | bytes passed through. -%% Numbers delimited by `|'. Zero or negative is to disable. -{mapping, "zone.$name.force_gc_policy", "emqx.zones", [ - {datatype, string} - ]}. - -%% @doc Max message queue length and total heap size to force shutdown -%% connection/session process. -%% Message queue here is the Erlang process mailbox, but not the number -%% of queued MQTT messages of QoS 1 and 2. -%% Zero or negative is to disable. -{mapping, "zone.$name.force_shutdown_policy", "emqx.zones", [ - {default, "default"}, - {datatype, string} -]}. - -{mapping, "zone.$name.mountpoint", "emqx.zones", [ - {datatype, string} -]}. - -%% @doc Use username replace client id -{mapping, "zone.$name.use_username_as_clientid", "emqx.zones", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether to parse the MQTT frame in strict mode -{mapping, "zone.$name.strict_mode", "emqx.zones", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Specify the response information returned to the client -{mapping, "zone.$name.response_information", "emqx.zones", [ - {datatype, string} -]}. - -%% @doc Whether to bypass the authentication step -{mapping, "zone.$name.bypass_auth_plugins", "emqx.zones", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.zones", fun(Conf) -> - Ratelimit = fun(Val) -> - [L, D] = string:tokens(Val, ", "), - Limit = case cuttlefish_bytesize:parse(L) of - Sz when is_integer(Sz) -> Sz; - {error, Reason1} -> error(Reason1) - end, - Duration = case cuttlefish_duration:parse(D, s) of - Secs when is_integer(Secs) -> Secs; - {error, Reason} -> error(Reason) - end, - {Limit, Duration} - end, - Mapping = fun(["publish_limit"], Val) -> - %% XXX: Deprecated at v4.2 - {publish_limit, Ratelimit(Val)}; - (["force_gc_policy"], Val) -> - [Count, Bytes] = string:tokens(Val, "| "), - GcPolicy = case cuttlefish_bytesize:parse(Bytes) of - {error, Reason} -> - error(Reason); - Bytes1 -> - #{bytes => Bytes1, - count => list_to_integer(Count)} - end, - {force_gc_policy, GcPolicy}; - (["force_shutdown_policy"], "default") -> - {DefaultLen, DefaultSize} = - case WordSize = erlang:system_info(wordsize) of - 8 -> % arch_64 - {10000, cuttlefish_bytesize:parse("64MB")}; - 4 -> % arch_32 - {1000, cuttlefish_bytesize:parse("32MB")} - end, - {force_shutdown_policy, #{message_queue_len => DefaultLen, - max_heap_size => DefaultSize div WordSize - }}; - (["force_shutdown_policy"], Val) -> - [Len, Siz] = string:tokens(Val, "| "), - MaxSiz = case WordSize = erlang:system_info(wordsize) of - 8 -> % arch_64 - (1 bsl 59) - 1; - 4 -> % arch_32 - (1 bsl 27) - 1 - end, - ShutdownPolicy = - case cuttlefish_bytesize:parse(Siz) of - {error, Reason} -> - error(Reason); - Siz1 when Siz1 > MaxSiz -> - cuttlefish:invalid(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); - Siz1 -> - #{message_queue_len => list_to_integer(Len), - max_heap_size => Siz1 div WordSize} - end, - {force_shutdown_policy, ShutdownPolicy}; - (["mqueue_priorities"], Val) -> - case Val of - "none" -> {mqueue_priorities, none}; % NO_PRIORITY_TABLE - _ -> - MqueuePriorities = lists:foldl(fun(T, Acc) -> - %% NOTE: space in "= " is intended - [Topic, Prio] = string:tokens(T, "= "), - P = list_to_integer(Prio), - (P < 0 orelse P > 255) andalso error({bad_priority, Topic, Prio}), - maps:put(iolist_to_binary(Topic), P, Acc) - end, #{}, string:tokens(Val, ",")), - {mqueue_priorities, MqueuePriorities} - end; - (["mountpoint"], Val) -> - {mountpoint, iolist_to_binary(Val)}; - (["response_information"], Val) -> - {response_information, iolist_to_binary(Val)}; - (["rate_limit", "conn_messages_in"], Val) -> - {ratelimit, {conn_messages_in, Ratelimit(Val)}}; - (["rate_limit", "conn_bytes_in"], Val) -> - {ratelimit, {conn_bytes_in, Ratelimit(Val)}}; - (["conn_congestion", "alarm"], Val) -> - {conn_congestion_alarm_enabled, Val}; - (["conn_congestion", "min_alarm_sustain_duration"], Val) -> - {conn_congestion_min_alarm_sustain_duration, Val}; - (["quota", "conn_messages_routing"], Val) -> - {quota, {conn_messages_routing, Ratelimit(Val)}}; - (["quota", "overall_messages_routing"], Val) -> - {quota, {overall_messages_routing, Ratelimit(Val)}}; - ([Opt], Val) -> - {list_to_atom(Opt), Val} - end, - maps:to_list( - lists:foldl( - fun({["zone", Name | Opt], Val}, Zones) -> - NVal = Mapping(Opt, Val), - maps:update_with(list_to_atom(Name), - fun(Opts) -> - case NVal of - {Key, Rl} when Key == ratelimit; - Key == quota -> - Rls = proplists:get_value(Key, Opts, []), - lists:keystore(Key, 1, Opts, {Key, [Rl|Rls]}); - _ -> - [NVal|Opts] - end - end, [NVal], Zones) - end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) -end}. - -%%-------------------------------------------------------------------- -%% Listeners -%%-------------------------------------------------------------------- - -%%-------------------------------------------------------------------- -%% TCP Listeners - -{mapping, "listener.tcp.$name.endpoint", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.tcp.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.tcp.$name.max_connections", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.tcp.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - -{mapping, "listener.tcp.$name.active_n", "emqx.listeners", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "listener.tcp.$name.zone", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.tcp.$name.rate_limit", "emqx.listeners", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "listener.tcp.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.tcp.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.tcp.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -%% The proxy-protocol protocol can get the certificate CN through tcp -{mapping, "listener.tcp.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn]}} -]}. - -%% The proxy-protocol protocol can get the certificate CN through tcp -{mapping, "listener.tcp.$name.peer_cert_as_clientid", "emqx.listeners", [ - {datatype, {enum, [cn]}} -]}. - -{mapping, "listener.tcp.$name.backlog", "emqx.listeners", [ - {datatype, integer}, - {default, 1024} -]}. - -{mapping, "listener.tcp.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.tcp.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.tcp.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.tcp.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.tcp.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.tcp.$name.high_watermark", "emqx.listeners", [ - {datatype, bytesize}, - {default, "1MB"} - ]}. - -{mapping, "listener.tcp.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.tcp.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.tcp.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -%%-------------------------------------------------------------------- -%% SSL Listeners - -{mapping, "listener.ssl.$name.endpoint", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.ssl.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.max_connections", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.active_n", "emqx.listeners", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.zone", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.rate_limit", "emqx.listeners", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ssl.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -{mapping, "listener.ssl.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.ssl.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.ssl.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ssl.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ssl.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ssl.$name.high_watermark", "emqx.listeners", [ - {datatype, bytesize}, - {default, "1MB"} - ]}. - -{mapping, "listener.ssl.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.ssl.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.ssl.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.ssl.$name.tls_versions", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.ciphers", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.psk_ciphers", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.handshake_timeout", "emqx.listeners", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - -{mapping, "listener.ssl.$name.depth", "emqx.listeners", [ - {default, 10}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.key_password", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.dhfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.keyfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.certfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.cacertfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.verify", "emqx.listeners", [ - {datatype, atom} -]}. - -{mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqx.listeners", [ - {datatype, {enum, [true, false]}} -]}. - -{mapping, "listener.ssl.$name.secure_renegotiate", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ssl.$name.reuse_sessions", "emqx.listeners", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "listener.ssl.$name.honor_cipher_order", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ssl.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn, crt, pem, md5]}} -]}. - -{mapping, "listener.ssl.$name.peer_cert_as_clientid", "emqx.listeners", [ - {datatype, {enum, [cn, dn, crt, pem, md5]}} -]}. - -%%-------------------------------------------------------------------- -%% MQTT/WebSocket Listeners - -{mapping, "listener.ws.$name.endpoint", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.ws.$name.mqtt_path", "emqx.listeners", [ - {default, "/mqtt"}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.max_connections", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.active_n", "emqx.listeners", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.zone", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ws.$name.rate_limit", "emqx.listeners", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ws.$name.fail_if_no_subprotocol", "emqx.listeners", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "listener.ws.$name.supported_subprotocols", "emqx.listeners", [ - {default, "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5"}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.proxy_address_header", "emqx.listeners", [ - {default, "X-Forwarded-For"}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.proxy_port_header", "emqx.listeners", [ - {default, "X-Forwarded-Port"}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ws.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -{mapping, "listener.ws.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.ws.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.ws.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ws.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ws.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ws.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.ws.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.ws.$name.compress", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.level", "emqx.listeners", [ - {datatype, {enum, [none, default, best_compression, best_speed]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.mem_level", "emqx.listeners", [ - {datatype, integer}, - {validators, ["range:1-9"]}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.strategy", "emqx.listeners", [ - {datatype, {enum, [default, filtered, huffman_only, rle]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.server_context_takeover", "emqx.listeners", [ - {datatype, {enum, [takeover, no_takeover]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.client_context_takeover", "emqx.listeners", [ - {datatype, {enum, [takeover, no_takeover]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.server_max_window_bits", "emqx.listeners", [ - {datatype, integer}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.client_max_window_bits", "emqx.listeners", [ - {datatype, integer}, - hidden -]}. - -{mapping, "listener.ws.$name.idle_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - hidden -]}. - -{mapping, "listener.ws.$name.max_frame_size", "emqx.listeners", [ - {datatype, integer}, - hidden -]}. - -{mapping, "listener.ws.$name.mqtt_piggyback", "emqx.listeners", [ - {datatype, {enum, [single, multiple]}}, - {default, multiple}, - hidden -]}. - -{mapping, "listener.ws.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn]}} -]}. - -{mapping, "listener.ws.$name.peer_cert_as_clientid", "emqx.listeners", [ - {datatype, {enum, [cn]}} -]}. - -{mapping, "listener.ws.$name.check_origin_enable", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - {default, false}, - hidden -]}. - -{mapping, "listener.ws.$name.allow_origin_absence", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - {default, true}, - hidden -]}. - -{mapping, "listener.ws.$name.check_origins", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -%%-------------------------------------------------------------------- -%% MQTT/WebSocket/SSL Listeners - -{mapping, "listener.wss.$name.endpoint", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.wss.$name.mqtt_path", "emqx.listeners", [ - {default, "/mqtt"}, - {datatype, string} -]}. - -{mapping, "listener.wss.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.max_connections", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.active_n", "emqx.listeners", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.zone", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.rate_limit", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.fail_if_no_subprotocol", "emqx.listeners", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "listener.wss.$name.supported_subprotocols", "emqx.listeners", [ - {default, "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5"}, - {datatype, string} -]}. - -{mapping, "listener.wss.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.proxy_address_header", "emqx.listeners", [ - {default, "X-Forwarded-For"}, - {datatype, string} -]}. - -{mapping, "listener.wss.$name.proxy_port_header", "emqx.listeners", [ - {default, "X-Forwarded-Port"}, - {datatype, string} -]}. - -{mapping, "listener.wss.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -%%{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ -%% {default, "15s"}, -%% {datatype, {duration, ms}} -%%]}. - -{mapping, "listener.wss.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.wss.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.wss.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.wss.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.wss.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.wss.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.wss.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.wss.$name.tls_versions", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.ciphers", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.psk_ciphers", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.keyfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.certfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.cacertfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.dhfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.depth", "emqx.listeners", [ - {default, 10}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.key_password", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.verify", "emqx.listeners", [ - {datatype, atom} -]}. - -{mapping, "listener.wss.$name.fail_if_no_peer_cert", "emqx.listeners", [ - {datatype, {enum, [true, false]}} -]}. - -{mapping, "listener.wss.$name.secure_renegotiate", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.reuse_sessions", "emqx.listeners", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.honor_cipher_order", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn, crt, pem, md5]}} -]}. - -{mapping, "listener.wss.$name.peer_cert_as_clientid", "emqx.listeners", [ - {datatype, {enum, [cn, dn, crt, pem, md5]}} -]}. - -{mapping, "listener.wss.$name.compress", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.level", "emqx.listeners", [ - {datatype, {enum, [none, default, best_compression, best_speed]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.mem_level", "emqx.listeners", [ - {datatype, integer}, - {validators, ["range:1-9"]}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.strategy", "emqx.listeners", [ - {datatype, {enum, [default, filtered, huffman_only, rle]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.server_context_takeover", "emqx.listeners", [ - {datatype, {enum, [takeover, no_takeover]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.client_context_takeover", "emqx.listeners", [ - {datatype, {enum, [takeover, no_takeover]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.server_max_window_bits", "emqx.listeners", [ - {datatype, integer}, - {validators, ["range:8-15"]}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.client_max_window_bits", "emqx.listeners", [ - {datatype, integer}, - {validators, ["range:8-15"]}, - hidden -]}. - -{mapping, "listener.wss.$name.idle_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - hidden -]}. - -{mapping, "listener.wss.$name.max_frame_size", "emqx.listeners", [ - {datatype, integer}, - hidden -]}. - -{mapping, "listener.wss.$name.mqtt_piggyback", "emqx.listeners", [ - {datatype, {enum, [single, multiple]}}, - {default, multiple}, - hidden -]}. - -{mapping, "listener.wss.$name.check_origin_enable", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - {default, false}, - hidden -]}. - -{mapping, "listener.wss.$name.allow_origin_absence", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - {default, true}, - hidden -]}. - -{mapping, "listener.wss.$name.check_origins", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{translation, "emqx.listeners", fun(Conf) -> - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - - Atom = fun(undefined) -> undefined; (S) -> list_to_atom(S) end, - - Access = fun(S) -> - [A, CIDR] = string:tokens(S, " "), - {list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end} - end, - - AccOpts = fun(Prefix) -> - case cuttlefish_variable:filter_by_prefix(Prefix ++ ".access", Conf) of - [] -> []; - Rules -> [{access_rules, [Access(Rule) || {_, Rule} <- Rules]}] - end - end, - - RateLimit = fun(undefined) -> - undefined; - (Val) -> - [L, D] = string:tokens(Val, ", "), - Limit = case cuttlefish_bytesize:parse(L) of - Sz when is_integer(Sz) -> Sz; - {error, Reason} -> error(Reason) - end, - Duration = case cuttlefish_duration:parse(D, s) of - Secs when is_integer(Secs) -> Secs; - {error, Reason1} -> error(Reason1) - end, - {Limit, Duration} - end, - - CheckOrigin = fun(S) -> - Origins = string:tokens(S, ","), - [ list_to_binary(string:trim(O)) || O <- Origins] - end, - - WsOpts = fun(Prefix) -> - case cuttlefish_variable:filter_by_prefix(Prefix ++ ".check_origins", Conf) of - [] -> undefined; - Rules -> - OriginList = [CheckOrigin(Rule) || {_, Rule} <- Rules], - lists:flatten(OriginList) - end - end, - - LisOpts = fun(Prefix) -> - Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, - {mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, - {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, - {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, - {active_n, cuttlefish:conf_get(Prefix ++ ".active_n", Conf, undefined)}, - {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, - {zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, - {rate_limit, RateLimit(cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined))}, - {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, - {proxy_address_header, list_to_binary(string:lowercase(cuttlefish:conf_get(Prefix ++ ".proxy_address_header", Conf, "")))}, - {proxy_port_header, list_to_binary(string:lowercase(cuttlefish:conf_get(Prefix ++ ".proxy_port_header", Conf, "")))}, - {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, - {fail_if_no_subprotocol, cuttlefish:conf_get(Prefix ++ ".fail_if_no_subprotocol", Conf, undefined)}, - {supported_subprotocols, string:tokens(cuttlefish:conf_get(Prefix ++ ".supported_subprotocols", Conf, ""), ", ")}, - {peer_cert_as_username, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_username", Conf, undefined)}, - {peer_cert_as_clientid, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_clientid", Conf, undefined)}, - {compress, cuttlefish:conf_get(Prefix ++ ".compress", Conf, undefined)}, - {idle_timeout, cuttlefish:conf_get(Prefix ++ ".idle_timeout", Conf, undefined)}, - {max_frame_size, cuttlefish:conf_get(Prefix ++ ".max_frame_size", Conf, undefined)}, - {mqtt_piggyback, cuttlefish:conf_get(Prefix ++ ".mqtt_piggyback", Conf, undefined)}, - {check_origin_enable, cuttlefish:conf_get(Prefix ++ ".check_origin_enable", Conf, undefined)}, - {allow_origin_absence, cuttlefish:conf_get(Prefix ++ ".allow_origin_absence", Conf, undefined)}, - {check_origins, WsOpts(Prefix)} | AccOpts(Prefix)]) - end, - DeflateOpts = fun(Prefix) -> - Filter([{level, cuttlefish:conf_get(Prefix ++ ".deflate_opts.level", Conf, undefined)}, - {mem_level, cuttlefish:conf_get(Prefix ++ ".deflate_opts.mem_level", Conf, undefined)}, - {strategy, cuttlefish:conf_get(Prefix ++ ".deflate_opts.strategy", Conf, undefined)}, - {server_context_takeover, cuttlefish:conf_get(Prefix ++ ".deflate_opts.server_context_takeover", Conf, undefined)}, - {client_context_takeover, cuttlefish:conf_get(Prefix ++ ".deflate_opts.client_context_takeover", Conf, undefined)}, - {server_max_windows_bits, cuttlefish:conf_get(Prefix ++ ".deflate_opts.server_max_window_bits", Conf, undefined)}, - {client_max_windows_bits, cuttlefish:conf_get(Prefix ++ ".deflate_opts.client_max_window_bits", Conf, undefined)}]) - end, - TcpOpts = fun(Prefix) -> - Filter([{backlog, cuttlefish:conf_get(Prefix ++ ".backlog", Conf, undefined)}, - {send_timeout, cuttlefish:conf_get(Prefix ++ ".send_timeout", Conf, undefined)}, - {send_timeout_close, cuttlefish:conf_get(Prefix ++ ".send_timeout_close", Conf, undefined)}, - {recbuf, cuttlefish:conf_get(Prefix ++ ".recbuf", Conf, undefined)}, - {sndbuf, cuttlefish:conf_get(Prefix ++ ".sndbuf", Conf, undefined)}, - {buffer, cuttlefish:conf_get(Prefix ++ ".buffer", Conf, undefined)}, - {high_watermark, cuttlefish:conf_get(Prefix ++ ".high_watermark", Conf, undefined)}, - {nodelay, cuttlefish:conf_get(Prefix ++ ".nodelay", Conf, true)}, - {reuseaddr, cuttlefish:conf_get(Prefix ++ ".reuseaddr", Conf, undefined)}]) - end, - SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, - MapPSKCiphers = fun(PSKCiphers) -> - lists:map( - fun("PSK-AES128-CBC-SHA") -> {psk, aes_128_cbc, sha}; - ("PSK-AES256-CBC-SHA") -> {psk, aes_256_cbc, sha}; - ("PSK-3DES-EDE-CBC-SHA") -> {psk, '3des_ede_cbc', sha}; - ("PSK-RC4-SHA") -> {psk, rc4_128, sha} - end, PSKCiphers) - end, - SslOpts = fun(Prefix) -> - Versions = case SplitFun(cuttlefish:conf_get(Prefix ++ ".tls_versions", Conf, undefined)) of - undefined -> undefined; - L -> [list_to_atom(V) || V <- L] - end, - TLSCiphers = cuttlefish:conf_get(Prefix++".ciphers", Conf, undefined), - PSKCiphers = cuttlefish:conf_get(Prefix++".psk_ciphers", Conf, undefined), - Ciphers = - case {TLSCiphers, PSKCiphers} of - {undefined, undefined} -> - cuttlefish:invalid(Prefix++".ciphers or "++Prefix++".psk_ciphers is absent"); - {TLSCiphers, undefined} -> - SplitFun(TLSCiphers); - {undefined, PSKCiphers} -> - MapPSKCiphers(SplitFun(PSKCiphers)); - {_TLSCiphers, _PSKCiphers} -> - cuttlefish:invalid(Prefix++".ciphers and "++Prefix++".psk_ciphers cannot be configured at the same time") - end, - UserLookupFun = - case PSKCiphers of - undefined -> undefined; - _ -> {fun emqx_psk:lookup/3, <<>>} - end, - Filter([{versions, Versions}, - {ciphers, Ciphers}, - {user_lookup_fun, UserLookupFun}, - {handshake_timeout, cuttlefish:conf_get(Prefix ++ ".handshake_timeout", Conf, undefined)}, - {depth, cuttlefish:conf_get(Prefix ++ ".depth", Conf, undefined)}, - {password, cuttlefish:conf_get(Prefix ++ ".key_password", Conf, undefined)}, - {dhfile, cuttlefish:conf_get(Prefix ++ ".dhfile", Conf, undefined)}, - {keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)}, - {certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)}, - {cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}, - {verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)}, - {fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)}, - {secure_renegotiate, cuttlefish:conf_get(Prefix ++ ".secure_renegotiate", Conf, undefined)}, - {reuse_sessions, cuttlefish:conf_get(Prefix ++ ".reuse_sessions", Conf, undefined)}, - {honor_cipher_order, cuttlefish:conf_get(Prefix ++ ".honor_cipher_order", Conf, undefined)}]) - end, - - Listen_fix = fun({Ip, Port}) -> case inet:parse_address(Ip) of - {ok, R} -> {R, Port}; - _ -> {Ip, Port} - end; - (Other) -> Other - end, - - TcpListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - ListenOnN = case cuttlefish:conf_get(Prefix ++ ".endpoint", Conf, undefined) of - undefined -> []; - ListenOn -> Listen_fix(ListenOn) - end, - [#{ proto => Atom(Type) - , name => Name - , listen_on => ListenOnN - , opts => [ {deflate_options, DeflateOpts(Prefix)} - , {tcp_options, TcpOpts(Prefix)} - | LisOpts(Prefix) - ] - } - ] - end, - SslListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - case cuttlefish:conf_get(Prefix ++ ".endpoint", Conf, undefined) of - undefined -> - []; - ListenOn -> - [#{ proto => Atom(Type) - , name => Name - , listen_on => Listen_fix(ListenOn) - , opts => [ {deflate_options, DeflateOpts(Prefix)} - , {tcp_options, TcpOpts(Prefix)} - , {ssl_options, SslOpts(Prefix)} - | LisOpts(Prefix) - ] - } - ] - end - end, - lists:flatten([TcpListeners(Type, Name) || {["listener", Type, Name, "endpoint"], ListenOn} - <- cuttlefish_variable:filter_by_prefix("listener.tcp", Conf) - ++ cuttlefish_variable:filter_by_prefix("listener.ws", Conf)] - ++ - [SslListeners(Type, Name) || {["listener", Type, Name, "endpoint"], ListenOn} - <- cuttlefish_variable:filter_by_prefix("listener.ssl", Conf) - ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)]) -end}. - -%%-------------------------------------------------------------------- -%% Modules -%%-------------------------------------------------------------------- - -{mapping, "modules.loaded_file", "emqx.modules_loaded_file", [ - {datatype, string} -]}. - -{mapping, "module.presence.qos", "emqx.modules", [ - {default, 1}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -{mapping, "module.subscription.$id.topic", "emqx.modules", [ - {datatype, string} -]}. - -{mapping, "module.subscription.$id.qos", "emqx.modules", [ - {default, 1}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -{mapping, "module.subscription.$id.nl", "emqx.modules", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:0-1"]} -]}. - -{mapping, "module.subscription.$id.rap", "emqx.modules", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:0-1"]} -]}. - -{mapping, "module.subscription.$id.rh", "emqx.modules", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -{mapping, "module.rewrite.rule.$id", "emqx.modules", [ - {datatype, string} -]}. - -{mapping, "module.rewrite.pub.rule.$id", "emqx.modules", [ - {datatype, string} -]}. - -{mapping, "module.rewrite.sub.rule.$id", "emqx.modules", [ - {datatype, string} -]}. - -{translation, "emqx.modules", fun(Conf, _, Conf1) -> - Subscriptions = fun() -> - List = cuttlefish_variable:filter_by_prefix("module.subscription", Conf), - TopicList = [{N, Topic}|| {[_,"subscription",N,"topic"], Topic} <- List], - [{iolist_to_binary(T), #{ qos => cuttlefish:conf_get("module.subscription." ++ N ++ ".qos", Conf, 0), - nl => cuttlefish:conf_get("module.subscription." ++ N ++ ".nl", Conf, 0), - rap => cuttlefish:conf_get("module.subscription." ++ N ++ ".rap", Conf, 0), - rh => cuttlefish:conf_get("module.subscription." ++ N ++ ".rh", Conf, 0) - }} || {N, T} <- TopicList] - end, - Rewrites = fun() -> - Rules = cuttlefish_variable:filter_by_prefix("module.rewrite.rule", Conf), - PubRules = cuttlefish_variable:filter_by_prefix("module.rewrite.pub.rule", Conf), - SubRules = cuttlefish_variable:filter_by_prefix("module.rewrite.sub.rule", Conf), - TotalRules = lists:append( - [ {["module", "rewrite", "pub", "rule", I], Rule} || {["module", "rewrite", "rule", I], Rule} <- Rules] ++ PubRules, - [ {["module", "rewrite", "sub", "rule", I], Rule} || {["module", "rewrite", "rule", I], Rule} <- Rules] ++ SubRules - ), - lists:map(fun({[_, "rewrite", PubOrSub, "rule", I], Rule}) -> - [Topic, Re, Dest] = string:tokens(Rule, " "), - {rewrite, list_to_atom(PubOrSub), list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} - end, TotalRules) - end, - lists:append([ - [{emqx_mod_presence, [{qos, cuttlefish:conf_get("module.presence.qos", Conf, 1)}]}], - [{emqx_mod_subscription, Subscriptions()}], - [{emqx_mod_rewrite, Rewrites()}], - [{emqx_mod_topic_metrics, []}], - [{emqx_mod_delayed, []}], - [{emqx_mod_acl_internal, [{acl_file, cuttlefish:conf_get("acl_file", Conf1)}]}] - ]) -end}. - -%%------------------------------------------------------------------- -%% Plugins -%%------------------------------------------------------------------- - -{mapping, "plugins.etc_dir", "emqx.plugins_etc_dir", [ - {datatype, string} -]}. - -{mapping, "plugins.loaded_file", "emqx.plugins_loaded_file", [ - {datatype, string} -]}. - -{mapping, "plugins.expand_plugins_dir", "emqx.expand_plugins_dir", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Broker -%%-------------------------------------------------------------------- - -{mapping, "broker.sys_interval", "emqx.broker_sys_interval", [ - {datatype, {duration, ms}}, - {default, "1m"} -]}. - -{mapping, "broker.sys_heartbeat", "emqx.broker_sys_heartbeat", [ - {datatype, {duration, ms}}, - {default, "30s"} -]}. - -{mapping, "broker.enable_session_registry", "emqx.enable_session_registry", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "broker.session_locking_strategy", "emqx.session_locking_strategy", [ - {default, quorum}, - {datatype, {enum, [local,one,quorum,all]}} -]}. - -%% @doc Shared Subscription Dispatch Strategy. -{mapping, "broker.shared_subscription_strategy", "emqx.shared_subscription_strategy", [ - {default, round_robin}, - {datatype, - {enum, - [random, %% randomly pick a subscriber - round_robin, %% round robin alive subscribers one message after another - sticky, %% pick a random subscriber and stick to it - hash, %% hash client ID to a group member - hash_clientid, - hash_topic - ]}} -]}. - -%% @doc Enable or disable shared dispatch acknowledgement for QoS1 and QoS2 messages -{mapping, "broker.shared_dispatch_ack_enabled", "emqx.shared_dispatch_ack_enabled", - [ {default, false}, - {datatype, {enum, [true, false]}} - ]}. - -{mapping, "broker.route_batch_clean", "emqx.route_batch_clean", [ - {default, on}, - {datatype, flag} -]}. - -%% @doc Performance toggle for subscribe/unsubscribe wildcard topic. -%% Change this toggle only when there are many wildcard topics. -%% key: mnesia translational updates with per-key locks. recommended for single node setup. -%% tab: mnesia translational updates with table lock. recommended for multi-nodes setup. -%% global: global lock protected updates. recommended for larger cluster. -%% NOTE: when changing from/to 'global' lock, it requires all nodes in the cluster -%% -{mapping, "broker.perf.route_lock_type", "emqx.route_lock_type", [ - {default, key}, - {datatype, {enum, [key, tab, global]}} -]}. - -%% @doc Enable trie path compaction. -%% Enabling it significantly improves wildcard topic subscribe rate, -%% if wildcard topics have unique prefixes like: 'sensor/{{id}}/+/', -%% where ID is unique per subscriber. -%% -%% Topic match performance (when publishing) may degrade if messages -%% are mostly published to topics with large number of levels. -%% -%% NOTE: This is a cluster-wide configuration. -%% It rquires all nodes to be stopped before changing it. -{mapping, "broker.perf.trie_compaction", "emqx.trie_compaction", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%%-------------------------------------------------------------------- -%% System Monitor -%%-------------------------------------------------------------------- - -%% @doc Long GC, don't monitor in production mode for: -%% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 -{mapping, "sysmon.long_gc", "emqx.sysmon", [ - {default, 0}, - {datatype, [integer, {duration, ms}]} -]}. - -%% @doc Long Schedule(ms) -{mapping, "sysmon.long_schedule", "emqx.sysmon", [ - {default, 240}, - {datatype, [integer, {duration, ms}]} -]}. - -%% @doc Large Heap -{mapping, "sysmon.large_heap", "emqx.sysmon", [ - {default, "8MB"}, - {datatype, bytesize} -]}. - -%% @doc Monitor Busy Port -{mapping, "sysmon.busy_port", "emqx.sysmon", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Monitor Busy Dist Port -{mapping, "sysmon.busy_dist_port", "emqx.sysmon", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.sysmon", fun(Conf) -> - Configs = cuttlefish_variable:filter_by_prefix("sysmon", Conf), - [{list_to_atom(Name), Value} || {[_, Name], Value} <- Configs] -end}. - -%%-------------------------------------------------------------------- -%% Operating System Monitor -%%-------------------------------------------------------------------- - -{mapping, "os_mon.cpu_check_interval", "emqx.os_mon", [ - {default, 60}, - {datatype, {duration, s}} -]}. - -{mapping, "os_mon.cpu_high_watermark", "emqx.os_mon", [ - {default, "80%"}, - {datatype, {percent, float}} -]}. - -{mapping, "os_mon.cpu_low_watermark", "emqx.os_mon", [ - {default, "60%"}, - {datatype, {percent, float}} -]}. - -{mapping, "os_mon.mem_check_interval", "emqx.os_mon", [ - {default, 60}, - {datatype, {duration, s}} -]}. - -{mapping, "os_mon.sysmem_high_watermark", "emqx.os_mon", [ - {default, "70%"}, - {datatype, {percent, float}} -]}. - -{mapping, "os_mon.procmem_high_watermark", "emqx.os_mon", [ - {default, "5%"}, - {datatype, {percent, float}} -]}. - -{translation, "emqx.os_mon", fun(Conf) -> - [{cpu_check_interval, cuttlefish:conf_get("os_mon.cpu_check_interval", Conf)}, - {cpu_high_watermark, cuttlefish:conf_get("os_mon.cpu_high_watermark", Conf) * 100}, - {cpu_low_watermark, cuttlefish:conf_get("os_mon.cpu_low_watermark", Conf) * 100}, - {mem_check_interval, cuttlefish:conf_get("os_mon.mem_check_interval", Conf)}, - {sysmem_high_watermark, cuttlefish:conf_get("os_mon.sysmem_high_watermark", Conf) * 100}, - {procmem_high_watermark, cuttlefish:conf_get("os_mon.procmem_high_watermark", Conf) * 100}] -end}. - -%%-------------------------------------------------------------------- -%% VM Monitor -%%-------------------------------------------------------------------- -{mapping, "vm_mon.check_interval", "emqx.vm_mon", [ - {default, 30}, - {datatype, {duration, s}} -]}. - -{mapping, "vm_mon.process_high_watermark", "emqx.vm_mon", [ - {default, "80%"}, - {datatype, {percent, float}} -]}. - -{mapping, "vm_mon.process_low_watermark", "emqx.vm_mon", [ - {default, "60%"}, - {datatype, {percent, float}} -]}. - -{translation, "emqx.vm_mon", fun(Conf) -> - [{check_interval, cuttlefish:conf_get("vm_mon.check_interval", Conf)}, - {process_high_watermark, cuttlefish:conf_get("vm_mon.process_high_watermark", Conf) * 100}, - {process_low_watermark, cuttlefish:conf_get("vm_mon.process_low_watermark", Conf) * 100}] -end}. - -%%-------------------------------------------------------------------- -%% Alarm -%%-------------------------------------------------------------------- -{mapping, "alarm.actions", "emqx.alarm", [ - {default, "log,publish"}, - {datatype, string} -]}. - -{mapping, "alarm.size_limit", "emqx.alarm", [ - {default, 1000}, - {datatype, integer} -]}. - -{mapping, "alarm.validity_period", "emqx.alarm", [ - {default, "24h"}, - {datatype, {duration, s}} -]}. - -{translation, "emqx.alarm", fun(Conf) -> - [{actions, [list_to_atom(Action) || Action <- string:tokens(cuttlefish:conf_get("alarm.actions", Conf), ",")]}, - {size_limit, cuttlefish:conf_get("alarm.size_limit", Conf)}, - {validity_period, cuttlefish:conf_get("alarm.validity_period", Conf)}] -end}. - -%%-------------------------------------------------------------------- -%% Telemetry -%%-------------------------------------------------------------------- -{mapping, "telemetry.enabled", "emqx.telemetry", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "telemetry.url", "emqx.telemetry", [ - {default, "https://telemetry-emqx-io.bigpar.vercel.app/api/telemetry"}, - {datatype, string} -]}. - -{mapping, "telemetry.report_interval", "emqx.telemetry", [ - {default, "7d"}, - {datatype, {duration, s}} -]}. - -{translation, "emqx.telemetry", fun(Conf) -> - [ {enabled, cuttlefish:conf_get("telemetry.enabled", Conf)} - , {url, cuttlefish:conf_get("telemetry.url", Conf)} - , {report_interval, cuttlefish:conf_get("telemetry.report_interval", Conf)} - ] -end}. diff --git a/rebar.config b/rebar.config index 2ee94ff04..3a169c4a9 100644 --- a/rebar.config +++ b/rebar.config @@ -8,9 +8,8 @@ {edoc_opts, [{preprocess,true}]}. {erl_opts, [warn_unused_vars,warn_shadow_vars,warn_unused_import, - warn_obsolete_guard,compressed]}. - -{extra_src_dirs, [{"etc", [{recursive,true}]}]}. + warn_obsolete_guard,compressed, + {d, snk_kind, msg}]}. {xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, deprecated_function_calls,warnings_as_errors,deprecated_functions]}. @@ -32,10 +31,9 @@ {post_hooks,[]}. -{erl_first_files, ["src/emqx_logger.erl", "src/emqx_rule_actions_trans.erl"]}. - {deps, - [ {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.5"}}} + [ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps + , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.5"}}} , {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.6.5"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} @@ -43,17 +41,19 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.0"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.9.0"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.0"}}} + , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} % TODO: delete when all apps moved to hocon , {minirest, {git, "https://github.com/emqx/minirest", {tag, "0.3.5"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, {git, "https://github.com/emqx/replayq", {tag, "0.3.2"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3.1"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.2"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 , {getopt, "1.0.1"} - , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.12.0"}}} + , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.0"}}} + , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} ]}. {xref_ignores, diff --git a/rebar.config.erl b/rebar.config.erl index c5206e9e2..5cc857398 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -2,10 +2,15 @@ -export([do/2]). -do(_Dir, CONFIG) -> - {HasElixir, C1} = deps(CONFIG), - Config = dialyzer(C1), - maybe_dump(Config ++ [{overrides, overrides()}] ++ coveralls() ++ config(HasElixir)). +do(Dir, CONFIG) -> + case iolist_to_binary(Dir) of + <<".">> -> + {HasElixir, C1} = deps(CONFIG), + Config = dialyzer(C1), + maybe_dump(Config ++ [{overrides, overrides()}] ++ coveralls() ++ config(HasElixir)); + _ -> + CONFIG + end. bcrypt() -> {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {branch, "0.6.0"}}}. @@ -46,6 +51,8 @@ overrides() -> [ {add, [ {extra_src_dirs, [{"etc", [{recursive,true}]}]} , {erl_opts, [{compile_info, [{emqx_vsn, get_vsn()}]}]} ]} + , {add, snabbkaffe, + [{erl_opts, common_compile_opts()}]} ] ++ community_plugin_overrides(). community_plugin_overrides() -> @@ -99,13 +106,14 @@ test_plugins() -> test_deps() -> [ {bbmustache, "1.10.0"} - , {emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {branch, "hocon"}}} + , {emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "2.0.0"}}} , meck ]. common_compile_opts() -> [ debug_info % alwyas include debug_info , {compile_info, [{emqx_vsn, get_vsn()}]} + , {d, snk_kind, msg} ] ++ [{d, 'EMQX_ENTERPRISE'} || is_enterprise()] ++ [{d, 'EMQX_BENCHMARK'} || os:getenv("EMQX_BENCHMARK") =:= "1" ]. @@ -240,6 +248,7 @@ relx_apps(ReleaseType) -> , {ekka, load} , {emqx_plugin_libs, load} , observer_cli + , emqx_http_lib ] ++ [emqx_modules || not is_enterprise()] ++ [emqx_license || is_enterprise()] @@ -321,6 +330,7 @@ relx_overlay(ReleaseType) -> , {template, "data/loaded_plugins.tmpl", "data/loaded_plugins"} , {template, "data/loaded_modules.tmpl", "data/loaded_modules"} , {template, "data/emqx_vars", "releases/emqx_vars"} + , {template, "data/BUILT_ON", "releases/{{release_version}}/BUILT_ON"} , {copy, "bin/emqx", "bin/emqx"} , {copy, "bin/emqx_ctl", "bin/emqx_ctl"} , {copy, "bin/node_dump", "bin/node_dump"} @@ -332,9 +342,8 @@ relx_overlay(ReleaseType) -> , {template, "bin/emqx_ctl.cmd", "bin/emqx_ctl.cmd"} , {copy, "bin/nodetool", "bin/nodetool"} , {copy, "bin/nodetool", "bin/nodetool-{{release_version}}"} - , {copy, "_build/default/lib/cuttlefish/cuttlefish", "bin/cuttlefish"} - , {copy, "_build/default/lib/cuttlefish/cuttlefish", "bin/cuttlefish-{{release_version}}"} - , {copy, "priv/emqx.schema", "releases/{{release_version}}/"} + , {copy, "_build/default/lib/hocon/hocon", "bin/hocon"} + , {copy, "_build/default/lib/hocon/hocon", "bin/hocon-{{release_version}}"} ] ++ case is_enterprise() of true -> ee_etc_overlay(ReleaseType); false -> etc_overlay(ReleaseType) @@ -347,7 +356,6 @@ etc_overlay(ReleaseType) -> [community_plugin_etc_overlays(App) || App <- relx_plugin_apps_extra()], [ {mkdir, "etc/"} , {mkdir, "etc/plugins"} - , {template, "etc/BUILT_ON", "releases/{{release_version}}/BUILT_ON"} , {copy, "{{base_dir}}/lib/emqx/etc/certs","etc/"} ] ++ lists:map( @@ -364,18 +372,20 @@ extra_overlay(edge) -> []. emqx_etc_overlay(cloud) -> emqx_etc_overlay_common() ++ - [ {"etc/emqx_cloud/vm.args","etc/vm.args"} + [ {"{{base_dir}}/lib/emqx/etc/emqx_cloud/vm.args","etc/vm.args"} ]; emqx_etc_overlay(edge) -> emqx_etc_overlay_common() ++ - [ {"etc/emqx_edge/vm.args","etc/vm.args"} + [ {"{{base_dir}}/lib/emqx/etc/emqx_edge/vm.args","etc/vm.args"} ]. emqx_etc_overlay_common() -> - ["etc/acl.conf", "etc/emqx.conf", "etc/ssl_dist.conf", + [{"{{base_dir}}/lib/emqx/etc/acl.conf", "etc/acl.conf"}, + {"{{base_dir}}/lib/emqx/etc/emqx.conf", "etc/emqx.conf"}, + {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}, %% TODO: check why it has to end with .paho %% and why it is put to etc/plugins dir - {"etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}]. + {"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}]. plugin_etc_overlays(App0) -> App = atom_to_list(App0), diff --git a/scripts/apps-version-check.sh b/scripts/apps-version-check.sh index 098653444..0134bf187 100755 --- a/scripts/apps-version-check.sh +++ b/scripts/apps-version-check.sh @@ -1,26 +1,50 @@ #!/bin/bash set -euo pipefail -latest_release=$(git describe --tags "$(git rev-list --tags --max-count=1 --remotes=refs/remote/origin)") +remote="refs/remote/$(git remote -v | grep fetch | grep 'emqx/emqx' | awk '{print $1}')" +latest_release=$(git describe --tags "$(git rev-list --tags --max-count=1 --remotes="$remote")") bad_app_count=0 -while read -r app; do - if [ "$app" != "emqx" ]; then - app_path="$app" +get_vsn() { + commit="$1" + app_src_file="$2" + if [ "$commit" = 'HEAD' ]; then + if [ -f "$app_src_file" ]; then + grep vsn "$app_src_file" | grep -oE '"[0-9]+.[0-9]+.[0-9]+"' | tr -d '"' || true + fi else - app_path="." + git show "$commit":"$app_src_file" 2>/dev/null | grep vsn | grep -oE '"[0-9]+.[0-9]+.[0-9]+"' | tr -d '"' || true fi - src_file="$app_path/src/$(basename "$app").app.src" - old_app_version="$(git show "$latest_release":"$src_file" | grep vsn | grep -oE '"[0-9]+.[0-9]+.[0-9]+"' | tr -d '"')" - now_app_version=$(grep -E 'vsn' "$src_file" | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') - if [ "$old_app_version" = "$now_app_version" ]; then - changed="$(git diff --name-only "$latest_release"...HEAD \ +} + +while read -r app_path; do + app=$(basename "$app_path") + src_file="$app_path/src/$app.app.src" + old_app_version="$(get_vsn "$latest_release" "$src_file")" + ## TODO: delete it after new version is released with emqx app in apps dir + if [ "$app" = 'emqx' ] && [ "$old_app_version" = '' ]; then + old_app_version="$(get_vsn "$latest_release" 'src/emqx.app.src')" + fi + now_app_version="$(get_vsn 'HEAD' "$src_file")" + ## TODO: delete it after new version is released with emqx app in apps dir + if [ "$app" = 'emqx' ] && [ "$now_app_version" = '' ]; then + now_app_version="$(get_vsn 'HEAD' 'src/emqx.app.src')" + fi + if [ -z "$now_app_version" ]; then + echo "failed_to_get_new_app_vsn for $app" + exit 1 + fi + if [ -z "${old_app_version:-}" ]; then + echo "skiped checking new app ${app}" + elif [ "$old_app_version" = "$now_app_version" ]; then + lines="$(git diff --name-only "$latest_release"...HEAD \ -- "$app_path/src" \ -- "$app_path/priv" \ - -- "$app_path/c_src" | wc -l)" - if [ "$changed" -gt 0 ]; then - echo "$src_file needs a vsn bump" + -- "$app_path/c_src")" + if [ "$lines" != '' ]; then + echo "$src_file needs a vsn bump (old=$old_app_version)" + echo "changed: $lines" bad_app_count=$(( bad_app_count + 1)) fi fi diff --git a/scripts/find-apps.sh b/scripts/find-apps.sh index fabec239e..1f199269c 100755 --- a/scripts/find-apps.sh +++ b/scripts/find-apps.sh @@ -10,9 +10,6 @@ find_app() { find "${appdir}" -mindepth 1 -maxdepth 1 -type d } -# append emqx application first -echo 'emqx' - find_app 'apps' if [ -f 'EMQX_ENTERPRISE' ]; then find_app 'lib-ee' diff --git a/scripts/find-suites.sh b/scripts/find-suites.sh index 97939d931..da477694d 100755 --- a/scripts/find-suites.sh +++ b/scripts/find-suites.sh @@ -8,8 +8,5 @@ set -euo pipefail # ensure dir cd -P -- "$(dirname -- "$0")/.." -TESTDIR="test" -if [ "$1" != "emqx" ]; then - TESTDIR="$1/test" -fi +TESTDIR="$1/test" find "${TESTDIR}" -name "*_SUITE.erl" 2>/dev/null | xargs | tr ' ' ',' diff --git a/scripts/get-dashboard.sh b/scripts/get-dashboard.sh index e37badfb2..99c5b5330 100755 --- a/scripts/get-dashboard.sh +++ b/scripts/get-dashboard.sh @@ -12,11 +12,14 @@ if [ -f 'EMQX_ENTERPRISE' ]; then DASHBOARD_PATH='lib-ee/emqx_dashboard/priv' DASHBOARD_REPO='emqx-enterprise-dashboard-frontend-src' AUTH="Authorization: token $(cat scripts/git-token)" + # have to be resolved with auth and redirect + DIRECT_DOWNLOAD_URL="" else VERSION="${EMQX_CE_DASHBOARD_VERSION}" DASHBOARD_PATH='lib-ce/emqx_dashboard/priv' DASHBOARD_REPO='emqx-dashboard-frontend' AUTH="" + DIRECT_DOWNLOAD_URL="https://github.com/emqx/${DASHBOARD_REPO}/releases/download/${VERSION}/emqx-dashboard.zip" fi case $(uname) in @@ -32,27 +35,32 @@ if [ -d "$DASHBOARD_PATH/www" ] && [ "$(version)" = "$VERSION" ]; then exit 0 fi -get_assets(){ +find_url() { # Get the download URL of our desired asset - download_url="$(curl --silent --show-error \ - --header "${AUTH}" \ - --header "Accept: application/vnd.github.v3+json" \ - "https://api.github.com/repos/emqx/${DASHBOARD_REPO}/releases/tags/${VERSION}" \ - | jq --raw-output ".assets[] | select(.name==\"${RELEASE_ASSET_FILE}\").url" \ - | tr -d '\n' | tr -d '\r')" + release_url="https://api.github.com/repos/emqx/${DASHBOARD_REPO}/releases/tags/${VERSION}" + release_info="$(curl --silent --show-error --header "${AUTH}" --header "Accept: application/vnd.github.v3+json" "$release_url")" + if ! download_url="$(echo "$release_info" | jq --raw-output ".assets[] | select(.name==\"${RELEASE_ASSET_FILE}\").url" | tr -d '\n' | tr -d '\r')"; then + echo "failed to query $release_url" + echo "${release_info}" + exit 1 + fi # Get GitHub's S3 redirect URL - redirect_url=$(curl --silent --show-error \ - --header "${AUTH}" \ - --header "Accept: application/octet-stream" \ - --write-out "%{redirect_url}" \ - "$download_url") curl --silent --show-error \ + --header "${AUTH}" \ --header "Accept: application/octet-stream" \ - --output "${RELEASE_ASSET_FILE}" \ - "$redirect_url" + --write-out "%{redirect_url}" \ + "$download_url" } -get_assets +if [ -z "$DIRECT_DOWNLOAD_URL" ]; then + DIRECT_DOWNLOAD_URL="$(find_url)" +fi + +curl -L --silent --show-error \ + --header "Accept: application/octet-stream" \ + --output "${RELEASE_ASSET_FILE}" \ + "$DIRECT_DOWNLOAD_URL" + unzip -q "$RELEASE_ASSET_FILE" -d "$DASHBOARD_PATH" rm -rf "$DASHBOARD_PATH/www" mv "$DASHBOARD_PATH/dist" "$DASHBOARD_PATH/www" diff --git a/scripts/split-config.escript b/scripts/split-config.escript index 04f94269c..0a26d6fd2 100755 --- a/scripts/split-config.escript +++ b/scripts/split-config.escript @@ -13,7 +13,7 @@ -define(BASE, <<"emqx">>). main(_) -> - {ok, Bin} = file:read_file("etc/emqx.conf"), + {ok, Bin} = file:read_file(conf_file()), Lines = binary:split(Bin, <<"\n">>, [global]), Sections0 = parse_sections(Lines), {value, _, Sections1} = lists:keytake(<<"modules">>, 1, Sections0), @@ -24,6 +24,10 @@ main(_) -> end, IncludeNames), ok = dump_sections([{N, Base ++ Includes}| Sections2]). +etc_dir() -> filename:join(["apps", "emqx", "etc"]). + +conf_file() -> filename:join([etc_dir(), "emqx.conf"]). + parse_sections(Lines) -> {ok, P} = re:compile("#+\s*CONFIG_SECTION_(BGN|END)\s*=\s*([^\s-]+)\s*="), Parser = @@ -57,7 +61,7 @@ parse_sections([Line | Lines], Parse, Section, Sections) -> dump_sections([]) -> ok; dump_sections([{Name, Lines0} | Rest]) -> - Filename = filename:join(["etc", iolist_to_binary([Name, ".conf.seg"])]), + Filename = filename:join([etc_dir(), iolist_to_binary([Name, ".conf.seg"])]), Lines = [[L, "\n"] || L <- Lines0], ok = file:write_file(Filename, Lines), dump_sections(Rest). diff --git a/scripts/update_appup.escript b/scripts/update_appup.escript new file mode 100755 index 000000000..6a78012d2 --- /dev/null +++ b/scripts/update_appup.escript @@ -0,0 +1,75 @@ +#!/usr/bin/env -S escript -c +%% A script that adds changed modules to the corresponding appup files + +main(_Args) -> + ChangedFiles = string:lexemes(os:cmd("git diff --name-only origin/master..HEAD"), "\n"), + AppModules0 = lists:filtermap(fun filter_erlang_modules/1, ChangedFiles), + %% emqx_app must always be included as we bump version number in emqx_release.hrl for each release + AppModules1 = [{emqx, emqx_app} | AppModules0], + AppModules = group_modules(AppModules1), + io:format("Changed modules: ~p~n", [AppModules]), + _ = maps:map(fun process_app/2, AppModules), + ok. + +process_app(App, Modules) -> + AppupFiles = filelib:wildcard(lists:concat(["{src,apps,lib-*}/**/", App, ".appup.src"])), + case AppupFiles of + [AppupFile] -> + update_appup(AppupFile, Modules); + [] -> + io:format("~nWARNING: Please create an stub appup src file for ~p~n", [App]) + end. + +filter_erlang_modules(Filename) -> + case lists:reverse(filename:split(Filename)) of + [Module, "src"] -> + erl_basename("emqx", Module); + [Module, "src", App|_] -> + erl_basename(App, Module); + [Module, _, "src", App|_] -> + erl_basename(App, Module); + _ -> + false + end. + +erl_basename(App, Name) -> + case filename:basename(Name, ".erl") of + Name -> false; + Module -> {true, {list_to_atom(App), list_to_atom(Module)}} + end. + +group_modules(L) -> + lists:foldl(fun({App, Mod}, Acc) -> + maps:update_with(App, fun(Tl) -> [Mod|Tl] end, [Mod], Acc) + end, #{}, L). + +update_appup(File, Modules) -> + io:format("~nUpdating appup: ~p~n", [File]), + {_, Upgrade0, Downgrade0} = read_appup(File), + Upgrade = update_actions(Modules, Upgrade0), + Downgrade = update_actions(Modules, Downgrade0), + IOList = io_lib:format("%% -*- mode: erlang -*- +{VSN,~n ~p,~n ~p}.~n", [Upgrade, Downgrade]), + ok = file:write_file(File, IOList). + +update_actions(Modules, Versions) -> + lists:map(fun(L) -> do_update_actions(Modules, L) end, Versions). + +do_update_actions(_, Ret = {<<".*">>, _}) -> + Ret; +do_update_actions(Modules, {Vsn, Actions}) -> + {Vsn, add_modules(Modules, Actions)}. + +add_modules(NewModules, OldActions) -> + OldModules = lists:map(fun(It) -> element(2, It) end, OldActions), + Modules = NewModules -- OldModules, + OldActions ++ [{load_module, M, brutal_purge, soft_purge, []} || M <- Modules]. + +read_appup(File) -> + {ok, Bin0} = file:read_file(File), + %% Hack: + Bin1 = re:replace(Bin0, "VSN", "\"VSN\""), + TmpFile = filename:join("/tmp", filename:basename(File)), + ok = file:write_file(TmpFile, Bin1), + {ok, [Terms]} = file:consult(TmpFile), + Terms. diff --git a/src/emqx.appup.src b/src/emqx.appup.src deleted file mode 100644 index 3bf40272c..000000000 --- a/src/emqx.appup.src +++ /dev/null @@ -1,58 +0,0 @@ -%% -*-: erlang -*- -{VSN, - [ - {"4.3.1", [ - {load_module, emqx_connection, brutal_purge, soft_purge, []}, - {load_module, emqx_cm, brutal_purge, soft_purge, []}, - {load_module, emqx_congestion, brutal_purge, soft_purge, []}, - {load_module, emqx_node_dump, brutal_purge, soft_purge, []}, - {load_module, emqx_channel, brutal_purge, soft_purge, []}, - {load_module, emqx_app, brutal_purge, soft_purge, []}, - {load_module, emqx_plugins, brutal_purge, soft_purge, []} - ]}, - {"4.3.0", [ - {load_module, emqx_logger_jsonfmt, brutal_purge, soft_purge, []}, - {load_module, emqx_congestion, brutal_purge, soft_purge, []}, - {load_module, emqx_connection, brutal_purge, soft_purge, []}, - {load_module, emqx_frame, brutal_purge, soft_purge, []}, - {load_module, emqx_trie, brutal_purge, soft_purge, []}, - {load_module, emqx_cm, brutal_purge, soft_purge, []}, - {load_module, emqx_node_dump, brutal_purge, soft_purge, []}, - {load_module, emqx_channel, brutal_purge, soft_purge, []}, - {load_module, emqx_app, brutal_purge, soft_purge, []}, - {load_module, emqx_plugins, brutal_purge, soft_purge, []}, - %% - {load_module, emqx_metrics, brutal_purge, soft_purge, []}, - {apply, {emqx_metrics, upgrade_retained_delayed_counter_type, []}} - ]}, - {<<".*">>, []} - ], - [ - {"4.3.1", [ - {load_module, emqx_connection, brutal_purge, soft_purge, []}, - {load_module, emqx_cm, brutal_purge, soft_purge, []}, - {load_module, emqx_congestion, brutal_purge, soft_purge, []}, - {load_module, emqx_node_dump, brutal_purge, soft_purge, []}, - {load_module, emqx_channel, brutal_purge, soft_purge, []}, - {load_module, emqx_app, brutal_purge, soft_purge, []}, - {load_module, emqx_plugins, brutal_purge, soft_purge, []} - ]}, - {"4.3.0", [ - {load_module, emqx_logger_jsonfmt, brutal_purge, soft_purge, []}, - {load_module, emqx_connection, brutal_purge, soft_purge, []}, - {load_module, emqx_congestion, brutal_purge, soft_purge, []}, - {load_module, emqx_frame, brutal_purge, soft_purge, []}, - {load_module, emqx_trie, brutal_purge, soft_purge, []}, - {load_module, emqx_cm, brutal_purge, soft_purge, []}, - {load_module, emqx_node_dump, brutal_purge, soft_purge, []}, - {load_module, emqx_channel, brutal_purge, soft_purge, []}, - {load_module, emqx_app, brutal_purge, soft_purge, []}, - {load_module, emqx_plugins, brutal_purge, soft_purge, []}, - %% Just load the module. We don't need to change the 'messages.retained' - %% and 'messages.retained' counter type. - {load_module, emqx_metrics, brutal_purge, soft_purge, []}, - {apply, {emqx_metrics, upgrade_retained_delayed_counter_type, []}} - ]}, - {<<".*">>, []} - ] -}. diff --git a/src/emqx_http_lib.erl b/src/emqx_http_lib.erl deleted file mode 100644 index 60f19e6bb..000000000 --- a/src/emqx_http_lib.erl +++ /dev/null @@ -1,177 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_http_lib). - --export([ uri_encode/1 - , uri_decode/1 - , uri_parse/1 - , normalise_headers/1 - ]). - --export_type([uri_map/0]). - --type uri_map() :: #{scheme := http | https, - host := unicode:chardata(), - port := non_neg_integer(), - path => unicode:chardata(), - query => unicode:chardata(), - fragment => unicode:chardata(), - userinfo => unicode:chardata()}. - --type hex_uri() :: string() | binary(). --type maybe_hex_uri() :: string() | binary(). %% A possibly hexadecimal encoded URI. --type uri() :: string() | binary(). - -%% @doc Decode percent-encoded URI. -%% This is copied from http_uri.erl which has been deprecated since OTP-23 -%% The recommended replacement uri_string function is not quite equivalent -%% and not backward compatible. --spec uri_decode(maybe_hex_uri()) -> uri(). -uri_decode(String) when is_list(String) -> - do_uri_decode(String); -uri_decode(String) when is_binary(String) -> - do_uri_decode_binary(String). - -do_uri_decode([$%,Hex1,Hex2|Rest]) -> - [hex2dec(Hex1)*16+hex2dec(Hex2)|do_uri_decode(Rest)]; -do_uri_decode([First|Rest]) -> - [First|do_uri_decode(Rest)]; -do_uri_decode([]) -> - []. - -do_uri_decode_binary(<<$%, Hex:2/binary, Rest/bits>>) -> - <<(binary_to_integer(Hex, 16)), (do_uri_decode_binary(Rest))/binary>>; -do_uri_decode_binary(<>) -> - <>; -do_uri_decode_binary(<<>>) -> - <<>>. - -%% @doc Encode URI. --spec uri_encode(uri()) -> hex_uri(). -uri_encode(URI) when is_list(URI) -> - lists:append([do_uri_encode(Char) || Char <- URI]); -uri_encode(URI) when is_binary(URI) -> - << <<(do_uri_encode_binary(Char))/binary>> || <> <= URI >>. - -%% @doc Parse URI into a map as uri_string:uri_map(), but with two fields -%% normalised: (1): port number is never 'undefined', default ports are used -%% if missing. (2): scheme is always atom. --spec uri_parse(string() | binary()) -> {ok, uri_map()} | {error, any()}. -uri_parse(URI) -> - try - {ok, do_parse(uri_string:normalize(URI))} - catch - throw : Reason -> - {error, Reason} - end. - -do_parse({error, Reason, Which}) -> throw({Reason, Which}); -do_parse(URI) -> - %% ensure we return string() instead of binary() in uri_map() values. - Map = uri_string:parse(unicode:characters_to_list(URI)), - case maps:is_key(scheme, Map) of - true -> - normalise_parse_result(Map); - false -> - %% missing scheme, add "http://" and try again - Map2 = uri_string:parse(unicode:characters_to_list(["http://", URI])), - normalise_parse_result(Map2) - end. - -%% @doc Return HTTP headers list with keys lower-cased and -%% underscores replaced with hyphens -%% NOTE: assuming the input Headers list is a proplists, -%% that is, when a key is duplicated, list header overrides tail -%% e.g. [{"Content_Type", "applicaiton/binary"}, {"content-type", "applicaiton/json"}] -%% results in: [{"content-type", "applicaiton/binary"}] -normalise_headers(Headers0) -> - F = fun({K0, V}) -> - K = re:replace(K0, "_", "-", [{return,list}]), - {string:lowercase(K), V} - end, - Headers = lists:map(F, Headers0), - Keys = proplists:get_keys(Headers), - [{K, proplists:get_value(K, Headers)} || K <- Keys]. - -normalise_parse_result(#{host := Host, scheme := Scheme0} = Map) -> - Scheme = atom_scheme(Scheme0), - DefaultPort = case https =:= Scheme of - true -> 443; - false -> 80 - end, - Port = case maps:get(port, Map, undefined) of - N when is_number(N) -> N; - _ -> DefaultPort - end, - Map#{ scheme := Scheme - , host := emqx_misc:maybe_parse_ip(Host) - , port => Port - }. - -%% NOTE: so far we only support http schemes. -atom_scheme(Scheme) when is_list(Scheme) -> atom_scheme(list_to_binary(Scheme)); -atom_scheme(<<"https">>) -> https; -atom_scheme(<<"http">>) -> http; -atom_scheme(Other) -> throw({unsupported_scheme, Other}). - -do_uri_encode(Char) -> - case reserved(Char) of - true -> - [ $% | integer_to_hexlist(Char)]; - false -> - [Char] - end. - -do_uri_encode_binary(Char) -> - case reserved(Char) of - true -> - << $%, (integer_to_binary(Char, 16))/binary >>; - false -> - <> - end. - -reserved($;) -> true; -reserved($:) -> true; -reserved($@) -> true; -reserved($&) -> true; -reserved($=) -> true; -reserved($+) -> true; -reserved($,) -> true; -reserved($/) -> true; -reserved($?) -> true; -reserved($#) -> true; -reserved($[) -> true; -reserved($]) -> true; -reserved($<) -> true; -reserved($>) -> true; -reserved($\") -> true; -reserved(${) -> true; -reserved($}) -> true; -reserved($|) -> true; -reserved($\\) -> true; -reserved($') -> true; -reserved($^) -> true; -reserved($%) -> true; -reserved($\s) -> true; -reserved(_) -> false. - -integer_to_hexlist(Int) -> - integer_to_list(Int, 16). - -hex2dec(X) when (X>=$0) andalso (X=<$9) -> X-$0; -hex2dec(X) when (X>=$A) andalso (X=<$F) -> X-$A+10; -hex2dec(X) when (X>=$a) andalso (X=<$f) -> X-$a+10. diff --git a/test/emqx_http_lib_tests.erl b/test/emqx_http_lib_tests.erl deleted file mode 100644 index a850da8f5..000000000 --- a/test/emqx_http_lib_tests.erl +++ /dev/null @@ -1,84 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_http_lib_tests). - --include_lib("proper/include/proper.hrl"). --include_lib("eunit/include/eunit.hrl"). - -uri_encode_decode_test_() -> - Opts = [{numtests, 1000}, {to_file, user}], - {timeout, 10, - fun() -> ?assert(proper:quickcheck(prop_run(), Opts)) end}. - -prop_run() -> - ?FORALL(Generated, prop_uri(), test_prop_uri(iolist_to_binary(Generated))). - -prop_uri() -> - proper_types:non_empty(proper_types:list(proper_types:union([prop_char(), prop_reserved()]))). - -prop_char() -> proper_types:integer(32, 126). - -prop_reserved() -> - proper_types:oneof([$;, $:, $@, $&, $=, $+, $,, $/, $?, - $#, $[, $], $<, $>, $\", ${, $}, $|, - $\\, $', $^, $%, $ ]). - -test_prop_uri(URI) -> - Encoded = emqx_http_lib:uri_encode(URI), - Decoded1 = emqx_http_lib:uri_decode(Encoded), - ?assertEqual(URI, Decoded1), - Decoded2 = uri_string:percent_decode(Encoded), - ?assertEqual(URI, Decoded2), - true. - -uri_parse_test_() -> - [ {"default port http", - fun() -> ?assertMatch({ok, #{port := 80, scheme := http, host := "localhost"}}, - emqx_http_lib:uri_parse("localhost")) - end - } - , {"default port https", - fun() -> ?assertMatch({ok, #{port := 443, scheme := https}}, - emqx_http_lib:uri_parse("https://localhost")) - end - } - , {"bad url", - fun() -> ?assertMatch({error, {invalid_uri, _}}, - emqx_http_lib:uri_parse("https://localhost:notnumber")) - end - } - , {"normalise", - fun() -> ?assertMatch({ok, #{scheme := https, host := {127, 0, 0, 1}}}, - emqx_http_lib:uri_parse("HTTPS://127.0.0.1")) - end - } - , {"unsupported_scheme", - fun() -> ?assertEqual({error, {unsupported_scheme, <<"wss">>}}, - emqx_http_lib:uri_parse("wss://127.0.0.1")) - end - } - , {"ipv6 host", - fun() -> ?assertMatch({ok, #{scheme := http, host := T}} when size(T) =:= 8, - emqx_http_lib:uri_parse("http://[::1]:80")) - end - } - ]. - -normalise_headers_test() -> - ?assertEqual([{"content-type", "applicaiton/binary"}], - emqx_http_lib:normalise_headers([{"Content_Type", "applicaiton/binary"}, - {"content-type", "applicaiton/json"}])).