diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index b01d6ca27..53ab9ac57 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -36,9 +36,9 @@ emqx_test(){ "zip") packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.zip) unzip -q "${PACKAGE_PATH}/${packagename}" - export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \ + export EMQX_ZONES__DEFAULT__MQTT__SERVER_KEEPALIVE=60 \ EMQX_MQTT__MAX_TOPIC_ALIAS=10 - [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_ZONES__DEFAULT__LISTENERS__MQTT_QUIC__ENABLED=false + [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_LISTENERS__QUIC__DEFAULT__ENABLED=false # sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins echo "running ${packagename} start" @@ -48,7 +48,7 @@ emqx_test(){ exit 1 fi IDLE_TIME=0 - while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do + while ! curl http://localhost:18083/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx running error" @@ -91,6 +91,12 @@ emqx_test(){ ;; "rpm") packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.rpm) + + if [[ "${ARCH}" == "amd64" && $(rpm -E '%{rhel}') == 7 ]] ; + then + # EMQX OTP requires openssl11 to have TLS1.3 support + yum install -y openssl11; + fi rpm -ivh "${PACKAGE_PATH}/${packagename}" if ! rpm -q emqx | grep -q emqx; then echo "package install error" @@ -119,15 +125,14 @@ run_test(){ if [ -f "$emqx_env_vars" ]; then tee -a "$emqx_env_vars" </dev/null 2>&1; do + while ! curl http://localhost:18083/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx running error" @@ -169,7 +174,7 @@ EOF exit 1 fi IDLE_TIME=0 - while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do + while ! curl http://localhost:18083/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx service error" diff --git a/.ci/docker-compose-file/conf.cluster.env b/.ci/docker-compose-file/conf.cluster.env index 2a10bd321..b69500ec5 100644 --- a/.ci/docker-compose-file/conf.cluster.env +++ b/.ci/docker-compose-file/conf.cluster.env @@ -1,8 +1,7 @@ EMQX_NAME=emqx EMQX_CLUSTER__DISCOVERY_STRATEGY=static EMQX_CLUSTER__STATIC__SEEDS="[emqx@node1.emqx.io, emqx@node2.emqx.io]" -EMQX_ZONES__DEFAULT__LISTENERS__MQTT_TCP__PROXY_PROTOCOL=true -EMQX_ZONES__DEFAULT__LISTENERS__MQTT_WS__PROXY_PROTOCOL=true +EMQX_LISTENERS__TCP__DEFAULT__PROXY_PROTOCOL=true +EMQX_LISTENERS__WS__DEFAULT__PROXY_PROTOCOL=true EMQX_LOG__CONSOLE_HANDLER__ENABLE=true EMQX_LOG__CONSOLE_HANDLER__LEVEL=debug -EMQX_LOG__PRIMARY_LEVEL=debug diff --git a/.ci/docker-compose-file/haproxy/haproxy.cfg b/.ci/docker-compose-file/haproxy/haproxy.cfg index 4361ccadb..b658789da 100644 --- a/.ci/docker-compose-file/haproxy/haproxy.cfg +++ b/.ci/docker-compose-file/haproxy/haproxy.cfg @@ -33,7 +33,7 @@ defaults frontend emqx_mgmt mode tcp option tcplog - bind *:8081 + bind *:18083 default_backend emqx_mgmt_back frontend emqx_dashboard @@ -45,8 +45,8 @@ frontend emqx_dashboard backend emqx_mgmt_back mode http # balance static-rr - server emqx-1 node1.emqx.io:8081 - server emqx-2 node2.emqx.io:8081 + server emqx-1 node1.emqx.io:18083 + server emqx-2 node2.emqx.io:18083 backend emqx_dashboard_back mode http diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 96d193913..3ec513a37 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -2,7 +2,7 @@ name: Bug Report about: Create a report to help us improve title: '' -labels: Support +labels: "Support, needs-triage" --- diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 0519e5699..1fb5f401f 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -2,7 +2,7 @@ name: Feature Request about: Suggest an idea for this project title: '' -labels: Feature +labels: "Feature, needs-triage" --- diff --git a/.github/ISSUE_TEMPLATE/support-needed.md b/.github/ISSUE_TEMPLATE/support-needed.md index 18b47bfb5..a19299c42 100644 --- a/.github/ISSUE_TEMPLATE/support-needed.md +++ b/.github/ISSUE_TEMPLATE/support-needed.md @@ -2,7 +2,7 @@ name: Support Needed about: Asking a question about usages, docs or anything you're insterested in title: '' -labels: Support +labels: "Support, needs-triage" --- diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 7388b4d05..0c6d065a9 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -20,8 +20,8 @@ jobs: container: ${{ matrix.container }} outputs: - profiles: ${{ steps.set_profile.outputs.profiles}} - old_vsns: ${{ steps.set_profile.outputs.old_vsns}} + profiles: ${{ steps.set_profile.outputs.profiles }} + old_vsns: ${{ steps.set_profile.outputs.old_vsns }} steps: - uses: actions/checkout@v2 @@ -44,6 +44,11 @@ jobs: echo "::set-output name=old_vsns::$old_vsns" echo "::set-output name=profiles::[\"emqx\", \"emqx-edge\"]" fi + - name: get otp version + id: get_otp_version + run: | + otp="$(erl -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), "OTP_VERSION"])), io:fwrite(Version), halt().' -noshell)" + echo "::set-output name=otp::$otp" - name: set get token if: endsWith(github.repository, 'enterprise') run: | @@ -54,12 +59,13 @@ jobs: run: | make ensure-rebar3 ./rebar3 as default get-deps + rm -rf rebar.lock - name: gen zip file - run: zip -ryq source.zip source/* source/.[^.]* + run: zip -ryq source-${{ steps.get_otp_version.outputs.otp }}.zip source/* source/.[^.]* - uses: actions/upload-artifact@v2 with: - name: source - path: source.zip + name: source-${{ steps.get_otp_version.outputs.otp }} + path: source-${{ steps.get_otp_version.outputs.otp }}.zip windows: runs-on: windows-2019 @@ -77,19 +83,21 @@ jobs: steps: - uses: actions/download-artifact@v2 with: - name: source + name: source-23.2.7.2-emqx-2 path: . - name: unzip source code - run: Expand-Archive -Path source.zip -DestinationPath ./ + run: Expand-Archive -Path source-23.2.7.2-emqx-2.zip -DestinationPath ./ - uses: ilammy/msvc-dev-cmd@v1 - - uses: gleam-lang/setup-erlang@v1.1.0 + - uses: gleam-lang/setup-erlang@v1.1.2 id: install_erlang + ## gleam-lang/setup-erlang does not yet support the installation of otp24 on windows with: - otp-version: 24.0.5 + otp-version: 23.2 - name: build env: PYTHON: python DIAGNOSTIC: 1 + working-directory: source run: | $env:PATH = "${{ steps.install_erlang.outputs.erlpath }}\bin;$env:PATH" @@ -101,9 +109,9 @@ jobs: else { $pkg_name = "${{ matrix.profile }}-windows-$($version -replace '/').zip" } - cd source - ## We do not build/release bcrypt for windows package + ## We do not build/release bcrypt and quic for windows package Remove-Item -Recurse -Force -Path _build/default/lib/bcrypt/ + Remove-Item -Recurse -Force -Path _build/default/lib/quicer/ if (Test-Path rebar.lock) { Remove-Item -Force -Path rebar.lock } @@ -118,8 +126,8 @@ jobs: Get-FileHash -Path "_packages/${{ matrix.profile }}/$pkg_name" | Format-List | grep 'Hash' | awk '{print $3}' > _packages/${{ matrix.profile }}/$pkg_name.sha256 - name: run emqx timeout-minutes: 1 + working-directory: source run: | - cd source ./_build/${{ matrix.profile }}/rel/emqx/bin/emqx start Start-Sleep -s 5 ./_build/${{ matrix.profile }}/rel/emqx/bin/emqx stop @@ -128,7 +136,7 @@ jobs: - uses: actions/upload-artifact@v1 if: startsWith(github.ref, 'refs/tags/') with: - name: ${{ matrix.profile }} + name: ${{ matrix.profile }}-23.2.7.2-emqx-2 path: source/_packages/${{ matrix.profile }}/. mac: @@ -140,7 +148,7 @@ jobs: fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} - erl_otp: + otp: - 24.0.5-emqx-1 exclude: - profile: emqx-edge @@ -148,10 +156,10 @@ jobs: steps: - uses: actions/download-artifact@v2 with: - name: source + name: source-${{ matrix.otp }} path: . - name: unzip source code - run: unzip -q source.zip + run: unzip -q source-${{ matrix.otp }}.zip - name: prepare run: | brew update @@ -162,7 +170,7 @@ jobs: id: cache with: path: ~/.kerl - key: erl${{ matrix.erl_otp }}-macos10.15 + key: erl${{ matrix.otp }}-macos10.15 - name: build erlang if: steps.cache.outputs.cache-hit != 'true' timeout-minutes: 60 @@ -171,25 +179,25 @@ jobs: OTP_GITHUB_URL: https://github.com/emqx/otp run: | kerl update releases - kerl build ${{ matrix.erl_otp }} - kerl install ${{ matrix.erl_otp }} $HOME/.kerl/${{ matrix.erl_otp }} + kerl build ${{ matrix.otp }} + kerl install ${{ matrix.otp }} $HOME/.kerl/${{ matrix.otp }} - name: build + working-directory: source run: | - . $HOME/.kerl/${{ matrix.erl_otp }}/activate - cd source + . $HOME/.kerl/${{ matrix.otp }}/activate make ensure-rebar3 sudo cp rebar3 /usr/local/bin/rebar3 make ${{ matrix.profile }}-zip - name: test + working-directory: source run: | - cd source pkg_name=$(basename _packages/${{ matrix.profile }}/${{ matrix.profile }}-*.zip) unzip -q _packages/${{ matrix.profile }}/$pkg_name # gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do - if curl -fs 127.0.0.1:8081/api/v5/status > /dev/null; then + if curl -fs 127.0.0.1:18083/api/v5/status > /dev/null; then ready='yes' break fi @@ -207,7 +215,7 @@ jobs: - uses: actions/upload-artifact@v1 if: startsWith(github.ref, 'refs/tags/') with: - name: ${{ matrix.profile }} + name: ${{ matrix.profile }}-${{ matrix.otp }} path: source/_packages/${{ matrix.profile }}/. linux: @@ -219,12 +227,6 @@ jobs: fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} - erl_otp: - - 23.2.7.2-emqx-2 - - 24.0.5-emqx-1 - arch: - - amd64 - - arm64 os: - ubuntu20.04 - ubuntu18.04 @@ -237,6 +239,12 @@ jobs: - centos6 - raspbian10 # - raspbian9 + arch: + - amd64 + - arm64 + otp: + - 23.2.7.2-emqx-2 + - 24.0.5-emqx-1 exclude: - os: centos6 arch: arm64 @@ -265,10 +273,10 @@ jobs: platforms: all - uses: actions/download-artifact@v2 with: - name: source + name: source-${{ matrix.otp }} path: . - name: unzip source code - run: unzip -q source.zip + run: unzip -q source-${{ matrix.otp }}.zip - name: downloads old emqx zip packages env: PROFILE: ${{ matrix.profile }} @@ -298,7 +306,7 @@ jobs: done - name: build emqx packages env: - ERL_OTP: erl${{ matrix.erl_otp }} + ERL_OTP: erl${{ matrix.otp }} PROFILE: ${{ matrix.profile }} ARCH: ${{ matrix.arch }} SYSTEM: ${{ matrix.os }} @@ -327,7 +335,7 @@ jobs: - uses: actions/upload-artifact@v1 if: startsWith(github.ref, 'refs/tags/') with: - name: ${{ matrix.profile }} + name: ${{ matrix.profile }}-${{ matrix.otp }} path: source/_packages/${{ matrix.profile }}/. docker: @@ -338,67 +346,74 @@ jobs: fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} - erl_otp: + otp: - 24.0.5-emqx-1 steps: - uses: actions/download-artifact@v2 with: - name: source + name: source-${{ matrix.otp }} path: . - name: unzip source code - run: unzip -q source.zip + run: unzip -q source-${{ matrix.otp }}.zip + - name: get version + id: version + working-directory: source + run: echo "::set-output name=version::$(./pkg-vsn.sh)" - uses: docker/setup-buildx-action@v1 - uses: docker/setup-qemu-action@v1 with: image: tonistiigi/binfmt:latest platforms: all - - name: build emqx docker image + - uses: docker/build-push-action@v2 if: github.event_name != 'release' - env: - ERL_OTP: erl${{ matrix.erl_otp }} - PROFILE: ${{ matrix.profile }} - working-directory: source - run: | - PKG_VSN="$(./pkg-vsn.sh)" - docker buildx build --no-cache \ - --platform=linux/amd64,linux/arm64 \ - --build-arg PKG_VSN=$PKG_VSN \ - --build-arg BUILD_FROM=emqx/build-env:$ERL_OTP-alpine \ - --build-arg RUN_FROM=alpine:3.14 \ - --build-arg EMQX_NAME=$PROFILE \ - --tag emqx/$PROFILE:$PKG_VSN \ - -f deploy/docker/Dockerfile . + with: + push: false + pull: true + no-cache: true + platforms: linux/amd64,linux/arm64 + tags: emqx/${{ matrix.profile }}:${{ steps.version.outputs.version }} + build-args: | + PKG_VSN=${{ steps.version.outputs.version }} + BUILD_FROM=emqx/build-env:erl${{ matrix.otp }}-alpine + RUN_FROM=alpine:3.14 + EMQX_NAME=${{ matrix.profile }} + file: source/deploy/docker/Dockerfile + context: source - uses: docker/login-action@v1 if: github.event_name == 'release' with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - - name: build emqx docker image + - uses: docker/build-push-action@v2 if: github.event_name == 'release' - env: - ERL_OTP: erl${{ matrix.erl_otp }} - PROFILE: ${{ matrix.profile }} - working-directory: source - run: | - PKG_VSN="$(./pkg-vsn.sh)" - docker buildx build --no-cache \ - --platform=linux/amd64,linux/arm64 \ - --build-arg PKG_VSN=$PKG_VSN \ - --build-arg BUILD_FROM=emqx/build-env:$ERL_OTP-alpine \ - --build-arg RUN_FROM=alpine:3.14 \ - --build-arg EMQX_NAME=$PROFILE \ - --tag emqx/$PROFILE:$PKG_VSN \ - -f deploy/docker/Dockerfile \ - --push . + with: + push: true + pull: true + no-cache: true + platforms: linux/amd64,linux/arm64 + tags: emqx/${{ matrix.profile }}:${{ steps.version.outputs.version }} + build-args: | + PKG_VSN=${{ steps.version.outputs.version }} + BUILD_FROM=emqx/build-env:erl${{ matrix.otp }}-alpine + RUN_FROM=alpine:3.14 + EMQX_NAME=${{ matrix.profile }} + file: source/deploy/docker/Dockerfile + context: source delete-artifact: + runs-on: ubuntu-20.04 + strategy: + matrix: + otp: + - 23.2.7.2-emqx-2 + - 24.0.5-emqx-1 needs: [prepare, mac, linux, docker] steps: - uses: geekyeggo/delete-artifact@v1 with: - name: source + name: source-${{ matrix.otp }} upload: runs-on: ubuntu-20.04 @@ -410,6 +425,8 @@ jobs: strategy: matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} + otp: + - 24.0.5-emqx-1 steps: - uses: actions/checkout@v2 @@ -420,7 +437,7 @@ jobs: echo 'EOF' >> $GITHUB_ENV - uses: actions/download-artifact@v2 with: - name: ${{ matrix.profile }} + name: ${{ matrix.profile }}-${{ matrix.otp }} path: ./_packages/${{ matrix.profile }} - name: install dos2unix run: sudo apt-get update && sudo apt install -y dos2unix diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 9e19889fc..9578c6f9d 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -113,7 +113,7 @@ jobs: ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do - if curl -fs 127.0.0.1:8081/api/v5/status > /dev/null; then + if curl -fs 127.0.0.1:18083/api/v5/status > /dev/null; then ready='yes' break fi diff --git a/.github/workflows/git_sync.yaml b/.github/workflows/git_sync.yaml index 50fa8c364..48e29a37d 100644 --- a/.github/workflows/git_sync.yaml +++ b/.github/workflows/git_sync.yaml @@ -1,9 +1,10 @@ name: Sync to enterprise on: + schedule: + - cron: '0 */6 * * *' push: branches: - - master - main-v* jobs: diff --git a/.github/workflows/run_api_tests.yaml b/.github/workflows/run_api_tests.yaml new file mode 100644 index 000000000..618b9383a --- /dev/null +++ b/.github/workflows/run_api_tests.yaml @@ -0,0 +1,102 @@ +name: API Test Suite + +on: + push: + tags: + - e* + - v* + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + container: "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" + steps: + - uses: actions/checkout@v2 + - name: zip emqx-broker + if: endsWith(github.repository, 'emqx') + run: | + make emqx-zip + - name: zip emqx-broker + if: endsWith(github.repository, 'enterprise') + run: | + echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials + git config --global credential.helper store + make emqx-ee-zip + - uses: actions/upload-artifact@v2 + with: + name: emqx-broker + path: _packages/**/*.zip + api-test: + needs: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + script_name: + - api_metrics + - api_subscriptions + steps: + - uses: actions/checkout@v2 + with: + repository: emqx/emqx-fvt + path: . + - uses: actions/setup-java@v1 + with: + java-version: '8.0.282' # The JDK version to make available on the path. + java-package: jdk # (jre, jdk, or jdk+fx) - defaults to jdk + architecture: x64 # (x64 or x86) - defaults to x64 + - uses: actions/download-artifact@v2 + with: + name: emqx-broker + path: . + - name: start emqx-broker + env: + EMQX_LISTENERS__WSS__DEFAULT__BIND: "0.0.0.0:8085" + run: | + unzip ./emqx/*.zip + ./emqx/bin/emqx start + - name: install jmeter + timeout-minutes: 10 + env: + JMETER_VERSION: 5.3 + run: | + wget --no-verbose --no-check-certificate -O /tmp/apache-jmeter.tgz https://downloads.apache.org/jmeter/binaries/apache-jmeter-$JMETER_VERSION.tgz + cd /tmp && tar -xvf apache-jmeter.tgz + echo "jmeter.save.saveservice.output_format=xml" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties + echo "jmeter.save.saveservice.response_data.on_error=true" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties + wget --no-verbose -O /tmp/apache-jmeter-$JMETER_VERSION/lib/ext/mqtt-xmeter-2.0.2-jar-with-dependencies.jar https://raw.githubusercontent.com/xmeter-net/mqtt-jmeter/master/Download/v2.0.2/mqtt-xmeter-2.0.2-jar-with-dependencies.jar + ln -s /tmp/apache-jmeter-$JMETER_VERSION /opt/jmeter + - name: run ${{ matrix.script_name }} + run: | + /opt/jmeter/bin/jmeter.sh \ + -Jjmeter.save.saveservice.output_format=xml -n \ + -t .ci/api-test-suite/${{ matrix.script_name }}.jmx \ + -Demqx_ip="127.0.0.1" \ + -l jmeter_logs/${{ matrix.script_name }}.jtl \ + -j jmeter_logs/logs/${{ matrix.script_name }}.log + - name: check test logs + run: | + if cat jmeter_logs/${{ matrix.script_name }}.jtl | grep -e 'true' > /dev/null 2>&1; then + grep -A 5 -B 3 'true' jmeter_logs/${{ matrix.script_name }}.jtl > jmeter_logs/${{ matrix.script_name }}_err_api.txt + echo "check logs failed" + exit 1 + fi + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: jmeter_logs + path: ./jmeter_logs + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: jmeter_logs + path: emqx/log + delete-package: + runs-on: ubuntu-20.04 + needs: api-test + if: always() + steps: + - uses: geekyeggo/delete-artifact@v1 + with: + name: emqx-broker diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index 272d69ca7..a4b9df5c2 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -8,300 +8,186 @@ on: pull_request: jobs: - docker_test: - runs-on: ubuntu-20.04 + prepare: + strategy: + matrix: + container: + - "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" + - "emqx/build-env:erl24.0.5-emqx-1-ubuntu20.04" - steps: - - uses: actions/checkout@v1 - - uses: gleam-lang/setup-erlang@v1.1.2 - id: install_erlang - with: - otp-version: 24.0.5 - - name: prepare - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials - git config --global credential.helper store - echo "${{ secrets.CI_GIT_TOKEN }}" >> scripts/git-token - make deps-emqx-ee - echo "PROFILE=emqx-ee" >> $GITHUB_ENV - echo "TARGET=emqx/emqx-ee" >> $GITHUB_ENV - echo "EMQX_TAG=$(./pkg-vsn.sh)" >> $GITHUB_ENV - else - echo "PROFILE=emqx" >> $GITHUB_ENV - echo "TARGET=emqx/emqx" >> $GITHUB_ENV - echo "EMQX_TAG=$(./pkg-vsn.sh)" >> $GITHUB_ENV - fi - - name: make emqx image - run: make $PROFILE-docker - - name: run emqx - timeout-minutes: 5 - run: | - set -e -u -x - echo "HOCON_ENV_OVERRIDE_PREFIX=EMQX_" >> .ci/docker-compose-file/conf.cluster.env - echo "EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s" >> .ci/docker-compose-file/conf.cluster.env - echo "EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10" >> .ci/docker-compose-file/conf.cluster.env - docker-compose \ - -f .ci/docker-compose-file/docker-compose-emqx-cluster.yaml \ - -f .ci/docker-compose-file/docker-compose-python.yaml \ - up -d - while ! docker exec -i node1.emqx.io bash -c "emqx eval \"['emqx@node1.emqx.io','emqx@node2.emqx.io'] = maps:get(running_nodes, ekka_cluster:info()).\"" > /dev/null 2>&1; do - echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:waiting emqx"; - sleep 5; - done - # - name: verify EMQX_LOADED_PLUGINS override working - # run: | - # expected="{emqx_sn, true}." - # output=$(docker exec -i node1.emqx.io bash -c "cat data/loaded_plugins" | tail -n1) - # if [ "$expected" != "$output" ]; then - # exit 1 - # fi - - name: make paho tests - run: | - if ! docker exec -i python /scripts/pytest.sh; then - echo "DUMP_CONTAINER_LOGS_BGN" - docker logs haproxy - docker logs node1.emqx.io - docker logs node2.emqx.io - echo "DUMP_CONTAINER_LOGS_END" - exit 1 - fi + runs-on: ubuntu-20.04 + container: ${{ matrix.container }} - helm_test: - runs-on: ubuntu-20.04 + outputs: + profile: ${{ steps.profile.outputs.profile }} - steps: - - uses: actions/checkout@v1 - - uses: gleam-lang/setup-erlang@v1.1.2 - id: install_erlang - with: - otp-version: 24.0.5 - - name: prepare - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials - git config --global credential.helper store - echo "${{ secrets.CI_GIT_TOKEN }}" >> scripts/git-token - make deps-emqx-ee - echo "TARGET=emqx/emqx-ee" >> $GITHUB_ENV - echo "PROFILE=emqx-ee" >> $GITHUB_ENV - else - echo "TARGET=emqx/emqx" >> $GITHUB_ENV - echo "PROFILE=emqx" >> $GITHUB_ENV - fi - - name: make emqx image - run: make $PROFILE-docker - - name: install k3s - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - sudo sh -c "echo \"127.0.0.1 $(hostname)\" >> /etc/hosts" - curl -sfL https://get.k3s.io | sh - - sudo chmod 644 /etc/rancher/k3s/k3s.yaml - kubectl cluster-info - - name: install helm - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 - sudo chmod 700 get_helm.sh - sudo ./get_helm.sh - helm version - - name: run emqx on chart - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - timeout-minutes: 5 - run: | - version=$(./pkg-vsn.sh) - sudo docker save ${TARGET}:$version -o emqx.tar.gz - sudo k3s ctr image import emqx.tar.gz + steps: + - name: get otp version + id: get_otp_version + run: | + otp="$(erl -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), "OTP_VERSION"])), io:fwrite(Version), halt().' -noshell)" + echo "::set-output name=otp::$otp" + - uses: actions/checkout@v2 + with: + path: source + fetch-depth: 0 + - name: set profile + id: profile + shell: bash + working-directory: source + run: | + vsn="$(./pkg-vsn.sh)" + if make emqx-ee --dry-run > /dev/null 2>&1; then + echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials + git config --global credential.helper store + echo "::set-output name=profile::emqx-ee" + else + echo "::set-output name=profile::emqx" + fi + - name: get deps + working-directory: source + run: | + make ensure-rebar3 + ./rebar3 as default get-deps + rm -rf rebar.lock + - name: gen zip file + run: zip -ryq source-${{ steps.get_otp_version.outputs.otp }}.zip source/* source/.[^.]* + - uses: actions/upload-artifact@v2 + with: + name: source-${{ steps.get_otp_version.outputs.otp }} + path: source-${{ steps.get_otp_version.outputs.otp }}.zip - sed -i -r "s/^appVersion: .*$/appVersion: \"${version}\"/g" deploy/charts/emqx/Chart.yaml - sed -i '/emqx_telemetry/d' deploy/charts/emqx/values.yaml + docker_test: + runs-on: ubuntu-20.04 + needs: prepare - helm install emqx \ - --set image.repository=${TARGET} \ - --set image.pullPolicy=Never \ - --set emqxAclConfig="" \ - --set image.pullPolicy=Never \ - --set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s \ - --set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10 \ - deploy/charts/emqx \ - --debug + strategy: + fail-fast: false + matrix: + otp: + - 23.2.7.2-emqx-2 + - 24.0.5-emqx-1 - while [ "$(kubectl get StatefulSet -l app.kubernetes.io/name=emqx -o jsonpath='{.items[0].status.replicas}')" \ - != "$(kubectl get StatefulSet -l app.kubernetes.io/name=emqx -o jsonpath='{.items[0].status.readyReplicas}')" ]; do - echo "=============================="; - kubectl get pods; - echo "=============================="; - echo "waiting emqx started"; - sleep 10; - done - - name: get emqx-0 pods log - if: failure() - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - kubectl describe pods emqx-0 - kubectl logs emqx-0 - - name: get emqx-1 pods log - if: failure() - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - kubectl describe pods emqx-1 - kubectl logs emqx-1 - - name: get emqx-2 pods log - if: failure() - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - kubectl describe pods emqx-2 - kubectl logs emqx-2 - - uses: actions/checkout@v2 - with: - repository: emqx/paho.mqtt.testing - ref: develop-4.0 - path: paho.mqtt.testing - - name: install pytest - run: | - pip install pytest - echo "$HOME/.local/bin" >> $GITHUB_PATH - - name: run paho test - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - emqx_svc=$(kubectl get svc --namespace default emqx -o jsonpath="{.spec.clusterIP}") - emqx1=$(kubectl get pods emqx-1 -o jsonpath='{.status.podIP}') - emqx2=$(kubectl get pods emqx-2 -o jsonpath='{.status.podIP}') + steps: + - uses: actions/download-artifact@v2 + with: + name: source-${{ matrix.otp }} + path: . + - name: unzip source code + run: unzip -q source-${{ matrix.otp }}.zip + - name: make docker image + working-directory: source + env: + OTP: ${{ matrix.otp }} + run: | + make ${{ needs.prepare.outputs.profile }}-docker + echo "TARGET=emqx/${{ needs.prepare.outputs.profile }}" >> $GITHUB_ENV + echo "EMQX_TAG=$(./pkg-vsn.sh)" >> $GITHUB_ENV + - name: run emqx + timeout-minutes: 5 + working-directory: source + run: | + set -e -u -x + echo "HOCON_ENV_OVERRIDE_PREFIX=EMQX_" >> .ci/docker-compose-file/conf.cluster.env + echo "EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s" >> .ci/docker-compose-file/conf.cluster.env + echo "EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10" >> .ci/docker-compose-file/conf.cluster.env + docker-compose \ + -f .ci/docker-compose-file/docker-compose-emqx-cluster.yaml \ + -f .ci/docker-compose-file/docker-compose-python.yaml \ + up -d + while ! docker exec -i node1.emqx.io bash -c "emqx eval \"['emqx@node1.emqx.io','emqx@node2.emqx.io'] = maps:get(running_nodes, ekka_cluster:info()).\"" > /dev/null 2>&1; do + echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:waiting emqx"; + sleep 5; + done + - name: make paho tests + run: | + if ! docker exec -i python /scripts/pytest.sh; then + echo "DUMP_CONTAINER_LOGS_BGN" + docker logs haproxy + docker logs node1.emqx.io + docker logs node2.emqx.io + echo "DUMP_CONTAINER_LOGS_END" + exit 1 + fi - pytest -v paho.mqtt.testing/interoperability/test_client/V5/test_connect.py -k test_basic --host $emqx_svc - RESULT=$? - pytest -v paho.mqtt.testing/interoperability/test_cluster --host1 $emqx1 --host2 $emqx2 - RESULT=$((RESULT + $?)) - if [ 0 -ne $RESULT ]; then - kubectl logs emqx-1 - kubectl logs emqx-2 - fi - exit $RESULT + helm_test: + runs-on: ubuntu-20.04 + needs: prepare - relup_test: - strategy: - matrix: - container: - - "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" - - "emqx/build-env:erl24.0.5-emqx-1-ubuntu20.04" + strategy: + fail-fast: false + matrix: + otp: + - 23.2.7.2-emqx-2 + - 24.0.5-emqx-1 - runs-on: ubuntu-20.04 - container: ${{ matrix.container }} + steps: + - uses: actions/download-artifact@v2 + with: + name: source-${{ matrix.otp }} + path: . + - name: unzip source code + run: unzip -q source-${{ matrix.otp }}.zip + - name: make docker image + working-directory: source + env: + OTP: ${{ matrix.otp }} + run: | + make ${{ needs.prepare.outputs.profile }}-docker + echo "TARGET=emqx/${{ needs.prepare.outputs.profile }}" >> $GITHUB_ENV + echo "EMQX_TAG=$(./pkg-vsn.sh)" >> $GITHUB_ENV + - run: minikube start + - name: run emqx on chart + timeout-minutes: 5 + working-directory: source + run: | + minikube image load $TARGET:$EMQX_TAG - defaults: - run: - shell: bash - steps: - - uses: actions/setup-python@v2 - with: - python-version: '3.8' - architecture: 'x64' - - uses: actions/checkout@v2 - with: - repository: emqx/paho.mqtt.testing - ref: develop-4.0 - path: paho.mqtt.testing - - uses: actions/checkout@v2 - with: - repository: terry-xiaoyu/one_more_emqx - ref: master - path: one_more_emqx - - uses: actions/checkout@v2 - with: - repository: emqx/emqtt-bench - ref: master - path: emqtt-bench - - uses: actions/checkout@v2 - with: - repository: hawk/lux - ref: lux-2.6 - path: lux - - uses: actions/checkout@v2 - with: - repository: ${{ github.repository }} - path: emqx - fetch-depth: 0 - - name: prepare - run: | - if make -C emqx emqx-ee --dry-run > /dev/null 2>&1; then - echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials - git config --global credential.helper store - echo "${{ secrets.CI_GIT_TOKEN }}" >> emqx/scripts/git-token - echo "PROFILE=emqx-ee" >> $GITHUB_ENV - else - echo "PROFILE=emqx" >> $GITHUB_ENV - fi - - name: get version - run: | - set -e -x -u - cd emqx - if [ $PROFILE = "emqx" ];then - broker="emqx-ce" - edition='opensource' - else - broker="emqx-ee" - edition='enterprise' - fi - echo "BROKER=$broker" >> $GITHUB_ENV + sed -i -r "s/^appVersion: .*$/appVersion: \"$EMQX_TAG\"/g" deploy/charts/emqx/Chart.yaml - vsn="$(./pkg-vsn.sh)" - echo "VSN=$vsn" >> $GITHUB_ENV + helm install emqx \ + --set image.repository=$TARGET \ + --set image.pullPolicy=Never \ + --set emqxAclConfig="" \ + --set image.pullPolicy=Never \ + --set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s \ + --set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10 \ + deploy/charts/emqx \ + --debug - pre_vsn="$(echo $vsn | grep -oE '^[0-9]+.[0-9]')" - if [ $PROFILE = "emqx" ]; then - old_vsns="$(git tag -l "v$pre_vsn.[0-9]" | xargs echo -n | sed "s/v$vsn//")" - else - old_vsns="$(git tag -l "e$pre_vsn.[0-9]" | xargs echo -n | sed "s/e$vsn//")" - fi - echo "OLD_VSNS=$old_vsns" >> $GITHUB_ENV - - name: download emqx - run: | - set -e -x -u - mkdir -p emqx/_upgrade_base - cd emqx/_upgrade_base - old_vsns=($(echo $OLD_VSNS | tr ' ' ' ')) - for old_vsn in ${old_vsns[@]}; do - wget --no-verbose https://s3-us-west-2.amazonaws.com/packages.emqx/$BROKER/$old_vsn/$PROFILE-ubuntu20.04-${old_vsn#[e|v]}-amd64.zip - done - - name: build emqx - run: make -C emqx ${PROFILE}-zip - - name: build emqtt-bench - run: make -C emqtt-bench - - name: build lux - run: | - set -e -u -x - cd lux - autoconf - ./configure - make - make install - - name: run relup test - timeout-minutes: 20 - run: | - set -e -x -u - if [ -n "$OLD_VSNS" ]; then - mkdir -p packages - cp emqx/_packages/${PROFILE}/*.zip packages - cp emqx/_upgrade_base/*.zip packages - lux \ - --case_timeout infinity \ - --var PROFILE=$PROFILE \ - --var PACKAGE_PATH=$(pwd)/packages \ - --var BENCH_PATH=$(pwd)/emqtt-bench \ - --var ONE_MORE_EMQX_PATH=$(pwd)/one_more_emqx \ - --var VSN="$VSN" \ - --var OLD_VSNS="$OLD_VSNS" \ - emqx/.ci/fvt_tests/relup.lux - fi - - uses: actions/upload-artifact@v1 - if: failure() - with: - name: lux_logs - path: lux_logs + while [ "$(kubectl get StatefulSet -l app.kubernetes.io/name=emqx -o jsonpath='{.items[0].status.replicas}')" \ + != "$(kubectl get StatefulSet -l app.kubernetes.io/name=emqx -o jsonpath='{.items[0].status.readyReplicas}')" ]; do + echo "=============================="; + kubectl get pods; + echo "=============================="; + echo "waiting emqx started"; + sleep 10; + done + - name: get emqx-0 pods log + if: failure() + run: | + kubectl describe pods emqx-0 + kubectl logs emqx-0 + - name: get emqx-1 pods log + if: failure() + run: | + kubectl describe pods emqx-1 + kubectl logs emqx-1 + - name: get emqx-2 pods log + if: failure() + run: | + kubectl describe pods emqx-2 + kubectl logs emqx-2 + - uses: actions/checkout@v2 + with: + repository: emqx/paho.mqtt.testing + ref: develop-4.0 + path: paho.mqtt.testing + - name: install pytest + run: | + pip install pytest + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: run paho test + run: | + kubectl port-forward service/emqx 1883:1883 > /dev/null & + pytest -v paho.mqtt.testing/interoperability/test_client/V5/test_connect.py -k test_basic --host "127.0.0.1" diff --git a/.github/workflows/run_relup_tests.yaml b/.github/workflows/run_relup_tests.yaml new file mode 100644 index 000000000..312ef1152 --- /dev/null +++ b/.github/workflows/run_relup_tests.yaml @@ -0,0 +1,130 @@ +name: Release Upgrade Tests + +on: + push: + tags: + - v* + - e* + pull_request: + +jobs: + relup_test: + strategy: + matrix: + container: + - "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" + - "emqx/build-env:erl24.0.5-emqx-1-ubuntu20.04" + + runs-on: ubuntu-20.04 + container: ${{ matrix.container }} + + defaults: + run: + shell: bash + steps: + - uses: actions/setup-python@v2 + with: + python-version: '3.8' + architecture: 'x64' + - uses: actions/checkout@v2 + with: + repository: emqx/paho.mqtt.testing + ref: develop-4.0 + path: paho.mqtt.testing + - uses: actions/checkout@v2 + with: + repository: terry-xiaoyu/one_more_emqx + ref: master + path: one_more_emqx + - uses: actions/checkout@v2 + with: + repository: emqx/emqtt-bench + ref: master + path: emqtt-bench + - uses: actions/checkout@v2 + with: + repository: hawk/lux + ref: lux-2.6 + path: lux + - uses: actions/checkout@v2 + with: + repository: ${{ github.repository }} + path: emqx + fetch-depth: 0 + - name: prepare + run: | + if make -C emqx emqx-ee --dry-run > /dev/null 2>&1; then + echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials + git config --global credential.helper store + echo "${{ secrets.CI_GIT_TOKEN }}" >> emqx/scripts/git-token + echo "PROFILE=emqx-ee" >> $GITHUB_ENV + else + echo "PROFILE=emqx" >> $GITHUB_ENV + fi + - name: get version + run: | + set -e -x -u + cd emqx + if [ $PROFILE = "emqx" ];then + broker="emqx-ce" + edition='opensource' + else + broker="emqx-ee" + edition='enterprise' + fi + echo "BROKER=$broker" >> $GITHUB_ENV + + vsn="$(./pkg-vsn.sh)" + echo "VSN=$vsn" >> $GITHUB_ENV + + pre_vsn="$(echo $vsn | grep -oE '^[0-9]+.[0-9]')" + if [ $PROFILE = "emqx" ]; then + old_vsns="$(git tag -l "v$pre_vsn.[0-9]" | xargs echo -n | sed "s/v$vsn//")" + else + old_vsns="$(git tag -l "e$pre_vsn.[0-9]" | xargs echo -n | sed "s/e$vsn//")" + fi + echo "OLD_VSNS=$old_vsns" >> $GITHUB_ENV + - name: download emqx + run: | + set -e -x -u + mkdir -p emqx/_upgrade_base + cd emqx/_upgrade_base + old_vsns=($(echo $OLD_VSNS | tr ' ' ' ')) + for old_vsn in ${old_vsns[@]}; do + wget --no-verbose https://s3-us-west-2.amazonaws.com/packages.emqx/$BROKER/$old_vsn/$PROFILE-ubuntu20.04-${old_vsn#[e|v]}-amd64.zip + done + - name: build emqx + run: make -C emqx ${PROFILE}-zip + - name: build emqtt-bench + run: make -C emqtt-bench + - name: build lux + run: | + set -e -u -x + cd lux + autoconf + ./configure + make + make install + - name: run relup test + timeout-minutes: 20 + run: | + set -e -x -u + if [ -n "$OLD_VSNS" ]; then + mkdir -p packages + cp emqx/_packages/${PROFILE}/*.zip packages + cp emqx/_upgrade_base/*.zip packages + lux \ + --case_timeout infinity \ + --var PROFILE=$PROFILE \ + --var PACKAGE_PATH=$(pwd)/packages \ + --var BENCH_PATH=$(pwd)/emqtt-bench \ + --var ONE_MORE_EMQX_PATH=$(pwd)/one_more_emqx \ + --var VSN="$VSN" \ + --var OLD_VSNS="$OLD_VSNS" \ + emqx/.ci/fvt_tests/relup.lux + fi + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: lux_logs + path: lux_logs diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index c2b30ed3d..b97c3c003 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -98,19 +98,19 @@ jobs: - name: run cover run: | printenv > .env - docker exec -i ${{ matrix.otp_release }} bash -c "make cover" - docker exec --env-file .env -i ${{ matrix.otp_release }} bash -c "make coveralls" + docker exec -i ${{ matrix.otp_release }} bash -c "DIAGNOSTIC=1 make cover" + docker exec --env-file .env -i ${{ matrix.otp_release }} bash -c "DIAGNOSTIC=1 make coveralls" - name: cat rebar.crashdump if: failure() run: if [ -f 'rebar3.crashdump' ];then cat 'rebar3.crashdump'; fi - uses: actions/upload-artifact@v1 if: failure() with: - name: logs + name: logs_${{ matrix.otp_release }} path: _build/test/logs - uses: actions/upload-artifact@v1 with: - name: cover + name: cover_${{ matrix.otp_release }} path: _build/test/cover finish: diff --git a/Makefile b/Makefile index dc44d208b..e646f5b7a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.4 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.11 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif diff --git a/README-CN.md b/README-CN.md index b430d4b5f..80e926199 100644 --- a/README-CN.md +++ b/README-CN.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](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) @@ -90,7 +90,7 @@ make eunit ct ### 执行部分应用的 common tests ```bash -make apps/emqx_bridge_mqtt-ct +make apps/emqx_retainer-ct ``` ### 静态分析(Dialyzer) diff --git a/README-JP.md b/README-JP.md index 6e1c62f2f..57c9a1809 100644 --- a/README-JP.md +++ b/README-JP.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](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) @@ -84,7 +84,7 @@ make eunit ct ### common test の一部を実行する ```bash -make apps/emqx_bridge_mqtt-ct +make apps/emqx_retainer-ct ``` ### Dialyzer diff --git a/README-RU.md b/README-RU.md index 2a06dac71..e02f47aa4 100644 --- a/README-RU.md +++ b/README-RU.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](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) @@ -93,7 +93,7 @@ make eunit ct Пример: ```bash -make apps/emqx_bridge_mqtt-ct +make apps/emqx_retainer-ct ``` ### Dialyzer diff --git a/README.md b/README.md index 1726d426b..f60ed3cd9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Follow-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) @@ -92,7 +92,7 @@ make eunit ct Examples ```bash -make apps/emqx_bridge_mqtt-ct +make apps/emqx_retainer-ct ``` ### Dialyzer diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 5200f5239..6834d2a6e 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1,3 +1,997 @@ +##================================================================== +## Listeners +##================================================================== +## MQTT/TCP - TCP Listeners for MQTT Protocol +## syntax: listeners.tcp. +## example: listeners.tcp.my_tcp_listener +listeners.tcp.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.tcp..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 1883, 127.0.0.1:1883, ::1:1883 + bind = "0.0.0.0:1883" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.tcp..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.tcp..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.tcp..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections = 1024000 + + ## The access control rules for this listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## @doc listeners.tcp..access_rules + ## ValueType: Array + ## Default: [] + ## Examples: + ## access_rules: [ + ## "deny 192.168.0.0/24", + ## "all all" + ## ] + access_rules = [ + "allow all" + ] + + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## @doc listeners.tcp..proxy_protocol + ## ValueType: Boolean + ## Default: false + proxy_protocol = false + + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet received within the timeout. + ## + ## @doc listeners.tcp..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout = 3s + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + ## The prefixed string will be removed from the topic name when the message + ## is delivered to the subscriber. The mountpoint is a way that users can use + ## to implement isolation of message routing between different listeners. + ## + ## For example if a clientA subscribes to "t" with `listeners.tcp..mountpoint` + ## set to "some_tenant", then the client accually subscribes to the topic + ## "some_tenant/t". Similarly if another clientB (connected to the same listener + ## with the clientA) send a message to topic "t", the message is accually route + ## to all the clients subscribed "some_tenant/t", so clientA will receive the + ## message, with topic name "t". + ## + ## Set to "" to disable the feature. + ## + ## Variables in mountpoint string: + ## - %c: clientid + ## - %u: username + ## + ## @doc listeners.tcp..mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog = 1024 + tcp.buffer = 4KB +} + +## MQTT/SSL - SSL Listeners for MQTT Protocol +## syntax: listeners.ssl. +## example: listeners.ssl.my_ssl_listener +listeners.ssl.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.ssl..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8883, 127.0.0.1:8883, ::1:8883 + bind = "0.0.0.0:8883" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.ssl..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.ssl..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.ssl..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections = 512000 + + ## The access control rules for this listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## @doc listeners.ssl..access_rules + ## ValueType: Array + ## Default: [] + ## Examples: + ## access_rules: [ + ## "deny 192.168.0.0/24", + ## "all all" + ## ] + access_rules = [ + "allow all" + ] + + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## @doc listeners.ssl..proxy_protocol + ## ValueType: Boolean + ## Default: true + proxy_protocol = false + + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet received within the timeout. + ## + ## @doc listeners.ssl..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout = 3s + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + ## The prefixed string will be removed from the topic name when the message + ## is delivered to the subscriber. The mountpoint is a way that users can use + ## to implement isolation of message routing between different listeners. + ## + ## For example if a clientA subscribes to "t" with `listeners.ssl..mountpoint` + ## set to "some_tenant", then the client accually subscribes to the topic + ## "some_tenant/t". Similarly if another clientB (connected to the same listener + ## with the clientA) send a message to topic "t", the message is accually route + ## to all the clients subscribed "some_tenant/t", so clientA will receive the + ## message, with topic name "t". + ## + ## Set to "" to disable the feature. + ## + ## Variables in mountpoint string: + ## - %c: clientid + ## - %u: username + ## + ## @doc listeners.ssl..mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" + + ## SSL options + ## See ${example_common_ssl_options} for more information + ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog = 1024 + tcp.buffer = 4KB +} + +## MQTT/QUIC - QUIC Listeners for MQTT Protocol +## syntax: listeners.quic. +## example: listeners.quic.my_quic_listener +listeners.quic.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.quic..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 14567, 127.0.0.1:14567, ::1:14567 + bind = "0.0.0.0:14567" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## NOTE: This is a cluster-wide configuration. + ## It requires all nodes to be stopped before changing it. + ## + ## @doc listeners.quic..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.quic..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.quic..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections = 1024000 + + ## Path to the file containing the user's private PEM-encoded key. + ## + ## @doc listeners.quic..keyfile + ## ValueType: String + ## Default: "{{ platform_etc_dir }}/certs/key.pem" + keyfile = "{{ platform_etc_dir }}/certs/key.pem" + + ## Path to a file containing the user certificate. + ## + ## @doc listeners.quic..certfile + ## ValueType: String + ## Default: "{{ platform_etc_dir }}/certs/cert.pem" + certfile = "{{ platform_etc_dir }}/certs/cert.pem" + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + ## The prefixed string will be removed from the topic name when the message + ## is delivered to the subscriber. The mountpoint is a way that users can use + ## to implement isolation of message routing between different listeners. + ## + ## For example if a clientA subscribes to "t" with `listeners.quic..mountpoint` + ## set to "some_tenant", then the client accually subscribes to the topic + ## "some_tenant/t". Similarly if another clientB (connected to the same listener + ## with the clientA) send a message to topic "t", the message is accually route + ## to all the clients subscribed "some_tenant/t", so clientA will receive the + ## message, with topic name "t". + ## + ## Set to "" to disable the feature. + ## + ## Variables in mountpoint string: + ## - %c: clientid + ## - %u: username + ## + ## @doc listeners.quic..mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" +} + +## MQTT/WS - Websocket Listeners for MQTT Protocol +## syntax: listeners.ws. +## example: listeners.ws.my_ws_listener +listeners.ws.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.ws..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8083, 127.0.0.1:8083, ::1:8083 + bind = "0.0.0.0:8083" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.ws..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.ws..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.ws..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections = 1024000 + + ## The access control rules for this listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## @doc listeners.ws..access_rules + ## ValueType: Array + ## Default: [] + ## Examples: + ## access_rules: [ + ## "deny 192.168.0.0/24", + ## "all all" + ## ] + access_rules = [ + "allow all" + ] + + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## @doc listeners.ws..proxy_protocol + ## ValueType: Boolean + ## Default: true + proxy_protocol = false + + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet received within the timeout. + ## + ## @doc listeners.ws..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout = 3s + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + ## The prefixed string will be removed from the topic name when the message + ## is delivered to the subscriber. The mountpoint is a way that users can use + ## to implement isolation of message routing between different listeners. + ## + ## For example if a clientA subscribes to "t" with `listeners.ws..mountpoint` + ## set to "some_tenant", then the client accually subscribes to the topic + ## "some_tenant/t". Similarly if another clientB (connected to the same listener + ## with the clientA) send a message to topic "t", the message is accually route + ## to all the clients subscribed "some_tenant/t", so clientA will receive the + ## message, with topic name "t". + ## + ## Set to "" to disable the feature. + ## + ## Variables in mountpoint string: + ## - %c: clientid + ## - %u: username + ## + ## @doc listeners.ws..mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog = 1024 + tcp.buffer = 4KB + + ## Websocket options + ## See ${example_common_websocket_options} for more information + websocket.idle_timeout = 86400s +} + +## MQTT/WSS - WebSocket Secure Listeners for MQTT Protocol +## syntax: listeners.wss. +## example: listeners.wss.my_wss_listener +listeners.wss.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.wss..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8084, 127.0.0.1:8084, ::1:8084 + bind = "0.0.0.0:8084" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.wss..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.wss..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.wss..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections = 512000 + + ## The access control rules for this listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## @doc listeners.wss..access_rules + ## ValueType: Array + ## Default: [] + ## Examples: + ## access_rules: [ + ## "deny 192.168.0.0/24", + ## "all all" + ## ] + access_rules = [ + "allow all" + ] + + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## @doc listeners.wss..proxy_protocol + ## ValueType: Boolean + ## Default: true + proxy_protocol = false + + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet received within the timeout. + ## + ## @doc listeners.wss..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout = 3s + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + ## The prefixed string will be removed from the topic name when the message + ## is delivered to the subscriber. The mountpoint is a way that users can use + ## to implement isolation of message routing between different listeners. + ## + ## For example if a clientA subscribes to "t" with `listeners.wss..mountpoint` + ## set to "some_tenant", then the client accually subscribes to the topic + ## "some_tenant/t". Similarly if another clientB (connected to the same listener + ## with the clientA) send a message to topic "t", the message is accually route + ## to all the clients subscribed "some_tenant/t", so clientA will receive the + ## message, with topic name "t". + ## + ## Set to "" to disable the feature. + ## + ## Variables in mountpoint string: + ## - %c: clientid + ## - %u: username + ## + ## @doc listeners.wss..mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" + + ## SSL options + ## See ${example_common_ssl_options} for more information + ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog = 1024 + tcp.buffer = 4KB + + ## Websocket options + ## See ${example_common_websocket_options} for more information + websocket.idle_timeout = 86400s + +} + +## Enable per connection statistics. +## +## @doc stats.enable +## ValueType: Boolean +## Default: true +stats.enable = true + +authorization { + ## Behaviour after not matching a rule. + ## + ## @doc authorization.no_match + ## ValueType: allow | deny + ## Default: allow + no_match: allow + + ## The action when authorization check reject current operation + ## + ## @doc authorization.deny_action + ## ValueType: ignore | disconnect + ## Default: ignore + deny_action: ignore + + ## Whether to enable Authorization cache. + ## + ## If enabled, Authorization roles for each client will be cached in the memory + ## + ## @doc authorization.cache.enable + ## ValueType: Boolean + ## Default: true + cache.enable: true + + ## The maximum count of Authorization entries can be cached for a client. + ## + ## @doc authorization.cache.max_size + ## ValueType: Integer + ## Range: [0, 1048576] + ## Default: 32 + cache.max_size: 32 + + ## The time after which an Authorization cache entry will be deleted + ## + ## @doc authorization.cache.ttl + ## ValueType: Duration + ## Default: 1m + cache.ttl: 1m +} + +mqtt { + ## How long time the MQTT connection will be disconnected if the + ## TCP connection is established but MQTT CONNECT has not been + ## received. + ## + ## @doc mqtt.idle_timeout + ## ValueType: Duration + ## Default: 15s + idle_timeout = 15s + + ## Maximum MQTT packet size allowed. + ## + ## @doc mqtt.max_packet_size + ## ValueType: Bytes + ## Default: 1MB + max_packet_size = 1MB + + ## Maximum length of MQTT clientId allowed. + ## + ## @doc mqtt.max_clientid_len + ## ValueType: Integer + ## Range: [23, 65535] + ## Default: 65535 + max_clientid_len = 65535 + + ## Maximum topic levels allowed. + ## + ## @doc mqtt.max_topic_levels + ## ValueType: Integer + ## Range: [1, 65535] + ## Default: 65535 + max_topic_levels = 65535 + + ## Maximum QoS allowed. + ## + ## @doc mqtt.max_qos_allowed + ## ValueType: 0 | 1 | 2 + ## Default: 2 + max_qos_allowed = 2 + + ## Maximum Topic Alias, 0 means no topic alias supported. + ## + ## @doc mqtt.max_topic_alias + ## ValueType: Integer + ## Range: [0, 65535] + ## Default: 65535 + max_topic_alias = 65535 + + ## Whether the Server supports MQTT retained messages. + ## + ## @doc mqtt.retain_available + ## ValueType: Boolean + ## Default: true + retain_available = true + + ## Whether the Server supports MQTT Wildcard Subscriptions + ## + ## @doc mqtt.wildcard_subscription + ## ValueType: Boolean + ## Default: true + wildcard_subscription = true + + ## Whether the Server supports MQTT Shared Subscriptions. + ## + ## @doc mqtt.shared_subscription + ## ValueType: Boolean + ## Default: true + shared_subscription = true + + ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) + ## + ## @doc mqtt.ignore_loop_deliver + ## ValueType: Boolean + ## Default: false + ignore_loop_deliver = false + + ## Whether to parse the MQTT frame in strict mode + ## + ## @doc mqtt.strict_mode + ## ValueType: Boolean + ## Default: false + strict_mode = false + + ## Specify the response information returned to the client + ## + ## This feature is disabled if is set to "" + ## + ## @doc mqtt.response_information + ## ValueType: String + ## Default: "" + response_information = "" + + ## Server Keep Alive of MQTT 5.0 + ## + ## @doc mqtt.server_keepalive + ## ValueType: Number | disabled + ## Default: disabled + server_keepalive = disabled + + ## The backoff for MQTT keepalive timeout. The broker will kick a connection out + ## until 'Keepalive * backoff * 2' timeout. + ## + ## @doc mqtt.keepalive_backoff + ## ValueType: Float + ## Range: (0.5, 1] + ## Default: 0.75 + keepalive_backoff = 0.75 + + ## Maximum number of subscriptions allowed. + ## + ## @doc mqtt.max_subscriptions + ## ValueType: Integer | infinity + ## Range: [1, infinity) + ## Default: infinity + max_subscriptions = infinity + + ## Force to upgrade QoS according to subscription. + ## + ## @doc mqtt.upgrade_qos + ## ValueType: Boolean + ## Default: false + upgrade_qos = false + + ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. + ## + ## @doc mqtt.max_inflight + ## ValueType: Integer + ## Range: [1, 65535] + ## Default: 32 + max_inflight = 32 + + ## Retry interval for QoS1/2 message delivering. + ## + ## @doc mqtt.retry_interval + ## ValueType: Duration + ## Default: 30s + retry_interval = 30s + + ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL. + ## + ## @doc mqtt.max_awaiting_rel + ## ValueType: Integer | infinity + ## Range: [1, infinity) + ## Default: 100 + max_awaiting_rel = 100 + + ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. + ## + ## @doc mqtt.await_rel_timeout + ## ValueType: Duration + ## Default: 300s + await_rel_timeout = 300s + + ## Default session expiry interval for MQTT V3.1.1 connections. + ## + ## @doc mqtt.session_expiry_interval + ## ValueType: Duration + ## Default: 2h + session_expiry_interval = 2h + + ## Maximum queue length. Enqueued messages when persistent client disconnected, + ## or inflight window is full. + ## + ## @doc mqtt.max_mqueue_len + ## ValueType: Integer | infinity + ## Range: [0, infinity) + ## Default: 1000 + max_mqueue_len = 1000 + + ## Topic priorities. + ## + ## There's no priority table by default, hence all messages + ## are treated equal. + ## + ## Priority number [1-255] + ## + ## NOTE: comma and equal signs are not allowed for priority topic names + ## NOTE: Messages for topics not in the priority table are treated as + ## either highest or lowest priority depending on the configured + ## value for mqtt.mqueue_default_priority + ## + ## @doc mqtt.mqueue_priorities + ## ValueType: Map | disabled + ## Examples: + ## To configure "topic/1" > "topic/2": + ## mqueue_priorities: {"topic/1": 10, "topic/2": 8} + ## Default: disabled + mqueue_priorities = disabled + + ## Default to highest priority for topics not matching priority table + ## + ## @doc mqtt.mqueue_default_priority + ## ValueType: highest | lowest + ## Default: lowest + mqueue_default_priority = lowest + + ## Whether to enqueue QoS0 messages. + ## + ## @doc mqtt.mqueue_store_qos0 + ## ValueType: Boolean + ## Default: true + mqueue_store_qos0 = true + + ## Whether use username replace client id + ## + ## @doc mqtt.use_username_as_clientid + ## ValueType: Boolean + ## Default: false + use_username_as_clientid = false + + ## Use the CN, DN or CRT field from the client certificate as a username. + ## Only works for SSL connection. + ## + ## @doc mqtt.peer_cert_as_username + ## ValueType: cn | dn | crt | disabled + ## Default: disabled + peer_cert_as_username = disabled + + ## Use the CN, DN or CRT field from the client certificate as a clientid. + ## Only works for SSL connection. + ## + ## @doc mqtt.peer_cert_as_clientid + ## ValueType: cn | dn | crt | disabled + ## Default: disabled + peer_cert_as_clientid = disabled +} + +flapping_detect { + ## Enable Flapping Detection. + ## + ## This config controls the allowed maximum number of CONNECT received + ## from the same clientid in a time frame defined by `window_time`. + ## After the limit is reached, successive CONNECT requests are forbidden + ## (banned) until the end of the time period defined by `ban_time`. + ## + ## @doc flapping_detect.enable + ## ValueType: Boolean + ## Default: true + enable = false + + ## The max disconnect allowed of a MQTT Client in `window_time` + ## + ## @doc flapping_detect.max_count + ## ValueType: Integer + ## Default: 15 + max_count = 15 + + ## The time window for flapping detect + ## + ## @doc flapping_detect.window_time + ## ValueType: Duration + ## Default: 1m + window_time = 1m + + ## How long the clientid will be banned + ## + ## @doc flapping_detect.ban_time + ## ValueType: Duration + ## Default: 5m + ban_time = 5m + +} + +force_shutdown { + ## Enable force_shutdown + ## + ## @doc force_shutdown.enable + ## ValueType: Boolean + ## Default: true + enable = true + + ## Max message queue length + ## @doc force_shutdown.max_message_queue_len + ## ValueType: Integer + ## Range: (0, infinity) + ## Default: 1000 + max_message_queue_len = 1000 + + ## Total heap size + ## + ## @doc force_shutdown.max_heap_size + ## ValueType: Size + ## Default: 32MB + max_heap_size = 32MB +} + +force_gc { + ## Force the MQTT connection process GC after this number of + ## messages or bytes passed through. + ## + ## @doc force_gc.enable + ## ValueType: Boolean + ## Default: true + enable = true + + ## GC the process after how many messages received + ## @doc force_gc.max_message_queue_len + ## ValueType: Integer + ## Range: (0, infinity) + ## Default: 16000 + count = 16000 + + ## GC the process after how much bytes passed through + ## + ## @doc force_gc.bytes + ## ValueType: Size + ## Default: 16MB + bytes = 16MB +} + +conn_congestion { + ## Whether to alarm the congested connections. + ## + ## Sometimes the mqtt connection (usually an MQTT subscriber) may + ## get "congested" because there're too many packets to sent. + ## The socket trys to buffer the packets until the buffer is + ## full. If more packets comes after that, the packets will be + ## "pending" in a queue and we consider the connection is + ## "congested". + ## + ## Enable this to send an alarm when there's any bytes pending in + ## the queue. You could set the `sndbuf` to a larger value if the + ## alarm is triggered too often. + ## + ## The name of the alarm is of format "conn_congestion//". + ## Where the is the client-id of the congested MQTT connection. + ## And the is the username or "unknown_user" of not provided by the client. + ## + ## @doc conn_congestion.enable_alarm + ## ValueType: Boolean + ## Default: true + enable_alarm = true + + ## Won't clear the congested alarm in how long time. + ## The alarm is cleared only when there're no pending bytes in + ## the queue, and also it has been `min_alarm_sustain_duration` + ## time since the last time we considered the connection is "congested". + ## + ## This is to avoid clearing and sending the alarm again too often. + ## + ## @doc conn_congestion.min_alarm_sustain_duration + ## ValueType: Duration + ## Default: 1m + min_alarm_sustain_duration = 1m +} + +rate_limit { + ## Maximum connections per second. + ## + ## @doc zones..max_conn_rate + ## ValueType: Number | infinity + ## Default: 1000 + ## Examples: + ## max_conn_rate: 1000 + max_conn_rate = 1000 + + ## Message limit for the a external MQTT connection. + ## + ## @doc rate_limit.conn_messages_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messages per 10 seconds. + ## conn_messages_in: "100,10s" + conn_messages_in = "100,10s" + + ## Limit the rate of receiving packets for a MQTT connection. + ## The rate is counted by bytes of packets per second. + ## + ## The connection won't accept more messages if the messages come + ## faster than the limit. + ## + ## @doc rate_limit.conn_bytes_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100KB incoming per 10 seconds. + ## conn_bytes_in: "100KB,10s" + ## + conn_bytes_in = "100KB,10s" +} + +quota { + ## Messages quota for the each of external MQTT connection. + ## This value consumed by the number of recipient on a message. + ## + ## @doc quota.conn_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messaegs per 1s: + ## quota.conn_messages_routing: "100,1s" + conn_messages_routing = "100,1s" + + ## Messages quota for the all of external MQTT connections. + ## This value consumed by the number of recipient on a message. + ## + ## @doc quota.overall_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 200000 messages per 1s: + ## quota.overall_messages_routing: "200000,1s" + ## + overall_messages_routing = "200000,1s" +} + +##================================================================== +## Zones +##================================================================== +## A zone contains a set of configurations for listeners. +## +## A zone can be used by a listener via `listener...zone`. +## +## The configs defined in zones will override the global configs with the same key. +## +## For example given the following config: +## +## ``` +## a { +## b: 1, c: 1 +## } +## +## zone.my_zone { +## a { +## b:2 +## } +## } +## ``` +## +## The global config "a" is overridden by the configs "a" inside the zone "my_zone". +## If there is a listener uses the zone "my_zone", the value of config "a" will be: +## `{b:2, c: 1}`. +## Note that although the default value of `a.c` is `0`, the global value is used. +## i.e. configs in the zone have no default values. To overridde `a.c` we must configure +## it explicitly in the zone. +## +## All the global configs that can be overridden in zones are: +## - `stats.*` +## - `mqtt.*` +## - `authorization.*` +## - `flapping_detect.*` +## - `force_shutdown.*` +## - `conn_congestion.*` +## - `rate_limit.*` +## - `quota.*` +## - `force_gc.*` +## +## syntax: zones. +## example: zones.my_zone +zones.default { + +} + ##================================================================== ## Broker ##================================================================== @@ -7,7 +1001,7 @@ broker { ## @doc broker.sys_msg_interval ## ValueType: Duration | disabled ## Default: 1m - sys_msg_interval: 1m + sys_msg_interval = 1m ## System heartbeat interval of publishing following heart beat message: ## - "$SYS/brokers//uptime" @@ -16,7 +1010,7 @@ broker { ## @doc broker.sys_heartbeat_interval ## ValueType: Duration ## Default: 30s | disabled - sys_heartbeat_interval: 30s + sys_heartbeat_interval = 30s ## Session locking strategy in a cluster. ## @@ -27,7 +1021,7 @@ broker { ## - quorum: select some nodes to lock the session ## - all: lock the session on all of the nodes in the cluster ## Default: quorum - session_locking_strategy: quorum + session_locking_strategy = quorum ## Dispatch strategy for shared subscription ## @@ -39,7 +1033,7 @@ broker { ## until the susbcriber disconnected. ## - hash: select the subscribers by the hash of clientIds ## Default: round_robin - shared_subscription_strategy: round_robin + shared_subscription_strategy = round_robin ## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages ## This should allow messages to be dispatched to a different subscriber in @@ -48,14 +1042,14 @@ broker { ## @doc broker.shared_dispatch_ack_enabled ## ValueType: Boolean ## Default: false - shared_dispatch_ack_enabled: false + shared_dispatch_ack_enabled = false ## Enable batch clean for deleted routes. ## ## @doc broker.route_batch_clean ## ValueType: Boolean ## Default: true - route_batch_clean: true + route_batch_clean = true ## Performance toggle for subscribe/unsubscribe wildcard topic. ## Change this toggle only when there are many wildcard topics. @@ -69,7 +1063,7 @@ broker { ## - tab: mnesia translational updates with table lock. recommended for multi-nodes setup. ## - global: global lock protected updates. recommended for larger cluster. ## Default: key - perf.route_lock_type: key + perf.route_lock_type = key ## Enable trie path compaction. ## Enabling it significantly improves wildcard topic subscribe @@ -80,1094 +1074,12 @@ broker { ## 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. + ## It requires all nodes to be stopped before changing it. ## ## @doc broker.perf.trie_compaction ## ValueType: Boolean ## Default: true - perf.trie_compaction: true -} - -##================================================================== -## Zones and Listeners -##================================================================== -## A zone contains a set of configurations for listeners. -## -## The configurations defined in zone can be overridden by the ones -## defined in listeners with the same key. -## -## For example given the following config: -## ``` -## -## zone.x { -## a: {b:1, c: 1} -## listeners.y { -## a: {b: 2} -## } -## } -## ``` -## The config "a" in zone "x" is overridden by the configs inside -## the listener "y". So the value of config "a" in listener "y" -## is `a: {b:2, c: 1}`. -## -## All the configs that can be set in zones and be overridden in listenser are: -## - `auth.*` -## - `stats.*` -## - `mqtt.*` -## - `authorization.*` -## - `flapping_detect.*` -## - `force_shutdown.*` -## - `conn_congestion.*` -## -## Syntax: zones. {} -zones.default { - ## Enable authentication - ## - ## @doc zones..auth.enable - ## ValueType: Boolean - ## Default: false - auth.enable: false - - ## Enable per connection statistics. - ## - ## @doc zones..stats.enable - ## ValueType: Boolean - ## Default: true - stats.enable: true - - ## Maximum number of concurrent connections in this zone. - ## - ## This value must be larger than the sum of `max_connections` set - ## in the listeners under this zone. - ## - ## @doc zones..overall_max_connections - ## ValueType: Number | infinity - ## Default: infinity - overall_max_connections: infinity - - mqtt { - ## When publishing or subscribing, prefix all topics with a mountpoint string. - ## The prefixed string will be removed from the topic name when the message - ## is delivered to the subscriber. The mountpoint is a way that users can use - ## to implement isolation of message routing between different listeners. - ## - ## For example if a clientA subscribes to "t" with `zones..mqtt.mountpoint` - ## set to "some_tenant", then the client accually subscribes to the topic - ## "some_tenant/t". Similarly if another clientB (connected to the same listener - ## with the clientA) send a message to topic "t", the message is accually route - ## to all the clients subscribed "some_tenant/t", so clientA will receive the - ## message, with topic name "t". - ## - ## Set to "" to disable the feature. - ## - ## Variables in mountpoint string: - ## - %c: clientid - ## - %u: username - ## - ## @doc zones..listeners..mountpoint - ## ValueType: String - ## Default: "" - mountpoint: "" - - ## How long time the MQTT connection will be disconnected if the - ## TCP connection is established but MQTT CONNECT has not been - ## received. - ## - ## @doc zones..mqtt.idle_timeout - ## ValueType: Duration - ## Default: 15s - idle_timeout: 15s - - ## Maximum MQTT packet size allowed. - ## - ## @doc zones..mqtt.max_packet_size - ## ValueType: Bytes - ## Default: 1MB - max_packet_size: 1MB - - ## Maximum length of MQTT clientId allowed. - ## - ## @doc zones..mqtt.max_clientid_len - ## ValueType: Integer - ## Range: [23, 65535] - ## Default: 65535 - max_clientid_len: 65535 - - ## Maximum topic levels allowed. - ## - ## @doc zones..mqtt.max_topic_levels - ## ValueType: Integer - ## Range: [1, 65535] - ## Default: 65535 - max_topic_levels: 65535 - - ## Maximum QoS allowed. - ## - ## @doc zones..mqtt.max_qos_allowed - ## ValueType: 0 | 1 | 2 - ## Default: 2 - max_qos_allowed: 2 - - ## Maximum Topic Alias, 0 means no topic alias supported. - ## - ## @doc zones..mqtt.max_topic_alias - ## ValueType: Integer - ## Range: [0, 65535] - ## Default: 65535 - max_topic_alias: 65535 - - ## Whether the Server supports MQTT retained messages. - ## - ## @doc zones..mqtt.retain_available - ## ValueType: Boolean - ## Default: true - retain_available: true - - ## Whether the Server supports MQTT Wildcard Subscriptions - ## - ## @doc zones..mqtt.wildcard_subscription - ## ValueType: Boolean - ## Default: true - wildcard_subscription: true - - ## Whether the Server supports MQTT Shared Subscriptions. - ## - ## @doc zones..mqtt.shared_subscription - ## ValueType: Boolean - ## Default: true - shared_subscription: true - - ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) - ## - ## @doc zones..mqtt.ignore_loop_deliver - ## ValueType: Boolean - ## Default: false - ignore_loop_deliver: false - - ## Whether to parse the MQTT frame in strict mode - ## - ## @doc zones..mqtt.strict_mode - ## ValueType: Boolean - ## Default: false - strict_mode: false - - ## Specify the response information returned to the client - ## - ## This feature is disabled if is set to "" - ## - ## @doc zones..mqtt.response_information - ## ValueType: String - ## Default: "" - response_information: "" - - ## Server Keep Alive of MQTT 5.0 - ## - ## @doc zones..mqtt.server_keepalive - ## ValueType: Number | disabled - ## Default: disabled - server_keepalive: disabled - - ## The backoff for MQTT keepalive timeout. The broker will kick a connection out - ## until 'Keepalive * backoff * 2' timeout. - ## - ## @doc zones..mqtt.keepalive_backoff - ## ValueType: Float - ## Range: (0.5, 1] - ## Default: 0.75 - keepalive_backoff: 0.75 - - ## Maximum number of subscriptions allowed. - ## - ## @doc zones..mqtt.max_subscriptions - ## ValueType: Integer | infinity - ## Range: [1, infinity) - ## Default: infinity - max_subscriptions: infinity - - ## Force to upgrade QoS according to subscription. - ## - ## @doc zones..mqtt.upgrade_qos - ## ValueType: Boolean - ## Default: false - upgrade_qos: false - - ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. - ## - ## @doc zones..mqtt.max_inflight - ## ValueType: Integer - ## Range: [1, 65535] - ## Default: 32 - max_inflight: 32 - - ## Retry interval for QoS1/2 message delivering. - ## - ## @doc zones..mqtt.retry_interval - ## ValueType: Duration - ## Default: 30s - retry_interval: 30s - - ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL. - ## - ## @doc zones..mqtt.max_awaiting_rel - ## ValueType: Integer | infinity - ## Range: [1, infinity) - ## Default: 100 - max_awaiting_rel: 100 - - ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. - ## - ## @doc zones..mqtt.await_rel_timeout - ## ValueType: Duration - ## Default: 300s - await_rel_timeout: 300s - - ## Default session expiry interval for MQTT V3.1.1 connections. - ## - ## @doc zones..mqtt.session_expiry_interval - ## ValueType: Duration - ## Default: 2h - session_expiry_interval: 2h - - ## Maximum queue length. Enqueued messages when persistent client disconnected, - ## or inflight window is full. - ## - ## @doc zones..mqtt.max_mqueue_len - ## ValueType: Integer | infinity - ## Range: [0, infinity) - ## Default: 1000 - max_mqueue_len: 1000 - - ## Topic priorities. - ## - ## There's no priority table by default, hence all messages - ## are treated equal. - ## - ## Priority number [1-255] - ## - ## NOTE: comma and equal signs are not allowed for priority topic names - ## NOTE: Messages for topics not in the priority table are treated as - ## either highest or lowest priority depending on the configured - ## value for mqtt.mqueue_default_priority - ## - ## @doc zones..mqtt.mqueue_priorities - ## ValueType: Map | disabled - ## Examples: - ## To configure "topic/1" > "topic/2": - ## mqueue_priorities: {"topic/1": 10, "topic/2": 8} - ## Default: disabled - mqueue_priorities: disabled - - ## Default to highest priority for topics not matching priority table - ## - ## @doc zones..mqtt.mqueue_default_priority - ## ValueType: highest | lowest - ## Default: lowest - mqueue_default_priority: lowest - - ## Whether to enqueue QoS0 messages. - ## - ## @doc zones..mqtt.mqueue_store_qos0 - ## ValueType: Boolean - ## Default: true - mqueue_store_qos0: true - - ## Whether use username replace client id - ## - ## @doc zones..mqtt.use_username_as_clientid - ## ValueType: Boolean - ## Default: false - use_username_as_clientid: false - - ## Use the CN, DN or CRT field from the client certificate as a username. - ## Only works for SSL connection. - ## - ## @doc zones..mqtt.peer_cert_as_username - ## ValueType: cn | dn | crt | disabled - ## Default: disabled - peer_cert_as_username: disabled - - ## Use the CN, DN or CRT field from the client certificate as a clientid. - ## Only works for SSL connection. - ## - ## @doc zones..mqtt.peer_cert_as_clientid - ## ValueType: cn | dn | crt | disabled - ## Default: disabled - peer_cert_as_clientid: disabled - - } - - authorization { - - ## Enable Authorization check. - ## - ## @doc zones..authorization.enable - ## ValueType: Boolean - ## Default: true - enable: true - - ## The action when authorization check reject current operation - ## - ## @doc zones..authorization.deny_action - ## ValueType: ignore | disconnect - ## Default: ignore - deny_action: ignore - - ## Whether to enable Authorization cache. - ## - ## If enabled, Authorization roles for each client will be cached in the memory - ## - ## @doc zones..authorization.cache.enable - ## ValueType: Boolean - ## Default: true - cache.enable: true - - ## The maximum count of Authorization entries can be cached for a client. - ## - ## @doc zones..authorization.cache.max_size - ## ValueType: Integer - ## Range: [0, 1048576] - ## Default: 32 - cache.max_size: 32 - - ## The time after which an Authorization cache entry will be deleted - ## - ## @doc zones..authorization.cache.ttl - ## ValueType: Duration - ## Default: 1m - cache.ttl: 1m - } - - flapping_detect { - ## Enable Flapping Detection. - ## - ## This config controls the allowed maximum number of CONNECT received - ## from the same clientid in a time frame defined by `window_time`. - ## After the limit is reached, successive CONNECT requests are forbidden - ## (banned) until the end of the time period defined by `ban_time`. - ## - ## @doc zones..flapping_detect.enable - ## ValueType: Boolean - ## Default: true - enable: false - - ## The max disconnect allowed of a MQTT Client in `window_time` - ## - ## @doc zones..flapping_detect.max_count - ## ValueType: Integer - ## Default: 15 - max_count: 15 - - ## The time window for flapping detect - ## - ## @doc zones..flapping_detect.window_time - ## ValueType: Duration - ## Default: 1m - window_time: 1m - - ## How long the clientid will be banned - ## - ## @doc zones..flapping_detect.ban_time - ## ValueType: Duration - ## Default: 5m - ban_time: 5m - - } - - force_shutdown: { - ## Enable force_shutdown - ## - ## @doc zones..force_shutdown.enable - ## ValueType: Boolean - ## Default: true - enable: true - - ## Max message queue length - ## @doc zones..force_shutdown.max_message_queue_len - ## ValueType: Integer - ## Range: (0, infinity) - ## Default: 1000 - max_message_queue_len: 1000 - - ## Total heap size - ## - ## @doc zones..force_shutdown.max_heap_size - ## ValueType: Size - ## Default: 32MB - max_heap_size: 32MB - } - - force_gc: { - ## Force the MQTT connection process GC after this number of - ## messages or bytes passed through. - ## - ## @doc zones..force_gc.enable - ## ValueType: Boolean - ## Default: true - enable: true - - ## GC the process after how many messages received - ## @doc zones..force_gc.max_message_queue_len - ## ValueType: Integer - ## Range: (0, infinity) - ## Default: 16000 - count: 16000 - - ## GC the process after how much bytes passed through - ## - ## @doc zones..force_gc.bytes - ## ValueType: Size - ## Default: 16MB - bytes: 16MB - } - - conn_congestion: { - ## Whether to alarm the congested connections. - ## - ## Sometimes the mqtt connection (usually an MQTT subscriber) may - ## get "congested" because there're too many packets to sent. - ## The socket trys to buffer the packets until the buffer is - ## full. If more packets comes after that, the packets will be - ## "pending" in a queue and we consider the connection is - ## "congested". - ## - ## Enable this to send an alarm when there's any bytes pending in - ## the queue. You could set the `sndbuf` to a larger value if the - ## alarm is triggered too often. - ## - ## The name of the alarm is of format "conn_congestion//". - ## Where the is the client-id of the congested MQTT connection. - ## And the is the username or "unknown_user" of not provided by the client. - ## - ## @doc zones..conn_congestion.enable_alarm - ## ValueType: Boolean - ## Default: true - enable_alarm: true - - ## Won't clear the congested alarm in how long time. - ## The alarm is cleared only when there're no pending bytes in - ## the queue, and also it has been `min_alarm_sustain_duration` - ## time since the last time we considered the connection is "congested". - ## - ## This is to avoid clearing and sending the alarm again too often. - ## - ## @doc zones..conn_congestion.min_alarm_sustain_duration - ## ValueType: Duration - ## Default: 1m - min_alarm_sustain_duration: 1m - } - - listeners.mqtt_tcp: - #${example_common_tcp_options} # common options can be written in a separate config entry and reference it from here. - { - - ## The type of the listener. - ## - ## @doc zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type: tcp - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..listeners..bind - ## ValueType: IPAddress | Port | IPAddrPort - ## Required: true - ## Examples: 1883, 127.0.0.1:1883, ::1:1883 - bind: "0.0.0.0:1883" - - ## The size of the acceptor pool for this listener. - ## - ## @doc zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors: 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..listeners..max_connections - ## ValueType: Number | infinity - ## Default: infinity - max_connections: 1024000 - - ## The access control rules for this listener. - ## - ## See: https://github.com/emqtt/esockd#allowdeny - ## - ## @doc zones..listeners..access_rules - ## ValueType: Array - ## Default: [] - ## Examples: - ## access_rules: [ - ## "deny 192.168.0.0/24", - ## "all all" - ## ] - access_rules: [ - "allow all" - ] - - ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed - ## behind HAProxy or Nginx. - ## - ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ - ## - ## @doc zones..listeners..proxy_protocol - ## ValueType: Boolean - ## Default: false - proxy_protocol: false - - ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection - ## if no proxy protocol packet recevied within the timeout. - ## - ## @doc zones..listeners..proxy_protocol_timeout - ## ValueType: Duration - ## Default: 3s - proxy_protocol_timeout: 3s - - rate_limit { - ## Maximum connections per second. - ## - ## @doc zones..max_conn_rate - ## ValueType: Number | infinity - ## Default: 1000 - ## Examples: - ## max_conn_rate: 1000 - max_conn_rate: 1000 - - ## Message limit for the a external MQTT connection. - ## - ## @doc zones..rate_limit.conn_messages_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messages per 10 seconds. - ## conn_messages_in: "100,10s" - conn_messages_in: "100,10s" - - ## Limit the rate of receiving packets for a MQTT connection. - ## The rate is counted by bytes of packets per second. - ## - ## The connection won't accept more messages if the messages come - ## faster than the limit. - ## - ## @doc zones..rate_limit.conn_bytes_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100KB incoming per 10 seconds. - ## conn_bytes_in: "100KB,10s" - ## - conn_bytes_in: "100KB,10s" - - ## Messages quota for the each of external MQTT connection. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zones..rate_limit.quota.conn_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messaegs per 1s: - ## quota.conn_messages_routing: "100,1s" - quota.conn_messages_routing: "100,1s" - - ## Messages quota for the all of external MQTT connections. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zones..rate_limit.quota.overall_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 200000 messages per 1s: - ## quota.overall_messages_routing: "200000,1s" - ## - quota.overall_messages_routing: "200000,1s" - } - - ## TCP options - ## See ${example_common_tcp_options} for more information - tcp.backlog: 1024 - tcp.buffer: 4KB - } - - ## MQTT/SSL - SSL Listener for MQTT Protocol - listeners.mqtt_ssl: - #${example_common_tcp_options} ${example_common_ssl_options} # common options can be written in a separate config entry and reference it from here. - { - - ## The type of the listener. - ## - ## @doc zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type: tcp - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..listeners..bind - ## ValueType: IPAddress | Port | IPAddrPort - ## Required: true - ## Examples: 8883, 127.0.0.1:8883, ::1:8883 - bind: "0.0.0.0:8883" - - ## The size of the acceptor pool for this listener. - ## - ## @doc zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors: 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..listeners..max_connections - ## ValueType: Number | infinity - ## Default: infinity - max_connections: 512000 - - ## The access control rules for this listener. - ## - ## See: https://github.com/emqtt/esockd#allowdeny - ## - ## @doc zones..listeners..access_rules - ## ValueType: Array - ## Default: [] - ## Examples: - ## access_rules: [ - ## "deny 192.168.0.0/24", - ## "all all" - ## ] - access_rules: [ - "allow all" - ] - - ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed - ## behind HAProxy or Nginx. - ## - ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ - ## - ## @doc zones..listeners..proxy_protocol - ## ValueType: Boolean - ## Default: true - proxy_protocol: false - - ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection - ## if no proxy protocol packet recevied within the timeout. - ## - ## @doc zones..listeners..proxy_protocol_timeout - ## ValueType: Duration - ## Default: 3s - proxy_protocol_timeout: 3s - - rate_limit { - ## Maximum connections per second. - ## - ## @doc zones..max_conn_rate - ## ValueType: Number | infinity - ## Default: 1000 - ## Examples: - ## max_conn_rate: 1000 - max_conn_rate: 1000 - - ## Message limit for the a external MQTT connection. - ## - ## @doc zones..rate_limit.conn_messages_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messages per 10 seconds. - ## conn_messages_in: "100,10s" - conn_messages_in: "100,10s" - - ## Limit the rate of receiving packets for a MQTT connection. - ## The rate is counted by bytes of packets per second. - ## - ## The connection won't accept more messages if the messages come - ## faster than the limit. - ## - ## @doc zones..rate_limit.conn_bytes_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100KB incoming per 10 seconds. - ## conn_bytes_in: "100KB,10s" - ## - conn_bytes_in: "100KB,10s" - - ## Messages quota for the each of external MQTT connection. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zones..rate_limit.quota.conn_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messaegs per 1s: - ## quota.conn_messages_routing: "100,1s" - quota.conn_messages_routing: "100,1s" - - ## Messages quota for the all of external MQTT connections. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zones..rate_limit.quota.overall_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 200000 messages per 1s: - ## quota.overall_messages_routing: "200000,1s" - ## - quota.overall_messages_routing: "200000,1s" - } - - ## SSL options - ## See ${example_common_ssl_options} for more information - ssl.enable: true - ssl.versions: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] - ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" - ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" - ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" - - ## TCP options - ## See ${example_common_tcp_options} for more information - tcp.backlog: 1024 - tcp.buffer: 4KB - } - - listeners.mqtt_quic: - { - ## The type of the listener. - ## - ## @doc zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type: quic - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..listeners..bind - ## ValueType: IPAddress | Port | IPAddrPort - ## Required: true - ## Examples: 14567, 127.0.0.1:14567, ::1:14567 - bind: "0.0.0.0:14567" - - ## The size of the acceptor pool for this listener. - ## - ## @doc zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors: 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..listeners..max_connections - ## ValueType: Number | infinity - ## Default: infinity - max_connections: 1024000 - - ## Path to the file containing the user's private PEM-encoded key. - ## - ## @doc zones..listeners..keyfile - ## ValueType: String - ## Default: "{{ platform_etc_dir }}/certs/key.pem" - keyfile: "{{ platform_etc_dir }}/certs/key.pem" - - ## Path to a file containing the user certificate. - ## - ## @doc zones..listeners..certfile - ## ValueType: String - ## Default: "{{ platform_etc_dir }}/certs/cert.pem" - certfile: "{{ platform_etc_dir }}/certs/cert.pem" - } - - listeners.mqtt_ws: - #${example_common_tcp_options} ${example_common_websocket_options} # common options can be written in a separate config entry and reference it from here. - { - - ## The type of the listener. - ## - ## @doc zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type: ws - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..listeners..bind - ## ValueType: IPAddress | Port | IPAddrPort - ## Required: true - ## Examples: 8083, 127.0.0.1:8083, ::1:8083 - bind: "0.0.0.0:8083" - - ## The size of the acceptor pool for this listener. - ## - ## @doc zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors: 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..listeners..max_connections - ## ValueType: Number | infinity - ## Default: infinity - max_connections: 1024000 - - ## The access control rules for this listener. - ## - ## See: https://github.com/emqtt/esockd#allowdeny - ## - ## @doc zones..listeners..access_rules - ## ValueType: Array - ## Default: [] - ## Examples: - ## access_rules: [ - ## "deny 192.168.0.0/24", - ## "all all" - ## ] - access_rules: [ - "allow all" - ] - - ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed - ## behind HAProxy or Nginx. - ## - ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ - ## - ## @doc zones..listeners..proxy_protocol - ## ValueType: Boolean - ## Default: true - proxy_protocol: false - - ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection - ## if no proxy protocol packet recevied within the timeout. - ## - ## @doc zones..listeners..proxy_protocol_timeout - ## ValueType: Duration - ## Default: 3s - proxy_protocol_timeout: 3s - - rate_limit { - ## Maximum connections per second. - ## - ## @doc zones..max_conn_rate - ## ValueType: Number | infinity - ## Default: 1000 - ## Examples: - ## max_conn_rate: 1000 - max_conn_rate: 1000 - - ## Message limit for the a external MQTT connection. - ## - ## @doc zones..rate_limit.conn_messages_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messages per 10 seconds. - ## conn_messages_in: "100,10s" - conn_messages_in: "100,10s" - - ## Limit the rate of receiving packets for a MQTT connection. - ## The rate is counted by bytes of packets per second. - ## - ## The connection won't accept more messages if the messages come - ## faster than the limit. - ## - ## @doc zones..rate_limit.conn_bytes_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100KB incoming per 10 seconds. - ## conn_bytes_in: "100KB,10s" - ## - conn_bytes_in: "100KB,10s" - - ## Messages quota for the each of external MQTT connection. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zones..rate_limit.quota.conn_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messaegs per 1s: - ## quota.conn_messages_routing: "100,1s" - quota.conn_messages_routing: "100,1s" - - ## Messages quota for the all of external MQTT connections. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zones..rate_limit.quota.overall_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 200000 messages per 1s: - ## quota.overall_messages_routing: "200000,1s" - ## - quota.overall_messages_routing: "200000,1s" - } - - ## TCP options - ## See ${example_common_tcp_options} for more information - tcp.backlog: 1024 - tcp.buffer: 4KB - - ## Websocket options - ## See ${example_common_websocket_options} for more information - websocket.idle_timeout: 86400s - } - - listeners.mqtt_wss: - #${example_common_tcp_options} ${example_common_ssl_options} ${example_common_websocket_options} # common options can be written in a separate config entry and reference it from here. - { - - ## The type of the listener. - ## - ## @doc zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type: ws - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..listeners..bind - ## ValueType: IPAddress | Port | IPAddrPort - ## Required: true - ## Examples: 8084, 127.0.0.1:8084, ::1:8084 - bind: "0.0.0.0:8084" - - ## The size of the acceptor pool for this listener. - ## - ## @doc zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors: 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..listeners..max_connections - ## ValueType: Number | infinity - ## Default: infinity - max_connections: 512000 - - ## The access control rules for this listener. - ## - ## See: https://github.com/emqtt/esockd#allowdeny - ## - ## @doc zones..listeners..access_rules - ## ValueType: Array - ## Default: [] - ## Examples: - ## access_rules: [ - ## "deny 192.168.0.0/24", - ## "all all" - ## ] - access_rules: [ - "allow all" - ] - - ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed - ## behind HAProxy or Nginx. - ## - ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ - ## - ## @doc zones..listeners..proxy_protocol - ## ValueType: Boolean - ## Default: true - proxy_protocol: false - - ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection - ## if no proxy protocol packet recevied within the timeout. - ## - ## @doc zones..listeners..proxy_protocol_timeout - ## ValueType: Duration - ## Default: 3s - proxy_protocol_timeout: 3s - - rate_limit { - ## Maximum connections per second. - ## - ## @doc zones..max_conn_rate - ## ValueType: Number | infinity - ## Default: 1000 - ## Examples: - ## max_conn_rate: 1000 - max_conn_rate: 1000 - - ## Message limit for the a external MQTT connection. - ## - ## @doc zones..rate_limit.conn_messages_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messages per 10 seconds. - ## conn_messages_in: "100,10s" - conn_messages_in: "100,10s" - - ## Limit the rate of receiving packets for a MQTT connection. - ## The rate is counted by bytes of packets per second. - ## - ## The connection won't accept more messages if the messages come - ## faster than the limit. - ## - ## @doc zones..rate_limit.conn_bytes_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100KB incoming per 10 seconds. - ## conn_bytes_in: "100KB,10s" - ## - conn_bytes_in: "100KB,10s" - - ## Messages quota for the each of external MQTT connection. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zones..rate_limit.quota.conn_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messaegs per 1s: - ## quota.conn_messages_routing: "100,1s" - quota.conn_messages_routing: "100,1s" - - ## Messages quota for the all of external MQTT connections. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zones..rate_limit.quota.overall_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 200000 messages per 1s: - ## quota.overall_messages_routing: "200000,1s" - ## - quota.overall_messages_routing: "200000,1s" - } - - ## SSL options - ## See ${example_common_ssl_options} for more information - ssl.enable: true - ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" - ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" - ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" - - ## TCP options - ## See ${example_common_tcp_options} for more information - tcp.backlog: 1024 - tcp.buffer: 4KB - - ## Websocket options - ## See ${example_common_websocket_options} for more information - websocket.idle_timeout: 86400s - } - -} - -#This is an example zone which has less "strict" settings. -#It's useful to clients connecting the broker from trusted networks. -zones.internal { - authorization.enable: true - auth.enable: false - listeners.mqtt_internal: { - type: tcp - bind: "127.0.0.1:11883" - acceptors: 4 - max_connections: 1024000 - tcp.active_n: 1000 - tcp.backlog: 512 - } + perf.trie_compaction = true } ##================================================================== @@ -1179,21 +1091,21 @@ sysmon { ## @doc sysmon.vm.process_check_interval ## ValueType: Duration ## Default: 30s - vm.process_check_interval: 30s + vm.process_check_interval = 30s ## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is set. ## ## @doc sysmon.vm.process_high_watermark ## ValueType: Percentage ## Default: 80% - vm.process_high_watermark: 80% + vm.process_high_watermark = 80% ## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is clear. ## ## @doc sysmon.vm.process_low_watermark ## ValueType: Percentage ## Default: 60% - vm.process_low_watermark: 60% + vm.process_low_watermark = 60% ## Enable Long GC monitoring. ## Notice: don't enable the monitor in production for: @@ -1202,7 +1114,7 @@ sysmon { ## @doc sysmon.vm.long_gc ## ValueType: Duration | disabled ## Default: disabled - vm.long_gc: disabled + vm.long_gc = disabled ## Enable Long Schedule(ms) monitoring. ## @@ -1211,7 +1123,7 @@ sysmon { ## @doc sysmon.vm.long_schedule ## ValueType: Duration | disabled ## Default: disabled - vm.long_schedule: 240ms + vm.long_schedule = 240ms ## Enable Large Heap monitoring. ## @@ -1220,7 +1132,7 @@ sysmon { ## @doc sysmon.vm.large_heap ## ValueType: Size | disabled ## Default: 32MB - vm.large_heap: 32MB + vm.large_heap = 32MB ## Enable Busy Port monitoring. ## @@ -1229,7 +1141,7 @@ sysmon { ## @doc sysmon.vm.busy_port ## ValueType: Boolean ## Default: true - vm.busy_port: true + vm.busy_port = true ## Enable Busy Dist Port monitoring. ## @@ -1238,49 +1150,49 @@ sysmon { ## @doc sysmon.vm.busy_dist_port ## ValueType: Boolean ## Default: true - vm.busy_dist_port: true + vm.busy_dist_port = true ## The time interval for the periodic cpu check ## ## @doc sysmon.os.cpu_check_interval ## ValueType: Duration ## Default: 60s - os.cpu_check_interval: 60s + os.cpu_check_interval = 60s ## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is set. ## ## @doc sysmon.os.cpu_high_watermark ## ValueType: Percentage ## Default: 80% - os.cpu_high_watermark: 80% + os.cpu_high_watermark = 80% ## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is clear. ## ## @doc sysmon.os.cpu_low_watermark ## ValueType: Percentage ## Default: 60% - os.cpu_low_watermark: 60% + os.cpu_low_watermark = 60% ## The time interval for the periodic memory check ## ## @doc sysmon.os.mem_check_interval ## ValueType: Duration | disabled ## Default: 60s - os.mem_check_interval: 60s + os.mem_check_interval = 60s ## The threshold, as percentage of system memory, for how much system memory can be allocated before the corresponding alarm is set. ## ## @doc sysmon.os.sysmem_high_watermark ## ValueType: Percentage ## Default: 70% - os.sysmem_high_watermark: 70% + os.sysmem_high_watermark = 70% ## The threshold, as percentage of system memory, for how much system memory can be allocated by one Erlang process before the corresponding alarm is set. ## ## @doc sysmon.os.procmem_high_watermark ## ValueType: Percentage ## Default: 5% - os.procmem_high_watermark: 5% + os.procmem_high_watermark = 5% } ##================================================================== @@ -1292,21 +1204,21 @@ alarm { ## @doc alarm.actions ## ValueType: Array ## Default: [log, publish] - actions: [log, publish] + actions = [log, publish] ## The maximum number of deactivated alarms ## ## @doc alarm.size_limit ## ValueType: Integer ## Default: 1000 - size_limit: 1000 + size_limit = 1000 ## Validity Period of deactivated alarms ## ## @doc alarm.validity_period ## ValueType: Duration ## Default: 24h - validity_period: 24h + validity_period = 24h } ## Config references for listeners @@ -1321,7 +1233,7 @@ example_common_tcp_options { ## @doc listeners..tcp.active_n ## ValueType: Number ## Default: 100 - tcp.active_n: 100 + tcp.active_n = 100 ## TCP backlog defines the maximum length that the queue of ## pending connections can grow to. @@ -1330,21 +1242,21 @@ example_common_tcp_options { ## ValueType: Number ## Range: [0, 1048576] ## Default: 1024 - tcp.backlog: 1024 + tcp.backlog = 1024 ## The TCP send timeout for the connections. ## ## @doc listeners..tcp.send_timeout ## ValueType: Duration ## Default: 15s - tcp.send_timeout: 15s + tcp.send_timeout = 15s ## Close the connection if send timeout. ## ## @doc listeners..tcp.send_timeout_close ## ValueType: Boolean ## Default: true - tcp.send_timeout_close: true + tcp.send_timeout_close = true ## The TCP receive buffer(os kernel) for the connections. ## @@ -1373,21 +1285,21 @@ example_common_tcp_options { ## @doc listeners..tcp.high_watermark ## ValueType: Size ## Default: 1MB - tcp.high_watermark: 1MB + tcp.high_watermark = 1MB ## The TCP_NODELAY flag for the connections. ## ## @doc listeners..tcp.nodelay ## ValueType: Boolean ## Default: false - tcp.nodelay: false + tcp.nodelay = false ## The SO_REUSEADDR flag for the connections. ## ## @doc listeners..tcp.reuseaddr ## ValueType: Boolean ## Default: true - tcp.reuseaddr: true + tcp.reuseaddr = true } ## Socket options for SSL connections @@ -1401,7 +1313,7 @@ example_common_ssl_options { ## @doc listeners..ssl.reuse_sessions ## ValueType: Boolean ## Default: true - ssl.reuse_sessions: true + ssl.reuse_sessions = true ## SSL parameter renegotiation is a feature that allows a client and a server ## to renegotiate the parameters of the SSL connection on the fly. @@ -1411,7 +1323,7 @@ example_common_ssl_options { ## @doc listeners..ssl.secure_renegotiate ## ValueType: Boolean ## Default: true - ssl.secure_renegotiate: true + ssl.secure_renegotiate = true ## An important security setting, it forces the cipher to be set based ## on the server-specified order instead of the client-specified order, @@ -1421,21 +1333,21 @@ example_common_ssl_options { ## @doc listeners..ssl.honor_cipher_order ## ValueType: Boolean ## Default: true - ssl.honor_cipher_order: true + ssl.honor_cipher_order = true ## TLS versions only to protect from POODLE attack. ## ## @doc listeners..ssl.versions ## ValueType: Array ## Default: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] - ssl.versions: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] ## TLS Handshake timeout. ## ## @doc listeners..ssl.handshake_timeout ## ValueType: Duration ## Default: 15s - ssl.handshake_timeout: 15s + ssl.handshake_timeout = 15s ## Maximum number of non-self-issued intermediate certificates that ## can follow the peer certificate in a valid certification path. @@ -1443,21 +1355,21 @@ example_common_ssl_options { ## @doc listeners..ssl.depth ## ValueType: Integer ## Default: 10 - ssl.depth: 10 + ssl.depth = 10 ## Path to the file containing the user's private PEM-encoded key. ## ## @doc listeners..ssl.keyfile ## ValueType: File ## Default: "{{ platform_etc_dir }}/certs/key.pem" - ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" + ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ## Path to a file containing the user certificate. ## ## @doc listeners..ssl.certfile ## ValueType: File ## Default: "{{ platform_etc_dir }}/certs/cert.pem" - ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" + ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" ## Path to the file containing PEM-encoded CA certificates. The CA certificates ## are used during server authentication and when building the client certificate chain. @@ -1465,7 +1377,7 @@ example_common_ssl_options { ## @doc listeners..ssl.cacertfile ## ValueType: File ## Default: "{{ platform_etc_dir }}/certs/cacert.pem" - ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" ## Maximum number of non-self-issued intermediate certificates that ## can follow the peer certificate in a valid certification path. @@ -1473,7 +1385,7 @@ example_common_ssl_options { ## @doc listeners..ssl.depth ## ValueType: Number ## Default: 10 - ssl.depth: 10 + ssl.depth = 10 ## String containing the user's password. Only used if the private keyfile ## is password-protected. @@ -1513,7 +1425,7 @@ example_common_ssl_options { ## @doc listeners..ssl.verify ## ValueType: verify_peer | verify_none ## Default: verify_none - ssl.verify: verify_none + ssl.verify = verify_none ## Used together with {verify, verify_peer} by an SSL server. If set to true, ## the server fails if the client does not have a certificate to send, that is, @@ -1522,7 +1434,7 @@ example_common_ssl_options { ## @doc listeners..ssl.fail_if_no_peer_cert ## ValueType: Boolean ## Default: true - ssl.fail_if_no_peer_cert: false + ssl.fail_if_no_peer_cert = false ## This is the single most important configuration option of an Erlang SSL ## application. Ciphers (and their ordering) define the way the client and @@ -1543,7 +1455,7 @@ example_common_ssl_options { ## @doc listeners..ssl.ciphers ## ValueType: Array ## Default: [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA,PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] - ssl.ciphers: [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA,PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] + ssl.ciphers = [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA,PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] } @@ -1554,14 +1466,14 @@ example_common_websocket_options { ## @doc listeners..websocket.mqtt_path ## ValueType: Path ## Default: "/mqtt" - websocket.mqtt_path: "/mqtt" + websocket.mqtt_path = "/mqtt" ## Whether a WebSocket message is allowed to contain multiple MQTT packets ## ## @doc listeners..websocket.mqtt_piggyback ## ValueType: single | multiple ## Default: multiple - websocket.mqtt_piggyback: multiple + websocket.mqtt_piggyback = multiple ## The compress flag for external WebSocket connections. ## @@ -1570,21 +1482,21 @@ example_common_websocket_options { ## @doc listeners..websocket.compress ## ValueType: Boolean ## Default: false - websocket.compress: false + websocket.compress = false ## The idle timeout for external WebSocket connections. ## ## @doc listeners..websocket.idle_timeout ## ValueType: Duration | infinity ## Default: infinity - websocket.idle_timeout: infinity + websocket.idle_timeout = infinity ## The max frame size for external WebSocket connections. ## ## @doc listeners..websocket.max_frame_size ## ValueType: Size ## Default: infinity - websocket.max_frame_size: infinity + websocket.max_frame_size = infinity ## If set to true, the server fails if the client does not ## have a Sec-WebSocket-Protocol to send. @@ -1593,21 +1505,21 @@ example_common_websocket_options { ## @doc listeners..websocket.fail_if_no_subprotocol ## ValueType: Boolean ## Default: true - websocket.fail_if_no_subprotocol: true + websocket.fail_if_no_subprotocol = true ## Supported subprotocols ## ## @doc listeners..websocket.supported_subprotocols ## ValueType: String ## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 - websocket.supported_subprotocols: "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + websocket.supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" ## Enable origin check in header for websocket connection ## ## @doc listeners..websocket.check_origin_enable ## ValueType: Boolean ## Default: false - websocket.check_origin_enable: false + websocket.check_origin_enable = false ## Allow origin to be absent in header in websocket connection ## when check_origin_enable is true @@ -1615,7 +1527,7 @@ example_common_websocket_options { ## @doc listeners..websocket.allow_origin_absence ## ValueType: Boolean ## Default: true - websocket.allow_origin_absence: true + websocket.allow_origin_absence = true ## Comma separated list of allowed origin in header for websocket connection ## @@ -1625,7 +1537,7 @@ example_common_websocket_options { ## local http dashboard url ## check_origins: "http://localhost:18083, http://127.0.0.1:18083" ## Default: "" - websocket.check_origins: "http://localhost:18083, http://127.0.0.1:18083" + websocket.check_origins = "http://localhost:18083, http://127.0.0.1:18083" ## Specify which HTTP header for real source IP if the EMQ X cluster is ## deployed behind NGINX or HAProxy. @@ -1633,7 +1545,7 @@ example_common_websocket_options { ## @doc listeners..websocket.proxy_address_header ## ValueType: String ## Default: X-Forwarded-For - websocket.proxy_address_header: X-Forwarded-For + websocket.proxy_address_header = X-Forwarded-For ## Specify which HTTP header for real source port if the EMQ X cluster is ## deployed behind NGINX or HAProxy. @@ -1641,7 +1553,7 @@ example_common_websocket_options { ## @doc listeners..websocket.proxy_port_header ## ValueType: String ## Default: X-Forwarded-Port - websocket.proxy_port_header: X-Forwarded-Port + websocket.proxy_port_header = X-Forwarded-Port websocket.deflate_opts { ## The level of deflate options for external WebSocket connections. @@ -1649,7 +1561,7 @@ example_common_websocket_options { ## @doc listeners..websocket.deflate_opts.level ## ValueType: none | default | best_compression | best_speed ## Default: default - level: default + level = default ## The mem_level of deflate options for external WebSocket connections. ## @@ -1657,28 +1569,28 @@ example_common_websocket_options { ## ValueType: Integer ## Range: [1,9] ## Default: 8 - mem_level: 8 + mem_level = 8 ## The strategy of deflate options for external WebSocket connections. ## ## @doc listeners..websocket.deflate_opts.strategy ## ValueType: default | filtered | huffman_only | rle ## Default: default - strategy: default + strategy = default ## The deflate option for external WebSocket connections. ## ## @doc listeners..websocket.deflate_opts.server_context_takeover ## ValueType: takeover | no_takeover ## Default: takeover - server_context_takeover: takeover + server_context_takeover = takeover ## The deflate option for external WebSocket connections. ## ## @doc listeners..websocket.deflate_opts.client_context_takeover ## ValueType: takeover | no_takeover ## Default: takeover - client_context_takeover: takeover + client_context_takeover = takeover ## The deflate options for external WebSocket connections. ## @@ -1687,7 +1599,7 @@ example_common_websocket_options { ## ValueType: Integer ## Range: [8,15] ## Default: 15 - server_max_window_bits: 15 + server_max_window_bits = 15 ## The deflate options for external WebSocket connections. ## @@ -1695,6 +1607,6 @@ example_common_websocket_options { ## ValueType: Integer ## Range: [8,15] ## Default: 15 - client_max_window_bits: 15 + client_max_window_bits = 15 } } diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 60dccd9a3..550e650a2 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -26,6 +26,7 @@ -define(COMMON_SHARD, emqx_common_shard). -define(SHARED_SUB_SHARD, emqx_shared_sub_shard). -define(MOD_DELAYED_SHARD, emqx_delayed_shard). +-define(CM_SHARD, emqx_cm_shard). %%-------------------------------------------------------------------- %% Banner @@ -125,8 +126,7 @@ -record(banned, { who :: {clientid, binary()} | {peerhost, inet:ip_address()} - | {username, binary()} - | {ip_address, inet:ip_address()}, + | {username, binary()}, by :: binary(), reason :: binary(), at :: integer(), @@ -134,3 +134,19 @@ }). -endif. + +%%-------------------------------------------------------------------- +%% Authentication +%%-------------------------------------------------------------------- + +-record(authenticator, + { id :: binary() + , provider :: module() + , enable :: boolean() + , state :: map() + }). + +-record(chain, + { name :: atom() + , authenticators :: [#authenticator{}] + }). \ No newline at end of file diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index 61444224c..572d23155 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -29,7 +29,7 @@ -ifndef(EMQX_ENTERPRISE). --define(EMQX_RELEASE, {opensource, "5.0-alpha.3"}). +-define(EMQX_RELEASE, {opensource, "5.0-alpha.5"}). -else. diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index ebe46559b..bb3a588a9 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -13,9 +13,9 @@ , {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.2"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.4"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} @@ -28,8 +28,8 @@ [{deps, [ meck , {bbmustache,"1.10.0"} - , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers", {branch,"hocon"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.2"}}} + , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {tag,"2.1.0"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.3"}}} ]}, {extra_src_dirs, [{"test",[recursive]}]} ]} diff --git a/apps/emqx/rebar.config.script b/apps/emqx/rebar.config.script index 860be0a10..a56403dec 100644 --- a/apps/emqx/rebar.config.script +++ b/apps/emqx/rebar.config.script @@ -18,7 +18,7 @@ IsQuicSupp = fun() -> end, Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {branch, "0.6.0"}}}, -Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {branch, "0.0.7"}}}, +Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {branch, "0.0.8"}}}, ExtraDeps = fun(C) -> {deps, Deps0} = lists:keyfind(deps, 1, C), diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index 3a5935a8b..1d4686561 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -55,6 +55,18 @@ -export([ set_debug_secret/1 ]). +%% Configs APIs +-export([ get_config/1 + , get_config/2 + , get_raw_config/1 + , get_raw_config/2 + , update_config/2 + , update_config/3 + , remove_config/1 + , remove_config/2 + , reset_config/2 + ]). + -define(APP, ?MODULE). %% @hidden Path to the file which has debug_info encryption secret in it. @@ -184,3 +196,53 @@ run_hook(HookPoint, Args) -> -spec(run_fold_hook(emqx_hooks:hookpoint(), list(any()), any()) -> any()). run_fold_hook(HookPoint, Args, Acc) -> emqx_hooks:run_fold(HookPoint, Args, Acc). + +-spec get_config(emqx_map_lib:config_key_path()) -> term(). +get_config(KeyPath) -> + emqx_config:get(KeyPath). + +-spec get_config(emqx_map_lib:config_key_path(), term()) -> term(). +get_config(KeyPath, Default) -> + emqx_config:get(KeyPath, Default). + +-spec get_raw_config(emqx_map_lib:config_key_path()) -> term(). +get_raw_config(KeyPath) -> + emqx_config:get_raw(KeyPath). + +-spec get_raw_config(emqx_map_lib:config_key_path(), term()) -> term(). +get_raw_config(KeyPath, Default) -> + emqx_config:get_raw(KeyPath, Default). + +-spec update_config(emqx_map_lib:config_key_path(), emqx_config:update_request()) -> + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. +update_config(KeyPath, UpdateReq) -> + update_config(KeyPath, UpdateReq, #{}). + +-spec update_config(emqx_map_lib:config_key_path(), emqx_config:update_request(), + emqx_config:update_opts()) -> + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. +update_config([RootName | _] = KeyPath, UpdateReq, Opts) -> + emqx_config_handler:update_config(emqx_config:get_schema_mod(RootName), KeyPath, + {{update, UpdateReq}, Opts}). + +-spec remove_config(emqx_map_lib:config_key_path()) -> + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. +remove_config(KeyPath) -> + remove_config(KeyPath, #{}). + +-spec remove_config(emqx_map_lib:config_key_path(), emqx_config:update_opts()) -> + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. +remove_config([RootName | _] = KeyPath, Opts) -> + emqx_config_handler:update_config(emqx_config:get_schema_mod(RootName), + KeyPath, {remove, Opts}). + +-spec reset_config(emqx_map_lib:config_key_path(), emqx_config:update_opts()) -> + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. +reset_config([RootName | _] = KeyPath, Opts) -> + case emqx_config:get_default_value(KeyPath) of + {ok, Default} -> + emqx_config_handler:update_config(emqx_config:get_schema_mod(RootName), KeyPath, + {{update, Default}, Opts}); + {error, _} = Error -> + Error + end. diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 65991d222..914651535 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -27,30 +27,36 @@ %%-------------------------------------------------------------------- -spec(authenticate(emqx_types:clientinfo()) -> - ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). + {ok, map()} | {ok, map(), binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). authenticate(Credential) -> - run_hooks('client.authenticate', [Credential], ok). + case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of + ok -> + {ok, #{is_superuser => false}}; + Other -> + Other + end. %% @doc Check Authorization -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny. -authorize(ClientInfo = #{zone := Zone}, PubSub, Topic) -> - case emqx_authz_cache:is_enabled(Zone) of +authorize(ClientInfo, PubSub, Topic) -> + case emqx_authz_cache:is_enabled() of true -> check_authorization_cache(ClientInfo, PubSub, Topic); false -> do_authorize(ClientInfo, PubSub, Topic) end. -check_authorization_cache(ClientInfo = #{zone := Zone}, PubSub, Topic) -> - case emqx_authz_cache:get_authz_cache(Zone, PubSub, Topic) of +check_authorization_cache(ClientInfo, PubSub, Topic) -> + case emqx_authz_cache:get_authz_cache(PubSub, Topic) of not_found -> AuthzResult = do_authorize(ClientInfo, PubSub, Topic), - emqx_authz_cache:put_authz_cache(Zone, PubSub, Topic, AuthzResult), + emqx_authz_cache:put_authz_cache(PubSub, Topic, AuthzResult), AuthzResult; AuthzResult -> AuthzResult end. do_authorize(ClientInfo, PubSub, Topic) -> - case run_hooks('client.authorize', [ClientInfo, PubSub, Topic], allow) of + NoMatch = emqx:get_config([authorization, no_match], allow), + case run_hooks('client.authorize', [ClientInfo, PubSub, Topic], NoMatch) of allow -> allow; _Other -> deny end. diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index ab3e2d702..b43f5c52e 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -28,7 +28,7 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --export([pre_config_update/2]). +-export([post_config_update/4]). -export([ start_link/0 , stop/0 @@ -85,9 +85,6 @@ -define(DEACTIVATED_ALARM, emqx_deactivated_alarm). --rlog_shard({?COMMON_SHARD, ?ACTIVATED_ALARM}). --rlog_shard({?COMMON_SHARD, ?DEACTIVATED_ALARM}). - -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). @@ -151,14 +148,9 @@ get_alarms(activated) -> get_alarms(deactivated) -> gen_server:call(?MODULE, {get_alarms, deactivated}). -pre_config_update(#{<<"validity_period">> := Period0} = NewConf, OldConf) -> - ?MODULE ! {update_timer, hocon_postprocess:duration(Period0)}, - merge(OldConf, NewConf); -pre_config_update(NewConf, OldConf) -> - merge(OldConf, NewConf). - -merge(undefined, New) -> New; -merge(Old, New) -> maps:merge(Old, New). +post_config_update(_, #{validity_period := Period0}, _OldConf, _AppEnv) -> + ?MODULE ! {update_timer, Period0}, + ok. format(#activated_alarm{name = Name, message = Message, activate_at = At, details = Details}) -> Now = erlang:system_time(microsecond), @@ -166,7 +158,8 @@ format(#activated_alarm{name = Name, message = Message, activate_at = At, detail node => node(), name => Name, message => Message, - duration => Now - At, + duration => (Now - At) div 1000, %% to millisecond + activate_at => to_rfc3339(At), details => Details }; format(#deactivated_alarm{name = Name, message = Message, activate_at = At, details = Details, @@ -176,18 +169,23 @@ format(#deactivated_alarm{name = Name, message = Message, activate_at = At, deta name => Name, message => Message, duration => DAt - At, + activate_at => to_rfc3339(At), + deactivate_at => to_rfc3339(DAt), details => Details }; format(_) -> {error, unknow_alarm}. +to_rfc3339(Timestamp) -> + list_to_binary(calendar:system_time_to_rfc3339(Timestamp div 1000, [{unit, millisecond}])). + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> deactivate_all_alarms(), - emqx_config_handler:add_handler([alarm], ?MODULE), + ok = emqx_config_handler:add_handler([alarm], ?MODULE), {ok, #state{timer = ensure_timer(undefined, get_validity_period())}}. %% suppress dialyzer warning due to dirty read/write race condition. @@ -204,7 +202,7 @@ handle_call({activate_alarm, Name, Details}, _From, State) -> message = normalize_message(Name, Details), activate_at = erlang:system_time(microsecond)}, ekka_mnesia:dirty_write(?ACTIVATED_ALARM, Alarm), - do_actions(activate, Alarm, emqx_config:get([alarm, actions])), + do_actions(activate, Alarm, emqx:get_config([alarm, actions])), {reply, ok, State} end; @@ -263,6 +261,7 @@ handle_info(Info, State) -> {noreply, State}. terminate(_Reason, _State) -> + ok = emqx_config_handler:remove_handler([alarm]), ok. code_change(_OldVsn, State, _Extra) -> @@ -273,11 +272,11 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ get_validity_period() -> - emqx_config:get([alarm, validity_period]). + emqx:get_config([alarm, validity_period]). deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name, details = Details0, message = Msg0}) -> - SizeLimit = emqx_config:get([alarm, size_limit]), + SizeLimit = emqx:get_config([alarm, size_limit]), case SizeLimit > 0 andalso (mnesia:table_info(?DEACTIVATED_ALARM, size) >= SizeLimit) of true -> case mnesia:dirty_first(?DEACTIVATED_ALARM) of @@ -294,7 +293,7 @@ deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name erlang:system_time(microsecond)), ekka_mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm), ekka_mnesia:dirty_delete(?ACTIVATED_ALARM, Name), - do_actions(deactivate, DeActAlarm, emqx_config:get([alarm, actions])). + do_actions(deactivate, DeActAlarm, emqx:get_config([alarm, actions])). make_deactivated_alarm(ActivateAt, Name, Details, Message, DeActivateAt) -> #deactivated_alarm{ diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl new file mode 100644 index 000000000..8cc8cf2df --- /dev/null +++ b/apps/emqx/src/emqx_authentication.erl @@ -0,0 +1,735 @@ +%%-------------------------------------------------------------------- +%% 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_authentication). + +-behaviour(gen_server). +-behaviour(hocon_schema). +-behaviour(emqx_config_handler). + +-include("emqx.hrl"). +-include("logger.hrl"). + +-export([ roots/0 + , fields/1 + ]). + +-export([ pre_config_update/2 + , post_config_update/4 + ]). + +-export([ authenticate/2 + ]). + +-export([ initialize_authentication/2 ]). + +-export([ start_link/0 + , stop/0 + ]). + +-export([ add_provider/2 + , remove_provider/1 + , create_chain/1 + , delete_chain/1 + , lookup_chain/1 + , list_chains/0 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/3 + , lookup_authenticator/2 + , list_authenticators/1 + , move_authenticator/3 + ]). + +-export([ import_users/3 + , add_user/3 + , delete_user/3 + , update_user/4 + , lookup_user/3 + , list_users/2 + ]). + +-export([ generate_id/1 ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-define(CHAINS_TAB, emqx_authn_chains). + +-define(VER_1, <<"1">>). +-define(VER_2, <<"2">>). + +-type config() :: #{atom() => term()}. +-type state() :: #{atom() => term()}. +-type extra() :: #{is_superuser := boolean(), + atom() => term()}. +-type user_info() :: #{user_id := binary(), + atom() => term()}. + +-callback refs() -> [{ref, Module, Name}] when Module::module(), Name::atom(). + +-callback create(Config) + -> {ok, State} + | {error, term()} + when Config::config(), State::state(). + +-callback update(Config, State) + -> {ok, NewState} + | {error, term()} + when Config::config(), State::state(), NewState::state(). + +-callback authenticate(Credential, State) + -> ignore + | {ok, Extra} + | {ok, Extra, AuthData} + | {continue, AuthCache} + | {continue, AuthData, AuthCache} + | {error, term()} + when Credential::map(), State::state(), Extra::extra(), AuthData::binary(), AuthCache::map(). + +-callback destroy(State) + -> ok + when State::state(). + +-callback import_users(Filename, State) + -> ok + | {error, term()} + when Filename::binary(), State::state(). + +-callback add_user(UserInfo, State) + -> {ok, User} + | {error, term()} + when UserInfo::user_info(), State::state(), User::user_info(). + +-callback delete_user(UserID, State) + -> ok + | {error, term()} + when UserID::binary(), State::state(). + +-callback update_user(UserID, UserInfo, State) + -> {ok, User} + | {error, term()} + when UserID::binary, UserInfo::map(), State::state(), User::user_info(). + +-callback list_users(State) + -> {ok, Users} + when State::state(), Users::[user_info()]. + +-optional_callbacks([ import_users/2 + , add_user/2 + , delete_user/2 + , update_user/3 + , list_users/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +roots() -> [{authentication, fun authentication/1}]. + +fields(_) -> []. + +authentication(type) -> + {ok, Refs} = get_refs(), + hoconsc:union([hoconsc:array(hoconsc:union(Refs)) | Refs]); +authentication(default) -> []; +authentication(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% Callbacks of config handler +%%------------------------------------------------------------------------------ + +pre_config_update(UpdateReq, OldConfig) -> + case do_pre_config_update(UpdateReq, to_list(OldConfig)) of + {error, Reason} -> {error, Reason}; + {ok, NewConfig} -> {ok, may_to_map(NewConfig)} + end. + +do_pre_config_update({create_authenticator, _ChainName, Config}, OldConfig) -> + {ok, OldConfig ++ [Config]}; +do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) -> + NewConfig = lists:filter(fun(OldConfig0) -> + AuthenticatorID =/= generate_id(OldConfig0) + end, OldConfig), + {ok, NewConfig}; +do_pre_config_update({update_authenticator, _ChainName, AuthenticatorID, Config}, OldConfig) -> + NewConfig = lists:map(fun(OldConfig0) -> + case AuthenticatorID =:= generate_id(OldConfig0) of + true -> maps:merge(OldConfig0, Config); + false -> OldConfig0 + end + end, OldConfig), + {ok, NewConfig}; +do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) -> + case split_by_id(AuthenticatorID, OldConfig) of + {error, Reason} -> {error, Reason}; + {ok, Part1, [Found | Part2]} -> + case Position of + <<"top">> -> + {ok, [Found | Part1] ++ Part2}; + <<"bottom">> -> + {ok, Part1 ++ Part2 ++ [Found]}; + <<"before:", Before/binary>> -> + case split_by_id(Before, Part1 ++ Part2) of + {error, Reason} -> + {error, Reason}; + {ok, NPart1, [NFound | NPart2]} -> + {ok, NPart1 ++ [Found, NFound | NPart2]} + end; + _ -> + {error, {invalid_parameter, position}} + end + end. + +post_config_update(UpdateReq, NewConfig, OldConfig, AppEnvs) -> + do_post_config_update(UpdateReq, check_config(to_list(NewConfig)), OldConfig, AppEnvs). + +do_post_config_update({create_authenticator, ChainName, Config}, _NewConfig, _OldConfig, _AppEnvs) -> + NConfig = check_config(Config), + _ = create_chain(ChainName), + create_authenticator(ChainName, NConfig); + +do_post_config_update({delete_authenticator, ChainName, AuthenticatorID}, _NewConfig, _OldConfig, _AppEnvs) -> + delete_authenticator(ChainName, AuthenticatorID); + +do_post_config_update({update_authenticator, ChainName, AuthenticatorID, _Config}, NewConfig, _OldConfig, _AppEnvs) -> + [Config] = lists:filter(fun(NewConfig0) -> + AuthenticatorID =:= generate_id(NewConfig0) + end, NewConfig), + NConfig = check_config(Config), + update_authenticator(ChainName, AuthenticatorID, NConfig); + +do_post_config_update({move_authenticator, ChainName, AuthenticatorID, Position}, _NewConfig, _OldConfig, _AppEnvs) -> + NPosition = case Position of + <<"top">> -> top; + <<"bottom">> -> bottom; + <<"before:", Before/binary>> -> + {before, Before} + end, + move_authenticator(ChainName, AuthenticatorID, NPosition). + +check_config(Config) -> + #{authentication := CheckedConfig} = hocon_schema:check_plain(emqx_authentication, + #{<<"authentication">> => Config}, #{nullable => true, atom_key => true}), + CheckedConfig. + +%%------------------------------------------------------------------------------ +%% Authenticate +%%------------------------------------------------------------------------------ + +authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) -> + case ets:lookup(?CHAINS_TAB, Listener) of + [#chain{authenticators = Authenticators}] when Authenticators =/= [] -> + do_authenticate(Authenticators, Credential); + _ -> + case ets:lookup(?CHAINS_TAB, global_chain(Protocol)) of + [#chain{authenticators = Authenticators}] when Authenticators =/= [] -> + do_authenticate(Authenticators, Credential); + _ -> + ignore + end + end. + +do_authenticate([], _) -> + {stop, {error, not_authorized}}; +do_authenticate([#authenticator{provider = Provider, state = State} | More], Credential) -> + case Provider:authenticate(Credential, State) of + ignore -> + do_authenticate(More, Credential); + Result -> + %% {ok, Extra} + %% {ok, Extra, AuthData} + %% {continue, AuthCache} + %% {continue, AuthData, AuthCache} + %% {error, Reason} + {stop, Result} + end. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +initialize_authentication(_, []) -> + ok; +initialize_authentication(ChainName, AuthenticatorsConfig) -> + _ = create_chain(ChainName), + CheckedConfig = check_config(to_list(AuthenticatorsConfig)), + lists:foreach(fun(AuthenticatorConfig) -> + case create_authenticator(ChainName, AuthenticatorConfig) of + {ok, _} -> + ok; + {error, Reason} -> + ?LOG(error, "Failed to create authenticator '~s': ~p", [generate_id(AuthenticatorConfig), Reason]) + end + end, CheckedConfig). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +stop() -> + gen_server:stop(?MODULE). + +get_refs() -> + gen_server:call(?MODULE, get_refs). + +add_provider(AuthNType, Provider) -> + gen_server:call(?MODULE, {add_provider, AuthNType, Provider}). + +remove_provider(AuthNType) -> + gen_server:call(?MODULE, {remove_provider, AuthNType}). + +create_chain(Name) -> + gen_server:call(?MODULE, {create_chain, Name}). + +delete_chain(Name) -> + gen_server:call(?MODULE, {delete_chain, Name}). + +lookup_chain(Name) -> + gen_server:call(?MODULE, {lookup_chain, Name}). + +list_chains() -> + Chains = ets:tab2list(?CHAINS_TAB), + {ok, [serialize_chain(Chain) || Chain <- Chains]}. + +create_authenticator(ChainName, Config) -> + gen_server:call(?MODULE, {create_authenticator, ChainName, Config}). + +delete_authenticator(ChainName, AuthenticatorID) -> + gen_server:call(?MODULE, {delete_authenticator, ChainName, AuthenticatorID}). + +update_authenticator(ChainName, AuthenticatorID, Config) -> + gen_server:call(?MODULE, {update_authenticator, ChainName, AuthenticatorID, Config}). + +lookup_authenticator(ChainName, AuthenticatorID) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [#chain{authenticators = Authenticators}] -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + Authenticator -> + {ok, serialize_authenticator(Authenticator)} + end + end. + +list_authenticators(ChainName) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [#chain{authenticators = Authenticators}] -> + {ok, serialize_authenticators(Authenticators)} + end. + +move_authenticator(ChainName, AuthenticatorID, Position) -> + gen_server:call(?MODULE, {move_authenticator, ChainName, AuthenticatorID, Position}). + +import_users(ChainName, AuthenticatorID, Filename) -> + gen_server:call(?MODULE, {import_users, ChainName, AuthenticatorID, Filename}). + +add_user(ChainName, AuthenticatorID, UserInfo) -> + gen_server:call(?MODULE, {add_user, ChainName, AuthenticatorID, UserInfo}). + +delete_user(ChainName, AuthenticatorID, UserID) -> + gen_server:call(?MODULE, {delete_user, ChainName, AuthenticatorID, UserID}). + +update_user(ChainName, AuthenticatorID, UserID, NewUserInfo) -> + gen_server:call(?MODULE, {update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}). + +lookup_user(ChainName, AuthenticatorID, UserID) -> + gen_server:call(?MODULE, {lookup_user, ChainName, AuthenticatorID, UserID}). + +%% TODO: Support pagination +list_users(ChainName, AuthenticatorID) -> + gen_server:call(?MODULE, {list_users, ChainName, AuthenticatorID}). + +generate_id(#{mechanism := Mechanism0, backend := Backend0}) -> + Mechanism = atom_to_binary(Mechanism0), + Backend = atom_to_binary(Backend0), + <>; +generate_id(#{mechanism := Mechanism}) -> + atom_to_binary(Mechanism); +generate_id(#{<<"mechanism">> := Mechanism, <<"backend">> := Backend}) -> + <>; +generate_id(#{<<"mechanism">> := Mechanism}) -> + Mechanism. + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init(_Opts) -> + _ = ets:new(?CHAINS_TAB, [ named_table, set, public + , {keypos, #chain.name} + , {read_concurrency, true}]), + ok = emqx_config_handler:add_handler([authentication], ?MODULE), + ok = emqx_config_handler:add_handler([listeners, '?', '?', authentication], ?MODULE), + {ok, #{hooked => false, providers => #{}}}. + +handle_call({add_provider, AuthNType, Provider}, _From, #{providers := Providers} = State) -> + reply(ok, State#{providers := Providers#{AuthNType => Provider}}); + +handle_call({remove_provider, AuthNType}, _From, #{providers := Providers} = State) -> + reply(ok, State#{providers := maps:remove(AuthNType, Providers)}); + +handle_call(get_refs, _From, #{providers := Providers} = State) -> + Refs = lists:foldl(fun({_, Provider}, Acc) -> + Acc ++ Provider:refs() + end, [], maps:to_list(Providers)), + reply({ok, Refs}, State); + +handle_call({create_chain, Name}, _From, State) -> + case ets:member(?CHAINS_TAB, Name) of + true -> + reply({error, {already_exists, {chain, Name}}}, State); + false -> + Chain = #chain{name = Name, + authenticators = []}, + true = ets:insert(?CHAINS_TAB, Chain), + reply({ok, serialize_chain(Chain)}, State) + end; + +handle_call({delete_chain, Name}, _From, State) -> + case ets:lookup(?CHAINS_TAB, Name) of + [] -> + reply({error, {not_found, {chain, Name}}}, State); + [#chain{authenticators = Authenticators}] -> + _ = [do_delete_authenticator(Authenticator) || Authenticator <- Authenticators], + true = ets:delete(?CHAINS_TAB, Name), + reply(ok, may_unhook(State)) + end; + +handle_call({lookup_chain, Name}, _From, State) -> + case ets:lookup(?CHAINS_TAB, Name) of + [] -> + reply({error, {not_found, {chain, Name}}}, State); + [Chain] -> + reply({ok, serialize_chain(Chain)}, State) + end; + +handle_call({create_authenticator, ChainName, Config}, _From, #{providers := Providers} = State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + AuthenticatorID = generate_id(Config), + case lists:keymember(AuthenticatorID, #authenticator.id, Authenticators) of + true -> + {error, {already_exists, {authenticator, AuthenticatorID}}}; + false -> + case do_create_authenticator(ChainName, AuthenticatorID, Config, Providers) of + {ok, Authenticator} -> + NAuthenticators = Authenticators ++ [Authenticator], + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, may_hook(State)); + +handle_call({delete_authenticator, ChainName, AuthenticatorID}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keytake(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {value, Authenticator, NAuthenticators} -> + _ = do_delete_authenticator(Authenticator), + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + ok + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, may_unhook(State)); + +handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + #authenticator{provider = Provider, + state = #{version := Version} = ST} = Authenticator -> + case AuthenticatorID =:= generate_id(Config) of + true -> + Unique = unique(ChainName, AuthenticatorID, Version), + case Provider:update(Config#{'_unique' => Unique}, ST) of + {ok, NewST} -> + NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST)}, + NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NewAuthenticators}), + {ok, serialize_authenticator(NewAuthenticator)}; + {error, Reason} -> + {error, Reason} + end; + false -> + {error, mechanism_or_backend_change_is_not_alloed} + end + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, State); + +handle_call({move_authenticator, ChainName, AuthenticatorID, Position}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case do_move_authenticator(AuthenticatorID, Authenticators, Position) of + {ok, NAuthenticators} -> + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + ok; + {error, Reason} -> + {error, Reason} + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, State); + +handle_call({import_users, ChainName, AuthenticatorID, Filename}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, import_users, [Filename]), + reply(Reply, State); + +handle_call({add_user, ChainName, AuthenticatorID, UserInfo}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, add_user, [UserInfo]), + reply(Reply, State); + +handle_call({delete_user, ChainName, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, delete_user, [UserID]), + reply(Reply, State); + +handle_call({update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, update_user, [UserID, NewUserInfo]), + reply(Reply, State); + +handle_call({lookup_user, ChainName, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, lookup_user, [UserID]), + reply(Reply, State); + +handle_call({list_users, ChainName, AuthenticatorID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, list_users, []), + reply(Reply, State); + +handle_call(Req, _From, State) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Req, State) -> + ?LOG(error, "Unexpected case: ~p", [Req]), + {noreply, State}. + +handle_info(Info, State) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + emqx_config_handler:remove_handler([authentication]), + emqx_config_handler:remove_handler([listeners, '?', '?', authentication]), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +reply(Reply, State) -> + {reply, Reply, State}. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +split_by_id(ID, AuthenticatorsConfig) -> + case lists:foldl( + fun(C, {P1, P2, F0}) -> + F = case ID =:= generate_id(C) of + true -> true; + false -> F0 + end, + case F of + false -> {[C | P1], P2, F}; + true -> {P1, [C | P2], F} + end + end, {[], [], false}, AuthenticatorsConfig) of + {_, _, false} -> + {error, {not_found, {authenticator, ID}}}; + {Part1, Part2, true} -> + {ok, lists:reverse(Part1), lists:reverse(Part2)} + end. + +global_chain(mqtt) -> + 'mqtt:global'; +global_chain('mqtt-sn') -> + 'mqtt-sn:global'; +global_chain(coap) -> + 'coap:global'; +global_chain(lwm2m) -> + 'lwm2m:global'; +global_chain(stomp) -> + 'stomp:global'; +global_chain(_) -> + 'unknown:global'. + +may_hook(#{hooked := false} = State) -> + case lists:any(fun(#chain{authenticators = []}) -> false; + (_) -> true + end, ets:tab2list(?CHAINS_TAB)) of + true -> + _ = emqx:hook('client.authenticate', {emqx_authentication, authenticate, []}), + State#{hooked => true}; + false -> + State + end; +may_hook(State) -> + State. + +may_unhook(#{hooked := true} = State) -> + case lists:all(fun(#chain{authenticators = []}) -> true; + (_) -> false + end, ets:tab2list(?CHAINS_TAB)) of + true -> + _ = emqx:unhook('client.authenticate', {emqx_authentication, authenticate, []}), + State#{hooked => false}; + false -> + State + end; +may_unhook(State) -> + State. + +do_create_authenticator(ChainName, AuthenticatorID, #{enable := Enable} = Config, Providers) -> + case maps:get(authn_type(Config), Providers, undefined) of + undefined -> + {error, no_available_provider}; + Provider -> + Unique = unique(ChainName, AuthenticatorID, ?VER_1), + case Provider:create(Config#{'_unique' => Unique}) of + {ok, State} -> + Authenticator = #authenticator{id = AuthenticatorID, + provider = Provider, + enable = Enable, + state = switch_version(State)}, + {ok, Authenticator}; + {error, Reason} -> + {error, Reason} + end + end. + +do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> + _ = Provider:destroy(State), + ok. + +replace_authenticator(ID, Authenticator, Authenticators) -> + lists:keyreplace(ID, #authenticator.id, Authenticators, Authenticator). + +do_move_authenticator(ID, Authenticators, Position) -> + case lists:keytake(ID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, ID}}}; + {value, Authenticator, NAuthenticators} -> + case Position of + top -> + {ok, [Authenticator | NAuthenticators]}; + bottom -> + {ok, NAuthenticators ++ [Authenticator]}; + {before, ID0} -> + insert(Authenticator, NAuthenticators, ID0, []) + end + end. + +insert(_, [], ID, _) -> + {error, {not_found, {authenticator, ID}}}; +insert(Authenticator, [#authenticator{id = ID} | _] = Authenticators, ID, Acc) -> + {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]}; +insert(Authenticator, [Authenticator0 | More], ID, Acc) -> + insert(Authenticator, More, ID, [Authenticator0 | Acc]). + +update_chain(ChainName, UpdateFun) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [Chain] -> + UpdateFun(Chain) + end. + +call_authenticator(ChainName, AuthenticatorID, Func, Args) -> + UpdateFun = + fun(#chain{authenticators = Authenticators}) -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + #authenticator{provider = Provider, state = State} -> + case erlang:function_exported(Provider, Func, length(Args) + 1) of + true -> + erlang:apply(Provider, Func, Args ++ [State]); + false -> + {error, unsupported_feature} + end + end + end, + update_chain(ChainName, UpdateFun). + +serialize_chain(#chain{name = Name, + authenticators = Authenticators}) -> + #{ name => Name + , authenticators => serialize_authenticators(Authenticators) + }. + +serialize_authenticators(Authenticators) -> + [serialize_authenticator(Authenticator) || Authenticator <- Authenticators]. + +serialize_authenticator(#authenticator{id = ID, + provider = Provider, + enable = Enable, + state = State}) -> + #{ id => ID + , provider => Provider + , enable => Enable + , state => State + }. + +unique(ChainName, AuthenticatorID, Version) -> + NChainName = atom_to_binary(ChainName), + <>. + +switch_version(State = #{version := ?VER_1}) -> + State#{version := ?VER_2}; +switch_version(State = #{version := ?VER_2}) -> + State#{version := ?VER_1}; +switch_version(State) -> + State#{version => ?VER_1}. + +authn_type(#{mechanism := Mechanism, backend := Backend}) -> + {Mechanism, Backend}; +authn_type(#{mechanism := Mechanism}) -> + Mechanism. + +may_to_map([L]) -> + L; +may_to_map(L) -> + L. + +to_list(undefined) -> + []; +to_list(M) when M =:= #{} -> + []; +to_list(M) when is_map(M) -> + [M]; +to_list(L) when is_list(L) -> + L. diff --git a/apps/emqx/src/emqx_authz_cache.erl b/apps/emqx/src/emqx_authz_cache.erl index a13294da2..10ddbd21c 100644 --- a/apps/emqx/src/emqx_authz_cache.erl +++ b/apps/emqx/src/emqx_authz_cache.erl @@ -18,15 +18,15 @@ -include("emqx.hrl"). --export([ list_authz_cache/1 - , get_authz_cache/3 - , put_authz_cache/4 - , cleanup_authz_cache/1 +-export([ list_authz_cache/0 + , get_authz_cache/2 + , put_authz_cache/3 + , cleanup_authz_cache/0 , empty_authz_cache/0 , dump_authz_cache/0 - , get_cache_max_size/1 - , get_cache_ttl/1 - , is_enabled/1 + , get_cache_max_size/0 + , get_cache_ttl/0 + , is_enabled/0 , drain_cache/0 ]). @@ -50,45 +50,45 @@ cache_k(PubSub, Topic)-> {PubSub, Topic}. cache_v(AuthzResult)-> {AuthzResult, time_now()}. drain_k() -> {?MODULE, drain_timestamp}. --spec(is_enabled(atom()) -> boolean()). -is_enabled(Zone) -> - emqx_config:get_zone_conf(Zone, [authorization, cache, enable]). +-spec(is_enabled() -> boolean()). +is_enabled() -> + emqx:get_config([authorization, cache, enable], false). --spec(get_cache_max_size(atom()) -> integer()). -get_cache_max_size(Zone) -> - emqx_config:get_zone_conf(Zone, [authorization, cache, max_size]). +-spec(get_cache_max_size() -> integer()). +get_cache_max_size() -> + emqx:get_config([authorization, cache, max_size]). --spec(get_cache_ttl(atom()) -> integer()). -get_cache_ttl(Zone) -> - emqx_config:get_zone_conf(Zone, [authorization, cache, ttl]). +-spec(get_cache_ttl() -> integer()). +get_cache_ttl() -> + emqx:get_config([authorization, cache, ttl]). --spec(list_authz_cache(atom()) -> [authz_cache_entry()]). -list_authz_cache(Zone) -> - cleanup_authz_cache(Zone), +-spec(list_authz_cache() -> [authz_cache_entry()]). +list_authz_cache() -> + cleanup_authz_cache(), map_authz_cache(fun(Cache) -> Cache end). %% We'll cleanup the cache before replacing an expired authz. --spec get_authz_cache(atom(), emqx_types:pubsub(), emqx_topic:topic()) -> +-spec get_authz_cache(emqx_types:pubsub(), emqx_topic:topic()) -> authz_result() | not_found. -get_authz_cache(Zone, PubSub, Topic) -> +get_authz_cache(PubSub, Topic) -> case erlang:get(cache_k(PubSub, Topic)) of undefined -> not_found; {AuthzResult, CachedAt} -> - if_expired(get_cache_ttl(Zone), CachedAt, + if_expired(get_cache_ttl(), CachedAt, fun(false) -> AuthzResult; (true) -> - cleanup_authz_cache(Zone), + cleanup_authz_cache(), not_found end) end. %% If the cache get full, and also the latest one %% is expired, then delete all the cache entries --spec put_authz_cache(atom(), emqx_types:pubsub(), emqx_topic:topic(), authz_result()) +-spec put_authz_cache(emqx_types:pubsub(), emqx_topic:topic(), authz_result()) -> ok. -put_authz_cache(Zone, PubSub, Topic, AuthzResult) -> - MaxSize = get_cache_max_size(Zone), true = (MaxSize =/= 0), +put_authz_cache(PubSub, Topic, AuthzResult) -> + MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), Size = get_cache_size(), case Size < MaxSize of true -> @@ -96,7 +96,7 @@ put_authz_cache(Zone, PubSub, Topic, AuthzResult) -> false -> NewestK = get_newest_key(), {_AuthzResult, CachedAt} = erlang:get(NewestK), - if_expired(get_cache_ttl(Zone), CachedAt, + if_expired(get_cache_ttl(), CachedAt, fun(true) -> % all cache expired, cleanup first empty_authz_cache(), @@ -123,10 +123,10 @@ evict_authz_cache() -> decr_cache_size(). %% cleanup all the expired cache entries --spec(cleanup_authz_cache(atom()) -> ok). -cleanup_authz_cache(Zone) -> +-spec(cleanup_authz_cache() -> ok). +cleanup_authz_cache() -> keys_queue_set( - cleanup_authz(get_cache_ttl(Zone), keys_queue_get())). + cleanup_authz(get_cache_ttl(), keys_queue_get())). get_oldest_key() -> keys_queue_pick(queue_front()). @@ -143,8 +143,8 @@ dump_authz_cache() -> map_authz_cache(fun(Cache) -> Cache end). map_authz_cache(Fun) -> - [Fun(R) || R = {{SubPub, _T}, _Authz} <- get(), SubPub =:= publish - orelse SubPub =:= subscribe]. + [Fun(R) || R = {{SubPub, _T}, _Authz} <- erlang:get(), + SubPub =:= publish orelse SubPub =:= subscribe]. foreach_authz_cache(Fun) -> _ = map_authz_cache(Fun), ok. diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index f0991d967..608734363 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/apps/emqx/src/emqx_banned.erl @@ -33,8 +33,11 @@ -export([ check/1 , create/1 + , look_up/1 , delete/1 , info/1 + , format/1 + , parse/1 ]). %% gen_server callbacks @@ -50,8 +53,6 @@ -define(BANNED_TAB, ?MODULE). --rlog_shard({?COMMON_SHARD, ?BANNED_TAB}). - %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -59,6 +60,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?BANNED_TAB, [ {type, set}, + {rlog_shard, ?COMMON_SHARD}, {disc_copies, [node()]}, {record_name, banned}, {attributes, record_info(fields, banned)}, @@ -91,7 +93,63 @@ do_check(Who) when is_tuple(Who) -> Until > erlang:system_time(second) end. --spec(create(emqx_types:banned()) -> ok). +format(#banned{who = Who0, + by = By, + reason = Reason, + at = At, + until = Until}) -> + {As, Who} = maybe_format_host(Who0), + #{ + as => As, + who => Who, + by => By, + reason => Reason, + at => to_rfc3339(At), + until => to_rfc3339(Until) + }. + +parse(Params) -> + Who = pares_who(Params), + By = maps:get(<<"by">>, Params, <<"mgmt_api">>), + Reason = maps:get(<<"reason">>, Params, <<"">>), + At = pares_time(maps:get(<<"at">>, Params, undefined), erlang:system_time(second)), + Until = pares_time(maps:get(<<"until">>, Params, undefined), At + 5 * 60), + #banned{ + who = Who, + by = By, + reason = Reason, + at = At, + until = Until + }. + +pares_who(#{as := As, who := Who}) -> + pares_who(#{<<"as">> => As, <<"who">> => Who}); +pares_who(#{<<"as">> := <<"peerhost">>, <<"who">> := Peerhost0}) -> + {ok, Peerhost} = inet:parse_address(binary_to_list(Peerhost0)), + {peerhost, Peerhost}; +pares_who(#{<<"as">> := As, <<"who">> := Who}) -> + {binary_to_atom(As, utf8), Who}. + +pares_time(undefined, Default) -> + Default; +pares_time(Rfc3339, _Default) -> + to_timestamp(Rfc3339). + +maybe_format_host({peerhost, Host}) -> + AddrBinary = list_to_binary(inet:ntoa(Host)), + {peerhost, AddrBinary}; +maybe_format_host({As, Who}) -> + {As, Who}. + +to_rfc3339(Timestamp) -> + list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, second}])). + +to_timestamp(Rfc3339) when is_binary(Rfc3339) -> + to_timestamp(binary_to_list(Rfc3339)); +to_timestamp(Rfc3339) -> + calendar:rfc3339_to_system_time(Rfc3339, [{unit, second}]). + +-spec(create(emqx_types:banned() | map()) -> ok). create(#{who := Who, by := By, reason := Reason, @@ -105,9 +163,16 @@ create(#{who := Who, create(Banned) when is_record(Banned, banned) -> ekka_mnesia:dirty_write(?BANNED_TAB, Banned). +look_up(Who) when is_map(Who) -> + look_up(pares_who(Who)); +look_up(Who) -> + mnesia:dirty_read(?BANNED_TAB, Who). + -spec(delete({clientid, emqx_types:clientid()} | {username, emqx_types:username()} | {peerhost, emqx_types:peerhost()}) -> ok). +delete(Who) when is_map(Who)-> + delete(pares_who(Who)); delete(Who) -> ekka_mnesia:dirty_delete(?BANNED_TAB, Who). diff --git a/apps/emqx/src/emqx_broker.erl b/apps/emqx/src/emqx_broker.erl index 1248f9980..46accb9fe 100644 --- a/apps/emqx/src/emqx_broker.erl +++ b/apps/emqx/src/emqx_broker.erl @@ -242,7 +242,7 @@ route(Routes, Delivery) -> do_route({To, Node}, Delivery) when Node =:= node() -> {Node, To, dispatch(To, Delivery)}; do_route({To, Node}, Delivery) when is_atom(Node) -> - {Node, To, forward(Node, To, Delivery, emqx_config:get([rpc, mode]))}; + {Node, To, forward(Node, To, Delivery, emqx:get_config([rpc, mode]))}; do_route({To, Group}, Delivery) when is_tuple(Group); is_binary(Group) -> {share, To, emqx_shared_sub:dispatch(Group, To, Delivery)}. diff --git a/apps/emqx/src/emqx_broker_sup.erl b/apps/emqx/src/emqx_broker_sup.erl index 69df72408..a479e9ff1 100644 --- a/apps/emqx/src/emqx_broker_sup.erl +++ b/apps/emqx/src/emqx_broker_sup.erl @@ -43,6 +43,14 @@ init([]) -> type => worker, modules => [emqx_shared_sub]}, + %% Authentication + AuthN = #{id => authn, + start => {emqx_authentication, start_link, []}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => [emqx_authentication]}, + %% Broker helper Helper = #{id => helper, start => {emqx_broker_helper, start_link, []}, @@ -51,5 +59,5 @@ init([]) -> type => worker, modules => [emqx_broker_helper]}, - {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}. + {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, AuthN, Helper]}}. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5e4d11953..0b1ff7e25 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -99,7 +99,7 @@ -type(channel() :: #channel{}). --type(opts() :: #{zone := atom(), listener := atom(), atom() => term()}). +-type(opts() :: #{zone := atom(), listener := {Type::atom(), Name::atom()}, atom() => term()}). -type(conn_state() :: idle | connecting | connected | reauthenticating | disconnected). @@ -202,18 +202,19 @@ caps(#channel{clientinfo = #{zone := Zone}}) -> -spec(init(emqx_types:conninfo(), opts()) -> channel()). init(ConnInfo = #{peername := {PeerHost, _Port}, - sockname := {_Host, SockPort}}, #{zone := Zone, listener := Listener}) -> + sockname := {_Host, SockPort}}, + #{zone := Zone, listener := {Type, Listener}}) -> Peercert = maps:get(peercert, ConnInfo, undefined), Protocol = maps:get(protocol, ConnInfo, mqtt), - MountPoint = case get_mqtt_conf(Zone, mountpoint) of + MountPoint = case emqx_config:get_listener_conf(Type, Listener, [mountpoint]) of <<>> -> undefined; MP -> MP end, - QuotaPolicy = emqx_config:get_listener_conf(Zone, Listener,[rate_limit, quota], []), + QuotaPolicy = emqx_config:get_zone_conf(Zone, [quota], #{}), ClientInfo = set_peercert_infos( Peercert, #{zone => Zone, - listener => Listener, + listener => emqx_listeners:listener_id(Type, Listener), protocol => Protocol, peerhost => PeerHost, sockport => SockPort, @@ -222,7 +223,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, mountpoint => MountPoint, is_bridge => false, is_superuser => false - }, Zone, Listener), + }, Zone), {NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo), #channel{conninfo = NConnInfo, clientinfo = NClientInfo, @@ -243,12 +244,12 @@ quota_policy(RawPolicy) -> erlang:trunc(hocon_postprocess:duration(StrWind) / 1000)}} || {Name, [StrCount, StrWind]} <- maps:to_list(RawPolicy)]. -set_peercert_infos(NoSSL, ClientInfo, _, _) +set_peercert_infos(NoSSL, ClientInfo, _) when NoSSL =:= nossl; NoSSL =:= undefined -> ClientInfo#{username => undefined}; -set_peercert_infos(Peercert, ClientInfo, Zone, _Listener) -> +set_peercert_infos(Peercert, ClientInfo, Zone) -> {DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)}, PeercetAs = fun(Key) -> @@ -425,7 +426,7 @@ handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = S end; handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), - Channel = #channel{clientinfo = ClientInfo = #{zone := Zone}}) -> + Channel = #channel{clientinfo = ClientInfo}) -> case emqx_packet:check(Packet) of ok -> TopicFilters0 = parse_topic_filters(TopicFilters), @@ -434,7 +435,7 @@ handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), HasAuthzDeny = lists:any(fun({_TopicFilter, ReasonCode}) -> ReasonCode =:= ?RC_NOT_AUTHORIZED end, TupleTopicFilters0), - DenyAction = emqx_config:get_zone_conf(Zone, [authorization, deny_action]), + DenyAction = emqx:get_config([authorization, deny_action], ignore), case DenyAction =:= disconnect andalso HasAuthzDeny of true -> handle_out(disconnect, ?RC_NOT_AUTHORIZED, Channel); false -> @@ -536,8 +537,7 @@ process_connect(AckProps, Channel = #channel{conninfo = ConnInfo, %% Process Publish %%-------------------------------------------------------------------- -process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), - Channel = #channel{clientinfo = #{zone := Zone}}) -> +process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel) -> case pipeline([fun check_quota_exceeded/2, fun process_alias/2, fun check_pub_alias/2, @@ -550,7 +550,7 @@ process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), {error, Rc = ?RC_NOT_AUTHORIZED, NChannel} -> ?LOG(warning, "Cannot publish message to ~s due to ~s.", [Topic, emqx_reason_codes:text(Rc)]), - case emqx_config:get_zone_conf(Zone, [authorization, deny_action]) of + case emqx:get_config([authorization, deny_action], ignore) of ignore -> case QoS of ?QOS_0 -> {ok, NChannel}; @@ -955,9 +955,8 @@ handle_call({takeover, 'end'}, Channel = #channel{session = Session, AllPendings = lists:append(Delivers, Pendings), disconnect_and_shutdown(takeovered, AllPendings, Channel); -handle_call(list_authz_cache, #channel{clientinfo = #{zone := Zone}} - = Channel) -> - {reply, emqx_authz_cache:list_authz_cache(Zone), Channel}; +handle_call(list_authz_cache, Channel) -> + {reply, emqx_authz_cache:list_authz_cache(), Channel}; handle_call({quota, Policy}, Channel) -> Zone = info(zone, Channel), @@ -1299,14 +1298,17 @@ authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properti {error, ?RC_BAD_AUTHENTICATION_METHOD} end. -do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> +do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = ClientInfo} = Channel) -> Properties = #{'Authentication-Method' => AuthMethod}, case emqx_access_control:authenticate(Credential) of - ok -> - {ok, Properties, Channel#channel{auth_cache = #{}}}; - {ok, AuthData} -> + {ok, Result} -> + {ok, Properties, + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)}, + auth_cache = #{}}}; + {ok, Result, AuthData} -> {ok, Properties#{'Authentication-Data' => AuthData}, - Channel#channel{auth_cache = #{}}}; + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)}, + auth_cache = #{}}}; {continue, AuthCache} -> {continue, Properties, Channel#channel{auth_cache = AuthCache}}; {continue, AuthData, AuthCache} -> @@ -1316,10 +1318,10 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> {error, emqx_reason_codes:connack_error(Reason)} end; -do_authenticate(Credential, Channel) -> +do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - ok -> - {ok, #{}, Channel}; + {ok, #{is_superuser := IsSuperuser}} -> + {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => IsSuperuser}}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} end. @@ -1417,9 +1419,7 @@ check_pub_alias(_Packet, _Channel) -> ok. check_pub_authz(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, #channel{clientinfo = ClientInfo}) -> - case is_authz_enabled(ClientInfo) andalso - emqx_access_control:authorize(ClientInfo, publish, Topic) of - false -> ok; + case emqx_access_control:authorize(ClientInfo, publish, Topic) of allow -> ok; deny -> {error, ?RC_NOT_AUTHORIZED} end. @@ -1440,8 +1440,10 @@ check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, check_sub_authzs(TopicFilters, Channel) -> check_sub_authzs(TopicFilters, Channel, []). -check_sub_authzs([ TopicFilter = {Topic, _} | More] , Channel, Acc) -> - case check_sub_authz(Topic, Channel) of +check_sub_authzs([ TopicFilter = {Topic, _} | More], + Channel = #channel{clientinfo = ClientInfo}, + Acc) -> + case emqx_access_control:authorize(ClientInfo, subscribe, Topic) of allow -> check_sub_authzs(More, Channel, [ {TopicFilter, 0} | Acc]); deny -> @@ -1450,13 +1452,6 @@ check_sub_authzs([ TopicFilter = {Topic, _} | More] , Channel, Acc) -> check_sub_authzs([], _Channel, Acc) -> lists:reverse(Acc). -check_sub_authz(TopicFilter, #channel{clientinfo = ClientInfo}) -> - case is_authz_enabled(ClientInfo) andalso - emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of - false -> allow; - Result -> Result - end. - %%-------------------------------------------------------------------- %% Check Sub Caps @@ -1618,11 +1613,6 @@ maybe_shutdown(Reason, Channel = #channel{conninfo = ConnInfo}) -> _ -> shutdown(Reason, Channel) end. -%%-------------------------------------------------------------------- -%% Is Authorization enabled? -is_authz_enabled(#{zone := Zone, is_superuser := IsSuperuser}) -> - (not IsSuperuser) andalso emqx_config:get_zone_conf(Zone, [authorization, enable]). - %%-------------------------------------------------------------------- %% Parse Topic Filters diff --git a/apps/emqx/src/emqx_cm_locker.erl b/apps/emqx/src/emqx_cm_locker.erl index c1a85d6c9..5a336d61c 100644 --- a/apps/emqx/src/emqx_cm_locker.erl +++ b/apps/emqx/src/emqx_cm_locker.erl @@ -62,5 +62,5 @@ unlock(ClientId) -> -spec(strategy() -> local | leader | quorum | all). strategy() -> - emqx_config:get([broker, session_locking_strategy]). + emqx:get_config([broker, session_locking_strategy]). diff --git a/apps/emqx/src/emqx_cm_registry.erl b/apps/emqx/src/emqx_cm_registry.erl index c04f6ccaf..6fc34dee8 100644 --- a/apps/emqx/src/emqx_cm_registry.erl +++ b/apps/emqx/src/emqx_cm_registry.erl @@ -47,10 +47,6 @@ -define(TAB, emqx_channel_registry). -define(LOCK, {?MODULE, cleanup_down}). --define(CM_SHARD, emqx_cm_shard). - --rlog_shard({?CM_SHARD, ?TAB}). - -record(channel, {chid, pid}). %% @doc Start the global channel registry. @@ -65,7 +61,7 @@ start_link() -> %% @doc Is the global registry enabled? -spec(is_enabled() -> boolean()). is_enabled() -> - emqx_config:get([broker, enable_session_registry]). + emqx:get_config([broker, enable_session_registry]). %% @doc Register a global channel. -spec(register_channel(emqx_types:clientid() @@ -106,6 +102,7 @@ record(ClientId, ChanPid) -> init([]) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, + {rlog_shard, ?CM_SHARD}, {ram_copies, [node()]}, {record_name, channel}, {attributes, record_info(fields, channel)}, diff --git a/apps/emqx/src/emqx_cm_sup.erl b/apps/emqx/src/emqx_cm_sup.erl index f332a0868..cddd8aa5e 100644 --- a/apps/emqx/src/emqx_cm_sup.erl +++ b/apps/emqx/src/emqx_cm_sup.erl @@ -22,49 +22,38 @@ -export([init/1]). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). +%%-------------------------------------------------------------------- +%% Supervisor callbacks +%%-------------------------------------------------------------------- + init([]) -> - Banned = #{id => banned, - start => {emqx_banned, start_link, []}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [emqx_banned]}, - Flapping = #{id => flapping, - start => {emqx_flapping, start_link, []}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [emqx_flapping]}, - %% Channel locker - Locker = #{id => locker, - start => {emqx_cm_locker, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm_locker] - }, - %% Channel registry - Registry = #{id => registry, - start => {emqx_cm_registry, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm_registry] - }, - %% Channel Manager - Manager = #{id => manager, - start => {emqx_cm, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm] - }, SupFlags = #{strategy => one_for_one, intensity => 100, period => 10 }, + Banned = child_spec(emqx_banned, 1000, worker), + Flapping = child_spec(emqx_flapping, 1000, worker), + Locker = child_spec(emqx_cm_locker, 5000, worker), + Registry = child_spec(emqx_cm_registry, 5000, worker), + Manager = child_spec(emqx_cm, 5000, worker), {ok, {SupFlags, [Banned, Flapping, Locker, Registry, Manager]}}. +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +child_spec(Mod, Shutdown, Type) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => Shutdown, + type => Type, + modules => [Mod] + }. diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 151cf8e8e..bd6e14e8e 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -20,14 +20,20 @@ -export([ init_load/2 , read_override_conf/0 , check_config/2 + , fill_defaults/1 + , fill_defaults/2 , save_configs/4 , save_to_app_env/1 , save_to_config_map/2 , save_to_override_conf/1 ]). --export([get_root/1, - get_root_raw/1]). +-export([ get_root/1 + , get_root_raw/1 + ]). + +-export([ get_default_value/1 + ]). -export([ get/1 , get/2 @@ -37,10 +43,21 @@ , put/2 ]). +-export([ get_raw/1 + , get_raw/2 + , put_raw/1 + , put_raw/2 + ]). + +-export([ save_schema_mod_and_names/1 + , get_schema_mod/0 + , get_schema_mod/1 + , get_root_names/0 + ]). + -export([ get_zone_conf/2 , get_zone_conf/3 , put_zone_conf/3 - , find_zone_conf/2 ]). -export([ get_listener_conf/3 @@ -49,23 +66,12 @@ , find_listener_conf/3 ]). --export([ update/2 - , update/3 - , remove/1 - , remove/2 - ]). - --export([ get_raw/1 - , get_raw/2 - , put_raw/1 - , put_raw/2 - ]). - -define(CONF, conf). -define(RAW_CONF, raw_conf). +-define(PERSIS_SCHEMA_MODS, {?MODULE, schema_mods}). -define(PERSIS_KEY(TYPE, ROOT), {?MODULE, TYPE, ROOT}). -define(ZONE_CONF_PATH(ZONE, PATH), [zones, ZONE | PATH]). --define(LISTENER_CONF_PATH(ZONE, LISTENER, PATH), [zones, ZONE, listeners, LISTENER | PATH]). +-define(LISTENER_CONF_PATH(TYPE, LISTENER, PATH), [listeners, TYPE, LISTENER | PATH]). -define(ATOM_CONF_PATH(PATH, EXP, EXP_ON_FAIL), try [atom(Key) || Key <- PATH] of @@ -74,12 +80,35 @@ error:badarg -> EXP_ON_FAIL end). --export_type([update_request/0, raw_config/0, config/0]). +-export_type([update_request/0, raw_config/0, config/0, app_envs/0, + update_opts/0, update_cmd/0, update_args/0, + update_error/0, update_result/0]). + -type update_request() :: term(). +-type update_cmd() :: {update, update_request()} | remove. +-type update_opts() :: #{ + %% rawconf_with_defaults: + %% fill the default values into the `raw_config` field of the return value + %% defaults to `false` + rawconf_with_defaults => boolean(), + %% persistent: + %% save the updated config to the emqx_override.conf file + %% defaults to `true` + persistent => boolean() + }. +-type update_args() :: {update_cmd(), Opts :: update_opts()}. +-type update_stage() :: pre_config_update | post_config_update. +-type update_error() :: {update_stage(), module(), term()} | {save_configs, term()} | term(). +-type update_result() :: #{ + config => emqx_config:config(), + raw_config => emqx_config:raw_config(), + post_config_update => #{module() => any()} +}. + %% raw_config() is the config that is NOT parsed and tranlated by hocon schema --type raw_config() :: #{binary() => term()} | undefined. +-type raw_config() :: #{binary() => term()} | list() | undefined. %% config() is the config that is parsed and tranlated by hocon schema --type config() :: #{atom() => term()} | undefined. +-type config() :: #{atom() => term()} | list() | undefined. -type app_envs() :: [proplists:property()]. %% @doc For the given path, get root value enclosed in a single-key map. @@ -127,63 +156,66 @@ find_raw(KeyPath) -> -spec get_zone_conf(atom(), emqx_map_lib:config_key_path()) -> term(). get_zone_conf(Zone, KeyPath) -> - ?MODULE:get(?ZONE_CONF_PATH(Zone, KeyPath)). + case find(?ZONE_CONF_PATH(Zone, KeyPath)) of + {not_found, _, _} -> %% not found in zones, try to find the global config + ?MODULE:get(KeyPath); + {ok, Value} -> Value + end. -spec get_zone_conf(atom(), emqx_map_lib:config_key_path(), term()) -> term(). get_zone_conf(Zone, KeyPath, Default) -> - ?MODULE:get(?ZONE_CONF_PATH(Zone, KeyPath), Default). + case find(?ZONE_CONF_PATH(Zone, KeyPath)) of + {not_found, _, _} -> %% not found in zones, try to find the global config + ?MODULE:get(KeyPath, Default); + {ok, Value} -> Value + end. -spec put_zone_conf(atom(), emqx_map_lib:config_key_path(), term()) -> ok. put_zone_conf(Zone, KeyPath, Conf) -> ?MODULE:put(?ZONE_CONF_PATH(Zone, KeyPath), Conf). --spec find_zone_conf(atom(), emqx_map_lib:config_key_path()) -> - {ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}. -find_zone_conf(Zone, KeyPath) -> - find(?ZONE_CONF_PATH(Zone, KeyPath)). - -spec get_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> term(). -get_listener_conf(Zone, Listener, KeyPath) -> - ?MODULE:get(?LISTENER_CONF_PATH(Zone, Listener, KeyPath)). +get_listener_conf(Type, Listener, KeyPath) -> + ?MODULE:get(?LISTENER_CONF_PATH(Type, Listener, KeyPath)). -spec get_listener_conf(atom(), atom(), emqx_map_lib:config_key_path(), term()) -> term(). -get_listener_conf(Zone, Listener, KeyPath, Default) -> - ?MODULE:get(?LISTENER_CONF_PATH(Zone, Listener, KeyPath), Default). +get_listener_conf(Type, Listener, KeyPath, Default) -> + ?MODULE:get(?LISTENER_CONF_PATH(Type, Listener, KeyPath), Default). -spec put_listener_conf(atom(), atom(), emqx_map_lib:config_key_path(), term()) -> ok. -put_listener_conf(Zone, Listener, KeyPath, Conf) -> - ?MODULE:put(?LISTENER_CONF_PATH(Zone, Listener, KeyPath), Conf). +put_listener_conf(Type, Listener, KeyPath, Conf) -> + ?MODULE:put(?LISTENER_CONF_PATH(Type, Listener, KeyPath), Conf). -spec find_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> {ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}. -find_listener_conf(Zone, Listener, KeyPath) -> - find(?LISTENER_CONF_PATH(Zone, Listener, KeyPath)). +find_listener_conf(Type, Listener, KeyPath) -> + find(?LISTENER_CONF_PATH(Type, Listener, KeyPath)). -spec put(map()) -> ok. put(Config) -> maps:fold(fun(RootName, RootValue, _) -> - ?MODULE:put([RootName], RootValue) - end, [], Config). + ?MODULE:put([RootName], RootValue) + end, ok, Config). -spec put(emqx_map_lib:config_key_path(), term()) -> ok. put(KeyPath, Config) -> do_put(?CONF, KeyPath, Config). --spec update(emqx_map_lib:config_key_path(), update_request()) -> - ok | {error, term()}. -update(KeyPath, UpdateReq) -> - update(emqx_schema, KeyPath, UpdateReq). - --spec update(module(), emqx_map_lib:config_key_path(), update_request()) -> - ok | {error, term()}. -update(SchemaModule, KeyPath, UpdateReq) -> - emqx_config_handler:update_config(SchemaModule, KeyPath, {update, UpdateReq}). - --spec remove(emqx_map_lib:config_key_path()) -> ok | {error, term()}. -remove(KeyPath) -> - remove(emqx_schema, KeyPath). - -remove(SchemaModule, KeyPath) -> - emqx_config_handler:update_config(SchemaModule, KeyPath, remove). +-spec get_default_value(emqx_map_lib:config_key_path()) -> {ok, term()} | {error, term()}. +get_default_value([RootName | _] = KeyPath) -> + BinKeyPath = [bin(Key) || Key <- KeyPath], + case find_raw([RootName]) of + {ok, RawConf} -> + RawConf1 = emqx_map_lib:deep_remove(BinKeyPath, #{bin(RootName) => RawConf}), + try fill_defaults(get_schema_mod(RootName), RawConf1) of FullConf -> + case emqx_map_lib:deep_find(BinKeyPath, FullConf) of + {not_found, _, _} -> {error, no_default_value}; + {ok, Val} -> {ok, Val} + end + catch error : Reason -> {error, Reason} + end; + {not_found, _, _} -> + {error, {rootname_not_found, RootName}} + end. -spec get_raw(emqx_map_lib:config_key_path()) -> term(). get_raw(KeyPath) -> do_get(?RAW_CONF, KeyPath). @@ -194,8 +226,8 @@ get_raw(KeyPath, Default) -> do_get(?RAW_CONF, KeyPath, Default). -spec put_raw(map()) -> ok. put_raw(Config) -> maps:fold(fun(RootName, RootV, _) -> - ?MODULE:put_raw([RootName], RootV) - end, [], hocon_schema:get_value([], Config)). + ?MODULE:put_raw([RootName], RootV) + end, ok, hocon_schema:get_value([], Config)). -spec put_raw(emqx_map_lib:config_key_path(), term()) -> ok. put_raw(KeyPath, Config) -> do_put(?RAW_CONF, KeyPath, Config). @@ -208,47 +240,93 @@ put_raw(KeyPath, Config) -> do_put(?RAW_CONF, KeyPath, Config). %% NOTE: The order of the files is significant, configs from files orderd %% in the rear of the list overrides prior values. -spec init_load(module(), [string()] | binary() | hocon:config()) -> ok. -init_load(SchemaModule, Conf) when is_list(Conf) orelse is_binary(Conf) -> - ParseOptions = #{format => richmap}, +init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) -> + ParseOptions = #{format => map}, Parser = case is_binary(Conf) of true -> fun hocon:binary/2; false -> fun hocon:files/2 end, case Parser(Conf, ParseOptions) of {ok, RawRichConf} -> - init_load(SchemaModule, RawRichConf); + init_load(SchemaMod, RawRichConf); {error, Reason} -> logger:error(#{msg => failed_to_load_hocon_conf, reason => Reason }), error(failed_to_load_hocon_conf) end; -init_load(SchemaModule, RawRichConf) when is_map(RawRichConf) -> - %% check with richmap for line numbers in error reports (future enhancement) - Opts = #{return_plain => true, - nullable => true - }, - %% this call throws exception in case of check failure - {_AppEnvs, CheckedConf} = hocon_schema:map_translate(SchemaModule, RawRichConf, Opts), - ok = save_to_config_map(emqx_map_lib:unsafe_atom_key_map(CheckedConf), - hocon_schema:richmap_to_map(RawRichConf)). +init_load(SchemaMod, RawConf0) when is_map(RawConf0) -> + ok = save_schema_mod_and_names(SchemaMod), + %% override part of the input conf using emqx_override.conf + RawConf = merge_with_override_conf(RawConf0), + %% check and save configs + {_AppEnvs, CheckedConf} = check_config(SchemaMod, RawConf), + ok = save_to_config_map(maps:with(get_atom_root_names(), CheckedConf), + maps:with(get_root_names(), RawConf)). + +merge_with_override_conf(RawConf) -> + maps:merge(RawConf, maps:with(maps:keys(RawConf), read_override_conf())). -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). -check_config(SchemaModule, RawConf) -> +check_config(SchemaMod, RawConf) -> Opts = #{return_plain => true, nullable => true, format => map }, {AppEnvs, CheckedConf} = - hocon_schema:map_translate(SchemaModule, RawConf, Opts), + hocon_schema:map_translate(SchemaMod, RawConf, Opts), Conf = maps:with(maps:keys(RawConf), CheckedConf), {AppEnvs, emqx_map_lib:unsafe_atom_key_map(Conf)}. +-spec fill_defaults(raw_config()) -> map(). +fill_defaults(RawConf) -> + RootNames = get_root_names(), + maps:fold(fun(Key, Conf, Acc) -> + SubMap = #{Key => Conf}, + WithDefaults = case lists:member(Key, RootNames) of + true -> fill_defaults(get_schema_mod(Key), SubMap); + false -> SubMap + end, + maps:merge(Acc, WithDefaults) + end, #{}, RawConf). + +-spec fill_defaults(module(), raw_config()) -> map(). +fill_defaults(SchemaMod, RawConf) -> + hocon_schema:check_plain(SchemaMod, RawConf, + #{nullable => true, no_conversion => true}, root_names_from_conf(RawConf)). + -spec read_override_conf() -> raw_config(). read_override_conf() -> load_hocon_file(emqx_override_conf_name(), map). +-spec save_schema_mod_and_names(module()) -> ok. +save_schema_mod_and_names(SchemaMod) -> + RootNames = hocon_schema:root_names(SchemaMod), + OldMods = get_schema_mod(), + OldNames = get_root_names(), + %% map from root name to schema module name + NewMods = maps:from_list([{Name, SchemaMod} || Name <- RootNames]), + persistent_term:put(?PERSIS_SCHEMA_MODS, #{ + mods => maps:merge(OldMods, NewMods), + names => lists:usort(OldNames ++ RootNames) + }). + +-spec get_schema_mod() -> #{binary() => atom()}. +get_schema_mod() -> + maps:get(mods, persistent_term:get(?PERSIS_SCHEMA_MODS, #{mods => #{}})). + +-spec get_schema_mod(atom() | binary()) -> module(). +get_schema_mod(RootName) -> + maps:get(bin(RootName), get_schema_mod()). + +-spec get_root_names() -> [binary()]. +get_root_names() -> + maps:get(names, persistent_term:get(?PERSIS_SCHEMA_MODS, #{names => []})). + +get_atom_root_names() -> + [atom(N) || N <- get_root_names()]. + -spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}. save_configs(_AppEnvs, Conf, RawConf, OverrideConf) -> %% We may need also support hot config update for the apps that use application envs. @@ -270,14 +348,19 @@ save_to_config_map(Conf, RawConf) -> ?MODULE:put_raw(RawConf). -spec save_to_override_conf(raw_config()) -> ok | {error, term()}. +save_to_override_conf(undefined) -> + ok; save_to_override_conf(RawConf) -> - FileName = emqx_override_conf_name(), - ok = filelib:ensure_dir(FileName), - case file:write_file(FileName, jsx:prettify(jsx:encode(RawConf))) of - ok -> ok; - {error, Reason} -> - logger:error("write to ~s failed, ~p", [FileName, Reason]), - {error, Reason} + case emqx_override_conf_name() of + undefined -> ok; + FileName -> + ok = filelib:ensure_dir(FileName), + case file:write_file(FileName, jsx:prettify(jsx:encode(RawConf))) of + ok -> ok; + {error, Reason} -> + logger:error("write to ~s failed, ~p", [FileName, Reason]), + {error, Reason} + end end. load_hocon_file(FileName, LoadType) -> @@ -289,7 +372,7 @@ load_hocon_file(FileName, LoadType) -> end. emqx_override_conf_name() -> - application:get_env(emqx, override_conf_file, "emqx_override.conf"). + application:get_env(emqx, override_conf_file, undefined). do_get(Type, KeyPath) -> Ref = make_ref(), @@ -336,12 +419,19 @@ do_deep_put(?CONF, KeyPath, Map, Value) -> do_deep_put(?RAW_CONF, KeyPath, Map, Value) -> emqx_map_lib:deep_put([bin(Key) || Key <- KeyPath], Map, Value). +root_names_from_conf(RawConf) -> + Keys = maps:keys(RawConf), + [Name || Name <- get_root_names(), lists:member(Name, Keys)]. + atom(Bin) when is_binary(Bin) -> binary_to_existing_atom(Bin, latin1); +atom(Str) when is_list(Str) -> + list_to_existing_atom(Str); atom(Atom) when is_atom(Atom) -> Atom. bin(Bin) when is_binary(Bin) -> Bin; +bin(Str) when is_list(Str) -> list_to_binary(Str); bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8). conf_key(?CONF, RootName) -> diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index e4285b503..d92f1d35a 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -23,7 +23,9 @@ %% API functions -export([ start_link/0 + , stop/0 , add_handler/2 + , remove_handler/1 , update_config/3 , merge_to_old_config/2 ]). @@ -37,20 +39,28 @@ code_change/3]). -define(MOD, {mod}). +-define(WKEY, '?'). + +-define(ATOM_CONF_PATH(PATH, EXP, EXP_ON_FAIL), + try [safe_atom(Key) || Key <- PATH] of + AtomKeyPath -> EXP + catch + error:badarg -> EXP_ON_FAIL + end). -type handler_name() :: module(). -type handlers() :: #{emqx_config:config_key() => handlers(), ?MOD => handler_name()}. --type update_args() :: {update, emqx_config:update_request()} | remove. -optional_callbacks([ pre_config_update/2 - , post_config_update/3 + , post_config_update/4 ]). -callback pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) -> - emqx_config:update_request(). + {ok, emqx_config:update_request()} | {error, term()}. -callback post_config_update(emqx_config:update_request(), emqx_config:config(), - emqx_config:config()) -> any(). + emqx_config:config(), emqx_config:app_envs()) -> + ok | {ok, Result::any()} | {error, Reason::term()}. -type state() :: #{ handlers := handlers(), @@ -60,14 +70,22 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []). --spec update_config(module(), emqx_config:config_key_path(), update_args()) -> - ok | {error, term()}. +stop() -> + gen_server:stop(?MODULE). + +-spec update_config(module(), emqx_config:config_key_path(), emqx_config:update_args()) -> + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> - gen_server:call(?MODULE, {change_config, SchemaModule, ConfKeyPath, UpdateArgs}). + ?ATOM_CONF_PATH(ConfKeyPath, gen_server:call(?MODULE, {change_config, SchemaModule, + AtomKeyPath, UpdateArgs}), {error, ConfKeyPath}). -spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok. add_handler(ConfKeyPath, HandlerName) -> - gen_server:call(?MODULE, {add_child, ConfKeyPath, HandlerName}). + gen_server:call(?MODULE, {add_handler, ConfKeyPath, HandlerName}). + +-spec remove_handler(emqx_config:config_key_path()) -> ok. +remove_handler(ConfKeyPath) -> + gen_server:call(?MODULE, {remove_handler, ConfKeyPath}). %%============================================================================ @@ -75,26 +93,34 @@ add_handler(ConfKeyPath, HandlerName) -> init(_) -> {ok, #{handlers => #{?MOD => ?MODULE}}}. -handle_call({add_child, ConfKeyPath, HandlerName}, _From, +handle_call({add_handler, ConfKeyPath, HandlerName}, _From, State = #{handlers := Handlers}) -> + case deep_put_handler(ConfKeyPath, Handlers, HandlerName) of + {ok, NewHandlers} -> + {reply, ok, State#{handlers => NewHandlers}}; + Error -> + {reply, Error, State} + end; + +handle_call({remove_handler, ConfKeyPath}, _From, State = #{handlers := Handlers}) -> {reply, ok, State#{handlers => - emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}}; + emqx_map_lib:deep_remove(ConfKeyPath ++ [?MOD], Handlers)}}; handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, #{handlers := Handlers} = State) -> - OldConf = emqx_config:get_root(ConfKeyPath), - OldRawConf = emqx_config:get_root_raw(ConfKeyPath), - Result = try - {NewRawConf, OverrideConf} = process_upadate_request(ConfKeyPath, OldRawConf, - Handlers, UpdateArgs), - {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, NewRawConf), - _ = do_post_config_update(ConfKeyPath, Handlers, OldConf, CheckedConf, UpdateArgs), - emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) + Reply = try + case process_update_request(ConfKeyPath, Handlers, UpdateArgs) of + {ok, NewRawConf, OverrideConf} -> + check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, + OverrideConf, UpdateArgs); + {error, Result} -> + {error, Result} + end catch Error:Reason:ST -> ?LOG(error, "change_config failed: ~p", [{Error, Reason, ST}]), {error, Reason} end, - {reply, Result, State}; + {reply, Reply, State}; handle_call(_Request, _From, State) -> Reply = ok, @@ -112,32 +138,93 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -process_upadate_request(ConfKeyPath, OldRawConf, _Handlers, remove) -> +deep_put_handler([], Handlers, Mod) when is_map(Handlers) -> + {ok, Handlers#{?MOD => Mod}}; +deep_put_handler([], _Handlers, Mod) -> + {ok, #{?MOD => Mod}}; +deep_put_handler([?WKEY | KeyPath], Handlers, Mod) -> + deep_put_handler2(?WKEY, KeyPath, Handlers, Mod); +deep_put_handler([Key | KeyPath], Handlers, Mod) -> + case maps:find(?WKEY, Handlers) of + error -> + deep_put_handler2(Key, KeyPath, Handlers, Mod); + {ok, _SubHandlers} -> + {error, {cannot_override_a_wildcard_path, [?WKEY | KeyPath]}} + end. + +deep_put_handler2(Key, KeyPath, Handlers, Mod) -> + SubHandlers = maps:get(Key, Handlers, #{}), + case deep_put_handler(KeyPath, SubHandlers, Mod) of + {ok, SubHandlers1} -> + {ok, Handlers#{Key => SubHandlers1}}; + Error -> + Error + end. + +process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) -> + OldRawConf = emqx_config:get_root_raw(ConfKeyPath), BinKeyPath = bin_path(ConfKeyPath), NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf), - OverrideConf = emqx_map_lib:deep_remove(BinKeyPath, emqx_config:read_override_conf()), - {NewRawConf, OverrideConf}; -process_upadate_request(ConfKeyPath, OldRawConf, Handlers, {update, UpdateReq}) -> - NewRawConf = do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq), - OverrideConf = update_override_config(NewRawConf), - {NewRawConf, OverrideConf}. + OverrideConf = remove_from_override_config(BinKeyPath, Opts), + {ok, NewRawConf, OverrideConf}; +process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, Opts}) -> + OldRawConf = emqx_config:get_root_raw(ConfKeyPath), + case do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq) of + {ok, NewRawConf} -> + OverrideConf = update_override_config(NewRawConf, Opts), + {ok, NewRawConf, OverrideConf}; + Error -> Error + end. do_update_config([], Handlers, OldRawConf, UpdateReq) -> call_pre_config_update(Handlers, OldRawConf, UpdateReq); do_update_config([ConfKey | ConfKeyPath], Handlers, OldRawConf, UpdateReq) -> SubOldRawConf = get_sub_config(bin(ConfKey), OldRawConf), - SubHandlers = maps:get(ConfKey, Handlers, #{}), - NewUpdateReq = do_update_config(ConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq), - call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq}). + SubHandlers = get_sub_handlers(ConfKey, Handlers), + case do_update_config(ConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq) of + {ok, NewUpdateReq} -> + call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq}); + Error -> + Error + end. -do_post_config_update([], Handlers, OldConf, NewConf, UpdateArgs) -> - call_post_config_update(Handlers, OldConf, NewConf, up_req(UpdateArgs)); -do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, UpdateArgs) -> +check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OverrideConf, + UpdateArgs) -> + OldConf = emqx_config:get_root(ConfKeyPath), + FullRawConf = with_full_raw_confs(NewRawConf), + {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, FullRawConf), + NewConf = maps:with(maps:keys(OldConf), CheckedConf), + case do_post_config_update(ConfKeyPath, Handlers, OldConf, NewConf, AppEnvs, UpdateArgs, #{}) of + {ok, Result0} -> + case save_configs(ConfKeyPath, AppEnvs, NewConf, NewRawConf, OverrideConf, + UpdateArgs) of + {ok, Result1} -> + {ok, Result1#{post_config_update => Result0}}; + Error -> Error + end; + Error -> Error + end. + +do_post_config_update([], Handlers, OldConf, NewConf, AppEnvs, UpdateArgs, Result) -> + call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, up_req(UpdateArgs), Result); +do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, AppEnvs, UpdateArgs, + Result) -> SubOldConf = get_sub_config(ConfKey, OldConf), SubNewConf = get_sub_config(ConfKey, NewConf), - SubHandlers = maps:get(ConfKey, Handlers, #{}), - _ = do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, UpdateArgs), - call_post_config_update(Handlers, OldConf, NewConf, up_req(UpdateArgs)). + SubHandlers = get_sub_handlers(ConfKey, Handlers), + case do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, AppEnvs, + UpdateArgs, Result) of + {ok, Result1} -> + call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, up_req(UpdateArgs), + Result1); + Error -> Error + end. + +get_sub_handlers(ConfKey, Handlers) -> + case maps:find(ConfKey, Handlers) of + error -> maps:get(?WKEY, Handlers, #{}); + {ok, SubHandlers} -> SubHandlers + end. get_sub_config(ConfKey, Conf) when is_map(Conf) -> maps:get(ConfKey, Conf, undefined); @@ -147,15 +234,30 @@ get_sub_config(_, _Conf) -> %% the Conf is a primitive call_pre_config_update(Handlers, OldRawConf, UpdateReq) -> HandlerName = maps:get(?MOD, Handlers, undefined), case erlang:function_exported(HandlerName, pre_config_update, 2) of - true -> HandlerName:pre_config_update(UpdateReq, OldRawConf); + true -> + case HandlerName:pre_config_update(UpdateReq, OldRawConf) of + {ok, NewUpdateReq} -> {ok, NewUpdateReq}; + {error, Reason} -> {error, {pre_config_update, HandlerName, Reason}} + end; false -> merge_to_old_config(UpdateReq, OldRawConf) end. -call_post_config_update(Handlers, OldConf, NewConf, UpdateReq) -> +call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, UpdateReq, Result) -> HandlerName = maps:get(?MOD, Handlers, undefined), - case erlang:function_exported(HandlerName, post_config_update, 3) of - true -> HandlerName:post_config_update(UpdateReq, NewConf, OldConf); - false -> ok + case erlang:function_exported(HandlerName, post_config_update, 4) of + true -> + case HandlerName:post_config_update(UpdateReq, NewConf, OldConf, AppEnvs) of + ok -> {ok, Result}; + {ok, Result1} -> {ok, Result#{HandlerName => Result1}}; + {error, Reason} -> {error, {post_config_update, HandlerName, Reason}} + end; + false -> {ok, Result} + end. + +save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, UpdateArgs) -> + case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) of + ok -> {ok, return_change_result(ConfKeyPath, UpdateArgs)}; + {error, Reason} -> {error, {save_configs, Reason}} end. %% The default callback of config handlers @@ -164,18 +266,48 @@ call_post_config_update(Handlers, OldConf, NewConf, UpdateReq) -> %% 2. either the old or the new config is not of map type %% the behaviour is merging the new the config to the old config if they are maps. merge_to_old_config(UpdateReq, RawConf) when is_map(UpdateReq), is_map(RawConf) -> - maps:merge(RawConf, UpdateReq); + {ok, maps:merge(RawConf, UpdateReq)}; merge_to_old_config(UpdateReq, _RawConf) -> - UpdateReq. + {ok, UpdateReq}. -update_override_config(RawConf) -> +remove_from_override_config(_BinKeyPath, #{persistent := false}) -> + undefined; +remove_from_override_config(BinKeyPath, _Opts) -> + OldConf = emqx_config:read_override_conf(), + emqx_map_lib:deep_remove(BinKeyPath, OldConf). + +update_override_config(_RawConf, #{persistent := false}) -> + undefined; +update_override_config(RawConf, _Opts) -> OldConf = emqx_config:read_override_conf(), maps:merge(OldConf, RawConf). -up_req(remove) -> '$remove'; -up_req({update, Req}) -> Req. +up_req({remove, _Opts}) -> '$remove'; +up_req({{update, Req}, _Opts}) -> Req. + +return_change_result(ConfKeyPath, {{update, _Req}, Opts}) -> + #{config => emqx_config:get(ConfKeyPath), + raw_config => return_rawconf(ConfKeyPath, Opts)}; +return_change_result(_ConfKeyPath, {remove, _Opts}) -> + #{}. + +return_rawconf(ConfKeyPath, #{rawconf_with_defaults := true}) -> + FullRawConf = emqx_config:fill_defaults(emqx_config:get_raw([])), + emqx_map_lib:deep_get(bin_path(ConfKeyPath), FullRawConf); +return_rawconf(ConfKeyPath, _) -> + emqx_config:get_raw(ConfKeyPath). + +with_full_raw_confs(PartialConf) -> + maps:merge(emqx_config:get_raw([]), PartialConf). bin_path(ConfKeyPath) -> [bin(Key) || Key <- ConfKeyPath]. bin(A) when is_atom(A) -> atom_to_binary(A, utf8); bin(B) when is_binary(B) -> B. + +safe_atom(Bin) when is_binary(Bin) -> + binary_to_existing_atom(Bin, latin1); +safe_atom(Str) when is_list(Str) -> + list_to_existing_atom(Str); +safe_atom(Atom) when is_atom(Atom) -> + Atom. \ No newline at end of file diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index ac66c4daf..26eb346a4 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -102,8 +102,8 @@ idle_timer :: maybe(reference()), %% Zone name zone :: atom(), - %% Listener Name - listener :: atom() + %% Listener Type and Name + listener :: {Type::atom(), Name::atom()} }). -type(state() :: #state{}). @@ -135,7 +135,9 @@ , system_code_change/4 ]}). --spec(start_link(esockd:transport(), esockd:socket(), emqx_channel:opts()) +-spec(start_link(esockd:transport(), + esockd:socket() | {pid(), quicer:connection_handler()}, + emqx_channel:opts()) -> {ok, pid()}). start_link(Transport, Socket, Options) -> Args = [self(), Transport, Socket, Options], @@ -463,15 +465,15 @@ handle_msg({Passive, _Sock}, State) NState1 = check_oom(run_gc(InStats, NState)), handle_info(activate_socket, NState1); -handle_msg(Deliver = {deliver, _Topic, _Msg}, #state{zone = Zone, - listener = Listener} = State) -> - ActiveN = get_active_n(Zone, Listener), +handle_msg(Deliver = {deliver, _Topic, _Msg}, #state{ + listener = {Type, Listener}} = State) -> + ActiveN = get_active_n(Type, Listener), Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); %% Something sent -handle_msg({inet_reply, _Sock, ok}, State = #state{zone = Zone, listener = Listener}) -> - case emqx_pd:get_counter(outgoing_pubs) > get_active_n(Zone, Listener) of +handle_msg({inet_reply, _Sock, ok}, State = #state{listener = {Type, Listener}}) -> + case emqx_pd:get_counter(outgoing_pubs) > get_active_n(Type, Listener) of true -> Pubs = emqx_pd:reset_counter(outgoing_pubs), Bytes = emqx_pd:reset_counter(outgoing_bytes), @@ -820,8 +822,8 @@ activate_socket(State = #state{sockstate = closed}) -> activate_socket(State = #state{sockstate = blocked}) -> {ok, State}; activate_socket(State = #state{transport = Transport, socket = Socket, - zone = Zone, listener = Listener}) -> - ActiveN = get_active_n(Zone, Listener), + listener = {Type, Listener}}) -> + ActiveN = get_active_n(Type, Listener), case Transport:setopts(Socket, [{active, ActiveN}]) of ok -> {ok, State#state{sockstate = running}}; Error -> Error @@ -904,8 +906,6 @@ get_state(Pid) -> maps:from_list(lists:zip(record_info(fields, state), tl(tuple_to_list(State)))). -get_active_n(Zone, Listener) -> - case emqx_config:get([zones, Zone, listeners, Listener, type]) of - quic -> 100; - _ -> emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) - end. +get_active_n(quic, _Listener) -> 100; +get_active_n(Type, Listener) -> + emqx_config:get_listener_conf(Type, Listener, [tcp, active_n]). diff --git a/apps/emqx/src/emqx_flapping.erl b/apps/emqx/src/emqx_flapping.erl index c4a523669..1908430be 100644 --- a/apps/emqx/src/emqx_flapping.erl +++ b/apps/emqx/src/emqx_flapping.erl @@ -160,4 +160,4 @@ start_timer(Zone) -> start_timers() -> lists:foreach(fun({Zone, _ZoneConf}) -> start_timer(Zone) - end, maps:to_list(emqx_config:get([zones], #{}))). + end, maps:to_list(emqx:get_config([zones], #{}))). diff --git a/apps/emqx/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl index 082801bad..79a740bed 100644 --- a/apps/emqx/src/emqx_frame.erl +++ b/apps/emqx/src/emqx_frame.erl @@ -100,14 +100,10 @@ parse(<>, StrictMode andalso validate_header(Type, Dup, QoS, Retain), Header = #mqtt_packet_header{type = Type, dup = bool(Dup), - qos = QoS, + qos = fixqos(Type, QoS), retain = bool(Retain) }, - Header1 = case fixqos(Type, QoS) of - QoS -> Header; - FixedQoS -> Header#mqtt_packet_header{qos = FixedQoS} - end, - parse_remaining_len(Rest, Header1, Options); + parse_remaining_len(Rest, Header, Options); parse(Bin, {{len, #{hdr := Header, len := {Multiplier, Length}} diff --git a/apps/emqx/src/emqx_global_gc.erl b/apps/emqx/src/emqx_global_gc.erl index 9449efe9a..5192508e5 100644 --- a/apps/emqx/src/emqx_global_gc.erl +++ b/apps/emqx/src/emqx_global_gc.erl @@ -85,7 +85,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- ensure_timer(State) -> - case emqx_config:get([node, global_gc_interval]) of + case emqx:get_config([node, global_gc_interval]) of undefined -> State; Interval -> TRef = emqx_misc:start_timer(Interval, run), State#{timer := TRef} diff --git a/apps/emqx/src/emqx_kernel_sup.erl b/apps/emqx/src/emqx_kernel_sup.erl index defe96182..b854c60b7 100644 --- a/apps/emqx/src/emqx_kernel_sup.erl +++ b/apps/emqx/src/emqx_kernel_sup.erl @@ -34,6 +34,7 @@ init([]) -> , child_spec(emqx_stats, worker) , child_spec(emqx_metrics, worker) , child_spec(emqx_ctl, worker) + , child_spec(emqx_logger, worker) ]}}. child_spec(M, Type) -> diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index f80800768..06d900ed5 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -26,6 +26,8 @@ , restart/0 , stop/0 , is_running/1 + , current_conns/2 + , max_conns/2 ]). -export([ start_listener/1 @@ -37,35 +39,39 @@ , has_enabled_listener_conf_by_type/1 ]). +-export([ listener_id/2 + , parse_listener_id/1 + ]). + +-export([post_config_update/4]). + +-define(CONF_KEY_PATH, [listeners]). + %% @doc List configured listeners. -spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]). list() -> - [{listener_id(ZoneName, LName), LConf} || {ZoneName, LName, LConf} <- do_list()]. + [{listener_id(Type, LName), LConf} || {Type, LName, LConf} <- do_list()]. do_list() -> - Zones = maps:to_list(emqx_config:get([zones], #{})), - lists:append([list(ZoneName, ZoneConf) || {ZoneName, ZoneConf} <- Zones]). + Listeners = maps:to_list(emqx:get_config([listeners], #{})), + lists:append([list(Type, maps:to_list(Conf)) || {Type, Conf} <- Listeners]). -list(ZoneName, ZoneConf) -> - Listeners = maps:to_list(maps:get(listeners, ZoneConf, #{})), - [ - begin - Conf = merge_zone_and_listener_confs(ZoneConf, LConf), - Running = is_running(listener_id(ZoneName, LName), Conf), - {ZoneName , LName, maps:put(running, Running, Conf)} - end - || {LName, LConf} <- Listeners, is_map(LConf)]. +list(Type, Conf) -> + [begin + Running = is_running(Type, listener_id(Type, LName), LConf), + {Type, LName, maps:put(running, Running, LConf)} + end || {LName, LConf} <- Conf, is_map(LConf)]. -spec is_running(ListenerId :: atom()) -> boolean() | {error, no_found}. is_running(ListenerId) -> - case lists:filtermap(fun({_Zone, Id, #{running := IsRunning}}) -> + case lists:filtermap(fun({_Type, Id, #{running := IsRunning}}) -> Id =:= ListenerId andalso {true, IsRunning} end, do_list()) of [IsRunning] -> IsRunning; [] -> {error, not_found} end. -is_running(ListenerId, #{type := tcp, bind := ListenOn})-> +is_running(Type, ListenerId, #{bind := ListenOn}) when Type =:= tcp; Type =:= ssl -> try esockd:listener({ListenerId, ListenOn}) of Pid when is_pid(Pid)-> true @@ -73,7 +79,7 @@ is_running(ListenerId, #{type := tcp, bind := ListenOn})-> false end; -is_running(ListenerId, #{type := ws})-> +is_running(Type, ListenerId, _Conf) when Type =:= ws; Type =:= wss -> try Info = ranch:info(ListenerId), proplists:get_value(status, Info) =:= running @@ -81,13 +87,38 @@ is_running(ListenerId, #{type := ws})-> false end; -is_running(_ListenerId, #{type := quic})-> -%% TODO: quic support +is_running(quic, _ListenerId, _Conf)-> + %% TODO: quic support {error, no_found}. +current_conns(ID, ListenOn) -> + {Type, Name} = parse_listener_id(ID), + current_conns(Type, Name, ListenOn). + +current_conns(Type, Name, ListenOn) when Type == tcl; Type == ssl -> + esockd:get_current_connections({listener_id(Type, Name), ListenOn}); +current_conns(Type, Name, _ListenOn) when Type =:= ws; Type =:= wss -> + proplists:get_value(all_connections, ranch:info(listener_id(Type, Name))); +current_conns(_, _, _) -> + {error, not_support}. + +max_conns(ID, ListenOn) -> + {Type, Name} = parse_listener_id(ID), + max_conns(Type, Name, ListenOn). + +max_conns(Type, Name, ListenOn) when Type == tcl; Type == ssl -> + esockd:get_max_connections({listener_id(Type, Name), ListenOn}); +max_conns(Type, Name, _ListenOn) when Type =:= ws; Type =:= wss -> + proplists:get_value(max_connections, ranch:info(listener_id(Type, Name))); +max_conns(_, _, _) -> + {error, not_support}. + %% @doc Start all listeners. -spec(start() -> ok). start() -> + %% The ?MODULE:start/0 will be called by emqx_app when emqx get started, + %% so we install the config handler here. + ok = emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), foreach_listeners(fun start_listener/3). -spec start_listener(atom()) -> ok | {error, term()}. @@ -95,23 +126,76 @@ start_listener(ListenerId) -> apply_on_listener(ListenerId, fun start_listener/3). -spec start_listener(atom(), atom(), map()) -> ok | {error, term()}. -start_listener(ZoneName, ListenerName, #{type := Type, bind := Bind} = Conf) -> - case do_start_listener(ZoneName, ListenerName, Conf) of +start_listener(Type, ListenerName, #{bind := Bind} = Conf) -> + case do_start_listener(Type, ListenerName, Conf) of {ok, {skipped, Reason}} when Reason =:= listener_disabled; Reason =:= quic_app_missing -> - console_print("- Skip - starting ~s listener ~s on ~s ~n due to ~p", - [Type, listener_id(ZoneName, ListenerName), format(Bind), Reason]); + console_print("- Skip - starting listener ~s on ~s ~n due to ~p", + [listener_id(Type, ListenerName), format_addr(Bind), Reason]); {ok, _} -> - console_print("Start ~s listener ~s on ~s successfully.~n", - [Type, listener_id(ZoneName, ListenerName), format(Bind)]); + console_print("Listener ~s on ~s started.~n", + [listener_id(Type, ListenerName), format_addr(Bind)]); {error, {already_started, Pid}} -> {error, {already_started, Pid}}; {error, Reason} -> - ?ELOG("Failed to start ~s listener ~s on ~s: ~0p~n", - [Type, listener_id(ZoneName, ListenerName), format(Bind), Reason]), + ?ELOG("Failed to start listener ~s on ~s: ~0p~n", + [listener_id(Type, ListenerName), format_addr(Bind), Reason]), error(Reason) end. +%% @doc Restart all listeners +-spec(restart() -> ok). +restart() -> + foreach_listeners(fun restart_listener/3). + +-spec(restart_listener(atom()) -> ok | {error, term()}). +restart_listener(ListenerId) -> + apply_on_listener(ListenerId, fun restart_listener/3). + +-spec(restart_listener(atom(), atom(), map()) -> ok | {error, term()}). +restart_listener(Type, ListenerName, {OldConf, NewConf}) -> + restart_listener(Type, ListenerName, OldConf, NewConf); +restart_listener(Type, ListenerName, Conf) -> + restart_listener(Type, ListenerName, Conf, Conf). + +restart_listener(Type, ListenerName, OldConf, NewConf) -> + case stop_listener(Type, ListenerName, OldConf) of + ok -> start_listener(Type, ListenerName, NewConf); + Error -> Error + end. + +%% @doc Stop all listeners. +-spec(stop() -> ok). +stop() -> + %% The ?MODULE:stop/0 will be called by emqx_app when emqx is going to shutdown, + %% so we uninstall the config handler here. + _ = emqx_config_handler:remove_handler(?CONF_KEY_PATH), + foreach_listeners(fun stop_listener/3). + +-spec(stop_listener(atom()) -> ok | {error, term()}). +stop_listener(ListenerId) -> + apply_on_listener(ListenerId, fun stop_listener/3). + +stop_listener(Type, ListenerName, #{bind := Bind} = Conf) -> + case do_stop_listener(Type, ListenerName, Conf) of + ok -> + console_print("Listener ~s on ~s stopped.~n", + [listener_id(Type, ListenerName), format_addr(Bind)]), + ok; + {error, Reason} -> + ?ELOG("Failed to stop listener ~s on ~s: ~0p~n", + [listener_id(Type, ListenerName), format_addr(Bind), Reason]), + {error, Reason} + end. + +-spec(do_stop_listener(atom(), atom(), map()) -> ok | {error, term()}). +do_stop_listener(Type, ListenerName, #{bind := ListenOn}) when Type == tcp; Type == ssl -> + esockd:close(listener_id(Type, ListenerName), ListenOn); +do_stop_listener(Type, ListenerName, _Conf) when Type == ws; Type == wss -> + cowboy:stop_listener(listener_id(Type, ListenerName)); +do_stop_listener(quic, ListenerName, _Conf) -> + quicer:stop_listener(listener_id(quic, ListenerName)). + -ifndef(TEST). console_print(Fmt, Args) -> ?ULOG(Fmt, Args). -else. @@ -121,79 +205,108 @@ console_print(_Fmt, _Args) -> ok. %% Start MQTT/TCP listener -spec(do_start_listener(atom(), atom(), map()) -> {ok, pid() | {skipped, atom()}} | {error, term()}). -do_start_listener(_ZoneName, _ListenerName, #{enabled := false}) -> +do_start_listener(_Type, _ListenerName, #{enabled := false}) -> {ok, {skipped, listener_disabled}}; -do_start_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn} = Opts) -> - esockd:open(listener_id(ZoneName, ListenerName), ListenOn, merge_default(esockd_opts(Opts)), +do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) + when Type == tcp; Type == ssl -> + esockd:open(listener_id(Type, ListenerName), ListenOn, merge_default(esockd_opts(Type, Opts)), {emqx_connection, start_link, - [#{zone => ZoneName, listener => ListenerName}]}); + [#{listener => {Type, ListenerName}, + zone => zone(Opts)}]}); %% Start MQTT/WS listener -do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts) -> - Id = listener_id(ZoneName, ListenerName), - RanchOpts = ranch_opts(ListenOn, Opts), - WsOpts = ws_opts(ZoneName, ListenerName, Opts), - case is_ssl(Opts) of - false -> - cowboy:start_clear(Id, RanchOpts, WsOpts); - true -> - cowboy:start_tls(Id, RanchOpts, WsOpts) +do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) + when Type == ws; Type == wss -> + Id = listener_id(Type, ListenerName), + RanchOpts = ranch_opts(Type, ListenOn, Opts), + WsOpts = ws_opts(Type, ListenerName, Opts), + case Type of + ws -> cowboy:start_clear(Id, RanchOpts, WsOpts); + wss -> cowboy:start_tls(Id, RanchOpts, WsOpts) end; %% Start MQTT/QUIC listener -do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Opts) -> +do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> case [ A || {quicer, _, _} = A<-application:which_applications() ] of [_] -> - %% @fixme unsure why we need reopen lib and reopen config. - quicer_nif:open_lib(), - quicer_nif:reg_open(), DefAcceptors = erlang:system_info(schedulers_online) * 8, ListenOpts = [ {cert, maps:get(certfile, Opts)} , {key, maps:get(keyfile, Opts)} , {alpn, ["mqtt"]} - , {conn_acceptors, maps:get(acceptors, Opts, DefAcceptors)} - , {idle_timeout_ms, emqx_config:get_zone_conf(ZoneName, [mqtt, idle_timeout])} + , {conn_acceptors, lists:max([DefAcceptors, maps:get(acceptors, Opts, 0)])} + , {idle_timeout_ms, lists:max([ + emqx_config:get_zone_conf(zone(Opts), [mqtt, idle_timeout]) * 3 + , timer:seconds(maps:get(idle_timeout, Opts))] + )} ], - ConnectionOpts = #{conn_callback => emqx_quic_connection + ConnectionOpts = #{ conn_callback => emqx_quic_connection , peer_unidi_stream_count => 1 , peer_bidi_stream_count => 10 - , zone => ZoneName - , listener => ListenerName + , zone => zone(Opts) + , listener => {quic, ListenerName} }, - StreamOpts = [], - quicer:start_listener(listener_id(ZoneName, ListenerName), + StreamOpts = [{stream_callback, emqx_quic_stream}], + quicer:start_listener(listener_id(quic, ListenerName), port(ListenOn), {ListenOpts, ConnectionOpts, StreamOpts}); [] -> {ok, {skipped, quic_app_missing}} end. -esockd_opts(Opts0) -> +delete_authentication(Type, ListenerName, _Conf) -> + emqx_authentication:delete_chain(atom_to_binary(listener_id(Type, ListenerName))). + +%% Update the listeners at runtime +post_config_update(_Req, NewListeners, OldListeners, _AppEnvs) -> + #{added := Added, removed := Removed, changed := Updated} + = diff_listeners(NewListeners, OldListeners), + perform_listener_changes(fun stop_listener/3, Removed), + perform_listener_changes(fun delete_authentication/3, Removed), + perform_listener_changes(fun start_listener/3, Added), + perform_listener_changes(fun restart_listener/3, Updated). + +perform_listener_changes(Action, MapConfs) -> + lists:foreach(fun + ({Id, Conf}) -> + {Type, Name} = parse_listener_id(Id), + Action(Type, Name, Conf) + end, maps:to_list(MapConfs)). + +diff_listeners(NewListeners, OldListeners) -> + emqx_map_lib:diff_maps(flatten_listeners(NewListeners), flatten_listeners(OldListeners)). + +flatten_listeners(Conf0) -> + maps:from_list( + lists:append([do_flatten_listeners(Type, Conf) + || {Type, Conf} <- maps:to_list(Conf0)])). + +do_flatten_listeners(Type, Conf0) -> + [{listener_id(Type, Name), maps:remove(authentication, Conf)} || {Name, Conf} <- maps:to_list(Conf0)]. + +esockd_opts(Type, Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), - Opts2 = case emqx_map_lib:deep_get([rate_limit, max_conn_rate], Opts0) of + Opts2 = case emqx_config:get_zone_conf(zone(Opts0), [rate_limit, max_conn_rate]) of infinity -> Opts1; Rate -> Opts1#{max_conn_rate => Rate} end, Opts3 = Opts2#{access_rules => esockd_access_rules(maps:get(access_rules, Opts0, []))}, - maps:to_list(case is_ssl(Opts0) of - false -> - Opts3#{tcp_options => tcp_opts(Opts0)}; - true -> - Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)} + maps:to_list(case Type of + tcp -> Opts3#{tcp_options => tcp_opts(Opts0)}; + ssl -> Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)} end). -ws_opts(ZoneName, ListenerName, Opts) -> +ws_opts(Type, ListenerName, Opts) -> WsPaths = [{maps:get(mqtt_path, Opts, "/mqtt"), emqx_ws_connection, - #{zone => ZoneName, listener => ListenerName}}], + #{zone => zone(Opts), listener => {Type, ListenerName}}}], Dispatch = cowboy_router:compile([{'_', WsPaths}]), ProxyProto = maps:get(proxy_protocol, Opts, false), #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. -ranch_opts(ListenOn, Opts) -> +ranch_opts(Type, ListenOn, Opts) -> NumAcceptors = maps:get(acceptors, Opts, 4), MaxConnections = maps:get(max_connections, Opts, 1024), - SocketOpts = case is_ssl(Opts) of - true -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts)); - false -> tcp_opts(Opts) + SocketOpts = case Type of + wss -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts)); + ws -> tcp_opts(Opts) end, #{num_acceptors => NumAcceptors, max_connections => MaxConnections, @@ -217,39 +330,6 @@ esockd_access_rules(StrRules) -> end, [Access(R) || R <- StrRules]. -%% @doc Restart all listeners --spec(restart() -> ok). -restart() -> - foreach_listeners(fun restart_listener/3). - --spec(restart_listener(atom()) -> ok | {error, term()}). -restart_listener(ListenerId) -> - apply_on_listener(ListenerId, fun restart_listener/3). - --spec(restart_listener(atom(), atom(), map()) -> ok | {error, term()}). -restart_listener(ZoneName, ListenerName, Conf) -> - case stop_listener(ZoneName, ListenerName, Conf) of - ok -> start_listener(ZoneName, ListenerName, Conf); - Error -> Error - end. - -%% @doc Stop all listeners. --spec(stop() -> ok). -stop() -> - foreach_listeners(fun stop_listener/3). - --spec(stop_listener(atom()) -> ok | {error, term()}). -stop_listener(ListenerId) -> - apply_on_listener(ListenerId, fun stop_listener/3). - --spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}). -stop_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn}) -> - esockd:close(listener_id(ZoneName, ListenerName), ListenOn); -stop_listener(ZoneName, ListenerName, #{type := ws}) -> - cowboy:stop_listener(listener_id(ZoneName, ListenerName)); -stop_listener(ZoneName, ListenerName, #{type := quic}) -> - quicer:stop_listener(listener_id(ZoneName, ListenerName)). - merge_default(Options) -> case lists:keytake(tcp_options, 1, Options) of {value, {tcp_options, TcpOpts}, Options1} -> @@ -258,24 +338,27 @@ merge_default(Options) -> [{tcp_options, ?MQTT_SOCKOPTS} | Options] end. -format(Port) when is_integer(Port) -> +format_addr(Port) when is_integer(Port) -> io_lib:format("0.0.0.0:~w", [Port]); -format({Addr, Port}) when is_list(Addr) -> +format_addr({Addr, Port}) when is_list(Addr) -> io_lib:format("~s:~w", [Addr, Port]); -format({Addr, Port}) when is_tuple(Addr) -> +format_addr({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). -listener_id(ZoneName, ListenerName) -> - list_to_atom(lists:append([atom_to_list(ZoneName), ":", atom_to_list(ListenerName)])). +listener_id(Type, ListenerName) -> + list_to_atom(lists:append([str(Type), ":", str(ListenerName)])). -decode_listener_id(Id) -> +parse_listener_id(Id) -> try - [Zone, Listen] = string:split(atom_to_list(Id), ":", leading), - {list_to_existing_atom(Zone), list_to_existing_atom(Listen)} + [Type, Name] = string:split(str(Id), ":", leading), + {list_to_existing_atom(Type), list_to_atom(Name)} catch _ : _ -> error({invalid_listener_id, Id}) end. +zone(Opts) -> + maps:get(zone, Opts, undefined). + ssl_opts(Opts) -> maps:to_list( emqx_tls_lib:drop_tls13_for_old_otp( @@ -287,32 +370,28 @@ tcp_opts(Opts) -> maps:without([active_n], maps:get(tcp, Opts, #{}))). -is_ssl(Opts) -> - emqx_map_lib:deep_get([ssl, enable], Opts, false). - foreach_listeners(Do) -> lists:foreach( - fun({ZoneName, LName, LConf}) -> - Do(ZoneName, LName, LConf) + fun({Type, LName, LConf}) -> + Do(Type, LName, LConf) end, do_list()). has_enabled_listener_conf_by_type(Type) -> lists:any( - fun({_Zone, _LName, LConf}) when is_map(LConf) -> - Type =:= maps:get(type, LConf) andalso - maps:get(enabled, LConf, true) + fun({Type0, _LName, LConf}) when is_map(LConf) -> + Type =:= Type0 andalso maps:get(enabled, LConf, true) end, do_list()). -%% merge the configs in zone and listeners in a manner that -%% all config entries in the listener are prior to the ones in the zone. -merge_zone_and_listener_confs(ZoneConf, ListenerConf) -> - ConfsInZonesOnly = [listeners, overall_max_connections], - BaseConf = maps:without(ConfsInZonesOnly, ZoneConf), - emqx_map_lib:deep_merge(BaseConf, ListenerConf). - apply_on_listener(ListenerId, Do) -> - {ZoneName, ListenerName} = decode_listener_id(ListenerId), - case emqx_config:find_listener_conf(ZoneName, ListenerName, []) of - {not_found, _, _} -> error({listener_config_not_found, ZoneName, ListenerName}); - {ok, Conf} -> Do(ZoneName, ListenerName, Conf) + {Type, ListenerName} = parse_listener_id(ListenerId), + case emqx_config:find_listener_conf(Type, ListenerName, []) of + {not_found, _, _} -> error({listener_config_not_found, Type, ListenerName}); + {ok, Conf} -> Do(Type, ListenerName, Conf) end. + +str(A) when is_atom(A) -> + atom_to_list(A); +str(B) when is_binary(B) -> + binary_to_list(B); +str(S) when is_list(S) -> + S. diff --git a/apps/emqx/src/emqx_logger.erl b/apps/emqx/src/emqx_logger.erl index 986ba11e0..29f5bd597 100644 --- a/apps/emqx/src/emqx_logger.erl +++ b/apps/emqx/src/emqx_logger.erl @@ -18,6 +18,19 @@ -compile({no_auto_import, [error/1]}). +-behaviour(gen_server). +-behaviour(emqx_config_handler). + +%% gen_server callbacks +-export([ start_link/0 + , init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + %% Logs -export([ debug/1 , debug/2 @@ -47,6 +60,7 @@ ]). -export([ get_primary_log_level/0 + , tune_primary_log_level/0 , get_log_handlers/0 , get_log_handlers/1 , get_log_handler/1 @@ -56,6 +70,8 @@ , stop_log_handler/1 ]). +-export([post_config_update/4]). + -type(peername_str() :: list()). -type(logger_dst() :: file:filename() | console | unknown). -type(logger_handler_info() :: #{ @@ -66,6 +82,49 @@ }). -define(stopped_handlers, {?MODULE, stopped_handlers}). +-define(CONF_PATH, [log]). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- +init([]) -> + ok = emqx_config_handler:add_handler(?CONF_PATH, ?MODULE), + {ok, #{}}. + +handle_call({update_config, AppEnvs}, _From, State) -> + OldEnvs = application:get_env(kernel, logger, []), + NewEnvs = proplists:get_value(logger, proplists:get_value(kernel, AppEnvs, []), []), + ok = application:set_env(kernel, logger, NewEnvs), + _ = [logger:remove_handler(HandlerId) || {handler, HandlerId, _Mod, _Conf} <- OldEnvs], + _ = [logger:add_handler(HandlerId, Mod, Conf) || {handler, HandlerId, Mod, Conf} <- NewEnvs], + ok = tune_primary_log_level(), + {reply, ok, State}; + +handle_call(_Req, _From, State) -> + {reply, ignored, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok = emqx_config_handler:remove_handler(?CONF_PATH), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%%-------------------------------------------------------------------- +%% emqx_config_handler callbacks +%%-------------------------------------------------------------------- +post_config_update(_Req, _NewConf, _OldConf, AppEnvs) -> + gen_server:call(?MODULE, {update_config, AppEnvs}, 5000). %%-------------------------------------------------------------------- %% APIs @@ -159,6 +218,16 @@ get_primary_log_level() -> #{level := Level} = logger:get_primary_config(), Level. +-spec tune_primary_log_level() -> ok. +tune_primary_log_level() -> + LowestLevel = lists:foldl(fun(#{level := Level}, OldLevel) -> + case logger:compare_levels(Level, OldLevel) of + lt -> Level; + _ -> OldLevel + end + end, get_primary_log_level(), get_log_handlers()), + set_primary_log_level(LowestLevel). + -spec(set_primary_log_level(logger:level()) -> ok | {error, term()}). set_primary_log_level(Level) -> logger:set_primary_config(level, Level). diff --git a/apps/emqx/src/emqx_logger_textfmt.erl b/apps/emqx/src/emqx_logger_textfmt.erl index cf419f381..94af0ca2a 100644 --- a/apps/emqx/src/emqx_logger_textfmt.erl +++ b/apps/emqx/src/emqx_logger_textfmt.erl @@ -23,17 +23,17 @@ check_config(X) -> logger_formatter:check_config(X). format(#{msg := {report, Report}, meta := Meta} = Event, Config) when is_map(Report) -> logger_formatter:format(Event#{msg := {report, enrich(Report, Meta)}}, Config); -format(#{msg := {Fmt, Args}, meta := Meta} = Event, Config) when is_list(Fmt) -> - {NewFmt, NewArgs} = enrich_fmt(Fmt, Args, Meta), - logger_formatter:format(Event#{msg := {NewFmt, NewArgs}}, Config). +format(#{msg := Msg, meta := Meta} = Event, Config) -> + NewMsg = enrich_fmt(Msg, Meta), + logger_formatter:format(Event#{msg := NewMsg}, Config). enrich(Report, #{mfa := Mfa, line := Line}) -> Report#{mfa => mfa(Mfa), line => Line}; enrich(Report, _) -> Report. -enrich_fmt(Fmt, Args, #{mfa := Mfa, line := Line}) -> +enrich_fmt({Fmt, Args}, #{mfa := Mfa, line := Line}) when is_list(Fmt) -> {Fmt ++ " mfa: ~s line: ~w", Args ++ [mfa(Mfa), Line]}; -enrich_fmt(Fmt, Args, _) -> - {Fmt, Args}. +enrich_fmt(Msg, _) -> + Msg. mfa({M, F, A}) -> atom_to_list(M) ++ ":" ++ atom_to_list(F) ++ "/" ++ integer_to_list(A). diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index d720e771e..6aa6606c0 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -23,11 +23,17 @@ , deep_merge/2 , safe_atom_key_map/1 , unsafe_atom_key_map/1 + , jsonable_map/1 + , jsonable_map/2 + , binary_string/1 + , deep_convert/3 + , diff_maps/2 ]). -export_type([config_key/0, config_key_path/0]). -type config_key() :: atom() | binary(). -type config_key_path() :: [config_key()]. +-type convert_fun() :: fun((...) -> {K1::any(), V1::any()} | drop). %%----------------------------------------------------------------- -spec deep_get(config_key_path(), map()) -> term(). @@ -59,13 +65,11 @@ deep_find(_KeyPath, Data) -> {not_found, _KeyPath, Data}. -spec deep_put(config_key_path(), map(), term()) -> map(). -deep_put([], Map, Config) when is_map(Map) -> - Config; -deep_put([], _Map, Config) -> %% not map, replace it - Config; -deep_put([Key | KeyPath], Map, Config) -> - SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Config), - Map#{Key => SubMap}. +deep_put([], _Map, Data) -> + Data; +deep_put([Key | KeyPath], Map, Data) -> + SubMap = maps:get(Key, Map, #{}), + Map#{Key => deep_put(KeyPath, SubMap, Data)}. -spec deep_remove(config_key_path(), map()) -> map(). deep_remove([], Map) -> @@ -97,21 +101,72 @@ deep_merge(BaseMap, NewMap) -> end, #{}, BaseMap), maps:merge(MergedBase, maps:with(NewKeys, NewMap)). +-spec deep_convert(map(), convert_fun(), Args::list()) -> map(). +deep_convert(Map, ConvFun, Args) when is_map(Map) -> + maps:fold(fun(K, V, Acc) -> + case apply(ConvFun, [K, deep_convert(V, ConvFun, Args) | Args]) of + drop -> Acc; + {K1, V1} -> Acc#{K1 => V1} + end + end, #{}, Map); +deep_convert(ListV, ConvFun, Args) when is_list(ListV) -> + [deep_convert(V, ConvFun, Args) || V <- ListV]; +deep_convert(Val, _, _Args) -> Val. + +-spec unsafe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}. unsafe_atom_key_map(Map) -> covert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end). +-spec safe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}. safe_atom_key_map(Map) -> covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end). +-spec jsonable_map(map() | list()) -> map() | list(). +jsonable_map(Map) -> + jsonable_map(Map, fun(K, V) -> {K, V} end). + +jsonable_map(Map, JsonableFun) -> + deep_convert(Map, fun binary_string_kv/3, [JsonableFun]). + +-spec diff_maps(map(), map()) -> + #{added := map(), identical := map(), removed := map(), + changed := #{any() => {OldValue::any(), NewValue::any()}}}. +diff_maps(NewMap, OldMap) -> + InitR = #{identical => #{}, changed => #{}, removed => #{}}, + {Result, RemInNew} = + lists:foldl(fun({OldK, OldV}, {Result0 = #{identical := I, changed := U, removed := D}, + RemNewMap}) -> + Result1 = case maps:find(OldK, NewMap) of + error -> + Result0#{removed => D#{OldK => OldV}}; + {ok, NewV} when NewV == OldV -> + Result0#{identical => I#{OldK => OldV}}; + {ok, NewV} -> + Result0#{changed => U#{OldK => {OldV, NewV}}} + end, + {Result1, maps:remove(OldK, RemNewMap)} + end, {InitR, NewMap}, maps:to_list(OldMap)), + Result#{added => RemInNew}. + + +binary_string_kv(K, V, JsonableFun) -> + case JsonableFun(K, V) of + drop -> drop; + {K1, V1} -> {binary_string(K1), binary_string(V1)} + end. + +binary_string([]) -> []; +binary_string(Val) when is_list(Val) -> + case io_lib:printable_unicode_list(Val) of + true -> unicode:characters_to_binary(Val); + false -> [binary_string(V) || V <- Val] + end; +binary_string(Val) -> + Val. + %%--------------------------------------------------------------------------- -covert_keys_to_atom(BinKeyMap, Conv) when is_map(BinKeyMap) -> - maps:fold( - fun(K, V, Acc) when is_binary(K) -> - Acc#{Conv(K) => covert_keys_to_atom(V, Conv)}; - (K, V, Acc) when is_atom(K) -> - %% richmap keys - Acc#{K => covert_keys_to_atom(V, Conv)} - end, #{}, BinKeyMap); -covert_keys_to_atom(ListV, Conv) when is_list(ListV) -> - [covert_keys_to_atom(V, Conv) || V <- ListV]; -covert_keys_to_atom(Val, _) -> Val. +covert_keys_to_atom(BinKeyMap, Conv) -> + deep_convert(BinKeyMap, fun + (K, V) when is_atom(K) -> {K, V}; + (K, V) when is_binary(K) -> {Conv(K), V} + end, []). diff --git a/apps/emqx/src/emqx_metrics.erl b/apps/emqx/src/emqx_metrics.erl index 736bb05b0..282b8b5f3 100644 --- a/apps/emqx/src/emqx_metrics.erl +++ b/apps/emqx/src/emqx_metrics.erl @@ -22,8 +22,6 @@ -include("logger.hrl"). -include("types.hrl"). -include("emqx_mqtt.hrl"). --include("emqx.hrl"). - -export([ start_link/0 , stop/0 diff --git a/apps/emqx/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl index 9fd52c21b..85e448f41 100644 --- a/apps/emqx/src/emqx_os_mon.erl +++ b/apps/emqx/src/emqx_os_mon.erl @@ -76,7 +76,7 @@ set_procmem_high_watermark(Float) -> %%-------------------------------------------------------------------- init([]) -> - Opts = emqx_config:get([sysmon, os]), + Opts = emqx:get_config([sysmon, os]), set_mem_check_interval(maps:get(mem_check_interval, Opts)), set_sysmem_high_watermark(maps:get(sysmem_high_watermark, Opts)), set_procmem_high_watermark(maps:get(procmem_high_watermark, Opts)), @@ -91,8 +91,8 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({timeout, _Timer, check}, State) -> - CPUHighWatermark = emqx_config:get([sysmon, os, cpu_high_watermark]) * 100, - CPULowWatermark = emqx_config:get([sysmon, os, cpu_low_watermark]) * 100, + CPUHighWatermark = emqx:get_config([sysmon, os, cpu_high_watermark]) * 100, + CPULowWatermark = emqx:get_config([sysmon, os, cpu_low_watermark]) * 100, _ = case emqx_vm:cpu_util() of %% TODO: should be improved? 0 -> ok; Busy when Busy >= CPUHighWatermark -> @@ -123,7 +123,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- start_check_timer() -> - Interval = emqx_config:get([sysmon, os, cpu_check_interval]), + Interval = emqx:get_config([sysmon, os, cpu_check_interval]), case erlang:system_info(system_architecture) of "x86_64-pc-linux-musl" -> ok; _ -> emqx_misc:start_timer(Interval, check) diff --git a/apps/emqx/src/emqx_plugins.erl b/apps/emqx/src/emqx_plugins.erl index 6c99305d4..7bb9c084b 100644 --- a/apps/emqx/src/emqx_plugins.erl +++ b/apps/emqx/src/emqx_plugins.erl @@ -43,7 +43,7 @@ %% @doc Load all plugins when the broker started. -spec(load() -> ok | ignore | {error, term()}). load() -> - ok = load_ext_plugins(emqx_config:get([plugins, expand_plugins_dir], undefined)). + ok = load_ext_plugins(emqx:get_config([plugins, expand_plugins_dir], undefined)). %% @doc Load a Plugin -spec(load(atom()) -> ok | {error, term()}). diff --git a/apps/emqx/src/emqx_quic_connection.erl b/apps/emqx/src/emqx_quic_connection.erl index cd41e74a7..c23aec17b 100644 --- a/apps/emqx/src/emqx_quic_connection.erl +++ b/apps/emqx/src/emqx_quic_connection.erl @@ -17,8 +17,41 @@ -module(emqx_quic_connection). %% Callbacks --export([ new_conn/2 +-export([ init/1 + , new_conn/2 + , connected/2 + , shutdown/2 ]). -new_conn(Conn, {_L, COpts, _S}) when is_map(COpts) -> - emqx_connection:start_link(emqx_quic_stream, Conn, COpts). +-type cb_state() :: map() | proplists:proplist(). + + +-spec init(cb_state()) -> cb_state(). +init(ConnOpts) when is_list(ConnOpts) -> + init(maps:from_list(ConnOpts)); +init(ConnOpts) when is_map(ConnOpts) -> + ConnOpts. + +-spec new_conn(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. +new_conn(Conn, S) -> + process_flag(trap_exit, true), + {ok, Pid} = emqx_connection:start_link(emqx_quic_stream, {self(), Conn}, S), + receive + {Pid, stream_acceptor_ready} -> + ok = quicer:async_handshake(Conn), + {ok, S}; + {'EXIT', Pid, _Reason} -> + {error, stream_accept_error} + end. + +-spec connected(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. +connected(Conn, #{slow_start := false} = S) -> + {ok, _Pid} = emqx_connection:start_link(emqx_quic_stream, Conn, S), + {ok, S}; +connected(_Conn, S) -> + {ok, S}. + +-spec shutdown(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. +shutdown(Conn, S) -> + quicer:async_close_connection(Conn), + {ok, S}. diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index 236c11ad3..bba1876c4 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -31,8 +31,16 @@ , peercert/1 ]). -wait(Conn) -> - quicer:accept_stream(Conn, []). +wait({ConnOwner, Conn}) -> + {ok, Conn} = quicer:async_accept_stream(Conn, []), + ConnOwner ! {self(), stream_acceptor_ready}, + receive + %% from msquic + {quic, new_stream, Stream} -> + {ok, Stream}; + {'EXIT', ConnOwner, _Reason} -> + {error, enotconn} + end. type(_) -> quic. @@ -44,6 +52,7 @@ sockname(S) -> quicer:sockname(S). peercert(_S) -> + %% @todo but unsupported by msquic nossl. getstat(Socket, Stats) -> @@ -88,5 +97,8 @@ ensure_ok_or_exit(Fun, Args = [Sock|_]) when is_atom(Fun), is_list(Args) -> async_send(Stream, Data, Options) when is_list(Data) -> async_send(Stream, iolist_to_binary(Data), Options); async_send(Stream, Data, _Options) when is_binary(Data) -> - {ok, _Len} = quicer:send(Stream, Data), - ok. + case quicer:send(Stream, Data) of + {ok, _Len} -> ok; + Other -> + Other + end. diff --git a/apps/emqx/src/emqx_router.erl b/apps/emqx/src/emqx_router.erl index 1a5e344f2..c39571d9f 100644 --- a/apps/emqx/src/emqx_router.erl +++ b/apps/emqx/src/emqx_router.erl @@ -68,7 +68,6 @@ -type(dest() :: node() | {group(), node()}). -define(ROUTE_TAB, emqx_route). --rlog_shard({?ROUTE_SHARD, ?ROUTE_TAB}). %%-------------------------------------------------------------------- %% Mnesia bootstrap @@ -77,6 +76,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTE_TAB, [ {type, bag}, + {rlog_shard, ?ROUTE_SHARD}, {ram_copies, [node()]}, {record_name, route}, {attributes, record_info(fields, route)}, @@ -250,7 +250,7 @@ delete_trie_route(Route = #route{topic = Topic}) -> %% @private -spec(maybe_trans(function(), list(any())) -> ok | {error, term()}). maybe_trans(Fun, Args) -> - case emqx_config:get([broker, perf, route_lock_type]) of + case emqx:get_config([broker, perf, route_lock_type]) of key -> trans(Fun, Args); global -> diff --git a/apps/emqx/src/emqx_router_helper.erl b/apps/emqx/src/emqx_router_helper.erl index 5866e86b3..78d763cac 100644 --- a/apps/emqx/src/emqx_router_helper.erl +++ b/apps/emqx/src/emqx_router_helper.erl @@ -52,8 +52,6 @@ -define(ROUTING_NODE, emqx_routing_node). -define(LOCK, {?MODULE, cleanup_routes}). --rlog_shard({?ROUTE_SHARD, ?ROUTING_NODE}). - -dialyzer({nowarn_function, [cleanup_routes/1]}). %%-------------------------------------------------------------------- @@ -63,6 +61,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTING_NODE, [ {type, set}, + {rlog_shard, ?ROUTE_SHARD}, {ram_copies, [node()]}, {record_name, routing_node}, {attributes, record_info(fields, routing_node)}, diff --git a/apps/emqx/src/emqx_rpc.erl b/apps/emqx/src/emqx_rpc.erl index e950e9e3d..527123745 100644 --- a/apps/emqx/src/emqx_rpc.erl +++ b/apps/emqx/src/emqx_rpc.erl @@ -72,4 +72,4 @@ filter_result(Delivery) -> Delivery. max_client_num() -> - emqx_config:get([rpc, tcp_client_num], ?DefaultClientNum). + emqx:get_config([rpc, tcp_client_num], ?DefaultClientNum). diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 820754363..01989c5a1 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -49,6 +49,10 @@ -typerefl_from_string({cipher/0, emqx_schema, to_erl_cipher_suite}). -typerefl_from_string({comma_separated_atoms/0, emqx_schema, to_comma_separated_atoms}). +-export([ validate_heap_size/1 + , parse_user_lookup_fun/1 + ]). + % workaround: prevent being recognized as unused functions -export([to_duration/1, to_duration_s/1, to_duration_ms/1, to_bytesize/1, to_wordsize/1, @@ -65,194 +69,541 @@ cipher/0, comma_separated_atoms/0]). --export([structs/0, fields/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0, fields/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([ssl/1]). -structs() -> ["zones", "listeners", "broker", "plugins", "sysmon", "alarm"]. +namespace() -> undefined. + +roots() -> + ["zones", + "mqtt", + "flapping_detect", + "force_shutdown", + "force_gc", + "conn_congestion", + "rate_limit", + "quota", + {"listeners", + sc(ref("listeners"), + #{ desc => "MQTT listeners identified by their protocol type and assigned names. " + "The listeners enabled by default are named with 'default'"}) + }, + "broker", + "plugins", + "stats", + "sysmon", + "alarm", + "authorization", + {"authentication", sc(hoconsc:lazy(hoconsc:array(map())), #{})} + ]. fields("stats") -> - [ {"enable", t(boolean(), undefined, true)} + [ {"enable", + sc(boolean(), + #{ default => true + })} ]; -fields("auth") -> - [ {"enable", t(boolean(), undefined, false)} +fields("authorization") -> + [ {"no_match", + sc(union(allow, deny), + #{ default => allow + })} + , {"deny_action", + sc(union(ignore, disconnect), + #{ default => ignore + })} + , {"cache", + sc(ref(?MODULE, "cache"), + #{ + }) + } ]; -fields("authorization_settings") -> - [ {"enable", t(boolean(), undefined, true)} - , {"cache", ref("authorization_cache")} - , {"deny_action", t(union(ignore, disconnect), undefined, ignore)} - ]; - -fields("authorization_cache") -> - [ {"enable", t(boolean(), undefined, true)} - , {"max_size", t(range(1, 1048576), undefined, 32)} - , {"ttl", t(duration(), undefined, "1m")} +fields("cache") -> + [ {"enable", + sc(boolean(), + #{ default => true + }) + } + , {"max_size", + sc(range(1, 1048576), + #{ default => 32 + }) + } + , {"ttl", + sc(duration(), + #{ default => "1m" + }) + } ]; fields("mqtt") -> - [ {"mountpoint", t(binary(), undefined, <<>>)} - , {"idle_timeout", maybe_infinity(duration(), "15s")} - , {"max_packet_size", t(bytesize(), undefined, "1MB")} - , {"max_clientid_len", t(range(23, 65535), undefined, 65535)} - , {"max_topic_levels", t(range(1, 65535), undefined, 65535)} - , {"max_qos_allowed", t(range(0, 2), undefined, 2)} - , {"max_topic_alias", t(range(0, 65535), undefined, 65535)} - , {"retain_available", t(boolean(), undefined, true)} - , {"wildcard_subscription", t(boolean(), undefined, true)} - , {"shared_subscription", t(boolean(), undefined, true)} - , {"ignore_loop_deliver", t(boolean(), undefined, false)} - , {"strict_mode", t(boolean(), undefined, false)} - , {"response_information", t(string(), undefined, "")} - , {"server_keepalive", maybe_disabled(integer())} - , {"keepalive_backoff", t(float(), undefined, 0.75)} - , {"max_subscriptions", maybe_infinity(range(1, inf))} - , {"upgrade_qos", t(boolean(), undefined, false)} - , {"max_inflight", t(range(1, 65535), undefined, 32)} - , {"retry_interval", t(duration(), undefined, "30s")} - , {"max_awaiting_rel", maybe_infinity(integer(), 100)} - , {"await_rel_timeout", t(duration(), undefined, "300s")} - , {"session_expiry_interval", t(duration(), undefined, "2h")} - , {"max_mqueue_len", maybe_infinity(range(0, inf), 1000)} - , {"mqueue_priorities", maybe_disabled(map())} - , {"mqueue_default_priority", t(union(highest, lowest), undefined, lowest)} - , {"mqueue_store_qos0", t(boolean(), undefined, true)} - , {"use_username_as_clientid", t(boolean(), undefined, false)} - , {"peer_cert_as_username", maybe_disabled(union([cn, dn, crt, pem, md5]))} - , {"peer_cert_as_clientid", maybe_disabled(union([cn, dn, crt, pem, md5]))} + [ {"idle_timeout", + sc(hoconsc:union([infinity, duration()]), + #{ default => "15s" + })} + , {"max_packet_size", + sc(bytesize(), + #{ default => "1MB" + })} + , {"max_clientid_len", + sc(range(23, 65535), + #{ default => 65535 + })} + , {"max_topic_levels", + sc(range(1, 65535), + #{ default => 65535 + })} + , {"max_qos_allowed", + sc(range(0, 2), + #{ default => 2 + })} + , {"max_topic_alias", + sc(range(0, 65535), + #{ default => 65535 + })} + , {"retain_available", + sc(boolean(), + #{ default => true + })} + , {"wildcard_subscription", + sc(boolean(), + #{ default => true + })} + , {"shared_subscription", + sc(boolean(), + #{ default => true + })} + , {"ignore_loop_deliver", + sc(boolean(), + #{ default => false + })} + , {"strict_mode", + sc(boolean(), + #{default => false + }) + } + , {"response_information", + sc(string(), + #{default => "" + }) + } + , {"server_keepalive", + sc(hoconsc:union([integer(), disabled]), + #{ default => disabled + }) + } + , {"keepalive_backoff", + sc(float(), + #{default => 0.75 + }) + } + , {"max_subscriptions", + sc(hoconsc:union([range(1, inf), infinity]), + #{ default => infinity + }) + } + , {"upgrade_qos", + sc(boolean(), + #{ default => false + }) + } + , {"max_inflight", + sc(range(1, 65535), + #{ default => 32 + }) + } + , {"retry_interval", + sc(duration(), + #{default => "30s" + }) + } + , {"max_awaiting_rel", + sc(hoconsc:union([integer(), infinity]), + #{ default => 100 + }) + } + , {"await_rel_timeout", + sc(duration(), + #{ default => "300s" + }) + } + , {"session_expiry_interval", + sc(duration(), + #{ default => "2h" + }) + } + , {"max_mqueue_len", + sc(hoconsc:union([range(0, inf), infinity]), + #{ default => 1000 + }) + } + , {"mqueue_priorities", + sc(hoconsc:union([map(), disabled]), + #{ default => disabled + }) + } + , {"mqueue_default_priority", + sc(union(highest, lowest), + #{ default => lowest + }) + } + , {"mqueue_store_qos0", + sc(boolean(), + #{ default => true + }) + } + , {"use_username_as_clientid", + sc(boolean(), + #{ default => false + }) + } + , {"peer_cert_as_username", + sc(hoconsc:union([disabled, cn, dn, crt, pem, md5]), + #{ default => disabled + })} + , {"peer_cert_as_clientid", + sc(hoconsc:union([disabled, cn, dn, crt, pem, md5]), + #{ default => disabled + })} ]; fields("zones") -> - [ {"$name", ref("zone_settings")}]; + [ {"$name", + sc(ref("zone_settings"), + #{ + } + )}]; fields("zone_settings") -> - [ {"mqtt", ref("mqtt")} - , {"authorization", ref("authorization_settings")} - , {"auth", ref("auth")} - , {"stats", ref("stats")} - , {"flapping_detect", ref("flapping_detect")} - , {"force_shutdown", ref("force_shutdown")} - , {"conn_congestion", ref("conn_congestion")} - , {"force_gc", ref("force_gc")} - , {"overall_max_connections", maybe_infinity(integer())} - , {"listeners", t("listeners")} - ]; + Fields = ["mqtt", "stats", "flapping_detect", "force_shutdown", + "conn_congestion", "rate_limit", "quota", "force_gc"], + [{F, ref(emqx_zone_schema, F)} || F <- Fields]; fields("rate_limit") -> - [ {"max_conn_rate", maybe_infinity(integer(), 1000)} - , {"conn_messages_in", maybe_infinity(comma_separated_list())} - , {"conn_bytes_in", maybe_infinity(comma_separated_list())} - , {"quota", ref("rate_limit_quota")} + [ {"max_conn_rate", + sc(hoconsc:union([infinity, integer()]), + #{ default => 1000 + }) + } + , {"conn_messages_in", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } + , {"conn_bytes_in", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } ]; -fields("rate_limit_quota") -> - [ {"conn_messages_routing", maybe_infinity(comma_separated_list())} - , {"overall_messages_routing", maybe_infinity(comma_separated_list())} +fields("quota") -> + [ {"conn_messages_routing", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } + , {"overall_messages_routing", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } ]; fields("flapping_detect") -> - [ {"enable", t(boolean(), undefined, false)} - , {"max_count", t(integer(), undefined, 15)} - , {"window_time", t(duration(), undefined, "1m")} - , {"ban_time", t(duration(), undefined, "5m")} + [ {"enable", + sc(boolean(), + #{ default => false + })} + , {"max_count", + sc(integer(), + #{ default => 15 + })} + , {"window_time", + sc(duration(), + #{ default => "1m" + })} + , {"ban_time", + sc(duration(), + #{ default => "5m" + })} ]; fields("force_shutdown") -> - [ {"enable", t(boolean(), undefined, true)} - , {"max_message_queue_len", t(range(0, inf), undefined, 1000)} - , {"max_heap_size", t(wordsize(), undefined, "32MB", undefined, - fun(Siz) -> - MaxSiz = case erlang:system_info(wordsize) of - 8 -> % arch_64 - (1 bsl 59) - 1; - 4 -> % arch_32 - (1 bsl 27) - 1 - end, - case Siz > MaxSiz of - true -> - error(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); - false -> - ok - end - end)} + [ {"enable", + sc(boolean(), + #{ default => true})} + , {"max_message_queue_len", + sc(range(0, inf), + #{ default => 1000 + })} + , {"max_heap_size", + sc(wordsize(), + #{ default => "32MB", + validator => fun ?MODULE:validate_heap_size/1 + })} ]; fields("conn_congestion") -> - [ {"enable_alarm", t(boolean(), undefined, false)} - , {"min_alarm_sustain_duration", t(duration(), undefined, "1m")} + [ {"enable_alarm", + sc(boolean(), + #{ default => false + })} + , {"min_alarm_sustain_duration", + sc(duration(), + #{ default => "1m" + })} ]; fields("force_gc") -> - [ {"enable", t(boolean(), undefined, true)} - , {"count", t(range(0, inf), undefined, 16000)} - , {"bytes", t(bytesize(), undefined, "16MB")} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"count", + sc(range(0, inf), + #{ default => 16000 + })} + , {"bytes", + sc(bytesize(), + #{ default => "16MB" + })} ]; fields("listeners") -> - [ {"$name", hoconsc:union( - [ disabled - , hoconsc:ref("mqtt_tcp_listener") - , hoconsc:ref("mqtt_ws_listener") - , hoconsc:ref("mqtt_quic_listener") - ])} + [ {"tcp", + sc(ref("tcp_listeners"), + #{ desc => "TCP listeners" + }) + } + , {"ssl", + sc(ref("ssl_listeners"), + #{ desc => "SSL listeners" + }) + } + , {"ws", + sc(ref("ws_listeners"), + #{ desc => "HTTP websocket listeners" + }) + } + , {"wss", + sc(ref("wss_listeners"), + #{ desc => "HTTPS websocket listeners" + }) + } + , {"quic", + sc(ref("quic_listeners"), + #{ desc => "QUIC listeners" + }) + } + ]; + +fields("tcp_listeners") -> + [ {"$name", ref("mqtt_tcp_listener")} + ]; +fields("ssl_listeners") -> + [ {"$name", ref("mqtt_ssl_listener")} + ]; +fields("ws_listeners") -> + [ {"$name", ref("mqtt_ws_listener")} + ]; +fields("wss_listeners") -> + [ {"$name", ref("mqtt_wss_listener")} + ]; +fields("quic_listeners") -> + [ {"$name", ref("mqtt_quic_listener")} ]; fields("mqtt_tcp_listener") -> - [ {"type", t(tcp)} - , {"tcp", ref("tcp_opts")} - , {"ssl", ref("ssl_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{ desc => "TCP listener options" + }) + } + ] ++ mqtt_listener(); + +fields("mqtt_ssl_listener") -> + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"ssl", + sc(ref("listener_ssl_opts"), + #{}) + } ] ++ mqtt_listener(); fields("mqtt_ws_listener") -> - [ {"type", t(ws)} - , {"tcp", ref("tcp_opts")} - , {"ssl", ref("ssl_opts")} - , {"websocket", ref("ws_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"websocket", + sc(ref("ws_opts"), + #{}) + } + ] ++ mqtt_listener(); + +fields("mqtt_wss_listener") -> + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"ssl", + sc(ref("listener_ssl_opts"), + #{}) + } + , {"websocket", + sc(ref("ws_opts"), + #{}) + } ] ++ mqtt_listener(); fields("mqtt_quic_listener") -> - [ {"enabled", t(boolean(), undefined, true)} - , {"type", t(quic)} - , {"certfile", t(string(), undefined, undefined)} - , {"keyfile", t(string(), undefined, undefined)} - , {"ciphers", t(comma_separated_list(), undefined, "TLS_AES_256_GCM_SHA384," - "TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256")} - , {"idle_timeout", t(duration(), undefined, "15s")} + [ {"enabled", + sc(boolean(), + #{ default => true + }) + } + , {"certfile", + sc(string(), + #{}) + } + , {"keyfile", + sc(string(), + #{}) + } + , {"ciphers", + sc(comma_separated_list(), + #{ default => "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256," + "TLS_CHACHA20_POLY1305_SHA256" + })} + , {"idle_timeout", + sc(duration(), + #{ default => "15s" + }) + } ] ++ base_listener(); fields("ws_opts") -> - [ {"mqtt_path", t(string(), undefined, "/mqtt")} - , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} - , {"compress", t(boolean(), undefined, false)} - , {"idle_timeout", t(duration(), undefined, "15s")} - , {"max_frame_size", maybe_infinity(integer())} - , {"fail_if_no_subprotocol", t(boolean(), undefined, true)} - , {"supported_subprotocols", t(comma_separated_list(), undefined, - "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5")} - , {"check_origin_enable", t(boolean(), undefined, false)} - , {"allow_origin_absence", t(boolean(), undefined, true)} - , {"check_origins", t(hoconsc:array(binary()), undefined, [])} - , {"proxy_address_header", t(string(), undefined, "x-forwarded-for")} - , {"proxy_port_header", t(string(), undefined, "x-forwarded-port")} - , {"deflate_opts", ref("deflate_opts")} + [ {"mqtt_path", + sc(string(), + #{ default => "/mqtt" + }) + } + , {"mqtt_piggyback", + sc(hoconsc:union([single, multiple]), + #{ default => multiple + }) + } + , {"compress", + sc(boolean(), + #{ default => false + }) + } + , {"idle_timeout", + sc(duration(), + #{ default => "15s" + }) + } + , {"max_frame_size", + sc(hoconsc:union([infinity, integer()]), + #{ default => infinity + }) + } + , {"fail_if_no_subprotocol", + sc(boolean(), + #{ default => true + }) + } + , {"supported_subprotocols", + sc(comma_separated_list(), + #{ default => "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + }) + } + , {"check_origin_enable", + sc(boolean(), + #{ default => false + }) + } + , {"allow_origin_absence", + sc(boolean(), + #{ default => true + }) + } + , {"check_origins", + sc(hoconsc:array(binary()), + #{ default => [] + }) + } + , {"proxy_address_header", + sc(string(), + #{ default => "x-forwarded-for" + }) + } + , {"proxy_port_header", + sc(string(), + #{ default => "x-forwarded-port" + }) + } + , {"deflate_opts", + sc(ref("deflate_opts"), + #{}) + } ]; fields("tcp_opts") -> - [ {"active_n", t(integer(), undefined, 100)} - , {"backlog", t(integer(), undefined, 1024)} - , {"send_timeout", t(duration(), undefined, "15s")} - , {"send_timeout_close", t(boolean(), undefined, true)} - , {"recbuf", t(bytesize())} - , {"sndbuf", t(bytesize())} - , {"buffer", t(bytesize())} - , {"high_watermark", t(bytesize(), undefined, "1MB")} - , {"nodelay", t(boolean(), undefined, false)} - , {"reuseaddr", t(boolean(), undefined, true)} + [ {"active_n", + sc(integer(), + #{ default => 100 + }) + } + , {"backlog", + sc(integer(), + #{ default => 1024 + }) + } + , {"send_timeout", + sc(duration(), + #{ default => "15s" + }) + } + , {"send_timeout_close", + sc(boolean(), + #{ default => true + }) + } + , {"recbuf", + sc(bytesize(), + #{}) + } + , {"sndbuf", + sc(bytesize(), + #{}) + } + , {"buffer", + sc(bytesize(), + #{}) + } + , {"high_watermark", + sc(bytesize(), + #{ default => "1MB"}) + } + , {"nodelay", + sc(boolean(), + #{ default => false}) + } + , {"reuseaddr", + sc(boolean(), + #{ default => true + }) + } ]; -fields("ssl_opts") -> +fields("listener_ssl_opts") -> ssl(#{handshake_timeout => "15s" , depth => 10 , reuse_sessions => true @@ -261,78 +612,241 @@ fields("ssl_opts") -> }); fields("deflate_opts") -> - [ {"level", t(union([none, default, best_compression, best_speed]))} - , {"mem_level", t(range(1, 9), undefined, 8)} - , {"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(range(8, 15), undefined, 15)} - , {"client_max_window_bits", t(range(8, 15), undefined, 15)} + [ {"level", + sc(hoconsc:union([none, default, best_compression, best_speed]), + #{}) + } + , {"mem_level", + sc(range(1, 9), + #{ default => 8 + }) + } + , {"strategy", + sc(hoconsc:union([default, filtered, huffman_only, rle]), + #{}) + } + , {"server_context_takeover", + sc(hoconsc:union([takeover, no_takeover]), + #{}) + } + , {"client_context_takeover", + sc(hoconsc:union([takeover, no_takeover]), + #{}) + } + , {"server_max_window_bits", + sc(range(8, 15), + #{ default => 15 + }) + } + , {"client_max_window_bits", + sc(range(8, 15), + #{ default => 15 + }) + } ]; fields("plugins") -> - [ {"expand_plugins_dir", t(string())} + [ {"expand_plugins_dir", + sc(string(), + #{}) + } ]; fields("broker") -> - [ {"sys_msg_interval", maybe_disabled(duration(), "1m")} - , {"sys_heartbeat_interval", maybe_disabled(duration(), "30s")} - , {"enable_session_registry", t(boolean(), undefined, true)} - , {"session_locking_strategy", t(union([local, leader, quorum, all]), undefined, quorum)} - , {"shared_subscription_strategy", t(union(random, round_robin), undefined, round_robin)} - , {"shared_dispatch_ack_enabled", t(boolean(), undefined, false)} - , {"route_batch_clean", t(boolean(), undefined, true)} - , {"perf", ref("perf")} + [ {"sys_msg_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "1m" + }) + } + , {"sys_heartbeat_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "30s" + }) + } + , {"enable_session_registry", + sc(boolean(), + #{ default => true + }) + } + , {"session_locking_strategy", + sc(hoconsc:union([local, leader, quorum, all]), + #{ default => quorum + }) + } + , {"shared_subscription_strategy", + sc(hoconsc:union([random, round_robin]), + #{ default => round_robin + }) + } + , {"shared_dispatch_ack_enabled", + sc(boolean(), + #{ default => false + }) + } + , {"route_batch_clean", + sc(boolean(), + #{ default => true + })} + , {"perf", + sc(ref("broker_perf"), + #{ desc => "Broker performance tuning pamaters" + }) + } ]; -fields("perf") -> - [ {"route_lock_type", t(union([key, tab, global]), undefined, key)} - , {"trie_compaction", t(boolean(), undefined, true)} +fields("broker_perf") -> + [ {"route_lock_type", + sc(hoconsc:union([key, tab, global]), + #{ default => key + })} + , {"trie_compaction", + sc(boolean(), + #{ default => true + })} ]; fields("sysmon") -> - [ {"vm", ref("sysmon_vm")} - , {"os", ref("sysmon_os")} + [ {"vm", + sc(ref("sysmon_vm"), + #{}) + } + , {"os", + sc(ref("sysmon_os"), + #{}) + } ]; fields("sysmon_vm") -> - [ {"process_check_interval", t(duration(), undefined, "30s")} - , {"process_high_watermark", t(percent(), undefined, "80%")} - , {"process_low_watermark", t(percent(), undefined, "60%")} - , {"long_gc", maybe_disabled(duration())} - , {"long_schedule", maybe_disabled(duration(), "240ms")} - , {"large_heap", maybe_disabled(bytesize(), "32MB")} - , {"busy_dist_port", t(boolean(), undefined, true)} - , {"busy_port", t(boolean(), undefined, true)} + [ {"process_check_interval", + sc(duration(), + #{ default => "30s" + }) + } + , {"process_high_watermark", + sc(percent(), + #{ default => "80%" + }) + } + , {"process_low_watermark", + sc(percent(), + #{ default => "60%" + }) + } + , {"long_gc", + sc(hoconsc:union([disabled, duration()]), + #{}) + } + , {"long_schedule", + sc(hoconsc:union([disabled, duration()]), + #{ default => "240ms" + }) + } + , {"large_heap", + sc(hoconsc:union([disabled, bytesize()]), + #{default => "32MB"}) + } + , {"busy_dist_port", + sc(boolean(), + #{ default => true + }) + } + , {"busy_port", + sc(boolean(), + #{ default => true + })} ]; fields("sysmon_os") -> - [ {"cpu_check_interval", t(duration(), undefined, "60s")} - , {"cpu_high_watermark", t(percent(), undefined, "80%")} - , {"cpu_low_watermark", t(percent(), undefined, "60%")} - , {"mem_check_interval", maybe_disabled(duration(), "60s")} - , {"sysmem_high_watermark", t(percent(), undefined, "70%")} - , {"procmem_high_watermark", t(percent(), undefined, "5%")} + [ {"cpu_check_interval", + sc(duration(), + #{ default => "60s"}) + } + , {"cpu_high_watermark", + sc(percent(), + #{ default => "80%" + }) + } + , {"cpu_low_watermark", + sc(percent(), + #{ default => "60%" + }) + } + , {"mem_check_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "60s" + })} + , {"sysmem_high_watermark", + sc(percent(), + #{ default => "70%" + }) + } + , {"procmem_high_watermark", + sc(percent(), + #{ default => "5%" + }) + } ]; fields("alarm") -> - [ {"actions", t(hoconsc:array(atom()), undefined, [log, publish])} - , {"size_limit", t(integer(), undefined, 1000)} - , {"validity_period", t(duration(), undefined, "24h")} + [ {"actions", + sc(hoconsc:array(atom()), + #{ default => [log, publish] + }) + } + , {"size_limit", + sc(integer(), + #{ default => 1000 + }) + } + , {"validity_period", + sc(duration(), + #{ default => "24h" + }) + } ]. mqtt_listener() -> base_listener() ++ - [ {"access_rules", t(hoconsc:array(string()))} - , {"proxy_protocol", t(boolean(), undefined, false)} - , {"proxy_protocol_timeout", t(duration())} + [ {"access_rules", + sc(hoconsc:array(string()), + #{}) + } + , {"proxy_protocol", + sc(boolean(), + #{ default => false + }) + } + , {"proxy_protocol_timeout", + sc(duration(), + #{}) + } + , {"authentication", + sc(hoconsc:lazy(hoconsc:array(map())), + #{}) + } ]. base_listener() -> - [ {"bind", t(union(ip_port(), integer()))} - , {"acceptors", t(integer(), undefined, 16)} - , {"max_connections", maybe_infinity(integer(), infinity)} - , {"rate_limit", ref("rate_limit")} + [ {"bind", + sc(hoconsc:union([ip_port(), integer()]), + #{ nullable => false + })} + , {"acceptors", + sc(integer(), + #{ default => 16 + })} + , {"max_connections", + sc(hoconsc:union([infinity, integer()]), + #{ default => infinity + })} + , {"mountpoint", + sc(binary(), + #{ default => <<>> + })} + , {"zone", + sc(atom(), + #{ default => 'default' + })} ]. %% utils @@ -358,43 +872,101 @@ conf_get(Key, Conf, Default) -> filter(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined]. -%% generate a ssl field. -%% ssl(#{"verify" => verify_peer}) will return: -%% [ {"cacertfile", t(string(), undefined, undefined)} -%% , {"certfile", t(string(), undefined, undefined)} -%% , {"keyfile", t(string(), undefined, undefined)} -%% , {"verify", t(union(verify_peer, verify_none), undefined, verify_peer)} -%% , {"server_name_indication", undefined, undefined)} -%% ...] ssl(Defaults) -> D = fun (Field) -> maps:get(to_atom(Field), Defaults, undefined) end, - [ {"enable", t(boolean(), undefined, D("enable"))} - , {"cacertfile", t(string(), undefined, D("cacertfile"))} - , {"certfile", t(string(), undefined, D("certfile"))} - , {"keyfile", t(string(), undefined, D("keyfile"))} - , {"verify", t(union(verify_peer, verify_none), undefined, D("verify"))} - , {"fail_if_no_peer_cert", t(boolean(), undefined, D("fail_if_no_peer_cert"))} - , {"secure_renegotiate", t(boolean(), undefined, D("secure_renegotiate"))} - , {"reuse_sessions", t(boolean(), undefined, D("reuse_sessions"))} - , {"honor_cipher_order", t(boolean(), undefined, D("honor_cipher_order"))} - , {"handshake_timeout", t(duration(), undefined, D("handshake_timeout"))} - , {"depth", t(integer(), undefined, D("depth"))} - , {"password", hoconsc:t(string(), #{default => D("key_password"), - sensitive => true - })} - , {"dhfile", t(string(), undefined, D("dhfile"))} - , {"server_name_indication", t(union(disable, string()), undefined, - D("server_name_indication"))} - , {"versions", #{ type => list(atom()) - , default => maps:get(versions, Defaults, default_tls_vsns()) - , converter => fun (Vsns) -> [tls_vsn(V) || V <- Vsns] end - }} - , {"ciphers", t(hoconsc:array(string()), undefined, D("ciphers"))} - , {"user_lookup_fun", t(any(), undefined, {fun emqx_psk:lookup/3, <<>>})} + [ {"enable", + sc(boolean(), + #{ default => D("enable") + }) + } + , {"cacertfile", + sc(string(), + #{ default => D("cacertfile") + }) + } + , {"certfile", + sc(string(), + #{ default => D("certfile") + }) + } + , {"keyfile", + sc(string(), + #{ default => D("keyfile") + }) + } + , {"verify", + sc(hoconsc:union([verify_peer, verify_none]), + #{ default => D("verify") + }) + } + , {"fail_if_no_peer_cert", + sc(boolean(), + #{ default => D("fail_if_no_peer_cert") + }) + } + , {"secure_renegotiate", + sc(boolean(), + #{ default => D("secure_renegotiate") + }) + } + , {"reuse_sessions", + sc(boolean(), + #{ default => D("reuse_sessions") + }) + } + , {"honor_cipher_order", + sc(boolean(), + #{ default => D("honor_cipher_order") + }) + } + , {"handshake_timeout", + sc(duration(), + #{ default => D("handshake_timeout") + }) + } + , {"depth", + sc(integer(), + #{default => D("depth") + }) + } + , {"password", + sc(string(), + #{ default => D("key_password") + , sensitive => true + }) + } + , {"dhfile", + sc(string(), + #{ default => D("dhfile") + }) + } + , {"server_name_indication", + sc(hoconsc:union([disable, string()]), + #{ default => D("server_name_indication") + }) + } + , {"versions", + sc(typerefl:alias("string", list(atom())), + #{ default => maps:get(versions, Defaults, default_tls_vsns()) + , converter => fun (Vsns) -> [tls_vsn(iolist_to_binary(V)) || V <- Vsns] end + }) + } + , {"ciphers", + sc(hoconsc:array(string()), + #{ default => D("ciphers") + }) + } + , {"user_lookup_fun", + sc(typerefl:alias("string", any()), + #{ default => "emqx_psk:lookup" + , converter => fun ?MODULE:parse_user_lookup_fun/1 + }) + } ]. %% on erl23.2.7.2-emqx-2, sufficient_crypto_support('tlsv1.3') -> false default_tls_vsns() -> [<<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>]. + tls_vsn(<<"tlsv1.3">>) -> 'tlsv1.3'; tls_vsn(<<"tlsv1.2">>) -> 'tlsv1.2'; tls_vsn(<<"tlsv1.1">>) -> 'tlsv1.1'; @@ -437,40 +1009,11 @@ ceiling(X) -> %% types -t(Type) -> hoconsc:t(Type). +sc(Type, Meta) -> hoconsc:mk(Type, Meta). -t(Type, Mapping, Default) -> - hoconsc:t(Type, #{mapping => Mapping, default => Default}). +ref(Field) -> hoconsc:ref(?MODULE, Field). -t(Type, Mapping, Default, OverrideEnv) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - }). - -t(Type, Mapping, Default, OverrideEnv, Validator) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - , validator => Validator - }). - -ref(Field) -> hoconsc:t(hoconsc:ref(?MODULE, Field)). - -maybe_disabled(T) -> - maybe_sth(disabled, T, disabled). - -maybe_disabled(T, Default) -> - maybe_sth(disabled, T, Default). - -maybe_infinity(T) -> - maybe_sth(infinity, T, infinity). - -maybe_infinity(T, Default) -> - maybe_sth(infinity, T, Default). - -maybe_sth(What, Type, Default) -> - t(union([What, Type]), undefined, Default). +ref(Module, Field) -> hoconsc:ref(Module, Field). to_duration(Str) -> case hocon_postprocess:duration(Str) of @@ -537,3 +1080,20 @@ to_atom(Str) when is_list(Str) -> list_to_atom(Str); to_atom(Bin) when is_binary(Bin) -> binary_to_atom(Bin, utf8). + +validate_heap_size(Siz) -> + MaxSiz = case erlang:system_info(wordsize) of + 8 -> % arch_64 + (1 bsl 59) - 1; + 4 -> % arch_32 + (1 bsl 27) - 1 + end, + case Siz > MaxSiz of + true -> error(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); + false -> ok + end. +parse_user_lookup_fun(StrConf) -> + [ModStr, FunStr] = string:tokens(StrConf, ":"), + Mod = list_to_atom(ModStr), + Fun = list_to_atom(FunStr), + {fun Mod:Fun/3, <<>>}. diff --git a/apps/emqx/src/emqx_shared_sub.erl b/apps/emqx/src/emqx_shared_sub.erl index 1968c47d8..9e5dd726f 100644 --- a/apps/emqx/src/emqx_shared_sub.erl +++ b/apps/emqx/src/emqx_shared_sub.erl @@ -76,8 +76,6 @@ -define(NACK(Reason), {shared_sub_nack, Reason}). -define(NO_ACK, no_ack). --rlog_shard({?SHARED_SUB_SHARD, ?TAB}). - -record(state, {pmon}). -record(emqx_shared_subscription, {group, topic, subpid}). @@ -89,6 +87,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, + {rlog_shard, ?SHARED_SUB_SHARD}, {ram_copies, [node()]}, {record_name, emqx_shared_subscription}, {attributes, record_info(fields, emqx_shared_subscription)}]); @@ -136,11 +135,11 @@ dispatch(Group, Topic, Delivery = #delivery{message = Msg}, FailedSubs) -> -spec(strategy() -> strategy()). strategy() -> - emqx_config:get([broker, shared_subscription_strategy]). + emqx:get_config([broker, shared_subscription_strategy]). -spec(ack_enabled() -> boolean()). ack_enabled() -> - emqx_config:get([broker, shared_dispatch_ack_enabled]). + emqx:get_config([broker, shared_dispatch_ack_enabled]). do_dispatch(SubPid, Topic, Msg, _Type) when SubPid =:= self() -> %% Deadlock otherwise diff --git a/apps/emqx/src/emqx_sys.erl b/apps/emqx/src/emqx_sys.erl index 6baae8c1e..70043e2bb 100644 --- a/apps/emqx/src/emqx_sys.erl +++ b/apps/emqx/src/emqx_sys.erl @@ -102,10 +102,10 @@ datetime() -> "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])). sys_interval() -> - emqx_config:get([broker, sys_msg_interval]). + emqx:get_config([broker, sys_msg_interval]). sys_heatbeat_interval() -> - emqx_config:get([broker, sys_heartbeat_interval]). + emqx:get_config([broker, sys_heartbeat_interval]). %% @doc Get sys info -spec(info() -> list(tuple())). diff --git a/apps/emqx/src/emqx_sys_mon.erl b/apps/emqx/src/emqx_sys_mon.erl index 0b981ffec..80f5e49ec 100644 --- a/apps/emqx/src/emqx_sys_mon.erl +++ b/apps/emqx/src/emqx_sys_mon.erl @@ -60,7 +60,7 @@ start_timer(State) -> State#{timer := emqx_misc:start_timer(timer:seconds(2), reset)}. sysm_opts() -> - sysm_opts(maps:to_list(emqx_config:get([sysmon, vm])), []). + sysm_opts(maps:to_list(emqx:get_config([sysmon, vm])), []). sysm_opts([], Acc) -> Acc; sysm_opts([{_, disabled}|Opts], Acc) -> diff --git a/apps/emqx/src/emqx_trie.erl b/apps/emqx/src/emqx_trie.erl index 32c176b65..ea70ff7f3 100644 --- a/apps/emqx/src/emqx_trie.erl +++ b/apps/emqx/src/emqx_trie.erl @@ -50,8 +50,6 @@ , count = 0 :: non_neg_integer() }). --rlog_shard({?ROUTE_SHARD, ?TRIE}). - %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -64,6 +62,7 @@ mnesia(boot) -> {write_concurrency, true} ]}], ok = ekka_mnesia:create_table(?TRIE, [ + {rlog_shard, ?ROUTE_SHARD}, {ram_copies, [node()]}, {record_name, ?TRIE}, {attributes, record_info(fields, ?TRIE)}, @@ -270,7 +269,7 @@ match_compact([Word | Words], Prefix, IsWildcard, Acc0) -> lookup_topic(MlTopic). is_compact() -> - emqx_config:get([broker, perf, trie_compaction], true). + emqx:get_config([broker, perf, trie_compaction], true). set_compact(Bool) -> emqx_config:put([broker, perf, trie_compaction], Bool). diff --git a/apps/emqx/src/emqx_vm_mon.erl b/apps/emqx/src/emqx_vm_mon.erl index 13a470959..51710b5b5 100644 --- a/apps/emqx/src/emqx_vm_mon.erl +++ b/apps/emqx/src/emqx_vm_mon.erl @@ -57,8 +57,8 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({timeout, _Timer, check}, State) -> - ProcHighWatermark = emqx_config:get([sysmon, vm, process_high_watermark]), - ProcLowWatermark = emqx_config:get([sysmon, vm, process_low_watermark]), + ProcHighWatermark = emqx:get_config([sysmon, vm, process_high_watermark]), + ProcLowWatermark = emqx:get_config([sysmon, vm, process_low_watermark]), ProcessCount = erlang:system_info(process_count), case ProcessCount / erlang:system_info(process_limit) of Percent when Percent >= ProcHighWatermark -> @@ -89,5 +89,5 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- start_check_timer() -> - Interval = emqx_config:get([sysmon, vm, process_check_interval]), + Interval = emqx:get_config([sysmon, vm, process_check_interval]), emqx_misc:start_timer(Interval, check). diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index b76567f6c..32a81c26a 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -85,8 +85,8 @@ idle_timer :: maybe(reference()), %% Zone name zone :: atom(), - %% Listener Name - listener :: atom() + %% Listener Type and Name + listener :: {Type::atom(), Name::atom()} }). -type(state() :: #state{}). @@ -173,12 +173,12 @@ call(WsPid, Req, Timeout) when is_pid(WsPid) -> %% WebSocket callbacks %%-------------------------------------------------------------------- -init(Req, #{zone := Zone, listener := Listener} = Opts) -> +init(Req, #{listener := {Type, Listener}} = Opts) -> %% WS Transport Idle Timeout - WsOpts = #{compress => get_ws_opts(Zone, Listener, compress), - deflate_opts => get_ws_opts(Zone, Listener, deflate_opts), - max_frame_size => get_ws_opts(Zone, Listener, max_frame_size), - idle_timeout => get_ws_opts(Zone, Listener, idle_timeout) + WsOpts = #{compress => get_ws_opts(Type, Listener, compress), + deflate_opts => get_ws_opts(Type, Listener, deflate_opts), + max_frame_size => get_ws_opts(Type, Listener, max_frame_size), + idle_timeout => get_ws_opts(Type, Listener, idle_timeout) }, case check_origin_header(Req, Opts) of {error, Message} -> @@ -187,17 +187,17 @@ init(Req, #{zone := Zone, listener := Listener} = Opts) -> ok -> parse_sec_websocket_protocol(Req, Opts, WsOpts) end. -parse_sec_websocket_protocol(Req, #{zone := Zone, listener := Listener} = Opts, WsOpts) -> +parse_sec_websocket_protocol(Req, #{listener := {Type, Listener}} = Opts, WsOpts) -> case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of undefined -> - case get_ws_opts(Zone, Listener, fail_if_no_subprotocol) of + case get_ws_opts(Type, Listener, fail_if_no_subprotocol) of true -> {ok, cowboy_req:reply(400, Req), WsOpts}; false -> {cowboy_websocket, Req, [Req, Opts], WsOpts} end; Subprotocols -> - SupportedSubprotocols = get_ws_opts(Zone, Listener, supported_subprotocols), + SupportedSubprotocols = get_ws_opts(Type, Listener, supported_subprotocols), NSupportedSubprotocols = [list_to_binary(Subprotocol) || Subprotocol <- SupportedSubprotocols], case pick_subprotocol(Subprotocols, NSupportedSubprotocols) of @@ -221,29 +221,29 @@ pick_subprotocol([Subprotocol | Rest], SupportedSubprotocols) -> pick_subprotocol(Rest, SupportedSubprotocols) end. -parse_header_fun_origin(Req, #{zone := Zone, listener := Listener}) -> +parse_header_fun_origin(Req, #{listener := {Type, Listener}}) -> case cowboy_req:header(<<"origin">>, Req) of undefined -> - case get_ws_opts(Zone, Listener, allow_origin_absence) of + case get_ws_opts(Type, Listener, allow_origin_absence) of true -> ok; false -> {error, origin_header_cannot_be_absent} end; Value -> - case lists:member(Value, get_ws_opts(Zone, Listener, check_origins)) of + case lists:member(Value, get_ws_opts(Type, Listener, check_origins)) of true -> ok; false -> {origin_not_allowed, Value} end end. -check_origin_header(Req, #{zone := Zone, listener := Listener} = Opts) -> - case get_ws_opts(Zone, Listener, check_origin_enable) of +check_origin_header(Req, #{listener := {Type, Listener}} = Opts) -> + case get_ws_opts(Type, Listener, check_origin_enable) of true -> parse_header_fun_origin(Req, Opts); false -> ok end. -websocket_init([Req, #{zone := Zone, listener := Listener} = Opts]) -> +websocket_init([Req, #{zone := Zone, listener := {Type, Listener}} = Opts]) -> {Peername, Peercert} = - case emqx_config:get_listener_conf(Zone, Listener, [proxy_protocol]) andalso + case emqx_config:get_listener_conf(Type, Listener, [proxy_protocol]) andalso maps:get(proxy_header, Req) of #{src_address := SrcAddr, src_port := SrcPort, ssl := SSL} -> SourceName = {SrcAddr, SrcPort}, @@ -278,7 +278,7 @@ websocket_init([Req, #{zone := Zone, listener := Listener} = Opts]) -> conn_mod => ?MODULE }, Limiter = emqx_limiter:init(Zone, undefined, undefined, []), - MQTTPiggyback = get_ws_opts(Zone, Listener, mqtt_piggyback), + MQTTPiggyback = get_ws_opts(Type, Listener, mqtt_piggyback), FrameOpts = #{ strict_mode => emqx_config:get_zone_conf(Zone, [mqtt, strict_mode]), max_size => emqx_config:get_zone_conf(Zone, [mqtt, max_packet_size]) @@ -317,7 +317,7 @@ websocket_init([Req, #{zone := Zone, listener := Listener} = Opts]) -> idle_timeout = IdleTimeout, idle_timer = IdleTimer, zone = Zone, - listener = Listener + listener = {Type, Listener} }, hibernate}. websocket_handle({binary, Data}, State) when is_list(Data) -> @@ -370,8 +370,8 @@ websocket_info({check_gc, Stats}, State) -> return(check_oom(run_gc(Stats, State))); websocket_info(Deliver = {deliver, _Topic, _Msg}, - State = #state{zone = Zone, listener = Listener}) -> - ActiveN = emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]), + State = #state{listener = {Type, Listener}}) -> + ActiveN = get_active_n(Type, Listener), Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); @@ -558,12 +558,12 @@ parse_incoming(Data, State = #state{parse_state = ParseState}) -> %% Handle incoming packet %%-------------------------------------------------------------------- -handle_incoming(Packet, State = #state{zone = Zone, listener = Listener}) +handle_incoming(Packet, State = #state{listener = {Type, Listener}}) when is_record(Packet, mqtt_packet) -> ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), ok = inc_incoming_stats(Packet), NState = case emqx_pd:get_counter(incoming_pubs) > - emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of + get_active_n(Type, Listener) of true -> postpone({cast, rate_limit}, State); false -> State end, @@ -595,12 +595,12 @@ with_channel(Fun, Args, State = #state{channel = Channel}) -> %%-------------------------------------------------------------------- handle_outgoing(Packets, State = #state{mqtt_piggyback = MQTTPiggyback, - zone = Zone, listener = Listener}) -> + listener = {Type, Listener}}) -> IoData = lists:map(serialize_and_inc_stats_fun(State), Packets), Oct = iolist_size(IoData), ok = inc_sent_stats(length(Packets), Oct), NState = case emqx_pd:get_counter(outgoing_pubs) > - emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of + get_active_n(Type, Listener) of true -> Stats = #{cnt => emqx_pd:reset_counter(outgoing_pubs), oct => emqx_pd:reset_counter(outgoing_bytes) @@ -749,10 +749,10 @@ classify([Event|More], Packets, Cmds, Events) -> trigger(Event) -> erlang:send(self(), Event). -get_peer(Req, #{zone := Zone, listener := Listener}) -> +get_peer(Req, #{listener := {Type, Listener}}) -> {PeerAddr, PeerPort} = cowboy_req:peer(Req), AddrHeader = cowboy_req:header( - get_ws_opts(Zone, Listener, proxy_address_header), Req, <<>>), + get_ws_opts(Type, Listener, proxy_address_header), Req, <<>>), ClientAddr = case string:tokens(binary_to_list(AddrHeader), ", ") of [] -> undefined; @@ -766,7 +766,7 @@ get_peer(Req, #{zone := Zone, listener := Listener}) -> PeerAddr end, PortHeader = cowboy_req:header( - get_ws_opts(Zone, Listener, proxy_port_header), Req, <<>>), + get_ws_opts(Type, Listener, proxy_port_header), Req, <<>>), ClientPort = case string:tokens(binary_to_list(PortHeader), ", ") of [] -> undefined; @@ -787,5 +787,8 @@ set_field(Name, Value, State) -> Pos = emqx_misc:index_of(Name, record_info(fields, state)), setelement(Pos+1, State, Value). -get_ws_opts(Zone, Listener, Key) -> - emqx_config:get_listener_conf(Zone, Listener, [websocket, Key]). +get_ws_opts(Type, Listener, Key) -> + emqx_config:get_listener_conf(Type, Listener, [websocket, Key]). + +get_active_n(Type, Listener) -> + emqx_config:get_listener_conf(Type, Listener, [tcp, active_n]). \ No newline at end of file diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl b/apps/emqx/src/emqx_zone_schema.erl similarity index 59% rename from apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl rename to apps/emqx/src/emqx_zone_schema.erl index 4c2fde6dd..013ffb22f 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl +++ b/apps/emqx/src/emqx_zone_schema.erl @@ -14,25 +14,21 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_stub_conn). +-module(emqx_zone_schema). --export([ start/1 - , send/2 - , stop/1 - ]). +-export([namespace/0, roots/0, fields/1]). --type ack_ref() :: emqx_bridge_worker:ack_ref(). --type batch() :: emqx_bridge_worker:batch(). +namespace() -> zone. -start(#{client_pid := Pid} = Cfg) -> - Pid ! {self(), ?MODULE, ready}, - {ok, Cfg}. +roots() -> []. -stop(_) -> ok. +%% zone schemas are clones from the same name from root level +%% only not allowed to have default values. +fields(Name) -> + [{N, no_default(Sc)} || {N, Sc} <- emqx_schema:fields(Name)]. -%% @doc Callback for `emqx_bridge_connect' behaviour --spec send(_, batch()) -> {ok, ack_ref()} | {error, any()}. -send(#{client_pid := Pid}, Batch) -> - Ref = make_ref(), - Pid ! {stub_message, self(), Ref, Batch}, - {ok, Ref}. +%% no default values for zone settings +no_default(Sc) -> + fun(default) -> undefined; + (Other) -> hocon_schema:field_schema(Sc, Other) + end. diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index d459d28b2..00a1f9fbe 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -33,7 +33,7 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). t_authenticate(_) -> - ?assertMatch(ok, emqx_access_control:authenticate(clientinfo())). + ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). t_authorize(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), @@ -46,7 +46,7 @@ t_authorize(_) -> clientinfo() -> clientinfo(#{}). clientinfo(InitProps) -> maps:merge(#{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, diff --git a/apps/emqx/test/emqx_alarm_SUITE.erl b/apps/emqx/test/emqx_alarm_SUITE.erl index 1157f94bc..605300d2f 100644 --- a/apps/emqx/test/emqx_alarm_SUITE.erl +++ b/apps/emqx/test/emqx_alarm_SUITE.erl @@ -28,14 +28,14 @@ all() -> emqx_ct:all(?MODULE). init_per_testcase(t_size_limit, Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - emqx_config:update([alarm], #{ + {ok, _} = emqx:update_config([alarm], #{ <<"size_limit">> => 2 }), Config; init_per_testcase(t_validity_period, Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - emqx_config:update([alarm], #{ + {ok, _} = emqx:update_config([alarm], #{ <<"validity_period">> => <<"1s">> }), Config; diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl new file mode 100644 index 000000000..aa4d55fee --- /dev/null +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -0,0 +1,238 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authentication_SUITE). + +-behaviour(hocon_schema). +-behaviour(emqx_authentication). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-export([ fields/1 ]). + +-export([ refs/0 + , create/1 + , update/2 + , authenticate/2 + , destroy/1 + ]). + +-define(AUTHN, emqx_authentication). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +fields(type1) -> + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['built-in-database']}} + , {enable, fun enable/1} + ]; + +fields(type2) -> + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['mysql']}} + , {enable, fun enable/1} + ]. + +enable(type) -> boolean(); +enable(default) -> true; +enable(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% Callbacks +%%------------------------------------------------------------------------------ + +refs() -> + [ hoconsc:ref(?MODULE, type1) + , hoconsc:ref(?MODULE, type2) + ]. + +create(_Config) -> + {ok, #{mark => 1}}. + +update(_Config, _State) -> + {ok, #{mark => 2}}. + +authenticate(#{username := <<"good">>}, _State) -> + {ok, #{is_superuser => true}}; +authenticate(#{username := _}, _State) -> + {error, bad_username_or_password}. + +destroy(_State) -> + ok. + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + application:set_env(ekka, strict_mode, true), + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([]), + ok. + +t_chain(_) -> + % CRUD of authentication chain + ChainName = 'test', + ?assertMatch({ok, []}, ?AUTHN:list_chains()), + ?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:create_chain(ChainName)), + ?assertEqual({error, {already_exists, {chain, ChainName}}}, ?AUTHN:create_chain(ChainName)), + ?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:lookup_chain(ChainName)), + ?assertMatch({ok, [#{name := ChainName}]}, ?AUTHN:list_chains()), + ?assertEqual(ok, ?AUTHN:delete_chain(ChainName)), + ?assertMatch({error, {not_found, {chain, ChainName}}}, ?AUTHN:lookup_chain(ChainName)), + ok. + +t_authenticator(_) -> + ChainName = 'test', + AuthenticatorConfig1 = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + + % Create an authenticator when the authentication chain does not exist + ?assertEqual({error, {not_found, {chain, ChainName}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?AUTHN:create_chain(ChainName), + % Create an authenticator when the provider does not exist + ?assertEqual({error, no_available_provider}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + + AuthNType1 = {'password-based', 'built-in-database'}, + ?AUTHN:add_provider(AuthNType1, ?MODULE), + ID1 = <<"password-based:built-in-database">>, + + % CRUD of authencaticator + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID1}}, ?AUTHN:lookup_authenticator(ChainName, ID1)), + ?assertMatch({ok, [#{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual({error, {already_exists, {authenticator, ID1}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)), + ?assertEqual(ok, ?AUTHN:delete_authenticator(ChainName, ID1)), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)), + ?assertMatch({ok, []}, ?AUTHN:list_authenticators(ChainName)), + + % Multiple authenticators exist at the same time + AuthNType2 = {'password-based', mysql}, + ?AUTHN:add_provider(AuthNType2, ?MODULE), + ID2 = <<"password-based:mysql">>, + AuthenticatorConfig2 = #{mechanism => 'password-based', + backend => mysql, + enable => true}, + ?assertMatch({ok, #{id := ID1}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID2}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig2)), + + % Move authenticator + ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, top)), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, bottom)), + ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, {before, ID1})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + + ?AUTHN:delete_chain(ChainName), + ?AUTHN:remove_provider(AuthNType1), + ?AUTHN:remove_provider(AuthNType2), + ok. + +t_authenticate(_) -> + ListenerID = 'tcp:default', + ClientInfo = #{zone => default, + listener => ListenerID, + protocol => mqtt, + username => <<"good">>, + password => <<"any">>}, + ?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)), + + AuthNType = {'password-based', 'built-in-database'}, + ?AUTHN:add_provider(AuthNType, ?MODULE), + + AuthenticatorConfig = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + ?AUTHN:create_chain(ListenerID), + ?assertMatch({ok, _}, ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig)), + ?assertEqual({ok, #{is_superuser => true}}, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo#{username => <<"bad">>})), + + ?AUTHN:delete_chain(ListenerID), + ?AUTHN:remove_provider(AuthNType), + ok. + +t_update_config(_) -> + emqx_config_handler:add_handler([authentication], emqx_authentication), + + AuthNType1 = {'password-based', 'built-in-database'}, + AuthNType2 = {'password-based', mysql}, + ?AUTHN:add_provider(AuthNType1, ?MODULE), + ?AUTHN:add_provider(AuthNType2, ?MODULE), + + Global = 'mqtt:global', + AuthenticatorConfig1 = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + AuthenticatorConfig2 = #{mechanism => 'password-based', + backend => mysql, + enable => true}, + ID1 = <<"password-based:built-in-database">>, + ID2 = <<"password-based:mysql">>, + + ?assertMatch({ok, []}, ?AUTHN:list_chains()), + ?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig1})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig2})), + ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID2)), + + ?assertMatch({ok, _}, update_config([authentication], {update_authenticator, Global, ID1, #{}})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ?assertMatch({ok, _}, update_config([authentication], {move_authenticator, Global, ID2, <<"top">>})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(Global)), + + ?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID1})), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ListenerID = 'tcp:default', + ConfKeyPath = [listeners, tcp, default, authentication], + ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig1})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig2})), + ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID2)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {update_authenticator, ListenerID, ID1, #{}})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, <<"top">>})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ListenerID)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {delete_authenticator, ListenerID, ID1})), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?AUTHN:delete_chain(Global), + ?AUTHN:remove_provider(AuthNType1), + ?AUTHN:remove_provider(AuthNType2), + ok. + +update_config(Path, ConfigRequest) -> + emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). diff --git a/apps/emqx/test/emqx_authz_cache_SUITE.erl b/apps/emqx/test/emqx_authz_cache_SUITE.erl index 849997298..46a4d7d74 100644 --- a/apps/emqx/test/emqx_authz_cache_SUITE.erl +++ b/apps/emqx/test/emqx_authz_cache_SUITE.erl @@ -26,7 +26,6 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - toggle_authz(true), Config. end_per_suite(_Config) -> @@ -78,6 +77,3 @@ t_drain_authz_cache(_) -> {ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0), ?assert(length(gen_server:call(ClientPid, list_authz_cache)) > 0), emqtt:stop(Client). - -toggle_authz(Bool) when is_boolean(Bool) -> - emqx_config:put_zone_conf(default, [authorization, enable], Bool). diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index be7c94ede..775b40ee8 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -27,149 +27,112 @@ all() -> emqx_ct:all(?MODULE). +force_gc_conf() -> + #{bytes => 16777216,count => 16000,enable => true}. + +force_shutdown_conf() -> + #{enable => true,max_heap_size => 4194304, max_message_queue_len => 1000}. + +rate_limit_conf() -> + #{conn_bytes_in => ["100KB","10s"], + conn_messages_in => ["100","10s"], + max_conn_rate => 1000, + quota => + #{conn_messages_routing => infinity, + overall_messages_routing => infinity}}. + +rpc_conf() -> + #{async_batch_size => 256,authentication_timeout => 5000, + call_receive_timeout => 15000,connect_timeout => 5000, + mode => async,port_discovery => stateless, + send_timeout => 5000,socket_buffer => 1048576, + socket_keepalive_count => 9,socket_keepalive_idle => 900, + socket_keepalive_interval => 75,socket_recbuf => 1048576, + socket_sndbuf => 1048576,tcp_client_num => 1, + tcp_server_port => 5369}. + mqtt_conf() -> - #{await_rel_timeout => 300000, - idle_timeout => 15000, - ignore_loop_deliver => false, - keepalive_backoff => 0.75, - max_awaiting_rel => 100, - max_clientid_len => 65535, - max_inflight => 32, - max_mqueue_len => 1000, - max_packet_size => 1048576, - max_qos_allowed => 2, - max_subscriptions => infinity, - max_topic_alias => 65535, - max_topic_levels => 65535, - mountpoint => <<>>, - mqueue_default_priority => lowest, - mqueue_priorities => #{}, - mqueue_store_qos0 => true, - peer_cert_as_clientid => disabled, - peer_cert_as_username => disabled, - response_information => [], - retain_available => true, - retry_interval => 30000, - server_keepalive => disabled, - session_expiry_interval => 7200000, - shared_subscription => true, - strict_mode => false, - upgrade_qos => false, - use_username_as_clientid => false, - wildcard_subscription => true}. + #{await_rel_timeout => 300000,idle_timeout => 15000, + ignore_loop_deliver => false,keepalive_backoff => 0.75, + max_awaiting_rel => 100,max_clientid_len => 65535, + max_inflight => 32,max_mqueue_len => 1000, + max_packet_size => 1048576,max_qos_allowed => 2, + max_subscriptions => infinity,max_topic_alias => 65535, + max_topic_levels => 65535,mqueue_default_priority => lowest, + mqueue_priorities => disabled,mqueue_store_qos0 => true, + peer_cert_as_clientid => disabled, + peer_cert_as_username => disabled, + response_information => [],retain_available => true, + retry_interval => 30000,server_keepalive => disabled, + session_expiry_interval => 7200000, + shared_subscription => true,strict_mode => false, + upgrade_qos => false,use_username_as_clientid => false, + wildcard_subscription => true}. + listener_mqtt_tcp_conf() -> #{acceptors => 16, - access_rules => ["allow all"], - bind => {{0,0,0,0},1883}, - max_connections => 1024000, - proxy_protocol => false, - proxy_protocol_timeout => 3000, - rate_limit => - #{conn_bytes_in => - ["100KB","10s"], - conn_messages_in => - ["100","10s"], - max_conn_rate => 1000, - quota => - #{conn_messages_routing => infinity, - overall_messages_routing => infinity}}, - tcp => - #{active_n => 100, - backlog => 1024, - buffer => 4096, - high_watermark => 1048576, - send_timeout => 15000, - send_timeout_close => - true}, - type => tcp}. + zone => default, + access_rules => ["allow all"], + bind => {{0,0,0,0},1883}, + max_connections => 1024000,mountpoint => <<>>, + proxy_protocol => false,proxy_protocol_timeout => 3000, + tcp => #{ + active_n => 100,backlog => 1024,buffer => 4096, + high_watermark => 1048576,nodelay => false, + reuseaddr => true,send_timeout => 15000, + send_timeout_close => true}}. listener_mqtt_ws_conf() -> #{acceptors => 16, - access_rules => ["allow all"], - bind => {{0,0,0,0},8083}, - max_connections => 1024000, - proxy_protocol => false, - proxy_protocol_timeout => 3000, - rate_limit => - #{conn_bytes_in => - ["100KB","10s"], - conn_messages_in => - ["100","10s"], - max_conn_rate => 1000, - quota => - #{conn_messages_routing => infinity, - overall_messages_routing => infinity}}, - tcp => - #{active_n => 100, - backlog => 1024, - buffer => 4096, - high_watermark => 1048576, - send_timeout => 15000, - send_timeout_close => - true}, - type => ws, - websocket => - #{allow_origin_absence => - true, - check_origin_enable => - false, - check_origins => [], - compress => false, - deflate_opts => - #{client_max_window_bits => - 15, - mem_level => 8, - server_max_window_bits => - 15}, - fail_if_no_subprotocol => - true, - idle_timeout => 86400000, - max_frame_size => infinity, - mqtt_path => "/mqtt", - mqtt_piggyback => multiple, - proxy_address_header => - "x-forwarded-for", - proxy_port_header => - "x-forwarded-port", - supported_subprotocols => - ["mqtt","mqtt-v3", - "mqtt-v3.1.1", - "mqtt-v5"]}}. + zone => default, + access_rules => ["allow all"], + bind => {{0,0,0,0},8083}, + max_connections => 1024000,mountpoint => <<>>, + proxy_protocol => false,proxy_protocol_timeout => 3000, + tcp => + #{active_n => 100,backlog => 1024,buffer => 4096, + high_watermark => 1048576,nodelay => false, + reuseaddr => true,send_timeout => 15000, + send_timeout_close => true}, + websocket => + #{allow_origin_absence => true,check_origin_enable => false, + check_origins => [],compress => false, + deflate_opts => + #{client_max_window_bits => 15,mem_level => 8, + server_max_window_bits => 15}, + fail_if_no_subprotocol => true,idle_timeout => 86400000, + max_frame_size => infinity,mqtt_path => "/mqtt", + mqtt_piggyback => multiple, + proxy_address_header => "x-forwarded-for", + proxy_port_header => "x-forwarded-port", + supported_subprotocols => + ["mqtt","mqtt-v3","mqtt-v3.1.1","mqtt-v5"]}}. -default_zone_conf() -> - #{zones => - #{default => - #{ authorization => #{ - cache => #{enable => true,max_size => 32, ttl => 60000}, - deny_action => ignore, - enable => false - }, - auth => #{enable => false}, - overall_max_connections => infinity, - stats => #{enable => true}, - conn_congestion => - #{enable_alarm => true, min_alarm_sustain_duration => 60000}, - flapping_detect => - #{ban_time => 300000,enable => false, - max_count => 15,window_time => 60000}, - force_gc => - #{bytes => 16777216,count => 16000, - enable => true}, - force_shutdown => - #{enable => true, - max_heap_size => 4194304, - max_message_queue_len => 1000}, - mqtt => mqtt_conf(), - listeners => - #{mqtt_tcp => listener_mqtt_tcp_conf(), - mqtt_ws => listener_mqtt_ws_conf()} - } - } +listeners_conf() -> + #{tcp => #{default => listener_mqtt_tcp_conf()}, + ws => #{default => listener_mqtt_ws_conf()} }. -set_default_zone_conf() -> - emqx_config:put(default_zone_conf()). +stats_conf() -> + #{enable => true}. + +zone_conf() -> + #{}. + +basic_conf() -> + #{rate_limit => rate_limit_conf(), + force_gc => force_gc_conf(), + force_shutdown => force_shutdown_conf(), + mqtt => mqtt_conf(), + rpc => rpc_conf(), + stats => stats_conf(), + listeners => listeners_conf(), + zones => zone_conf() + }. + +set_test_listenser_confs() -> + emqx_config:put(basic_conf()). %%-------------------------------------------------------------------- %% CT Callbacks @@ -181,7 +144,7 @@ init_per_suite(Config) -> %% Access Control Meck ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:expect(emqx_access_control, authenticate, - fun(_) -> ok end), + fun(_) -> {ok, #{is_superuser => false}} end), ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), @@ -211,7 +174,7 @@ end_per_suite(_Config) -> ]). init_per_testcase(_TestCase, Config) -> - set_default_zone_conf(), + set_test_listenser_confs(), Config. end_per_testcase(_TestCase, Config) -> @@ -917,7 +880,7 @@ t_ws_cookie_init(_) -> conn_mod => emqx_ws_connection, ws_cookie => WsCookie }, - Channel = emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), + Channel = emqx_channel:init(ConnInfo, #{zone => default, listener => {tcp, default}}), ?assertMatch(#{ws_cookie := WsCookie}, emqx_channel:info(clientinfo, Channel)). %%-------------------------------------------------------------------- @@ -942,7 +905,7 @@ channel(InitFields) -> maps:fold(fun(Field, Value, Channel) -> emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), + emqx_channel:init(ConnInfo, #{zone => default, listener => {tcp, default}}), maps:merge(#{clientinfo => clientinfo(), session => session(), conn_state => connected @@ -951,7 +914,7 @@ channel(InitFields) -> clientinfo() -> clientinfo(#{}). clientinfo(InitProps) -> maps:merge(#{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index c6a450471..0a3a050ac 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -79,8 +79,8 @@ groups() -> init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - emqx_config:put_listener_conf(default, mqtt_ssl, [ssl, verify], verify_peer), - emqx_listeners:restart_listener('default:mqtt_ssl'), + emqx_config:put_listener_conf(ssl, default, [ssl, verify], verify_peer), + emqx_listeners:restart_listener('ssl:default'), Config. end_per_suite(_Config) -> @@ -114,8 +114,8 @@ t_cm(_) -> emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 15000). t_cm_registry(_) -> - Info = supervisor:which_children(emqx_cm_sup), - {_, Pid, _, _} = lists:keyfind(registry, 1, Info), + Children = supervisor:which_children(emqx_cm_sup), + {_, Pid, _, _} = lists:keyfind(emqx_cm_registry, 1, Children), ignored = gen_server:call(Pid, <<"Unexpected call">>), gen_server:cast(Pid, <<"Unexpected cast">>), Pid ! <<"Unexpected info">>. diff --git a/apps/emqx/test/emqx_cm_SUITE.erl b/apps/emqx/test/emqx_cm_SUITE.erl index 75d0a899c..d492edd0e 100644 --- a/apps/emqx/test/emqx_cm_SUITE.erl +++ b/apps/emqx/test/emqx_cm_SUITE.erl @@ -89,7 +89,7 @@ t_open_session(_) -> ok = meck:expect(emqx_connection, call, fun(_, _) -> ok end), ok = meck:expect(emqx_connection, call, fun(_, _, _) -> ok end), - ClientInfo = #{zone => default, listener => mqtt_tcp, + ClientInfo = #{zone => default, listener => {tcp, default}, clientid => <<"clientid">>, username => <<"username">>, peerhost => {127,0,0,1}}, @@ -114,7 +114,7 @@ rand_client_id() -> t_open_session_race_condition(_) -> ClientId = rand_client_id(), - ClientInfo = #{zone => default, listener => mqtt_tcp, + ClientInfo = #{zone => default, listener => {tcp, default}, clientid => ClientId, username => <<"username">>, peerhost => {127,0,0,1}}, diff --git a/apps/emqx/test/emqx_config_SUITE.erl b/apps/emqx/test/emqx_config_SUITE.erl new file mode 100644 index 000000000..50d575c0e --- /dev/null +++ b/apps/emqx/test/emqx_config_SUITE.erl @@ -0,0 +1,50 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_config_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:boot_modules(all), + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([]). + +t_fill_default_values(_) -> + Conf = #{ + <<"broker">> => #{ + <<"perf">> => #{}, + <<"route_batch_clean">> => false} + }, + ?assertMatch(#{<<"broker">> := + #{<<"enable_session_registry">> := true, + <<"perf">> := + #{<<"route_lock_type">> := key, + <<"trie_compaction">> := true}, + <<"route_batch_clean">> := false, + <<"session_locking_strategy">> := quorum, + <<"shared_dispatch_ack_enabled">> := false, + <<"shared_subscription_strategy">> := round_robin, + <<"sys_heartbeat_interval">> := "30s", + <<"sys_msg_interval">> := "1m"}}, + emqx_config:fill_defaults(Conf)). diff --git a/apps/emqx/test/emqx_connection_SUITE.erl b/apps/emqx/test/emqx_connection_SUITE.erl index 0d5114325..5784ad6aa 100644 --- a/apps/emqx/test/emqx_connection_SUITE.erl +++ b/apps/emqx/test/emqx_connection_SUITE.erl @@ -57,7 +57,7 @@ init_per_suite(Config) -> ok = meck:expect(emqx_alarm, deactivate, fun(_) -> ok end), ok = meck:expect(emqx_alarm, deactivate, fun(_, _) -> ok end), - emqx_channel_SUITE:set_default_zone_conf(), + emqx_channel_SUITE:set_test_listenser_confs(), Config. end_per_suite(_Config) -> @@ -219,9 +219,9 @@ t_handle_msg_deliver(_) -> t_handle_msg_inet_reply(_) -> ok = meck:expect(emqx_pd, get_counter, fun(_) -> 10 end), - emqx_config:put_listener_conf(default, mqtt_tcp, [tcp, active_n], 0), + emqx_config:put_listener_conf(tcp, default, [tcp, active_n], 0), ?assertMatch({ok, _St}, handle_msg({inet_reply, for_testing, ok}, st())), - emqx_config:put_listener_conf(default, mqtt_tcp, [tcp, active_n], 100), + emqx_config:put_listener_conf(tcp, default, [tcp, active_n], 100), ?assertEqual(ok, handle_msg({inet_reply, for_testing, ok}, st())), ?assertMatch({stop, {shutdown, for_testing}, _St}, handle_msg({inet_reply, for_testing, {error, for_testing}}, st())). @@ -456,7 +456,7 @@ with_conn(TestFun, Opts) when is_map(Opts) -> TrapExit = maps:get(trap_exit, Opts, false), process_flag(trap_exit, TrapExit), {ok, CPid} = emqx_connection:start_link(emqx_transport, sock, - maps:merge(Opts, #{zone => default, listener => mqtt_tcp})), + maps:merge(Opts, #{zone => default, listener => {tcp, default}})), TestFun(CPid), TrapExit orelse emqx_connection:stop(CPid), ok. @@ -479,7 +479,7 @@ st(InitFields) when is_map(InitFields) -> st(InitFields, #{}). st(InitFields, ChannelFields) when is_map(InitFields) -> St = emqx_connection:init_state(emqx_transport, sock, #{zone => default, - listener => mqtt_tcp}), + listener => {tcp, default}}), maps:fold(fun(N, V, S) -> emqx_connection:set_field(N, V, S) end, emqx_connection:set_field(channel, channel(ChannelFields), St), InitFields @@ -500,7 +500,7 @@ channel(InitFields) -> expiry_interval => 0 }, ClientInfo = #{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, @@ -513,7 +513,7 @@ channel(InitFields) -> maps:fold(fun(Field, Value, Channel) -> emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), + emqx_channel:init(ConnInfo, #{zone => default, listener => {tcp, default}}), maps:merge(#{clientinfo => ClientInfo, session => Session, conn_state => connected diff --git a/apps/emqx/test/emqx_flapping_SUITE.erl b/apps/emqx/test/emqx_flapping_SUITE.erl index eca276b84..a8e783c49 100644 --- a/apps/emqx/test/emqx_flapping_SUITE.erl +++ b/apps/emqx/test/emqx_flapping_SUITE.erl @@ -40,7 +40,7 @@ end_per_suite(_Config) -> t_detect_check(_) -> ClientInfo = #{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, clientid => <<"client007">>, peerhost => {127,0,0,1} }, @@ -55,8 +55,8 @@ t_detect_check(_) -> true = emqx_banned:check(ClientInfo), timer:sleep(3000), false = emqx_banned:check(ClientInfo), - Childrens = supervisor:which_children(emqx_cm_sup), - {flapping, Pid, _, _} = lists:keyfind(flapping, 1, Childrens), + Children = supervisor:which_children(emqx_cm_sup), + {emqx_flapping, Pid, _, _} = lists:keyfind(emqx_flapping, 1, Children), gen_server:call(Pid, unexpected_msg), gen_server:cast(Pid, unexpected_msg), Pid ! test, @@ -64,7 +64,7 @@ t_detect_check(_) -> t_expired_detecting(_) -> ClientInfo = #{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, clientid => <<"client008">>, peerhost => {127,0,0,1}}, false = emqx_flapping:detect(ClientInfo), @@ -72,4 +72,4 @@ t_expired_detecting(_) -> (_) -> false end, ets:tab2list(emqx_flapping))), timer:sleep(200), ?assertEqual(true, lists:all(fun({flapping, <<"client008">>, _, _, _}) -> false; - (_) -> true end, ets:tab2list(emqx_flapping))). \ No newline at end of file + (_) -> true end, ets:tab2list(emqx_flapping))). diff --git a/apps/emqx/test/emqx_listeners_SUITE.erl b/apps/emqx/test/emqx_listeners_SUITE.erl index a8760c7e8..a3bfb2d47 100644 --- a/apps/emqx/test/emqx_listeners_SUITE.erl +++ b/apps/emqx/test/emqx_listeners_SUITE.erl @@ -37,6 +37,14 @@ end_per_suite(_Config) -> application:stop(esockd), application:stop(cowboy). +init_per_testcase(_, Config) -> + {ok, _} = emqx_config_handler:start_link(), + Config. + +end_per_testcase(_, _Config) -> + _ = emqx_config_handler:stop(), + ok. + t_start_stop_listeners(_) -> ok = emqx_listeners:start(), ?assertException(error, _, emqx_listeners:start_listener({ws,{"127.0.0.1", 8083}, []})), diff --git a/apps/emqx/test/emqx_mqtt_caps_SUITE.erl b/apps/emqx/test/emqx_mqtt_caps_SUITE.erl index c01420f49..f8b5a7ab6 100644 --- a/apps/emqx/test/emqx_mqtt_caps_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_caps_SUITE.erl @@ -25,7 +25,7 @@ all() -> emqx_ct:all(?MODULE). t_check_pub(_) -> - OldConf = emqx_config:get([zones]), + OldConf = emqx:get_config([zones]), emqx_config:put_zone_conf(default, [mqtt, max_qos_allowed], ?QOS_1), emqx_config:put_zone_conf(default, [mqtt, retain_available], false), timer:sleep(50), @@ -39,7 +39,7 @@ t_check_pub(_) -> emqx_config:put([zones], OldConf). t_check_sub(_) -> - OldConf = emqx_config:get([zones]), + OldConf = emqx:get_config([zones]), SubOpts = #{rh => 0, rap => 0, nl => 0, diff --git a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf index e1f7bc5b9..991afdfdd 100644 --- a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf +++ b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf @@ -1,3 +1,3 @@ -emqx_hocon_plugin: { - name: test +emqx_hocon_plugin { + name = test } diff --git a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config index 888a03bc4..57bf1245c 100644 --- a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config +++ b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config @@ -17,7 +17,7 @@ {profiles, [{test, [ - {deps, [ {emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.1.4"}}} + {deps, [{emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {branch,"hocon"}}} ]} ]} ]}. diff --git a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl index fab74b5e8..8e333a3e0 100644 --- a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl +++ b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl @@ -2,11 +2,11 @@ -include_lib("typerefl/include/types.hrl"). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -behaviour(hocon_schema). -structs() -> ["emqx_hocon_plugin"]. +roots() -> ["emqx_hocon_plugin"]. fields("emqx_hocon_plugin") -> [{name, fun name/1}]. diff --git a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config index 4c49da1dc..b2bf39c55 100644 --- a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config +++ b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config @@ -17,7 +17,7 @@ {profiles, [{test, [ - {deps, [ {emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.1.4"}}} + {deps, [{emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {branch,"hocon"}}} ]} ]} ]}. diff --git a/apps/emqx/test/emqx_session_SUITE.erl b/apps/emqx/test/emqx_session_SUITE.erl index 1aa5b0196..f52dacc14 100644 --- a/apps/emqx/test/emqx_session_SUITE.erl +++ b/apps/emqx/test/emqx_session_SUITE.erl @@ -29,7 +29,7 @@ all() -> emqx_ct:all(?MODULE). %%-------------------------------------------------------------------- init_per_suite(Config) -> - emqx_channel_SUITE:set_default_zone_conf(), + emqx_channel_SUITE:set_test_listenser_confs(), ok = meck:new([emqx_hooks, emqx_metrics, emqx_broker], [passthrough, no_history, no_link]), ok = meck:expect(emqx_metrics, inc, fun(_) -> ok end), diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index b25f051eb..767a7994e 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -48,7 +48,7 @@ init_per_testcase(TestCase, Config) when TestCase =/= t_ws_pingreq_before_connected, TestCase =/= t_ws_non_check_origin -> - emqx_channel_SUITE:set_default_zone_conf(), + emqx_channel_SUITE:set_test_listenser_confs(), %% Mock cowboy_req ok = meck:new(cowboy_req, [passthrough, no_history, no_link]), ok = meck:expect(cowboy_req, header, fun(_, _, _) -> <<>> end), @@ -119,7 +119,7 @@ t_info(_) -> } = SockInfo. set_ws_opts(Key, Val) -> - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, Key], Val). + emqx_config:put_listener_conf(ws, default, [websocket, Key], Val). t_header(_) -> ok = meck:expect(cowboy_req, header, @@ -127,7 +127,7 @@ t_header(_) -> (<<"x-forwarded-port">>, _, _) -> <<"1000">> end), set_ws_opts(proxy_address_header, <<"x-forwarded-for">>), set_ws_opts(proxy_port_header, <<"x-forwarded-port">>), - {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => mqtt_ws}]), + {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => {ws, default}}]), WsPid = spawn(fun() -> receive {call, From, info} -> gen_server:reply(From, ?ws_conn:info(St)) @@ -222,8 +222,8 @@ t_ws_sub_protocols_mqtt_equivalents(_) -> start_ws_client(#{protocols => [<<"not-mqtt">>]})). t_ws_check_origin(_) -> - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origin_enable], true), - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origins], + emqx_config:put_listener_conf(ws, default, [websocket, check_origin_enable], true), + emqx_config:put_listener_conf(ws, default, [websocket, check_origins], [<<"http://localhost:18083">>]), {ok, _} = application:ensure_all_started(gun), ?assertMatch({gun_upgrade, _}, @@ -234,8 +234,8 @@ t_ws_check_origin(_) -> headers => [{<<"origin">>, <<"http://localhost:18080">>}]})). t_ws_non_check_origin(_) -> - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origin_enable], false), - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origins], []), + emqx_config:put_listener_conf(ws, default, [websocket, check_origin_enable], false), + emqx_config:put_listener_conf(ws, default, [websocket, check_origins], []), {ok, _} = application:ensure_all_started(gun), ?assertMatch({gun_upgrade, _}, start_ws_client(#{protocols => [<<"mqtt">>], @@ -245,7 +245,7 @@ t_ws_non_check_origin(_) -> headers => [{<<"origin">>, <<"http://localhost:18080">>}]})). t_init(_) -> - Opts = #{listener => mqtt_ws, zone => default}, + Opts = #{listener => {ws, default}, zone => default}, ok = meck:expect(cowboy_req, parse_header, fun(_, req) -> undefined end), ok = meck:expect(cowboy_req, reply, fun(_, Req) -> Req end), {ok, req, _} = ?ws_conn:init(req, Opts), @@ -438,7 +438,7 @@ t_shutdown(_) -> st() -> st(#{}). st(InitFields) when is_map(InitFields) -> - {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => mqtt_ws}]), + {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => {ws, default}}]), maps:fold(fun(N, V, S) -> ?ws_conn:set_field(N, V, S) end, ?ws_conn:set_field(channel, channel(), St), InitFields @@ -459,7 +459,7 @@ channel(InitFields) -> expiry_interval => 0 }, ClientInfo = #{zone => default, - listener => mqtt_ws, + listener => {ws, default}, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, @@ -472,7 +472,7 @@ channel(InitFields) -> maps:fold(fun(Field, Value, Channel) -> emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_ws}), + emqx_channel:init(ConnInfo, #{zone => default, listener => {ws, default}}), maps:merge(#{clientinfo => ClientInfo, session => Session, conn_state => connected diff --git a/apps/emqx_authn/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv index 2543d39ca..cbadaefbc 100644 --- a/apps/emqx_authn/data/user-credentials.csv +++ b/apps/emqx_authn/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt -myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 -myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 +user_id,password_hash,salt,is_superuser +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json index 169122bd2..94375df22 100644 --- a/apps/emqx_authn/data/user-credentials.json +++ b/apps/emqx_authn/data/user-credentials.json @@ -2,11 +2,13 @@ { "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", - "salt": "e378187547bf2d6f0545a3f441aa4d8a" + "salt": "e378187547bf2d6f0545a3f441aa4d8a", + "is_superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", - "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", + "is_superuser": false } ] diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index bc2036fea..d1d3d16f8 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,37 +1,6 @@ -authentication: { - enable: false - authenticators: [ - # { - # name: "authenticator1" - # mechanism: password-based - # server_type: built-in-database - # user_id_type: clientid - # }, - # { - # name: "authenticator2" - # mechanism: password-based - # server_type: mongodb - # server: "127.0.0.1:27017" - # database: mqtt - # collection: users - # selector: { - # username: "${mqtt-username}" - # } - # password_hash_field: password_hash - # salt_field: salt - # password_hash_algorithm: sha256 - # salt_position: prefix - # }, - # { - # name: "authenticator 3" - # mechanism: password-based - # server_type: redis - # server: "127.0.0.1:6379" - # password: "public" - # database: 0 - # query: "HMGET ${mqtt-username} password_hash salt" - # password_hash_algorithm: sha256 - # salt_position: prefix - # } - ] -} +# authentication: { +# mechanism: password-based +# backend: built-in-database +# user_id_type: clientid +# } + diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index f9ba7c3b5..5eef08012 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -15,25 +15,11 @@ %%-------------------------------------------------------------------- -define(APP, emqx_authn). --define(CHAIN, <<"mqtt">>). --define(VER_1, <<"1">>). --define(VER_2, <<"2">>). +-define(AUTHN, emqx_authentication). + +-define(GLOBAL, 'mqtt:global'). -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}"). --record(authenticator, - { id :: binary() - , name :: binary() - , provider :: module() - , config :: map() - , state :: map() - }). - --record(chain, - { id :: binary() - , authenticators :: [{binary(), binary(), #authenticator{}}] - , created_at :: integer() - }). - -define(AUTH_SHARD, emqx_authn_shard). diff --git a/apps/emqx_authn/rebar.config b/apps/emqx_authn/rebar.config index 32b5a43e0..73696b033 100644 --- a/apps/emqx_authn/rebar.config +++ b/apps/emqx_authn/rebar.config @@ -1,6 +1,4 @@ -{deps, [ - {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} -]}. +{deps, []}. {edoc_opts, [{preprocess, true}]}. {erl_opts, [warn_unused_vars, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 034e06b89..3ab05e6b0 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -15,427 +15,3 @@ %%-------------------------------------------------------------------- -module(emqx_authn). - --include("emqx_authn.hrl"). - --export([ enable/0 - , disable/0 - , is_enabled/0 - ]). - --export([authenticate/2]). - --export([ create_chain/1 - , delete_chain/1 - , lookup_chain/1 - , list_chains/0 - , create_authenticator/2 - , delete_authenticator/2 - , update_authenticator/3 - , update_or_create_authenticator/3 - , lookup_authenticator/2 - , list_authenticators/1 - , move_authenticator_to_the_nth/3 - ]). - --export([ import_users/3 - , add_user/3 - , delete_user/3 - , update_user/4 - , lookup_user/3 - , list_users/2 - ]). - --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - --define(CHAIN_TAB, emqx_authn_chain). - --rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). - -%%------------------------------------------------------------------------------ -%% Mnesia bootstrap -%%------------------------------------------------------------------------------ - -%% @doc Create or replicate tables. --spec(mnesia(boot) -> ok). -mnesia(boot) -> - %% Optimize storage - StoreProps = [{ets, [{read_concurrency, true}]}], - %% Chain table - ok = ekka_mnesia:create_table(?CHAIN_TAB, [ - {ram_copies, [node()]}, - {record_name, chain}, - {local_content, true}, - {attributes, record_info(fields, chain)}, - {storage_properties, StoreProps}]); - -mnesia(copy) -> - ok = ekka_mnesia:copy_table(?CHAIN_TAB, ram_copies). - -enable() -> - case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of - ok -> ok; - {error, already_exists} -> ok - end. - -disable() -> - emqx:unhook('client.authenticate', {?MODULE, authenticate, []}), - ok. - -is_enabled() -> - Callbacks = emqx_hooks:lookup('client.authenticate'), - lists:any(fun({callback, {?MODULE, authenticate, []}, _, _}) -> - true; - (_) -> - false - end, Callbacks). - -authenticate(Credential, _AuthResult) -> - case mnesia:dirty_read(?CHAIN_TAB, ?CHAIN) of - [#chain{authenticators = Authenticators}] -> - do_authenticate(Authenticators, Credential); - [] -> - {stop, {error, not_authorized}} - end. - -do_authenticate([], _) -> - {stop, {error, not_authorized}}; -do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | More], Credential) -> - case Provider:authenticate(Credential, State) of - ignore -> - do_authenticate(More, Credential); - Result -> - %% ok - %% {ok, AuthData} - %% {continue, AuthCache} - %% {continue, AuthData, AuthCache} - %% {error, Reason} - {stop, Result} - end. - -create_chain(#{id := ID}) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ID, write) of - [] -> - Chain = #chain{id = ID, - authenticators = [], - created_at = erlang:system_time(millisecond)}, - mnesia:write(?CHAIN_TAB, Chain, write), - {ok, serialize_chain(Chain)}; - [_ | _] -> - {error, {already_exists, {chain, ID}}} - end - end). - -delete_chain(ID) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ID, write) of - [] -> - {error, {not_found, {chain, ID}}}; - [#chain{authenticators = Authenticators}] -> - _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators], - mnesia:delete(?CHAIN_TAB, ID, write) - end - end). - -lookup_chain(ID) -> - case mnesia:dirty_read(?CHAIN_TAB, ID) of - [] -> - {error, {not_found, {chain, ID}}}; - [Chain] -> - {ok, serialize_chain(Chain)} - end. - -list_chains() -> - Chains = ets:tab2list(?CHAIN_TAB), - {ok, [serialize_chain(Chain) || Chain <- Chains]}. - -create_authenticator(ChainID, #{name := Name} = Config) -> - UpdateFun = - fun(Chain = #chain{authenticators = Authenticators}) -> - case lists:keymember(Name, 2, Authenticators) of - true -> - {error, name_has_be_used}; - false -> - AlreadyExist = fun(ID) -> - lists:keymember(ID, 1, Authenticators) - end, - AuthenticatorID = gen_id(AlreadyExist), - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}], - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end, - update_chain(ChainID, UpdateFun). - -delete_authenticator(ChainID, AuthenticatorID) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {value, {_, _, Authenticator}, NAuthenticators} -> - _ = do_delete_authenticator(Authenticator), - NChain = Chain#chain{authenticators = NAuthenticators}, - mnesia:write(?CHAIN_TAB, NChain, write) - end - end, - update_chain(ChainID, UpdateFun). - -update_authenticator(ChainID, AuthenticatorID, Config) -> - do_update_authenticator(ChainID, AuthenticatorID, Config, false). - -update_or_create_authenticator(ChainID, AuthenticatorID, Config) -> - do_update_authenticator(ChainID, AuthenticatorID, Config, true). - -do_update_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - case CreateWhenNotFound of - true -> - case lists:keymember(NewName, 2, Authenticators) of - true -> - {error, name_has_be_used}; - false -> - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}], - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end - end; - false -> - {error, {not_found, {authenticator, AuthenticatorID}}} - end; - {value, - {_, _, #authenticator{provider = Provider, - state = #{version := Version} = State} = Authenticator}, - Others} -> - case lists:keymember(NewName, 2, Others) of - true -> - {error, name_has_be_used}; - false -> - case (NewProvider = authenticator_provider(Config)) =:= Provider of - true -> - Unique = <>, - case Provider:update(Config#{'_unique' => Unique}, State) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - config = Config, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end; - false -> - Unique = <>, - case NewProvider:create(Config#{'_unique' => Unique}) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - provider = NewProvider, - config = Config, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), - _ = Provider:destroy(State), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end - end - end, - update_chain(ChainID, UpdateFun). - -lookup_authenticator(ChainID, AuthenticatorID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> - case lists:keyfind(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {_, _, Authenticator} -> - {ok, serialize_authenticator(Authenticator)} - end - end. - -list_authenticators(ChainID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> - {ok, serialize_authenticators(Authenticators)} - end. - -move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) of - {ok, NAuthenticators} -> - NChain = Chain#chain{authenticators = NAuthenticators}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). - -import_users(ChainID, AuthenticatorID, Filename) -> - call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]). - -add_user(ChainID, AuthenticatorID, UserInfo) -> - call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]). - -delete_user(ChainID, AuthenticatorID, UserID) -> - call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]). - -update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) -> - call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]). - -lookup_user(ChainID, AuthenticatorID, UserID) -> - call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]). - -list_users(ChainID, AuthenticatorID) -> - call_authenticator(ChainID, AuthenticatorID, list_users, []). - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -authenticator_provider(#{mechanism := 'password-based', server_type := 'built-in-database'}) -> - emqx_authn_mnesia; -authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'}) -> - emqx_authn_mysql; -authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) -> - emqx_authn_pgsql; -authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) -> - emqx_authn_mongodb; -authenticator_provider(#{mechanism := 'password-based', server_type := 'redis'}) -> - emqx_authn_redis; -authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) -> - emqx_authn_http; -authenticator_provider(#{mechanism := jwt}) -> - emqx_authn_jwt; -authenticator_provider(#{mechanism := scram, server_type := 'built-in-database'}) -> - emqx_enhanced_authn_scram_mnesia. - -gen_id(AlreadyExist) -> - ID = list_to_binary(emqx_rule_id:gen()), - case AlreadyExist(ID) of - true -> gen_id(AlreadyExist); - false -> ID - end. - -switch_version(State = #{version := ?VER_1}) -> - State#{version := ?VER_2}; -switch_version(State = #{version := ?VER_2}) -> - State#{version := ?VER_1}; -switch_version(State) -> - State#{version => ?VER_1}. - -do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> - Provider = authenticator_provider(Config), - Unique = <>, - case Provider:create(Config#{'_unique' => Unique}) of - {ok, State} -> - Authenticator = #authenticator{id = AuthenticatorID, - name = Name, - provider = Provider, - config = Config, - state = switch_version(State)}, - {ok, Authenticator}; - {error, Reason} -> - {error, Reason} - end. - -do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> - _ = Provider:destroy(State), - ok. - -replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) -> - lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}). - -move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) - when N =< length(Authenticators) andalso N > 0 -> - move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N, []); -move_authenticator_to_the_nth_(_, _, _) -> - {error, out_of_range}. - -move_authenticator_to_the_nth_(AuthenticatorID, [], _, _) -> - {error, {not_found, {authenticator, AuthenticatorID}}}; -move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed) - when N =< length(Passed) -> - {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), - {ok, L1 ++ [Authenticator] ++ L2 ++ More}; -move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed) -> - {L1, L2} = lists:split(N - length(Passed) - 1, More), - {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2}; -move_authenticator_to_the_nth_(AuthenticatorID, [Authenticator | More], N, Passed) -> - move_authenticator_to_the_nth_(AuthenticatorID, More, N, [Authenticator | Passed]). - -update_chain(ChainID, UpdateFun) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ChainID, write) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [Chain] -> - UpdateFun(Chain) - end - end). - -call_authenticator(ChainID, AuthenticatorID, Func, Args) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> - case lists:keyfind(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {_, _, #authenticator{provider = Provider, state = State}} -> - case erlang:function_exported(Provider, Func, length(Args) + 1) of - true -> - erlang:apply(Provider, Func, Args ++ [State]); - false -> - {error, unsupported_feature} - end - end - end. - -serialize_chain(#chain{id = ID, - authenticators = Authenticators, - created_at = CreatedAt}) -> - #{id => ID, - authenticators => serialize_authenticators(Authenticators), - created_at => CreatedAt}. - -serialize_authenticators(Authenticators) -> - [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators]. - -serialize_authenticator(#authenticator{id = ID, - config = Config}) -> - Config#{id => ID}. - -trans(Fun) -> - trans(Fun, []). - -trans(Fun, Args) -> - case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of - {atomic, Res} -> Res; - {aborted, Reason} -> {error, Reason} - end. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 78ef5fd35..5ba1419f0 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -22,37 +22,39 @@ -export([ api_spec/0 , authentication/2 - , authenticators/2 - , authenticators2/2 - , position/2 + , authentication2/2 + , authentication3/2 + , authentication4/2 + , move/2 + , move2/2 , import_users/2 + , import_users2/2 , users/2 , users2/2 + , users3/2 + , users4/2 ]). --define(EXAMPLE_1, #{name => <<"example 1">>, - mechanism => <<"password-based">>, - server_type => <<"built-in-database">>, - user_id_type => <<"username">>, +-define(EXAMPLE_1, #{mechanism => <<"password-based">>, + backend => <<"built-in-database">>, + query => <<"SELECT password_hash from built-in-database WHERE username = ${username}">>, password_hash_algorithm => #{ - name => <<"sha256">> + name => <<"sha256">> }}). --define(EXAMPLE_2, #{name => <<"example 2">>, - mechanism => <<"password-based">>, - server_type => <<"http-server">>, +-define(EXAMPLE_2, #{mechanism => <<"password-based">>, + backend => <<"http-server">>, method => <<"post">>, url => <<"http://localhost:80/login">>, headers => #{ <<"content-type">> => <<"application/json">> }, - form_data => #{ + body => #{ <<"username">> => <<"${mqtt-username}">>, <<"password">> => <<"${mqtt-password}">> }}). --define(EXAMPLE_3, #{name => <<"example 3">>, - mechanism => <<"jwt">>, +-define(EXAMPLE_3, #{mechanism => <<"jwt">>, use_jwks => false, algorithm => <<"hmac-based">>, secret => <<"mysecret">>, @@ -61,9 +63,8 @@ <<"username">> => <<"${mqtt-username}">> }}). --define(EXAMPLE_4, #{name => <<"example 4">>, - mechanism => <<"password-based">>, - server_type => <<"mongodb">>, +-define(EXAMPLE_4, #{mechanism => <<"password-based">>, + backend => <<"mongodb">>, server => <<"127.0.0.1:27017">>, database => example, collection => users, @@ -72,13 +73,13 @@ }, password_hash_field => <<"password_hash">>, salt_field => <<"salt">>, + is_superuser_field => <<"is_superuser">>, password_hash_algorithm => <<"sha256">>, salt_position => <<"prefix">> }). --define(EXAMPLE_5, #{name => <<"example 5">>, - mechanism => <<"password-based">>, - server_type => <<"redis">>, +-define(EXAMPLE_5, #{mechanism => <<"password-based">>, + backend => <<"redis">>, server => <<"127.0.0.1:6379">>, database => 0, query => <<"HMGET ${mqtt-username} password_hash salt">>, @@ -86,10 +87,53 @@ salt_position => <<"prefix">> }). +-define(INSTANCE_EXAMPLE_1, maps:merge(?EXAMPLE_1, #{id => <<"password-based:built-in-database">>, + enable => true})). + +-define(INSTANCE_EXAMPLE_2, maps:merge(?EXAMPLE_2, #{id => <<"password-based:http-server">>, + connect_timeout => 5000, + enable_pipelining => true, + headers => #{ + <<"accept">> => <<"application/json">>, + <<"cache-control">> => <<"no-cache">>, + <<"connection">> => <<"keepalive">>, + <<"content-type">> => <<"application/json">>, + <<"keep-alive">> => <<"timeout=5">> + }, + max_retries => 5, + pool_size => 8, + request_timeout => 5000, + retry_interval => 1000, + enable => true})). + +-define(INSTANCE_EXAMPLE_3, maps:merge(?EXAMPLE_3, #{id => <<"jwt">>, + enable => true})). + +-define(INSTANCE_EXAMPLE_4, maps:merge(?EXAMPLE_4, #{id => <<"password-based:mongodb">>, + mongo_type => <<"single">>, + pool_size => 8, + ssl => #{ + enable => false + }, + topology => #{ + max_overflow => 8, + pool_size => 8 + }, + enable => true})). + +-define(INSTANCE_EXAMPLE_5, maps:merge(?EXAMPLE_5, #{id => <<"password-based:redis">>, + auto_reconnect => true, + redis_type => single, + pool_size => 8, + ssl => #{ + enable => false + }, + enable => true})). + -define(ERR_RESPONSE(Desc), #{description => Desc, content => #{ 'application/json' => #{ - schema => minirest:ref(<<"error">>), + schema => minirest:ref(<<"Error">>), examples => #{ example1 => #{ summary => <<"Not Found">>, @@ -107,617 +151,1022 @@ api_spec() -> {[ authentication_api() - , authenticators_api() - , authenticators_api2() - , position_api() + , authentication_api2() + , move_api() + , authentication_api3() + , authentication_api4() + , move_api2() , import_users_api() + , import_users_api2() , users_api() , users2_api() + , users3_api() + , users4_api() ], definitions()}. authentication_api() -> Metadata = #{ - post => #{ - description => "Enable or disbale authentication", - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [enable], - properties => #{ - enable => #{ - type => boolean, - example => true - } - } - } - } - } - }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>) - } - }, - get => #{ - description => "Get status of authentication", - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - properties => #{ - enabled => #{ - type => boolean, - example => true - } - } - } - } - } - } - } - } + post => create_authenticator_api_spec(), + get => list_authenticators_api_spec() }, {"/authentication", Metadata, authentication}. -authenticators_api() -> +authentication_api2() -> Metadata = #{ - post => #{ - description => "Create authenticator", - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"authenticator">>), - examples => #{ - default => #{ - summary => <<"Default">>, - value => emqx_json:encode(?EXAMPLE_1) - }, - http => #{ - summary => <<"Authentication provided by HTTP Server">>, - value => emqx_json:encode(?EXAMPLE_2) - }, - jwt => #{ - summary => <<"JWT Authentication">>, - value => emqx_json:encode(?EXAMPLE_3) - }, - mongodb => #{ - summary => <<"Authentication with MongoDB">>, - value => emqx_json:encode(?EXAMPLE_4) - }, - redis => #{ - summary => <<"Authentication with Redis">>, - value => emqx_json:encode(?EXAMPLE_5) - } - } - } - } - }, - responses => #{ - <<"201">> => #{ - description => <<"Created">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - %% TODO: return full content - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) - } - }, - get => #{ - description => "List authenticators", - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => minirest:ref(<<"returned_authenticator">>) - }, - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode([ maps:put(id, <<"example 1">>, ?EXAMPLE_1) - , maps:put(id, <<"example 2">>, ?EXAMPLE_2) - , maps:put(id, <<"example 3">>, ?EXAMPLE_3) - , maps:put(id, <<"example 4">>, ?EXAMPLE_4) - , maps:put(id, <<"example 5">>, ?EXAMPLE_5) - ]) - } - } - } - } - } - } - } + get => find_authenticator_api_spec(), + put => update_authenticator_api_spec(), + delete => delete_authenticator_api_spec() }, - {"/authentication/authenticators", Metadata, authenticators}. + {"/authentication/:id", Metadata, authentication2}. -authenticators_api2() -> +authentication_api3() -> Metadata = #{ - get => #{ - description => "Get authenicator by id", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 5">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - put => #{ - description => "Update authenticator", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] - }, - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(?EXAMPLE_1) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(?EXAMPLE_2) - } - } - } - } - }, - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 5">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>), - <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) - } - }, - delete => #{ - description => "Delete authenticator", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - } + post => create_authenticator_api_spec2(), + get => list_authenticators_api_spec2() }, - {"/authentication/authenticators/:id", Metadata, authenticators2}. + {"/listeners/:listener_id/authentication", Metadata, authentication3}. -position_api() -> +authentication_api4() -> Metadata = #{ - post => #{ - description => "Change the order of authenticators", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [position], - properties => #{ - position => #{ - type => integer, - example => 1 - } - } - } - } - } - }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - } + get => find_authenticator_api_spec2(), + put => update_authenticator_api_spec2(), + delete => delete_authenticator_api_spec2() }, - {"/authentication/authenticators/:id/position", Metadata, position}. + {"/listeners/:listener_id/authentication/:id", Metadata, authentication4}. + +move_api() -> + Metadata = #{ + post => move_authenticator_api_spec() + }, + {"/authentication/:id/move", Metadata, move}. + +move_api2() -> + Metadata = #{ + post => move_authenticator_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/move", Metadata, move2}. import_users_api() -> Metadata = #{ - post => #{ - description => "Import users from json/csv file", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true + post => import_users_api_spec() + }, + {"/authentication/:id/import_users", Metadata, import_users}. + +import_users_api2() -> + Metadata = #{ + post => import_users_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/import_users", Metadata, import_users2}. + +users_api() -> + Metadata = #{ + post => create_user_api_spec(), + get => list_users_api_spec() + }, + {"/authentication/:id/users", Metadata, users}. + +users2_api() -> + Metadata = #{ + put => update_user_api_spec(), + get => find_user_api_spec(), + delete => delete_user_api_spec() + }, + {"/authentication/:id/users/:user_id", Metadata, users2}. + +users3_api() -> + Metadata = #{ + post => create_user_api_spec2(), + get => list_users_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/users", Metadata, users3}. + +users4_api() -> + Metadata = #{ + put => update_user_api_spec2(), + get => find_user_api_spec2(), + delete => delete_user_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/users/:user_id", Metadata, users4}. + +create_authenticator_api_spec() -> + #{ + description => "Create a authenticator for global authentication", + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorConfig">>), + examples => #{ + default => #{ + summary => <<"Default">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + http => #{ + summary => <<"Authentication provided by HTTP Server">>, + value => emqx_json:encode(?EXAMPLE_2) + }, + jwt => #{ + summary => <<"JWT Authentication">>, + value => emqx_json:encode(?EXAMPLE_3) + }, + mongodb => #{ + summary => <<"Authentication with MongoDB">>, + value => emqx_json:encode(?EXAMPLE_4) + }, + redis => #{ + summary => <<"Authentication with Redis">>, + value => emqx_json:encode(?EXAMPLE_5) + } + } } - ], - requestBody => #{ + } + }, + responses => #{ + <<"201">> => #{ + description => <<"Created">>, content => #{ 'application/json' => #{ - schema => #{ - type => object, - required => [filename], - properties => #{ - filename => #{ - type => string - } + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) } } } } }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - } - }, - {"/authentication/authenticators/:id/import-users", Metadata, import_users}. + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) + } + }. -users_api() -> - Metadata = #{ - post => #{ - description => "Add user", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true +create_authenticator_api_spec2() -> + Spec = create_authenticator_api_spec(), + Spec#{ + description => "Create a authenticator for listener", + parameters => [ + #{ + name => listener_id, + in => path, + schema => #{ + type => string + }, + required => true + } + ] + }. + +list_authenticators_api_spec() -> + #{ + description => "List authenticators for global authentication", + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => minirest:ref(<<"AuthenticatorInstance">>) + }, + examples => #{ + example => #{ + summary => <<"Example">>, + value => emqx_json:encode([ ?INSTANCE_EXAMPLE_1 + , ?INSTANCE_EXAMPLE_2 + , ?INSTANCE_EXAMPLE_3 + , ?INSTANCE_EXAMPLE_4 + , ?INSTANCE_EXAMPLE_5 + ])}}}}}}}. + +list_authenticators_api_spec2() -> + Spec = list_authenticators_api_spec(), + Spec#{ + description => "List authenticators for listener", + parameters => [ + #{ + name => listener_id, + in => path, + schema => #{ + type => string + }, + required => true + } + ] + }. + +find_authenticator_api_spec() -> + #{ + description => "Get authenticator by id", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) + } + } + } } - ], - requestBody => #{ + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +find_authenticator_api_spec2() -> + Spec = find_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +update_authenticator_api_spec() -> + #{ + description => "Update authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorConfig">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?EXAMPLE_5) + } + } + } + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) + } + } + } + } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) + } + }. + +update_authenticator_api_spec2() -> + Spec = update_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +delete_authenticator_api_spec() -> + #{ + description => "Delete authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +delete_authenticator_api_spec2() -> + Spec = delete_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +move_authenticator_api_spec() -> + #{ + description => "Move authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + oneOf => [ + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + enum => [<<"top">>, <<"bottom">>], + example => <<"top">> + } + } + }, + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + description => <<"before:">>, + example => <<"before:password-based:mysql">> + } + } + } + ] + } + } + } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +move_authenticator_api_spec2() -> + Spec = move_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +import_users_api_spec() -> + #{ + description => "Import users from json/csv file", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [filename], + properties => #{ + filename => #{ + type => string + } + } + } + } + } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +import_users_api_spec2() -> + Spec = import_users_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +create_user_api_spec() -> + #{ + description => "Add user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [user_id, password], + properties => #{ + user_id => #{ + type => string + }, + password => #{ + type => string + }, + is_superuser => #{ + type => boolean, + default => false + } + } + } + } + } + }, + responses => #{ + <<"201">> => #{ + description => <<"Created">>, content => #{ 'application/json' => #{ schema => #{ type => object, - required => [user_id, password], properties => #{ user_id => #{ type => string }, - password => #{ - type => string + is_superuser => #{ + type => boolean } } } } } }, - responses => #{ - <<"201">> => #{ - description => <<"Created">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [user_id], - properties => #{ - user_id => #{ - type => string - } - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - get => #{ - description => "List users", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - required => [user_id], - properties => #{ - user_id => #{ - type => string - } - } - } - } - } - } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } - }, - {"/authentication/authenticators/:id/users", Metadata, users}. + }. -users2_api() -> - Metadata = #{ - patch => #{ - description => "Update user", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true +create_user_api_spec2() -> + Spec = create_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +list_users_api_spec() -> + #{ + description => "List users", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, content => #{ 'application/json' => #{ schema => #{ - type => object, - required => [password], - properties => #{ - password => #{ - type => string + type => array, + items => #{ + type => object, + properties => #{ + user_id => #{ + type => string + }, + is_superuser => #{ + type => boolean + } } } } } } }, - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - properties => #{ - user_id => #{ - type => string - } - } - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - get => #{ - description => "Get user info", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - properties => #{ - user_id => #{ - type => string - } - } - } - } - } - } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - delete => #{ - description => "Delete user", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } - }, - {"/authentication/authenticators/:id/users/:user_id", Metadata, users2}. + }. + +list_users_api_spec2() -> + Spec = list_users_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +update_user_api_spec() -> + #{ + description => "Update user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + properties => #{ + password => #{ + type => string + }, + is_superuser => #{ + type => boolean + } + } + } + } + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ + type => object, + properties => #{ + user_id => #{ + type => string + }, + is_superuser => #{ + type => boolean + } + } + } + } + } + } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +update_user_api_spec2() -> + Spec = update_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +find_user_api_spec() -> + #{ + description => "Get user info", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ + type => object, + properties => #{ + user_id => #{ + type => string + }, + is_superuser => #{ + type => boolean + } + } + } + } + } + } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +find_user_api_spec2() -> + Spec = find_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +delete_user_api_spec() -> + #{ + description => "Delete user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +delete_user_api_spec2() -> + Spec = delete_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + definitions() -> - AuthenticatorDef = #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] + AuthenticatorConfigDef = #{ + allOf => [ + #{ + type => object, + properties => #{ + enable => #{ + type => boolean, + default => true, + example => true + } + } + }, + #{ + oneOf => [ minirest:ref(<<"PasswordBasedBuiltInDatabase">>) + , minirest:ref(<<"PasswordBasedMySQL">>) + , minirest:ref(<<"PasswordBasedPostgreSQL">>) + , minirest:ref(<<"PasswordBasedMongoDB">>) + , minirest:ref(<<"PasswordBasedRedis">>) + , minirest:ref(<<"PasswordBasedHTTPServer">>) + , minirest:ref(<<"JWT">>) + , minirest:ref(<<"SCRAMBuiltInDatabase">>) + ] + } + ] }, - ReturnedAuthenticatorDef = #{ + AuthenticatorInstanceDef = #{ allOf => [ #{ type => object, @@ -726,147 +1175,49 @@ definitions() -> type => string } } - }, - #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] } - ] - }, - - PasswordBasedDef = #{ - allOf => [ - #{ - type => object, - required => [name, mechanism], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"password-based">>], - example => <<"password-based">> - } - } - }, - #{ - oneOf => [ minirest:ref(<<"password_based_built_in_database">>) - , minirest:ref(<<"password_based_mysql">>) - , minirest:ref(<<"password_based_pgsql">>) - , minirest:ref(<<"password_based_mongodb">>) - , minirest:ref(<<"password_based_http_server">>) - ] - } - ] - }, - - JWTDef = #{ - type => object, - required => [name, mechanism], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"jwt">>], - example => <<"jwt">> - }, - use_jwks => #{ - type => boolean, - default => false, - example => false - }, - algorithm => #{ - type => string, - enum => [<<"hmac-based">>, <<"public-key">>], - default => <<"hmac-based">>, - example => <<"hmac-based">> - }, - secret => #{ - type => string - }, - secret_base64_encoded => #{ - type => boolean, - default => false - }, - certificate => #{ - type => string - }, - verify_claims => #{ - type => object, - additionalProperties => #{ - type => string - } - }, - ssl => minirest:ref(<<"ssl">>) - } - }, - - SCRAMDef = #{ - type => object, - required => [name, mechanism, server_type], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"scram">>], - example => <<"scram">> - }, - server_type => #{ - type => string, - enum => [<<"built-in-database">>], - default => <<"built-in-database">> - }, - algorithm => #{ - type => string, - enum => [<<"sha256">>, <<"sha512">>], - default => <<"sha256">> - }, - iteration_count => #{ - type => integer, - default => 4096 - } - } + ] ++ maps:get(allOf, AuthenticatorConfigDef) }, PasswordBasedBuiltInDatabaseDef = #{ type => object, - required => [server_type], + required => [mechanism, backend], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"built-in-database">>], example => <<"built-in-database">> }, - user_id_type => #{ + query => #{ type => string, - enum => [<<"username">>, <<"clientid">>], - default => <<"username">>, - example => <<"username">> + default => <<"SELECT password_hash from built-in-database WHERE username = ${username}">>, + example => <<"SELECT password_hash from built-in-database WHERE username = ${username}">> }, - password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>) + password_hash_algorithm => minirest:ref(<<"PasswordHashAlgorithm">>) } }, PasswordBasedMySQLDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , database , username , password , query], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"mysql">>], example => <<"mysql">> @@ -892,7 +1243,7 @@ definitions() -> type => boolean, default => true }, - ssl => minirest:ref(<<"ssl">>), + ssl => minirest:ref(<<"SSL">>), password_hash_algorithm => #{ type => string, enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], @@ -915,19 +1266,25 @@ definitions() -> } }, - PasswordBasedPgSQLDef = #{ + PasswordBasedPostgreSQLDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , database , username , password , query], properties => #{ - server_type => #{ + mechanism => #{ type => string, - enum => [<<"pgsql">>], - example => <<"pgsql">> + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ + type => string, + enum => [<<"postgresql">>], + example => <<"postgresql">> }, server => #{ type => string, @@ -969,7 +1326,8 @@ definitions() -> PasswordBasedMongoDBDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , servers , replica_set_name @@ -981,10 +1339,15 @@ definitions() -> , password_hash_field ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"mongodb">>], - example => [<<"mongodb">>] + example => <<"mongodb">> }, server => #{ description => <<"Mutually exclusive with the 'servers' field, only valid in standalone mode">>, @@ -1036,6 +1399,10 @@ definitions() -> type => string, example => <<"salt">> }, + is_superuser_field => #{ + type => string, + example => <<"is_superuser">> + }, password_hash_algorithm => #{ type => string, enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], @@ -1054,12 +1421,24 @@ definitions() -> PasswordBasedRedisDef = #{ type => object, - required => [], + required => [ mechanism + , backend + , server + , servers + , password + , database + , query + ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"redis">>], - example => [<<"redis">>] + example => <<"redis">> }, server => #{ description => <<"Mutually exclusive with the 'servers' field, only valid in standalone mode">>, @@ -1083,7 +1462,7 @@ definitions() -> }, database => #{ type => integer, - exmaple => 0 + example => 0 }, query => #{ type => string, @@ -1114,12 +1493,18 @@ definitions() -> PasswordBasedHTTPServerDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , url - , form_data + , body ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"http-server">>], example => <<"http-server">> @@ -1139,8 +1524,8 @@ definitions() -> type => string } }, - form_data => #{ - type => string + body => #{ + type => object }, connect_timeout => #{ type => integer, @@ -1166,7 +1551,73 @@ definitions() -> type => boolean, default => true } - } + } + }, + + JWTDef = #{ + type => object, + required => [mechanism], + properties => #{ + mechanism => #{ + type => string, + enum => [<<"jwt">>], + example => <<"jwt">> + }, + use_jwks => #{ + type => boolean, + default => false, + example => false + }, + algorithm => #{ + type => string, + enum => [<<"hmac-based">>, <<"public-key">>], + default => <<"hmac-based">>, + example => <<"hmac-based">> + }, + secret => #{ + type => string + }, + secret_base64_encoded => #{ + type => boolean, + default => false + }, + certificate => #{ + type => string + }, + verify_claims => #{ + type => object, + additionalProperties => #{ + type => string + } + }, + ssl => minirest:ref(<<"SSL">>) + } + }, + + SCRAMBuiltInDatabaseDef = #{ + type => object, + required => [mechanism, backend], + properties => #{ + mechanism => #{ + type => string, + enum => [<<"scram">>], + example => <<"scram">> + }, + backend => #{ + type => string, + enum => [<<"built-in-database">>], + example => <<"built-in-database">> + }, + algorithm => #{ + type => string, + enum => [<<"sha256">>, <<"sha512">>], + default => <<"sha256">> + }, + iteration_count => #{ + type => integer, + default => 4096 + } + } }, PasswordHashAlgorithmDef = #{ @@ -1190,7 +1641,7 @@ definitions() -> properties => #{ enable => #{ type => boolean, - default => false + default => false }, certfile => #{ type => string @@ -1234,184 +1685,293 @@ definitions() -> } }, - [ #{<<"authenticator">> => AuthenticatorDef} - , #{<<"returned_authenticator">> => ReturnedAuthenticatorDef} - , #{<<"password_based">> => PasswordBasedDef} - , #{<<"jwt">> => JWTDef} - , #{<<"scram">> => SCRAMDef} - , #{<<"password_based_built_in_database">> => PasswordBasedBuiltInDatabaseDef} - , #{<<"password_based_mysql">> => PasswordBasedMySQLDef} - , #{<<"password_based_pgsql">> => PasswordBasedPgSQLDef} - , #{<<"password_based_mongodb">> => PasswordBasedMongoDBDef} - , #{<<"password_based_redis">> => PasswordBasedRedisDef} - , #{<<"password_based_http_server">> => PasswordBasedHTTPServerDef} - , #{<<"password_hash_algorithm">> => PasswordHashAlgorithmDef} - , #{<<"ssl">> => SSLDef} - , #{<<"error">> => ErrorDef} + [ #{<<"AuthenticatorConfig">> => AuthenticatorConfigDef} + , #{<<"AuthenticatorInstance">> => AuthenticatorInstanceDef} + , #{<<"PasswordBasedBuiltInDatabase">> => PasswordBasedBuiltInDatabaseDef} + , #{<<"PasswordBasedMySQL">> => PasswordBasedMySQLDef} + , #{<<"PasswordBasedPostgreSQL">> => PasswordBasedPostgreSQLDef} + , #{<<"PasswordBasedMongoDB">> => PasswordBasedMongoDBDef} + , #{<<"PasswordBasedRedis">> => PasswordBasedRedisDef} + , #{<<"PasswordBasedHTTPServer">> => PasswordBasedHTTPServerDef} + , #{<<"JWT">> => JWTDef} + , #{<<"SCRAMBuiltInDatabase">> => SCRAMBuiltInDatabaseDef} + , #{<<"PasswordHashAlgorithm">> => PasswordHashAlgorithmDef} + , #{<<"SSL">> => SSLDef} + , #{<<"Error">> => ErrorDef} ]. -authentication(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of - #{<<"enable">> := true} -> - ok = emqx_authn:enable(), - {204}; - #{<<"enable">> := false} -> - ok = emqx_authn:disable(), - {204}; - #{<<"enable">> := _} -> - serialize_error({invalid_parameter, enable}); - _ -> - serialize_error({missing_parameter, enable}) - end; -authentication(get, _Request) -> - Enabled = emqx_authn:is_enabled(), - {200, #{enabled => Enabled}}. +authentication(post, #{body := Config}) -> + create_authenticator([authentication], ?GLOBAL, Config); -authenticators(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - AuthenticatorConfig = emqx_json:decode(Body, [return_maps]), - Config = #{<<"authentication">> => #{ - <<"authenticators">> => [AuthenticatorConfig] - }}, - NConfig = hocon_schema:check_plain(emqx_authn_schema, Config, - #{nullable => true}), - #{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig), - case emqx_authn:create_authenticator(?CHAIN, NAuthenticatorConfig) of - {ok, Authenticator2} -> - {201, Authenticator2}; - {error, Reason} -> - serialize_error(Reason) - end; -authenticators(get, _Request) -> - {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN), - {200, Authenticators}. +authentication(get, _Params) -> + list_authenticators([authentication]). -authenticators2(get, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of - {ok, Authenticator} -> - {200, Authenticator}; +authentication2(get, #{bindings := #{id := AuthenticatorID}}) -> + list_authenticator([authentication], AuthenticatorID); + +authentication2(put, #{bindings := #{id := AuthenticatorID}, body := Config}) -> + update_authenticator([authentication], ?GLOBAL, AuthenticatorID, Config); + +authentication2(delete, #{bindings := #{id := AuthenticatorID}}) -> + delete_authenticator([authentication], ?GLOBAL, AuthenticatorID). + +authentication3(post, #{bindings := #{listener_id := ListenerID}, body := Config}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + create_authenticator([listeners, Type, Name, authentication], ListenerID, Config); {error, Reason} -> serialize_error(Reason) end; -authenticators2(put, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - AuthenticatorConfig = emqx_json:decode(Body, [return_maps]), - Config = #{<<"authentication">> => #{ - <<"authenticators">> => [AuthenticatorConfig] - }}, - NConfig = hocon_schema:check_plain(emqx_authn_schema, Config, - #{nullable => true}), - #{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig), - case emqx_authn:update_or_create_authenticator(?CHAIN, AuthenticatorID, NAuthenticatorConfig) of - {ok, Authenticator} -> - {200, Authenticator}; - {error, Reason} -> - serialize_error(Reason) - end; -authenticators2(delete, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - case emqx_authn:delete_authenticator(?CHAIN, AuthenticatorID) of - ok -> - {204}; +authentication3(get, #{bindings := #{listener_id := ListenerID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + list_authenticators([listeners, Type, Name, authentication]); {error, Reason} -> serialize_error(Reason) end. -position(post, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"position">> => NBody}, - #{nullable => true}, ["position"]), - #{position := #{position := Position}} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of - ok -> - {204}; +authentication4(get, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + list_authenticator([listeners, Type, Name, authentication], AuthenticatorID); + {error, Reason} -> + serialize_error(Reason) + end; +authentication4(put, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := Config}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + update_authenticator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID, Config); + {error, Reason} -> + serialize_error(Reason) + end; +authentication4(delete, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + delete_authenticator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID); {error, Reason} -> serialize_error(Reason) end. -import_users(post, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"filename">> => NBody}, - #{nullable => true}, ["filename"]), - #{filename := #{filename := Filename}} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of - ok -> - {204}; +move(post, #{bindings := #{id := AuthenticatorID}, body := #{<<"position">> := Position}}) -> + move_authenitcator([authentication], ?GLOBAL, AuthenticatorID, Position); +move(post, #{bindings := #{id := _}, body := _}) -> + serialize_error({missing_parameter, position}). + +move2(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := #{<<"position">> := Position}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + move_authenitcator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID, Position); + {error, Reason} -> + serialize_error(Reason) + end; +move2(post, #{bindings := #{listener_id := _, id := _}, body := _}) -> + serialize_error({missing_parameter, position}). + +import_users(post, #{bindings := #{id := AuthenticatorID}, body := #{<<"filename">> := Filename}}) -> + case ?AUTHN:import_users(?GLOBAL, AuthenticatorID, Filename) of + ok -> {204}; + {error, Reason} -> serialize_error(Reason) + end; +import_users(post, #{bindings := #{id := _}, body := _}) -> + serialize_error({missing_parameter, filename}). + +import_users2(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := #{<<"filename">> := Filename}}) -> + case ?AUTHN:import_users(ListenerID, AuthenticatorID, Filename) of + ok -> {204}; + {error, Reason} -> serialize_error(Reason) + end; +import_users2(post, #{bindings := #{listener_id := _, id := _}, body := _}) -> + serialize_error({missing_parameter, filename}). + +users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> + add_user(?GLOBAL, AuthenticatorID, UserInfo); +users(get, #{bindings := #{id := AuthenticatorID}}) -> + list_users(?GLOBAL, AuthenticatorID). + +users2(put, #{bindings := #{id := AuthenticatorID, + user_id := UserID}, body := UserInfo}) -> + update_user(?GLOBAL, AuthenticatorID, UserID, UserInfo); +users2(get, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> + find_user(?GLOBAL, AuthenticatorID, UserID); +users2(delete, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> + delete_user(?GLOBAL, AuthenticatorID, UserID). + +users3(post, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID}, body := UserInfo}) -> + add_user(ListenerID, AuthenticatorID, UserInfo); +users3(get, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID}}) -> + list_users(ListenerID, AuthenticatorID). + +users4(put, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}, body := UserInfo}) -> + update_user(ListenerID, AuthenticatorID, UserID, UserInfo); +users4(get, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}}) -> + find_user(ListenerID, AuthenticatorID, UserID); +users4(delete, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}}) -> + delete_user(ListenerID, AuthenticatorID, UserID). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +find_listener(ListenerID) -> + {Type, Name} = emqx_listeners:parse_listener_id(ListenerID), + case emqx_config:find([listeners, Type, Name]) of + {not_found, _, _} -> + {error, {not_found, {listener, ListenerID}}}; + {ok, _} -> + {ok, {Type, Name}} + end. + +create_authenticator(ConfKeyPath, ChainName0, Config) -> + ChainName = to_atom(ChainName0), + case update_config(ConfKeyPath, {create_authenticator, ChainName, Config}) of + {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, + raw_config := AuthenticatorsConfig}} -> + {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig), + {200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +list_authenticators(ConfKeyPath) -> + AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath), + NAuthenticators = [maps:put(id, ?AUTHN:generate_id(AuthenticatorConfig), AuthenticatorConfig) + || AuthenticatorConfig <- AuthenticatorsConfig], + {200, NAuthenticators}. + +list_authenticator(ConfKeyPath, AuthenticatorID) -> + AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath), + case find_config(AuthenticatorID, AuthenticatorsConfig) of + {ok, AuthenticatorConfig} -> + {200, AuthenticatorConfig#{id => AuthenticatorID}}; {error, Reason} -> serialize_error(Reason) end. -users(post, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"user_info">> => NBody}, - #{nullable => true}, ["user_info"]), - #{user_info := UserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of +update_authenticator(ConfKeyPath, ChainName0, AuthenticatorID, Config) -> + ChainName = to_atom(ChainName0), + case update_config(ConfKeyPath, + {update_authenticator, ChainName, AuthenticatorID, Config}) of + {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, + raw_config := AuthenticatorsConfig}} -> + {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig), + {200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +delete_authenticator(ConfKeyPath, ChainName0, AuthenticatorID) -> + ChainName = to_atom(ChainName0), + case update_config(ConfKeyPath, {delete_authenticator, ChainName, AuthenticatorID}) of + {ok, _} -> + {204}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +move_authenitcator(ConfKeyPath, ChainName0, AuthenticatorID, Position) -> + ChainName = to_atom(ChainName0), + case update_config(ConfKeyPath, {move_authenticator, ChainName, AuthenticatorID, Position}) of + {ok, _} -> + {204}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +add_user(ChainName0, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> + ChainName = to_atom(ChainName0), + IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false), + case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID + , password => Password + , is_superuser => IsSuperuser}) of {ok, User} -> {201, User}; {error, Reason} -> serialize_error(Reason) end; -users(get, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - case emqx_authn:list_users(?CHAIN, AuthenticatorID) of +add_user(_, _, #{<<"user_id">> := _}) -> + serialize_error({missing_parameter, password}); +add_user(_, _, _) -> + serialize_error({missing_parameter, user_id}). + +update_user(ChainName0, AuthenticatorID, UserID, UserInfo) -> + ChainName = to_atom(ChainName0), + case maps:with([<<"password">>, <<"is_superuser">>], UserInfo) =:= #{} of + true -> + serialize_error({missing_parameter, password}); + false -> + case ?AUTHN:update_user(ChainName, AuthenticatorID, UserID, UserInfo) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end + end. + +find_user(ChainName0, AuthenticatorID, UserID) -> + ChainName = to_atom(ChainName0), + case ?AUTHN:lookup_user(ChainName, AuthenticatorID, UserID) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end. + +delete_user(ChainName0, AuthenticatorID, UserID) -> + ChainName = to_atom(ChainName0), + case ?AUTHN:delete_user(ChainName, AuthenticatorID, UserID) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) + end. + +list_users(ChainName0, AuthenticatorID) -> + ChainName = to_atom(ChainName0), + case ?AUTHN:list_users(ChainName, AuthenticatorID) of {ok, Users} -> {200, Users}; {error, Reason} -> serialize_error(Reason) end. -users2(patch, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - UserID = cowboy_req:binding(user_id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"new_user_info">> => NBody}, - #{nullable => true}, ["new_user_info"]), - #{new_user_info := NewUserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, NewUserInfo) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) - end; -users2(get, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - UserID = cowboy_req:binding(user_id, Request), - case emqx_authn:lookup_user(?CHAIN, AuthenticatorID, UserID) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) - end; -users2(delete, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - UserID = cowboy_req:binding(user_id, Request), - case emqx_authn:delete_user(?CHAIN, AuthenticatorID, UserID) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) +update_config(Path, ConfigRequest) -> + emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). + +get_raw_config_with_defaults(ConfKeyPath) -> + NConfKeyPath = [atom_to_binary(Key, utf8) || Key <- ConfKeyPath], + RawConfig = emqx_map_lib:deep_get(NConfKeyPath, emqx_config:get_raw([]), []), + to_list(fill_defaults(RawConfig)). + +find_config(AuthenticatorID, AuthenticatorsConfig) -> + case [AC || AC <- to_list(AuthenticatorsConfig), AuthenticatorID =:= ?AUTHN:generate_id(AC)] of + [] -> {error, {not_found, {authenticator, AuthenticatorID}}}; + [AuthenticatorConfig] -> {ok, AuthenticatorConfig} end. +fill_defaults(Config) -> + #{<<"authentication">> := CheckedConfig} = hocon_schema:check_plain( + ?AUTHN, #{<<"authentication">> => Config}, #{nullable => true, no_conversion => true}), + CheckedConfig. + serialize_error({not_found, {authenticator, ID}}) -> {404, #{code => <<"NOT_FOUND">>, message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}}; -serialize_error(name_has_be_used) -> +serialize_error({not_found, {listener, ID}}) -> + {404, #{code => <<"NOT_FOUND">>, + message => list_to_binary(io_lib:format("Listener '~s' does not exist", [ID]))}}; +serialize_error({already_exists, {authenticator, ID}}) -> {409, #{code => <<"ALREADY_EXISTS">>, - message => <<"Name has be used">>}}; -serialize_error(out_of_range) -> - {400, #{code => <<"OUT_OF_RANGE">>, - message => <<"Out of range">>}}; + message => list_to_binary( + io_lib:format("Authenticator '~s' already exist", [ID]) + )}}; serialize_error({missing_parameter, Name}) -> {400, #{code => <<"MISSING_PARAMETER">>, message => list_to_binary( @@ -1424,4 +1984,14 @@ serialize_error({invalid_parameter, Name}) -> )}}; serialize_error(Reason) -> {400, #{code => <<"BAD_REQUEST">>, - message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. \ No newline at end of file + message => list_to_binary(io_lib:format("~p", [Reason]))}}. + +to_list(M) when is_map(M) -> + [M]; +to_list(L) when is_list(L) -> + L. + +to_atom(B) when is_binary(B) -> + binary_to_atom(B); +to_atom(A) when is_atom(A) -> + A. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 52e59b2a6..016decdd2 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -17,7 +17,6 @@ -module(emqx_authn_app). -include("emqx_authn.hrl"). --include_lib("emqx/include/logger.hrl"). -behaviour(application). @@ -26,32 +25,45 @@ , stop/1 ]). +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_authn_sup:start_link(), ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), - initialize(), + {ok, Sup} = emqx_authn_sup:start_link(), + ok = add_providers(), + ok = initialize(), {ok, Sup}. stop(_State) -> + ok = remove_providers(), ok. +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +add_providers() -> + _ = [?AUTHN:add_provider(AuthNType, Provider) || {AuthNType, Provider} <- providers()], ok. + +remove_providers() -> + _ = [?AUTHN:remove_provider(AuthNType) || {AuthNType, _} <- providers()], ok. + initialize() -> - AuthNConfig = emqx_config:get([authentication], #{enable => false, - authenticators => []}), - initialize(AuthNConfig). - -initialize(#{enable := Enable, authenticators := AuthenticatorsConfig}) -> - {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), - initialize_authenticators(AuthenticatorsConfig), - Enable =:= true andalso emqx_authn:enable(), + ?AUTHN:initialize_authentication(?GLOBAL, emqx:get_raw_config([authentication], [])), + lists:foreach(fun({ListenerID, ListenerConfig}) -> + ?AUTHN:initialize_authentication(ListenerID, maps:get(authentication, ListenerConfig, [])) + end, emqx_listeners:list()), ok. -initialize_authenticators([]) -> - ok; -initialize_authenticators([#{name := Name} = AuthenticatorConfig | More]) -> - case emqx_authn:create_authenticator(?CHAIN, AuthenticatorConfig) of - {ok, _} -> - initialize_authenticators(More); - {error, Reason} -> - ?LOG(error, "Failed to create authenticator '~s': ~p", [Name, Reason]) - end. +providers() -> + [ {{'password-based', 'built-in-database'}, emqx_authn_mnesia} + , {{'password-based', mysql}, emqx_authn_mysql} + , {{'password-based', posgresql}, emqx_authn_pgsql} + , {{'password-based', mongodb}, emqx_authn_mongodb} + , {{'password-based', redis}, emqx_authn_redis} + , {{'password-based', 'http-server'}, emqx_authn_http} + , {jwt, emqx_authn_jwt} + , {{scram, 'built-in-database'}, emqx_enhanced_authn_scram_mnesia} + ]. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index 6a834df1f..23e412088 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -16,53 +16,15 @@ -module(emqx_authn_schema). --include("emqx_authn.hrl"). -include_lib("typerefl/include/types.hrl"). --behaviour(hocon_schema). - --export([ structs/0 - , fields/1 +-export([ common_fields/0 ]). --export([ authenticator_name/1 - ]). - -%% Export it for emqx_gateway_schema module --export([ authenticators/1 - ]). - -structs() -> [ "authentication" ]. - -fields("authentication") -> - [ {enable, fun enable/1} - , {authenticators, fun authenticators/1} +common_fields() -> + [ {enable, fun enable/1} ]. -authenticator_name(type) -> binary(); -authenticator_name(nullable) -> false; -authenticator_name(_) -> undefined. - enable(type) -> boolean(); -enable(default) -> false; +enable(default) -> true; enable(_) -> undefined. - -authenticators(type) -> - hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config) - , hoconsc:ref(emqx_authn_mysql, config) - , hoconsc:ref(emqx_authn_pgsql, config) - , hoconsc:ref(emqx_authn_mongodb, standalone) - , hoconsc:ref(emqx_authn_mongodb, 'replica-set') - , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') - , hoconsc:ref(emqx_authn_redis, standalone) - , hoconsc:ref(emqx_authn_redis, cluster) - , hoconsc:ref(emqx_authn_redis, sentinel) - , hoconsc:ref(emqx_authn_http, get) - , hoconsc:ref(emqx_authn_http, post) - , hoconsc:ref(emqx_authn_jwt, 'hmac-based') - , hoconsc:ref(emqx_authn_jwt, 'public-key') - , hoconsc:ref(emqx_authn_jwt, 'jwks') - , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) - ]}); -authenticators(default) -> []; -authenticators(_) -> undefined. diff --git a/apps/emqx_authn/src/emqx_authn_sup.erl b/apps/emqx_authn/src/emqx_authn_sup.erl index bb26af0ad..dd672a7c7 100644 --- a/apps/emqx_authn/src/emqx_authn_sup.erl +++ b/apps/emqx_authn/src/emqx_authn_sup.erl @@ -26,4 +26,5 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_one, 10, 10}, []}}. + ChildSpecs = [], + {ok, {{one_for_one, 10, 10}, ChildSpecs}}. diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index 56629c568..e0f37a50d 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -17,16 +17,18 @@ -module(emqx_enhanced_authn_scram_mnesia). -include("emqx_authn.hrl"). --include_lib("esasl/include/esasl_scram.hrl"). -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ structs/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -46,7 +48,13 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --rlog_shard({?AUTH_SHARD, ?TAB}). +-record(user_info, + { user_id + , stored_key + , server_key + , salt + , is_superuser + }). %%------------------------------------------------------------------------------ %% Mnesia bootstrap @@ -56,9 +64,10 @@ -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ + {rlog_shard, ?AUTH_SHARD}, {disc_copies, [node()]}, - {record_name, scram_user_credentail}, - {attributes, record_info(fields, scram_user_credentail)}, + {record_name, user_info}, + {attributes, record_info(fields, user_info)}, {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> @@ -68,19 +77,16 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +namespace() -> "authn:scram:builtin-db". + +roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, [scram]}} - , {server_type, fun server_type/1} + [ {mechanism, {enum, [scram]}} + , {backend, {enum, ['built-in-database']}} , {algorithm, fun algorithm/1} , {iteration_count, fun iteration_count/1} - ]. - -server_type(type) -> hoconsc:enum(['built-in-database']); -server_type(default) -> 'built-in-database'; -server_type(_) -> undefined. + ] ++ emqx_authn_schema:common_fields(). algorithm(type) -> hoconsc:enum([sha256, sha512]); algorithm(default) -> sha256; @@ -94,6 +100,9 @@ iteration_count(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ algorithm := Algorithm , iteration_count := IterationCount , '_unique' := Unique @@ -105,7 +114,7 @@ create(#{ algorithm := Algorithm update(Config, #{user_group := Unique}) -> create(Config#{'_unique' => Unique}). - + authenticate(#{auth_method := AuthMethod, auth_data := AuthData, auth_cache := AuthCache}, State) -> @@ -126,20 +135,21 @@ authenticate(_Credential, _State) -> destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], - ok = lists:foreach(fun(UserCredential) -> - mnesia:delete_object(?TAB, UserCredential, write) + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_', '_'}, [], ['$_']}], + ok = lists:foreach(fun(UserInfo) -> + mnesia:delete_object(?TAB, UserInfo, write) end, mnesia:select(?TAB, MatchSpec, write)) end). add_user(#{user_id := UserID, - password := Password}, #{user_group := UserGroup} = State) -> + password := Password} = UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add_user(UserID, Password, State), - {ok, #{user_id => UserID}}; + IsSuperuser = maps:get(is_superuser, UserInfo, false), + add_user(UserID, Password, IsSuperuser, State), + {ok, #{user_id => UserID, is_superuser => IsSuperuser}}; [_] -> {error, already_exist} end @@ -156,31 +166,41 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{password := Password}, +update_user(UserID, User, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [_] -> - add_user(UserID, Password, State), - {ok, #{user_id => UserID}} + [#user_info{is_superuser = IsSuperuser} = UserInfo] -> + UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, IsSuperuser)}, + UserInfo2 = case maps:get(password, User, undefined) of + undefined -> + UserInfo1; + Password -> + {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State), + UserInfo1#user_info{stored_key = StoredKey, + server_key = ServerKey, + salt = Salt} + end, + mnesia:write(?TAB, UserInfo2, write), + {ok, serialize_user_info(UserInfo2)} end end). lookup_user(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#scram_user_credentail{user_id = {_, UserID}}] -> - {ok, #{user_id => UserID}}; + [UserInfo] -> + {ok, serialize_user_info(UserInfo)}; [] -> {error, not_found} end. %% TODO: Support Pagination list_users(#{user_group := UserGroup}) -> - Users = [#{user_id => UserID} || - #scram_user_credentail{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], + Users = [serialize_user_info(UserInfo) || + #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], {ok, Users}. %%------------------------------------------------------------------------------ @@ -195,13 +215,13 @@ ensure_auth_method(_, _) -> false. check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) -> - LookupFun = fun(Username) -> - lookup_user2(Username, State) + RetrieveFun = fun(Username) -> + retrieve(Username, State) end, case esasl_scram:check_client_first_message( Bin, #{iteration_count => IterationCount, - lookup => LookupFun} + retrieve => RetrieveFun} ) of {cotinue, ServerFirstMessage, Cache} -> {cotinue, ServerFirstMessage, Cache}; @@ -209,25 +229,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S {error, not_authorized} end. -check_client_final_message(Bin, Cache, #{algorithm := Alg}) -> +check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}) -> case esasl_scram:check_client_final_message( Bin, Cache#{algorithm => Alg} ) of {ok, ServerFinalMessage} -> - {ok, ServerFinalMessage}; + {ok, #{is_superuser => IsSuperuser}, ServerFinalMessage}; {error, _Reason} -> {error, not_authorized} end. -add_user(UserID, Password, State) -> - UserCredential = esasl_scram:generate_user_credential(UserID, Password, State), - mnesia:write(?TAB, UserCredential, write). +add_user(UserID, Password, IsSuperuser, State) -> + {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State), + UserInfo = #user_info{user_id = UserID, + stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + is_superuser = IsSuperuser}, + mnesia:write(?TAB, UserInfo, write). -lookup_user2(UserID, #{user_group := UserGroup}) -> +retrieve(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#scram_user_credentail{} = UserCredential] -> - {ok, UserCredential}; + [#user_info{stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + is_superuser = IsSuperuser}] -> + {ok, #{stored_key => StoredKey, + server_key => ServerKey, + salt => Salt, + is_superuser => IsSuperuser}}; [] -> {error, not_found} end. @@ -241,3 +272,6 @@ trans(Fun, Args) -> {atomic, Res} -> Res; {aborted, Reason} -> {error, Reason} end. + +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) -> + #{user_id => UserID, is_superuser => IsSuperuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index aa10a3b98..5495b139a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -21,13 +21,16 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ structs/0 +-export([ namespace/0 + , roots/0 , fields/1 , validations/0 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -37,13 +40,13 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [""]. +namespace() -> "authn:password-based:http-server". -fields("") -> - [ {config, {union, [ hoconsc:t(get) - , hoconsc:t(post) +roots() -> + [ {config, {union, [ hoconsc:ref(?MODULE, get) + , hoconsc:ref(?MODULE, post) ]}} - ]; + ]. fields(get) -> [ {method, #{type => get, @@ -58,15 +61,15 @@ fields(post) -> ] ++ common_fields(). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, ['http-server']}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['http-server']}} , {url, fun url/1} - , {form_data, fun form_data/1} + , {body, fun body/1} , {request_timeout, fun request_timeout/1} - ] ++ maps:to_list(maps:without([ base_url - , pool_type], - maps:from_list(emqx_connector_http:fields(config)))). + ] ++ emqx_authn_schema:common_fields() + ++ maps:to_list(maps:without([ base_url + , pool_type], + maps:from_list(emqx_connector_http:fields(config)))). validations() -> [ {check_ssl_opts, fun check_ssl_opts/1} @@ -89,16 +92,15 @@ headers(_) -> undefined. headers_no_content_type(type) -> map(); headers_no_content_type(converter) -> fun(Headers) -> - maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) + maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) end; headers_no_content_type(default) -> default_headers_no_content_type(); headers_no_content_type(_) -> undefined. -%% TODO: Using map() -form_data(type) -> map(); -form_data(nullable) -> false; -form_data(validate) -> [fun check_form_data/1]; -form_data(_) -> undefined. +body(type) -> map(); +body(nullable) -> false; +body(validate) -> [fun check_body/1]; +body(_) -> undefined. request_timeout(type) -> non_neg_integer(); request_timeout(default) -> 5000; @@ -108,10 +110,15 @@ request_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, get) + , hoconsc:ref(?MODULE, post) + ]. + create(#{ method := Method , url := URL , headers := Headers - , form_data := FormData + , body := Body , request_timeout := RequestTimeout , '_unique' := Unique } = Config) -> @@ -120,8 +127,8 @@ create(#{ method := Method State = #{ method => Method , path => Path , base_query => cow_qs:parse_qs(list_to_binary(Query)) - , headers => normalize_headers(Headers) - , form_data => maps:to_list(FormData) + , headers => maps:to_list(Headers) + , body => maps:to_list(Body) , request_timeout => RequestTimeout , '_unique' => Unique }, @@ -129,9 +136,9 @@ create(#{ method := Method emqx_connector_http, Config#{base_url => maps:remove(query, URIMap), pool_type => random}) of - {ok, _} -> + {ok, already_created} -> {ok, State}; - {error, already_created} -> + {ok, _} -> {ok, State}; {error, Reason} -> {error, Reason} @@ -154,15 +161,16 @@ authenticate(Credential, #{'_unique' := Unique, try Request = generate_request(Credential, State), case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of - {ok, 204, _Headers} -> ok; + {ok, 204, _Headers} -> {ok, #{is_superuser => false}}; {ok, 200, Headers, Body} -> ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>), case safely_parse_body(ContentType, Body) of - {ok, _NBody} -> + {ok, NBody} -> %% TODO: Return by user property - ok; + {ok, #{is_superuser => maps:get(<<"is_superuser">>, NBody, false), + user_property => NBody}}; {error, _Reason} -> - ok + {ok, #{is_superuser => false}} end; {error, _Reason} -> ignore @@ -187,10 +195,10 @@ check_url(URL) -> {error, _} -> false end. -check_form_data(FormData) -> +check_body(Body) -> lists:any(fun({_, V}) -> not is_binary(V) - end, maps:to_list(FormData)). + end, maps:to_list(Body)). default_headers() -> maps:put(<<"content-type">>, @@ -230,24 +238,21 @@ parse_url(URL) -> URIMap end. -normalize_headers(Headers) -> - [{atom_to_binary(K), V} || {K, V} <- maps:to_list(Headers)]. - generate_request(Credential, #{method := Method, path := Path, base_query := BaseQuery, headers := Headers, - form_data := FormData0}) -> - FormData = replace_placeholders(FormData0, Credential), + body := Body0}) -> + Body = replace_placeholders(Body0, Credential), case Method of get -> - NPath = append_query(Path, BaseQuery ++ FormData), + NPath = append_query(Path, BaseQuery ++ Body), {NPath, Headers}; post -> NPath = append_query(Path, BaseQuery), ContentType = proplists:get_value(<<"content-type">>, Headers), - Body = serialize_body(ContentType, FormData), - {NPath, Headers, Body} + NBody = serialize_body(ContentType, Body), + {NPath, Headers, NBody} end. replace_placeholders(KVs, Credential) -> @@ -277,10 +282,10 @@ qs([], Acc) -> qs([{K, V} | More], Acc) -> qs(More, [["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] | Acc]). -serialize_body(<<"application/json">>, FormData) -> - emqx_json:encode(FormData); -serialize_body(<<"application/x-www-form-urlencoded">>, FormData) -> - qs(FormData). +serialize_body(<<"application/json">>, Body) -> + emqx_json:encode(Body); +serialize_body(<<"application/x-www-form-urlencoded">>, Body) -> + qs(Body). safely_parse_body(ContentType, Body) -> try parse_body(ContentType, Body) of @@ -291,8 +296,8 @@ safely_parse_body(ContentType, Body) -> end. parse_body(<<"application/json">>, Body) -> - {ok, emqx_json:decode(Body)}; + {ok, emqx_json:decode(Body, [return_maps])}; parse_body(<<"application/x-www-form-urlencoded">>, Body) -> - {ok, cow_qs:parse_qs(Body)}; + {ok, maps:from_list(cow_qs:parse_qs(Body))}; parse_body(ContentType, _) -> - {error, {unsupported_content_type, ContentType}}. \ No newline at end of file + {error, {unsupported_content_type, ContentType}}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index fe034994e..774d75157 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -19,12 +19,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ structs/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -34,14 +37,14 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [""]. +namespace() -> "authn:jwt". -fields("") -> - [ {config, {union, [ hoconsc:t('hmac-based') - , hoconsc:t('public-key') - , hoconsc:t('jwks') +roots() -> + [ {config, {union, [ hoconsc:mk('hmac-based') + , hoconsc:mk('public-key') + , hoconsc:mk('jwks') ]}} - ]; + ]. fields('hmac-based') -> [ {use_jwks, {enum, [false]}} @@ -80,12 +83,11 @@ fields(ssl_disable) -> [ {enable, #{type => false}} ]. common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, [jwt]}} + [ {mechanism, {enum, [jwt]}} , {verify_claims, fun verify_claims/1} - ]. + ] ++ emqx_authn_schema:common_fields(). -secret(type) -> string(); +secret(type) -> binary(); secret(_) -> undefined. secret_base64_encoded(type) -> boolean(); @@ -132,6 +134,12 @@ verify_claims(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, 'hmac-based') + , hoconsc:ref(?MODULE, 'public-key') + , hoconsc:ref(?MODULE, 'jwks') + ]. + create(#{verify_claims := VerifyClaims} = Config) -> create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}). @@ -169,7 +177,7 @@ authenticate(Credential = #{password := JWT}, #{jwk := JWK, end, VerifyClaims = replace_placeholder(VerifyClaims0, Credential), case verify(JWT, JWKs, VerifyClaims) of - ok -> ok; + {ok, Extra} -> {ok, Extra}; {error, invalid_signature} -> ignore; {error, {claims, _}} -> {error, bad_username_or_password} end. @@ -239,7 +247,12 @@ verify(JWS, [JWK | More], VerifyClaims) -> try jose_jws:verify(JWK, JWS) of {true, Payload, _JWS} -> Claims = emqx_json:decode(Payload, [return_maps]), - verify_claims(Claims, VerifyClaims); + case verify_claims(Claims, VerifyClaims) of + ok -> + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Claims, false)}}; + {error, Reason} -> + {error, Reason} + end; {false, _, _} -> verify(JWS, More, VerifyClaims) catch diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index ce845d4e3..b69d613f8 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -20,10 +20,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ structs/0, fields/1 ]). +-export([ namespace/0 + , roots/0 + , fields/1 + ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -46,6 +51,7 @@ { user_id :: {user_group(), user_id()} , password_hash :: binary() , salt :: binary() + , is_superuser :: boolean() }). -reflect_type([ user_id_type/0 ]). @@ -57,7 +63,6 @@ -define(TAB, ?MODULE). --rlog_shard({?AUTH_SHARD, ?TAB}). %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -66,6 +71,7 @@ -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ + {rlog_shard, ?AUTH_SHARD}, {disc_copies, [node()]}, {record_name, user_info}, {attributes, record_info(fields, user_info)}, @@ -78,15 +84,16 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +namespace() -> "authn:password-based:builtin-db". + +roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, ['built-in-database']}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['built-in-database']}} , {user_id_type, fun user_id_type/1} , {password_hash_algorithm, fun password_hash_algorithm/1} - ]; + ] ++ emqx_authn_schema:common_fields(); fields(bcrypt) -> [ {name, {enum, [bcrypt]}} @@ -101,7 +108,8 @@ user_id_type(type) -> user_id_type(); user_id_type(default) -> username; user_id_type(_) -> undefined. -password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; +password_hash_algorithm(type) -> hoconsc:union([hoconsc:ref(?MODULE, bcrypt), + hoconsc:ref(?MODULE, other_algorithms)]); password_hash_algorithm(default) -> #{<<"name">> => sha256}; password_hash_algorithm(_) -> undefined. @@ -113,6 +121,9 @@ salt_rounds(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ user_id_type := Type , password_hash_algorithm := #{name := bcrypt, salt_rounds := SaltRounds} @@ -147,13 +158,13 @@ authenticate(#{password := Password} = Credential, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt0}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = IsSuperuser}] -> Salt = case Algorithm of bcrypt -> PasswordHash; _ -> Salt0 end, case PasswordHash =:= hash(Algorithm, Password, Salt) of - true -> ok; + true -> {ok, #{is_superuser => IsSuperuser}}; false -> {error, bad_username_or_password} end end. @@ -161,7 +172,7 @@ authenticate(#{password := Password} = Credential, destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_'}, [], ['$_']}], + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write)) end). @@ -179,14 +190,16 @@ import_users(Filename0, State) -> end. add_user(#{user_id := UserID, - password := Password}, + password := Password} = UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add(UserID, Password, State), - {ok, #{user_id => UserID}}; + {PasswordHash, Salt} = hash(Password, State), + IsSuperuser = maps:get(is_superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), + {ok, #{user_id => UserID, is_superuser => IsSuperuser}}; [_] -> {error, already_exist} end @@ -203,29 +216,38 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{password := Password}, +update_user(UserID, UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [_] -> - add(UserID, Password, State), - {ok, #{user_id => UserID}} + [#user_info{ password_hash = PasswordHash + , salt = Salt + , is_superuser = IsSuperuser}] -> + NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser), + {NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of + undefined -> + {PasswordHash, Salt}; + Password -> + hash(Password, State) + end, + insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser), + {ok, #{user_id => UserID, is_superuser => NSuperuser}} end end). lookup_user(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#user_info{user_id = {_, UserID}}] -> - {ok, #{user_id => UserID}}; + [UserInfo] -> + {ok, serialize_user_info(UserInfo)}; [] -> {error, not_found} end. list_users(#{user_group := UserGroup}) -> - Users = [#{user_id => UserID} || #user_info{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], + Users = [serialize_user_info(UserInfo) || #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], {ok, Users}. %%------------------------------------------------------------------------------ @@ -268,7 +290,8 @@ import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> Salt = maps:get(<<"salt">>, UserInfo, <<>>), - insert_user(UserGroup, UserID, PasswordHash, Salt), + IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), import(UserGroup, More); import(_UserGroup, [_ | _More]) -> {error, bad_format}. @@ -282,7 +305,8 @@ import(UserGroup, File, Seq) -> {ok, #{user_id := UserID, password_hash := PasswordHash} = UserInfo} -> Salt = maps:get(salt, UserInfo, <<>>), - insert_user(UserGroup, UserID, PasswordHash, Salt), + IsSuperuser = maps:get(is_superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), import(UserGroup, File, Seq); {error, Reason} -> {error, Reason} @@ -307,8 +331,6 @@ get_csv_header(File) -> get_user_info_by_seq(Fields, Seq) -> get_user_info_by_seq(Fields, Seq, #{}). -get_user_info_by_seq([], [], #{user_id := _, password_hash := _, salt := _} = Acc) -> - {ok, Acc}; get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) -> {ok, Acc}; get_user_info_by_seq(_, [], _) -> @@ -319,19 +341,13 @@ get_user_info_by_seq([PasswordHash | More1], [<<"password_hash">> | More2], Acc) get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash}); get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) -> get_user_info_by_seq(More1, More2, Acc#{salt => Salt}); +get_user_info_by_seq([<<"true">> | More1], [<<"is_superuser">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{is_superuser => true}); +get_user_info_by_seq([<<"false">> | More1], [<<"is_superuser">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{is_superuser => false}); get_user_info_by_seq(_, _, _) -> {error, bad_format}. --compile({inline, [add/3]}). -add(UserID, Password, #{user_group := UserGroup, - password_hash_algorithm := Algorithm} = State) -> - Salt = gen_salt(State), - PasswordHash = hash(Algorithm, Password, Salt), - case Algorithm of - bcrypt -> insert_user(UserGroup, UserID, PasswordHash); - _ -> insert_user(UserGroup, UserID, PasswordHash, Salt) - end. - gen_salt(#{password_hash_algorithm := plain}) -> <<>>; gen_salt(#{password_hash_algorithm := bcrypt, @@ -347,13 +363,16 @@ hash(bcrypt, Password, Salt) -> hash(Algorithm, Password, Salt) -> emqx_passwd:hash(Algorithm, <>). -insert_user(UserGroup, UserID, PasswordHash) -> - insert_user(UserGroup, UserID, PasswordHash, <<>>). +hash(Password, #{password_hash_algorithm := Algorithm} = State) -> + Salt = gen_salt(State), + PasswordHash = hash(Algorithm, Password, Salt), + {PasswordHash, Salt}. -insert_user(UserGroup, UserID, PasswordHash, Salt) -> +insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) -> UserInfo = #user_info{user_id = {UserGroup, UserID}, password_hash = PasswordHash, - salt = Salt}, + salt = Salt, + is_superuser = IsSuperuser}, mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> @@ -376,8 +395,10 @@ trans(Fun, Args) -> {aborted, Reason} -> {error, Reason} end. - to_binary(B) when is_binary(B) -> B; to_binary(L) when is_list(L) -> iolist_to_binary(L). + +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) -> + #{user_id => UserID, is_superuser => IsSuperuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index ff1b2161a..9d77c673c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -21,12 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ structs/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -36,14 +39,14 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [""]. +namespace() -> "authn:password-based:mongodb". -fields("") -> - [ {config, {union, [ hoconsc:t(standalone) - , hoconsc:t('replica-set') - , hoconsc:t('sharded-cluster') +roots() -> + [ {config, {union, [ hoconsc:mk(standalone) + , hoconsc:mk('replica-set') + , hoconsc:mk('sharded-cluster') ]}} - ]; + ]. fields(standalone) -> common_fields() ++ emqx_connector_mongo:fields(single); @@ -55,16 +58,16 @@ fields('sharded-cluster') -> common_fields() ++ emqx_connector_mongo:fields(sharded). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [mongodb]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [mongodb]}} , {collection, fun collection/1} , {selector, fun selector/1} , {password_hash_field, fun password_hash_field/1} , {salt_field, fun salt_field/1} + , {is_superuser_field, fun is_superuser_field/1} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} - ]. + ] ++ emqx_authn_schema:common_fields(). collection(type) -> binary(); collection(nullable) -> false; @@ -82,6 +85,10 @@ salt_field(type) -> binary(); salt_field(nullable) -> true; salt_field(_) -> undefined. +is_superuser_field(type) -> binary(); +is_superuser_field(nullable) -> true; +is_superuser_field(_) -> undefined. + password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; password_hash_algorithm(default) -> sha256; password_hash_algorithm(_) -> undefined. @@ -94,6 +101,12 @@ salt_position(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, standalone) + , hoconsc:ref(?MODULE, 'replica-set') + , hoconsc:ref(?MODULE, 'sharded-cluster') + ]. + create(#{ selector := Selector , '_unique' := Unique } = Config) -> @@ -101,14 +114,15 @@ create(#{ selector := Selector State = maps:with([ collection , password_hash_field , salt_field + , is_superuser_field , password_hash_algorithm , salt_position , '_unique'], Config), NState = State#{selector => NSelector}, case emqx_resource:create_local(Unique, emqx_connector_mongo, Config) of - {ok, _} -> + {ok, already_created} -> {ok, NState}; - {error, already_created} -> + {ok, _} -> {ok, NState}; {error, Reason} -> {error, Reason} @@ -140,7 +154,8 @@ authenticate(#{password := Password} = Credential, ignore; Doc -> case check_password(Password, Doc, State) of - ok -> ok; + ok -> + {ok, #{is_superuser => is_superuser(Doc, State)}}; {error, {cannot_find_password_hash_field, PasswordHashField}} -> ?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]), {error, bad_username_or_password}; @@ -221,6 +236,11 @@ check_password(Password, end end. +is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) -> + maps:get(IsSuperuserField, Doc, false); +is_superuser(_, _) -> + false. + hash(Algorithm, Password, Salt, prefix) -> emqx_passwd:hash(Algorithm, <>); hash(Algorithm, Password, Salt, suffix) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index f2a01e7e1..991bb6aee 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -21,12 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ structs/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -36,17 +39,19 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +namespace() -> "authn:password-based:mysql". + +roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [mysql]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [mysql]}} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} , {query, fun query/1} , {query_timeout, fun query_timeout/1} - ] ++ emqx_connector_schema_lib:relational_db_fields() + ] ++ emqx_authn_schema:common_fields() + ++ emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; @@ -69,6 +74,9 @@ query_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ password_hash_algorithm := Algorithm , salt_position := SaltPosition , query := Query0 @@ -83,9 +91,9 @@ create(#{ password_hash_algorithm := Algorithm query_timeout => QueryTimeout, '_unique' => Unique}, case emqx_resource:create_local(Unique, emqx_connector_mysql, Config) of - {ok, _} -> + {ok, already_created} -> {ok, State}; - {error, already_created} -> + {ok, _} -> {ok, State}; {error, Reason} -> {error, Reason} @@ -112,22 +120,26 @@ authenticate(#{password := Password} = Credential, case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> - %% TODO: Support superuser Selected = maps:from_list(lists:zip(Columns, Rows)), - check_password(Password, Selected, State); + case check_password(Password, Selected, State) of + ok -> + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}}; + {error, Reason} -> + {error, Reason} + end; {error, _Reason} -> ignore end catch - error:Reason -> - ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), + error:Error -> + ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]), ignore end. destroy(#{'_unique' := Unique}) -> _ = emqx_resource:remove_local(Unique), ok. - + %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ @@ -135,17 +147,17 @@ destroy(#{'_unique' := Unique}) -> check_password(undefined, _Selected, _State) -> {error, bad_username_or_password}; check_password(Password, - #{password_hash := Hash}, + #{<<"password_hash">> := Hash}, #{password_hash_algorithm := bcrypt}) -> case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of true -> ok; false -> {error, bad_username_or_password} end; check_password(Password, - #{password_hash := Hash} = Selected, + #{<<"password_hash">> := Hash} = Selected, #{password_hash_algorithm := Algorithm, salt_position := SaltPosition}) -> - Salt = maps:get(salt, Selected, <<>>), + Salt = maps:get(<<"salt">>, Selected, <<>>), case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of true -> ok; false -> {error, bad_username_or_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl deleted file mode 100644 index 0f5c8abb8..000000000 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl +++ /dev/null @@ -1,58 +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_authn_other_schema). - --include("emqx_authn.hrl"). --include_lib("typerefl/include/types.hrl"). - --behaviour(hocon_schema). - --export([ structs/0 - , fields/1 - ]). - -structs() -> [ "filename", "position", "user_info", "new_user_info"]. - -fields("filename") -> - [ {filename, fun filename/1} ]; -fields("position") -> - [ {position, fun position/1} ]; -fields("user_info") -> - [ {user_id, fun user_id/1} - , {password, fun password/1} - ]; -fields("new_user_info") -> - [ {password, fun password/1} - ]. - -filename(type) -> string(); -filename(nullable) -> false; -filename(_) -> undefined. - -position(type) -> integer(); -position(validate) -> [fun (Position) -> Position > 0 end]; -position(nullable) -> false; -position(_) -> undefined. - -user_id(type) -> binary(); -user_id(nullable) -> false; -user_id(_) -> undefined. - -password(type) -> binary(); -password(nullable) -> false; -password(_) -> undefined. - diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index b83e111c3..c497074de 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -18,13 +18,19 @@ -include("emqx_authn.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("epgsql/include/epgsql.hrl"). -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ structs/0, fields/1 ]). +-export([ namespace/0 + , roots/0 + , fields/1 + ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -34,16 +40,18 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +namespace() -> "authn:password-based:postgresql". + +roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [pgsql]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [postgresql]}} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, {enum, [prefix, suffix]}} , {query, fun query/1} - ] ++ emqx_connector_schema_lib:relational_db_fields() + ] ++ emqx_authn_schema:common_fields() + ++ emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; @@ -58,6 +66,9 @@ query(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ query := Query0 , password_hash_algorithm := Algorithm , salt_position := SaltPosition @@ -70,9 +81,9 @@ create(#{ query := Query0 salt_position => SaltPosition, '_unique' => Unique}, case emqx_resource:create_local(Unique, emqx_connector_pgsql, Config) of - {ok, _} -> + {ok, already_created} -> {ok, State}; - {error, already_created} -> + {ok, _} -> {ok, State}; {error, Reason} -> {error, Reason} @@ -98,22 +109,27 @@ authenticate(#{password := Password} = Credential, case emqx_resource:query(Unique, {sql, Query, Params}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> - %% TODO: Support superuser - Selected = maps:from_list(lists:zip(Columns, Rows)), - check_password(Password, Selected, State); + NColumns = [Name || #column{name = Name} <- Columns], + Selected = maps:from_list(lists:zip(NColumns, Rows)), + case check_password(Password, Selected, State) of + ok -> + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}}; + {error, Reason} -> + {error, Reason} + end; {error, _Reason} -> ignore end catch - error:Reason -> - ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), + error:Error -> + ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]), ignore end. destroy(#{'_unique' := Unique}) -> _ = emqx_resource:remove_local(Unique), ok. - + %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ @@ -121,17 +137,17 @@ destroy(#{'_unique' := Unique}) -> check_password(undefined, _Selected, _State) -> {error, bad_username_or_password}; check_password(Password, - #{password_hash := Hash}, + #{<<"password_hash">> := Hash}, #{password_hash_algorithm := bcrypt}) -> case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of true -> ok; false -> {error, bad_username_or_password} end; check_password(Password, - #{password_hash := Hash} = Selected, + #{<<"password_hash">> := Hash} = Selected, #{password_hash_algorithm := Algorithm, salt_position := SaltPosition}) -> - Salt = maps:get(salt, Selected, <<>>), + Salt = maps:get(<<"salt">>, Selected, <<>>), case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of true -> ok; false -> {error, bad_username_or_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 5d6e579ac..949aeeaea 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -21,12 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ structs/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -36,14 +39,14 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [""]. +namespace() -> "authn:password-based:redis". -fields("") -> - [ {config, {union, [ hoconsc:t(standalone) - , hoconsc:t(cluster) - , hoconsc:t(sentinel) +roots() -> + [ {config, {union, [ hoconsc:mk(standalone) + , hoconsc:mk(cluster) + , hoconsc:mk(sentinel) ]}} - ]; + ]. fields(standalone) -> common_fields() ++ emqx_connector_redis:fields(single); @@ -55,13 +58,12 @@ fields(sentinel) -> common_fields() ++ emqx_connector_redis:fields(sentinel). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [redis]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [redis]}} , {query, fun query/1} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} - ]. + ] ++ emqx_authn_schema:common_fields(). query(type) -> string(); query(nullable) -> false; @@ -79,6 +81,12 @@ salt_position(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, standalone) + , hoconsc:ref(?MODULE, cluster) + , hoconsc:ref(?MODULE, sentinel) + ]. + create(#{ query := Query , '_unique' := Unique } = Config) -> @@ -89,9 +97,9 @@ create(#{ query := Query , '_unique'], Config), NState = State#{query => NQuery}, case emqx_resource:create_local(Unique, emqx_connector_redis, Config) of - {ok, _} -> + {ok, already_created} -> {ok, NState}; - {error, already_created} -> + {ok, _} -> {ok, NState}; {error, Reason} -> {error, Reason} @@ -124,7 +132,13 @@ authenticate(#{password := Password} = Credential, NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))), case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of {ok, Values} -> - check_password(Password, merge(Fields, Values), State); + Selected = merge(Fields, Values), + case check_password(Password, Selected, State) of + ok -> + {ok, #{is_superuser => maps:get("is_superuser", Selected, false)}}; + {error, Reason} -> + {error, Reason} + end; {error, Reason} -> ?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]), ignore @@ -166,11 +180,11 @@ check_fields(["password_hash" | More], false) -> check_fields(More, true); check_fields(["salt" | More], HasPassHash) -> check_fields(More, HasPassHash); -% check_fields(["is_superuser" | More], HasPassHash) -> -% check_fields(More, HasPassHash); +check_fields(["is_superuser" | More], HasPassHash) -> + check_fields(More, HasPassHash); check_fields([Field | _], _) -> error({unsupported_field, Field}). - + parse_key(Key) -> Tokens = re:split(Key, "(" ++ ?RE_PLACEHOLDER ++ ")", [{return, binary}, group, trim]), parse_key(Tokens, []). diff --git a/apps/emqx_authn/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv index 2543d39ca..cbadaefbc 100644 --- a/apps/emqx_authn/test/data/user-credentials.csv +++ b/apps/emqx_authn/test/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt -myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 -myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 +user_id,password_hash,salt,is_superuser +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json index 169122bd2..94375df22 100644 --- a/apps/emqx_authn/test/data/user-credentials.json +++ b/apps/emqx_authn/test/data/user-credentials.json @@ -2,11 +2,13 @@ { "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", - "salt": "e378187547bf2d6f0545a3f441aa4d8a" + "salt": "e378187547bf2d6f0545a3f441aa4d8a", + "is_superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", - "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", + "is_superuser": false } ] diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 92e506d51..74ec397cc 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -19,89 +19,4 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --include("emqx_authn.hrl"). - --define(AUTH, emqx_authn). - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - application:set_env(ekka, strict_mode, true), - emqx_ct_helpers:start_apps([emqx_authn]), - Config. - -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. - -t_chain(_) -> - ?assertMatch({ok, #{id := ?CHAIN, authenticators := []}}, ?AUTH:lookup_chain(?CHAIN)), - - ChainID = <<"mychain">>, - Chain = #{id => ChainID}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)), - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), - ok. - -t_authenticator(_) -> - AuthenticatorName1 = <<"myauthenticator1">>, - AuthenticatorConfig1 = #{name => AuthenticatorName1, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), - ?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)), - ?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), - - AuthenticatorConfig2 = #{name => AuthenticatorName1, - mechanism => jwt, - use_jwks => false, - algorithm => 'hmac-based', - secret => <<"abcdef">>, - secret_base64_encoded => false, - verify_claims => []}, - {ok, #{name := AuthenticatorName1, id := ID1, mechanism := jwt}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2), - - ID2 = <<"random">>, - ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), - ?assertEqual({error, name_has_be_used}, ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), - - AuthenticatorName2 = <<"myauthenticator2">>, - AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2}, - {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"abcdef">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3), - ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)), - {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"fedcba">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}), - - ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)), - ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), - ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 3)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 0)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), - ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), - ok. - -t_authenticate(_) -> - ClientInfo = #{zone => default, - listener => mqtt_tcp, - username => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), - ?assertEqual(false, emqx_authn:is_enabled()), - emqx_authn:enable(), - ?assertEqual(true, emqx_authn:is_enabled()), - ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo)). +all() -> emqx_ct:all(?MODULE). \ No newline at end of file diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 7435deaa0..16c04771d 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -19,134 +19,140 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). +% -include_lib("common_test/include/ct.hrl"). +% -include_lib("eunit/include/eunit.hrl"). --include("emqx_authn.hrl"). +% -include("emqx_authn.hrl"). --define(AUTH, emqx_authn). +% -define(AUTH, emqx_authn). all() -> emqx_ct:all(?MODULE). -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authn]), - Config. +% init_per_suite(Config) -> +% emqx_ct_helpers:start_apps([emqx_authn]), +% Config. -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. +% end_per_suite(_) -> +% emqx_ct_helpers:stop_apps([emqx_authn]), +% ok. -t_jwt_authenticator(_) -> - AuthenticatorName = <<"myauthenticator">>, - Config = #{name => AuthenticatorName, - mechanism => jwt, - use_jwks => false, - algorithm => 'hmac-based', - secret => <<"abcdef">>, - secret_base64_encoded => false, - verify_claims => []}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), +% t_jwt_authenticator(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% Config = #{name => AuthenticatorName, +% mechanism => jwt, +% use_jwks => false, +% algorithm => 'hmac-based', +% secret => <<"abcdef">>, +% secret_base64_encoded => false, +% verify_claims => []}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), - Payload = #{<<"username">> => <<"myuser">>}, - JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), - ClientInfo = #{username => <<"myuser">>, - password => JWS}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), +% Payload = #{<<"username">> => <<"myuser">>}, +% JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), +% ClientInfo = #{username => <<"myuser">>, +% password => JWS}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), - ClientInfo2 = ClientInfo#{password => BadJWS}, - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), +% Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true}, +% JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>), +% ClientInfo1 = #{username => <<"myuser">>, +% password => JWS1}, +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), - %% secret_base64_encoded - Config2 = Config#{secret => base64:encode(<<"abcdef">>), - secret_base64_encoded => true}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), +% BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), +% ClientInfo2 = ClientInfo#{password => BadJWS}, +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), - Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), +% %% secret_base64_encoded +% Config2 = Config#{secret => base64:encode(<<"abcdef">>), +% secret_base64_encoded => true}, +% ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - %% Expiration - Payload3 = #{ <<"username">> => <<"myuser">> - , <<"exp">> => erlang:system_time(second) - 60}, - JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), - ClientInfo3 = ClientInfo#{password => JWS3}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), +% Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, +% ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), - Payload4 = #{ <<"username">> => <<"myuser">> - , <<"exp">> => erlang:system_time(second) + 60}, - JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), - ClientInfo4 = ClientInfo#{password => JWS4}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), +% %% Expiration +% Payload3 = #{ <<"username">> => <<"myuser">> +% , <<"exp">> => erlang:system_time(second) - 60}, +% JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), +% ClientInfo3 = ClientInfo#{password => JWS3}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), - %% Issued At - Payload5 = #{ <<"username">> => <<"myuser">> - , <<"iat">> => erlang:system_time(second) - 60}, - JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), - ClientInfo5 = ClientInfo#{password => JWS5}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)), +% Payload4 = #{ <<"username">> => <<"myuser">> +% , <<"exp">> => erlang:system_time(second) + 60}, +% JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), +% ClientInfo4 = ClientInfo#{password => JWS4}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - Payload6 = #{ <<"username">> => <<"myuser">> - , <<"iat">> => erlang:system_time(second) + 60}, - JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), - ClientInfo6 = ClientInfo#{password => JWS6}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ok)), +% %% Issued At +% Payload5 = #{ <<"username">> => <<"myuser">> +% , <<"iat">> => erlang:system_time(second) - 60}, +% JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), +% ClientInfo5 = ClientInfo#{password => JWS5}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), - %% Not Before - Payload7 = #{ <<"username">> => <<"myuser">> - , <<"nbf">> => erlang:system_time(second) - 60}, - JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), - ClientInfo7 = ClientInfo#{password => JWS7}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)), +% Payload6 = #{ <<"username">> => <<"myuser">> +% , <<"iat">> => erlang:system_time(second) + 60}, +% JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), +% ClientInfo6 = ClientInfo#{password => JWS6}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)), - Payload8 = #{ <<"username">> => <<"myuser">> - , <<"nbf">> => erlang:system_time(second) + 60}, - JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), - ClientInfo8 = ClientInfo#{password => JWS8}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)), +% %% Not Before +% Payload7 = #{ <<"username">> => <<"myuser">> +% , <<"nbf">> => erlang:system_time(second) - 60}, +% JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), +% ClientInfo7 = ClientInfo#{password => JWS7}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% Payload8 = #{ <<"username">> => <<"myuser">> +% , <<"nbf">> => erlang:system_time(second) + 60}, +% JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), +% ClientInfo8 = ClientInfo#{password => JWS8}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)), -t_jwt_authenticator2(_) -> - Dir = code:lib_dir(emqx_authn, test), - PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), - PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), - AuthenticatorName = <<"myauthenticator">>, - Config = #{name => AuthenticatorName, - mechanism => jwt, - use_jwks => false, - algorithm => 'public-key', - certificate => PublicKey, - verify_claims => []}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ok. - Payload = #{<<"username">> => <<"myuser">>}, - JWS = generate_jws('public-key', Payload, PrivateKey), - ClientInfo = #{username => <<"myuser">>, - password => JWS}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), +% t_jwt_authenticator2(_) -> +% Dir = code:lib_dir(emqx_authn, test), +% PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), +% PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), +% AuthenticatorName = <<"myauthenticator">>, +% Config = #{name => AuthenticatorName, +% mechanism => jwt, +% use_jwks => false, +% algorithm => 'public-key', +% certificate => PublicKey, +% verify_claims => []}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% Payload = #{<<"username">> => <<"myuser">>}, +% JWS = generate_jws('public-key', Payload, PrivateKey), +% ClientInfo = #{username => <<"myuser">>, +% password => JWS}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), -generate_jws('hmac-based', Payload, Secret) -> - JWK = jose_jwk:from_oct(Secret), - Header = #{ <<"alg">> => <<"HS256">> - , <<"typ">> => <<"JWT">> - }, - Signed = jose_jwt:sign(JWK, Header, Payload), - {_, JWS} = jose_jws:compact(Signed), - JWS; -generate_jws('public-key', Payload, PrivateKey) -> - JWK = jose_jwk:from_pem_file(PrivateKey), - Header = #{ <<"alg">> => <<"RS256">> - , <<"typ">> => <<"JWT">> - }, - Signed = jose_jwt:sign(JWK, Header, Payload), - {_, JWS} = jose_jws:compact(Signed), - JWS. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ok. + +% generate_jws('hmac-based', Payload, Secret) -> +% JWK = jose_jwk:from_oct(Secret), +% Header = #{ <<"alg">> => <<"HS256">> +% , <<"typ">> => <<"JWT">> +% }, +% Signed = jose_jwt:sign(JWK, Header, Payload), +% {_, JWS} = jose_jws:compact(Signed), +% JWS; +% generate_jws('public-key', Payload, PrivateKey) -> +% JWK = jose_jwk:from_pem_file(PrivateKey), +% Header = #{ <<"alg">> => <<"RS256">> +% , <<"typ">> => <<"JWT">> +% }, +% Signed = jose_jwt:sign(JWK, Header, Payload), +% {_, JWS} = jose_jws:compact(Signed), +% JWS. diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index 4a5a24844..959cf0323 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -19,137 +19,146 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). +% -include_lib("common_test/include/ct.hrl"). +% -include_lib("eunit/include/eunit.hrl"). --include("emqx_authn.hrl"). +% -include("emqx_authn.hrl"). --define(AUTH, emqx_authn). +% -define(AUTH, emqx_authn). all() -> emqx_ct:all(?MODULE). -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authn]), - Config. +% init_per_suite(Config) -> +% emqx_ct_helpers:start_apps([emqx_authn]), +% Config. -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. +% end_per_suite(_) -> +% emqx_ct_helpers:stop_apps([emqx_authn]), +% ok. -t_mnesia_authenticator(_) -> - AuthenticatorName = <<"myauthenticator">>, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% t_mnesia_authenticator(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% AuthenticatorConfig = #{name => AuthenticatorName, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - UserInfo = #{user_id => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% UserInfo = #{user_id => <<"myuser">>, +% password => <<"mypass">>}, +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ClientInfo = #{zone => external, - username => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), - ?AUTH:enable(), - ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), +% ClientInfo = #{zone => external, +% username => <<"myuser">>, +% password => <<"mypass">>}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?AUTH:enable(), +% ?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)), - ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), - ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), +% ClientInfo2 = ClientInfo#{username => <<"baduser">>}, +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), - ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), - ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), +% ClientInfo3 = ClientInfo#{password => <<"badpass">>}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), - UserInfo2 = UserInfo#{password => <<"mypass2">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), - ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), +% UserInfo2 = UserInfo#{password => <<"mypass2">>}, +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), +% ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{is_superuser => true})), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), +% ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ok. +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), -t_import(_) -> - AuthenticatorName = <<"myauthenticator">>, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), +% ok. - Dir = code:lib_dir(emqx_authn, test), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))), - ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)), - ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)), +% t_import(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% AuthenticatorConfig = #{name => AuthenticatorName, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - ClientInfo1 = #{username => <<"myuser1">>, - password => <<"mypassword1">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, - password => <<"mypassword3">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% Dir = code:lib_dir(emqx_authn, test), +% ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))), +% ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))), +% ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)), +% ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)), -t_multi_mnesia_authenticator(_) -> - AuthenticatorName1 = <<"myauthenticator1">>, - AuthenticatorConfig1 = #{name => AuthenticatorName1, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - AuthenticatorName2 = <<"myauthenticator2">>, - AuthenticatorConfig2 = #{name => AuthenticatorName2, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => clientid, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), - {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), +% ClientInfo1 = #{username => <<"myuser1">>, +% password => <<"mypassword1">>}, +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, - ?AUTH:add_user(?CHAIN, ID1, - #{user_id => <<"myuser">>, - password => <<"mypass1">>})), - ?assertEqual({ok, #{user_id => <<"myclient">>}}, - ?AUTH:add_user(?CHAIN, ID2, - #{user_id => <<"myclient">>, - password => <<"mypass2">>})), +% ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, +% password => <<"mypassword2">>}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), - ClientInfo1 = #{username => <<"myuser">>, - clientid => <<"myclient">>, - password => <<"mypass1">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), +% ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, +% password => <<"mypassword3">>}, +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)), - ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ok. - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), - ok. +% t_multi_mnesia_authenticator(_) -> +% AuthenticatorName1 = <<"myauthenticator1">>, +% AuthenticatorConfig1 = #{name => AuthenticatorName1, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% AuthenticatorName2 = <<"myauthenticator2">>, +% AuthenticatorConfig2 = #{name => AuthenticatorName2, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => clientid, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), +% {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), + +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, +% ?AUTH:add_user(?CHAIN, ID1, +% #{user_id => <<"myuser">>, +% password => <<"mypass1">>})), +% ?assertMatch({ok, #{user_id := <<"myclient">>}}, +% ?AUTH:add_user(?CHAIN, ID2, +% #{user_id => <<"myclient">>, +% password => <<"mypass2">>})), + +% ClientInfo1 = #{username => <<"myuser">>, +% clientid => <<"myclient">>, +% password => <<"mypass1">>}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), + +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), + +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), +% ok. diff --git a/apps/emqx_authz/etc/acl.conf b/apps/emqx_authz/etc/acl.conf new file mode 100644 index 000000000..2948f2af7 --- /dev/null +++ b/apps/emqx_authz/etc/acl.conf @@ -0,0 +1,28 @@ +%%-------------------------------------------------------------------- +%% -type(ipaddr() :: {ipaddr, string()}). +%% +%% -type(ipaddrs() :: {ipaddrs, string()}). +%% +%% -type(username() :: {username, regex()}). +%% +%% -type(clientid() :: {clientid, regex()}). +%% +%% -type(who() :: ipaddr() | ipaddrs() |username() | clientid() | +%% {'and', [ipaddr() | ipaddrs()| username() | clientid()]} | +%% {'or', [ipaddr() | ipaddrs()| username() | clientid()]} | +%% all). +%% +%% -type(action() :: subscribe | publish | all). +%% +%% -type(topic_filters() :: string()). +%% +%% -type(topics() :: [topic_filters() | {eq, topic_filters()}]). +%% +%% -type(permission() :: allow | deny). +%% +%% -type(rule() :: {permission(), who(), access(), topics()}). +%%-------------------------------------------------------------------- + +{allow, {username, "^dashboard?"}, subscribe, ["$SYS/#"]}. + +{allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}. diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 8826a94f7..ed4ad573c 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,74 +1,63 @@ -authorization:{ - rules: [ +authorization { + sources = [ # { # type: http - # config: { - # url: "https://emqx.com" - # headers: { - # Accept: "application/json" - # Content-Type: "application/json" - # } + # url: "https://emqx.com" + # headers: { + # Accept: "application/json" + # Content-Type: "application/json" # } # }, # { # type: mysql - # config: { - # server: "127.0.0.1:3306" - # database: mqtt - # pool_size: 1 - # username: root - # password: public - # auto_reconnect: true - # ssl: { - # enable: true - # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" - # certfile: "{{ platform_etc_dir }}/certs/client-cert.pem" - # keyfile: "{{ platform_etc_dir }}/certs/client-key.pem" - # } + # server: "127.0.0.1:3306" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: { + # enable: true + # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + # certfile: "{{ platform_etc_dir }}/certs/client-cert.pem" + # keyfile: "{{ platform_etc_dir }}/certs/client-key.pem" # } # sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or clientid = '%c'" # }, # { # type: pgsql - # config: { - # server: "127.0.0.1:5432" - # database: mqtt - # pool_size: 1 - # username: root - # password: public - # auto_reconnect: true - # ssl: {enable: false} - # } + # server: "127.0.0.1:5432" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: {enable: false} # sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'" # }, # { # type: redis - # config: { - # server: "127.0.0.1:6379" - # database: 0 - # pool_size: 1 - # password: public - # auto_reconnect: true - # ssl: {enable: false} - # } + # server: "127.0.0.1:6379" + # database: 0 + # pool_size: 1 + # password: public + # auto_reconnect: true + # ssl: {enable: false} # cmd: "HGETALL mqtt_authz:%u" # }, # { # type: mongo - # config: { - # mongo_type: single - # server: "127.0.0.1:27017" - # pool_size: 1 - # database: mqtt - # ssl: {enable: false} - # } + # mongo_type: single + # server: "127.0.0.1:27017" + # pool_size: 1 + # database: mqtt + # ssl: {enable: false} # collection: mqtt_authz # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } # }, { - permission: allow - action: all - topics: ["#"] + type: file + path: "{{ platform_etc_dir }}/acl.conf" } ] } diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index 76aa20688..83d7601c6 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -1,10 +1,33 @@ --type(rule() :: #{atom() => any()}). +-type(ipaddress() :: {ipaddr, esockd_cidr:cidr_string()} | + {ipaddrs, list(esockd_cidr:cidr_string())}). + +-type(username() :: {username, binary()}). + +-type(clientid() :: {clientid, binary()}). + +-type(who() :: ipaddress() | username() | clientid() | + {'and', [ipaddress() | username() | clientid()]} | + {'or', [ipaddress() | username() | clientid()]} | + all). + +-type(action() :: subscribe | publish | all). + +-type(permission() :: allow | deny). + +-type(rule() :: {permission(), who(), action(), list(emqx_topic:topic())}). -type(rules() :: [rule()]). +-type(sources() :: [map()]). + -define(APP, emqx_authz). --define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). --define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= all))). +-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse + (A =:= deny) orelse (A =:= <<"deny">>) + )). +-define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= <<"subscribe">>) orelse + (A =:= publish) orelse (A =:= <<"publish">>) orelse + (A =:= all) orelse (A =:= <<"all">>) + )). -record(authz_metrics, { allow = 'client.authorize.allow', diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 5c46e7749..e0e584806 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -27,18 +27,19 @@ -export([ register_metrics/0 , init/0 - , init_rule/1 , lookup/0 , lookup/1 , move/2 + , move/3 , update/2 + , update/3 , authorize/5 - , match/4 ]). --export([post_config_update/3, pre_config_update/2]). +-export([post_config_update/4, pre_config_update/2]). --define(CONF_KEY_PATH, [authorization, rules]). +-define(CONF_KEY_PATH, [authorization, sources]). +-define(SOURCE_TYPES, [file, http, mongo, mysql, pgsql, redis]). -spec(register_metrics() -> ok). register_metrics() -> @@ -47,310 +48,273 @@ register_metrics() -> init() -> ok = register_metrics(), emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), - NRules = [init_rule(Rule) || Rule <- emqx_config:get(?CONF_KEY_PATH, [])], - ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). + Sources = emqx:get_config(?CONF_KEY_PATH, []), + ok = check_dup_types(Sources), + NSources = [init_source(Source) || Source <- Sources], + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NSources]}, -1). lookup() -> {_M, _F, [A]}= find_action_in_hooks(), A. -lookup(Id) -> - try find_rule_by_id(Id, lookup()) of - {_, Rule} -> Rule +lookup(Type) -> + try find_source_by_type(atom(Type), lookup()) of + {_, Source} -> Source catch error:Reason -> {error, Reason} end. -move(Id, Position) -> - emqx_config:update(emqx_authz_schema, ?CONF_KEY_PATH, {move, Id, Position}). +move(Type, Cmd) -> + move(Type, Cmd, #{}). -update(Cmd, Rules) -> - emqx_config:update(emqx_authz_schema, ?CONF_KEY_PATH, {Cmd, Rules}). +move(Type, #{<<"before">> := Before}, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}, Opts); +move(Type, #{<<"after">> := After}, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}, Opts); +move(Type, Position, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}, Opts). -pre_config_update({move, Id, <<"top">>}, Conf) when is_list(Conf) -> - {Index, _} = find_rule_by_id(Id), +update(Cmd, Sources) -> + update(Cmd, Sources, #{}). + +update({replace_once, Type}, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}, Opts); +update({delete_once, Type}, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}, Opts); +update(Cmd, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}, Opts). + +pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2; + NConf = [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2, + ok = check_dup_types(NConf), + {ok, NConf}; -pre_config_update({move, Id, <<"bottom">>}, Conf) when is_list(Conf) -> - {Index, _} = find_rule_by_id(Id), +pre_config_update({move, Type, <<"bottom">>}, Conf) when is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]; + NConf = lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)], + ok = check_dup_types(NConf), + {ok, NConf}; -pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Conf) -> - {Index1, _} = find_rule_by_id(Id), +pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Conf) -> + {Index1, _} = find_source_by_type(Type), Conf1 = lists:nth(Index1, Conf), - {Index2, _} = find_rule_by_id(BeforeId), + {Index2, _} = find_source_by_type(Before), Conf2 = lists:nth(Index2, Conf), {List1, List2} = lists:split(Index2, Conf), - lists:delete(Conf1, lists:droplast(List1)) - ++ [Conf1] ++ [Conf2] - ++ lists:delete(Conf1, List2); + NConf = lists:delete(Conf1, lists:droplast(List1)) + ++ [Conf1] ++ [Conf2] + ++ lists:delete(Conf1, List2), + ok = check_dup_types(NConf), + {ok, NConf}; -pre_config_update({move, Id, #{<<"after">> := AfterId}}, Conf) when is_list(Conf) -> - {Index1, _} = find_rule_by_id(Id), +pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf) -> + {Index1, _} = find_source_by_type(Type), Conf1 = lists:nth(Index1, Conf), - {Index2, _} = find_rule_by_id(AfterId), + {Index2, _} = find_source_by_type(After), {List1, List2} = lists:split(Index2, Conf), - lists:delete(Conf1, List1) - ++ [Conf1] - ++ lists:delete(Conf1, List2); + NConf = lists:delete(Conf1, List1) + ++ [Conf1] + ++ lists:delete(Conf1, List2), + ok = check_dup_types(NConf), + {ok, NConf}; -pre_config_update({head, Rules}, Conf) when is_list(Rules), is_list(Conf) -> - Rules ++ Conf; -pre_config_update({tail, Rules}, Conf) when is_list(Rules), is_list(Conf) -> - Conf ++ Rules; -pre_config_update({{replace_once, Id}, Rule}, Conf) when is_map(Rule), is_list(Conf) -> - {Index, _} = find_rule_by_id(Id), +pre_config_update({head, Sources}, Conf) when is_list(Sources), is_list(Conf) -> + NConf = Sources ++ Conf, + ok = check_dup_types(NConf), + {ok, Sources ++ Conf}; +pre_config_update({tail, Sources}, Conf) when is_list(Sources), is_list(Conf) -> + NConf = Conf ++ Sources, + ok = check_dup_types(NConf), + {ok, Conf ++ Sources}; +pre_config_update({{replace_once, Type}, Source}, Conf) when is_map(Source), is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - lists:droplast(List1) ++ [Rule] ++ List2; -pre_config_update({_, Rules}, _Conf) when is_list(Rules)-> + NConf = lists:droplast(List1) ++ [Source] ++ List2, + ok = check_dup_types(NConf), + {ok, NConf}; +pre_config_update({{delete_once, Type}, _Source}, Conf) when is_list(Conf) -> + {_, Source} = find_source_by_type(Type), + NConf = lists:delete(Source, Conf), + ok = check_dup_types(NConf), + {ok, NConf}; +pre_config_update({_, Sources}, _Conf) when is_list(Sources)-> %% overwrite the entire config! - Rules. + {ok, Sources}. -post_config_update(_, undefined, _Conf) -> +post_config_update(_, undefined, _Conf, _AppEnvs) -> ok; -post_config_update({move, Id, <<"top">>}, _NewRules, _OldRules) -> - InitedRules = lookup(), - {Index, Rule} = find_rule_by_id(Id, InitedRules), - {Rules1, Rules2 } = lists:split(Index, InitedRules), - Rules3 = [Rule] ++ lists:droplast(Rules1) ++ Rules2, - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), +post_config_update({move, Type, <<"top">>}, _NewSources, _OldSources, _AppEnvs) -> + InitedSources = lookup(), + {Index, Source} = find_source_by_type(Type, InitedSources), + {Sources1, Sources2 } = lists:split(Index, InitedSources), + Sources3 = [Source] ++ lists:droplast(Sources1) ++ Sources2, + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, <<"bottom">>}, _NewRules, _OldRules) -> - InitedRules = lookup(), - {Index, Rule} = find_rule_by_id(Id, InitedRules), - {Rules1, Rules2 } = lists:split(Index, InitedRules), - Rules3 = lists:droplast(Rules1) ++ Rules2 ++ [Rule], - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), +post_config_update({move, Type, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) -> + InitedSources = lookup(), + {Index, Source} = find_source_by_type(Type, InitedSources), + {Sources1, Sources2 } = lists:split(Index, InitedSources), + Sources3 = lists:droplast(Sources1) ++ Sources2 ++ [Source], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewRules, _OldRules) -> - InitedRules = lookup(), - {_, Rule0} = find_rule_by_id(Id, InitedRules), - {Index, Rule1} = find_rule_by_id(BeforeId, InitedRules), - {Rules1, Rules2} = lists:split(Index, InitedRules), - Rules3 = lists:delete(Rule0, lists:droplast(Rules1)) - ++ [Rule0] ++ [Rule1] - ++ lists:delete(Rule0, Rules2), - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), +post_config_update({move, Type, #{<<"before">> := Before}}, _NewSources, _OldSources, _AppEnvs) -> + InitedSources = lookup(), + {_, Source0} = find_source_by_type(Type, InitedSources), + {Index, Source1} = find_source_by_type(Before, InitedSources), + {Sources1, Sources2} = lists:split(Index, InitedSources), + Sources3 = lists:delete(Source0, lists:droplast(Sources1)) + ++ [Source0] ++ [Source1] + ++ lists:delete(Source0, Sources2), + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewRules, _OldRules) -> - InitedRules = lookup(), - {_, Rule} = find_rule_by_id(Id, InitedRules), - {Index, _} = find_rule_by_id(AfterId, InitedRules), - {Rules1, Rules2} = lists:split(Index, InitedRules), - Rules3 = lists:delete(Rule, Rules1) - ++ [Rule] - ++ lists:delete(Rule, Rules2), - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), +post_config_update({move, Type, #{<<"after">> := After}}, _NewSources, _OldSources, _AppEnvs) -> + InitedSources = lookup(), + {_, Source} = find_source_by_type(Type, InitedSources), + {Index, _} = find_source_by_type(After, InitedSources), + {Sources1, Sources2} = lists:split(Index, InitedSources), + Sources3 = lists:delete(Source, Sources1) + ++ [Source] + ++ lists:delete(Source, Sources2), + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({head, Rules}, _NewRules, _OldConf) -> - InitedRules = [init_rule(R) || R <- check_rules(Rules)], - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedRules ++ lookup()]}, -1), +post_config_update({head, Sources}, _NewSources, _OldConf, _AppEnvs) -> + InitedSources = [init_source(R) || R <- check_sources(Sources)], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedSources ++ lookup()]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({tail, Rules}, _NewRules, _OldConf) -> - InitedRules = [init_rule(R) || R <- check_rules(Rules)], - emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedRules]}, -1), +post_config_update({tail, Sources}, _NewSources, _OldConf, _AppEnvs) -> + InitedSources = [init_source(R) || R <- check_sources(Sources)], + emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedSources]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({{replace_once, Id}, Rule}, _NewRules, _OldConf) when is_map(Rule) -> - OldInitedRules = lookup(), - {Index, OldRule} = find_rule_by_id(Id, OldInitedRules), - case maps:get(type, OldRule, undefined) of +post_config_update({{replace_once, Type}, #{type := Type} = Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) -> + OldInitedSources = lookup(), + {Index, OldSource} = find_source_by_type(Type, OldInitedSources), + case maps:get(type, OldSource, undefined) of undefined -> ok; + file -> ok; _ -> - #{annotations := #{id := Id}} = OldRule, + #{annotations := #{id := Id}} = OldSource, ok = emqx_resource:remove(Id) end, - {OldRules1, OldRules2 } = lists:split(Index, OldInitedRules), - InitedRules = [init_rule(R#{annotations => #{id => Id}}) || R <- check_rules([Rule])], - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldRules1) ++ InitedRules ++ OldRules2]}, -1), + {OldSources1, OldSources2 } = lists:split(Index, OldInitedSources), + InitedSources = [init_source(R) || R <- check_sources([Source])], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldSources1) ++ InitedSources ++ OldSources2]}, -1), ok = emqx_authz_cache:drain_cache(); - -post_config_update(_, NewRules, _OldConf) -> +post_config_update({{delete_once, Type}, _Source}, _NewSources, _OldConf, _AppEnvs) -> + OldInitedSources = lookup(), + {_, OldSource} = find_source_by_type(Type, OldInitedSources), + case OldSource of + #{annotations := #{id := Id}} -> + ok = emqx_resource:remove(Id); + _ -> ok + end, + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:delete(OldSource, OldInitedSources)]}, -1), + ok = emqx_authz_cache:drain_cache(); +post_config_update(_, NewSources, _OldConf, _AppEnvs) -> %% overwrite the entire config! - OldInitedRules = lookup(), - InitedRules = [init_rule(Rule) || Rule <- NewRules], - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedRules]}, -1), + OldInitedSources = lookup(), + InitedSources = [init_source(Source) || Source <- NewSources], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedSources]}, -1), lists:foreach(fun (#{type := _Type, enable := true, annotations := #{id := Id}}) -> ok = emqx_resource:remove(Id); (_) -> ok - end, OldInitedRules), + end, OldInitedSources), ok = emqx_authz_cache:drain_cache(). %%-------------------------------------------------------------------- -%% Internal functions +%% Initialize source %%-------------------------------------------------------------------- -check_rules(RawRules) -> - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"rules">> => RawRules}}), #{format => richmap}), - CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), - #{authorization := #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), - Rules. - -find_rule_by_id(Id) -> find_rule_by_id(Id, lookup()). -find_rule_by_id(Id, Rules) -> find_rule_by_id(Id, Rules, 1). -find_rule_by_id(_RuleId, [], _N) -> error(not_found_rule); -find_rule_by_id(RuleId, [ Rule = #{annotations := #{id := Id}} | Tail], N) -> - case RuleId =:= Id of - true -> {N, Rule}; - false -> find_rule_by_id(RuleId, Tail, N + 1) +check_dup_types(Sources) -> + check_dup_types(Sources, ?SOURCE_TYPES). +check_dup_types(_Sources, []) -> ok; +check_dup_types(Sources, [T0 | Tail]) -> + case lists:foldl(fun (#{type := T1}, AccIn) -> + case T0 =:= T1 of + true -> AccIn + 1; + false -> AccIn + end; + (#{<<"type">> := T1}, AccIn) -> + case T0 =:= atom(T1) of + true -> AccIn + 1; + false -> AccIn + end + end, 0, Sources) > 1 of + true -> + ?LOG(error, "The type is duplicated in the Authorization source"), + {error, authz_source_dup}; + false -> check_dup_types(Sources, Tail) end. -find_action_in_hooks() -> - Callbacks = emqx_hooks:lookup('client.authorize'), - [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], - Action. - -gen_id(Type) -> - iolist_to_binary([io_lib:format("~s_~s",[?APP, Type]), "_", integer_to_list(erlang:system_time())]). - -create_resource(#{type := DB, - config := Config, - annotations := #{id := ResourceID}}) -> - case emqx_resource:update( - ResourceID, - list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - Config, - []) - of - {ok, _} -> ResourceID; - {error, already_created} -> ResourceID; - {error, Reason} -> {error, Reason} - end; -create_resource(#{type := DB, - config := Config}) -> - ResourceID = gen_id(DB), - case emqx_resource:create( - ResourceID, - list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - Config) - of - {ok, _} -> ResourceID; - {error, already_created} -> ResourceID; - {error, Reason} -> {error, Reason} - end. - --spec(init_rule(rule()) -> rule()). -init_rule(#{topics := Topics, - action := Action, - permission := Permission, - principal := Principal, - annotations := #{id := Id} - } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> - Rule#{annotations => - #{id => Id, - principal => compile_principal(Principal), - topics => [compile_topic(Topic) || Topic <- Topics]} - }; -init_rule(#{topics := Topics, - action := Action, - permission := Permission - } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> - init_rule(Rule#{annotations =>#{id => gen_id(simple)}}); - -init_rule(#{principal := Principal, - enable := true, - type := http, - config := #{url := Url} = Config - } = Rule) -> - NConfig = maps:merge(Config, #{base_url => maps:remove(query, Url)}), - case create_resource(Rule#{config := NConfig}) of +init_source(#{enable := true, + type := file, + path := Path + } = Source) -> + Rules = case file:consult(Path) of + {ok, Terms} -> + [emqx_authz_rule:compile(Term) || Term <- Terms]; + {error, eacces} -> + ?LOG(alert, "Insufficient permissions to read the ~s file", [Path]), + error(eaccess); + {error, enoent} -> + ?LOG(alert, "The ~s file does not exist", [Path]), + error(enoent); + {error, Reason} -> + ?LOG(alert, "Failed to read ~s: ~p", [Path, Reason]), + error(Reason) + end, + Source#{annotations => #{rules => Rules}}; +init_source(#{enable := true, + type := http, + url := Url + } = Source) -> + NSource= maps:put(base_url, maps:remove(query, Url), Source), + case create_resource(NSource) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Rule#{annotations => - #{id => Id, - principal => compile_principal(Principal) - } - } + Id -> Source#{annotations => #{id => Id}} end; - -init_rule(#{principal := Principal, - enable := true, - type := DB - } = Rule) when DB =:= redis; - DB =:= mongo -> - case create_resource(Rule) of +init_source(#{enable := true, + type := DB + } = Source) when DB =:= redis; + DB =:= mongo -> + case create_resource(Source) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Rule#{annotations => - #{id => Id, - principal => compile_principal(Principal) - } - } + Id -> Source#{annotations => #{id => Id}} end; - -init_rule(#{principal := Principal, - enable := true, - type := DB, - sql := SQL - } = Rule) when DB =:= mysql; - DB =:= pgsql -> - Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])), - case create_resource(Rule) of +init_source(#{enable := true, + type := DB, + sql := SQL + } = Source) when DB =:= mysql; + DB =:= pgsql -> + Mod = authz_module(DB), + case create_resource(Source) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Rule#{annotations => + Id -> Source#{annotations => #{id => Id, - principal => compile_principal(Principal), sql => Mod:parse_query(SQL) } } end; - -init_rule(#{enable := false, - type := _DB - } = Rule) -> - Rule. - -compile_principal(all) -> all; -compile_principal(#{username := Username}) -> - {ok, MP} = re:compile(bin(Username)), - #{username => MP}; -compile_principal(#{clientid := Clientid}) -> - {ok, MP} = re:compile(bin(Clientid)), - #{clientid => MP}; -compile_principal(#{ipaddress := IpAddress}) -> - #{ipaddress => esockd_cidr:parse(b2l(IpAddress), true)}; -compile_principal(#{'and' := Principals}) when is_list(Principals) -> - #{'and' => [compile_principal(Principal) || Principal <- Principals]}; -compile_principal(#{'or' := Principals}) when is_list(Principals) -> - #{'or' => [compile_principal(Principal) || Principal <- Principals]}. - -compile_topic(<<"eq ", Topic/binary>>) -> - compile_topic(#{'eq' => Topic}); -compile_topic(#{'eq' := Topic}) -> - #{'eq' => emqx_topic:words(bin(Topic))}; -compile_topic(Topic) when is_binary(Topic)-> - Words = emqx_topic:words(bin(Topic)), - case pattern(Words) of - true -> #{pattern => Words}; - false -> Words - end. - -pattern(Words) -> - lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words). - -bin(A) when is_atom(A) -> atom_to_binary(A, utf8); -bin(B) when is_binary(B) -> B; -bin(L) when is_list(L) -> list_to_binary(L); -bin(X) -> X. - -b2l(B) when is_list(B) -> B; -b2l(B) when is_binary(B) -> binary_to_list(B). +init_source(#{enable := false} = Source) ->Source. %%-------------------------------------------------------------------- %% AuthZ callbacks %%-------------------------------------------------------------------- %% @doc Check AuthZ --spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), allow | deny, rules()) +-spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), allow | deny, sources()) -> {stop, allow} | {ok, deny}). authorize(#{username := Username, peerhost := IpAddress - } = Client, PubSub, Topic, _DefaultResult, Rules) -> - case do_authorize(Client, PubSub, Topic, Rules) of + } = Client, PubSub, Topic, DefaultResult, Sources) -> + case do_authorize(Client, PubSub, Topic, Sources) of {matched, allow} -> ?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), emqx_metrics:inc(?AUTHZ_METRICS(allow)), @@ -361,101 +325,77 @@ authorize(#{username := Username, {stop, deny}; nomatch -> ?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]), - {stop, deny} + {stop, DefaultResult} end. -do_authorize(Client, PubSub, Topic, - [Connector = #{type := DB, - enable := true, - annotations := #{principal := Principal} - } | Tail] ) -> - case match_principal(Client, Principal) of - true -> - Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, DB])), - case Mod:authorize(Client, PubSub, Topic, Connector) of - nomatch -> do_authorize(Client, PubSub, Topic, Tail); - Matched -> Matched - end; - false -> do_authorize(Client, PubSub, Topic, Tail) +do_authorize(_Client, _PubSub, _Topic, []) -> + nomatch; +do_authorize(Client, PubSub, Topic, [#{enable := false} | Rest]) -> + do_authorize(Client, PubSub, Topic, Rest); +do_authorize(Client, PubSub, Topic, [#{type := file} = F | Tail]) -> + #{annotations := #{rules := Rules}} = F, + case emqx_authz_rule:matches(Client, PubSub, Topic, Rules) of + nomatch -> do_authorize(Client, PubSub, Topic, Tail); + Matched -> Matched end; do_authorize(Client, PubSub, Topic, - [#{permission := Permission} = Rule | Tail]) -> - case match(Client, PubSub, Topic, Rule) of - true -> {matched, Permission}; - false -> do_authorize(Client, PubSub, Topic, Tail) + [Connector = #{type := Type} | Tail] ) -> + Mod = authz_module(Type), + case Mod:authorize(Client, PubSub, Topic, Connector) of + nomatch -> do_authorize(Client, PubSub, Topic, Tail); + Matched -> Matched + end. + +%%-------------------------------------------------------------------- +%% Internal function +%%-------------------------------------------------------------------- + +check_sources(RawSources) -> + Schema = #{roots => emqx_authz_schema:fields("authorization"), fields => #{}}, + Conf = #{<<"sources">> => RawSources}, + #{sources := Sources} = hocon_schema:check_plain(Schema, Conf, #{atom_key => true}), + Sources. + +find_source_by_type(Type) -> find_source_by_type(Type, lookup()). +find_source_by_type(Type, Sources) -> find_source_by_type(Type, Sources, 1). +find_source_by_type(_, [], _N) -> error(not_found_source); +find_source_by_type(Type, [ Source = #{type := T} | Tail], N) -> + case Type =:= T of + true -> {N, Source}; + false -> find_source_by_type(Type, Tail, N + 1) + end. + +find_action_in_hooks() -> + Callbacks = emqx_hooks:lookup('client.authorize'), + [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], + Action. + +gen_id(Type) -> + iolist_to_binary([io_lib:format("~s_~s",[?APP, Type])]). + +create_resource(#{type := DB, + annotations := #{id := ResourceID}} = Source) -> + case emqx_resource:update(ResourceID, connector_module(DB), Source, []) of + {ok, _} -> ResourceID; + {error, Reason} -> {error, Reason} end; -do_authorize(_Client, _PubSub, _Topic, []) -> nomatch. +create_resource(#{type := DB} = Source) -> + ResourceID = gen_id(DB), + case emqx_resource:create(ResourceID, connector_module(DB), Source) of + {ok, already_created} -> ResourceID; + {ok, _} -> ResourceID; + {error, Reason} -> {error, Reason} + end. -match(Client, PubSub, Topic, - #{action := Action, - annotations := #{ - principal := Principal, - topics := TopicFilters - } - }) -> - match_action(PubSub, Action) andalso - match_principal(Client, Principal) andalso - match_topics(Client, Topic, TopicFilters). +authz_module(Type) -> + list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)). -match_action(publish, publish) -> true; -match_action(subscribe, subscribe) -> true; -match_action(_, all) -> true; -match_action(_, _) -> false. +connector_module(Type) -> + list_to_existing_atom("emqx_connector_" ++ atom_to_list(Type)). -match_principal(_, all) -> true; -match_principal(#{username := undefined}, #{username := _MP}) -> - false; -match_principal(#{username := Username}, #{username := MP}) -> - case re:run(Username, MP) of - {match, _} -> true; - _ -> false +atom(B) when is_binary(B) -> + try binary_to_existing_atom(B, utf8) + catch + _ -> binary_to_atom(B) end; -match_principal(#{clientid := Clientid}, #{clientid := MP}) -> - case re:run(Clientid, MP) of - {match, _} -> true; - _ -> false - end; -match_principal(#{peerhost := undefined}, #{ipaddress := _CIDR}) -> - false; -match_principal(#{peerhost := IpAddress}, #{ipaddress := CIDR}) -> - esockd_cidr:match(IpAddress, CIDR); -match_principal(ClientInfo, #{'and' := Principals}) when is_list(Principals) -> - lists:foldl(fun(Principal, Permission) -> - match_principal(ClientInfo, Principal) andalso Permission - end, true, Principals); -match_principal(ClientInfo, #{'or' := Principals}) when is_list(Principals) -> - lists:foldl(fun(Principal, Permission) -> - match_principal(ClientInfo, Principal) orelse Permission - end, false, Principals); -match_principal(_, _) -> false. - -match_topics(_ClientInfo, _Topic, []) -> - false; -match_topics(ClientInfo, Topic, [#{pattern := PatternFilter}|Filters]) -> - TopicFilter = feed_var(ClientInfo, PatternFilter), - match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(ClientInfo, Topic, Filters); -match_topics(ClientInfo, Topic, [TopicFilter|Filters]) -> - match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(ClientInfo, Topic, Filters). - -match_topic(Topic, #{'eq' := TopicFilter}) -> - Topic == TopicFilter; -match_topic(Topic, TopicFilter) -> - emqx_topic:match(Topic, TopicFilter). - -feed_var(ClientInfo, Pattern) -> - feed_var(ClientInfo, Pattern, []). -feed_var(_ClientInfo, [], Acc) -> - lists:reverse(Acc); -feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) -> - feed_var(ClientInfo, Words, [<<"%c">>|Acc]); -feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) -> - feed_var(ClientInfo, Words, [ClientId |Acc]); -feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) -> - feed_var(ClientInfo, Words, [<<"%u">>|Acc]); -feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) -> - feed_var(ClientInfo, Words, [Username|Acc]); -feed_var(ClientInfo, [W|Words], Acc) -> - feed_var(ClientInfo, Words, [W|Acc]). - +atom(A) when is_atom(A) -> A. diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl deleted file mode 100644 index e6d1732a6..000000000 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ /dev/null @@ -1,517 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authz_api). - --behavior(minirest_api). - --include("emqx_authz.hrl"). - --define(EXAMPLE_RETURNED_RULE1, - #{principal => <<"all">>, - permission => <<"allow">>, - action => <<"all">>, - topics => [<<"#">>], - annotations => #{id => 1} - }). - - --define(EXAMPLE_RETURNED_RULES, - #{rules => [?EXAMPLE_RETURNED_RULE1 - ] - }). - --define(EXAMPLE_RULE1, #{principal => <<"all">>, - permission => <<"allow">>, - action => <<"all">>, - topics => [<<"#">>]}). - --export([ api_spec/0 - , rules/2 - , rule/2 - , move_rule/2 - ]). - -api_spec() -> - {[ rules_api() - , rule_api() - , move_rule_api() - ], definitions()}. - -definitions() -> emqx_authz_api_schema:definitions(). - -rules_api() -> - Metadata = #{ - get => #{ - description => "List authorization rules", - parameters => [ - #{ - name => page, - in => query, - schema => #{ - type => integer - }, - required => false - }, - #{ - name => limit, - in => query, - schema => #{ - type => integer - }, - required => false - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [rules], - properties => #{rules => #{ - type => array, - items => minirest:ref(<<"returned_rules">>) - } - } - }, - examples => #{ - rules => #{ - summary => <<"Rules">>, - value => jsx:encode(?EXAMPLE_RETURNED_RULES) - } - } - } - } - } - } - }, - post => #{ - description => "Add new rule", - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"rules">>), - examples => #{ - simple_rule => #{ - summary => <<"Rules">>, - value => jsx:encode(?EXAMPLE_RULE1) - } - } - } - } - }, - responses => #{ - <<"204">> => #{description => <<"Created">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } - } - }, - put => #{ - - description => "Update all rules", - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => minirest:ref(<<"returned_rules">>) - }, - examples => #{ - rules => #{ - summary => <<"Rules">>, - value => jsx:encode([?EXAMPLE_RULE1]) - } - } - } - } - }, - responses => #{ - <<"204">> => #{description => <<"Created">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } - } - } - }, - {"/authorization", Metadata, rules}. - -rule_api() -> - Metadata = #{ - get => #{ - description => "List authorization rules", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_rules">>), - examples => #{ - rules => #{ - summary => <<"Rules">>, - value => jsx:encode(?EXAMPLE_RETURNED_RULE1) - } - } - } - } - }, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> - } - } - } - } - } - } - } - }, - put => #{ - description => "Update rule", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"rules">>), - examples => #{ - simple_rule => #{ - summary => <<"Rules">>, - value => jsx:encode(?EXAMPLE_RULE1) - } - } - } - } - }, - responses => #{ - <<"204">> => #{description => <<"No Content">>}, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> - } - } - } - } - } - }, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } - } - }, - delete => #{ - description => "Delete rule", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"204">> => #{description => <<"No Content">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } - } - } - }, - {"/authorization/:id", Metadata, rule}. - -move_rule_api() -> - Metadata = #{ - post => #{ - description => "Change the order of rules", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [position], - properties => #{ - position => #{ - oneOf => [ - #{type => string, - enum => [<<"top">>, <<"bottom">>] - }, - #{type => object, - required => ['after'], - properties => #{ - 'after' => #{ - type => string - } - } - }, - #{type => object, - required => ['before'], - properties => #{ - 'before' => #{ - type => string - } - } - } - ] - } - } - } - } - } - }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> - } - } - } - } - } - }, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } - } - } - }, - {"/authorization/:id/move", Metadata, move_rule}. - -rules(get, Request) -> - Rules = lists:foldl(fun (#{type := _Type, enable := true, annotations := #{id := Id} = Annotations} = Rule, AccIn) -> - NRule = case emqx_resource:health_check(Id) of - ok -> - Rule#{annotations => Annotations#{status => healthy}}; - _ -> - Rule#{annotations => Annotations#{status => unhealthy}} - end, - lists:append(AccIn, [NRule]); - (Rule, AccIn) -> - lists:append(AccIn, [Rule]) - end, [], emqx_authz:lookup()), - Query = cowboy_req:parse_qs(Request), - case lists:keymember(<<"page">>, 1, Query) andalso lists:keymember(<<"limit">>, 1, Query) of - true -> - {<<"page">>, Page} = lists:keyfind(<<"page">>, 1, Query), - {<<"limit">>, Limit} = lists:keyfind(<<"limit">>, 1, Query), - Index = (binary_to_integer(Page) - 1) * binary_to_integer(Limit), - {_, Rules1} = lists:split(Index, Rules), - case binary_to_integer(Limit) < length(Rules1) of - true -> - {Rules2, _} = lists:split(binary_to_integer(Limit), Rules1), - {200, #{rules => Rules2}}; - false -> {200, #{rules => Rules1}} - end; - false -> {200, #{rules => Rules}} - end; -rules(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - RawConfig = jsx:decode(Body, [return_maps]), - case emqx_authz:update(head, [RawConfig]) of - ok -> {204}; - {error, Reason} -> - {400, #{code => <<"BAD_REQUEST">>, - messgae => atom_to_binary(Reason)}} - end; -rules(put, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - RawConfig = jsx:decode(Body, [return_maps]), - case emqx_authz:update(replace, RawConfig) of - ok -> {204}; - {error, Reason} -> - {400, #{code => <<"BAD_REQUEST">>, - messgae => atom_to_binary(Reason)}} - end. - -rule(get, Request) -> - Id = cowboy_req:binding(id, Request), - case emqx_authz:lookup(Id) of - {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - Rule -> - case maps:get(type, Rule, undefined) of - undefined -> {200, Rule}; - _ -> - case emqx_resource:health_check(Id) of - ok -> - {200, Rule#{annotations => #{status => healthy}}}; - _ -> - {200, Rule#{annotations => #{status => unhealthy}}} - end - - end - end; -rule(put, Request) -> - RuleId = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - RawConfig = jsx:decode(Body, [return_maps]), - case emqx_authz:update({replace_once, RuleId}, RawConfig) of - ok -> {204}; - {error, not_found_rule} -> - {404, #{code => <<"NOT_FOUND">>, - messgae => <<"rule ", RuleId/binary, " not found">>}}; - {error, Reason} -> - {400, #{code => <<"BAD_REQUEST">>, - messgae => atom_to_binary(Reason)}} - end; -rule(delete, Request) -> - RuleId = cowboy_req:binding(id, Request), - case emqx_authz:update({replace_once, RuleId}, #{}) of - ok -> {204}; - {error, Reason} -> - {400, #{code => <<"BAD_REQUEST">>, - messgae => atom_to_binary(Reason)}} - end. -move_rule(post, Request) -> - RuleId = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - #{<<"position">> := Position} = jsx:decode(Body, [return_maps]), - case emqx_authz:move(RuleId, Position) of - ok -> {204}; - {error, not_found_rule} -> - {404, #{code => <<"NOT_FOUND">>, - messgae => <<"rule ", RuleId/binary, " not found">>}}; - {error, Reason} -> - {400, #{code => <<"BAD_REQUEST">>, - messgae => atom_to_binary(Reason)}} - end. diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 2dcc7c564..09f145075 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -19,139 +19,491 @@ -export([definitions/0]). definitions() -> - RetruenedRules = #{ + RetruenedSources = #{ allOf => [ #{type => object, properties => #{ annotations => #{ type => object, - required => [id], + required => [status], properties => #{ id => #{ type => string }, - principal => minirest:ref(<<"principal">>) + status => #{ + type => string, + example => <<"healthy">> + } } - } - } } - , minirest:ref(<<"rules">>) + , minirest:ref(<<"sources">>) ] }, - Rules = #{ - oneOf => [ minirest:ref(<<"simple_rule">>) - % , minirest:ref(<<"connector_redis">>) + Sources = #{ + oneOf => [ minirest:ref(<<"http">>) + , minirest:ref(<<"mongo_single">>) + , minirest:ref(<<"mongo_rs">>) + , minirest:ref(<<"mongo_sharded">>) + , minirest:ref(<<"mysql">>) + , minirest:ref(<<"pgsql">>) + , minirest:ref(<<"redis_single">>) + , minirest:ref(<<"redis_sentinel">>) + , minirest:ref(<<"redis_cluster">>) + , minirest:ref(<<"file">>) ] }, - % ConnectorRedis = #{ - % type => object, - % required => [principal, type, enable, config, cmd] - % properties => #{ - % principal => minirest:ref(<<"principal">>), - % type => #{ - % type => string, - % enum => [<<"redis">>], - % example => <<"redis">> - % }, - % enable => #{ - % type => boolean, - % example => true - % } - % config => #{ - % type => - % } - % } - % } - SimpleRule = #{ + SSL = #{ + type => object, + required => [enable], + properties => #{ + enable => #{type => boolean, example => true}, + cacertfile => #{type => string}, + keyfile => #{type => string}, + certfile => #{type => string}, + verify => #{type => boolean, example => false} + } + }, + HTTP = #{ type => object, - required => [principal, permission, action, topics], + required => [ type + , enable + , method + , headers + , request_timeout + , connect_timeout + , max_retries + , retry_interval + , pool_type + , pool_size + , enable_pipelining + , ssl + ], properties => #{ - action => #{ + type => #{ type => string, - enum => [<<"publish">>, <<"subscribe">>, <<"all">>], - example => <<"publish">> + enum => [<<"http">>], + example => <<"http">> }, - permission => #{ + enable => #{ + type => boolean, + example => true + }, + url => #{ type => string, - enum => [<<"allow">>, <<"deny">>], - example => <<"allow">> + example => <<"https://emqx.com">> }, - topics => #{ - type => array, - items => #{ - oneOf => [ #{type => string, example => <<"#">>} - , #{type => object, - required => [eq], - properties => #{ - eq => #{type => string} - }, - example => #{eq => <<"#">>} - } - ] - } + method => #{ + type => string, + enum => [<<"get">>, <<"post">>, <<"put">>], + example => <<"get">> }, - principal => minirest:ref(<<"principal">>) + headers => #{type => object}, + body => #{type => object}, + connect_timeout => #{type => integer}, + max_retries => #{type => integer}, + retry_interval => #{type => integer}, + pool_type => #{ + type => string, + enum => [<<"random">>, <<"hash">>], + example => <<"random">> + }, + pool_size => #{type => integer}, + enable_pipelining => #{type => boolean}, + ssl => minirest:ref(<<"ssl">>) } }, - Principal = #{ - oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - , #{type => string, enum=>[<<"all">>], example => <<"all">>} - , #{type => object, - required => ['and'], - properties => #{'and' => #{type => array, - items => #{oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - ]}}}, - example => #{'and' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]} - } - , #{type => object, - required => ['or'], - properties => #{'and' => #{type => array, - items => #{oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - ]}}}, - example => #{'or' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]} - } - ] - }, - PrincipalUsername = #{type => object, - required => [username], - properties => #{username => #{type => string}}, - example => #{username => <<"emqx">>} - }, - PrincipalClientid = #{type => object, - required => [clientid], - properties => #{clientid => #{type => string}}, - example => #{clientid => <<"emqx">>} - }, - PrincipalIpaddress = #{type => object, - required => [ipaddress], - properties => #{ipaddress => #{type => string}}, - example => #{ipaddress => <<"127.0.0.1">>} - }, - ErrorDef = #{ + MongoSingle= #{ type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , server + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], properties => #{ - code => #{ + type => #{ type => string, - example => <<"BAD_REQUEST">> + enum => [<<"mongo">>], + example => <<"mongo">> }, - message => #{ - type => string + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"single">>], + example => <<"single">>}, + server => #{type => string, example => <<"127.0.0.1:27017">>}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + MongoRs= #{ + type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , servers + , replica_set_name + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mongo">>], + example => <<"mongo">> + }, + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"rs">>], + example => <<"rs">>}, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:27017">>}}, + replica_set_name => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + MongoSharded = #{ + type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , servers + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mongo">>], + example => <<"mongo">> + }, + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"sharded">>], + example => <<"sharded">>}, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:27017">>}}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + Mysql = #{ + type => object, + required => [ type + , enable + , sql + , server + , database + , pool_size + , username + , password + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mysql">>], + example => <<"mysql">> + }, + enable => #{ + type => boolean, + example => true + }, + sql => #{type => string}, + server => #{type => string, + example => <<"127.0.0.1:3306">> + }, + database => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auto_reconnect => #{type => boolean, + example => true + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + Pgsql = #{ + type => object, + required => [ type + , enable + , sql + , server + , database + , pool_size + , username + , password + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"pgsql">>], + example => <<"pgsql">> + }, + enable => #{ + type => boolean, + example => true + }, + sql => #{type => string}, + server => #{type => string, + example => <<"127.0.0.1:5432">> + }, + database => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auto_reconnect => #{type => boolean, + example => true + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisSingle = #{ + type => object, + required => [ type + , enable + , cmd + , server + , redis_type + , pool_size + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + cmd => #{ + type => string, + example => <<"HGETALL mqtt_authz">> + }, + server => #{type => string, example => <<"127.0.0.1:3306">>}, + redis_type => #{type => string, + enum => [<<"single">>], + example => <<"single">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisSentinel= #{ + type => object, + required => [ type + , enable + , cmd + , servers + , redis_type + , sentinel + , pool_size + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + cmd => #{ + type => string, + example => <<"HGETALL mqtt_authz">> + }, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"sentinel">>], + example => <<"sentinel">>}, + sentinel => #{type => string}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisCluster= #{ + type => object, + required => [ type + , enable + , cmd + , servers + , redis_type + , pool_size + , auto_reconnect + , ssl], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + cmd => #{ + type => string, + example => <<"HGETALL mqtt_authz">> + }, + servers => #{type => array, + items => #{type => string, example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"cluster">>], + example => <<"cluster">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) + } + }, + File = #{ + type => object, + required => [type, enable, rules], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + rules => #{ + type => array, + items => #{ + type => string, + example => <<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">> + } + }, + path => #{ + type => string, + example => <<"/path/to/authorizaiton_rules.conf">> } } }, - [ #{<<"returned_rules">> => RetruenedRules} - , #{<<"rules">> => Rules} - , #{<<"simple_rule">> => SimpleRule} - , #{<<"principal">> => Principal} - , #{<<"principal_username">> => PrincipalUsername} - , #{<<"principal_clientid">> => PrincipalClientid} - , #{<<"principal_ipaddress">> => PrincipalIpaddress} - , #{<<"error">> => ErrorDef} + [ #{<<"returned_sources">> => RetruenedSources} + , #{<<"sources">> => Sources} + , #{<<"ssl">> => SSL} + , #{<<"http">> => HTTP} + , #{<<"mongo_single">> => MongoSingle} + , #{<<"mongo_rs">> => MongoRs} + , #{<<"mongo_sharded">> => MongoSharded} + , #{<<"mysql">> => Mysql} + , #{<<"pgsql">> => Pgsql} + , #{<<"redis_single">> => RedisSingle} + , #{<<"redis_sentinel">> => RedisSentinel} + , #{<<"redis_cluster">> => RedisCluster} + , #{<<"file">> => File} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_settings.erl b/apps/emqx_authz/src/emqx_authz_api_settings.erl new file mode 100644 index 000000000..ac48fafd9 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_api_settings.erl @@ -0,0 +1,61 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_api_settings). + +-behavior(minirest_api). + +-export([ api_spec/0 + , settings/2 + ]). + +api_spec() -> + {[settings_api()], []}. + +authorization_settings() -> + maps:remove(<<"sources">>, emqx:get_raw_config([authorization], #{})). + +conf_schema() -> + emqx_mgmt_api_configs:gen_schema(authorization_settings()). + +settings_api() -> + Metadata = #{ + get => #{ + description => "Get authorization settings", + responses => #{<<"200">> => emqx_mgmt_util:schema(conf_schema())} + }, + put => #{ + description => "Update authorization settings", + requestBody => emqx_mgmt_util:schema(conf_schema()), + responses => #{ + <<"200">> => emqx_mgmt_util:schema(conf_schema()), + <<"400">> => emqx_mgmt_util:bad_request() + } + } + }, + {"/authorization/settings", Metadata, settings}. + +settings(get, _Params) -> + {200, authorization_settings()}; + +settings(put, #{body := #{<<"no_match">> := NoMatch, + <<"deny_action">> := DenyAction, + <<"cache">> := Cache}}) -> + {ok, _} = emqx:update_config([authorization, no_match], NoMatch), + {ok, _} = emqx:update_config([authorization, deny_action], DenyAction), + {ok, _} = emqx:update_config([authorization, cache], Cache), + ok = emqx_authz_cache:drain_cache(), + {200, authorization_settings()}. diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl new file mode 100644 index 000000000..209bbc01f --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -0,0 +1,493 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_api_sources). + +-behavior(minirest_api). + +-include("emqx_authz.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-define(EXAMPLE_REDIS, + #{type=> redis, + enable => true, + server => <<"127.0.0.1:3306">>, + redis_type => single, + pool_size => 1, + auto_reconnect => true, + cmd => <<"HGETALL mqtt_authz">>}). +-define(EXAMPLE_FILE, + #{type=> file, + enable => true, + rules => [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>, + <<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> + ]}). + +-define(EXAMPLE_RETURNED_REDIS, + maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) + ). +-define(EXAMPLE_RETURNED_FILE, + maps:put(annotations, #{status => healthy}, ?EXAMPLE_FILE) + ). + +-define(EXAMPLE_RETURNED, + #{sources => [ ?EXAMPLE_RETURNED_REDIS + , ?EXAMPLE_RETURNED_FILE + ] + }). + +-export([ api_spec/0 + , sources/2 + , source/2 + , move_source/2 + ]). + +api_spec() -> + {[ sources_api() + , source_api() + , move_source_api() + ], definitions()}. + +definitions() -> emqx_authz_api_schema:definitions(). + +sources_api() -> + Metadata = #{ + get => #{ + description => "List authorization sources", + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [sources], + properties => #{sources => #{ + type => array, + items => minirest:ref(<<"returned_sources">>) + } + } + }, + examples => #{ + sources => #{ + summary => <<"Sources">>, + value => jsx:encode(?EXAMPLE_RETURNED) + } + } + } + } + } + } + }, + post => #{ + description => "Add new source", + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"sources">>), + examples => #{ + redis => #{ + summary => <<"Redis">>, + value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) + } + } + } + } + }, + responses => #{ + <<"204">> => #{description => <<"Created">>}, + <<"400">> => emqx_mgmt_util:bad_request() + } + }, + put => #{ + description => "Update all sources", + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => minirest:ref(<<"returned_sources">>) + }, + examples => #{ + redis => #{ + summary => <<"Redis">>, + value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) + } + } + } + } + }, + responses => #{ + <<"204">> => #{description => <<"Created">>}, + <<"400">> => emqx_mgmt_util:bad_request() + } + } + }, + {"/authorization/sources", Metadata, sources}. + +source_api() -> + Metadata = #{ + get => #{ + description => "List authorization sources", + parameters => [ + #{ + name => type, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"returned_sources">>), + examples => #{ + redis => #{ + summary => <<"Redis">>, + value => jsx:encode(?EXAMPLE_RETURNED_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_RETURNED_FILE) + } + } + } + } + }, + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>) + } + }, + put => #{ + description => "Update source", + parameters => [ + #{ + name => type, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"sources">>), + examples => #{ + redis => #{ + summary => <<"Redis">>, + value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) + } + } + } + } + }, + responses => #{ + <<"204">> => #{description => <<"No Content">>}, + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>), + <<"400">> => emqx_mgmt_util:bad_request() + } + }, + delete => #{ + description => "Delete source", + parameters => [ + #{ + name => type, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{description => <<"No Content">>}, + <<"400">> => emqx_mgmt_util:bad_request() + } + } + }, + {"/authorization/sources/:type", Metadata, source}. + +move_source_api() -> + Metadata = #{ + post => #{ + description => "Change the order of sources", + parameters => [ + #{ + name => type, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [position], + properties => #{ + position => #{ + oneOf => [ + #{type => string, + enum => [<<"top">>, <<"bottom">>] + }, + #{type => object, + required => ['after'], + properties => #{ + 'after' => #{ + type => string + } + } + }, + #{type => object, + required => ['before'], + properties => #{ + 'before' => #{ + type => string + } + } + } + ] + } + } + } + } + } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>), + <<"400">> => emqx_mgmt_util:bad_request() + } + } + }, + {"/authorization/sources/:type/move", Metadata, move_source}. + +sources(get, _) -> + Sources = lists:foldl(fun (#{type := file, enable := Enable, path := Path}, AccIn) -> + {ok, Rules} = file:consult(Path), + lists:append(AccIn, [#{type => file, + enable => Enable, + rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], + annotations => #{status => healthy} + }]); + (#{enable := false} = Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]); + (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> + NSource0 = case maps:get(server, Source, undefined) of + undefined -> Source; + Server -> + Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)} + end, + NSource1 = case maps:get(servers, Source, undefined) of + undefined -> NSource0; + Servers -> + NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]} + end, + NSource2 = case emqx_resource:health_check(Id) of + ok -> + NSource1#{annotations => #{status => healthy}}; + _ -> + NSource1#{annotations => #{status => unhealthy}} + end, + lists:append(AccIn, [read_cert(NSource2)]); + (Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) + end, [], emqx_authz:lookup()), + {200, #{sources => Sources}}; +sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) -> + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), + case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +sources(post, #{body := Body}) when is_map(Body) -> + case emqx_authz:update(head, [write_cert(Body)]) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +sources(put, #{body := Body}) when is_list(Body) -> + NBody = [ begin + case Source of + #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} -> + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), + #{type => file, enable => Enable, path => Filename}; + _ -> write_cert(Source) + end + end || Source <- Body], + case emqx_authz:update(replace, NBody) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end. + +source(get, #{bindings := #{type := Type}}) -> + case emqx_authz:lookup(Type) of + {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; + #{type := file, enable := Enable, path := Path}-> + {ok, Rules} = file:consult(Path), + {200, #{type => file, + enable => Enable, + rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], + annotations => #{status => healthy} + } + }; + #{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}}; + #{annotations := #{id := Id}} = Source -> + NSource0 = case maps:get(server, Source, undefined) of + undefined -> Source; + Server -> + Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)} + end, + NSource1 = case maps:get(servers, Source, undefined) of + undefined -> NSource0; + Servers -> + NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]} + end, + NSource2 = case emqx_resource:health_check(Id) of + ok -> + NSource1#{annotations => #{status => healthy}}; + _ -> + NSource1#{annotations => #{status => unhealthy}} + end, + {200, read_cert(NSource2)} + end; +source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> + {ok, Filename} = write_file(maps:get(path, emqx_authz:lookup(file), ""), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), + case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Filename}) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) -> + case emqx_authz:update({replace_once, Type}, write_cert(Body)) of + {ok, _} -> {204}; + {error, not_found_source} -> + {404, #{code => <<"NOT_FOUND">>, + messgae => <<"source ", Type/binary, " not found">>}}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +source(delete, #{bindings := #{type := Type}}) -> + case emqx_authz:update({delete_once, Type}, #{}) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end. +move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) -> + case emqx_authz:move(Type, Position) of + {ok, _} -> {204}; + {error, not_found_source} -> + {404, #{code => <<"NOT_FOUND">>, + messgae => <<"source ", Type/binary, " not found">>}}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end. + +read_cert(#{ssl := #{enable := true} = SSL} = Source) -> + CaCert = case file:read_file(maps:get(cacertfile, SSL, "")) of + {ok, CaCert0} -> CaCert0; + _ -> "" + end, + Cert = case file:read_file(maps:get(certfile, SSL, "")) of + {ok, Cert0} -> Cert0; + _ -> "" + end, + Key = case file:read_file(maps:get(keyfile, SSL, "")) of + {ok, Key0} -> Key0; + _ -> "" + end, + Source#{ssl => SSL#{cacertfile => CaCert, + certfile => Cert, + keyfile => Key + } + }; +read_cert(Source) -> Source. + +write_cert(#{<<"ssl">> := #{<<"enable">> := true} = SSL} = Source) -> + CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), + CaCert = case maps:is_key(<<"cacertfile">>, SSL) of + true -> + {ok, CaCertFile} = write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"cacertfile">>, SSL)), + CaCertFile; + false -> "" + end, + Cert = case maps:is_key(<<"certfile">>, SSL) of + true -> + {ok, CertFile} = write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"certfile">>, SSL)), + CertFile; + false -> "" + end, + Key = case maps:is_key(<<"keyfile">>, SSL) of + true -> + {ok, KeyFile} = write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"keyfile">>, SSL)), + KeyFile; + false -> "" + end, + Source#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, + <<"certfile">> => Cert, + <<"keyfile">> => Key + } + }; +write_cert(Source) -> Source. + +write_file(Filename, Bytes) -> + ok = filelib:ensure_dir(Filename), + case file:write_file(Filename, Bytes) of + ok -> {ok, iolist_to_binary(Filename)}; + {error, Reason} -> + ?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]), + error(Reason) + end. diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index c95d200e1..93aa634f3 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -35,12 +35,12 @@ description() -> authorize(Client, PubSub, Topic, #{type := http, - config := #{url := #{path := Path} = Url, - headers := Headers, - method := Method, - request_timeout := RequestTimeout} = Config, + url := #{path := Path} = Url, + headers := Headers, + method := Method, + request_timeout := RequestTimeout, annotations := #{id := ResourceID} - }) -> + } = Source) -> Request = case Method of get -> Query = maps:get(query, Url, ""), @@ -49,7 +49,7 @@ authorize(Client, PubSub, Topic, _ -> Body0 = serialize_body( maps:get('Accept', Headers, <<"application/json">>), - maps:get(body, Config, #{}) + maps:get(body, Source, #{}) ), Body1 = replvar(Body0, PubSub, Topic, Client), Path1 = replvar(Path, PubSub, Topic, Client), diff --git a/apps/emqx_authz/src/emqx_authz_mongo.erl b/apps/emqx_authz/src/emqx_authz_mongo.erl index c015f8208..25a787b8f 100644 --- a/apps/emqx_authz/src/emqx_authz_mongo.erl +++ b/apps/emqx_authz/src/emqx_authz_mongo.erl @@ -44,38 +44,19 @@ authorize(Client, PubSub, Topic, nomatch; [] -> nomatch; Rows -> - do_authorize(Client, PubSub, Topic, Rows) + Rules = [ emqx_authz_rule:compile({Permission, all, Action, Topics}) + || #{<<"topics">> := Topics, <<"permission">> := Permission, <<"action">> := Action} <- Rows], + do_authorize(Client, PubSub, Topic, Rules) end. do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; do_authorize(Client, PubSub, Topic, [Rule | Tail]) -> - case match(Client, PubSub, Topic, Rule) of + case emqx_authz_rule:match(Client, PubSub, Topic, Rule) of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Tail) end. -match(Client, PubSub, Topic, - #{<<"topics">> := Topics, - <<"permission">> := Permission, - <<"action">> := Action - }) -> - Rule = #{<<"permission">> => Permission, - <<"topics">> => Topics, - <<"action">> => Action - }, - #{simple_rule := - #{permission := NPermission} = NRule - } = hocon_schema:check_plain( - emqx_authz_schema, - #{<<"simple_rule">> => Rule}, - #{atom_key => true}, - [simple_rule]), - case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of - true -> {matched, NPermission}; - false -> nomatch - end. - replvar(Find, #{clientid := Clientid, username := Username, peerhost := IpAddress diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index 2ce991eba..d5550b2fb 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -62,39 +62,25 @@ authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> nomatch; do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> - case match(Client, PubSub, Topic, format_result(Columns, Row)) of + case emqx_authz_rule:match(Client, PubSub, Topic, + emqx_authz_rule:compile(format_result(Columns, Row)) + ) of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) end. -format_result(Columns, Row) -> - L = [ begin - K = lists:nth(I, Columns), - V = lists:nth(I, Row), - {K, V} - end || I <- lists:seq(1, length(Columns)) ], - maps:from_list(L). -match(Client, PubSub, Topic, - #{<<"permission">> := Permission, - <<"action">> := Action, - <<"topic">> := TopicFilter - }) -> - Rule = #{<<"topics">> => [TopicFilter], - <<"action">> => Action, - <<"permission">> => Permission - }, - #{simple_rule := - #{permission := NPermission} = NRule - } = hocon_schema:check_plain( - emqx_authz_schema, - #{<<"simple_rule">> => Rule}, - #{atom_key => true}, - [simple_rule]), - case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of - true -> {matched, NPermission}; - false -> nomatch - end. +format_result(Columns, Row) -> + Permission = lists:nth(index(<<"permission">>, Columns), Row), + Action = lists:nth(index(<<"action">>, Columns), Row), + Topic = lists:nth(index(<<"topic">>, Columns), Row), + {Permission, all, Action, [Topic]}. + +index(Elem, List) -> + index(Elem, List, 1). +index(_Elem, [], _Index) -> {error, not_found}; +index(Elem, [ Elem | _List], Index) -> Index; +index(Elem, [ _ | List], Index) -> index(Elem, List, Index + 1). replvar(Params, ClientInfo) -> replvar(Params, ClientInfo, []). diff --git a/apps/emqx_authz/src/emqx_authz_pgsql.erl b/apps/emqx_authz/src/emqx_authz_pgsql.erl index f3e793763..d9555b85d 100644 --- a/apps/emqx_authz/src/emqx_authz_pgsql.erl +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -66,39 +66,25 @@ authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> nomatch; do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> - case match(Client, PubSub, Topic, format_result(Columns, Row)) of + case emqx_authz_rule:match(Client, PubSub, Topic, + emqx_authz_rule:compile(format_result(Columns, Row)) + ) of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) end. format_result(Columns, Row) -> - L = [ begin - {column, K, _, _, _, _, _, _, _} = lists:nth(I, Columns), - V = lists:nth(I, tuple_to_list(Row)), - {K, V} - end || I <- lists:seq(1, length(Columns)) ], - maps:from_list(L). + Permission = lists:nth(index(<<"permission">>, 2, Columns), erlang:tuple_to_list(Row)), + Action = lists:nth(index(<<"action">>, 2, Columns), erlang:tuple_to_list(Row)), + Topic = lists:nth(index(<<"topic">>, 2, Columns), erlang:tuple_to_list(Row)), + {Permission, all, Action, [Topic]}. -match(Client, PubSub, Topic, - #{<<"permission">> := Permission, - <<"action">> := Action, - <<"topic">> := TopicFilter - }) -> - Rule = #{<<"topics">> => [TopicFilter], - <<"action">> => Action, - <<"permission">> => Permission - }, - #{simple_rule := - #{permission := NPermission} = NRule - } = hocon_schema:check_plain( - emqx_authz_schema, - #{<<"simple_rule">> => Rule}, - #{atom_key => true}, - [simple_rule]), - case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of - true -> {matched, NPermission}; - false -> nomatch - end. +index(Key, N, TupleList) when is_integer(N) -> + Tuple = lists:keyfind(Key, N, TupleList), + index(Tuple, TupleList, 1); +index(_Tuple, [], _Index) -> {error, not_found}; +index(Tuple, [Tuple | _TupleList], Index) -> Index; +index(Tuple, [_ | TupleList], Index) -> index(Tuple, TupleList, Index + 1). replvar(Params, ClientInfo) -> replvar(Params, ClientInfo, []). diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 8f6731fd8..3ac7d7e3f 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -50,35 +50,13 @@ authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> - case match(Client, PubSub, Topic, - #{topics => TopicFilter, - action => Action - }) - of + case emqx_authz_rule:match(Client, PubSub, Topic, + emqx_authz_rule:compile({allow, all, Action, [TopicFilter]}) + )of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Tail) end. -match(Client, PubSub, Topic, - #{topics := TopicFilter, - action := Action - }) -> - Rule = #{<<"principal">> => all, - <<"topics">> => [TopicFilter], - <<"action">> => Action, - <<"permission">> => allow - }, - #{simple_rule := NRule - } = hocon_schema:check_plain( - emqx_authz_schema, - #{<<"simple_rule">> => Rule}, - #{atom_key => true}, - [simple_rule]), - case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of - true -> {matched, allow}; - false -> nomatch - end. - replvar(Cmd, Client = #{cn := CN}) -> replvar(repl(Cmd, "%C", CN), maps:remove(cn, Client)); replvar(Cmd, Client = #{dn := DN}) -> diff --git a/apps/emqx_authz/src/emqx_authz_rule.erl b/apps/emqx_authz/src/emqx_authz_rule.erl new file mode 100644 index 000000000..deb8968c6 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_rule.erl @@ -0,0 +1,165 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_rule). + +-include("emqx_authz.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-endif. + +%% APIs +-export([ match/4 + , matches/4 + , compile/1 + ]). + +-export_type([rule/0]). + +compile({Permission, Who, Action, TopicFilters}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) -> + {atom(Permission), compile_who(Who), atom(Action), [compile_topic(Topic) || Topic <- TopicFilters]}. + +compile_who(all) -> all; +compile_who({username, Username}) -> + {ok, MP} = re:compile(bin(Username)), + {username, MP}; +compile_who({clientid, Clientid}) -> + {ok, MP} = re:compile(bin(Clientid)), + {clientid, MP}; +compile_who({ipaddr, CIDR}) -> + {ipaddr, esockd_cidr:parse(CIDR, true)}; +compile_who({ipaddrs, CIDRs}) -> + {ipaddrs, lists:map(fun(CIDR) -> esockd_cidr:parse(CIDR, true) end, CIDRs)}; +compile_who({'and', L}) when is_list(L) -> + {'and', [compile_who(Who) || Who <- L]}; +compile_who({'or', L}) when is_list(L) -> + {'or', [compile_who(Who) || Who <- L]}. + +compile_topic(<<"eq ", Topic/binary>>) -> + {eq, emqx_topic:words(Topic)}; +compile_topic({eq, Topic}) -> + {eq, emqx_topic:words(bin(Topic))}; +compile_topic(Topic) -> + Words = emqx_topic:words(bin(Topic)), + case pattern(Words) of + true -> {pattern, Words}; + false -> Words + end. + +pattern(Words) -> + lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words). + +atom(B) when is_binary(B) -> + try binary_to_existing_atom(B, utf8) + catch + _ -> binary_to_atom(B) + end; +atom(A) when is_atom(A) -> A. + +bin(L) when is_list(L) -> + list_to_binary(L); +bin(B) when is_binary(B) -> + B. + +-spec(matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()]) + -> {matched, allow} | {matched, deny} | nomatch). +matches(_Client, _PubSub, _Topic, []) -> nomatch; +matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) -> + case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of + nomatch -> matches(Client, PubSub, Topic, Tail); + Matched -> Matched + end. + +-spec(match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule()) + -> {matched, allow} | {matched, deny} | nomatch). +match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) -> + case match_action(PubSub, Action) andalso + match_who(Client, Who) andalso + match_topics(Client, Topic, TopicFilters) of + true -> {matched, Permission}; + _ -> nomatch + end. + +match_action(publish, publish) -> true; +match_action(subscribe, subscribe) -> true; +match_action(_, all) -> true; +match_action(_, _) -> false. + +match_who(_, all) -> true; +match_who(#{username := undefined}, {username, _MP}) -> + false; +match_who(#{username := Username}, {username, MP}) -> + case re:run(Username, MP) of + {match, _} -> true; + _ -> false + end; +match_who(#{clientid := Clientid}, {clientid, MP}) -> + case re:run(Clientid, MP) of + {match, _} -> true; + _ -> false + end; +match_who(#{peerhost := undefined}, {ipaddr, _CIDR}) -> + false; +match_who(#{peerhost := IpAddress}, {ipaddr, CIDR}) -> + esockd_cidr:match(IpAddress, CIDR); +match_who(#{peerhost := undefined}, {ipaddrs, _CIDR}) -> + false; +match_who(#{peerhost := IpAddress}, {ipaddrs, CIDRs}) -> + lists:any(fun(CIDR) -> + esockd_cidr:match(IpAddress, CIDR) + end, CIDRs); +match_who(ClientInfo, {'and', Principals}) when is_list(Principals) -> + lists:foldl(fun(Principal, Permission) -> + match_who(ClientInfo, Principal) andalso Permission + end, true, Principals); +match_who(ClientInfo, {'or', Principals}) when is_list(Principals) -> + lists:foldl(fun(Principal, Permission) -> + match_who(ClientInfo, Principal) orelse Permission + end, false, Principals); +match_who(_, _) -> false. + +match_topics(_ClientInfo, _Topic, []) -> + false; +match_topics(ClientInfo, Topic, [{pattern, PatternFilter}|Filters]) -> + TopicFilter = feed_var(ClientInfo, PatternFilter), + match_topic(emqx_topic:words(Topic), TopicFilter) + orelse match_topics(ClientInfo, Topic, Filters); +match_topics(ClientInfo, Topic, [TopicFilter|Filters]) -> + match_topic(emqx_topic:words(Topic), TopicFilter) + orelse match_topics(ClientInfo, Topic, Filters). + +match_topic(Topic, {'eq', TopicFilter}) -> + Topic =:= TopicFilter; +match_topic(Topic, TopicFilter) -> + emqx_topic:match(Topic, TopicFilter). + +feed_var(ClientInfo, Pattern) -> + feed_var(ClientInfo, Pattern, []). +feed_var(_ClientInfo, [], Acc) -> + lists:reverse(Acc); +feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) -> + feed_var(ClientInfo, Words, [<<"%c">>|Acc]); +feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) -> + feed_var(ClientInfo, Words, [ClientId |Acc]); +feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) -> + feed_var(ClientInfo, Words, [<<"%u">>|Acc]); +feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) -> + feed_var(ClientInfo, Words, [Username|Acc]); +feed_var(ClientInfo, [W|Words], Acc) -> + feed_var(ClientInfo, Words, [W|Acc]). diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index cc109534f..b90d522e8 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -13,27 +13,51 @@ -type permission() :: allow | deny. -type url() :: emqx_http_lib:uri_map(). --export([ structs/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). -structs() -> ["authorization"]. +namespace() -> authz. + +%% @doc authorization schema is not exported +%% but directly used by emqx_schema +roots() -> []. fields("authorization") -> - [ {rules, rules()} - ]; -fields(http) -> - [ {principal, principal()} - , {type, #{type => http}} - , {enable, #{type => boolean(), - default => true}} - , {config, #{type => hoconsc:union([ hoconsc:ref(?MODULE, http_get) - , hoconsc:ref(?MODULE, http_post) - ])} + [ {sources, #{type => union_array( + [ hoconsc:ref(?MODULE, file) + , hoconsc:ref(?MODULE, http_get) + , hoconsc:ref(?MODULE, http_post) + , hoconsc:ref(?MODULE, mongo_single) + , hoconsc:ref(?MODULE, mongo_rs) + , hoconsc:ref(?MODULE, mongo_sharded) + , hoconsc:ref(?MODULE, mysql) + , hoconsc:ref(?MODULE, pgsql) + , hoconsc:ref(?MODULE, redis_single) + , hoconsc:ref(?MODULE, redis_sentinel) + , hoconsc:ref(?MODULE, redis_cluster) + ])} } ]; +fields(file) -> + [ {type, #{type => file}} + , {enable, #{type => boolean(), + default => true}} + , {path, #{type => string(), + validator => fun(S) -> case filelib:is_file(S) of + true -> ok; + _ -> {error, "File does not exist"} + end + end + }} + ]; fields(http_get) -> - [ {url, #{type => url()}} + [ {type, #{type => http}} + , {enable, #{type => boolean(), + default => true}} + , {url, #{type => url()}} + , {method, #{type => get, default => get }} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -53,11 +77,15 @@ fields(http_get) -> end } } - , {method, #{type => get, default => get }} , {request_timeout, #{type => timeout(), default => 30000 }} ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)); fields(http_post) -> - [ {url, #{type => url()}} + [ {type, #{type => http}} + , {enable, #{type => boolean(), + default => true}} + , {url, #{type => url()}} + , {method, #{type => hoconsc:enum([post, put]), + default => get}} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -79,64 +107,42 @@ fields(http_post) -> end } } - , {method, #{type => hoconsc:enum([post, put]), - default => get}} + , {request_timeout, #{type => timeout(), default => 30000 }} , {body, #{type => map(), nullable => true } } ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)); -fields(mongo) -> - connector_fields(mongo) ++ +fields(mongo_single) -> + connector_fields(mongo, single) ++ + [ {collection, #{type => atom()}} + , {find, #{type => map()}} + ]; +fields(mongo_rs) -> + connector_fields(mongo, rs) ++ + [ {collection, #{type => atom()}} + , {find, #{type => map()}} + ]; +fields(mongo_sharded) -> + connector_fields(mongo, sharded) ++ [ {collection, #{type => atom()}} , {find, #{type => map()}} ]; -fields(redis) -> - connector_fields(redis) ++ - [ {cmd, query()} ]; fields(mysql) -> connector_fields(mysql) ++ [ {sql, query()} ]; fields(pgsql) -> connector_fields(pgsql) ++ [ {sql, query()} ]; -fields(simple_rule) -> - [ {permission, #{type => permission()}} - , {action, #{type => action()}} - , {topics, #{type => union_array( - [ binary() - , hoconsc:ref(?MODULE, eq_topic) - ] - )}} - , {principal, principal()} - ]; -fields(username) -> - [{username, #{type => binary()}}]; -fields(clientid) -> - [{clientid, #{type => binary()}}]; -fields(ipaddress) -> - [{ipaddress, #{type => string()}}]; -fields(andlist) -> - [{'and', #{type => union_array( - [ hoconsc:ref(?MODULE, username) - , hoconsc:ref(?MODULE, clientid) - , hoconsc:ref(?MODULE, ipaddress) - ]) - } - } - ]; -fields(orlist) -> - [{'or', #{type => union_array( - [ hoconsc:ref(?MODULE, username) - , hoconsc:ref(?MODULE, clientid) - , hoconsc:ref(?MODULE, ipaddress) - ]) - } - } - ]; -fields(eq_topic) -> - [{eq, #{type => binary()}}]. - +fields(redis_single) -> + connector_fields(redis, single) ++ + [ {cmd, query()} ]; +fields(redis_sentinel) -> + connector_fields(redis, sentinel) ++ + [ {cmd, query()} ]; +fields(redis_cluster) -> + connector_fields(redis, cluster) ++ + [ {cmd, query()} ]. %%-------------------------------------------------------------------- %% Internal functions @@ -145,29 +151,6 @@ fields(eq_topic) -> union_array(Item) when is_list(Item) -> hoconsc:array(hoconsc:union(Item)). -rules() -> - #{type => union_array( - [ hoconsc:ref(?MODULE, simple_rule) - , hoconsc:ref(?MODULE, http) - , hoconsc:ref(?MODULE, mysql) - , hoconsc:ref(?MODULE, pgsql) - , hoconsc:ref(?MODULE, redis) - , hoconsc:ref(?MODULE, mongo) - ]) - }. - -principal() -> - #{default => all, - type => hoconsc:union( - [ all - , hoconsc:ref(?MODULE, username) - , hoconsc:ref(?MODULE, clientid) - , hoconsc:ref(?MODULE, ipaddress) - , hoconsc:ref(?MODULE, andlist) - , hoconsc:ref(?MODULE, orlist) - ]) - }. - query() -> #{type => binary(), validator => fun(S) -> @@ -179,6 +162,8 @@ query() -> }. connector_fields(DB) -> + connector_fields(DB, config). +connector_fields(DB, Fields) -> Mod0 = io_lib:format("~s_~s",[emqx_connector, DB]), Mod = try list_to_existing_atom(Mod0) @@ -188,8 +173,7 @@ connector_fields(DB) -> Error -> erlang:error(Error) end, - [ {principal, principal()} - , {type, #{type => DB}} + [ {type, #{type => DB}} , {enable, #{type => boolean(), default => true}} - ] ++ Mod:fields(""). + ] ++ Mod:fields(Fields). diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 6f88fe865..f2cb01d05 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). @@ -31,195 +31,171 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), + meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end), + meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), - emqx_ct_helpers:stop_apps([emqx_authz]), + {ok, _} = emqx_authz:update(replace, []), + emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), + meck:unload(emqx_resource), + meck:unload(emqx_schema), ok. init_per_testcase(_, Config) -> - ok = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), Config. --define(RULE1, #{<<"principal">> => <<"all">>, - <<"topics">> => [<<"#">>], - <<"action">> => <<"all">>, - <<"permission">> => <<"deny">>} - ). --define(RULE2, #{<<"principal">> => - #{<<"ipaddress">> => <<"127.0.0.1">>}, - <<"topics">> => - [#{<<"eq">> => <<"#">>}, - #{<<"eq">> => <<"+">>} - ] , - <<"action">> => <<"all">>, - <<"permission">> => <<"allow">>} - ). --define(RULE3,#{<<"principal">> => - #{<<"and">> => [#{<<"username">> => <<"^test?">>}, - #{<<"clientid">> => <<"^test?">>} - ]}, - <<"topics">> => [<<"test">>], - <<"action">> => <<"publish">>, - <<"permission">> => <<"allow">>} - ). --define(RULE4,#{<<"principal">> => - #{<<"or">> => [#{<<"username">> => <<"^test">>}, - #{<<"clientid">> => <<"test?">>} - ]}, - <<"topics">> => [<<"%u">>,<<"%c">>], - <<"action">> => <<"publish">>, - <<"permission">> => <<"deny">>} - ). +-define(SOURCE1, #{<<"type">> => <<"http">>, + <<"enable">> => true, + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 + }). +-define(SOURCE2, #{<<"type">> => <<"mongo">>, + <<"enable">> => true, + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }). +-define(SOURCE3, #{<<"type">> => <<"mysql">>, + <<"enable">> => true, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"sql">> => <<"abcb">> + }). +-define(SOURCE4, #{<<"type">> => <<"pgsql">>, + <<"enable">> => true, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"sql">> => <<"abcb">> + }). +-define(SOURCE5, #{<<"type">> => <<"redis">>, + <<"enable">> => true, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }). +-define(SOURCE6, #{<<"type">> => <<"file">>, + <<"enable">> => true, + <<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/acl.conf") + }). + %%------------------------------------------------------------------------------ %% Testcases %%------------------------------------------------------------------------------ -t_update_rule(_) -> - ok = emqx_authz:update(replace, [?RULE2]), - ok = emqx_authz:update(head, [?RULE1]), - ok = emqx_authz:update(tail, [?RULE3]), +t_update_source(_) -> + {ok, _} = emqx_authz:update(replace, [?SOURCE3]), + {ok, _} = emqx_authz:update(head, [?SOURCE2]), + {ok, _} = emqx_authz:update(head, [?SOURCE1]), + {ok, _} = emqx_authz:update(tail, [?SOURCE4]), + {ok, _} = emqx_authz:update(tail, [?SOURCE5]), + {ok, _} = emqx_authz:update(tail, [?SOURCE6]), - Lists1 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE3]), - ?assertMatch(Lists1, emqx_config:get([authorization, rules], [])), + ?assertMatch([ #{type := http, enable := true} + , #{type := mongo, enable := true} + , #{type := mysql, enable := true} + , #{type := pgsql, enable := true} + , #{type := redis, enable := true} + , #{type := file, enable := true} + ], emqx:get_config([authorization, sources], [])), - [#{annotations := #{id := Id1, - principal := all, - topics := [['#']]} - }, - #{annotations := #{id := Id2, - principal := #{ipaddress := {{127,0,0,1},{127,0,0,1},32}}, - topics := [#{eq := ['#']}, #{eq := ['+']}]} - }, - #{annotations := #{id := Id3, - principal := - #{'and' := [#{username := {re_pattern, _, _, _, _}}, - #{clientid := {re_pattern, _, _, _, _}} - ] - }, - topics := [[<<"test">>]]} - } - ] = emqx_authz:lookup(), + {ok, _} = emqx_authz:update({replace_once, http}, ?SOURCE1#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, mongo}, ?SOURCE2#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, mysql}, ?SOURCE3#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, pgsql}, ?SOURCE4#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, redis}, ?SOURCE5#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, file}, ?SOURCE6#{<<"enable">> := false}), - ok = emqx_authz:update({replace_once, Id3}, ?RULE4), - Lists2 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE4]), - ?assertMatch(Lists2, emqx_config:get([authorization, rules], [])), + ?assertMatch([ #{type := http, enable := false} + , #{type := mongo, enable := false} + , #{type := mysql, enable := false} + , #{type := pgsql, enable := false} + , #{type := redis, enable := false} + , #{type := file, enable := false} + ], emqx:get_config([authorization, sources], [])), - [#{annotations := #{id := Id1, - principal := all, - topics := [['#']]} - }, - #{annotations := #{id := Id2, - principal := #{ipaddress := {{127,0,0,1},{127,0,0,1},32}}, - topics := [#{eq := ['#']}, - #{eq := ['+']}]} - }, - #{annotations := #{id := Id3, - principal := - #{'or' := [#{username := {re_pattern, _, _, _, _}}, - #{clientid := {re_pattern, _, _, _, _}} - ] - }, - topics := [#{pattern := [<<"%u">>]}, - #{pattern := [<<"%c">>]} - ]} - } - ] = emqx_authz:lookup(), + {ok, _} = emqx_authz:update(replace, []). - ok = emqx_authz:update(replace, []). - -t_move_rule(_) -> - ok = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), - [#{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}} - ] = emqx_authz:lookup(), - - ok = emqx_authz:move(Id4, <<"top">>), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}} +t_move_source(_) -> + {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), + ?assertMatch([ #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := pgsql} + , #{type := redis} + , #{type := file} ], emqx_authz:lookup()), - ok = emqx_authz:move(Id1, <<"bottom">>), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id1}} + {ok, _} = emqx_authz:move(pgsql, <<"top">>), + ?assertMatch([ #{type := pgsql} + , #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} + , #{type := file} ], emqx_authz:lookup()), - ok = emqx_authz:move(Id3, #{<<"before">> => Id4}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id1}} + {ok, _} = emqx_authz:move(http, <<"bottom">>), + ?assertMatch([ #{type := pgsql} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} + , #{type := file} + , #{type := http} ], emqx_authz:lookup()), - ok = emqx_authz:move(Id2, #{<<"after">> => Id1}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}} + {ok, _} = emqx_authz:move(mysql, #{<<"before">> => pgsql}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := mongo} + , #{type := redis} + , #{type := file} + , #{type := http} ], emqx_authz:lookup()), - ok. - -t_authz(_) -> - ClientInfo1 = #{clientid => <<"test">>, - username => <<"test">>, - peerhost => {127,0,0,1}, - zone => default, - listener => mqtt_tcp - }, - ClientInfo2 = #{clientid => <<"test">>, - username => <<"test">>, - peerhost => {192,168,0,10}, - zone => default, - listener => mqtt_tcp - }, - ClientInfo3 = #{clientid => <<"test">>, - username => <<"fake">>, - peerhost => {127,0,0,1}, - zone => default, - listener => mqtt_tcp - }, - ClientInfo4 = #{clientid => <<"fake">>, - username => <<"test">>, - peerhost => {127,0,0,1}, - zone => default, - listener => mqtt_tcp - }, - - Rules1 = [emqx_authz:init_rule(Rule) || Rule <- emqx_authz:check_rules([?RULE1, ?RULE2])], - Rules2 = [emqx_authz:init_rule(Rule) || Rule <- emqx_authz:check_rules([?RULE2, ?RULE1])], - Rules3 = [emqx_authz:init_rule(Rule) || Rule <- emqx_authz:check_rules([?RULE3, ?RULE4])], - Rules4 = [emqx_authz:init_rule(Rule) || Rule <- emqx_authz:check_rules([?RULE4, ?RULE1])], - - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny, [])), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), - ?assertEqual({stop, allow}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules2)), - ?assertEqual({stop, allow}, - emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules3)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules4)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny, Rules2)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny, Rules3)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny, Rules4)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny, Rules3)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny, Rules4)), + + {ok, _} = emqx_authz:move(mongo, #{<<"after">> => http}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := redis} + , #{type := file} + , #{type := http} + , #{type := mongo} + ], emqx_authz:lookup()), + ok. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl deleted file mode 100644 index 3ed55e6e1..000000000 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ /dev/null @@ -1,224 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authz_api_SUITE). - --compile(nowarn_export_all). --compile(export_all). - --include("emqx_authz.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --import(emqx_ct_http, [ request_api/3 - , request_api/5 - , get_http_data/1 - , create_default_app/0 - , delete_default_app/0 - , default_auth_header/0 - , auth_header/2 - ]). - --define(HOST, "http://127.0.0.1:8081/"). --define(API_VERSION, "v5"). --define(BASE_PATH, "api"). - --define(RULE1, #{<<"principal">> => <<"all">>, - <<"topics">> => [<<"#">>], - <<"action">> => <<"all">>, - <<"permission">> => <<"deny">>} - ). --define(RULE2, #{<<"principal">> => - #{<<"ipaddress">> => <<"127.0.0.1">>}, - <<"topics">> => - [#{<<"eq">> => <<"#">>}, - #{<<"eq">> => <<"+">>} - ] , - <<"action">> => <<"all">>, - <<"permission">> => <<"allow">>} - ). --define(RULE3,#{<<"principal">> => - #{<<"and">> => [#{<<"username">> => <<"^test?">>}, - #{<<"clientid">> => <<"^test?">>} - ]}, - <<"topics">> => [<<"test">>], - <<"action">> => <<"publish">>, - <<"permission">> => <<"allow">>} - ). --define(RULE4,#{<<"principal">> => - #{<<"or">> => [#{<<"username">> => <<"^test">>}, - #{<<"clientid">> => <<"test?">>} - ]}, - <<"topics">> => [<<"%u">>,<<"%c">>], - <<"action">> => <<"publish">>, - <<"permission">> => <<"deny">>} - ). - -all() -> - emqx_ct:all(?MODULE). - -groups() -> - []. - -init_per_suite(Config) -> - ekka_mnesia:start(), - emqx_mgmt_auth:mnesia(boot), - ok = emqx_ct_helpers:start_apps([emqx_management, emqx_authz], fun set_special_configs/1), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), - - Config. - -end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), - emqx_ct_helpers:stop_apps([emqx_authz, emqx_management]), - ok. - -set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], - applications =>[#{id => "admin", secret => "public"}]}), - ok; -set_special_configs(emqx_authz) -> - emqx_config:put([authorization], #{rules => []}), - ok; -set_special_configs(_App) -> - ok. - -%%------------------------------------------------------------------------------ -%% Testcases -%%------------------------------------------------------------------------------ - -t_api(_) -> - {ok, 200, Result1} = request(get, uri(["authorization"]), []), - ?assertEqual([], get_rules(Result1)), - - lists:foreach(fun(_) -> - {ok, 204, _} = request(post, uri(["authorization"]), - #{<<"action">> => <<"all">>, - <<"permission">> => <<"deny">>, - <<"principal">> => <<"all">>, - <<"topics">> => [<<"#">>]} - ) - end, lists:seq(1, 20)), - {ok, 200, Result2} = request(get, uri(["authorization"]), []), - ?assertEqual(20, length(get_rules(Result2))), - - lists:foreach(fun(Page) -> - Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10", - Url = uri(["authorization" ++ Query]), - {ok, 200, Result} = request(get, Url, []), - ?assertEqual(10, length(get_rules(Result))) - end, lists:seq(1, 2)), - - {ok, 204, _} = request(put, uri(["authorization"]), - [ #{<<"action">> => <<"all">>, <<"permission">> => <<"allow">>, <<"principal">> => <<"all">>, <<"topics">> => [<<"#">>]} - , #{<<"action">> => <<"all">>, <<"permission">> => <<"allow">>, <<"principal">> => <<"all">>, <<"topics">> => [<<"#">>]} - , #{<<"action">> => <<"all">>, <<"permission">> => <<"allow">>, <<"principal">> => <<"all">>, <<"topics">> => [<<"#">>]} - ]), - - {ok, 200, Result3} = request(get, uri(["authorization"]), []), - Rules = get_rules(Result3), - ?assertEqual(3, length(Rules)), - - lists:foreach(fun(#{<<"permission">> := Allow}) -> - ?assertEqual(<<"allow">>, Allow) - end, Rules), - - #{<<"annotations">> := #{<<"id">> := Id}} = lists:nth(2, Rules), - - {ok, 204, _} = request(put, uri(["authorization", binary_to_list(Id)]), - #{<<"action">> => <<"all">>, <<"permission">> => <<"deny">>, - <<"principal">> => <<"all">>, <<"topics">> => [<<"#">>]}), - - {ok, 200, Result4} = request(get, uri(["authorization", binary_to_list(Id)]), []), - ?assertMatch(#{<<"annotations">> := #{<<"id">> := Id}, - <<"permission">> := <<"deny">> - }, jsx:decode(Result4)), - - lists:foreach(fun(#{<<"annotations">> := #{<<"id">> := Id}}) -> - {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Id)]), []) - end, Rules), - {ok, 200, Result5} = request(get, uri(["authorization"]), []), - ?assertEqual([], get_rules(Result5)), - ok. - -t_move_rule(_) -> - ok = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), - [#{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}} - ] = emqx_authz:lookup(), - - {ok, 204, _} = request(post, uri(["authorization", Id4, "move"]), - #{<<"position">> => <<"top">>}), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}} - ], emqx_authz:lookup()), - - {ok, 204, _} = request(post, uri(["authorization", Id1, "move"]), - #{<<"position">> => <<"bottom">>}), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id1}} - ], emqx_authz:lookup()), - - {ok, 204, _} = request(post, uri(["authorization", Id3, "move"]), - #{<<"position">> => #{<<"before">> => Id4}}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id1}} - ], emqx_authz:lookup()), - - {ok, 204, _} = request(post, uri(["authorization", Id2, "move"]), - #{<<"position">> => #{<<"after">> => Id1}}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}} - ], emqx_authz:lookup()), - - ok. - -%%-------------------------------------------------------------------- -%% HTTP Request -%%-------------------------------------------------------------------- - -request(Method, Url, Body) -> - Request = case Body of - [] -> {Url, [auth_header("admin", "public")]}; - _ -> {Url, [auth_header("admin", "public")], "application/json", jsx:encode(Body)} - end, - ct:pal("Method: ~p, Request: ~p", [Method, Request]), - case httpc:request(Method, Request, [], [{body_format, binary}]) of - {error, socket_closed_remotely} -> - {error, socket_closed_remotely}; - {ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } -> - {ok, Code, Return}; - {ok, {Reason, _, _}} -> - {error, Reason} - end. - -uri() -> uri([]). -uri(Parts) when is_list(Parts) -> - NParts = [E || E <- Parts], - ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). - -get_rules(Result) -> - maps:get(<<"rules">>, jsx:decode(Result), []). diff --git a/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl new file mode 100644 index 000000000..1db9fff2b --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl @@ -0,0 +1,135 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_api_settings_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_authz.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). + +-import(emqx_ct_http, [ request_api/3 + , request_api/5 + , get_http_data/1 + , create_default_app/0 + , delete_default_app/0 + , default_auth_header/0 + , auth_header/2 + ]). + +-define(HOST, "http://127.0.0.1:18083/"). +-define(API_VERSION, "v5"). +-define(BASE_PATH, "api"). + +all() -> + emqx_ct:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]), + ok. + +set_special_configs(emqx_dashboard) -> + Config = #{ + default_username => <<"admin">>, + default_password => <<"public">>, + listeners => [#{ + protocol => http, + port => 18083 + }] + }, + emqx_config:put([emqx_dashboard], Config), + ok; +set_special_configs(_App) -> + ok. + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_api(_) -> + Settings1 = #{<<"no_match">> => <<"deny">>, + <<"deny_action">> => <<"disconnect">>, + <<"cache">> => #{ + <<"enable">> => false, + <<"max_size">> => 32, + <<"ttl">> => 60000 + } + }, + + {ok, 200, Result1} = request(put, uri(["authorization", "settings"]), Settings1), + {ok, 200, Result1} = request(get, uri(["authorization", "settings"]), []), + ?assertEqual(Settings1, jsx:decode(Result1)), + + Settings2 = #{<<"no_match">> => <<"allow">>, + <<"deny_action">> => <<"ignore">>, + <<"cache">> => #{ + <<"enable">> => true, + <<"max_size">> => 32, + <<"ttl">> => 60000 + } + }, + + {ok, 200, Result2} = request(put, uri(["authorization", "settings"]), Settings2), + {ok, 200, Result2} = request(get, uri(["authorization", "settings"]), []), + ?assertEqual(Settings2, jsx:decode(Result2)), + + ok. + +%%-------------------------------------------------------------------- +%% HTTP Request +%%-------------------------------------------------------------------- + +request(Method, Url, Body) -> + Request = case Body of + [] -> {Url, [auth_header_()]}; + _ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)} + end, + ct:pal("Method: ~p, Request: ~p", [Method, Request]), + case httpc:request(Method, Request, [], [{body_format, binary}]) of + {error, socket_closed_remotely} -> + {error, socket_closed_remotely}; + {ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } -> + {ok, Code, Return}; + {ok, {Reason, _, _}} -> + {error, Reason} + end. + +uri() -> uri([]). +uri(Parts) when is_list(Parts) -> + NParts = [E || E <- Parts], + ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). + +get_sources(Result) -> + maps:get(<<"sources">>, jsx:decode(Result), []). + +auth_header_() -> + Username = <<"admin">>, + Password = <<"public">>, + {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {"Authorization", "Bearer " ++ binary_to_list(Token)}. diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl new file mode 100644 index 000000000..8c37189c9 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -0,0 +1,305 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_api_sources_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_authz.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). + +-import(emqx_ct_http, [ request_api/3 + , request_api/5 + , get_http_data/1 + , create_default_app/0 + , delete_default_app/0 + , default_auth_header/0 + , auth_header/2 + ]). + +-define(HOST, "http://127.0.0.1:18083/"). +-define(API_VERSION, "v5"). +-define(BASE_PATH, "api"). + +-define(SOURCE1, #{<<"type">> => <<"http">>, + <<"enable">> => true, + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 + }). +-define(SOURCE2, #{<<"type">> => <<"mongo">>, + <<"enable">> => true, + <<"mongo_type">> => <<"sharded">>, + <<"servers">> => [<<"127.0.0.1:27017">>, + <<"192.168.0.1:27017">> + ], + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }). +-define(SOURCE3, #{<<"type">> => <<"mysql">>, + <<"enable">> => true, + <<"server">> => <<"127.0.0.1:3306">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"sql">> => <<"abcb">> + }). +-define(SOURCE4, #{<<"type">> => <<"pgsql">>, + <<"enable">> => true, + <<"server">> => <<"127.0.0.1:5432">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"sql">> => <<"abcb">> + }). +-define(SOURCE5, #{<<"type">> => <<"redis">>, + <<"enable">> => true, + <<"servers">> => [<<"127.0.0.1:6379">>, + <<"127.0.0.1:6380">> + ], + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }). +-define(SOURCE6, #{<<"type">> => <<"file">>, + <<"enable">> => true, + <<"rules">> => + [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>, + <<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> + ] + }). + +all() -> + emqx_ct:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), + meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end), + meck:expect(emqx_resource, health_check, fun(_) -> ok end), + meck:expect(emqx_resource, remove, fun(_) -> ok end ), + + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), + + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + + Config. + +end_per_suite(_Config) -> + {ok, _} = emqx_authz:update(replace, []), + emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]), + meck:unload(emqx_resource), + meck:unload(emqx_schema), + ok. + +set_special_configs(emqx_dashboard) -> + Config = #{ + default_username => <<"admin">>, + default_password => <<"public">>, + listeners => [#{ + protocol => http, + port => 18083 + }] + }, + emqx_config:put([emqx_dashboard], Config), + ok; +set_special_configs(emqx_authz) -> + emqx_config:put([authorization], #{sources => []}), + ok; +set_special_configs(_App) -> + ok. + +init_per_testcase(t_api, Config) -> + meck:new(emqx_rule_id, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_rule_id, gen, fun() -> "fake" end), + + meck:new(emqx, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx, get_config, fun([node, data_dir]) -> + % emqx_ct_helpers:deps_path(emqx_authz, "test"); + {data_dir, Data} = lists:keyfind(data_dir, 1, Config), + Data; + (C) -> meck:passthrough([C]) + end), + Config; +init_per_testcase(_, Config) -> Config. + +end_per_testcase(t_api, _Config) -> + meck:unload(emqx_rule_id), + meck:unload(emqx), + ok; +end_per_testcase(_, _Config) -> ok. + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_api(_) -> + {ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []), + ?assertEqual([], get_sources(Result1)), + + {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), + {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1), + + {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), + Sources = get_sources(Result2), + ?assertMatch([ #{<<"type">> := <<"http">>} + , #{<<"type">> := <<"mongo">>} + , #{<<"type">> := <<"mysql">>} + , #{<<"type">> := <<"pgsql">>} + , #{<<"type">> := <<"redis">>} + , #{<<"type">> := <<"file">>} + ], Sources), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]))), + + {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), + {ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []), + ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result3)), + + {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), + ?SOURCE2#{<<"ssl">> := #{ + <<"enable">> => true, + <<"cacertfile">> => <<"fake cacert file">>, + <<"certfile">> => <<"fake cert file">>, + <<"keyfile">> => <<"fake key file">>, + <<"verify">> => false + }}), + {ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongo"]), []), + ?assertMatch(#{<<"type">> := <<"mongo">>, + <<"ssl">> := #{<<"enable">> := true, + <<"cacertfile">> := <<"fake cacert file">>, + <<"certfile">> := <<"fake cert file">>, + <<"keyfile">> := <<"fake key file">>, + <<"verify">> := false + } + }, jsx:decode(Result4)), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "key-fake.pem"]))), + + lists:foreach(fun(#{<<"type">> := Type}) -> + {ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), []) + end, Sources), + {ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []), + ?assertEqual([], get_sources(Result5)), + ok. + +t_move_source(_) -> + {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), + ?assertMatch([ #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := pgsql} + , #{type := redis} + ], emqx_authz:lookup()), + + {ok, 204, _} = request(post, uri(["authorization", "sources", "pgsql", "move"]), + #{<<"position">> => <<"top">>}), + ?assertMatch([ #{type := pgsql} + , #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} + ], emqx_authz:lookup()), + + {ok, 204, _} = request(post, uri(["authorization", "sources", "http", "move"]), + #{<<"position">> => <<"bottom">>}), + ?assertMatch([ #{type := pgsql} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} + , #{type := http} + ], emqx_authz:lookup()), + + {ok, 204, _} = request(post, uri(["authorization", "sources", "mysql", "move"]), + #{<<"position">> => #{<<"before">> => <<"pgsql">>}}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := mongo} + , #{type := redis} + , #{type := http} + ], emqx_authz:lookup()), + + {ok, 204, _} = request(post, uri(["authorization", "sources", "mongo", "move"]), + #{<<"position">> => #{<<"after">> => <<"http">>}}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := redis} + , #{type := http} + , #{type := mongo} + ], emqx_authz:lookup()), + + ok. + +%%-------------------------------------------------------------------- +%% HTTP Request +%%-------------------------------------------------------------------- + +request(Method, Url, Body) -> + Request = case Body of + [] -> {Url, [auth_header_()]}; + _ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)} + end, + ct:pal("Method: ~p, Request: ~p", [Method, Request]), + case httpc:request(Method, Request, [], [{body_format, binary}]) of + {error, socket_closed_remotely} -> + {error, socket_closed_remotely}; + {ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } -> + {ok, Code, Return}; + {ok, {Reason, _, _}} -> + {error, Reason} + end. + +uri() -> uri([]). +uri(Parts) when is_list(Parts) -> + NParts = [E || E <- Parts], + ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). + +get_sources(Result) -> + maps:get(<<"sources">>, jsx:decode(Result), []). + +auth_header_() -> + Username = <<"admin">>, + Password = <<"public">>, + {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {"Authorization", "Bearer " ++ binary_to_list(Token)}. diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index 3d6d918eb..17763d993 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -21,6 +21,7 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). @@ -29,29 +30,37 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), + meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), - Rules = [#{ <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000 - }, - <<"principal">> => <<"all">>, - <<"type">> => <<"http">>} + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + Rules = [#{<<"type">> => <<"http">>, + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 + } ], - ok = emqx_authz:update(replace, Rules), + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), + meck:unload(emqx_schema), ok. %%------------------------------------------------------------------------------ @@ -65,7 +74,7 @@ t_authz(_) -> protocol => mqtt, mountpoint => <<"fake">>, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, 204, fake_headers} end), diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index 67f9a3bfe..8f4a6f29f 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). + all() -> emqx_ct:all(?MODULE). @@ -29,43 +31,50 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), - Rules = [#{ <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, - <<"principal">> => <<"all">>, - <<"collection">> => <<"fake">>, - <<"find">> => #{<<"a">> => <<"b">>}, - <<"type">> => <<"mongo">>} - ], - ok = emqx_authz:update(replace, Rules), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + Rules = [#{<<"type">> => <<"mongo">>, + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }], + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), + meck:unload(emqx_schema), ok. --define(RULE1,[#{<<"topics">> => [<<"#">>], +-define(SOURCE1,[#{<<"topics">> => [<<"#">>], <<"permission">> => <<"deny">>, <<"action">> => <<"all">>}]). --define(RULE2,[#{<<"topics">> => [<<"eq #">>], +-define(SOURCE2,[#{<<"topics">> => [<<"eq #">>], <<"permission">> => <<"allow">>, <<"action">> => <<"all">>}]). --define(RULE3,[#{<<"topics">> => [<<"test/%c">>], +-define(SOURCE3,[#{<<"topics">> => [<<"test/%c">>], <<"permission">> => <<"allow">>, <<"action">> => <<"subscribe">>}]). --define(RULE4,[#{<<"topics">> => [<<"test/%u">>], +-define(SOURCE4,[#{<<"topics">> => [<<"test/%u">>], <<"permission">> => <<"allow">>, <<"action">> => <<"publish">>}]). @@ -78,34 +87,34 @@ t_authz(_) -> username => <<"test">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, peerhost => {192,168,0,10}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, meck:expect(emqx_resource, query, fun(_, _) -> [] end), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch - meck:expect(emqx_resource, query, fun(_, _) -> ?RULE1 ++ ?RULE2 end), + meck:expect(emqx_resource, query, fun(_, _) -> ?SOURCE1 ++ ?SOURCE2 end), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), - meck:expect(emqx_resource, query, fun(_, _) -> ?RULE2 ++ ?RULE1 end), + meck:expect(emqx_resource, query, fun(_, _) -> ?SOURCE2 ++ ?SOURCE1 end), ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), - meck:expect(emqx_resource, query, fun(_, _) -> ?RULE3 ++ ?RULE4 end), + meck:expect(emqx_resource, query, fun(_, _) -> ?SOURCE3 ++ ?SOURCE4 end), ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index a1120684e..1173b0e3e 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). + all() -> emqx_ct:all(?MODULE). @@ -29,45 +31,50 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), - Rules = [#{ <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, - <<"principal">> => <<"all">>, - <<"sql">> => <<"abcb">>, - <<"type">> => <<"mysql">> }], - emqx_authz:update(replace, Rules), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + Rules = [#{<<"type">> => <<"mysql">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"sql">> => <<"abcb">> + }], + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). + meck:unload(emqx_resource), + meck:unload(emqx_schema), + ok. --define(COLUMNS, [ <<"ipaddress">> - , <<"username">> - , <<"clientid">> - , <<"action">> +-define(COLUMNS, [ <<"action">> , <<"permission">> , <<"topic">> ]). --define(RULE1, [[<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"deny">>, <<"#">>]]). --define(RULE2, [[<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"allow">>, <<"eq #">>]]). --define(RULE3, [[<<>>, <<"^test">>, <<"^test">> ,<<"subscribe">>, <<"allow">>, <<"test/%c">>]]). --define(RULE4, [[<<>>, <<"^test">>, <<"^test">> ,<<"publish">>, <<"allow">>, <<"test/%u">>]]). +-define(SOURCE1, [[<<"all">>, <<"deny">>, <<"#">>]]). +-define(SOURCE2, [[<<"all">>, <<"allow">>, <<"eq #">>]]). +-define(SOURCE3, [[<<"subscribe">>, <<"allow">>, <<"test/%c">>]]). +-define(SOURCE4, [[<<"publish">>, <<"allow">>, <<"test/%u">>]]). %%------------------------------------------------------------------------------ %% Testcases @@ -78,34 +85,34 @@ t_authz(_) -> username => <<"test">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, peerhost => {192,168,0,10}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE1 ++ ?SOURCE2} end), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE2 ++ ?SOURCE1} end), ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE3 ++ ?SOURCE4} end), ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 61a719474..24c2e7b35 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). + all() -> emqx_ct:all(?MODULE). @@ -29,44 +31,50 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), - Rules = [#{ <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, - <<"sql">> => <<"abcb">>, - <<"type">> => <<"pgsql">> }], - emqx_authz:update(replace, Rules), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + Rules = [#{<<"type">> => <<"pgsql">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"sql">> => <<"abcb">> + }], + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). + meck:unload(emqx_resource), + meck:unload(emqx_schema), + ok. --define(COLUMNS, [ {column, <<"ipaddress">>, meck, meck, meck, meck, meck, meck, meck} - , {column, <<"username">>, meck, meck, meck, meck, meck, meck, meck} - , {column, <<"clientid">>, meck, meck, meck, meck, meck, meck, meck} - , {column, <<"action">>, meck, meck, meck, meck, meck, meck, meck} +-define(COLUMNS, [ {column, <<"action">>, meck, meck, meck, meck, meck, meck, meck} , {column, <<"permission">>, meck, meck, meck, meck, meck, meck, meck} , {column, <<"topic">>, meck, meck, meck, meck, meck, meck, meck} ]). --define(RULE1, [{<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"deny">>, <<"#">>}]). --define(RULE2, [{<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"allow">>, <<"eq #">>}]). --define(RULE3, [{<<>>, <<"^test">>, <<"^test">> ,<<"subscribe">>, <<"allow">>, <<"test/%c">>}]). --define(RULE4, [{<<>>, <<"^test">>, <<"^test">> ,<<"publish">>, <<"allow">>, <<"test/%u">>}]). +-define(SOURCE1, [{<<"all">>, <<"deny">>, <<"#">>}]). +-define(SOURCE2, [{<<"all">>, <<"allow">>, <<"eq #">>}]). +-define(SOURCE3, [{<<"subscribe">>, <<"allow">>, <<"test/%c">>}]). +-define(SOURCE4, [{<<"publish">>, <<"allow">>, <<"test/%u">>}]). %%------------------------------------------------------------------------------ %% Testcases @@ -77,34 +85,34 @@ t_authz(_) -> username => <<"test">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, peerhost => {192,168,0,10}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE1 ++ ?SOURCE2} end), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE2 ++ ?SOURCE1} end), ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"+">>)), - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE3 ++ ?SOURCE4} end), ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 4a1765589..9949e8b51 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -21,6 +21,7 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). @@ -29,35 +30,44 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), - Rules = [#{ <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, - <<"cmd">> => <<"HGETALL mqtt_authz:%u">>, - <<"type">> => <<"redis">> }], - emqx_authz:update(replace, Rules), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + Rules = [#{<<"type">> => <<"redis">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }], + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). + meck:unload(emqx_resource), + meck:unload(emqx_schema), + ok. --define(RULE1, [<<"test/%u">>, <<"publish">>]). --define(RULE2, [<<"test/%c">>, <<"publish">>]). --define(RULE3, [<<"#">>, <<"subscribe">>]). +-define(SOURCE1, [<<"test/%u">>, <<"publish">>]). +-define(SOURCE2, [<<"test/%c">>, <<"publish">>]). +-define(SOURCE3, [<<"#">>, <<"subscribe">>]). %%------------------------------------------------------------------------------ %% Testcases @@ -68,7 +78,7 @@ t_authz(_) -> username => <<"username">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end), @@ -79,7 +89,7 @@ t_authz(_) -> emqx_access_control:authorize(ClientInfo, publish, <<"#">>)), - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE1 ++ ?RULE2} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?SOURCE1 ++ ?SOURCE2} end), % nomatch ?assertEqual(deny, emqx_access_control:authorize(ClientInfo, subscribe, <<"+">>)), @@ -92,7 +102,7 @@ t_authz(_) -> ?assertEqual(allow, emqx_access_control:authorize(ClientInfo, publish, <<"test/clientid">>)), - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE3} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?SOURCE3} end), ?assertEqual(allow, emqx_access_control:authorize(ClientInfo, subscribe, <<"#">>)), diff --git a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl new file mode 100644 index 000000000..c38d99cba --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl @@ -0,0 +1,138 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_rule_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_authz.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(SOURCE1, {deny, all, all, ["#"]}). +-define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}). +-define(SOURCE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, ["%c"]}). +-define(SOURCE4, {allow, {'and', [{clientid, "^test?"}, {username, "^test?"}]}, publish, ["topic/test"]}). +-define(SOURCE5, {allow, {'or', [{username, "^test"}, {clientid, "test?"}]}, publish, ["%u", "%c"]}). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_ct_helpers:start_apps([emqx_authz]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([emqx_authz]), + ok. + +t_compile(_) -> + ?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?SOURCE1)), + + ?assertEqual({allow, {ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]}, emqx_authz_rule:compile(?SOURCE2)), + + ?assertEqual({allow, + {ipaddrs,[{{127,0,0,1},{127,0,0,1},32}, + {{192,168,1,0},{192,168,1,255},24}]}, + subscribe, + [{pattern,[<<"%c">>]}] + }, emqx_authz_rule:compile(?SOURCE3)), + + ?assertMatch({allow, + {'and', [{clientid, {re_pattern, _, _, _, _}}, {username, {re_pattern, _, _, _, _}}]}, + publish, + [[<<"topic">>, <<"test">>]] + }, emqx_authz_rule:compile(?SOURCE4)), + + ?assertMatch({allow, + {'or', [{username, {re_pattern, _, _, _, _}}, {clientid, {re_pattern, _, _, _, _}}]}, + publish, + [{pattern, [<<"%u">>]}, {pattern, [<<"%c">>]}] + }, emqx_authz_rule:compile(?SOURCE5)), + ok. + + +t_match(_) -> + ClientInfo1 = #{clientid => <<"test">>, + username => <<"test">>, + peerhost => {127,0,0,1}, + zone => default, + listener => {tcp, default} + }, + ClientInfo2 = #{clientid => <<"test">>, + username => <<"test">>, + peerhost => {192,168,1,10}, + zone => default, + listener => {tcp, default} + }, + ClientInfo3 = #{clientid => <<"test">>, + username => <<"fake">>, + peerhost => {127,0,0,1}, + zone => default, + listener => {tcp, default} + }, + ClientInfo4 = #{clientid => <<"fake">>, + username => <<"test">>, + peerhost => {127,0,0,1}, + zone => default, + listener => {tcp, default} + }, + + ?assertEqual({matched, deny}, + emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?SOURCE1))), + ?assertEqual({matched, deny}, + emqx_authz_rule:match(ClientInfo2, subscribe, <<"+">>, emqx_authz_rule:compile(?SOURCE1))), + ?assertEqual({matched, deny}, + emqx_authz_rule:match(ClientInfo3, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE1))), + + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?SOURCE2))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo1, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE2))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo2, subscribe, <<"#">>, emqx_authz_rule:compile(?SOURCE2))), + + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo1, subscribe, <<"test">>, emqx_authz_rule:compile(?SOURCE3))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo2, subscribe, <<"test">>, emqx_authz_rule:compile(?SOURCE3))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo2, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE3))), + + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo1, publish, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE4))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo2, publish, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE4))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo3, publish, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE4))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo4, publish, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE4))), + + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo1, publish, <<"test">>, emqx_authz_rule:compile(?SOURCE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo2, publish, <<"test">>, emqx_authz_rule:compile(?SOURCE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo3, publish, <<"test">>, emqx_authz_rule:compile(?SOURCE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo3, publish, <<"fake">>, emqx_authz_rule:compile(?SOURCE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo4, publish, <<"test">>, emqx_authz_rule:compile(?SOURCE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo4, publish, <<"fake">>, emqx_authz_rule:compile(?SOURCE5))), + + ok. + diff --git a/apps/emqx_data_bridge/.gitignore b/apps/emqx_auto_subscribe/.gitignore similarity index 100% rename from apps/emqx_data_bridge/.gitignore rename to apps/emqx_auto_subscribe/.gitignore diff --git a/apps/emqx_auto_subscribe/LICENSE b/apps/emqx_auto_subscribe/LICENSE new file mode 100644 index 000000000..e16434416 --- /dev/null +++ b/apps/emqx_auto_subscribe/LICENSE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021, DDDHuang <904897578@qq.com>. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/apps/emqx_auto_subscribe/README.md b/apps/emqx_auto_subscribe/README.md new file mode 100644 index 000000000..96d368715 --- /dev/null +++ b/apps/emqx_auto_subscribe/README.md @@ -0,0 +1,9 @@ +emqx_auto_subscribe +===== + +An OTP application + +Build +----- + + $ rebar3 compile diff --git a/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf b/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf new file mode 100644 index 000000000..f6d041dab --- /dev/null +++ b/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf @@ -0,0 +1,28 @@ + +auto_subscribe { + topics = [ + # { + # topic = "/c/${clientid}", + # qos = 0 + # rh = 0 + # rap = 0 + # nl = 0 + # } + # { + # topic = "/u/${username}", + # }, + # { + # topic = "/h/${host}", + # qos = 2 + # }, + # { + # topic = "/p/${port}", + # }, + # { + # topic = "/topic/abc", + # }, + # { + # topic = "/client/${clientid}/username/${username}/host/${host}/port/${port}", + # } + ] +} diff --git a/apps/emqx_auto_subscribe/include/emqx_auto_subscribe.hrl b/apps/emqx_auto_subscribe/include/emqx_auto_subscribe.hrl new file mode 100644 index 000000000..e69de29bb diff --git a/apps/emqx_auto_subscribe/mix.exs b/apps/emqx_auto_subscribe/mix.exs new file mode 100644 index 000000000..7fe4f4a4e --- /dev/null +++ b/apps/emqx_auto_subscribe/mix.exs @@ -0,0 +1,32 @@ +defmodule EMQXAutoSubscribe.MixProject do + use Mix.Project + + def project do + [ + app: :emqx_auto_subscribe, + version: "0.1.0", + build_path: "../../_build", + config_path: "../../config/config.exs", + deps_path: "../../deps", + lockfile: "../../mix.lock", + elixir: "~> 1.12", + start_permanent: Mix.env() == :prod, + deps: deps(), + description: "EMQ X Auto Subscribe" + ] + end + + def application do + [ + registered: [], + mod: {:emqx_auto_subscribe_app, []}, + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:emqx, in_umbrella: true} + ] + end +end diff --git a/apps/emqx_auto_subscribe/rebar.config b/apps/emqx_auto_subscribe/rebar.config new file mode 100644 index 000000000..88793f7ba --- /dev/null +++ b/apps/emqx_auto_subscribe/rebar.config @@ -0,0 +1,6 @@ +{erl_opts, [debug_info]}. +{deps, []}. + +{shell, [ + {apps, [emqx_auto_subscribe]} +]}. diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src new file mode 100644 index 000000000..0d87af87a --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src @@ -0,0 +1,15 @@ +{application, emqx_auto_subscribe, + [{description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {emqx_auto_subscribe_app, []}}, + {applications, + [kernel, + stdlib + ]}, + {env,[]}, + {modules, []}, + + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl new file mode 100644 index 000000000..b13b3760a --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl @@ -0,0 +1,80 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_auto_subscribe). + +-define(HOOK_POINT, 'client.connected'). + +-define(MAX_AUTO_SUBSCRIBE, 20). + +-export([load/0]). + +-export([ max_limit/0 + , list/0 + , update/1 + , test/1 + ]). + +%% hook callback +-export([on_client_connected/3]). + +load() -> + update_hook(). + +max_limit() -> + ?MAX_AUTO_SUBSCRIBE. + +list() -> + emqx:get_config([auto_subscribe, topics], []). + +update(Topics) -> + update_(Topics). + +test(_) -> +%% TODO: test rule with info map + ok. + +% test(Topic) when is_map(Topic) -> +% test([Topic]); + +% test(Topics) when is_list(Topics) -> +% PlaceHolders = emqx_auto_subscribe_placeholder:generate(Topics), +% ClientInfo = #{}, +% ConnInfo = #{}, +% emqx_auto_subscribe_placeholder:to_topic_table([PlaceHolders], ClientInfo, ConnInfo). + +%%-------------------------------------------------------------------- +%% hook + +on_client_connected(ClientInfo, ConnInfo, {TopicHandler, Options}) -> + TopicTables = erlang:apply(TopicHandler, handle, [ClientInfo, ConnInfo, Options]), + self() ! {subscribe, TopicTables}; +on_client_connected(_, _, _) -> + ok. + +%%-------------------------------------------------------------------- +%% internal + +update_(Topics) when length(Topics) =< ?MAX_AUTO_SUBSCRIBE -> + {ok, _} = emqx:update_config([auto_subscribe, topics], Topics), + update_hook(); +update_(_Topics) -> + {error, quota_exceeded}. + +update_hook() -> + {TopicHandler, Options} = emqx_auto_subscribe_handler:init(), + emqx_hooks:put(?HOOK_POINT, {?MODULE, on_client_connected, [{TopicHandler, Options}]}), + ok. diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl new file mode 100644 index 000000000..97c9674b9 --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl @@ -0,0 +1,69 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_auto_subscribe_api). + +-behaviour(minirest_api). + +-export([api_spec/0]). + +-export([auto_subscribe/2]). + +-define(EXCEED_LIMIT, 'EXCEED_LIMIT'). +-define(BAD_REQUEST, 'BAD_REQUEST'). + +api_spec() -> + {[auto_subscribe_api()], []}. + +schema() -> + emqx_mgmt_util:schema( + emqx_mgmt_api_configs:gen_schema( + emqx:get_raw_config([auto_subscribe, topics])), <<"">>). + +auto_subscribe_api() -> + Metadata = #{ + get => #{ + description => <<"Auto subscribe list">>, + responses => #{ + <<"200">> => schema()}}, + put => #{ + description => <<"Update auto subscribe topic list">>, + 'requestBody' => schema(), + responses => #{ + <<"200">> => schema(), + <<"400">> => emqx_mgmt_util:error_schema( + <<"Request body required">>, [?BAD_REQUEST]), + <<"409">> => emqx_mgmt_util:error_schema( + <<"Auto Subscribe topics max limit">>, [?EXCEED_LIMIT])}} + }, + {"/mqtt/auto_subscribe", Metadata, auto_subscribe}. + +%%%============================================================================================== +%% api apply +auto_subscribe(get, _) -> + {200, emqx_auto_subscribe:list()}; + +auto_subscribe(put, #{body := #{}}) -> + {400, #{code => ?BAD_REQUEST, message => <<"Request body required">>}}; +auto_subscribe(put, #{body := Params}) -> + case emqx_auto_subscribe:update(Params) of + {error, quota_exceeded} -> + Message = list_to_binary(io_lib:format("Max auto subscribe topic count is ~p", + [emqx_auto_subscribe:max_limit()])), + {409, #{code => ?EXCEED_LIMIT, message => Message}}; + ok -> + {200, emqx_auto_subscribe:list()} + end. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl similarity index 79% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl rename to apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl index a145009c9..0be813bcd 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl @@ -14,18 +14,18 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_mqtt_app). +-module(emqx_auto_subscribe_app). -behaviour(application). -export([start/2, stop/1]). start(_StartType, _StartArgs) -> - emqx_ctl:register_command(bridges, {emqx_bridge_mqtt_cli, cli}, []), - emqx_bridge_worker:register_metrics(), - emqx_bridge_mqtt_sup:start_link(). + {ok, Sup} = emqx_auto_subscribe_sup:start_link(), + ok = emqx_auto_subscribe:load(), + {ok, Sup}. stop(_State) -> - emqx_ctl:unregister_command(bridges), ok. +%% internal functions diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl new file mode 100644 index 000000000..70779770d --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl @@ -0,0 +1,73 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_auto_subscribe_placeholder). + +-export([generate/1]). + +-export([to_topic_table/3]). + +-spec(generate(list() | map()) -> list() | map()). +generate(Topics) when is_list(Topics) -> + [generate(Topic) || Topic <- Topics]; + +generate(T0 = #{topic := Topic}) -> + T = maps:without([topic], T0), + T#{placeholder => generate(Topic, [])}. + +-spec(to_topic_table(list(), map(), map()) -> list()). +to_topic_table(PHs, ClientInfo, ConnInfo) -> + [begin + Topic0 = to_topic(PlaceHolder, ClientInfo, ConnInfo, []), + {Topic, Opts} = emqx_topic:parse(Topic0), + {Topic, Opts#{qos => Qos, rh => RH, rap => RAP, nl => NL}} + end || #{qos := Qos, rh := RH, rap := RAP, nl := NL, placeholder := PlaceHolder} <- PHs]. + +%%-------------------------------------------------------------------- +%% internal + +generate(<<"">>, Result) -> + lists:reverse(Result); +generate(<<"${clientid}", Tail/binary>>, Result) -> + generate(Tail, [clientid | Result]); +generate(<<"${username}", Tail/binary>>, Result) -> + generate(Tail, [username | Result]); +generate(<<"${host}", Tail/binary>>, Result) -> + generate(Tail, [host | Result]); +generate(<<"${port}", Tail/binary>>, Result) -> + generate(Tail, [port | Result]); +generate(<>, []) -> + generate(Tail, [<>]); +generate(<>, [R | Result]) when is_binary(R) -> + generate(Tail, [<> | Result]); +generate(<>, [R | Result]) when is_atom(R) -> + generate(Tail, [<> | [R | Result]]). + +to_topic([], _, _, Res) -> + list_to_binary(lists:reverse(Res)); +to_topic([Binary | PTs], C, Co, Res) when is_binary(Binary) -> + to_topic(PTs, C, Co, [Binary | Res]); +to_topic([clientid | PTs], C = #{clientid := ClientID}, Co, Res) -> + to_topic(PTs, C, Co, [ClientID | Res]); +to_topic([username | PTs], C = #{username := undefined}, Co, Res) -> + to_topic(PTs, C, Co, [<<"${username}">> | Res]); +to_topic([username | PTs], C = #{username := Username}, Co, Res) -> + to_topic(PTs, C, Co, [Username | Res]); +to_topic([host | PTs], C, Co = #{peername := {Host, _}}, Res) -> + HostBinary = list_to_binary(inet:ntoa(Host)), + to_topic(PTs, C, Co, [HostBinary | Res]); +to_topic([port | PTs], C, Co = #{peername := {_, Port}}, Res) -> + PortBinary = integer_to_binary(Port), + to_topic(PTs, C, Co, [PortBinary | Res]). diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl new file mode 100644 index 000000000..5b781455d --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl @@ -0,0 +1,48 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_auto_subscribe_schema). + +-behaviour(hocon_schema). + +-include_lib("typerefl/include/types.hrl"). + +-export([ namespace/0 + , roots/0 + , fields/1]). + +namespace() -> "auto_subscribe". + +roots() -> + ["auto_subscribe"]. + +fields("auto_subscribe") -> + [ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))} + ]; + +fields("topic") -> + [ {topic, sc(binary(), #{})} + , {qos, sc(typerefl:union([0, 1, 2]), #{default => 0})} + , {rh, sc(typerefl:union([0, 1, 2]), #{default => 0})} + , {rap, sc(typerefl:union([0, 1]), #{default => 0})} + , {nl, sc(typerefl:union([0, 1]), #{default => 0})} + ]. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +sc(Type, Meta) -> + hoconsc:mk(Type, Meta). diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_sup.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_sup.erl new file mode 100644 index 000000000..9e35f825f --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_sup.erl @@ -0,0 +1,37 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_auto_subscribe_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +-define(SERVER, ?MODULE). + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +init([]) -> + SupFlags = #{strategy => one_for_all, + intensity => 0, + period => 1}, + ChildSpecs = [], + {ok, {SupFlags, ChildSpecs}}. + +%% internal functions diff --git a/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_handler.erl b/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_handler.erl new file mode 100644 index 000000000..11865153a --- /dev/null +++ b/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_handler.erl @@ -0,0 +1,29 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_auto_subscribe_handler). + +-export([init/0]). + +-spec(init() -> {Module :: atom(), Config :: term()}). +init() -> + do_init(emqx:get_config([auto_subscribe], #{})). + +do_init(Config = #{topics := _Topics}) -> + Options = emqx_auto_subscribe_internal:init(Config), + {emqx_auto_subscribe_internal, Options}; + +do_init(_) -> + erlang:error("only support in EMQ X Enterprise"). diff --git a/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_internal.erl b/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_internal.erl new file mode 100644 index 000000000..f3cd58980 --- /dev/null +++ b/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_internal.erl @@ -0,0 +1,29 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_auto_subscribe_internal). + +-export([init/1]). + +-export([handle/3]). + +-spec(init(Config :: map()) -> HandlerOptions :: term()). +init(#{topics := Topics}) -> + emqx_auto_subscribe_placeholder:generate(Topics). + +-spec(handle(ClientInfo :: map(), ConnInfo :: map(), HandlerOptions :: term()) -> + TopicTables :: list()). +handle(ClientInfo, ConnInfo, PlaceHolders) -> + emqx_auto_subscribe_placeholder:to_topic_table(PlaceHolders, ClientInfo, ConnInfo). diff --git a/apps/emqx_bridge/.gitignore b/apps/emqx_bridge/.gitignore new file mode 100644 index 000000000..f1c455451 --- /dev/null +++ b/apps/emqx_bridge/.gitignore @@ -0,0 +1,19 @@ +.rebar3 +_* +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +_build +.idea +*.iml +rebar3.crashdump +*~ diff --git a/apps/emqx_data_bridge/README.md b/apps/emqx_bridge/README.md similarity index 95% rename from apps/emqx_data_bridge/README.md rename to apps/emqx_bridge/README.md index 8f76f17a5..0f274eea1 100644 --- a/apps/emqx_data_bridge/README.md +++ b/apps/emqx_bridge/README.md @@ -1,4 +1,4 @@ -# emqx_data_bridge +# emqx_bridge EMQ X Data Bridge is an application that managing the resources (see emqx_resource) used by emqx rule engine. diff --git a/apps/emqx_bridge/etc/emqx_bridge.conf b/apps/emqx_bridge/etc/emqx_bridge.conf new file mode 100644 index 000000000..08873228d --- /dev/null +++ b/apps/emqx_bridge/etc/emqx_bridge.conf @@ -0,0 +1,49 @@ +##-------------------------------------------------------------------- +## EMQ X Bridge +##-------------------------------------------------------------------- + +#bridges.mqtt.my_mqtt_bridge { +# server = "127.0.0.1:1883" +# proto_ver = "v4" +# ## the clientid will be the concatenation of `clientid_prefix` and ids in `in` and `out`. +# clientid_prefix = "bridge_client:" +# username = "username1" +# password = "" +# clean_start = true +# keepalive = 300 +# retry_interval = "30s" +# max_inflight = 32 +# reconnect_interval = "30s" +# bridge_mode = true +# replayq { +# dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/" +# seg_bytes = "100MB" +# offload = false +# max_total_bytes = "1GB" +# } +# ssl { +# enable = false +# keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" +# certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" +# cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" +# } +# ## we will create one MQTT connection for each element of the `in` +# in: [{ +# id = "pull_msgs_from_aws" +# subscribe_remote_topic = "aws/#" +# subscribe_qos = 1 +# local_topic = "from_aws/${topic}" +# payload = "${payload}" +# qos = "${qos}" +# retain = "${retain}" +# }] +# ## we will create one MQTT connection for each element of the `out` +# out: [{ +# id = "push_msgs_to_aws" +# subscribe_local_topic = "emqx/#" +# remote_topic = "from_emqx/${topic}" +# payload = "${payload}" +# qos = 1 +# retain = false +# }] +#} diff --git a/apps/emqx_bridge/mix.exs b/apps/emqx_bridge/mix.exs new file mode 100644 index 000000000..09640e0a4 --- /dev/null +++ b/apps/emqx_bridge/mix.exs @@ -0,0 +1,33 @@ +defmodule EMQXBridge.MixProject do + use Mix.Project + + def project do + [ + app: :emqx_bridge, + version: "0.1.0", + build_path: "../../_build", + config_path: "../../config/config.exs", + deps_path: "../../deps", + lockfile: "../../mix.lock", + elixir: "~> 1.12", + start_permanent: Mix.env() == :prod, + deps: deps(), + description: "EMQ X Bridge" + ] + end + + def application do + [ + registered: [], + mod: {:emqx_bridge_app, []}, + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:emqx, in_umbrella: true}, + {:emqx_connector, in_umbrella: true} + ] + end +end diff --git a/apps/emqx_data_bridge/rebar.config b/apps/emqx_bridge/rebar.config similarity index 73% rename from apps/emqx_data_bridge/rebar.config rename to apps/emqx_bridge/rebar.config index cf4cfcf1b..3fd6b41e0 100644 --- a/apps/emqx_data_bridge/rebar.config +++ b/apps/emqx_bridge/rebar.config @@ -3,5 +3,5 @@ {shell, [ % {config, "config/sys.config"}, - {apps, [emqx_data_bridge]} + {apps, [emqx_bridge]} ]}. diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge.app.src b/apps/emqx_bridge/src/emqx_bridge.app.src similarity index 69% rename from apps/emqx_data_bridge/src/emqx_data_bridge.app.src rename to apps/emqx_bridge/src/emqx_bridge.app.src index 84486da19..9c0f6b779 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge.app.src +++ b/apps/emqx_bridge/src/emqx_bridge.app.src @@ -1,12 +1,13 @@ -{application, emqx_data_bridge, +{application, emqx_bridge, [{description, "An OTP application"}, {vsn, "0.1.0"}, {registered, []}, - {mod, {emqx_data_bridge_app, []}}, + {mod, {emqx_bridge_app, []}}, {applications, [kernel, stdlib, - emqx + emqx, + emqx_connector ]}, {env,[]}, {modules, []}, diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl similarity index 75% rename from apps/emqx_data_bridge/src/emqx_data_bridge.erl rename to apps/emqx_bridge/src/emqx_bridge.erl index 9c27ff8d5..75ebfac0c 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge). +-module(emqx_bridge). -export([ load_bridges/0 , resource_type/1 @@ -27,15 +27,17 @@ ]). load_bridges() -> - Bridges = emqx_config:get([emqx_data_bridge, bridges], []), - emqx_data_bridge_monitor:ensure_all_started(Bridges). + Bridges = emqx:get_config([bridges], #{}), + emqx_bridge_monitor:ensure_all_started(Bridges). +resource_type(mqtt) -> emqx_connector_mqtt; resource_type(mysql) -> emqx_connector_mysql; resource_type(pgsql) -> emqx_connector_pgsql; resource_type(mongo) -> emqx_connector_mongo; resource_type(redis) -> emqx_connector_redis; resource_type(ldap) -> emqx_connector_ldap. +bridge_type(emqx_connector_mqtt) -> mqtt; bridge_type(emqx_connector_mysql) -> mysql; bridge_type(emqx_connector_pgsql) -> pgsql; bridge_type(emqx_connector_mongo) -> mongo; @@ -43,13 +45,14 @@ bridge_type(emqx_connector_redis) -> redis; bridge_type(emqx_connector_ldap) -> ldap. name_to_resource_id(BridgeName) -> - <<"bridge:", BridgeName/binary>>. + Name = bin(BridgeName), + <<"bridge:", Name/binary>>. resource_id_to_name(<<"bridge:", BridgeName/binary>> = _ResourceId) -> BridgeName. list_bridges() -> - emqx_resource_api:list_instances(fun emqx_data_bridge:is_bridge/1). + emqx_resource_api:list_instances(fun emqx_bridge:is_bridge/1). is_bridge(#{id := <<"bridge:", _/binary>>}) -> true; @@ -57,7 +60,11 @@ is_bridge(_Data) -> false. config_key_path() -> - [emqx_data_bridge, bridges]. + [emqx_bridge, bridges]. update_config(ConfigReq) -> - emqx_config:update(config_key_path(), ConfigReq). + emqx:update_config(config_key_path(), ConfigReq). + +bin(Bin) when is_binary(Bin) -> Bin; +bin(Str) when is_list(Str) -> list_to_binary(Str); +bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl similarity index 81% rename from apps/emqx_data_bridge/src/emqx_data_bridge_api.erl rename to apps/emqx_bridge/src/emqx_bridge_api.erl index 7b1b4981d..c10875e55 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_api). +-module(emqx_bridge_api). -rest_api(#{ name => list_data_bridges , method => 'GET' @@ -61,10 +61,10 @@ list_bridges(_Binding, _Params) -> {200, #{code => 0, data => [format_api_reply(Data) || - Data <- emqx_data_bridge:list_bridges()]}}. + Data <- emqx_bridge:list_bridges()]}}. get_bridge(#{name := Name}, _Params) -> - case emqx_resource:get_instance(emqx_data_bridge:name_to_resource_id(Name)) of + case emqx_resource:get_instance(emqx_bridge:name_to_resource_id(Name)) of {ok, Data} -> {200, #{code => 0, data => format_api_reply(emqx_resource_api:format_data(Data))}}; {error, not_found} -> @@ -75,12 +75,12 @@ create_bridge(#{name := Name}, Params) -> Config = proplists:get_value(<<"config">>, Params), BridgeType = proplists:get_value(<<"type">>, Params), case emqx_resource:check_and_create( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of + {ok, already_created} -> + {400, #{code => 102, message => <<"bridge already created: ", Name/binary>>}}; {ok, Data} -> update_config_and_reply(Name, BridgeType, Config, Data); - {error, already_created} -> - {400, #{code => 102, message => <<"bridge already created: ", Name/binary>>}}; {error, Reason0} -> Reason = emqx_resource_api:stringnify(Reason0), {500, #{code => 102, message => <<"create bridge ", Name/binary, @@ -91,8 +91,8 @@ update_bridge(#{name := Name}, Params) -> Config = proplists:get_value(<<"config">>, Params), BridgeType = proplists:get_value(<<"type">>, Params), case emqx_resource:check_and_update( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of {ok, Data} -> update_config_and_reply(Name, BridgeType, Config, Data); {error, not_found} -> @@ -104,27 +104,27 @@ update_bridge(#{name := Name}, Params) -> end. delete_bridge(#{name := Name}, _Params) -> - case emqx_resource:remove(emqx_data_bridge:name_to_resource_id(Name)) of + case emqx_resource:remove(emqx_bridge:name_to_resource_id(Name)) of ok -> delete_config_and_reply(Name); {error, Reason} -> {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}} end. format_api_reply(#{resource_type := Type, id := Id, config := Conf, status := Status}) -> - #{type => emqx_data_bridge:bridge_type(Type), - name => emqx_data_bridge:resource_id_to_name(Id), + #{type => emqx_bridge:bridge_type(Type), + name => emqx_bridge:resource_id_to_name(Id), config => Conf, status => Status}. % format_conf(#{resource_type := Type, id := Id, config := Conf}) -> -% #{type => Type, name => emqx_data_bridge:resource_id_to_name(Id), +% #{type => Type, name => emqx_bridge:resource_id_to_name(Id), % config => Conf}. % get_all_configs() -> -% [format_conf(Data) || Data <- emqx_data_bridge:list_bridges()]. +% [format_conf(Data) || Data <- emqx_bridge:list_bridges()]. update_config_and_reply(Name, BridgeType, Config, Data) -> - case emqx_data_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of - ok -> + case emqx_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of + {ok, _} -> {200, #{code => 0, data => format_api_reply( emqx_resource_api:format_data(Data))}}; {error, Reason} -> @@ -132,8 +132,8 @@ update_config_and_reply(Name, BridgeType, Config, Data) -> end. delete_config_and_reply(Name) -> - case emqx_data_bridge:update_config({delete, Name}) of - ok -> {200, #{code => 0, data => #{}}}; + case emqx_bridge:update_config({delete, Name}) of + {ok, _} -> {200, #{code => 0, data => #{}}}; {error, Reason} -> {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}} end. diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl b/apps/emqx_bridge/src/emqx_bridge_app.erl similarity index 81% rename from apps/emqx_data_bridge/src/emqx_data_bridge_app.erl rename to apps/emqx_bridge/src/emqx_bridge_app.erl index 26128841b..cfefe118f 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl +++ b/apps/emqx_bridge/src/emqx_bridge_app.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_app). +-module(emqx_bridge_app). -behaviour(application). @@ -22,9 +22,9 @@ -export([start/2, stop/1, pre_config_update/2]). start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_data_bridge_sup:start_link(), - ok = emqx_data_bridge:load_bridges(), - emqx_config_handler:add_handler(emqx_data_bridge:config_key_path(), ?MODULE), + {ok, Sup} = emqx_bridge_sup:start_link(), + ok = emqx_bridge:load_bridges(), + emqx_config_handler:add_handler(emqx_bridge:config_key_path(), ?MODULE), {ok, Sup}. stop(_State) -> @@ -32,12 +32,12 @@ stop(_State) -> %% internal functions pre_config_update({update, Bridge = #{<<"name">> := Name}}, OldConf) -> - [Bridge | remove_bridge(Name, OldConf)]; + {ok, [Bridge | remove_bridge(Name, OldConf)]}; pre_config_update({delete, Name}, OldConf) -> - remove_bridge(Name, OldConf); + {ok, remove_bridge(Name, OldConf)}; pre_config_update(NewConf, _OldConf) when is_list(NewConf) -> %% overwrite the entire config! - NewConf. + {ok, NewConf}. remove_bridge(_Name, undefined) -> []; diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl b/apps/emqx_bridge/src/emqx_bridge_monitor.erl similarity index 83% rename from apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl rename to apps/emqx_bridge/src/emqx_bridge_monitor.erl index d408a8062..d76af5fb9 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl +++ b/apps/emqx_bridge/src/emqx_bridge_monitor.erl @@ -15,7 +15,7 @@ %%-------------------------------------------------------------------- %% This process monitors all the data bridges, and try to restart a bridge %% when one of it stopped. --module(emqx_data_bridge_monitor). +-module(emqx_bridge_monitor). -behaviour(gen_server). @@ -65,16 +65,20 @@ code_change(_OldVsn, State, _Extra) -> %%============================================================================ load_bridges(Configs) -> - lists:foreach(fun load_bridge/1, Configs). + lists:foreach(fun({Type, NamedConf}) -> + lists:foreach(fun({Name, Conf}) -> + load_bridge(Name, Type, Conf) + end, maps:to_list(NamedConf)) + end, maps:to_list(Configs)). %% TODO: move this monitor into emqx_resource %% emqx_resource:check_and_create_local(ResourceId, ResourceType, Config, #{keep_retry => true}). -load_bridge(#{name := Name, type := Type, config := Config}) -> +load_bridge(Name, Type, Config) -> case emqx_resource:create_local( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(Type), Config) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(Type), Config) of + {ok, already_created} -> ok; {ok, _} -> ok; - {error, already_created} -> ok; {error, Reason} -> error({load_bridge, Reason}) end. diff --git a/apps/emqx_bridge/src/emqx_bridge_schema.erl b/apps/emqx_bridge/src/emqx_bridge_schema.erl new file mode 100644 index 000000000..beb0f282c --- /dev/null +++ b/apps/emqx_bridge/src/emqx_bridge_schema.erl @@ -0,0 +1,17 @@ +-module(emqx_bridge_schema). + +-export([roots/0, fields/1]). + +%%====================================================================================== +%% Hocon Schema Definitions + +roots() -> ["bridges"]. + +fields("bridges") -> + [{mqtt, hoconsc:ref(?MODULE, "mqtt")}]; + +fields("mqtt") -> + [{"$name", hoconsc:ref(?MODULE, "mqtt_bridge")}]; + +fields("mqtt_bridge") -> + emqx_connector_mqtt:fields("config"). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl b/apps/emqx_bridge/src/emqx_bridge_sup.erl similarity index 86% rename from apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl rename to apps/emqx_bridge/src/emqx_bridge_sup.erl index a699a72a0..fd12b1a99 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl +++ b/apps/emqx_bridge/src/emqx_bridge_sup.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_sup). +-module(emqx_bridge_sup). -behaviour(supervisor). @@ -31,11 +31,11 @@ init([]) -> intensity => 10, period => 10}, ChildSpecs = [ - #{id => emqx_data_bridge_monitor, - start => {emqx_data_bridge_monitor, start_link, []}, + #{id => emqx_bridge_monitor, + start => {emqx_bridge_monitor, start_link, []}, restart => permanent, type => worker, - modules => [emqx_data_bridge_monitor]} + modules => [emqx_bridge_monitor]} ], {ok, {SupFlags, ChildSpecs}}. diff --git a/apps/emqx_bridge_mqtt/.gitignore b/apps/emqx_bridge_mqtt/.gitignore deleted file mode 100644 index bf9523be5..000000000 --- a/apps/emqx_bridge_mqtt/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin/*.beam -rel -_build -.concrete/DEV_MODE -.rebar -.erlang.mk -data -ebin -emqx_bridge_mqtt.d -*.rendered -.rebar3/ -*.coverdata -rebar.lock -.DS_Store -Mnesia.*/ \ No newline at end of file diff --git a/apps/emqx_bridge_mqtt/README.md b/apps/emqx_bridge_mqtt/README.md deleted file mode 100644 index 812645627..000000000 --- a/apps/emqx_bridge_mqtt/README.md +++ /dev/null @@ -1,265 +0,0 @@ -# EMQ Bridge MQTT - -The concept of **Bridge** means that EMQ X supports forwarding messages -of one of its own topics to another MQTT Broker in some way. - -**Bridge** differs from **Cluster** in that the bridge does not -replicate the topic trie and routing tables and only forwards MQTT -messages based on bridging rules. - -At present, the bridging methods supported by EMQ X are as follows: - -- RPC bridge: RPC Bridge only supports message forwarding and does not - support subscribing to the topic of remote nodes to synchronize - data; -- MQTT Bridge: MQTT Bridge supports both forwarding and data - synchronization through subscription topic. - -These concepts are shown below: - -![bridge](docs/images/bridge.png) - -In addition, the EMQ X message broker supports multi-node bridge mode interconnection - -``` - --------- --------- --------- -Publisher --> | Node1 | --Bridge Forward--> | Node2 | --Bridge Forward--> | Node3 | --> Subscriber - --------- --------- --------- -``` - -In EMQ X, bridge is configured by modifying `etc/emqx.conf`. EMQ X distinguishes between different bridges based on different names. E.g - -``` -## Bridge address: node name for local bridge, host:port for remote. -bridge.mqtt.aws.address = 127.0.0.1:1883 -``` - -This configuration declares a bridge named `aws` and specifies that it is bridged to the MQTT broker of 127.0.0.1:1883 by MQTT mode. - -In case of creating multiple bridges, it is convenient to replicate all configuration items of the first bridge, and modify the bridge name and other configuration items if necessary (such as bridge.$name.address, where $name refers to the name of bridge) - -The next two sections describe how to create a bridge in RPC and MQTT mode respectively and create a forwarding rule that forwards the messages from sensors. Assuming that two EMQ X nodes are running on two hosts: - - -| Name | Node | MQTT Port | -|------|-------------------|-----------| -| emqx1| emqx1@192.168.1.1.| 1883 | -| emqx2| emqx2@192.168.1.2 | 1883 | - - -## EMQ X RPC Bridge Configuration - -The following is the basic configuration of RPC bridging. A simplest RPC bridging only requires the following three items - -``` -## Bridge Address: Use node name (nodename@host) for rpc bridging, and host:port for mqtt connection -bridge.mqtt.emqx2.address = "emqx2@192.168.1.2" - -## Forwarding topics of the message -bridge.mqtt.emqx2.forwards = "sensor1/#,sensor2/#" - -## bridged mountpoint -bridge.mqtt.emqx2.mountpoint = "bridge/emqx2/${node}/" -``` - -If the messages received by the local node emqx1 matches the topic `sersor1/#` or `sensor2/#`, these messages will be forwarded to the `sensor1/#` or `sensor2/#` topic of the remote node emqx2. - -`forwards` is used to specify topics. Messages of the in `forwards` specified topics on local node are forwarded to the remote node. - -`mountpoint` is used to add a topic prefix when forwarding a message. To use `mountpoint`, the `forwards` directive must be set. In the above example, a message with the topic `sensor1/hello` received by the local node will be forwarded to the remote node with the topic `bridge/emqx2/emqx1@192.168.1.1/sensor1/hello`. - -Limitations of RPC bridging: - -1. The RPC bridge of emqx can only forward local messages to the remote node, and cannot synchronize the messages of the remote node to the local node; - -2. RPC bridge can only bridge two EMQ X broker together and cannot bridge EMQ X broker to other MQTT brokers. - -## EMQ X MQTT Bridge Configuration - -EMQ X 3.0 officially introduced MQTT bridge, so that EMQ X can bridge any MQTT broker. Because of the characteristics of the MQTT protocol, EMQ X can subscribe to the remote mqtt broker's topic through MQTT bridge, and then synchronize the remote MQTT broker's message to the local. - -EMQ X MQTT bridging principle: Create an MQTT client on the EMQ X broker, and connect this MQTT client to the remote MQTT broker. Therefore, in the MQTT bridge configuration, following fields may be set for the EMQ X to connect to the remote broker as an mqtt client - -``` -## Bridge Address: Use node name for rpc bridging, use host:port for mqtt connection -bridge.mqtt.emqx2.address = "192.168.1.2:1883" - -## Bridged Protocol Version -## Enumeration value: mqttv3 | mqttv4 | mqttv5 -bridge.mqtt.emqx2.proto_ver = "mqttv4" - -## mqtt client's clientid -bridge.mqtt.emqx2.clientid = "bridge_emq" - -## mqtt client's clean_start field -## Note: Some MQTT Brokers need to set the clean_start value as `true` -bridge.mqtt.emqx2.clean_start = true - -## mqtt client's username field -bridge.mqtt.emqx2.username = "user" - -## mqtt client's password field -bridge.mqtt.emqx2.password = "passwd" - -## Whether the mqtt client uses ssl to connect to a remote serve or not -bridge.mqtt.emqx2.ssl = off - -## CA Certificate of Client SSL Connection (PEM format) -bridge.mqtt.emqx2.cacertfile = "etc/certs/cacert.pem" - -## SSL certificate of Client SSL connection -bridge.mqtt.emqx2.certfile = "etc/certs/client-cert.pem" - -## Key file of Client SSL connection -bridge.mqtt.emqx2.keyfile = "etc/certs/client-key.pem" - -## SSL encryption -bridge.mqtt.emqx2.ciphers = "ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384" - -## TTLS PSK password -## Note 'listener.ssl.external.ciphers' and 'listener.ssl.external.psk_ciphers' cannot be configured at the same time -## -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -## bridge.mqtt.emqx2.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" - -## Client's heartbeat interval -bridge.mqtt.emqx2.keepalive = 60s - -## Supported TLS version -bridge.mqtt.emqx2.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" - -## Forwarding topics of the message -bridge.mqtt.emqx2.forwards = "sensor1/#,sensor2/#" - -## Bridged mountpoint -bridge.mqtt.emqx2.mountpoint = "bridge/emqx2/${node}/" - -## Subscription topic for bridging -bridge.mqtt.emqx2.subscription.1.topic = "cmd/topic1" - -## Subscription qos for bridging -bridge.mqtt.emqx2.subscription.1.qos = 1 - -## Subscription topic for bridging -bridge.mqtt.emqx2.subscription.2.topic = "cmd/topic2" - -## Subscription qos for bridging -bridge.mqtt.emqx2.subscription.2.qos = 1 - -## Bridging reconnection interval -## Default: 30s -bridge.mqtt.emqx2.reconnect_interval = 30s - -## QoS1 message retransmission interval -bridge.mqtt.emqx2.retry_interval = 20s - -## Inflight Size. -bridge.mqtt.emqx2.max_inflight_batches = 32 -``` - -## Bridge Cache Configuration - -The bridge of EMQ X has a message caching mechanism. The caching mechanism is applicable to both RPC bridging and MQTT bridging. When the bridge is disconnected (such as when the network connection is unstable), the messages with a topic specified in `forwards` can be cached to the local message queue. Until the bridge is restored, these messages are re-forwarded to the remote node. The configuration of the cache queue is as follows - -``` -## emqx_bridge internal number of messages used for batch -bridge.mqtt.emqx2.queue.batch_count_limit = 32 - -## emqx_bridge internal number of message bytes used for batch -bridge.mqtt.emqx2.queue.batch_bytes_limit = 1000MB - -## The path for placing replayq queue. If it is not specified, then replayq will run in `mem-only` mode and messages will not be cached on disk. -bridge.mqtt.emqx2.queue.replayq_dir = data/emqx_emqx2_bridge/ - -## Replayq data segment size -bridge.mqtt.emqx2.queue.replayq_seg_bytes = 10MB -``` - -`bridge.mqtt.emqx2.queue.replayq_dir` is a configuration parameter for specifying the path of the bridge storage queue. - -`bridge.mqtt.emqx2.queue.replayq_seg_bytes` is used to specify the size of the largest single file of the message queue that is cached on disk. If the message queue size exceeds the specified value, a new file is created to store the message queue. - -## CLI for EMQ X Bridge MQTT - -CLI for EMQ X Bridge MQTT: - -``` bash -$ cd emqx1/ && ./bin/emqx_ctl bridges -bridges list # List bridges -bridges start # Start a bridge -bridges stop # Stop a bridge -bridges forwards # Show a bridge forward topic -bridges add-forward # Add bridge forward topic -bridges del-forward # Delete bridge forward topic -bridges subscriptions # Show a bridge subscriptions topic -bridges add-subscription # Add bridge subscriptions topic -``` - -List all bridge states - -``` bash -$ ./bin/emqx_ctl bridges list -name: emqx status: Stopped $ ./bin/emqx_ctl bridges list -name: emqx status: Stopped -``` - -Start the specified bridge - -``` bash -$ ./bin/emqx_ctl bridges start emqx -Start bridge successfully. -``` - -Stop the specified bridge - -``` bash -$ ./bin/emqx_ctl bridges stop emqx -Stop bridge successfully. -``` -List the forwarding topics for the specified bridge - -``` bash -$ ./bin/emqx_ctl bridges forwards emqx -topic: topic1/# -topic: topic2/# -``` - -Add a forwarding topic for the specified bridge - -``` bash -$ ./bin/emqx_ctl bridges add-forwards emqx topic3/# -Add-forward topic successfully. -``` - -Delete the forwarding topic for the specified bridge - - -``` bash -$ ./bin/emqx_ctl bridges del-forwards emqx topic3/# -Del-forward topic successfully. -``` - -List subscriptions for the specified bridge - -``` bash -$ ./bin/emqx_ctl bridges subscriptions emqx -topic: cmd/topic1, qos: 1 -topic: cmd/topic2, qos: 1 -``` - -Add a subscription topic for the specified bridge - -``` bash -$ ./bin/emqx_ctl bridges add-subscription emqx cmd/topic3 1 -Add-subscription topic successfully. -``` - -Delete the subscription topic for the specified bridge - -``` bash -$ ./bin/emqx_ctl bridges del-subscription emqx cmd/topic3 -Del-subscription topic successfully. -``` - -Note: In case of creating multiple bridges, it is convenient to replicate all configuration items of the first bridge, and modify the bridge name and other configuration items if necessary. - diff --git a/apps/emqx_bridge_mqtt/docs/guide.rst b/apps/emqx_bridge_mqtt/docs/guide.rst deleted file mode 100644 index a1c2a9126..000000000 --- a/apps/emqx_bridge_mqtt/docs/guide.rst +++ /dev/null @@ -1,286 +0,0 @@ - -EMQ Bridge MQTT -=============== - -The concept of **Bridge** means that EMQ X supports forwarding messages -of one of its own topics to another MQTT Broker in some way. - -**Bridge** differs from **Cluster** in that the bridge does not -replicate the topic trie and routing tables and only forwards MQTT -messages based on bridging rules. - -At present, the bridging methods supported by EMQ X are as follows: - - -* RPC bridge: RPC Bridge only supports message forwarding and does not - support subscribing to the topic of remote nodes to synchronize - data; -* MQTT Bridge: MQTT Bridge supports both forwarding and data - synchronization through subscription topic. - -These concepts are shown below: - - -.. image:: images/bridge.png - :target: images/bridge.png - :alt: bridge - - -In addition, the EMQ X message broker supports multi-node bridge mode interconnection - -.. code-block:: - - --------- --------- --------- - Publisher --> | Node1 | --Bridge Forward--> | Node2 | --Bridge Forward--> | Node3 | --> Subscriber - --------- --------- --------- - -In EMQ X, bridge is configured by modifying ``etc/emqx.conf``. EMQ X distinguishes between different bridges based on different names. E.g - -.. code-block:: - - ## Bridge address: node name for local bridge, host:port for remote. - bridge.mqtt.aws.address = "127.0.0.1:1883" - -This configuration declares a bridge named ``aws`` and specifies that it is bridged to the MQTT broker of 127.0.0.1:1883 by MQTT mode. - -In case of creating multiple bridges, it is convenient to replicate all configuration items of the first bridge, and modify the bridge name and other configuration items if necessary (such as bridge.$name.address, where $name refers to the name of bridge) - -The next two sections describe how to create a bridge in RPC and MQTT mode respectively and create a forwarding rule that forwards the messages from sensors. Assuming that two EMQ X nodes are running on two hosts: - -.. list-table:: - :header-rows: 1 - - * - Name - - Node - - MQTT Port - * - emqx1 - - emqx1@192.168.1.1. - - 1883 - * - emqx2 - - emqx2@192.168.1.2 - - 1883 - - -EMQ X RPC Bridge Configuration ------------------------------- - -The following is the basic configuration of RPC bridging. A simplest RPC bridging only requires the following three items - -.. code-block:: - - ## Bridge Address: Use node name (nodename@host) for rpc bridging, and host:port for mqtt connection - bridge.mqtt.emqx2.address = "emqx2@192.168.1.2" - - ## Forwarding topics of the message - bridge.mqtt.emqx2.forwards = "sensor1/#,sensor2/#" - - ## bridged mountpoint - bridge.mqtt.emqx2.mountpoint = "bridge/emqx2/${node}/" - -If the messages received by the local node emqx1 matches the topic ``sersor1/#`` or ``sensor2/#``\ , these messages will be forwarded to the ``sensor1/#`` or ``sensor2/#`` topic of the remote node emqx2. - -``forwards`` is used to specify topics. Messages of the in ``forwards`` specified topics on local node are forwarded to the remote node. - -``mountpoint`` is used to add a topic prefix when forwarding a message. To use ``mountpoint``\ , the ``forwards`` directive must be set. In the above example, a message with the topic ``sensor1/hello`` received by the local node will be forwarded to the remote node with the topic ``bridge/emqx2/emqx1@192.168.1.1/sensor1/hello``. - -Limitations of RPC bridging: - - -#. - The RPC bridge of emqx can only forward local messages to the remote node, and cannot synchronize the messages of the remote node to the local node; - -#. - RPC bridge can only bridge two EMQ X broker together and cannot bridge EMQ X broker to other MQTT brokers. - -EMQ X MQTT Bridge Configuration -------------------------------- - -EMQ X 3.0 officially introduced MQTT bridge, so that EMQ X can bridge any MQTT broker. Because of the characteristics of the MQTT protocol, EMQ X can subscribe to the remote mqtt broker's topic through MQTT bridge, and then synchronize the remote MQTT broker's message to the local. - -EMQ X MQTT bridging principle: Create an MQTT client on the EMQ X broker, and connect this MQTT client to the remote MQTT broker. Therefore, in the MQTT bridge configuration, following fields may be set for the EMQ X to connect to the remote broker as an mqtt client - -.. code-block:: - - ## Bridge Address: Use node name for rpc bridging, use host:port for mqtt connection - bridge.mqtt.emqx2.address = "192.168.1.2:1883" - - ## Bridged Protocol Version - ## Enumeration value: mqttv3 | mqttv4 | mqttv5 - bridge.mqtt.emqx2.proto_ver = "mqttv4" - - ## mqtt client's clientid - bridge.mqtt.emqx2.clientid = "bridge_emq" - - ## mqtt client's clean_start field - ## Note: Some MQTT Brokers need to set the clean_start value as `true` - bridge.mqtt.emqx2.clean_start = true - - ## mqtt client's username field - bridge.mqtt.emqx2.username = "user" - - ## mqtt client's password field - bridge.mqtt.emqx2.password = "passwd" - - ## Whether the mqtt client uses ssl to connect to a remote serve or not - bridge.mqtt.emqx2.ssl = off - - ## CA Certificate of Client SSL Connection (PEM format) - bridge.mqtt.emqx2.cacertfile = "etc/certs/cacert.pem" - - ## SSL certificate of Client SSL connection - bridge.mqtt.emqx2.certfile = "etc/certs/client-cert.pem" - - ## Key file of Client SSL connection - bridge.mqtt.emqx2.keyfile = "etc/certs/client-key.pem" - - ## TTLS PSK password - ## Note 'listener.ssl.external.ciphers' and 'listener.ssl.external.psk_ciphers' cannot be configured at the same time - ## - ## See 'https://tools.ietf.org/html/rfc4279#section-2'. - ## bridge.mqtt.emqx2.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" - - ## Client's heartbeat interval - bridge.mqtt.emqx2.keepalive = 60s - - ## Supported TLS version - bridge.mqtt.emqx2.tls_versions = "tlsv1.2" - - ## SSL encryption - bridge.mqtt.emqx2.ciphers = "ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384" - - ## Forwarding topics of the message - bridge.mqtt.emqx2.forwards = "sensor1/#,sensor2/#" - - ## Bridged mountpoint - bridge.mqtt.emqx2.mountpoint = "bridge/emqx2/${node}/" - - ## Subscription topic for bridging - bridge.mqtt.emqx2.subscription.1.topic = "cmd/topic1" - - ## Subscription qos for bridging - bridge.mqtt.emqx2.subscription.1.qos = 1 - - ## Subscription topic for bridging - bridge.mqtt.emqx2.subscription.2.topic = "cmd/topic2" - - ## Subscription qos for bridging - bridge.mqtt.emqx2.subscription.2.qos = 1 - - ## Bridging reconnection interval - ## Default: 30s - bridge.mqtt.emqx2.reconnect_interval = 30s - - ## QoS1 message retransmission interval - bridge.mqtt.emqx2.retry_interval = 20s - - ## Inflight Size. - bridge.mqtt.emqx2.max_inflight_batches = 32 - -Bridge Cache Configuration --------------------------- - -The bridge of EMQ X has a message caching mechanism. The caching mechanism is applicable to both RPC bridging and MQTT bridging. When the bridge is disconnected (such as when the network connection is unstable), the messages with a topic specified in ``forwards`` can be cached to the local message queue. Until the bridge is restored, these messages are re-forwarded to the remote node. The configuration of the cache queue is as follows - -.. code-block:: - - ## emqx_bridge internal number of messages used for batch - bridge.mqtt.emqx2.queue.batch_count_limit = 32 - - ## emqx_bridge internal number of message bytes used for batch - bridge.mqtt.emqx2.queue.batch_bytes_limit = 1000MB - - ## The path for placing replayq queue. If it is not specified, then replayq will run in `mem-only` mode and messages will not be cached on disk. - bridge.mqtt.emqx2.queue.replayq_dir = "data/emqx_emqx2_bridge/" - - ## Replayq data segment size - bridge.mqtt.emqx2.queue.replayq_seg_bytes = 10MB - -``bridge.mqtt.emqx2.queue.replayq_dir`` is a configuration parameter for specifying the path of the bridge storage queue. - -``bridge.mqtt.emqx2.queue.replayq_seg_bytes`` is used to specify the size of the largest single file of the message queue that is cached on disk. If the message queue size exceeds the specified value, a new file is created to store the message queue. - -CLI for EMQ X Bridge MQTT -------------------------- - -CLI for EMQ X Bridge MQTT: - -.. code-block:: bash - - $ cd emqx1/ && ./bin/emqx_ctl bridges - bridges list # List bridges - bridges start # Start a bridge - bridges stop # Stop a bridge - bridges forwards # Show a bridge forward topic - bridges add-forward # Add bridge forward topic - bridges del-forward # Delete bridge forward topic - bridges subscriptions # Show a bridge subscriptions topic - bridges add-subscription # Add bridge subscriptions topic - -List all bridge states - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges list - name: emqx status: Stopped $ ./bin/emqx_ctl bridges list - name: emqx status: Stopped - -Start the specified bridge - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges start emqx - Start bridge successfully. - -Stop the specified bridge - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges stop emqx - Stop bridge successfully. - -List the forwarding topics for the specified bridge - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges forwards emqx - topic: topic1/# - topic: topic2/# - -Add a forwarding topic for the specified bridge - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges add-forwards emqx topic3/# - Add-forward topic successfully. - -Delete the forwarding topic for the specified bridge - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges del-forwards emqx topic3/# - Del-forward topic successfully. - -List subscriptions for the specified bridge - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges subscriptions emqx - topic: cmd/topic1, qos: 1 - topic: cmd/topic2, qos: 1 - -Add a subscription topic for the specified bridge - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges add-subscription emqx cmd/topic3 1 - Add-subscription topic successfully. - -Delete the subscription topic for the specified bridge - -.. code-block:: bash - - $ ./bin/emqx_ctl bridges del-subscription emqx cmd/topic3 - Del-subscription topic successfully. - -Note: In case of creating multiple bridges, it is convenient to replicate all configuration items of the first bridge, and modify the bridge name and other configuration items if necessary. - diff --git a/apps/emqx_bridge_mqtt/docs/images/bridge.png b/apps/emqx_bridge_mqtt/docs/images/bridge.png deleted file mode 100644 index 9bb9c024c..000000000 Binary files a/apps/emqx_bridge_mqtt/docs/images/bridge.png and /dev/null differ diff --git a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf deleted file mode 100644 index c34567ee4..000000000 --- a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf +++ /dev/null @@ -1,58 +0,0 @@ -##==================================================================== -## Configuration for EMQ X MQTT Broker Bridge -##==================================================================== - -emqx_bridge_mqtt:{ - bridges:[ - # { - # name: "mqtt1" - # start_type: auto - # forwards: ["test/#"], - # forward_mountpoint: "" - # reconnect_interval: "30s" - # batch_size: 100 - # queue:{ - # replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" - # replayq_seg_bytes: "100MB" - # replayq_offload_mode: false - # replayq_max_total_bytes: "1GB" - # }, - # config:{ - # conn_type: mqtt - # address: "127.0.0.1:1883" - # proto_ver: v4 - # bridge_mode: true - # clientid: "client1" - # clean_start: true - # username: "username1" - # password: "" - # keepalive: 300 - # subscriptions: [{ - # topic: "t/#" - # qos: 1 - # }] - # receive_mountpoint: "" - # retry_interval: "30s" - # max_inflight: 32 - # } - # }, - # { - # name: "rpc1" - # start_type: auto - # forwards: ["test/#"], - # forward_mountpoint: "" - # reconnect_interval: "30s" - # batch_size: 100 - # queue:{ - # replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" - # replayq_seg_bytes: "100MB" - # replayq_offload_mode: false - # replayq_max_total_bytes: "1GB" - # }, - # config:{ - # conn_type: rpc - # node: "emqx@127.0.0.1" - # } - # } - ] -} diff --git a/apps/emqx_bridge_mqtt/rebar.config b/apps/emqx_bridge_mqtt/rebar.config deleted file mode 100644 index 37ac5b034..000000000 --- a/apps/emqx_bridge_mqtt/rebar.config +++ /dev/null @@ -1,19 +0,0 @@ -{deps, []}. -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - -{shell, [ - % {config, "config/sys.config"}, - {apps, [emqx, emqx_bridge_mqtt]} -]}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src deleted file mode 100644 index afac20404..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_bridge_mqtt, - [{description, "EMQ X Bridge to MQTT Broker"}, - {vsn, "5.0.0"}, % strict semver, bump manually! - {modules, []}, - {registered, []}, - {applications, [kernel,stdlib,replayq,emqtt,emqx]}, - {mod, {emqx_bridge_mqtt_app, []}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-bridge-mqtt"} - ]} - ]}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.erl deleted file mode 100644 index a76ea3a8c..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.erl +++ /dev/null @@ -1,92 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_mqtt_cli). - --include("emqx_bridge_mqtt.hrl"). - --import(lists, [foreach/2]). - --export([cli/1]). - -cli(["list"]) -> - foreach(fun({Name, State0}) -> - State = case State0 of - connected -> <<"Running">>; - _ -> <<"Stopped">> - end, - emqx_ctl:print("name: ~s status: ~s~n", [Name, State]) - end, emqx_bridge_mqtt_sup:bridges()); - -cli(["start", Name]) -> - emqx_ctl:print("~s.~n", [try emqx_bridge_worker:ensure_started(Name) of - ok -> <<"Start bridge successfully">>; - connected -> <<"Bridge already started">>; - _ -> <<"Start bridge failed">> - catch - _Error:_Reason -> - <<"Start bridge failed">> - end]); - -cli(["stop", Name]) -> - emqx_ctl:print("~s.~n", [try emqx_bridge_worker:ensure_stopped(Name) of - ok -> <<"Stop bridge successfully">>; - _ -> <<"Stop bridge failed">> - catch - _Error:_Reason -> - <<"Stop bridge failed">> - end]); - -cli(["forwards", Name]) -> - foreach(fun(Topic) -> - emqx_ctl:print("topic: ~s~n", [Topic]) - end, emqx_bridge_worker:get_forwards(Name)); - -cli(["add-forward", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_forward_present(Name, iolist_to_binary(Topic)), - emqx_ctl:print("Add-forward topic successfully.~n"); - -cli(["del-forward", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_forward_absent(Name, iolist_to_binary(Topic)), - emqx_ctl:print("Del-forward topic successfully.~n"); - -cli(["subscriptions", Name]) -> - foreach(fun({Topic, Qos}) -> - emqx_ctl:print("topic: ~s, qos: ~p~n", [Topic, Qos]) - end, emqx_bridge_worker:get_subscriptions(Name)); - -cli(["add-subscription", Name, Topic, Qos]) -> - case emqx_bridge_worker:ensure_subscription_present(Name, Topic, list_to_integer(Qos)) of - ok -> emqx_ctl:print("Add-subscription topic successfully.~n"); - {error, Reason} -> emqx_ctl:print("Add-subscription failed reason: ~p.~n", [Reason]) - end; - -cli(["del-subscription", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_subscription_absent(Name, Topic), - emqx_ctl:print("Del-subscription topic successfully.~n"); - -cli(_) -> - emqx_ctl:usage([{"bridges list", "List bridges"}, - {"bridges start ", "Start a bridge"}, - {"bridges stop ", "Stop a bridge"}, - {"bridges forwards ", "Show a bridge forward topic"}, - {"bridges add-forward ", "Add bridge forward topic"}, - {"bridges del-forward ", "Delete bridge forward topic"}, - {"bridges subscriptions ", "Show a bridge subscriptions topic"}, - {"bridges add-subscription ", "Add bridge subscriptions topic"}, - {"bridges del-subscription ", "Delete bridge subscriptions topic"}]). - - diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl deleted file mode 100644 index 8cc87ef64..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl +++ /dev/null @@ -1,89 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_mqtt_schema). - --include_lib("typerefl/include/types.hrl"). - --behaviour(hocon_schema). - --export([ structs/0 - , fields/1]). - -structs() -> ["emqx_bridge_mqtt"]. - -fields("emqx_bridge_mqtt") -> - [ {bridges, hoconsc:array(hoconsc:ref(?MODULE, "bridges"))} - ]; - -fields("bridges") -> - [ {name, emqx_schema:t(string(), undefined, true)} - , {start_type, fun start_type/1} - , {forwards, fun forwards/1} - , {forward_mountpoint, emqx_schema:t(string())} - , {reconnect_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} - , {batch_size, emqx_schema:t(integer(), undefined, 100)} - , {queue, emqx_schema:t(hoconsc:ref(?MODULE, "queue"))} - , {config, hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), hoconsc:ref(?MODULE, "rpc")])} - ]; - -fields("mqtt") -> - [ {conn_type, fun conn_type/1} - , {address, emqx_schema:t(string(), undefined, "127.0.0.1:1883")} - , {proto_ver, fun proto_ver/1} - , {bridge_mode, emqx_schema:t(boolean(), undefined, true)} - , {clientid, emqx_schema:t(string())} - , {username, emqx_schema:t(string())} - , {password, emqx_schema:t(string())} - , {clean_start, emqx_schema:t(boolean(), undefined, true)} - , {keepalive, emqx_schema:t(integer(), undefined, 300)} - , {subscriptions, hoconsc:array("subscriptions")} - , {receive_mountpoint, emqx_schema:t(string())} - , {retry_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} - , {max_inflight, emqx_schema:t(integer(), undefined, 32)} - ]; - -fields("rpc") -> - [ {conn_type, fun conn_type/1} - , {node, emqx_schema:t(atom(), undefined, 'emqx@127.0.0.1')} - ]; - -fields("subscriptions") -> - [ {topic, #{type => binary(), nullable => false}} - , {qos, emqx_schema:t(integer(), undefined, 1)} - ]; - -fields("queue") -> - [ {replayq_dir, hoconsc:union([boolean(), string()])} - , {replayq_seg_bytes, emqx_schema:t(emqx_schema:bytesize(), undefined, "100MB")} - , {replayq_offload_mode, emqx_schema:t(boolean(), undefined, false)} - , {replayq_max_total_bytes, emqx_schema:t(emqx_schema:bytesize(), undefined, "1024MB")} - ]. - -conn_type(type) -> hoconsc:enum([mqtt, rpc]); -conn_type(_) -> undefined. - -proto_ver(type) -> hoconsc:enum([v3, v4, v5]); -proto_ver(default) -> v4; -proto_ver(_) -> undefined. - -start_type(type) -> hoconsc:enum([auto, manual]); -start_type(default) -> auto; -start_type(_) -> undefined. - -forwards(type) -> hoconsc:array(binary()); -forwards(default) -> []; -forwards(_) -> undefined. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl deleted file mode 100644 index ef4d076a4..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl +++ /dev/null @@ -1,72 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_mqtt_sup). --behaviour(supervisor). - --include("emqx_bridge_mqtt.hrl"). --include_lib("emqx/include/logger.hrl"). - - -%% APIs --export([ start_link/0 - ]). - --export([ create_bridge/1 - , drop_bridge/1 - , bridges/0 - ]). - -%% supervisor callbacks --export([init/1]). - --define(WORKER_SUP, emqx_bridge_worker_sup). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - BridgesConf = emqx_config:get([?APP, bridges], []), - BridgeSpec = lists:map(fun bridge_spec/1, BridgesConf), - SupFlag = #{strategy => one_for_one, - intensity => 100, - period => 10}, - {ok, {SupFlag, BridgeSpec}}. - -bridge_spec(Config) -> - Name = list_to_atom(maps:get(name, Config)), - #{id => Name, - start => {emqx_bridge_worker, start_link, [Config]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_bridge_worker]}. - --spec(bridges() -> [{node(), map()}]). -bridges() -> - [{Name, emqx_bridge_worker:status(Name)} || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. - -create_bridge(Config) -> - supervisor:start_child(?MODULE, bridge_spec(Config)). - -drop_bridge(Name) -> - case supervisor:terminate_child(?MODULE, Name) of - ok -> - supervisor:delete_child(?MODULE, Name); - {error, Error} -> - ?LOG(error, "Delete bridge failed, error : ~p", [Error]), - {error, Error} - end. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl deleted file mode 100644 index 91fd18bf4..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl +++ /dev/null @@ -1,99 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_msg). - --export([ to_binary/1 - , from_binary/1 - , to_export/3 - , to_broker_msgs/1 - , to_broker_msg/1 - , to_broker_msg/2 - , estimate_size/1 - ]). - --export_type([msg/0]). - --include_lib("emqx/include/emqx.hrl"). - --include_lib("emqx_bridge_mqtt/include/emqx_bridge_mqtt.hrl"). --include_lib("emqtt/include/emqtt.hrl"). - - --type msg() :: emqx_types:message(). --type exp_msg() :: emqx_types:message() | #mqtt_msg{}. - -%% @doc Make export format: -%% 1. Mount topic to a prefix -%% 2. Fix QoS to 1 -%% @end -%% Shame that we have to know the callback module here -%% would be great if we can get rid of #mqtt_msg{} record -%% and use #message{} in all places. --spec to_export(emqx_bridge_rpc | emqx_bridge_worker, - undefined | binary(), msg()) -> exp_msg(). -to_export(emqx_bridge_mqtt, Mountpoint, - #message{topic = Topic, - payload = Payload, - flags = Flags, - qos = QoS - }) -> - Retain = maps:get(retain, Flags, false), - #mqtt_msg{qos = QoS, - retain = Retain, - topic = topic(Mountpoint, Topic), - props = #{}, - payload = Payload}; -to_export(_Module, Mountpoint, - #message{topic = Topic} = Msg) -> - Msg#message{topic = topic(Mountpoint, Topic)}. - -%% @doc Make `binary()' in order to make iodata to be persisted on disk. --spec to_binary(msg()) -> binary(). -to_binary(Msg) -> term_to_binary(Msg). - -%% @doc Unmarshal binary into `msg()'. --spec from_binary(binary()) -> msg(). -from_binary(Bin) -> binary_to_term(Bin). - -%% @doc Estimate the size of a message. -%% Count only the topic length + payload size --spec estimate_size(msg()) -> integer(). -estimate_size(#message{topic = Topic, payload = Payload}) -> - size(Topic) + size(Payload). - -%% @doc By message/batch receiver, transform received batch into -%% messages to deliver to local brokers. -to_broker_msgs(Batch) -> lists:map(fun to_broker_msg/1, Batch). - -to_broker_msg(#message{} = Msg) -> - %% internal format from another EMQX node via rpc - Msg; -to_broker_msg(Msg) -> - to_broker_msg(Msg, undefined). -to_broker_msg(#{qos := QoS, dup := Dup, retain := Retain, topic := Topic, - properties := Props, payload := Payload}, Mountpoint) -> - %% published from remote node over a MQTT connection - set_headers(Props, - emqx_message:set_flags(#{dup => Dup, retain => Retain}, - emqx_message:make(bridge, QoS, topic(Mountpoint, Topic), Payload))). - -set_headers(undefined, Msg) -> - Msg; -set_headers(Val, Msg) -> - emqx_message:set_headers(Val, Msg). -topic(undefined, Topic) -> Topic; -topic(Prefix, Topic) -> emqx_topic:prepend(Prefix, Topic). diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl deleted file mode 100644 index 33511cc03..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl +++ /dev/null @@ -1,95 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - -%% @doc This module implements EMQX Bridge transport layer based on gen_rpc. - --module(emqx_bridge_rpc). - --export([ start/1 - , send/2 - , stop/1 - ]). - -%% Internal exports --export([ handle_send/1 - , heartbeat/2 - ]). - --type ack_ref() :: emqx_bridge_worker:ack_ref(). --type batch() :: emqx_bridge_worker:batch(). --define(HEARTBEAT_INTERVAL, timer:seconds(1)). - --define(RPC, emqx_rpc). - -start(#{node := RemoteNode}) -> - case poke(RemoteNode) of - ok -> - Pid = proc_lib:spawn_link(?MODULE, heartbeat, [self(), RemoteNode]), - {ok, #{client_pid => Pid, remote_node => RemoteNode}}; - Error -> - Error - end. - -stop(#{client_pid := Pid}) when is_pid(Pid) -> - Ref = erlang:monitor(process, Pid), - unlink(Pid), - Pid ! stop, - receive - {'DOWN', Ref, process, Pid, _Reason} -> - ok - after - 1000 -> - exit(Pid, kill) - end, - ok. - -%% @doc Callback for `emqx_bridge_connect' behaviour --spec send(#{remote_node := atom(), _ => _}, batch()) -> {ok, ack_ref()} | {error, any()}. -send(#{remote_node := RemoteNode}, Batch) -> - case ?RPC:call(RemoteNode, ?MODULE, handle_send, [Batch]) of - ok -> - Ref = make_ref(), - self() ! {batch_ack, Ref}, - {ok, Ref}; - {badrpc, Reason} -> {error, Reason} - end. - -%% @doc Handle send on receiver side. --spec handle_send(batch()) -> ok. -handle_send(Batch) -> - lists:foreach(fun(Msg) -> emqx_broker:publish(Msg) end, Batch). - -%% @hidden Heartbeat loop -heartbeat(Parent, RemoteNode) -> - Interval = ?HEARTBEAT_INTERVAL, - receive - stop -> exit(normal) - after - Interval -> - case poke(RemoteNode) of - ok -> - ?MODULE:heartbeat(Parent, RemoteNode); - {error, Reason} -> - Parent ! {disconnected, self(), Reason}, - exit(normal) - end - end. - -poke(RemoteNode) -> - case ?RPC:call(RemoteNode, erlang, node, []) of - RemoteNode -> ok; - {badrpc, Reason} -> {error, Reason} - end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl deleted file mode 100644 index cbd80ba3d..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl +++ /dev/null @@ -1,42 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_rpc_tests). --include_lib("eunit/include/eunit.hrl"). - -send_and_ack_test() -> - %% delegate from emqx_rpc to rpc for unit test - meck:new(emqx_rpc, [passthrough, no_history]), - meck:expect(emqx_rpc, call, 4, - fun(Node, Module, Fun, Args) -> - rpc:call(Node, Module, Fun, Args) - end), - meck:expect(emqx_rpc, cast, 4, - fun(Node, Module, Fun, Args) -> - rpc:cast(Node, Module, Fun, Args) - end), - meck:new(emqx_bridge_worker, [passthrough, no_history]), - try - {ok, #{client_pid := Pid, remote_node := Node}} = emqx_bridge_rpc:start(#{node => node()}), - {ok, Ref} = emqx_bridge_rpc:send(#{remote_node => Node}, []), - receive - {batch_ack, Ref} -> - ok - end, - ok = emqx_bridge_rpc:stop( #{client_pid => Pid}) - after - meck:unload(emqx_rpc) - end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl deleted file mode 100644 index f3f5d5ceb..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl +++ /dev/null @@ -1,372 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_worker_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("snabbkaffe/include/snabbkaffe.hrl"). - --define(wait(For, Timeout), emqx_ct_helpers:wait_for(?FUNCTION_NAME, ?LINE, fun() -> For end, Timeout)). - --define(SNK_WAIT(WHAT), ?assertMatch({ok, _}, ?block_until(#{?snk_kind := WHAT}, 2000, 1000))). - -receive_messages(Count) -> - receive_messages(Count, []). - -receive_messages(0, Msgs) -> - Msgs; -receive_messages(Count, Msgs) -> - receive - {publish, Msg} -> - receive_messages(Count-1, [Msg|Msgs]); - _Other -> - receive_messages(Count, Msgs) - after 1000 -> - Msgs - end. - -all() -> - lists:filtermap( - fun({FunName, _Arity}) -> - case atom_to_list(FunName) of - "t_" ++ _ -> {true, FunName}; - _ -> false - end - end, - ?MODULE:module_info(exports)). - -init_per_suite(Config) -> - case node() of - nonode@nohost -> net_kernel:start(['emqx@127.0.0.1', longnames]); - _ -> ok - end, - emqx_ct_helpers:start_apps([emqx_bridge_mqtt]), - emqx_logger:set_log_level(error), - [{log_level, error} | Config]. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_bridge_mqtt]). - -init_per_testcase(_TestCase, Config) -> - ok = snabbkaffe:start_trace(), - Config. - -end_per_testcase(_TestCase, _Config) -> - ok = snabbkaffe:stop(). - -t_rpc_mngr(_Config) -> - Name = "rpc_name", - Cfg = #{ - name => Name, - forwards => [<<"mngr">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - config => #{ - conn_type => rpc, - node => node() - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), - ?assertEqual([<<"mngr2">>, <<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual({error, no_remote_subscription_support}, - emqx_bridge_worker:ensure_subscription_present(Name, <<"t">>, 0)), - ?assertEqual({error, no_remote_subscription_support}, - emqx_bridge_worker:ensure_subscription_absent(Name, <<"t">>)), - ok = emqx_bridge_worker:stop(Pid). - -t_mqtt_mngr(_Config) -> - Name = "mqtt_name", - Cfg = #{ - name => Name, - forwards => [<<"mngr">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - config => #{ - address => "127.0.0.1:1883", - conn_type => mqtt, - clientid => <<"client1">>, - keepalive => 300, - subscriptions => [#{topic => <<"t/#">>, qos => 1}] - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), - ?assertEqual([<<"mngr2">>, <<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_subscription_present(Name, <<"t">>, 0)), - ?assertEqual(ok, emqx_bridge_worker:ensure_subscription_absent(Name, <<"t">>)), - ?assertEqual([{<<"t/#">>,1}], emqx_bridge_worker:get_subscriptions(Name)), - ok = emqx_bridge_worker:stop(Pid). - -%% A loopback RPC to local node -t_rpc(_Config) -> - Name = "rpc", - Cfg = #{ - name => Name, - forwards => [<<"t_rpc/#">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - config => #{ - conn_type => rpc, - node => node() - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), - {ok, _Props} = emqtt:connect(ConnPid), - {ok, _Props, [1]} = emqtt:subscribe(ConnPid, {<<"forwarded/t_rpc/one">>, ?QOS_1}), - timer:sleep(100), - {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_rpc/one">>, <<"hello">>, ?QOS_1), - timer:sleep(100), - ?assertEqual(1, length(receive_messages(1))), - emqtt:disconnect(ConnPid), - emqx_bridge_worker:stop(Pid). - -%% Full data loopback flow explained: -%% mqtt-client ----> local-broker ---(local-subscription)---> -%% bridge(export) --- (mqtt-connection)--> local-broker ---(remote-subscription) --> -%% bridge(import) --> mqtt-client -t_mqtt(_Config) -> - SendToTopic = <<"t_mqtt/one">>, - SendToTopic2 = <<"t_mqtt/two">>, - SendToTopic3 = <<"t_mqtt/three">>, - Mountpoint = <<"forwarded/${node}/">>, - Name = "mqtt", - Cfg = #{ - name => Name, - forwards => [SendToTopic], - forward_mountpoint => Mountpoint, - start_type => auto, - config => #{ - address => "127.0.0.1:1883", - conn_type => mqtt, - clientid => <<"client1">>, - keepalive => 300, - subscriptions => [#{topic => SendToTopic2, qos => 1}], - receive_mountpoint => <<"receive/aws/">> - }, - queue => #{ - replayq_dir => "data/t_mqtt/", - replayq_seg_bytes => 10000, - batch_bytes_limit => 1000, - batch_count_limit => 10 - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ?assertEqual([{SendToTopic2, 1}], emqx_bridge_worker:get_subscriptions(Name)), - ok = emqx_bridge_worker:ensure_subscription_present(Name, SendToTopic3, _QoS = 1), - ?assertEqual([{SendToTopic3, 1},{SendToTopic2, 1}], - emqx_bridge_worker:get_subscriptions(Name)), - {ok, ConnPid} = emqtt:start_link([{clientid, <<"client-1">>}]), - {ok, _Props} = emqtt:connect(ConnPid), - emqtt:subscribe(ConnPid, <<"forwarded/+/t_mqtt/one">>, 1), - %% message from a different client, to avoid getting terminated by no-local - Max = 10, - Msgs = lists:seq(1, Max), - lists:foreach(fun(I) -> - {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic, integer_to_binary(I), ?QOS_1) - end, Msgs), - ?assertEqual(10, length(receive_messages(200))), - - emqtt:subscribe(ConnPid, <<"receive/aws/t_mqtt/two">>, 1), - %% message from a different client, to avoid getting terminated by no-local - Max = 10, - Msgs = lists:seq(1, Max), - lists:foreach(fun(I) -> - {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic2, integer_to_binary(I), ?QOS_1) - end, Msgs), - ?assertEqual(10, length(receive_messages(200))), - - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Pid). - -t_stub_normal(Config) when is_list(Config) -> - Name = "stub_normal", - Cfg = #{ - name => Name, - forwards => [<<"t_stub_normal/#">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - config => #{ - conn_type => emqx_bridge_stub_conn, - client_pid => self() - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - receive - {Pid, emqx_bridge_stub_conn, ready} -> ok - after - 5000 -> - error(timeout) - end, - {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), - {ok, _} = emqtt:connect(ConnPid), - {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_stub_normal/one">>, <<"hello">>, ?QOS_1), - receive - {stub_message, WorkerPid, BatchRef, _Batch} -> - WorkerPid ! {batch_ack, BatchRef}, - ok - after - 5000 -> - error(timeout) - end, - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Pid). - -t_stub_overflow(_Config) -> - Topic = <<"t_stub_overflow/one">>, - MaxInflight = 20, - Name = "stub_overflow", - Cfg = #{ - name => Name, - forwards => [<<"t_stub_overflow/one">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - max_inflight => MaxInflight, - config => #{ - conn_type => emqx_bridge_stub_conn, - client_pid => self() - } - }, - {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight * 2)), - ?SNK_WAIT(inflight_full), - Acks = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks), - Acks2 = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks2), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Worker). - -t_stub_random_order(_Config) -> - Topic = <<"t_stub_random_order/a">>, - MaxInflight = 10, - Name = "stub_random_order", - Cfg = #{ - name => Name, - forwards => [Topic], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - max_inflight => MaxInflight, - config => #{ - conn_type => emqx_bridge_stub_conn, - client_pid => self() - } - }, - {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ClientId = <<"ClientId">>, - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight)), - Acks = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, - lists:reverse(Acks)), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Worker). - -t_stub_retry_inflight(_Config) -> - Topic = <<"to_stub_retry_inflight/a">>, - MaxInflight = 10, - Name = "stub_retry_inflight", - Cfg = #{ - name => Name, - forwards => [Topic], - forward_mountpoint => <<"forwarded">>, - reconnect_interval => 10, - start_type => auto, - max_inflight => MaxInflight, - config => #{ - conn_type => emqx_bridge_stub_conn, - client_pid => self() - } - }, - {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ClientId = <<"ClientId2">>, - case ?block_until(#{?snk_kind := connected, inflight := 0}, 2000, 1000) of - {ok, #{inflight := 0}} -> ok; - Other -> ct:fail("~p", [Other]) - end, - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight)), - %% receive acks but do not ack - Acks1 = stub_receive(MaxInflight), - ?assertEqual(MaxInflight, length(Acks1)), - %% simulate a disconnect - Worker ! {disconnected, self(), test}, - ?SNK_WAIT(disconnected), - case ?block_until(#{?snk_kind := connected, inflight := MaxInflight}, 2000, 20) of - {ok, _} -> ok; - Error -> ct:fail("~p", [Error]) - end, - %% expect worker to retry inflight, so to receive acks again - Acks2 = stub_receive(MaxInflight), - ?assertEqual(MaxInflight, length(Acks2)), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, - lists:reverse(Acks2)), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Worker). - -stub_receive(N) -> - stub_receive(N, []). - -stub_receive(0, Acc) -> lists:reverse(Acc); -stub_receive(N, Acc) -> - receive - {stub_message, WorkerPid, BatchRef, _Batch} -> - stub_receive(N - 1, [{WorkerPid, BatchRef} | Acc]) - after - 5000 -> - lists:reverse(Acc) - end. diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index cbeff37eb..fd2329cbd 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -17,7 +17,8 @@ %% By accident, We have always been using the upstream fork due to %% eredis_cluster's dependency getting resolved earlier. %% Here we pin 1.5.2 to avoid surprises in the future. - {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}} + {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}}, + {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.3"}}} ]}. {shell, [ diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 5e1ca2ca8..f4481dc2c 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -13,7 +13,8 @@ epgsql, mysql, mongodb, - emqx + emqx, + emqtt ]}, {env,[]}, {modules, []}, diff --git a/apps/emqx_connector/src/emqx_connector_app.erl b/apps/emqx_connector/src/emqx_connector_app.erl index 64e6b8109..4de078076 100644 --- a/apps/emqx_connector/src/emqx_connector_app.erl +++ b/apps/emqx_connector/src/emqx_connector_app.erl @@ -21,6 +21,7 @@ -export([start/2, stop/1]). start(_StartType, _StartArgs) -> + emqx_connector_mqtt_worker:register_metrics(), emqx_connector_sup:start_link(). stop(_State) -> diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 11860f32d..159562f33 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -32,7 +32,7 @@ -reflect_type([url/0]). -typerefl_from_string({url/0, emqx_http_lib, uri_parse}). --export([ structs/0 +-export([ roots/0 , fields/1 , validations/0]). @@ -47,10 +47,8 @@ %%===================================================================== %% Hocon schema -structs() -> [""]. - -fields("") -> - [{config, #{type => hoconsc:ref(?MODULE, config)}}]; +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]. fields(config) -> [ {base_url, fun base_url/1} @@ -60,9 +58,7 @@ fields(config) -> , {pool_type, fun pool_type/1} , {pool_size, fun pool_size/1} , {enable_pipelining, fun enable_pipelining/1} - , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), - default => #{}}} - ]; + ] ++ emqx_connector_schema_lib:ssl_fields(); fields(ssl_opts) -> [ {cacertfile, fun cacertfile/1} @@ -202,12 +198,11 @@ check_ssl_opts(Conf) -> check_ssl_opts(URLFrom, Conf) -> #{schema := Scheme} = hocon_schema:get_value(URLFrom, Conf), - SSLOpts = hocon_schema:get_value("ssl_opts", Conf), - case {Scheme, maps:size(SSLOpts)} of - {http, 0} -> true; - {http, _} -> false; - {https, 0} -> false; - {https, _} -> true + SSL= hocon_schema:get_value("ssl", Conf), + case {Scheme, maps:get(enable, SSL, false)} of + {http, false} -> true; + {https, true} -> true; + {_, _} -> false end. update_path(BasePath, {Path, Headers}) -> diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl index 8c0504d53..fadf7f56f 100644 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ b/apps/emqx_connector/src/emqx_connector_ldap.erl @@ -19,7 +19,7 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). %% callbacks of behaviour emqx_resource -export([ on_start/2 @@ -35,11 +35,11 @@ -export([search/4]). %%===================================================================== -structs() -> [""]. +roots() -> + ldap_fields() ++ emqx_connector_schema_lib:ssl_fields(). -fields("") -> - ldap_fields() ++ - emqx_connector_schema_lib:ssl_fields(). +%% this schema has no sub-structs +fields(_) -> []. on_jsonify(Config) -> Config. diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 4af339538..906b57fb3 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -19,8 +19,9 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --type server() :: string(). +-type server() :: emqx_schema:ip_port(). -reflect_type([server/0]). +-typerefl_from_string({server/0, emqx_connector_schema_lib, to_ip_port}). %% callbacks of behaviour emqx_resource -export([ on_start/2 @@ -32,19 +33,18 @@ -export([connect/1]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -export([mongo_query/5]). %%===================================================================== -structs() -> [""]. - -fields("") -> +roots() -> [ {config, #{type => hoconsc:union( [ hoconsc:ref(?MODULE, single) , hoconsc:ref(?MODULE, rs) , hoconsc:ref(?MODULE, sharded) ])}} - ]; + ]. + fields(single) -> [ {mongo_type, #{type => single, default => single}} @@ -62,7 +62,8 @@ fields(sharded) -> , {servers, fun servers/1} ] ++ mongo_fields(); fields(topology) -> - [ {max_overflow, fun emqx_connector_schema_lib:pool_size/1} + [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} + , {max_overflow, fun emqx_connector_schema_lib:pool_size/1} , {overflow_ttl, fun duration/1} , {overflow_check_period, fun duration/1} , {local_threshold_ms, fun duration/1} @@ -81,6 +82,8 @@ mongo_fields() -> , {auth_source, #{type => binary(), nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} + , {topology, #{type => hoconsc:ref(?MODULE, topology), + nullable => true}} ] ++ emqx_connector_schema_lib:ssl_fields(). @@ -92,7 +95,7 @@ on_start(InstId, Config = #{server := Server, mongo_type := single}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), Opts = [{type, single}, - {hosts, [Server]} + {hosts, [emqx_connector_schema_lib:ip_port_to_string(Server)]} ], do_start(InstId, Opts, Config); @@ -101,14 +104,17 @@ on_start(InstId, Config = #{servers := Servers, replica_set_name := RsName}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), Opts = [{type, {rs, RsName}}, - {hosts, Servers}], + {hosts, [emqx_connector_schema_lib:ip_port_to_string(S) + || S <- Servers]} + ], do_start(InstId, Opts, Config); on_start(InstId, Config = #{servers := Servers, mongo_type := sharded}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), Opts = [{type, sharded}, - {hosts, Servers} + {hosts, [emqx_connector_schema_lib:ip_port_to_string(S) + || S <- Servers]} ], do_start(InstId, Opts, Config). @@ -170,9 +176,10 @@ do_start(InstId, Opts0, Config = #{mongo_type := Type, ]; false -> [{ssl, false}] end, + Topology= maps:get(topology, Config, #{}), Opts = Opts0 ++ [{pool_size, PoolSize}, - {options, init_topology_options(maps:to_list(Config), [])}, + {options, init_topology_options(maps:to_list(Topology), [])}, {worker_options, init_worker_options(maps:to_list(Config), SslOpts)}], %% test the connection TestOpts = case maps:is_key(server, Config) of @@ -235,15 +242,8 @@ init_worker_options([_ | R], Acc) -> init_worker_options(R, Acc); init_worker_options([], Acc) -> Acc. -host_port(HostPort) -> - case string:split(HostPort, ":") of - [Host, Port] -> - {ok, Host1} = inet:parse_address(Host), - [{host, Host1}, {port, list_to_integer(Port)}]; - [Host] -> - {ok, Host1} = inet:parse_address(Host), - [{host, Host1}] - end. +host_port({Host, Port}) -> + [{host, Host}, {port, Port}]. server(type) -> server(); server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl new file mode 100644 index 000000000..6631fd23a --- /dev/null +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -0,0 +1,214 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_connector_mqtt). + +-include_lib("typerefl/include/types.hrl"). +-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). + +-behaviour(supervisor). + +%% API and callbacks for supervisor +-export([ start_link/0 + , init/1 + , create_bridge/1 + , drop_bridge/1 + , bridges/0 + ]). + +%% callbacks of behaviour emqx_resource +-export([ on_start/2 + , on_stop/2 + , on_query/4 + , on_health_check/2 + ]). + +-behaviour(hocon_schema). + +-export([ roots/0 + , fields/1]). + +%%===================================================================== +%% Hocon schema +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, "config")}}]. + +fields("config") -> + emqx_connector_mqtt_schema:fields("config"). + +%% =================================================================== +%% supervisor APIs +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + SupFlag = #{strategy => one_for_one, + intensity => 100, + period => 10}, + {ok, {SupFlag, []}}. + +bridge_spec(Config) -> + #{id => maps:get(name, Config), + start => {emqx_connector_mqtt_worker, start_link, [Config]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_connector_mqtt_worker]}. + +-spec(bridges() -> [{node(), map()}]). +bridges() -> + [{Name, emqx_connector_mqtt_worker:status(Name)} + || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. + +create_bridge(Config) -> + supervisor:start_child(?MODULE, bridge_spec(Config)). + +drop_bridge(Name) -> + case supervisor:terminate_child(?MODULE, Name) of + ok -> + supervisor:delete_child(?MODULE, Name); + {error, Error} -> + {error, Error} + end. + +%% =================================================================== +on_start(InstId, Conf) -> + logger:info("starting mqtt connector: ~p, ~p", [InstId, Conf]), + NamePrefix = binary_to_list(InstId), + BasicConf = basic_config(Conf), + InitRes = {ok, #{name_prefix => NamePrefix, baisc_conf => BasicConf, sub_bridges => []}}, + InOutConfigs = check_channel_id_dup(maps:get(in, Conf, []) ++ maps:get(out, Conf, [])), + lists:foldl(fun + (_InOutConf, {error, Reason}) -> + {error, Reason}; + (InOutConf, {ok, #{sub_bridges := SubBridges} = Res}) -> + case create_channel(InOutConf, NamePrefix, BasicConf) of + {error, Reason} -> {error, Reason}; + {ok, Name} -> {ok, Res#{sub_bridges => [Name | SubBridges]}} + end + end, InitRes, InOutConfigs). + +on_stop(InstId, #{}) -> + logger:info("stopping mqtt connector: ~p", [InstId]), + case ?MODULE:drop_bridge(InstId) of + ok -> ok; + {error, not_found} -> ok; + {error, Reason} -> + logger:error("stop bridge failed, error: ~p", [Reason]) + end. + +%% TODO: let the emqx_resource trigger on_query/4 automatically according to the +%% `in` and `out` config +on_query(InstId, {create_channel, Conf}, _AfterQuery, #{name_prefix := Prefix, + baisc_conf := BasicConf}) -> + logger:debug("create channel to connector: ~p, conf: ~p", [InstId, Conf]), + create_channel(Conf, Prefix, BasicConf); +on_query(InstId, {publish_to_local, Msg}, _AfterQuery, _State) -> + logger:debug("publish to local node, connector: ~p, msg: ~p", [InstId, Msg]); +on_query(InstId, {publish_to_remote, Msg}, _AfterQuery, _State) -> + logger:debug("publish to remote node, connector: ~p, msg: ~p", [InstId, Msg]). + +on_health_check(_InstId, #{sub_bridges := NameList} = State) -> + Results = [{Name, emqx_connector_mqtt_worker:ping(Name)} || Name <- NameList], + case lists:all(fun({_, pong}) -> true; ({_, _}) -> false end, Results) of + true -> {ok, State}; + false -> {error, {some_sub_bridge_down, Results}, State} + end. + +check_channel_id_dup(Confs) -> + lists:foreach(fun(#{id := Id}) -> + case length([Id || #{id := Id0} <- Confs, Id0 == Id]) of + 1 -> ok; + L when L > 1 -> error({mqtt_bridge_conf, {duplicate_id_found, Id}}) + end + end, Confs), + Confs. + +%% this is an `in` bridge +create_channel(#{subscribe_remote_topic := _, id := BridgeId} = InConf, NamePrefix, + #{clientid_prefix := ClientPrefix} = BasicConf) -> + logger:info("creating 'in' channel for: ~p", [BridgeId]), + create_sub_bridge(BasicConf#{name => bridge_name(NamePrefix, BridgeId), + clientid => clientid(ClientPrefix, BridgeId), + subscriptions => InConf, forwards => undefined}); +%% this is an `out` bridge +create_channel(#{subscribe_local_topic := _, id := BridgeId} = OutConf, NamePrefix, + #{clientid_prefix := ClientPrefix} = BasicConf) -> + logger:info("creating 'out' channel for: ~p", [BridgeId]), + create_sub_bridge(BasicConf#{name => bridge_name(NamePrefix, BridgeId), + clientid => clientid(ClientPrefix, BridgeId), + subscriptions => undefined, forwards => OutConf}). + +create_sub_bridge(#{name := Name} = Conf) -> + case ?MODULE:create_bridge(Conf) of + {ok, _Pid} -> + start_sub_bridge(Name); + {error, {already_started, _Pid}} -> + ok; + {error, Reason} -> + {error, Reason} + end. + +start_sub_bridge(Name) -> + case emqx_connector_mqtt_worker:ensure_started(Name) of + ok -> {ok, Name}; + {error, Reason} -> {error, Reason} + end. + +basic_config(#{ + server := Server, + reconnect_interval := ReconnIntv, + proto_ver := ProtoVer, + bridge_mode := BridgeMod, + clientid_prefix := ClientIdPrefix, + username := User, + password := Password, + clean_start := CleanStart, + keepalive := KeepAlive, + retry_interval := RetryIntv, + max_inflight := MaxInflight, + replayq := ReplayQ, + ssl := #{enable := EnableSsl} = Ssl}) -> + #{ + replayq => ReplayQ, + %% connection opts + server => Server, + reconnect_interval => ReconnIntv, + proto_ver => ProtoVer, + bridge_mode => BridgeMod, + clientid_prefix => ClientIdPrefix, + username => User, + password => Password, + clean_start => CleanStart, + keepalive => KeepAlive, + retry_interval => RetryIntv, + max_inflight => MaxInflight, + ssl => EnableSsl, + ssl_opts => maps:to_list(maps:remove(enable, Ssl)), + if_record_metrics => true + }. + +bridge_name(Prefix, Id) -> + list_to_atom(str(Prefix) ++ ":" ++ str(Id)). + +clientid(Prefix, Id) -> + list_to_binary(str(Prefix) ++ str(Id)). + +str(A) when is_atom(A) -> + atom_to_list(A); +str(B) when is_binary(B) -> + binary_to_list(B); +str(S) when is_list(S) -> + S. diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index 6a5d93ca2..9dc194c55 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -28,16 +28,14 @@ -export([connect/1]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -export([do_health_check/1]). %%===================================================================== %% Hocon schema -structs() -> [""]. - -fields("") -> - [{config, #{type => hoconsc:ref(?MODULE, config)}}]; +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]. fields(config) -> emqx_connector_schema_lib:relational_db_fields() ++ diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index e89ab7401..8472c661e 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -18,7 +18,7 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). %% callbacks of behaviour emqx_resource -export([ on_start/2 @@ -35,10 +35,9 @@ -export([do_health_check/1]). %%===================================================================== -structs() -> [""]. -fields("") -> - [{config, #{type => hoconsc:ref(?MODULE, config)}}]; +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]. fields(config) -> emqx_connector_schema_lib:relational_db_fields() ++ diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 1ea31ced8..44b036f39 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -20,11 +20,14 @@ -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). -type server() :: tuple(). + -reflect_type([server/0]). + -typerefl_from_string({server/0, ?MODULE, to_server}). + -export([to_server/1]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). %% callbacks of behaviour emqx_resource -export([ on_start/2 @@ -41,16 +44,15 @@ -export([cmd/3]). %%===================================================================== -structs() -> [""]. - -fields("") -> +roots() -> [ {config, #{type => hoconsc:union( [ hoconsc:ref(?MODULE, cluster) , hoconsc:ref(?MODULE, single) , hoconsc:ref(?MODULE, sentinel) ])} } - ]; + ]. + fields(single) -> [ {server, #{type => server()}} , {redis_type, #{type => hoconsc:enum([single]), @@ -175,4 +177,4 @@ to_server(Server) -> case string:tokens(Server, ":") of [Host, Port] -> {ok, {Host, list_to_integer(Port)}}; _ -> {error, Server} - end. + end. \ No newline at end of file diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index d0b314077..6dcc564af 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -51,26 +51,20 @@ , servers/0 ]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -structs() -> [ssl_on, ssl_off]. +roots() -> ["ssl"]. -fields(ssl_on) -> - [ {enable, #{type => true}} +fields("ssl") -> + [ {enable, #{type => boolean(), default => false}} , {cacertfile, fun cacertfile/1} , {keyfile, fun keyfile/1} , {certfile, fun certfile/1} , {verify, fun verify/1} - ]; - -fields(ssl_off) -> - [ {enable, #{type => false}} ]. + ]. ssl_fields() -> - [ {ssl, #{type => hoconsc:union( - [ hoconsc:ref(?MODULE, ssl_on) - , hoconsc:ref(?MODULE, ssl_off) - ]), + [ {ssl, #{type => hoconsc:ref(?MODULE, "ssl"), default => #{<<"enable">> => false} } } @@ -142,7 +136,9 @@ to_ip_port(Str) -> _ -> {error, Str} end. -ip_port_to_string({Ip, Port}) -> +ip_port_to_string({Ip, Port}) when is_list(Ip) -> + iolist_to_binary([Ip, ":", integer_to_list(Port)]); +ip_port_to_string({Ip, Port}) when is_tuple(Ip) -> iolist_to_binary([inet:ntoa(Ip), ":", integer_to_list(Port)]). to_servers(Str) -> diff --git a/apps/emqx_connector/src/emqx_connector_sup.erl b/apps/emqx_connector/src/emqx_connector_sup.erl index 603b9a8ad..a24a97b8f 100644 --- a/apps/emqx_connector/src/emqx_connector_sup.erl +++ b/apps/emqx_connector/src/emqx_connector_sup.erl @@ -28,9 +28,19 @@ start_link() -> init([]) -> SupFlags = #{strategy => one_for_all, - intensity => 0, - period => 1}, - ChildSpecs = [], + intensity => 5, + period => 20}, + ChildSpecs = [ + child_spec(emqx_connector_mqtt) + ], {ok, {SupFlags, ChildSpecs}}. +child_spec(Mod) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 3000, + type => supervisor, + modules => [Mod]}. + %% internal functions diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl similarity index 78% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl rename to apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl index 8d442463b..3de7feac4 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl @@ -16,11 +16,12 @@ %% @doc This module implements EMQX Bridge transport layer on top of MQTT protocol --module(emqx_bridge_mqtt). +-module(emqx_connector_mqtt_mod). -export([ start/1 , send/2 , stop/1 + , ping/1 ]). -export([ ensure_subscribed/3 @@ -33,9 +34,6 @@ , handle_disconnected/2 ]). --export([ check_subscriptions/1 - ]). - -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). @@ -50,15 +48,11 @@ start(Config) -> Parent = self(), - Address = maps:get(address, Config), + {Host, Port} = maps:get(server, Config), Mountpoint = maps:get(receive_mountpoint, Config, undefined), - Subscriptions = maps:get(subscriptions, Config, []), - Subscriptions1 = check_subscriptions(Subscriptions), - Handlers = make_hdlr(Parent, Mountpoint), - {Host, Port} = case string:tokens(Address, ":") of - [H] -> {H, 1883}; - [H, P] -> {H, list_to_integer(P)} - end, + Subscriptions = maps:get(subscriptions, Config, undefined), + Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Subscriptions), + Handlers = make_hdlr(Parent, Vars), Config1 = Config#{ msg_handler => Handlers, host => Host, @@ -66,13 +60,13 @@ start(Config) -> force_ping => true, proto_ver => maps:get(proto_ver, Config, v4) }, - case emqtt:start_link(without_config(Config1)) of + case emqtt:start_link(process_config(Config1)) of {ok, Pid} -> case emqtt:connect(Pid) of {ok, _} -> try - Subscriptions2 = subscribe_remote_topics(Pid, Subscriptions1), - {ok, #{client_pid => Pid, subscriptions => Subscriptions2}} + ok = subscribe_remote_topics(Pid, Subscriptions), + {ok, #{client_pid => Pid, subscriptions => Subscriptions}} catch throw : Reason -> ok = stop(#{client_pid => Pid}), @@ -90,6 +84,9 @@ stop(#{client_pid := Pid}) -> safe_stop(Pid, fun() -> emqtt:stop(Pid) end, 1000), ok. +ping(#{client_pid := Pid}) -> + emqtt:ping(Pid). + ensure_subscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic, QoS) when is_pid(Pid) -> case emqtt:subscribe(Pid, Topic, QoS) of {ok, _, _} -> Conn#{subscriptions => [{Topic, QoS}|Subs]}; @@ -158,33 +155,29 @@ handle_puback(#{packet_id := PktId, reason_code := RC}, Parent) RC =:= ?RC_NO_MATCHING_SUBSCRIBERS -> Parent ! {batch_ack, PktId}, ok; handle_puback(#{packet_id := PktId, reason_code := RC}, _Parent) -> - ?LOG(warning, "Publish ~p to remote node falied, reason_code: ~p", [PktId, RC]). + ?LOG(warning, "publish ~p to remote node falied, reason_code: ~p", [PktId, RC]). -handle_publish(Msg, Mountpoint) -> - emqx_broker:publish(emqx_bridge_msg:to_broker_msg(Msg, Mountpoint)). +handle_publish(Msg, undefined) -> + ?LOG(error, "cannot publish to local broker as 'bridge.mqtt..in' not configured, msg: ~p", [Msg]); +handle_publish(Msg, Vars) -> + ?LOG(debug, "publish to local broker, msg: ~p, vars: ~p", [Msg, Vars]), + emqx_broker:publish(emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars)). handle_disconnected(Reason, Parent) -> Parent ! {disconnected, self(), Reason}. -make_hdlr(Parent, Mountpoint) -> +make_hdlr(Parent, Vars) -> #{puback => {fun ?MODULE:handle_puback/2, [Parent]}, - publish => {fun ?MODULE:handle_publish/2, [Mountpoint]}, + publish => {fun ?MODULE:handle_publish/2, [Vars]}, disconnected => {fun ?MODULE:handle_disconnected/2, [Parent]} }. -subscribe_remote_topics(ClientPid, Subscriptions) -> - lists:map(fun({Topic, Qos}) -> - case emqtt:subscribe(ClientPid, Topic, Qos) of - {ok, _, _} -> {Topic, Qos}; - Error -> throw(Error) - end - end, Subscriptions). +subscribe_remote_topics(_ClientPid, undefined) -> ok; +subscribe_remote_topics(ClientPid, #{subscribe_remote_topic := FromTopic, subscribe_qos := QoS}) -> + case emqtt:subscribe(ClientPid, FromTopic, QoS) of + {ok, _, _} -> ok; + Error -> throw(Error) + end. -without_config(Config) -> - maps:without([conn_type, address, receive_mountpoint, subscriptions], Config). - -check_subscriptions(Subscriptions) -> - lists:map(fun(#{qos := QoS, topic := Topic}) -> - true = emqx_topic:validate({filter, Topic}), - {Topic, QoS} - end, Subscriptions). +process_config(Config) -> + maps:without([conn_type, address, receive_mountpoint, subscriptions, name], Config). diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl new file mode 100644 index 000000000..5f076ed9e --- /dev/null +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -0,0 +1,125 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_connector_mqtt_msg). + +-export([ to_binary/1 + , from_binary/1 + , make_pub_vars/2 + , to_remote_msg/2 + , to_broker_msg/2 + , estimate_size/1 + ]). + +-export_type([msg/0]). + +-include_lib("emqx/include/emqx.hrl"). + +-include_lib("emqtt/include/emqtt.hrl"). + + +-type msg() :: emqx_types:message(). +-type exp_msg() :: emqx_types:message() | #mqtt_msg{}. + +-type variables() :: #{ + mountpoint := undefined | binary(), + topic := binary(), + qos := original | integer(), + retain := original | boolean(), + payload := binary() +}. + +make_pub_vars(_, undefined) -> undefined; +make_pub_vars(Mountpoint, #{payload := _, qos := _, retain := _, remote_topic := Topic} = Conf) -> + Conf#{topic => Topic, mountpoint => Mountpoint}; +make_pub_vars(Mountpoint, #{payload := _, qos := _, retain := _, local_topic := Topic} = Conf) -> + Conf#{topic => Topic, mountpoint => Mountpoint}. + +%% @doc Make export format: +%% 1. Mount topic to a prefix +%% 2. Fix QoS to 1 +%% @end +%% Shame that we have to know the callback module here +%% would be great if we can get rid of #mqtt_msg{} record +%% and use #message{} in all places. +-spec to_remote_msg(msg() | map(), variables()) + -> exp_msg(). +to_remote_msg(#message{flags = Flags0} = Msg, Vars) -> + Retain0 = maps:get(retain, Flags0, false), + MapMsg = maps:put(retain, Retain0, emqx_message:to_map(Msg)), + to_remote_msg(MapMsg, Vars); +to_remote_msg(MapMsg, #{topic := TopicToken, payload := PayloadToken, + qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}) when is_map(MapMsg) -> + Topic = replace_vars_in_str(TopicToken, MapMsg), + Payload = replace_vars_in_str(PayloadToken, MapMsg), + QoS = replace_simple_var(QoSToken, MapMsg), + Retain = replace_simple_var(RetainToken, MapMsg), + #mqtt_msg{qos = QoS, + retain = Retain, + topic = topic(Mountpoint, Topic), + props = #{}, + payload = Payload}; +to_remote_msg(#message{topic = Topic} = Msg, #{mountpoint := Mountpoint}) -> + Msg#message{topic = topic(Mountpoint, Topic)}. + +%% published from remote node over a MQTT connection +to_broker_msg(#{dup := Dup, properties := Props} = MapMsg, + #{topic := TopicToken, payload := PayloadToken, + qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}) -> + Topic = replace_vars_in_str(TopicToken, MapMsg), + Payload = replace_vars_in_str(PayloadToken, MapMsg), + QoS = replace_simple_var(QoSToken, MapMsg), + Retain = replace_simple_var(RetainToken, MapMsg), + set_headers(Props, + emqx_message:set_flags(#{dup => Dup, retain => Retain}, + emqx_message:make(bridge, QoS, topic(Mountpoint, Topic), Payload))). + +%% Replace a string contains vars to another string in which the placeholders are replace by the +%% corresponding values. For example, given "a: ${var}", if the var=1, the result string will be: +%% "a: 1". +replace_vars_in_str(Tokens, Data) when is_list(Tokens) -> + emqx_plugin_libs_rule:proc_tmpl(Tokens, Data, #{return => full_binary}); +replace_vars_in_str(Val, _Data) -> + Val. + +%% Replace a simple var to its value. For example, given "${var}", if the var=1, then the result +%% value will be an integer 1. +replace_simple_var(Tokens, Data) when is_list(Tokens) -> + [Var] = emqx_plugin_libs_rule:proc_tmpl(Tokens, Data, #{return => rawlist}), + Var; +replace_simple_var(Val, _Data) -> + Val. + +%% @doc Make `binary()' in order to make iodata to be persisted on disk. +-spec to_binary(msg()) -> binary(). +to_binary(Msg) -> term_to_binary(Msg). + +%% @doc Unmarshal binary into `msg()'. +-spec from_binary(binary()) -> msg(). +from_binary(Bin) -> binary_to_term(Bin). + +%% @doc Estimate the size of a message. +%% Count only the topic length + payload size +-spec estimate_size(msg()) -> integer(). +estimate_size(#message{topic = Topic, payload = Payload}) -> + size(Topic) + size(Payload). + +set_headers(undefined, Msg) -> + Msg; +set_headers(Val, Msg) -> + emqx_message:set_headers(Val, Msg). +topic(undefined, Topic) -> Topic; +topic(Prefix, Topic) -> emqx_topic:prepend(Prefix, Topic). diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl new file mode 100644 index 000000000..ed7fd4408 --- /dev/null +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -0,0 +1,78 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_connector_mqtt_schema). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ roots/0 + , fields/1]). + +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, "config")}}]. + +fields("config") -> + [ {server, hoconsc:mk(emqx_schema:ip_port(), #{default => "127.0.0.1:1883"})} + , {reconnect_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} + , {proto_ver, fun proto_ver/1} + , {bridge_mode, hoconsc:mk(boolean(), #{default => true})} + , {clientid_prefix, hoconsc:mk(string(), #{default => ""})} + , {username, hoconsc:mk(string())} + , {password, hoconsc:mk(string())} + , {clean_start, hoconsc:mk(boolean(), #{default => true})} + , {keepalive, hoconsc:mk(integer(), #{default => 300})} + , {retry_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} + , {max_inflight, hoconsc:mk(integer(), #{default => 32})} + , {replayq, hoconsc:mk(hoconsc:ref(?MODULE, "replayq"))} + , {in, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "in")), #{default => []})} + , {out, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "out")), #{default => []})} + ] ++ emqx_connector_schema_lib:ssl_fields(); + +fields("in") -> + [ {subscribe_remote_topic, #{type => binary(), nullable => false}} + , {local_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} + , {subscribe_qos, hoconsc:mk(qos(), #{default => 1})} + ] ++ common_inout_confs(); + +fields("out") -> + [ {subscribe_local_topic, #{type => binary(), nullable => false}} + , {remote_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} + ] ++ common_inout_confs(); + +fields("replayq") -> + [ {dir, hoconsc:union([boolean(), string()])} + , {seg_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "100MB"})} + , {offload, hoconsc:mk(boolean(), #{default => false})} + , {max_total_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "1024MB"})} + ]. + +common_inout_confs() -> + [{id, #{type => binary(), nullable => false}}] ++ publish_confs(). + +publish_confs() -> + [ {qos, hoconsc:mk(qos(), #{default => <<"${qos}">>})} + , {retain, hoconsc:mk(hoconsc:union([boolean(), binary()]), #{default => <<"${retain}">>})} + , {payload, hoconsc:mk(binary(), #{default => <<"${payload}">>})} + ]. + +qos() -> + hoconsc:union([typerefl:integer(0), typerefl:integer(1), typerefl:integer(2), binary()]). + +proto_ver(type) -> hoconsc:enum([v3, v4, v5]); +proto_ver(default) -> v4; +proto_ver(_) -> undefined. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl similarity index 68% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl rename to apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 630fb4443..6ced719df 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -19,7 +19,7 @@ %% to remote MQTT node/cluster via `connection' transport layer. %% In case `REMOTE' is also an EMQX node, `connection' is recommended to be %% the `gen_rpc' based implementation `emqx_bridge_rpc'. Otherwise `connection' -%% has to be `emqx_bridge_mqtt'. +%% has to be `emqx_connector_mqtt_mod'. %% %% ``` %% +------+ +--------+ @@ -59,7 +59,7 @@ %% NOTES: %% * Local messages are all normalised to QoS-1 when exporting to remote --module(emqx_bridge_worker). +-module(emqx_connector_mqtt_worker). -behaviour(gen_statem). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). @@ -86,16 +86,13 @@ -export([ ensure_started/1 , ensure_stopped/1 , status/1 + , ping/1 ]). -export([ get_forwards/1 - , ensure_forward_present/2 - , ensure_forward_absent/2 ]). -export([ get_subscriptions/1 - , ensure_subscription_present/3 - , ensure_subscription_absent/2 ]). %% Internal @@ -109,7 +106,7 @@ -type id() :: atom() | string() | pid(). -type qos() :: emqx_mqtt_types:qos(). -type config() :: map(). --type batch() :: [emqx_bridge_msg:exp_msg()]. +-type batch() :: [emqx_connector_mqtt_msg:exp_msg()]. -type ack_ref() :: term(). -type topic() :: emqx_topic:topic(). @@ -135,12 +132,12 @@ %% mountpoint: The topic mount point for messages sent to remote node/cluster %% `undefined', `<<>>' or `""' to disable %% forwards: Local topics to subscribe. -%% queue.batch_bytes_limit: Max number of bytes to collect in a batch for each +%% replayq.batch_bytes_limit: Max number of bytes to collect in a batch for each %% send call towards emqx_bridge_connect -%% queue.batch_count_limit: Max number of messages to collect in a batch for +%% replayq.batch_count_limit: Max number of messages to collect in a batch for %% each send call towards emqx_bridge_connect -%% queue.replayq_dir: Directory where replayq should persist messages -%% queue.replayq_seg_bytes: Size in bytes for each replayq segment file +%% replayq.dir: Directory where replayq should persist messages +%% replayq.seg_bytes: Size in bytes for each replayq segment file %% %% Find more connection specific configs in the callback modules %% of emqx_bridge_connect behaviour. @@ -169,6 +166,11 @@ status(Pid) when is_pid(Pid) -> status(Name) -> gen_statem:call(name(Name), status). +ping(Pid) when is_pid(Pid) -> + gen_statem:call(Pid, ping); +ping(Name) -> + gen_statem:call(name(Name), ping). + %% @doc Return all forwards (local subscriptions). -spec get_forwards(id()) -> [topic()]. get_forwards(Name) -> gen_statem:call(name(Name), get_forwards, timer:seconds(1000)). @@ -177,47 +179,21 @@ get_forwards(Name) -> gen_statem:call(name(Name), get_forwards, timer:seconds(10 -spec get_subscriptions(id()) -> [{emqx_topic:topic(), qos()}]. get_subscriptions(Name) -> gen_statem:call(name(Name), get_subscriptions). -%% @doc Add a new forward (local topic subscription). --spec ensure_forward_present(id(), topic()) -> ok. -ensure_forward_present(Name, Topic) -> - gen_statem:call(name(Name), {ensure_forward_present, topic(Topic)}). - -%% @doc Ensure a forward topic is deleted. --spec ensure_forward_absent(id(), topic()) -> ok. -ensure_forward_absent(Name, Topic) -> - gen_statem:call(name(Name), {ensure_forward_absent, topic(Topic)}). - -%% @doc Ensure subscribed to remote topic. -%% NOTE: only applicable when connection module is emqx_bridge_mqtt -%% return `{error, no_remote_subscription_support}' otherwise. --spec ensure_subscription_present(id(), topic(), qos()) -> ok | {error, any()}. -ensure_subscription_present(Name, Topic, QoS) -> - gen_statem:call(name(Name), {ensure_subscription_present, topic(Topic), QoS}). - -%% @doc Ensure unsubscribed from remote topic. -%% NOTE: only applicable when connection module is emqx_bridge_mqtt --spec ensure_subscription_absent(id(), topic()) -> ok. -ensure_subscription_absent(Name, Topic) -> - gen_statem:call(name(Name), {ensure_subscription_absent, topic(Topic)}). - callback_mode() -> [state_functions]. %% @doc Config should be a map(). -init(Opts) -> +init(#{name := Name} = ConnectOpts) -> + ?LOG(info, "starting bridge worker for ~p", [Name]), erlang:process_flag(trap_exit, true), - ConnectOpts = maps:get(config, Opts), - ConnectModule = conn_type(maps:get(conn_type, ConnectOpts)), - Forwards = maps:get(forwards, Opts, []), - Queue = open_replayq(maps:get(queue, Opts, #{})), - State = init_opts(Opts), + Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})), + State = init_state(ConnectOpts), self() ! idle, - {ok, idle, State#{connect_module => ConnectModule, - connect_opts => ConnectOpts, - forwards => Forwards, - replayq => Queue - }}. + {ok, idle, State#{ + connect_opts => pre_process_opts(ConnectOpts), + replayq => Queue + }}. -init_opts(Opts) -> +init_state(Opts) -> IfRecordMetrics = maps:get(if_record_metrics, Opts, true), ReconnDelayMs = maps:get(reconnect_interval, Opts, ?DEFAULT_RECONNECT_DELAY_MS), StartType = maps:get(start_type, Opts, manual), @@ -235,17 +211,39 @@ init_opts(Opts) -> if_record_metrics => IfRecordMetrics, name => Name}. -open_replayq(QCfg) -> - Dir = maps:get(replayq_dir, QCfg, undefined), - SegBytes = maps:get(replayq_seg_bytes, QCfg, ?DEFAULT_SEG_BYTES), +open_replayq(Name, QCfg) -> + Dir = maps:get(dir, QCfg, undefined), + SegBytes = maps:get(seg_bytes, QCfg, ?DEFAULT_SEG_BYTES), MaxTotalSize = maps:get(max_total_size, QCfg, ?DEFAULT_MAX_TOTAL_SIZE), QueueConfig = case Dir =:= undefined orelse Dir =:= "" of true -> #{mem_only => true}; - false -> #{dir => Dir, seg_bytes => SegBytes, max_total_size => MaxTotalSize} + false -> #{dir => filename:join([Dir, node(), Name]), + seg_bytes => SegBytes, max_total_size => MaxTotalSize} end, - replayq:open(QueueConfig#{sizer => fun emqx_bridge_msg:estimate_size/1, + replayq:open(QueueConfig#{sizer => fun emqx_connector_mqtt_msg:estimate_size/1, marshaller => fun ?MODULE:msg_marshaller/1}). +pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts) -> + ConnectOpts#{subscriptions => pre_process_in_out(InConf), + forwards => pre_process_in_out(OutConf)}. + +pre_process_in_out(undefined) -> undefined; +pre_process_in_out(Conf) when is_map(Conf) -> + Conf1 = pre_process_conf(local_topic, Conf), + Conf2 = pre_process_conf(remote_topic, Conf1), + Conf3 = pre_process_conf(payload, Conf2), + Conf4 = pre_process_conf(qos, Conf3), + pre_process_conf(retain, Conf4). + +pre_process_conf(Key, Conf) -> + case maps:find(Key, Conf) of + error -> Conf; + {ok, Val} when is_binary(Val) -> + Conf#{Key => emqx_plugin_libs_rule:preproc_tmpl(Val)}; + {ok, Val} -> + Conf#{Key => Val} + end. + code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. @@ -311,28 +309,18 @@ connected(Type, Content, State) -> %% Common handlers common(StateName, {call, From}, status, _State) -> {keep_state_and_data, [{reply, From, StateName}]}; +common(_StateName, {call, From}, ping, #{connection := Conn} =_State) -> + Reply = emqx_connector_mqtt_mod:ping(Conn), + {keep_state_and_data, [{reply, From, Reply}]}; common(_StateName, {call, From}, ensure_stopped, #{connection := undefined} = _State) -> {keep_state_and_data, [{reply, From, ok}]}; -common(_StateName, {call, From}, ensure_stopped, #{connection := Conn, - connect_module := ConnectModule} = State) -> - Reply = ConnectModule:stop(Conn), +common(_StateName, {call, From}, ensure_stopped, #{connection := Conn} = State) -> + Reply = emqx_connector_mqtt_mod:stop(Conn), {next_state, idle, State#{connection => undefined}, [{reply, From, Reply}]}; -common(_StateName, {call, From}, get_forwards, #{forwards := Forwards}) -> +common(_StateName, {call, From}, get_forwards, #{connect_opts := #{forwards := Forwards}}) -> {keep_state_and_data, [{reply, From, Forwards}]}; common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) -> - {keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, [])}]}; -common(_StateName, {call, From}, {ensure_forward_present, Topic}, State) -> - {Result, NewState} = do_ensure_forward_present(Topic, State), - {keep_state, NewState, [{reply, From, Result}]}; -common(_StateName, {call, From}, {ensure_subscription_present, Topic, QoS}, State) -> - {Result, NewState} = do_ensure_subscription_present(Topic, QoS, State), - {keep_state, NewState, [{reply, From, Result}]}; -common(_StateName, {call, From}, {ensure_forward_absent, Topic}, State) -> - {Result, NewState} = do_ensure_forward_absent(Topic, State), - {keep_state, NewState, [{reply, From, Result}]}; -common(_StateName, {call, From}, {ensure_subscription_absent, Topic}, State) -> - {Result, NewState} = do_ensure_subscription_absent(Topic, State), - {keep_state, NewState, [{reply, From, Result}]}; + {keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, #{})}]}; common(_StateName, info, {deliver, _, Msg}, State = #{replayq := Q, if_record_metrics := IfRecordMetric}) -> Msgs = collect([Msg]), @@ -349,76 +337,21 @@ common(StateName, Type, Content, #{name := Name} = State) -> [Name, Type, StateName, Content]), {keep_state, State}. -do_ensure_forward_present(Topic, #{forwards := Forwards, name := Name} = State) -> - case is_topic_present(Topic, Forwards) of - true -> - {ok, State}; - false -> - R = subscribe_local_topic(Topic, Name), - {R, State#{forwards => [Topic | Forwards]}} - end. - -do_ensure_subscription_present(_Topic, _QoS, #{connection := undefined} = State) -> - {{error, no_connection}, State}; -do_ensure_subscription_present(_Topic, _QoS, #{connect_module := emqx_bridge_rpc} = State) -> - {{error, no_remote_subscription_support}, State}; -do_ensure_subscription_present(Topic, QoS, #{connect_module := ConnectModule, - connection := Conn} = State) -> - case is_topic_present(Topic, maps:get(subscriptions, Conn, [])) of - true -> - {ok, State}; - false -> - case ConnectModule:ensure_subscribed(Conn, Topic, QoS) of - {error, Error} -> - {{error, Error}, State}; - Conn1 -> - {ok, State#{connection => Conn1}} - end - end. - -do_ensure_forward_absent(Topic, #{forwards := Forwards} = State) -> - case is_topic_present(Topic, Forwards) of - true -> - R = do_unsubscribe(Topic), - {R, State#{forwards => lists:delete(Topic, Forwards)}}; - false -> - {ok, State} - end. -do_ensure_subscription_absent(_Topic, #{connection := undefined} = State) -> - {{error, no_connection}, State}; -do_ensure_subscription_absent(_Topic, #{connect_module := emqx_bridge_rpc} = State) -> - {{error, no_remote_subscription_support}, State}; -do_ensure_subscription_absent(Topic, #{connect_module := ConnectModule, - connection := Conn} = State) -> - case is_topic_present(Topic, maps:get(subscriptions, Conn, [])) of - true -> - case ConnectModule:ensure_unsubscribed(Conn, Topic) of - {error, Error} -> - {{error, Error}, State}; - Conn1 -> - {ok, State#{connection => Conn1}} - end; - false -> - {ok, State} - end. - -is_topic_present(Topic, Topics) -> - lists:member(Topic, Topics) orelse false =/= lists:keyfind(Topic, 1, Topics). - -do_connect(#{forwards := Forwards, - connect_module := ConnectModule, - connect_opts := ConnectOpts, +do_connect(#{connect_opts := ConnectOpts = #{forwards := Forwards}, inflight := Inflight, name := Name} = State) -> - ok = subscribe_local_topics(Forwards, Name), - case ConnectModule:start(ConnectOpts) of + case Forwards of + undefined -> ok; + #{subscribe_local_topic := Topic} -> subscribe_local_topic(Topic, Name) + end, + case emqx_connector_mqtt_mod:start(ConnectOpts) of {ok, Conn} -> ?tp(info, connected, #{name => Name, inflight => length(Inflight)}), {ok, State#{connection => Conn}}; {error, Reason} -> ConnectOpts1 = obfuscate(ConnectOpts), - ?LOG(error, "Failed to connect with module=~p\n" - "config=~p\nreason:~p", [ConnectModule, ConnectOpts1, Reason]), + ?LOG(error, "Failed to connect \n" + "config=~p\nreason:~p", [ConnectOpts1, Reason]), {error, Reason, State} end. @@ -441,22 +374,19 @@ retry_inflight(State, [#{q_ack_ref := QAckRef, batch := Batch} | Rest] = OldInf) {error, State1#{inflight := NewInf ++ OldInf}} end. -pop_and_send(#{inflight := Inflight, max_inflight := Max } = State) -> +pop_and_send(#{inflight := Inflight, max_inflight := Max} = State) -> pop_and_send_loop(State, Max - length(Inflight)). pop_and_send_loop(State, 0) -> ?tp(debug, inflight_full, #{}), {ok, State}; -pop_and_send_loop(#{replayq := Q, connect_module := Module} = State, N) -> +pop_and_send_loop(#{replayq := Q} = State, N) -> case replayq:is_empty(Q) of true -> ?tp(debug, replayq_drained, #{}), {ok, State}; false -> - BatchSize = case Module of - emqx_bridge_rpc -> maps:get(batch_size, State); - _ -> 1 - end, + BatchSize = 1, Opts = #{count_limit => BatchSize, bytes_limit => 999999999}, {Q1, QAckRef, Batch} = replayq:pop(Q, Opts), case do_send(State#{replayq := Q1}, QAckRef, Batch) of @@ -466,16 +396,20 @@ pop_and_send_loop(#{replayq := Q, connect_module := Module} = State, N) -> end. %% Assert non-empty batch because we have a is_empty check earlier. +do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Batch) -> + ?LOG(error, "cannot forward messages to remote broker as 'bridge.mqtt..in' not configured, msg: ~p", [Batch]); do_send(#{inflight := Inflight, - connect_module := Module, connection := Connection, mountpoint := Mountpoint, + connect_opts := #{forwards := Forwards}, if_record_metrics := IfRecordMetrics} = State, QAckRef, [_ | _] = Batch) -> + Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), ExportMsg = fun(Message) -> bridges_metrics_inc(IfRecordMetrics, 'bridge.mqtt.message_sent'), - emqx_bridge_msg:to_export(Module, Mountpoint, Message) + emqx_connector_mqtt_msg:to_remote_msg(Message, Vars) end, - case Module:send(Connection, [ExportMsg(M) || M <- Batch]) of + ?LOG(debug, "publish to remote broker, msg: ~p, vars: ~p", [Batch, Vars]), + case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(M) || M <- Batch]) of {ok, Refs} -> {ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef, send_ack_ref => map_set(Refs), @@ -530,9 +464,6 @@ drop_acked_batches(Q, [#{send_ack_ref := Refs, All end. -subscribe_local_topics(Topics, Name) -> - lists:foreach(fun(Topic) -> subscribe_local_topic(Topic, Name) end, Topics). - subscribe_local_topic(Topic, Name) -> do_subscribe(Topic, Name). @@ -549,32 +480,25 @@ validate(RawTopic) -> do_subscribe(RawTopic, Name) -> TopicFilter = validate(RawTopic), - {Topic, SubOpts} = emqx_topic:parse(TopicFilter, #{qos => ?QOS_1}), + {Topic, SubOpts} = emqx_topic:parse(TopicFilter, #{qos => ?QOS_2}), emqx_broker:subscribe(Topic, Name, SubOpts). -do_unsubscribe(RawTopic) -> - TopicFilter = validate(RawTopic), - {Topic, _SubOpts} = emqx_topic:parse(TopicFilter), - emqx_broker:unsubscribe(Topic). - -disconnect(#{connection := Conn, - connect_module := Module - } = State) when Conn =/= undefined -> - Module:stop(Conn), +disconnect(#{connection := Conn} = State) when Conn =/= undefined -> + emqx_connector_mqtt_mod:stop(Conn), State#{connection => undefined}; disconnect(State) -> State. %% Called only when replayq needs to dump it to disk. -msg_marshaller(Bin) when is_binary(Bin) -> emqx_bridge_msg:from_binary(Bin); -msg_marshaller(Msg) -> emqx_bridge_msg:to_binary(Msg). +msg_marshaller(Bin) when is_binary(Bin) -> emqx_connector_mqtt_msg:from_binary(Bin); +msg_marshaller(Msg) -> emqx_connector_mqtt_msg:to_binary(Msg). format_mountpoint(undefined) -> undefined; format_mountpoint(Prefix) -> binary:replace(iolist_to_binary(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). -name(Id) -> list_to_atom(lists:concat([?MODULE, "_", Id])). +name(Id) -> list_to_atom(str(Id)). register_metrics() -> lists:foreach(fun emqx_metrics:ensure/1, @@ -603,9 +527,9 @@ obfuscate(Map) -> is_sensitive(password) -> true; is_sensitive(_) -> false. -conn_type(rpc) -> - emqx_bridge_rpc; -conn_type(mqtt) -> - emqx_bridge_mqtt; -conn_type(Mod) when is_atom(Mod) -> - Mod. +str(A) when is_atom(A) -> + atom_to_list(A); +str(B) when is_binary(B) -> + binary_to_list(B); +str(S) when is_list(S) -> + S. \ No newline at end of file diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl similarity index 87% rename from apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl rename to apps/emqx_connector/test/emqx_connector_mqtt_tests.erl index 5babe0ed9..0f4d651c9 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl +++ b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_mqtt_tests). +-module(emqx_connector_mqtt_tests). -include_lib("eunit/include/eunit.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). @@ -37,11 +37,11 @@ send_and_ack_test() -> try Max = 1, Batch = lists:seq(1, Max), - {ok, Conn} = emqx_bridge_mqtt:start(#{address => "127.0.0.1:1883"}), + {ok, Conn} = emqx_connector_mqtt_mod:start(#{server => {{127,0,0,1}, 1883}}), % %% return last packet id as batch reference - {ok, _AckRef} = emqx_bridge_mqtt:send(Conn, Batch), + {ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch), - ok = emqx_bridge_mqtt:stop(Conn) + ok = emqx_connector_mqtt_mod:stop(Conn) after meck:unload(emqtt) end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl similarity index 55% rename from apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl rename to apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl index ffa2e9ee5..090106cef 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl +++ b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl @@ -14,14 +14,14 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_worker_tests). +-module(emqx_connector_mqtt_worker_tests). -include_lib("eunit/include/eunit.hrl"). -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). -define(BRIDGE_NAME, test). --define(BRIDGE_REG_NAME, emqx_bridge_worker_test). +-define(BRIDGE_REG_NAME, emqx_connector_mqtt_worker_test). -define(WAIT(PATTERN, TIMEOUT), receive PATTERN -> @@ -31,7 +31,6 @@ error(timeout) end). -%% stub callbacks -export([start/1, send/2, stop/1]). start(#{connect_result := Result, test_pid := Pid, test_ref := Ref}) -> @@ -49,33 +48,41 @@ stop(_Pid) -> ok. %% bridge worker should retry connecting remote node indefinitely % reconnect_test() -> % emqx_metrics:start_link(), -% emqx_bridge_worker:register_metrics(), +% emqx_connector_mqtt_worker:register_metrics(), % Ref = make_ref(), % Config = make_config(Ref, self(), {error, test}), -% {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), +% {ok, Pid} = emqx_connector_mqtt_worker:start_link(?BRIDGE_NAME, Config), % %% assert name registered % ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), % ?WAIT({connection_start_attempt, Ref}, 1000), % %% expect same message again % ?WAIT({connection_start_attempt, Ref}, 1000), -% ok = emqx_bridge_worker:stop(?BRIDGE_REG_NAME), +% ok = emqx_connector_mqtt_worker:stop(?BRIDGE_REG_NAME), % emqx_metrics:stop(), % ok. %% connect first, disconnect, then connect again disturbance_test() -> - emqx_metrics:start_link(), - emqx_bridge_worker:register_metrics(), - Ref = make_ref(), - TestPid = self(), - Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), - {ok, Pid} = emqx_bridge_worker:start_link(Config#{name => disturbance}), - ?assertEqual(Pid, whereis(emqx_bridge_worker_disturbance)), - ?WAIT({connection_start_attempt, Ref}, 1000), - Pid ! {disconnected, TestPid, test}, - ?WAIT({connection_start_attempt, Ref}, 1000), - emqx_metrics:stop(), - ok = emqx_bridge_worker:stop(Pid). + meck:new(emqx_connector_mqtt_mod, [passthrough, no_history]), + meck:expect(emqx_connector_mqtt_mod, start, 1, fun(Conf) -> start(Conf) end), + meck:expect(emqx_connector_mqtt_mod, send, 2, fun(SendFun, Batch) -> send(SendFun, Batch) end), + meck:expect(emqx_connector_mqtt_mod, stop, 1, fun(Pid) -> stop(Pid) end), + try + emqx_metrics:start_link(), + emqx_connector_mqtt_worker:register_metrics(), + Ref = make_ref(), + TestPid = self(), + Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), + {ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => bridge_disturbance}), + ?assertEqual(Pid, whereis(bridge_disturbance)), + ?WAIT({connection_start_attempt, Ref}, 1000), + Pid ! {disconnected, TestPid, test}, + ?WAIT({connection_start_attempt, Ref}, 1000), + emqx_metrics:stop(), + ok = emqx_connector_mqtt_worker:stop(Pid) + after + meck:unload(emqx_connector_mqtt_mod) + end. % % %% buffer should continue taking in messages when disconnected % buffer_when_disconnected_test_() -> @@ -96,40 +103,47 @@ disturbance_test() -> % Config0 = make_config(Ref, false, {ok, #{client_pid => undefined}}), % Config = Config0#{reconnect_delay_ms => 100}, % emqx_metrics:start_link(), -% emqx_bridge_worker:register_metrics(), -% {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), +% emqx_connector_mqtt_worker:register_metrics(), +% {ok, Pid} = emqx_connector_mqtt_worker:start_link(?BRIDGE_NAME, Config), % Sender ! {bridge, Pid}, % Receiver ! {bridge, Pid}, % ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), % Pid ! {disconnected, Ref, test}, % ?WAIT({'DOWN', SenderMref, process, Sender, normal}, 5000), % ?WAIT({'DOWN', ReceiverMref, process, Receiver, normal}, 1000), -% ok = emqx_bridge_worker:stop(?BRIDGE_REG_NAME), +% ok = emqx_connector_mqtt_worker:stop(?BRIDGE_REG_NAME), % emqx_metrics:stop(). manual_start_stop_test() -> - emqx_metrics:start_link(), - emqx_bridge_worker:register_metrics(), - Ref = make_ref(), - TestPid = self(), - BridgeName = manual_start_stop, - Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), - Config = Config0#{start_type := manual}, - {ok, Pid} = emqx_bridge_worker:start_link(Config#{name => BridgeName}), - %% call ensure_started again should yeld the same result - ok = emqx_bridge_worker:ensure_started(BridgeName), - emqx_bridge_worker:ensure_stopped(BridgeName), - emqx_metrics:stop(), - ok = emqx_bridge_worker:stop(Pid). + meck:new(emqx_connector_mqtt_mod, [passthrough, no_history]), + meck:expect(emqx_connector_mqtt_mod, start, 1, fun(Conf) -> start(Conf) end), + meck:expect(emqx_connector_mqtt_mod, send, 2, fun(SendFun, Batch) -> send(SendFun, Batch) end), + meck:expect(emqx_connector_mqtt_mod, stop, 1, fun(Pid) -> stop(Pid) end), + try + emqx_metrics:start_link(), + emqx_connector_mqtt_worker:register_metrics(), + Ref = make_ref(), + TestPid = self(), + BridgeName = manual_start_stop, + Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), + Config = Config0#{start_type := manual}, + {ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => BridgeName}), + %% call ensure_started again should yeld the same result + ok = emqx_connector_mqtt_worker:ensure_started(BridgeName), + emqx_connector_mqtt_worker:ensure_stopped(BridgeName), + emqx_metrics:stop(), + ok = emqx_connector_mqtt_worker:stop(Pid) + after + meck:unload(emqx_connector_mqtt_mod) + end. make_config(Ref, TestPid, Result) -> #{ start_type => auto, + subscriptions => undefined, + forwards => undefined, reconnect_interval => 50, - config => #{ - test_pid => TestPid, - test_ref => Ref, - conn_type => ?MODULE, - connect_result => Result - } + test_pid => TestPid, + test_ref => Ref, + connect_result => Result }. diff --git a/apps/emqx_dashboard/etc/emqx_dashboard.conf b/apps/emqx_dashboard/etc/emqx_dashboard.conf index c25c2802d..70b1d1d71 100644 --- a/apps/emqx_dashboard/etc/emqx_dashboard.conf +++ b/apps/emqx_dashboard/etc/emqx_dashboard.conf @@ -2,42 +2,39 @@ ## EMQ X Dashboard ##-------------------------------------------------------------------- -emqx_dashboard:{ - default_username: "admin" - default_password: "public" +emqx_dashboard { + default_username = "admin" + default_password = "public" ## notice: sample_interval should be divisible by 60. - sample_interval: 10s - listeners: [ + sample_interval = 10s + ## api jwt timeout. default is 30 minute + token_expired_time = 60m + listeners = [ { - num_acceptors: 4 - max_connections: 512 - protocol: http - port: 18083 - backlog: 512 - send_timeout: 15s - send_timeout_close: true - inet6: false - ipv6_v6only: false + protocol = http + num_acceptors = 4 + max_connections = 512 + port = 18083 + backlog = 512 + send_timeout = 5s + inet6 = false + ipv6_v6only = false } -## , -## { -## protocol: https -## port: 18084 -## acceptors: 2 -## backlog: 512 -## send_timeout: 15s -## send_timeout_close: true -## inet6: false -## ipv6_v6only: false -## certfile = "etc/certs/cert.pem" -## keyfile = "etc/certs/key.pem" -## cacertfile = "etc/certs/cacert.pem" -## verify = verify_peer -## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" -## ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" -## fail_if_no_peer_cert = true -## inet6 = false -## ipv6_v6only = false -## } + # , + # { + # protocol = https + # port = 18084 + # num_acceptors = 2 + # backlog = 512 + # send_timeout = 5s + # inet6 = false + # ipv6_v6only = false + # certfile = "etc/certs/cert.pem" + # keyfile = "etc/certs/key.pem" + # cacertfile = "etc/certs/cacert.pem" + # verify = verify_peer + # versions = ["tlsv1.3","tlsv1.2","tlsv1.1","tlsv1"] + # ciphers = ["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_CHACHA20_POLY1305_SHA256","TLS_AES_128_CCM_SHA256","TLS_AES_128_CCM_8_SHA256","ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384","ECDHE-ECDSA-AES256-SHA384","ECDHE-RSA-AES256-SHA384","ECDHE-ECDSA-DES-CBC3-SHA","ECDH-ECDSA-AES256-GCM-SHA384","ECDH-RSA-AES256-GCM-SHA384","ECDH-ECDSA-AES256-SHA384","ECDH-RSA-AES256-SHA384","DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256","AES256-GCM-SHA384","AES256-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256","ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-SHA256","ECDHE-RSA-AES128-SHA256","ECDH-ECDSA-AES128-GCM-SHA256","ECDH-RSA-AES128-GCM-SHA256","ECDH-ECDSA-AES128-SHA256","ECDH-RSA-AES128-SHA256","DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256","AES128-GCM-SHA256","AES128-SHA256","ECDHE-ECDSA-AES256-SHA","ECDHE-RSA-AES256-SHA","DHE-DSS-AES256-SHA","ECDH-ECDSA-AES256-SHA","ECDH-RSA-AES256-SHA","AES256-SHA","ECDHE-ECDSA-AES128-SHA","ECDHE-RSA-AES128-SHA","DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA","ECDH-RSA-AES128-SHA","AES128-SHA"] + # } ] } diff --git a/apps/emqx_dashboard/include/emqx_dashboard.hrl b/apps/emqx_dashboard/include/emqx_dashboard.hrl index 65f1d6ff5..265552bf7 100644 --- a/apps/emqx_dashboard/include/emqx_dashboard.hrl +++ b/apps/emqx_dashboard/include/emqx_dashboard.hrl @@ -14,7 +14,18 @@ %% limitations under the License. %%-------------------------------------------------------------------- --record(mqtt_admin, {username, password, tags, role = undefined}). +-record(mqtt_admin, { + username :: binary(), + password :: binary(), + tags :: list() | binary(), + role = undefined :: atom() + }). + +-record(mqtt_admin_jwt, { + token :: binary(), + username :: binary(), + exptime :: integer() + }). -type(mqtt_admin() :: #mqtt_admin{}). diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 656283bf6..d109dd445 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -20,9 +20,7 @@ -export([ start_listeners/0 - , stop_listeners/0 - , start_listener/1 - , stop_listener/1]). + , stop_listeners/0]). %% Authorization -export([authorize_appid/1]). @@ -36,15 +34,8 @@ %%-------------------------------------------------------------------- start_listeners() -> - lists:foreach(fun start_listener/1, listeners()). - -stop_listeners() -> - lists:foreach(fun stop_listener/1, listeners()). - -start_listener({Proto, Port, Options}) -> {ok, _} = application:ensure_all_started(minirest), Authorization = {?MODULE, authorize_appid}, - RanchOptions = ranch_opts(Port, Options), GlobalSpec = #{ openapi => "3.0.0", info => #{title => "EMQ X Dashboard API", version => "5.0.0"}, @@ -56,19 +47,33 @@ start_listener({Proto, Port, Options}) -> type => apiKey, name => "authorization", in => header}}}}, - Dispatch = [{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, - {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}], - Minirest = #{ - protocol => Proto, + Dispatch = [ + {"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, + {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, + {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}} + ], + BaseMinirest = #{ base_path => ?BASE_PATH, modules => minirest_api:find_api_modules(apps()), authorization => Authorization, security => [#{application => []}], swagger_global_spec => GlobalSpec, - dispatch => Dispatch}, - MinirestOptions = maps:merge(Minirest, RanchOptions), - {ok, _} = minirest:start(listener_name(Proto), MinirestOptions), - ?ULOG("Start ~p listener on ~p successfully.~n", [listener_name(Proto), Port]). + dispatch => Dispatch + }, + [begin + Minirest = maps:put(protocol, Protocol, BaseMinirest), + {ok, _} = minirest:start(Name, RanchOptions, Minirest), + ?ULOG("Start listener ~s on ~p successfully.~n", [Name, Port]) + end || {Name, Protocol, Port, RanchOptions} <- listeners()]. + +stop_listeners() -> + [begin + ok = minirest:stop(Name), + ?ULOG("Stop listener ~s on ~p successfully.~n", [Name, Port]) + end || {Name, _, Port, _} <- listeners()]. + +%%-------------------------------------------------------------------- +%% internal apps() -> [App || {App, _, _} <- application:loaded_applications(), @@ -77,52 +82,79 @@ apps() -> _ -> false end]. -ranch_opts(Port, Options0) -> - Options = lists:foldl( - fun - ({K, _V}, Acc) when K =:= max_connections orelse K =:= num_acceptors -> Acc; - ({inet6, true}, Acc) -> [inet6 | Acc]; - ({inet6, false}, Acc) -> Acc; - ({ipv6_v6only, true}, Acc) -> [{ipv6_v6only, true} | Acc]; - ({ipv6_v6only, false}, Acc) -> Acc; - ({K, V}, Acc)-> - [{K, V} | Acc] - end, [], Options0), - maps:from_list([{port, Port} | Options]). - -stop_listener({Proto, Port, _}) -> - ?ULOG("Stop dashboard listener on ~s successfully.~n", [format(Port)]), - minirest:stop(listener_name(Proto)). - listeners() -> - [{Protocol, Port, maps:to_list(maps:without([protocol, port], Map))} - || Map = #{protocol := Protocol,port := Port} - <- emqx_config:get([emqx_dashboard, listeners], [])]. + [begin + Protocol = maps:get(protocol, ListenerOptions, http), + Port = maps:get(port, ListenerOptions, 18083), + Name = listener_name(Protocol, Port), + RanchOptions = ranch_opts(maps:without([protocol], ListenerOptions)), + {Name, Protocol, Port, RanchOptions} + end || ListenerOptions <- emqx_config:get([emqx_dashboard, listeners], [])]. -listener_name(Proto) -> - list_to_atom(atom_to_list(Proto) ++ ":dashboard"). +ranch_opts(RanchOptions) -> + Keys = [ {ack_timeout, handshake_timeout} + , connection_type + , max_connections + , num_acceptors + , shutdown + , socket], + {S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys), + R#{socket_opts => maps:fold(fun key_only/3, [], S)}. + + +key_take({K, K1}, {All, R}) -> + case maps:get(K, All, undefined) of + undefined -> + {All, R}; + V -> + {maps:remove(K, All), R#{K1 => V}} + end; +key_take(K, {All, R}) -> + case maps:get(K, All, undefined) of + undefined -> + {All, R}; + V -> + {maps:remove(K, All), R#{K => V}} + end. + +key_only(K , true , S) -> [K | S]; +key_only(_K, false, S) -> S; +key_only(K , V , S) -> [{K, V} | S]. + +listener_name(Protocol, Port) -> + Name = "dashboard:" ++ atom_to_list(Protocol) ++ ":" ++ integer_to_list(Port), + list_to_atom(Name). authorize_appid(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of {basic, Username, Password} -> - case emqx_dashboard_admin:check(iolist_to_binary(Username), - iolist_to_binary(Password)) of + case emqx_dashboard_admin:check(Username, Password) of ok -> ok; {error, _} -> {401, #{<<"WWW-Authenticate">> => - <<"Basic Realm=\"minirest-server\"">>}, - <<"UNAUTHORIZED">>} + <<"Basic Realm=\"minirest-server\"">>}, + #{code => <<"ERROR_USERNAME_OR_PWD">>, + message => <<"Check your username and password">>}} + end; + {bearer, Token} -> + case emqx_dashboard_admin:verify_token(Token) of + ok -> + ok; + {error, token_timeout} -> + {401, #{<<"WWW-Authenticate">> => + <<"Bearer Realm=\"minirest-server\"">>}, + #{code => <<"TOKEN_TIME_OUT">>, + message => <<"POST '/login', get your new token">>}}; + {error, not_found} -> + {401, #{<<"WWW-Authenticate">> => + <<"Bearer Realm=\"minirest-server\"">>}, + #{code => <<"BAD_TOKEN">>, + message => <<"POST '/login'">>}} end; _ -> {401, #{<<"WWW-Authenticate">> => - <<"Basic Realm=\"minirest-server\"">>}, - <<"UNAUTHORIZED">>} + <<"Basic Realm=\"minirest-server\"">>}, + #{code => <<"ERROR_USERNAME_OR_PWD">>, + message => <<"Check your username and password">>}} end. - -format(Port) when is_integer(Port) -> - io_lib:format("0.0.0.0:~w", [Port]); -format({Addr, Port}) when is_list(Addr) -> - io_lib:format("~s:~w", [Addr, Port]); -format({Addr, Port}) when is_tuple(Addr) -> - io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index fdec41b2b..b477bd779 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -18,21 +18,14 @@ -module(emqx_dashboard_admin). --behaviour(gen_server). - -include("emqx_dashboard.hrl"). --rlog_shard({?DASHBOARD_SHARD, mqtt_admin}). - -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). %% Mnesia bootstrap -export([mnesia/1]). -%% API Function Exports --export([start_link/0]). - %% mqtt_admin api -export([ add_user/3 , force_add_user/3 @@ -45,15 +38,13 @@ , check/2 ]). -%% gen_server Function Exports --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 +-export([ sign_token/2 + , verify_token/1 + , destroy_token_by_username/2 ]). +-export([add_default_user/0]). + %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -61,6 +52,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(mqtt_admin, [ {type, set}, + {rlog_shard, ?DASHBOARD_SHARD}, {disc_copies, [node()]}, {record_name, mqtt_admin}, {attributes, record_info(fields, mqtt_admin)}, @@ -73,10 +65,6 @@ mnesia(copy) -> %% API %%-------------------------------------------------------------------- --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -spec(add_user(binary(), binary(), binary()) -> ok | {error, any()}). add_user(Username, Password, Tags) when is_binary(Username), is_binary(Password) -> Admin = #mqtt_admin{username = Username, password = hash(Password), tags = Tags}, @@ -170,35 +158,32 @@ check(Username, Password) -> [#mqtt_admin{password = <>}] -> case Hash =:= md5_hash(Salt, Password) of true -> ok; - false -> {error, <<"Password Error">>} + false -> {error, <<"PASSWORD_ERROR">>} end; [] -> - {error, <<"Username Not Found">>} + {error, <<"USERNAME_ERROR">>} end. %%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- +%% token +sign_token(Username, Password) -> + case check(Username, Password) of + ok -> + emqx_dashboard_token:sign(Username, Password); + Error -> + Error + end. -init([]) -> - %% Add default admin user - _ = add_default_user(binenv(default_username), binenv(default_password)), - {ok, state}. +verify_token(Token) -> + emqx_dashboard_token:verify(Token). -handle_call(_Req, _From, State) -> - {reply, error, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info(_Msg, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +destroy_token_by_username(Username, Token) -> + case emqx_dashboard_token:lookup(Token) of + {ok, #mqtt_admin_jwt{username = Username}} -> + emqx_dashboard_token:destroy(Token); + _ -> + {error, not_found} + end. %%-------------------------------------------------------------------- %% Internal functions @@ -216,8 +201,11 @@ salt() -> Salt = rand:uniform(16#ffffffff), <>. +add_default_user() -> + add_default_user(binenv(default_username), binenv(default_password)). + binenv(Key) -> - iolist_to_binary(emqx_config:get([emqx_dashboard, Key], "")). + iolist_to_binary(emqx:get_config([emqx_dashboard, Key], "")). add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY(Password) -> igonre; diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index a56df7ec3..68c737488 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -16,93 +16,104 @@ -module(emqx_dashboard_api). +-ifndef(EMQX_ENTERPRISE). + +-define(RELEASE, community). + +-else. + +-define(VERSION, enterprise). + +-endif. + -behaviour(minirest_api). -include("emqx_dashboard.hrl"). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 - , request_body_schema/1 - , response_array_schema/2 +-import(emqx_mgmt_util, [ schema/1 + , object_schema/1 + , object_schema/2 + , object_array_schema/1 + , bad_request/0 + , properties/1 ]). -export([api_spec/0]). --export([ auth/2 +-export([ login/2 + , logout/2 , users/2 , user/2 , change_pwd/2 ]). +-define(EMPTY(V), (V == undefined orelse V == <<>>)). + +-define(ERROR_USERNAME_OR_PWD, 'ERROR_USERNAME_OR_PWD'). + api_spec() -> - {[auth_api(), users_api(), user_api(), change_pwd_api()], schemas()}. + {[ login_api() + , logout_api() + , users_api() + , user_api() + , change_pwd_api() + ], + []}. -schemas() -> - [#{auth => #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - password => #{ - type => string, - description => <<"password">>} - } - }}, - #{show_user => #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - tag => #{ - type => string, - description => <<"Tag">>} - } - }}, - #{create_user => #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - password => #{ - type => string, - description => <<"Password">>}, - tag => #{ - type => string, - description => <<"Tag">>} - } - }}]. +login_api() -> + AuthProps = properties([{username, string, <<"Username">>}, + {password, string, <<"Password">>}]), -auth_api() -> + TokenProps = properties([{token, string, <<"JWT Token">>}, + {license, object, [{edition, string, <<"License">>, [community, enterprise]}]}, + {version, string}]), Metadata = #{ post => #{ + tags => [dashboard], description => <<"Dashboard Auth">>, - 'requestBody' => request_body_schema(auth), + 'requestBody' => object_schema(AuthProps), responses => #{ <<"200">> => - response_schema(<<"Dashboard Auth successfully">>), - <<"400">> => bad_request() + object_schema(TokenProps, <<"Dashboard Auth successfully">>), + <<"401">> => unauthorized_request() }, security => [] } }, - {"/auth", Metadata, auth}. + {"/login", Metadata, login}. + +logout_api() -> + LogoutProps = properties([{username, string, <<"Username">>}]), + Metadata = #{ + post => #{ + tags => [dashboard], + description => <<"Dashboard Auth">>, + 'requestBody' => object_schema(LogoutProps), + responses => #{ + <<"200">> => schema(<<"Dashboard Auth successfully">>) + } + } + }, + {"/logout", Metadata, logout}. users_api() -> + BaseProps = properties([{username, string, <<"Username">>}, + {password, string, <<"Password">>}, + {tag, string, <<"Tag">>}]), Metadata = #{ get => #{ + tags => [dashboard], description => <<"Get dashboard users">>, responses => #{ - <<"200">> => response_array_schema(<<"">>, show_user) + <<"200">> => object_array_schema(maps:without([password], BaseProps)) } }, post => #{ + tags => [dashboard], description => <<"Create dashboard users">>, - 'requestBody' => request_body_schema(create_user), + 'requestBody' => object_schema(BaseProps), responses => #{ - <<"200">> => response_schema(<<"Create Users successfully">>), + <<"200">> => schema(<<"Create Users successfully">>), <<"400">> => bad_request() } } @@ -112,26 +123,21 @@ users_api() -> user_api() -> Metadata = #{ delete => #{ + tags => [dashboard], description => <<"Delete dashboard users">>, - parameters => [path_param_username()], + parameters => parameters(), responses => #{ - <<"200">> => response_schema(<<"Delete User successfully">>), + <<"200">> => schema(<<"Delete User successfully">>), <<"400">> => bad_request() } }, put => #{ + tags => [dashboard], description => <<"Update dashboard users">>, - parameters => [path_param_username()], - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - tag => #{ - type => string - } - } - }), + parameters => parameters(), + 'requestBody' => object_schema(properties([{tag, string, <<"Tag">>}])), responses => #{ - <<"200">> => response_schema(<<"Update Users successfully">>), + <<"200">> => schema(<<"Update Users successfully">>), <<"400">> => bad_request() } } @@ -141,56 +147,42 @@ user_api() -> change_pwd_api() -> Metadata = #{ put => #{ + tags => [dashboard], description => <<"Update dashboard users password">>, - parameters => [path_param_username()], - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - old_pwd => #{ - type => string - }, - new_pwd => #{ - type => string - } - } - }), + parameters => parameters(), + 'requestBody' => object_schema(properties([old_pwd, new_pwd])), responses => #{ - <<"200">> => response_schema(<<"Update Users password successfully">>), + <<"200">> => schema(<<"Update Users password successfully">>), <<"400">> => bad_request() } } }, {"/users/:username/change_pwd", Metadata, change_pwd}. -path_param_username() -> - #{ - name => username, - in => path, - required => true, - schema => #{type => string}, - example => <<"admin">> - }. - --define(EMPTY(V), (V == undefined orelse V == <<>>)). - -auth(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +login(post, #{body := Params}) -> Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), - case emqx_dashboard_admin:check(Username, Password) of + case emqx_dashboard_admin:sign_token(Username, Password) of + {ok, Token} -> + Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), + {200, #{token => Token, version => Version, license => #{edition => ?RELEASE}}}; + {error, _} -> + {401, #{code => ?ERROR_USERNAME_OR_PWD, message => <<"Auth filed">>}} + end. + +logout(_, #{body := #{<<"username">> := Username}, + headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}}) -> + case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of ok -> - {200}; - {error, Reason} -> - {400, #{code => <<"AUTH_FAIL">>, message => Reason}} + 200; + _R -> + {401, 'BAD_TOKEN_OR_USERNAME', <<"Ensure your token & username">>} end. users(get, _Request) -> {200, [row(User) || User <- emqx_dashboard_admin:all_users()]}; -users(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +users(post, #{body := Params}) -> Tag = maps:get(<<"tag">>, Params), Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), @@ -206,10 +198,7 @@ users(post, Request) -> end end. -user(put, Request) -> - Username = cowboy_req:binding(username, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +user(put, #{bindings := #{username := Username}, body := Params}) -> Tag = maps:get(<<"tag">>, Params), case emqx_dashboard_admin:update_user(Username, Tag) of ok -> {200}; @@ -217,8 +206,7 @@ user(put, Request) -> {400, #{code => <<"UPDATE_FAIL">>, message => Reason}} end; -user(delete, Request) -> - Username = cowboy_req:binding(username, Request), +user(delete, #{bindings := #{username := Username}}) -> case Username == <<"admin">> of true -> {400, #{code => <<"CONNOT_DELETE_ADMIN">>, message => <<"Cannot delete admin">>}}; @@ -227,10 +215,7 @@ user(delete, Request) -> {200} end. -change_pwd(put, Request) -> - Username = cowboy_req:binding(username, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +change_pwd(put, #{bindings := #{username := Username}, body := Params}) -> OldPwd = maps:get(<<"old_pwd">>, Params), NewPwd = maps:get(<<"new_pwd">>, Params), case emqx_dashboard_admin:change_password(Username, OldPwd, NewPwd) of @@ -242,12 +227,19 @@ change_pwd(put, Request) -> row(#mqtt_admin{username = Username, tags = Tag}) -> #{username => Username, tag => Tag}. -bad_request() -> - response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - }). +parameters() -> + [#{ + name => username, + in => path, + required => true, + schema => #{type => string}, + example => <<"admin">> + }]. + +unauthorized_request() -> + object_schema( + properties([{message, string}, + {code, string, <<"Resp Code">>, [?ERROR_USERNAME_OR_PWD]} + ]), + <<"Unauthorized">> + ). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_app.erl b/apps/emqx_dashboard/src/emqx_dashboard_app.erl index 54202d806..4e1b0caec 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_app.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_app.erl @@ -27,8 +27,9 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqx_dashboard_sup:start_link(), ok = ekka_rlog:wait_for_shards([?DASHBOARD_SHARD], infinity), - emqx_dashboard:start_listeners(), + _ = emqx_dashboard:start_listeners(), emqx_dashboard_cli:load(), + ok = emqx_dashboard_admin:add_default_user(), {ok, Sup}. stop(_State) -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_collection.erl b/apps/emqx_dashboard/src/emqx_dashboard_collection.erl index 91d60e1ab..8b0576342 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_collection.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_collection.erl @@ -58,7 +58,7 @@ get_collect() -> gen_server:call(whereis(?MODULE), get_collect). init([]) -> timer(next_interval(), collect), timer(get_today_remaining_seconds(), clear_expire_data), - ExpireInterval = emqx_config:get([emqx_dashboard, monitor, interval], ?EXPIRE_INTERVAL), + ExpireInterval = emqx:get_config([emqx_dashboard, monitor, interval], ?EXPIRE_INTERVAL), State = #{ count => count(), expire_interval => ExpireInterval, @@ -78,7 +78,7 @@ next_interval() -> (1000 * interval()) - (erlang:system_time(millisecond) rem (1000 * interval())) - 1. interval() -> - emqx_config:get([?APP, sample_interval], ?DEFAULT_INTERVAL). + emqx:get_config([?APP, sample_interval], ?DEFAULT_INTERVAL). count() -> 60 div interval(). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index 277b0b1fd..c00310211 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -8,9 +8,15 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [schema/2]). -export([api_spec/0]). --export([counters/2, current_counters/2]). +-export([ monitor/2 + , counters/2 + , monitor_nodes/2 + , monitor_nodes_counters/2 + , current_counters/2 + ]). -define(COUNTERS, [ connection , route @@ -20,7 +26,13 @@ , dropped]). api_spec() -> - {[monitor_api(), monitor_current_api()], [counters_schema()]}. + {[ monitor_api() + , monitor_nodes_api() + , monitor_nodes_counters_api() + , monitor_counters_api() + , monitor_current_api() + ], + []}. monitor_api() -> Metadata = #{ @@ -28,30 +40,74 @@ monitor_api() -> description => <<"List monitor data">>, parameters => [ #{ - name => node, + name => aggregate, in => query, required => false, - schema => #{type => string} - }, - #{ - name => counter, - in => query, - required => false, - schema => #{type => string, enum => ?COUNTERS} + schema => #{type => boolean} } ], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor count data">>, counters)}}}, - {"/monitor", Metadata, counters}. + <<"200">> => schema(counters_schema(), <<"Monitor count data">>)}}}, + {"/monitor", Metadata, monitor}. + +monitor_nodes_api() -> + Metadata = #{ + get => #{ + description => <<"List monitor data">>, + parameters => [path_param_node()], + responses => #{ + <<"200">> => schema(counters_schema(), <<"Monitor count data in node">>)}}}, + {"/monitor/nodes/:node", Metadata, monitor_nodes}. + +monitor_nodes_counters_api() -> + Metadata = #{ + get => #{ + description => <<"List monitor data">>, + parameters => [ + path_param_node(), + path_param_counter() + ], + responses => #{ + <<"200">> => schema(counter_schema(), <<"Monitor single count data in node">>)}}}, + {"/monitor/nodes/:node/counters/:counter", Metadata, monitor_nodes_counters}. + +monitor_counters_api() -> + Metadata = #{ + get => #{ + description => <<"List monitor data">>, + parameters => [ + path_param_counter() + ], + responses => #{ + <<"200">> => + schema(counter_schema(), <<"Monitor single count data">>)}}}, + {"/monitor/counters/:counter", Metadata, counters}. monitor_current_api() -> Metadata = #{ get => #{ description => <<"Current monitor data">>, responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Current monitor data">>, - current_counters_schema())}}}, + <<"200">> => schema(current_counters_schema(), <<"Current monitor data">>)}}}, {"/monitor/current", Metadata, current_counters}. +path_param_node() -> + #{ + name => node, + in => path, + required => true, + schema => #{type => string}, + example => node() + }. + +path_param_counter() -> + #{ + name => counter, + in => path, + required => true, + schema => #{type => string, enum => ?COUNTERS}, + example => hd(?COUNTERS) + }. + current_counters_schema() -> #{ type => object, @@ -69,32 +125,39 @@ counters_schema() -> end, Properties = lists:foldl(Fun, #{}, ?COUNTERS), #{ - counters => #{ - type => object, - properties => Properties} + type => object, + properties => Properties }. counters_schema(Name) -> - #{Name => #{ + #{Name => counter_schema()}. +counter_schema() -> + #{ type => array, items => #{ type => object, properties => #{ timestamp => #{ - type => integer}, + type => integer, + description => <<"Millisecond">>}, count => #{ - type => integer}}}}}. + type => integer}}}}. %%%============================================================================================== %% parameters trans -counters(get, Request) -> - case cowboy_req:parse_qs(Request) of - [] -> - {200, get_collect()}; - Params -> - lookup(Params) - end. +monitor(get, #{query_string := Qs}) -> + Aggregate = maps:get(<<"aggregate">>, Qs, <<"false">>), + {200, list_collect(Aggregate)}. -current_counters(get, _) -> +monitor_nodes(get, #{bindings := #{node := Node}}) -> + lookup([{<<"node">>, Node}]). + +monitor_nodes_counters(get, #{bindings := #{node := Node, counter := Counter}}) -> + lookup([{<<"node">>, Node}, {<<"counter">>, Counter}]). + +counters(get, #{bindings := #{counter := Counter}}) -> + lookup([{<<"counter">>, Counter}]). + +current_counters(get, _Params) -> Data = [get_collect(Node) || Node <- ekka_mnesia:running_nodes()], Nodes = length(ekka_mnesia:running_nodes()), {Received, Sent, Sub, Conn} = format_current_metrics(Data), @@ -107,7 +170,15 @@ current_counters(get, _) -> }, {200, Response}. - %%%============================================================================================== +format_current_metrics(Collects) -> + format_current_metrics(Collects, {0,0,0,0}). +format_current_metrics([], Acc) -> + Acc; +format_current_metrics([{Received, Sent, Sub, Conn} | Collects], {Received1, Sent1, Sub1, Conn1}) -> + format_current_metrics(Collects, {Received1 + Received, Sent1 + Sent, Sub1 + Sub, Conn1 + Conn}). + + +%%%============================================================================================== %% api apply lookup(Params) -> @@ -118,23 +189,23 @@ lookup(Params) -> lookup_(lists:foldl(Fun, #{}, Params)). lookup_(#{node := Node, counter := Counter}) -> - {200, sampling(Node, Counter)}; + Data = hd(maps:values(sampling(Node, Counter))), + {200, Data}; lookup_(#{node := Node}) -> {200, sampling(Node)}; lookup_(#{counter := Counter}) -> - Data = [sampling(Node, Counter) || Node <- ekka_mnesia:running_nodes()], + CounterData = merger_counters([sampling(Node, Counter) || Node <- ekka_mnesia:running_nodes()]), + Data = hd(maps:values(CounterData)), {200, Data}. -format_current_metrics(Collects) -> - format_current_metrics(Collects, {0,0,0,0}). -format_current_metrics([], Acc) -> - Acc; -format_current_metrics([{Received, Sent, Sub, Conn} | Collects], {Received1, Sent1, Sub1, Conn1}) -> - format_current_metrics(Collects, {Received1 + Received, Sent1 + Sent, Sub1 + Sub, Conn1 + Conn}). - -get_collect() -> - Counters = [sampling(Node) || Node <- ekka_mnesia:running_nodes()], - merger_counters(Counters). +list_collect(Aggregate) -> + case Aggregate of + <<"true">> -> + [maps:put(node, Node, sampling(Node)) || Node <- ekka_mnesia:running_nodes()]; + _ -> + Counters = [sampling(Node) || Node <- ekka_mnesia:running_nodes()], + merger_counters(Counters) + end. get_collect(Node) when Node =:= node() -> emqx_dashboard_collection:get_collect(); @@ -223,7 +294,7 @@ format([#mqtt_collect{timestamp = Ts, collect = {C, R, S, Re, S1, D}} | Collects [[Ts, S1] | Sent], [[Ts, D] | Dropped]}). add_key(Collects) -> - lists:reverse([#{timestamp => Ts, count => C} || [Ts, C] <- Collects]). + lists:reverse([#{timestamp => Ts * 1000, count => C} || [Ts, C] <- Collects]). format_single(Collects, Counter) -> #{Counter => format_single(Collects, counter_index(Counter), [])}. @@ -231,7 +302,7 @@ format_single([], _Index, Acc) -> lists:reverse(Acc); format_single([#mqtt_collect{timestamp = Ts, collect = Collect} | Collects], Index, Acc) -> format_single(Collects, Index, - [#{timestamp => Ts, count => erlang:element(Index, Collect)} | Acc]). + [#{timestamp => Ts * 1000, count => erlang:element(Index, Collect)} | Acc]). counter_index(connection) -> 1; counter_index(route) -> 2; diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 2dae5e7e4..3ba3dc803 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -17,33 +17,33 @@ -include_lib("typerefl/include/types.hrl"). --export([ structs/0 +-export([ roots/0 , fields/1]). -structs() -> ["emqx_dashboard"]. +roots() -> ["emqx_dashboard"]. fields("emqx_dashboard") -> [ {listeners, hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "http"), hoconsc:ref(?MODULE, "https")]))} , {default_username, fun default_username/1} , {default_password, fun default_password/1} - , {sample_interval, emqx_schema:t(emqx_schema:duration_s(), undefined, "10s")} + , {sample_interval, sc(emqx_schema:duration_s(), #{default => "10s"})} + , {token_expired_time, sc(emqx_schema:duration(), #{default => "30m"})} ]; fields("http") -> [ {"protocol", hoconsc:enum([http, https])} - , {"port", emqx_schema:t(integer(), undefined, 8081)} - , {"num_acceptors", emqx_schema:t(integer(), undefined, 4)} - , {"max_connections", emqx_schema:t(integer(), undefined, 512)} - , {"backlog", emqx_schema:t(integer(), undefined, 1024)} - , {"send_timeout", emqx_schema:t(emqx_schema:duration(), undefined, "15s")} - , {"send_timeout_close", emqx_schema:t(boolean(), undefined, true)} - , {"inet6", emqx_schema:t(boolean(), undefined, false)} - , {"ipv6_v6only", emqx_schema:t(boolean(), undefined, false)} + , {"port", hoconsc:mk(integer(), #{default => 18083})} + , {"num_acceptors", sc(integer(), #{default => 4})} + , {"max_connections", sc(integer(), #{default => 512})} + , {"backlog", sc(integer(), #{default => 1024})} + , {"send_timeout", sc(emqx_schema:duration(), #{default => "5s"})} + , {"inet6", sc(boolean(), #{default => false})} + , {"ipv6_v6only", sc(boolean(), #{dfeault => false})} ]; fields("https") -> - emqx_schema:ssl(#{enable => true}) ++ fields("http"). + proplists:delete("fail_if_no_peer_cert", emqx_schema:ssl(#{})) ++ fields("http"). default_username(type) -> string(); default_username(default) -> "admin"; @@ -54,3 +54,5 @@ default_password(type) -> string(); default_password(default) -> "public"; default_password(nullable) -> false; default_password(_) -> undefined. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_sup.erl b/apps/emqx_dashboard/src/emqx_dashboard_sup.erl index 8ec161f11..90e84fcef 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_sup.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_sup.erl @@ -29,4 +29,4 @@ start_link() -> init([]) -> {ok, {{one_for_all, 10, 100}, - [?CHILD(emqx_dashboard_admin), ?CHILD(emqx_dashboard_collection)]}}. + [?CHILD(emqx_dashboard_token), ?CHILD(emqx_dashboard_collection)]}}. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl new file mode 100644 index 000000000..2acf00f13 --- /dev/null +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -0,0 +1,203 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_token). + +-include("emqx_dashboard.hrl"). + +-define(TAB, mqtt_admin_jwt). + +-export([ sign/2 + , verify/1 + , lookup/1 + , destroy/1 + , destroy_by_username/1 + ]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-export([mnesia/1]). + +-define(EXPTIME, 60 * 60 * 1000). + +-define(CLEAN_JWT_INTERVAL, 60 * 60 * 1000). + +%%-------------------------------------------------------------------- +%% gen server part +-behaviour(gen_server). + +-export([start_link/0]). + +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +%%-------------------------------------------------------------------- +%% jwt function +-spec(sign(Username :: binary(), Password :: binary()) -> + {ok, Token :: binary()} | {error, Reason :: term()}). +sign(Username, Password) -> + do_sign(Username, Password). + +-spec(verify(Token :: binary()) -> Result :: ok | {error, token_timeout | not_found}). +verify(Token) -> + do_verify(Token). + +-spec(destroy(KeyOrKeys :: list() | binary() | #mqtt_admin_jwt{}) -> ok). +destroy([]) -> + ok; +destroy(JWTorTokenList) when is_list(JWTorTokenList)-> + [destroy(JWTorToken) || JWTorToken <- JWTorTokenList], + ok; +destroy(#mqtt_admin_jwt{token = Token}) -> + destroy(Token); +destroy(Token) when is_binary(Token)-> + do_destroy(Token). + +-spec(destroy_by_username(Username :: binary()) -> ok). +destroy_by_username(Username) -> + do_destroy_by_username(Username). + +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TAB, [ + {type, set}, + {rlog_shard, ?DASHBOARD_SHARD}, + {disc_copies, [node()]}, + {record_name, mqtt_admin_jwt}, + {attributes, record_info(fields, mqtt_admin_jwt)}, + {storage_properties, [{ets, [{read_concurrency, true}, + {write_concurrency, true}]}]}]); +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TAB, disc_copies). + +%%-------------------------------------------------------------------- +%% jwt apply +do_sign(Username, Password) -> + ExpTime = jwt_expiration_time(), + Salt = salt(), + JWK = jwk(Username, Password, Salt), + JWS = #{ + <<"alg">> => <<"HS256">> + }, + JWT = #{ + <<"iss">> => <<"EMQ X">>, + <<"exp">> => ExpTime + }, + Signed = jose_jwt:sign(JWK, JWS, JWT), + {_, Token} = jose_jws:compact(Signed), + ok = ekka_mnesia:dirty_write(format(Token, Username, ExpTime)), + {ok, Token}. + +do_verify(Token)-> + case lookup(Token) of + {ok, JWT = #mqtt_admin_jwt{exptime = ExpTime}} -> + case ExpTime > erlang:system_time(millisecond) of + true -> + ekka_mnesia:dirty_write(JWT#mqtt_admin_jwt{exptime = jwt_expiration_time()}), + ok; + _ -> + {error, token_timeout} + end; + Error -> + Error + end. + +do_destroy(Token) -> + Fun = fun mnesia:delete/1, + {atomic, ok} = ekka_mnesia:transaction(?DASHBOARD_SHARD, Fun, [{?TAB, Token}]), + ok. + +do_destroy_by_username(Username) -> + gen_server:cast(?MODULE, {destroy, Username}). + +%%-------------------------------------------------------------------- +%% jwt internal util function +-spec(lookup(Token :: binary()) -> {ok, #mqtt_admin_jwt{}} | {error, not_found}). +lookup(Token) -> + case mnesia:dirty_read(?TAB, Token) of + [JWT] -> {ok, JWT}; + [] -> {error, not_found} + end. + +lookup_by_username(Username) -> + Spec = [{{mqtt_admin_jwt, '_', Username, '_'}, [], ['$_']}], + mnesia:dirty_select(?TAB, Spec). + +jwk(Username, Password, Salt) -> + Key = erlang:md5(<>), + #{ + <<"kty">> => <<"oct">>, + <<"k">> => jose_base64url:encode(Key) + }. + +jwt_expiration_time() -> + ExpTime = emqx:get_config([emqx_dashboard, token_expired_time], ?EXPTIME), + erlang:system_time(millisecond) + ExpTime. + +salt() -> + _ = emqx_misc:rand_seed(), + Salt = rand:uniform(16#ffffffff), + <>. + +format(Token, Username, ExpTime) -> + #mqtt_admin_jwt{ + token = Token, + username = Username, + exptime = ExpTime + }. + +%%-------------------------------------------------------------------- +%% gen server +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + timer_clean(self()), + {ok, state}. + +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast({destroy, Username}, State) -> + Tokens = lookup_by_username(Username), + destroy(Tokens), + {noreply, State}; +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info(clean_jwt, State) -> + timer_clean(self()), + Now = erlang:system_time(millisecond), + Spec = [{{mqtt_admin_jwt, '_', '_', '$1'}, [{'<', '$1', Now}], ['$_']}], + JWTList = mnesia:dirty_select(?TAB, Spec), + destroy(JWTList), + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +timer_clean(Pid) -> + erlang:send_after(?CLEAN_JWT_INTERVAL, Pid, clean_jwt). diff --git a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf b/apps/emqx_data_bridge/etc/emqx_data_bridge.conf deleted file mode 100644 index c299b97a1..000000000 --- a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf +++ /dev/null @@ -1,129 +0,0 @@ -##-------------------------------------------------------------------- -## EMQ X Bridge Plugin -##-------------------------------------------------------------------- - -emqx_data_bridge:{ - bridges:[ - # {name: "mysql_bridge_1" - # type: mysql - # config: { - # server: "192.168.0.172:3306" - # database: mqtt - # pool_size: 1 - # username: root - # password: public - # auto_reconnect: true - # ssl: false - # } - # } - # , {name: "pgsql_bridge_1" - # type: pgsql - # config: { - # server: "192.168.0.172:5432" - # database: mqtt - # pool_size: 1 - # username: root - # password: public - # auto_reconnect: true - # ssl: false - # } - # } - # , {name: "mongodb_bridge_single" - # type: mongo - # config: { - # servers: "192.168.0.172:27017" - # mongo_type: single - # pool_size: 1 - # login: root - # password: public - # auth_source: mqtt - # database: mqtt - # ssl: false - # } - # } - # ,{name: "mongodb_bridge_rs" - # type: mongo - # config: { - # servers: "127.0.0.1:27017" - # mongo_type: rs - # rs_set_name: rs_name - # pool_size: 1 - # login: root - # password: public - # auth_source: mqtt - # database: mqtt - # ssl: false - # } - # } - # ,{name: "mongodb_bridge_shared" - # type: mongo - # config: { - # servers: "127.0.0.1:27017" - # mongo_type: shared - # pool_size: 1 - # login: root - # password: public - # auth_source: mqtt - # database: mqtt - # ssl: false - # max_overflow: 1 - # overflow_ttl: - # overflow_check_period: 10s - # local_threshold_ms: 10s - # connect_timeout_ms: 10s - # socket_timeout_ms: 10s - # server_selection_timeout_ms: 10s - # wait_queue_timeout_ms: 10s - # heartbeat_frequency_ms: 10s - # min_heartbeat_frequency_ms: 10s - # } - # } - # , {name: "redis_bridge_single" - # type: redis - # config: { - # servers: "192.168.0.172:6379" - # redis_type: single - # pool_size: 1 - # database: 0 - # password: public - # auto_reconnect: true - # ssl: false - # } - # } - # ,{name: "redis_bridge_sentinel" - # type: redis - # config: { - # servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" - # redis_type: sentinel - # sentinel_name: mymaster - # pool_size: 1 - # database: 0 - # ssl: false - # } - # } - # ,{name: "redis_bridge_cluster" - # type: redis - # config: { - # servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" - # redis_type: cluster - # pool_size: 1 - # database: 0 - # password: "public" - # ssl: false - # } - # } - # , {name: "ldap_bridge_1" - # type: ldap - # config: { - # servers: "192.168.0.172" - # port: 389 - # bind_dn: "cn=root,dc=emqx,dc=io" - # bind_password: "public" - # timeout: 30s - # pool_size: 1 - # ssl: false - # } - # } - - ] -} diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl index 066d72096..e3c6d8ee9 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl @@ -1,6 +1,6 @@ -module(emqx_data_bridge_schema). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). %%====================================================================================== %% Hocon Schema Definitions @@ -8,7 +8,7 @@ -define(TYPES, [mysql, pgsql, mongo, redis, ldap]). -define(BRIDGES, [hoconsc:ref(?MODULE, T) || T <- ?TYPES]). -structs() -> ["emqx_data_bridge"]. +roots() -> ["emqx_data_bridge"]. fields("emqx_data_bridge") -> [{bridges, #{type => hoconsc:array(hoconsc:union(?BRIDGES)), @@ -22,5 +22,5 @@ fields(ldap) -> connector_fields(ldap). connector_fields(DB) -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - [{name, hoconsc:t(typerefl:binary())}, - {type, #{type => DB}}] ++ Mod:fields(""). + [{name, hoconsc:mk(typerefl:binary())}, + {type, #{type => DB}}] ++ Mod:roots(). diff --git a/apps/emqx_exhook/README.md b/apps/emqx_exhook/README.md new file mode 100644 index 000000000..216c39275 --- /dev/null +++ b/apps/emqx_exhook/README.md @@ -0,0 +1,39 @@ +# emqx_exhook + +The `emqx_exhook` extremly enhance the extensibility for EMQ X. It allow using an others programming language to mount the hooks intead of erlang. + +## Feature + +- [x] Based on gRPC, it brings a very wide range of applicability +- [x] Allows you to use the return value to extend emqx behavior. + +## Architecture + +``` +EMQ X Third-party Runtime ++========================+ +========+==========+ +| ExHook | | | | +| +----------------+ | gRPC | gRPC | User's | +| | gPRC Client | ------------------> | Server | Codes | +| +----------------+ | (HTTP/2) | | | +| | | | | ++========================+ +========+==========+ +``` + +## Usage + +### gRPC service + +See: `priv/protos/exhook.proto` + +### CLI + +## Example + +## Recommended gRPC Framework + +See: https://github.com/grpc-ecosystem/awesome-grpc + +## Thanks + +- [grpcbox](https://github.com/tsloughter/grpcbox) diff --git a/apps/emqx_exhook/docs/design-cn.md b/apps/emqx_exhook/docs/design-cn.md new file mode 100644 index 000000000..423a53bf5 --- /dev/null +++ b/apps/emqx_exhook/docs/design-cn.md @@ -0,0 +1,112 @@ +# 设计 + +## 动机 + +在 EMQ X Broker v4.1-v4.2 中,我们发布了 2 个插件来扩展 emqx 的编程能力: + +1. `emqx-extension-hook` 提供了使用 Java, Python 向 Broker 挂载钩子的功能 +2. `emqx-exproto` 提供了使用 Java,Python 编写用户自定义协议接入插件的功能 + +但在后续的支持中发现许多难以处理的问题: + +1. 有大量的编程语言需要支持,需要编写和维护如 Go, JavaScript, Lua.. 等语言的驱动。 +2. `erlport` 使用的操作系统的管道进行通信,这让用户代码只能部署在和 emqx 同一个操作系统上。部署方式受到了极大的限制。 +3. 用户程序的启动参数直接打包到 Broker 中,导致用户开发无法实时的进行调试,单步跟踪等。 +4. `erlport` 会占用 `stdin` `stdout`。 + +因此,我们计划重构这部分的实现,其中主要的内容是: +1. 使用 `gRPC` 替换 `erlport`。 +2. 将 `emqx-extension-hook` 重命名为 `emqx-exhook` + + +旧版本的设计:[emqx-extension-hook design in v4.2.0](https://github.com/emqx/emqx-exhook/blob/v4.2.0/docs/design.md) + +## 设计 + +架构如下: + +``` + EMQ X ++========================+ +========+==========+ +| ExHook | | | | +| +----------------+ | gRPC | gRPC | User's | +| | gRPC Client | ------------------> | Server | Codes | +| +----------------+ | (HTTP/2) | | | +| | | | | ++========================+ +========+==========+ +``` + +`emqx-exhook` 通过 gRPC 的方式向用户部署的 gRPC 服务发送钩子的请求,并处理其返回的值。 + + +和 emqx 原生的钩子一致,emqx-exhook 也按照链式的方式执行: + + + +### gRPC 服务示例 + +用户需要实现的方法,和数据类型的定义在 `priv/protos/exhook.proto` 文件中: + +```protobuff +syntax = "proto3"; + +package emqx.exhook.v1; + +service HookProvider { + + rpc OnProviderLoaded(ProviderLoadedRequest) returns (LoadedResponse) {}; + + rpc OnProviderUnloaded(ProviderUnloadedRequest) returns (EmptySuccess) {}; + + rpc OnClientConnect(ClientConnectRequest) returns (EmptySuccess) {}; + + rpc OnClientConnack(ClientConnackRequest) returns (EmptySuccess) {}; + + rpc OnClientConnected(ClientConnectedRequest) returns (EmptySuccess) {}; + + rpc OnClientDisconnected(ClientDisconnectedRequest) returns (EmptySuccess) {}; + + rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {}; + + rpc OnClientAuthorize(ClientAuthorizeRequest) returns (ValuedResponse) {}; + + rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {}; + + rpc OnClientUnsubscribe(ClientUnsubscribeRequest) returns (EmptySuccess) {}; + + rpc OnSessionCreated(SessionCreatedRequest) returns (EmptySuccess) {}; + + rpc OnSessionSubscribed(SessionSubscribedRequest) returns (EmptySuccess) {}; + + rpc OnSessionUnsubscribed(SessionUnsubscribedRequest) returns (EmptySuccess) {}; + + rpc OnSessionResumed(SessionResumedRequest) returns (EmptySuccess) {}; + + rpc OnSessionDiscarded(SessionDiscardedRequest) returns (EmptySuccess) {}; + + rpc OnSessionTakeovered(SessionTakeoveredRequest) returns (EmptySuccess) {}; + + rpc OnSessionTerminated(SessionTerminatedRequest) returns (EmptySuccess) {}; + + rpc OnMessagePublish(MessagePublishRequest) returns (ValuedResponse) {}; + + rpc OnMessageDelivered(MessageDeliveredRequest) returns (EmptySuccess) {}; + + rpc OnMessageDropped(MessageDroppedRequest) returns (EmptySuccess) {}; + + rpc OnMessageAcked(MessageAckedRequest) returns (EmptySuccess) {}; +} +``` + +### 配置文件示例 + +``` +exhook: { + ## 配置 gRPC 服务地址 (HTTP) + ## + ## default 为服务器的名称 + server.default: { + url: "http://127.0.0.1:9000" + } +} +``` diff --git a/apps/emqx_exhook/etc/emqx_exhook.conf b/apps/emqx_exhook/etc/emqx_exhook.conf new file mode 100644 index 000000000..8f3e25686 --- /dev/null +++ b/apps/emqx_exhook/etc/emqx_exhook.conf @@ -0,0 +1,38 @@ +##==================================================================== +## EMQ X Hooks +##==================================================================== + +exhook { + ## The default value or action will be returned, while the request to + ## the gRPC server failed or no available grpc server running. + ## + ## Default: deny + ## Value: ignore | deny + request_failed_action = deny + + ## The timeout to request grpc server + ## + ## Default: 5s + ## Value: Duration + request_timeout = 5s + + ## Whether to automatically reconnect (initialize) the gRPC server + ## + ## When gRPC is not available, exhook tries to request the gRPC service at + ## that interval and reinitialize the list of mounted hooks. + ## + ## Default: false + ## Value: false | Duration + auto_reconnect = 60s + + servers = [ + # { name: "default" + # url: "http://127.0.0.1:9000" + # #ssl: { + # # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + # # certfile: "{{ platform_etc_dir }}/certs/cert.pem" + # # keyfile: "{{ platform_etc_dir }}/certs/key.pem" + # #} + # } + ] +} diff --git a/apps/emqx_exhook/include/emqx_exhook.hrl b/apps/emqx_exhook/include/emqx_exhook.hrl new file mode 100644 index 000000000..64131735e --- /dev/null +++ b/apps/emqx_exhook/include/emqx_exhook.hrl @@ -0,0 +1,44 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-ifndef(EMQX_EXHOOK_HRL). +-define(EMQX_EXHOOK_HRL, true). + +-define(APP, emqx_exhook). + +-define(ENABLED_HOOKS, + [ {'client.connect', {emqx_exhook_handler, on_client_connect, []}} + , {'client.connack', {emqx_exhook_handler, on_client_connack, []}} + , {'client.connected', {emqx_exhook_handler, on_client_connected, []}} + , {'client.disconnected', {emqx_exhook_handler, on_client_disconnected, []}} + , {'client.authenticate', {emqx_exhook_handler, on_client_authenticate, []}} + , {'client.authorize', {emqx_exhook_handler, on_client_authorize, []}} + , {'client.subscribe', {emqx_exhook_handler, on_client_subscribe, []}} + , {'client.unsubscribe', {emqx_exhook_handler, on_client_unsubscribe, []}} + , {'session.created', {emqx_exhook_handler, on_session_created, []}} + , {'session.subscribed', {emqx_exhook_handler, on_session_subscribed, []}} + , {'session.unsubscribed',{emqx_exhook_handler, on_session_unsubscribed, []}} + , {'session.resumed', {emqx_exhook_handler, on_session_resumed, []}} + , {'session.discarded', {emqx_exhook_handler, on_session_discarded, []}} + , {'session.takeovered', {emqx_exhook_handler, on_session_takeovered, []}} + , {'session.terminated', {emqx_exhook_handler, on_session_terminated, []}} + , {'message.publish', {emqx_exhook_handler, on_message_publish, []}} + , {'message.delivered', {emqx_exhook_handler, on_message_delivered, []}} + , {'message.acked', {emqx_exhook_handler, on_message_acked, []}} + , {'message.dropped', {emqx_exhook_handler, on_message_dropped, []}} + ]). + +-endif. diff --git a/apps/emqx_exhook/mix.exs b/apps/emqx_exhook/mix.exs new file mode 100644 index 000000000..af7c10ea4 --- /dev/null +++ b/apps/emqx_exhook/mix.exs @@ -0,0 +1,33 @@ +defmodule EMQXExhook.MixProject do + use Mix.Project + + def project do + [ + app: :emqx_exhook, + version: "5.0.0", + build_path: "../../_build", + config_path: "../../config/config.exs", + deps_path: "../../deps", + lockfile: "../../mix.lock", + elixir: "~> 1.12", + start_permanent: Mix.env() == :prod, + deps: deps(), + description: "EMQ X Extension for Hook" + ] + end + + def application do + [ + registered: [], + mod: {:emqx_exhook_app, []}, + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:emqx, in_umbrella: true, runtime: false}, + {:grpc, github: "emqx/grpc-erl", tag: "0.6.2"} + ] + end +end diff --git a/apps/emqx_exhook/priv/protos/exhook.proto b/apps/emqx_exhook/priv/protos/exhook.proto new file mode 100644 index 000000000..5e931054c --- /dev/null +++ b/apps/emqx_exhook/priv/protos/exhook.proto @@ -0,0 +1,407 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//------------------------------------------------------------------------------ + +syntax = "proto3"; + +option csharp_namespace = "Emqx.Exhook.V1"; +option go_package = "emqx.io/grpc/exhook"; +option java_multiple_files = true; +option java_package = "io.emqx.exhook"; +option java_outer_classname = "EmqxExHookProto"; + +package emqx.exhook.v1; + +service HookProvider { + + rpc OnProviderLoaded(ProviderLoadedRequest) returns (LoadedResponse) {}; + + rpc OnProviderUnloaded(ProviderUnloadedRequest) returns (EmptySuccess) {}; + + rpc OnClientConnect(ClientConnectRequest) returns (EmptySuccess) {}; + + rpc OnClientConnack(ClientConnackRequest) returns (EmptySuccess) {}; + + rpc OnClientConnected(ClientConnectedRequest) returns (EmptySuccess) {}; + + rpc OnClientDisconnected(ClientDisconnectedRequest) returns (EmptySuccess) {}; + + rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {}; + + rpc OnClientAuthorize(ClientAuthorizeRequest) returns (ValuedResponse) {}; + + rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {}; + + rpc OnClientUnsubscribe(ClientUnsubscribeRequest) returns (EmptySuccess) {}; + + rpc OnSessionCreated(SessionCreatedRequest) returns (EmptySuccess) {}; + + rpc OnSessionSubscribed(SessionSubscribedRequest) returns (EmptySuccess) {}; + + rpc OnSessionUnsubscribed(SessionUnsubscribedRequest) returns (EmptySuccess) {}; + + rpc OnSessionResumed(SessionResumedRequest) returns (EmptySuccess) {}; + + rpc OnSessionDiscarded(SessionDiscardedRequest) returns (EmptySuccess) {}; + + rpc OnSessionTakeovered(SessionTakeoveredRequest) returns (EmptySuccess) {}; + + rpc OnSessionTerminated(SessionTerminatedRequest) returns (EmptySuccess) {}; + + rpc OnMessagePublish(MessagePublishRequest) returns (ValuedResponse) {}; + + rpc OnMessageDelivered(MessageDeliveredRequest) returns (EmptySuccess) {}; + + rpc OnMessageDropped(MessageDroppedRequest) returns (EmptySuccess) {}; + + rpc OnMessageAcked(MessageAckedRequest) returns (EmptySuccess) {}; +} + +//------------------------------------------------------------------------------ +// Request & Response +//------------------------------------------------------------------------------ + +message ProviderLoadedRequest { + + BrokerInfo broker = 1; +} + +message LoadedResponse { + + repeated HookSpec hooks = 1; +} + +message ProviderUnloadedRequest { } + +message ClientConnectRequest { + + ConnInfo conninfo = 1; + + // MQTT CONNECT packet's properties (MQTT v5.0) + // + // It should be empty on MQTT v3.1.1/v3.1 or others protocol + repeated Property props = 2; +} + +message ClientConnackRequest { + + ConnInfo conninfo = 1; + + string result_code = 2; + + repeated Property props = 3; +} + +message ClientConnectedRequest { + + ClientInfo clientinfo = 1; +} + +message ClientDisconnectedRequest { + + ClientInfo clientinfo = 1; + + string reason = 2; +} + +message ClientAuthenticateRequest { + + ClientInfo clientinfo = 1; + + bool result = 2; +} + +message ClientAuthorizeRequest { + + ClientInfo clientinfo = 1; + + enum AuthorizeReqType { + + PUBLISH = 0; + + SUBSCRIBE = 1; + } + + AuthorizeReqType type = 2; + + string topic = 3; + + bool result = 4; +} + +message ClientSubscribeRequest { + + ClientInfo clientinfo = 1; + + repeated Property props = 2; + + repeated TopicFilter topic_filters = 3; +} + +message ClientUnsubscribeRequest { + + ClientInfo clientinfo = 1; + + repeated Property props = 2; + + repeated TopicFilter topic_filters = 3; +} + +message SessionCreatedRequest { + + ClientInfo clientinfo = 1; +} + +message SessionSubscribedRequest { + + ClientInfo clientinfo = 1; + + string topic = 2; + + SubOpts subopts = 3; +} + +message SessionUnsubscribedRequest { + + ClientInfo clientinfo = 1; + + string topic = 2; +} + +message SessionResumedRequest { + + ClientInfo clientinfo = 1; +} + +message SessionDiscardedRequest { + + ClientInfo clientinfo = 1; +} + +message SessionTakeoveredRequest { + + ClientInfo clientinfo = 1; +} + +message SessionTerminatedRequest { + + ClientInfo clientinfo = 1; + + string reason = 2; +} + +message MessagePublishRequest { + + Message message = 1; +} + +message MessageDeliveredRequest { + + ClientInfo clientinfo = 1; + + Message message = 2; +} + +message MessageDroppedRequest { + + Message message = 1; + + string reason = 2; +} + +message MessageAckedRequest { + + ClientInfo clientinfo = 1; + + Message message = 2; +} + +//------------------------------------------------------------------------------ +// Basic data types +//------------------------------------------------------------------------------ + +message EmptySuccess { } + +message ValuedResponse { + + // The responsed value type + // - 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 { + + CONTINUE = 0; + + IGNORE = 1; + + STOP_AND_RETURN = 2; + } + + ResponsedType type = 1; + + oneof value { + + // Boolean result, used on the 'client.authenticate', 'client.authorize' hooks + bool bool_result = 3; + + // Message result, used on the 'message.*' hooks + Message message = 4; + } +} + +message BrokerInfo { + + string version = 1; + + string sysdescr = 2; + + int64 uptime = 3; + + string datetime = 4; +} + +message HookSpec { + + // The registered hooks name + // + // Available value: + // "client.connect", "client.connack" + // "client.connected", "client.disconnected" + // "client.authenticate", "client.authorize" + // "client.subscribe", "client.unsubscribe" + // + // "session.created", "session.subscribed" + // "session.unsubscribed", "session.resumed" + // "session.discarded", "session.takeovered" + // "session.terminated" + // + // "message.publish", "message.delivered" + // "message.acked", "message.dropped" + string name = 1; + + // The topic filters for message hooks + repeated string topics = 2; +} + +message ConnInfo { + + string node = 1; + + string clientid = 2; + + string username = 3; + + string peerhost = 4; + + uint32 sockport = 5; + + string proto_name = 6; + + string proto_ver = 7; + + uint32 keepalive = 8; +} + +message ClientInfo { + + string node = 1; + + string clientid = 2; + + string username = 3; + + string password = 4; + + string peerhost = 5; + + uint32 sockport = 6; + + string protocol = 7; + + string mountpoint = 8; + + bool is_superuser = 9; + + bool anonymous = 10; + + // common name of client TLS cert + string cn = 11; + + // subject of client TLS cert + string dn = 12; +} + +message Message { + + string node = 1; + + string id = 2; + + uint32 qos = 3; + + string from = 4; + + string topic = 5; + + bytes payload = 6; + + uint64 timestamp = 7; +} + +message Property { + + string name = 1; + + string value = 2; +} + +message TopicFilter { + + string name = 1; + + uint32 qos = 2; +} + +message SubOpts { + + // The QoS level + uint32 qos = 1; + + // The group name for shared subscription + string share = 2; + + // The Retain Handling option (MQTT v5.0) + // + // 0 = Send retained messages at the time of the subscribe + // 1 = Send retained messages at subscribe only if the subscription does + // not currently exist + // 2 = Do not send retained messages at the time of the subscribe + uint32 rh = 3; + + // The Retain as Published option (MQTT v5.0) + // + // If 1, Application Messages forwarded using this subscription keep the + // RETAIN flag they were published with. + // If 0, Application Messages forwarded using this subscription have the + // RETAIN flag set to 0. + // Retained messages sent when the subscription is established have the RETAIN flag set to 1. + uint32 rap = 4; + + // The No Local option (MQTT v5.0) + // + // If the value is 1, Application Messages MUST NOT be forwarded to a + // connection with a ClientID equal to the ClientID of the publishing + uint32 nl = 5; +} diff --git a/apps/emqx_exhook/rebar.config b/apps/emqx_exhook/rebar.config new file mode 100644 index 000000000..89dcb20a7 --- /dev/null +++ b/apps/emqx_exhook/rebar.config @@ -0,0 +1,41 @@ +%%-*- mode: erlang -*- +{plugins, + [rebar3_proper, + {grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}} +]}. + +{deps, + [{grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.2"}}} +]}. + +{grpc, + [{protos, ["priv/protos"]}, + {gpb_opts, [{module_name_prefix, "emqx_"}, + {module_name_suffix, "_pb"}]} +]}. + +{provider_hooks, + [{pre, [{compile, {grpc, gen}}, + {clean, {grpc, clean}}]} +]}. + +{edoc_opts, [{preprocess, true}]}. + +{erl_opts, [warn_unused_vars, + warn_shadow_vars, + warn_unused_import, + warn_obsolete_guard, + debug_info, + {parse_transform}]}. + +{xref_checks, [undefined_function_calls, undefined_functions, + locals_not_used, deprecated_function_calls, + warnings_as_errors, deprecated_functions]}. +{xref_ignores, [emqx_exhook_pb]}. + +{cover_enabled, true}. +{cover_opts, [verbose]}. +{cover_export_enabled, true}. +{cover_excl_mods, [emqx_exhook_pb, + emqx_exhook_v_1_hook_provider_bhvr, + emqx_exhook_v_1_hook_provider_client]}. diff --git a/apps/emqx_exhook/src/emqx_exhook.app.src b/apps/emqx_exhook/src/emqx_exhook.app.src new file mode 100644 index 000000000..c306a5ea4 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook.app.src @@ -0,0 +1,12 @@ +{application, emqx_exhook, + [{description, "EMQ X Extension for Hook"}, + {vsn, "5.0.0"}, + {modules, []}, + {registered, []}, + {mod, {emqx_exhook_app, []}}, + {applications, [kernel,stdlib,grpc,emqx]}, + {env,[]}, + {licenses, ["Apache-2.0"]}, + {maintainers, ["EMQ X Team "]}, + {links, [{"Homepage", "https://emqx.io/"}]} + ]}. 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..9e142d9e2 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook.appup.src @@ -0,0 +1,9 @@ +%% -*-: erlang -*- +{VSN, + [ + {<<".*">>, []} + ], + [ + {<<".*">>, []} + ] +}. diff --git a/apps/emqx_exhook/src/emqx_exhook.erl b/apps/emqx_exhook/src/emqx_exhook.erl new file mode 100644 index 000000000..c6b02e716 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook.erl @@ -0,0 +1,117 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook). + +-include("emqx_exhook.hrl"). +-include_lib("emqx/include/logger.hrl"). + + +-export([ enable/1 + , disable/1 + , list/0 + ]). + +-export([ cast/2 + , call_fold/3 + ]). + +%%-------------------------------------------------------------------- +%% Mgmt APIs +%%-------------------------------------------------------------------- + +-spec enable(binary()) -> ok | {error, term()}. +enable(Name) -> + with_mngr(fun(Pid) -> emqx_exhook_mngr:enable(Pid, Name) end). + +-spec disable(binary()) -> ok | {error, term()}. +disable(Name) -> + with_mngr(fun(Pid) -> emqx_exhook_mngr:disable(Pid, Name) end). + +-spec list() -> [atom() | string()]. +list() -> + with_mngr(fun(Pid) -> emqx_exhook_mngr:list(Pid) end). + +with_mngr(Fun) -> + case lists:keyfind(emqx_exhook_mngr, 1, + supervisor:which_children(emqx_exhook_sup)) of + {_, Pid, _, _} -> + Fun(Pid); + _ -> + {error, no_manager_svr} + end. + +%%-------------------------------------------------------------------- +%% Dispatch APIs +%%-------------------------------------------------------------------- + +-spec cast(atom(), map()) -> ok. +cast(Hookpoint, Req) -> + cast(Hookpoint, Req, emqx_exhook_mngr:running()). + +cast(_, _, []) -> + ok; +cast(Hookpoint, Req, [ServerName|More]) -> + %% XXX: Need a real asynchronous running + _ = emqx_exhook_server:call(Hookpoint, Req, + emqx_exhook_mngr:server(ServerName)), + cast(Hookpoint, Req, More). + +-spec call_fold(atom(), term(), function()) + -> {ok, term()} + | {stop, term()}. +call_fold(Hookpoint, Req, AccFun) -> + FailedAction = emqx_exhook_mngr:get_request_failed_action(), + ServerNames = emqx_exhook_mngr:running(), + case ServerNames == [] andalso FailedAction == deny of + true -> + {stop, deny_action_result(Hookpoint, Req)}; + _ -> + call_fold(Hookpoint, Req, FailedAction, AccFun, ServerNames) + end. + +call_fold(_, Req, _, _, []) -> + {ok, Req}; +call_fold(Hookpoint, Req, FailedAction, AccFun, [ServerName|More]) -> + Server = emqx_exhook_mngr:server(ServerName), + case emqx_exhook_server:call(Hookpoint, Req, Server) of + {ok, Resp} -> + case AccFun(Req, Resp) of + {stop, NReq} -> + {stop, NReq}; + {ok, NReq} -> + call_fold(Hookpoint, NReq, FailedAction, AccFun, More); + _ -> + call_fold(Hookpoint, Req, FailedAction, AccFun, More) + end; + _ -> + case FailedAction of + deny -> + {stop, deny_action_result(Hookpoint, Req)}; + _ -> + call_fold(Hookpoint, Req, FailedAction, AccFun, More) + end + end. + +%% XXX: Hard-coded the deny response +deny_action_result('client.authenticate', _) -> + #{result => false}; +deny_action_result('client.authorize', _) -> + #{result => false}; +deny_action_result('message.publish', Msg) -> + %% TODO: Not support to deny a message + %% maybe we can put the 'allow_publish' into message header + Msg. diff --git a/apps/emqx_exhook/src/emqx_exhook_app.erl b/apps/emqx_exhook/src/emqx_exhook_app.erl new file mode 100644 index 000000000..80dc24b70 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_app.erl @@ -0,0 +1,46 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook_app). + +-behaviour(application). + +-include("emqx_exhook.hrl"). + +-export([ start/2 + , stop/1 + , prep_stop/1 + ]). + +%%-------------------------------------------------------------------- +%% Application callbacks +%%-------------------------------------------------------------------- + +start(_StartType, _StartArgs) -> + {ok, Sup} = emqx_exhook_sup:start_link(), + emqx_ctl:register_command(exhook, {emqx_exhook_cli, cli}, []), + {ok, Sup}. + +prep_stop(State) -> + emqx_ctl:unregister_command(exhook), + State. + +stop(_State) -> + ok. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- diff --git a/apps/emqx_exhook/src/emqx_exhook_cli.erl b/apps/emqx_exhook/src/emqx_exhook_cli.erl new file mode 100644 index 000000000..860499698 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_cli.erl @@ -0,0 +1,84 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook_cli). + +-include("emqx_exhook.hrl"). + +-export([cli/1]). + +cli(["server", "list"]) -> + if_enabled(fun() -> + ServerNames = emqx_exhook:list(), + [emqx_ctl:print("Server(~s)~n", [format(Name)]) || Name <- ServerNames] + end); + +cli(["server", "enable", Name]) -> + if_enabled(fun() -> + print(emqx_exhook:enable(iolist_to_binary(Name))) + end); + +cli(["server", "disable", Name]) -> + if_enabled(fun() -> + print(emqx_exhook:disable(iolist_to_binary(Name))) + end); + +cli(["server", "stats"]) -> + if_enabled(fun() -> + [emqx_ctl:print("~-35s:~w~n", [Name, N]) || {Name, N} <- stats()] + end); + +cli(_) -> + emqx_ctl:usage([{"exhook server list", "List all running exhook server"}, + {"exhook server enable ", "Enable a exhook server in the configuration"}, + {"exhook server disable ", "Disable a exhook server"}, + {"exhook server stats", "Print exhook server statistic"}]). + +print(ok) -> + emqx_ctl:print("ok~n"); +print({error, Reason}) -> + emqx_ctl:print("~p~n", [Reason]). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +if_enabled(Fun) -> + case lists:keymember(?APP, 1, application:which_applications()) of + true -> + Fun(); + _ -> hint() + end. + +hint() -> + emqx_ctl:print("Please './bin/emqx_ctl plugins load emqx_exhook' first.~n"). + +stats() -> + lists:usort(lists:foldr(fun({K, N}, Acc) -> + case atom_to_list(K) of + "exhook." ++ Key -> [{Key, N} | Acc]; + _ -> Acc + end + end, [], emqx_metrics:all())). + +format(Name) -> + case emqx_exhook_mngr:server(Name) of + undefined -> + lists:flatten( + io_lib:format("name=~s, hooks=#{}, active=false", [Name])); + Server -> + emqx_exhook_server:format(Server) + end. diff --git a/apps/emqx_exhook/src/emqx_exhook_handler.erl b/apps/emqx_exhook/src/emqx_exhook_handler.erl new file mode 100644 index 000000000..1e81646e0 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_handler.erl @@ -0,0 +1,320 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook_handler). + +-include("emqx_exhook.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). + + +-export([ on_client_connect/2 + , on_client_connack/3 + , on_client_connected/2 + , on_client_disconnected/3 + , on_client_authenticate/2 + , on_client_authorize/4 + , on_client_subscribe/3 + , on_client_unsubscribe/3 + ]). + +%% Session Lifecircle Hooks +-export([ on_session_created/2 + , on_session_subscribed/3 + , on_session_unsubscribed/3 + , on_session_resumed/2 + , on_session_discarded/2 + , on_session_takeovered/2 + , on_session_terminated/3 + ]). + +-export([ on_message_publish/1 + , on_message_dropped/3 + , on_message_delivered/2 + , on_message_acked/2 + ]). + +%% Utils +-export([ message/1 + , stringfy/1 + , merge_responsed_bool/2 + , merge_responsed_message/2 + , assign_to_message/2 + , clientinfo/1 + ]). + +-import(emqx_exhook, + [ cast/2 + , call_fold/3 + ]). + +%%-------------------------------------------------------------------- +%% Clients +%%-------------------------------------------------------------------- + +on_client_connect(ConnInfo, Props) -> + Req = #{conninfo => conninfo(ConnInfo), + props => properties(Props) + }, + cast('client.connect', Req). + +on_client_connack(ConnInfo, Rc, Props) -> + Req = #{conninfo => conninfo(ConnInfo), + result_code => stringfy(Rc), + props => properties(Props)}, + cast('client.connack', Req). + +on_client_connected(ClientInfo, _ConnInfo) -> + Req = #{clientinfo => clientinfo(ClientInfo)}, + cast('client.connected', Req). + +on_client_disconnected(ClientInfo, Reason, _ConnInfo) -> + Req = #{clientinfo => clientinfo(ClientInfo), + reason => stringfy(Reason) + }, + cast('client.disconnected', Req). + +on_client_authenticate(ClientInfo, AuthResult) -> + %% XXX: Bool is missing more information about the atom of the result + %% So, the `Req` has missed detailed info too. + %% + %% The return value of `call_fold` just a bool, that has missed + %% detailed info too. + %% + Bool = AuthResult == ok, + Req = #{clientinfo => clientinfo(ClientInfo), + result => Bool + }, + + case call_fold('client.authenticate', Req, + fun merge_responsed_bool/2) of + {StopOrOk, #{result := Result0}} when is_boolean(Result0) -> + Result = case Result0 of true -> ok; _ -> {error, not_authorized} end, + {StopOrOk, Result}; + _ -> + {ok, AuthResult} + end. + +on_client_authorize(ClientInfo, PubSub, Topic, Result) -> + Bool = Result == allow, + Type = case PubSub of + publish -> 'PUBLISH'; + subscribe -> 'SUBSCRIBE' + end, + Req = #{clientinfo => clientinfo(ClientInfo), + type => Type, + topic => Topic, + result => Bool + }, + case call_fold('client.authorize', Req, + fun merge_responsed_bool/2) of + {StopOrOk, #{result := Result0}} when is_boolean(Result0) -> + NResult = case Result0 of true -> allow; _ -> deny end, + {StopOrOk, NResult}; + _ -> {ok, Result} + end. + +on_client_subscribe(ClientInfo, Props, TopicFilters) -> + Req = #{clientinfo => clientinfo(ClientInfo), + props => properties(Props), + topic_filters => topicfilters(TopicFilters) + }, + cast('client.subscribe', Req). + +on_client_unsubscribe(ClientInfo, Props, TopicFilters) -> + Req = #{clientinfo => clientinfo(ClientInfo), + props => properties(Props), + topic_filters => topicfilters(TopicFilters) + }, + cast('client.unsubscribe', Req). + +%%-------------------------------------------------------------------- +%% Session +%%-------------------------------------------------------------------- + +on_session_created(ClientInfo, _SessInfo) -> + Req = #{clientinfo => clientinfo(ClientInfo)}, + cast('session.created', Req). + +on_session_subscribed(ClientInfo, Topic, SubOpts) -> + Req = #{clientinfo => clientinfo(ClientInfo), + topic => Topic, + subopts => maps:with([qos, share, rh, rap, nl], SubOpts) + }, + cast('session.subscribed', Req). + +on_session_unsubscribed(ClientInfo, Topic, _SubOpts) -> + Req = #{clientinfo => clientinfo(ClientInfo), + topic => Topic + }, + cast('session.unsubscribed', Req). + +on_session_resumed(ClientInfo, _SessInfo) -> + Req = #{clientinfo => clientinfo(ClientInfo)}, + cast('session.resumed', Req). + +on_session_discarded(ClientInfo, _SessInfo) -> + Req = #{clientinfo => clientinfo(ClientInfo)}, + cast('session.discarded', Req). + +on_session_takeovered(ClientInfo, _SessInfo) -> + Req = #{clientinfo => clientinfo(ClientInfo)}, + cast('session.takeovered', Req). + +on_session_terminated(ClientInfo, Reason, _SessInfo) -> + Req = #{clientinfo => clientinfo(ClientInfo), + reason => stringfy(Reason)}, + cast('session.terminated', Req). + +%%-------------------------------------------------------------------- +%% Message +%%-------------------------------------------------------------------- + +on_message_publish(#message{topic = <<"$SYS/", _/binary>>}) -> + ok; +on_message_publish(Message) -> + Req = #{message => message(Message)}, + case call_fold('message.publish', Req, + fun emqx_exhook_handler:merge_responsed_message/2) of + {StopOrOk, #{message := NMessage}} -> + {StopOrOk, assign_to_message(NMessage, Message)}; + _ -> {ok, Message} + end. + +on_message_dropped(#message{topic = <<"$SYS/", _/binary>>}, _By, _Reason) -> + ok; +on_message_dropped(Message, _By, Reason) -> + Req = #{message => message(Message), + reason => stringfy(Reason) + }, + cast('message.dropped', Req). + +on_message_delivered(_ClientInfo, #message{topic = <<"$SYS/", _/binary>>}) -> + ok; +on_message_delivered(ClientInfo, Message) -> + Req = #{clientinfo => clientinfo(ClientInfo), + message => message(Message) + }, + cast('message.delivered', Req). + +on_message_acked(_ClientInfo, #message{topic = <<"$SYS/", _/binary>>}) -> + ok; +on_message_acked(ClientInfo, Message) -> + Req = #{clientinfo => clientinfo(ClientInfo), + message => message(Message) + }, + cast('message.acked', Req). + +%%-------------------------------------------------------------------- +%% Types + +properties(undefined) -> []; +properties(M) when is_map(M) -> + maps:fold(fun(K, V, Acc) -> + [#{name => stringfy(K), + value => stringfy(V)} | Acc] + end, [], M). + +conninfo(_ConnInfo = + #{clientid := ClientId, username := Username, peername := {Peerhost, _}, + sockname := {_, SockPort}, proto_name := ProtoName, proto_ver := ProtoVer, + keepalive := Keepalive}) -> + #{node => stringfy(node()), + clientid => ClientId, + username => maybe(Username), + peerhost => ntoa(Peerhost), + sockport => SockPort, + proto_name => ProtoName, + proto_ver => stringfy(ProtoVer), + keepalive => Keepalive}. + +clientinfo(ClientInfo = + #{clientid := ClientId, username := Username, peerhost := PeerHost, + sockport := SockPort, protocol := Protocol, mountpoint := Mountpoiont}) -> + #{node => stringfy(node()), + clientid => ClientId, + username => maybe(Username), + password => maybe(maps:get(password, ClientInfo, undefined)), + peerhost => ntoa(PeerHost), + sockport => SockPort, + protocol => stringfy(Protocol), + mountpoint => maybe(Mountpoiont), + is_superuser => maps:get(is_superuser, ClientInfo, false), + anonymous => maps:get(anonymous, ClientInfo, true), + cn => maybe(maps:get(cn, ClientInfo, undefined)), + dn => maybe(maps:get(dn, ClientInfo, undefined))}. + +message(#message{id = Id, qos = Qos, from = From, topic = Topic, payload = Payload, timestamp = Ts}) -> + #{node => stringfy(node()), + id => emqx_guid:to_hexstr(Id), + qos => Qos, + from => stringfy(From), + topic => Topic, + payload => Payload, + timestamp => Ts}. + +assign_to_message(#{qos := Qos, topic := Topic, payload := Payload}, Message) -> + Message#message{qos = Qos, topic = Topic, payload = Payload}. + +topicfilters(Tfs) when is_list(Tfs) -> + [#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs]. + +ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> + list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256})); +ntoa(IP) -> + list_to_binary(inet_parse:ntoa(IP)). + +maybe(undefined) -> <<>>; +maybe(B) -> B. + +%% @private +stringfy(Term) when is_binary(Term) -> + Term; +stringfy(Term) when is_integer(Term) -> + integer_to_binary(Term); +stringfy(Term) when is_atom(Term) -> + atom_to_binary(Term, utf8); +stringfy(Term) -> + unicode:characters_to_binary((io_lib:format("~0p", [Term]))). + +%%-------------------------------------------------------------------- +%% Acc funcs + +%% see exhook.proto +merge_responsed_bool(_Req, #{type := 'IGNORE'}) -> + ignore; +merge_responsed_bool(Req, #{type := Type, value := {bool_result, NewBool}}) + when is_boolean(NewBool) -> + NReq = Req#{result => NewBool}, + case Type of + 'CONTINUE' -> {ok, NReq}; + 'STOP_AND_RETURN' -> {stop, NReq} + end; +merge_responsed_bool(_Req, Resp) -> + ?LOG(warning, "Unknown responsed value ~0p to merge to callback chain", [Resp]), + ignore. + +merge_responsed_message(_Req, #{type := 'IGNORE'}) -> + ignore; +merge_responsed_message(Req, #{type := Type, value := {message, NMessage}}) -> + NReq = Req#{message => NMessage}, + case Type of + 'CONTINUE' -> {ok, NReq}; + 'STOP_AND_RETURN' -> {stop, NReq} + end; +merge_responsed_message(_Req, Resp) -> + ?LOG(warning, "Unknown responsed value ~0p to merge to callback chain", [Resp]), + ignore. diff --git a/apps/emqx_exhook/src/emqx_exhook_mngr.erl b/apps/emqx_exhook/src/emqx_exhook_mngr.erl new file mode 100644 index 000000000..1a6e10bf0 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_mngr.erl @@ -0,0 +1,315 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +%% @doc Manage the server status and reload strategy +-module(emqx_exhook_mngr). + +-behaviour(gen_server). + +-include("emqx_exhook.hrl"). +-include_lib("emqx/include/logger.hrl"). + +%% APIs +-export([start_link/3]). + +%% Mgmt API +-export([ enable/2 + , disable/2 + , list/1 + ]). + +%% Helper funcs +-export([ running/0 + , server/1 + , put_request_failed_action/1 + , get_request_failed_action/0 + ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-record(state, { + %% Running servers + running :: map(), %% XXX: server order? + %% Wait to reload servers + waiting :: map(), + %% Marked stopped servers + stopped :: map(), + %% Auto reconnect timer interval + auto_reconnect :: false | non_neg_integer(), + %% Request options + request_options :: grpc_client:options(), + %% Timer references + trefs :: map() + }). + +-type servers() :: [{Name :: atom(), server_options()}]. + +-type server_options() :: [ {scheme, http | https} + | {host, string()} + | {port, inet:port_number()} + ]. + +-define(DEFAULT_TIMEOUT, 60000). + +-define(CNTER, emqx_exhook_counter). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +-spec start_link(servers(), false | non_neg_integer(), grpc_client:options()) + ->ignore + | {ok, pid()} + | {error, any()}. +start_link(Servers, AutoReconnect, ReqOpts) -> + gen_server:start_link(?MODULE, [Servers, AutoReconnect, ReqOpts], []). + +-spec enable(pid(), binary()) -> ok | {error, term()}. +enable(Pid, Name) -> + call(Pid, {load, Name}). + +-spec disable(pid(), binary()) -> ok | {error, term()}. +disable(Pid, Name) -> + call(Pid, {unload, Name}). + +list(Pid) -> + call(Pid, list). + +call(Pid, Req) -> + gen_server:call(Pid, Req, ?DEFAULT_TIMEOUT). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Servers, AutoReconnect, ReqOpts0]) -> + process_flag(trap_exit, true), + %% XXX: Due to the ExHook Module in the enterprise, + %% this process may start multiple times and they will share this table + try + _ = ets:new(?CNTER, [named_table, public]), ok + catch + error:badarg:_ -> + ok + end, + + %% put the global option + put_request_failed_action( + maps:get(request_failed_action, ReqOpts0, deny) + ), + + %% Load the hook servers + ReqOpts = maps:without([request_failed_action], ReqOpts0), + {Waiting, Running} = load_all_servers(Servers, ReqOpts), + {ok, ensure_reload_timer( + #state{waiting = Waiting, + running = Running, + stopped = #{}, + request_options = ReqOpts, + auto_reconnect = AutoReconnect, + trefs = #{} + } + )}. + +%% @private +load_all_servers(Servers, ReqOpts) -> + load_all_servers(Servers, ReqOpts, #{}, #{}). +load_all_servers([], _Request, Waiting, Running) -> + {Waiting, Running}; +load_all_servers([#{name := Name0} = Options0|More], ReqOpts, Waiting, Running) -> + Name = iolist_to_binary(Name0), + Options = Options0#{name => Name}, + {NWaiting, NRunning} = + case emqx_exhook_server:load(Name, Options, ReqOpts) of + {ok, ServerState} -> + save(Name, ServerState), + {Waiting, Running#{Name => Options}}; + {error, _} -> + {Waiting#{Name => Options}, Running} + end, + load_all_servers(More, ReqOpts, NWaiting, NRunning). + +handle_call({load, Name}, _From, State) -> + {Result, NState} = do_load_server(Name, State), + {reply, Result, NState}; + +handle_call({unload, Name}, _From, State) -> + case do_unload_server(Name, State) of + {error, Reason} -> + {reply, {error, Reason}, State}; + {ok, NState} -> + {reply, ok, NState} + end; + +handle_call(list, _From, State = #state{ + running = Running, + waiting = Waiting, + stopped = Stopped}) -> + ServerNames = maps:keys(Running) + ++ maps:keys(Waiting) + ++ maps:keys(Stopped), + {reply, ServerNames, State}; + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({timeout, _Ref, {reload, Name}}, State) -> + {Result, NState} = do_load_server(Name, State), + case Result of + ok -> + {noreply, NState}; + {error, not_found} -> + {noreply, NState}; + {error, Reason} -> + ?LOG(warning, "Failed to reload exhook callback server \"~s\", " + "Reason: ~0p", [Name, Reason]), + {noreply, ensure_reload_timer(NState)} + end; + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, State = #state{running = Running}) -> + _ = maps:fold(fun(Name, _, AccIn) -> + case do_unload_server(Name, AccIn) of + {ok, NAccIn} -> NAccIn; + _ -> AccIn + end + end, State, Running), + _ = unload_exhooks(), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +unload_exhooks() -> + [emqx:unhook(Name, {M, F}) || + {Name, {M, F, _A}} <- ?ENABLED_HOOKS]. + +do_load_server(Name, State0 = #state{ + waiting = Waiting, + running = Running, + stopped = Stopped, + request_options = ReqOpts}) -> + State = clean_reload_timer(Name, State0), + case maps:get(Name, Running, undefined) of + undefined -> + case maps:get(Name, Stopped, + maps:get(Name, Waiting, undefined)) of + undefined -> + {{error, not_found}, State}; + Options -> + case emqx_exhook_server:load(Name, Options, ReqOpts) of + {ok, ServerState} -> + save(Name, ServerState), + ?LOG(info, "Load exhook callback server " + "\"~s\" successfully!", [Name]), + {ok, State#state{ + running = maps:put(Name, Options, Running), + waiting = maps:remove(Name, Waiting), + stopped = maps:remove(Name, Stopped) + } + }; + {error, Reason} -> + {{error, Reason}, State} + end + end; + _ -> + {{error, already_started}, State} + end. + +do_unload_server(Name, State = #state{running = Running, stopped = Stopped}) -> + case maps:take(Name, Running) of + error -> {error, not_running}; + {Options, NRunning} -> + ok = emqx_exhook_server:unload(server(Name)), + ok = unsave(Name), + {ok, State#state{running = NRunning, + stopped = maps:put(Name, Options, Stopped) + }} + end. + +ensure_reload_timer(State = #state{auto_reconnect = false}) -> + State; +ensure_reload_timer(State = #state{waiting = Waiting, + trefs = TRefs, + auto_reconnect = Intv}) -> + NRefs = maps:fold(fun(Name, _, AccIn) -> + case maps:get(Name, AccIn, undefined) of + undefined -> + Ref = erlang:start_timer(Intv, self(), {reload, Name}), + AccIn#{Name => Ref}; + _HasRef -> + AccIn + end + end, TRefs, Waiting), + State#state{trefs = NRefs}. + +clean_reload_timer(Name, State = #state{trefs = TRefs}) -> + case maps:take(Name, TRefs) of + error -> State; + {TRef, NTRefs} -> + _ = erlang:cancel_timer(TRef), + State#state{trefs = NTRefs} + end. + +%%-------------------------------------------------------------------- +%% Server state persistent + +put_request_failed_action(Val) -> + persistent_term:put({?APP, request_failed_action}, Val). + +get_request_failed_action() -> + persistent_term:get({?APP, request_failed_action}). + +save(Name, ServerState) -> + Saved = persistent_term:get(?APP, []), + persistent_term:put(?APP, lists:reverse([Name | Saved])), + persistent_term:put({?APP, Name}, ServerState). + +unsave(Name) -> + case persistent_term:get(?APP, []) of + [] -> + persistent_term:erase(?APP); + Saved -> + persistent_term:put(?APP, lists:delete(Name, Saved)) + end, + persistent_term:erase({?APP, Name}), + ok. + +running() -> + persistent_term:get(?APP, []). + +server(Name) -> + case catch persistent_term:get({?APP, Name}) of + {'EXIT', {badarg,_}} -> undefined; + Service -> Service + end. diff --git a/apps/emqx_exhook/src/emqx_exhook_pb.erl b/apps/emqx_exhook/src/emqx_exhook_pb.erl new file mode 100644 index 000000000..aec57785d --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_pb.erl @@ -0,0 +1,15782 @@ +%% -*- coding: utf-8 -*- +%% @private +%% Automatically generated, do not edit +%% Generated by gpb_compile version 4.11.2 +-module(emqx_exhook_pb). + +-export([encode_msg/2, encode_msg/3]). +-export([decode_msg/2, decode_msg/3]). +-export([merge_msgs/3, merge_msgs/4]). +-export([verify_msg/2, verify_msg/3]). +-export([get_msg_defs/0]). +-export([get_msg_names/0]). +-export([get_group_names/0]). +-export([get_msg_or_group_names/0]). +-export([get_enum_names/0]). +-export([find_msg_def/1, fetch_msg_def/1]). +-export([find_enum_def/1, fetch_enum_def/1]). +-export([enum_symbol_by_value/2, enum_value_by_symbol/2]). +-export(['enum_symbol_by_value_client_authorize_request.AuthorizeReqType'/1, 'enum_value_by_symbol_client_authorize_request.AuthorizeReqType'/1]). +-export(['enum_symbol_by_value_valued_response.ResponsedType'/1, 'enum_value_by_symbol_valued_response.ResponsedType'/1]). +-export([get_service_names/0]). +-export([get_service_def/1]). +-export([get_rpc_names/1]). +-export([find_rpc_def/2, fetch_rpc_def/2]). +-export([fqbin_to_service_name/1]). +-export([service_name_to_fqbin/1]). +-export([fqbins_to_service_and_rpc_name/2]). +-export([service_and_rpc_name_to_fqbins/2]). +-export([fqbin_to_msg_name/1]). +-export([msg_name_to_fqbin/1]). +-export([fqbin_to_enum_name/1]). +-export([enum_name_to_fqbin/1]). +-export([get_package_name/0]). +-export([uses_packages/0]). +-export([source_basename/0]). +-export([get_all_source_basenames/0]). +-export([get_all_proto_names/0]). +-export([get_msg_containment/1]). +-export([get_pkg_containment/1]). +-export([get_service_containment/1]). +-export([get_rpc_containment/1]). +-export([get_enum_containment/1]). +-export([get_proto_by_msg_name_as_fqbin/1]). +-export([get_proto_by_service_name_as_fqbin/1]). +-export([get_proto_by_enum_name_as_fqbin/1]). +-export([get_protos_by_pkg_name_as_fqbin/1]). +-export([gpb_version_as_string/0, gpb_version_as_list/0]). + + +%% enumerated types +-type 'client_authorize_request.AuthorizeReqType'() :: 'PUBLISH' | 'SUBSCRIBE'. +-type 'valued_response.ResponsedType'() :: 'CONTINUE' | 'IGNORE' | 'STOP_AND_RETURN'. +-export_type(['client_authorize_request.AuthorizeReqType'/0, 'valued_response.ResponsedType'/0]). + +%% message types +-type provider_loaded_request() :: + #{broker => broker_info() % = 1 + }. + +-type loaded_response() :: + #{hooks => [hook_spec()] % = 1 + }. + +-type provider_unloaded_request() :: + #{ + }. + +-type client_connect_request() :: + #{conninfo => conn_info(), % = 1 + props => [property()] % = 2 + }. + +-type client_connack_request() :: + #{conninfo => conn_info(), % = 1 + result_code => iodata(), % = 2 + props => [property()] % = 3 + }. + +-type client_connected_request() :: + #{clientinfo => client_info() % = 1 + }. + +-type client_disconnected_request() :: + #{clientinfo => client_info(), % = 1 + reason => iodata() % = 2 + }. + +-type client_authenticate_request() :: + #{clientinfo => client_info(), % = 1 + result => boolean() | 0 | 1 % = 2 + }. + +-type client_authorize_request() :: + #{clientinfo => client_info(), % = 1 + type => 'PUBLISH' | 'SUBSCRIBE' | integer(), % = 2, enum client_authorize_request.AuthorizeReqType + topic => iodata(), % = 3 + result => boolean() | 0 | 1 % = 4 + }. + +-type client_subscribe_request() :: + #{clientinfo => client_info(), % = 1 + props => [property()], % = 2 + topic_filters => [topic_filter()] % = 3 + }. + +-type client_unsubscribe_request() :: + #{clientinfo => client_info(), % = 1 + props => [property()], % = 2 + topic_filters => [topic_filter()] % = 3 + }. + +-type session_created_request() :: + #{clientinfo => client_info() % = 1 + }. + +-type session_subscribed_request() :: + #{clientinfo => client_info(), % = 1 + topic => iodata(), % = 2 + subopts => sub_opts() % = 3 + }. + +-type session_unsubscribed_request() :: + #{clientinfo => client_info(), % = 1 + topic => iodata() % = 2 + }. + +-type session_resumed_request() :: + #{clientinfo => client_info() % = 1 + }. + +-type session_discarded_request() :: + #{clientinfo => client_info() % = 1 + }. + +-type session_takeovered_request() :: + #{clientinfo => client_info() % = 1 + }. + +-type session_terminated_request() :: + #{clientinfo => client_info(), % = 1 + reason => iodata() % = 2 + }. + +-type message_publish_request() :: + #{message => message() % = 1 + }. + +-type message_delivered_request() :: + #{clientinfo => client_info(), % = 1 + message => message() % = 2 + }. + +-type message_dropped_request() :: + #{message => message(), % = 1 + reason => iodata() % = 2 + }. + +-type message_acked_request() :: + #{clientinfo => client_info(), % = 1 + message => message() % = 2 + }. + +-type empty_success() :: + #{ + }. + +-type valued_response() :: + #{type => 'CONTINUE' | 'IGNORE' | 'STOP_AND_RETURN' | integer(), % = 1, enum valued_response.ResponsedType + value => {bool_result, boolean() | 0 | 1} | {message, message()} % oneof + }. + +-type broker_info() :: + #{version => iodata(), % = 1 + sysdescr => iodata(), % = 2 + uptime => integer(), % = 3, 64 bits + datetime => iodata() % = 4 + }. + +-type hook_spec() :: + #{name => iodata(), % = 1 + topics => [iodata()] % = 2 + }. + +-type conn_info() :: + #{node => iodata(), % = 1 + clientid => iodata(), % = 2 + username => iodata(), % = 3 + peerhost => iodata(), % = 4 + sockport => non_neg_integer(), % = 5, 32 bits + proto_name => iodata(), % = 6 + proto_ver => iodata(), % = 7 + keepalive => non_neg_integer() % = 8, 32 bits + }. + +-type client_info() :: + #{node => iodata(), % = 1 + clientid => iodata(), % = 2 + username => iodata(), % = 3 + password => iodata(), % = 4 + peerhost => iodata(), % = 5 + sockport => non_neg_integer(), % = 6, 32 bits + protocol => iodata(), % = 7 + mountpoint => iodata(), % = 8 + is_superuser => boolean() | 0 | 1, % = 9 + anonymous => boolean() | 0 | 1, % = 10 + cn => iodata(), % = 11 + dn => iodata() % = 12 + }. + +-type message() :: + #{node => iodata(), % = 1 + id => iodata(), % = 2 + qos => non_neg_integer(), % = 3, 32 bits + from => iodata(), % = 4 + topic => iodata(), % = 5 + payload => iodata(), % = 6 + timestamp => non_neg_integer() % = 7, 64 bits + }. + +-type property() :: + #{name => iodata(), % = 1 + value => iodata() % = 2 + }. + +-type topic_filter() :: + #{name => iodata(), % = 1 + qos => non_neg_integer() % = 2, 32 bits + }. + +-type sub_opts() :: + #{qos => non_neg_integer(), % = 1, 32 bits + share => iodata(), % = 2 + rh => non_neg_integer(), % = 3, 32 bits + rap => non_neg_integer(), % = 4, 32 bits + nl => non_neg_integer() % = 5, 32 bits + }. + +-export_type(['provider_loaded_request'/0, 'loaded_response'/0, 'provider_unloaded_request'/0, 'client_connect_request'/0, 'client_connack_request'/0, 'client_connected_request'/0, 'client_disconnected_request'/0, 'client_authenticate_request'/0, 'client_authorize_request'/0, 'client_subscribe_request'/0, 'client_unsubscribe_request'/0, 'session_created_request'/0, 'session_subscribed_request'/0, 'session_unsubscribed_request'/0, 'session_resumed_request'/0, 'session_discarded_request'/0, 'session_takeovered_request'/0, 'session_terminated_request'/0, 'message_publish_request'/0, 'message_delivered_request'/0, 'message_dropped_request'/0, 'message_acked_request'/0, 'empty_success'/0, 'valued_response'/0, 'broker_info'/0, 'hook_spec'/0, 'conn_info'/0, 'client_info'/0, 'message'/0, 'property'/0, 'topic_filter'/0, 'sub_opts'/0]). + +-spec encode_msg(provider_loaded_request() | loaded_response() | provider_unloaded_request() | client_connect_request() | client_connack_request() | client_connected_request() | client_disconnected_request() | client_authenticate_request() | client_authorize_request() | client_subscribe_request() | client_unsubscribe_request() | session_created_request() | session_subscribed_request() | session_unsubscribed_request() | session_resumed_request() | session_discarded_request() | session_takeovered_request() | session_terminated_request() | message_publish_request() | message_delivered_request() | message_dropped_request() | message_acked_request() | empty_success() | valued_response() | broker_info() | hook_spec() | conn_info() | client_info() | message() | property() | topic_filter() | sub_opts(), atom()) -> binary(). +encode_msg(Msg, MsgName) when is_atom(MsgName) -> + encode_msg(Msg, MsgName, []). + +-spec encode_msg(provider_loaded_request() | loaded_response() | provider_unloaded_request() | client_connect_request() | client_connack_request() | client_connected_request() | client_disconnected_request() | client_authenticate_request() | client_authorize_request() | client_subscribe_request() | client_unsubscribe_request() | session_created_request() | session_subscribed_request() | session_unsubscribed_request() | session_resumed_request() | session_discarded_request() | session_takeovered_request() | session_terminated_request() | message_publish_request() | message_delivered_request() | message_dropped_request() | message_acked_request() | empty_success() | valued_response() | broker_info() | hook_spec() | conn_info() | client_info() | message() | property() | topic_filter() | sub_opts(), atom(), list()) -> binary(). +encode_msg(Msg, MsgName, Opts) -> + case proplists:get_bool(verify, Opts) of + true -> verify_msg(Msg, MsgName, Opts); + false -> ok + end, + TrUserData = proplists:get_value(user_data, Opts), + case MsgName of + provider_loaded_request -> + encode_msg_provider_loaded_request(id(Msg, TrUserData), + TrUserData); + loaded_response -> + encode_msg_loaded_response(id(Msg, TrUserData), + TrUserData); + provider_unloaded_request -> + encode_msg_provider_unloaded_request(id(Msg, + TrUserData), + TrUserData); + client_connect_request -> + encode_msg_client_connect_request(id(Msg, TrUserData), + TrUserData); + client_connack_request -> + encode_msg_client_connack_request(id(Msg, TrUserData), + TrUserData); + client_connected_request -> + encode_msg_client_connected_request(id(Msg, TrUserData), + TrUserData); + client_disconnected_request -> + encode_msg_client_disconnected_request(id(Msg, + TrUserData), + TrUserData); + client_authenticate_request -> + encode_msg_client_authenticate_request(id(Msg, + TrUserData), + TrUserData); + client_authorize_request -> + encode_msg_client_authorize_request(id(Msg, TrUserData), + TrUserData); + client_subscribe_request -> + encode_msg_client_subscribe_request(id(Msg, TrUserData), + TrUserData); + client_unsubscribe_request -> + encode_msg_client_unsubscribe_request(id(Msg, + TrUserData), + TrUserData); + session_created_request -> + encode_msg_session_created_request(id(Msg, TrUserData), + TrUserData); + session_subscribed_request -> + encode_msg_session_subscribed_request(id(Msg, + TrUserData), + TrUserData); + session_unsubscribed_request -> + encode_msg_session_unsubscribed_request(id(Msg, + TrUserData), + TrUserData); + session_resumed_request -> + encode_msg_session_resumed_request(id(Msg, TrUserData), + TrUserData); + session_discarded_request -> + encode_msg_session_discarded_request(id(Msg, + TrUserData), + TrUserData); + session_takeovered_request -> + encode_msg_session_takeovered_request(id(Msg, + TrUserData), + TrUserData); + session_terminated_request -> + encode_msg_session_terminated_request(id(Msg, + TrUserData), + TrUserData); + message_publish_request -> + encode_msg_message_publish_request(id(Msg, TrUserData), + TrUserData); + message_delivered_request -> + encode_msg_message_delivered_request(id(Msg, + TrUserData), + TrUserData); + message_dropped_request -> + encode_msg_message_dropped_request(id(Msg, TrUserData), + TrUserData); + message_acked_request -> + encode_msg_message_acked_request(id(Msg, TrUserData), + TrUserData); + empty_success -> + encode_msg_empty_success(id(Msg, TrUserData), + TrUserData); + valued_response -> + encode_msg_valued_response(id(Msg, TrUserData), + TrUserData); + broker_info -> + encode_msg_broker_info(id(Msg, TrUserData), TrUserData); + hook_spec -> + encode_msg_hook_spec(id(Msg, TrUserData), TrUserData); + conn_info -> + encode_msg_conn_info(id(Msg, TrUserData), TrUserData); + client_info -> + encode_msg_client_info(id(Msg, TrUserData), TrUserData); + message -> + encode_msg_message(id(Msg, TrUserData), TrUserData); + property -> + encode_msg_property(id(Msg, TrUserData), TrUserData); + topic_filter -> + encode_msg_topic_filter(id(Msg, TrUserData), + TrUserData); + sub_opts -> + encode_msg_sub_opts(id(Msg, TrUserData), TrUserData) + end. + + +encode_msg_provider_loaded_request(Msg, TrUserData) -> + encode_msg_provider_loaded_request(Msg, + <<>>, + TrUserData). + + +encode_msg_provider_loaded_request(#{} = M, Bin, + TrUserData) -> + case M of + #{broker := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_provider_loaded_request_broker(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end. + +encode_msg_loaded_response(Msg, TrUserData) -> + encode_msg_loaded_response(Msg, <<>>, TrUserData). + + +encode_msg_loaded_response(#{} = M, Bin, TrUserData) -> + case M of + #{hooks := F1} -> + TrF1 = id(F1, TrUserData), + if TrF1 == [] -> Bin; + true -> + e_field_loaded_response_hooks(TrF1, Bin, TrUserData) + end; + _ -> Bin + end. + +encode_msg_provider_unloaded_request(_Msg, + _TrUserData) -> + <<>>. + +encode_msg_client_connect_request(Msg, TrUserData) -> + encode_msg_client_connect_request(Msg, + <<>>, + TrUserData). + + +encode_msg_client_connect_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{conninfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_client_connect_request_conninfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{props := F2} -> + TrF2 = id(F2, TrUserData), + if TrF2 == [] -> B1; + true -> + e_field_client_connect_request_props(TrF2, + B1, + TrUserData) + end; + _ -> B1 + end. + +encode_msg_client_connack_request(Msg, TrUserData) -> + encode_msg_client_connack_request(Msg, + <<>>, + TrUserData). + + +encode_msg_client_connack_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{conninfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_client_connack_request_conninfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{result_code := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end, + case M of + #{props := F3} -> + TrF3 = id(F3, TrUserData), + if TrF3 == [] -> B2; + true -> + e_field_client_connack_request_props(TrF3, + B2, + TrUserData) + end; + _ -> B2 + end. + +encode_msg_client_connected_request(Msg, TrUserData) -> + encode_msg_client_connected_request(Msg, + <<>>, + TrUserData). + + +encode_msg_client_connected_request(#{} = M, Bin, + TrUserData) -> + case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_client_connected_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end. + +encode_msg_client_disconnected_request(Msg, + TrUserData) -> + encode_msg_client_disconnected_request(Msg, + <<>>, + TrUserData). + + +encode_msg_client_disconnected_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_client_disconnected_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{reason := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_client_authenticate_request(Msg, + TrUserData) -> + encode_msg_client_authenticate_request(Msg, + <<>>, + TrUserData). + + +encode_msg_client_authenticate_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_client_authenticate_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{result := F2} -> + begin + TrF2 = id(F2, TrUserData), + if TrF2 =:= false -> B1; + true -> e_type_bool(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_client_authorize_request(Msg, TrUserData) -> + encode_msg_client_authorize_request(Msg, + <<>>, + TrUserData). + + +encode_msg_client_authorize_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_client_authorize_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{type := F2} -> + begin + TrF2 = id(F2, TrUserData), + if TrF2 =:= 'PUBLISH'; TrF2 =:= 0 -> B1; + true -> + 'e_enum_client_authorize_request.AuthorizeReqType'(TrF2, + <>, + TrUserData) + end + end; + _ -> B1 + end, + B3 = case M of + #{topic := F3} -> + begin + TrF3 = id(F3, TrUserData), + case is_empty_string(TrF3) of + true -> B2; + false -> + e_type_string(TrF3, <>, TrUserData) + end + end; + _ -> B2 + end, + case M of + #{result := F4} -> + begin + TrF4 = id(F4, TrUserData), + if TrF4 =:= false -> B3; + true -> e_type_bool(TrF4, <>, TrUserData) + end + end; + _ -> B3 + end. + +encode_msg_client_subscribe_request(Msg, TrUserData) -> + encode_msg_client_subscribe_request(Msg, + <<>>, + TrUserData). + + +encode_msg_client_subscribe_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_client_subscribe_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{props := F2} -> + TrF2 = id(F2, TrUserData), + if TrF2 == [] -> B1; + true -> + e_field_client_subscribe_request_props(TrF2, + B1, + TrUserData) + end; + _ -> B1 + end, + case M of + #{topic_filters := F3} -> + TrF3 = id(F3, TrUserData), + if TrF3 == [] -> B2; + true -> + e_field_client_subscribe_request_topic_filters(TrF3, + B2, + TrUserData) + end; + _ -> B2 + end. + +encode_msg_client_unsubscribe_request(Msg, + TrUserData) -> + encode_msg_client_unsubscribe_request(Msg, + <<>>, + TrUserData). + + +encode_msg_client_unsubscribe_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_client_unsubscribe_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{props := F2} -> + TrF2 = id(F2, TrUserData), + if TrF2 == [] -> B1; + true -> + e_field_client_unsubscribe_request_props(TrF2, + B1, + TrUserData) + end; + _ -> B1 + end, + case M of + #{topic_filters := F3} -> + TrF3 = id(F3, TrUserData), + if TrF3 == [] -> B2; + true -> + e_field_client_unsubscribe_request_topic_filters(TrF3, + B2, + TrUserData) + end; + _ -> B2 + end. + +encode_msg_session_created_request(Msg, TrUserData) -> + encode_msg_session_created_request(Msg, + <<>>, + TrUserData). + + +encode_msg_session_created_request(#{} = M, Bin, + TrUserData) -> + case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_session_created_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end. + +encode_msg_session_subscribed_request(Msg, + TrUserData) -> + encode_msg_session_subscribed_request(Msg, + <<>>, + TrUserData). + + +encode_msg_session_subscribed_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_session_subscribed_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{topic := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end, + case M of + #{subopts := F3} -> + begin + TrF3 = id(F3, TrUserData), + if TrF3 =:= undefined -> B2; + true -> + e_mfield_session_subscribed_request_subopts(TrF3, + <>, + TrUserData) + end + end; + _ -> B2 + end. + +encode_msg_session_unsubscribed_request(Msg, + TrUserData) -> + encode_msg_session_unsubscribed_request(Msg, + <<>>, + TrUserData). + + +encode_msg_session_unsubscribed_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_session_unsubscribed_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{topic := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_session_resumed_request(Msg, TrUserData) -> + encode_msg_session_resumed_request(Msg, + <<>>, + TrUserData). + + +encode_msg_session_resumed_request(#{} = M, Bin, + TrUserData) -> + case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_session_resumed_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end. + +encode_msg_session_discarded_request(Msg, TrUserData) -> + encode_msg_session_discarded_request(Msg, + <<>>, + TrUserData). + + +encode_msg_session_discarded_request(#{} = M, Bin, + TrUserData) -> + case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_session_discarded_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end. + +encode_msg_session_takeovered_request(Msg, + TrUserData) -> + encode_msg_session_takeovered_request(Msg, + <<>>, + TrUserData). + + +encode_msg_session_takeovered_request(#{} = M, Bin, + TrUserData) -> + case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_session_takeovered_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end. + +encode_msg_session_terminated_request(Msg, + TrUserData) -> + encode_msg_session_terminated_request(Msg, + <<>>, + TrUserData). + + +encode_msg_session_terminated_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_session_terminated_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{reason := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_message_publish_request(Msg, TrUserData) -> + encode_msg_message_publish_request(Msg, + <<>>, + TrUserData). + + +encode_msg_message_publish_request(#{} = M, Bin, + TrUserData) -> + case M of + #{message := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_message_publish_request_message(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end. + +encode_msg_message_delivered_request(Msg, TrUserData) -> + encode_msg_message_delivered_request(Msg, + <<>>, + TrUserData). + + +encode_msg_message_delivered_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_message_delivered_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{message := F2} -> + begin + TrF2 = id(F2, TrUserData), + if TrF2 =:= undefined -> B1; + true -> + e_mfield_message_delivered_request_message(TrF2, + <>, + TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_message_dropped_request(Msg, TrUserData) -> + encode_msg_message_dropped_request(Msg, + <<>>, + TrUserData). + + +encode_msg_message_dropped_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{message := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_message_dropped_request_message(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{reason := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_message_acked_request(Msg, TrUserData) -> + encode_msg_message_acked_request(Msg, <<>>, TrUserData). + + +encode_msg_message_acked_request(#{} = M, Bin, + TrUserData) -> + B1 = case M of + #{clientinfo := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= undefined -> Bin; + true -> + e_mfield_message_acked_request_clientinfo(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{message := F2} -> + begin + TrF2 = id(F2, TrUserData), + if TrF2 =:= undefined -> B1; + true -> + e_mfield_message_acked_request_message(TrF2, + <>, + TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_empty_success(_Msg, _TrUserData) -> <<>>. + +encode_msg_valued_response(Msg, TrUserData) -> + encode_msg_valued_response(Msg, <<>>, TrUserData). + + +encode_msg_valued_response(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{type := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= 'CONTINUE'; TrF1 =:= 0 -> Bin; + true -> + 'e_enum_valued_response.ResponsedType'(TrF1, + <>, + TrUserData) + end + end; + _ -> Bin + end, + case M of + #{value := F2} -> + case id(F2, TrUserData) of + {bool_result, TF2} -> + begin + TrTF2 = id(TF2, TrUserData), + e_type_bool(TrTF2, <>, TrUserData) + end; + {message, TF2} -> + begin + TrTF2 = id(TF2, TrUserData), + e_mfield_valued_response_message(TrTF2, + <>, + TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_broker_info(Msg, TrUserData) -> + encode_msg_broker_info(Msg, <<>>, TrUserData). + + +encode_msg_broker_info(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{version := F1} -> + begin + TrF1 = id(F1, TrUserData), + case is_empty_string(TrF1) of + true -> Bin; + false -> + e_type_string(TrF1, <>, TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{sysdescr := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end, + B3 = case M of + #{uptime := F3} -> + begin + TrF3 = id(F3, TrUserData), + if TrF3 =:= 0 -> B2; + true -> + e_type_int64(TrF3, <>, TrUserData) + end + end; + _ -> B2 + end, + case M of + #{datetime := F4} -> + begin + TrF4 = id(F4, TrUserData), + case is_empty_string(TrF4) of + true -> B3; + false -> + e_type_string(TrF4, <>, TrUserData) + end + end; + _ -> B3 + end. + +encode_msg_hook_spec(Msg, TrUserData) -> + encode_msg_hook_spec(Msg, <<>>, TrUserData). + + +encode_msg_hook_spec(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{name := F1} -> + begin + TrF1 = id(F1, TrUserData), + case is_empty_string(TrF1) of + true -> Bin; + false -> + e_type_string(TrF1, <>, TrUserData) + end + end; + _ -> Bin + end, + case M of + #{topics := F2} -> + TrF2 = id(F2, TrUserData), + if TrF2 == [] -> B1; + true -> e_field_hook_spec_topics(TrF2, B1, TrUserData) + end; + _ -> B1 + end. + +encode_msg_conn_info(Msg, TrUserData) -> + encode_msg_conn_info(Msg, <<>>, TrUserData). + + +encode_msg_conn_info(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{node := F1} -> + begin + TrF1 = id(F1, TrUserData), + case is_empty_string(TrF1) of + true -> Bin; + false -> + e_type_string(TrF1, <>, TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{clientid := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end, + B3 = case M of + #{username := F3} -> + begin + TrF3 = id(F3, TrUserData), + case is_empty_string(TrF3) of + true -> B2; + false -> + e_type_string(TrF3, <>, TrUserData) + end + end; + _ -> B2 + end, + B4 = case M of + #{peerhost := F4} -> + begin + TrF4 = id(F4, TrUserData), + case is_empty_string(TrF4) of + true -> B3; + false -> + e_type_string(TrF4, <>, TrUserData) + end + end; + _ -> B3 + end, + B5 = case M of + #{sockport := F5} -> + begin + TrF5 = id(F5, TrUserData), + if TrF5 =:= 0 -> B4; + true -> e_varint(TrF5, <>, TrUserData) + end + end; + _ -> B4 + end, + B6 = case M of + #{proto_name := F6} -> + begin + TrF6 = id(F6, TrUserData), + case is_empty_string(TrF6) of + true -> B5; + false -> + e_type_string(TrF6, <>, TrUserData) + end + end; + _ -> B5 + end, + B7 = case M of + #{proto_ver := F7} -> + begin + TrF7 = id(F7, TrUserData), + case is_empty_string(TrF7) of + true -> B6; + false -> + e_type_string(TrF7, <>, TrUserData) + end + end; + _ -> B6 + end, + case M of + #{keepalive := F8} -> + begin + TrF8 = id(F8, TrUserData), + if TrF8 =:= 0 -> B7; + true -> e_varint(TrF8, <>, TrUserData) + end + end; + _ -> B7 + end. + +encode_msg_client_info(Msg, TrUserData) -> + encode_msg_client_info(Msg, <<>>, TrUserData). + + +encode_msg_client_info(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{node := F1} -> + begin + TrF1 = id(F1, TrUserData), + case is_empty_string(TrF1) of + true -> Bin; + false -> + e_type_string(TrF1, <>, TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{clientid := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end, + B3 = case M of + #{username := F3} -> + begin + TrF3 = id(F3, TrUserData), + case is_empty_string(TrF3) of + true -> B2; + false -> + e_type_string(TrF3, <>, TrUserData) + end + end; + _ -> B2 + end, + B4 = case M of + #{password := F4} -> + begin + TrF4 = id(F4, TrUserData), + case is_empty_string(TrF4) of + true -> B3; + false -> + e_type_string(TrF4, <>, TrUserData) + end + end; + _ -> B3 + end, + B5 = case M of + #{peerhost := F5} -> + begin + TrF5 = id(F5, TrUserData), + case is_empty_string(TrF5) of + true -> B4; + false -> + e_type_string(TrF5, <>, TrUserData) + end + end; + _ -> B4 + end, + B6 = case M of + #{sockport := F6} -> + begin + TrF6 = id(F6, TrUserData), + if TrF6 =:= 0 -> B5; + true -> e_varint(TrF6, <>, TrUserData) + end + end; + _ -> B5 + end, + B7 = case M of + #{protocol := F7} -> + begin + TrF7 = id(F7, TrUserData), + case is_empty_string(TrF7) of + true -> B6; + false -> + e_type_string(TrF7, <>, TrUserData) + end + end; + _ -> B6 + end, + B8 = case M of + #{mountpoint := F8} -> + begin + TrF8 = id(F8, TrUserData), + case is_empty_string(TrF8) of + true -> B7; + false -> + e_type_string(TrF8, <>, TrUserData) + end + end; + _ -> B7 + end, + B9 = case M of + #{is_superuser := F9} -> + begin + TrF9 = id(F9, TrUserData), + if TrF9 =:= false -> B8; + true -> e_type_bool(TrF9, <>, TrUserData) + end + end; + _ -> B8 + end, + B10 = case M of + #{anonymous := F10} -> + begin + TrF10 = id(F10, TrUserData), + if TrF10 =:= false -> B9; + true -> + e_type_bool(TrF10, <>, TrUserData) + end + end; + _ -> B9 + end, + B11 = case M of + #{cn := F11} -> + begin + TrF11 = id(F11, TrUserData), + case is_empty_string(TrF11) of + true -> B10; + false -> + e_type_string(TrF11, + <>, + TrUserData) + end + end; + _ -> B10 + end, + case M of + #{dn := F12} -> + begin + TrF12 = id(F12, TrUserData), + case is_empty_string(TrF12) of + true -> B11; + false -> + e_type_string(TrF12, <>, TrUserData) + end + end; + _ -> B11 + end. + +encode_msg_message(Msg, TrUserData) -> + encode_msg_message(Msg, <<>>, TrUserData). + + +encode_msg_message(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{node := F1} -> + begin + TrF1 = id(F1, TrUserData), + case is_empty_string(TrF1) of + true -> Bin; + false -> + e_type_string(TrF1, <>, TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{id := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end, + B3 = case M of + #{qos := F3} -> + begin + TrF3 = id(F3, TrUserData), + if TrF3 =:= 0 -> B2; + true -> e_varint(TrF3, <>, TrUserData) + end + end; + _ -> B2 + end, + B4 = case M of + #{from := F4} -> + begin + TrF4 = id(F4, TrUserData), + case is_empty_string(TrF4) of + true -> B3; + false -> + e_type_string(TrF4, <>, TrUserData) + end + end; + _ -> B3 + end, + B5 = case M of + #{topic := F5} -> + begin + TrF5 = id(F5, TrUserData), + case is_empty_string(TrF5) of + true -> B4; + false -> + e_type_string(TrF5, <>, TrUserData) + end + end; + _ -> B4 + end, + B6 = case M of + #{payload := F6} -> + begin + TrF6 = id(F6, TrUserData), + case iolist_size(TrF6) of + 0 -> B5; + _ -> e_type_bytes(TrF6, <>, TrUserData) + end + end; + _ -> B5 + end, + case M of + #{timestamp := F7} -> + begin + TrF7 = id(F7, TrUserData), + if TrF7 =:= 0 -> B6; + true -> e_varint(TrF7, <>, TrUserData) + end + end; + _ -> B6 + end. + +encode_msg_property(Msg, TrUserData) -> + encode_msg_property(Msg, <<>>, TrUserData). + + +encode_msg_property(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{name := F1} -> + begin + TrF1 = id(F1, TrUserData), + case is_empty_string(TrF1) of + true -> Bin; + false -> + e_type_string(TrF1, <>, TrUserData) + end + end; + _ -> Bin + end, + case M of + #{value := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_topic_filter(Msg, TrUserData) -> + encode_msg_topic_filter(Msg, <<>>, TrUserData). + + +encode_msg_topic_filter(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{name := F1} -> + begin + TrF1 = id(F1, TrUserData), + case is_empty_string(TrF1) of + true -> Bin; + false -> + e_type_string(TrF1, <>, TrUserData) + end + end; + _ -> Bin + end, + case M of + #{qos := F2} -> + begin + TrF2 = id(F2, TrUserData), + if TrF2 =:= 0 -> B1; + true -> e_varint(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end. + +encode_msg_sub_opts(Msg, TrUserData) -> + encode_msg_sub_opts(Msg, <<>>, TrUserData). + + +encode_msg_sub_opts(#{} = M, Bin, TrUserData) -> + B1 = case M of + #{qos := F1} -> + begin + TrF1 = id(F1, TrUserData), + if TrF1 =:= 0 -> Bin; + true -> e_varint(TrF1, <>, TrUserData) + end + end; + _ -> Bin + end, + B2 = case M of + #{share := F2} -> + begin + TrF2 = id(F2, TrUserData), + case is_empty_string(TrF2) of + true -> B1; + false -> + e_type_string(TrF2, <>, TrUserData) + end + end; + _ -> B1 + end, + B3 = case M of + #{rh := F3} -> + begin + TrF3 = id(F3, TrUserData), + if TrF3 =:= 0 -> B2; + true -> e_varint(TrF3, <>, TrUserData) + end + end; + _ -> B2 + end, + B4 = case M of + #{rap := F4} -> + begin + TrF4 = id(F4, TrUserData), + if TrF4 =:= 0 -> B3; + true -> e_varint(TrF4, <>, TrUserData) + end + end; + _ -> B3 + end, + case M of + #{nl := F5} -> + begin + TrF5 = id(F5, TrUserData), + if TrF5 =:= 0 -> B4; + true -> e_varint(TrF5, <>, TrUserData) + end + end; + _ -> B4 + end. + +e_mfield_provider_loaded_request_broker(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_broker_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_loaded_response_hooks(Msg, Bin, TrUserData) -> + SubBin = encode_msg_hook_spec(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_field_loaded_response_hooks([Elem | Rest], Bin, + TrUserData) -> + Bin2 = <>, + Bin3 = e_mfield_loaded_response_hooks(id(Elem, + TrUserData), + Bin2, + TrUserData), + e_field_loaded_response_hooks(Rest, Bin3, TrUserData); +e_field_loaded_response_hooks([], Bin, _TrUserData) -> + Bin. + +e_mfield_client_connect_request_conninfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_conn_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_client_connect_request_props(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_property(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_field_client_connect_request_props([Elem | Rest], Bin, + TrUserData) -> + Bin2 = <>, + Bin3 = e_mfield_client_connect_request_props(id(Elem, + TrUserData), + Bin2, + TrUserData), + e_field_client_connect_request_props(Rest, + Bin3, + TrUserData); +e_field_client_connect_request_props([], Bin, + _TrUserData) -> + Bin. + +e_mfield_client_connack_request_conninfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_conn_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_client_connack_request_props(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_property(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_field_client_connack_request_props([Elem | Rest], Bin, + TrUserData) -> + Bin2 = <>, + Bin3 = e_mfield_client_connack_request_props(id(Elem, + TrUserData), + Bin2, + TrUserData), + e_field_client_connack_request_props(Rest, + Bin3, + TrUserData); +e_field_client_connack_request_props([], Bin, + _TrUserData) -> + Bin. + +e_mfield_client_connected_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_client_disconnected_request_clientinfo(Msg, + Bin, TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_client_authenticate_request_clientinfo(Msg, + Bin, TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_client_authorize_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_client_subscribe_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_client_subscribe_request_props(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_property(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_field_client_subscribe_request_props([Elem | Rest], + Bin, TrUserData) -> + Bin2 = <>, + Bin3 = e_mfield_client_subscribe_request_props(id(Elem, + TrUserData), + Bin2, + TrUserData), + e_field_client_subscribe_request_props(Rest, + Bin3, + TrUserData); +e_field_client_subscribe_request_props([], Bin, + _TrUserData) -> + Bin. + +e_mfield_client_subscribe_request_topic_filters(Msg, + Bin, TrUserData) -> + SubBin = encode_msg_topic_filter(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_field_client_subscribe_request_topic_filters([Elem + | Rest], + Bin, TrUserData) -> + Bin2 = <>, + Bin3 = + e_mfield_client_subscribe_request_topic_filters(id(Elem, + TrUserData), + Bin2, + TrUserData), + e_field_client_subscribe_request_topic_filters(Rest, + Bin3, + TrUserData); +e_field_client_subscribe_request_topic_filters([], Bin, + _TrUserData) -> + Bin. + +e_mfield_client_unsubscribe_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_client_unsubscribe_request_props(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_property(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_field_client_unsubscribe_request_props([Elem | Rest], + Bin, TrUserData) -> + Bin2 = <>, + Bin3 = + e_mfield_client_unsubscribe_request_props(id(Elem, + TrUserData), + Bin2, + TrUserData), + e_field_client_unsubscribe_request_props(Rest, + Bin3, + TrUserData); +e_field_client_unsubscribe_request_props([], Bin, + _TrUserData) -> + Bin. + +e_mfield_client_unsubscribe_request_topic_filters(Msg, + Bin, TrUserData) -> + SubBin = encode_msg_topic_filter(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_field_client_unsubscribe_request_topic_filters([Elem + | Rest], + Bin, TrUserData) -> + Bin2 = <>, + Bin3 = + e_mfield_client_unsubscribe_request_topic_filters(id(Elem, + TrUserData), + Bin2, + TrUserData), + e_field_client_unsubscribe_request_topic_filters(Rest, + Bin3, + TrUserData); +e_field_client_unsubscribe_request_topic_filters([], + Bin, _TrUserData) -> + Bin. + +e_mfield_session_created_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_session_subscribed_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_session_subscribed_request_subopts(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_sub_opts(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_session_unsubscribed_request_clientinfo(Msg, + Bin, TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_session_resumed_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_session_discarded_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_session_takeovered_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_session_terminated_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_message_publish_request_message(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_message(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_message_delivered_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_message_delivered_request_message(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_message(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_message_dropped_request_message(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_message(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_message_acked_request_clientinfo(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_client_info(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_message_acked_request_message(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_message(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_mfield_valued_response_message(Msg, Bin, + TrUserData) -> + SubBin = encode_msg_message(Msg, <<>>, TrUserData), + Bin2 = e_varint(byte_size(SubBin), Bin), + <>. + +e_field_hook_spec_topics([Elem | Rest], Bin, + TrUserData) -> + Bin2 = <>, + Bin3 = e_type_string(id(Elem, TrUserData), + Bin2, + TrUserData), + e_field_hook_spec_topics(Rest, Bin3, TrUserData); +e_field_hook_spec_topics([], Bin, _TrUserData) -> Bin. + +'e_enum_client_authorize_request.AuthorizeReqType'('PUBLISH', + Bin, _TrUserData) -> + <>; +'e_enum_client_authorize_request.AuthorizeReqType'('SUBSCRIBE', + Bin, _TrUserData) -> + <>; +'e_enum_client_authorize_request.AuthorizeReqType'(V, + Bin, _TrUserData) -> + e_varint(V, Bin). + +'e_enum_valued_response.ResponsedType'('CONTINUE', Bin, + _TrUserData) -> + <>; +'e_enum_valued_response.ResponsedType'('IGNORE', Bin, + _TrUserData) -> + <>; +'e_enum_valued_response.ResponsedType'('STOP_AND_RETURN', + Bin, _TrUserData) -> + <>; +'e_enum_valued_response.ResponsedType'(V, Bin, + _TrUserData) -> + e_varint(V, Bin). + +-compile({nowarn_unused_function,e_type_sint/3}). +e_type_sint(Value, Bin, _TrUserData) when Value >= 0 -> + e_varint(Value * 2, Bin); +e_type_sint(Value, Bin, _TrUserData) -> + e_varint(Value * -2 - 1, Bin). + +-compile({nowarn_unused_function,e_type_int32/3}). +e_type_int32(Value, Bin, _TrUserData) + when 0 =< Value, Value =< 127 -> + <>; +e_type_int32(Value, Bin, _TrUserData) -> + <> = <>, + e_varint(N, Bin). + +-compile({nowarn_unused_function,e_type_int64/3}). +e_type_int64(Value, Bin, _TrUserData) + when 0 =< Value, Value =< 127 -> + <>; +e_type_int64(Value, Bin, _TrUserData) -> + <> = <>, + e_varint(N, Bin). + +-compile({nowarn_unused_function,e_type_bool/3}). +e_type_bool(true, Bin, _TrUserData) -> + <>; +e_type_bool(false, Bin, _TrUserData) -> + <>; +e_type_bool(1, Bin, _TrUserData) -> <>; +e_type_bool(0, Bin, _TrUserData) -> <>. + +-compile({nowarn_unused_function,e_type_string/3}). +e_type_string(S, Bin, _TrUserData) -> + Utf8 = unicode:characters_to_binary(S), + Bin2 = e_varint(byte_size(Utf8), Bin), + <>. + +-compile({nowarn_unused_function,e_type_bytes/3}). +e_type_bytes(Bytes, Bin, _TrUserData) + when is_binary(Bytes) -> + Bin2 = e_varint(byte_size(Bytes), Bin), + <>; +e_type_bytes(Bytes, Bin, _TrUserData) + when is_list(Bytes) -> + BytesBin = iolist_to_binary(Bytes), + Bin2 = e_varint(byte_size(BytesBin), Bin), + <>. + +-compile({nowarn_unused_function,e_type_fixed32/3}). +e_type_fixed32(Value, Bin, _TrUserData) -> + <>. + +-compile({nowarn_unused_function,e_type_sfixed32/3}). +e_type_sfixed32(Value, Bin, _TrUserData) -> + <>. + +-compile({nowarn_unused_function,e_type_fixed64/3}). +e_type_fixed64(Value, Bin, _TrUserData) -> + <>. + +-compile({nowarn_unused_function,e_type_sfixed64/3}). +e_type_sfixed64(Value, Bin, _TrUserData) -> + <>. + +-compile({nowarn_unused_function,e_type_float/3}). +e_type_float(V, Bin, _) when is_number(V) -> + <>; +e_type_float(infinity, Bin, _) -> + <>; +e_type_float('-infinity', Bin, _) -> + <>; +e_type_float(nan, Bin, _) -> + <>. + +-compile({nowarn_unused_function,e_type_double/3}). +e_type_double(V, Bin, _) when is_number(V) -> + <>; +e_type_double(infinity, Bin, _) -> + <>; +e_type_double('-infinity', Bin, _) -> + <>; +e_type_double(nan, Bin, _) -> + <>. + +-compile({nowarn_unused_function,e_varint/3}). +e_varint(N, Bin, _TrUserData) -> e_varint(N, Bin). + +-compile({nowarn_unused_function,e_varint/2}). +e_varint(N, Bin) when N =< 127 -> <>; +e_varint(N, Bin) -> + Bin2 = <>, + e_varint(N bsr 7, Bin2). + +is_empty_string("") -> true; +is_empty_string(<<>>) -> true; +is_empty_string(L) when is_list(L) -> + not string_has_chars(L); +is_empty_string(B) when is_binary(B) -> false. + +string_has_chars([C | _]) when is_integer(C) -> true; +string_has_chars([H | T]) -> + case string_has_chars(H) of + true -> true; + false -> string_has_chars(T) + end; +string_has_chars(B) + when is_binary(B), byte_size(B) =/= 0 -> + true; +string_has_chars(C) when is_integer(C) -> true; +string_has_chars(<<>>) -> false; +string_has_chars([]) -> false. + + +decode_msg(Bin, MsgName) when is_binary(Bin) -> + decode_msg(Bin, MsgName, []). + +decode_msg(Bin, MsgName, Opts) when is_binary(Bin) -> + TrUserData = proplists:get_value(user_data, Opts), + decode_msg_1_catch(Bin, MsgName, TrUserData). + +-ifdef('OTP_RELEASE'). +decode_msg_1_catch(Bin, MsgName, TrUserData) -> + try decode_msg_2_doit(MsgName, Bin, TrUserData) + catch Class:Reason:StackTrace -> error({gpb_error,{decoding_failure, {Bin, MsgName, {Class, Reason, StackTrace}}}}) + end. +-else. +decode_msg_1_catch(Bin, MsgName, TrUserData) -> + try decode_msg_2_doit(MsgName, Bin, TrUserData) + catch Class:Reason -> + StackTrace = erlang:get_stacktrace(), + error({gpb_error,{decoding_failure, {Bin, MsgName, {Class, Reason, StackTrace}}}}) + end. +-endif. + +decode_msg_2_doit(provider_loaded_request, Bin, + TrUserData) -> + id(decode_msg_provider_loaded_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(loaded_response, Bin, TrUserData) -> + id(decode_msg_loaded_response(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(provider_unloaded_request, Bin, + TrUserData) -> + id(decode_msg_provider_unloaded_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(client_connect_request, Bin, + TrUserData) -> + id(decode_msg_client_connect_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(client_connack_request, Bin, + TrUserData) -> + id(decode_msg_client_connack_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(client_connected_request, Bin, + TrUserData) -> + id(decode_msg_client_connected_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(client_disconnected_request, Bin, + TrUserData) -> + id(decode_msg_client_disconnected_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(client_authenticate_request, Bin, + TrUserData) -> + id(decode_msg_client_authenticate_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(client_authorize_request, Bin, + TrUserData) -> + id(decode_msg_client_authorize_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(client_subscribe_request, Bin, + TrUserData) -> + id(decode_msg_client_subscribe_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(client_unsubscribe_request, Bin, + TrUserData) -> + id(decode_msg_client_unsubscribe_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(session_created_request, Bin, + TrUserData) -> + id(decode_msg_session_created_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(session_subscribed_request, Bin, + TrUserData) -> + id(decode_msg_session_subscribed_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(session_unsubscribed_request, Bin, + TrUserData) -> + id(decode_msg_session_unsubscribed_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(session_resumed_request, Bin, + TrUserData) -> + id(decode_msg_session_resumed_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(session_discarded_request, Bin, + TrUserData) -> + id(decode_msg_session_discarded_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(session_takeovered_request, Bin, + TrUserData) -> + id(decode_msg_session_takeovered_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(session_terminated_request, Bin, + TrUserData) -> + id(decode_msg_session_terminated_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(message_publish_request, Bin, + TrUserData) -> + id(decode_msg_message_publish_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(message_delivered_request, Bin, + TrUserData) -> + id(decode_msg_message_delivered_request(Bin, + TrUserData), + TrUserData); +decode_msg_2_doit(message_dropped_request, Bin, + TrUserData) -> + id(decode_msg_message_dropped_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(message_acked_request, Bin, + TrUserData) -> + id(decode_msg_message_acked_request(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(empty_success, Bin, TrUserData) -> + id(decode_msg_empty_success(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(valued_response, Bin, TrUserData) -> + id(decode_msg_valued_response(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(broker_info, Bin, TrUserData) -> + id(decode_msg_broker_info(Bin, TrUserData), TrUserData); +decode_msg_2_doit(hook_spec, Bin, TrUserData) -> + id(decode_msg_hook_spec(Bin, TrUserData), TrUserData); +decode_msg_2_doit(conn_info, Bin, TrUserData) -> + id(decode_msg_conn_info(Bin, TrUserData), TrUserData); +decode_msg_2_doit(client_info, Bin, TrUserData) -> + id(decode_msg_client_info(Bin, TrUserData), TrUserData); +decode_msg_2_doit(message, Bin, TrUserData) -> + id(decode_msg_message(Bin, TrUserData), TrUserData); +decode_msg_2_doit(property, Bin, TrUserData) -> + id(decode_msg_property(Bin, TrUserData), TrUserData); +decode_msg_2_doit(topic_filter, Bin, TrUserData) -> + id(decode_msg_topic_filter(Bin, TrUserData), + TrUserData); +decode_msg_2_doit(sub_opts, Bin, TrUserData) -> + id(decode_msg_sub_opts(Bin, TrUserData), TrUserData). + + + +decode_msg_provider_loaded_request(Bin, TrUserData) -> + dfp_read_field_def_provider_loaded_request(Bin, + 0, + 0, + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_provider_loaded_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + d_field_provider_loaded_request_broker(Rest, + Z1, + Z2, + F@_1, + TrUserData); +dfp_read_field_def_provider_loaded_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{broker => F@_1} + end; +dfp_read_field_def_provider_loaded_request(Other, Z1, + Z2, F@_1, TrUserData) -> + dg_read_field_def_provider_loaded_request(Other, + Z1, + Z2, + F@_1, + TrUserData). + +dg_read_field_def_provider_loaded_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_provider_loaded_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +dg_read_field_def_provider_loaded_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_provider_loaded_request_broker(Rest, + 0, + 0, + F@_1, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_provider_loaded_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 1 -> + skip_64_provider_loaded_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 2 -> + skip_length_delimited_provider_loaded_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 3 -> + skip_group_provider_loaded_request(Rest, + Key bsr 3, + 0, + F@_1, + TrUserData); + 5 -> + skip_32_provider_loaded_request(Rest, + 0, + 0, + F@_1, + TrUserData) + end + end; +dg_read_field_def_provider_loaded_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{broker => F@_1} + end. + +d_field_provider_loaded_request_broker(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + d_field_provider_loaded_request_broker(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +d_field_provider_loaded_request_broker(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_broker_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_provider_loaded_request(RestF, + 0, + 0, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_broker_info(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_provider_loaded_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + skip_varint_provider_loaded_request(Rest, + Z1, + Z2, + F@_1, + TrUserData); +skip_varint_provider_loaded_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_provider_loaded_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_length_delimited_provider_loaded_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + skip_length_delimited_provider_loaded_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +skip_length_delimited_provider_loaded_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_provider_loaded_request(Rest2, + 0, + 0, + F@_1, + TrUserData). + +skip_group_provider_loaded_request(Bin, FNum, Z2, F@_1, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_provider_loaded_request(Rest, + 0, + Z2, + F@_1, + TrUserData). + +skip_32_provider_loaded_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_provider_loaded_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_64_provider_loaded_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_provider_loaded_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +decode_msg_loaded_response(Bin, TrUserData) -> + dfp_read_field_def_loaded_response(Bin, + 0, + 0, + id([], TrUserData), + TrUserData). + +dfp_read_field_def_loaded_response(<<10, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + d_field_loaded_response_hooks(Rest, + Z1, + Z2, + F@_1, + TrUserData); +dfp_read_field_def_loaded_response(<<>>, 0, 0, R1, + TrUserData) -> + S1 = #{}, + if R1 == '$undef' -> S1; + true -> S1#{hooks => lists_reverse(R1, TrUserData)} + end; +dfp_read_field_def_loaded_response(Other, Z1, Z2, F@_1, + TrUserData) -> + dg_read_field_def_loaded_response(Other, + Z1, + Z2, + F@_1, + TrUserData). + +dg_read_field_def_loaded_response(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_loaded_response(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +dg_read_field_def_loaded_response(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_loaded_response_hooks(Rest, + 0, + 0, + F@_1, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_loaded_response(Rest, + 0, + 0, + F@_1, + TrUserData); + 1 -> + skip_64_loaded_response(Rest, 0, 0, F@_1, TrUserData); + 2 -> + skip_length_delimited_loaded_response(Rest, + 0, + 0, + F@_1, + TrUserData); + 3 -> + skip_group_loaded_response(Rest, + Key bsr 3, + 0, + F@_1, + TrUserData); + 5 -> + skip_32_loaded_response(Rest, 0, 0, F@_1, TrUserData) + end + end; +dg_read_field_def_loaded_response(<<>>, 0, 0, R1, + TrUserData) -> + S1 = #{}, + if R1 == '$undef' -> S1; + true -> S1#{hooks => lists_reverse(R1, TrUserData)} + end. + +d_field_loaded_response_hooks(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + d_field_loaded_response_hooks(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +d_field_loaded_response_hooks(<<0:1, X:7, Rest/binary>>, + N, Acc, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_hook_spec(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_loaded_response(RestF, + 0, + 0, + cons(NewFValue, Prev, TrUserData), + TrUserData). + +skip_varint_loaded_response(<<1:1, _:7, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + skip_varint_loaded_response(Rest, + Z1, + Z2, + F@_1, + TrUserData); +skip_varint_loaded_response(<<0:1, _:7, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_loaded_response(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_length_delimited_loaded_response(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + skip_length_delimited_loaded_response(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +skip_length_delimited_loaded_response(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_loaded_response(Rest2, + 0, + 0, + F@_1, + TrUserData). + +skip_group_loaded_response(Bin, FNum, Z2, F@_1, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_loaded_response(Rest, + 0, + Z2, + F@_1, + TrUserData). + +skip_32_loaded_response(<<_:32, Rest/binary>>, Z1, Z2, + F@_1, TrUserData) -> + dfp_read_field_def_loaded_response(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_64_loaded_response(<<_:64, Rest/binary>>, Z1, Z2, + F@_1, TrUserData) -> + dfp_read_field_def_loaded_response(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +decode_msg_provider_unloaded_request(Bin, TrUserData) -> + dfp_read_field_def_provider_unloaded_request(Bin, + 0, + 0, + TrUserData). + +dfp_read_field_def_provider_unloaded_request(<<>>, 0, 0, + _) -> + #{}; +dfp_read_field_def_provider_unloaded_request(Other, Z1, + Z2, TrUserData) -> + dg_read_field_def_provider_unloaded_request(Other, + Z1, + Z2, + TrUserData). + +dg_read_field_def_provider_unloaded_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_provider_unloaded_request(Rest, + N + 7, + X bsl N + Acc, + TrUserData); +dg_read_field_def_provider_unloaded_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, TrUserData) -> + Key = X bsl N + Acc, + case Key band 7 of + 0 -> + skip_varint_provider_unloaded_request(Rest, + 0, + 0, + TrUserData); + 1 -> + skip_64_provider_unloaded_request(Rest, + 0, + 0, + TrUserData); + 2 -> + skip_length_delimited_provider_unloaded_request(Rest, + 0, + 0, + TrUserData); + 3 -> + skip_group_provider_unloaded_request(Rest, + Key bsr 3, + 0, + TrUserData); + 5 -> + skip_32_provider_unloaded_request(Rest, + 0, + 0, + TrUserData) + end; +dg_read_field_def_provider_unloaded_request(<<>>, 0, 0, + _) -> + #{}. + +skip_varint_provider_unloaded_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, TrUserData) -> + skip_varint_provider_unloaded_request(Rest, + Z1, + Z2, + TrUserData); +skip_varint_provider_unloaded_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, TrUserData) -> + dfp_read_field_def_provider_unloaded_request(Rest, + Z1, + Z2, + TrUserData). + +skip_length_delimited_provider_unloaded_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, TrUserData) + when N < 57 -> + skip_length_delimited_provider_unloaded_request(Rest, + N + 7, + X bsl N + Acc, + TrUserData); +skip_length_delimited_provider_unloaded_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_provider_unloaded_request(Rest2, + 0, + 0, + TrUserData). + +skip_group_provider_unloaded_request(Bin, FNum, Z2, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_provider_unloaded_request(Rest, + 0, + Z2, + TrUserData). + +skip_32_provider_unloaded_request(<<_:32, Rest/binary>>, + Z1, Z2, TrUserData) -> + dfp_read_field_def_provider_unloaded_request(Rest, + Z1, + Z2, + TrUserData). + +skip_64_provider_unloaded_request(<<_:64, Rest/binary>>, + Z1, Z2, TrUserData) -> + dfp_read_field_def_provider_unloaded_request(Rest, + Z1, + Z2, + TrUserData). + +decode_msg_client_connect_request(Bin, TrUserData) -> + dfp_read_field_def_client_connect_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id([], TrUserData), + TrUserData). + +dfp_read_field_def_client_connect_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_client_connect_request_conninfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_client_connect_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_client_connect_request_props(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_client_connect_request(<<>>, 0, 0, + F@_1, R1, TrUserData) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{conninfo => F@_1} + end, + if R1 == '$undef' -> S2; + true -> S2#{props => lists_reverse(R1, TrUserData)} + end; +dfp_read_field_def_client_connect_request(Other, Z1, Z2, + F@_1, F@_2, TrUserData) -> + dg_read_field_def_client_connect_request(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_client_connect_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_connect_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_client_connect_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_connect_request_conninfo(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_client_connect_request_props(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_connect_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_client_connect_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_client_connect_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_client_connect_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_client_connect_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_client_connect_request(<<>>, 0, 0, + F@_1, R1, TrUserData) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{conninfo => F@_1} + end, + if R1 == '$undef' -> S2; + true -> S2#{props => lists_reverse(R1, TrUserData)} + end. + +d_field_client_connect_request_conninfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_client_connect_request_conninfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_client_connect_request_conninfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, F@_2, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_conn_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_connect_request(RestF, + 0, + 0, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_conn_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + TrUserData). + +d_field_client_connect_request_props(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_client_connect_request_props(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_client_connect_request_props(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_property(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_connect_request(RestF, + 0, + 0, + F@_1, + cons(NewFValue, Prev, TrUserData), + TrUserData). + +skip_varint_client_connect_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_client_connect_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_client_connect_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_connect_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_client_connect_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_client_connect_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_client_connect_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_connect_request(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_client_connect_request(Bin, FNum, Z2, F@_1, + F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_connect_request(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_client_connect_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_connect_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_client_connect_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_connect_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_client_connack_request(Bin, TrUserData) -> + dfp_read_field_def_client_connack_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id(<<>>, TrUserData), + id([], TrUserData), + TrUserData). + +dfp_read_field_def_client_connack_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_connack_request_conninfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_connack_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_connack_request_result_code(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_connack_request(<<26, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_connack_request_props(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_connack_request(<<>>, 0, 0, + F@_1, F@_2, R1, TrUserData) -> + S1 = #{result_code => F@_2}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{conninfo => F@_1} + end, + if R1 == '$undef' -> S2; + true -> S2#{props => lists_reverse(R1, TrUserData)} + end; +dfp_read_field_def_client_connack_request(Other, Z1, Z2, + F@_1, F@_2, F@_3, TrUserData) -> + dg_read_field_def_client_connack_request(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +dg_read_field_def_client_connack_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_connack_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +dg_read_field_def_client_connack_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_connack_request_conninfo(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 18 -> + d_field_client_connack_request_result_code(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 26 -> + d_field_client_connack_request_props(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_connack_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 1 -> + skip_64_client_connack_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 2 -> + skip_length_delimited_client_connack_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 3 -> + skip_group_client_connack_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 5 -> + skip_32_client_connack_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData) + end + end; +dg_read_field_def_client_connack_request(<<>>, 0, 0, + F@_1, F@_2, R1, TrUserData) -> + S1 = #{result_code => F@_2}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{conninfo => F@_1} + end, + if R1 == '$undef' -> S2; + true -> S2#{props => lists_reverse(R1, TrUserData)} + end. + +d_field_client_connack_request_conninfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 57 -> + d_field_client_connack_request_conninfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_connack_request_conninfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, F@_2, F@_3, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_conn_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_connack_request(RestF, + 0, + 0, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_conn_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + F@_3, + TrUserData). + +d_field_client_connack_request_result_code(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 57 -> + d_field_client_connack_request_result_code(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_connack_request_result_code(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, F@_3, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_connack_request(RestF, + 0, + 0, + F@_1, + NewFValue, + F@_3, + TrUserData). + +d_field_client_connack_request_props(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 57 -> + d_field_client_connack_request_props(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_connack_request_props(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_property(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_connack_request(RestF, + 0, + 0, + F@_1, + F@_2, + cons(NewFValue, Prev, TrUserData), + TrUserData). + +skip_varint_client_connack_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + skip_varint_client_connack_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +skip_varint_client_connack_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_connack_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_length_delimited_client_connack_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + skip_length_delimited_client_connack_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +skip_length_delimited_client_connack_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_connack_request(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_group_client_connack_request(Bin, FNum, Z2, F@_1, + F@_2, F@_3, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_connack_request(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_32_client_connack_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_connack_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_64_client_connack_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_connack_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +decode_msg_client_connected_request(Bin, TrUserData) -> + dfp_read_field_def_client_connected_request(Bin, + 0, + 0, + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_client_connected_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + d_field_client_connected_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + TrUserData); +dfp_read_field_def_client_connected_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_client_connected_request(Other, Z1, + Z2, F@_1, TrUserData) -> + dg_read_field_def_client_connected_request(Other, + Z1, + Z2, + F@_1, + TrUserData). + +dg_read_field_def_client_connected_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_connected_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +dg_read_field_def_client_connected_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_connected_request_clientinfo(Rest, + 0, + 0, + F@_1, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_connected_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 1 -> + skip_64_client_connected_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 2 -> + skip_length_delimited_client_connected_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 3 -> + skip_group_client_connected_request(Rest, + Key bsr 3, + 0, + F@_1, + TrUserData); + 5 -> + skip_32_client_connected_request(Rest, + 0, + 0, + F@_1, + TrUserData) + end + end; +dg_read_field_def_client_connected_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_client_connected_request_clientinfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + d_field_client_connected_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +d_field_client_connected_request_clientinfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_connected_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_client_connected_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + skip_varint_client_connected_request(Rest, + Z1, + Z2, + F@_1, + TrUserData); +skip_varint_client_connected_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_client_connected_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_length_delimited_client_connected_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + skip_length_delimited_client_connected_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +skip_length_delimited_client_connected_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_connected_request(Rest2, + 0, + 0, + F@_1, + TrUserData). + +skip_group_client_connected_request(Bin, FNum, Z2, F@_1, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_connected_request(Rest, + 0, + Z2, + F@_1, + TrUserData). + +skip_32_client_connected_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_client_connected_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_64_client_connected_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_client_connected_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +decode_msg_client_disconnected_request(Bin, + TrUserData) -> + dfp_read_field_def_client_disconnected_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id(<<>>, TrUserData), + TrUserData). + +dfp_read_field_def_client_disconnected_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + d_field_client_disconnected_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_client_disconnected_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + d_field_client_disconnected_request_reason(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_client_disconnected_request(<<>>, 0, + 0, F@_1, F@_2, _) -> + S1 = #{reason => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_client_disconnected_request(Other, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + dg_read_field_def_client_disconnected_request(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_client_disconnected_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_disconnected_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_client_disconnected_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_disconnected_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_client_disconnected_request_reason(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_disconnected_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_client_disconnected_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_client_disconnected_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_client_disconnected_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_client_disconnected_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_client_disconnected_request(<<>>, 0, + 0, F@_1, F@_2, _) -> + S1 = #{reason => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_client_disconnected_request_clientinfo(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_client_disconnected_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_client_disconnected_request_clientinfo(<<0:1, + X:7, Rest/binary>>, + N, Acc, Prev, F@_2, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_disconnected_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + TrUserData). + +d_field_client_disconnected_request_reason(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_client_disconnected_request_reason(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_client_disconnected_request_reason(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_disconnected_request(RestF, + 0, + 0, + F@_1, + NewFValue, + TrUserData). + +skip_varint_client_disconnected_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_client_disconnected_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_client_disconnected_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_disconnected_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_client_disconnected_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) + when N < 57 -> + skip_length_delimited_client_disconnected_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_client_disconnected_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_disconnected_request(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_client_disconnected_request(Bin, FNum, Z2, + F@_1, F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_disconnected_request(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_client_disconnected_request(<<_:32, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_disconnected_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_client_disconnected_request(<<_:64, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_disconnected_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_client_authenticate_request(Bin, + TrUserData) -> + dfp_read_field_def_client_authenticate_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id(false, TrUserData), + TrUserData). + +dfp_read_field_def_client_authenticate_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + d_field_client_authenticate_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_client_authenticate_request(<<16, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + d_field_client_authenticate_request_result(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_client_authenticate_request(<<>>, 0, + 0, F@_1, F@_2, _) -> + S1 = #{result => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_client_authenticate_request(Other, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + dg_read_field_def_client_authenticate_request(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_client_authenticate_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_authenticate_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_client_authenticate_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_authenticate_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 16 -> + d_field_client_authenticate_request_result(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_authenticate_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_client_authenticate_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_client_authenticate_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_client_authenticate_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_client_authenticate_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_client_authenticate_request(<<>>, 0, + 0, F@_1, F@_2, _) -> + S1 = #{result => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_client_authenticate_request_clientinfo(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_client_authenticate_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_client_authenticate_request_clientinfo(<<0:1, + X:7, Rest/binary>>, + N, Acc, Prev, F@_2, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_authenticate_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + TrUserData). + +d_field_client_authenticate_request_result(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_client_authenticate_request_result(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_client_authenticate_request_result(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc =/= 0, + TrUserData), + Rest}, + dfp_read_field_def_client_authenticate_request(RestF, + 0, + 0, + F@_1, + NewFValue, + TrUserData). + +skip_varint_client_authenticate_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_client_authenticate_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_client_authenticate_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_authenticate_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_client_authenticate_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) + when N < 57 -> + skip_length_delimited_client_authenticate_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_client_authenticate_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_authenticate_request(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_client_authenticate_request(Bin, FNum, Z2, + F@_1, F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_authenticate_request(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_client_authenticate_request(<<_:32, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_authenticate_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_client_authenticate_request(<<_:64, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_client_authenticate_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_client_authorize_request(Bin, TrUserData) -> + dfp_read_field_def_client_authorize_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id('PUBLISH', TrUserData), + id(<<>>, TrUserData), + id(false, TrUserData), + TrUserData). + +dfp_read_field_def_client_authorize_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + d_field_client_authorize_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dfp_read_field_def_client_authorize_request(<<16, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + d_field_client_authorize_request_type(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dfp_read_field_def_client_authorize_request(<<26, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + d_field_client_authorize_request_topic(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dfp_read_field_def_client_authorize_request(<<32, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + d_field_client_authorize_request_result(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dfp_read_field_def_client_authorize_request(<<>>, 0, 0, + F@_1, F@_2, F@_3, F@_4, _) -> + S1 = #{type => F@_2, topic => F@_3, result => F@_4}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_client_authorize_request(Other, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + dg_read_field_def_client_authorize_request(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +dg_read_field_def_client_authorize_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, + TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_authorize_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dg_read_field_def_client_authorize_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_authorize_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 16 -> + d_field_client_authorize_request_type(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 26 -> + d_field_client_authorize_request_topic(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 32 -> + d_field_client_authorize_request_result(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_authorize_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 1 -> + skip_64_client_authorize_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 2 -> + skip_length_delimited_client_authorize_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 3 -> + skip_group_client_authorize_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 5 -> + skip_32_client_authorize_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData) + end + end; +dg_read_field_def_client_authorize_request(<<>>, 0, 0, + F@_1, F@_2, F@_3, F@_4, _) -> + S1 = #{type => F@_2, topic => F@_3, result => F@_4}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_client_authorize_request_clientinfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, + TrUserData) + when N < 57 -> + d_field_client_authorize_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +d_field_client_authorize_request_clientinfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, F@_2, F@_3, F@_4, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_authorize_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + F@_3, + F@_4, + TrUserData). + +d_field_client_authorize_request_type(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, + TrUserData) + when N < 57 -> + d_field_client_authorize_request_type(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +d_field_client_authorize_request_type(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, F@_3, F@_4, + TrUserData) -> + {NewFValue, RestF} = + {id('d_enum_client_authorize_request.AuthorizeReqType'(begin + <> = + <<(X bsl + N + + + Acc):32/unsigned-native>>, + id(Res, + TrUserData) + end), + TrUserData), + Rest}, + dfp_read_field_def_client_authorize_request(RestF, + 0, + 0, + F@_1, + NewFValue, + F@_3, + F@_4, + TrUserData). + +d_field_client_authorize_request_topic(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, + TrUserData) + when N < 57 -> + d_field_client_authorize_request_topic(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +d_field_client_authorize_request_topic(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, _, F@_4, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_authorize_request(RestF, + 0, + 0, + F@_1, + F@_2, + NewFValue, + F@_4, + TrUserData). + +d_field_client_authorize_request_result(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, + TrUserData) + when N < 57 -> + d_field_client_authorize_request_result(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +d_field_client_authorize_request_result(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, _, + TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc =/= 0, + TrUserData), + Rest}, + dfp_read_field_def_client_authorize_request(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + NewFValue, + TrUserData). + +skip_varint_client_authorize_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + skip_varint_client_authorize_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +skip_varint_client_authorize_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + dfp_read_field_def_client_authorize_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +skip_length_delimited_client_authorize_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, + TrUserData) + when N < 57 -> + skip_length_delimited_client_authorize_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +skip_length_delimited_client_authorize_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_authorize_request(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +skip_group_client_authorize_request(Bin, FNum, Z2, F@_1, + F@_2, F@_3, F@_4, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_authorize_request(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +skip_32_client_authorize_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + dfp_read_field_def_client_authorize_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +skip_64_client_authorize_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + dfp_read_field_def_client_authorize_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +decode_msg_client_subscribe_request(Bin, TrUserData) -> + dfp_read_field_def_client_subscribe_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id([], TrUserData), + id([], TrUserData), + TrUserData). + +dfp_read_field_def_client_subscribe_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_subscribe_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_subscribe_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_subscribe_request_props(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_subscribe_request(<<26, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_subscribe_request_topic_filters(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_subscribe_request(<<>>, 0, 0, + F@_1, R1, R2, TrUserData) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + S3 = if R1 == '$undef' -> S2; + true -> S2#{props => lists_reverse(R1, TrUserData)} + end, + if R2 == '$undef' -> S3; + true -> + S3#{topic_filters => lists_reverse(R2, TrUserData)} + end; +dfp_read_field_def_client_subscribe_request(Other, Z1, + Z2, F@_1, F@_2, F@_3, TrUserData) -> + dg_read_field_def_client_subscribe_request(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +dg_read_field_def_client_subscribe_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_subscribe_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +dg_read_field_def_client_subscribe_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_subscribe_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 18 -> + d_field_client_subscribe_request_props(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 26 -> + d_field_client_subscribe_request_topic_filters(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_subscribe_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 1 -> + skip_64_client_subscribe_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 2 -> + skip_length_delimited_client_subscribe_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 3 -> + skip_group_client_subscribe_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 5 -> + skip_32_client_subscribe_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData) + end + end; +dg_read_field_def_client_subscribe_request(<<>>, 0, 0, + F@_1, R1, R2, TrUserData) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + S3 = if R1 == '$undef' -> S2; + true -> S2#{props => lists_reverse(R1, TrUserData)} + end, + if R2 == '$undef' -> S3; + true -> + S3#{topic_filters => lists_reverse(R2, TrUserData)} + end. + +d_field_client_subscribe_request_clientinfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + d_field_client_subscribe_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_subscribe_request_clientinfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, F@_2, F@_3, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_subscribe_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + F@_3, + TrUserData). + +d_field_client_subscribe_request_props(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 57 -> + d_field_client_subscribe_request_props(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_subscribe_request_props(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, Prev, F@_3, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_property(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_subscribe_request(RestF, + 0, + 0, + F@_1, + cons(NewFValue, + Prev, + TrUserData), + F@_3, + TrUserData). + +d_field_client_subscribe_request_topic_filters(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + d_field_client_subscribe_request_topic_filters(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_subscribe_request_topic_filters(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, Prev, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_topic_filter(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_subscribe_request(RestF, + 0, + 0, + F@_1, + F@_2, + cons(NewFValue, + Prev, + TrUserData), + TrUserData). + +skip_varint_client_subscribe_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + skip_varint_client_subscribe_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +skip_varint_client_subscribe_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_subscribe_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_length_delimited_client_subscribe_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + skip_length_delimited_client_subscribe_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +skip_length_delimited_client_subscribe_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_subscribe_request(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_group_client_subscribe_request(Bin, FNum, Z2, F@_1, + F@_2, F@_3, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_subscribe_request(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_32_client_subscribe_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_subscribe_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_64_client_subscribe_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_subscribe_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +decode_msg_client_unsubscribe_request(Bin, + TrUserData) -> + dfp_read_field_def_client_unsubscribe_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id([], TrUserData), + id([], TrUserData), + TrUserData). + +dfp_read_field_def_client_unsubscribe_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_unsubscribe_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_unsubscribe_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_unsubscribe_request_props(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_unsubscribe_request(<<26, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_client_unsubscribe_request_topic_filters(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_client_unsubscribe_request(<<>>, 0, + 0, F@_1, R1, R2, TrUserData) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + S3 = if R1 == '$undef' -> S2; + true -> S2#{props => lists_reverse(R1, TrUserData)} + end, + if R2 == '$undef' -> S3; + true -> + S3#{topic_filters => lists_reverse(R2, TrUserData)} + end; +dfp_read_field_def_client_unsubscribe_request(Other, Z1, + Z2, F@_1, F@_2, F@_3, + TrUserData) -> + dg_read_field_def_client_unsubscribe_request(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +dg_read_field_def_client_unsubscribe_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_unsubscribe_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +dg_read_field_def_client_unsubscribe_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_unsubscribe_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 18 -> + d_field_client_unsubscribe_request_props(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 26 -> + d_field_client_unsubscribe_request_topic_filters(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_unsubscribe_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 1 -> + skip_64_client_unsubscribe_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 2 -> + skip_length_delimited_client_unsubscribe_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 3 -> + skip_group_client_unsubscribe_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 5 -> + skip_32_client_unsubscribe_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData) + end + end; +dg_read_field_def_client_unsubscribe_request(<<>>, 0, 0, + F@_1, R1, R2, TrUserData) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + S3 = if R1 == '$undef' -> S2; + true -> S2#{props => lists_reverse(R1, TrUserData)} + end, + if R2 == '$undef' -> S3; + true -> + S3#{topic_filters => lists_reverse(R2, TrUserData)} + end. + +d_field_client_unsubscribe_request_clientinfo(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + d_field_client_unsubscribe_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_unsubscribe_request_clientinfo(<<0:1, + X:7, Rest/binary>>, + N, Acc, Prev, F@_2, F@_3, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_unsubscribe_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + F@_3, + TrUserData). + +d_field_client_unsubscribe_request_props(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 57 -> + d_field_client_unsubscribe_request_props(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_unsubscribe_request_props(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, Prev, F@_3, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_property(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_unsubscribe_request(RestF, + 0, + 0, + F@_1, + cons(NewFValue, + Prev, + TrUserData), + F@_3, + TrUserData). + +d_field_client_unsubscribe_request_topic_filters(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + d_field_client_unsubscribe_request_topic_filters(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_client_unsubscribe_request_topic_filters(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, Prev, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_topic_filter(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_client_unsubscribe_request(RestF, + 0, + 0, + F@_1, + F@_2, + cons(NewFValue, + Prev, + TrUserData), + TrUserData). + +skip_varint_client_unsubscribe_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + skip_varint_client_unsubscribe_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +skip_varint_client_unsubscribe_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_unsubscribe_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_length_delimited_client_unsubscribe_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + skip_length_delimited_client_unsubscribe_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +skip_length_delimited_client_unsubscribe_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_unsubscribe_request(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_group_client_unsubscribe_request(Bin, FNum, Z2, + F@_1, F@_2, F@_3, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_unsubscribe_request(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_32_client_unsubscribe_request(<<_:32, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_unsubscribe_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_64_client_unsubscribe_request(<<_:64, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_client_unsubscribe_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +decode_msg_session_created_request(Bin, TrUserData) -> + dfp_read_field_def_session_created_request(Bin, + 0, + 0, + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_session_created_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + d_field_session_created_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + TrUserData); +dfp_read_field_def_session_created_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_session_created_request(Other, Z1, + Z2, F@_1, TrUserData) -> + dg_read_field_def_session_created_request(Other, + Z1, + Z2, + F@_1, + TrUserData). + +dg_read_field_def_session_created_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_session_created_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +dg_read_field_def_session_created_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_session_created_request_clientinfo(Rest, + 0, + 0, + F@_1, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_session_created_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 1 -> + skip_64_session_created_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 2 -> + skip_length_delimited_session_created_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 3 -> + skip_group_session_created_request(Rest, + Key bsr 3, + 0, + F@_1, + TrUserData); + 5 -> + skip_32_session_created_request(Rest, + 0, + 0, + F@_1, + TrUserData) + end + end; +dg_read_field_def_session_created_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_session_created_request_clientinfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + d_field_session_created_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +d_field_session_created_request_clientinfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_session_created_request(RestF, + 0, + 0, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_session_created_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + skip_varint_session_created_request(Rest, + Z1, + Z2, + F@_1, + TrUserData); +skip_varint_session_created_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_created_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_length_delimited_session_created_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + skip_length_delimited_session_created_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +skip_length_delimited_session_created_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_session_created_request(Rest2, + 0, + 0, + F@_1, + TrUserData). + +skip_group_session_created_request(Bin, FNum, Z2, F@_1, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_session_created_request(Rest, + 0, + Z2, + F@_1, + TrUserData). + +skip_32_session_created_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_created_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_64_session_created_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_created_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +decode_msg_session_subscribed_request(Bin, + TrUserData) -> + dfp_read_field_def_session_subscribed_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id(<<>>, TrUserData), + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_session_subscribed_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_session_subscribed_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_session_subscribed_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_session_subscribed_request_topic(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_session_subscribed_request(<<26, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, + TrUserData) -> + d_field_session_subscribed_request_subopts(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +dfp_read_field_def_session_subscribed_request(<<>>, 0, + 0, F@_1, F@_2, F@_3, _) -> + S1 = #{topic => F@_2}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + if F@_3 == '$undef' -> S2; + true -> S2#{subopts => F@_3} + end; +dfp_read_field_def_session_subscribed_request(Other, Z1, + Z2, F@_1, F@_2, F@_3, + TrUserData) -> + dg_read_field_def_session_subscribed_request(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +dg_read_field_def_session_subscribed_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 32 - 7 -> + dg_read_field_def_session_subscribed_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +dg_read_field_def_session_subscribed_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_session_subscribed_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 18 -> + d_field_session_subscribed_request_topic(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 26 -> + d_field_session_subscribed_request_subopts(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_session_subscribed_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 1 -> + skip_64_session_subscribed_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 2 -> + skip_length_delimited_session_subscribed_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 3 -> + skip_group_session_subscribed_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + TrUserData); + 5 -> + skip_32_session_subscribed_request(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData) + end + end; +dg_read_field_def_session_subscribed_request(<<>>, 0, 0, + F@_1, F@_2, F@_3, _) -> + S1 = #{topic => F@_2}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + if F@_3 == '$undef' -> S2; + true -> S2#{subopts => F@_3} + end. + +d_field_session_subscribed_request_clientinfo(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + d_field_session_subscribed_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_session_subscribed_request_clientinfo(<<0:1, + X:7, Rest/binary>>, + N, Acc, Prev, F@_2, F@_3, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_session_subscribed_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + F@_3, + TrUserData). + +d_field_session_subscribed_request_topic(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 57 -> + d_field_session_subscribed_request_topic(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_session_subscribed_request_topic(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, F@_3, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_session_subscribed_request(RestF, + 0, + 0, + F@_1, + NewFValue, + F@_3, + TrUserData). + +d_field_session_subscribed_request_subopts(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, TrUserData) + when N < 57 -> + d_field_session_subscribed_request_subopts(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +d_field_session_subscribed_request_subopts(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, Prev, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_sub_opts(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_session_subscribed_request(RestF, + 0, + 0, + F@_1, + F@_2, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_sub_opts(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_session_subscribed_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + skip_varint_session_subscribed_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData); +skip_varint_session_subscribed_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_session_subscribed_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_length_delimited_session_subscribed_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) + when N < 57 -> + skip_length_delimited_session_subscribed_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + TrUserData); +skip_length_delimited_session_subscribed_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_session_subscribed_request(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_group_session_subscribed_request(Bin, FNum, Z2, + F@_1, F@_2, F@_3, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_session_subscribed_request(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_32_session_subscribed_request(<<_:32, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_session_subscribed_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +skip_64_session_subscribed_request(<<_:64, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, F@_3, TrUserData) -> + dfp_read_field_def_session_subscribed_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + TrUserData). + +decode_msg_session_unsubscribed_request(Bin, + TrUserData) -> + dfp_read_field_def_session_unsubscribed_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id(<<>>, TrUserData), + TrUserData). + +dfp_read_field_def_session_unsubscribed_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + d_field_session_unsubscribed_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_session_unsubscribed_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + d_field_session_unsubscribed_request_topic(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_session_unsubscribed_request(<<>>, 0, + 0, F@_1, F@_2, _) -> + S1 = #{topic => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_session_unsubscribed_request(Other, + Z1, Z2, F@_1, F@_2, + TrUserData) -> + dg_read_field_def_session_unsubscribed_request(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_session_unsubscribed_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_session_unsubscribed_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_session_unsubscribed_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_session_unsubscribed_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_session_unsubscribed_request_topic(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_session_unsubscribed_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_session_unsubscribed_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_session_unsubscribed_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_session_unsubscribed_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_session_unsubscribed_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_session_unsubscribed_request(<<>>, 0, + 0, F@_1, F@_2, _) -> + S1 = #{topic => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_session_unsubscribed_request_clientinfo(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_session_unsubscribed_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_session_unsubscribed_request_clientinfo(<<0:1, + X:7, Rest/binary>>, + N, Acc, Prev, F@_2, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_session_unsubscribed_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + TrUserData). + +d_field_session_unsubscribed_request_topic(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_session_unsubscribed_request_topic(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_session_unsubscribed_request_topic(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_session_unsubscribed_request(RestF, + 0, + 0, + F@_1, + NewFValue, + TrUserData). + +skip_varint_session_unsubscribed_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_session_unsubscribed_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_session_unsubscribed_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_session_unsubscribed_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_session_unsubscribed_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) + when N < 57 -> + skip_length_delimited_session_unsubscribed_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_session_unsubscribed_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_session_unsubscribed_request(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_session_unsubscribed_request(Bin, FNum, Z2, + F@_1, F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_session_unsubscribed_request(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_session_unsubscribed_request(<<_:32, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_session_unsubscribed_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_session_unsubscribed_request(<<_:64, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_session_unsubscribed_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_session_resumed_request(Bin, TrUserData) -> + dfp_read_field_def_session_resumed_request(Bin, + 0, + 0, + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_session_resumed_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + d_field_session_resumed_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + TrUserData); +dfp_read_field_def_session_resumed_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_session_resumed_request(Other, Z1, + Z2, F@_1, TrUserData) -> + dg_read_field_def_session_resumed_request(Other, + Z1, + Z2, + F@_1, + TrUserData). + +dg_read_field_def_session_resumed_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_session_resumed_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +dg_read_field_def_session_resumed_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_session_resumed_request_clientinfo(Rest, + 0, + 0, + F@_1, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_session_resumed_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 1 -> + skip_64_session_resumed_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 2 -> + skip_length_delimited_session_resumed_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 3 -> + skip_group_session_resumed_request(Rest, + Key bsr 3, + 0, + F@_1, + TrUserData); + 5 -> + skip_32_session_resumed_request(Rest, + 0, + 0, + F@_1, + TrUserData) + end + end; +dg_read_field_def_session_resumed_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_session_resumed_request_clientinfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + d_field_session_resumed_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +d_field_session_resumed_request_clientinfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_session_resumed_request(RestF, + 0, + 0, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_session_resumed_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + skip_varint_session_resumed_request(Rest, + Z1, + Z2, + F@_1, + TrUserData); +skip_varint_session_resumed_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_resumed_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_length_delimited_session_resumed_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + skip_length_delimited_session_resumed_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +skip_length_delimited_session_resumed_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_session_resumed_request(Rest2, + 0, + 0, + F@_1, + TrUserData). + +skip_group_session_resumed_request(Bin, FNum, Z2, F@_1, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_session_resumed_request(Rest, + 0, + Z2, + F@_1, + TrUserData). + +skip_32_session_resumed_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_resumed_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_64_session_resumed_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_resumed_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +decode_msg_session_discarded_request(Bin, TrUserData) -> + dfp_read_field_def_session_discarded_request(Bin, + 0, + 0, + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_session_discarded_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + d_field_session_discarded_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + TrUserData); +dfp_read_field_def_session_discarded_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_session_discarded_request(Other, Z1, + Z2, F@_1, TrUserData) -> + dg_read_field_def_session_discarded_request(Other, + Z1, + Z2, + F@_1, + TrUserData). + +dg_read_field_def_session_discarded_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_session_discarded_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +dg_read_field_def_session_discarded_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_session_discarded_request_clientinfo(Rest, + 0, + 0, + F@_1, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_session_discarded_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 1 -> + skip_64_session_discarded_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 2 -> + skip_length_delimited_session_discarded_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 3 -> + skip_group_session_discarded_request(Rest, + Key bsr 3, + 0, + F@_1, + TrUserData); + 5 -> + skip_32_session_discarded_request(Rest, + 0, + 0, + F@_1, + TrUserData) + end + end; +dg_read_field_def_session_discarded_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_session_discarded_request_clientinfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + d_field_session_discarded_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +d_field_session_discarded_request_clientinfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_session_discarded_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_session_discarded_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + skip_varint_session_discarded_request(Rest, + Z1, + Z2, + F@_1, + TrUserData); +skip_varint_session_discarded_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_discarded_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_length_delimited_session_discarded_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + skip_length_delimited_session_discarded_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +skip_length_delimited_session_discarded_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_session_discarded_request(Rest2, + 0, + 0, + F@_1, + TrUserData). + +skip_group_session_discarded_request(Bin, FNum, Z2, + F@_1, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_session_discarded_request(Rest, + 0, + Z2, + F@_1, + TrUserData). + +skip_32_session_discarded_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_discarded_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_64_session_discarded_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_discarded_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +decode_msg_session_takeovered_request(Bin, + TrUserData) -> + dfp_read_field_def_session_takeovered_request(Bin, + 0, + 0, + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_session_takeovered_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + d_field_session_takeovered_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + TrUserData); +dfp_read_field_def_session_takeovered_request(<<>>, 0, + 0, F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_session_takeovered_request(Other, Z1, + Z2, F@_1, TrUserData) -> + dg_read_field_def_session_takeovered_request(Other, + Z1, + Z2, + F@_1, + TrUserData). + +dg_read_field_def_session_takeovered_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_session_takeovered_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +dg_read_field_def_session_takeovered_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_session_takeovered_request_clientinfo(Rest, + 0, + 0, + F@_1, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_session_takeovered_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 1 -> + skip_64_session_takeovered_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 2 -> + skip_length_delimited_session_takeovered_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 3 -> + skip_group_session_takeovered_request(Rest, + Key bsr 3, + 0, + F@_1, + TrUserData); + 5 -> + skip_32_session_takeovered_request(Rest, + 0, + 0, + F@_1, + TrUserData) + end + end; +dg_read_field_def_session_takeovered_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_session_takeovered_request_clientinfo(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + d_field_session_takeovered_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +d_field_session_takeovered_request_clientinfo(<<0:1, + X:7, Rest/binary>>, + N, Acc, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_session_takeovered_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_session_takeovered_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + skip_varint_session_takeovered_request(Rest, + Z1, + Z2, + F@_1, + TrUserData); +skip_varint_session_takeovered_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_takeovered_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_length_delimited_session_takeovered_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + skip_length_delimited_session_takeovered_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +skip_length_delimited_session_takeovered_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_session_takeovered_request(Rest2, + 0, + 0, + F@_1, + TrUserData). + +skip_group_session_takeovered_request(Bin, FNum, Z2, + F@_1, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_session_takeovered_request(Rest, + 0, + Z2, + F@_1, + TrUserData). + +skip_32_session_takeovered_request(<<_:32, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_takeovered_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_64_session_takeovered_request(<<_:64, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_session_takeovered_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +decode_msg_session_terminated_request(Bin, + TrUserData) -> + dfp_read_field_def_session_terminated_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id(<<>>, TrUserData), + TrUserData). + +dfp_read_field_def_session_terminated_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_session_terminated_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_session_terminated_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_session_terminated_request_reason(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_session_terminated_request(<<>>, 0, + 0, F@_1, F@_2, _) -> + S1 = #{reason => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end; +dfp_read_field_def_session_terminated_request(Other, Z1, + Z2, F@_1, F@_2, TrUserData) -> + dg_read_field_def_session_terminated_request(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_session_terminated_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_session_terminated_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_session_terminated_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_session_terminated_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_session_terminated_request_reason(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_session_terminated_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_session_terminated_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_session_terminated_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_session_terminated_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_session_terminated_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_session_terminated_request(<<>>, 0, 0, + F@_1, F@_2, _) -> + S1 = #{reason => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end. + +d_field_session_terminated_request_clientinfo(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_session_terminated_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_session_terminated_request_clientinfo(<<0:1, + X:7, Rest/binary>>, + N, Acc, Prev, F@_2, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_session_terminated_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + TrUserData). + +d_field_session_terminated_request_reason(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_session_terminated_request_reason(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_session_terminated_request_reason(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_session_terminated_request(RestF, + 0, + 0, + F@_1, + NewFValue, + TrUserData). + +skip_varint_session_terminated_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_session_terminated_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_session_terminated_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_session_terminated_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_session_terminated_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_session_terminated_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_session_terminated_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_session_terminated_request(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_session_terminated_request(Bin, FNum, Z2, + F@_1, F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_session_terminated_request(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_session_terminated_request(<<_:32, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_session_terminated_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_session_terminated_request(<<_:64, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_session_terminated_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_message_publish_request(Bin, TrUserData) -> + dfp_read_field_def_message_publish_request(Bin, + 0, + 0, + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_message_publish_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + d_field_message_publish_request_message(Rest, + Z1, + Z2, + F@_1, + TrUserData); +dfp_read_field_def_message_publish_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{message => F@_1} + end; +dfp_read_field_def_message_publish_request(Other, Z1, + Z2, F@_1, TrUserData) -> + dg_read_field_def_message_publish_request(Other, + Z1, + Z2, + F@_1, + TrUserData). + +dg_read_field_def_message_publish_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_message_publish_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +dg_read_field_def_message_publish_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_message_publish_request_message(Rest, + 0, + 0, + F@_1, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_message_publish_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 1 -> + skip_64_message_publish_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 2 -> + skip_length_delimited_message_publish_request(Rest, + 0, + 0, + F@_1, + TrUserData); + 3 -> + skip_group_message_publish_request(Rest, + Key bsr 3, + 0, + F@_1, + TrUserData); + 5 -> + skip_32_message_publish_request(Rest, + 0, + 0, + F@_1, + TrUserData) + end + end; +dg_read_field_def_message_publish_request(<<>>, 0, 0, + F@_1, _) -> + S1 = #{}, + if F@_1 == '$undef' -> S1; + true -> S1#{message => F@_1} + end. + +d_field_message_publish_request_message(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + d_field_message_publish_request_message(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +d_field_message_publish_request_message(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_message(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_message_publish_request(RestF, + 0, + 0, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_message(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_message_publish_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + skip_varint_message_publish_request(Rest, + Z1, + Z2, + F@_1, + TrUserData); +skip_varint_message_publish_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_message_publish_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_length_delimited_message_publish_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) + when N < 57 -> + skip_length_delimited_message_publish_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + TrUserData); +skip_length_delimited_message_publish_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_message_publish_request(Rest2, + 0, + 0, + F@_1, + TrUserData). + +skip_group_message_publish_request(Bin, FNum, Z2, F@_1, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_message_publish_request(Rest, + 0, + Z2, + F@_1, + TrUserData). + +skip_32_message_publish_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_message_publish_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +skip_64_message_publish_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, TrUserData) -> + dfp_read_field_def_message_publish_request(Rest, + Z1, + Z2, + F@_1, + TrUserData). + +decode_msg_message_delivered_request(Bin, TrUserData) -> + dfp_read_field_def_message_delivered_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_message_delivered_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_message_delivered_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_message_delivered_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_message_delivered_request_message(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_message_delivered_request(<<>>, 0, 0, + F@_1, F@_2, _) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + if F@_2 == '$undef' -> S2; + true -> S2#{message => F@_2} + end; +dfp_read_field_def_message_delivered_request(Other, Z1, + Z2, F@_1, F@_2, TrUserData) -> + dg_read_field_def_message_delivered_request(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_message_delivered_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_message_delivered_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_message_delivered_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_message_delivered_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_message_delivered_request_message(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_message_delivered_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_message_delivered_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_message_delivered_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_message_delivered_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_message_delivered_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_message_delivered_request(<<>>, 0, 0, + F@_1, F@_2, _) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + if F@_2 == '$undef' -> S2; + true -> S2#{message => F@_2} + end. + +d_field_message_delivered_request_clientinfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_message_delivered_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_message_delivered_request_clientinfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, F@_2, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_message_delivered_request(RestF, + 0, + 0, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + TrUserData). + +d_field_message_delivered_request_message(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_message_delivered_request_message(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_message_delivered_request_message(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_message(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_message_delivered_request(RestF, + 0, + 0, + F@_1, + if Prev == '$undef' -> + NewFValue; + true -> + merge_msg_message(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_message_delivered_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_message_delivered_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_message_delivered_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_delivered_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_message_delivered_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_message_delivered_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_message_delivered_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_message_delivered_request(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_message_delivered_request(Bin, FNum, Z2, + F@_1, F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_message_delivered_request(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_message_delivered_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_delivered_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_message_delivered_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_delivered_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_message_dropped_request(Bin, TrUserData) -> + dfp_read_field_def_message_dropped_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id(<<>>, TrUserData), + TrUserData). + +dfp_read_field_def_message_dropped_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_message_dropped_request_message(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_message_dropped_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_message_dropped_request_reason(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_message_dropped_request(<<>>, 0, 0, + F@_1, F@_2, _) -> + S1 = #{reason => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{message => F@_1} + end; +dfp_read_field_def_message_dropped_request(Other, Z1, + Z2, F@_1, F@_2, TrUserData) -> + dg_read_field_def_message_dropped_request(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_message_dropped_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_message_dropped_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_message_dropped_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_message_dropped_request_message(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_message_dropped_request_reason(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_message_dropped_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_message_dropped_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_message_dropped_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_message_dropped_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_message_dropped_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_message_dropped_request(<<>>, 0, 0, + F@_1, F@_2, _) -> + S1 = #{reason => F@_2}, + if F@_1 == '$undef' -> S1; + true -> S1#{message => F@_1} + end. + +d_field_message_dropped_request_message(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_message_dropped_request_message(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_message_dropped_request_message(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, F@_2, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_message(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_message_dropped_request(RestF, + 0, + 0, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_message(Prev, + NewFValue, + TrUserData) + end, + F@_2, + TrUserData). + +d_field_message_dropped_request_reason(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_message_dropped_request_reason(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_message_dropped_request_reason(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_message_dropped_request(RestF, + 0, + 0, + F@_1, + NewFValue, + TrUserData). + +skip_varint_message_dropped_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_message_dropped_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_message_dropped_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_dropped_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_message_dropped_request(<<1:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_message_dropped_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_message_dropped_request(<<0:1, + X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_message_dropped_request(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_message_dropped_request(Bin, FNum, Z2, F@_1, + F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_message_dropped_request(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_message_dropped_request(<<_:32, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_dropped_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_message_dropped_request(<<_:64, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_dropped_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_message_acked_request(Bin, TrUserData) -> + dfp_read_field_def_message_acked_request(Bin, + 0, + 0, + id('$undef', TrUserData), + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_message_acked_request(<<10, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_message_acked_request_clientinfo(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_message_acked_request(<<18, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_message_acked_request_message(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_message_acked_request(<<>>, 0, 0, + F@_1, F@_2, _) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + if F@_2 == '$undef' -> S2; + true -> S2#{message => F@_2} + end; +dfp_read_field_def_message_acked_request(Other, Z1, Z2, + F@_1, F@_2, TrUserData) -> + dg_read_field_def_message_acked_request(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_message_acked_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_message_acked_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_message_acked_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_message_acked_request_clientinfo(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_message_acked_request_message(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_message_acked_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_message_acked_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_message_acked_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_message_acked_request(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_message_acked_request(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_message_acked_request(<<>>, 0, 0, + F@_1, F@_2, _) -> + S1 = #{}, + S2 = if F@_1 == '$undef' -> S1; + true -> S1#{clientinfo => F@_1} + end, + if F@_2 == '$undef' -> S2; + true -> S2#{message => F@_2} + end. + +d_field_message_acked_request_clientinfo(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_message_acked_request_clientinfo(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_message_acked_request_clientinfo(<<0:1, X:7, + Rest/binary>>, + N, Acc, Prev, F@_2, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_client_info(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_message_acked_request(RestF, + 0, + 0, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_client_info(Prev, + NewFValue, + TrUserData) + end, + F@_2, + TrUserData). + +d_field_message_acked_request_message(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_message_acked_request_message(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_message_acked_request_message(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_message(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_message_acked_request(RestF, + 0, + 0, + F@_1, + if Prev == '$undef' -> NewFValue; + true -> + merge_msg_message(Prev, + NewFValue, + TrUserData) + end, + TrUserData). + +skip_varint_message_acked_request(<<1:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_message_acked_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_message_acked_request(<<0:1, _:7, + Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_acked_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_message_acked_request(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_message_acked_request(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_message_acked_request(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_message_acked_request(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_message_acked_request(Bin, FNum, Z2, F@_1, + F@_2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_message_acked_request(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_message_acked_request(<<_:32, Rest/binary>>, Z1, + Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_acked_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_message_acked_request(<<_:64, Rest/binary>>, Z1, + Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_message_acked_request(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_empty_success(Bin, TrUserData) -> + dfp_read_field_def_empty_success(Bin, 0, 0, TrUserData). + +dfp_read_field_def_empty_success(<<>>, 0, 0, _) -> #{}; +dfp_read_field_def_empty_success(Other, Z1, Z2, + TrUserData) -> + dg_read_field_def_empty_success(Other, + Z1, + Z2, + TrUserData). + +dg_read_field_def_empty_success(<<1:1, X:7, + Rest/binary>>, + N, Acc, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_empty_success(Rest, + N + 7, + X bsl N + Acc, + TrUserData); +dg_read_field_def_empty_success(<<0:1, X:7, + Rest/binary>>, + N, Acc, TrUserData) -> + Key = X bsl N + Acc, + case Key band 7 of + 0 -> skip_varint_empty_success(Rest, 0, 0, TrUserData); + 1 -> skip_64_empty_success(Rest, 0, 0, TrUserData); + 2 -> + skip_length_delimited_empty_success(Rest, + 0, + 0, + TrUserData); + 3 -> + skip_group_empty_success(Rest, + Key bsr 3, + 0, + TrUserData); + 5 -> skip_32_empty_success(Rest, 0, 0, TrUserData) + end; +dg_read_field_def_empty_success(<<>>, 0, 0, _) -> #{}. + +skip_varint_empty_success(<<1:1, _:7, Rest/binary>>, Z1, + Z2, TrUserData) -> + skip_varint_empty_success(Rest, Z1, Z2, TrUserData); +skip_varint_empty_success(<<0:1, _:7, Rest/binary>>, Z1, + Z2, TrUserData) -> + dfp_read_field_def_empty_success(Rest, + Z1, + Z2, + TrUserData). + +skip_length_delimited_empty_success(<<1:1, X:7, + Rest/binary>>, + N, Acc, TrUserData) + when N < 57 -> + skip_length_delimited_empty_success(Rest, + N + 7, + X bsl N + Acc, + TrUserData); +skip_length_delimited_empty_success(<<0:1, X:7, + Rest/binary>>, + N, Acc, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_empty_success(Rest2, + 0, + 0, + TrUserData). + +skip_group_empty_success(Bin, FNum, Z2, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_empty_success(Rest, + 0, + Z2, + TrUserData). + +skip_32_empty_success(<<_:32, Rest/binary>>, Z1, Z2, + TrUserData) -> + dfp_read_field_def_empty_success(Rest, + Z1, + Z2, + TrUserData). + +skip_64_empty_success(<<_:64, Rest/binary>>, Z1, Z2, + TrUserData) -> + dfp_read_field_def_empty_success(Rest, + Z1, + Z2, + TrUserData). + +decode_msg_valued_response(Bin, TrUserData) -> + dfp_read_field_def_valued_response(Bin, + 0, + 0, + id('CONTINUE', TrUserData), + id('$undef', TrUserData), + TrUserData). + +dfp_read_field_def_valued_response(<<8, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_valued_response_type(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_valued_response(<<24, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_valued_response_bool_result(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_valued_response(<<34, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + d_field_valued_response_message(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_valued_response(<<>>, 0, 0, F@_1, + F@_2, _) -> + S1 = #{type => F@_1}, + if F@_2 == '$undef' -> S1; + true -> S1#{value => F@_2} + end; +dfp_read_field_def_valued_response(Other, Z1, Z2, F@_1, + F@_2, TrUserData) -> + dg_read_field_def_valued_response(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_valued_response(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_valued_response(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_valued_response(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 8 -> + d_field_valued_response_type(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 24 -> + d_field_valued_response_bool_result(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 34 -> + d_field_valued_response_message(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_valued_response(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_valued_response(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_valued_response(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_valued_response(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_valued_response(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData) + end + end; +dg_read_field_def_valued_response(<<>>, 0, 0, F@_1, + F@_2, _) -> + S1 = #{type => F@_1}, + if F@_2 == '$undef' -> S1; + true -> S1#{value => F@_2} + end. + +d_field_valued_response_type(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_valued_response_type(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_valued_response_type(<<0:1, X:7, Rest/binary>>, + N, Acc, _, F@_2, TrUserData) -> + {NewFValue, RestF} = + {id('d_enum_valued_response.ResponsedType'(begin + <> = + <<(X bsl N + + Acc):32/unsigned-native>>, + id(Res, TrUserData) + end), + TrUserData), + Rest}, + dfp_read_field_def_valued_response(RestF, + 0, + 0, + NewFValue, + F@_2, + TrUserData). + +d_field_valued_response_bool_result(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_valued_response_bool_result(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_valued_response_bool_result(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, _, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc =/= 0, + TrUserData), + Rest}, + dfp_read_field_def_valued_response(RestF, + 0, + 0, + F@_1, + id({bool_result, NewFValue}, TrUserData), + TrUserData). + +d_field_valued_response_message(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_valued_response_message(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_valued_response_message(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(decode_msg_message(Bs, TrUserData), + TrUserData), + Rest2} + end, + dfp_read_field_def_valued_response(RestF, + 0, + 0, + F@_1, + case Prev of + '$undef' -> + id({message, NewFValue}, + TrUserData); + {message, MVPrev} -> + id({message, + merge_msg_message(MVPrev, + NewFValue, + TrUserData)}, + TrUserData); + _ -> + id({message, NewFValue}, + TrUserData) + end, + TrUserData). + +skip_varint_valued_response(<<1:1, _:7, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + skip_varint_valued_response(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_valued_response(<<0:1, _:7, Rest/binary>>, + Z1, Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_valued_response(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_valued_response(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_valued_response(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_valued_response(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_valued_response(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_valued_response(Bin, FNum, Z2, F@_1, F@_2, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_valued_response(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_valued_response(<<_:32, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + dfp_read_field_def_valued_response(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_valued_response(<<_:64, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + dfp_read_field_def_valued_response(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_broker_info(Bin, TrUserData) -> + dfp_read_field_def_broker_info(Bin, + 0, + 0, + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(0, TrUserData), + id(<<>>, TrUserData), + TrUserData). + +dfp_read_field_def_broker_info(<<10, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + d_field_broker_info_version(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dfp_read_field_def_broker_info(<<18, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + d_field_broker_info_sysdescr(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dfp_read_field_def_broker_info(<<24, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + d_field_broker_info_uptime(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dfp_read_field_def_broker_info(<<34, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + d_field_broker_info_datetime(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dfp_read_field_def_broker_info(<<>>, 0, 0, F@_1, F@_2, + F@_3, F@_4, _) -> + #{version => F@_1, sysdescr => F@_2, uptime => F@_3, + datetime => F@_4}; +dfp_read_field_def_broker_info(Other, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, TrUserData) -> + dg_read_field_def_broker_info(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +dg_read_field_def_broker_info(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_broker_info(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +dg_read_field_def_broker_info(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_broker_info_version(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 18 -> + d_field_broker_info_sysdescr(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 24 -> + d_field_broker_info_uptime(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 34 -> + d_field_broker_info_datetime(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_broker_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 1 -> + skip_64_broker_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 2 -> + skip_length_delimited_broker_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 3 -> + skip_group_broker_info(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); + 5 -> + skip_32_broker_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData) + end + end; +dg_read_field_def_broker_info(<<>>, 0, 0, F@_1, F@_2, + F@_3, F@_4, _) -> + #{version => F@_1, sysdescr => F@_2, uptime => F@_3, + datetime => F@_4}. + +d_field_broker_info_version(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, TrUserData) + when N < 57 -> + d_field_broker_info_version(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +d_field_broker_info_version(<<0:1, X:7, Rest/binary>>, + N, Acc, _, F@_2, F@_3, F@_4, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_broker_info(RestF, + 0, + 0, + NewFValue, + F@_2, + F@_3, + F@_4, + TrUserData). + +d_field_broker_info_sysdescr(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, TrUserData) + when N < 57 -> + d_field_broker_info_sysdescr(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +d_field_broker_info_sysdescr(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, _, F@_3, F@_4, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_broker_info(RestF, + 0, + 0, + F@_1, + NewFValue, + F@_3, + F@_4, + TrUserData). + +d_field_broker_info_uptime(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, TrUserData) + when N < 57 -> + d_field_broker_info_uptime(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +d_field_broker_info_uptime(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, _, F@_4, TrUserData) -> + {NewFValue, RestF} = {begin + <> = <<(X bsl N + + Acc):64/unsigned-native>>, + id(Res, TrUserData) + end, + Rest}, + dfp_read_field_def_broker_info(RestF, + 0, + 0, + F@_1, + F@_2, + NewFValue, + F@_4, + TrUserData). + +d_field_broker_info_datetime(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, TrUserData) + when N < 57 -> + d_field_broker_info_datetime(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +d_field_broker_info_datetime(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, _, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_broker_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + NewFValue, + TrUserData). + +skip_varint_broker_info(<<1:1, _:7, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + skip_varint_broker_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +skip_varint_broker_info(<<0:1, _:7, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + dfp_read_field_def_broker_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +skip_length_delimited_broker_info(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, TrUserData) + when N < 57 -> + skip_length_delimited_broker_info(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData); +skip_length_delimited_broker_info(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_broker_info(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +skip_group_broker_info(Bin, FNum, Z2, F@_1, F@_2, F@_3, + F@_4, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_broker_info(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +skip_32_broker_info(<<_:32, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, TrUserData) -> + dfp_read_field_def_broker_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +skip_64_broker_info(<<_:64, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, TrUserData) -> + dfp_read_field_def_broker_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + TrUserData). + +decode_msg_hook_spec(Bin, TrUserData) -> + dfp_read_field_def_hook_spec(Bin, + 0, + 0, + id(<<>>, TrUserData), + id([], TrUserData), + TrUserData). + +dfp_read_field_def_hook_spec(<<10, Rest/binary>>, Z1, + Z2, F@_1, F@_2, TrUserData) -> + d_field_hook_spec_name(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_hook_spec(<<18, Rest/binary>>, Z1, + Z2, F@_1, F@_2, TrUserData) -> + d_field_hook_spec_topics(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_hook_spec(<<>>, 0, 0, F@_1, R1, + TrUserData) -> + #{name => F@_1, + topics => lists_reverse(R1, TrUserData)}; +dfp_read_field_def_hook_spec(Other, Z1, Z2, F@_1, F@_2, + TrUserData) -> + dg_read_field_def_hook_spec(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_hook_spec(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_hook_spec(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_hook_spec(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_hook_spec_name(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_hook_spec_topics(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_hook_spec(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_hook_spec(Rest, 0, 0, F@_1, F@_2, TrUserData); + 2 -> + skip_length_delimited_hook_spec(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_hook_spec(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_hook_spec(Rest, 0, 0, F@_1, F@_2, TrUserData) + end + end; +dg_read_field_def_hook_spec(<<>>, 0, 0, F@_1, R1, + TrUserData) -> + #{name => F@_1, + topics => lists_reverse(R1, TrUserData)}. + +d_field_hook_spec_name(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_hook_spec_name(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_hook_spec_name(<<0:1, X:7, Rest/binary>>, N, + Acc, _, F@_2, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_hook_spec(RestF, + 0, + 0, + NewFValue, + F@_2, + TrUserData). + +d_field_hook_spec_topics(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_hook_spec_topics(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_hook_spec_topics(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, Prev, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_hook_spec(RestF, + 0, + 0, + F@_1, + cons(NewFValue, Prev, TrUserData), + TrUserData). + +skip_varint_hook_spec(<<1:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + skip_varint_hook_spec(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_hook_spec(<<0:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + dfp_read_field_def_hook_spec(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_hook_spec(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_hook_spec(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_hook_spec(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_hook_spec(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_hook_spec(Bin, FNum, Z2, F@_1, F@_2, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_hook_spec(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_hook_spec(<<_:32, Rest/binary>>, Z1, Z2, F@_1, + F@_2, TrUserData) -> + dfp_read_field_def_hook_spec(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_hook_spec(<<_:64, Rest/binary>>, Z1, Z2, F@_1, + F@_2, TrUserData) -> + dfp_read_field_def_hook_spec(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_conn_info(Bin, TrUserData) -> + dfp_read_field_def_conn_info(Bin, + 0, + 0, + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(0, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(0, TrUserData), + TrUserData). + +dfp_read_field_def_conn_info(<<10, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + d_field_conn_info_node(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dfp_read_field_def_conn_info(<<18, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + d_field_conn_info_clientid(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dfp_read_field_def_conn_info(<<26, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + d_field_conn_info_username(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dfp_read_field_def_conn_info(<<34, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + d_field_conn_info_peerhost(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dfp_read_field_def_conn_info(<<40, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + d_field_conn_info_sockport(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dfp_read_field_def_conn_info(<<50, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + d_field_conn_info_proto_name(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dfp_read_field_def_conn_info(<<58, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + d_field_conn_info_proto_ver(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dfp_read_field_def_conn_info(<<64, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + d_field_conn_info_keepalive(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dfp_read_field_def_conn_info(<<>>, 0, 0, F@_1, F@_2, + F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, _) -> + #{node => F@_1, clientid => F@_2, username => F@_3, + peerhost => F@_4, sockport => F@_5, proto_name => F@_6, + proto_ver => F@_7, keepalive => F@_8}; +dfp_read_field_def_conn_info(Other, Z1, Z2, F@_1, F@_2, + F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, TrUserData) -> + dg_read_field_def_conn_info(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +dg_read_field_def_conn_info(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_conn_info(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +dg_read_field_def_conn_info(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_conn_info_node(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 18 -> + d_field_conn_info_clientid(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 26 -> + d_field_conn_info_username(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 34 -> + d_field_conn_info_peerhost(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 40 -> + d_field_conn_info_sockport(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 50 -> + d_field_conn_info_proto_name(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 58 -> + d_field_conn_info_proto_ver(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 64 -> + d_field_conn_info_keepalive(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_conn_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 1 -> + skip_64_conn_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 2 -> + skip_length_delimited_conn_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 3 -> + skip_group_conn_info(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); + 5 -> + skip_32_conn_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData) + end + end; +dg_read_field_def_conn_info(<<>>, 0, 0, F@_1, F@_2, + F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, _) -> + #{node => F@_1, clientid => F@_2, username => F@_3, + peerhost => F@_4, sockport => F@_5, proto_name => F@_6, + proto_ver => F@_7, keepalive => F@_8}. + +d_field_conn_info_node(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) + when N < 57 -> + d_field_conn_info_node(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +d_field_conn_info_node(<<0:1, X:7, Rest/binary>>, N, + Acc, _, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_conn_info(RestF, + 0, + 0, + NewFValue, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +d_field_conn_info_clientid(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) + when N < 57 -> + d_field_conn_info_clientid(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +d_field_conn_info_clientid(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, _, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_conn_info(RestF, + 0, + 0, + F@_1, + NewFValue, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +d_field_conn_info_username(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) + when N < 57 -> + d_field_conn_info_username(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +d_field_conn_info_username(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, _, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_conn_info(RestF, + 0, + 0, + F@_1, + F@_2, + NewFValue, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +d_field_conn_info_peerhost(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) + when N < 57 -> + d_field_conn_info_peerhost(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +d_field_conn_info_peerhost(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, _, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_conn_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + NewFValue, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +d_field_conn_info_sockport(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) + when N < 57 -> + d_field_conn_info_sockport(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +d_field_conn_info_sockport(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, _, F@_6, F@_7, F@_8, + TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_conn_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + NewFValue, + F@_6, + F@_7, + F@_8, + TrUserData). + +d_field_conn_info_proto_name(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, TrUserData) + when N < 57 -> + d_field_conn_info_proto_name(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +d_field_conn_info_proto_name(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, _, F@_7, + F@_8, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_conn_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + NewFValue, + F@_7, + F@_8, + TrUserData). + +d_field_conn_info_proto_ver(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, TrUserData) + when N < 57 -> + d_field_conn_info_proto_ver(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +d_field_conn_info_proto_ver(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, _, F@_8, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_conn_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + NewFValue, + F@_8, + TrUserData). + +d_field_conn_info_keepalive(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, TrUserData) + when N < 57 -> + d_field_conn_info_keepalive(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +d_field_conn_info_keepalive(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, _, + TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_conn_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + NewFValue, + TrUserData). + +skip_varint_conn_info(<<1:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + skip_varint_conn_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +skip_varint_conn_info(<<0:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + TrUserData) -> + dfp_read_field_def_conn_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +skip_length_delimited_conn_info(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, + F@_7, F@_8, TrUserData) + when N < 57 -> + skip_length_delimited_conn_info(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData); +skip_length_delimited_conn_info(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, + F@_7, F@_8, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_conn_info(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +skip_group_conn_info(Bin, FNum, Z2, F@_1, F@_2, F@_3, + F@_4, F@_5, F@_6, F@_7, F@_8, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_conn_info(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +skip_32_conn_info(<<_:32, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, TrUserData) -> + dfp_read_field_def_conn_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +skip_64_conn_info(<<_:64, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, TrUserData) -> + dfp_read_field_def_conn_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + TrUserData). + +decode_msg_client_info(Bin, TrUserData) -> + dfp_read_field_def_client_info(Bin, + 0, + 0, + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(0, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(false, TrUserData), + id(false, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + TrUserData). + +dfp_read_field_def_client_info(<<10, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_node(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<18, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_clientid(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<26, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_username(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<34, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_password(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<42, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_peerhost(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<48, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_sockport(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<58, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_protocol(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<66, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_mountpoint(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<72, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_is_superuser(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<80, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_anonymous(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<90, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_cn(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<98, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + d_field_client_info_dn(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dfp_read_field_def_client_info(<<>>, 0, 0, F@_1, F@_2, + F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, F@_9, F@_10, + F@_11, F@_12, _) -> + #{node => F@_1, clientid => F@_2, username => F@_3, + password => F@_4, peerhost => F@_5, sockport => F@_6, + protocol => F@_7, mountpoint => F@_8, + is_superuser => F@_9, anonymous => F@_10, cn => F@_11, + dn => F@_12}; +dfp_read_field_def_client_info(Other, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, F@_9, + F@_10, F@_11, F@_12, TrUserData) -> + dg_read_field_def_client_info(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +dg_read_field_def_client_info(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_client_info(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +dg_read_field_def_client_info(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_client_info_node(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 18 -> + d_field_client_info_clientid(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 26 -> + d_field_client_info_username(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 34 -> + d_field_client_info_password(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 42 -> + d_field_client_info_peerhost(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 48 -> + d_field_client_info_sockport(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 58 -> + d_field_client_info_protocol(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 66 -> + d_field_client_info_mountpoint(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 72 -> + d_field_client_info_is_superuser(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 80 -> + d_field_client_info_anonymous(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 90 -> + d_field_client_info_cn(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 98 -> + d_field_client_info_dn(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_client_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 1 -> + skip_64_client_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 2 -> + skip_length_delimited_client_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 3 -> + skip_group_client_info(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); + 5 -> + skip_32_client_info(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData) + end + end; +dg_read_field_def_client_info(<<>>, 0, 0, F@_1, F@_2, + F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, F@_9, F@_10, + F@_11, F@_12, _) -> + #{node => F@_1, clientid => F@_2, username => F@_3, + password => F@_4, peerhost => F@_5, sockport => F@_6, + protocol => F@_7, mountpoint => F@_8, + is_superuser => F@_9, anonymous => F@_10, cn => F@_11, + dn => F@_12}. + +d_field_client_info_node(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_node(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_node(<<0:1, X:7, Rest/binary>>, N, + Acc, _, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, F@_9, + F@_10, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + NewFValue, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_clientid(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_clientid(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_clientid(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, _, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + NewFValue, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_username(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_username(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_username(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, _, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + NewFValue, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_password(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_password(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_password(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, _, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + NewFValue, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_peerhost(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_peerhost(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_peerhost(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, _, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + NewFValue, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_sockport(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_sockport(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_sockport(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, _, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + NewFValue, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_protocol(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_protocol(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_protocol(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, _, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + NewFValue, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_mountpoint(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_mountpoint(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_mountpoint(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + _, F@_9, F@_10, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + NewFValue, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_is_superuser(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, + F@_7, F@_8, F@_9, F@_10, F@_11, F@_12, + TrUserData) + when N < 57 -> + d_field_client_info_is_superuser(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_is_superuser(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, + F@_7, F@_8, _, F@_10, F@_11, F@_12, + TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc =/= 0, + TrUserData), + Rest}, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + NewFValue, + F@_10, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_anonymous(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_anonymous(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_anonymous(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + F@_8, F@_9, _, F@_11, F@_12, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc =/= 0, + TrUserData), + Rest}, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + NewFValue, + F@_11, + F@_12, + TrUserData). + +d_field_client_info_cn(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_cn(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_cn(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + F@_9, F@_10, _, F@_12, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + NewFValue, + F@_12, + TrUserData). + +d_field_client_info_dn(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + F@_9, F@_10, F@_11, F@_12, TrUserData) + when N < 57 -> + d_field_client_info_dn(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +d_field_client_info_dn(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + F@_9, F@_10, F@_11, _, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_client_info(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + NewFValue, + TrUserData). + +skip_varint_client_info(<<1:1, _:7, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + F@_9, F@_10, F@_11, F@_12, TrUserData) -> + skip_varint_client_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +skip_varint_client_info(<<0:1, _:7, Rest/binary>>, Z1, + Z2, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, + F@_9, F@_10, F@_11, F@_12, TrUserData) -> + dfp_read_field_def_client_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +skip_length_delimited_client_info(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, + F@_7, F@_8, F@_9, F@_10, F@_11, F@_12, + TrUserData) + when N < 57 -> + skip_length_delimited_client_info(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData); +skip_length_delimited_client_info(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, + F@_7, F@_8, F@_9, F@_10, F@_11, F@_12, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_client_info(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +skip_group_client_info(Bin, FNum, Z2, F@_1, F@_2, F@_3, + F@_4, F@_5, F@_6, F@_7, F@_8, F@_9, F@_10, F@_11, F@_12, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_client_info(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +skip_32_client_info(<<_:32, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, F@_9, F@_10, + F@_11, F@_12, TrUserData) -> + dfp_read_field_def_client_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +skip_64_client_info(<<_:64, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, F@_8, F@_9, F@_10, + F@_11, F@_12, TrUserData) -> + dfp_read_field_def_client_info(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + F@_8, + F@_9, + F@_10, + F@_11, + F@_12, + TrUserData). + +decode_msg_message(Bin, TrUserData) -> + dfp_read_field_def_message(Bin, + 0, + 0, + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(0, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(<<>>, TrUserData), + id(0, TrUserData), + TrUserData). + +dfp_read_field_def_message(<<10, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + d_field_message_node(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +dfp_read_field_def_message(<<18, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + d_field_message_id(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +dfp_read_field_def_message(<<24, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + d_field_message_qos(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +dfp_read_field_def_message(<<34, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + d_field_message_from(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +dfp_read_field_def_message(<<42, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + d_field_message_topic(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +dfp_read_field_def_message(<<50, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + d_field_message_payload(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +dfp_read_field_def_message(<<56, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + d_field_message_timestamp(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +dfp_read_field_def_message(<<>>, 0, 0, F@_1, F@_2, F@_3, + F@_4, F@_5, F@_6, F@_7, _) -> + #{node => F@_1, id => F@_2, qos => F@_3, from => F@_4, + topic => F@_5, payload => F@_6, timestamp => F@_7}; +dfp_read_field_def_message(Other, Z1, Z2, F@_1, F@_2, + F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> + dg_read_field_def_message(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +dg_read_field_def_message(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) + when N < 32 - 7 -> + dg_read_field_def_message(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +dg_read_field_def_message(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_message_node(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 18 -> + d_field_message_id(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 24 -> + d_field_message_qos(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 34 -> + d_field_message_from(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 42 -> + d_field_message_topic(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 50 -> + d_field_message_payload(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 56 -> + d_field_message_timestamp(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_message(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 1 -> + skip_64_message(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 2 -> + skip_length_delimited_message(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 3 -> + skip_group_message(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); + 5 -> + skip_32_message(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData) + end + end; +dg_read_field_def_message(<<>>, 0, 0, F@_1, F@_2, F@_3, + F@_4, F@_5, F@_6, F@_7, _) -> + #{node => F@_1, id => F@_2, qos => F@_3, from => F@_4, + topic => F@_5, payload => F@_6, timestamp => F@_7}. + +d_field_message_node(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) + when N < 57 -> + d_field_message_node(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +d_field_message_node(<<0:1, X:7, Rest/binary>>, N, Acc, + _, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_message(RestF, + 0, + 0, + NewFValue, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +d_field_message_id(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) + when N < 57 -> + d_field_message_id(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +d_field_message_id(<<0:1, X:7, Rest/binary>>, N, Acc, + F@_1, _, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_message(RestF, + 0, + 0, + F@_1, + NewFValue, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +d_field_message_qos(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) + when N < 57 -> + d_field_message_qos(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +d_field_message_qos(<<0:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, _, F@_4, F@_5, F@_6, F@_7, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_message(RestF, + 0, + 0, + F@_1, + F@_2, + NewFValue, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +d_field_message_from(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) + when N < 57 -> + d_field_message_from(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +d_field_message_from(<<0:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, _, F@_5, F@_6, F@_7, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_message(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + NewFValue, + F@_5, + F@_6, + F@_7, + TrUserData). + +d_field_message_topic(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) + when N < 57 -> + d_field_message_topic(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +d_field_message_topic(<<0:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, _, F@_6, F@_7, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_message(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + NewFValue, + F@_6, + F@_7, + TrUserData). + +d_field_message_payload(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) + when N < 57 -> + d_field_message_payload(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +d_field_message_payload(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, _, F@_7, + TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_message(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + NewFValue, + F@_7, + TrUserData). + +d_field_message_timestamp(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) + when N < 57 -> + d_field_message_timestamp(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +d_field_message_timestamp(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, _, + TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_message(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + NewFValue, + TrUserData). + +skip_varint_message(<<1:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> + skip_varint_message(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +skip_varint_message(<<0:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> + dfp_read_field_def_message(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +skip_length_delimited_message(<<1:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) + when N < 57 -> + skip_length_delimited_message(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData); +skip_length_delimited_message(<<0:1, X:7, Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_message(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +skip_group_message(Bin, FNum, Z2, F@_1, F@_2, F@_3, + F@_4, F@_5, F@_6, F@_7, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_message(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +skip_32_message(<<_:32, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> + dfp_read_field_def_message(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +skip_64_message(<<_:64, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, F@_6, F@_7, TrUserData) -> + dfp_read_field_def_message(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + F@_6, + F@_7, + TrUserData). + +decode_msg_property(Bin, TrUserData) -> + dfp_read_field_def_property(Bin, + 0, + 0, + id(<<>>, TrUserData), + id(<<>>, TrUserData), + TrUserData). + +dfp_read_field_def_property(<<10, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + d_field_property_name(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_property(<<18, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + d_field_property_value(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_property(<<>>, 0, 0, F@_1, F@_2, + _) -> + #{name => F@_1, value => F@_2}; +dfp_read_field_def_property(Other, Z1, Z2, F@_1, F@_2, + TrUserData) -> + dg_read_field_def_property(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_property(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_property(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_property(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_property_name(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 18 -> + d_field_property_value(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_property(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_property(Rest, 0, 0, F@_1, F@_2, TrUserData); + 2 -> + skip_length_delimited_property(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_property(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_property(Rest, 0, 0, F@_1, F@_2, TrUserData) + end + end; +dg_read_field_def_property(<<>>, 0, 0, F@_1, F@_2, _) -> + #{name => F@_1, value => F@_2}. + +d_field_property_name(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_property_name(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_property_name(<<0:1, X:7, Rest/binary>>, N, Acc, + _, F@_2, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_property(RestF, + 0, + 0, + NewFValue, + F@_2, + TrUserData). + +d_field_property_value(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_property_value(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_property_value(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, _, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_property(RestF, + 0, + 0, + F@_1, + NewFValue, + TrUserData). + +skip_varint_property(<<1:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + skip_varint_property(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_property(<<0:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + dfp_read_field_def_property(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_property(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_property(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_property(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_property(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_property(Bin, FNum, Z2, F@_1, F@_2, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_property(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_property(<<_:32, Rest/binary>>, Z1, Z2, F@_1, + F@_2, TrUserData) -> + dfp_read_field_def_property(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_property(<<_:64, Rest/binary>>, Z1, Z2, F@_1, + F@_2, TrUserData) -> + dfp_read_field_def_property(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_topic_filter(Bin, TrUserData) -> + dfp_read_field_def_topic_filter(Bin, + 0, + 0, + id(<<>>, TrUserData), + id(0, TrUserData), + TrUserData). + +dfp_read_field_def_topic_filter(<<10, Rest/binary>>, Z1, + Z2, F@_1, F@_2, TrUserData) -> + d_field_topic_filter_name(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_topic_filter(<<16, Rest/binary>>, Z1, + Z2, F@_1, F@_2, TrUserData) -> + d_field_topic_filter_qos(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +dfp_read_field_def_topic_filter(<<>>, 0, 0, F@_1, F@_2, + _) -> + #{name => F@_1, qos => F@_2}; +dfp_read_field_def_topic_filter(Other, Z1, Z2, F@_1, + F@_2, TrUserData) -> + dg_read_field_def_topic_filter(Other, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +dg_read_field_def_topic_filter(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_topic_filter(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +dg_read_field_def_topic_filter(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 10 -> + d_field_topic_filter_name(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 16 -> + d_field_topic_filter_qos(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_topic_filter(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 1 -> + skip_64_topic_filter(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 2 -> + skip_length_delimited_topic_filter(Rest, + 0, + 0, + F@_1, + F@_2, + TrUserData); + 3 -> + skip_group_topic_filter(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + TrUserData); + 5 -> + skip_32_topic_filter(Rest, 0, 0, F@_1, F@_2, TrUserData) + end + end; +dg_read_field_def_topic_filter(<<>>, 0, 0, F@_1, F@_2, + _) -> + #{name => F@_1, qos => F@_2}. + +d_field_topic_filter_name(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_topic_filter_name(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_topic_filter_name(<<0:1, X:7, Rest/binary>>, N, + Acc, _, F@_2, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_topic_filter(RestF, + 0, + 0, + NewFValue, + F@_2, + TrUserData). + +d_field_topic_filter_qos(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + d_field_topic_filter_qos(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +d_field_topic_filter_qos(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, _, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_topic_filter(RestF, + 0, + 0, + F@_1, + NewFValue, + TrUserData). + +skip_varint_topic_filter(<<1:1, _:7, Rest/binary>>, Z1, + Z2, F@_1, F@_2, TrUserData) -> + skip_varint_topic_filter(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData); +skip_varint_topic_filter(<<0:1, _:7, Rest/binary>>, Z1, + Z2, F@_1, F@_2, TrUserData) -> + dfp_read_field_def_topic_filter(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_length_delimited_topic_filter(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) + when N < 57 -> + skip_length_delimited_topic_filter(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + TrUserData); +skip_length_delimited_topic_filter(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_topic_filter(Rest2, + 0, + 0, + F@_1, + F@_2, + TrUserData). + +skip_group_topic_filter(Bin, FNum, Z2, F@_1, F@_2, + TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_topic_filter(Rest, + 0, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_32_topic_filter(<<_:32, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + dfp_read_field_def_topic_filter(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +skip_64_topic_filter(<<_:64, Rest/binary>>, Z1, Z2, + F@_1, F@_2, TrUserData) -> + dfp_read_field_def_topic_filter(Rest, + Z1, + Z2, + F@_1, + F@_2, + TrUserData). + +decode_msg_sub_opts(Bin, TrUserData) -> + dfp_read_field_def_sub_opts(Bin, + 0, + 0, + id(0, TrUserData), + id(<<>>, TrUserData), + id(0, TrUserData), + id(0, TrUserData), + id(0, TrUserData), + TrUserData). + +dfp_read_field_def_sub_opts(<<8, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + d_field_sub_opts_qos(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +dfp_read_field_def_sub_opts(<<18, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + d_field_sub_opts_share(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +dfp_read_field_def_sub_opts(<<24, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + d_field_sub_opts_rh(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +dfp_read_field_def_sub_opts(<<32, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + d_field_sub_opts_rap(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +dfp_read_field_def_sub_opts(<<40, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + d_field_sub_opts_nl(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +dfp_read_field_def_sub_opts(<<>>, 0, 0, F@_1, F@_2, + F@_3, F@_4, F@_5, _) -> + #{qos => F@_1, share => F@_2, rh => F@_3, rap => F@_4, + nl => F@_5}; +dfp_read_field_def_sub_opts(Other, Z1, Z2, F@_1, F@_2, + F@_3, F@_4, F@_5, TrUserData) -> + dg_read_field_def_sub_opts(Other, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData). + +dg_read_field_def_sub_opts(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) + when N < 32 - 7 -> + dg_read_field_def_sub_opts(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +dg_read_field_def_sub_opts(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + Key = X bsl N + Acc, + case Key of + 8 -> + d_field_sub_opts_qos(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + 18 -> + d_field_sub_opts_share(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + 24 -> + d_field_sub_opts_rh(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + 32 -> + d_field_sub_opts_rap(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + 40 -> + d_field_sub_opts_nl(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + _ -> + case Key band 7 of + 0 -> + skip_varint_sub_opts(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + 1 -> + skip_64_sub_opts(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + 2 -> + skip_length_delimited_sub_opts(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + 3 -> + skip_group_sub_opts(Rest, + Key bsr 3, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); + 5 -> + skip_32_sub_opts(Rest, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData) + end + end; +dg_read_field_def_sub_opts(<<>>, 0, 0, F@_1, F@_2, F@_3, + F@_4, F@_5, _) -> + #{qos => F@_1, share => F@_2, rh => F@_3, rap => F@_4, + nl => F@_5}. + +d_field_sub_opts_qos(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) + when N < 57 -> + d_field_sub_opts_qos(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +d_field_sub_opts_qos(<<0:1, X:7, Rest/binary>>, N, Acc, + _, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_sub_opts(RestF, + 0, + 0, + NewFValue, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData). + +d_field_sub_opts_share(<<1:1, X:7, Rest/binary>>, N, + Acc, F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) + when N < 57 -> + d_field_sub_opts_share(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +d_field_sub_opts_share(<<0:1, X:7, Rest/binary>>, N, + Acc, F@_1, _, F@_3, F@_4, F@_5, TrUserData) -> + {NewFValue, RestF} = begin + Len = X bsl N + Acc, + <> = Rest, + {id(binary:copy(Bytes), TrUserData), Rest2} + end, + dfp_read_field_def_sub_opts(RestF, + 0, + 0, + F@_1, + NewFValue, + F@_3, + F@_4, + F@_5, + TrUserData). + +d_field_sub_opts_rh(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) + when N < 57 -> + d_field_sub_opts_rh(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +d_field_sub_opts_rh(<<0:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, _, F@_4, F@_5, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_sub_opts(RestF, + 0, + 0, + F@_1, + F@_2, + NewFValue, + F@_4, + F@_5, + TrUserData). + +d_field_sub_opts_rap(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) + when N < 57 -> + d_field_sub_opts_rap(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +d_field_sub_opts_rap(<<0:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, _, F@_5, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_sub_opts(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + NewFValue, + F@_5, + TrUserData). + +d_field_sub_opts_nl(<<1:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) + when N < 57 -> + d_field_sub_opts_nl(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +d_field_sub_opts_nl(<<0:1, X:7, Rest/binary>>, N, Acc, + F@_1, F@_2, F@_3, F@_4, _, TrUserData) -> + {NewFValue, RestF} = {id(X bsl N + Acc, TrUserData), + Rest}, + dfp_read_field_def_sub_opts(RestF, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + NewFValue, + TrUserData). + +skip_varint_sub_opts(<<1:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + skip_varint_sub_opts(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +skip_varint_sub_opts(<<0:1, _:7, Rest/binary>>, Z1, Z2, + F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) -> + dfp_read_field_def_sub_opts(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData). + +skip_length_delimited_sub_opts(<<1:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, TrUserData) + when N < 57 -> + skip_length_delimited_sub_opts(Rest, + N + 7, + X bsl N + Acc, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData); +skip_length_delimited_sub_opts(<<0:1, X:7, + Rest/binary>>, + N, Acc, F@_1, F@_2, F@_3, F@_4, F@_5, + TrUserData) -> + Length = X bsl N + Acc, + <<_:Length/binary, Rest2/binary>> = Rest, + dfp_read_field_def_sub_opts(Rest2, + 0, + 0, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData). + +skip_group_sub_opts(Bin, FNum, Z2, F@_1, F@_2, F@_3, + F@_4, F@_5, TrUserData) -> + {_, Rest} = read_group(Bin, FNum), + dfp_read_field_def_sub_opts(Rest, + 0, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData). + +skip_32_sub_opts(<<_:32, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, TrUserData) -> + dfp_read_field_def_sub_opts(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData). + +skip_64_sub_opts(<<_:64, Rest/binary>>, Z1, Z2, F@_1, + F@_2, F@_3, F@_4, F@_5, TrUserData) -> + dfp_read_field_def_sub_opts(Rest, + Z1, + Z2, + F@_1, + F@_2, + F@_3, + F@_4, + F@_5, + TrUserData). + +'d_enum_client_authorize_request.AuthorizeReqType'(0) -> + 'PUBLISH'; +'d_enum_client_authorize_request.AuthorizeReqType'(1) -> + 'SUBSCRIBE'; +'d_enum_client_authorize_request.AuthorizeReqType'(V) -> + V. + +'d_enum_valued_response.ResponsedType'(0) -> 'CONTINUE'; +'d_enum_valued_response.ResponsedType'(1) -> 'IGNORE'; +'d_enum_valued_response.ResponsedType'(2) -> + 'STOP_AND_RETURN'; +'d_enum_valued_response.ResponsedType'(V) -> V. + +read_group(Bin, FieldNum) -> + {NumBytes, EndTagLen} = read_gr_b(Bin, 0, 0, 0, 0, FieldNum), + <> = Bin, + {Group, Rest}. + +%% Like skipping over fields, but record the total length, +%% Each field is <(FieldNum bsl 3) bor FieldType> ++ +%% Record the length because varints may be non-optimally encoded. +%% +%% Groups can be nested, but assume the same FieldNum cannot be nested +%% because group field numbers are shared with the rest of the fields +%% numbers. Thus we can search just for an group-end with the same +%% field number. +%% +%% (The only time the same group field number could occur would +%% be in a nested sub message, but then it would be inside a +%% length-delimited entry, which we skip-read by length.) +read_gr_b(<<1:1, X:7, Tl/binary>>, N, Acc, NumBytes, TagLen, FieldNum) + when N < (32-7) -> + read_gr_b(Tl, N+7, X bsl N + Acc, NumBytes, TagLen+1, FieldNum); +read_gr_b(<<0:1, X:7, Tl/binary>>, N, Acc, NumBytes, TagLen, + FieldNum) -> + Key = X bsl N + Acc, + TagLen1 = TagLen + 1, + case {Key bsr 3, Key band 7} of + {FieldNum, 4} -> % 4 = group_end + {NumBytes, TagLen1}; + {_, 0} -> % 0 = varint + read_gr_vi(Tl, 0, NumBytes + TagLen1, FieldNum); + {_, 1} -> % 1 = bits64 + <<_:64, Tl2/binary>> = Tl, + read_gr_b(Tl2, 0, 0, NumBytes + TagLen1 + 8, 0, FieldNum); + {_, 2} -> % 2 = length_delimited + read_gr_ld(Tl, 0, 0, NumBytes + TagLen1, FieldNum); + {_, 3} -> % 3 = group_start + read_gr_b(Tl, 0, 0, NumBytes + TagLen1, 0, FieldNum); + {_, 4} -> % 4 = group_end + read_gr_b(Tl, 0, 0, NumBytes + TagLen1, 0, FieldNum); + {_, 5} -> % 5 = bits32 + <<_:32, Tl2/binary>> = Tl, + read_gr_b(Tl2, 0, 0, NumBytes + TagLen1 + 4, 0, FieldNum) + end. + +read_gr_vi(<<1:1, _:7, Tl/binary>>, N, NumBytes, FieldNum) + when N < (64-7) -> + read_gr_vi(Tl, N+7, NumBytes+1, FieldNum); +read_gr_vi(<<0:1, _:7, Tl/binary>>, _, NumBytes, FieldNum) -> + read_gr_b(Tl, 0, 0, NumBytes+1, 0, FieldNum). + +read_gr_ld(<<1:1, X:7, Tl/binary>>, N, Acc, NumBytes, FieldNum) + when N < (64-7) -> + read_gr_ld(Tl, N+7, X bsl N + Acc, NumBytes+1, FieldNum); +read_gr_ld(<<0:1, X:7, Tl/binary>>, N, Acc, NumBytes, FieldNum) -> + Len = X bsl N + Acc, + NumBytes1 = NumBytes + 1, + <<_:Len/binary, Tl2/binary>> = Tl, + read_gr_b(Tl2, 0, 0, NumBytes1 + Len, 0, FieldNum). + +merge_msgs(Prev, New, MsgName) when is_atom(MsgName) -> + merge_msgs(Prev, New, MsgName, []). + +merge_msgs(Prev, New, MsgName, Opts) -> + TrUserData = proplists:get_value(user_data, Opts), + case MsgName of + provider_loaded_request -> + merge_msg_provider_loaded_request(Prev, + New, + TrUserData); + loaded_response -> + merge_msg_loaded_response(Prev, New, TrUserData); + provider_unloaded_request -> + merge_msg_provider_unloaded_request(Prev, + New, + TrUserData); + client_connect_request -> + merge_msg_client_connect_request(Prev, New, TrUserData); + client_connack_request -> + merge_msg_client_connack_request(Prev, New, TrUserData); + client_connected_request -> + merge_msg_client_connected_request(Prev, + New, + TrUserData); + client_disconnected_request -> + merge_msg_client_disconnected_request(Prev, + New, + TrUserData); + client_authenticate_request -> + merge_msg_client_authenticate_request(Prev, + New, + TrUserData); + client_authorize_request -> + merge_msg_client_authorize_request(Prev, + New, + TrUserData); + client_subscribe_request -> + merge_msg_client_subscribe_request(Prev, + New, + TrUserData); + client_unsubscribe_request -> + merge_msg_client_unsubscribe_request(Prev, + New, + TrUserData); + session_created_request -> + merge_msg_session_created_request(Prev, + New, + TrUserData); + session_subscribed_request -> + merge_msg_session_subscribed_request(Prev, + New, + TrUserData); + session_unsubscribed_request -> + merge_msg_session_unsubscribed_request(Prev, + New, + TrUserData); + session_resumed_request -> + merge_msg_session_resumed_request(Prev, + New, + TrUserData); + session_discarded_request -> + merge_msg_session_discarded_request(Prev, + New, + TrUserData); + session_takeovered_request -> + merge_msg_session_takeovered_request(Prev, + New, + TrUserData); + session_terminated_request -> + merge_msg_session_terminated_request(Prev, + New, + TrUserData); + message_publish_request -> + merge_msg_message_publish_request(Prev, + New, + TrUserData); + message_delivered_request -> + merge_msg_message_delivered_request(Prev, + New, + TrUserData); + message_dropped_request -> + merge_msg_message_dropped_request(Prev, + New, + TrUserData); + message_acked_request -> + merge_msg_message_acked_request(Prev, New, TrUserData); + empty_success -> + merge_msg_empty_success(Prev, New, TrUserData); + valued_response -> + merge_msg_valued_response(Prev, New, TrUserData); + broker_info -> + merge_msg_broker_info(Prev, New, TrUserData); + hook_spec -> merge_msg_hook_spec(Prev, New, TrUserData); + conn_info -> merge_msg_conn_info(Prev, New, TrUserData); + client_info -> + merge_msg_client_info(Prev, New, TrUserData); + message -> merge_msg_message(Prev, New, TrUserData); + property -> merge_msg_property(Prev, New, TrUserData); + topic_filter -> + merge_msg_topic_filter(Prev, New, TrUserData); + sub_opts -> merge_msg_sub_opts(Prev, New, TrUserData) + end. + +-compile({nowarn_unused_function,merge_msg_provider_loaded_request/3}). +merge_msg_provider_loaded_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + case {PMsg, NMsg} of + {#{broker := PFbroker}, #{broker := NFbroker}} -> + S1#{broker => + merge_msg_broker_info(PFbroker, NFbroker, TrUserData)}; + {_, #{broker := NFbroker}} -> S1#{broker => NFbroker}; + {#{broker := PFbroker}, _} -> S1#{broker => PFbroker}; + {_, _} -> S1 + end. + +-compile({nowarn_unused_function,merge_msg_loaded_response/3}). +merge_msg_loaded_response(PMsg, NMsg, TrUserData) -> + S1 = #{}, + case {PMsg, NMsg} of + {#{hooks := PFhooks}, #{hooks := NFhooks}} -> + S1#{hooks => 'erlang_++'(PFhooks, NFhooks, TrUserData)}; + {_, #{hooks := NFhooks}} -> S1#{hooks => NFhooks}; + {#{hooks := PFhooks}, _} -> S1#{hooks => PFhooks}; + {_, _} -> S1 + end. + +-compile({nowarn_unused_function,merge_msg_provider_unloaded_request/3}). +merge_msg_provider_unloaded_request(_Prev, New, + _TrUserData) -> + New. + +-compile({nowarn_unused_function,merge_msg_client_connect_request/3}). +merge_msg_client_connect_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{conninfo := PFconninfo}, + #{conninfo := NFconninfo}} -> + S1#{conninfo => + merge_msg_conn_info(PFconninfo, + NFconninfo, + TrUserData)}; + {_, #{conninfo := NFconninfo}} -> + S1#{conninfo => NFconninfo}; + {#{conninfo := PFconninfo}, _} -> + S1#{conninfo => PFconninfo}; + {_, _} -> S1 + end, + case {PMsg, NMsg} of + {#{props := PFprops}, #{props := NFprops}} -> + S2#{props => 'erlang_++'(PFprops, NFprops, TrUserData)}; + {_, #{props := NFprops}} -> S2#{props => NFprops}; + {#{props := PFprops}, _} -> S2#{props => PFprops}; + {_, _} -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_client_connack_request/3}). +merge_msg_client_connack_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{conninfo := PFconninfo}, + #{conninfo := NFconninfo}} -> + S1#{conninfo => + merge_msg_conn_info(PFconninfo, + NFconninfo, + TrUserData)}; + {_, #{conninfo := NFconninfo}} -> + S1#{conninfo => NFconninfo}; + {#{conninfo := PFconninfo}, _} -> + S1#{conninfo => PFconninfo}; + {_, _} -> S1 + end, + S3 = case {PMsg, NMsg} of + {_, #{result_code := NFresult_code}} -> + S2#{result_code => NFresult_code}; + {#{result_code := PFresult_code}, _} -> + S2#{result_code => PFresult_code}; + _ -> S2 + end, + case {PMsg, NMsg} of + {#{props := PFprops}, #{props := NFprops}} -> + S3#{props => 'erlang_++'(PFprops, NFprops, TrUserData)}; + {_, #{props := NFprops}} -> S3#{props => NFprops}; + {#{props := PFprops}, _} -> S3#{props => PFprops}; + {_, _} -> S3 + end. + +-compile({nowarn_unused_function,merge_msg_client_connected_request/3}). +merge_msg_client_connected_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end. + +-compile({nowarn_unused_function,merge_msg_client_disconnected_request/3}). +merge_msg_client_disconnected_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + case {PMsg, NMsg} of + {_, #{reason := NFreason}} -> S2#{reason => NFreason}; + {#{reason := PFreason}, _} -> S2#{reason => PFreason}; + _ -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_client_authenticate_request/3}). +merge_msg_client_authenticate_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + case {PMsg, NMsg} of + {_, #{result := NFresult}} -> S2#{result => NFresult}; + {#{result := PFresult}, _} -> S2#{result => PFresult}; + _ -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_client_authorize_request/3}). +merge_msg_client_authorize_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + S3 = case {PMsg, NMsg} of + {_, #{type := NFtype}} -> S2#{type => NFtype}; + {#{type := PFtype}, _} -> S2#{type => PFtype}; + _ -> S2 + end, + S4 = case {PMsg, NMsg} of + {_, #{topic := NFtopic}} -> S3#{topic => NFtopic}; + {#{topic := PFtopic}, _} -> S3#{topic => PFtopic}; + _ -> S3 + end, + case {PMsg, NMsg} of + {_, #{result := NFresult}} -> S4#{result => NFresult}; + {#{result := PFresult}, _} -> S4#{result => PFresult}; + _ -> S4 + end. + +-compile({nowarn_unused_function,merge_msg_client_subscribe_request/3}). +merge_msg_client_subscribe_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + S3 = case {PMsg, NMsg} of + {#{props := PFprops}, #{props := NFprops}} -> + S2#{props => 'erlang_++'(PFprops, NFprops, TrUserData)}; + {_, #{props := NFprops}} -> S2#{props => NFprops}; + {#{props := PFprops}, _} -> S2#{props => PFprops}; + {_, _} -> S2 + end, + case {PMsg, NMsg} of + {#{topic_filters := PFtopic_filters}, + #{topic_filters := NFtopic_filters}} -> + S3#{topic_filters => + 'erlang_++'(PFtopic_filters, + NFtopic_filters, + TrUserData)}; + {_, #{topic_filters := NFtopic_filters}} -> + S3#{topic_filters => NFtopic_filters}; + {#{topic_filters := PFtopic_filters}, _} -> + S3#{topic_filters => PFtopic_filters}; + {_, _} -> S3 + end. + +-compile({nowarn_unused_function,merge_msg_client_unsubscribe_request/3}). +merge_msg_client_unsubscribe_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + S3 = case {PMsg, NMsg} of + {#{props := PFprops}, #{props := NFprops}} -> + S2#{props => 'erlang_++'(PFprops, NFprops, TrUserData)}; + {_, #{props := NFprops}} -> S2#{props => NFprops}; + {#{props := PFprops}, _} -> S2#{props => PFprops}; + {_, _} -> S2 + end, + case {PMsg, NMsg} of + {#{topic_filters := PFtopic_filters}, + #{topic_filters := NFtopic_filters}} -> + S3#{topic_filters => + 'erlang_++'(PFtopic_filters, + NFtopic_filters, + TrUserData)}; + {_, #{topic_filters := NFtopic_filters}} -> + S3#{topic_filters => NFtopic_filters}; + {#{topic_filters := PFtopic_filters}, _} -> + S3#{topic_filters => PFtopic_filters}; + {_, _} -> S3 + end. + +-compile({nowarn_unused_function,merge_msg_session_created_request/3}). +merge_msg_session_created_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end. + +-compile({nowarn_unused_function,merge_msg_session_subscribed_request/3}). +merge_msg_session_subscribed_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + S3 = case {PMsg, NMsg} of + {_, #{topic := NFtopic}} -> S2#{topic => NFtopic}; + {#{topic := PFtopic}, _} -> S2#{topic => PFtopic}; + _ -> S2 + end, + case {PMsg, NMsg} of + {#{subopts := PFsubopts}, #{subopts := NFsubopts}} -> + S3#{subopts => + merge_msg_sub_opts(PFsubopts, NFsubopts, TrUserData)}; + {_, #{subopts := NFsubopts}} -> + S3#{subopts => NFsubopts}; + {#{subopts := PFsubopts}, _} -> + S3#{subopts => PFsubopts}; + {_, _} -> S3 + end. + +-compile({nowarn_unused_function,merge_msg_session_unsubscribed_request/3}). +merge_msg_session_unsubscribed_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + case {PMsg, NMsg} of + {_, #{topic := NFtopic}} -> S2#{topic => NFtopic}; + {#{topic := PFtopic}, _} -> S2#{topic => PFtopic}; + _ -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_session_resumed_request/3}). +merge_msg_session_resumed_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end. + +-compile({nowarn_unused_function,merge_msg_session_discarded_request/3}). +merge_msg_session_discarded_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end. + +-compile({nowarn_unused_function,merge_msg_session_takeovered_request/3}). +merge_msg_session_takeovered_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end. + +-compile({nowarn_unused_function,merge_msg_session_terminated_request/3}). +merge_msg_session_terminated_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + case {PMsg, NMsg} of + {_, #{reason := NFreason}} -> S2#{reason => NFreason}; + {#{reason := PFreason}, _} -> S2#{reason => PFreason}; + _ -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_message_publish_request/3}). +merge_msg_message_publish_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + case {PMsg, NMsg} of + {#{message := PFmessage}, #{message := NFmessage}} -> + S1#{message => + merge_msg_message(PFmessage, NFmessage, TrUserData)}; + {_, #{message := NFmessage}} -> + S1#{message => NFmessage}; + {#{message := PFmessage}, _} -> + S1#{message => PFmessage}; + {_, _} -> S1 + end. + +-compile({nowarn_unused_function,merge_msg_message_delivered_request/3}). +merge_msg_message_delivered_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + case {PMsg, NMsg} of + {#{message := PFmessage}, #{message := NFmessage}} -> + S2#{message => + merge_msg_message(PFmessage, NFmessage, TrUserData)}; + {_, #{message := NFmessage}} -> + S2#{message => NFmessage}; + {#{message := PFmessage}, _} -> + S2#{message => PFmessage}; + {_, _} -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_message_dropped_request/3}). +merge_msg_message_dropped_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{message := PFmessage}, #{message := NFmessage}} -> + S1#{message => + merge_msg_message(PFmessage, NFmessage, TrUserData)}; + {_, #{message := NFmessage}} -> + S1#{message => NFmessage}; + {#{message := PFmessage}, _} -> + S1#{message => PFmessage}; + {_, _} -> S1 + end, + case {PMsg, NMsg} of + {_, #{reason := NFreason}} -> S2#{reason => NFreason}; + {#{reason := PFreason}, _} -> S2#{reason => PFreason}; + _ -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_message_acked_request/3}). +merge_msg_message_acked_request(PMsg, NMsg, + TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {#{clientinfo := PFclientinfo}, + #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => + merge_msg_client_info(PFclientinfo, + NFclientinfo, + TrUserData)}; + {_, #{clientinfo := NFclientinfo}} -> + S1#{clientinfo => NFclientinfo}; + {#{clientinfo := PFclientinfo}, _} -> + S1#{clientinfo => PFclientinfo}; + {_, _} -> S1 + end, + case {PMsg, NMsg} of + {#{message := PFmessage}, #{message := NFmessage}} -> + S2#{message => + merge_msg_message(PFmessage, NFmessage, TrUserData)}; + {_, #{message := NFmessage}} -> + S2#{message => NFmessage}; + {#{message := PFmessage}, _} -> + S2#{message => PFmessage}; + {_, _} -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_empty_success/3}). +merge_msg_empty_success(_Prev, New, _TrUserData) -> New. + +-compile({nowarn_unused_function,merge_msg_valued_response/3}). +merge_msg_valued_response(PMsg, NMsg, TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{type := NFtype}} -> S1#{type => NFtype}; + {#{type := PFtype}, _} -> S1#{type => PFtype}; + _ -> S1 + end, + case {PMsg, NMsg} of + {#{value := {message, OPFvalue}}, + #{value := {message, ONFvalue}}} -> + S2#{value => + {message, + merge_msg_message(OPFvalue, ONFvalue, TrUserData)}}; + {_, #{value := NFvalue}} -> S2#{value => NFvalue}; + {#{value := PFvalue}, _} -> S2#{value => PFvalue}; + {_, _} -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_broker_info/3}). +merge_msg_broker_info(PMsg, NMsg, _) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{version := NFversion}} -> + S1#{version => NFversion}; + {#{version := PFversion}, _} -> + S1#{version => PFversion}; + _ -> S1 + end, + S3 = case {PMsg, NMsg} of + {_, #{sysdescr := NFsysdescr}} -> + S2#{sysdescr => NFsysdescr}; + {#{sysdescr := PFsysdescr}, _} -> + S2#{sysdescr => PFsysdescr}; + _ -> S2 + end, + S4 = case {PMsg, NMsg} of + {_, #{uptime := NFuptime}} -> S3#{uptime => NFuptime}; + {#{uptime := PFuptime}, _} -> S3#{uptime => PFuptime}; + _ -> S3 + end, + case {PMsg, NMsg} of + {_, #{datetime := NFdatetime}} -> + S4#{datetime => NFdatetime}; + {#{datetime := PFdatetime}, _} -> + S4#{datetime => PFdatetime}; + _ -> S4 + end. + +-compile({nowarn_unused_function,merge_msg_hook_spec/3}). +merge_msg_hook_spec(PMsg, NMsg, TrUserData) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{name := NFname}} -> S1#{name => NFname}; + {#{name := PFname}, _} -> S1#{name => PFname}; + _ -> S1 + end, + case {PMsg, NMsg} of + {#{topics := PFtopics}, #{topics := NFtopics}} -> + S2#{topics => + 'erlang_++'(PFtopics, NFtopics, TrUserData)}; + {_, #{topics := NFtopics}} -> S2#{topics => NFtopics}; + {#{topics := PFtopics}, _} -> S2#{topics => PFtopics}; + {_, _} -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_conn_info/3}). +merge_msg_conn_info(PMsg, NMsg, _) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{node := NFnode}} -> S1#{node => NFnode}; + {#{node := PFnode}, _} -> S1#{node => PFnode}; + _ -> S1 + end, + S3 = case {PMsg, NMsg} of + {_, #{clientid := NFclientid}} -> + S2#{clientid => NFclientid}; + {#{clientid := PFclientid}, _} -> + S2#{clientid => PFclientid}; + _ -> S2 + end, + S4 = case {PMsg, NMsg} of + {_, #{username := NFusername}} -> + S3#{username => NFusername}; + {#{username := PFusername}, _} -> + S3#{username => PFusername}; + _ -> S3 + end, + S5 = case {PMsg, NMsg} of + {_, #{peerhost := NFpeerhost}} -> + S4#{peerhost => NFpeerhost}; + {#{peerhost := PFpeerhost}, _} -> + S4#{peerhost => PFpeerhost}; + _ -> S4 + end, + S6 = case {PMsg, NMsg} of + {_, #{sockport := NFsockport}} -> + S5#{sockport => NFsockport}; + {#{sockport := PFsockport}, _} -> + S5#{sockport => PFsockport}; + _ -> S5 + end, + S7 = case {PMsg, NMsg} of + {_, #{proto_name := NFproto_name}} -> + S6#{proto_name => NFproto_name}; + {#{proto_name := PFproto_name}, _} -> + S6#{proto_name => PFproto_name}; + _ -> S6 + end, + S8 = case {PMsg, NMsg} of + {_, #{proto_ver := NFproto_ver}} -> + S7#{proto_ver => NFproto_ver}; + {#{proto_ver := PFproto_ver}, _} -> + S7#{proto_ver => PFproto_ver}; + _ -> S7 + end, + case {PMsg, NMsg} of + {_, #{keepalive := NFkeepalive}} -> + S8#{keepalive => NFkeepalive}; + {#{keepalive := PFkeepalive}, _} -> + S8#{keepalive => PFkeepalive}; + _ -> S8 + end. + +-compile({nowarn_unused_function,merge_msg_client_info/3}). +merge_msg_client_info(PMsg, NMsg, _) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{node := NFnode}} -> S1#{node => NFnode}; + {#{node := PFnode}, _} -> S1#{node => PFnode}; + _ -> S1 + end, + S3 = case {PMsg, NMsg} of + {_, #{clientid := NFclientid}} -> + S2#{clientid => NFclientid}; + {#{clientid := PFclientid}, _} -> + S2#{clientid => PFclientid}; + _ -> S2 + end, + S4 = case {PMsg, NMsg} of + {_, #{username := NFusername}} -> + S3#{username => NFusername}; + {#{username := PFusername}, _} -> + S3#{username => PFusername}; + _ -> S3 + end, + S5 = case {PMsg, NMsg} of + {_, #{password := NFpassword}} -> + S4#{password => NFpassword}; + {#{password := PFpassword}, _} -> + S4#{password => PFpassword}; + _ -> S4 + end, + S6 = case {PMsg, NMsg} of + {_, #{peerhost := NFpeerhost}} -> + S5#{peerhost => NFpeerhost}; + {#{peerhost := PFpeerhost}, _} -> + S5#{peerhost => PFpeerhost}; + _ -> S5 + end, + S7 = case {PMsg, NMsg} of + {_, #{sockport := NFsockport}} -> + S6#{sockport => NFsockport}; + {#{sockport := PFsockport}, _} -> + S6#{sockport => PFsockport}; + _ -> S6 + end, + S8 = case {PMsg, NMsg} of + {_, #{protocol := NFprotocol}} -> + S7#{protocol => NFprotocol}; + {#{protocol := PFprotocol}, _} -> + S7#{protocol => PFprotocol}; + _ -> S7 + end, + S9 = case {PMsg, NMsg} of + {_, #{mountpoint := NFmountpoint}} -> + S8#{mountpoint => NFmountpoint}; + {#{mountpoint := PFmountpoint}, _} -> + S8#{mountpoint => PFmountpoint}; + _ -> S8 + end, + S10 = case {PMsg, NMsg} of + {_, #{is_superuser := NFis_superuser}} -> + S9#{is_superuser => NFis_superuser}; + {#{is_superuser := PFis_superuser}, _} -> + S9#{is_superuser => PFis_superuser}; + _ -> S9 + end, + S11 = case {PMsg, NMsg} of + {_, #{anonymous := NFanonymous}} -> + S10#{anonymous => NFanonymous}; + {#{anonymous := PFanonymous}, _} -> + S10#{anonymous => PFanonymous}; + _ -> S10 + end, + S12 = case {PMsg, NMsg} of + {_, #{cn := NFcn}} -> S11#{cn => NFcn}; + {#{cn := PFcn}, _} -> S11#{cn => PFcn}; + _ -> S11 + end, + case {PMsg, NMsg} of + {_, #{dn := NFdn}} -> S12#{dn => NFdn}; + {#{dn := PFdn}, _} -> S12#{dn => PFdn}; + _ -> S12 + end. + +-compile({nowarn_unused_function,merge_msg_message/3}). +merge_msg_message(PMsg, NMsg, _) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{node := NFnode}} -> S1#{node => NFnode}; + {#{node := PFnode}, _} -> S1#{node => PFnode}; + _ -> S1 + end, + S3 = case {PMsg, NMsg} of + {_, #{id := NFid}} -> S2#{id => NFid}; + {#{id := PFid}, _} -> S2#{id => PFid}; + _ -> S2 + end, + S4 = case {PMsg, NMsg} of + {_, #{qos := NFqos}} -> S3#{qos => NFqos}; + {#{qos := PFqos}, _} -> S3#{qos => PFqos}; + _ -> S3 + end, + S5 = case {PMsg, NMsg} of + {_, #{from := NFfrom}} -> S4#{from => NFfrom}; + {#{from := PFfrom}, _} -> S4#{from => PFfrom}; + _ -> S4 + end, + S6 = case {PMsg, NMsg} of + {_, #{topic := NFtopic}} -> S5#{topic => NFtopic}; + {#{topic := PFtopic}, _} -> S5#{topic => PFtopic}; + _ -> S5 + end, + S7 = case {PMsg, NMsg} of + {_, #{payload := NFpayload}} -> + S6#{payload => NFpayload}; + {#{payload := PFpayload}, _} -> + S6#{payload => PFpayload}; + _ -> S6 + end, + case {PMsg, NMsg} of + {_, #{timestamp := NFtimestamp}} -> + S7#{timestamp => NFtimestamp}; + {#{timestamp := PFtimestamp}, _} -> + S7#{timestamp => PFtimestamp}; + _ -> S7 + end. + +-compile({nowarn_unused_function,merge_msg_property/3}). +merge_msg_property(PMsg, NMsg, _) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{name := NFname}} -> S1#{name => NFname}; + {#{name := PFname}, _} -> S1#{name => PFname}; + _ -> S1 + end, + case {PMsg, NMsg} of + {_, #{value := NFvalue}} -> S2#{value => NFvalue}; + {#{value := PFvalue}, _} -> S2#{value => PFvalue}; + _ -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_topic_filter/3}). +merge_msg_topic_filter(PMsg, NMsg, _) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{name := NFname}} -> S1#{name => NFname}; + {#{name := PFname}, _} -> S1#{name => PFname}; + _ -> S1 + end, + case {PMsg, NMsg} of + {_, #{qos := NFqos}} -> S2#{qos => NFqos}; + {#{qos := PFqos}, _} -> S2#{qos => PFqos}; + _ -> S2 + end. + +-compile({nowarn_unused_function,merge_msg_sub_opts/3}). +merge_msg_sub_opts(PMsg, NMsg, _) -> + S1 = #{}, + S2 = case {PMsg, NMsg} of + {_, #{qos := NFqos}} -> S1#{qos => NFqos}; + {#{qos := PFqos}, _} -> S1#{qos => PFqos}; + _ -> S1 + end, + S3 = case {PMsg, NMsg} of + {_, #{share := NFshare}} -> S2#{share => NFshare}; + {#{share := PFshare}, _} -> S2#{share => PFshare}; + _ -> S2 + end, + S4 = case {PMsg, NMsg} of + {_, #{rh := NFrh}} -> S3#{rh => NFrh}; + {#{rh := PFrh}, _} -> S3#{rh => PFrh}; + _ -> S3 + end, + S5 = case {PMsg, NMsg} of + {_, #{rap := NFrap}} -> S4#{rap => NFrap}; + {#{rap := PFrap}, _} -> S4#{rap => PFrap}; + _ -> S4 + end, + case {PMsg, NMsg} of + {_, #{nl := NFnl}} -> S5#{nl => NFnl}; + {#{nl := PFnl}, _} -> S5#{nl => PFnl}; + _ -> S5 + end. + + +verify_msg(Msg, MsgName) when is_atom(MsgName) -> + verify_msg(Msg, MsgName, []). + +verify_msg(Msg, MsgName, Opts) -> + TrUserData = proplists:get_value(user_data, Opts), + case MsgName of + provider_loaded_request -> + v_msg_provider_loaded_request(Msg, + [MsgName], + TrUserData); + loaded_response -> + v_msg_loaded_response(Msg, [MsgName], TrUserData); + provider_unloaded_request -> + v_msg_provider_unloaded_request(Msg, + [MsgName], + TrUserData); + client_connect_request -> + v_msg_client_connect_request(Msg, + [MsgName], + TrUserData); + client_connack_request -> + v_msg_client_connack_request(Msg, + [MsgName], + TrUserData); + client_connected_request -> + v_msg_client_connected_request(Msg, + [MsgName], + TrUserData); + client_disconnected_request -> + v_msg_client_disconnected_request(Msg, + [MsgName], + TrUserData); + client_authenticate_request -> + v_msg_client_authenticate_request(Msg, + [MsgName], + TrUserData); + client_authorize_request -> + v_msg_client_authorize_request(Msg, + [MsgName], + TrUserData); + client_subscribe_request -> + v_msg_client_subscribe_request(Msg, + [MsgName], + TrUserData); + client_unsubscribe_request -> + v_msg_client_unsubscribe_request(Msg, + [MsgName], + TrUserData); + session_created_request -> + v_msg_session_created_request(Msg, + [MsgName], + TrUserData); + session_subscribed_request -> + v_msg_session_subscribed_request(Msg, + [MsgName], + TrUserData); + session_unsubscribed_request -> + v_msg_session_unsubscribed_request(Msg, + [MsgName], + TrUserData); + session_resumed_request -> + v_msg_session_resumed_request(Msg, + [MsgName], + TrUserData); + session_discarded_request -> + v_msg_session_discarded_request(Msg, + [MsgName], + TrUserData); + session_takeovered_request -> + v_msg_session_takeovered_request(Msg, + [MsgName], + TrUserData); + session_terminated_request -> + v_msg_session_terminated_request(Msg, + [MsgName], + TrUserData); + message_publish_request -> + v_msg_message_publish_request(Msg, + [MsgName], + TrUserData); + message_delivered_request -> + v_msg_message_delivered_request(Msg, + [MsgName], + TrUserData); + message_dropped_request -> + v_msg_message_dropped_request(Msg, + [MsgName], + TrUserData); + message_acked_request -> + v_msg_message_acked_request(Msg, [MsgName], TrUserData); + empty_success -> + v_msg_empty_success(Msg, [MsgName], TrUserData); + valued_response -> + v_msg_valued_response(Msg, [MsgName], TrUserData); + broker_info -> + v_msg_broker_info(Msg, [MsgName], TrUserData); + hook_spec -> + v_msg_hook_spec(Msg, [MsgName], TrUserData); + conn_info -> + v_msg_conn_info(Msg, [MsgName], TrUserData); + client_info -> + v_msg_client_info(Msg, [MsgName], TrUserData); + message -> v_msg_message(Msg, [MsgName], TrUserData); + property -> v_msg_property(Msg, [MsgName], TrUserData); + topic_filter -> + v_msg_topic_filter(Msg, [MsgName], TrUserData); + sub_opts -> v_msg_sub_opts(Msg, [MsgName], TrUserData); + _ -> mk_type_error(not_a_known_message, Msg, []) + end. + + +-compile({nowarn_unused_function,v_msg_provider_loaded_request/3}). +-dialyzer({nowarn_function,v_msg_provider_loaded_request/3}). +v_msg_provider_loaded_request(#{} = M, Path, + TrUserData) -> + case M of + #{broker := F1} -> + v_msg_broker_info(F1, [broker | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (broker) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_provider_loaded_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + provider_loaded_request}, + M, + Path); +v_msg_provider_loaded_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, provider_loaded_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_loaded_response/3}). +-dialyzer({nowarn_function,v_msg_loaded_response/3}). +v_msg_loaded_response(#{} = M, Path, TrUserData) -> + case M of + #{hooks := F1} -> + if is_list(F1) -> + _ = [v_msg_hook_spec(Elem, [hooks | Path], TrUserData) + || Elem <- F1], + ok; + true -> + mk_type_error({invalid_list_of, {msg, hook_spec}}, + F1, + [hooks | Path]) + end; + _ -> ok + end, + lists:foreach(fun (hooks) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_loaded_response(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + loaded_response}, + M, + Path); +v_msg_loaded_response(X, Path, _TrUserData) -> + mk_type_error({expected_msg, loaded_response}, X, Path). + +-compile({nowarn_unused_function,v_msg_provider_unloaded_request/3}). +-dialyzer({nowarn_function,v_msg_provider_unloaded_request/3}). +v_msg_provider_unloaded_request(#{} = M, Path, _) -> + lists:foreach(fun (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_provider_unloaded_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + provider_unloaded_request}, + M, + Path); +v_msg_provider_unloaded_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, provider_unloaded_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_client_connect_request/3}). +-dialyzer({nowarn_function,v_msg_client_connect_request/3}). +v_msg_client_connect_request(#{} = M, Path, + TrUserData) -> + case M of + #{conninfo := F1} -> + v_msg_conn_info(F1, [conninfo | Path], TrUserData); + _ -> ok + end, + case M of + #{props := F2} -> + if is_list(F2) -> + _ = [v_msg_property(Elem, [props | Path], TrUserData) + || Elem <- F2], + ok; + true -> + mk_type_error({invalid_list_of, {msg, property}}, + F2, + [props | Path]) + end; + _ -> ok + end, + lists:foreach(fun (conninfo) -> ok; + (props) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_connect_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_connect_request}, + M, + Path); +v_msg_client_connect_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, client_connect_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_client_connack_request/3}). +-dialyzer({nowarn_function,v_msg_client_connack_request/3}). +v_msg_client_connack_request(#{} = M, Path, + TrUserData) -> + case M of + #{conninfo := F1} -> + v_msg_conn_info(F1, [conninfo | Path], TrUserData); + _ -> ok + end, + case M of + #{result_code := F2} -> + v_type_string(F2, [result_code | Path], TrUserData); + _ -> ok + end, + case M of + #{props := F3} -> + if is_list(F3) -> + _ = [v_msg_property(Elem, [props | Path], TrUserData) + || Elem <- F3], + ok; + true -> + mk_type_error({invalid_list_of, {msg, property}}, + F3, + [props | Path]) + end; + _ -> ok + end, + lists:foreach(fun (conninfo) -> ok; + (result_code) -> ok; + (props) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_connack_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_connack_request}, + M, + Path); +v_msg_client_connack_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, client_connack_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_client_connected_request/3}). +-dialyzer({nowarn_function,v_msg_client_connected_request/3}). +v_msg_client_connected_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_connected_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_connected_request}, + M, + Path); +v_msg_client_connected_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, client_connected_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_client_disconnected_request/3}). +-dialyzer({nowarn_function,v_msg_client_disconnected_request/3}). +v_msg_client_disconnected_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{reason := F2} -> + v_type_string(F2, [reason | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (reason) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_disconnected_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_disconnected_request}, + M, + Path); +v_msg_client_disconnected_request(X, Path, + _TrUserData) -> + mk_type_error({expected_msg, + client_disconnected_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_client_authenticate_request/3}). +-dialyzer({nowarn_function,v_msg_client_authenticate_request/3}). +v_msg_client_authenticate_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{result := F2} -> + v_type_bool(F2, [result | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (result) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_authenticate_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_authenticate_request}, + M, + Path); +v_msg_client_authenticate_request(X, Path, + _TrUserData) -> + mk_type_error({expected_msg, + client_authenticate_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_client_authorize_request/3}). +-dialyzer({nowarn_function,v_msg_client_authorize_request/3}). +v_msg_client_authorize_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{type := F2} -> + 'v_enum_client_authorize_request.AuthorizeReqType'(F2, + [type | Path], + TrUserData); + _ -> ok + end, + case M of + #{topic := F3} -> + v_type_string(F3, [topic | Path], TrUserData); + _ -> ok + end, + case M of + #{result := F4} -> + v_type_bool(F4, [result | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (type) -> ok; + (topic) -> ok; + (result) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_authorize_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_authorize_request}, + M, + Path); +v_msg_client_authorize_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, client_authorize_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_client_subscribe_request/3}). +-dialyzer({nowarn_function,v_msg_client_subscribe_request/3}). +v_msg_client_subscribe_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{props := F2} -> + if is_list(F2) -> + _ = [v_msg_property(Elem, [props | Path], TrUserData) + || Elem <- F2], + ok; + true -> + mk_type_error({invalid_list_of, {msg, property}}, + F2, + [props | Path]) + end; + _ -> ok + end, + case M of + #{topic_filters := F3} -> + if is_list(F3) -> + _ = [v_msg_topic_filter(Elem, + [topic_filters | Path], + TrUserData) + || Elem <- F3], + ok; + true -> + mk_type_error({invalid_list_of, {msg, topic_filter}}, + F3, + [topic_filters | Path]) + end; + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (props) -> ok; + (topic_filters) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_subscribe_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_subscribe_request}, + M, + Path); +v_msg_client_subscribe_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, client_subscribe_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_client_unsubscribe_request/3}). +-dialyzer({nowarn_function,v_msg_client_unsubscribe_request/3}). +v_msg_client_unsubscribe_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{props := F2} -> + if is_list(F2) -> + _ = [v_msg_property(Elem, [props | Path], TrUserData) + || Elem <- F2], + ok; + true -> + mk_type_error({invalid_list_of, {msg, property}}, + F2, + [props | Path]) + end; + _ -> ok + end, + case M of + #{topic_filters := F3} -> + if is_list(F3) -> + _ = [v_msg_topic_filter(Elem, + [topic_filters | Path], + TrUserData) + || Elem <- F3], + ok; + true -> + mk_type_error({invalid_list_of, {msg, topic_filter}}, + F3, + [topic_filters | Path]) + end; + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (props) -> ok; + (topic_filters) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_unsubscribe_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_unsubscribe_request}, + M, + Path); +v_msg_client_unsubscribe_request(X, Path, + _TrUserData) -> + mk_type_error({expected_msg, + client_unsubscribe_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_session_created_request/3}). +-dialyzer({nowarn_function,v_msg_session_created_request/3}). +v_msg_session_created_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_session_created_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + session_created_request}, + M, + Path); +v_msg_session_created_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, session_created_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_session_subscribed_request/3}). +-dialyzer({nowarn_function,v_msg_session_subscribed_request/3}). +v_msg_session_subscribed_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{topic := F2} -> + v_type_string(F2, [topic | Path], TrUserData); + _ -> ok + end, + case M of + #{subopts := F3} -> + v_msg_sub_opts(F3, [subopts | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (topic) -> ok; + (subopts) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_session_subscribed_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + session_subscribed_request}, + M, + Path); +v_msg_session_subscribed_request(X, Path, + _TrUserData) -> + mk_type_error({expected_msg, + session_subscribed_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_session_unsubscribed_request/3}). +-dialyzer({nowarn_function,v_msg_session_unsubscribed_request/3}). +v_msg_session_unsubscribed_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{topic := F2} -> + v_type_string(F2, [topic | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (topic) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_session_unsubscribed_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + session_unsubscribed_request}, + M, + Path); +v_msg_session_unsubscribed_request(X, Path, + _TrUserData) -> + mk_type_error({expected_msg, + session_unsubscribed_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_session_resumed_request/3}). +-dialyzer({nowarn_function,v_msg_session_resumed_request/3}). +v_msg_session_resumed_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_session_resumed_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + session_resumed_request}, + M, + Path); +v_msg_session_resumed_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, session_resumed_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_session_discarded_request/3}). +-dialyzer({nowarn_function,v_msg_session_discarded_request/3}). +v_msg_session_discarded_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_session_discarded_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + session_discarded_request}, + M, + Path); +v_msg_session_discarded_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, session_discarded_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_session_takeovered_request/3}). +-dialyzer({nowarn_function,v_msg_session_takeovered_request/3}). +v_msg_session_takeovered_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_session_takeovered_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + session_takeovered_request}, + M, + Path); +v_msg_session_takeovered_request(X, Path, + _TrUserData) -> + mk_type_error({expected_msg, + session_takeovered_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_session_terminated_request/3}). +-dialyzer({nowarn_function,v_msg_session_terminated_request/3}). +v_msg_session_terminated_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{reason := F2} -> + v_type_string(F2, [reason | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (reason) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_session_terminated_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + session_terminated_request}, + M, + Path); +v_msg_session_terminated_request(X, Path, + _TrUserData) -> + mk_type_error({expected_msg, + session_terminated_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_message_publish_request/3}). +-dialyzer({nowarn_function,v_msg_message_publish_request/3}). +v_msg_message_publish_request(#{} = M, Path, + TrUserData) -> + case M of + #{message := F1} -> + v_msg_message(F1, [message | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (message) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_message_publish_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + message_publish_request}, + M, + Path); +v_msg_message_publish_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, message_publish_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_message_delivered_request/3}). +-dialyzer({nowarn_function,v_msg_message_delivered_request/3}). +v_msg_message_delivered_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{message := F2} -> + v_msg_message(F2, [message | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (message) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_message_delivered_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + message_delivered_request}, + M, + Path); +v_msg_message_delivered_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, message_delivered_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_message_dropped_request/3}). +-dialyzer({nowarn_function,v_msg_message_dropped_request/3}). +v_msg_message_dropped_request(#{} = M, Path, + TrUserData) -> + case M of + #{message := F1} -> + v_msg_message(F1, [message | Path], TrUserData); + _ -> ok + end, + case M of + #{reason := F2} -> + v_type_string(F2, [reason | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (message) -> ok; + (reason) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_message_dropped_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + message_dropped_request}, + M, + Path); +v_msg_message_dropped_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, message_dropped_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_message_acked_request/3}). +-dialyzer({nowarn_function,v_msg_message_acked_request/3}). +v_msg_message_acked_request(#{} = M, Path, + TrUserData) -> + case M of + #{clientinfo := F1} -> + v_msg_client_info(F1, [clientinfo | Path], TrUserData); + _ -> ok + end, + case M of + #{message := F2} -> + v_msg_message(F2, [message | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (clientinfo) -> ok; + (message) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_message_acked_request(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + message_acked_request}, + M, + Path); +v_msg_message_acked_request(X, Path, _TrUserData) -> + mk_type_error({expected_msg, message_acked_request}, + X, + Path). + +-compile({nowarn_unused_function,v_msg_empty_success/3}). +-dialyzer({nowarn_function,v_msg_empty_success/3}). +v_msg_empty_success(#{} = M, Path, _) -> + lists:foreach(fun (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_empty_success(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + empty_success}, + M, + Path); +v_msg_empty_success(X, Path, _TrUserData) -> + mk_type_error({expected_msg, empty_success}, X, Path). + +-compile({nowarn_unused_function,v_msg_valued_response/3}). +-dialyzer({nowarn_function,v_msg_valued_response/3}). +v_msg_valued_response(#{} = M, Path, TrUserData) -> + case M of + #{type := F1} -> + 'v_enum_valued_response.ResponsedType'(F1, + [type | Path], + TrUserData); + _ -> ok + end, + case M of + #{value := {bool_result, OF2}} -> + v_type_bool(OF2, + [bool_result, value | Path], + TrUserData); + #{value := {message, OF2}} -> + v_msg_message(OF2, [message, value | Path], TrUserData); + #{value := F2} -> + mk_type_error(invalid_oneof, F2, [value | Path]); + _ -> ok + end, + lists:foreach(fun (type) -> ok; + (value) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_valued_response(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + valued_response}, + M, + Path); +v_msg_valued_response(X, Path, _TrUserData) -> + mk_type_error({expected_msg, valued_response}, X, Path). + +-compile({nowarn_unused_function,v_msg_broker_info/3}). +-dialyzer({nowarn_function,v_msg_broker_info/3}). +v_msg_broker_info(#{} = M, Path, TrUserData) -> + case M of + #{version := F1} -> + v_type_string(F1, [version | Path], TrUserData); + _ -> ok + end, + case M of + #{sysdescr := F2} -> + v_type_string(F2, [sysdescr | Path], TrUserData); + _ -> ok + end, + case M of + #{uptime := F3} -> + v_type_int64(F3, [uptime | Path], TrUserData); + _ -> ok + end, + case M of + #{datetime := F4} -> + v_type_string(F4, [datetime | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (version) -> ok; + (sysdescr) -> ok; + (uptime) -> ok; + (datetime) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_broker_info(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + broker_info}, + M, + Path); +v_msg_broker_info(X, Path, _TrUserData) -> + mk_type_error({expected_msg, broker_info}, X, Path). + +-compile({nowarn_unused_function,v_msg_hook_spec/3}). +-dialyzer({nowarn_function,v_msg_hook_spec/3}). +v_msg_hook_spec(#{} = M, Path, TrUserData) -> + case M of + #{name := F1} -> + v_type_string(F1, [name | Path], TrUserData); + _ -> ok + end, + case M of + #{topics := F2} -> + if is_list(F2) -> + _ = [v_type_string(Elem, [topics | Path], TrUserData) + || Elem <- F2], + ok; + true -> + mk_type_error({invalid_list_of, string}, + F2, + [topics | Path]) + end; + _ -> ok + end, + lists:foreach(fun (name) -> ok; + (topics) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_hook_spec(M, Path, _TrUserData) when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + hook_spec}, + M, + Path); +v_msg_hook_spec(X, Path, _TrUserData) -> + mk_type_error({expected_msg, hook_spec}, X, Path). + +-compile({nowarn_unused_function,v_msg_conn_info/3}). +-dialyzer({nowarn_function,v_msg_conn_info/3}). +v_msg_conn_info(#{} = M, Path, TrUserData) -> + case M of + #{node := F1} -> + v_type_string(F1, [node | Path], TrUserData); + _ -> ok + end, + case M of + #{clientid := F2} -> + v_type_string(F2, [clientid | Path], TrUserData); + _ -> ok + end, + case M of + #{username := F3} -> + v_type_string(F3, [username | Path], TrUserData); + _ -> ok + end, + case M of + #{peerhost := F4} -> + v_type_string(F4, [peerhost | Path], TrUserData); + _ -> ok + end, + case M of + #{sockport := F5} -> + v_type_uint32(F5, [sockport | Path], TrUserData); + _ -> ok + end, + case M of + #{proto_name := F6} -> + v_type_string(F6, [proto_name | Path], TrUserData); + _ -> ok + end, + case M of + #{proto_ver := F7} -> + v_type_string(F7, [proto_ver | Path], TrUserData); + _ -> ok + end, + case M of + #{keepalive := F8} -> + v_type_uint32(F8, [keepalive | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (node) -> ok; + (clientid) -> ok; + (username) -> ok; + (peerhost) -> ok; + (sockport) -> ok; + (proto_name) -> ok; + (proto_ver) -> ok; + (keepalive) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_conn_info(M, Path, _TrUserData) when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + conn_info}, + M, + Path); +v_msg_conn_info(X, Path, _TrUserData) -> + mk_type_error({expected_msg, conn_info}, X, Path). + +-compile({nowarn_unused_function,v_msg_client_info/3}). +-dialyzer({nowarn_function,v_msg_client_info/3}). +v_msg_client_info(#{} = M, Path, TrUserData) -> + case M of + #{node := F1} -> + v_type_string(F1, [node | Path], TrUserData); + _ -> ok + end, + case M of + #{clientid := F2} -> + v_type_string(F2, [clientid | Path], TrUserData); + _ -> ok + end, + case M of + #{username := F3} -> + v_type_string(F3, [username | Path], TrUserData); + _ -> ok + end, + case M of + #{password := F4} -> + v_type_string(F4, [password | Path], TrUserData); + _ -> ok + end, + case M of + #{peerhost := F5} -> + v_type_string(F5, [peerhost | Path], TrUserData); + _ -> ok + end, + case M of + #{sockport := F6} -> + v_type_uint32(F6, [sockport | Path], TrUserData); + _ -> ok + end, + case M of + #{protocol := F7} -> + v_type_string(F7, [protocol | Path], TrUserData); + _ -> ok + end, + case M of + #{mountpoint := F8} -> + v_type_string(F8, [mountpoint | Path], TrUserData); + _ -> ok + end, + case M of + #{is_superuser := F9} -> + v_type_bool(F9, [is_superuser | Path], TrUserData); + _ -> ok + end, + case M of + #{anonymous := F10} -> + v_type_bool(F10, [anonymous | Path], TrUserData); + _ -> ok + end, + case M of + #{cn := F11} -> + v_type_string(F11, [cn | Path], TrUserData); + _ -> ok + end, + case M of + #{dn := F12} -> + v_type_string(F12, [dn | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (node) -> ok; + (clientid) -> ok; + (username) -> ok; + (password) -> ok; + (peerhost) -> ok; + (sockport) -> ok; + (protocol) -> ok; + (mountpoint) -> ok; + (is_superuser) -> ok; + (anonymous) -> ok; + (cn) -> ok; + (dn) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_client_info(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + client_info}, + M, + Path); +v_msg_client_info(X, Path, _TrUserData) -> + mk_type_error({expected_msg, client_info}, X, Path). + +-compile({nowarn_unused_function,v_msg_message/3}). +-dialyzer({nowarn_function,v_msg_message/3}). +v_msg_message(#{} = M, Path, TrUserData) -> + case M of + #{node := F1} -> + v_type_string(F1, [node | Path], TrUserData); + _ -> ok + end, + case M of + #{id := F2} -> + v_type_string(F2, [id | Path], TrUserData); + _ -> ok + end, + case M of + #{qos := F3} -> + v_type_uint32(F3, [qos | Path], TrUserData); + _ -> ok + end, + case M of + #{from := F4} -> + v_type_string(F4, [from | Path], TrUserData); + _ -> ok + end, + case M of + #{topic := F5} -> + v_type_string(F5, [topic | Path], TrUserData); + _ -> ok + end, + case M of + #{payload := F6} -> + v_type_bytes(F6, [payload | Path], TrUserData); + _ -> ok + end, + case M of + #{timestamp := F7} -> + v_type_uint64(F7, [timestamp | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (node) -> ok; + (id) -> ok; + (qos) -> ok; + (from) -> ok; + (topic) -> ok; + (payload) -> ok; + (timestamp) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_message(M, Path, _TrUserData) when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + message}, + M, + Path); +v_msg_message(X, Path, _TrUserData) -> + mk_type_error({expected_msg, message}, X, Path). + +-compile({nowarn_unused_function,v_msg_property/3}). +-dialyzer({nowarn_function,v_msg_property/3}). +v_msg_property(#{} = M, Path, TrUserData) -> + case M of + #{name := F1} -> + v_type_string(F1, [name | Path], TrUserData); + _ -> ok + end, + case M of + #{value := F2} -> + v_type_string(F2, [value | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (name) -> ok; + (value) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_property(M, Path, _TrUserData) when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + property}, + M, + Path); +v_msg_property(X, Path, _TrUserData) -> + mk_type_error({expected_msg, property}, X, Path). + +-compile({nowarn_unused_function,v_msg_topic_filter/3}). +-dialyzer({nowarn_function,v_msg_topic_filter/3}). +v_msg_topic_filter(#{} = M, Path, TrUserData) -> + case M of + #{name := F1} -> + v_type_string(F1, [name | Path], TrUserData); + _ -> ok + end, + case M of + #{qos := F2} -> + v_type_uint32(F2, [qos | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (name) -> ok; + (qos) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_topic_filter(M, Path, _TrUserData) + when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + topic_filter}, + M, + Path); +v_msg_topic_filter(X, Path, _TrUserData) -> + mk_type_error({expected_msg, topic_filter}, X, Path). + +-compile({nowarn_unused_function,v_msg_sub_opts/3}). +-dialyzer({nowarn_function,v_msg_sub_opts/3}). +v_msg_sub_opts(#{} = M, Path, TrUserData) -> + case M of + #{qos := F1} -> + v_type_uint32(F1, [qos | Path], TrUserData); + _ -> ok + end, + case M of + #{share := F2} -> + v_type_string(F2, [share | Path], TrUserData); + _ -> ok + end, + case M of + #{rh := F3} -> + v_type_uint32(F3, [rh | Path], TrUserData); + _ -> ok + end, + case M of + #{rap := F4} -> + v_type_uint32(F4, [rap | Path], TrUserData); + _ -> ok + end, + case M of + #{nl := F5} -> + v_type_uint32(F5, [nl | Path], TrUserData); + _ -> ok + end, + lists:foreach(fun (qos) -> ok; + (share) -> ok; + (rh) -> ok; + (rap) -> ok; + (nl) -> ok; + (OtherKey) -> + mk_type_error({extraneous_key, OtherKey}, M, Path) + end, + maps:keys(M)), + ok; +v_msg_sub_opts(M, Path, _TrUserData) when is_map(M) -> + mk_type_error({missing_fields, + [] -- maps:keys(M), + sub_opts}, + M, + Path); +v_msg_sub_opts(X, Path, _TrUserData) -> + mk_type_error({expected_msg, sub_opts}, X, Path). + +-compile({nowarn_unused_function,'v_enum_client_authorize_request.AuthorizeReqType'/3}). +-dialyzer({nowarn_function,'v_enum_client_authorize_request.AuthorizeReqType'/3}). +'v_enum_client_authorize_request.AuthorizeReqType'('PUBLISH', + _Path, _TrUserData) -> + ok; +'v_enum_client_authorize_request.AuthorizeReqType'('SUBSCRIBE', + _Path, _TrUserData) -> + ok; +'v_enum_client_authorize_request.AuthorizeReqType'(V, + Path, TrUserData) + when is_integer(V) -> + v_type_sint32(V, Path, TrUserData); +'v_enum_client_authorize_request.AuthorizeReqType'(X, + Path, _TrUserData) -> + mk_type_error({invalid_enum, + 'client_authorize_request.AuthorizeReqType'}, + X, + Path). + +-compile({nowarn_unused_function,'v_enum_valued_response.ResponsedType'/3}). +-dialyzer({nowarn_function,'v_enum_valued_response.ResponsedType'/3}). +'v_enum_valued_response.ResponsedType'('CONTINUE', + _Path, _TrUserData) -> + ok; +'v_enum_valued_response.ResponsedType'('IGNORE', _Path, + _TrUserData) -> + ok; +'v_enum_valued_response.ResponsedType'('STOP_AND_RETURN', + _Path, _TrUserData) -> + ok; +'v_enum_valued_response.ResponsedType'(V, Path, + TrUserData) + when is_integer(V) -> + v_type_sint32(V, Path, TrUserData); +'v_enum_valued_response.ResponsedType'(X, Path, + _TrUserData) -> + mk_type_error({invalid_enum, + 'valued_response.ResponsedType'}, + X, + Path). + +-compile({nowarn_unused_function,v_type_sint32/3}). +-dialyzer({nowarn_function,v_type_sint32/3}). +v_type_sint32(N, _Path, _TrUserData) + when -2147483648 =< N, N =< 2147483647 -> + ok; +v_type_sint32(N, Path, _TrUserData) + when is_integer(N) -> + mk_type_error({value_out_of_range, sint32, signed, 32}, + N, + Path); +v_type_sint32(X, Path, _TrUserData) -> + mk_type_error({bad_integer, sint32, signed, 32}, + X, + Path). + +-compile({nowarn_unused_function,v_type_int64/3}). +-dialyzer({nowarn_function,v_type_int64/3}). +v_type_int64(N, _Path, _TrUserData) + when -9223372036854775808 =< N, + N =< 9223372036854775807 -> + ok; +v_type_int64(N, Path, _TrUserData) when is_integer(N) -> + mk_type_error({value_out_of_range, int64, signed, 64}, + N, + Path); +v_type_int64(X, Path, _TrUserData) -> + mk_type_error({bad_integer, int64, signed, 64}, + X, + Path). + +-compile({nowarn_unused_function,v_type_uint32/3}). +-dialyzer({nowarn_function,v_type_uint32/3}). +v_type_uint32(N, _Path, _TrUserData) + when 0 =< N, N =< 4294967295 -> + ok; +v_type_uint32(N, Path, _TrUserData) + when is_integer(N) -> + mk_type_error({value_out_of_range, + uint32, + unsigned, + 32}, + N, + Path); +v_type_uint32(X, Path, _TrUserData) -> + mk_type_error({bad_integer, uint32, unsigned, 32}, + X, + Path). + +-compile({nowarn_unused_function,v_type_uint64/3}). +-dialyzer({nowarn_function,v_type_uint64/3}). +v_type_uint64(N, _Path, _TrUserData) + when 0 =< N, N =< 18446744073709551615 -> + ok; +v_type_uint64(N, Path, _TrUserData) + when is_integer(N) -> + mk_type_error({value_out_of_range, + uint64, + unsigned, + 64}, + N, + Path); +v_type_uint64(X, Path, _TrUserData) -> + mk_type_error({bad_integer, uint64, unsigned, 64}, + X, + Path). + +-compile({nowarn_unused_function,v_type_bool/3}). +-dialyzer({nowarn_function,v_type_bool/3}). +v_type_bool(false, _Path, _TrUserData) -> ok; +v_type_bool(true, _Path, _TrUserData) -> ok; +v_type_bool(0, _Path, _TrUserData) -> ok; +v_type_bool(1, _Path, _TrUserData) -> ok; +v_type_bool(X, Path, _TrUserData) -> + mk_type_error(bad_boolean_value, X, Path). + +-compile({nowarn_unused_function,v_type_string/3}). +-dialyzer({nowarn_function,v_type_string/3}). +v_type_string(S, Path, _TrUserData) + when is_list(S); is_binary(S) -> + try unicode:characters_to_binary(S) of + B when is_binary(B) -> ok; + {error, _, _} -> + mk_type_error(bad_unicode_string, S, Path) + catch + error:badarg -> + mk_type_error(bad_unicode_string, S, Path) + end; +v_type_string(X, Path, _TrUserData) -> + mk_type_error(bad_unicode_string, X, Path). + +-compile({nowarn_unused_function,v_type_bytes/3}). +-dialyzer({nowarn_function,v_type_bytes/3}). +v_type_bytes(B, _Path, _TrUserData) when is_binary(B) -> + ok; +v_type_bytes(B, _Path, _TrUserData) when is_list(B) -> + ok; +v_type_bytes(X, Path, _TrUserData) -> + mk_type_error(bad_binary_value, X, Path). + +-compile({nowarn_unused_function,mk_type_error/3}). +-spec mk_type_error(_, _, list()) -> no_return(). +mk_type_error(Error, ValueSeen, Path) -> + Path2 = prettify_path(Path), + erlang:error({gpb_type_error, + {Error, [{value, ValueSeen}, {path, Path2}]}}). + + +-compile({nowarn_unused_function,prettify_path/1}). +-dialyzer({nowarn_function,prettify_path/1}). +prettify_path([]) -> top_level; +prettify_path(PathR) -> + list_to_atom(lists:append(lists:join(".", + lists:map(fun atom_to_list/1, + lists:reverse(PathR))))). + + +-compile({nowarn_unused_function,id/2}). +-compile({inline,id/2}). +id(X, _TrUserData) -> X. + +-compile({nowarn_unused_function,v_ok/3}). +-compile({inline,v_ok/3}). +v_ok(_Value, _Path, _TrUserData) -> ok. + +-compile({nowarn_unused_function,m_overwrite/3}). +-compile({inline,m_overwrite/3}). +m_overwrite(_Prev, New, _TrUserData) -> New. + +-compile({nowarn_unused_function,cons/3}). +-compile({inline,cons/3}). +cons(Elem, Acc, _TrUserData) -> [Elem | Acc]. + +-compile({nowarn_unused_function,lists_reverse/2}). +-compile({inline,lists_reverse/2}). +'lists_reverse'(L, _TrUserData) -> lists:reverse(L). +-compile({nowarn_unused_function,'erlang_++'/3}). +-compile({inline,'erlang_++'/3}). +'erlang_++'(A, B, _TrUserData) -> A ++ B. + + +get_msg_defs() -> + [{{enum, 'client_authorize_request.AuthorizeReqType'}, + [{'PUBLISH', 0}, {'SUBSCRIBE', 1}]}, + {{enum, 'valued_response.ResponsedType'}, + [{'CONTINUE', 0}, + {'IGNORE', 1}, + {'STOP_AND_RETURN', 2}]}, + {{msg, provider_loaded_request}, + [#{name => broker, fnum => 1, rnum => 2, + type => {msg, broker_info}, occurrence => optional, + opts => []}]}, + {{msg, loaded_response}, + [#{name => hooks, fnum => 1, rnum => 2, + type => {msg, hook_spec}, occurrence => repeated, + opts => []}]}, + {{msg, provider_unloaded_request}, []}, + {{msg, client_connect_request}, + [#{name => conninfo, fnum => 1, rnum => 2, + type => {msg, conn_info}, occurrence => optional, + opts => []}, + #{name => props, fnum => 2, rnum => 3, + type => {msg, property}, occurrence => repeated, + opts => []}]}, + {{msg, client_connack_request}, + [#{name => conninfo, fnum => 1, rnum => 2, + type => {msg, conn_info}, occurrence => optional, + opts => []}, + #{name => result_code, fnum => 2, rnum => 3, + type => string, occurrence => optional, opts => []}, + #{name => props, fnum => 3, rnum => 4, + type => {msg, property}, occurrence => repeated, + opts => []}]}, + {{msg, client_connected_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]}, + {{msg, client_disconnected_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => reason, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]}, + {{msg, client_authenticate_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => result, fnum => 2, rnum => 3, type => bool, + occurrence => optional, opts => []}]}, + {{msg, client_authorize_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => type, fnum => 2, rnum => 3, + type => + {enum, 'client_authorize_request.AuthorizeReqType'}, + occurrence => optional, opts => []}, + #{name => topic, fnum => 3, rnum => 4, type => string, + occurrence => optional, opts => []}, + #{name => result, fnum => 4, rnum => 5, type => bool, + occurrence => optional, opts => []}]}, + {{msg, client_subscribe_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => props, fnum => 2, rnum => 3, + type => {msg, property}, occurrence => repeated, + opts => []}, + #{name => topic_filters, fnum => 3, rnum => 4, + type => {msg, topic_filter}, occurrence => repeated, + opts => []}]}, + {{msg, client_unsubscribe_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => props, fnum => 2, rnum => 3, + type => {msg, property}, occurrence => repeated, + opts => []}, + #{name => topic_filters, fnum => 3, rnum => 4, + type => {msg, topic_filter}, occurrence => repeated, + opts => []}]}, + {{msg, session_created_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]}, + {{msg, session_subscribed_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => topic, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}, + #{name => subopts, fnum => 3, rnum => 4, + type => {msg, sub_opts}, occurrence => optional, + opts => []}]}, + {{msg, session_unsubscribed_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => topic, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]}, + {{msg, session_resumed_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]}, + {{msg, session_discarded_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]}, + {{msg, session_takeovered_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]}, + {{msg, session_terminated_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => reason, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]}, + {{msg, message_publish_request}, + [#{name => message, fnum => 1, rnum => 2, + type => {msg, message}, occurrence => optional, + opts => []}]}, + {{msg, message_delivered_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => message, fnum => 2, rnum => 3, + type => {msg, message}, occurrence => optional, + opts => []}]}, + {{msg, message_dropped_request}, + [#{name => message, fnum => 1, rnum => 2, + type => {msg, message}, occurrence => optional, + opts => []}, + #{name => reason, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]}, + {{msg, message_acked_request}, + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => message, fnum => 2, rnum => 3, + type => {msg, message}, occurrence => optional, + opts => []}]}, + {{msg, empty_success}, []}, + {{msg, valued_response}, + [#{name => type, fnum => 1, rnum => 2, + type => {enum, 'valued_response.ResponsedType'}, + occurrence => optional, opts => []}, + #{name => value, rnum => 3, + fields => + [#{name => bool_result, fnum => 3, rnum => 3, + type => bool, occurrence => optional, opts => []}, + #{name => message, fnum => 4, rnum => 3, + type => {msg, message}, occurrence => optional, + opts => []}]}]}, + {{msg, broker_info}, + [#{name => version, fnum => 1, rnum => 2, + type => string, occurrence => optional, opts => []}, + #{name => sysdescr, fnum => 2, rnum => 3, + type => string, occurrence => optional, opts => []}, + #{name => uptime, fnum => 3, rnum => 4, type => int64, + occurrence => optional, opts => []}, + #{name => datetime, fnum => 4, rnum => 5, + type => string, occurrence => optional, opts => []}]}, + {{msg, hook_spec}, + [#{name => name, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => topics, fnum => 2, rnum => 3, type => string, + occurrence => repeated, opts => []}]}, + {{msg, conn_info}, + [#{name => node, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => clientid, fnum => 2, rnum => 3, + type => string, occurrence => optional, opts => []}, + #{name => username, fnum => 3, rnum => 4, + type => string, occurrence => optional, opts => []}, + #{name => peerhost, fnum => 4, rnum => 5, + type => string, occurrence => optional, opts => []}, + #{name => sockport, fnum => 5, rnum => 6, + type => uint32, occurrence => optional, opts => []}, + #{name => proto_name, fnum => 6, rnum => 7, + type => string, occurrence => optional, opts => []}, + #{name => proto_ver, fnum => 7, rnum => 8, + type => string, occurrence => optional, opts => []}, + #{name => keepalive, fnum => 8, rnum => 9, + type => uint32, occurrence => optional, opts => []}]}, + {{msg, client_info}, + [#{name => node, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => clientid, fnum => 2, rnum => 3, + type => string, occurrence => optional, opts => []}, + #{name => username, fnum => 3, rnum => 4, + type => string, occurrence => optional, opts => []}, + #{name => password, fnum => 4, rnum => 5, + type => string, occurrence => optional, opts => []}, + #{name => peerhost, fnum => 5, rnum => 6, + type => string, occurrence => optional, opts => []}, + #{name => sockport, fnum => 6, rnum => 7, + type => uint32, occurrence => optional, opts => []}, + #{name => protocol, fnum => 7, rnum => 8, + type => string, occurrence => optional, opts => []}, + #{name => mountpoint, fnum => 8, rnum => 9, + type => string, occurrence => optional, opts => []}, + #{name => is_superuser, fnum => 9, rnum => 10, + type => bool, occurrence => optional, opts => []}, + #{name => anonymous, fnum => 10, rnum => 11, + type => bool, occurrence => optional, opts => []}, + #{name => cn, fnum => 11, rnum => 12, type => string, + occurrence => optional, opts => []}, + #{name => dn, fnum => 12, rnum => 13, type => string, + occurrence => optional, opts => []}]}, + {{msg, message}, + [#{name => node, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => id, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}, + #{name => qos, fnum => 3, rnum => 4, type => uint32, + occurrence => optional, opts => []}, + #{name => from, fnum => 4, rnum => 5, type => string, + occurrence => optional, opts => []}, + #{name => topic, fnum => 5, rnum => 6, type => string, + occurrence => optional, opts => []}, + #{name => payload, fnum => 6, rnum => 7, type => bytes, + occurrence => optional, opts => []}, + #{name => timestamp, fnum => 7, rnum => 8, + type => uint64, occurrence => optional, opts => []}]}, + {{msg, property}, + [#{name => name, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => value, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]}, + {{msg, topic_filter}, + [#{name => name, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => qos, fnum => 2, rnum => 3, type => uint32, + occurrence => optional, opts => []}]}, + {{msg, sub_opts}, + [#{name => qos, fnum => 1, rnum => 2, type => uint32, + occurrence => optional, opts => []}, + #{name => share, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}, + #{name => rh, fnum => 3, rnum => 4, type => uint32, + occurrence => optional, opts => []}, + #{name => rap, fnum => 4, rnum => 5, type => uint32, + occurrence => optional, opts => []}, + #{name => nl, fnum => 5, rnum => 6, type => uint32, + occurrence => optional, opts => []}]}]. + + +get_msg_names() -> + [provider_loaded_request, + loaded_response, + provider_unloaded_request, + client_connect_request, + client_connack_request, + client_connected_request, + client_disconnected_request, + client_authenticate_request, + client_authorize_request, + client_subscribe_request, + client_unsubscribe_request, + session_created_request, + session_subscribed_request, + session_unsubscribed_request, + session_resumed_request, + session_discarded_request, + session_takeovered_request, + session_terminated_request, + message_publish_request, + message_delivered_request, + message_dropped_request, + message_acked_request, + empty_success, + valued_response, + broker_info, + hook_spec, + conn_info, + client_info, + message, + property, + topic_filter, + sub_opts]. + + +get_group_names() -> []. + + +get_msg_or_group_names() -> + [provider_loaded_request, + loaded_response, + provider_unloaded_request, + client_connect_request, + client_connack_request, + client_connected_request, + client_disconnected_request, + client_authenticate_request, + client_authorize_request, + client_subscribe_request, + client_unsubscribe_request, + session_created_request, + session_subscribed_request, + session_unsubscribed_request, + session_resumed_request, + session_discarded_request, + session_takeovered_request, + session_terminated_request, + message_publish_request, + message_delivered_request, + message_dropped_request, + message_acked_request, + empty_success, + valued_response, + broker_info, + hook_spec, + conn_info, + client_info, + message, + property, + topic_filter, + sub_opts]. + + +get_enum_names() -> + ['client_authorize_request.AuthorizeReqType', + 'valued_response.ResponsedType']. + + +fetch_msg_def(MsgName) -> + case find_msg_def(MsgName) of + Fs when is_list(Fs) -> Fs; + error -> erlang:error({no_such_msg, MsgName}) + end. + + +fetch_enum_def(EnumName) -> + case find_enum_def(EnumName) of + Es when is_list(Es) -> Es; + error -> erlang:error({no_such_enum, EnumName}) + end. + + +find_msg_def(provider_loaded_request) -> + [#{name => broker, fnum => 1, rnum => 2, + type => {msg, broker_info}, occurrence => optional, + opts => []}]; +find_msg_def(loaded_response) -> + [#{name => hooks, fnum => 1, rnum => 2, + type => {msg, hook_spec}, occurrence => repeated, + opts => []}]; +find_msg_def(provider_unloaded_request) -> []; +find_msg_def(client_connect_request) -> + [#{name => conninfo, fnum => 1, rnum => 2, + type => {msg, conn_info}, occurrence => optional, + opts => []}, + #{name => props, fnum => 2, rnum => 3, + type => {msg, property}, occurrence => repeated, + opts => []}]; +find_msg_def(client_connack_request) -> + [#{name => conninfo, fnum => 1, rnum => 2, + type => {msg, conn_info}, occurrence => optional, + opts => []}, + #{name => result_code, fnum => 2, rnum => 3, + type => string, occurrence => optional, opts => []}, + #{name => props, fnum => 3, rnum => 4, + type => {msg, property}, occurrence => repeated, + opts => []}]; +find_msg_def(client_connected_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]; +find_msg_def(client_disconnected_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => reason, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]; +find_msg_def(client_authenticate_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => result, fnum => 2, rnum => 3, type => bool, + occurrence => optional, opts => []}]; +find_msg_def(client_authorize_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => type, fnum => 2, rnum => 3, + type => + {enum, 'client_authorize_request.AuthorizeReqType'}, + occurrence => optional, opts => []}, + #{name => topic, fnum => 3, rnum => 4, type => string, + occurrence => optional, opts => []}, + #{name => result, fnum => 4, rnum => 5, type => bool, + occurrence => optional, opts => []}]; +find_msg_def(client_subscribe_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => props, fnum => 2, rnum => 3, + type => {msg, property}, occurrence => repeated, + opts => []}, + #{name => topic_filters, fnum => 3, rnum => 4, + type => {msg, topic_filter}, occurrence => repeated, + opts => []}]; +find_msg_def(client_unsubscribe_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => props, fnum => 2, rnum => 3, + type => {msg, property}, occurrence => repeated, + opts => []}, + #{name => topic_filters, fnum => 3, rnum => 4, + type => {msg, topic_filter}, occurrence => repeated, + opts => []}]; +find_msg_def(session_created_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]; +find_msg_def(session_subscribed_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => topic, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}, + #{name => subopts, fnum => 3, rnum => 4, + type => {msg, sub_opts}, occurrence => optional, + opts => []}]; +find_msg_def(session_unsubscribed_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => topic, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]; +find_msg_def(session_resumed_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]; +find_msg_def(session_discarded_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]; +find_msg_def(session_takeovered_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}]; +find_msg_def(session_terminated_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => reason, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]; +find_msg_def(message_publish_request) -> + [#{name => message, fnum => 1, rnum => 2, + type => {msg, message}, occurrence => optional, + opts => []}]; +find_msg_def(message_delivered_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => message, fnum => 2, rnum => 3, + type => {msg, message}, occurrence => optional, + opts => []}]; +find_msg_def(message_dropped_request) -> + [#{name => message, fnum => 1, rnum => 2, + type => {msg, message}, occurrence => optional, + opts => []}, + #{name => reason, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]; +find_msg_def(message_acked_request) -> + [#{name => clientinfo, fnum => 1, rnum => 2, + type => {msg, client_info}, occurrence => optional, + opts => []}, + #{name => message, fnum => 2, rnum => 3, + type => {msg, message}, occurrence => optional, + opts => []}]; +find_msg_def(empty_success) -> []; +find_msg_def(valued_response) -> + [#{name => type, fnum => 1, rnum => 2, + type => {enum, 'valued_response.ResponsedType'}, + occurrence => optional, opts => []}, + #{name => value, rnum => 3, + fields => + [#{name => bool_result, fnum => 3, rnum => 3, + type => bool, occurrence => optional, opts => []}, + #{name => message, fnum => 4, rnum => 3, + type => {msg, message}, occurrence => optional, + opts => []}]}]; +find_msg_def(broker_info) -> + [#{name => version, fnum => 1, rnum => 2, + type => string, occurrence => optional, opts => []}, + #{name => sysdescr, fnum => 2, rnum => 3, + type => string, occurrence => optional, opts => []}, + #{name => uptime, fnum => 3, rnum => 4, type => int64, + occurrence => optional, opts => []}, + #{name => datetime, fnum => 4, rnum => 5, + type => string, occurrence => optional, opts => []}]; +find_msg_def(hook_spec) -> + [#{name => name, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => topics, fnum => 2, rnum => 3, type => string, + occurrence => repeated, opts => []}]; +find_msg_def(conn_info) -> + [#{name => node, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => clientid, fnum => 2, rnum => 3, + type => string, occurrence => optional, opts => []}, + #{name => username, fnum => 3, rnum => 4, + type => string, occurrence => optional, opts => []}, + #{name => peerhost, fnum => 4, rnum => 5, + type => string, occurrence => optional, opts => []}, + #{name => sockport, fnum => 5, rnum => 6, + type => uint32, occurrence => optional, opts => []}, + #{name => proto_name, fnum => 6, rnum => 7, + type => string, occurrence => optional, opts => []}, + #{name => proto_ver, fnum => 7, rnum => 8, + type => string, occurrence => optional, opts => []}, + #{name => keepalive, fnum => 8, rnum => 9, + type => uint32, occurrence => optional, opts => []}]; +find_msg_def(client_info) -> + [#{name => node, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => clientid, fnum => 2, rnum => 3, + type => string, occurrence => optional, opts => []}, + #{name => username, fnum => 3, rnum => 4, + type => string, occurrence => optional, opts => []}, + #{name => password, fnum => 4, rnum => 5, + type => string, occurrence => optional, opts => []}, + #{name => peerhost, fnum => 5, rnum => 6, + type => string, occurrence => optional, opts => []}, + #{name => sockport, fnum => 6, rnum => 7, + type => uint32, occurrence => optional, opts => []}, + #{name => protocol, fnum => 7, rnum => 8, + type => string, occurrence => optional, opts => []}, + #{name => mountpoint, fnum => 8, rnum => 9, + type => string, occurrence => optional, opts => []}, + #{name => is_superuser, fnum => 9, rnum => 10, + type => bool, occurrence => optional, opts => []}, + #{name => anonymous, fnum => 10, rnum => 11, + type => bool, occurrence => optional, opts => []}, + #{name => cn, fnum => 11, rnum => 12, type => string, + occurrence => optional, opts => []}, + #{name => dn, fnum => 12, rnum => 13, type => string, + occurrence => optional, opts => []}]; +find_msg_def(message) -> + [#{name => node, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => id, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}, + #{name => qos, fnum => 3, rnum => 4, type => uint32, + occurrence => optional, opts => []}, + #{name => from, fnum => 4, rnum => 5, type => string, + occurrence => optional, opts => []}, + #{name => topic, fnum => 5, rnum => 6, type => string, + occurrence => optional, opts => []}, + #{name => payload, fnum => 6, rnum => 7, type => bytes, + occurrence => optional, opts => []}, + #{name => timestamp, fnum => 7, rnum => 8, + type => uint64, occurrence => optional, opts => []}]; +find_msg_def(property) -> + [#{name => name, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => value, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}]; +find_msg_def(topic_filter) -> + [#{name => name, fnum => 1, rnum => 2, type => string, + occurrence => optional, opts => []}, + #{name => qos, fnum => 2, rnum => 3, type => uint32, + occurrence => optional, opts => []}]; +find_msg_def(sub_opts) -> + [#{name => qos, fnum => 1, rnum => 2, type => uint32, + occurrence => optional, opts => []}, + #{name => share, fnum => 2, rnum => 3, type => string, + occurrence => optional, opts => []}, + #{name => rh, fnum => 3, rnum => 4, type => uint32, + occurrence => optional, opts => []}, + #{name => rap, fnum => 4, rnum => 5, type => uint32, + occurrence => optional, opts => []}, + #{name => nl, fnum => 5, rnum => 6, type => uint32, + occurrence => optional, opts => []}]; +find_msg_def(_) -> error. + + +find_enum_def('client_authorize_request.AuthorizeReqType') -> + [{'PUBLISH', 0}, {'SUBSCRIBE', 1}]; +find_enum_def('valued_response.ResponsedType') -> + [{'CONTINUE', 0}, + {'IGNORE', 1}, + {'STOP_AND_RETURN', 2}]; +find_enum_def(_) -> error. + + +enum_symbol_by_value('client_authorize_request.AuthorizeReqType', + Value) -> + 'enum_symbol_by_value_client_authorize_request.AuthorizeReqType'(Value); +enum_symbol_by_value('valued_response.ResponsedType', + Value) -> + 'enum_symbol_by_value_valued_response.ResponsedType'(Value). + + +enum_value_by_symbol('client_authorize_request.AuthorizeReqType', + Sym) -> + 'enum_value_by_symbol_client_authorize_request.AuthorizeReqType'(Sym); +enum_value_by_symbol('valued_response.ResponsedType', + Sym) -> + 'enum_value_by_symbol_valued_response.ResponsedType'(Sym). + + +'enum_symbol_by_value_client_authorize_request.AuthorizeReqType'(0) -> + 'PUBLISH'; +'enum_symbol_by_value_client_authorize_request.AuthorizeReqType'(1) -> + 'SUBSCRIBE'. + + +'enum_value_by_symbol_client_authorize_request.AuthorizeReqType'('PUBLISH') -> + 0; +'enum_value_by_symbol_client_authorize_request.AuthorizeReqType'('SUBSCRIBE') -> + 1. + +'enum_symbol_by_value_valued_response.ResponsedType'(0) -> + 'CONTINUE'; +'enum_symbol_by_value_valued_response.ResponsedType'(1) -> + 'IGNORE'; +'enum_symbol_by_value_valued_response.ResponsedType'(2) -> + 'STOP_AND_RETURN'. + + +'enum_value_by_symbol_valued_response.ResponsedType'('CONTINUE') -> + 0; +'enum_value_by_symbol_valued_response.ResponsedType'('IGNORE') -> + 1; +'enum_value_by_symbol_valued_response.ResponsedType'('STOP_AND_RETURN') -> + 2. + + +get_service_names() -> ['emqx.exhook.v1.HookProvider']. + + +get_service_def('emqx.exhook.v1.HookProvider') -> + {{service, 'emqx.exhook.v1.HookProvider'}, + [#{name => 'OnProviderLoaded', + input => provider_loaded_request, + output => loaded_response, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnProviderUnloaded', + input => provider_unloaded_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnClientConnect', + input => client_connect_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnClientConnack', + input => client_connack_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnClientConnected', + input => client_connected_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnClientDisconnected', + input => client_disconnected_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnClientAuthenticate', + input => client_authenticate_request, + output => valued_response, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnClientAuthorize', + input => client_authorize_request, + output => valued_response, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnClientSubscribe', + input => client_subscribe_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnClientUnsubscribe', + input => client_unsubscribe_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnSessionCreated', + input => session_created_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnSessionSubscribed', + input => session_subscribed_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnSessionUnsubscribed', + input => session_unsubscribed_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnSessionResumed', + input => session_resumed_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnSessionDiscarded', + input => session_discarded_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnSessionTakeovered', + input => session_takeovered_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnSessionTerminated', + input => session_terminated_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnMessagePublish', + input => message_publish_request, + output => valued_response, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnMessageDelivered', + input => message_delivered_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnMessageDropped', + input => message_dropped_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}, + #{name => 'OnMessageAcked', + input => message_acked_request, output => empty_success, + input_stream => false, output_stream => false, + opts => []}]}; +get_service_def(_) -> error. + + +get_rpc_names('emqx.exhook.v1.HookProvider') -> + ['OnProviderLoaded', + 'OnProviderUnloaded', + 'OnClientConnect', + 'OnClientConnack', + 'OnClientConnected', + 'OnClientDisconnected', + 'OnClientAuthenticate', + 'OnClientAuthorize', + 'OnClientSubscribe', + 'OnClientUnsubscribe', + 'OnSessionCreated', + 'OnSessionSubscribed', + 'OnSessionUnsubscribed', + 'OnSessionResumed', + 'OnSessionDiscarded', + 'OnSessionTakeovered', + 'OnSessionTerminated', + 'OnMessagePublish', + 'OnMessageDelivered', + 'OnMessageDropped', + 'OnMessageAcked']; +get_rpc_names(_) -> error. + + +find_rpc_def('emqx.exhook.v1.HookProvider', RpcName) -> + 'find_rpc_def_emqx.exhook.v1.HookProvider'(RpcName); +find_rpc_def(_, _) -> error. + + +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnProviderLoaded') -> + #{name => 'OnProviderLoaded', + input => provider_loaded_request, + output => loaded_response, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnProviderUnloaded') -> + #{name => 'OnProviderUnloaded', + input => provider_unloaded_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnClientConnect') -> + #{name => 'OnClientConnect', + input => client_connect_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnClientConnack') -> + #{name => 'OnClientConnack', + input => client_connack_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnClientConnected') -> + #{name => 'OnClientConnected', + input => client_connected_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnClientDisconnected') -> + #{name => 'OnClientDisconnected', + input => client_disconnected_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnClientAuthenticate') -> + #{name => 'OnClientAuthenticate', + input => client_authenticate_request, + output => valued_response, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnClientAuthorize') -> + #{name => 'OnClientAuthorize', + input => client_authorize_request, + output => valued_response, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnClientSubscribe') -> + #{name => 'OnClientSubscribe', + input => client_subscribe_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnClientUnsubscribe') -> + #{name => 'OnClientUnsubscribe', + input => client_unsubscribe_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnSessionCreated') -> + #{name => 'OnSessionCreated', + input => session_created_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnSessionSubscribed') -> + #{name => 'OnSessionSubscribed', + input => session_subscribed_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnSessionUnsubscribed') -> + #{name => 'OnSessionUnsubscribed', + input => session_unsubscribed_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnSessionResumed') -> + #{name => 'OnSessionResumed', + input => session_resumed_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnSessionDiscarded') -> + #{name => 'OnSessionDiscarded', + input => session_discarded_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnSessionTakeovered') -> + #{name => 'OnSessionTakeovered', + input => session_takeovered_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnSessionTerminated') -> + #{name => 'OnSessionTerminated', + input => session_terminated_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnMessagePublish') -> + #{name => 'OnMessagePublish', + input => message_publish_request, + output => valued_response, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnMessageDelivered') -> + #{name => 'OnMessageDelivered', + input => message_delivered_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnMessageDropped') -> + #{name => 'OnMessageDropped', + input => message_dropped_request, + output => empty_success, input_stream => false, + output_stream => false, opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'('OnMessageAcked') -> + #{name => 'OnMessageAcked', + input => message_acked_request, output => empty_success, + input_stream => false, output_stream => false, + opts => []}; +'find_rpc_def_emqx.exhook.v1.HookProvider'(_) -> error. + + +fetch_rpc_def(ServiceName, RpcName) -> + case find_rpc_def(ServiceName, RpcName) of + Def when is_map(Def) -> Def; + error -> + erlang:error({no_such_rpc, ServiceName, RpcName}) + end. + + +%% Convert a a fully qualified (ie with package name) service name +%% as a binary to a service name as an atom. +fqbin_to_service_name(<<"emqx.exhook.v1.HookProvider">>) -> + 'emqx.exhook.v1.HookProvider'; +fqbin_to_service_name(X) -> + error({gpb_error, {badservice, X}}). + + +%% Convert a service name as an atom to a fully qualified +%% (ie with package name) name as a binary. +service_name_to_fqbin('emqx.exhook.v1.HookProvider') -> + <<"emqx.exhook.v1.HookProvider">>; +service_name_to_fqbin(X) -> + error({gpb_error, {badservice, X}}). + + +%% Convert a a fully qualified (ie with package name) service name +%% and an rpc name, both as binaries to a service name and an rpc +%% name, as atoms. +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnProviderLoaded">>) -> + {'emqx.exhook.v1.HookProvider', 'OnProviderLoaded'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnProviderUnloaded">>) -> + {'emqx.exhook.v1.HookProvider', 'OnProviderUnloaded'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnClientConnect">>) -> + {'emqx.exhook.v1.HookProvider', 'OnClientConnect'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnClientConnack">>) -> + {'emqx.exhook.v1.HookProvider', 'OnClientConnack'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnClientConnected">>) -> + {'emqx.exhook.v1.HookProvider', 'OnClientConnected'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnClientDisconnected">>) -> + {'emqx.exhook.v1.HookProvider', 'OnClientDisconnected'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnClientAuthenticate">>) -> + {'emqx.exhook.v1.HookProvider', 'OnClientAuthenticate'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnClientAuthorize">>) -> + {'emqx.exhook.v1.HookProvider', 'OnClientAuthorize'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnClientSubscribe">>) -> + {'emqx.exhook.v1.HookProvider', 'OnClientSubscribe'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnClientUnsubscribe">>) -> + {'emqx.exhook.v1.HookProvider', 'OnClientUnsubscribe'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionCreated">>) -> + {'emqx.exhook.v1.HookProvider', 'OnSessionCreated'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionSubscribed">>) -> + {'emqx.exhook.v1.HookProvider', 'OnSessionSubscribed'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionUnsubscribed">>) -> + {'emqx.exhook.v1.HookProvider', + 'OnSessionUnsubscribed'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionResumed">>) -> + {'emqx.exhook.v1.HookProvider', 'OnSessionResumed'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionDiscarded">>) -> + {'emqx.exhook.v1.HookProvider', 'OnSessionDiscarded'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionTakeovered">>) -> + {'emqx.exhook.v1.HookProvider', 'OnSessionTakeovered'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionTerminated">>) -> + {'emqx.exhook.v1.HookProvider', 'OnSessionTerminated'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnMessagePublish">>) -> + {'emqx.exhook.v1.HookProvider', 'OnMessagePublish'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnMessageDelivered">>) -> + {'emqx.exhook.v1.HookProvider', 'OnMessageDelivered'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnMessageDropped">>) -> + {'emqx.exhook.v1.HookProvider', 'OnMessageDropped'}; +fqbins_to_service_and_rpc_name(<<"emqx.exhook.v1.HookProvider">>, <<"OnMessageAcked">>) -> + {'emqx.exhook.v1.HookProvider', 'OnMessageAcked'}; +fqbins_to_service_and_rpc_name(S, R) -> + error({gpb_error, {badservice_or_rpc, {S, R}}}). + + +%% Convert a service name and an rpc name, both as atoms, +%% to a fully qualified (ie with package name) service name and +%% an rpc name as binaries. +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnProviderLoaded') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnProviderLoaded">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnProviderUnloaded') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnProviderUnloaded">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnClientConnect') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnClientConnect">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnClientConnack') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnClientConnack">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnClientConnected') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnClientConnected">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnClientDisconnected') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnClientDisconnected">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnClientAuthenticate') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnClientAuthenticate">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnClientAuthorize') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnClientAuthorize">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnClientSubscribe') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnClientSubscribe">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnClientUnsubscribe') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnClientUnsubscribe">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnSessionCreated') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionCreated">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnSessionSubscribed') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionSubscribed">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnSessionUnsubscribed') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionUnsubscribed">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnSessionResumed') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionResumed">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnSessionDiscarded') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionDiscarded">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnSessionTakeovered') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionTakeovered">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnSessionTerminated') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnSessionTerminated">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnMessagePublish') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnMessagePublish">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnMessageDelivered') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnMessageDelivered">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnMessageDropped') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnMessageDropped">>}; +service_and_rpc_name_to_fqbins('emqx.exhook.v1.HookProvider', + 'OnMessageAcked') -> + {<<"emqx.exhook.v1.HookProvider">>, <<"OnMessageAcked">>}; +service_and_rpc_name_to_fqbins(S, R) -> + error({gpb_error, {badservice_or_rpc, {S, R}}}). + + +fqbin_to_msg_name(<<"emqx.exhook.v1.ProviderLoadedRequest">>) -> provider_loaded_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.LoadedResponse">>) -> loaded_response; +fqbin_to_msg_name(<<"emqx.exhook.v1.ProviderUnloadedRequest">>) -> provider_unloaded_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientConnectRequest">>) -> client_connect_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientConnackRequest">>) -> client_connack_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientConnectedRequest">>) -> client_connected_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientDisconnectedRequest">>) -> + client_disconnected_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientAuthenticateRequest">>) -> + client_authenticate_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientAuthorizeRequest">>) -> client_authorize_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientSubscribeRequest">>) -> client_subscribe_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientUnsubscribeRequest">>) -> + client_unsubscribe_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.SessionCreatedRequest">>) -> session_created_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.SessionSubscribedRequest">>) -> + session_subscribed_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.SessionUnsubscribedRequest">>) -> + session_unsubscribed_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.SessionResumedRequest">>) -> session_resumed_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.SessionDiscardedRequest">>) -> session_discarded_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.SessionTakeoveredRequest">>) -> + session_takeovered_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.SessionTerminatedRequest">>) -> + session_terminated_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.MessagePublishRequest">>) -> message_publish_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.MessageDeliveredRequest">>) -> message_delivered_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.MessageDroppedRequest">>) -> message_dropped_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.MessageAckedRequest">>) -> message_acked_request; +fqbin_to_msg_name(<<"emqx.exhook.v1.EmptySuccess">>) -> empty_success; +fqbin_to_msg_name(<<"emqx.exhook.v1.ValuedResponse">>) -> valued_response; +fqbin_to_msg_name(<<"emqx.exhook.v1.BrokerInfo">>) -> broker_info; +fqbin_to_msg_name(<<"emqx.exhook.v1.HookSpec">>) -> hook_spec; +fqbin_to_msg_name(<<"emqx.exhook.v1.ConnInfo">>) -> conn_info; +fqbin_to_msg_name(<<"emqx.exhook.v1.ClientInfo">>) -> client_info; +fqbin_to_msg_name(<<"emqx.exhook.v1.Message">>) -> message; +fqbin_to_msg_name(<<"emqx.exhook.v1.Property">>) -> property; +fqbin_to_msg_name(<<"emqx.exhook.v1.TopicFilter">>) -> topic_filter; +fqbin_to_msg_name(<<"emqx.exhook.v1.SubOpts">>) -> sub_opts; +fqbin_to_msg_name(E) -> error({gpb_error, {badmsg, E}}). + + +msg_name_to_fqbin(provider_loaded_request) -> <<"emqx.exhook.v1.ProviderLoadedRequest">>; +msg_name_to_fqbin(loaded_response) -> <<"emqx.exhook.v1.LoadedResponse">>; +msg_name_to_fqbin(provider_unloaded_request) -> <<"emqx.exhook.v1.ProviderUnloadedRequest">>; +msg_name_to_fqbin(client_connect_request) -> <<"emqx.exhook.v1.ClientConnectRequest">>; +msg_name_to_fqbin(client_connack_request) -> <<"emqx.exhook.v1.ClientConnackRequest">>; +msg_name_to_fqbin(client_connected_request) -> <<"emqx.exhook.v1.ClientConnectedRequest">>; +msg_name_to_fqbin(client_disconnected_request) -> + <<"emqx.exhook.v1.ClientDisconnectedRequest">>; +msg_name_to_fqbin(client_authenticate_request) -> + <<"emqx.exhook.v1.ClientAuthenticateRequest">>; +msg_name_to_fqbin(client_authorize_request) -> <<"emqx.exhook.v1.ClientAuthorizeRequest">>; +msg_name_to_fqbin(client_subscribe_request) -> <<"emqx.exhook.v1.ClientSubscribeRequest">>; +msg_name_to_fqbin(client_unsubscribe_request) -> + <<"emqx.exhook.v1.ClientUnsubscribeRequest">>; +msg_name_to_fqbin(session_created_request) -> <<"emqx.exhook.v1.SessionCreatedRequest">>; +msg_name_to_fqbin(session_subscribed_request) -> + <<"emqx.exhook.v1.SessionSubscribedRequest">>; +msg_name_to_fqbin(session_unsubscribed_request) -> + <<"emqx.exhook.v1.SessionUnsubscribedRequest">>; +msg_name_to_fqbin(session_resumed_request) -> <<"emqx.exhook.v1.SessionResumedRequest">>; +msg_name_to_fqbin(session_discarded_request) -> <<"emqx.exhook.v1.SessionDiscardedRequest">>; +msg_name_to_fqbin(session_takeovered_request) -> + <<"emqx.exhook.v1.SessionTakeoveredRequest">>; +msg_name_to_fqbin(session_terminated_request) -> + <<"emqx.exhook.v1.SessionTerminatedRequest">>; +msg_name_to_fqbin(message_publish_request) -> <<"emqx.exhook.v1.MessagePublishRequest">>; +msg_name_to_fqbin(message_delivered_request) -> <<"emqx.exhook.v1.MessageDeliveredRequest">>; +msg_name_to_fqbin(message_dropped_request) -> <<"emqx.exhook.v1.MessageDroppedRequest">>; +msg_name_to_fqbin(message_acked_request) -> <<"emqx.exhook.v1.MessageAckedRequest">>; +msg_name_to_fqbin(empty_success) -> <<"emqx.exhook.v1.EmptySuccess">>; +msg_name_to_fqbin(valued_response) -> <<"emqx.exhook.v1.ValuedResponse">>; +msg_name_to_fqbin(broker_info) -> <<"emqx.exhook.v1.BrokerInfo">>; +msg_name_to_fqbin(hook_spec) -> <<"emqx.exhook.v1.HookSpec">>; +msg_name_to_fqbin(conn_info) -> <<"emqx.exhook.v1.ConnInfo">>; +msg_name_to_fqbin(client_info) -> <<"emqx.exhook.v1.ClientInfo">>; +msg_name_to_fqbin(message) -> <<"emqx.exhook.v1.Message">>; +msg_name_to_fqbin(property) -> <<"emqx.exhook.v1.Property">>; +msg_name_to_fqbin(topic_filter) -> <<"emqx.exhook.v1.TopicFilter">>; +msg_name_to_fqbin(sub_opts) -> <<"emqx.exhook.v1.SubOpts">>; +msg_name_to_fqbin(E) -> error({gpb_error, {badmsg, E}}). + + +fqbin_to_enum_name(<<"emqx.exhook.v1.ClientAuthorizeRequest.AuthorizeReqType">>) -> + 'client_authorize_request.AuthorizeReqType'; +fqbin_to_enum_name(<<"emqx.exhook.v1.ValuedResponse.ResponsedType">>) -> + 'valued_response.ResponsedType'; +fqbin_to_enum_name(E) -> + error({gpb_error, {badenum, E}}). + + +enum_name_to_fqbin('client_authorize_request.AuthorizeReqType') -> + <<"emqx.exhook.v1.ClientAuthorizeRequest.AuthorizeReqType">>; +enum_name_to_fqbin('valued_response.ResponsedType') -> + <<"emqx.exhook.v1.ValuedResponse.ResponsedType">>; +enum_name_to_fqbin(E) -> + error({gpb_error, {badenum, E}}). + + +get_package_name() -> 'emqx.exhook.v1'. + + +%% Whether or not the message names +%% are prepended with package name or not. +uses_packages() -> true. + + +source_basename() -> "exhook.proto". + + +%% Retrieve all proto file names, also imported ones. +%% The order is top-down. The first element is always the main +%% source file. The files are returned with extension, +%% see get_all_proto_names/0 for a version that returns +%% the basenames sans extension +get_all_source_basenames() -> ["exhook.proto"]. + + +%% Retrieve all proto file names, also imported ones. +%% The order is top-down. The first element is always the main +%% source file. The files are returned sans .proto extension, +%% to make it easier to use them with the various get_xyz_containment +%% functions. +get_all_proto_names() -> ["exhook"]. + + +get_msg_containment("exhook") -> + [broker_info, + client_authenticate_request, + client_authorize_request, + client_connack_request, + client_connect_request, + client_connected_request, + client_disconnected_request, + client_info, + client_subscribe_request, + client_unsubscribe_request, + conn_info, + empty_success, + hook_spec, + loaded_response, + message, + message_acked_request, + message_delivered_request, + message_dropped_request, + message_publish_request, + property, + provider_loaded_request, + provider_unloaded_request, + session_created_request, + session_discarded_request, + session_resumed_request, + session_subscribed_request, + session_takeovered_request, + session_terminated_request, + session_unsubscribed_request, + sub_opts, + topic_filter, + valued_response]; +get_msg_containment(P) -> + error({gpb_error, {badproto, P}}). + + +get_pkg_containment("exhook") -> 'emqx.exhook.v1'; +get_pkg_containment(P) -> + error({gpb_error, {badproto, P}}). + + +get_service_containment("exhook") -> + ['emqx.exhook.v1.HookProvider']; +get_service_containment(P) -> + error({gpb_error, {badproto, P}}). + + +get_rpc_containment("exhook") -> + [{'emqx.exhook.v1.HookProvider', 'OnProviderLoaded'}, + {'emqx.exhook.v1.HookProvider', 'OnProviderUnloaded'}, + {'emqx.exhook.v1.HookProvider', 'OnClientConnect'}, + {'emqx.exhook.v1.HookProvider', 'OnClientConnack'}, + {'emqx.exhook.v1.HookProvider', 'OnClientConnected'}, + {'emqx.exhook.v1.HookProvider', 'OnClientDisconnected'}, + {'emqx.exhook.v1.HookProvider', 'OnClientAuthenticate'}, + {'emqx.exhook.v1.HookProvider', 'OnClientAuthorize'}, + {'emqx.exhook.v1.HookProvider', 'OnClientSubscribe'}, + {'emqx.exhook.v1.HookProvider', 'OnClientUnsubscribe'}, + {'emqx.exhook.v1.HookProvider', 'OnSessionCreated'}, + {'emqx.exhook.v1.HookProvider', 'OnSessionSubscribed'}, + {'emqx.exhook.v1.HookProvider', + 'OnSessionUnsubscribed'}, + {'emqx.exhook.v1.HookProvider', 'OnSessionResumed'}, + {'emqx.exhook.v1.HookProvider', 'OnSessionDiscarded'}, + {'emqx.exhook.v1.HookProvider', 'OnSessionTakeovered'}, + {'emqx.exhook.v1.HookProvider', 'OnSessionTerminated'}, + {'emqx.exhook.v1.HookProvider', 'OnMessagePublish'}, + {'emqx.exhook.v1.HookProvider', 'OnMessageDelivered'}, + {'emqx.exhook.v1.HookProvider', 'OnMessageDropped'}, + {'emqx.exhook.v1.HookProvider', 'OnMessageAcked'}]; +get_rpc_containment(P) -> + error({gpb_error, {badproto, P}}). + + +get_enum_containment("exhook") -> + ['client_authorize_request.AuthorizeReqType', + 'valued_response.ResponsedType']; +get_enum_containment(P) -> + error({gpb_error, {badproto, P}}). + + +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.TopicFilter">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.SubOpts">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.HookSpec">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.EmptySuccess">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.SessionUnsubscribedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.SessionTerminatedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.SessionTakeoveredRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.SessionSubscribedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.SessionResumedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.SessionDiscardedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.SessionCreatedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ProviderUnloadedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ProviderLoadedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.MessagePublishRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.MessageDroppedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.MessageDeliveredRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.MessageAckedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientUnsubscribeRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientSubscribeRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientDisconnectedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientConnectedRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientConnectRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientConnackRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientAuthorizeRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientAuthenticateRequest">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ValuedResponse">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.Message">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.LoadedResponse">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.Property">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ConnInfo">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.ClientInfo">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(<<"emqx.exhook.v1.BrokerInfo">>) -> "exhook"; +get_proto_by_msg_name_as_fqbin(E) -> + error({gpb_error, {badmsg, E}}). + + +get_proto_by_service_name_as_fqbin(<<"emqx.exhook.v1.HookProvider">>) -> "exhook"; +get_proto_by_service_name_as_fqbin(E) -> + error({gpb_error, {badservice, E}}). + + +get_proto_by_enum_name_as_fqbin(<<"emqx.exhook.v1.ValuedResponse.ResponsedType">>) -> "exhook"; +get_proto_by_enum_name_as_fqbin(<<"emqx.exhook.v1.ClientAuthorizeRequest.AuthorizeReqType">>) -> "exhook"; +get_proto_by_enum_name_as_fqbin(E) -> + error({gpb_error, {badenum, E}}). + + +get_protos_by_pkg_name_as_fqbin(<<"emqx.exhook.v1">>) -> ["exhook"]; +get_protos_by_pkg_name_as_fqbin(E) -> + error({gpb_error, {badpkg, E}}). + + + +gpb_version_as_string() -> + "4.11.2". + +gpb_version_as_list() -> + [4,11,2]. diff --git a/apps/emqx_exhook/src/emqx_exhook_schema.erl b/apps/emqx_exhook/src/emqx_exhook_schema.erl new file mode 100644 index 000000000..64d39eb52 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_schema.erl @@ -0,0 +1,88 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook_schema). + +-dialyzer(no_return). +-dialyzer(no_match). +-dialyzer(no_contracts). +-dialyzer(no_unused). +-dialyzer(no_fail_call). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-type duration() :: integer(). + +-typerefl_from_string({duration/0, emqx_schema, to_duration}). + +-reflect_type([duration/0]). + +-export([namespace/0, roots/0, fields/1]). + +namespace() -> exhook. + +roots() -> [exhook]. + +fields(exhook) -> + [ {request_failed_action, + sc(union([deny, ignore]), + #{default => deny})} + , {request_timeout, + sc(duration(), + #{default => "5s"})} + , {auto_reconnect, + sc(union([false, duration()]), + #{ default => "60s" + })} + , {servers, + sc(hoconsc:array(ref(servers)), + #{default => []})} + ]; + +fields(servers) -> + [ {name, + sc(string(), + #{})} + , {url, + sc(string(), + #{})} + , {ssl, + sc(ref(ssl_conf), + #{})} + ]; + +fields(ssl_conf) -> + [ {cacertfile, + sc(string(), + #{}) + } + , {certfile, + sc(string(), + #{}) + } + , {keyfile, + sc(string(), + #{})} + ]. + +%% types + +sc(Type, Meta) -> Meta#{type => Type}. + +ref(Field) -> + hoconsc:ref(?MODULE, Field). diff --git a/apps/emqx_exhook/src/emqx_exhook_server.erl b/apps/emqx_exhook/src/emqx_exhook_server.erl new file mode 100644 index 000000000..924e5d7ba --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_server.erl @@ -0,0 +1,326 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook_server). + +-include("emqx_exhook.hrl"). +-include_lib("emqx/include/logger.hrl"). + + +-define(CNTER, emqx_exhook_counter). +-define(PB_CLIENT_MOD, emqx_exhook_v_1_hook_provider_client). + +%% Load/Unload +-export([ load/3 + , unload/1 + ]). + +%% APIs +-export([call/3]). + +%% Infos +-export([ name/1 + , format/1 + ]). + +-record(server, { + %% Server name (equal to grpc client channel name) + name :: binary(), + %% The function options + options :: map(), + %% gRPC channel pid + channel :: pid(), + %% Registered hook names and options + hookspec :: #{hookpoint() => map()}, + %% Metrcis name prefix + prefix :: list() + }). + +-type server() :: #server{}. + +-type hookpoint() :: 'client.connect' + | 'client.connack' + | 'client.connected' + | 'client.disconnected' + | 'client.authenticate' + | 'client.authorize' + | 'client.subscribe' + | 'client.unsubscribe' + | 'session.created' + | 'session.subscribed' + | 'session.unsubscribed' + | 'session.resumed' + | 'session.discarded' + | 'session.takeovered' + | 'session.terminated' + | 'message.publish' + | 'message.delivered' + | 'message.acked' + | 'message.dropped'. + +-export_type([server/0]). + +-dialyzer({nowarn_function, [inc_metrics/2]}). + +%%-------------------------------------------------------------------- +%% Load/Unload APIs +%%-------------------------------------------------------------------- + +-spec load(binary(), map(), map()) -> {ok, server()} | {error, term()} . +load(Name, Opts0, ReqOpts) -> + {SvrAddr, ClientOpts} = channel_opts(Opts0), + case emqx_exhook_sup:start_grpc_client_channel( + Name, + SvrAddr, + ClientOpts) of + {ok, _ChannPoolPid} -> + case do_init(Name, ReqOpts) of + {ok, HookSpecs} -> + %% Reigster metrics + Prefix = lists:flatten( + io_lib:format("exhook.~s.", [Name])), + ensure_metrics(Prefix, HookSpecs), + %% Ensure hooks + ensure_hooks(HookSpecs), + {ok, #server{name = Name, + options = ReqOpts, + channel = _ChannPoolPid, + hookspec = HookSpecs, + prefix = Prefix }}; + {error, _} = E -> + emqx_exhook_sup:stop_grpc_client_channel(Name), E + end; + {error, _} = E -> E + end. + +%% @private +channel_opts(Opts = #{url := URL}) -> + case uri_string:parse(URL) of + #{scheme := "http", host := Host, port := Port} -> + {format_http_uri("http", Host, Port), #{}}; + #{scheme := "https", host := Host, port := Port} -> + SslOpts = + case maps:get(ssl, Opts, undefined) of + undefined -> []; + MapOpts -> + filter( + [{cacertfile, maps:get(cacertfile, MapOpts, undefined)}, + {certfile, maps:get(certfile, MapOpts, undefined)}, + {keyfile, maps:get(keyfile, MapOpts, undefined)} + ]) + end, + {format_http_uri("https", Host, Port), + #{gun_opts => #{transport => ssl, transport_opts => SslOpts}}}; + _ -> + error(bad_server_url) + end. + +format_http_uri(Scheme, Host, Port) -> + lists:flatten(io_lib:format("~s://~s:~w", [Scheme, Host, Port])). + +filter(Ls) -> + [ E || E <- Ls, E /= undefined]. + +-spec unload(server()) -> ok. +unload(#server{name = Name, options = ReqOpts, hookspec = HookSpecs}) -> + _ = do_deinit(Name, ReqOpts), + _ = may_unload_hooks(HookSpecs), + _ = emqx_exhook_sup:stop_grpc_client_channel(Name), + ok. + +do_deinit(Name, ReqOpts) -> + _ = do_call(Name, 'on_provider_unloaded', #{}, ReqOpts), + ok. + +do_init(ChannName, ReqOpts) -> + %% BrokerInfo defined at: exhook.protos + BrokerInfo = maps:with([version, sysdescr, uptime, datetime], + maps:from_list(emqx_sys:info())), + Req = #{broker => BrokerInfo}, + case do_call(ChannName, 'on_provider_loaded', Req, ReqOpts) of + {ok, InitialResp} -> + try + {ok, resovle_hookspec(maps:get(hooks, InitialResp, []))} + catch _:Reason:Stk -> + ?LOG(error, "try to init ~p failed, reason: ~p, stacktrace: ~0p", + [ChannName, Reason, Stk]), + {error, Reason} + end; + {error, Reason} -> + {error, Reason} + end. + +%% @private +resovle_hookspec(HookSpecs) when is_list(HookSpecs) -> + MessageHooks = message_hooks(), + AvailableHooks = available_hooks(), + lists:foldr(fun(HookSpec, Acc) -> + case maps:get(name, HookSpec, undefined) of + undefined -> Acc; + Name0 -> + Name = try binary_to_existing_atom(Name0, utf8) catch T:R:_ -> {T,R} end, + case lists:member(Name, AvailableHooks) of + true -> + case lists:member(Name, MessageHooks) of + true -> + Acc#{Name => #{topics => maps:get(topics, HookSpec, [])}}; + _ -> + Acc#{Name => #{}} + end; + _ -> error({unknown_hookpoint, Name}) + end + end + end, #{}, HookSpecs). + +ensure_metrics(Prefix, HookSpecs) -> + Keys = [list_to_atom(Prefix ++ atom_to_list(Hookpoint)) + || Hookpoint <- maps:keys(HookSpecs)], + lists:foreach(fun emqx_metrics:ensure/1, Keys). + +ensure_hooks(HookSpecs) -> + lists:foreach(fun(Hookpoint) -> + case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of + false -> + ?LOG(error, "Unknown name ~s to hook, skip it!", [Hookpoint]); + {Hookpoint, {M, F, A}} -> + emqx_hooks:put(Hookpoint, {M, F, A}), + ets:update_counter(?CNTER, Hookpoint, {2, 1}, {Hookpoint, 0}) + end + end, maps:keys(HookSpecs)). + +may_unload_hooks(HookSpecs) -> + lists:foreach(fun(Hookpoint) -> + case ets:update_counter(?CNTER, Hookpoint, {2, -1}, {Hookpoint, 0}) of + Cnt when Cnt =< 0 -> + case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of + {Hookpoint, {M, F, _A}} -> + emqx_hooks:del(Hookpoint, {M, F}); + _ -> ok + end, + ets:delete(?CNTER, Hookpoint); + _ -> ok + end + end, maps:keys(HookSpecs)). + +format(#server{name = Name, hookspec = Hooks}) -> + lists:flatten( + io_lib:format("name=~s, hooks=~0p, active=true", [Name, Hooks])). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +name(#server{name = Name}) -> + Name. + +-spec call(hookpoint(), map(), server()) + -> ignore + | {ok, Resp :: term()} + | {error, term()}. +call(Hookpoint, Req, #server{name = ChannName, options = ReqOpts, + hookspec = Hooks, prefix = Prefix}) -> + GrpcFunc = hk2func(Hookpoint), + case maps:get(Hookpoint, Hooks, undefined) of + undefined -> ignore; + Opts -> + NeedCall = case lists:member(Hookpoint, message_hooks()) of + false -> true; + _ -> + #{message := #{topic := Topic}} = Req, + match_topic_filter(Topic, maps:get(topics, Opts, [])) + end, + case NeedCall of + false -> ignore; + _ -> + inc_metrics(Prefix, Hookpoint), + do_call(ChannName, GrpcFunc, Req, ReqOpts) + end + end. + +%% @private +inc_metrics(IncFun, Name) when is_function(IncFun) -> + %% BACKW: e4.2.0-e4.2.2 + {env, [Prefix|_]} = erlang:fun_info(IncFun, env), + inc_metrics(Prefix, Name); +inc_metrics(Prefix, Name) when is_list(Prefix) -> + emqx_metrics:inc(list_to_atom(Prefix ++ atom_to_list(Name))). + +-compile({inline, [match_topic_filter/2]}). +match_topic_filter(_, []) -> + true; +match_topic_filter(TopicName, TopicFilter) -> + lists:any(fun(F) -> emqx_topic:match(TopicName, F) end, TopicFilter). + +-spec do_call(binary(), atom(), map(), map()) -> {ok, map()} | {error, term()}. +do_call(ChannName, Fun, Req, ReqOpts) -> + Options = ReqOpts#{channel => ChannName}, + ?LOG(debug, "Call ~0p:~0p(~0p, ~0p)", [?PB_CLIENT_MOD, Fun, Req, Options]), + case catch apply(?PB_CLIENT_MOD, Fun, [Req, Options]) of + {ok, Resp, _Metadata} -> + ?LOG(debug, "Response {ok, ~0p, ~0p}", [Resp, _Metadata]), + {ok, Resp}; + {error, {Code, Msg}, _Metadata} -> + ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) response errcode: ~0p, errmsg: ~0p", + [?PB_CLIENT_MOD, Fun, Req, Options, Code, Msg]), + {error, {Code, Msg}}; + {error, Reason} -> + ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) error: ~0p", + [?PB_CLIENT_MOD, Fun, Req, Options, Reason]), + {error, Reason}; + {'EXIT', {Reason, Stk}} -> + ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) throw an exception: ~0p, stacktrace: ~0p", + [?PB_CLIENT_MOD, Fun, Req, Options, Reason, Stk]), + {error, Reason} + end. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +-compile({inline, [hk2func/1]}). +hk2func('client.connect') -> 'on_client_connect'; +hk2func('client.connack') -> 'on_client_connack'; +hk2func('client.connected') -> 'on_client_connected'; +hk2func('client.disconnected') -> 'on_client_disconnected'; +hk2func('client.authenticate') -> 'on_client_authenticate'; +hk2func('client.authorize') -> 'on_client_authorize'; +hk2func('client.subscribe') -> 'on_client_subscribe'; +hk2func('client.unsubscribe') -> 'on_client_unsubscribe'; +hk2func('session.created') -> 'on_session_created'; +hk2func('session.subscribed') -> 'on_session_subscribed'; +hk2func('session.unsubscribed') -> 'on_session_unsubscribed'; +hk2func('session.resumed') -> 'on_session_resumed'; +hk2func('session.discarded') -> 'on_session_discarded'; +hk2func('session.takeovered') -> 'on_session_takeovered'; +hk2func('session.terminated') -> 'on_session_terminated'; +hk2func('message.publish') -> 'on_message_publish'; +hk2func('message.delivered') ->'on_message_delivered'; +hk2func('message.acked') -> 'on_message_acked'; +hk2func('message.dropped') ->'on_message_dropped'. + +-compile({inline, [message_hooks/0]}). +message_hooks() -> + ['message.publish', 'message.delivered', + 'message.acked', 'message.dropped']. + +-compile({inline, [available_hooks/0]}). +available_hooks() -> + ['client.connect', 'client.connack', 'client.connected', + 'client.disconnected', 'client.authenticate', 'client.authorize', + 'client.subscribe', 'client.unsubscribe', + 'session.created', 'session.subscribed', 'session.unsubscribed', + 'session.resumed', 'session.discarded', 'session.takeovered', + 'session.terminated' | message_hooks()]. diff --git a/apps/emqx_exhook/src/emqx_exhook_sup.erl b/apps/emqx_exhook/src/emqx_exhook_sup.erl new file mode 100644 index 000000000..60a6a2915 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_sup.erl @@ -0,0 +1,83 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook_sup). + +-behaviour(supervisor). + +-export([ start_link/0 + , init/1 + ]). + +-export([ start_grpc_client_channel/3 + , stop_grpc_client_channel/1 + ]). + +-define(CHILD(Mod, Type, Args), + #{ id => Mod + , start => {Mod, start_link, Args} + , type => Type + , shutdown => 15000 + } + ). + +%%-------------------------------------------------------------------- +%% Supervisor APIs & Callbacks +%%-------------------------------------------------------------------- + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + Mngr = ?CHILD(emqx_exhook_mngr, worker, + [servers(), auto_reconnect(), request_options()]), + {ok, {{one_for_one, 10, 100}, [Mngr]}}. + +servers() -> + env(servers, []). + +auto_reconnect() -> + env(auto_reconnect, 60000). + +request_options() -> + #{timeout => env(request_timeout, 5000), + request_failed_action => env(request_failed_action, deny) + }. + +env(Key, Def) -> + emqx:get_config([exhook, Key], Def). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +-spec start_grpc_client_channel( + binary(), + uri_string:uri_string(), + grpc_client:options()) -> {ok, pid()} | {error, term()}. +start_grpc_client_channel(Name, SvrAddr, Options) -> + grpc_client_sup:create_channel_pool(Name, SvrAddr, Options). + +-spec stop_grpc_client_channel(binary()) -> ok. +stop_grpc_client_channel(Name) -> + %% Avoid crash due to hot-upgrade had unloaded + %% grpc application + try + grpc_client_sup:stop_channel_pool(Name) + catch + _:_:_ -> + ok + end. diff --git a/apps/emqx_exhook/src/emqx_exhook_v_1_hook_provider_bhvr.erl b/apps/emqx_exhook/src/emqx_exhook_v_1_hook_provider_bhvr.erl new file mode 100644 index 000000000..7ea1377ec --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_v_1_hook_provider_bhvr.erl @@ -0,0 +1,93 @@ +%%%------------------------------------------------------------------- +%% @doc Behaviour to implement for grpc service emqx.exhook.v1.HookProvider. +%% @end +%%%------------------------------------------------------------------- + +%% this module was generated and should not be modified manually + +-module(emqx_exhook_v_1_hook_provider_bhvr). + +-callback on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_session_created(emqx_exhook_pb:session_created_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_session_takeovered(emqx_exhook_pb:session_takeovered_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + +-callback on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_stream:error_response()}. + diff --git a/apps/emqx_exhook/src/emqx_exhook_v_1_hook_provider_client.erl b/apps/emqx_exhook/src/emqx_exhook_v_1_hook_provider_client.erl new file mode 100644 index 000000000..b1d9421a3 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_v_1_hook_provider_client.erl @@ -0,0 +1,445 @@ +%%%------------------------------------------------------------------- +%% @doc Client module for grpc service emqx.exhook.v1.HookProvider. +%% @end +%%%------------------------------------------------------------------- + +%% this module was generated and should not be modified manually + +-module(emqx_exhook_v_1_hook_provider_client). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("grpc/include/grpc.hrl"). + +-define(SERVICE, 'emqx.exhook.v1.HookProvider'). +-define(PROTO_MODULE, 'emqx_exhook_pb'). +-define(MARSHAL(T), fun(I) -> ?PROTO_MODULE:encode_msg(I, T) end). +-define(UNMARSHAL(T), fun(I) -> ?PROTO_MODULE:decode_msg(I, T) end). +-define(DEF(Path, Req, Resp, MessageType), + #{path => Path, + service =>?SERVICE, + message_type => MessageType, + marshal => ?MARSHAL(Req), + unmarshal => ?UNMARSHAL(Resp)}). + +-spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request()) + -> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()} + | {error, term()}. +on_provider_loaded(Req) -> + on_provider_loaded(Req, #{}, #{}). + +-spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:options()) + -> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()} + | {error, term()}. +on_provider_loaded(Req, Options) -> + on_provider_loaded(Req, #{}, Options). + +-spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()} + | {error, term()}. +on_provider_loaded(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnProviderLoaded">>, + provider_loaded_request, loaded_response, <<"emqx.exhook.v1.ProviderLoadedRequest">>), + Req, Metadata, Options). + +-spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_provider_unloaded(Req) -> + on_provider_unloaded(Req, #{}, #{}). + +-spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_provider_unloaded(Req, Options) -> + on_provider_unloaded(Req, #{}, Options). + +-spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_provider_unloaded(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnProviderUnloaded">>, + provider_unloaded_request, empty_success, <<"emqx.exhook.v1.ProviderUnloadedRequest">>), + Req, Metadata, Options). + +-spec on_client_connect(emqx_exhook_pb:client_connect_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connect(Req) -> + on_client_connect(Req, #{}, #{}). + +-spec on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connect(Req, Options) -> + on_client_connect(Req, #{}, Options). + +-spec on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connect(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnClientConnect">>, + client_connect_request, empty_success, <<"emqx.exhook.v1.ClientConnectRequest">>), + Req, Metadata, Options). + +-spec on_client_connack(emqx_exhook_pb:client_connack_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connack(Req) -> + on_client_connack(Req, #{}, #{}). + +-spec on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connack(Req, Options) -> + on_client_connack(Req, #{}, Options). + +-spec on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connack(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnClientConnack">>, + client_connack_request, empty_success, <<"emqx.exhook.v1.ClientConnackRequest">>), + Req, Metadata, Options). + +-spec on_client_connected(emqx_exhook_pb:client_connected_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connected(Req) -> + on_client_connected(Req, #{}, #{}). + +-spec on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connected(Req, Options) -> + on_client_connected(Req, #{}, Options). + +-spec on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_connected(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnClientConnected">>, + client_connected_request, empty_success, <<"emqx.exhook.v1.ClientConnectedRequest">>), + Req, Metadata, Options). + +-spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_disconnected(Req) -> + on_client_disconnected(Req, #{}, #{}). + +-spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_disconnected(Req, Options) -> + on_client_disconnected(Req, #{}, Options). + +-spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_disconnected(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnClientDisconnected">>, + client_disconnected_request, empty_success, <<"emqx.exhook.v1.ClientDisconnectedRequest">>), + Req, Metadata, Options). + +-spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_client_authenticate(Req) -> + on_client_authenticate(Req, #{}, #{}). + +-spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:options()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_client_authenticate(Req, Options) -> + on_client_authenticate(Req, #{}, Options). + +-spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_client_authenticate(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnClientAuthenticate">>, + client_authenticate_request, valued_response, <<"emqx.exhook.v1.ClientAuthenticateRequest">>), + Req, Metadata, Options). + +-spec on_client_authorize(emqx_exhook_pb:client_authorize_request()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_client_authorize(Req) -> + on_client_authorize(Req, #{}, #{}). + +-spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:options()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_client_authorize(Req, Options) -> + on_client_authorize(Req, #{}, Options). + +-spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_client_authorize(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnClientAuthorize">>, + client_authorize_request, valued_response, <<"emqx.exhook.v1.ClientAuthorizeRequest">>), + Req, Metadata, Options). + +-spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_subscribe(Req) -> + on_client_subscribe(Req, #{}, #{}). + +-spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_subscribe(Req, Options) -> + on_client_subscribe(Req, #{}, Options). + +-spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_subscribe(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnClientSubscribe">>, + client_subscribe_request, empty_success, <<"emqx.exhook.v1.ClientSubscribeRequest">>), + Req, Metadata, Options). + +-spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_unsubscribe(Req) -> + on_client_unsubscribe(Req, #{}, #{}). + +-spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_unsubscribe(Req, Options) -> + on_client_unsubscribe(Req, #{}, Options). + +-spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_client_unsubscribe(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnClientUnsubscribe">>, + client_unsubscribe_request, empty_success, <<"emqx.exhook.v1.ClientUnsubscribeRequest">>), + Req, Metadata, Options). + +-spec on_session_created(emqx_exhook_pb:session_created_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_created(Req) -> + on_session_created(Req, #{}, #{}). + +-spec on_session_created(emqx_exhook_pb:session_created_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_created(Req, Options) -> + on_session_created(Req, #{}, Options). + +-spec on_session_created(emqx_exhook_pb:session_created_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_created(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnSessionCreated">>, + session_created_request, empty_success, <<"emqx.exhook.v1.SessionCreatedRequest">>), + Req, Metadata, Options). + +-spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_subscribed(Req) -> + on_session_subscribed(Req, #{}, #{}). + +-spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_subscribed(Req, Options) -> + on_session_subscribed(Req, #{}, Options). + +-spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_subscribed(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnSessionSubscribed">>, + session_subscribed_request, empty_success, <<"emqx.exhook.v1.SessionSubscribedRequest">>), + Req, Metadata, Options). + +-spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_unsubscribed(Req) -> + on_session_unsubscribed(Req, #{}, #{}). + +-spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_unsubscribed(Req, Options) -> + on_session_unsubscribed(Req, #{}, Options). + +-spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_unsubscribed(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnSessionUnsubscribed">>, + session_unsubscribed_request, empty_success, <<"emqx.exhook.v1.SessionUnsubscribedRequest">>), + Req, Metadata, Options). + +-spec on_session_resumed(emqx_exhook_pb:session_resumed_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_resumed(Req) -> + on_session_resumed(Req, #{}, #{}). + +-spec on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_resumed(Req, Options) -> + on_session_resumed(Req, #{}, Options). + +-spec on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_resumed(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnSessionResumed">>, + session_resumed_request, empty_success, <<"emqx.exhook.v1.SessionResumedRequest">>), + Req, Metadata, Options). + +-spec on_session_discarded(emqx_exhook_pb:session_discarded_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_discarded(Req) -> + on_session_discarded(Req, #{}, #{}). + +-spec on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_discarded(Req, Options) -> + on_session_discarded(Req, #{}, Options). + +-spec on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_discarded(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnSessionDiscarded">>, + session_discarded_request, empty_success, <<"emqx.exhook.v1.SessionDiscardedRequest">>), + Req, Metadata, Options). + +-spec on_session_takeovered(emqx_exhook_pb:session_takeovered_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_takeovered(Req) -> + on_session_takeovered(Req, #{}, #{}). + +-spec on_session_takeovered(emqx_exhook_pb:session_takeovered_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_takeovered(Req, Options) -> + on_session_takeovered(Req, #{}, Options). + +-spec on_session_takeovered(emqx_exhook_pb:session_takeovered_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_takeovered(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnSessionTakeovered">>, + session_takeovered_request, empty_success, <<"emqx.exhook.v1.SessionTakeoveredRequest">>), + Req, Metadata, Options). + +-spec on_session_terminated(emqx_exhook_pb:session_terminated_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_terminated(Req) -> + on_session_terminated(Req, #{}, #{}). + +-spec on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_terminated(Req, Options) -> + on_session_terminated(Req, #{}, Options). + +-spec on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_session_terminated(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnSessionTerminated">>, + session_terminated_request, empty_success, <<"emqx.exhook.v1.SessionTerminatedRequest">>), + Req, Metadata, Options). + +-spec on_message_publish(emqx_exhook_pb:message_publish_request()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_message_publish(Req) -> + on_message_publish(Req, #{}, #{}). + +-spec on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:options()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_message_publish(Req, Options) -> + on_message_publish(Req, #{}, Options). + +-spec on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, term()}. +on_message_publish(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnMessagePublish">>, + message_publish_request, valued_response, <<"emqx.exhook.v1.MessagePublishRequest">>), + Req, Metadata, Options). + +-spec on_message_delivered(emqx_exhook_pb:message_delivered_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_delivered(Req) -> + on_message_delivered(Req, #{}, #{}). + +-spec on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_delivered(Req, Options) -> + on_message_delivered(Req, #{}, Options). + +-spec on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_delivered(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnMessageDelivered">>, + message_delivered_request, empty_success, <<"emqx.exhook.v1.MessageDeliveredRequest">>), + Req, Metadata, Options). + +-spec on_message_dropped(emqx_exhook_pb:message_dropped_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_dropped(Req) -> + on_message_dropped(Req, #{}, #{}). + +-spec on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_dropped(Req, Options) -> + on_message_dropped(Req, #{}, Options). + +-spec on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_dropped(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnMessageDropped">>, + message_dropped_request, empty_success, <<"emqx.exhook.v1.MessageDroppedRequest">>), + Req, Metadata, Options). + +-spec on_message_acked(emqx_exhook_pb:message_acked_request()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_acked(Req) -> + on_message_acked(Req, #{}, #{}). + +-spec on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_acked(Req, Options) -> + on_message_acked(Req, #{}, Options). + +-spec on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:metadata(), grpc_client:options()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, term()}. +on_message_acked(Req, Metadata, Options) -> + grpc_client:unary(?DEF(<<"/emqx.exhook.v1.HookProvider/OnMessageAcked">>, + message_acked_request, empty_success, <<"emqx.exhook.v1.MessageAckedRequest">>), + Req, Metadata, Options). + diff --git a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl new file mode 100644 index 000000000..d2cc78b47 --- /dev/null +++ b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl @@ -0,0 +1,128 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<" +exhook: { + servers: [ + { name: \"default\" + url: \"http://127.0.0.1:9000\" + } + ] +} +">>). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Cfg) -> + _ = emqx_exhook_demo_svr:start(), + ok = emqx_config:init_load(emqx_exhook_schema, ?CONF_DEFAULT), + emqx_ct_helpers:start_apps([emqx_exhook]), + Cfg. + +end_per_suite(_Cfg) -> + emqx_ct_helpers:stop_apps([emqx_exhook]), + emqx_exhook_demo_svr:stop(). + +%%-------------------------------------------------------------------- +%% Test cases +%%-------------------------------------------------------------------- + +t_noserver_nohook(_) -> + emqx_exhook:disable(<<"default">>), + ?assertEqual([], ets:tab2list(emqx_hooks)), + ok = emqx_exhook:enable(<<"default">>), + ?assertNotEqual([], ets:tab2list(emqx_hooks)). + +t_access_failed_if_no_server_running(_) -> + emqx_exhook:disable(<<"default">>), + ClientInfo = #{clientid => <<"user-id-1">>, + username => <<"usera">>, + peerhost => {127,0,0,1}, + sockport => 1883, + protocol => mqtt, + mountpoint => undefined + }, + ?assertMatch({stop, {error, not_authorized}}, + emqx_exhook_handler:on_client_authenticate(ClientInfo, #{auth_result => success})), + + ?assertMatch({stop, deny}, + emqx_exhook_handler:on_client_authorize(ClientInfo, publish, <<"t/1">>, allow)), + + Message = emqx_message:make(<<"t/1">>, <<"abc">>), + ?assertMatch({stop, Message}, + emqx_exhook_handler:on_message_publish(Message)), + emqx_exhook:enable(<<"default">>). + +t_cli_list(_) -> + meck_print(), + ?assertEqual( [[emqx_exhook_server:format(emqx_exhook_mngr:server(Name)) || Name <- emqx_exhook:list()]] + , emqx_exhook_cli:cli(["server", "list"]) + ), + unmeck_print(). + +t_cli_enable_disable(_) -> + meck_print(), + ?assertEqual([already_started], emqx_exhook_cli:cli(["server", "enable", "default"])), + ?assertEqual(ok, emqx_exhook_cli:cli(["server", "disable", "default"])), + ?assertEqual([["name=default, hooks=#{}, active=false"]], emqx_exhook_cli:cli(["server", "list"])), + + ?assertEqual([not_running], emqx_exhook_cli:cli(["server", "disable", "default"])), + ?assertEqual(ok, emqx_exhook_cli:cli(["server", "enable", "default"])), + unmeck_print(). + +t_cli_stats(_) -> + meck_print(), + _ = emqx_exhook_cli:cli(["server", "stats"]), + _ = emqx_exhook_cli:cli(x), + unmeck_print(). + +%%-------------------------------------------------------------------- +%% Utils +%%-------------------------------------------------------------------- + +meck_print() -> + meck:new(emqx_ctl, [passthrough, no_history, no_link]), + meck:expect(emqx_ctl, print, fun(_) -> ok end), + meck:expect(emqx_ctl, print, fun(_, Args) -> Args end). + +unmeck_print() -> + meck:unload(emqx_ctl). + +loaded_exhook_hookpoints() -> + lists:filtermap(fun(E) -> + Name = element(2, E), + Callbacks = element(3, E), + case lists:any(fun is_exhook_callback/1, Callbacks) of + true -> {true, Name}; + _ -> false + end + end, ets:tab2list(emqx_hooks)). + +is_exhook_callback(Cb) -> + Action = element(2, Cb), + emqx_exhook_handler == element(1, Action). diff --git a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl new file mode 100644 index 000000000..656788b5e --- /dev/null +++ b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl @@ -0,0 +1,339 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exhook_demo_svr). + +-behavior(emqx_exhook_v_1_hook_provider_bhvr). + +%% +-export([ start/0 + , stop/0 + , take/0 + , in/1 + ]). + +%% gRPC server HookProvider callbacks +-export([ on_provider_loaded/2 + , on_provider_unloaded/2 + , on_client_connect/2 + , on_client_connack/2 + , on_client_connected/2 + , on_client_disconnected/2 + , on_client_authenticate/2 + , on_client_authorize/2 + , on_client_subscribe/2 + , on_client_unsubscribe/2 + , on_session_created/2 + , on_session_subscribed/2 + , on_session_unsubscribed/2 + , on_session_resumed/2 + , on_session_discarded/2 + , on_session_takeovered/2 + , on_session_terminated/2 + , on_message_publish/2 + , on_message_delivered/2 + , on_message_dropped/2 + , on_message_acked/2 + ]). + +-define(PORT, 9000). +-define(NAME, ?MODULE). + +%%-------------------------------------------------------------------- +%% Server APIs +%%-------------------------------------------------------------------- + +start() -> + Pid = spawn(fun mngr_main/0), + register(?MODULE, Pid), + {ok, Pid}. + +stop() -> + grpc:stop_server(?NAME), + ?MODULE ! stop. + +take() -> + ?MODULE ! {take, self()}, + receive {value, V} -> V + after 5000 -> error(timeout) end. + +in({FunName, Req}) -> + ?MODULE ! {in, FunName, Req}. + +mngr_main() -> + application:ensure_all_started(grpc), + Services = #{protos => [emqx_exhook_pb], + services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr} + }, + Options = [], + Svr = grpc:start_server(?NAME, ?PORT, Services, Options), + mngr_loop([Svr, queue:new(), queue:new()]). + +mngr_loop([Svr, Q, Takes]) -> + receive + {in, FunName, Req} -> + {NQ1, NQ2} = reply(queue:in({FunName, Req}, Q), Takes), + mngr_loop([Svr, NQ1, NQ2]); + {take, From} -> + {NQ1, NQ2} = reply(Q, queue:in(From, Takes)), + mngr_loop([Svr, NQ1, NQ2]); + stop -> + exit(normal) + end. + +reply(Q1, Q2) -> + case queue:len(Q1) =:= 0 orelse + queue:len(Q2) =:= 0 of + true -> {Q1, Q2}; + _ -> + {{value, {Name, V}}, NQ1} = queue:out(Q1), + {{value, From}, NQ2} = queue:out(Q2), + From ! {value, {Name, V}}, + {NQ1, NQ2} + end. + +%%-------------------------------------------------------------------- +%% callbacks +%%-------------------------------------------------------------------- + +-spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. + +on_provider_loaded(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{hooks => [ + #{name => <<"client.connect">>}, + #{name => <<"client.connack">>}, + #{name => <<"client.connected">>}, + #{name => <<"client.disconnected">>}, + #{name => <<"client.authenticate">>}, + #{name => <<"client.authorize">>}, + #{name => <<"client.subscribe">>}, + #{name => <<"client.unsubscribe">>}, + #{name => <<"session.created">>}, + #{name => <<"session.subscribed">>}, + #{name => <<"session.unsubscribed">>}, + #{name => <<"session.resumed">>}, + #{name => <<"session.discarded">>}, + #{name => <<"session.takeovered">>}, + #{name => <<"session.terminated">>}, + #{name => <<"message.publish">>}, + #{name => <<"message.delivered">>}, + #{name => <<"message.acked">>}, + #{name => <<"message.dropped">>}]}, Md}. +-spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_provider_unloaded(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_client_connect(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_client_connack(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_client_connected(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_client_disconnected(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + %% some cases for testing + case Username of + <<"baduser">> -> + {ok, #{type => 'STOP_AND_RETURN', + value => {bool_result, false}}, Md}; + <<"gooduser">> -> + {ok, #{type => 'STOP_AND_RETURN', + value => {bool_result, true}}, Md}; + <<"normaluser">> -> + {ok, #{type => 'CONTINUE', + value => {bool_result, true}}, Md}; + _ -> + {ok, #{type => 'IGNORE'}, Md} + end. + +-spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + %% some cases for testing + case Username of + <<"baduser">> -> + {ok, #{type => 'STOP_AND_RETURN', + value => {bool_result, false}}, Md}; + <<"gooduser">> -> + {ok, #{type => 'STOP_AND_RETURN', + value => {bool_result, true}}, Md}; + <<"normaluser">> -> + {ok, #{type => 'CONTINUE', + value => {bool_result, true}}, Md}; + _ -> + {ok, #{type => 'IGNORE'}, Md} + end. + +-spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_client_subscribe(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_client_unsubscribe(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_session_created(emqx_exhook_pb:session_created_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_session_created(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_session_subscribed(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_session_unsubscribed(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_session_resumed(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_session_discarded(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_session_takeovered(emqx_exhook_pb:session_takeovered_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_session_takeovered(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_session_terminated(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_message_publish(#{message := #{from := From} = Msg} = Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + %% some cases for testing + case From of + <<"baduser">> -> + NMsg = Msg#{qos => 0, + topic => <<"">>, + payload => <<"">> + }, + {ok, #{type => 'STOP_AND_RETURN', + value => {message, NMsg}}, Md}; + <<"gooduser">> -> + NMsg = Msg#{topic => From, + payload => From}, + {ok, #{type => 'STOP_AND_RETURN', + value => {message, NMsg}}, Md}; + _ -> + {ok, #{type => 'IGNORE'}, Md} + end. + +-spec on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_message_delivered(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_message_dropped(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. + +-spec on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:metadata()) + -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} + | {error, grpc_cowboy_h:error_response()}. +on_message_acked(Req, Md) -> + ?MODULE:in({?FUNCTION_NAME, Req}), + %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), + {ok, #{}, Md}. diff --git a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl new file mode 100644 index 000000000..a57e0b49c --- /dev/null +++ b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl @@ -0,0 +1,524 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(prop_exhook_hooks). + +-include_lib("proper/include/proper.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-import(emqx_ct_proper_types, + [ conninfo/0 + , clientinfo/0 + , sessioninfo/0 + , message/0 + , connack_return_code/0 + , topictab/0 + , topic/0 + , subopts/0 + ]). + +-define(CONF_DEFAULT, <<" +exhook: { + servers: [ + { name: \"default\" + url: \"http://127.0.0.1:9000\" + } + ] +} +">>). + +-define(ALL(Vars, Types, Exprs), + ?SETUP(fun() -> + State = do_setup(), + fun() -> do_teardown(State) end + end, ?FORALL(Vars, Types, Exprs))). + + +%%-------------------------------------------------------------------- +%% Properties +%%-------------------------------------------------------------------- + +prop_client_connect() -> + ?ALL({ConnInfo, ConnProps}, + {conninfo(), conn_properties()}, + begin + ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]), + {'on_client_connect', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{props => properties(ConnProps), + conninfo => from_conninfo(ConnInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_client_connack() -> + ?ALL({ConnInfo, Rc, AckProps}, + {conninfo(), connack_return_code(), ack_properties()}, + begin + ok = emqx_hooks:run('client.connack', [ConnInfo, Rc, AckProps]), + {'on_client_connack', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{props => properties(AckProps), + result_code => atom_to_binary(Rc, utf8), + conninfo => from_conninfo(ConnInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_client_authenticate() -> + ?ALL({ClientInfo0, AuthResult}, + {clientinfo(), authresult()}, + begin + ClientInfo = inject_magic_into(username, ClientInfo0), + OutAuthResult = emqx_hooks:run_fold('client.authenticate', [ClientInfo], AuthResult), + ExpectedAuthResult = case maps:get(username, ClientInfo) of + <<"baduser">> -> {error, not_authorized}; + <<"gooduser">> -> ok; + <<"normaluser">> -> ok; + _ -> case AuthResult of + ok -> ok; + _ -> {error, not_authorized} + end + end, + ?assertEqual(ExpectedAuthResult, OutAuthResult), + + {'on_client_authenticate', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{result => authresult_to_bool(AuthResult), + clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_client_authorize() -> + ?ALL({ClientInfo0, PubSub, Topic, Result}, + {clientinfo(), oneof([publish, subscribe]), + topic(), oneof([allow, deny])}, + begin + ClientInfo = inject_magic_into(username, ClientInfo0), + OutResult = emqx_hooks:run_fold( + 'client.authorize', + [ClientInfo, PubSub, Topic], + Result), + ExpectedOutResult = case maps:get(username, ClientInfo) of + <<"baduser">> -> deny; + <<"gooduser">> -> allow; + <<"normaluser">> -> allow; + _ -> Result + end, + ?assertEqual(ExpectedOutResult, OutResult), + + {'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{result => aclresult_to_bool(Result), + type => pubsub_to_enum(PubSub), + topic => Topic, + clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_client_connected() -> + ?ALL({ClientInfo, ConnInfo}, + {clientinfo(), conninfo()}, + begin + ok = emqx_hooks:run('client.connected', [ClientInfo, ConnInfo]), + {'on_client_connected', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_client_disconnected() -> + ?ALL({ClientInfo, Reason, ConnInfo}, + {clientinfo(), shutdown_reason(), conninfo()}, + begin + ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]), + {'on_client_disconnected', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{reason => stringfy(Reason), + clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_client_subscribe() -> + ?ALL({ClientInfo, SubProps, TopicTab}, + {clientinfo(), sub_properties(), topictab()}, + begin + ok = emqx_hooks:run('client.subscribe', [ClientInfo, SubProps, TopicTab]), + {'on_client_subscribe', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{props => properties(SubProps), + topic_filters => topicfilters(TopicTab), + clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_client_unsubscribe() -> + ?ALL({ClientInfo, UnSubProps, TopicTab}, + {clientinfo(), unsub_properties(), topictab()}, + begin + ok = emqx_hooks:run('client.unsubscribe', [ClientInfo, UnSubProps, TopicTab]), + {'on_client_unsubscribe', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{props => properties(UnSubProps), + topic_filters => topicfilters(TopicTab), + clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_session_created() -> + ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, + begin + ok = emqx_hooks:run('session.created', [ClientInfo, SessInfo]), + {'on_session_created', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_session_subscribed() -> + ?ALL({ClientInfo, Topic, SubOpts}, + {clientinfo(), topic(), subopts()}, + begin + ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]), + {'on_session_subscribed', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{topic => Topic, + subopts => subopts(SubOpts), + clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_session_unsubscribed() -> + ?ALL({ClientInfo, Topic, SubOpts}, + {clientinfo(), topic(), subopts()}, + begin + ok = emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, SubOpts]), + {'on_session_unsubscribed', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{topic => Topic, + clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_session_resumed() -> + ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, + begin + ok = emqx_hooks:run('session.resumed', [ClientInfo, SessInfo]), + {'on_session_resumed', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_session_discared() -> + ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, + begin + ok = emqx_hooks:run('session.discarded', [ClientInfo, SessInfo]), + {'on_session_discarded', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_session_takeovered() -> + ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, + begin + ok = emqx_hooks:run('session.takeovered', [ClientInfo, SessInfo]), + {'on_session_takeovered', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_session_terminated() -> + ?ALL({ClientInfo, Reason, SessInfo}, + {clientinfo(), shutdown_reason(), sessioninfo()}, + begin + ok = emqx_hooks:run('session.terminated', [ClientInfo, Reason, SessInfo]), + {'on_session_terminated', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{reason => stringfy(Reason), + clientinfo => from_clientinfo(ClientInfo) + }, + ?assertEqual(Expected, Resp), + true + end). + +prop_message_publish() -> + ?ALL(Msg0, message(), + begin + Msg = emqx_message:from_map( + inject_magic_into(from, emqx_message:to_map(Msg0))), + OutMsg= emqx_hooks:run_fold('message.publish', [], Msg), + case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of + true -> + ?assertEqual(Msg, OutMsg), + skip; + _ -> + ExpectedOutMsg = case emqx_message:from(Msg) of + <<"baduser">> -> + MsgMap = emqx_message:to_map(Msg), + emqx_message:from_map( + MsgMap#{qos => 0, + topic => <<"">>, + payload => <<"">> + }); + <<"gooduser">> = From -> + MsgMap = emqx_message:to_map(Msg), + emqx_message:from_map( + MsgMap#{topic => From, + payload => From + }); + _ -> Msg + end, + ?assertEqual(ExpectedOutMsg, OutMsg), + + {'on_message_publish', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{message => from_message(Msg) + }, + ?assertEqual(Expected, Resp) + end, + true + end). + +prop_message_dropped() -> + ?ALL({Msg, By, Reason}, {message(), hardcoded, shutdown_reason()}, + begin + ok = emqx_hooks:run('message.dropped', [Msg, By, Reason]), + case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of + true -> skip; + _ -> + {'on_message_dropped', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{reason => stringfy(Reason), + message => from_message(Msg) + }, + ?assertEqual(Expected, Resp) + end, + true + end). + +prop_message_delivered() -> + ?ALL({ClientInfo, Msg}, {clientinfo(), message()}, + begin + ok = emqx_hooks:run('message.delivered', [ClientInfo, Msg]), + case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of + true -> skip; + _ -> + {'on_message_delivered', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{clientinfo => from_clientinfo(ClientInfo), + message => from_message(Msg) + }, + ?assertEqual(Expected, Resp) + end, + true + end). + +prop_message_acked() -> + ?ALL({ClientInfo, Msg}, {clientinfo(), message()}, + begin + ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]), + case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of + true -> skip; + _ -> + {'on_message_acked', Resp} = emqx_exhook_demo_svr:take(), + Expected = + #{clientinfo => from_clientinfo(ClientInfo), + message => from_message(Msg) + }, + ?assertEqual(Expected, Resp) + end, + true + end). + +nodestr() -> + stringfy(node()). + +peerhost(#{peername := {Host, _}}) -> + ntoa(Host). + +sockport(#{sockname := {_, Port}}) -> + Port. + +%% copied from emqx_exhook + +ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> + list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256})); +ntoa(IP) -> + list_to_binary(inet_parse:ntoa(IP)). + +maybe(undefined) -> <<>>; +maybe(B) -> B. + +properties(undefined) -> []; +properties(M) when is_map(M) -> + maps:fold(fun(K, V, Acc) -> + [#{name => stringfy(K), + value => stringfy(V)} | Acc] + end, [], M). + +topicfilters(Tfs) when is_list(Tfs) -> + [#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs]. + +%% @private +stringfy(Term) when is_binary(Term) -> + Term; +stringfy(Term) when is_integer(Term) -> + integer_to_binary(Term); +stringfy(Term) when is_atom(Term) -> + atom_to_binary(Term, utf8); +stringfy(Term) -> + unicode:characters_to_binary((io_lib:format("~0p", [Term]))). + +subopts(SubOpts) -> + #{qos => maps:get(qos, SubOpts, 0), + rh => maps:get(rh, SubOpts, 0), + rap => maps:get(rap, SubOpts, 0), + nl => maps:get(nl, SubOpts, 0), + share => maps:get(share, SubOpts, <<>>) + }. + +authresult_to_bool(AuthResult) -> + AuthResult == ok. + +aclresult_to_bool(Result) -> + Result == allow. + +pubsub_to_enum(publish) -> 'PUBLISH'; +pubsub_to_enum(subscribe) -> 'SUBSCRIBE'. + +from_conninfo(ConnInfo) -> + #{node => nodestr(), + clientid => maps:get(clientid, ConnInfo), + username => maybe(maps:get(username, ConnInfo, <<>>)), + peerhost => peerhost(ConnInfo), + sockport => sockport(ConnInfo), + proto_name => maps:get(proto_name, ConnInfo), + proto_ver => stringfy(maps:get(proto_ver, ConnInfo)), + keepalive => maps:get(keepalive, ConnInfo) + }. + +from_clientinfo(ClientInfo) -> + #{node => nodestr(), + clientid => maps:get(clientid, ClientInfo), + username => maybe(maps:get(username, ClientInfo, <<>>)), + password => maybe(maps:get(password, ClientInfo, <<>>)), + peerhost => ntoa(maps:get(peerhost, ClientInfo)), + sockport => maps:get(sockport, ClientInfo), + protocol => stringfy(maps:get(protocol, ClientInfo)), + mountpoint => maybe(maps:get(mountpoint, ClientInfo, <<>>)), + is_superuser => maps:get(is_superuser, ClientInfo, false), + anonymous => maps:get(anonymous, ClientInfo, true), + cn => maybe(maps:get(cn, ClientInfo, <<>>)), + dn => maybe(maps:get(dn, ClientInfo, <<>>)) + }. + +from_message(Msg) -> + #{node => nodestr(), + id => emqx_guid:to_hexstr(emqx_message:id(Msg)), + qos => emqx_message:qos(Msg), + from => stringfy(emqx_message:from(Msg)), + topic => emqx_message:topic(Msg), + payload => emqx_message:payload(Msg), + timestamp => emqx_message:timestamp(Msg) + }. + +%%-------------------------------------------------------------------- +%% Helper +%%-------------------------------------------------------------------- + +do_setup() -> + logger:set_primary_config(#{level => warning}), + _ = emqx_exhook_demo_svr:start(), + ok = emqx_config:init_load(emqx_exhook_schema, ?CONF_DEFAULT), + emqx_ct_helpers:start_apps([emqx_exhook]), + %% waiting first loaded event + {'on_provider_loaded', _} = emqx_exhook_demo_svr:take(), + ok. + +do_teardown(_) -> + emqx_ct_helpers:stop_apps([emqx_exhook]), + %% waiting last unloaded event + {'on_provider_unloaded', _} = emqx_exhook_demo_svr:take(), + _ = emqx_exhook_demo_svr:stop(), + logger:set_primary_config(#{level => notice}), + timer:sleep(2000), + ok. + +%%-------------------------------------------------------------------- +%% Generators +%%-------------------------------------------------------------------- + +conn_properties() -> + #{}. + +ack_properties() -> + #{}. + +sub_properties() -> + #{}. + +unsub_properties() -> + #{}. + +shutdown_reason() -> + oneof([utf8(), {shutdown, emqx_ct_proper_types:limited_atom()}]). + +authresult() -> + ?LET(RC, connack_return_code(), + case RC of + success -> ok; + _ -> {error, RC} + end). + +inject_magic_into(Key, Object) -> + case castspell() of + muggles -> Object; + Spell -> + Object#{Key => Spell} + end. + +castspell() -> + L = [<<"baduser">>, <<"gooduser">>, <<"normaluser">>, muggles], + lists:nth(rand:uniform(length(L)), L). diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 16315b012..2ce48bf75 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -2,169 +2,299 @@ ## EMQ X Gateway configurations ##-------------------------------------------------------------------- -gateway: { +## TODO: These configuration options are temporary example here. +## In the final version, it will be commented out. - stomp.1: { - frame: { - max_headers: 10 - max_headers_length: 1024 - max_body_length: 8192 - } +gateway.stomp { - clientinfo_override: { - username: "${Packet.headers.login}" - password: "${Packet.headers.passcode}" - } + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s - authentication: { - enable: true - authenticators: [ - { - name: "authenticator1" - mechanism: password-based - server_type: built-in-database - user_id_type: clientid - } - ] - } + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true - listener.tcp.1: { - bind: 61613 - acceptors: 16 - max_connections: 1024000 - max_conn_rate: 1000 - active_n: 100 - } - } + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "" - coap.1: { - enable_stats: false - authentication.enable: false - heartbeat: 30s - resource: mqtt - notify_type: qos - subscribe_qos: qos0 - publish_qos: qos1 - listener.udp.1: { - bind: 5683 - } - } + frame { + max_headers = 10 + max_headers_length = 1024 + max_body_length = 8192 + } - coap.2: { - enable_stats: false - authentication.enable:false - heartbeat: 30s - resource: pubsub - notify_type: non - subscribe_qos: qos2 - publish_qos: coap - listener.udp.1: { - bind: 5687 - } - } + clientinfo_override { + username = "${Packet.headers.login}" + password = "${Packet.headers.passcode}" + } - mqttsn.1: { - ## The MQTT-SN Gateway ID in ADVERTISE message. - gateway_id: 1 + authentication: [ + # { + # name = "authenticator1" + # type = "password-based:built-in-database" + # user_id_type = clientid + # } + ] - ## Enable broadcast this gateway to WLAN - broadcast: true + listeners.tcp.default { + bind = 61613 + acceptors = 16 + max_connections = 1024000 + max_conn_rate = 1000 - ## To control whether write statistics data into ETS table - ## for dashbord to read. - enable_stats: true + access_rules = [ + "allow all" + ] - ## To control whether accept and process the received - ## publish message with qos=-1. - enable_qos3: true + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.active_n = 100 + tcp.backlog = 1024 + tcp.buffer = 4KB + } - ## Idle timeout for a MQTT-SN channel - idle_timeout: 30s + listeners.ssl.default { + bind = 61614 + acceptors = 16 + max_connections = 1024000 + max_conn_rate = 1000 - ## The pre-defined topic name corresponding to the pre-defined topic - ## id of N. - ## Note that the pre-defined topic id of 0 is reserved. - predefined: [ - { id: 1 - topic: "/predefined/topic/name/hello" - }, - { id: 2 - topic: "/predefined/topic/name/nice" - } - ] + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.active_n = 100 + tcp.backlog = 1024 + tcp.buffer = 4KB - ### ClientInfo override - clientinfo_override: { - username: "mqtt_sn_user" - password: "abc" - } - - listener.udp.1: { - bind: 1884 - max_connections: 10240000 - max_conn_rate: 1000 - } - } - - ## Extension Protocol Gateway - exproto.1: { - - ## The gRPC server to accept requests - server: { - bind: 9100 - #ssl.keyfile: - #ssl.certfile: - #ssl.cacertfile: - } - - handler: { - address: "http://127.0.0.1:9001" - #ssl.keyfile: - #ssl.certfile: - #ssl.cacertfile: - } - - authentication.enable: false - - listener.tcp.1: { - bind: 7993 - acceptors: 8 - max_connections: 10240 - max_conn_rate: 1000 - } - - #listener.ssl.1: {} - #listener.udp.1: {} - #listener.dtls.1: {} - } - - lwm2m_xml_dir: "{{ platform_etc_dir }}/lwm2m_xml" - - lwm2m.1: { - - lifetime_min: 1s - - lifetime_max: 86400s - - qmode_time_windonw: 22 - - auto_observe: false - - mountpoint: "lwm2m/%e/" - - ## always | contains_object_list - update_msg_publish_condition: contains_object_list - - translators: { - command: "dn/#" - response: "up/resp" - notify: "up/notify" - register: "up/resp" - update: "up/resp" - } - - listener.udp.1 { - bind: 5783 - } - } + ## SSL options + ## See ${example_common_ssl_options} for more information + ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + #ssl.verify = verify_none + #ssl.fail_if_no_peer_cert = false + #ssl.server_name_indication = disable + #ssl.secure_renegotiate = false + #ssl.reuse_sessions = false + #ssl.honor_cipher_order = false + #ssl.handshake_timeout = 15s + #ssl.depth = 10 + #ssl.password = foo + #ssl.dhfile = path-to-your-file + } +} + +gateway.coap { + + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "" + + notify_type = qos + + ## if true, you need to establish a connection before use + connection_required = false + subscribe_qos = qos0 + publish_qos = qos1 + + listeners.udp.default { + bind = 5683 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + } + listeners.dtls.default { + bind = 5684 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + + ## DTLS Options + ## See #{example_common_dtls_options} for more information + dtls.versions = ["dtlsv1.2", "dtlsv1"] + dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + } +} + +gateway.mqttsn { + + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "" + + ## The MQTT-SN Gateway ID in ADVERTISE message. + gateway_id = 1 + + ## Enable broadcast this gateway to WLAN + broadcast = true + + ## To control whether accept and process the received + ## publish message with qos=-1. + enable_qos3 = true + + ## The pre-defined topic name corresponding to the pre-defined topic + ## id of N. + ## Note that the pre-defined topic id of 0 is reserved. + predefined = [ + { id = 1 + topic = "/predefined/topic/name/hello" + }, + { id = 2 + topic = "/predefined/topic/name/nice" + } + ] + + ### ClientInfo override + clientinfo_override { + username = "mqtt_sn_user" + password = "abc" + } + + listeners.udp.default { + bind = 1884 + max_connections = 10240000 + max_conn_rate = 1000 + } + + listeners.dtls.default { + bind = 1885 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + + ## DTLS Options + ## See #{example_common_dtls_options} for more information + dtls.versions = ["dtlsv1.2", "dtlsv1"] + dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + } + +} + +gateway.lwm2m { + + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "lwm2m/%u" + + xml_dir = "{{ platform_etc_dir }}/lwm2m_xml" + + lifetime_min = 1s + lifetime_max = 86400s + qmode_time_windonw = 22 + auto_observe = false + + ## always | contains_object_list + update_msg_publish_condition = contains_object_list + + + translators { + command { + topic = "/dn/#" + qos = 0 + } + + response { + topic = "/up/resp" + qos = 0 + } + + notify { + topic = "/up/notify" + qos = 0 + } + + register { + topic = "/up/resp" + qos = 0 + } + + update { + topic = "/up/resp" + qos = 0 + } + } + + listeners.udp.default { + bind = 5783 + } +} + +gateway.exproto { + + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "" + + ## The gRPC server to accept requests + server { + bind = 9100 + #ssl.keyfile: + #ssl.certfile: + #ssl.cacertfile: + } + + handler { + address = "http://127.0.0.1:9001" + #ssl.keyfile: + #ssl.certfile: + #ssl.cacertfile: + } + + listeners.tcp.default { + bind = 7993 + acceptors = 8 + max_connections = 10240 + max_conn_rate = 1000 + } + #listeners.ssl.default: {} + #listeners.udp.default: {} + #listeners.dtls.default: {} } diff --git a/apps/emqx_gateway/include/emqx_coap.hrl b/apps/emqx_gateway/include/emqx_coap.hrl index 911d10a22..d47dd17fd 100644 --- a/apps/emqx_gateway/include/emqx_coap.hrl +++ b/apps/emqx_gateway/include/emqx_coap.hrl @@ -22,13 +22,6 @@ -define(DEFAULT_MAX_AGE, 60). -define(MAXIMUM_MAX_AGE, 4294967295). --define(EMPTY_RESULT, #{}). --define(TRANSFER_RESULT(Keys, From, Value, R1), - begin - R2 = maps:with(Keys, R1), - R2#{From => Value} - end). - -type coap_message_id() :: 1 .. ?MAX_MESSAGE_ID. -type message_type() :: con | non | ack | reset. -type max_age() :: 1 .. ?MAXIMUM_MAX_AGE. @@ -61,7 +54,7 @@ , uri_path => list(binary()) , content_format => 0 .. 65535 , max_age => non_neg_integer() - , uri_query => list(binary()) + , uri_query => list(binary()) | map() , 'accept' => 0 .. 65535 , location_query => list(binary()) , proxy_uri => binary() @@ -80,7 +73,4 @@ , options = #{} , payload = <<>>}). --record(coap_content, {etag, max_age = ?DEFAULT_MAX_AGE, format, location_path = [], payload = <<>>}). - -type emqx_coap_message() :: #coap_message{}. --type coap_content() :: #coap_content{}. diff --git a/apps/emqx_gateway/include/emqx_gateway.hrl b/apps/emqx_gateway/include/emqx_gateway.hrl index 35fad7f23..5c0893cb2 100644 --- a/apps/emqx_gateway/include/emqx_gateway.hrl +++ b/apps/emqx_gateway/include/emqx_gateway.hrl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% 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. @@ -17,19 +17,22 @@ -ifndef(EMQX_GATEWAY_HRL). -define(EMQX_GATEWAY_HRL, 1). --type instance_id() :: atom(). --type gateway_type() :: atom(). +-type gateway_name() :: atom(). -%% @doc The Gateway Instace defination --type instance() :: - #{ id := instance_id() - , type := gateway_type() - , name := binary() +-type listener() :: #{}. + +%% @doc The Gateway defination +-type gateway() :: + #{ name := gateway_name() , descr => binary() | undefined - %% Appears only in creating or detailed info - , rawconf => map() - %% Appears only in getting instance status/info - , status => stopped | running + %% Appears only in getting gateway info + , status => stopped | running | unloaded + %% Timestamp in millisecond + , created_at => integer() + %% Timestamp in millisecond + , started_at => integer() + %% Appears only in getting gateway info + , config => emqx_config:config() }. -endif. diff --git a/apps/emqx_gateway/include/emqx_lwm2m.hrl b/apps/emqx_gateway/include/emqx_lwm2m.hrl index 5462f489d..05e0f0503 100644 --- a/apps/emqx_gateway/include/emqx_lwm2m.hrl +++ b/apps/emqx_gateway/include/emqx_lwm2m.hrl @@ -14,15 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- --define(APP, emqx_lwm2m). +-define(LWAPP, emqx_lwm2m). --record(coap_mqtt_auth, { clientid - , username - , password - }). --record(lwm2m_context, { epn - , location - }). -define(OMA_ALTER_PATH_RT, <<"\"oma.lwm2m\"">>). @@ -42,7 +35,7 @@ -define(ERR_NOT_FOUND, <<"Not Found">>). -define(ERR_UNAUTHORIZED, <<"Unauthorized">>). -define(ERR_BAD_REQUEST, <<"Bad Request">>). - +-define(REG_PREFIX, <<"rd">>). -define(LWM2M_FORMAT_PLAIN_TEXT, 0). -define(LWM2M_FORMAT_LINK, 40). diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl index 1a032a017..c4fd114e4 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl @@ -44,6 +44,8 @@ -type conn_state() :: idle | connecting | connected | disconnected | atom(). +-type gen_server_from() :: {pid(), Tag :: term()}. + -type reply() :: {outgoing, emqx_gateway_frame:packet()} | {outgoing, [emqx_gateway_frame:packet()]} | {event, conn_state() | updated} @@ -71,9 +73,15 @@ | {shutdown, Reason :: any(), channel()}. %% @doc Handle the custom gen_server:call/2 for its connection process --callback handle_call(Req :: any(), channel()) +-callback handle_call(Req :: any(), From :: gen_server_from(), channel()) -> {reply, Reply :: any(), channel()} + %% Reply to caller and trigger an event(s) + | {reply, Reply :: any(), + EventOrEvents :: tuple() | list(tuple()), channel()} + | {noreply, channel()} + | {noreply, EventOrEvents :: tuple() | list(tuple()), channel()} | {shutdown, Reason :: any(), Reply :: any(), channel()} + %% Shutdown the process, reply to caller and write a packet to client | {shutdown, Reason :: any(), Reply :: any(), emqx_gateway_frame:frame(), channel()}. @@ -94,4 +102,3 @@ %% @doc The callback for process terminated -callback terminate(any(), channel()) -> ok. - diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index 1f5cff043..543b2e169 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -20,7 +20,6 @@ -include_lib("emqx/include/types.hrl"). -include_lib("emqx/include/logger.hrl"). - %% API -export([ start_link/3 , stop/1 @@ -48,7 +47,6 @@ %% Internal callback -export([wakeup_from_hib/2, recvloop/2]). - -record(state, { %% TCP/SSL/UDP/DTLS Wrapped Socket socket :: {esockd_transport, esockd:socket()} | {udp, _, _}, @@ -226,6 +224,9 @@ esockd_send(Data, #state{socket = {udp, _SockPid, Sock}, esockd_send(Data, #state{socket = {esockd_transport, Sock}}) -> esockd_transport:async_send(Sock, Data). +is_datadram_socket({esockd_transport, _}) -> false; +is_datadram_socket({udp, _, _}) -> true. + %%-------------------------------------------------------------------- %% callbacks %%-------------------------------------------------------------------- @@ -393,6 +394,10 @@ append_msg(Q, Msg) -> handle_msg({'$gen_call', From, Req}, State) -> case handle_call(From, Req, State) of + {noreply, NState} -> + {ok, NState}; + {noreply, Msgs, NState} -> + {ok, next_msgs(Msgs), NState}; {reply, Reply, NState} -> gen_server:reply(From, Reply), {ok, NState}; @@ -544,16 +549,24 @@ handle_call(_From, info, State) -> handle_call(_From, stats, State) -> {reply, stats(State), State}; -handle_call(_From, Req, State = #state{ +handle_call(From, Req, State = #state{ chann_mod = ChannMod, channel = Channel}) -> - case ChannMod:handle_call(Req, Channel) of + case ChannMod:handle_call(Req, From, Channel) of + {noreply, NChannel} -> + {noreply, State#state{channel = NChannel}}; + {noreply, Msgs, NChannel} -> + {noreply, Msgs, State#state{channel = NChannel}}; {reply, Reply, NChannel} -> {reply, Reply, State#state{channel = NChannel}}; - {reply, Reply, Replies, NChannel} -> - {reply, Reply, Replies, State#state{channel = NChannel}}; + {reply, Reply, Msgs, NChannel} -> + {reply, Reply, Msgs, State#state{channel = NChannel}}; {shutdown, Reason, Reply, NChannel} -> - shutdown(Reason, Reply, State#state{channel = NChannel}) + shutdown(Reason, Reply, State#state{channel = NChannel}); + {shutdown, Reason, Reply, Packet, NChannel} -> + NState = State#state{channel = NChannel}, + ok = handle_outgoing(Packet, NState), + shutdown(Reason, Reply, NState) end. %%-------------------------------------------------------------------- @@ -672,8 +685,20 @@ with_channel(Fun, Args, State = #state{ %%-------------------------------------------------------------------- %% Handle outgoing packets -handle_outgoing(Packets, State) when is_list(Packets) -> - send(lists:map(serialize_and_inc_stats_fun(State), Packets), State); +handle_outgoing(_Packets = [], _State) -> + ok; +handle_outgoing(Packets, + State = #state{socket = Socket}) when is_list(Packets) -> + case is_datadram_socket(Socket) of + false -> + send( + lists:map(serialize_and_inc_stats_fun(State), Packets), + State); + _ -> + lists:foreach(fun(Packet) -> + handle_outgoing(Packet, State) + end, Packets) + end; handle_outgoing(Packet, State) -> send((serialize_and_inc_stats_fun(State))(Packet), State). @@ -810,7 +835,7 @@ inc_incoming_stats(Ctx, FrameMod, Packet) -> ok end, Name = list_to_atom( - lists:concat(["packets.", FrameMod:type(Packet), ".recevied"])), + lists:concat(["packets.", FrameMod:type(Packet), ".received"])), emqx_gateway_ctx:metrics_inc(Ctx, Name). inc_outgoing_stats(Ctx, FrameMod, Packet) -> @@ -829,7 +854,6 @@ inc_outgoing_stats(Ctx, FrameMod, Packet) -> %%-------------------------------------------------------------------- %% Helper functions --compile({inline, [next_msgs/1]}). next_msgs(Event) when is_tuple(Event) -> Event; next_msgs(More) when is_list(More) -> diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl index 8d413e49c..ac3289dfa 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl @@ -22,29 +22,21 @@ -type reason() :: any(). %% @doc --callback init(Options :: list()) -> {error, reason()} | {ok, GwState :: state()}. - -%% @doc --callback on_insta_create(Insta :: instance(), - Ctx :: emqx_gateway_ctx:context(), - GwState :: state() - ) +-callback on_gateway_load(Gateway :: gateway(), + Ctx :: emqx_gateway_ctx:context()) -> {error, reason()} - | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} + | {ok, [ChildPid :: pid()], GwState :: state()} %% TODO: v0.2 The child spec is better for restarting child process - | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()}. + | {ok, [Childspec :: supervisor:child_spec()], GwState :: state()}. %% @doc --callback on_insta_update(NewInsta :: instance(), - OldInsta :: instance(), - GwInstaState :: state(), - GwState :: state()) +-callback on_gateway_update(Config :: emqx_config:config(), + Gateway :: gateway(), + GwState :: state()) -> ok - | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} - | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()} + | {ok, [ChildPid :: pid()], NGwState :: state()} + | {ok, [Childspec :: supervisor:child_spec()], NGwState :: state()} | {error, reason()}. %% @doc --callback on_insta_destroy(Insta :: instance(), - GwInstaState :: state(), - GwState :: state()) -> ok. +-callback on_gateway_unload(Gateway :: gateway(), GwState :: state()) -> ok. diff --git a/apps/emqx_gateway/src/coap/README.md b/apps/emqx_gateway/src/coap/README.md index f71938c92..54f0fde84 100644 --- a/apps/emqx_gateway/src/coap/README.md +++ b/apps/emqx_gateway/src/coap/README.md @@ -1,190 +1,443 @@ # Table of Contents -1. [EMQX 5.0 CoAP Gateway](#org6feb6de) -2. [CoAP Message Processing Flow](#org8458c1a) - 1. [Request Timing Diagram](#orgeaa4f53) - 1. [Transport && Transport Manager](#org88207b8) - 2. [Resource](#orgb32ce94) -3. [Resource](#org8956f90) - 1. [MQTT Resource](#orge8c21b1) - 2. [PubSub Resource](#org68ddce7) -4. [Heartbeat](#orgffdfecd) -5. [Command](#org43004c2) -6. [MQTT QOS <=> CoAP non/con](#org0157b5c) +1. [EMQX 5.0 CoAP Gateway](#org61e5bb8) + 1. [Features](#orgeddbc94) + 1. [PubSub Handler](#orgfc7be2d) + 2. [MQTT Handler](#org55be508) + 3. [Heartbeat](#org3d1a32e) + 4. [Query String](#org9a6b996) + 2. [Implementation](#org9985dfe) + 1. [Request/Response flow](#orge94210c) + 3. [Example](#ref_example) - + # EMQX 5.0 CoAP Gateway -emqx-coap is a CoAP Gateway for EMQ X Broker. -It translates CoAP messages into MQTT messages and make it possible to communiate between CoAP clients and MQTT clients. +emqx-coap is a CoAP Gateway for EMQ X Broker. It translates CoAP messages into MQTT messages and make it possible to communiate between CoAP clients and MQTT clients. - + -# CoAP Message Processing Flow +## Features + +- Partially achieves [Publish-Subscribe Broker for the Constrained Application Protocol (CoAP)](https://datatracker.ietf.org/doc/html/draft-ietf-core-coap-pubsub-09) + we called this as ps handler, include following functions: + - Publish + - Subscribe + - UnSubscribe +- Long connection and authorization verification called as MQTT handler - + -## Request Timing Diagram +### PubSub Handler +1. Publish - ,------. ,------------. ,-----------------. ,---------. ,--------. - |client| |coap_gateway| |transport_manager| |transport| |resource| - `--+---' `-----+------' `--------+--------' `----+----' `---+----' - | | | | | - | -------------------> | | | - | | | | | - | | | | | - | | ------------------------>| | | - | | | | | - | | | | | - | | |----------------------->| | - | | | | | - | | | | | - | | | |------------------>| - | | | | | - | | | | | - | | | |<------------------| - | | | | | - | | | | | - | | |<-----------------------| | - | | | | | - | | | | | - | | <------------------------| | | - | | | | | - | | | | | - | <------------------- | | | - ,--+---. ,-----+------. ,--------+--------. ,----+----. ,---+----. - |client| |coap_gateway| |transport_manager| |transport| |resource| - `------' `------------' `-----------------' `---------' `--------' + Method: POST\ + URI Schema: ps/{+topic}{?q\*}\ + q\*: [Shared Options](#orgc50043b)\ + Response: + - 2.04 "Changed" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" when with wrong auth uri query - +2. Subscribe -### Transport && Transport Manager + Method: GET + Options: -Transport is a module that manages the life cycle and behaviour of CoAP messages\ -And the transport manager is to manage all transport which in this gateway + - Observer = 0 + URI Schema: ps/{+topic}{?q\*}\ + q\*: see [Shared Options](#orgc50043b)\ + Response: - + - 2.05 "Content" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" when with wrong auth uri query -### Resource - -The Resource is a behaviour that must implement GET/PUT/POST/DELETE method\ -Different Resources can have different implementations of this four method\ -Each gateway can only use one Resource module to process CoAP Request Message - - - - -# Resource - - - - -## MQTT Resource - -The MQTT Resource is a simple CoAP to MQTT adapter, the implementation of each method is as follows: - -- use uri path as topic -- GET: subscribe the topic -- PUT: publish message to this topic -- POST: like PUT -- DELETE: unsubscribe the topic - - - - -## PubSub Resource - -The PubSub Resource like the MQTT Resource, but has a retained topic's message database\ -This Resource is shared, only can has one instance. The implementation: - -- use uri path as topic -- GET: - - GET with observe = 0: subscribe the topic - - GET with observe = 1: unsubscribe the topic - - GET without observe: read lastest message from the message database, key is the topic -- PUT: - insert message into the message database, key is the topic -- POST: - like PUT, but will publish the message -- DELETE: - delete message from the database, key is topic - - - - -# Heartbeat - -At present, the CoAP gateway only supports UDP/DTLS connection, don't support UDP over TCP and UDP over WebSocket. -Because UDP is connectionless, so the client needs to send heartbeat ping to the server interval. Otherwise, the server will close related resources -Use ****POST with empty uri path**** as a heartbeat ping - -example: ``` -coap-client -m post coap://127.0.0.1 + Client1 Client2 Broker + | | Subscribe | + | | ----- GET /ps/topic1 Observe:0 Token:XX ----> | + | | | + | | <---------- 2.05 Content Observe:10---------- | + | | | + | | | + | | Publish | + | ---------|----------- PUT /ps/topic1 "1033.3" --------> | + | | Notify | + | | <---------- 2.05 Content Observe:11 --------- | + | | | ``` - +3. UnSubscribe -# Command + Method : GET + Options: -Command is means the operation which outside the CoAP protocol, like authorization -The Command format: + - Observe = 1 -1. use ****POST**** method -2. uri path is empty -3. query string is like ****action=comandX&argX=valuex&argY=valueY**** + URI Schema: ps/{+topic}{?q\*}\ + q\*: see [Shared Options](#orgc50043b)\ + Response: -example: -1. connect: + - 2.07 "No Content" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" when with wrong auth uri query + + + + +### MQTT Handler + + Establishing a connection is optional. If the CoAP client needs to use connection-based operations, it must first establish a connection. +At the same time, the connectionless mode and the connected mode cannot be mixed. +In connection mode, the Publish/Subscribe/UnSubscribe sent by the client must be has Token and ClientId in query string. +If the Token and Clientid is wrong/miss, EMQ X will reset the request. +The communication token is the data carried in the response payload after the client successfully establishes a connection. +After obtaining the token, the client's subsequent request must attach "token=Token" to the Query String +ClientId is necessary when there is a connection, and is a unique identifier defined by the client. +The server manages the client through the ClientId. If the ClientId is wrong, EMQ X will reset the request. + +1. Create a Connection + + Method: POST + URI Schema: mqtt/connection{?q\*} + q\*: + + - clientid := client uid + - username + - password + + Response: + + - 2.01 "Created" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" wrong username or password + + Payload: Token if success + +2. Close a Connection + + Method : DELETE + URI Schema: mqtt/connection{?q\*} + q\*: + + - clientid := client uid + - token + + Resonse: + + - 2.01 "Deleted" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" wrong clientid or token + + + + +### Heartbeat + +The Coap client can maintain the "connection" with the server through the heartbeat, +regardless of whether it is authenticated or not, +so that the server will not release related resources +Method : PUT +URI Schema: mqtt/connection{?q\*} +q\*: + +- clientid if authenticated +- token if authenticated + +Response: + +- 2.01 "Changed" when success +- 4.00 "Bad Request" when error +- 4.01 "Unauthorized" wrong clientid or token + + + + +### Query String + +CoAP gateway uses some options in query string to conversion between MQTT CoAP. + +1. Shared Options + + - clientid + - token + +2. Connect Options + + - username + - password + +3. Publish + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeDefault
retainbooleanfalse
qosMQTT QosSee here
expiryMessage Expiry Interval0(Never expiry)
+ +4. Subscribe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeDefault
qosMQTT QosSee here
nlMQTT Subscribe No Local0
rhMQTT Subscribe Retain Handing0
+ +5. MQTT Qos <=> CoAP non/con + + 1.notif_type + Control the type of notify messages when the observed object has changed.Can be: + + - non + - con + - qos + in this value, MQTT Qos0 -> non, Qos1/Qos2 -> con + + 2.subscribe_qos + Control the qos of subscribe.Can be: + + - qos0 + - qos1 + - qos2 + - coap + in this value, CoAP non -> qos0, con -> qos1 + + 3.publish_qos + like subscribe_qos, but control the qos of the publish MQTT message + + + + +## Implementation + + + + +### Request/Response flow + +![img](./doc/flow.png) + +1. Authorization check + + Check whether the clientid and token in the query string match the current connection + +2. Session + + Manager the "Transport Manager" "Observe Resouces Manger" and next message id + +3. Transport Mnager + + Manager "Transport" create/close/dispatch + +4. Observe resources Mnager + + Mnager observe topic and token + +5. Transport + + ![img](./doc/transport.png) + + 1. Shared State + + ![img](./doc/shared_state.png) + +6. Handler + + 1. pubsub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodObserveAction
GET0subscribe and reply result
GET1unsubscribe and reply result
POSTXpublish and reply result
+ + 2. mqtt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodAction
PUTreply result
POSTreturn create connection action
DELETEreturn close connection action
+ + + +## Example +1. Create Connection ``` -coap-client -m post coap://127.0.0.1?action=connect&clientid=XXX&username=XXX&password=XXX +coap-client -m post -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public" ``` -2. disconnect: +Server will return token **X** in payload + +2. Update Connection ``` -coap-client -m post coap://127.0.0.1?action=disconnect +coap-client -m put -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&token=X" ``` - +3. Publish +``` +coap-client -m post -e "Hellow" "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" +``` +if you want to publish with auth, you must first establish a connection, and then post publish request on the same socket, so libcoap client can't simulation publish with a token -# MQTT QOS <=> CoAP non/con +``` +coap-client -m post -e "Hellow" "coap://127.0.0.1/ps/coap/test?clientid=123&token=X" +``` -CoAP gateway uses some options to control the conversion between MQTT qos and coap non/con: +4. Subscribe +``` +coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" +``` +**Or** -1.notify_type -Control the type of notify messages when the observed object has changed.Can be: +``` +coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&token=X" +``` +5. Close Connection +``` +coap-client -m delete -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&token=X +``` -- non -- con -- qos - in this value, MQTT QOS0 -> non, QOS1/QOS2 -> con - -2.subscribe_qos -Control the qos of subscribe.Can be: - -- qos0 -- qos1 -- qos2 -- coap - in this value, CoAP non -> qos0, con -> qos1 - -3.publish_qos -like subscribe_qos, but control the qos of the publish MQTT message - -License -------- - -Apache License Version 2.0 - -Author ------- - -EMQ X Team. diff --git a/apps/emqx_gateway/src/coap/doc/flow.png b/apps/emqx_gateway/src/coap/doc/flow.png new file mode 100644 index 000000000..bb9b775a5 Binary files /dev/null and b/apps/emqx_gateway/src/coap/doc/flow.png differ diff --git a/apps/emqx_gateway/src/coap/doc/shared_state.png b/apps/emqx_gateway/src/coap/doc/shared_state.png new file mode 100644 index 000000000..2a7df229f Binary files /dev/null and b/apps/emqx_gateway/src/coap/doc/shared_state.png differ diff --git a/apps/emqx_gateway/src/coap/doc/transport.png b/apps/emqx_gateway/src/coap/doc/transport.png new file mode 100644 index 000000000..e63af691a Binary files /dev/null and b/apps/emqx_gateway/src/coap/doc/transport.png differ diff --git a/apps/emqx_gateway/src/coap/emqx_coap_api.erl b/apps/emqx_gateway/src/coap/emqx_coap_api.erl new file mode 100644 index 000000000..ab04269b8 --- /dev/null +++ b/apps/emqx_gateway/src/coap/emqx_coap_api.erl @@ -0,0 +1,145 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_coap_api). + +-behaviour(minirest_api). + +-include("emqx_coap.hrl"). + +%% API +-export([api_spec/0]). + +-export([request/2]). + +-define(PREFIX, "/gateway/coap/:clientid"). +-define(DEF_WAIT_TIME, 10). + +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/1 + , object_schema/2 + , error_schema/2 + , properties/1]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +api_spec() -> + {[request_api()], []}. + +request_api() -> + Metadata = #{post => request_method_meta()}, + {?PREFIX ++ "/request", Metadata, request}. + +request(post, #{body := Body, bindings := Bindings}) -> + ClientId = maps:get(clientid, Bindings, undefined), + + Method = maps:get(<<"method">>, Body, <<"get">>), + CT = maps:get(<<"content_type">>, Body, <<"text/plain">>), + Token = maps:get(<<"token">>, Body, <<>>), + Payload = maps:get(<<"payload">>, Body, <<>>), + WaitTime = maps:get(<<"timeout">>, Body, ?DEF_WAIT_TIME), + + Payload2 = parse_payload(CT, Payload), + ReqType = erlang:binary_to_atom(Method), + + Msg = emqx_coap_message:request(con, + ReqType, Payload2, #{content_format => CT}), + + Msg2 = Msg#coap_message{token = Token}, + + case call_client(ClientId, Msg2, timer:seconds(WaitTime)) of + timeout -> + {504, #{code => 'CLIENT_NOT_RESPONSE'}}; + not_found -> + {404, #{code => 'CLIENT_NOT_FOUND'}}; + Response -> + {200, format_to_response(CT, Response)} + end. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +request_parameters() -> + [#{name => clientid, + in => path, + schema => #{type => string}, + required => true}]. + +request_properties() -> + properties([ {token, string, "message token, can be empty"} + , {method, string, "request method type", ["get", "put", "post", "delete"]} + , {timeout, integer, "timespan for response"} + , {content_type, string, "payload type", + [<<"text/plain">>, <<"application/json">>, <<"application/octet-stream">>]} + , {payload, string, "payload"}]). + +coap_message_properties() -> + properties([ {id, integer, "message id"} + , {token, string, "message token, can be empty"} + , {method, string, "response code"} + , {payload, string, "payload"}]). + +request_method_meta() -> + #{description => <<"lookup matching messages">>, + parameters => request_parameters(), + 'requestBody' => object_schema(request_properties(), + <<"request payload, binary must encode by base64">>), + responses => #{ + <<"200">> => object_schema(coap_message_properties()), + <<"404">> => error_schema("client not found error", ['CLIENT_NOT_FOUND']), + <<"504">> => error_schema("timeout", ['CLIENT_NOT_RESPONSE']) + }}. + + +format_to_response(ContentType, #coap_message{id = Id, + token = Token, + method = Method, + payload = Payload}) -> + #{id => Id, + token => Token, + method => format_to_binary(Method), + payload => format_payload(ContentType, Payload)}. + +format_to_binary(Obj) -> + erlang:list_to_binary(io_lib:format("~p", [Obj])). + +format_payload(<<"application/octet-stream">>, Payload) -> + base64:encode(Payload); + +format_payload(_, Payload) -> + Payload. + +parse_payload(<<"application/octet-stream">>, Body) -> + base64:decode(Body); + +parse_payload(_, Body) -> + Body. + +call_client(ClientId, Msg, Timeout) -> + case emqx_gateway_cm_registry:lookup_channels(coap, ClientId) of + [Channel | _] -> + RequestId = emqx_coap_channel:send_request(Channel, Msg), + case gen_server:wait_response(RequestId, Timeout) of + {reply, Reply} -> + Reply; + _ -> + timeout + end; + _ -> + not_found + end. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 16afcf303..87698c9bc 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -22,16 +22,13 @@ -include("emqx_coap.hrl"). %% API --export([]). - -export([ info/1 , info/2 , stats/1 - , auth_publish/2 - , auth_subscribe/2 - , reply/4 - , ack/4 - , transfer_result/3]). + , validator/4 + , metrics_inc/2 + , run_hooks/3 + , send_request/2]). -export([ init/2 , handle_in/2 @@ -40,7 +37,7 @@ , terminate/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -60,13 +57,21 @@ keepalive :: emqx_keepalive:keepalive() | undefined, %% Timer timers :: #{atom() => disable | undefined | reference()}, - config :: hocon:config() + + connection_required :: boolean(), + + conn_state :: idle | connected | disconnected, + + token :: binary() | undefined }). -type channel() :: #channel{}. --define(DISCONNECT_WAIT_TIME, timer:seconds(10)). +-define(TOKEN_MAXIMUM, 4294967295). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). +-define(DEF_IDLE_TIME, timer:seconds(30)). +-define(GET_IDLE_TIME(Cfg), maps:get(idle_timeout, Cfg, ?DEF_IDLE_TIME)). +-import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- @@ -79,8 +84,8 @@ info(Keys, Channel) when is_list(Keys) -> info(conninfo, #channel{conninfo = ConnInfo}) -> ConnInfo; -info(conn_state, _) -> - connected; +info(conn_state, #channel{conn_state = CState}) -> + CState; info(clientinfo, #channel{clientinfo = ClientInfo}) -> ClientInfo; info(session, #channel{session = Session}) -> @@ -97,7 +102,7 @@ init(ConnInfo = #{peername := {PeerHost, _}, sockname := {_, SockPort}}, #{ctx := Ctx} = Config) -> Peercert = maps:get(peercert, ConnInfo, undefined), - Mountpoint = maps:get(mountpoint, Config, undefined), + Mountpoint = maps:get(mountpoint, Config, <<>>), ClientInfo = set_peercert_infos( Peercert, #{ zone => default @@ -112,52 +117,42 @@ init(ConnInfo = #{peername := {PeerHost, _}, } ), + Heartbeat = ?GET_IDLE_TIME(Config), #channel{ ctx = Ctx , conninfo = ConnInfo , clientinfo = ClientInfo , timers = #{} , session = emqx_coap_session:new() - , config = Config#{clientinfo => ClientInfo, - ctx => Ctx} - , keepalive = emqx_keepalive:init(maps:get(heartbeat, Config)) + , keepalive = emqx_keepalive:init(Heartbeat) + , connection_required = maps:get(connection_required, Config, false) + , conn_state = idle }. -auth_publish(Topic, - #{ctx := Ctx, - clientinfo := ClientInfo}) -> - emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic). +validator(Type, Topic, Ctx, ClientInfo) -> + emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). -auth_subscribe(Topic, - #{ctx := Ctx, - clientinfo := ClientInfo}) -> - emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic). - -transfer_result(From, Value, Result) -> - ?TRANSFER_RESULT([out], From, Value, Result). +-spec send_request(pid(), emqx_coap_message()) -> any(). +send_request(Channel, Request) -> + gen_server:send_request(Channel, {?FUNCTION_NAME, Request}). %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- -%% treat post to root path as a heartbeat -%% treat post to root path with query string as a command -handle_in(#coap_message{method = post, - options = Options} = Msg, ChannelT) -> - Channel = ensure_keepalive_timer(ChannelT), - case maps:get(uri_path, Options, <<>>) of - <<>> -> - handle_command(Msg, Channel); +handle_in(Msg, ChannleT) -> + Channel = ensure_keepalive_timer(ChannleT), + case emqx_coap_message:is_request(Msg) of + true -> + check_auth_state(Msg, Channel); _ -> - call_session(received, [Msg], Channel) - end; - -handle_in(Msg, Channel) -> - call_session(received, [Msg], ensure_keepalive_timer(Channel)). + call_session(handle_response, Msg, Channel) + end. %%-------------------------------------------------------------------- %% Handle Delivers from broker to client %%-------------------------------------------------------------------- -handle_deliver(Delivers, Channel) -> - call_session(deliver, [Delivers], Channel). +handle_deliver(Delivers, #channel{session = Session, + ctx = Ctx} = Channel) -> + handle_result(emqx_coap_session:deliver(Delivers, Ctx, Session), Channel). %%-------------------------------------------------------------------- %% Handle timeout @@ -168,11 +163,11 @@ handle_timeout(_, {keepalive, NewVal}, #channel{keepalive = KeepAlive} = Channel Channel2 = ensure_keepalive_timer(fun make_timer/4, Channel), {ok, Channel2#channel{keepalive = NewKeepAlive}}; {error, timeout} -> - {shutdown, timeout, Channel} + {shutdown, timeout, ensure_disconnected(keepalive_timeout, Channel)} end; handle_timeout(_, {transport, Msg}, Channel) -> - call_session(timeout, [Msg], Channel); + call_session(timeout, Msg, Channel); handle_timeout(_, disconnect, Channel) -> {shutdown, normal, Channel}; @@ -183,7 +178,11 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- -handle_call(Req, Channel) -> +handle_call({send_request, Msg}, From, Channel) -> + Result = call_session(handle_out, {{send_request, From}, Msg}, Channel), + erlang:setelement(1, Result, noreply); + +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. @@ -197,6 +196,9 @@ handle_cast(Req, Channel) -> %%-------------------------------------------------------------------- %% Handle Info %%-------------------------------------------------------------------- +handle_info({subscribe, _}, Channel) -> + {ok, Channel}; + handle_info(Info, Channel) -> ?LOG(error, "Unexpected info: ~p", [Info]), {ok, Channel}. @@ -204,8 +206,10 @@ handle_info(Info, Channel) -> %%-------------------------------------------------------------------- %% Terminate %%-------------------------------------------------------------------- -terminate(_Reason, _Channel) -> - ok. +terminate(Reason, #channel{clientinfo = ClientInfo, + ctx = Ctx, + session = Session}) -> + run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]). %%-------------------------------------------------------------------- %% Internal functions @@ -234,52 +238,65 @@ make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) -> ensure_keepalive_timer(Channel) -> ensure_keepalive_timer(fun ensure_timer/4, Channel). -ensure_keepalive_timer(Fun, #channel{config = Cfg} = Channel) -> - Interval = maps:get(heartbeat, Cfg), - Fun(keepalive, Interval, keepalive, Channel). +ensure_keepalive_timer(Fun, #channel{keepalive = KeepAlive} = Channel) -> + Heartbeat = emqx_keepalive:info(interval, KeepAlive), + Fun(keepalive, Heartbeat, keepalive, Channel). -handle_command(#coap_message{options = Options} = Msg, Channel) -> - case maps:get(uri_query, Options, []) of - [] -> - %% heartbeat - ack(Channel, {ok, valid}, <<>>, Msg); - QueryPairs -> - Queries = lists:foldl(fun(Pair, Acc) -> - [{K, V}] = cow_qs:parse_qs(Pair), - Acc#{K => V} - end, - #{}, - QueryPairs), - case maps:get(<<"action">>, Queries, undefined) of - undefined -> - ack(Channel, {error, bad_request}, <<"command without actions">>, Msg); - Action -> - handle_command(Action, Queries, Msg, Channel) - end - end. +check_auth_state(Msg, #channel{connection_required = Required} = Channel) -> + check_token(Required, Msg, Channel). -handle_command(<<"connect">>, Queries, Msg, Channel) -> - case emqx_misc:pipeline( - [ fun run_conn_hooks/2 - , fun enrich_clientinfo/2 - , fun set_log_meta/2 - , fun auth_connect/2 - ], - {Queries, Msg}, - Channel) of - {ok, _Input, NChannel} -> - process_connect(ensure_connected(NChannel), Msg); - {error, ReasonCode, NChannel} -> - ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), - ack(NChannel, {error, bad_request}, ErrMsg, Msg) +check_token(true, + Msg, + #channel{token = Token, + clientinfo = ClientInfo, + conn_state = CState} = Channel) -> + #{clientid := ClientId} = ClientInfo, + case emqx_coap_message:get_option(uri_query, Msg) of + #{<<"clientid">> := ClientId, + <<"token">> := Token} -> + call_session(handle_request, Msg, Channel); + #{<<"clientid">> := DesireId} -> + try_takeover(CState, DesireId, Msg, Channel); + _ -> + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Channel} end; -handle_command(<<"disconnect">>, _, Msg, Channel) -> - Channel2 = ensure_timer(disconnect, ?DISCONNECT_WAIT_TIME, disconnect, Channel), - ack(Channel2, {ok, deleted}, <<>>, Msg); +check_token(false, Msg, Channel) -> + case emqx_coap_message:get_option(uri_query, Msg) of + #{<<"clientid">> := _} -> + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Channel}; + #{<<"token">> := _} -> + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Channel}; + _ -> + call_session(handle_request, Msg, Channel) + end. -handle_command(_, _, Msg, Channel) -> - ack(Channel, {error, bad_request}, <<"invalid action">>, Msg). +try_takeover(idle, DesireId, Msg, Channel) -> + case emqx_coap_message:get_option(uri_path, Msg, []) of + [<<"mqtt">>, <<"connection">> | _] -> + %% may be is a connect request + %% TODO need check repeat connect, unless we implement the + %% udp connection baseon the clientid + call_session(handle_request, Msg, Channel); + _ -> + case emqx:get_config([gateway, coap, authentication], undefined) of + undefined -> + call_session(handle_request, Msg, Channel); + _ -> + do_takeover(DesireId, Msg, Channel) + end + end; + +try_takeover(_, DesireId, Msg, Channel) -> + do_takeover(DesireId, Msg, Channel). + +do_takeover(_DesireId, Msg, Channel) -> + %% TODO completed the takeover, now only reset the message + Reset = emqx_coap_message:reset(Msg), + {ok, {outgoing, Reset}, Channel}. run_conn_hooks(Input, Channel = #channel{ctx = Ctx, conninfo = ConnInfo}) -> @@ -291,8 +308,7 @@ run_conn_hooks(Input, Channel = #channel{ctx = Ctx, end. enrich_clientinfo({Queries, Msg}, - Channel = #channel{clientinfo = ClientInfo0, - config = Cfg}) -> + Channel = #channel{clientinfo = ClientInfo0}) -> case Queries of #{<<"username">> := UserName, <<"password">> := Password, @@ -301,8 +317,7 @@ enrich_clientinfo({Queries, Msg}, password => Password, clientid => ClientId}, {ok, NClientInfo} = fix_mountpoint(Msg, ClientInfo), - {ok, Channel#channel{clientinfo = NClientInfo, - config = Cfg#{clientinfo := NClientInfo}}}; + {ok, Channel#channel{clientinfo = NClientInfo}}; _ -> {error, "invalid queries", Channel} end. @@ -324,37 +339,47 @@ auth_connect(_Input, Channel = #channel{ctx = Ctx, {error, Reason} end. -fix_mountpoint(_Packet, #{mountpoint := undefined}) -> ok; +fix_mountpoint(_Packet, #{mountpoint := <<>>} = ClientInfo) -> + {ok, ClientInfo}; fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> - %% TODO: Enrich the varibale replacement???? - %% i.e: ${ClientInfo.auth_result.productKey} Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo), {ok, ClientInfo#{mountpoint := Mountpoint1}}. ensure_connected(Channel = #channel{ctx = Ctx, conninfo = ConnInfo, clientinfo = ClientInfo}) -> - NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)}, + NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond) + , proto_name => <<"COAP">> + , proto_ver => <<"1">> + }, ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]), + _ = run_hooks(Ctx, 'client.connack', [NConnInfo, connection_accepted, []]), Channel#channel{conninfo = NConnInfo}. -process_connect(Channel = #channel{ctx = Ctx, - conninfo = ConnInfo, - clientinfo = ClientInfo}, - Msg) -> - SessFun = fun(_,_) -> emqx_coap_session:new() end, +process_connect(#channel{ctx = Ctx, + session = Session, + conninfo = ConnInfo, + clientinfo = ClientInfo} = Channel, + Msg, Result, Iter) -> + %% inherit the old session + SessFun = fun(_,_) -> Session end, case emqx_gateway_ctx:open_session( Ctx, true, ClientInfo, ConnInfo, - SessFun + SessFun, + emqx_coap_session ) of {ok, _Sess} -> - ack(Channel, {ok, created}, <<"connected">>, Msg); + RandVal = rand:uniform(?TOKEN_MAXIMUM), + Token = erlang:list_to_binary(erlang:integer_to_list(RandVal)), + iter(Iter, + reply({ok, created}, Token, Msg, Result), + Channel#channel{token = Token}); {error, Reason} -> ?LOG(error, "Failed to open session du to ~p", [Reason]), - ack(Channel, {error, bad_request}, <<>>, Msg) + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) end. run_hooks(Ctx, Name, Args) -> @@ -365,24 +390,110 @@ run_hooks(Ctx, Name, Args, Acc) -> emqx_gateway_ctx:metrics_inc(Ctx, Name), emqx_hooks:run_fold(Name, Args, Acc). -reply(Channel, Method, Payload, Req) -> - call_session(reply, [Req, Method, Payload], Channel). +metrics_inc(Name, Ctx) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name). -ack(Channel, Method, Payload, Req) -> - call_session(piggyback, [Req, Method, Payload], Channel). +ensure_disconnected(Reason, Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)}, + ok = run_hooks(Ctx, 'client.disconnected', [ClientInfo, Reason, NConnInfo]), + Channel#channel{conninfo = NConnInfo, conn_state = disconnected}. -call_session(F, - A, - #channel{session = Session, - config = Cfg} = Channel) -> - case erlang:apply(emqx_coap_session, F, A ++ [Cfg, Session]) of - #{out := Out, - session := Session2} -> - {ok, {outgoing, Out}, Channel#channel{session = Session2}}; - #{out := Out} -> - {ok, {outgoing, Out}, Channel}; - #{session := Session2} -> - {ok, Channel#channel{session = Session2}}; - _ -> - {ok, Channel} - end. +%%-------------------------------------------------------------------- +%% Call Chain +%%-------------------------------------------------------------------- +call_session(Fun, Msg, #channel{session = Session} = Channel) -> + Result = emqx_coap_session:Fun(Msg, Session), + handle_result(Result, Channel). + +handle_result(Result, Channel) -> + iter([ session, fun process_session/4 + , proto, fun process_protocol/4 + , reply, fun process_reply/4 + , out, fun process_out/4 + , fun process_nothing/3 + ], + Result, + Channel). + +call_handler(request, Msg, Result, + #channel{ctx = Ctx, + clientinfo = ClientInfo} = Channel, Iter) -> + HandlerResult = + case emqx_coap_message:get_option(uri_path, Msg) of + [<<"ps">> | RestPath] -> + emqx_coap_pubsub_handler:handle_request(RestPath, Msg, Ctx, ClientInfo); + [<<"mqtt">> | RestPath] -> + emqx_coap_mqtt_handler:handle_request(RestPath, Msg, Ctx, ClientInfo); + _ -> + reply({error, bad_request}, Msg) + end, + iter([ connection, fun process_connection/4 + , subscribe, fun process_subscribe/4 | Iter], + maps:merge(Result, HandlerResult), + Channel); + +call_handler(response, {{send_request, From}, Response}, Result, Channel, Iter) -> + gen_server:reply(From, Response), + iter(Iter, Result, Channel); + +call_handler(_, _, Result, Channel, Iter) -> + iter(Iter, Result, Channel). + +process_session(Session, Result, Channel, Iter) -> + iter(Iter, Result, Channel#channel{session = Session}). + +process_protocol({Type, Msg}, Result, Channel, Iter) -> + call_handler(Type, Msg, Result, Channel, Iter). + +%% leaf node +process_out(Outs, Result, Channel, _) -> + Outs2 = lists:reverse(Outs), + Outs3 = case maps:get(reply, Result, undefined) of + undefined -> + Outs2; + Reply -> + [Reply | Outs2] + end, + {ok, {outgoing, Outs3}, Channel}. + +%% leaf node +process_nothing(_, _, Channel) -> + {ok, Channel}. + +process_connection({open, Req}, Result, Channel, Iter) -> + Queries = emqx_coap_message:get_option(uri_query, Req), + case emqx_misc:pipeline( + [ fun run_conn_hooks/2 + , fun enrich_clientinfo/2 + , fun set_log_meta/2 + , fun auth_connect/2 + ], + {Queries, Req}, + Channel) of + {ok, _Input, NChannel} -> + process_connect(ensure_connected(NChannel), Req, Result, Iter); + {error, ReasonCode, NChannel} -> + ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), + Payload = erlang:list_to_binary(lists:flatten(ErrMsg)), + iter(Iter, + reply({error, bad_request}, Payload, Req, Result), + NChannel) + end; + +process_connection({close, Msg}, _, Channel, _) -> + Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), + {shutdown, close, Reply, Channel}. + +process_subscribe({Sub, Msg}, Result, #channel{session = Session} = Channel, Iter) -> + Result2 = emqx_coap_session:process_subscribe(Sub, Msg, Result, Session), + iter([session, fun process_session/4 | Iter], Result2, Channel). + +%% leaf node +process_reply(Reply, Result, #channel{session = Session} = Channel, _) -> + Session2 = emqx_coap_session:set_reply(Reply, Session), + Outs = maps:get(out, Result, []), + Outs2 = lists:reverse(Outs), + {ok, {outgoing, [Reply | Outs2]}, Channel#channel{session = Session2}}. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_frame.erl b/apps/emqx_gateway/src/coap/emqx_coap_frame.erl index a5327f239..7250ec872 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_frame.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_frame.erl @@ -103,11 +103,7 @@ flatten_options([{OptId, OptVal} | T], Acc) -> false -> [encode_option(OptId, OptVal) | Acc]; _ -> - lists:foldl(fun(undefined, InnerAcc) -> - InnerAcc; - (E, InnerAcc) -> - [encode_option(OptId, E) | InnerAcc] - end, Acc, OptVal) + try_encode_repeatable(OptId, OptVal) ++ Acc end); flatten_options([], Acc) -> @@ -141,6 +137,19 @@ encode_option_list([], _LastNum, Acc, <<>>) -> encode_option_list([], _, Acc, Payload) -> <>. +try_encode_repeatable(uri_query, Val) when is_map(Val) -> + maps:fold(fun(K, V, Acc) -> + [encode_option(uri_query, <>) | Acc] + end, + [], Val); + +try_encode_repeatable(K, Val) -> + lists:foldr(fun(undefined, Acc) -> + Acc; + (E, Acc) -> + [encode_option(K, E) | Acc] + end, [], Val). + %% RFC 7252 encode_option(if_match, OptVal) -> {?OPTION_IF_MATCH, OptVal}; encode_option(uri_host, OptVal) -> {?OPTION_URI_HOST, OptVal}; @@ -161,9 +170,7 @@ encode_option(location_query, OptVal) -> {?OPTION_LOCATION_QUERY, OptVal}; encode_option(proxy_uri, OptVal) -> {?OPTION_PROXY_URI, OptVal}; encode_option(proxy_scheme, OptVal) -> {?OPTION_PROXY_SCHEME, OptVal}; encode_option(size1, OptVal) -> {?OPTION_SIZE1, binary:encode_unsigned(OptVal)}; -%% draft-ietf-ore-observe-16 encode_option(observe, OptVal) -> {?OPTION_OBSERVE, binary:encode_unsigned(OptVal)}; -%% draft-ietf-ore-block-17 encode_option(block2, OptVal) -> {?OPTION_BLOCK2, encode_block(OptVal)}; encode_option(block1, OptVal) -> {?OPTION_BLOCK1, encode_block(OptVal)}; %% unknown opton @@ -190,6 +197,8 @@ content_format_to_code(<<"application/octet-stream">>) -> 42; content_format_to_code(<<"application/exi">>) -> 47; content_format_to_code(<<"application/json">>) -> 50; content_format_to_code(<<"application/cbor">>) -> 60; +content_format_to_code(<<"application/vnd.oma.lwm2m+tlv">>) -> 11542; +content_format_to_code(<<"application/vnd.oma.lwm2m+json">>) -> 11543; content_format_to_code(_) -> 42. %% use octet-stream as default method_to_class_code(get) -> {0, 01}; @@ -237,12 +246,7 @@ parse(< {Options, Payload} = decode_option_list(Tail), Options2 = maps:fold(fun(K, V, Acc) -> - case is_repeatable_option(K) of - true -> - Acc#{K => lists:reverse(V)}; - _ -> - Acc#{K => V} - end + Acc#{K => get_option_val(K, V)} end, #{}, Options), @@ -257,6 +261,24 @@ parse(<>, ParseState}. +get_option_val(uri_query, V) -> + KVList = lists:foldl(fun(E, Acc) -> + [Key, Val] = re:split(E, "="), + [{Key, Val} | Acc] + + end, + [], + V), + maps:from_list(KVList); + +get_option_val(K, V) -> + case is_repeatable_option(K) of + true -> + lists:reverse(V); + _ -> + V + end. + -spec decode_type(X) -> message_type() when X :: 0 .. 3. decode_type(0) -> con; @@ -361,6 +383,8 @@ content_code_to_format(42) -> <<"application/octet-stream">>; content_code_to_format(47) -> <<"application/exi">>; content_code_to_format(50) -> <<"application/json">>; content_code_to_format(60) -> <<"application/cbor">>; +content_code_to_format(11542) -> <<"application/vnd.oma.lwm2m+tlv">>; +content_code_to_format(11543) -> <<"application/vnd.oma.lwm2m+json">>; content_code_to_format(_) -> <<"application/octet-stream">>. %% use octet as default %% RFC 7252 diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index 6d27cd85a..055eab759 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -21,101 +21,85 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). -include_lib("emqx/include/logger.hrl"). --dialyzer({nowarn_function, [load/0]}). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -load() -> +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - Options = [], - emqx_gateway_registry:load(coap, RegistryOptions, Options). + emqx_gateway_registry:reg(coap, RegistryOptions). -unload() -> - emqx_gateway_registry:unload(coap). - -init([]) -> - GwState = #{}, - {ok, GwState}. +unreg() -> + emqx_gateway_registry:unreg(coap). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_insta_create(_Insta = #{id := InstaId, - rawconf := #{resource := Resource} = RawConf - }, Ctx, _GwState) -> - ResourceMod = get_resource_mod(Resource), - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), +on_gateway_load(_Gateway = #{name := GwName, + config := Config + }, Ctx) -> + Listeners = emqx_gateway_utils:normalize_config(Config), ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, ResourceMod, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), - {ok, ResCtx} = ResourceMod:init(RawConf), - {ok, ListenerPids, #{ctx => Ctx, - res_ctx => ResCtx}}. + {ok, ListenerPids, #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, Gateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? - on_insta_destroy(OldInsta, GwInstaState, GwState), - on_insta_create(NewInsta, Ctx, GwState) + on_gateway_unload(Gateway, GwState), + on_gateway_load(Gateway#{config => Config}, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update coap instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := #{resource := Resource} = RawConf - }, - #{res_ctx := ResCtx} = _GwInstaState, - _GWState) -> - ResourceMod = get_resource_mod(Resource), - ok = ResourceMod:stop(ResCtx), - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), +on_gateway_unload(_Gateway = #{ name := GwName, + config := Config + }, _GwState) -> + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) - end, Listeners). + stop_listener(GwName, Lis) + end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, ResourceMod, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - Cfg2 = Cfg#{resource => ResourceMod}, - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg2) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start coap ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start coap ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, Type), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_coap_frame, @@ -130,26 +114,18 @@ do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). - -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop coap ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop coap ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). - -get_resource_mod(mqtt) -> - emqx_coap_mqtt_resource; -get_resource_mod(pubsub) -> - emqx_coap_pubsub_resource. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_medium.erl b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl new file mode 100644 index 000000000..e56ba0322 --- /dev/null +++ b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl @@ -0,0 +1,109 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +%% Simplified semi-automatic CPS mode tree for coap +%% The tree must have a terminal leaf node, and it's return is the result of the entire tree. +%% This module currently only supports simple linear operation + +-module(emqx_coap_medium). + +-include("emqx_coap.hrl"). + +%% API +-export([ empty/0, reset/1, reset/2 + , out/1, out/2, proto_out/1 + , proto_out/2, iter/3, iter/4 + , reply/2, reply/3, reply/4]). + +%%-type result() :: map() | empty. + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +empty() -> #{}. + +reset(Msg) -> + reset(Msg, #{}). + +reset(Msg, Result) -> + out(emqx_coap_message:reset(Msg), Result). + +out(Msg) -> + #{out => [Msg]}. + +out(Msg, #{out := Outs} = Result) -> + Result#{out := [Msg | Outs]}; + +out(Msg, Result) -> + Result#{out => [Msg]}. + +proto_out(Proto) -> + proto_out(Proto, #{}). + +proto_out(Proto, Resut) -> + Resut#{proto => Proto}. + +reply(Method, Req) when not is_record(Method, coap_message) -> + reply(Method, <<>>, Req); + +reply(Reply, Result) -> + Result#{reply => Reply}. + +reply(Method, Req, Result) when is_record(Req, coap_message) -> + reply(Method, <<>>, Req, Result); + +reply(Method, Payload, Req) -> + reply(Method, Payload, Req, #{}). + +reply(Method, Payload, Req, Result) -> + Result#{reply => emqx_coap_message:piggyback(Method, Payload, Req)}. + +%% run a tree +iter([Key, Fun | T], Input, State) -> + case maps:get(Key, Input, undefined) of + undefined -> + iter(T, Input, State); + Val -> + Fun(Val, maps:remove(Key, Input), State, T) + %% reserved + %% if is_function(Fun) -> + %% Fun(Val, maps:remove(Key, Input), State, T); + %% true -> + %% %% switch to sub branch + %% [FunH | FunT] = Fun, + %% FunH(Val, maps:remove(Key, Input), State, FunT) + %% end + end; + +%% terminal node +iter([Fun], Input, State) -> + Fun(undefined, Input, State). + +%% run a tree with argument +iter([Key, Fun | T], Input, Arg, State) -> + case maps:get(Key, Input, undefined) of + undefined -> + iter(T, Input, Arg, State); + Val -> + Fun(Val, maps:remove(Key, Input), Arg, State, T) + end; + +iter([Fun], Input, Arg, State) -> + Fun(undefined, Input, Arg, State). + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/coap/emqx_coap_message.erl b/apps/emqx_gateway/src/coap/emqx_coap_message.erl index 64c019aec..93f15fb6d 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_message.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_message.erl @@ -24,8 +24,15 @@ %% convenience functions for message construction -module(emqx_coap_message). --export([request/2, request/3, request/4, ack/1, response/1, response/2, response/3]). --export([set/3, set_payload/2, get_content/1, set_content/2, set_content/3, get_option/2]). +-export([ request/2, request/3, request/4 + , ack/1, response/1, response/2 + , reset/1, piggyback/2, piggyback/3 + , response/3]). + +-export([is_request/1]). + +-export([ set/3, set_payload/2, get_option/2 + , get_option/3, set_payload_block/3, set_payload_block/4]). -include("emqx_coap.hrl"). @@ -36,30 +43,45 @@ request(Type, Method, Payload) -> request(Type, Method, Payload, []). request(Type, Method, Payload, Options) when is_binary(Payload) -> - #coap_message{type = Type, method = Method, payload = Payload, options = Options}; - -request(Type, Method, Content=#coap_content{}, Options) -> - set_content(Content, - #coap_message{type = Type, method = Method, options = Options}). - -ack(Request = #coap_message{}) -> - #coap_message{type = ack, - id = Request#coap_message.id}. - -response(#coap_message{type = Type, - id = Id, - token = Token}) -> #coap_message{type = Type, - id = Id, - token = Token}. + method = Method, + payload = Payload, + options = to_options(Options)}. + +ack(#coap_message{id = Id}) -> + #coap_message{type = ack, id = Id}. + +reset(#coap_message{id = Id}) -> + #coap_message{type = reset, id = Id}. + +%% just make a response +response(Request) -> + response(undefined, Request). response(Method, Request) -> - set_method(Method, response(Request)). + response(Method, <<>>, Request). -response(Method, Payload, Request) -> - set_method(Method, - set_payload(Payload, - response(Request))). +response(Method, Payload, #coap_message{type = Type, + id = Id, + token = Token}) -> + #coap_message{type = Type, + id = Id, + token = Token, + method = Method, + payload = Payload}. + +%% make a response which maybe is a piggyback ack +piggyback(Method, Request) -> + piggyback(Method, <<>>, Request). + +piggyback(Method, Payload, Request) -> + Reply = response(Method, Payload, Request), + case Reply of + #coap_message{type = con} -> + Reply#coap_message{type = ack}; + _ -> + Reply + end. %% omit option for its default value set(max_age, ?DEFAULT_MAX_AGE, Msg) -> Msg; @@ -68,14 +90,11 @@ set(max_age, ?DEFAULT_MAX_AGE, Msg) -> Msg; set(Option, Value, Msg = #coap_message{options = Options}) -> Msg#coap_message{options = Options#{Option => Value}}. -get_option(Option, #coap_message{options = Options}) -> - maps:get(Option, Options, undefined). +get_option(Option, Msg) -> + get_option(Option, Msg, undefined). -set_method(Method, Msg) -> - Msg#coap_message{method = Method}. - -set_payload(Payload = #coap_content{}, Msg) -> - set_content(Payload, undefined, Msg); +get_option(Option, #coap_message{options = Options}, Def) -> + maps:get(Option, Options, Def). set_payload(Payload, Msg) when is_binary(Payload) -> Msg#coap_message{payload = Payload}; @@ -83,49 +102,6 @@ set_payload(Payload, Msg) when is_binary(Payload) -> set_payload(Payload, Msg) when is_list(Payload) -> Msg#coap_message{payload = list_to_binary(Payload)}. -get_content(#coap_message{options = Options, payload = Payload}) -> - #coap_content{etag = maps:get(etag, Options, undefined), - max_age = maps:get(max_age, Options, ?DEFAULT_MAX_AGE), - format = maps:get(content_format, Options, undefined), - location_path = maps:get(location_path, Options, []), - payload = Payload}. - -set_content(Content, Msg) -> - set_content(Content, undefined, Msg). - -%% segmentation not requested and not required -set_content(#coap_content{etag = ETag, - max_age = MaxAge, - format = Format, - location_path = LocPath, - payload = Payload}, - undefined, - Msg) - when byte_size(Payload) =< ?MAX_BLOCK_SIZE -> - #coap_message{options = Options} = Msg2 = set_payload(Payload, Msg), - Options2 = Options#{etag => [ETag], - max_age => MaxAge, - content_format => Format, - location_path => LocPath}, - Msg2#coap_message{options = Options2}; - -%% segmentation not requested, but required (late negotiation) -set_content(Content, undefined, Msg) -> - set_content(Content, {0, true, ?MAX_BLOCK_SIZE}, Msg); - -%% segmentation requested (early negotiation) -set_content(#coap_content{etag = ETag, - max_age = MaxAge, - format = Format, - payload = Payload}, - Block, - Msg) -> - #coap_message{options = Options} = Msg2 = set_payload_block(Payload, Block, Msg), - Options2 = Options#{etag => [ETag], - max => MaxAge, - content_format => Format}, - Msg2#coap_message{options = Options2}. - set_payload_block(Content, Block, Msg = #coap_message{method = Method}) when is_atom(Method) -> set_payload_block(Content, block1, Block, Msg); @@ -144,3 +120,14 @@ set_payload_block(Content, BlockId, {Num, _, Size}, Msg) -> set(BlockId, {Num, false, Size}, set_payload(binary:part(Content, OffsetBegin, ContentSize - OffsetBegin), Msg)) end. + +is_request(#coap_message{method = Method}) when is_atom(Method) -> + Method =/= undefined; + +is_request(_) -> + false. + +to_options(Opts) when is_map(Opts) -> + Opts; +to_options(Opts) -> + maps:from_list(Opts). diff --git a/apps/emqx_gateway/src/coap/emqx_coap_observe_res.erl b/apps/emqx_gateway/src/coap/emqx_coap_observe_res.erl index 3cf925448..20473322e 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_observe_res.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_observe_res.erl @@ -18,7 +18,7 @@ %% API -export([ new_manager/0, insert/3, remove/2 - , res_changed/2, foreach/2]). + , res_changed/2, foreach/2, subscriptions/1]). -export_type([manager/0]). -define(MAX_SEQ_ID, 16777215). @@ -40,14 +40,15 @@ new_manager() -> #{}. --spec insert(topic(), token(), manager()) -> manager(). +-spec insert(topic(), token(), manager()) -> {seq_id(), manager()}. insert(Topic, Token, Manager) -> - case maps:get(Topic, Manager, undefined) of - undefined -> - Manager#{Topic => new_res(Token)}; - _ -> - Manager - end. + Res = case maps:get(Topic, Manager, undefined) of + undefined -> + new_res(Token); + Any -> + Any + end, + {maps:get(seq_id, Res), Manager#{Topic => Res}}. -spec remove(topic(), manager()) -> manager(). remove(Topic, Manager) -> @@ -72,6 +73,9 @@ foreach(F, Manager) -> Manager), ok. +subscriptions(Manager) -> + maps:keys(Manager). + %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/coap/emqx_coap_session.erl b/apps/emqx_gateway/src/coap/emqx_coap_session.erl index 7543d787f..cbfe0e748 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_session.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_session.erl @@ -21,25 +21,53 @@ -include("emqx_coap.hrl"). %% API --export([new/0, transfer_result/3]). +-export([ new/0 + , process_subscribe/4]). --export([ received/3 - , reply/4 - , reply/5 - , ack/3 - , piggyback/4 +-export([ info/1 + , info/2 + , stats/1 + ]). + +-export([ handle_request/2 + , handle_response/2 + , handle_out/2 + , set_reply/2 , deliver/3 - , timeout/3]). + , timeout/2]). -export_type([session/0]). -record(session, { transport_manager :: emqx_coap_tm:manager() , observe_manager :: emqx_coap_observe_res:manager() - , next_msg_id :: coap_message_id() + , created_at :: pos_integer() }). -type session() :: #session{}. +%% steal from emqx_session +-define(INFO_KEYS, [subscriptions, + upgrade_qos, + retry_interval, + await_rel_timeout, + created_at + ]). + +-define(STATS_KEYS, [subscriptions_cnt, + subscriptions_max, + inflight_cnt, + inflight_max, + mqueue_len, + mqueue_max, + mqueue_dropped, + next_pkt_id, + awaiting_rel_cnt, + awaiting_rel_max + ]). + +-import(emqx_coap_medium, [iter/3]). +-import(emqx_coap_channel, [metrics_inc/2]). + %%%------------------------------------------------------------------- %%% API %%%------------------------------------------------------------------- @@ -48,125 +76,147 @@ new() -> _ = emqx_misc:rand_seed(), #session{ transport_manager = emqx_coap_tm:new() , observe_manager = emqx_coap_observe_res:new_manager() - , next_msg_id = rand:uniform(?MAX_MESSAGE_ID)}. + , created_at = erlang:system_time(millisecond)}. + +%%-------------------------------------------------------------------- +%% Info, Stats +%%-------------------------------------------------------------------- +%% @doc Compatible with emqx_session +%% do we need use inflight and mqueue in here? +-spec(info(session()) -> emqx_types:infos()). +info(Session) -> + maps:from_list(info(?INFO_KEYS, Session)). + +info(Keys, Session) when is_list(Keys) -> + [{Key, info(Key, Session)} || Key <- Keys]; +info(subscriptions, #session{observe_manager = OM}) -> + emqx_coap_observe_res:subscriptions(OM); +info(subscriptions_cnt, #session{observe_manager = OM}) -> + erlang:length(emqx_coap_observe_res:subscriptions(OM)); +info(subscriptions_max, _) -> + infinity; +info(upgrade_qos, _) -> + ?QOS_0; +info(inflight, _) -> + emqx_inflight:new(); +info(inflight_cnt, _) -> + 0; +info(inflight_max, _) -> + 0; +info(retry_interval, _) -> + infinity; +info(mqueue, _) -> + emqx_mqueue:init(#{max_len => 0, store_qos0 => false}); +info(mqueue_len, #session{transport_manager = TM}) -> + maps:size(TM); +info(mqueue_max, _) -> + 0; +info(mqueue_dropped, _) -> + 0; +info(next_pkt_id, _) -> + 0; +info(awaiting_rel, _) -> + #{}; +info(awaiting_rel_cnt, _) -> + 0; +info(awaiting_rel_max, _) -> + infinity; +info(await_rel_timeout, _) -> + infinity; +info(created_at, #session{created_at = CreatedAt}) -> + CreatedAt. + +%% @doc Get stats of the session. +-spec(stats(session()) -> emqx_types:stats()). +stats(Session) -> info(?STATS_KEYS, Session). %%%------------------------------------------------------------------- %%% Process Message %%%------------------------------------------------------------------- -received(#coap_message{type = ack} = Msg, Cfg, Session) -> - handle_response(Msg, Cfg, Session); +handle_request(Msg, Session) -> + call_transport_manager(?FUNCTION_NAME, + Msg, + Session). -received(#coap_message{type = reset} = Msg, Cfg, Session) -> - handle_response(Msg, Cfg, Session); +handle_response(Msg, Session) -> + call_transport_manager(?FUNCTION_NAME, Msg, Session). -received(#coap_message{method = Method} = Msg, Cfg, Session) when is_atom(Method) -> - handle_request(Msg, Cfg, Session); +handle_out(Msg, Session) -> + call_transport_manager(?FUNCTION_NAME, Msg, Session). -received(Msg, Cfg, Session) -> - handle_response(Msg, Cfg, Session). +set_reply(Msg, #session{transport_manager = TM} = Session) -> + TM2 = emqx_coap_tm:set_reply(Msg, TM), + Session#session{transport_manager = TM2}. -reply(Req, Method, Cfg, Session) -> - reply(Req, Method, <<>>, Cfg, Session). - -reply(Req, Method, Payload, Cfg, Session) -> - Response = emqx_coap_message:response(Method, Payload, Req), - handle_out(Response, Cfg, Session). - -ack(Req, Cfg, Session) -> - piggyback(Req, <<>>, Cfg, Session). - -piggyback(Req, Payload, Cfg, Session) -> - Response = emqx_coap_message:ack(Req), - Response2 = emqx_coap_message:set_payload(Payload, Response), - handle_out(Response2, Cfg, Session). - -deliver(Delivers, Cfg, Session) -> - Fun = fun({_, Topic, Message}, - #{out := OutAcc, - session := #session{observe_manager = OM, - next_msg_id = MsgId} = SAcc} = Acc) -> - case emqx_coap_observe_res:res_changed(Topic, OM) of +deliver(Delivers, Ctx, #session{observe_manager = OM, + transport_manager = TM} = Session) -> + Fun = fun({_, Topic, Message}, {OutAcc, OMAcc, TMAcc} = Acc) -> + case emqx_coap_observe_res:res_changed(Topic, OMAcc) of undefined -> + metrics_inc('delivery.dropped', Ctx), + metrics_inc('delivery.dropped.no_subid', Ctx), Acc; {Token, SeqId, OM2} -> - Msg = mqtt_to_coap(Message, MsgId, Token, SeqId, Cfg), - SAcc2 = SAcc#session{next_msg_id = next_msg_id(MsgId), - observe_manager = OM2}, - #{out := Out} = Result = call_transport_manager(handle_out, Msg, Cfg, SAcc2), - Result#{out := [Out | OutAcc]} + metrics_inc('messages.delivered', Ctx), + Msg = mqtt_to_coap(Message, Token, SeqId), + #{out := Out, tm := TM2} = emqx_coap_tm:handle_out(Msg, TMAcc), + {Out ++ OutAcc, OM2, TM2} end end, - lists:foldl(Fun, - #{out => [], - session => Session}, - Delivers). + {Outs, OM2, TM2} = lists:foldl(Fun, {[], OM, TM}, lists:reverse(Delivers)), -timeout(Timer, Cfg, Session) -> - call_transport_manager(?FUNCTION_NAME, Timer, Cfg, Session). + #{out => lists:reverse(Outs), + session => Session#session{observe_manager = OM2, + transport_manager = TM2}}. -transfer_result(From, Value, Result) -> - ?TRANSFER_RESULT([out, subscribe], From, Value, Result). +timeout(Timer, Session) -> + call_transport_manager(?FUNCTION_NAME, Timer, Session). %%%------------------------------------------------------------------- %%% Internal functions %%%------------------------------------------------------------------- -handle_request(Msg, Cfg, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Cfg, Session). - -handle_response(Msg, Cfg, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Cfg, Session). - -handle_out(Msg, Cfg, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Cfg, Session). - call_transport_manager(Fun, Msg, - Cfg, #session{transport_manager = TM} = Session) -> - try - Result = emqx_coap_tm:Fun(Msg, Cfg, TM), - {ok, _, Session2} = emqx_misc:pipeline([fun process_tm/2, - fun process_subscribe/2], - Result, - Session), - emqx_coap_channel:transfer_result(session, Session2, Result) - catch Type:Reason:Stack -> - ?ERROR("process transmission with, message:~p failed~n -Type:~p,Reason:~p~n,StackTrace:~p~n", [Msg, Type, Reason, Stack]), - #{out => emqx_coap_message:response({error, internal_server_error}, Msg)} - end. + Result = emqx_coap_tm:Fun(Msg, TM), + iter([tm, fun process_tm/4, fun process_session/3], + Result, + Session). -process_tm(#{tm := TM}, Session) -> - {ok, Session#session{transport_manager = TM}}; -process_tm(_, Session) -> - {ok, Session}. +process_tm(TM, Result, Session, Cursor) -> + iter(Cursor, Result, Session#session{transport_manager = TM}). -process_subscribe(#{subscribe := Sub}, #session{observe_manager = OM} = Session) -> +process_session(_, Result, Session) -> + Result#{session => Session}. + +process_subscribe(Sub, Msg, Result, + #session{observe_manager = OM} = Session) -> case Sub of undefined -> - {ok, Session}; + Result; {Topic, Token} -> - OM2 = emqx_coap_observe_res:insert(Topic, Token, OM), - {ok, Session#session{observe_manager = OM2}}; + {SeqId, OM2} = emqx_coap_observe_res:insert(Topic, Token, OM), + Replay = emqx_coap_message:piggyback({ok, content}, Msg), + Replay2 = Replay#coap_message{options = #{observe => SeqId}}, + Result#{reply => Replay2, + session => Session#session{observe_manager = OM2}}; Topic -> OM2 = emqx_coap_observe_res:remove(Topic, OM), - {ok, Session#session{observe_manager = OM2}} - end; -process_subscribe(_, Session) -> - {ok, Session}. + Replay = emqx_coap_message:piggyback({ok, nocontent}, Msg), + Result#{reply => Replay, + session => Session#session{observe_manager = OM2}} + end. -mqtt_to_coap(MQTT, MsgId, Token, SeqId, Cfg) -> +mqtt_to_coap(MQTT, Token, SeqId) -> #message{payload = Payload} = MQTT, - #coap_message{type = get_notify_type(MQTT, Cfg), + #coap_message{type = get_notify_type(MQTT), method = {ok, content}, - id = MsgId, token = Token, payload = Payload, - options = #{observe => SeqId, - max_age => get_max_age(MQTT)}}. + options = #{observe => SeqId}}. -get_notify_type(#message{qos = Qos}, #{notify_type := Type}) -> - case Type of +get_notify_type(#message{qos = Qos}) -> + case emqx:get_config([gateway, coap, notify_qos], non) of qos -> case Qos of ?QOS_0 -> @@ -177,19 +227,3 @@ get_notify_type(#message{qos = Qos}, #{notify_type := Type}) -> Other -> Other end. - --spec get_max_age(#message{}) -> max_age(). -get_max_age(#message{headers = #{properties := #{'Message-Expiry-Interval' := 0}}}) -> - ?MAXIMUM_MAX_AGE; -get_max_age(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}}, - timestamp = Ts}) -> - Now = erlang:system_time(millisecond), - Diff = (Now - Ts + Interval * 1000) / 1000, - erlang:max(1, erlang:floor(Diff)); -get_max_age(_) -> - ?DEFAULT_MAX_AGE. - -next_msg_id(MsgId) when MsgId >= ?MAX_MESSAGE_ID -> - 1; -next_msg_id(MsgId) -> - MsgId + 1. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl index 0e13eed02..38cd68f56 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl @@ -18,10 +18,12 @@ -module(emqx_coap_tm). -export([ new/0 - , handle_request/3 - , handle_response/3 + , handle_request/2 + , handle_response/2 + , handle_out/2 , handle_out/3 - , timeout/3]). + , set_reply/2 + , timeout/2]). -export_type([manager/0, event_result/1]). @@ -29,17 +31,28 @@ -include("emqx_coap.hrl"). -type direction() :: in | out. --type state_machine_id() :: {direction(), non_neg_integer()}. --record(state_machine, { id :: state_machine_id() +-record(state_machine, { seq_id :: seq_id() + , id :: state_machine_key() + , token :: token() | undefined + , observe :: 0 | 1 | undefined | observed , state :: atom() , timers :: maps:map() , transport :: emqx_coap_transport:transport()}). -type state_machine() :: #state_machine{}. -type message_id() :: 0 .. ?MAX_MESSAGE_ID. +-type token_key() :: {token, token()}. +-type state_machine_key() :: {direction(), message_id()}. +-type seq_id() :: pos_integer(). +-type manager_key() :: token_key() | state_machine_key() | seq_id(). --type manager() :: #{message_id() => state_machine()}. +-type manager() :: #{ seq_id => seq_id() + , next_msg_id => coap_message_id() + , token_key() => seq_id() + , state_machine_key() => seq_id() + , seq_id() => state_machine() + }. -type ttimeout() :: {state_timeout, pos_integer(), any()} | {stop_timeout, pos_integer()}. @@ -47,6 +60,7 @@ -type topic() :: binary(). -type token() :: binary(). -type sub_register() :: {topic(), token()} | topic(). + -type event_result(State) :: #{next => State, outgoing => emqx_coap_message(), @@ -54,105 +68,164 @@ has_sub => undefined | sub_register(), transport => emqx_coap_transport:transprot()}. +-define(TOKEN_ID(T), {token, T}). + +-import(emqx_coap_medium, [empty/0, iter/4, reset/1, proto_out/2]). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- new() -> - #{}. + #{ seq_id => 1 + , next_msg_id => rand:uniform(?MAX_MESSAGE_ID) + }. -handle_request(#coap_message{id = MsgId} = Msg, Cfg, TM) -> +%% client request +handle_request(#coap_message{id = MsgId} = Msg, TM) -> Id = {in, MsgId}, - case maps:get(Id, TM, undefined) of + case find_machine(Id, TM) of undefined -> - Transport = emqx_coap_transport:new(), - Machine = new_state_machine(Id, Transport), - process_event(in, Msg, TM, Machine, Cfg); + {Machine, TM2} = new_in_machine(Id, TM), + process_event(in, Msg, TM2, Machine); Machine -> - process_event(in, Msg, TM, Machine, Cfg) + process_event(in, Msg, TM, Machine) end. -handle_response(#coap_message{type = Type, id = MsgId} = Msg, Cfg, TM) -> +%% client response +handle_response(#coap_message{type = Type, id = MsgId, token = Token} = Msg, TM) -> Id = {out, MsgId}, - case maps:get(Id, TM, undefined) of + TokenId = ?TOKEN_ID(Token), + case find_machine_by_keys([Id, TokenId], TM) of undefined -> case Type of reset -> - ?EMPTY_RESULT; + empty(); _ -> - #{out => #coap_message{type = reset, - id = MsgId}} + reset(Msg) end; Machine -> - process_event(in, Msg, TM, Machine, Cfg) + process_event(in, Msg, TM, Machine) end. -handle_out(#coap_message{id = MsgId} = Msg, Cfg, TM) -> +%% send to a client, msg can be request/piggyback/separate/notify +handle_out({Ctx, Msg}, TM) -> + handle_out(Msg, Ctx, TM); + +handle_out(Msg, TM) -> + handle_out(Msg, undefined, TM). + +handle_out(#coap_message{token = Token} = MsgT, Ctx, TM) -> + {MsgId, TM2} = alloc_message_id(TM), + Msg = MsgT#coap_message{id = MsgId}, Id = {out, MsgId}, - case maps:get(Id, TM, undefined) of + TokenId = ?TOKEN_ID(Token), + %% TODO why find by token ? + case find_machine_by_keys([Id, TokenId], TM2) of undefined -> - Transport = emqx_coap_transport:new(), - Machine = new_state_machine(Id, Transport), - process_event(out, Msg, TM, Machine, Cfg); + {Machine, TM3} = new_out_machine(Id, Ctx, Msg, TM2), + process_event(out, Msg, TM3, Machine); _ -> - ?WARN("Repeat sending message with id:~p~n", [Id]), - ?EMPTY_RESULT + %% ignore repeat send + empty() end. -timeout({Id, Type, Msg}, Cfg, TM) -> - case maps:get(Id, TM, undefined) of +set_reply(#coap_message{id = MsgId} = Msg, TM) -> + Id = {in, MsgId}, + case find_machine(Id, TM) of undefined -> - ?EMPTY_RESULT; + TM; + #state_machine{transport = Transport, + seq_id = SeqId} = Machine -> + Transport2 = emqx_coap_transport:set_cache(Msg, Transport), + Machine2 = Machine#state_machine{transport = Transport2}, + TM#{SeqId => Machine2} + end. + +timeout({SeqId, Type, Msg}, TM) -> + case maps:get(SeqId, TM, undefined) of + undefined -> + empty(); #state_machine{timers = Timers} = Machine -> %% maybe timer has been canceled case maps:is_key(Type, Timers) of true -> - process_event(Type, Msg, TM, Machine, Cfg); + process_event(Type, Msg, TM, Machine); _ -> - ?EMPTY_RESULT + empty() end end. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -new_state_machine(Id, Transport) -> - #state_machine{id = Id, - state = idle, - timers = #{}, - transport = Transport}. +process_event(stop_timeout, _, TM, Machine) -> + process_manager(stop, #{}, Machine, TM); -process_event(stop_timeout, - _, - TM, - #state_machine{id = Id, - timers = Timers}, - _) -> - lists:foreach(fun({_, Ref}) -> - emqx_misc:cancel_timer(Ref) - end, - maps:to_list(Timers)), - #{tm => maps:remove(Id, TM)}; +process_event(Event, Msg, TM, #state_machine{state = State, + transport = Transport} = Machine) -> + Result = emqx_coap_transport:State(Event, Msg, Transport), + iter([ proto, fun process_observe_response/5 + , next, fun process_state_change/5 + , transport, fun process_transport_change/5 + , timeouts, fun process_timeouts/5 + , fun process_manager/4], + Result, + Machine, + TM). -process_event(Event, - Msg, - TM, - #state_machine{id = Id, - state = State, - transport = Transport} = Machine, - Cfg) -> - Result = emqx_coap_transport:State(Event, Msg, Transport, Cfg), - {ok, _, Machine2} = emqx_misc:pipeline([fun process_state_change/2, - fun process_transport_change/2, - fun process_timeouts/2], - Result, - Machine), - TM2 = TM#{Id => Machine2}, - emqx_coap_session:transfer_result(tm, TM2, Result). +process_observe_response({response, {_, Msg}} = Response, + Result, + #state_machine{observe = 0} = Machine, + TM, + Iter) -> + Result2 = proto_out(Response, Result), + case Msg#coap_message.method of + {ok, _} -> + iter(Iter, + Result2#{next => observe}, + Machine#state_machine{observe = observed}, + TM); + _ -> + iter(Iter, Result2, Machine, TM) + end; -process_state_change(#{next := Next}, Machine) -> - {ok, cancel_state_timer(Machine#state_machine{state = Next})}; -process_state_change(_, Machine) -> - {ok, Machine}. +process_observe_response(Proto, Result, Machine, TM, Iter) -> + iter(Iter, proto_out(Proto, Result), Machine, TM). + +process_state_change(Next, Result, Machine, TM, Iter) -> + case Next of + stop -> + process_manager(stop, Result, Machine, TM); + _ -> + iter(Iter, + Result, + cancel_state_timer(Machine#state_machine{state = Next}), + TM) + end. + +process_transport_change(Transport, Result, Machine, TM, Iter) -> + iter(Iter, Result, Machine#state_machine{transport = Transport}, TM). + +process_timeouts([], Result, Machine, TM, Iter) -> + iter(Iter, Result, Machine, TM); + +process_timeouts(Timeouts, Result, + #state_machine{seq_id = SeqId, + timers = Timers} = Machine, TM, Iter) -> + NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) -> + process_timer(SeqId, Timer, Acc); + ({stop_timeout, I}, Acc) -> + process_timer(SeqId, {stop_timeout, I, stop}, Acc) + end, + Timers, + Timeouts), + iter(Iter, Result, Machine#state_machine{timers = NewTimers}, TM). + +process_manager(stop, Result, #state_machine{seq_id = SeqId}, TM) -> + Result#{tm => delete_machine(SeqId, TM)}; + +process_manager(_, Result, #state_machine{seq_id = SeqId} = Machine2, TM) -> + Result#{tm => TM#{SeqId => Machine2}}. cancel_state_timer(#state_machine{timers = Timers} = Machine) -> case maps:get(state_timer, Timers, undefined) of @@ -163,27 +236,119 @@ cancel_state_timer(#state_machine{timers = Timers} = Machine) -> Machine#state_machine{timers = maps:remove(state_timer, Timers)} end. -process_transport_change(#{transport := Transport}, Machine) -> - {ok, Machine#state_machine{transport = Transport}}; -process_transport_change(_, Machine) -> - {ok, Machine}. - -process_timeouts(#{timeouts := []}, Machine) -> - {ok, Machine}; -process_timeouts(#{timeouts := Timeouts}, - #state_machine{id = Id, timers = Timers} = Machine) -> - NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) -> - process_timer(Id, Timer, Acc); - ({stop_timeout, I}, Acc) -> - process_timer(Id, {stop_timeout, I, stop}, Acc) - end, - Timers, - Timeouts), - {ok, Machine#state_machine{timers = NewTimers}}; - -process_timeouts(_, Machine) -> - {ok, Machine}. - -process_timer(Id, {Type, Interval, Msg}, Timers) -> - Ref = emqx_misc:start_timer(Interval, {state_machine, {Id, Type, Msg}}), +process_timer(SeqId, {Type, Interval, Msg}, Timers) -> + Ref = emqx_misc:start_timer(Interval, {state_machine, {SeqId, Type, Msg}}), Timers#{Type => Ref}. + +-spec delete_machine(manager_key(), manager()) -> manager(). +delete_machine(Id, Manager) -> + case find_machine(Id, Manager) of + undefined -> + Manager; + #state_machine{seq_id = SeqId, + id = MachineId, + token = Token, + timers = Timers} -> + lists:foreach(fun({_, Ref}) -> + emqx_misc:cancel_timer(Ref) + end, + maps:to_list(Timers)), + maps:without([SeqId, MachineId, ?TOKEN_ID(Token)], Manager) + end. + +-spec find_machine(manager_key(), manager()) -> state_machine() | undefined. +find_machine({_, _} = Id, Manager) -> + find_machine_by_seqid(maps:get(Id, Manager, undefined), Manager); +find_machine(SeqId, Manager) -> + find_machine_by_seqid(SeqId, Manager). + +-spec find_machine_by_seqid(seq_id() | undefined, manager()) -> + state_machine() | undefined. +find_machine_by_seqid(SeqId, Manager) -> + maps:get(SeqId, Manager, undefined). + +-spec find_machine_by_keys(list(manager_key()), + manager()) -> state_machine() | undefined. +find_machine_by_keys([H | T], Manager) -> + case H of + ?TOKEN_ID(<<>>) -> + find_machine_by_keys(T, Manager); + _ -> + case find_machine(H, Manager) of + undefined -> + find_machine_by_keys(T, Manager); + Machine -> + Machine + end + end; +find_machine_by_keys(_, _) -> + undefined. + +-spec new_in_machine(state_machine_key(), manager()) -> + {state_machine(), manager()}. +new_in_machine(MachineId, #{seq_id := SeqId} = Manager) -> + Machine = #state_machine{ seq_id = SeqId + , id = MachineId + , state = idle + , timers = #{} + , transport = emqx_coap_transport:new()}, + {Machine, Manager#{seq_id := SeqId + 1, + SeqId => Machine, + MachineId => SeqId}}. + +-spec new_out_machine(state_machine_key(), any(), emqx_coap_message(), manager()) -> + {state_machine(), manager()}. +new_out_machine(MachineId, + Ctx, + #coap_message{type = Type, token = Token, options = Opts}, + #{seq_id := SeqId} = Manager) -> + Observe = maps:get(observe, Opts, undefined), + Machine = #state_machine{ seq_id = SeqId + , id = MachineId + , token = Token + , observe = Observe + , state = idle + , timers = #{} + , transport = emqx_coap_transport:new(Ctx)}, + + Manager2 = Manager#{seq_id := SeqId + 1, + SeqId => Machine, + MachineId => SeqId}, + {Machine, + if Token =:= <<>> -> + Manager2; + Observe =:= 1 -> + TokenId = ?TOKEN_ID(Token), + delete_machine(TokenId, Manager2); + Type =:= con orelse Observe =:= 0 -> + TokenId = ?TOKEN_ID(Token), + case maps:get(TokenId, Manager, undefined) of + undefined -> + Manager2#{TokenId => SeqId}; + _ -> + throw("token conflict") + end; + true -> + Manager2 + end + }. + +alloc_message_id(#{next_msg_id := MsgId} = TM) -> + alloc_message_id(MsgId, TM). + +alloc_message_id(MsgId, TM) -> + Id = {out, MsgId}, + case maps:get(Id, TM, undefined) of + undefined -> + {MsgId, TM#{next_msg_id => next_message_id(MsgId)}}; + _ -> + alloc_message_id(next_message_id(MsgId), TM) + end. + +next_message_id(MsgId) -> + Next = MsgId + 1, + if Next >= ?MAX_MESSAGE_ID -> + 1; + true -> + Next + end. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl index 6c14caaf1..4a2b178de 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl @@ -9,127 +9,183 @@ -define(EXCHANGE_LIFETIME, 247000). -define(NON_LIFETIME, 145000). +-type request_context() :: any(). + -record(transport, { cache :: undefined | emqx_coap_message() + , req_context :: request_context() , retry_interval :: non_neg_integer() , retry_count :: non_neg_integer() + , observe :: non_neg_integer() | undefined }). -type transport() :: #transport{}. --export([ new/0, idle/4, maybe_reset/4 - , maybe_resend/4, wait_ack/4, until_stop/4]). +-export([ new/0, new/1, idle/3, maybe_reset/3, set_cache/2 + , maybe_resend_4request/3, wait_ack/3, until_stop/3 + , observe/3, maybe_resend_4response/3]). -export_type([transport/0]). +-import(emqx_coap_message, [reset/1]). +-import(emqx_coap_medium, [ empty/0, reset/2, proto_out/2 + , out/1, out/2, proto_out/1 + , reply/2]). + -spec new() -> transport(). new() -> + new(undefined). + +new(ReqCtx) -> #transport{cache = undefined, retry_interval = 0, - retry_count = 0}. + retry_count = 0, + req_context = ReqCtx}. idle(in, - #coap_message{type = non, id = MsgId, method = Method} = Msg, - _, - #{resource := Resource} = Cfg) -> - Ret = #{next => until_stop, - timeouts => [{stop_timeout, ?NON_LIFETIME}]}, + #coap_message{type = non, method = Method} = Msg, + _) -> case Method of undefined -> - Ret#{out => #coap_message{type = reset, id = MsgId}}; + reset(Msg, #{next => stop}); _ -> - case erlang:apply(Resource, Method, [Msg, Cfg]) of - #coap_message{} = Result -> - Ret#{out => Result}; - {has_sub, Result, Sub} -> - Ret#{out => Result, subscribe => Sub}; - error -> - Ret#{out => - emqx_coap_message:response({error, internal_server_error}, Msg)} - end + proto_out({request, Msg}, + #{next => until_stop, + timeouts => + [{stop_timeout, ?NON_LIFETIME}]}) end; idle(in, - #coap_message{id = MsgId, - type = con, - method = Method} = Msg, - Transport, - #{resource := Resource} = Cfg) -> - Ret = #{next => maybe_resend, - timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}, + #coap_message{type = con, method = Method} = Msg, + _) -> case Method of undefined -> - ResetMsg = #coap_message{type = reset, id = MsgId}, - Ret#{transport => Transport#transport{cache = ResetMsg}, - out => ResetMsg}; + reset(Msg, #{next => stop}); _ -> - {RetMsg, SubInfo} = - case erlang:apply(Resource, Method, [Msg, Cfg]) of - #coap_message{} = Result -> - {Result, undefined}; - {has_sub, Result, Sub} -> - {Result, Sub}; - error -> - {emqx_coap_message:response({error, internal_server_error}, Msg), - undefined} - end, - RetMsg2 = RetMsg#coap_message{type = ack}, - Ret#{out => RetMsg2, - transport => Transport#transport{cache = RetMsg2}, - subscribe => SubInfo} + proto_out({request, Msg}, + #{next => maybe_resend_4request, + timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}) end; -idle(out, #coap_message{type = non} = Msg, _, _) -> - #{next => maybe_reset, - out => Msg, - timeouts => [{stop_timeout, ?NON_LIFETIME}]}; +idle(out, #coap_message{type = non} = Msg, _) -> + out(Msg, #{next => maybe_reset, + timeouts => [{stop_timeout, ?NON_LIFETIME}]}); -idle(out, Msg, Transport, _) -> +idle(out, Msg, Transport) -> _ = emqx_misc:rand_seed(), Timeout = ?ACK_TIMEOUT + rand:uniform(?ACK_RANDOM_FACTOR), - #{next => wait_ack, - transport => Transport#transport{cache = Msg}, - out => Msg, - timeouts => [ {state_timeout, Timeout, ack_timeout} - , {stop_timeout, ?EXCHANGE_LIFETIME}]}. + out(Msg, #{next => wait_ack, + transport => Transport#transport{cache = Msg}, + timeouts => [ {state_timeout, Timeout, ack_timeout} + , {stop_timeout, ?EXCHANGE_LIFETIME}]}). -maybe_reset(in, Message, _, _) -> - case Message of - #coap_message{type = reset} -> - ?INFO("Reset Message:~p~n", [Message]); +maybe_resend_4request(in, Msg, Transport) -> + maybe_resend(Msg, true, Transport). + +maybe_resend_4response(in, Msg, Transport) -> + maybe_resend(Msg, false, Transport). + +maybe_resend(Msg, IsExpecteReq, #transport{cache = Cache}) -> + IsExpected = emqx_coap_message:is_request(Msg) =:= IsExpecteReq, + case IsExpected of + true -> + case Cache of + undefined -> + %% handler in processing, ignore + empty(); + _ -> + out(Cache) + end; _ -> - ok - end, - ?EMPTY_RESULT. + reset(Msg, #{next => stop}) + end. -maybe_resend(in, _, _, #transport{cache = Cache}) -> - #{out => Cache}. +maybe_reset(in, #coap_message{type = Type, method = Method} = Message, + #transport{req_context = Ctx} = Transport) -> + Ret = #{next => stop}, + CtxMsg = {Ctx, Message}, + if Type =:= reset -> + proto_out({reset, CtxMsg}, Ret); + is_tuple(Method) -> + on_response(Message, + Transport, + if Type =:= non -> until_stop; + true -> maybe_resend_4response + end); + true -> + reset(Message, Ret) + end. -wait_ack(in, #coap_message{type = Type}, _, _) -> +wait_ack(in, #coap_message{type = Type, method = Method} = Msg, #transport{req_context = Ctx}) -> + CtxMsg = {Ctx, Msg}, case Type of - ack -> - #{next => until_stop}; reset -> - #{next => until_stop}; + proto_out({reset, CtxMsg}, #{next => stop}); _ -> - ?EMPTY_RESULT + case Method of + undefined -> + %% empty ack, keep transport to recv response + proto_out({ack, CtxMsg}); + {_, _} -> + %% ack with payload + proto_out({response, CtxMsg}, #{next => stop}); + _ -> + reset(Msg, #{next => stop}) + end end; wait_ack(state_timeout, ack_timeout, - _, #transport{cache = Msg, retry_interval = Timeout, retry_count = Count} =Transport) -> case Count < ?MAX_RETRANSMIT of true -> Timeout2 = Timeout * 2, - #{transport => Transport#transport{retry_interval = Timeout2, - retry_count = Count + 1}, - out => Msg, - timeouts => [{state_timeout, Timeout2, ack_timeout}]}; + out(Msg, + #{transport => Transport#transport{retry_interval = Timeout2, + retry_count = Count + 1}, + timeouts => [{state_timeout, Timeout2, ack_timeout}]}); _ -> - #{next_state => until_stop} + proto_out({ack_failure, Msg}, #{next_state => stop}) end. -until_stop(_, _, _, _) -> - ?EMPTY_RESULT. +observe(in, + #coap_message{method = Method} = Message, + #transport{observe = Observe} = Transport) -> + case Method of + {ok, _} -> + case emqx_coap_message:get_option(observe, Message, Observe) of + Observe -> + %% repeatd notify, ignore + empty(); + NewObserve -> + on_response(Message, + Transport#transport{observe = NewObserve}, + ?FUNCTION_NAME) + end; + {error, _} -> + #{next => stop}; + _ -> + reset(Message) + end. + +until_stop(_, _, _) -> + empty(). + +set_cache(Cache, Transport) -> + Transport#transport{cache = Cache}. + +on_response(#coap_message{type = Type} = Message, + #transport{req_context = Ctx} = Transport, + NextState) -> + CtxMsg = {Ctx, Message}, + if Type =:= non -> + proto_out({response, CtxMsg}, #{next => NextState}); + Type =:= con -> + Ack = emqx_coap_message:ack(Message), + proto_out({response, CtxMsg}, + out(Ack, #{next => NextState, + transport => Transport#transport{cache = Ack}})); + true -> + reset(Message) + end. diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl new file mode 100644 index 000000000..98cb74a42 --- /dev/null +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl @@ -0,0 +1,41 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_coap_mqtt_handler). + +-include("emqx_coap.hrl"). + +-export([handle_request/4]). +-import(emqx_coap_message, [response/2, response/3]). +-import(emqx_coap_medium, [reply/2]). + +handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _Ctx, _CInfo) -> + handle_method(Method, Msg); + +handle_request(_, Msg, _, _) -> + reply({error, bad_request}, Msg). + +handle_method(put, Msg) -> + reply({ok, changed}, Msg); + +handle_method(post, Msg) -> + #{connection => {open, Msg}}; + +handle_method(delete, Msg) -> + #{connection => {close, Msg}}; + +handle_method(_, Msg) -> + reply({error, method_not_allowed}, Msg). diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl new file mode 100644 index 000000000..b63c66cc6 --- /dev/null +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl @@ -0,0 +1,161 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +%% a coap to mqtt adapter with a retained topic message database +-module(emqx_coap_pubsub_handler). + +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include("emqx_coap.hrl"). + +-export([handle_request/4]). + +-import(emqx_coap_message, [response/2, response/3]). +-import(emqx_coap_medium, [reply/2, reply/3]). +-import(emqx_coap_channel, [run_hooks/3]). + +-define(UNSUB(Topic, Msg), #{subscribe => {Topic, Msg}}). +-define(SUB(Topic, Token, Msg), #{subscribe => {{Topic, Token}, Msg}}). +-define(SUBOPTS, #{qos => 0, rh => 0, rap => 0, nl => 0, is_new => false}). + +%% TODO maybe can merge this code into emqx_coap_session, simplify the call chain + +handle_request(Path, #coap_message{method = Method} = Msg, Ctx, CInfo) -> + case check_topic(Path) of + {ok, Topic} -> + handle_method(Method, Topic, Msg, Ctx, CInfo); + _ -> + reply({error, bad_request}, <<"invalid topic">>, Msg) + end. + +handle_method(get, Topic, Msg, Ctx, CInfo) -> + case emqx_coap_message:get_option(observe, Msg) of + 0 -> + subscribe(Msg, Topic, Ctx, CInfo); + 1 -> + unsubscribe(Msg, Topic, Ctx, CInfo); + _ -> + reply({error, bad_request}, <<"invalid observe value">>, Msg) + end; + +handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) -> + case emqx_coap_channel:validator(publish, Topic, Ctx, CInfo) of + allow -> + #{clientid := ClientId} = CInfo, + MountTopic = mount(CInfo, Topic), + QOS = get_publish_qos(Msg), + MQTTMsg = emqx_message:make(ClientId, QOS, MountTopic, Payload), + MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg), + _ = emqx_broker:publish(MQTTMsg2), + reply({ok, changed}, Msg); + _ -> + reply({error, unauthorized}, Msg) + end; + +handle_method(_, _, Msg, _, _) -> + reply({error, method_not_allowed}, Msg). + +check_topic([]) -> + error; + +check_topic(Path) -> + Sep = <<"/">>, + {ok, + emqx_http_lib:uri_decode( + lists:foldl(fun(Part, Acc) -> + <> + end, + <<>>, + Path))}. + +get_sub_opts(#coap_message{options = Opts} = Msg) -> + SubOpts = maps:fold(fun parse_sub_opts/3, #{}, Opts), + case SubOpts of + #{qos := _} -> + maps:merge(SubOpts, ?SUBOPTS); + _ -> + CfgType = emqx:get_config([gateway, coap, subscribe_qos], ?QOS_0), + maps:merge(SubOpts, ?SUBOPTS#{qos => type_to_qos(CfgType, Msg)}) + end. + +parse_sub_opts(<<"qos">>, V, Opts) -> + Opts#{qos => erlang:binary_to_integer(V)}; +parse_sub_opts(<<"nl">>, V, Opts) -> + Opts#{nl => erlang:binary_to_integer(V)}; +parse_sub_opts(<<"rh">>, V, Opts) -> + Opts#{rh => erlang:binary_to_integer(V)}; +parse_sub_opts(_, _, Opts) -> + Opts. + +type_to_qos(qos0, _) -> ?QOS_0; +type_to_qos(qos1, _) -> ?QOS_1; +type_to_qos(qos2, _) -> ?QOS_2; +type_to_qos(coap, #coap_message{type = Type}) -> + case Type of + non -> + ?QOS_0; + _ -> + ?QOS_1 + end. + +get_publish_qos(Msg) -> + case emqx_coap_message:get_option(uri_query, Msg) of + #{<<"qos">> := QOS} -> + erlang:binary_to_integer(QOS); + _ -> + CfgType = emqx:get_config([gateway, coap, publish_qos], ?QOS_0), + type_to_qos(CfgType, Msg) + end. + +apply_publish_opts(Msg, MQTTMsg) -> + maps:fold(fun(<<"retain">>, V, Acc) -> + Val = erlang:binary_to_atom(V), + emqx_message:set_flag(retain, Val, Acc); + (<<"expiry">>, V, Acc) -> + Val = erlang:binary_to_integer(V), + Props = emqx_message:get_header(properties, Acc), + emqx_message:set_header(properties, + Props#{'Message-Expiry-Interval' => Val}, + Acc); + (_, _, Acc) -> + Acc + end, + MQTTMsg, + emqx_coap_message:get_option(uri_query, Msg)). + +subscribe(#coap_message{token = <<>>} = Msg, _, _, _) -> + reply({error, bad_request}, <<"observe without token">>, Msg); + +subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) -> + case emqx_coap_channel:validator(subscribe, Topic, Ctx, CInfo) of + allow -> + #{clientid := ClientId} = CInfo, + SubOpts = get_sub_opts(Msg), + MountTopic = mount(CInfo, Topic), + emqx_broker:subscribe(MountTopic, ClientId, SubOpts), + run_hooks(Ctx, 'session.subscribed', [CInfo, Topic, SubOpts]), + ?SUB(MountTopic, Token, Msg); + _ -> + reply({error, unauthorized}, Msg) + end. + +unsubscribe(Msg, Topic, Ctx, CInfo) -> + MountTopic = mount(CInfo, Topic), + emqx_broker:unsubscribe(MountTopic), + run_hooks(Ctx, 'session.unsubscribed', [CInfo, Topic, ?SUBOPTS]), + ?UNSUB(MountTopic, Msg). + +mount(#{mountpoint := Mountpoint}, Topic) -> + <>. diff --git a/apps/emqx_gateway/src/coap/resources/emqx_coap_mqtt_resource.erl b/apps/emqx_gateway/src/coap/resources/emqx_coap_mqtt_resource.erl deleted file mode 100644 index 0dc8ded84..000000000 --- a/apps/emqx_gateway/src/coap/resources/emqx_coap_mqtt_resource.erl +++ /dev/null @@ -1,153 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - -%% a coap to mqtt adapter --module(emqx_coap_mqtt_resource). - --behaviour(emqx_coap_resource). - --include_lib("emqx/include/emqx_mqtt.hrl"). --include("emqx_coap.hrl"). - - --export([ init/1 - , stop/1 - , get/2 - , put/2 - , post/2 - , delete/2 - ]). - --export([ check_topic/1 - , publish/3 - , subscribe/3 - , unsubscribe/3]). - --define(SUBOPTS, #{rh => 0, rap => 0, nl => 0, is_new => false}). - -init(_) -> - {ok, undefined}. - -stop(_) -> - ok. - -%% get: subscribe, ignore observe option -get(#coap_message{token = Token} = Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - case Token of - <<>> -> - emqx_coap_message:response({error, bad_request}, <<"observer without token">>, Msg); - _ -> - Ret = subscribe(Msg, Topic, Cfg), - RetMsg = emqx_coap_message:response(Ret, Msg), - case Ret of - {ok, _} -> - {has_sub, RetMsg, {Topic, Token}}; - _ -> - RetMsg - end - end; - Any -> - Any - end. - -%% put: equal post -put(Msg, Cfg) -> - post(Msg, Cfg). - -%% post: publish a message -post(Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - emqx_coap_message:response(publish(Msg, Topic, Cfg), Msg); - Any -> - Any - end. - -%% delete: ubsubscribe -delete(Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - unsubscribe(Msg, Topic, Cfg), - {has_sub, emqx_coap_message:response({ok, deleted}, Msg), Topic}; - Any -> - Any - end. - -check_topic(#coap_message{options = Options} = Msg) -> - case maps:get(uri_path, Options, []) of - [] -> - emqx_coap_message:response({error, bad_request}, <<"invalid topic">> , Msg); - UriPath -> - Sep = <<"/">>, - {ok, lists:foldl(fun(Part, Acc) -> - <> - end, - <<>>, - UriPath)} - end. - -publish(#coap_message{payload = Payload} = Msg, - Topic, - #{clientinfo := ClientInfo, - publish_qos := QOS} = Cfg) -> - case emqx_coap_channel:auth_publish(Topic, Cfg) of - allow -> - #{clientid := ClientId} = ClientInfo, - MQTTMsg = emqx_message:make(ClientId, type_to_qos(QOS, Msg), Topic, Payload), - MQTTMsg2 = emqx_message:set_flag(retain, false, MQTTMsg), - _ = emqx_broker:publish(MQTTMsg2), - {ok, changed}; - _ -> - {error, unauthorized} - end. - -subscribe(Msg, Topic, #{clientinfo := ClientInfo}= Cfg) -> - case emqx_topic:wildcard(Topic) of - false -> - case emqx_coap_channel:auth_subscribe(Topic, Cfg) of - allow -> - #{clientid := ClientId} = ClientInfo, - SubOpts = get_sub_opts(Msg, Cfg), - emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]), - {ok, created}; - _ -> - {error, unauthorized} - end; - _ -> - %% now, we don't support wildcard in subscribe topic - {error, bad_request, <<"">>} - end. - -unsubscribe(Msg, Topic, #{clientinfo := ClientInfo} = Cfg) -> - emqx_broker:unsubscribe(Topic), - emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, get_sub_opts(Msg, Cfg)]). - -get_sub_opts(Msg, #{subscribe_qos := Type}) -> - ?SUBOPTS#{qos => type_to_qos(Type, Msg)}. - -type_to_qos(qos0, _) -> ?QOS_0; -type_to_qos(qos1, _) -> ?QOS_1; -type_to_qos(qos2, _) -> ?QOS_2; -type_to_qos(coap, #coap_message{type = Type}) -> - case Type of - non -> - ?QOS_0; - _ -> - ?QOS_1 - end. diff --git a/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_resource.erl b/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_resource.erl deleted file mode 100644 index 63879cfbd..000000000 --- a/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_resource.erl +++ /dev/null @@ -1,219 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - -%% a coap to mqtt adapter with a retained topic message database --module(emqx_coap_pubsub_resource). - --behaviour(emqx_coap_resource). - --include_lib("emqx/include/logger.hrl"). --include("emqx_coap.hrl"). - - --export([ init/1 - , stop/1 - , get/2 - , put/2 - , post/2 - , delete/2 - ]). --import(emqx_coap_mqtt_resource, [ check_topic/1, subscribe/3, unsubscribe/3 - , publish/3]). - --import(emqx_coap_message, [response/2, response/3, set_content/2]). -%%-------------------------------------------------------------------- -%% Resource Callbacks -%%-------------------------------------------------------------------- -init(_) -> - emqx_coap_pubsub_topics:start_link(). - -stop(Pid) -> - emqx_coap_pubsub_topics:stop(Pid). - -%% get: read last publish message -%% get with observe 0: subscribe -%% get with observe 1: unsubscribe -get(#coap_message{token = Token} = Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - case emqx_coap_message:get_option(observe, Msg) of - undefined -> - Content = emqx_coap_message:get_content(Msg), - read_last_publish_message(emqx_topic:wildcard(Topic), Msg, Topic, Content); - 0 -> - case Token of - <<>> -> - response({error, bad_reuqest}, <<"observe without token">>, Msg); - _ -> - Ret = subscribe(Msg, Topic, Cfg), - RetMsg = response(Ret, Msg), - case Ret of - {ok, _} -> - {has_sub, RetMsg, {Topic, Token}}; - _ -> - RetMsg - end - end; - 1 -> - unsubscribe(Msg, Topic, Cfg), - {has_sub, response({ok, deleted}, Msg), Topic} - end; - Any -> - Any - end. - -%% put: insert a message into topic database -put(Msg, _) -> - case check_topic(Msg) of - {ok, Topic} -> - Content = emqx_coap_message:get_content(Msg), - #coap_content{payload = Payload, - format = Format, - max_age = MaxAge} = Content, - handle_received_create(Msg, Topic, MaxAge, Format, Payload); - Any -> - Any - end. - -%% post: like put, but will publish the inserted message -post(Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - Content = emqx_coap_message:get_content(Msg), - #coap_content{max_age = MaxAge, - format = Format, - payload = Payload} = Content, - handle_received_publish(Msg, Topic, MaxAge, Format, Payload, Cfg); - Any -> - Any - end. - -%% delete: delete a message from topic database -delete(Msg, _) -> - case check_topic(Msg) of - {ok, Topic} -> - delete_topic_info(Msg, Topic); - Any -> - Any - end. - -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- -add_topic_info(Topic, MaxAge, Format, Payload) when is_binary(Topic), Topic =/= <<>> -> - case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of - [{_, StoredMaxAge, StoredCT, _, _}] -> - ?LOG(debug, "publish topic=~p already exists, need reset the topic info", [Topic]), - %% check whether the ct value stored matches the ct option in this POST message - case Format =:= StoredCT of - true -> - {ok, Ret} = - case StoredMaxAge =:= MaxAge of - true -> - emqx_coap_pubsub_topics:reset_topic_info(Topic, Payload); - false -> - emqx_coap_pubsub_topics:reset_topic_info(Topic, MaxAge, Payload) - end, - {changed, Ret}; - false -> - ?LOG(debug, "ct values of topic=~p do not match, stored ct=~p, new ct=~p, ignore the PUBLISH", [Topic, StoredCT, Format]), - {changed, false} - end; - [] -> - ?LOG(debug, "publish topic=~p will be created", [Topic]), - {ok, Ret} = emqx_coap_pubsub_topics:add_topic_info(Topic, MaxAge, Format, Payload), - {created, Ret} - end; - -add_topic_info(Topic, _MaxAge, _Format, _Payload) -> - ?LOG(debug, "create topic=~p info failed", [Topic]), - {badarg, false}. - -format_string_to_int(<<"application/octet-stream">>) -> - <<"42">>; -format_string_to_int(<<"application/exi">>) -> - <<"47">>; -format_string_to_int(<<"application/json">>) -> - <<"50">>; -format_string_to_int(_) -> - <<"42">>. - -handle_received_publish(Msg, Topic, MaxAge, Format, Payload, Cfg) -> - case add_topic_info(Topic, MaxAge, format_string_to_int(Format), Payload) of - {_, true} -> - response(publish(Msg, Topic, Cfg), Msg); - {_, false} -> - ?LOG(debug, "add_topic_info failed, will return bad_request", []), - response({error, bad_request}, Msg) - end. - -handle_received_create(Msg, Topic, MaxAge, Format, Payload) -> - case add_topic_info(Topic, MaxAge, format_string_to_int(Format), Payload) of - {Ret, true} -> - response({ok, Ret}, Msg); - {_, false} -> - ?LOG(debug, "add_topic_info failed, will return bad_request", []), - response({error, bad_request}, Msg) - end. - -return_resource(Msg, Topic, Payload, MaxAge, TimeStamp, Content) -> - TimeElapsed = trunc((erlang:system_time(millisecond) - TimeStamp) / 1000), - case TimeElapsed < MaxAge of - true -> - LeftTime = (MaxAge - TimeElapsed), - ?LOG(debug, "topic=~p has max age left time is ~p", [Topic, LeftTime]), - set_content(Content#coap_content{max_age = LeftTime, payload = Payload}, - response({ok, content}, Msg)); - false -> - ?LOG(debug, "topic=~p has been timeout, will return empty content", [Topic]), - response({ok, nocontent}, Msg) - end. - -read_last_publish_message(false, Msg, Topic, Content=#coap_content{format = QueryFormat}) when is_binary(QueryFormat)-> - ?LOG(debug, "the QueryFormat=~p", [QueryFormat]), - case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of - [] -> - response({error, not_found}, Msg); - [{_, MaxAge, CT, Payload, TimeStamp}] -> - case CT =:= format_string_to_int(QueryFormat) of - true -> - return_resource(Msg, Topic, Payload, MaxAge, TimeStamp, Content); - false -> - ?LOG(debug, "format value does not match, the queried format=~p, the stored format=~p", [QueryFormat, CT]), - response({error, bad_request}, Msg) - end - end; - -read_last_publish_message(false, Msg, Topic, Content) -> - case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of - [] -> - response({error, not_found}, Msg); - [{_, MaxAge, _, Payload, TimeStamp}] -> - return_resource(Msg, Topic, Payload, MaxAge, TimeStamp, Content) - end; - -read_last_publish_message(true, Msg, Topic, _Content) -> - ?LOG(debug, "the topic=~p is illegal wildcard topic", [Topic]), - response({error, bad_request}, Msg). - -delete_topic_info(Msg, Topic) -> - case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of - [] -> - response({error, not_found}, Msg); - [{_, _, _, _, _}] -> - emqx_coap_pubsub_topics:delete_sub_topics(Topic), - response({ok, deleted}, Msg) - end. diff --git a/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_topics.erl b/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_topics.erl deleted file mode 100644 index 65fa54451..000000000 --- a/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_topics.erl +++ /dev/null @@ -1,185 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_coap_pubsub_topics). - --behaviour(gen_server). - --include_lib("emqx/include/logger.hrl"). --include("emqx_coap.hrl"). - - --export([ start_link/0 - , stop/1 - ]). - --export([ add_topic_info/4 - , delete_topic_info/1 - , delete_sub_topics/1 - , is_topic_existed/1 - , is_topic_timeout/1 - , reset_topic_info/2 - , reset_topic_info/3 - , reset_topic_info/4 - , lookup_topic_info/1 - , lookup_topic_payload/1 - ]). - -%% gen_server. --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --record(state, {}). - --define(COAP_TOPIC_TABLE, coap_topic). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -stop(Pid) -> - gen_server:stop(Pid). - -add_topic_info(Topic, MaxAge, CT, Payload) when is_binary(Topic), is_integer(MaxAge), is_binary(CT), is_binary(Payload) -> - gen_server:call(?MODULE, {add_topic, {Topic, MaxAge, CT, Payload}}). - -delete_topic_info(Topic) when is_binary(Topic) -> - gen_server:call(?MODULE, {remove_topic, Topic}). - -delete_sub_topics(Topic) when is_binary(Topic) -> - gen_server:cast(?MODULE, {remove_sub_topics, Topic}). - -reset_topic_info(Topic, Payload) -> - gen_server:call(?MODULE, {reset_topic, {Topic, Payload}}). - -reset_topic_info(Topic, MaxAge, Payload) -> - gen_server:call(?MODULE, {reset_topic, {Topic, MaxAge, Payload}}). - -reset_topic_info(Topic, MaxAge, CT, Payload) -> - gen_server:call(?MODULE, {reset_topic, {Topic, MaxAge, CT, Payload}}). - -is_topic_existed(Topic) -> - ets:member(?COAP_TOPIC_TABLE, Topic). - -is_topic_timeout(Topic) when is_binary(Topic) -> - [{Topic, MaxAge, _, _, TimeStamp}] = ets:lookup(?COAP_TOPIC_TABLE, Topic), - %% MaxAge: x seconds - MaxAge < ((erlang:system_time(millisecond) - TimeStamp) / 1000). - -lookup_topic_info(Topic) -> - ets:lookup(?COAP_TOPIC_TABLE, Topic). - -lookup_topic_payload(Topic) -> - try ets:lookup_element(?COAP_TOPIC_TABLE, Topic, 4) - catch - error:badarg -> undefined - end. - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([]) -> - _ = ets:new(?COAP_TOPIC_TABLE, [set, named_table, protected]), - ?LOG(debug, "Create the coap_topic table", []), - {ok, #state{}}. - -handle_call({add_topic, {Topic, MaxAge, CT, Payload}}, _From, State) -> - Ret = create_table_element(Topic, MaxAge, CT, Payload), - {reply, {ok, Ret}, State, hibernate}; - -handle_call({reset_topic, {Topic, Payload}}, _From, State) -> - Ret = update_table_element(Topic, Payload), - {reply, {ok, Ret}, State, hibernate}; - -handle_call({reset_topic, {Topic, MaxAge, Payload}}, _From, State) -> - Ret = update_table_element(Topic, MaxAge, Payload), - {reply, {ok, Ret}, State, hibernate}; - -handle_call({reset_topic, {Topic, MaxAge, CT, Payload}}, _From, State) -> - Ret = update_table_element(Topic, MaxAge, CT, Payload), - {reply, {ok, Ret}, State, hibernate}; - -handle_call({remove_topic, {Topic, _Content}}, _From, State) -> - ets:delete(?COAP_TOPIC_TABLE, Topic), - ?LOG(debug, "Remove topic ~p in the coap_topic table", [Topic]), - {reply, ok, State, hibernate}; - -handle_call(Request, _From, State) -> - ?LOG(error, "adapter unexpected call ~p", [Request]), - {reply, ignored, State, hibernate}. - -handle_cast({remove_sub_topics, TopicPrefix}, State) -> - DeletedTopicNum = ets:foldl(fun ({Topic, _, _, _, _}, AccIn) -> - case binary:match(Topic, TopicPrefix) =/= nomatch of - true -> - ?LOG(debug, "Remove topic ~p in the coap_topic table", [Topic]), - ets:delete(?COAP_TOPIC_TABLE, Topic), - AccIn + 1; - false -> - AccIn - end - end, 0, ?COAP_TOPIC_TABLE), - ?LOG(debug, "Remove number of ~p topics with prefix=~p in the coap_topic table", [DeletedTopicNum, TopicPrefix]), - {noreply, State, hibernate}; - -handle_cast(Msg, State) -> - ?LOG(error, "broker_api unexpected cast ~p", [Msg]), - {noreply, State, hibernate}. - -handle_info(Info, State) -> - ?LOG(error, "adapter unexpected info ~p", [Info]), - {noreply, State, hibernate}. - -terminate(Reason, #state{}) -> - ets:delete(?COAP_TOPIC_TABLE), - Level = case Reason =:= normal orelse Reason =:= shutdown of - true -> debug; - false -> error - end, - ?SLOG(Level, #{terminate_reason => Reason}). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - - -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- -create_table_element(Topic, MaxAge, CT, Payload) -> - TopicInfo = {Topic, MaxAge, CT, Payload, erlang:system_time(millisecond)}, - ?LOG(debug, "Insert ~p in the coap_topic table", [TopicInfo]), - ets:insert_new(?COAP_TOPIC_TABLE, TopicInfo). - -update_table_element(Topic, Payload) -> - ?LOG(debug, "Update the topic=~p only with Payload", [Topic]), - ets:update_element(?COAP_TOPIC_TABLE, Topic, [{4, Payload}, {5, erlang:system_time(millisecond)}]). - -update_table_element(Topic, MaxAge, Payload) -> - ?LOG(debug, "Update the topic=~p info of MaxAge=~p and Payload", [Topic, MaxAge]), - ets:update_element(?COAP_TOPIC_TABLE, Topic, [{2, MaxAge}, {4, Payload}, {5, erlang:system_time(millisecond)}]). - -update_table_element(Topic, MaxAge, CT, <<>>) -> - ?LOG(debug, "Update the topic=~p info of MaxAge=~p, CT=~p, payload=<<>>", [Topic, MaxAge, CT]), - ets:update_element(?COAP_TOPIC_TABLE, Topic, [{2, MaxAge}, {3, CT}, {5, erlang:system_time(millisecond)}]). diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index f6c12ad53..596b47547 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -16,69 +16,105 @@ -module(emqx_gateway). +-behaviour(emqx_config_handler). + -include("include/emqx_gateway.hrl"). -%% APIs +%% callbacks for emqx_config_handler +-export([ pre_config_update/2 + , post_config_update/4 + ]). + +%% Gateway APIs -export([ registered_gateway/0 - , create/4 - , remove/1 + , load/2 + , unload/1 , lookup/1 - , update/1 + , update/2 , start/1 , stop/1 , list/0 ]). +-export([update_rawconf/2]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + -spec registered_gateway() -> - [{gateway_type(), emqx_gateway_registry:descriptor()}]. + [{gateway_name(), emqx_gateway_registry:descriptor()}]. registered_gateway() -> emqx_gateway_registry:list(). %%-------------------------------------------------------------------- -%% Gateway Instace APIs +%% Gateway APIs --spec list() -> [instance()]. +-spec list() -> [gateway()]. list() -> - lists:append(lists:map( - fun({_, Insta}) -> Insta end, - emqx_gateway_sup:list_gateway_insta() - )). + emqx_gateway_sup:list_gateway_insta(). --spec create(gateway_type(), binary(), binary(), map()) +-spec load(gateway_name(), emqx_config:config()) -> {ok, pid()} | {error, any()}. -create(Type, Name, Descr, RawConf) -> - Insta = #{ id => clacu_insta_id(Type, Name) - , type => Type - , name => Name - , descr => Descr - , rawconf => RawConf - }, - emqx_gateway_sup:create_gateway_insta(Insta). +load(Name, Config) -> + Gateway = #{ name => Name + , descr => undefined + , config => Config + }, + emqx_gateway_sup:load_gateway(Gateway). --spec remove(instance_id()) -> ok | {error, any()}. -remove(InstaId) -> - emqx_gateway_sup:remove_gateway_insta(InstaId). +-spec unload(gateway_name()) -> ok | {error, not_found}. +unload(Name) -> + emqx_gateway_sup:unload_gateway(Name). --spec lookup(instance_id()) -> instance() | undefined. -lookup(InstaId) -> - emqx_gateway_sup:lookup_gateway_insta(InstaId). +-spec lookup(gateway_name()) -> gateway() | undefined. +lookup(Name) -> + emqx_gateway_sup:lookup_gateway(Name). --spec update(instance()) -> ok | {error, any()}. -update(NewInsta) -> - emqx_gateway_sup:update_gateway_insta(NewInsta). +-spec update(gateway_name(), emqx_config:config()) -> ok | {error, any()}. +update(Name, Config) -> + emqx_gateway_sup:update_gateway(Name, Config). --spec start(instance_id()) -> ok | {error, any()}. -start(InstaId) -> - emqx_gateway_sup:start_gateway_insta(InstaId). +-spec start(gateway_name()) -> ok | {error, any()}. +start(Name) -> + emqx_gateway_sup:start_gateway_insta(Name). --spec stop(instance_id()) -> ok | {error, any()}. -stop(InstaId) -> - emqx_gateway_sup:stop_gateway_insta(InstaId). +-spec stop(gateway_name()) -> ok | {error, any()}. +stop(Name) -> + emqx_gateway_sup:stop_gateway_insta(Name). + +-spec update_rawconf(binary(), emqx_config:raw_config()) + -> ok + | {error, any()}. +update_rawconf(RawName, RawConfDiff) -> + case emqx:update_config([gateway], {RawName, RawConfDiff}) of + {ok, _Result} -> ok; + {error, Reason} -> {error, Reason} + end. + +%%-------------------------------------------------------------------- +%% Config Handler + +-spec pre_config_update(emqx_config:update_request(), + emqx_config:raw_config()) -> + {ok, emqx_config:update_request()} | {error, term()}. +pre_config_update({RawName, RawConfDiff}, RawConf) -> + {ok, emqx_map_lib:deep_merge(RawConf, #{RawName => RawConfDiff})}. + +-spec post_config_update(emqx_config:update_request(), emqx_config:config(), + emqx_config:config(), emqx_config:app_envs()) + -> ok | {ok, Result::any()} | {error, Reason::term()}. +post_config_update({RawName, _}, NewConfig, OldConfig, _AppEnvs) -> + GwName = binary_to_existing_atom(RawName), + SubConf = maps:get(GwName, NewConfig), + case maps:get(GwName, OldConfig, undefined) of + undefined -> + emqx_gateway:load(GwName, SubConf); + _ -> + emqx_gateway:update(GwName, SubConf) + end. %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- - -clacu_insta_id(Type, Name) when is_binary(Name) -> - list_to_atom(lists:concat([Type, "#", binary_to_list(Name)])). diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl new file mode 100644 index 000000000..f264339a4 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -0,0 +1,357 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +%% +-module(emqx_gateway_api). + +-behaviour(minirest_api). + +-import(emqx_gateway_http, + [ return_http_error/2 + ]). + +%% minirest behaviour callbacks +-export([api_spec/0]). + +%% http handlers +-export([ gateway/2 + , gateway_insta/2 + , gateway_insta_stats/2 + ]). + +%%-------------------------------------------------------------------- +%% minirest behaviour callbacks +%%-------------------------------------------------------------------- + +api_spec() -> + {metadata(apis()), []}. + +apis() -> + [ {"/gateway", gateway} + , {"/gateway/:name", gateway_insta} + , {"/gateway/:name/stats", gateway_insta_stats} + ]. +%%-------------------------------------------------------------------- +%% http handlers + +gateway(get, Request) -> + Params = maps:get(query_string, Request, #{}), + Status = case maps:get(<<"status">>, Params, undefined) of + undefined -> all; + S0 -> binary_to_existing_atom(S0, utf8) + end, + {200, emqx_gateway_http:gateways(Status)}. + +gateway_insta(delete, #{bindings := #{name := Name0}}) -> + Name = binary_to_existing_atom(Name0), + case emqx_gateway:unload(Name) of + ok -> + {204}; + {error, not_found} -> + return_http_error(404, <<"Gateway not found">>) + end; +gateway_insta(get, #{bindings := #{name := Name0}}) -> + Name = binary_to_existing_atom(Name0), + case emqx_gateway:lookup(Name) of + #{config := _Config} -> + GwCfs = filled_raw_confs([<<"gateway">>, Name0]), + NGwCfs = GwCfs#{<<"listeners">> => + emqx_gateway_http:mapping_listener_m2l( + Name0, maps:get(<<"listeners">>, GwCfs, #{}) + ) + }, + {200, NGwCfs}; + undefined -> + return_http_error(404, <<"Gateway not found">>) + end; +gateway_insta(put, #{body := RawConfsIn0, + bindings := #{name := Name} + }) -> + RawConfsIn = maps:without([<<"authentication">>, + <<"listeners">>], RawConfsIn0), + %% FIXME: Cluster Consistence ?? + case emqx_gateway:update_rawconf(Name, RawConfsIn) of + ok -> + {200}; + {error, not_found} -> + return_http_error(404, <<"Gateway not found">>); + {error, Reason} -> + return_http_error(500, Reason) + end. + +gateway_insta_stats(get, _Req) -> + return_http_error(401, <<"Implement it later (maybe 5.1)">>). + +filled_raw_confs(Path) -> + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw(Path) + ), + Confs = emqx_map_lib:deep_get(Path, RawConf), + emqx_map_lib:jsonable_map(Confs). + +%%-------------------------------------------------------------------- +%% Swagger defines +%%-------------------------------------------------------------------- + +metadata(APIs) -> + metadata(APIs, []). +metadata([], APIAcc) -> + lists:reverse(APIAcc); +metadata([{Path, Fun}|More], APIAcc) -> + Methods = [get, post, put, delete, patch], + Mds = lists:foldl(fun(M, Acc) -> + try + Acc#{M => swagger(Path, M)} + catch + error : function_clause -> + Acc + end + end, #{}, Methods), + metadata(More, [{Path, Mds, Fun} | APIAcc]). + +swagger("/gateway", get) -> + #{ description => <<"Get gateway list">> + , parameters => params_gateway_status_in_qs() + , responses => + #{ <<"200">> => schema_gateway_overview_list() } + }; +swagger("/gateway/:name", get) -> + #{ description => <<"Get the gateway configurations">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_gateway_conf() + } + }; +swagger("/gateway/:name", delete) -> + #{ description => <<"Delete/Unload the gateway">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }; +swagger("/gateway/:name", put) -> + #{ description => <<"Update the gateway configurations/status">> + , parameters => params_gateway_name_in_path() + , requestBody => schema_gateway_conf() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_no_content() + } + }; +swagger("/gateway/:name/stats", get) -> + #{ description => <<"Get gateway Statistic">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_gateway_stats() + } + }. + +%%-------------------------------------------------------------------- +%% params defines + +params_gateway_name_in_path() -> + [#{ name => name + , in => path + , schema => #{type => string} + , required => true + }]. + +params_gateway_status_in_qs() -> + [#{ name => status + , in => query + , schema => #{type => string} + , required => false + }]. + +%%-------------------------------------------------------------------- +%% schemas + +schema_not_found() -> + emqx_mgmt_util:error_schema(<<"Gateway not found or unloaded">>). + +schema_no_content() -> + #{description => <<"No Content">>}. + +schema_gateway_overview_list() -> + emqx_mgmt_util:array_schema( + #{ type => object + , properties => properties_gateway_overview() + }, + <<"Gateway Overview list">> + ). + +%% XXX: This is whole confs for all type gateways. It is used to fill the +%% default configurations and generate the swagger-schema +%% +%% NOTE: It is a temporary measure to generate swagger-schema +-define(COAP_GATEWAY_CONFS, +#{<<"authentication">> => + #{<<"mechanism">> => <<"password-based">>, + <<"name">> => <<"authenticator1">>, + <<"server_type">> => <<"built-in-database">>, + <<"user_id_type">> => <<"clientid">>}, + <<"enable">> => true, + <<"enable_stats">> => true,<<"heartbeat">> => <<"30s">>, + <<"idle_timeout">> => <<"30s">>, + <<"listeners">> => [ + #{<<"id">> => <<"coap:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"acceptors">> => 8,<<"bind">> => 5683, + <<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240}], + <<"mountpoint">> => <<>>,<<"notify_type">> => <<"qos">>, + <<"publish_qos">> => <<"qos1">>, + <<"subscribe_qos">> => <<"qos0">>} +). + +-define(EXPROTO_GATEWAY_CONFS, +#{<<"enable">> => true, + <<"enable_stats">> => true, + <<"handler">> => + #{<<"address">> => <<"http://127.0.0.1:9001">>}, + <<"idle_timeout">> => <<"30s">>, + <<"listeners">> => [ + #{<<"id">> => <<"exproto:tcp:default">>, + <<"type">> => <<"tcp">>, + <<"running">> => true, + <<"acceptors">> => 8,<<"bind">> => 7993, + <<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240}], + <<"mountpoint">> => <<>>, + <<"server">> => #{<<"bind">> => 9100}} +). + +-define(LWM2M_GATEWAY_CONFS, +#{<<"auto_observe">> => false, + <<"enable">> => true, + <<"enable_stats">> => true, + <<"idle_timeout">> => <<"30s">>, + <<"lifetime_max">> => <<"86400s">>, + <<"lifetime_min">> => <<"1s">>, + <<"listeners">> => [ + #{<<"id">> => <<"lwm2m:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"bind">> => 5783}], + <<"mountpoint">> => <<"lwm2m/%e/">>, + <<"qmode_time_windonw">> => 22, + <<"translators">> => + #{<<"command">> => <<"dn/#">>,<<"notify">> => <<"up/notify">>, + <<"register">> => <<"up/resp">>, + <<"response">> => <<"up/resp">>, + <<"update">> => <<"up/resp">>}, + <<"update_msg_publish_condition">> => + <<"contains_object_list">>, + <<"xml_dir">> => <<"etc/lwm2m_xml">>} +). + +-define(MQTTSN_GATEWAY_CONFS, +#{<<"broadcast">> => true, + <<"clientinfo_override">> => + #{<<"password">> => <<"abc">>, + <<"username">> => <<"mqtt_sn_user">>}, + <<"enable">> => true, + <<"enable_qos3">> => true,<<"enable_stats">> => true, + <<"gateway_id">> => 1,<<"idle_timeout">> => <<"30s">>, + <<"listeners">> => [ + #{<<"id">> => <<"mqttsn:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"bind">> => 1884,<<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240000}], + <<"mountpoint">> => <<>>, + <<"predefined">> => + [#{<<"id">> => 1, + <<"topic">> => <<"/predefined/topic/name/hello">>}, + #{<<"id">> => 2, + <<"topic">> => <<"/predefined/topic/name/nice">>}]} +). + +-define(STOMP_GATEWAY_CONFS, +#{<<"authentication">> => + #{<<"mechanism">> => <<"password-based">>, + <<"name">> => <<"authenticator1">>, + <<"server_type">> => <<"built-in-database">>, + <<"user_id_type">> => <<"clientid">>}, + <<"clientinfo_override">> => + #{<<"password">> => <<"${Packet.headers.passcode}">>, + <<"username">> => <<"${Packet.headers.login}">>}, + <<"enable">> => true, + <<"enable_stats">> => true, + <<"frame">> => + #{<<"max_body_length">> => 8192,<<"max_headers">> => 10, + <<"max_headers_length">> => 1024}, + <<"idle_timeout">> => <<"30s">>, + <<"listeners">> => [ + #{<<"id">> => <<"stomp:tcp:default">>, + <<"type">> => <<"tcp">>, + <<"running">> => true, + <<"acceptors">> => 16,<<"active_n">> => 100, + <<"bind">> => 61613,<<"max_conn_rate">> => 1000, + <<"max_connections">> => 1024000}], + <<"mountpoint">> => <<>>} +). + +%% --- END + +schema_gateway_conf() -> + emqx_mgmt_util:schema( + #{oneOf => + [ emqx_mgmt_api_configs:gen_schema(?STOMP_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?MQTTSN_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?COAP_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?LWM2M_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?EXPROTO_GATEWAY_CONFS) + ]}). + +schema_gateway_stats() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => + #{ a_key => #{type => string} + }}). + +%%-------------------------------------------------------------------- +%% properties + +properties_gateway_overview() -> + ListenerProps = + [ {id, string, + <<"Listener ID">>} + , {running, boolean, + <<"Listener Running status">>} + , {type, string, + <<"Listener Type">>, [<<"tcp">>, <<"ssl">>, <<"udp">>, <<"dtls">>]} + ], + emqx_mgmt_util:properties( + [ {name, string, + <<"Gateway Name">>} + , {status, string, + <<"Gateway Status">>, + [<<"running">>, <<"stopped">>, <<"unloaded">>]} + , {created_at, string, + <<>>} + , {started_at, string, + <<>>} + , {stopped_at, string, + <<>>} + , {max_connections, integer, <<>>} + , {current_connections, integer, <<>>} + , {listeners, {array, object}, ListenerProps} + ]). diff --git a/apps/emqx_bridge_mqtt/include/emqx_bridge_mqtt.hrl b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl similarity index 76% rename from apps/emqx_bridge_mqtt/include/emqx_bridge_mqtt.hrl rename to apps/emqx_gateway/src/emqx_gateway_api_authn.erl index 531518668..85eb4ddc7 100644 --- a/apps/emqx_bridge_mqtt/include/emqx_bridge_mqtt.hrl +++ b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% 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. @@ -13,6 +13,13 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- +%% +-module(emqx_gateway_api_authn). --define(APP, emqx_bridge_mqtt). +-behaviour(minirest_api). +%% minirest behaviour callbacks +-export([api_spec/0]). + +api_spec() -> + {[], []}. diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl new file mode 100644 index 000000000..fcfea7343 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -0,0 +1,624 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_api_clients). + +-behaviour(minirest_api). + +-include_lib("emqx/include/logger.hrl"). + +%% minirest behaviour callbacks +-export([api_spec/0]). + +%% http handlers +-export([ clients/2 + , clients_insta/2 + , subscriptions/2 + ]). + +%% internal exports (for client query) +-export([ query/4 + , format_channel_info/1 + ]). + +-import(emqx_gateway_http, + [ return_http_error/2 + ]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +api_spec() -> + {metadata(apis()), []}. + +apis() -> + [ {"/gateway/:name/clients", clients} + , {"/gateway/:name/clients/:clientid", clients_insta} + , {"/gateway/:name/clients/:clientid/subscriptions", subscriptions} + , {"/gateway/:name/clients/:clientid/subscriptions/:topic", subscriptions} + ]. + +-define(CLIENT_QS_SCHEMA, + [ {<<"node">>, atom} + , {<<"clientid">>, binary} + , {<<"username">>, binary} + , {<<"ip_address">>, ip} + , {<<"conn_state">>, atom} + , {<<"clean_start">>, atom} + , {<<"proto_ver">>, integer} + , {<<"like_clientid">>, binary} + , {<<"like_username">>, binary} + , {<<"gte_created_at">>, timestamp} + , {<<"lte_created_at">>, timestamp} + , {<<"gte_connected_at">>, timestamp} + , {<<"lte_connected_at">>, timestamp} + ]). + +-define(query_fun, {?MODULE, query}). +-define(format_fun, {?MODULE, format_channel_info}). + +clients(get, #{ bindings := #{name := GwName0} + , query_string := Qs + }) -> + GwName = binary_to_existing_atom(GwName0), + TabName = emqx_gateway_cm:tabname(info, GwName), + case maps:get(<<"node">>, Qs, undefined) of + undefined -> + Response = emqx_mgmt_api:cluster_query( + Qs, TabName, + ?CLIENT_QS_SCHEMA, ?query_fun + ), + {200, Response}; + Node1 -> + Node = binary_to_atom(Node1, utf8), + ParamsWithoutNode = maps:without([<<"node">>], Qs), + Response = emqx_mgmt_api:node_query( + Node, ParamsWithoutNode, + TabName, ?CLIENT_QS_SCHEMA, ?query_fun + ), + {200, Response} + end. + +clients_insta(get, #{ bindings := #{name := GwName0, + clientid := ClientId0} + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + + case emqx_gateway_http:lookup_client(GwName, ClientId, + {?MODULE, format_channel_info}) of + [ClientInfo] -> + {200, ClientInfo}; + [ClientInfo|_More] -> + ?LOG(warning, "More than one client info was returned on ~s", + [ClientId]), + {200, ClientInfo}; + [] -> + return_http_error(404, <<"Gateway or ClientId not found">>) + + end; + +clients_insta(delete, #{ bindings := #{name := GwName0, + clientid := ClientId0} + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + _ = emqx_gateway_http:kickout_client(GwName, ClientId), + {200}. + +%% FIXME: +%% List the subscription without mountpoint, but has SubOpts, +%% for example, share group ... +subscriptions(get, #{ bindings := #{name := GwName0, + clientid := ClientId0} + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + case emqx_gateway_http:list_client_subscriptions(GwName, ClientId) of + {error, Reason} -> + return_http_error(404, Reason); + {ok, Subs} -> + {200, Subs} + end; + +%% Create the subscription without mountpoint +subscriptions(post, #{ bindings := #{name := GwName0, + clientid := ClientId0}, + body := Body + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + + case {maps:get(<<"topic">>, Body, undefined), subopts(Body)} of + {undefined, _} -> + %% FIXME: more reasonable error code?? + return_http_error(404, <<"Request paramter missed: topic">>); + {Topic, QoS} -> + case emqx_gateway_http:client_subscribe(GwName, ClientId, Topic, QoS) of + {error, Reason} -> + return_http_error(404, Reason); + ok -> + {200} + end + end; + +%% Remove the subscription without mountpoint +subscriptions(delete, #{ bindings := #{name := GwName0, + clientid := ClientId0, + topic := Topic0 + } + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + Topic = emqx_mgmt_util:urldecode(Topic0), + _ = emqx_gateway_http:client_unsubscribe(GwName, ClientId, Topic), + {200}. + +%%-------------------------------------------------------------------- +%% Utils + +subopts(Req) -> + #{ qos => maps:get(<<"qos">>, Req, 0) + , rap => maps:get(<<"rap">>, Req, 0) + , nl => maps:get(<<"nl">>, Req, 0) + , rh => maps:get(<<"rh">>, Req, 0) + , sub_props => extra_sub_props(maps:get(<<"sub_props">>, Req, #{})) + }. + +extra_sub_props(Props) -> + maps:filter( + fun(_, V) -> V =/= undefined end, + #{subid => maps:get(<<"subid">>, Props, undefined)} + ). + +%%-------------------------------------------------------------------- +%% query funcs + +query(Tab, {Qs, []}, Start, Limit) -> + Ms = qs2ms(Qs), + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, + fun format_channel_info/1); + +query(Tab, {Qs, Fuzzy}, Start, Limit) -> + Ms = qs2ms(Qs), + MatchFun = match_fun(Ms, Fuzzy), + emqx_mgmt_api:traverse_table(Tab, MatchFun, Start, Limit, + fun format_channel_info/1). + +qs2ms(Qs) -> + {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}), + [{{'$1', MtchHead, '_'}, Conds, ['$_']}]. + +qs2ms([], _, {MtchHead, Conds}) -> + {MtchHead, lists:reverse(Conds)}; + +qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) -> + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)), + qs2ms(Rest, N, {NMtchHead, Conds}); +qs2ms([Qs | Rest], N, {MtchHead, Conds}) -> + Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8), + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)), + NConds = put_conds(Qs, Holder, Conds), + qs2ms(Rest, N+1, {NMtchHead, NConds}). + +put_conds({_, Op, V}, Holder, Conds) -> + [{Op, Holder, V} | Conds]; +put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) -> + [{Op2, Holder, V2}, + {Op1, Holder, V1} | Conds]. + +ms(clientid, X) -> + #{clientinfo => #{clientid => X}}; +ms(username, X) -> + #{clientinfo => #{username => X}}; +ms(zone, X) -> + #{clientinfo => #{zone => X}}; +ms(ip_address, X) -> + #{clientinfo => #{peerhost => X}}; +ms(conn_state, X) -> + #{conn_state => X}; +ms(clean_start, X) -> + #{conninfo => #{clean_start => X}}; +ms(proto_ver, X) -> + #{conninfo => #{proto_ver => X}}; +ms(connected_at, X) -> + #{conninfo => #{connected_at => X}}; +ms(created_at, X) -> + #{session => #{created_at => X}}. + +%%-------------------------------------------------------------------- +%% Match funcs + +match_fun(Ms, Fuzzy) -> + MsC = ets:match_spec_compile(Ms), + REFuzzy = lists:map(fun({K, like, S}) -> + {ok, RE} = re:compile(S), + {K, like, RE} + end, Fuzzy), + fun(Rows) -> + case ets:match_spec_run(Rows, MsC) of + [] -> []; + Ls -> + lists:filter(fun(E) -> + run_fuzzy_match(E, REFuzzy) + end, Ls) + end + end. + +run_fuzzy_match(_, []) -> + true; +run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) -> + Val = case maps:get(Key, ClientInfo, "") of + undefined -> ""; + V -> V + end, + re:run(Val, RE, [{capture, none}]) == match andalso run_fuzzy_match(E, Fuzzy). + +%%-------------------------------------------------------------------- +%% format funcs + +format_channel_info({_, Infos, Stats}) -> + ClientInfo = maps:get(clientinfo, Infos, #{}), + ConnInfo = maps:get(conninfo, Infos, #{}), + SessInfo = maps:get(session, Infos, #{}), + FetchX = [ {node, ClientInfo, node()} + , {clientid, ClientInfo} + , {username, ClientInfo} + , {proto_name, ConnInfo} + , {proto_ver, ConnInfo} + , {ip_address, {peername, ConnInfo, fun peer_to_binary/1}} + , {is_bridge, ClientInfo, false} + , {connected_at, + {connected_at, ConnInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {disconnected_at, + {disconnected_at, ConnInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {connected, {conn_state, Infos, fun conn_state_to_connected/1}} + , {keepalive, ClientInfo, 0} + , {clean_start, ConnInfo, true} + , {expiry_interval, ConnInfo, 0} + , {created_at, + {created_at, SessInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {subscriptions_cnt, Stats, 0} + , {subscriptions_max, Stats, infinity} + , {inflight_cnt, Stats, 0} + , {inflight_max, Stats, infinity} + , {mqueue_len, Stats, 0} + , {mqueue_max, Stats, infinity} + , {mqueue_dropped, Stats, 0} + , {awaiting_rel_cnt, Stats, 0} + , {awaiting_rel_max, Stats, infinity} + , {recv_oct, Stats, 0} + , {recv_cnt, Stats, 0} + , {recv_pkt, Stats, 0} + , {recv_msg, Stats, 0} + , {send_oct, Stats, 0} + , {send_cnt, Stats, 0} + , {send_pkt, Stats, 0} + , {send_msg, Stats, 0} + , {mailbox_len, Stats, 0} + , {heap_size, Stats, 0} + , {reductions, Stats, 0} + ], + eval(FetchX). + +eval(Ls) -> + eval(Ls, #{}). +eval([], AccMap) -> + AccMap; +eval([{K, Vx}|More], AccMap) -> + case valuex_get(K, Vx) of + undefined -> eval(More, AccMap#{K => null}); + Value -> eval(More, AccMap#{K => Value}) + end; +eval([{K, Vx, Default}|More], AccMap) -> + case valuex_get(K, Vx) of + undefined -> eval(More, AccMap#{K => Default}); + Value -> eval(More, AccMap#{K => Value}) + end. + +valuex_get(K, Vx) when is_map(Vx); is_list(Vx) -> + key_get(K, Vx); +valuex_get(_K, {InKey, Obj}) when is_map(Obj); is_list(Obj) -> + key_get(InKey, Obj); +valuex_get(_K, {InKey, Obj, MappingFun}) when is_map(Obj); is_list(Obj) -> + case key_get(InKey, Obj) of + undefined -> undefined; + Val -> MappingFun(Val) + end. + +key_get(K, M) when is_map(M) -> + maps:get(K, M, undefined); +key_get(K, L) when is_list(L) -> + proplists:get_value(K, L). + +peer_to_binary({Addr, Port}) -> + AddrBinary = list_to_binary(inet:ntoa(Addr)), + PortBinary = integer_to_binary(Port), + <>; +peer_to_binary(Addr) -> + list_to_binary(inet:ntoa(Addr)). + +conn_state_to_connected(connected) -> true; +conn_state_to_connected(_) -> false. + +%%-------------------------------------------------------------------- +%% Swagger defines +%%-------------------------------------------------------------------- + +metadata(APIs) -> + metadata(APIs, []). +metadata([], APIAcc) -> + lists:reverse(APIAcc); +metadata([{Path, Fun}|More], APIAcc) -> + Methods = [get, post, put, delete, patch], + Mds = lists:foldl(fun(M, Acc) -> + try + Acc#{M => swagger(Path, M)} + catch + error : function_clause -> + Acc + end + end, #{}, Methods), + metadata(More, [{Path, Mds, Fun} | APIAcc]). + +swagger("/gateway/:name/clients", get) -> + #{ description => <<"Get the gateway clients">> + , parameters => params_client_query() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_clients_list() + } + }; +swagger("/gateway/:name/clients/:clientid", get) -> + #{ description => <<"Get the gateway client infomation">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_client() + } + }; +swagger("/gateway/:name/clients/:clientid", delete) -> + #{ description => <<"Kick out the gateway client">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions", get) -> + #{ description => <<"Get the gateway client subscriptions">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_subscription_list() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions", post) -> + #{ description => <<"Get the gateway client subscriptions">> + , parameters => params_client_insta() + , requestBody => schema_subscription() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_no_content() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions/:topic", delete) -> + #{ description => <<"Unsubscribe the topic for client">> + , parameters => params_topic_name_in_path() ++ params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }. + +params_client_query() -> + params_gateway_name_in_path() + ++ params_client_searching_in_qs() + ++ emqx_mgmt_util:page_params(). + +params_client_insta() -> + params_clientid_in_path() + ++ params_gateway_name_in_path(). + +params_client_searching_in_qs() -> + queries( + [ {node, string} + , {clientid, string} + , {username, string} + , {ip_address, string} + , {conn_state, string} + , {proto_ver, string} + , {clean_start, boolean} + , {like_clientid, string} + , {like_username, string} + , {gte_created_at, string} + , {lte_created_at, string} + , {gte_connected_at, string} + , {lte_connected_at, string} + ]). + +params_gateway_name_in_path() -> + [#{ name => name + , in => path + , schema => #{type => string} + , required => true + }]. + +params_clientid_in_path() -> + [#{ name => clientid + , in => path + , schema => #{type => string} + , required => true + }]. + +params_topic_name_in_path() -> + [#{ name => topic + , in => path + , schema => #{type => string} + , required => true + }]. + +queries(Ls) -> + lists:map(fun({K, Type}) -> + #{name => K, in => query, + schema => #{type => Type}, + required => false + } + end, Ls). + +%%-------------------------------------------------------------------- +%% schemas + +schema_not_found() -> + emqx_mgmt_util:error_schema(<<"Gateway not found or unloaded">>). + +schema_no_content() -> + #{description => <<"No Content">>}. + +schema_clients_list() -> + emqx_mgmt_util:page_schema( + #{ type => object + , properties => properties_client() + } + ). + +schema_client() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => properties_client() + }). + +schema_subscription_list() -> + emqx_mgmt_util:array_schema( + #{ type => object + , properties => properties_subscription() + }, + <<"Client subscriptions">> + ). + +schema_subscription() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => properties_subscription() + } + ). + +%%-------------------------------------------------------------------- +%% properties defines + +properties_client() -> + emqx_mgmt_util:properties( + [ {node, string, + <<"Name of the node to which the client is connected">>} + , {clientid, string, + <<"Client identifier">>} + , {username, string, + <<"Username of client when connecting">>} + , {proto_name, string, + <<"Client protocol name">>} + , {proto_ver, string, + <<"Protocol version used by the client">>} + , {ip_address, string, + <<"Client's IP address">>} + , {is_bridge, boolean, + <<"Indicates whether the client is connectedvia bridge">>} + , {connected_at, string, + <<"Client connection time">>} + , {disconnected_at, string, + <<"Client offline time, This field is only valid and returned " + "when connected is false">>} + , {connected, boolean, + <<"Whether the client is connected">>} + %% FIXME: the will_msg attribute is not a general attribute + %% for every protocol. But it should be returned to frontend if someone + %% want it + %% + %, {will_msg, string, + % <<"Client will message">>} + %, {zone, string, + % <<"Indicate the configuration group used by the client">>} + , {keepalive, integer, + <<"keepalive time, with the unit of second">>} + , {clean_start, boolean, + <<"Indicate whether the client is using a brand new session">>} + , {expiry_interval, integer, + <<"Session expiration interval, with the unit of second">>} + , {created_at, string, + <<"Session creation time">>} + , {subscriptions_cnt, integer, + <<"Number of subscriptions established by this client">>} + , {subscriptions_max, integer, + <<"v4 api name [max_subscriptions] Maximum number of " + "subscriptions allowed by this client">>} + , {inflight_cnt, integer, + <<"Current length of inflight">>} + , {inflight_max, integer, + <<"v4 api name [max_inflight]. Maximum length of inflight">>} + , {mqueue_len, integer, + <<"Current length of message queue">>} + , {mqueue_max, integer, + <<"v4 api name [max_mqueue]. Maximum length of message queue">>} + , {mqueue_dropped, integer, + <<"Number of messages dropped by the message queue due to " + "exceeding the length">>} + , {awaiting_rel_cnt, integer, + <<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>} + , {awaiting_rel_max, integer, + <<"v4 api name [max_awaiting_rel]. Maximum allowed number of " + "awaiting PUBREC packet">>} + , {recv_oct, integer, + <<"Number of bytes received by EMQ X Broker (the same below)">>} + , {recv_cnt, integer, + <<"Number of TCP packets received">>} + , {recv_pkt, integer, + <<"Number of MQTT packets received">>} + , {recv_msg, integer, + <<"Number of PUBLISH packets received">>} + , {send_oct, integer, + <<"Number of bytes sent">>} + , {send_cnt, integer, + <<"Number of TCP packets sent">>} + , {send_pkt, integer, + <<"Number of MQTT packets sent">>} + , {send_msg, integer, + <<"Number of PUBLISH packets sent">>} + , {mailbox_len, integer, + <<"Process mailbox size">>} + , {heap_size, integer, + <<"Process heap size with the unit of byte">>} + , {reductions, integer, + <<"Erlang reduction">>} + ]). + +properties_subscription() -> + ExtraProps = [ {subid, string, + <<"Only stomp protocol, an uniquely identity for " + "the subscription. range: 1-65535.">>} + ], + emqx_mgmt_util:properties( + [ {topic, string, + <<"Topic Fillter">>} + , {qos, integer, + <<"QoS level, enum: 0, 1, 2">>} + , {nl, integer, %% FIXME: why not boolean? + <<"No Local option, enum: 0, 1">>} + , {rap, integer, + <<"Retain as Published option, enum: 0, 1">>} + , {rh, integer, + <<"Retain Handling option, enum: 0, 1, 2">>} + , {sub_props, object, ExtraProps} + ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl index f4918e75d..d90942220 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -20,18 +20,22 @@ -include_lib("emqx/include/logger.hrl"). - -export([start/2, stop/1]). +-define(CONF_CALLBACK_MODULE, emqx_gateway). + start(_StartType, _StartArgs) -> {ok, Sup} = emqx_gateway_sup:start_link(), emqx_gateway_cli:load(), load_default_gateway_applications(), - create_gateway_by_default(), + load_gateway_by_default(), + emqx_config_handler:add_handler([gateway], ?CONF_CALLBACK_MODULE), {ok, Sup}. stop(_State) -> emqx_gateway_cli:unload(), + %% XXX: No api now + %emqx_config_handler:remove_handler([gateway], ?MODULE), ok. %%-------------------------------------------------------------------- @@ -40,56 +44,43 @@ stop(_State) -> load_default_gateway_applications() -> Apps = gateway_type_searching(), ?LOG(info, "Starting the default gateway types: ~p", [Apps]), - lists:foreach(fun load/1, Apps). + lists:foreach(fun reg/1, Apps). gateway_type_searching() -> %% FIXME: Hardcoded apps [emqx_stomp_impl, emqx_sn_impl, emqx_exproto_impl, emqx_coap_impl, emqx_lwm2m_impl]. -load(Mod) -> +reg(Mod) -> try - Mod:load(), - ?LOG(info, "Load ~s gateway application successfully!", [Mod]) + Mod:reg(), + ?LOG(info, "Register ~s gateway application successfully!", [Mod]) catch - Class : Reason -> - ?LOG(error, "Load ~s gateway application failed: {~p, ~p}", - [Mod, Class, Reason]) + Class : Reason : Stk -> + ?LOG(error, "Failed to register ~s gateway application: {~p, ~p}\n" + "Stacktrace: ~0p", + [Mod, Class, Reason, Stk]) end. -create_gateway_by_default() -> - create_gateway_by_default(zipped_confs()). +load_gateway_by_default() -> + load_gateway_by_default(confs()). -create_gateway_by_default([]) -> +load_gateway_by_default([]) -> ok; -create_gateway_by_default([{Type, Name, Confs}|More]) -> +load_gateway_by_default([{Type, Confs}|More]) -> case emqx_gateway_registry:lookup(Type) of undefined -> - ?LOG(error, "Skip to start ~s#~s: not_registred_type", - [Type, Name]); + ?LOG(error, "Skip to load ~s gateway, because it is not registered", + [Type]); _ -> - case emqx_gateway:create(Type, - atom_to_binary(Name, utf8), - <<>>, - Confs) of + case emqx_gateway:load(Type, Confs) of {ok, _} -> - ?LOG(debug, "Start ~s#~s successfully!", [Type, Name]); + ?LOG(debug, "Load ~s gateway successfully!", [Type]); {error, Reason} -> - ?LOG(error, "Start ~s#~s failed: ~0p", - [Type, Name, Reason]) + ?LOG(error, "Failed to load ~s gateway: ~0p", [Type, Reason]) end end, - create_gateway_by_default(More). + load_gateway_by_default(More). -zipped_confs() -> - All = maps:to_list( - maps:without(exclude_options(), emqx_config:get([gateway]))), - lists:append(lists:foldr( - fun({Type, Gws}, Acc) -> - {Names, Confs} = lists:unzip(maps:to_list(Gws)), - Types = [ Type || _ <- lists:seq(1, length(Names))], - [lists:zip3(Types, Names, Confs) | Acc] - end, [], All)). - -exclude_options() -> - [lwm2m_xml_dir]. +confs() -> + maps:to_list(emqx:get_config([gateway], #{})). diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl index fa2363370..6ccb444f0 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cli.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -50,30 +50,31 @@ is_cmd(Fun) -> %% Cmds gateway(["list"]) -> - lists:foreach(fun(#{id := InstaId, name := Name, type := Type}) -> - %% FIXME: Get the real running status - emqx_ctl:print("Gateway(~s, name=~s, type=~s, status=running~n", - [InstaId, Name, Type]) + lists:foreach(fun(#{name := Name} = Gateway) -> + %% TODO: More infos: listeners?, connected? + Status = maps:get(status, Gateway, stopped), + emqx_ctl:print("Gateway(name=~s, status=~s)~n", + [Name, Status]) end, emqx_gateway:list()); -gateway(["lookup", GatewayInstaId]) -> - case emqx_gateway:lookup(atom(GatewayInstaId)) of +gateway(["lookup", Name]) -> + case emqx_gateway:lookup(atom(Name)) of undefined -> emqx_ctl:print("undefined~n"); Info -> emqx_ctl:print("~p~n", [Info]) end; -gateway(["stop", GatewayInstaId]) -> - case emqx_gateway:stop(atom(GatewayInstaId)) of +gateway(["stop", Name]) -> + case emqx_gateway:stop(atom(Name)) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) end; -gateway(["start", GatewayInstaId]) -> - case emqx_gateway:start(atom(GatewayInstaId)) of +gateway(["start", Name]) -> + case emqx_gateway:start(atom(Name)) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> @@ -83,61 +84,67 @@ gateway(["start", GatewayInstaId]) -> gateway(_) -> %% TODO: create/remove APIs emqx_ctl:usage([ {"gateway list", - "List all created gateway instances"} - , {"gateway lookup ", - "Looup a gateway detailed informations"} - , {"gateway stop ", - "Stop a gateway instance and release all resources"} - , {"gateway start ", + "List all gateway"} + , {"gateway lookup ", + "Lookup a gateway detailed informations"} + , {"gateway stop ", + "Stop a gateway instance"} + , {"gateway start ", "Start a gateway instance"} ]). 'gateway-registry'(["list"]) -> lists:foreach( - fun({GwType, #{cbkmod := CbMod}}) -> - emqx_ctl:print("Registered Type: ~s, Callback Module: ~s~n", [GwType, CbMod]) + fun({Name, #{cbkmod := CbMod}}) -> + emqx_ctl:print("Registered Name: ~s, Callback Module: ~s~n", [Name, CbMod]) end, emqx_gateway_registry:list()); 'gateway-registry'(_) -> emqx_ctl:usage([ {"gateway-registry list", - "List all registered gateway types"} + "List all registered gateways"} ]). -'gateway-clients'(["list", Type]) -> - InfoTab = emqx_gateway_cm:tabname(info, Type), - dump(InfoTab, client); +'gateway-clients'(["list", Name]) -> + %% FIXME: page me. for example: --limit 100 --page 10 ??? + InfoTab = emqx_gateway_cm:tabname(info, Name), + case ets:info(InfoTab) of + undefined -> + emqx_ctl:print("Bad Gateway Name.~n"); + _ -> + dump(InfoTab, client) + end; -'gateway-clients'(["lookup", Type, ClientId]) -> - ChanTab = emqx_gateway_cm:tabname(chan, Type), +'gateway-clients'(["lookup", Name, ClientId]) -> + ChanTab = emqx_gateway_cm:tabname(chan, Name), case ets:lookup(ChanTab, bin(ClientId)) of [] -> emqx_ctl:print("Not Found.~n"); [Chann] -> - InfoTab = emqx_gateway_cm:tabname(info, Type), + InfoTab = emqx_gateway_cm:tabname(info, Name), [ChannInfo] = ets:lookup(InfoTab, Chann), print({client, ChannInfo}) end; -'gateway-clients'(["kick", Type, ClientId]) -> - case emqx_gateway_cm:kick_session(Type, bin(ClientId)) of +'gateway-clients'(["kick", Name, ClientId]) -> + case emqx_gateway_cm:kick_session(Name, bin(ClientId)) of ok -> emqx_ctl:print("ok~n"); _ -> emqx_ctl:print("Not Found.~n") end; 'gateway-clients'(_) -> - emqx_ctl:usage([ {"gateway-clients list ", - "List all clients for a type of gateway"} - , {"gateway-clients lookup ", + emqx_ctl:usage([ {"gateway-clients list ", + "List all clients for a gateway"} + , {"gateway-clients lookup ", "Lookup the Client Info for specified client"} - , {"gateway-clients kick ", + , {"gateway-clients kick ", "Kick out a client"} ]). -'gateway-metrics'([GatewayType]) -> - Tab = emqx_gateway_metrics:tabname(GatewayType), +'gateway-metrics'([Name]) -> + Tab = emqx_gateway_metrics:tabname(Name), case ets:info(Tab) of undefined -> - emqx_ctl:print("Bad Gateway Type.~n"); + emqx_ctl:print("Bad Gateway Name.~n"); _ -> lists:foreach( fun({K, V}) -> @@ -146,8 +153,8 @@ gateway(_) -> end; 'gateway-metrics'(_) -> - emqx_ctl:usage([ {"gateway-metrics ", - "List all metrics for a type of gateway"} + emqx_ctl:usage([ {"gateway-metrics ", + "List all metrics for a gateway"} ]). atom(Id) -> @@ -190,7 +197,7 @@ print({client, {_, Infos, Stats}}) -> keepalive => SafeGet(keepalive, ConnInfo), subscriptions_cnt => StatsGet(subscriptions_cnt), send_msg => StatsGet(send_msg), - connected => SafeGet(conn_state, ClientInfo) == connected, + connected => SafeGet(conn_state, Infos) == connected, created_at => ConnectedAt, connected_at => ConnectedAt }, diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl index 546640a90..d8b615fe8 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -31,6 +31,7 @@ -export([start_link/1]). -export([ open_session/5 + , open_session/6 , kick_session/2 , kick_session/3 , register_channel/4 @@ -47,6 +48,10 @@ , connection_closed/2 ]). +-export([ with_channel/3 + , lookup_channels/2 + ]). + %% Internal funcs for getting tabname by GatewayId -export([cmtabs/1, tabname/2]). @@ -60,13 +65,13 @@ ]). -record(state, { - type :: atom(), %% Gateway Type - locker :: pid(), %% ClientId Locker for CM - registry :: pid(), %% ClientId Registry server + gwname :: gateway_name(), %% Gateway Name + locker :: pid(), %% ClientId Locker for CM + registry :: pid(), %% ClientId Registry server chan_pmon :: emqx_pmon:pmon() }). --type option() :: {type, gateway_type()}. +-type option() :: {gwname, gateway_name()}. -type options() :: list(option()). -define(T_TAKEOVER, 15000). @@ -78,142 +83,147 @@ -spec start_link(options()) -> {ok, pid()} | ignore | {error, any()}. start_link(Options) -> - Type = proplists:get_value(type, Options), - gen_server:start_link({local, procname(Type)}, ?MODULE, Options, []). + GwName = proplists:get_value(gwname, Options), + gen_server:start_link({local, procname(GwName)}, ?MODULE, Options, []). -procname(Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_cm'])). +procname(GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_cm'])). --spec cmtabs(Type :: atom()) -> {ChanTab :: atom(), - ConnTab :: atom(), - ChannInfoTab :: atom()}. -cmtabs(Type) -> - { tabname(chan, Type) %% Client Tabname; Record: {ClientId, Pid} - , tabname(conn, Type) %% Client ConnMod; Recrod: {{ClientId, Pid}, ConnMod} - , tabname(info, Type) %% ClientInfo Tabname; Record: {{ClientId, Pid}, ClientInfo, ClientStats} +-spec cmtabs(GwName :: gateway_name()) + -> {ChanTab :: atom(), + ConnTab :: atom(), + ChannInfoTab :: atom()}. +cmtabs(GwName) -> + { tabname(chan, GwName) %% Client Tabname; Record: {ClientId, Pid} + , tabname(conn, GwName) %% Client ConnMod; Recrod: {{ClientId, Pid}, ConnMod} + , tabname(info, GwName) %% ClientInfo Tabname; Record: {{ClientId, Pid}, ClientInfo, ClientStats} }. -tabname(chan, Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_channel'])); -tabname(conn, Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_conn'])); -tabname(info, Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_info'])). +tabname(chan, GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_channel'])); +tabname(conn, GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_channel_conn'])); +tabname(info, GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_channel_info'])). -lockername(Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_locker'])). +lockername(GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_locker'])). --spec register_channel(atom(), binary(), pid(), emqx_types:conninfo()) -> ok. -register_channel(Type, ClientId, ChanPid, #{conn_mod := ConnMod}) when is_pid(ChanPid) -> +-spec register_channel(gateway_name(), + emqx_types:clientid(), + pid(), + emqx_types:conninfo()) -> ok. +register_channel(GwName, ClientId, ChanPid, #{conn_mod := ConnMod}) when is_pid(ChanPid) -> Chan = {ClientId, ChanPid}, - true = ets:insert(tabname(chan, Type), Chan), - true = ets:insert(tabname(conn, Type), {Chan, ConnMod}), - ok = emqx_gateway_cm_registry:register_channel(Type, Chan), - cast(procname(Type), {registered, Chan}). + true = ets:insert(tabname(chan, GwName), Chan), + true = ets:insert(tabname(conn, GwName), {Chan, ConnMod}), + ok = emqx_gateway_cm_registry:register_channel(GwName, Chan), + cast(procname(GwName), {registered, Chan}). %% @doc Unregister a channel. --spec unregister_channel(atom(), emqx_types:clientid()) -> ok. -unregister_channel(Type, ClientId) when is_binary(ClientId) -> - true = do_unregister_channel(Type, {ClientId, self()}, cmtabs(Type)), +-spec unregister_channel(gateway_name(), emqx_types:clientid()) -> ok. +unregister_channel(GwName, ClientId) when is_binary(ClientId) -> + true = do_unregister_channel(GwName, {ClientId, self()}, cmtabs(GwName)), ok. %% @doc Insert/Update the channel info and stats --spec insert_channel_info(atom(), +-spec insert_channel_info(gateway_name(), emqx_types:clientid(), emqx_types:infos(), emqx_types:stats()) -> ok. -insert_channel_info(Type, ClientId, Info, Stats) -> +insert_channel_info(GwName, ClientId, Info, Stats) -> Chan = {ClientId, self()}, - true = ets:insert(tabname(info, Type), {Chan, Info, Stats}), + true = ets:insert(tabname(info, GwName), {Chan, Info, Stats}), %%?tp(debug, insert_channel_info, #{client_id => ClientId}), ok. %% @doc Get info of a channel. --spec get_chan_info(gateway_type(), emqx_types:clientid()) +-spec get_chan_info(gateway_name(), emqx_types:clientid()) -> emqx_types:infos() | undefined. -get_chan_info(Type, ClientId) -> - with_channel(Type, ClientId, +get_chan_info(GwName, ClientId) -> + with_channel(GwName, ClientId, fun(ChanPid) -> - get_chan_info(Type, ClientId, ChanPid) + get_chan_info(GwName, ClientId, ChanPid) end). --spec get_chan_info(gateway_type(), emqx_types:clientid(), pid()) +-spec get_chan_info(gateway_name(), emqx_types:clientid(), pid()) -> emqx_types:infos() | undefined. -get_chan_info(Type, ClientId, ChanPid) when node(ChanPid) == node() -> +get_chan_info(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try ets:lookup_element(tabname(info, Type), Chan, 2) + try ets:lookup_element(tabname(info, GwName), Chan, 2) catch error:badarg -> undefined end; -get_chan_info(Type, ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_chan_info, [Type, ClientId, ChanPid]). +get_chan_info(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_info, [GwName, ClientId, ChanPid]). %% @doc Update infos of the channel. --spec set_chan_info(gateway_type(), +-spec set_chan_info(gateway_name(), emqx_types:clientid(), emqx_types:infos()) -> boolean(). -set_chan_info(Type, ClientId, Infos) -> - set_chan_info(Type, ClientId, self(), Infos). +set_chan_info(GwName, ClientId, Infos) -> + set_chan_info(GwName, ClientId, self(), Infos). --spec set_chan_info(gateway_type(), +-spec set_chan_info(gateway_name(), emqx_types:clientid(), pid(), emqx_types:infos()) -> boolean(). -set_chan_info(Type, ClientId, ChanPid, Infos) when node(ChanPid) == node() -> +set_chan_info(GwName, ClientId, ChanPid, Infos) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try ets:update_element(tabname(info, Type), Chan, {2, Infos}) + try ets:update_element(tabname(info, GwName), Chan, {2, Infos}) catch error:badarg -> false end; -set_chan_info(Type, ClientId, ChanPid, Infos) -> - rpc_call(node(ChanPid), set_chan_info, [Type, ClientId, ChanPid, Infos]). +set_chan_info(GwName, ClientId, ChanPid, Infos) -> + rpc_call(node(ChanPid), set_chan_info, [GwName, ClientId, ChanPid, Infos]). %% @doc Get channel's stats. --spec get_chan_stats(gateway_type(), emqx_types:clientid()) +-spec get_chan_stats(gateway_name(), emqx_types:clientid()) -> emqx_types:stats() | undefined. -get_chan_stats(Type, ClientId) -> - with_channel(Type, ClientId, +get_chan_stats(GwName, ClientId) -> + with_channel(GwName, ClientId, fun(ChanPid) -> - get_chan_stats(Type, ClientId, ChanPid) + get_chan_stats(GwName, ClientId, ChanPid) end). --spec get_chan_stats(gateway_type(), emqx_types:clientid(), pid()) +-spec get_chan_stats(gateway_name(), emqx_types:clientid(), pid()) -> emqx_types:stats() | undefined. -get_chan_stats(Type, ClientId, ChanPid) when node(ChanPid) == node() -> +get_chan_stats(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try ets:lookup_element(tabname(info, Type), Chan, 3) + try ets:lookup_element(tabname(info, GwName), Chan, 3) catch error:badarg -> undefined end; -get_chan_stats(Type, ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_chan_stats, [Type, ClientId, ChanPid]). +get_chan_stats(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_stats, [GwName, ClientId, ChanPid]). --spec set_chan_stats(gateway_type(), +-spec set_chan_stats(gateway_name(), emqx_types:clientid(), emqx_types:stats()) -> boolean(). -set_chan_stats(Type, ClientId, Stats) -> - set_chan_stats(Type, ClientId, self(), Stats). +set_chan_stats(GwName, ClientId, Stats) -> + set_chan_stats(GwName, ClientId, self(), Stats). --spec set_chan_stats(gateway_type(), +-spec set_chan_stats(gateway_name(), emqx_types:clientid(), pid(), emqx_types:stats()) -> boolean(). -set_chan_stats(Type, ClientId, ChanPid, Stats) when node(ChanPid) == node() -> +set_chan_stats(GwName, ClientId, ChanPid, Stats) when node(ChanPid) == node() -> Chan = {ClientId, self()}, - try ets:update_element(tabname(info, Type), Chan, {3, Stats}) + try ets:update_element(tabname(info, GwName), Chan, {3, Stats}) catch error:badarg -> false end; -set_chan_stats(Type, ClientId, ChanPid, Stats) -> - rpc_call(node(ChanPid), set_chan_stats, [Type, ClientId, ChanPid, Stats]). +set_chan_stats(GwName, ClientId, ChanPid, Stats) -> + rpc_call(node(ChanPid), set_chan_stats, [GwName, ClientId, ChanPid, Stats]). --spec connection_closed(gateway_type(), emqx_types:clientid()) -> true. -connection_closed(Type, ClientId) -> +-spec connection_closed(gateway_name(), emqx_types:clientid()) -> true. +connection_closed(GwName, ClientId) -> %% XXX: Why we need to delete conn_mod tab ??? Chan = {ClientId, self()}, - ets:delete_object(tabname(conn, Type), Chan). + ets:delete_object(tabname(conn, GwName), Chan). --spec open_session(Type :: atom(), CleanStart :: boolean(), +-spec open_session(GwName :: gateway_name(), + CleanStart :: boolean(), ClientInfo :: emqx_types:clientinfo(), ConnInfo :: emqx_types:conninfo(), CreateSessionFun :: fun((emqx_types:clientinfo(), @@ -225,37 +235,41 @@ connection_closed(Type, ClientId) -> }} | {error, any()}. -open_session(Type, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> +open_session(GwName, CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + open_session(GwName, CleanStart, ClientInfo, ConnInfo, CreateSessionFun, emqx_session). + +open_session(GwName, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> Self = self(), ClientId = maps:get(clientid, ClientInfo), Fun = fun(_) -> - ok = discard_session(Type, ClientId), - Session = create_session(Type, - ClientInfo, - ConnInfo, - CreateSessionFun - ), - register_channel(Type, ClientId, Self, ConnInfo), - {ok, #{session => Session, present => false}} + ok = discard_session(GwName, ClientId), + Session = create_session(GwName, + ClientInfo, + ConnInfo, + CreateSessionFun, + SessionMod + ), + register_channel(GwName, ClientId, Self, ConnInfo), + {ok, #{session => Session, present => false}} end, - locker_trans(Type, ClientId, Fun); + locker_trans(GwName, ClientId, Fun); open_session(_Type, false = _CleanStart, - _ClientInfo, _ConnInfo, _CreateSessionFun) -> + _ClientInfo, _ConnInfo, _CreateSessionFun, _SessionMod) -> %% TODO: {error, not_supported_now}. %% @private -create_session(Type, ClientInfo, ConnInfo, CreateSessionFun) -> +create_session(GwName, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> try Session = emqx_gateway_utils:apply( CreateSessionFun, [ClientInfo, ConnInfo] ), - ok = emqx_gateway_metrics:inc(Type, 'session.created'), + ok = emqx_gateway_metrics:inc(GwName, 'session.created'), SessionInfo = case is_tuple(Session) andalso element(1, Session) == session of - true -> emqx_session:info(Session); + true -> SessionMod:info(Session); _ -> case is_map(Session) of false -> @@ -274,17 +288,17 @@ create_session(Type, ClientInfo, ConnInfo, CreateSessionFun) -> end. %% @doc Discard all the sessions identified by the ClientId. --spec discard_session(Type :: atom(), binary()) -> ok. -discard_session(Type, ClientId) when is_binary(ClientId) -> - case lookup_channels(Type, ClientId) of +-spec discard_session(GwName :: gateway_name(), binary()) -> ok. +discard_session(GwName, ClientId) when is_binary(ClientId) -> + case lookup_channels(GwName, ClientId) of [] -> ok; - ChanPids -> lists:foreach(fun(Pid) -> do_discard_session(Type, ClientId, Pid) end, ChanPids) + ChanPids -> lists:foreach(fun(Pid) -> do_discard_session(GwName, ClientId, Pid) end, ChanPids) end. %% @private -do_discard_session(Type, ClientId, Pid) -> +do_discard_session(GwName, ClientId, Pid) -> try - discard_session(Type, ClientId, Pid) + discard_session(GwName, ClientId, Pid) catch _ : noproc -> % emqx_ws_connection: call %?tp(debug, "session_already_gone", #{pid => Pid}), @@ -302,72 +316,72 @@ do_discard_session(Type, ClientId, Pid) -> end. %% @private -discard_session(Type, ClientId, ChanPid) when node(ChanPid) == node() -> - case get_chann_conn_mod(Type, ClientId, ChanPid) of +discard_session(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chann_conn_mod(GwName, ClientId, ChanPid) of undefined -> ok; ConnMod when is_atom(ConnMod) -> ConnMod:call(ChanPid, discard, ?T_TAKEOVER) end; %% @private -discard_session(Type, ClientId, ChanPid) -> - rpc_call(node(ChanPid), discard_session, [Type, ClientId, ChanPid]). +discard_session(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), discard_session, [GwName, ClientId, ChanPid]). --spec kick_session(gateway_type(), emqx_types:clientid()) +-spec kick_session(gateway_name(), emqx_types:clientid()) -> {error, any()} | ok. -kick_session(Type, ClientId) -> - case lookup_channels(Type, ClientId) of +kick_session(GwName, ClientId) -> + case lookup_channels(GwName, ClientId) of [] -> {error, not_found}; [ChanPid] -> - kick_session(Type, ClientId, ChanPid); + kick_session(GwName, ClientId, ChanPid); ChanPids -> [ChanPid|StalePids] = lists:reverse(ChanPids), ?LOG(error, "More than one channel found: ~p", [ChanPids]), lists:foreach(fun(StalePid) -> - catch discard_session(Type, ClientId, StalePid) + catch discard_session(GwName, ClientId, StalePid) end, StalePids), - kick_session(Type, ClientId, ChanPid) + kick_session(GwName, ClientId, ChanPid) end. -kick_session(Type, ClientId, ChanPid) when node(ChanPid) == node() -> - case get_chan_info(Type, ClientId, ChanPid) of +kick_session(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chan_info(GwName, ClientId, ChanPid) of #{conninfo := #{conn_mod := ConnMod}} -> ConnMod:call(ChanPid, kick, ?T_TAKEOVER); undefined -> {error, not_found} end; -kick_session(Type, ClientId, ChanPid) -> - rpc_call(node(ChanPid), kick_session, [Type, ClientId, ChanPid]). +kick_session(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), kick_session, [GwName, ClientId, ChanPid]). -with_channel(Type, ClientId, Fun) -> - case lookup_channels(Type, ClientId) of +with_channel(GwName, ClientId, Fun) -> + case lookup_channels(GwName, ClientId) of [] -> undefined; [Pid] -> Fun(Pid); Pids -> Fun(lists:last(Pids)) end. %% @doc Lookup channels. --spec(lookup_channels(atom(), emqx_types:clientid()) -> list(pid())). -lookup_channels(Type, ClientId) -> - emqx_gateway_cm_registry:lookup_channels(Type, ClientId). +-spec(lookup_channels(gateway_name(), emqx_types:clientid()) -> list(pid())). +lookup_channels(GwName, ClientId) -> + emqx_gateway_cm_registry:lookup_channels(GwName, ClientId). -get_chann_conn_mod(Type, ClientId, ChanPid) when node(ChanPid) == node() -> +get_chann_conn_mod(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try [ConnMod] = ets:lookup_element(tabname(conn, Type), Chan, 2), ConnMod + try [ConnMod] = ets:lookup_element(tabname(conn, GwName), Chan, 2), ConnMod catch error:badarg -> undefined end; -get_chann_conn_mod(Type, ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_chann_conn_mod, [Type, ClientId, ChanPid]). +get_chann_conn_mod(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chann_conn_mod, [GwName, ClientId, ChanPid]). %% Locker locker_trans(_Type, undefined, Fun) -> Fun([]); -locker_trans(Type, ClientId, Fun) -> - Locker = lockername(Type), +locker_trans(GwName, ClientId, Fun) -> + Locker = lockername(GwName), case locker_lock(Locker, ClientId) of {true, Nodes} -> try Fun(Nodes) after locker_unlock(Locker, ClientId) end; @@ -396,27 +410,27 @@ cast(Name, Msg) -> %%-------------------------------------------------------------------- init(Options) -> - Type = proplists:get_value(type, Options), + GwName = proplists:get_value(gwname, Options), TabOpts = [public, {write_concurrency, true}], - {ChanTab, ConnTab, InfoTab} = cmtabs(Type), + {ChanTab, ConnTab, InfoTab} = cmtabs(GwName), ok = emqx_tables:new(ChanTab, [bag, {read_concurrency, true}|TabOpts]), ok = emqx_tables:new(ConnTab, [bag | TabOpts]), ok = emqx_tables:new(InfoTab, [set, compressed | TabOpts]), %% Start link cm-registry process %% XXX: Should I hang it under a higher level supervisor? - {ok, Registry} = emqx_gateway_cm_registry:start_link(Type), + {ok, Registry} = emqx_gateway_cm_registry:start_link(GwName), %% Start locker process - {ok, Locker} = ekka_locker:start_link(lockername(Type)), + {ok, Locker} = ekka_locker:start_link(lockername(GwName)), %% Interval update stats %% TODO: v0.2 %ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0), - {ok, #state{type = Type, + {ok, #state{gwname = GwName, locker = Locker, registry = Registry, chan_pmon = emqx_pmon:new()}}. @@ -433,12 +447,12 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({'DOWN', _MRef, process, Pid, _Reason}, - State = #state{type = Type, chan_pmon = PMon}) -> + State = #state{gwname = GwName, chan_pmon = PMon}) -> ChanPids = [Pid | emqx_misc:drain_down(?DEFAULT_BATCH_SIZE)], {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), - CmTabs = cmtabs(Type), - ok = emqx_pool:async_submit(fun do_unregister_channel_task/3, [Items, Type, CmTabs]), + CmTabs = cmtabs(GwName), + ok = emqx_pool:async_submit(fun do_unregister_channel_task/3, [Items, GwName, CmTabs]), {noreply, State#state{chan_pmon = PMon1}}; handle_info(_Info, State) -> @@ -450,18 +464,18 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -do_unregister_channel_task(Items, Type, CmTabs) -> +do_unregister_channel_task(Items, GwName, CmTabs) -> lists:foreach( fun({ChanPid, ClientId}) -> - do_unregister_channel(Type, {ClientId, ChanPid}, CmTabs) + do_unregister_channel(GwName, {ClientId, ChanPid}, CmTabs) end, Items). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -do_unregister_channel(Type, Chan, {ChanTab, ConnTab, InfoTab}) -> - ok = emqx_gateway_cm_registry:unregister_channel(Type, Chan), +do_unregister_channel(GwName, Chan, {ChanTab, ConnTab, InfoTab}) -> + ok = emqx_gateway_cm_registry:unregister_channel(GwName, Chan), true = ets:delete(ConnTab, Chan), true = ets:delete(InfoTab, Chan), ets:delete_object(ChanTab, Chan). diff --git a/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl index 2c449828e..1d9daa637 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl @@ -40,6 +40,8 @@ , code_change/3 ]). +-include_lib("emqx/include/emqx.hrl"). + -define(LOCK, {?MODULE, cleanup_down}). -record(channel, {chid, pid}). @@ -89,6 +91,7 @@ init([Type]) -> Tab = tabname(Type), ok = ekka_mnesia:create_table(Tab, [ {type, bag}, + {rlog_shard, ?CM_SHARD}, {ram_copies, [node()]}, {record_name, channel}, {attributes, record_info(fields, channel)}, diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index 406de7767..8022c3797 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -27,10 +27,8 @@ %% configuration, register devices and other common operations. %% -type context() :: - #{ %% Gateway Instance ID - instid := instance_id() - %% Gateway ID - , type := gateway_type() + #{ %% Gateway Name + gwname := gateway_name() %% Autenticator , auth := emqx_authn:chain_id() | undefined %% The ConnectionManager PID @@ -40,6 +38,7 @@ %% Authentication circle -export([ authenticate/2 , open_session/5 + , open_session/6 , insert_channel_info/4 , set_chan_info/3 , set_chan_stats/3 @@ -70,11 +69,11 @@ authenticate(_Ctx = #{auth := undefined}, ClientInfo) -> authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) -> ClientInfo = ClientInfo0#{ zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, chain_id => ChainId }, case emqx_access_control:authenticate(ClientInfo) of - ok -> + {ok, _} -> {ok, mountpoint(ClientInfo)}; {error, Reason} -> {error, Reason} @@ -96,51 +95,56 @@ authenticate(_Ctx, ClientInfo) -> pendings => list() }} | {error, any()}. -open_session(Ctx, false, ClientInfo, ConnInfo, CreateSessionFun) -> +open_session(Ctx, CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + open_session(Ctx, CleanStart, ClientInfo, ConnInfo, + CreateSessionFun, emqx_session). + +open_session(Ctx, false, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> logger:warning("clean_start=false is not supported now, " "fallback to clean_start mode"), - open_session(Ctx, true, ClientInfo, ConnInfo, CreateSessionFun); + open_session(Ctx, true, ClientInfo, ConnInfo, CreateSessionFun, SessionMod); -open_session(_Ctx = #{type := Type}, - CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> - emqx_gateway_cm:open_session(Type, CleanStart, - ClientInfo, ConnInfo, CreateSessionFun). +open_session(_Ctx = #{gwname := GwName}, + CleanStart, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> + emqx_gateway_cm:open_session(GwName, CleanStart, + ClientInfo, ConnInfo, + CreateSessionFun, SessionMod). -spec insert_channel_info(context(), emqx_types:clientid(), emqx_types:infos(), emqx_types:stats()) -> ok. -insert_channel_info(_Ctx = #{type := Type}, ClientId, Infos, Stats) -> - emqx_gateway_cm:insert_channel_info(Type, ClientId, Infos, Stats). +insert_channel_info(_Ctx = #{gwname := GwName}, ClientId, Infos, Stats) -> + emqx_gateway_cm:insert_channel_info(GwName, ClientId, Infos, Stats). %% @doc Set the Channel Info to the ConnectionManager for this client -spec set_chan_info(context(), emqx_types:clientid(), emqx_types:infos()) -> boolean(). -set_chan_info(_Ctx = #{type := Type}, ClientId, Infos) -> - emqx_gateway_cm:set_chan_info(Type, ClientId, Infos). +set_chan_info(_Ctx = #{gwname := GwName}, ClientId, Infos) -> + emqx_gateway_cm:set_chan_info(GwName, ClientId, Infos). -spec set_chan_stats(context(), emqx_types:clientid(), emqx_types:stats()) -> boolean(). -set_chan_stats(_Ctx = #{type := Type}, ClientId, Stats) -> - emqx_gateway_cm:set_chan_stats(Type, ClientId, Stats). +set_chan_stats(_Ctx = #{gwname := GwName}, ClientId, Stats) -> + emqx_gateway_cm:set_chan_stats(GwName, ClientId, Stats). -spec connection_closed(context(), emqx_types:clientid()) -> boolean(). -connection_closed(_Ctx = #{type := Type}, ClientId) -> - emqx_gateway_cm:connection_closed(Type, ClientId). +connection_closed(_Ctx = #{gwname := GwName}, ClientId) -> + emqx_gateway_cm:connection_closed(GwName, ClientId). -spec authorize(context(), emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) - -> allow | deny. + -> allow | deny. authorize(_Ctx, ClientInfo, PubSub, Topic) -> emqx_access_control:authorize(ClientInfo, PubSub, Topic). -metrics_inc(_Ctx = #{type := Type}, Name) -> - emqx_gateway_metrics:inc(Type, Name). +metrics_inc(_Ctx = #{gwname := GwName}, Name) -> + emqx_gateway_metrics:inc(GwName, Name). -metrics_inc(_Ctx = #{type := Type}, Name, Oct) -> - emqx_gateway_metrics:inc(Type, Name, Oct). +metrics_inc(_Ctx = #{gwname := GwName}, Name, Oct) -> + emqx_gateway_metrics:inc(GwName, Name, Oct). %%-------------------------------------------------------------------- %% Internal funcs diff --git a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl index 21ad30c0d..d56c8783b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl @@ -15,6 +15,10 @@ %%-------------------------------------------------------------------- %% @doc The Gateway Top supervisor. +%% +%% This supervisor has monitor a bunch of process/resources depended by +%% gateway runtime +%% -module(emqx_gateway_gw_sup). -behaviour(supervisor). @@ -25,7 +29,7 @@ -export([ create_insta/3 , remove_insta/2 - , update_insta/2 + , update_insta/3 , start_insta/2 , stop_insta/2 , list_insta/1 @@ -38,92 +42,89 @@ %% APIs %%-------------------------------------------------------------------- -start_link(Type) -> - supervisor:start_link({local, Type}, ?MODULE, [Type]). +start_link(GwName) -> + supervisor:start_link({local, GwName}, ?MODULE, [GwName]). --spec create_insta(pid(), instance(), map()) -> {ok, GwInstaPid :: pid()} | {error, any()}. -create_insta(Sup, Insta = #{id := InstaId}, GwDscrptr) -> - case emqx_gateway_utils:find_sup_child(Sup, InstaId) of +-spec create_insta(pid(), gateway(), map()) -> {ok, GwInstaPid :: pid()} | {error, any()}. +create_insta(Sup, Gateway = #{name := Name}, GwDscrptr) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of {ok, _GwInstaPid} -> {error, alredy_existed}; false -> - %% XXX: More instances options to it? - %% - Ctx = ctx(Sup, InstaId), + Ctx = ctx(Sup, Name), %% ChildSpec = emqx_gateway_utils:childspec( - InstaId, + Name, worker, emqx_gateway_insta_sup, - [Insta, Ctx, GwDscrptr] + [Gateway, Ctx, GwDscrptr] ), emqx_gateway_utils:supervisor_ret( supervisor:start_child(Sup, ChildSpec) ) end. --spec remove_insta(pid(), InstaId :: atom()) -> ok | {error, any()}. -remove_insta(Sup, InstaId) -> - case emqx_gateway_utils:find_sup_child(Sup, InstaId) of +-spec remove_insta(pid(), Name :: gateway_name()) -> ok | {error, any()}. +remove_insta(Sup, Name) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> ok; {ok, _GwInstaPid} -> - ok = supervisor:terminate_child(Sup, InstaId), - ok = supervisor:delete_child(Sup, InstaId) + ok = supervisor:terminate_child(Sup, Name), + ok = supervisor:delete_child(Sup, Name) end. --spec update_insta(pid(), NewInsta :: instance()) -> ok | {error, any()}. -update_insta(Sup, NewInsta = #{id := InstaId}) -> - case emqx_gateway_utils:find_sup_child(Sup, InstaId) of +-spec update_insta(pid(), gateway_name(), emqx_config:config()) + -> ok | {error, any()}. +update_insta(Sup, Name, Config) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> {error, not_found}; {ok, GwInstaPid} -> - emqx_gateway_insta_sup:update(GwInstaPid, NewInsta) + emqx_gateway_insta_sup:update(GwInstaPid, Config) end. --spec start_insta(pid(), atom()) -> ok | {error, any()}. -start_insta(Sup, InstaId) -> - case emqx_gateway_utils:find_sup_child(Sup, InstaId) of +-spec start_insta(pid(), gateway_name()) -> ok | {error, any()}. +start_insta(Sup, Name) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> {error, not_found}; {ok, GwInstaPid} -> emqx_gateway_insta_sup:enable(GwInstaPid) end. --spec stop_insta(pid(), atom()) -> ok | {error, any()}. -stop_insta(Sup, InstaId) -> - case emqx_gateway_utils:find_sup_child(Sup, InstaId) of +-spec stop_insta(pid(), gateway_name()) -> ok | {error, any()}. +stop_insta(Sup, Name) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> {error, not_found}; {ok, GwInstaPid} -> emqx_gateway_insta_sup:disable(GwInstaPid) end. --spec list_insta(pid()) -> [instance()]. +-spec list_insta(pid()) -> [gateway()]. list_insta(Sup) -> lists:filtermap( - fun({InstaId, GwInstaPid, _Type, _Mods}) -> - is_gateway_insta_id(InstaId) + fun({Name, GwInstaPid, _Type, _Mods}) -> + is_gateway_insta_id(Name) andalso {true, emqx_gateway_insta_sup:info(GwInstaPid)} end, supervisor:which_children(Sup)). %% Supervisor callback %% @doc Initialize Top Supervisor for a Protocol -init([Type]) -> +init([GwName]) -> SupFlags = #{ strategy => one_for_one , intensity => 10 , period => 60 }, - CmOpts = [{type, Type}], + CmOpts = [{gwname, GwName}], CM = emqx_gateway_utils:childspec(worker, emqx_gateway_cm, [CmOpts]), - Metrics = emqx_gateway_utils:childspec(worker, emqx_gateway_metrics, [Type]), + Metrics = emqx_gateway_utils:childspec(worker, emqx_gateway_metrics, [GwName]), {ok, {SupFlags, [CM, Metrics]}}. %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -ctx(Sup, InstaId) -> - {_, Type} = erlang:process_info(Sup, registered_name), +ctx(Sup, Name) -> {ok, CM} = emqx_gateway_utils:find_sup_child(Sup, emqx_gateway_cm), - #{ instid => InstaId - , type => Type + #{ gwname => Name , cm => CM }. diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl new file mode 100644 index 000000000..f233a6151 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -0,0 +1,270 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +%% @doc Gateway Interface Module for HTTP-APIs +-module(emqx_gateway_http). + +-include("include/emqx_gateway.hrl"). +-include_lib("emqx/include/logger.hrl"). + +%% Mgmt APIs - gateway +-export([ gateways/1 + ]). + +%% Mgmt APIs - listeners +-export([ listeners/1 + , listener/2 + , mapping_listener_m2l/2 + ]). + +%% Mgmt APIs - clients +-export([ lookup_client/3 + , lookup_client/4 + , kickout_client/2 + , kickout_client/3 + , list_client_subscriptions/2 + , client_subscribe/4 + , client_unsubscribe/3 + ]). + +%% Utils for http, swagger, etc. +-export([ return_http_error/2 + ]). + +-type gateway_summary() :: + #{ name := binary() + , status := running | stopped | unloaded + , started_at => binary() + , max_connections => integer() + , current_connections => integer() + , listeners => [] + }. + +-define(DEFAULT_CALL_TIMEOUT, 15000). + +%%-------------------------------------------------------------------- +%% Mgmt APIs - gateway +%%-------------------------------------------------------------------- + +-spec gateways(Status :: all | running | stopped | unloaded) + -> [gateway_summary()]. +gateways(Status) -> + Gateways = lists:map(fun({GwName, _}) -> + case emqx_gateway:lookup(GwName) of + undefined -> #{name => GwName, status => unloaded}; + GwInfo = #{config := Config} -> + GwInfo0 = emqx_gateway_utils:unix_ts_to_rfc3339( + [created_at, started_at, stopped_at], + GwInfo), + GwInfo1 = maps:with([name, + status, + created_at, + started_at, + stopped_at], GwInfo0), + GwInfo1#{ + max_connections => max_connections_count(Config), + current_connections => current_connections_count(GwName), + listeners => get_listeners_status(GwName, Config)} + end + end, emqx_gateway_registry:list()), + case Status of + all -> Gateways; + _ -> + [Gw || Gw = #{status := S} <- Gateways, S == Status] + end. + +%% @private +max_connections_count(Config) -> + Listeners = emqx_gateway_utils:normalize_config(Config), + lists:foldl(fun({_, _, _, SocketOpts, _}, Acc) -> + Acc + proplists:get_value(max_connections, SocketOpts, 0) + end, 0, Listeners). + +%% @private +current_connections_count(GwName) -> + try + InfoTab = emqx_gateway_cm:tabname(info, GwName), + ets:info(InfoTab, size) + catch _ : _ -> + 0 + end. + +%% @private +get_listeners_status(GwName, Config) -> + Listeners = emqx_gateway_utils:normalize_config(Config), + lists:map(fun({Type, LisName, ListenOn, _, _}) -> + Name0 = emqx_gateway_utils:listener_id(GwName, Type, LisName), + Name = {Name0, ListenOn}, + LisO = #{id => Name0, type => Type}, + case catch esockd:listener(Name) of + _Pid when is_pid(_Pid) -> + LisO#{running => true}; + _ -> + LisO#{running => false} + end + end, Listeners). + +%%-------------------------------------------------------------------- +%% Mgmt APIs - listeners +%%-------------------------------------------------------------------- + +listeners(GwName) when is_atom (GwName) -> + listeners(atom_to_binary(GwName)); +listeners(GwName) -> + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw([<<"gateway">>])), + Listeners = emqx_map_lib:jsonable_map( + emqx_map_lib:deep_get( + [<<"gateway">>, GwName, <<"listeners">>], RawConf)), + mapping_listener_m2l(GwName, Listeners). + +listener(_GwName, _ListenerId) -> + ok. + +mapping_listener_m2l(GwName, Listeners0) -> + Listeners = maps:to_list(Listeners0), + lists:append([listener(GwName, Type, maps:to_list(Conf)) + || {Type, Conf} <- Listeners]). + +listener(GwName, Type, Conf) -> + [begin + ListenerId = emqx_gateway_utils:listener_id(GwName, Type, LName), + Running = is_running(ListenerId, LConf), + LConf#{ + id => ListenerId, + type => Type, + running => Running + } + end || {LName, LConf} <- Conf, is_map(LConf)]. + +is_running(ListenerId, #{<<"bind">> := ListenOn0}) -> + ListenOn = emqx_gateway_utils:parse_listenon(ListenOn0), + try esockd:listener({ListenerId, ListenOn}) of + Pid when is_pid(Pid)-> + true + catch _:_ -> + false + end. + +%%-------------------------------------------------------------------- +%% Mgmt APIs - clients +%%-------------------------------------------------------------------- + +-spec lookup_client(gateway_name(), + emqx_type:clientid(), {atom(), atom()}) -> list(). +lookup_client(GwName, ClientId, FormatFun) -> + lists:append([lookup_client(Node, GwName, {clientid, ClientId}, FormatFun) + || Node <- ekka_mnesia:running_nodes()]). + +lookup_client(Node, GwName, {clientid, ClientId}, {M,F}) when Node =:= node() -> + ChanTab = emqx_gateway_cm:tabname(chan, GwName), + InfoTab = emqx_gateway_cm:tabname(info, GwName), + + lists:append(lists:map( + fun(Key) -> + lists:map(fun M:F/1, ets:lookup(InfoTab, Key)) + end, ets:lookup(ChanTab, ClientId))); + +lookup_client(Node, GwName, {clientid, ClientId}, FormatFun) -> + rpc_call(Node, lookup_client, + [Node, GwName, {clientid, ClientId}, FormatFun]). + +-spec kickout_client(gateway_name(), emqx_type:clientid()) + -> {error, any()} + | ok. +kickout_client(GwName, ClientId) -> + Results = [kickout_client(Node, GwName, ClientId) + || Node <- ekka_mnesia:running_nodes()], + case lists:any(fun(Item) -> Item =:= ok end, Results) of + true -> ok; + false -> lists:last(Results) + end. + +kickout_client(Node, GwName, ClientId) when Node =:= node() -> + emqx_gateway_cm:kick_session(GwName, ClientId); + +kickout_client(Node, GwName, ClientId) -> + rpc_call(Node, kickout_client, [Node, GwName, ClientId]). + +-spec list_client_subscriptions(gateway_name(), emqx_type:clientid()) + -> {error, any()} + | {ok, list()}. +list_client_subscriptions(GwName, ClientId) -> + %% Get the subscriptions from session-info + with_channel(GwName, ClientId, + fun(Pid) -> + Subs = emqx_gateway_conn:call( + Pid, + subscriptions, ?DEFAULT_CALL_TIMEOUT), + {ok, lists:map(fun({Topic, SubOpts}) -> + SubOpts#{topic => Topic} + end, Subs)} + end). + +-spec client_subscribe(gateway_name(), emqx_type:clientid(), + emqx_type:topic(), emqx_type:subopts()) + -> {error, any()} + | ok. +client_subscribe(GwName, ClientId, Topic, SubOpts) -> + with_channel(GwName, ClientId, + fun(Pid) -> + emqx_gateway_conn:call( + Pid, {subscribe, Topic, SubOpts}, + ?DEFAULT_CALL_TIMEOUT + ) + end). + +-spec client_unsubscribe(gateway_name(), + emqx_type:clientid(), emqx_type:topic()) + -> {error, any()} + | ok. +client_unsubscribe(GwName, ClientId, Topic) -> + with_channel(GwName, ClientId, + fun(Pid) -> + emqx_gateway_conn:call( + Pid, {unsubscribe, Topic}, ?DEFAULT_CALL_TIMEOUT) + end). + +with_channel(GwName, ClientId, Fun) -> + case emqx_gateway_cm:with_channel(GwName, ClientId, Fun) of + undefined -> {error, not_found}; + Res -> Res + end. + +%%-------------------------------------------------------------------- +%% Utils +%%-------------------------------------------------------------------- + +-spec return_http_error(integer(), binary()) -> {integer(), binary()}. +return_http_error(Code, Msg) -> + {Code, emqx_json:encode( + #{code => codestr(Code), + reason => emqx_gateway_utils:stringfy(Msg) + }) + }. + +codestr(404) -> 'RESOURCE_NOT_FOUND'; +codestr(401) -> 'NOT_SUPPORTED_NOW'; +codestr(500) -> 'UNKNOW_ERROR'. + +%%-------------------------------------------------------------------- +%% Internal funcs + +rpc_call(Node, Fun, Args) -> + case rpc:call(Node, ?MODULE, Fun, Args) of + {badrpc, Reason} -> {error, Reason}; + Res -> Res + end. diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 286be76e5..39115f114 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -14,13 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc The gateway instance management +%% @doc The gateway runtime -module(emqx_gateway_insta_sup). -behaviour(gen_server). -include("include/emqx_gateway.hrl"). - +-include_lib("emqx/include/logger.hrl"). %% APIs -export([ start_link/3 @@ -40,42 +40,46 @@ ]). -record(state, { - insta :: instance(), - ctx :: emqx_gateway_ctx:context(), - status :: stopped | running, + name :: gateway_name(), + config :: emqx_config:config(), + ctx :: emqx_gateway_ctx:context(), + status :: stopped | running, child_pids :: [pid()], - insta_state :: emqx_gateway_impl:state() | undefined + gw_state :: emqx_gateway_impl:state() | undefined, + created_at :: integer(), + started_at :: integer() | undefined, + stopped_at :: integer() | undefined }). %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -start_link(Insta, Ctx, GwDscrptr) -> +start_link(Gateway, Ctx, GwDscrptr) -> gen_server:start_link( ?MODULE, - [Insta, Ctx, GwDscrptr], + [Gateway, Ctx, GwDscrptr], [] ). --spec info(pid()) -> instance(). +-spec info(pid()) -> gateway(). info(Pid) -> gen_server:call(Pid, info). -%% @doc Stop instance +%% @doc Stop gateway -spec disable(pid()) -> ok | {error, any()}. disable(Pid) -> call(Pid, disable). -%% @doc Start instance +%% @doc Start gateway -spec enable(pid()) -> ok | {error, any()}. enable(Pid) -> call(Pid, enable). -%% @doc Update the gateway instance configurations --spec update(pid(), instance()) -> ok | {error, any()}. -update(Pid, NewInsta) -> - call(Pid, {update, NewInsta}). +%% @doc Update the gateway configurations +-spec update(pid(), emqx_config:config()) -> ok | {error, any()}. +update(Pid, Config) -> + call(Pid, {update, Config}). call(Pid, Req) -> gen_server:call(Pid, Req, 5000). @@ -84,46 +88,32 @@ call(Pid, Req) -> %% gen_server callbacks %%-------------------------------------------------------------------- -init([Insta, Ctx0, _GwDscrptr]) -> +init([Gateway, Ctx, _GwDscrptr]) -> process_flag(trap_exit, true), - #{id := InstaId, rawconf := RawConf} = Insta, - Ctx = do_init_context(InstaId, RawConf, Ctx0), + #{name := GwName, config := Config } = Gateway, State = #state{ - insta = Insta, - ctx = Ctx, + ctx = Ctx, + name = GwName, + config = Config, child_pids = [], - status = stopped + status = stopped, + created_at = erlang:system_time(millisecond) }, - case cb_insta_create(State) of - {error, _Reason} -> - do_deinit_context(Ctx), - %% XXX: Return Reason?? - {stop, create_gateway_instance_failed}; + case cb_gateway_load(State) of + {error, Reason} -> + {stop, {load_gateway_failure, Reason}}; {ok, NState} -> {ok, NState} end. -do_init_context(InstaId, RawConf, Ctx) -> - Auth = case maps:get(authentication, RawConf, #{enable => false}) of - #{enable := true, - authenticators := AuthCfgs} when is_list(AuthCfgs) -> - create_authenticators_for_gateway_insta(InstaId, AuthCfgs); - _ -> - undefined - end, - Ctx#{auth => Auth}. - -do_deinit_context(Ctx) -> - cleanup_authenticators_for_gateway_insta(maps:get(auth, Ctx)), - ok. - -handle_call(info, _From, State = #state{insta = Insta}) -> - {reply, Insta, State}; +handle_call(info, _From, State) -> + {reply, detailed_gateway_info(State), State}; handle_call(disable, _From, State = #state{status = Status}) -> + %% XXX: The `disable` opertaion is not persist to config database case Status of running -> - case cb_insta_destroy(State) of + case cb_gateway_unload(State) of {ok, NState} -> {reply, ok, NState}; {error, Reason} -> @@ -136,7 +126,7 @@ handle_call(disable, _From, State = #state{status = Status}) -> handle_call(enable, _From, State = #state{status = Status}) -> case Status of stopped -> - case cb_insta_create(State) of + case cb_gateway_load(State) of {error, Reason} -> {reply, {error, Reason}, State}; {ok, NState} -> @@ -146,29 +136,13 @@ handle_call(enable, _From, State = #state{status = Status}) -> {reply, {error, already_started}, State} end; -%% Stopped -> update -handle_call({update, NewInsta}, _From, State = #state{insta = Insta, - status = stopped}) -> - case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of - true -> - {reply, ok, State#state{insta = NewInsta}}; - false -> - {reply, {error, bad_instan_id}, State} - end; - -%% Running -> update -handle_call({update, NewInsta}, _From, State = #state{insta = Insta, - status = running}) -> - case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of - true -> - case cb_insta_update(NewInsta, State) of - {ok, NState} -> - {reply, ok, NState}; - {error, Reason} -> - {reply, {error, Reason}, State} - end; - false -> - {reply, {error, bad_instan_id}, State} +handle_call({update, Config}, _From, State) -> + case do_update_one_by_one(Config, State) of + {ok, NState} -> + {reply, ok, NState}; + {error, Reason} -> + %% If something wrong, nothing to update + {reply, {error, Reason}, State} end; handle_call(_Request, _From, State) -> @@ -181,152 +155,227 @@ handle_cast(_Msg, State) -> handle_info({'EXIT', Pid, Reason}, State = #state{child_pids = Pids}) -> case lists:member(Pid, Pids) of true -> - logger:error("Child process ~p exited: ~0p.", [Pid, Reason]), + ?LOG(error, "Child process ~p exited: ~0p.", [Pid, Reason]), case Pids -- [Pid]of [] -> - logger:error("All child process exited!"), + ?LOG(error, "All child process exited!"), {noreply, State#state{status = stopped, child_pids = [], - insta_state = undefined}}; + gw_state = undefined}}; RemainPids -> {noreply, State#state{child_pids = RemainPids}} end; _ -> - logger:error("Unknown process exited ~p:~0p", [Pid, Reason]), + ?LOG(error, "Unknown process exited ~p:~0p", [Pid, Reason]), {noreply, State} end; handle_info(Info, State) -> - logger:warning("Unexcepted info: ~p", [Info]), + ?LOG(warning, "Unexcepted info: ~p", [Info]), {noreply, State}. terminate(_Reason, State = #state{ctx = Ctx, child_pids = Pids}) -> - %% Cleanup instances - %% Step1. Destory instance - Pids /= [] andalso (_ = cb_insta_destroy(State)), - %% Step2. Delete authenticator resources - _ = do_deinit_context(Ctx), + Pids /= [] andalso (_ = cb_gateway_unload(State)), + _ = do_deinit_authn(maps:get(auth, Ctx, undefined)), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. +detailed_gateway_info(State) -> + maps:filter( + fun(_, V) -> V =/= undefined end, + #{name => State#state.name, + config => State#state.config, + status => State#state.status, + created_at => State#state.created_at, + started_at => State#state.started_at, + stopped_at => State#state.stopped_at + }). + %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -%% @doc AuthCfgs is a array of authenticatior configurations, -%% see: emqx_authn_schema:authenticators/1 -create_authenticators_for_gateway_insta(InstaId0, AuthCfgs) -> - ChainId = atom_to_binary(InstaId0, utf8), - case emqx_authn:create_chain(#{id => ChainId}) of - {ok, _ChainInfo} -> - Results = lists:map(fun(AuthCfg = #{name := Name}) -> - case emqx_authn:create_authenticator( - ChainId, - AuthCfg) of - {ok, _AuthInfo} -> ok; - {error, Reason} -> {Name, Reason} - end - end, AuthCfgs), - NResults = [ E || E <- Results, E /= ok], - NResults /= [] andalso begin - logger:error("Failed to create authenticators: ~p", [NResults]), - throw({bad_autheticators, NResults}) - end, ok; - {error, Reason} -> - logger:error("Failed to create authenticator chain: ~p", [Reason]), - throw({bad_chain, {ChainId, Reason}}) +do_init_authn(GwName, Config) -> + case maps:get(authentication, Config, #{enable => false}) of + #{enable := false} -> undefined; + AuthCfg when is_map(AuthCfg) -> + case maps:get(enable, AuthCfg, true) of + false -> + undefined; + _ -> + %% TODO: Implement Authentication + GwName + %case emqx_authn:create_chain(#{id => ChainId}) of + % {ok, _ChainInfo} -> + % case emqx_authn:create_authenticator(ChainId, AuthCfg) of + % {ok, _} -> ChainId; + % {error, Reason} -> + % ?LOG(error, "Failed to create authentication ~p", [Reason]), + % throw({bad_authentication, Reason}) + % end; + % {error, Reason} -> + % ?LOG(error, "Failed to create authentication chain: ~p", [Reason]), + % throw({bad_chain, {ChainId, Reason}}) + %end. + end; + _ -> + undefined end. -cleanup_authenticators_for_gateway_insta(undefined) -> +do_deinit_authn(undefined) -> ok; -cleanup_authenticators_for_gateway_insta(ChainId) -> - case emqx_authn:delete_chain(ChainId) of - ok -> ok; - {error, {not_found, _}} -> - logger:warning("Failed clean authenticator chain: ~s, " - "reason: not_found", [ChainId]); - {error, Reason} -> - logger:error("Failed clean authenticator chain: ~s, " - "reason: ~p", [ChainId, Reason]) +do_deinit_authn(AuthnRef) -> + %% TODO: + ?LOG(warning, "Failed to clean authn ~p, not suppported now", [AuthnRef]). + %case emqx_authn:delete_chain(AuthnRef) of + % ok -> ok; + % {error, {not_found, _}} -> + % ?LOG(warning, "Failed to clean authentication chain: ~s, " + % "reason: not_found", [AuthnRef]); + % {error, Reason} -> + % ?LOG(error, "Failed to clean authentication chain: ~s, " + % "reason: ~p", [AuthnRef, Reason]) + %end. + +do_update_one_by_one(NCfg0, State = #state{ + ctx = Ctx, + config = OCfg, + status = Status}) -> + + NCfg = emqx_map_lib:deep_merge(OCfg, NCfg0), + + OEnable = maps:get(enable, OCfg, true), + NEnable = maps:get(enable, NCfg0, OEnable), + + OAuth = maps:get(authentication, OCfg, undefined), + NAuth = maps:get(authentication, NCfg0, OAuth), + + if + Status == stopped, NEnable == true -> + NState = State#state{config = NCfg}, + cb_gateway_load(NState); + Status == stopped, NEnable == false -> + {ok, State#state{config = NCfg}}; + Status == running, NEnable == true -> + NState = case NAuth == OAuth of + true -> State; + false -> + %% Reset Authentication first + _ = do_deinit_authn(maps:get(auth, Ctx, undefined)), + NCtx = Ctx#{ + auth => do_init_authn( + State#state.name, + NCfg + ) + }, + State#state{ctx = NCtx} + end, + cb_gateway_update(NCfg, NState); + Status == running, NEnable == false -> + case cb_gateway_unload(State) of + {ok, NState} -> {ok, NState#state{config = NCfg}}; + {error, Reason} -> {error, Reason} + end; + true -> + throw(nomatch) end. -cb_insta_destroy(State = #state{insta = Insta = #{type := Type}, - insta_state = InstaState}) -> +cb_gateway_unload(State = #state{name = GwName, + gw_state = GwState}) -> + Gateway = detailed_gateway_info(State), try - #{cbkmod := CbMod, - state := GwState} = emqx_gateway_registry:lookup(Type), - CbMod:on_insta_destroy(Insta, InstaState, GwState), + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), + CbMod:on_gateway_unload(Gateway, GwState), {ok, State#state{child_pids = [], - insta_state = undefined, - status = stopped}} + status = stopped, + gw_state = undefined, + started_at = undefined, + stopped_at = erlang:system_time(millisecond)}} catch Class : Reason : Stk -> - logger:error("Destroy instance (~0p, ~0p, _) crashed: " - "{~p, ~p}, stacktrace: ~0p", - [Insta, InstaState, + ?LOG(error, "Failed to unload gateway (~0p, ~0p) crashed: " + "{~p, ~p}, stacktrace: ~0p", + [GwName, GwState, Class, Reason, Stk]), {error, {Class, Reason, Stk}} end. -cb_insta_create(State = #state{insta = Insta = #{type := Type}, - ctx = Ctx}) -> - try - #{cbkmod := CbMod, - state := GwState} = emqx_gateway_registry:lookup(Type), - case CbMod:on_insta_create(Insta, Ctx, GwState) of - {error, Reason} -> throw({callback_return_error, Reason}); - {ok, InstaPidOrSpecs, InstaState} -> - ChildPids = start_child_process(InstaPidOrSpecs), - {ok, State#state{ - status = running, - child_pids = ChildPids, - insta_state = InstaState - }} - end - catch - Class : Reason1 : Stk -> - logger:error("Create instance (~0p, ~0p, _) crashed: " - "{~p, ~p}, stacktrace: ~0p", - [Insta, Ctx, - Class, Reason1, Stk]), - {error, {Class, Reason1, Stk}} +%% @doc 1. Create Authentcation Context +%% 2. Callback to Mod:on_gateway_load/2 +%% +%% Notes: If failed, rollback +cb_gateway_load(State = #state{name = GwName, + config = Config, + ctx = Ctx}) -> + + Gateway = detailed_gateway_info(State), + + case maps:get(enable, Config, true) of + false -> + ?LOG(info, "Skipp to start ~s gateway due to disabled", [GwName]); + true -> + try + AuthnRef = do_init_authn(GwName, Config), + NCtx = Ctx#{auth => AuthnRef}, + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), + case CbMod:on_gateway_load(Gateway, NCtx) of + {error, Reason} -> + do_deinit_authn(AuthnRef), + throw({callback_return_error, Reason}); + {ok, ChildPidOrSpecs, GwState} -> + ChildPids = start_child_process(ChildPidOrSpecs), + {ok, State#state{ + ctx = NCtx, + status = running, + child_pids = ChildPids, + gw_state = GwState, + stopped_at = undefined, + started_at = erlang:system_time(millisecond) + }} + end + catch + Class : Reason1 : Stk -> + ?LOG(error, "Failed to load ~s gateway (~0p, ~0p) " + "crashed: {~p, ~p}, stacktrace: ~0p", + [GwName, Gateway, Ctx, + Class, Reason1, Stk]), + {error, {Class, Reason1, Stk}} + end end. -cb_insta_update(NewInsta, - State = #state{insta = Insta = #{type := Type}, - ctx = Ctx, - insta_state = GwInstaState}) -> +cb_gateway_update(Config, + State = #state{name = GwName, + gw_state = GwState}) -> try - #{cbkmod := CbMod, - state := GwState} = emqx_gateway_registry:lookup(Type), - case CbMod:on_insta_update(NewInsta, Insta, GwInstaState, GwState) of + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), + case CbMod:on_gateway_update(Config, detailed_gateway_info(State), GwState) of {error, Reason} -> throw({callback_return_error, Reason}); - {ok, InstaPidOrSpecs, InstaState} -> + {ok, ChildPidOrSpecs, NGwState} -> %% XXX: Hot-upgrade ??? - ChildPids = start_child_process(InstaPidOrSpecs), + ChildPids = start_child_process(ChildPidOrSpecs), {ok, State#state{ - status = running, + config = Config, child_pids = ChildPids, - insta_state = InstaState + gw_state = NGwState }} end catch Class : Reason1 : Stk -> - logger:error("Update instance (~0p, ~0p, ~0p, _) crashed: " - "{~p, ~p}, stacktrace: ~0p", - [NewInsta, Insta, Ctx, - Class, Reason1, Stk]), + ?LOG(error, "Failed to update ~s gateway to config: ~0p crashed: " + "{~p, ~p}, stacktrace: ~0p", + [GwName, Config, Class, Reason1, Stk]), {error, {Class, Reason1, Stk}} end. -start_child_process([Indictor|_] = InstaPidOrSpecs) -> +start_child_process([]) -> []; +start_child_process([Indictor|_] = ChildPidOrSpecs) -> case erlang:is_pid(Indictor) of true -> - InstaPidOrSpecs; + ChildPidOrSpecs; _ -> - do_start_child_process(InstaPidOrSpecs) + do_start_child_process(ChildPidOrSpecs) end. do_start_child_process(ChildSpecs) when is_list(ChildSpecs) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_metrics.erl b/apps/emqx_gateway/src/emqx_gateway_metrics.erl index d4e39443c..77b97a6a1 100644 --- a/apps/emqx_gateway/src/emqx_gateway_metrics.erl +++ b/apps/emqx_gateway/src/emqx_gateway_metrics.erl @@ -18,7 +18,7 @@ -behaviour(gen_server). --include("include/emqx_gateway.hrl"). +-include_lib("emqx_gateway/include/emqx_gateway.hrl"). %% APIs @@ -47,36 +47,36 @@ %% APIs %%-------------------------------------------------------------------- -start_link(Type) -> - gen_server:start_link(?MODULE, [Type], []). +start_link(GwName) -> + gen_server:start_link(?MODULE, [GwName], []). --spec inc(gateway_type(), atom()) -> ok. -inc(Type, Name) -> - inc(Type, Name, 1). +-spec inc(gateway_name(), atom()) -> ok. +inc(GwName, Name) -> + inc(GwName, Name, 1). --spec inc(gateway_type(), atom(), integer()) -> ok. -inc(Type, Name, Oct) -> - ets:update_counter(tabname(Type), Name, {2, Oct}, {Name, 0}), +-spec inc(gateway_name(), atom(), integer()) -> ok. +inc(GwName, Name, Oct) -> + ets:update_counter(tabname(GwName), Name, {2, Oct}, {Name, 0}), ok. --spec dec(gateway_type(), atom()) -> ok. -dec(Type, Name) -> - inc(Type, Name, -1). +-spec dec(gateway_name(), atom()) -> ok. +dec(GwName, Name) -> + inc(GwName, Name, -1). --spec dec(gateway_type(), atom(), non_neg_integer()) -> ok. -dec(Type, Name, Oct) -> - inc(Type, Name, -Oct). +-spec dec(gateway_name(), atom(), non_neg_integer()) -> ok. +dec(GwName, Name, Oct) -> + inc(GwName, Name, -Oct). -tabname(Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_metrics'])). +tabname(GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_metrics'])). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([Type]) -> +init([GwName]) -> TabOpts = [public, {write_concurrency, true}], - ok = emqx_tables:new(tabname(Type), [set|TabOpts]), + ok = emqx_tables:new(tabname(GwName), [set|TabOpts]), {ok, #state{}}. handle_call(_Request, _From, State) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_registry.erl b/apps/emqx_gateway/src/emqx_gateway_registry.erl index 07cc37bd8..cfa6d424a 100644 --- a/apps/emqx_gateway/src/emqx_gateway_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_registry.erl @@ -14,20 +14,17 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc The Registry Centre of Gateway Type +%% @doc The Registry Centre of Gateway -module(emqx_gateway_registry). -include("include/emqx_gateway.hrl"). - -behavior(gen_server). -%% APIs for Impl. --export([ load/3 - , unload/1 - ]). - --export([ list/0 +%% APIs +-export([ reg/2 + , unreg/1 + , list/0 , lookup/1 ]). @@ -44,9 +41,17 @@ ]). -record(state, { - loaded = #{} :: #{ gateway_type() => descriptor() } + reged = #{} :: #{ gateway_name() => descriptor() } }). +-type registry_options() :: [registry_option()]. + +-type registry_option() :: {cbkmod, atom()}. + +-type descriptor() :: #{ cbkmod := atom() + , rgopts := registry_options() + }. + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- @@ -58,46 +63,33 @@ start_link() -> %% Mgmt %%-------------------------------------------------------------------- --type registry_options() :: [registry_option()]. - --type registry_option() :: {cbkmod, atom()}. - --type gateway_options() :: list(). - --type descriptor() :: #{ cbkmod := atom() - , rgopts := registry_options() - , gwopts := gateway_options() - , state => any() - }. - --spec load(gateway_type(), registry_options(), gateway_options()) +-spec reg(gateway_name(), registry_options()) -> ok | {error, any()}. -load(Type, RgOpts, GwOpts) -> - CbMod = proplists:get_value(cbkmod, RgOpts, Type), +reg(Name, RgOpts) -> + CbMod = proplists:get_value(cbkmod, RgOpts, Name), Dscrptr = #{ cbkmod => CbMod , rgopts => RgOpts - , gwopts => GwOpts }, - call({load, Type, Dscrptr}). + call({reg, Name, Dscrptr}). --spec unload(gateway_type()) -> ok | {error, any()}. -unload(Type) -> +-spec unreg(gateway_name()) -> ok | {error, any()}. +unreg(Name) -> %% TODO: Checking ALL INSTACE HAS STOPPED - call({unload, Type}). + call({unreg, Name}). %% TODO: -%unload(Type, Force) -> -% call({unload, Type, Froce}). +%unreg(Name, Force) -> +% call({unreg, Name, Froce}). %% @doc Return all registered protocol gateway implementation --spec list() -> [{gateway_type(), descriptor()}]. +-spec list() -> [{gateway_name(), descriptor()}]. list() -> call(all). --spec lookup(gateway_type()) -> undefined | descriptor(). -lookup(Type) -> - call({lookup, Type}). +-spec lookup(gateway_name()) -> undefined | descriptor(). +lookup(Name) -> + call({lookup, Name}). call(Req) -> gen_server:call(?MODULE, Req, 5000). @@ -107,44 +99,32 @@ call(Req) -> %%-------------------------------------------------------------------- init([]) -> - %% TODO: Metrics ??? process_flag(trap_exit, true), - {ok, #state{loaded = #{}}}. + {ok, #state{reged = #{}}}. -handle_call({load, Type, Dscrptr}, _From, State = #state{loaded = Gateways}) -> - case maps:get(Type, Gateways, notfound) of +handle_call({reg, Name, Dscrptr}, _From, State = #state{reged = Gateways}) -> + case maps:get(Name, Gateways, notfound) of notfound -> - try - GwOpts = maps:get(gwopts, Dscrptr), - CbMod = maps:get(cbkmod, Dscrptr), - {ok, GwState} = CbMod:init(GwOpts), - NDscrptr = maps:put(state, GwState, Dscrptr), - NGateways = maps:put(Type, NDscrptr, Gateways), - {reply, ok, State#state{loaded = NGateways}} - catch - Class : Reason : Stk -> - logger:error("Load ~s crashed {~p, ~p}; stacktrace: ~0p", - [Type, Class, Reason, Stk]), - {reply, {error, {Class, Reason}}, State} - end; + NGateways = maps:put(Name, Dscrptr, Gateways), + {reply, ok, State#state{reged = NGateways}}; _ -> {reply, {error, already_existed}, State} end; -handle_call({unload, Type}, _From, State = #state{loaded = Gateways}) -> - case maps:get(Type, Gateways, undefined) of +handle_call({unreg, Name}, _From, State = #state{reged = Gateways}) -> + case maps:get(Name, Gateways, undefined) of undefined -> {reply, ok, State}; _ -> - emqx_gateway_sup:stop_all_suptree(Type), - {reply, ok, State#state{loaded = maps:remove(Type, Gateways)}} + _ = emqx_gateway_sup:unload_gateway(Name), + {reply, ok, State#state{reged = maps:remove(Name, Gateways)}} end; -handle_call(all, _From, State = #state{loaded = Gateways}) -> +handle_call(all, _From, State = #state{reged = Gateways}) -> {reply, maps:to_list(Gateways), State}; -handle_call({lookup, Type}, _From, State = #state{loaded = Gateways}) -> - Reply = maps:get(Type, Gateways, undefined), +handle_call({lookup, Name}, _From, State = #state{reged = Gateways}) -> + Reply = maps:get(Name, Gateways, undefined), {reply, Reply, State}; handle_call(Req, _From, State) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 5cb958701..7fb945ba0 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -1,5 +1,23 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + -module(emqx_gateway_schema). +-behaviour(hocon_schema). + -dialyzer(no_return). -dialyzer(no_match). -dialyzer(no_contracts). @@ -8,17 +26,16 @@ -include_lib("typerefl/include/types.hrl"). +-type ip_port() :: tuple(). -type duration() :: integer(). -type bytesize() :: integer(). -type comma_separated_list() :: list(). --type ip_port() :: tuple(). +-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}). -typerefl_from_string({duration/0, emqx_schema, to_duration}). -typerefl_from_string({bytesize/0, emqx_schema, to_bytesize}). --typerefl_from_string({comma_separated_list/0, emqx_schema, to_comma_separated_list}). --typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}). - --behaviour(hocon_schema). +-typerefl_from_string({comma_separated_list/0, emqx_schema, + to_comma_separated_list}). -reflect_type([ duration/0 , bytesize/0 @@ -26,265 +43,283 @@ , ip_port/0 ]). --export([structs/0 , fields/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0 , fields/1]). -structs() -> ["gateway"]. +namespace() -> gateway. -fields("gateway") -> - [{stomp, t(ref(stomp))}, - {mqttsn, t(ref(mqttsn))}, - {coap, t(ref(coap))}, - {lwm2m, t(ref(lwm2m))}, - {lwm2m_xml_dir, t(string())}, - {exproto, t(ref(exproto))} +roots() -> [gateway]. + +fields(gateway) -> + [{stomp, sc(ref(stomp_structs))}, + {mqttsn, sc(ref(mqttsn_structs))}, + {coap, sc(ref(coap_structs))}, + {lwm2m, sc(ref(lwm2m_structs))}, + {exproto, sc(ref(exproto_structs))} ]; -fields(stomp) -> - [{"$id", t(ref(stomp_structs))}]; - fields(stomp_structs) -> - [ {frame, t(ref(stomp_frame))} - , {clientinfo_override, t(ref(clientinfo_override))} - , {authentication, t(ref(authentication))} - , {listener, t(ref(tcp_listener_group))} - ]; + [ {frame, sc(ref(stomp_frame))} + , {listeners, sc(ref(tcp_listener_group))} + ] ++ gateway_common_options(); fields(stomp_frame) -> - [ {max_headers, t(integer(), undefined, 10)} - , {max_headers_length, t(integer(), undefined, 1024)} - , {max_body_length, t(integer(), undefined, 8192)} + [ {max_headers, sc(integer(), 10)} + , {max_headers_length, sc(integer(), 1024)} + , {max_body_length, sc(integer(), 8192)} ]; -fields(mqttsn) -> - [{"$id", t(ref(mqttsn_structs))}]; - fields(mqttsn_structs) -> - [ {gateway_id, t(integer())} - , {broadcast, t(boolean())} - , {enable_stats, t(boolean())} - , {enable_qos3, t(boolean())} - , {idle_timeout, t(duration())} + [ {gateway_id, sc(integer())} + , {broadcast, sc(boolean())} + , {enable_qos3, sc(boolean())} , {predefined, hoconsc:array(ref(mqttsn_predefined))} - , {clientinfo_override, t(ref(clientinfo_override))} - , {listener, t(ref(udp_listener_group))} - ]; + , {listeners, sc(ref(udp_listener_group))} + ] ++ gateway_common_options(); fields(mqttsn_predefined) -> - %% FIXME: How to check the $id is a integer ??? - [ {id, t(integer())} - , {topic, t(string())} + [ {id, sc(integer())} + , {topic, sc(binary())} ]; -fields(lwm2m) -> - [{"$id", t(ref(lwm2m_structs))} - ]; +fields(coap_structs) -> + [ {heartbeat, sc(duration(), <<"30s">>)} + , {connection_required, sc(boolean(), false)} + , {notify_type, sc(union([non, con, qos]), qos)} + , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), coap)} + , {publish_qos, sc(union([qos0, qos1, qos2, coap]), coap)} + , {listeners, sc(ref(udp_listener_group))} + ] ++ gateway_common_options(); fields(lwm2m_structs) -> - [ {lifetime_min, t(duration())} - , {lifetime_max, t(duration())} - , {qmode_time_windonw, t(integer())} - , {auto_observe, t(boolean())} - , {mountpoint, t(string())} - , {update_msg_publish_condition, t(union([always, contains_object_list]))} - , {translators, t(ref(translators))} - , {listener, t(ref(udp_listener_group))} - ]; - -fields(exproto) -> - [{"$id", t(ref(exproto_structs))}]; + [ {xml_dir, sc(binary())} + , {lifetime_min, sc(duration())} + , {lifetime_max, sc(duration())} + , {qmode_time_windonw, sc(integer())} + , {auto_observe, sc(boolean())} + , {update_msg_publish_condition, sc(union([always, contains_object_list]))} + , {translators, sc(ref(translators))} + , {listeners, sc(ref(udp_listener_group))} + ] ++ gateway_common_options(); fields(exproto_structs) -> - [ {server, t(ref(exproto_grpc_server))} - , {handler, t(ref(exproto_grpc_handler))} - , {authentication, t(ref(authentication))} - , {listener, t(ref(udp_tcp_listener_group))} - ]; + [ {server, sc(ref(exproto_grpc_server))} + , {handler, sc(ref(exproto_grpc_handler))} + , {listeners, sc(ref(udp_tcp_listener_group))} + ] ++ gateway_common_options(); fields(exproto_grpc_server) -> - [ {bind, t(integer())} + [ {bind, sc(union(ip_port(), integer()))} %% TODO: ssl options ]; fields(exproto_grpc_handler) -> - [ {address, t(string())} + [ {address, sc(binary())} %% TODO: ssl ]; -fields(authentication) -> - [ {enable, #{type => boolean(), default => false}} - , {authenticators, fun emqx_authn_schema:authenticators/1} - ]; - fields(clientinfo_override) -> - [ {username, t(string())} - , {password, t(string())} - , {clientid, t(string())} + [ {username, sc(binary())} + , {password, sc(binary())} + , {clientid, sc(binary())} ]; fields(translators) -> - [{"$name", t(string())}]; + [ {command, sc(ref(translator))} + , {response, sc(ref(translator))} + , {notify, sc(ref(translator))} + , {register, sc(ref(translator))} + , {update, sc(ref(translator))} + ]; + +fields(translator) -> + [ {topic, sc(binary())} + , {qos, sc(range(0, 2))} + ]; fields(udp_listener_group) -> - [ {udp, t(ref(udp_listener))} - , {dtls, t(ref(dtls_listener))} + [ {udp, sc(ref(udp_listener))} + , {dtls, sc(ref(dtls_listener))} ]; fields(tcp_listener_group) -> - [ {tcp, t(ref(tcp_listener))} - , {ssl, t(ref(ssl_listener))} + [ {tcp, sc(ref(tcp_listener))} + , {ssl, sc(ref(ssl_listener))} ]; fields(udp_tcp_listener_group) -> - [ {udp, t(ref(udp_listener))} - , {dtls, t(ref(dtls_listener))} - , {tcp, t(ref(tcp_listener))} - , {ssl, t(ref(ssl_listener))} + [ {udp, sc(ref(udp_listener))} + , {dtls, sc(ref(dtls_listener))} + , {tcp, sc(ref(tcp_listener))} + , {ssl, sc(ref(ssl_listener))} ]; fields(tcp_listener) -> - [ {"$name", t(ref(tcp_listener_settings))}]; + [ {"$name", sc(ref(tcp_listener_settings))}]; fields(ssl_listener) -> - [ {"$name", t(ref(ssl_listener_settings))}]; + [ {"$name", sc(ref(ssl_listener_settings))}]; fields(udp_listener) -> - [ {"$name", t(ref(udp_listener_settings))}]; + [ {"$name", sc(ref(udp_listener_settings))}]; fields(dtls_listener) -> - [ {"$name", t(ref(dtls_listener_settings))}]; - -fields(listener_settings) -> - % FIXME: - %[ {"bind", t(union(ip_port(), integer()))} - [ {bind, t(integer())} - , {acceptors, t(integer(), undefined, 8)} - , {max_connections, t(integer(), undefined, 1024)} - , {max_conn_rate, t(integer())} - , {active_n, t(integer(), undefined, 100)} - %, {zone, t(string())} - %, {rate_limit, t(comma_separated_list())} - , {access, t(ref(access))} - , {proxy_protocol, t(boolean())} - , {proxy_protocol_timeout, t(duration())} - , {backlog, t(integer(), undefined, 1024)} - , {send_timeout, t(duration(), undefined, "15s")} %% FIXME: mapping it - , {send_timeout_close, t(boolean(), undefined, true)} - , {recbuf, t(bytesize())} - , {sndbuf, t(bytesize())} - , {buffer, t(bytesize())} - , {high_watermark, t(bytesize(), undefined, "1MB")} - , {tune_buffer, t(boolean())} - , {nodelay, t(boolean())} - , {reuseaddr, t(boolean())} - ]; + [ {"$name", sc(ref(dtls_listener_settings))}]; fields(tcp_listener_settings) -> [ %% some special confs for tcp listener - ] ++ fields(listener_settings); + ] ++ tcp_opts() + ++ proxy_protocol_opts() + ++ common_listener_opts(); fields(ssl_listener_settings) -> [ - %% some special confs for ssl listener - ] ++ - ssl(undefined, #{handshake_timeout => "15s" - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + %% some special confs for ssl listener + ] ++ tcp_opts() + ++ ssl_opts() + ++ proxy_protocol_opts() + ++ common_listener_opts(); fields(udp_listener_settings) -> [ - %% some special confs for udp listener - ] ++ fields(listener_settings); + %% some special confs for udp listener + ] ++ udp_opts() + ++ common_listener_opts(); fields(dtls_listener_settings) -> [ - %% some special confs for dtls listener - ] ++ - ssl(undefined, #{handshake_timeout => "15s" - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + %% some special confs for dtls listener + ] ++ udp_opts() + ++ dtls_opts() + ++ common_listener_opts(); -fields(access) -> - [ {"$id", #{type => string(), - nullable => true}}]; - -fields(coap) -> - [{"$id", t(ref(coap_structs))}]; - -fields(coap_structs) -> - [ {enable_stats, t(boolean(), undefined, true)} - , {authentication, t(ref(authentication))} - , {heartbeat, t(duration(), undefined, "15s")} - , {resource, t(union([mqtt, pubsub]), undefined, mqtt)} - , {notify_type, t(union([non, con, qos]), undefined, qos)} - , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {listener, t(ref(udp_listener_group))} +fields(udp_opts) -> + [ {active_n, sc(integer(), 100)} + , {recbuf, sc(bytesize())} + , {sndbuf, sc(bytesize())} + , {buffer, sc(bytesize())} + , {reuseaddr, sc(boolean(), true)} ]; +fields(dtls_listener_ssl_opts) -> + Base = emqx_schema:fields("listener_ssl_opts"), + DtlsVers = hoconsc:mk( + typerefl:alias("string", list(atom())), + #{ default => default_dtls_vsns(), + converter => fun (Vsns) -> + [dtls_vsn(iolist_to_binary(V)) || V <- Vsns] + end + }), + Ciphers = sc(hoconsc:array(string()), default_ciphers()), + lists:keydelete( + "handshake_timeout", 1, + lists:keyreplace( + "ciphers", 1, + lists:keyreplace("versions", 1, Base, {"versions", DtlsVers}), + {"ciphers", Ciphers} + ) + ); + fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). -%translations() -> []. -% -%translations(_) -> []. +default_ciphers() -> + ["ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-AES256-SHA384", "ECDHE-RSA-AES256-SHA384", + "ECDHE-ECDSA-DES-CBC3-SHA", "ECDH-ECDSA-AES256-GCM-SHA384", "ECDH-RSA-AES256-GCM-SHA384", + "ECDH-ECDSA-AES256-SHA384", "ECDH-RSA-AES256-SHA384", "DHE-DSS-AES256-GCM-SHA384", + "DHE-DSS-AES256-SHA256", "AES256-GCM-SHA384", "AES256-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-SHA256", "ECDHE-RSA-AES128-SHA256", "ECDH-ECDSA-AES128-GCM-SHA256", + "ECDH-RSA-AES128-GCM-SHA256", "ECDH-ECDSA-AES128-SHA256", "ECDH-RSA-AES128-SHA256", + "DHE-DSS-AES128-GCM-SHA256", "DHE-DSS-AES128-SHA256", "AES128-GCM-SHA256", "AES128-SHA256", + "ECDHE-ECDSA-AES256-SHA", "ECDHE-RSA-AES256-SHA", "DHE-DSS-AES256-SHA", + "ECDH-ECDSA-AES256-SHA", "ECDH-RSA-AES256-SHA", "AES256-SHA", "ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA", "DHE-DSS-AES128-SHA", "ECDH-ECDSA-AES128-SHA", + "ECDH-RSA-AES128-SHA", "AES128-SHA" + ] ++ psk_ciphers(). + +psk_ciphers() -> + ["PSK-AES128-CBC-SHA", "PSK-AES256-CBC-SHA", + "PSK-3DES-EDE-CBC-SHA", "PSK-RC4-SHA" + ]. + +% authentication() -> +% hoconsc:union( +% [ undefined +% , hoconsc:ref(emqx_authn_mnesia, config) +% , hoconsc:ref(emqx_authn_mysql, config) +% , hoconsc:ref(emqx_authn_pgsql, config) +% , hoconsc:ref(emqx_authn_mongodb, standalone) +% , hoconsc:ref(emqx_authn_mongodb, 'replica-set') +% , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') +% , hoconsc:ref(emqx_authn_redis, standalone) +% , hoconsc:ref(emqx_authn_redis, cluster) +% , hoconsc:ref(emqx_authn_redis, sentinel) +% , hoconsc:ref(emqx_authn_http, get) +% , hoconsc:ref(emqx_authn_http, post) +% , hoconsc:ref(emqx_authn_jwt, 'hmac-based') +% , hoconsc:ref(emqx_authn_jwt, 'public-key') +% , hoconsc:ref(emqx_authn_jwt, 'jwks') +% , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) +% ]). + +gateway_common_options() -> + [ {enable, sc(boolean(), true)} + , {enable_stats, sc(boolean(), true)} + , {idle_timeout, sc(duration(), <<"30s">>)} + , {mountpoint, sc(binary(), <<>>)} + , {clientinfo_override, sc(ref(clientinfo_override))} + , {authentication, sc(hoconsc:lazy(map()))} + ]. + +common_listener_opts() -> + [ {enable, sc(boolean(), true)} + , {bind, sc(union(ip_port(), integer()))} + , {acceptors, sc(integer(), 16)} + , {max_connections, sc(integer(), 1024)} + , {max_conn_rate, sc(integer())} + %, {rate_limit, sc(comma_separated_list())} + , {mountpoint, sc(binary(), undefined)} + , {access_rules, sc(hoconsc:array(string()), [])} + ]. + +tcp_opts() -> + [{tcp, sc(ref(emqx_schema, "tcp_opts"), #{})}]. + +udp_opts() -> + [{udp, sc(ref(udp_opts), #{})}]. + +ssl_opts() -> + [{ssl, sc(ref(emqx_schema, "listener_ssl_opts"), #{})}]. + +dtls_opts() -> + [{dtls, sc(ref(dtls_listener_ssl_opts), #{})}]. + +proxy_protocol_opts() -> + [ {proxy_protocol, sc(boolean())} + , {proxy_protocol_timeout, sc(duration())} + ]. + +default_dtls_vsns() -> + [<<"dtlsv1.2">>, <<"dtlsv1">>]. + +dtls_vsn(<<"dtlsv1.2">>) -> 'dtlsv1.2'; +dtls_vsn(<<"dtlsv1">>) -> 'dtlsv1'. %%-------------------------------------------------------------------- %% Helpers %% types -t(Type) -> #{type => Type}. +sc(Type) -> #{type => Type}. -t(Type, Mapping, Default) -> - hoconsc:t(Type, #{mapping => Mapping, default => Default}). - -t(Type, Mapping, Default, OverrideEnv) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - }). +sc(Type, Default) -> + hoconsc:mk(Type, #{default => Default}). ref(Field) -> hoconsc:ref(?MODULE, Field). -%% utils - -%% generate a ssl field. -%% ssl("emqx", #{"verify" => verify_peer}) will return -%% [ {"cacertfile", t(string(), "emqx.cacertfile", undefined)} -%% , {"certfile", t(string(), "emqx.certfile", undefined)} -%% , {"keyfile", t(string(), "emqx.keyfile", undefined)} -%% , {"verify", t(union(verify_peer, verify_none), "emqx.verify", verify_peer)} -%% , {"server_name_indication", "emqx.server_name_indication", undefined)} -%% ... -ssl(Mapping, Defaults) -> - M = fun (Field) -> - case (Mapping) of - undefined -> undefined; - _ -> Mapping ++ "." ++ Field - end end, - D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, - [ {"enable", t(boolean(), 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(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} - , {"reuse_sessions", t(boolean(), M("reuse_sessions"), D("reuse_sessions"))} - , {"honor_cipher_order", t(boolean(), M("honor_cipher_order"), D("honor_cipher_order"))} - , {"handshake_timeout", t(duration(), M("handshake_timeout"), D("handshake_timeout"))} - , {"depth", t(integer(), M("depth"), D("depth"))} - , {"password", hoconsc:t(string(), #{mapping => M("key_password"), - default => D("key_password"), - sensitive => true - })} - , {"dhfile", t(string(), M("dhfile"), D("dhfile"))} - , {"server_name_indication", t(union(disable, string()), M("server_name_indication"), - D("server_name_indication"))} - , {"tls_versions", t(comma_separated_list(), M("tls_versions"), D("tls_versions"))} - , {"ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))} - , {"psk_ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))}]. +ref(Mod, Field) -> + hoconsc:ref(Mod, Field). diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl index c974060d0..09b74450a 100644 --- a/apps/emqx_gateway/src/emqx_gateway_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -22,22 +22,16 @@ -export([start_link/0]). -%% Gateway Instance APIs --export([ create_gateway_insta/1 - , remove_gateway_insta/1 - , lookup_gateway_insta/1 - , update_gateway_insta/1 +%% Gateway APIs +-export([ load_gateway/1 + , unload_gateway/1 + , lookup_gateway/1 + , update_gateway/2 , start_gateway_insta/1 , stop_gateway_insta/1 - , list_gateway_insta/1 , list_gateway_insta/0 ]). -%% Gateway APs --export([ list_started_gateway/0 - , stop_all_suptree/1 - ]). - %% supervisor callbacks -export([init/1]). @@ -48,87 +42,73 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). --spec create_gateway_insta(instance()) -> {ok, pid()} | {error, any()}. -create_gateway_insta(Insta = #{type := Type}) -> - case emqx_gateway_registry:lookup(Type) of - undefined -> {error, {unknown_gateway_id, Type}}; + +-spec load_gateway(gateway()) -> {ok, pid()} | {error, any()}. +load_gateway(Gateway = #{name := GwName}) -> + case emqx_gateway_registry:lookup(GwName) of + undefined -> {error, {unknown_gateway_name, GwName}}; GwDscrptr -> - {ok, GwSup} = ensure_gateway_suptree_ready(gatewayid(Type)), - emqx_gateway_gw_sup:create_insta(GwSup, Insta, GwDscrptr) + {ok, GwSup} = ensure_gateway_suptree_ready(GwName), + emqx_gateway_gw_sup:create_insta(GwSup, Gateway, GwDscrptr) end. --spec remove_gateway_insta(instance_id()) -> ok | {error, any()}. -remove_gateway_insta(InstaId) -> - case search_gateway_insta_proc(InstaId) of - {ok, {GwSup, _}} -> - emqx_gateway_gw_sup:remove_insta(GwSup, InstaId); +-spec unload_gateway(gateway_name()) + -> ok + | {error, not_found} + | {error, any()}. +unload_gateway(GwName) -> + case lists:keyfind(GwName, 1, supervisor:which_children(?MODULE)) of + false -> {error, not_found}; _ -> + _ = supervisor:terminate_child(?MODULE, GwName), + _ = supervisor:delete_child(?MODULE, GwName), ok end. --spec lookup_gateway_insta(instance_id()) -> instance() | undefined. -lookup_gateway_insta(InstaId) -> - case search_gateway_insta_proc(InstaId) of +-spec lookup_gateway(gateway_name()) -> gateway() | undefined. +lookup_gateway(GwName) -> + case search_gateway_insta_proc(GwName) of {ok, {_, GwInstaPid}} -> emqx_gateway_insta_sup:info(GwInstaPid); _ -> undefined end. --spec update_gateway_insta(instance()) +-spec update_gateway(gateway_name(), emqx_config:config()) -> ok | {error, any()}. -update_gateway_insta(NewInsta = #{type := Type}) -> - case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of +update_gateway(GwName, Config) -> + case emqx_gateway_utils:find_sup_child(?MODULE, GwName) of {ok, GwSup} -> - emqx_gateway_gw_sup:update_insta(GwSup, NewInsta); + emqx_gateway_gw_sup:update_insta(GwSup, GwName, Config); _ -> {error, not_found} end. -start_gateway_insta(InstaId) -> - case search_gateway_insta_proc(InstaId) of +start_gateway_insta(GwName) -> + case search_gateway_insta_proc(GwName) of {ok, {GwSup, _}} -> - emqx_gateway_gw_sup:start_insta(GwSup, InstaId); + emqx_gateway_gw_sup:start_insta(GwSup, GwName); _ -> {error, not_found} end. --spec stop_gateway_insta(instance_id()) -> ok | {error, any()}. -stop_gateway_insta(InstaId) -> - case search_gateway_insta_proc(InstaId) of +-spec stop_gateway_insta(gateway_name()) -> ok | {error, any()}. +stop_gateway_insta(GwName) -> + case search_gateway_insta_proc(GwName) of {ok, {GwSup, _}} -> - emqx_gateway_gw_sup:stop_insta(GwSup, InstaId); + emqx_gateway_gw_sup:stop_insta(GwSup, GwName); _ -> {error, not_found} end. --spec list_gateway_insta(gateway_type()) -> {ok, [instance()]} | {error, any()}. -list_gateway_insta(Type) -> - case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of - {ok, GwSup} -> - {ok, emqx_gateway_gw_sup:list_insta(GwSup)}; - _ -> {error, not_found} - end. - --spec list_gateway_insta() -> [{gateway_type(), instance()}]. +-spec list_gateway_insta() -> [gateway()]. list_gateway_insta() -> - lists:map( + lists:append(lists:map( fun(SupId) -> - Instas = emqx_gateway_gw_sup:list_insta(SupId), - {SupId, Instas} - end, list_started_gateway()). + emqx_gateway_gw_sup:list_insta(SupId) + end, list_started_gateway())). --spec list_started_gateway() -> [gateway_type()]. +-spec list_started_gateway() -> [gateway_name()]. list_started_gateway() -> - started_gateway_type(). - --spec stop_all_suptree(atom()) -> ok. -stop_all_suptree(Type) -> - case lists:keyfind(Type, 1, supervisor:which_children(?MODULE)) of - false -> ok; - _ -> - _ = supervisor:terminate_child(?MODULE, Type), - _ = supervisor:delete_child(?MODULE, Type), - ok - end. + started_gateway(). %% Supervisor callback @@ -145,17 +125,14 @@ init([]) -> %% Internal funcs %%-------------------------------------------------------------------- -gatewayid(Type) -> - list_to_atom(lists:concat([Type])). - -ensure_gateway_suptree_ready(Type) -> - case lists:keyfind(Type, 1, supervisor:which_children(?MODULE)) of +ensure_gateway_suptree_ready(GwName) -> + case lists:keyfind(GwName, 1, supervisor:which_children(?MODULE)) of false -> ChildSpec = emqx_gateway_utils:childspec( - Type, + GwName, supervisor, emqx_gateway_gw_sup, - [Type] + [GwName] ), emqx_gateway_utils:supervisor_ret( supervisor:start_child(?MODULE, ChildSpec) @@ -176,7 +153,7 @@ search_gateway_insta_proc(InstaId, [SupPid|More]) -> search_gateway_insta_proc(InstaId, More) end. -started_gateway_type() -> +started_gateway() -> lists:filtermap( fun({Id, _, _, _}) -> is_a_gateway_id(Id) andalso {true, Id} @@ -190,4 +167,3 @@ started_gateway_pid() -> is_a_gateway_id(Id) -> Id /= emqx_gateway_registry. - diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 97d62da52..4f19db23b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -17,6 +17,8 @@ %% @doc Utils funcs for emqx-gateway -module(emqx_gateway_utils). +-include("emqx_gateway.hrl"). + -export([ childspec/2 , childspec/3 , childspec/4 @@ -26,9 +28,17 @@ -export([ apply/2 , format_listenon/1 + , parse_listenon/1 + , unix_ts_to_rfc3339/1 + , unix_ts_to_rfc3339/2 + , listener_id/3 + , parse_listener_id/1 ]). --export([ normalize_rawconf/1 +-export([ stringfy/1 + ]). + +-export([ normalize_config/1 ]). %% Common Envs @@ -105,37 +115,110 @@ format_listenon({Addr, Port}) when is_list(Addr) -> format_listenon({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). --type listener() :: #{}. +parse_listenon(Port) when is_integer(Port) -> + Port; +parse_listenon(Str) when is_binary(Str) -> + parse_listenon(binary_to_list(Str)); +parse_listenon(Str) when is_list(Str) -> + case emqx_schema:to_ip_port(Str) of + {ok, R} -> R; + {error, _} -> + error({invalid_listenon_name, Str}) + end. --type rawconf() :: - #{ clientinfo_override => #{} - , authenticators := list() - , listeners => listener() - , atom() => any() - }. +listener_id(GwName, Type, LisName) -> + binary_to_atom( + <<(bin(GwName))/binary, ":", + (bin(Type))/binary, ":", + (bin(LisName))/binary + >>). --spec normalize_rawconf(rawconf()) +parse_listener_id(Id) -> + try + [GwName, Type, Name] = binary:split(bin(Id), <<":">>, [global]), + {binary_to_existing_atom(GwName), binary_to_existing_atom(Type), + binary_to_atom(Name)} + catch + _ : _ -> error({invalid_listener_id, Id}) + end. + +bin(A) when is_atom(A) -> + atom_to_binary(A); +bin(L) when is_list(L); is_binary(L) -> + iolist_to_binary(L). + +unix_ts_to_rfc3339(Keys, Map) when is_list(Keys) -> + lists:foldl(fun(K, Acc) -> unix_ts_to_rfc3339(K, Acc) end, Map, Keys); +unix_ts_to_rfc3339(Key, Map) -> + case maps:get(Key, Map, undefined) of + undefined -> Map; + Ts -> + Map#{Key => + emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)} + end. + +unix_ts_to_rfc3339(Ts) -> + emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>). + +-spec stringfy(term()) -> binary(). +stringfy(T) -> + iolist_to_binary(io_lib:format("~0p", [T])). + +-spec normalize_config(emqx_config:config()) -> list({ Type :: udp | tcp | ssl | dtls + , Name :: atom() , ListenOn :: esockd:listen_on() , SocketOpts :: esockd:option() , Cfg :: map() }). -normalize_rawconf(RawConf = #{listener := LisMap}) -> - Cfg0 = maps:without([listener], RawConf), +normalize_config(RawConf) -> + LisMap = maps:get(listeners, RawConf, #{}), + Cfg0 = maps:without([listeners], RawConf), lists:append(maps:fold(fun(Type, Liss, AccIn1) -> Listeners = - maps:fold(fun(_Name, Confs, AccIn2) -> + maps:fold(fun(Name, Confs, AccIn2) -> ListenOn = maps:get(bind, Confs), - SocketOpts = esockd:parse_opt(maps:to_list(Confs)), + SocketOpts = esockd_opts(Type, Confs), RemainCfgs = maps:without( - [bind] ++ proplists:get_keys(SocketOpts), - Confs), + [bind, tcp, ssl, udp, dtls] + ++ proplists:get_keys(SocketOpts), Confs), Cfg = maps:merge(Cfg0, RemainCfgs), - [{Type, ListenOn, SocketOpts, Cfg}|AccIn2] + [{Type, Name, ListenOn, SocketOpts, Cfg}|AccIn2] end, [], Liss), [Listeners|AccIn1] end, [], LisMap)). +esockd_opts(Type, Opts0) -> + Opts1 = maps:with([acceptors, max_connections, max_conn_rate, + proxy_protocol, proxy_protocol_timeout], Opts0), + Opts2 = Opts1#{access_rules => esockd_access_rules(maps:get(access_rules, Opts0, []))}, + maps:to_list(case Type of + tcp -> Opts2#{tcp_options => sock_opts(tcp, Opts0)}; + ssl -> Opts2#{tcp_options => sock_opts(tcp, Opts0), + ssl_options => ssl_opts(ssl, Opts0)}; + udp -> Opts2#{udp_options => sock_opts(udp, Opts0)}; + dtls -> Opts2#{udp_options => sock_opts(udp, Opts0), + dtls_options => ssl_opts(dtls, Opts0)} + end). + +esockd_access_rules(StrRules) -> + Access = fun(S) -> + [A, CIDR] = string:tokens(S, " "), + {list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end} + end, + [Access(R) || R <- StrRules]. + +ssl_opts(Name, Opts) -> + maps:to_list( + emqx_tls_lib:drop_tls13_for_old_otp( + maps:without([enable], + maps:get(Name, Opts, #{})))). + +sock_opts(Name, Opts) -> + maps:to_list( + maps:without([active_n], + maps:get(Name, Opts, #{}))). + %%-------------------------------------------------------------------- %% Envs @@ -192,5 +275,6 @@ default_subopts() -> #{rh => 0, %% Retain Handling rap => 0, %% Retain as Publish nl => 0, %% No Local - qos => 0 %% QoS + qos => 0, %% QoS + is_new => true }. diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl index f6b33e0b2..4893f6d00 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl @@ -31,7 +31,7 @@ , handle_in/2 , handle_deliver/2 , handle_timeout/3 - , handle_call/2 + , handle_call/3 , handle_cast/2 , handle_info/2 , terminate/2 @@ -243,23 +243,24 @@ handle_timeout(_TRef, Msg, Channel) -> ?WARN("Unexpected timeout: ~p", [Msg]), {ok, Channel}. --spec handle_call(any(), channel()) +-spec handle_call(Req :: any(), From :: any(), channel()) -> {reply, Reply :: term(), channel()} | {reply, Reply :: term(), replies(), channel()} | {shutdown, Reason :: term(), Reply :: term(), channel()}. -handle_call({send, Data}, Channel) -> +handle_call({send, Data}, _From, Channel) -> {reply, ok, [{outgoing, Data}], Channel}; -handle_call(close, Channel = #channel{conn_state = connected}) -> +handle_call(close, _From, Channel = #channel{conn_state = connected}) -> {reply, ok, [{event, disconnected}, {close, normal}], Channel}; -handle_call(close, Channel) -> +handle_call(close, _From, Channel) -> {reply, ok, [{close, normal}], Channel}; -handle_call({auth, ClientInfo, _Password}, Channel = #channel{conn_state = connected}) -> +handle_call({auth, ClientInfo, _Password}, _From, + Channel = #channel{conn_state = connected}) -> ?LOG(warning, "Duplicated authorized command, dropped ~p", [ClientInfo]), {reply, {error, ?RESP_PERMISSION_DENY, <<"Duplicated authenticate command">>}, Channel}; -handle_call({auth, ClientInfo0, Password}, +handle_call({auth, ClientInfo0, Password}, _From, Channel = #channel{ ctx = Ctx, conninfo = ConnInfo, @@ -300,7 +301,7 @@ handle_call({auth, ClientInfo0, Password}, {reply, {error, ?RESP_PERMISSION_DENY, Reason}, Channel} end; -handle_call({start_timer, keepalive, Interval}, +handle_call({start_timer, keepalive, Interval}, _From, Channel = #channel{ conninfo = ConnInfo, clientinfo = ClientInfo @@ -310,7 +311,7 @@ handle_call({start_timer, keepalive, Interval}, NChannel = Channel#channel{conninfo = NConnInfo, clientinfo = NClientInfo}, {reply, ok, ensure_keepalive(NChannel)}; -handle_call({subscribe, TopicFilter, Qos}, +handle_call({subscribe_from_client, TopicFilter, Qos}, _From, Channel = #channel{ ctx = Ctx, conn_state = connected, @@ -323,12 +324,20 @@ handle_call({subscribe, TopicFilter, Qos}, {reply, ok, NChannel} end; -handle_call({unsubscribe, TopicFilter}, +handle_call({subscribe, Topic, SubOpts}, _From, Channel) -> + {ok, NChannel} = do_subscribe([{Topic, SubOpts}], Channel), + {reply, ok, NChannel}; + +handle_call({unsubscribe_from_client, TopicFilter}, _From, Channel = #channel{conn_state = connected}) -> {ok, NChannel} = do_unsubscribe([{TopicFilter, #{}}], Channel), {reply, ok, NChannel}; -handle_call({publish, Topic, Qos, Payload}, +handle_call({unsubscribe, Topic}, _From, Channel) -> + {ok, NChannel} = do_unsubscribe([Topic], Channel), + {reply, ok, NChannel}; + +handle_call({publish, Topic, Qos, Payload}, _From, Channel = #channel{ ctx = Ctx, conn_state = connected, @@ -345,10 +354,10 @@ handle_call({publish, Topic, Qos, Payload}, {reply, ok, Channel} end; -handle_call(kick, Channel) -> +handle_call(kick, _From, Channel) -> {shutdown, kicked, ok, Channel}; -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(warning, "Unexpected call: ~p", [Req]), {reply, {error, unexpected_call}, Channel}. @@ -363,12 +372,6 @@ handle_cast(Req, Channel) -> -spec handle_info(any(), channel()) -> {ok, channel()} | {shutdown, Reason :: term(), channel()}. -handle_info({subscribe, TopicFilters}, Channel) -> - do_subscribe(TopicFilters, Channel); - -handle_info({unsubscribe, TopicFilters}, Channel) -> - do_unsubscribe(TopicFilters, Channel); - handle_info({sock_closed, Reason}, Channel = #channel{rqueue = Queue, inflight = Inflight}) -> case queue:len(Queue) =:= 0 diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl index 83c148968..d7a82023b 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl @@ -22,9 +22,10 @@ -include("emqx_exproto.hrl"). -include_lib("emqx/include/logger.hrl"). - -define(IS_QOS(X), (X =:= 0 orelse X =:= 1 orelse X =:= 2)). +-define(DEFAULT_CALL_TIMEOUT, 5000). + %% gRPC server callbacks -export([ send/2 , close/2 @@ -96,7 +97,7 @@ publish(Req, Md) -> subscribe(Req = #{conn := Conn, topic := Topic, qos := Qos}, Md) when ?IS_QOS(Qos) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), - {ok, response(call(Conn, {subscribe, Topic, Qos})), Md}; + {ok, response(call(Conn, {subscribe_from_client, Topic, Qos})), Md}; subscribe(Req, Md) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), @@ -107,7 +108,7 @@ subscribe(Req, Md) -> | {error, grpc_cowboy_h:error_response()}. unsubscribe(Req = #{conn := Conn, topic := Topic}, Md) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), - {ok, response(call(Conn, {unsubscribe, Topic})), Md}. + {ok, response(call(Conn, {unsubscribe_from_client, Topic})), Md}. %%-------------------------------------------------------------------- %% Internal funcs @@ -117,18 +118,22 @@ to_pid(ConnStr) -> binary_to_term(base64:decode(ConnStr)). call(ConnStr, Req) -> - case catch to_pid(ConnStr) of - {'EXIT', {badarg, _}} -> - {error, ?RESP_PARAMS_TYPE_ERROR, - <<"The conn type error">>}; - Pid when is_pid(Pid) -> - case erlang:is_process_alive(Pid) of - true -> - emqx_gateway_conn:call(Pid, Req); - false -> - {error, ?RESP_CONN_PROCESS_NOT_ALIVE, - <<"Connection process is not alive">>} - end + try + Pid = to_pid(ConnStr), + emqx_gateway_conn:call(Pid, Req, ?DEFAULT_CALL_TIMEOUT) + catch + exit : badarg -> + {error, ?RESP_PARAMS_TYPE_ERROR, <<"The conn type error">>}; + exit : noproc -> + {error, ?RESP_CONN_PROCESS_NOT_ALIVE, + <<"Connection process is not alive">>}; + exit : timeout -> + {error, ?RESP_UNKNOWN, <<"Connection is not answered">>}; + Class : Reason : Stk-> + ?LOG(error, "Call ~p crashed: {~0p, ~0p}, " + "stacktrace: ~0p", + [Class, Reason, Stk]), + {error, ?RESP_UNKNOWN, <<"Unkwown crashs">>} end. %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 6f47e25ff..3e142f3dc 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -20,16 +20,13 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([]). - --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). -include_lib("emqx/include/logger.hrl"). @@ -38,24 +35,21 @@ %% APIs %%-------------------------------------------------------------------- -load() -> +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - emqx_gateway_registry:load(exproto, RegistryOptions, []). - -unload() -> - emqx_gateway_registry:unload(exproto). - -init(_) -> - GwState = #{}, - {ok, GwState}. + emqx_gateway_registry:reg(exproto, RegistryOptions). +unreg() -> + emqx_gateway_registry:unreg(exproto). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -start_grpc_server(InstaId, Options = #{bind := ListenOn}) -> +start_grpc_server(_GwName, undefined) -> + undefined; +start_grpc_server(GwName, Options = #{bind := ListenOn}) -> Services = #{protos => [emqx_exproto_pb], services => #{ 'emqx.exproto.v1.ConnectionAdapter' => emqx_exproto_gsvr} @@ -65,10 +59,12 @@ start_grpc_server(InstaId, Options = #{bind := ListenOn}) -> SslOpts -> [{ssl_options, SslOpts}] end, - _ = grpc:start_server(InstaId, ListenOn, Services, SvrOptions), - ?ULOG("Start ~s gRPC server on ~p successfully.~n", [InstaId, ListenOn]). + _ = grpc:start_server(GwName, ListenOn, Services, SvrOptions), + ?ULOG("Start ~s gRPC server on ~p successfully.~n", [GwName, ListenOn]). -start_grpc_client_channel(InstaId, Options = #{address := UriStr}) -> +start_grpc_client_channel(_GwType, undefined) -> + undefined; +start_grpc_client_channel(GwName, Options = #{address := UriStr}) -> UriMap = uri_string:parse(UriStr), Scheme = maps:get(scheme, UriMap), Host = maps:get(host, UriMap), @@ -85,79 +81,79 @@ start_grpc_client_channel(InstaId, Options = #{address := UriStr}) -> transport_opts => SslOpts}}; _ -> #{} end, - grpc_client_sup:create_channel_pool(InstaId, SvrAddr, ClientOpts). + grpc_client_sup:create_channel_pool(GwName, SvrAddr, ClientOpts). -on_insta_create(_Insta = #{ id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> +on_gateway_load(_Gateway = #{ name := GwName, + config := Config + }, Ctx) -> %% XXX: How to monitor it ? %% Start grpc client pool & client channel - PoolName = pool_name(InstaId), + PoolName = pool_name(GwName), PoolSize = emqx_vm:schedulers() * 2, {ok, _} = emqx_pool_sup:start_link(PoolName, hash, PoolSize, {emqx_exproto_gcli, start_link, []}), - _ = start_grpc_client_channel(InstaId, maps:get(handler, RawConf)), + _ = start_grpc_client_channel(GwName, maps:get(handler, Config, undefined)), %% XXX: How to monitor it ? - _ = start_grpc_server(InstaId, maps:get(server, RawConf)), + _ = start_grpc_server(GwName, maps:get(server, Config, undefined)), - NRawConf = maps:without( + NConfig = maps:without( [server, handler], - RawConf#{pool_name => PoolName} + Config#{pool_name => PoolName} ), - Listeners = emqx_gateway_utils:normalize_rawconf( - NRawConf#{handler => InstaId} + Listeners = emqx_gateway_utils:normalize_config( + NConfig#{handler => GwName} ), ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), - {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. + {ok, ListenerPids, _GwState = #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, Gateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? - on_insta_destroy(OldInsta, GwInstaState, GwState), - on_insta_create(NewInsta, Ctx, GwState) + on_gateway_unload(Gateway, GwState), + on_gateway_load(Gateway#{config => Config}, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update exproto instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, _GwInstaState, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), +on_gateway_unload(_Gateway = #{ name := GwName, + config := Config + }, _GwState) -> + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) + stop_listener(GwName, Lis) end, Listeners). -pool_name(InstaId) -> - list_to_atom(lists:concat([InstaId, "_gcli_pool"])). +pool_name(GwName) -> + list_to_atom(lists:concat([GwName, "_gcli_pool"])). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start exproto ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start exproto ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, Type), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_exproto_frame, @@ -176,9 +172,6 @@ do_start_listener(udp, Name, ListenOn, Opts, MFA) -> do_start_listener(dtls, Name, ListenOn, Opts, MFA) -> esockd:open_dtls(Name, ListenOn, Opts, MFA). -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). - merge_default_by_type(Type, Options) when Type =:= tcp; Type =:= ssl -> Default = emqx_gateway_utils:default_tcp_options(), @@ -200,18 +193,18 @@ merge_default_by_type(Type, Options) when Type =:= udp; [{udp_options, Default} | Options] end. -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop exproto ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop exproto ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl index 80449238c..98c9fabe8 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl @@ -16,140 +16,119 @@ -module(emqx_lwm2m_api). --rest_api(#{name => list, - method => 'GET', - path => "/lwm2m_channels/", - func => list, - descr => "A list of all lwm2m channel" - }). +-behaviour(minirest_api). --rest_api(#{name => list, - method => 'GET', - path => "/nodes/:atom:node/lwm2m_channels/", - func => list, - descr => "A list of lwm2m channel of a node" - }). +-export([api_spec/0]). --rest_api(#{name => lookup_cmd, - method => 'GET', - path => "/lookup_cmd/:bin:ep/", - func => lookup_cmd, - descr => "Send a lwm2m downlink command" - }). +-export([lookup_cmd/2]). --rest_api(#{name => lookup_cmd, - method => 'GET', - path => "/nodes/:atom:node/lookup_cmd/:bin:ep/", - func => lookup_cmd, - descr => "Send a lwm2m downlink command of a node" - }). +-define(PREFIX, "/gateway/lwm2m/:clientid"). --export([ list/2 - , lookup_cmd/2 - ]). +-import(emqx_mgmt_util, [ object_schema/1 + , error_schema/2 + , properties/1]). -list(#{node := Node }, Params) -> - case Node = node() of - true -> list(#{}, Params); - _ -> rpc_call(Node, list, [#{}, Params]) - end; +api_spec() -> + {[lookup_cmd_api()], []}. -list(#{}, _Params) -> - Channels = emqx_lwm2m_cm:all_channels(), - return({ok, format(Channels)}). +lookup_cmd_paramters() -> + [ make_paramter(clientid, path, true, "string") + , make_paramter(path, query, true, "string") + , make_paramter(action, query, true, "string")]. -lookup_cmd(#{ep := Ep, node := Node}, Params) -> - case Node = node() of - true -> lookup_cmd(#{ep => Ep}, Params); - _ -> rpc_call(Node, lookup_cmd, [#{ep => Ep}, Params]) - end; +lookup_cmd_properties() -> + properties([ {clientid, string} + , {path, string} + , {action, string} + , {code, string} + , {codeMsg, string} + , {content, {array, object}, lookup_cmd_content_props()}]). -lookup_cmd(#{ep := Ep}, Params) -> - MsgType = proplists:get_value(<<"msgType">>, Params), - Path0 = proplists:get_value(<<"path">>, Params), - case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of - [] -> return({ok, []}); - [{_, undefined} | _] -> return({ok, []}); - [{{IMEI, Path, MsgType}, undefined}] -> - return({ok, [{imei, IMEI}, - {'msgType', IMEI}, - {'code', <<"6.01">>}, - {'codeMsg', <<"reply_not_received">>}, - {'path', Path}]}); - [{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] -> - Payload1 = format_cmd_content(Content, MsgType), - return({ok, [{imei, IMEI}, - {'msgType', IMEI}, - {'code', Code}, - {'codeMsg', CodeMsg}, - {'path', Path}] ++ Payload1}) +lookup_cmd_content_props() -> + [ {operations, string, <<"Resource Operations">>} + , {dataType, string, <<"Resource Type">>} + , {path, string, <<"Resource Path">>} + , {name, string, <<"Resource Name">>}]. + +lookup_cmd_api() -> + Metadata = #{get => + #{description => <<"look up resource">>, + parameters => lookup_cmd_paramters(), + responses => + #{<<"200">> => object_schema(lookup_cmd_properties()), + <<"404">> => error_schema("client not found error", ['CLIENT_NOT_FOUND']) + } + }}, + {?PREFIX ++ "/lookup_cmd", Metadata, lookup_cmd}. + + +lookup_cmd(get, #{bindings := Bindings, query_string := QS}) -> + ClientId = maps:get(clientid, Bindings), + case emqx_gateway_cm_registry:lookup_channels(lwm2m, ClientId) of + [Channel | _] -> + #{<<"path">> := Path, + <<"action">> := Action} = QS, + {ok, Result} = emqx_lwm2m_channel:lookup_cmd(Channel, Path, Action), + lookup_cmd_return(Result, ClientId, Action, Path); + _ -> + {404, #{code => 'CLIENT_NOT_FOUND'}} end. -rpc_call(Node, Fun, Args) -> - case rpc:call(Node, ?MODULE, Fun, Args) of - {badrpc, Reason} -> {error, Reason}; - Res -> Res - end. +lookup_cmd_return(undefined, ClientId, Action, Path) -> + {200, + #{clientid => ClientId, + action => Action, + code => <<"6.01">>, + codeMsg => <<"reply_not_received">>, + path => Path}}; -format(Channels) -> - lists:map(fun({IMEI, #{lifetime := LifeTime, - peername := Peername, - version := Version, - reg_info := RegInfo}}) -> - ObjectList = lists:map(fun(Path) -> - [ObjId | _] = path_list(Path), - case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of - {error, _} -> - {Path, Path}; - ObjDefinition -> - ObjectName = emqx_lwm2m_xml_object:get_object_name(ObjDefinition), - {Path, list_to_binary(ObjectName)} - end - end, maps:get(<<"objectList">>, RegInfo)), - {IpAddr, Port} = Peername, - [{imei, IMEI}, - {lifetime, LifeTime}, - {ip_address, iolist_to_binary(ntoa(IpAddr))}, - {port, Port}, - {version, Version}, - {'objectList', ObjectList}] - end, Channels). +lookup_cmd_return({Code, CodeMsg, Content}, ClientId, Action, Path) -> + {200, + format_cmd_content(Content, + Action, + #{clientid => ClientId, + action => Action, + code => Code, + codeMsg => CodeMsg, + path => Path})}. -format_cmd_content(undefined, _MsgType) -> []; -format_cmd_content(Content, <<"discover">>) -> +format_cmd_content(undefined, _MsgType, Result) -> + Result; + +format_cmd_content(Content, <<"discover">>, Result) -> [H | Content1] = Content, - {_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H), + {_, [HObjId]} = emqx_lwm2m_session:parse_object_list(H), [ObjId | _]= path_list(HObjId), ObjectList = case Content1 of - [Content2 | _] -> - {_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2), - ObjL; - [] -> [] - end, - R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of - {error, _} -> - lists:map(fun(Object) -> {Object, Object} end, ObjectList); - ObjDefinition -> - lists:map(fun(Object) -> - [_, _, ResId| _] = path_list(Object), - Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of - "E" -> [{operations, list_to_binary("E")}]; - Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))}, - {operations, list_to_binary(Oper)}] - end, - [{path, Object}, - {name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))} - ] ++ Operations - end, ObjectList) - end, - [{content, R}]; -format_cmd_content(Content, _) -> - [{content, Content}]. + [Content2 | _] -> + {_, ObjL} = emqx_lwm2m_session:parse_object_list(Content2), + ObjL; + [] -> [] + end, -ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> - inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); -ntoa(IP) -> - inet_parse:ntoa(IP). + R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of + {error, _} -> + lists:map(fun(Object) -> #{Object => Object} end, ObjectList); + ObjDefinition -> + lists:map( + fun(Object) -> + [_, _, RawResId| _] = path_list(Object), + ResId = binary_to_integer(RawResId), + Operations = case emqx_lwm2m_xml_object:get_resource_operations(ResId, ObjDefinition) of + "E" -> + #{operations => list_to_binary("E")}; + Oper -> + #{'dataType' => list_to_binary(emqx_lwm2m_xml_object:get_resource_type(ResId, ObjDefinition)), + operations => list_to_binary(Oper)} + end, + Operations#{path => Object, + name => list_to_binary(emqx_lwm2m_xml_object:get_resource_name(ResId, ObjDefinition))} + end, ObjectList) + end, + Result#{content => R}; + +format_cmd_content(Content, _, Result) -> + Result#{content => Content}. path_list(Path) -> case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of @@ -159,6 +138,8 @@ path_list(Path) -> [ObjId] -> [ObjId] end. -return(_) -> -%% TODO: V5 API - ok. +make_paramter(Name, In, IsRequired, Type) -> + #{name => Name, + in => In, + required => IsRequired, + schema => #{type => Type}}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl new file mode 100644 index 000000000..e25be88fc --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -0,0 +1,489 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_lwm2m_channel). + +-include_lib("emqx/include/logger.hrl"). +-include("emqx_coap.hrl"). +-include("emqx_lwm2m.hrl"). + +%% API +-export([ info/1 + , info/2 + , stats/1 + , with_context/2 + , do_takeover/3 + , lookup_cmd/3]). + +-export([ init/2 + , handle_in/2 + , handle_deliver/2 + , handle_timeout/3 + , terminate/2 + ]). + +-export([ handle_call/3 + , handle_cast/2 + , handle_info/2 + ]). + +-record(channel, { + %% Context + ctx :: emqx_gateway_ctx:context(), + %% Connection Info + conninfo :: emqx_types:conninfo(), + %% Client Info + clientinfo :: emqx_types:clientinfo(), + %% Session + session :: emqx_lwm2m_session:session() | undefined, + + %% Timer + timers :: #{atom() => disable | undefined | reference()}, + + with_context :: function() + }). + +-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). +-import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +info(Channel) -> + maps:from_list(info(?INFO_KEYS, Channel)). + +info(Keys, Channel) when is_list(Keys) -> + [{Key, info(Key, Channel)} || Key <- Keys]; + +info(conninfo, #channel{conninfo = ConnInfo}) -> + ConnInfo; +info(conn_state, _) -> + connected; +info(clientinfo, #channel{clientinfo = ClientInfo}) -> + ClientInfo; +info(session, #channel{session = Session}) -> + emqx_misc:maybe_apply(fun emqx_session:info/1, Session); +info(clientid, #channel{clientinfo = #{clientid := ClientId}}) -> + ClientId; +info(ctx, #channel{ctx = Ctx}) -> + Ctx. + +stats(_) -> + []. + +init(ConnInfo = #{peername := {PeerHost, _}, + sockname := {_, SockPort}}, + #{ctx := Ctx} = Config) -> + Peercert = maps:get(peercert, ConnInfo, undefined), + Mountpoint = maps:get(mountpoint, Config, undefined), + ClientInfo = set_peercert_infos( + Peercert, + #{ zone => default + , protocol => lwm2m + , peerhost => PeerHost + , sockport => SockPort + , username => undefined + , clientid => undefined + , is_bridge => false + , is_superuser => false + , mountpoint => Mountpoint + } + ), + + #channel{ ctx = Ctx + , conninfo = ConnInfo + , clientinfo = ClientInfo + , timers = #{} + , session = emqx_lwm2m_session:new() + , with_context = with_context(Ctx, ClientInfo) + }. + + +with_context(Ctx, ClientInfo) -> + fun(Type, Topic) -> + with_context(Type, Topic, Ctx, ClientInfo) + end. + +lookup_cmd(Channel, Path, Action) -> + gen_server:call(Channel, {?FUNCTION_NAME, Path, Action}). + +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- +handle_in(Msg, ChannleT) -> + Channel = update_life_timer(ChannleT), + call_session(handle_coap_in, Msg, Channel). + +%%-------------------------------------------------------------------- +%% Handle Delivers from broker to client +%%-------------------------------------------------------------------- +handle_deliver(Delivers, Channel) -> + call_session(handle_deliver, Delivers, Channel). + +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- +handle_timeout(_, lifetime, #channel{ctx = Ctx, + clientinfo = ClientInfo, + conninfo = ConnInfo} = Channel) -> + ok = run_hooks(Ctx, 'client.disconnected', [ClientInfo, timeout, ConnInfo]), + {shutdown, timeout, Channel}; + +handle_timeout(_, {transport, _} = Msg, Channel) -> + call_session(timeout, Msg, Channel); + +handle_timeout(_, disconnect, Channel) -> + {shutdown, normal, Channel}; + +handle_timeout(_, _, Channel) -> + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle call +%%-------------------------------------------------------------------- +handle_call({lookup_cmd, Path, Type}, _From, #channel{session = Session} = Channel) -> + Result = emqx_lwm2m_session:find_cmd_record(Path, Type, Session), + {reply, {ok, Result}, Channel}; + +handle_call(Req, _From, Channel) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {reply, ignored, Channel}. + +%%-------------------------------------------------------------------- +%% Handle Cast +%%-------------------------------------------------------------------- +handle_cast(Req, Channel) -> + ?LOG(error, "Unexpected cast: ~p", [Req]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle Info +%%-------------------------------------------------------------------- +handle_info({subscribe, _AutoSubs}, Channel) -> + %% not need handle this message + {ok, Channel}; + +handle_info(Info, Channel) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Terminate +%%-------------------------------------------------------------------- +terminate(Reason, #channel{ctx = Ctx, + clientinfo = ClientInfo, + session = Session}) -> + MountedTopic = emqx_lwm2m_session:on_close(Session), + _ = run_hooks(Ctx, 'session.unsubscribe', [ClientInfo, MountedTopic, #{}]), + run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]). + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +set_peercert_infos(NoSSL, ClientInfo) + when NoSSL =:= nossl; + NoSSL =:= undefined -> + ClientInfo; +set_peercert_infos(Peercert, ClientInfo) -> + {DN, CN} = {esockd_peercert:subject(Peercert), + esockd_peercert:common_name(Peercert)}, + ClientInfo#{dn => DN, cn => CN}. + +make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) -> + TRef = emqx_misc:start_timer(Time, Msg), + Channel#channel{timers = Timers#{Name => TRef}}. + +update_life_timer(#channel{session = Session, timers = Timers} = Channel) -> + LifeTime = emqx_lwm2m_session:info(lifetime, Session), + _ = case maps:get(lifetime, Timers, undefined) of + undefined -> ok; + Ref -> erlang:cancel_timer(Ref) + end, + make_timer(lifetime, LifeTime, lifetime, Channel). + +check_location(Location, #channel{session = Session}) -> + SLocation = emqx_lwm2m_session:info(location_path, Session), + Location =:= SLocation. + +do_takeover(_DesireId, Msg, Channel) -> + %% TODO completed the takeover, now only reset the message + Reset = emqx_coap_message:reset(Msg), + call_session(handle_out, Reset, Channel). + +do_connect(Req, Result, Channel, Iter) -> + case emqx_misc:pipeline( + [ fun check_lwm2m_version/2 + , fun run_conn_hooks/2 + , fun enrich_clientinfo/2 + , fun set_log_meta/2 + , fun auth_connect/2 + ], + Req, + Channel) of + {ok, _Input, #channel{session = Session, + with_context = WithContext} = NChannel} -> + case emqx_lwm2m_session:info(reg_info, Session) of + undefined -> + process_connect(ensure_connected(NChannel), Req, Result, Iter); + _ -> + NewResult = emqx_lwm2m_session:reregister(Req, WithContext, Session), + iter(Iter, maps:merge(Result, NewResult), NChannel) + end; + {error, ReasonCode, NChannel} -> + ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), + Payload = erlang:list_to_binary(lists:flatten(ErrMsg)), + iter(Iter, + reply({error, bad_request}, Payload, Req, Result), + NChannel) + end. + +check_lwm2m_version(#coap_message{options = Opts}, + #channel{conninfo = ConnInfo} = Channel) -> + Ver = gets([uri_query, <<"lwm2m">>], Opts), + IsValid = case Ver of + <<"1.0">> -> + true; + <<"1">> -> + true; + <<"1.1">> -> + true; + _ -> + false + end, + if IsValid -> + NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond) + , proto_name => <<"LwM2M">> + , proto_ver => Ver + }, + {ok, Channel#channel{conninfo = NConnInfo}}; + true -> + ?LOG(error, "Reject REGISTER due to unsupported version: ~0p", [Ver]), + {error, "invalid lwm2m version", Channel} + end. + +run_conn_hooks(Input, Channel = #channel{ctx = Ctx, + conninfo = ConnInfo}) -> + ConnProps = #{}, + case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of + Error = {error, _Reason} -> Error; + _NConnProps -> + {ok, Input, Channel} + end. + +enrich_clientinfo(#coap_message{options = Options} = Msg, + Channel = #channel{clientinfo = ClientInfo0}) -> + Query = maps:get(uri_query, Options, #{}), + case Query of + #{<<"ep">> := Epn} -> + UserName = maps:get(<<"imei">>, Query, Epn), + Password = maps:get(<<"password">>, Query, undefined), + ClientId = maps:get(<<"device_id">>, Query, Epn), + ClientInfo = + ClientInfo0#{username => UserName, + password => Password, + clientid => ClientId}, + {ok, NClientInfo} = fix_mountpoint(Msg, ClientInfo), + {ok, Channel#channel{clientinfo = NClientInfo}}; + _ -> + ?LOG(error, "Reject REGISTER due to wrong parameters, Query=~p", [Query]), + {error, "invalid queries", Channel} + end. + +set_log_meta(_Input, #channel{clientinfo = #{clientid := ClientId}}) -> + emqx_logger:set_metadata_clientid(ClientId), + ok. + +auth_connect(_Input, Channel = #channel{ctx = Ctx, + clientinfo = ClientInfo}) -> + #{clientid := ClientId, username := Username} = ClientInfo, + case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of + {ok, NClientInfo} -> + {ok, Channel#channel{clientinfo = NClientInfo, + with_context = with_context(Ctx, ClientInfo)}}; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", + [ClientId, Username, Reason]), + {error, Reason} + end. + +fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) -> + {ok, ClientInfo}; +fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> + Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo), + {ok, ClientInfo#{mountpoint := Mountpoint1}}. + +ensure_connected(Channel = #channel{ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + _ = run_hooks(Ctx, 'client.connack', [ConnInfo, connection_accepted, []]), + ok = run_hooks(Ctx, 'client.connected', [ClientInfo, ConnInfo]), + Channel. + +process_connect(Channel = #channel{ctx = Ctx, + session = Session, + conninfo = ConnInfo, + clientinfo = ClientInfo, + with_context = WithContext}, + Msg, Result, Iter) -> + %% inherit the old session + SessFun = fun(_,_) -> #{} end, + case emqx_gateway_ctx:open_session( + Ctx, + true, + ClientInfo, + ConnInfo, + SessFun, + emqx_lwm2m_session + ) of + {ok, _} -> + Mountpoint = maps:get(mountpoint, ClientInfo, <<>>), + NewResult = emqx_lwm2m_session:init(Msg, Mountpoint, WithContext, Session), + iter(Iter, maps:merge(Result, NewResult), Channel); + {error, Reason} -> + ?LOG(error, "Failed to open session du to ~p", [Reason]), + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) + end. + +run_hooks(Ctx, Name, Args) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run(Name, Args). + +run_hooks(Ctx, Name, Args, Acc) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run_fold(Name, Args, Acc). + +gets(_, undefined) -> + undefined; +gets([H | T], Map) -> + gets(T, maps:get(H, Map, undefined)); +gets([], Val) -> + Val. + +with_context(publish, [Topic, Msg], Ctx, ClientInfo) -> + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of + allow -> + emqx:publish(Msg); + _ -> + ?LOG(error, "topic:~p not allow to publish ", [Topic]) + end; + +with_context(subscribe, [Topic, Opts], Ctx, #{username := UserName} = ClientInfo) -> + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic) of + allow -> + run_hooks(Ctx, 'session.subscribed', [ClientInfo, Topic, UserName]), + ?LOG(debug, "Subscribe topic: ~0p, Opts: ~0p, EndpointName: ~0p", [Topic, Opts, UserName]), + emqx:subscribe(Topic, UserName, Opts); + _ -> + ?LOG(error, "Topic: ~0p not allow to subscribe", [Topic]) + end; + +with_context(metrics, Name, Ctx, _ClientInfo) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name). + +%%-------------------------------------------------------------------- +%% Call Chain +%%-------------------------------------------------------------------- +call_session(Fun, + Msg, + #channel{session = Session, + with_context = WithContext} = Channel) -> + iter([ session, fun process_session/4 + , proto, fun process_protocol/4 + , return, fun process_return/4 + , lifetime, fun process_lifetime/4 + , reply, fun process_reply/4 + , out, fun process_out/4 + , fun process_nothing/3 + ], + emqx_lwm2m_session:Fun(Msg, WithContext, Session), + Channel). + +process_session(Session, Result, Channel, Iter) -> + iter(Iter, Result, Channel#channel{session = Session}). + +process_protocol({request, Msg}, Result, Channel, Iter) -> + #coap_message{method = Method} = Msg, + handle_request_protocol(Method, Msg, Result, Channel, Iter); + +process_protocol(Msg, Result, + #channel{with_context = WithContext, session = Session} = Channel, Iter) -> + ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, WithContext, Session), + iter(Iter, maps:merge(Result, ProtoResult), Channel). + +handle_request_protocol(post, #coap_message{options = Opts} = Msg, + Result, Channel, Iter) -> + case Opts of + #{uri_path := [?REG_PREFIX]} -> + do_connect(Msg, Result, Channel, Iter); + #{uri_path := Location} -> + do_update(Location, Msg, Result, Channel, Iter); + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end; + +handle_request_protocol(delete, #coap_message{options = Opts} = Msg, + Result, Channel, Iter) -> + case Opts of + #{uri_path := Location} -> + case check_location(Location, Channel) of + true -> + Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), + {shutdown, close, Reply, Channel}; + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end; + _ -> + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) + end. + +do_update(Location, Msg, Result, + #channel{session = Session, with_context = WithContext} = Channel, Iter) -> + case check_location(Location, Channel) of + true -> + NewResult = emqx_lwm2m_session:update(Msg, WithContext, Session), + iter(Iter, maps:merge(Result, NewResult), Channel); + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end. + +process_return({Outs, Session}, Result, Channel, Iter) -> + OldOuts = maps:get(out, Result, []), + iter(Iter, + Result#{out => Outs ++ OldOuts}, + Channel#channel{session = Session}). + +process_out(Outs, Result, Channel, _) -> + Outs2 = lists:reverse(Outs), + Outs3 = case maps:get(reply, Result, undefined) of + undefined -> + Outs2; + Reply -> + [Reply | Outs2] + end, + + {ok, {outgoing, Outs3}, Channel}. + +process_reply(Reply, Result, #channel{session = Session} = Channel, _) -> + Session2 = emqx_lwm2m_session:set_reply(Reply, Session), + Outs = maps:get(out, Result, []), + Outs2 = lists:reverse(Outs), + {ok, {outgoing, [Reply | Outs2]}, Channel#channel{session = Session2}}. + +process_lifetime(_, Result, Channel, Iter) -> + iter(Iter, Result, update_life_timer(Channel)). + +process_nothing(_, _, Channel) -> + {ok, Channel}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl deleted file mode 100644 index 16e938b84..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl +++ /dev/null @@ -1,153 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_cm). - --export([start_link/0]). - --export([ register_channel/5 - , update_reg_info/2 - , unregister_channel/1 - ]). - --export([ lookup_channel/1 - , all_channels/0 - ]). - --export([ register_cmd/3 - , register_cmd/4 - , lookup_cmd/3 - , lookup_cmd_by_imei/1 - ]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-CM: " ++ Format, Args)). - -%% Server name --define(CM, ?MODULE). - --define(LWM2M_CHANNEL_TAB, emqx_lwm2m_channel). --define(LWM2M_CMD_TAB, emqx_lwm2m_cmd). - -%% Batch drain --define(BATCH_SIZE, 100000). - -%% @doc Start the channel manager. -start_link() -> - gen_server:start_link({local, ?CM}, ?MODULE, [], []). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -register_channel(IMEI, RegInfo, LifeTime, Ver, Peername) -> - Info = #{ - reg_info => RegInfo, - lifetime => LifeTime, - version => Ver, - peername => Peername - }, - true = ets:insert(?LWM2M_CHANNEL_TAB, {IMEI, Info}), - cast({registered, {IMEI, self()}}). - -update_reg_info(IMEI, RegInfo) -> - case lookup_channel(IMEI) of - [{_, RegInfo0}] -> - true = ets:insert(?LWM2M_CHANNEL_TAB, {IMEI, RegInfo0#{reg_info => RegInfo}}), - ok; - [] -> - ok - end. - -unregister_channel(IMEI) when is_binary(IMEI) -> - true = ets:delete(?LWM2M_CHANNEL_TAB, IMEI), - ok. - -lookup_channel(IMEI) -> - ets:lookup(?LWM2M_CHANNEL_TAB, IMEI). - -all_channels() -> - ets:tab2list(?LWM2M_CHANNEL_TAB). - -register_cmd(IMEI, Path, Type) -> - true = ets:insert(?LWM2M_CMD_TAB, {{IMEI, Path, Type}, undefined}). - -register_cmd(_IMEI, undefined, _Type, _Result) -> - ok; -register_cmd(IMEI, Path, Type, Result) -> - true = ets:insert(?LWM2M_CMD_TAB, {{IMEI, Path, Type}, Result}). - -lookup_cmd(IMEI, Path, Type) -> - ets:lookup(?LWM2M_CMD_TAB, {IMEI, Path, Type}). - -lookup_cmd_by_imei(IMEI) -> - ets:select(?LWM2M_CHANNEL_TAB, [{{{IMEI, '_', '_'}, '$1'}, [], ['$_']}]). - -%% @private -cast(Msg) -> gen_server:cast(?CM, Msg). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([]) -> - TabOpts = [public, {write_concurrency, true}, {read_concurrency, true}], - ok = emqx_tables:new(?LWM2M_CHANNEL_TAB, [set, compressed | TabOpts]), - ok = emqx_tables:new(?LWM2M_CMD_TAB, [set, compressed | TabOpts]), - {ok, #{chan_pmon => emqx_pmon:new()}}. - -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast({registered, {IMEI, ChanPid}}, State = #{chan_pmon := PMon}) -> - PMon1 = emqx_pmon:monitor(ChanPid, IMEI, PMon), - {noreply, State#{chan_pmon := PMon1}}; - -handle_cast(Msg, State) -> - ?LOG(error, "Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) -> - ChanPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], - {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), - ok = emqx_pool:async_submit(fun lists:foreach/2, [fun clean_down/1, Items]), - {noreply, State#{chan_pmon := PMon1}}; - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - emqx_stats:cancel_update(chan_stats). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -clean_down({_ChanPid, IMEI}) -> - unregister_channel(IMEI). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl new file mode 100644 index 000000000..d2704d691 --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl @@ -0,0 +1,410 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2016-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_lwm2m_cmd). + +-include_lib("emqx/include/logger.hrl"). +-include("emqx_coap.hrl"). +-include("emqx_lwm2m.hrl"). + +-export([ mqtt_to_coap/2 + , coap_to_mqtt/4 + , empty_ack_to_mqtt/1 + , coap_failure_to_mqtt/2 + ]). + +-export([path_list/1, extract_path/1]). + +-define(STANDARD, 1). + +%%-type msg_type() :: <<"create">> +%% | <<"delete">> +%% | <<"read">> +%% | <<"write">> +%% | <<"execute">> +%% | <<"discover">> +%% | <<"write-attr">> +%% | <<"observe">> +%% | <<"cancel-observe">>. +%% +%%-type cmd() :: #{ <<"msgType">> := msg_type() +%% , <<"data">> := maps() +%% %%%% more keys? +%% }. + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"basePath">>, Data, <<"/">>)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)), + Payload = emqx_lwm2m_tlv:encode(TlvData), + CoapRequest = emqx_coap_message:request(con, post, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]), + {CoapRequest, InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, delete, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) -> + CoapRequest = + case maps:get(<<"basePath">>, Data, <<"/">>) of + <<"/">> -> + single_write_request(AlternatePath, Data); + BasePath -> + batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data)) + end, + {CoapRequest, InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + Args = + case maps:get(<<"args">>, Data, <<>>) of + <<"undefined">> -> <<>>; + undefined -> <<>>; + Arg1 -> Arg1 + end, + {emqx_coap_message:request(con, post, Args, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"text/plain">>}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {'accept', ?LWM2M_FORMAT_LINK}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + Query = attr_query_list(Data), + {emqx_coap_message:request(con, put, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {uri_query, Query}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {observe, 0}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {observe, 1}]), InputCmd}. + +coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) -> + make_response(Code, Ref); + +coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) -> + make_response(Code, Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) -> + read_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) -> + write_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) -> + execute_resp_to_mqtt(Method, Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) -> + discover_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) -> + writeattr_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) -> + observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) -> + cancel_observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref). + +read_resp_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) -> + make_response(ErrorCode, Ref); + +read_resp_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) -> + try + Result = content_to_mqtt(CoapPayload, Format, Ref), + make_response(SuccessCode, Ref, Format, Result) + catch + error:not_implemented -> make_response(not_implemented, Ref); + _:Ex:_ST -> + ?LOG(error, "~0p, bad payload format: ~0p", [Ex, CoapPayload]), + make_response(bad_request, Ref) + end. + +empty_ack_to_mqtt(Ref) -> + make_base_response(maps:put(<<"msgType">>, <<"ack">>, Ref)). + +coap_failure_to_mqtt(Ref, MsgType) -> + make_base_response(maps:put(<<"msgType">>, MsgType, Ref)). + +content_to_mqtt(CoapPayload, <<"text/plain">>, Ref) -> + emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/octet-stream">>, Ref) -> + emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) -> + emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) -> + emqx_lwm2m_message:translate_json(CoapPayload). + +write_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +write_resp_to_mqtt({ok, content}, CoapPayload, Ref) when CoapPayload =:= <<>> -> + make_response(method_not_allowed, Ref); + +write_resp_to_mqtt({ok, content}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +write_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +execute_resp_to_mqtt({ok, changed}, Ref) -> + make_response(changed, Ref); + +execute_resp_to_mqtt({error, Error}, Ref) -> + make_response(Error, Ref). + +discover_resp_to_mqtt({ok, content}, CoapPayload, Ref) -> + Links = binary:split(CoapPayload, <<",">>, [global]), + make_response(content, Ref, <<"application/link-format">>, Links); + +discover_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +writeattr_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +writeattr_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) -> + make_response(Error, Ref); + +observe_resp_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref); + +observe_resp_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref#{<<"seqNum">> => ObserveSeqNum}). + +cancel_observe_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref); + +cancel_observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) -> + make_response(Error, Ref). + +make_response(Code, Ref=#{}) -> + BaseRsp = make_base_response(Ref), + make_data_response(BaseRsp, Code). + +make_response(Code, Ref=#{}, _Format, Result) -> + BaseRsp = make_base_response(Ref), + make_data_response(BaseRsp, Code, _Format, Result). + +%% The base response format is what included in the request: +%% +%% #{ +%% <<"seqNum">> => SeqNum, +%% <<"imsi">> => maps:get(<<"imsi">>, Ref, null), +%% <<"imei">> => maps:get(<<"imei">>, Ref, null), +%% <<"requestID">> => maps:get(<<"requestID">>, Ref, null), +%% <<"cacheID">> => maps:get(<<"cacheID">>, Ref, null), +%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null) +%% } + +make_base_response(Ref=#{}) -> + remove_tmp_fields(Ref). + +make_data_response(BaseRsp, Code) -> + BaseRsp#{ + <<"data">> => #{ + <<"reqPath">> => extract_path(BaseRsp), + <<"code">> => code(Code), + <<"codeMsg">> => Code + } + }. + +make_data_response(BaseRsp, Code, _Format, Result) -> + BaseRsp#{ + <<"data">> => + #{ + <<"reqPath">> => extract_path(BaseRsp), + <<"code">> => code(Code), + <<"codeMsg">> => Code, + <<"content">> => Result + } + }. + +remove_tmp_fields(Ref) -> + maps:remove(observe_type, Ref). + +-spec path_list(Path::binary()) -> {[PathWord::binary()], [Query::binary()]}. +path_list(Path) -> + case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of + [ObjId, ObjInsId, ResId, LastPart] -> + {ResInstId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId, ResId, ResInstId], QueryList}; + [ObjId, ObjInsId, LastPart] -> + {ResId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId, ResId], QueryList}; + [ObjId, LastPart] -> + {ObjInsId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId], QueryList}; + [LastPart] -> + {ObjId, QueryList} = query_list(LastPart), + {[ObjId], QueryList} + end. + +query_list(PathWithQuery) -> + case binary:split(PathWithQuery, [<<$?>>], []) of + [Path] -> {Path, []}; + [Path, Querys] -> + {Path, binary:split(Querys, [<<$&>>], [global])} + end. + +attr_query_list(Data) -> + attr_query_list(Data, valid_attr_keys(), []). + +attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) -> + maps:fold( + fun + (_K, null, Acc) -> Acc; + (K, V, Acc) -> + case lists:member(K, ValidAttrKeys) of + true -> + KV = <>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>]. + +data_format(Options) -> + maps:get(content_format, Options, <<"text/plain">>). + +observe_seq(Options) -> + maps:get(observe, Options, rand:uniform(1000000) + 1 ). + +add_alternate_path_prefix(<<"/">>, PathList) -> + PathList; + +add_alternate_path_prefix(AlternatePath, PathList) -> + [binary_util:trim(AlternatePath, $/) | PathList]. + +extract_path(Ref = #{}) -> + drop_query( + case Ref of + #{<<"data">> := Data} -> + case maps:get(<<"path">>, Data, undefined) of + undefined -> maps:get(<<"basePath">>, Data, undefined); + Path -> Path + end; + #{<<"path">> := Path} -> + Path + end). + + +batch_write_request(AlternatePath, BasePath, Content) -> + {PathList, QueryList} = path_list(BasePath), + Method = case length(PathList) of + 2 -> post; + 3 -> put + end, + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content), + Payload = emqx_lwm2m_tlv:encode(TlvData), + emqx_coap_message:request(con, Method, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). + +single_write_request(AlternatePath, Data) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + %% TO DO: handle write to resource instance, e.g. /4/0/1/0 + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, [Data]), + Payload = emqx_lwm2m_tlv:encode(TlvData), + emqx_coap_message:request(con, put, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). + +drop_query(Path) -> + case binary:split(Path, [<<$?>>]) of + [Path] -> Path; + [PathOnly, _Query] -> PathOnly + end. + +code(get) -> <<"0.01">>; +code(post) -> <<"0.02">>; +code(put) -> <<"0.03">>; +code(delete) -> <<"0.04">>; +code(created) -> <<"2.01">>; +code(deleted) -> <<"2.02">>; +code(valid) -> <<"2.03">>; +code(changed) -> <<"2.04">>; +code(content) -> <<"2.05">>; +code(continue) -> <<"2.31">>; +code(bad_request) -> <<"4.00">>; +code(unauthorized) -> <<"4.01">>; +code(bad_option) -> <<"4.02">>; +code(forbidden) -> <<"4.03">>; +code(not_found) -> <<"4.04">>; +code(method_not_allowed) -> <<"4.05">>; +code(not_acceptable) -> <<"4.06">>; +code(request_entity_incomplete) -> <<"4.08">>; +code(precondition_failed) -> <<"4.12">>; +code(request_entity_too_large) -> <<"4.13">>; +code(unsupported_content_format) -> <<"4.15">>; +code(internal_server_error) -> <<"5.00">>; +code(not_implemented) -> <<"5.01">>; +code(bad_gateway) -> <<"5.02">>; +code(service_unavailable) -> <<"5.03">>; +code(gateway_timeout) -> <<"5.04">>; +code(proxying_not_supported) -> <<"5.05">>. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl deleted file mode 100644 index 0d1ea0568..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl +++ /dev/null @@ -1,386 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_coap_resource). - --include_lib("emqx/include/emqx.hrl"). - --include_lib("emqx/include/emqx_mqtt.hrl"). - --include_lib("lwm2m_coap/include/coap.hrl"). - -% -behaviour(lwm2m_coap_resource). - --export([ coap_discover/2 - , coap_get/5 - , coap_post/5 - , coap_put/5 - , coap_delete/4 - , coap_observe/5 - , coap_unobserve/1 - , coap_response/7 - , coap_ack/3 - , handle_info/2 - , handle_call/3 - , handle_cast/2 - , terminate/2 - ]). - --export([parse_object_list/1]). - --include("emqx_lwm2m.hrl"). - --define(PREFIX, <<"rd">>). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-RESOURCE: " ++ Format, Args)). - --dialyzer([{nowarn_function, [coap_discover/2]}]). -% we use {'absolute', list(binary()), [{atom(), binary()}]} as coap_uri() -% https://github.com/emqx/lwm2m-coap/blob/258e9bd3762124395e83c1e68a1583b84718230f/src/lwm2m_coap_resource.erl#L61 -% resource operations -coap_discover(_Prefix, _Args) -> - [{absolute, [<<"mqtt">>], []}]. - -coap_get(ChId, [?PREFIX], Query, Content, Lwm2mState) -> - ?LOG(debug, "~p ~p GET Query=~p, Content=~p", [self(),ChId, Query, Content]), - {ok, #coap_content{}, Lwm2mState}; -coap_get(ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "ignore bad put request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -% LWM2M REGISTER COMMAND -coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = [?PREFIX]}, Lwm2mState) -> - ?LOG(debug, "~p ~p REGISTER command Query=~p, Content=~p", [self(), ChId, Query, Content]), - case parse_options(Query) of - {error, {bad_opt, _CustomOption}} -> - ?LOG(error, "Reject REGISTER from ~p due to wrong option", [ChId]), - {error, bad_request, Lwm2mState}; - {ok, LwM2MQuery} -> - process_register(ChId, LwM2MQuery, Content#coap_content.payload, Lwm2mState) - end; - -% LWM2M UPDATE COMMAND -coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = LocationPath}, Lwm2mState) -> - ?LOG(debug, "~p ~p UPDATE command location=~p, Query=~p, Content=~p", [self(), ChId, LocationPath, Query, Content]), - case parse_options(Query) of - {error, {bad_opt, _CustomOption}} -> - ?LOG(error, "Reject UPDATE from ~p due to wrong option, Query=~p", [ChId, Query]), - {error, bad_request, Lwm2mState}; - {ok, LwM2MQuery} -> - process_update(ChId, LwM2MQuery, LocationPath, Content#coap_content.payload, Lwm2mState) - end; - -coap_post(ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "bad post request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -coap_put(_ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "put has error, Prefix=~p, Query=~p, Content=~p", [Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -% LWM2M DE-REGISTER COMMAND -coap_delete(ChId, [?PREFIX], #coap_content{uri_path = Location}, Lwm2mState) -> - LocationPath = binary_util:join_path(Location), - ?LOG(debug, "~p ~p DELETE command location=~p", [self(), ChId, LocationPath]), - case get(lwm2m_context) of - #lwm2m_context{location = LocationPath} -> - lwm2m_coap_responder:stop(deregister), - {ok, Lwm2mState}; - undefined -> - ?LOG(error, "Reject DELETE from ~p, Location: ~p not found", [ChId, Location]), - {error, forbidden, Lwm2mState}; - TrueLocation -> - ?LOG(error, "Reject DELETE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]), - {error, not_found, Lwm2mState} - end; -coap_delete(_ChId, _Prefix, _Content, Lwm2mState) -> - {error, forbidden, Lwm2mState}. - -coap_observe(ChId, Prefix, Name, Ack, Lwm2mState) -> - ?LOG(error, "unsupported observe request ChId=~p, Prefix=~p, Name=~p, Ack=~p", [ChId, Prefix, Name, Ack]), - {error, method_not_allowed, Lwm2mState}. - -coap_unobserve(Lwm2mState) -> - ?LOG(error, "unsupported unobserve request: ~p", [Lwm2mState]), - {ok, Lwm2mState}. - -coap_response(ChId, Ref, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Lwm2mState) -> - ?LOG(info, "~p, RCV CoAP response, CoapMsgType: ~p, CoapMsgMethod: ~p, CoapMsgPayload: ~p, - CoapMsgOpts: ~p, Ref: ~p", - [ChId, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref]), - MqttPayload = emqx_lwm2m_cmd_handler:coap2mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref), - Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState), - {noreply, Lwm2mState2}. - -coap_ack(_ChId, Ref, Lwm2mState) -> - ?LOG(info, "~p, RCV CoAP Empty ACK, Ref: ~p", [_ChId, Ref]), - AckRef = maps:put(<<"msgType">>, <<"ack">>, Ref), - MqttPayload = emqx_lwm2m_cmd_handler:ack2mqtt(AckRef), - Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState), - {ok, Lwm2mState2}. - -%% Batch deliver -handle_info({deliver, Topic, Msgs}, Lwm2mState) when is_list(Msgs) -> - {noreply, lists:foldl(fun(Msg, NewState) -> - element(2, handle_info({deliver, Topic, Msg}, NewState)) - end, Lwm2mState, Msgs)}; -%% Handle MQTT Message -handle_info({deliver, _Topic, MqttMsg}, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:deliver(MqttMsg, Lwm2mState), - {noreply, Lwm2mState2}; - -%% Deliver Coap Message to Device -handle_info({deliver_to_coap, CoapRequest, Ref}, Lwm2mState) -> - {send_request, CoapRequest, Ref, Lwm2mState}; - -handle_info({'EXIT', _Pid, Reason}, Lwm2mState) -> - ?LOG(info, "~p, received exit from: ~p, reason: ~p, quit now!", [self(), _Pid, Reason]), - {stop, Reason, Lwm2mState}; - -handle_info(post_init, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:post_init(Lwm2mState), - {noreply, Lwm2mState2}; - -handle_info(auto_observe, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:auto_observe(Lwm2mState), - {noreply, Lwm2mState2}; - -handle_info({life_timer, expired}, Lwm2mState) -> - ?LOG(debug, "lifetime expired, shutdown", []), - {stop, life_timer_expired, Lwm2mState}; - -handle_info({shutdown, Error}, Lwm2mState) -> - {stop, Error, Lwm2mState}; - -handle_info({shutdown, conflict, {ClientId, NewPid}}, Lwm2mState) -> - ?LOG(warning, "lwm2m '~s' conflict with ~p, shutdown", [ClientId, NewPid]), - {stop, conflict, Lwm2mState}; - -handle_info({suback, _MsgId, [_GrantedQos]}, Lwm2mState) -> - {noreply, Lwm2mState}; - -handle_info(emit_stats, Lwm2mState) -> - {noreply, Lwm2mState}; - -handle_info(Message, Lwm2mState) -> - ?LOG(error, "Unknown Message ~p", [Message]), - {noreply, Lwm2mState}. - - -handle_call(info, _From, Lwm2mState) -> - {Info, Lwm2mState2} = emqx_lwm2m_protocol:get_info(Lwm2mState), - {reply, Info, Lwm2mState2}; - -handle_call(stats, _From, Lwm2mState) -> - {Stats, Lwm2mState2} = emqx_lwm2m_protocol:get_stats(Lwm2mState), - {reply, Stats, Lwm2mState2}; - -handle_call(kick, _From, Lwm2mState) -> - {stop, kick, Lwm2mState}; - -handle_call({set_rate_limit, _Rl}, _From, Lwm2mState) -> - ?LOG(error, "set_rate_limit is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(get_rate_limit, _From, Lwm2mState) -> - ?LOG(error, "get_rate_limit is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(session, _From, Lwm2mState) -> - ?LOG(error, "get_session is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(Request, _From, Lwm2mState) -> - ?LOG(error, "adapter unexpected call ~p", [Request]), - {reply, ok, Lwm2mState}. - -handle_cast(Msg, Lwm2mState) -> - ?LOG(error, "unexpected cast ~p", [Msg]), - {noreply, Lwm2mState, hibernate}. - -terminate(Reason, Lwm2mState) -> - emqx_lwm2m_protocol:terminate(Reason, Lwm2mState). - -%%%%%%%%%%%%%%%%%%%%%% -%% Internal Functions -%%%%%%%%%%%%%%%%%%%%%% -process_register(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState) -> - Epn = maps:get(<<"ep">>, LwM2MQuery, undefined), - LifeTime = maps:get(<<"lt">>, LwM2MQuery, undefined), - Ver = maps:get(<<"lwm2m">>, LwM2MQuery, undefined), - case check_lwm2m_version(Ver) of - false -> - ?LOG(error, "Reject REGISTER from ~p due to unsupported version: ~p", [ChId, Ver]), - lwm2m_coap_responder:stop(invalid_version), - {error, precondition_failed, Lwm2mState}; - true -> - case check_epn(Epn) andalso check_lifetime(LifeTime) of - true -> - init_lwm2m_emq_client(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState); - false -> - ?LOG(error, "Reject REGISTER from ~p due to wrong parameters, epn=~p, lifetime=~p", [ChId, Epn, LifeTime]), - lwm2m_coap_responder:stop(invalid_query_params), - {error, bad_request, Lwm2mState} - end - end. - -process_update(ChId, LwM2MQuery, Location, LwM2MPayload, Lwm2mState) -> - LocationPath = binary_util:join_path(Location), - case get(lwm2m_context) of - #lwm2m_context{location = LocationPath} -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - Lwm2mState2 = emqx_lwm2m_protocol:update_reg_info(RegInfo, Lwm2mState), - ?LOG(info, "~p, UPDATE Success, assgined location: ~p", [ChId, LocationPath]), - {ok, changed, #coap_content{}, Lwm2mState2}; - undefined -> - ?LOG(error, "Reject UPDATE from ~p, Location: ~p not found", [ChId, Location]), - {error, forbidden, Lwm2mState}; - TrueLocation -> - ?LOG(error, "Reject UPDATE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]), - {error, not_found, Lwm2mState} - end. - -init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, _Lwm2mState = undefined) -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - case emqx_lwm2m_protocol:init(self(), Epn, ChId, RegInfo) of - {ok, Lwm2mState} -> - LocationPath = assign_location_path(Epn), - ?LOG(info, "~p, REGISTER Success, assgined location: ~p", [ChId, LocationPath]), - {ok, created, #coap_content{location_path = LocationPath}, Lwm2mState}; - {error, Error} -> - lwm2m_coap_responder:stop(Error), - ?LOG(error, "~p, REGISTER Failed, error: ~p", [ChId, Error]), - {error, forbidden, undefined} - end; -init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, Lwm2mState) -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - LocationPath = assign_location_path(Epn), - ?LOG(info, "~p, RE-REGISTER Success, location: ~p", [ChId, LocationPath]), - Lwm2mState2 = emqx_lwm2m_protocol:replace_reg_info(RegInfo, Lwm2mState), - {ok, created, #coap_content{location_path = LocationPath}, Lwm2mState2}. - -append_object_list(LwM2MQuery, <<>>) when map_size(LwM2MQuery) == 0 -> #{}; -append_object_list(LwM2MQuery, <<>>) -> LwM2MQuery; -append_object_list(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) -> - {AlterPath, ObjList} = parse_object_list(LwM2MPayload), - LwM2MQuery#{ - <<"alternatePath">> => AlterPath, - <<"objectList">> => ObjList - }. - -parse_options(InputQuery) -> - parse_options(InputQuery, maps:new()). - -parse_options([], Query) -> {ok, Query}; -parse_options([<<"ep=", Epn/binary>>|T], Query) -> - parse_options(T, maps:put(<<"ep">>, Epn, Query)); -parse_options([<<"lt=", Lt/binary>>|T], Query) -> - parse_options(T, maps:put(<<"lt">>, binary_to_integer(Lt), Query)); -parse_options([<<"lwm2m=", Ver/binary>>|T], Query) -> - parse_options(T, maps:put(<<"lwm2m">>, Ver, Query)); -parse_options([<<"b=", Binding/binary>>|T], Query) -> - parse_options(T, maps:put(<<"b">>, Binding, Query)); -parse_options([CustomOption|T], Query) -> - case binary:split(CustomOption, <<"=">>) of - [OptKey, OptValue] when OptKey =/= <<>> -> - ?LOG(debug, "non-standard option: ~p", [CustomOption]), - parse_options(T, maps:put(OptKey, OptValue, Query)); - _BadOpt -> - ?LOG(error, "bad option: ~p", [CustomOption]), - {error, {bad_opt, CustomOption}} - end. - -parse_object_list(<<>>) -> {<<"/">>, <<>>}; -parse_object_list(ObjLinks) when is_binary(ObjLinks) -> - parse_object_list(binary:split(ObjLinks, <<",">>, [global])); - -parse_object_list(FullObjLinkList) when is_list(FullObjLinkList) -> - case drop_attr(FullObjLinkList) of - {<<"/">>, _} = RootPrefixedLinks -> - RootPrefixedLinks; - {AlterPath, ObjLinkList} -> - LenAlterPath = byte_size(AlterPath), - WithOutPrefix = - lists:map( - fun - (<>) when Prefix =:= AlterPath -> - trim(Link); - (Link) -> Link - end, ObjLinkList), - {AlterPath, WithOutPrefix} - end. - -drop_attr(LinkList) -> - lists:foldr( - fun(Link, {AlternatePath, LinkAcc}) -> - {MainLink, LinkAttrs} = parse_link(Link), - case is_alternate_path(LinkAttrs) of - false -> {AlternatePath, [MainLink | LinkAcc]}; - true -> {MainLink, LinkAcc} - end - end, {<<"/">>, []}, LinkList). - -is_alternate_path(#{<<"rt">> := ?OMA_ALTER_PATH_RT}) -> true; -is_alternate_path(_) -> false. - -parse_link(Link) -> - [MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]), - {delink(trim(MainLink)), parse_link_attrs(Attrs)}. - -parse_link_attrs(LinkAttrs) when is_list(LinkAttrs) -> - lists:foldl( - fun(Attr, Acc) -> - case binary:split(trim(Attr), <<"=">>) of - [AttrKey, AttrValue] when AttrKey =/= <<>> -> - maps:put(AttrKey, AttrValue, Acc); - _BadAttr -> throw({bad_attr, _BadAttr}) - end - end, maps:new(), LinkAttrs). - -trim(Str)-> binary_util:trim(Str, $ ). -delink(Str) -> - Ltrim = binary_util:ltrim(Str, $<), - binary_util:rtrim(Ltrim, $>). - -check_lwm2m_version(<<"1">>) -> true; -check_lwm2m_version(<<"1.", _PatchVerNum/binary>>) -> true; -check_lwm2m_version(_) -> false. - -check_epn(undefined) -> false; -check_epn(_) -> true. - -check_lifetime(undefined) -> false; -check_lifetime(LifeTime0) when is_integer(LifeTime0) -> - LifeTime = timer:seconds(LifeTime0), - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Max = maps:get(lifetime_max, Envs, 315360000), - Min = maps:get(lifetime_min, Envs, 0), - - if - LifeTime >= Min, LifeTime =< Max -> - true; - true -> - false - end; -check_lifetime(_) -> false. - - -assign_location_path(Epn) -> - %Location = list_to_binary(io_lib:format("~.16B", [rand:uniform(65535)])), - %LocationPath = <<"/rd/", Location/binary>>, - Location = [<<"rd">>, Epn], - put(lwm2m_context, #lwm2m_context{epn = Epn, location = binary_util:join_path(Location)}), - Location. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index d94c9fa8b..649a14643 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -20,124 +20,94 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([]). - --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). +-include_lib("emqx/include/logger.hrl"). + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -load() -> +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - emqx_gateway_registry:load(lwm2m, RegistryOptions, []). + emqx_gateway_registry:reg(lwm2m, RegistryOptions). -unload() -> - %% XXX: - lwm2m_coap_server_registry:remove_handler( - [<<"rd">>], - emqx_lwm2m_coap_resource, undefined - ), - emqx_gateway_registry:unload(lwm2m). - -init(_) -> - %% Handler - _ = lwm2m_coap_server:start_registry(), - lwm2m_coap_server_registry:add_handler( - [<<"rd">>], - emqx_lwm2m_coap_resource, undefined - ), - %% Xml registry - {ok, _} = emqx_lwm2m_xml_object_db:start_link( - emqx_config:get([gateway, lwm2m_xml_dir]) - ), - - %% XXX: Self managed table? - %% TODO: Improve it later - {ok, _} = emqx_lwm2m_cm:start_link(), - - GwState = #{}, - {ok, GwState}. - -%% TODO: deinit +unreg() -> + emqx_gateway_registry:unreg(lwm2m). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_insta_create(_Insta = #{ id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), - ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, Lis) - end, Listeners), - {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. +on_gateway_load(_Gateway = #{ name := GwName, + config := Config + }, Ctx) -> + %% Xml registry + {ok, _} = emqx_lwm2m_xml_object_db:start_link(maps:get(xml_dir, Config)), -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), + Listeners = emqx_gateway_utils:normalize_config(Config), + ListenerPids = lists:map(fun(Lis) -> + start_listener(GwName, Ctx, Lis) + end, Listeners), + {ok, ListenerPids, _GwState = #{ctx => Ctx}}. + +on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, NewGateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? - on_insta_destroy(OldInsta, GwInstaState, GwState), - on_insta_create(NewInsta, Ctx, GwState) + on_gateway_unload(OldGateway, GwState), + on_gateway_load(NewGateway, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update stomp instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, _GwInstaState, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), +on_gateway_unload(_Gateway = #{ name := GwName, + config := Config + }, _GwState) -> + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) + stop_listener(GwName, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - io:format("Start lwm2m ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - io:format(standard_error, - "Failed to start lwm2m ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, udp), - NCfg = Cfg#{ctx => Ctx}, +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), + NCfg = Cfg#{ ctx => Ctx + , frame_mod => emqx_coap_frame + , chann_mod => emqx_lwm2m_channel + }, NSocketOpts = merge_default(SocketOpts), - Options = [{config, NCfg}|NSocketOpts], - case Type of - udp -> - lwm2m_coap_server:start_udp(Name, ListenOn, Options); - dtls -> - lwm2m_coap_server:start_dtls(Name, ListenOn, Options) - end. - -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). + MFA = {emqx_gateway_conn, start_link, [NCfg]}, + do_start_listener(Type, Name, ListenOn, NSocketOpts, MFA). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -149,25 +119,24 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> + esockd:open_udp(Name, ListenOn, SocketOpts, MFA); + +do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> + esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). + +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> io:format("Stop lwm2m ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - io:format(standard_error, - "Failed to stop lwm2m ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason] - ) + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), - case Type of - udp -> - lwm2m_coap_server:stop_udp(Name, ListenOn); - dtls -> - lwm2m_coap_server:stop_dtls(Name, ListenOn) - end. +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), + esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl new file mode 100644 index 000000000..9c915ce46 --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl @@ -0,0 +1,734 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_lwm2m_session). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include("emqx_coap.hrl"). +-include("emqx_lwm2m.hrl"). + +%% API +-export([ new/0, init/4, update/3, parse_object_list/1 + , reregister/3, on_close/1, find_cmd_record/3]). + +-export([ info/1 + , info/2 + , stats/1 + ]). + +-export([ handle_coap_in/3 + , handle_protocol_in/3 + , handle_deliver/3 + , timeout/3 + , set_reply/2]). + +-export_type([session/0]). + +-type request_context() :: map(). + +-type timestamp() :: non_neg_integer(). +-type queued_request() :: {timestamp(), request_context(), emqx_coap_message()}. + +-type cmd_path() :: binary(). +-type cmd_type() :: binary(). +-type cmd_record_key() :: {cmd_path(), cmd_type()}. +-type cmd_code() :: binary(). +-type cmd_code_msg() :: binary(). +-type cmd_code_content() :: list(map()). +-type cmd_result() :: undefined | {cmd_code(), cmd_code_msg(), cmd_code_content()}. +-type cmd_record() :: #{cmd_record_key() => cmd_result()}. + +-record(session, { coap :: emqx_coap_tm:manager() + , queue :: queue:queue(queued_request()) + , wait_ack :: request_context() | undefined + , endpoint_name :: binary() | undefined + , location_path :: list(binary()) | undefined + , reg_info :: map() | undefined + , lifetime :: non_neg_integer() | undefined + , is_cache_mode :: boolean() + , mountpoint :: binary() + , last_active_at :: non_neg_integer() + , cmd_record :: cmd_record() + }). + +-type session() :: #session{}. + +-define(PREFIX, <<"rd">>). +-define(NOW, erlang:system_time(second)). +-define(IGNORE_OBJECT, [<<"0">>, <<"1">>, <<"2">>, <<"4">>, <<"5">>, <<"6">>, + <<"7">>, <<"9">>, <<"15">>]). + +-define(CMD_KEY(Path, Type), {Path, Type}). + +%% uplink and downlink topic configuration +-define(lwm2m_up_dm_topic, {<<"/v1/up/dm">>, 0}). + +%% steal from emqx_session +-define(INFO_KEYS, [subscriptions, + upgrade_qos, + retry_interval, + await_rel_timeout, + created_at + ]). + +-define(STATS_KEYS, [subscriptions_cnt, + subscriptions_max, + inflight_cnt, + inflight_max, + mqueue_len, + mqueue_max, + mqueue_dropped, + next_pkt_id, + awaiting_rel_cnt, + awaiting_rel_max + ]). + +-define(OUT_LIST_KEY, out_list). + +-import(emqx_coap_medium, [iter/3, reply/2]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +-spec new () -> session(). +new() -> + #session{ coap = emqx_coap_tm:new() + , queue = queue:new() + , last_active_at = ?NOW + , is_cache_mode = false + , mountpoint = <<>> + , cmd_record = #{} + , lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])}. + +-spec init(emqx_coap_message(), binary(), function(), session()) -> map(). +init(#coap_message{options = Opts, + payload = Payload} = Msg, MountPoint, WithContext, Session) -> + Query = maps:get(uri_query, Opts), + RegInfo = append_object_list(Query, Payload), + LifeTime = get_lifetime(RegInfo), + Epn = maps:get(<<"ep">>, Query), + Location = [?PREFIX, Epn], + + NewSession = Session#session{endpoint_name = Epn, + location_path = Location, + reg_info = RegInfo, + lifetime = LifeTime, + mountpoint = MountPoint, + is_cache_mode = is_psm(RegInfo) orelse is_qmode(RegInfo), + queue = queue:new()}, + + Result = return(register_init(WithContext, NewSession)), + Reply = emqx_coap_message:piggyback({ok, created}, Msg), + Reply2 = emqx_coap_message:set(location_path, Location, Reply), + reply(Reply2, Result#{lifetime => true}). + +reregister(Msg, WithContext, Session) -> + update(Msg, WithContext, <<"register">>, Session). + +update(Msg, WithContext, Session) -> + update(Msg, WithContext, <<"update">>, Session). + +-spec on_close(session()) -> binary(). +on_close(Session) -> + #{topic := Topic} = downlink_topic(), + MountedTopic = mount(Topic, Session), + emqx:unsubscribe(MountedTopic), + MountedTopic. + +-spec find_cmd_record(cmd_path(), cmd_type(), session()) -> cmd_result(). +find_cmd_record(Path, Type, #session{cmd_record = Record}) -> + maps:get(?CMD_KEY(Path, Type), Record, undefined). + +%%-------------------------------------------------------------------- +%% Info, Stats +%%-------------------------------------------------------------------- +-spec(info(session()) -> emqx_types:infos()). +info(Session) -> + maps:from_list(info(?INFO_KEYS, Session)). + +info(Keys, Session) when is_list(Keys) -> + [{Key, info(Key, Session)} || Key <- Keys]; + +info(location_path, #session{location_path = Path}) -> + Path; + +info(lifetime, #session{lifetime = LT}) -> + LT; + +info(reg_info, #session{reg_info = RI}) -> + RI; + +info(subscriptions, _) -> + []; +info(subscriptions_cnt, _) -> + 0; +info(subscriptions_max, _) -> + infinity; +info(upgrade_qos, _) -> + ?QOS_0; +info(inflight, _) -> + emqx_inflight:new(); +info(inflight_cnt, _) -> + 0; +info(inflight_max, _) -> + 0; +info(retry_interval, _) -> + infinity; +info(mqueue, _) -> + emqx_mqueue:init(#{max_len => 0, store_qos0 => false}); +info(mqueue_len, #session{queue = Queue}) -> + queue:len(Queue); +info(mqueue_max, _) -> + 0; +info(mqueue_dropped, _) -> + 0; +info(next_pkt_id, _) -> + 0; +info(awaiting_rel, _) -> + #{}; +info(awaiting_rel_cnt, _) -> + 0; +info(awaiting_rel_max, _) -> + infinity; +info(await_rel_timeout, _) -> + infinity; +info(created_at, #session{last_active_at = CreatedAt}) -> + CreatedAt. + +%% @doc Get stats of the session. +-spec(stats(session()) -> emqx_types:stats()). +stats(Session) -> info(?STATS_KEYS, Session). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +handle_coap_in(Msg, _WithContext, Session) -> + call_coap(case emqx_coap_message:is_request(Msg) of + true -> handle_request; + _ -> handle_response + end, + Msg, Session#session{last_active_at = ?NOW}). + +handle_deliver(Delivers, WithContext, Session) -> + return(deliver(Delivers, WithContext, Session)). + +timeout({transport, Msg}, _, Session) -> + call_coap(timeout, Msg, Session). + +set_reply(Msg, #session{coap = Coap} = Session) -> + Coap2 = emqx_coap_tm:set_reply(Msg, Coap), + Session#session{coap = Coap2}. + +%%-------------------------------------------------------------------- +%% Protocol Stack +%%-------------------------------------------------------------------- +handle_protocol_in({response, CtxMsg}, WithContext, Session) -> + return(handle_coap_response(CtxMsg, WithContext, Session)); + +handle_protocol_in({ack, CtxMsg}, WithContext, Session) -> + return(handle_ack(CtxMsg, WithContext, Session)); + +handle_protocol_in({ack_failure, CtxMsg}, WithContext, Session) -> + return(handle_ack_failure(CtxMsg, WithContext, Session)); + +handle_protocol_in({reset, CtxMsg}, WithContext, Session) -> + return(handle_ack_reset(CtxMsg, WithContext, Session)). + +%%-------------------------------------------------------------------- +%% Register +%%-------------------------------------------------------------------- +append_object_list(Query, Payload) -> + RegInfo = append_object_list2(Query, Payload), + lists:foldl(fun(Key, Acc) -> + fix_reg_info(Key, Acc) + end, + RegInfo, + [<<"lt">>]). + +append_object_list2(LwM2MQuery, <<>>) -> LwM2MQuery; +append_object_list2(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) -> + {AlterPath, ObjList} = parse_object_list(LwM2MPayload), + LwM2MQuery#{ + <<"alternatePath">> => AlterPath, + <<"objectList">> => ObjList + }. + +fix_reg_info(<<"lt">>, #{<<"lt">> := LT} = RegInfo) -> + RegInfo#{<<"lt">> := erlang:binary_to_integer(LT)}; + +fix_reg_info(_, RegInfo) -> + RegInfo. + +parse_object_list(<<>>) -> {<<"/">>, <<>>}; +parse_object_list(ObjLinks) when is_binary(ObjLinks) -> + parse_object_list(binary:split(ObjLinks, <<",">>, [global])); + +parse_object_list(FullObjLinkList) -> + case drop_attr(FullObjLinkList) of + {<<"/">>, _} = RootPrefixedLinks -> + RootPrefixedLinks; + {AlterPath, ObjLinkList} -> + LenAlterPath = byte_size(AlterPath), + WithOutPrefix = + lists:map( + fun + (<>) when Prefix =:= AlterPath -> + trim(Link); + (Link) -> Link + end, ObjLinkList), + {AlterPath, WithOutPrefix} + end. + +drop_attr(LinkList) -> + lists:foldr( + fun(Link, {AlternatePath, LinkAcc}) -> + case parse_link(Link) of + {false, MainLink} -> {AlternatePath, [MainLink | LinkAcc]}; + {true, MainLink} -> {MainLink, LinkAcc} + end + end, {<<"/">>, []}, LinkList). + +parse_link(Link) -> + [MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]), + {is_alternate_path(Attrs), delink(trim(MainLink))}. + +is_alternate_path(LinkAttrs) -> + lists:any(fun(Attr) -> + case binary:split(trim(Attr), <<"=">>) of + [<<"rt">>, ?OMA_ALTER_PATH_RT] -> + true; + [AttrKey, _] when AttrKey =/= <<>> -> + false; + _BadAttr -> throw({bad_attr, _BadAttr}) + end + end, + LinkAttrs). + +trim(Str)-> binary_util:trim(Str, $ ). + +delink(Str) -> + Ltrim = binary_util:ltrim(Str, $<), + binary_util:rtrim(Ltrim, $>). + +get_lifetime(#{<<"lt">> := LT}) -> + case LT of + 0 -> emqx:get_config([gateway, lwm2m, lifetime_max]); + _ -> LT * 1000 + end; +get_lifetime(_) -> + emqx:get_config([gateway, lwm2m, lifetime_max]). + +get_lifetime(#{<<"lt">> := _} = NewRegInfo, _) -> + get_lifetime(NewRegInfo); + +get_lifetime(_, OldRegInfo) -> + get_lifetime(OldRegInfo). + +-spec update(emqx_coap_message(), function(), binary(), session()) -> map(). +update(#coap_message{options = Opts, payload = Payload} = Msg, + WithContext, + CmdType, + #session{reg_info = OldRegInfo} = Session) -> + Query = maps:get(uri_query, Opts), + RegInfo = append_object_list(Query, Payload), + UpdateRegInfo = maps:merge(OldRegInfo, RegInfo), + LifeTime = get_lifetime(UpdateRegInfo, OldRegInfo), + + NewSession = Session#session{reg_info = UpdateRegInfo, + is_cache_mode = + is_psm(UpdateRegInfo) orelse is_qmode(UpdateRegInfo), + lifetime = LifeTime}, + + Session2 = proto_subscribe(WithContext, NewSession), + Session3 = send_dl_msg(Session2), + RegPayload = #{<<"data">> => UpdateRegInfo}, + Session4 = send_to_mqtt(#{}, CmdType, RegPayload, WithContext, Session3), + + Result = return(Session4), + + Reply = emqx_coap_message:piggyback({ok, changed}, Msg), + reply(Reply, Result#{lifetime => true}). + +register_init(WithContext, #session{reg_info = RegInfo} = Session) -> + Session2 = send_auto_observe(RegInfo, Session), + %% - subscribe to the downlink_topic and wait for commands + #{topic := Topic, qos := Qos} = downlink_topic(), + MountedTopic = mount(Topic, Session), + Session3 = subscribe(MountedTopic, Qos, WithContext, Session2), + Session4 = send_dl_msg(Session3), + + %% - report the registration info + RegPayload = #{<<"data">> => RegInfo}, + send_to_mqtt(#{}, <<"register">>, RegPayload, WithContext, Session4). + +%%-------------------------------------------------------------------- +%% Subscribe +%%-------------------------------------------------------------------- +proto_subscribe(WithContext, #session{wait_ack = WaitAck} = Session) -> + #{topic := Topic, qos := Qos} = downlink_topic(), + MountedTopic = mount(Topic, Session), + Session2 = case WaitAck of + undefined -> + Session; + Ctx -> + MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, <<"coap_timeout">>), + send_to_mqtt(Ctx, <<"coap_timeout">>, MqttPayload, WithContext, Session) + end, + subscribe(MountedTopic, Qos, WithContext, Session2). + +subscribe(Topic, Qos, WithContext, Session) -> + Opts = get_sub_opts(Qos), + WithContext(subscribe, [Topic, Opts]), + Session. + +send_auto_observe(RegInfo, Session) -> + %% - auto observe the objects + case is_auto_observe() of + true -> + AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), + ObjectList = maps:get(<<"objectList">>, RegInfo, []), + observe_object_list(AlternatePath, ObjectList, Session); + _ -> + ?LOG(info, "Auto Observe Disabled", []), + Session + end. + +observe_object_list(_, [], Session) -> + Session; +observe_object_list(AlternatePath, ObjectList, Session) -> + Fun = fun(ObjectPath, Acc) -> + {[ObjId| _], _} = emqx_lwm2m_cmd:path_list(ObjectPath), + case lists:member(ObjId, ?IGNORE_OBJECT) of + true -> Acc; + false -> + try + emqx_lwm2m_xml_object_db:find_objectid(binary_to_integer(ObjId)), + observe_object(AlternatePath, ObjectPath, Acc) + catch error:no_xml_definition -> + Acc + end + end + end, + lists:foldl(Fun, Session, ObjectList). + +observe_object(AlternatePath, ObjectPath, Session) -> + Payload = #{<<"msgType">> => <<"observe">>, + <<"data">> => #{<<"path">> => ObjectPath}, + <<"is_auto_observe">> => true + }, + deliver_auto_observe_to_coap(AlternatePath, Payload, Session). + +deliver_auto_observe_to_coap(AlternatePath, TermData, Session) -> + ?LOG(info, "Auto Observe, SEND To CoAP, AlternatePath=~0p, Data=~0p ", [AlternatePath, TermData]), + {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), + maybe_do_deliver_to_coap(Ctx, Req, 0, false, Session). + +get_sub_opts(Qos) -> + #{ + qos => Qos, + rap => 0, + nl => 0, + rh => 0, + is_new => false + }. + +is_auto_observe() -> + emqx:get_config([gateway, lwm2m, auto_observe]). + +%%-------------------------------------------------------------------- +%% Response +%%-------------------------------------------------------------------- +handle_coap_response({Ctx = #{<<"msgType">> := EventType}, + #coap_message{method = CoapMsgMethod, + type = CoapMsgType, + payload = CoapMsgPayload, + options = CoapMsgOpts}}, + WithContext, + Session) -> + MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx), + {ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)), + Session2 = record_response(EventType, MqttPayload, Session), + Session3 = + case {ReqPath, MqttPayload, EventType, CoapMsgType} of + {[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> + %% this is a notification for status update during NB firmware upgrade. + %% need to reply to DM http callbacks + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session2); + {_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> + %% this is actually a notification, correct the msgType + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, WithContext, Session2); + _ -> + send_to_mqtt(Ctx, EventType, MqttPayload, WithContext, Session2) + end, + send_dl_msg(Ctx, Session3). + +%%-------------------------------------------------------------------- +%% Ack +%%-------------------------------------------------------------------- +handle_ack({Ctx, _}, WithContext, Session) -> + Session2 = send_dl_msg(Ctx, Session), + MqttPayload = emqx_lwm2m_cmd:empty_ack_to_mqtt(Ctx), + send_to_mqtt(Ctx, <<"ack">>, MqttPayload, WithContext, Session2). + +%%-------------------------------------------------------------------- +%% Ack Failure(Timeout/Reset) +%%-------------------------------------------------------------------- +handle_ack_failure({Ctx, _}, WithContext, Session) -> + handle_ack_failure(Ctx, <<"coap_timeout">>, WithContext, Session). + +handle_ack_reset({Ctx, _}, WithContext, Session) -> + handle_ack_failure(Ctx, <<"coap_reset">>, WithContext, Session). + +handle_ack_failure(Ctx, MsgType, WithContext, Session) -> + Session2 = may_send_dl_msg(coap_timeout, Ctx, Session), + MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, MsgType), + send_to_mqtt(Ctx, MsgType, MqttPayload, WithContext, Session2). + +%%-------------------------------------------------------------------- +%% Send To CoAP +%%-------------------------------------------------------------------- + +may_send_dl_msg(coap_timeout, Ctx, #session{wait_ack = WaitAck} = Session) -> + case is_cache_mode(Session) of + false -> send_dl_msg(Ctx, Session); + true -> + case WaitAck of + Ctx -> + Session#session{wait_ack = undefined}; + _ -> + Session + end + end. + +is_cache_mode(#session{is_cache_mode = IsCacheMode, + last_active_at = LastActiveAt}) -> + IsCacheMode andalso + ((?NOW - LastActiveAt) >= + emqx:get_config([gateway, lwm2m, qmode_time_window])). + +is_psm(#{<<"apn">> := APN}) when APN =:= <<"Ctnb">>; + APN =:= <<"psmA.eDRX0.ctnb">>; + APN =:= <<"psmC.eDRX0.ctnb">>; + APN =:= <<"psmF.eDRXC.ctnb">> + -> true; +is_psm(_) -> false. + +is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>; + Binding =:= <<"SQ">>; + Binding =:= <<"UQS">> + -> true; +is_qmode(_) -> false. + +send_dl_msg(Session) -> + %% if has in waiting donot send + case Session#session.wait_ack of + undefined -> + send_to_coap(Session); + _ -> + Session + end. + +send_dl_msg(Ctx, Session) -> + case Session#session.wait_ack of + undefined -> + send_to_coap(Session); + Ctx -> + send_to_coap(Session#session{wait_ack = undefined}); + _ -> + Session + end. + +send_to_coap(#session{queue = Queue} = Session) -> + case queue:out(Queue) of + {{value, {Timestamp, Ctx, Req}}, Q2} -> + Now = ?NOW, + if Timestamp =:= 0 orelse Timestamp > Now -> + send_to_coap(Ctx, Req, Session#session{queue = Q2}); + true -> + send_to_coap(Session#session{queue = Q2}) + end; + {empty, _} -> + Session + end. + +send_to_coap(Ctx, Req, Session) -> + ?LOG(debug, "Deliver To CoAP, CoapRequest: ~0p", [Req]), + out_to_coap(Ctx, Req, Session#session{wait_ack = Ctx}). + +send_msg_not_waiting_ack(Ctx, Req, Session) -> + ?LOG(debug, "Deliver To CoAP not waiting ack, CoapRequest: ~0p", [Req]), + %% cmd_sent(Ref, LwM2MOpts). + out_to_coap(Ctx, Req, Session). + +%%-------------------------------------------------------------------- +%% Send To MQTT +%%-------------------------------------------------------------------- +send_to_mqtt(Ref, EventType, Payload, WithContext, Session) -> + #{topic := Topic, qos := Qos} = uplink_topic(EventType), + Mheaders = maps:get(mheaders, Ref, #{}), + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, Mheaders, WithContext, Session). + +send_to_mqtt(Ctx, EventType, Payload, {Topic, Qos}, + WithContext, Session) -> + Mheaders = maps:get(mheaders, Ctx, #{}), + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, Mheaders, WithContext, Session). + +proto_publish(Topic, Payload, Qos, Headers, WithContext, + #session{endpoint_name = Epn} = Session) -> + MountedTopic = mount(Topic, Session), + Msg = emqx_message:make(Epn, Qos, MountedTopic, + emqx_json:encode(Payload), #{}, Headers), + WithContext(publish, [MountedTopic, Msg]), + Session. + +mount(Topic, #session{mountpoint = MountPoint}) when is_binary(Topic) -> + <>. + +downlink_topic() -> + emqx:get_config([gateway, lwm2m, translators, command]). + +uplink_topic(<<"notify">>) -> + emqx:get_config([gateway, lwm2m, translators, notify]); + +uplink_topic(<<"register">>) -> + emqx:get_config([gateway, lwm2m, translators, register]); + +uplink_topic(<<"update">>) -> + emqx:get_config([gateway, lwm2m, translators, update]); + +uplink_topic(_) -> + emqx:get_config([gateway, lwm2m, translators, response]). + +%%-------------------------------------------------------------------- +%% Deliver +%%-------------------------------------------------------------------- + +deliver(Delivers, WithContext, #session{reg_info = RegInfo} = Session) -> + IsCacheMode = is_cache_mode(Session), + AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), + lists:foldl(fun({deliver, _, MQTT}, Acc) -> + deliver_to_coap(AlternatePath, + MQTT#message.payload, MQTT, IsCacheMode, WithContext, Acc) + end, + Session, + Delivers). + +deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, WithContext, Session) when is_binary(JsonData)-> + try + TermData = emqx_json:decode(JsonData, [return_maps]), + deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) + catch + ExClass:Error:ST -> + ?LOG(error, "deliver_to_coap - Invalid JSON: ~0p, Exception: ~0p, stacktrace: ~0p", + [JsonData, {ExClass, Error}, ST]), + WithContext(metrics, 'delivery.dropped'), + Session + end; + +deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) when is_map(TermData) -> + WithContext(metrics, 'messages.delivered'), + {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), + ExpiryTime = get_expiry_time(MQTT), + Session2 = record_request(Ctx, Session), + maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session2). + +maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, + #session{wait_ack = WaitAck, + queue = Queue} = Session) -> + MHeaders = maps:get(mheaders, Ctx, #{}), + TTL = maps:get(<<"ttl">>, MHeaders, 7200), + case TTL of + 0 -> + send_msg_not_waiting_ack(Ctx, Req, Session); + _ -> + case not CacheMode + andalso queue:is_empty(Queue) andalso WaitAck =:= undefined of + true -> + send_to_coap(Ctx, Req, Session); + false -> + Session#session{queue = queue:in({ExpiryTime, Ctx, Req}, Queue)} + end + end. + +get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}}, + timestamp = Ts}) -> + Ts + Interval * 1000; +get_expiry_time(_) -> + 0. + +%%-------------------------------------------------------------------- +%% Call CoAP +%%-------------------------------------------------------------------- +call_coap(Fun, Msg, #session{coap = Coap} = Session) -> + iter([tm, fun process_tm/4, fun process_session/3], + emqx_coap_tm:Fun(Msg, Coap), + Session). + +process_tm(TM, Result, Session, Cursor) -> + iter(Cursor, Result, Session#session{coap = TM}). + +process_session(_, Result, Session) -> + Result#{session => Session}. + +out_to_coap(Context, Msg, Session) -> + out_to_coap({Context, Msg}, Session). + +out_to_coap(Msg, Session) -> + Outs = get_outs(), + erlang:put(?OUT_LIST_KEY, [Msg | Outs]), + Session. + +get_outs() -> + case erlang:get(?OUT_LIST_KEY) of + undefined -> []; + Any -> Any + end. + +return(#session{coap = CoAP} = Session) -> + Outs = get_outs(), + erlang:put(?OUT_LIST_KEY, []), + {ok, Coap2, Msgs} = do_out(Outs, CoAP, []), + #{return => {Msgs, Session#session{coap = Coap2}}}. + +do_out([{Ctx, Out} | T], TM, Msgs) -> + %% TODO maybe set a special token? + #{out := [Msg], + tm := TM2} = emqx_coap_tm:handle_out(Out, Ctx, TM), + do_out(T, TM2, [Msg | Msgs]); + +do_out(_, TM, Msgs) -> + {ok, TM, Msgs}. + + +%%-------------------------------------------------------------------- +%% CMD Record +%%-------------------------------------------------------------------- +-spec record_request(request_context(), session()) -> session(). +record_request(#{<<"msgType">> := Type} = Context, Session) -> + Path = emqx_lwm2m_cmd:extract_path(Context), + record_cmd(Path, Type, undefined, Session). + +record_response(EventType, #{<<"data">> := Data}, Session) -> + ReqPath = maps:get(<<"reqPath">>, Data, undefined), + Code = maps:get(<<"code">>, Data, undefined), + CodeMsg = maps:get(<<"codeMsg">>, Data, undefined), + Content = maps:get(<<"content">>, Data, undefined), + record_cmd(ReqPath, EventType, {Code, CodeMsg, Content}, Session). + +record_cmd(Path, Type, Result, #session{cmd_record = Record} = Session) -> + Record2 = Record#{?CMD_KEY(Path, Type) => Result}, + Session#session{cmd_record = Record2}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl index dd9911407..6dbbf8bfa 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl @@ -16,8 +16,8 @@ -module(emqx_lwm2m_xml_object). --include("emqx_lwm2m.hrl"). -include_lib("xmerl/include/xmerl.hrl"). +-include("emqx_lwm2m.hrl"). -export([ get_obj_def/2 , get_object_id/1 @@ -38,8 +38,6 @@ get_obj_def(ObjectIdInt, true) -> get_obj_def(ObjectNameStr, false) -> emqx_lwm2m_xml_object_db:find_name(ObjectNameStr). - - get_object_id(ObjDefinition) -> [#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition), ObjectId. @@ -48,7 +46,6 @@ get_object_name(ObjDefinition) -> [#xmlText{value=ObjectName}] = xmerl_xpath:string("Name/text()", ObjDefinition), ObjectName. - get_object_and_resource_id(ResourceNameBinary, ObjDefinition) -> ResourceNameString = binary_to_list(ResourceNameBinary), [#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition), @@ -56,7 +53,6 @@ get_object_and_resource_id(ResourceNameBinary, ObjDefinition) -> ?LOG(debug, "get_object_and_resource_id ObjectId=~p, ResourceId=~p", [ObjectId, ResourceId]), {ObjectId, ResourceId}. - get_resource_type(ResourceIdInt, ObjDefinition) -> ResourceIdString = integer_to_list(ResourceIdInt), [#xmlText{value=DataType}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Type/text()", ObjDefinition), diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl index 6412d68c8..be08b22a2 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl @@ -16,8 +16,8 @@ -module(emqx_lwm2m_xml_object_db). --include("emqx_lwm2m.hrl"). -include_lib("xmerl/include/xmerl.hrl"). +-include("emqx_lwm2m.hrl"). % This module is for future use. Disabled now. @@ -53,10 +53,10 @@ start_link(XmlDir) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [XmlDir], []). find_objectid(ObjectId) -> - ObjectIdInt = case is_list(ObjectId) of - true -> list_to_integer(ObjectId); - false -> ObjectId - end, + ObjectIdInt = case is_list(ObjectId) of + true -> list_to_integer(ObjectId); + false -> ObjectId + end, case ets:lookup(?LWM2M_OBJECT_DEF_TAB, ObjectIdInt) of [] -> {error, no_xml_definition}; [{ObjectId, Xml}] -> Xml @@ -80,7 +80,6 @@ find_name(Name) -> stop() -> gen_server:stop(?MODULE). - %% ------------------------------------------------------------------ %% gen_server Function Definitions %% ------------------------------------------------------------------ @@ -112,11 +111,13 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- load(BaseDir) -> - Wild = case lists:last(BaseDir) == $/ of - true -> BaseDir++"*.xml"; - false -> BaseDir++"/*.xml" - end, - case filelib:wildcard(Wild) of + Wild = filename:join(BaseDir, "*.xml"), + Wild2 = if is_binary(Wild) -> + erlang:binary_to_list(Wild); + true -> + Wild + end, + case filelib:wildcard(Wild2) of [] -> error(no_xml_files_found, BaseDir); AllXmlFiles -> load_loop(AllXmlFiles) end. @@ -134,9 +135,7 @@ load_loop([FileName|T]) -> ets:insert(?LWM2M_OBJECT_NAME_TO_ID_TAB, {NameBinary, ObjectId}), load_loop(T). - load_xml(FileName) -> {Xml, _Rest} = xmerl_scan:file(FileName), [ObjectXml] = xmerl_xpath:string("/LWM2M/Object", Xml), ObjectXml. - diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index 32ec918a9..4476abfea 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -23,7 +23,6 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/logger.hrl"). - %% API -export([ info/1 , info/2 @@ -39,7 +38,7 @@ , set_conn_state/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -96,9 +95,9 @@ }). -define(DEFAULT_OVERRIDE, - #{ clientid => <<"">> %% Generate clientid by default - , username => <<"${Packet.headers.login}">> - , password => <<"${Packet.headers.passcode}">> + #{ clientid => <<"${ConnInfo.clientid}">> + %, username => <<"${ConnInfo.clientid}">> + %, password => <<"${Packet.headers.passcode}">> }). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session, will_msg]). @@ -190,9 +189,10 @@ stats(#channel{session = Session})-> set_conn_state(ConnState, Channel) -> Channel#channel{conn_state = ConnState}. -enrich_conninfo(?SN_CONNECT_MSG(_Flags, _ProtoId, Duration, _ClientId), +enrich_conninfo(?SN_CONNECT_MSG(_Flags, _ProtoId, Duration, ClientId), Channel = #channel{conninfo = ConnInfo}) -> - NConnInfo = ConnInfo#{ proto_name => <<"MQTT-SN">> + NConnInfo = ConnInfo#{ clientid => ClientId + , proto_name => <<"MQTT-SN">> , proto_ver => <<"1.2">> , clean_start => true , keepalive => Duration @@ -233,8 +233,8 @@ feedvar(Override, Packet, ConnInfo, ClientInfo) -> , 'Packet' => connect_packet_to_map(Packet) }, maps:map(fun(_K, V) -> - Tokens = emqx_rule_utils:preproc_tmpl(V), - emqx_rule_utils:proc_tmpl(Tokens, Envs) + Tokens = emqx_plugin_libs_rule:preproc_tmpl(V), + emqx_plugin_libs_rule:proc_tmpl(Tokens, Envs) end, Override). connect_packet_to_map(#mqtt_sn_message{}) -> @@ -593,9 +593,11 @@ handle_in(SubPkt = ?SN_SUBSCRIBE_MSG(_, MsgId, _), Channel) -> case emqx_misc:pipeline( [ fun preproc_subs_type/2 , fun check_subscribe_authz/2 + , fun run_client_subs_hook/2 , fun do_subscribe/2 ], SubPkt, Channel) of - {ok, {TopicId, GrantedQoS}, NChannel} -> + {ok, {TopicId, _TopicName, SubOpts}, NChannel} -> + GrantedQoS = maps:get(qos, SubOpts), SubAck = ?SN_SUBACK_MSG(#mqtt_sn_flags{qos = GrantedQoS}, TopicId, MsgId, ?SN_RC_ACCEPTED), {ok, outgoing_and_update(SubAck), NChannel}; @@ -611,6 +613,7 @@ handle_in(UnsubPkt = ?SN_UNSUBSCRIBE_MSG(_, MsgId, TopicIdOrName), Channel) -> case emqx_misc:pipeline( [ fun preproc_unsub_type/2 + , fun run_client_unsub_hook/2 , fun do_unsubscribe/2 ], UnsubPkt, Channel) of {ok, _TopicName, NChannel} -> @@ -842,13 +845,10 @@ check_subscribe_authz({_TopicId, TopicName, _QoS}, {error, ?SN_RC_NOT_AUTHORIZE} end. -do_subscribe({TopicId, TopicName, QoS}, - Channel = #channel{ - ctx = Ctx, - session = Session, - clientinfo = ClientInfo - = #{mountpoint := Mountpoint}}) -> - +run_client_subs_hook({TopicId, TopicName, QoS}, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo}) -> {TopicName1, SubOpts0} = emqx_topic:parse(TopicName), TopicFilters = [{TopicName1, SubOpts0#{qos => QoS}}], case run_hooks(Ctx, 'client.subscribe', @@ -856,19 +856,26 @@ do_subscribe({TopicId, TopicName, QoS}, [] -> ?LOG(warning, "Skip to subscribe ~s, " "due to 'client.subscribe' denied!", [TopicName]), - {ok, Channel}; + {error, ?SN_EXCEED_LIMITATION}; [{NTopicName, NSubOpts}|_] -> - NTopicName1 = emqx_mountpoint:mount(Mountpoint, NTopicName), - NSubOpts1 = maps:merge(?DEFAULT_SUBOPTS, NSubOpts), - case emqx_session:subscribe(ClientInfo, NTopicName1, NSubOpts1, Session) of - {ok, NSession} -> - {ok, {TopicId, QoS}, - Channel#channel{session = NSession}}; - {error, ?RC_QUOTA_EXCEEDED} -> - ?LOG(warning, "Cannot subscribe ~s due to ~s.", - [TopicName, emqx_reason_codes:text(?RC_QUOTA_EXCEEDED)]), - {error, ?SN_EXCEED_LIMITATION} - end + {ok, {TopicId, NTopicName, NSubOpts}, Channel} + end. + +do_subscribe({TopicId, TopicName, SubOpts}, + Channel = #channel{ + session = Session, + clientinfo = ClientInfo + = #{mountpoint := Mountpoint}}) -> + NTopicName = emqx_mountpoint:mount(Mountpoint, TopicName), + NSubOpts = maps:merge(?DEFAULT_SUBOPTS, SubOpts), + case emqx_session:subscribe(ClientInfo, NTopicName, NSubOpts, Session) of + {ok, NSession} -> + {ok, {TopicId, NTopicName, NSubOpts}, + Channel#channel{session = NSession}}; + {error, ?RC_QUOTA_EXCEEDED} -> + ?LOG(warning, "Cannot subscribe ~s due to ~s.", + [TopicName, emqx_reason_codes:text(?RC_QUOTA_EXCEEDED)]), + {error, ?SN_EXCEED_LIMITATION} end. %%-------------------------------------------------------------------- @@ -900,33 +907,42 @@ preproc_unsub_type(?SN_UNSUBSCRIBE_MSG_TYPE(?SN_SHORT_TOPIC, end, {ok, TopicName, Channel}. -do_unsubscribe(TopicName, - Channel = #channel{ - ctx = Ctx, - session = Session, - clientinfo = ClientInfo - = #{mountpoint := Mountpoint}}) -> +run_client_unsub_hook(TopicName, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo + }) -> TopicFilters = [emqx_topic:parse(TopicName)], case run_hooks(Ctx, 'client.unsubscribe', [ClientInfo, #{}], TopicFilters) of [] -> - %% Skip to unsubscribe - {ok, Channel}; - [{NTopicName, NSubOpts}|_] -> - NTopicName1 = emqx_mountpoint:mount(Mountpoint, NTopicName), - NSubOpts1 = maps:merge( - emqx_gateway_utils:default_subopts(), - NSubOpts - ), - case emqx_session:unsubscribe(ClientInfo, NTopicName1, - NSubOpts1, Session) of - {ok, NSession} -> - {ok, Channel#channel{session = NSession}}; - {error, ?RC_NO_SUBSCRIPTION_EXISTED} -> - {ok, Channel} - end + {ok, [], Channel}; + NTopicFilters -> + {ok, NTopicFilters, Channel} end. +do_unsubscribe(TopicFilters, + Channel = #channel{ + session = Session, + clientinfo = ClientInfo + = #{mountpoint := Mountpoint}}) -> + NChannel = + lists:foldl(fun({TopicName, SubOpts}, ChannAcc) -> + NTopicName = emqx_mountpoint:mount(Mountpoint, TopicName), + NSubOpts = maps:merge( + emqx_gateway_utils:default_subopts(), + SubOpts + ), + case emqx_session:unsubscribe(ClientInfo, NTopicName, + NSubOpts, Session) of + {ok, NSession} -> + ChannAcc#channel{session = NSession}; + {error, ?RC_NO_SUBSCRIPTION_EXISTED} -> + ChannAcc + end + end, Channel, TopicFilters), + {ok, TopicFilters, NChannel}. + %%-------------------------------------------------------------------- %% Awake & Asleep @@ -1097,23 +1113,55 @@ message_to_packet(MsgId, Message, %% Handle call %%-------------------------------------------------------------------- --spec handle_call(Req :: term(), channel()) - -> {reply, Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), - emqx_types:packet(), channel()}. -handle_call(kick, Channel) -> +-spec handle_call(Req :: term(), From :: term(), channel()) + -> {reply, Reply :: term(), channel()} + | {reply, Reply :: term(), replies(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), + emqx_types:packet(), channel()}. +handle_call({subscribe, Topic, SubOpts}, _From, Channel) -> + %% XXX: Only support short_topic_name + SubProps = maps:get(sub_props, SubOpts, #{}), + case maps:get(subtype, SubProps, short_topic_name) of + short_topic_name -> + case byte_size(Topic) of + 2 -> + case do_subscribe({?SN_INVALID_TOPIC_ID, + Topic, SubOpts}, Channel) of + {ok, _, NChannel} -> + reply(ok, NChannel); + {error, ?SN_EXCEED_LIMITATION} -> + reply({error, exceed_limitation}, Channel) + end; + _ -> + reply({error, bad_topic_name}, Channel) + end; + predefined_topic_id -> + reply({error, only_support_short_name_topic}, Channel); + _ -> + reply({error, only_support_short_name_topic}, Channel) + end; + +handle_call({unsubscribe, Topic}, _From, Channel) -> + TopicFilters = [emqx_topic:parse(Topic)], + {ok, _, NChannel} = do_unsubscribe(TopicFilters, Channel), + reply(ok, NChannel); + +handle_call(subscriptions, _From, Channel = #channel{session = Session}) -> + reply(maps:to_list(emqx_session:info(subscriptions, Session)), Channel); + +handle_call(kick, _From, Channel) -> NChannel = ensure_disconnected(kicked, Channel), shutdown_and_reply(kicked, ok, NChannel); -handle_call(discard, Channel) -> +handle_call(discard, _From, Channel) -> shutdown_and_reply(discarded, ok, Channel); %% XXX: No Session Takeover -%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> +%handle_call({takeover, 'begin'}, _From, Channel = #channel{session = Session}) -> % reply(Session, Channel#channel{takeover = true}); % -%handle_call({takeover, 'end'}, Channel = #channel{session = Session, +%handle_call({takeover, 'end'}, _From, Channel = #channel{session = Session, % pendings = Pendings}) -> % ok = emqx_session:takeover(Session), % %% TODO: Should not drain deliver here (side effect) @@ -1121,16 +1169,16 @@ handle_call(discard, Channel) -> % AllPendings = lists:append(Delivers, Pendings), % shutdown_and_reply(takeovered, AllPendings, Channel); -%handle_call(list_authz_cache, Channel) -> +%handle_call(list_authz_cache, _From, Channel) -> % {reply, emqx_authz_cache:list_authz_cache(), Channel}; %% XXX: No Quota Now -% handle_call({quota, Policy}, Channel) -> +% handle_call({quota, Policy}, _From, Channel) -> % Zone = info(zone, Channel), % Quota = emqx_limiter:init(Zone, Policy), % reply(ok, Channel#channel{quota = Quota}); -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), reply(ignored, Channel). @@ -1150,18 +1198,6 @@ handle_cast(_Req, Channel) -> -spec handle_info(Info :: term(), channel()) -> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}. -%% XXX: Received from the emqx-management ??? -%handle_info({subscribe, TopicFilters}, Channel ) -> -% {_, NChannel} = lists:foldl( -% fun({TopicFilter, SubOpts}, {_, ChannelAcc}) -> -% do_subscribe(TopicFilter, SubOpts, ChannelAcc) -% end, {[], Channel}, parse_topic_filters(TopicFilters)), -% {ok, NChannel}; -% -%handle_info({unsubscribe, TopicFilters}, Channel) -> -% {_RC, NChannel} = process_unsubscribe(TopicFilters, #{}, Channel), -% {ok, NChannel}; - handle_info({sock_closed, Reason}, Channel = #channel{conn_state = idle}) -> shutdown(Reason, Channel); diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 57070206c..a79173cff 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -20,16 +20,13 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([]). - --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). -include_lib("emqx/include/logger.hrl"). @@ -38,94 +35,90 @@ %% APIs %%-------------------------------------------------------------------- -load() -> +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - emqx_gateway_registry:load(mqttsn, RegistryOptions, []). + emqx_gateway_registry:reg(mqttsn, RegistryOptions). -unload() -> - emqx_gateway_registry:unload(mqttsn). - -init(_) -> - GwState = #{}, - {ok, GwState}. +unreg() -> + emqx_gateway_registry:unreg(mqttsn). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_insta_create(_Insta = #{ id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> +on_gateway_load(_Gateway = #{ name := GwName, + config := Config + }, Ctx) -> %% We Also need to start `emqx_sn_broadcast` & %% `emqx_sn_registry` process - SnGwId = maps:get(gateway_id, RawConf), - case maps:get(broadcast, RawConf) of + case maps:get(broadcast, Config, false) of false -> ok; true -> %% FIXME: Port = 1884, + SnGwId = maps:get(gateway_id, Config, undefined), _ = emqx_sn_broadcast:start_link(SnGwId, Port), ok end, - PredefTopics = maps:get(predefined, RawConf), - {ok, RegistrySvr} = emqx_sn_registry:start_link(InstaId, PredefTopics), + PredefTopics = maps:get(predefined, Config, []), + {ok, RegistrySvr} = emqx_sn_registry:start_link(GwName, PredefTopics), - NRawConf = maps:without( + NConfig = maps:without( [broadcast, predefined], - RawConf#{registry => emqx_sn_registry:lookup_name(RegistrySvr)} + Config#{registry => emqx_sn_registry:lookup_name(RegistrySvr)} ), - Listeners = emqx_gateway_utils:normalize_rawconf(NRawConf), + Listeners = emqx_gateway_utils:normalize_config(NConfig), ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, Gateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? - on_insta_destroy(OldInsta, GwInstaState, GwState), - on_insta_create(NewInsta, Ctx, GwState) + on_gateway_unload(Gateway, GwState), + on_gateway_load(Gateway#{config => Config}, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update stomp instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, _GwInstaState, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), +on_gateway_unload(_Gateway = #{ name := GwName, + config := Config + }, _GwState) -> + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) + stop_listener(GwName, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start mqttsn ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start mqttsn ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, Type), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_sn_frame, @@ -134,9 +127,6 @@ start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> esockd:open_udp(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). - merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), case lists:keytake(udp_options, 1, Options) of @@ -147,18 +137,18 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, LisName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop mqttsn ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop mqttsn ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop gatewat ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl index 991acaa7b..7bb9e608c 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl @@ -60,8 +60,6 @@ %-boot_mnesia({mnesia, [boot]}). %-copy_mnesia({mnesia, [copy]}). -%-rlog_shard({?SN_SHARD, ?TAB}). - %%% @doc Create or replicate tables. %-spec(mnesia(boot | copy) -> ok). %mnesia(boot) -> @@ -149,9 +147,11 @@ init([InstaId, PredefTopics]) -> {ram_copies, [node()]}, {record_name, emqx_sn_registry}, {attributes, record_info(fields, emqx_sn_registry)}, - {storage_properties, [{ets, [{read_concurrency, true}]}]} + {storage_properties, [{ets, [{read_concurrency, true}]}]}, + {rlog_shard, ?SN_SHARD} ]), ok = ekka_mnesia:copy_table(Tab, ram_copies), + ok = ekka_rlog:wait_for_shards([?SN_SHARD], infinity), % FIXME: %ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity), MaxPredefId = lists:foldl( diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index c7426a40d..31b1904bb 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -22,7 +22,6 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). - -import(proplists, [get_value/2, get_value/3]). %% API @@ -40,7 +39,7 @@ , set_conn_state/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -113,7 +112,7 @@ init(ConnInfo = #{peername := {PeerHost, _}, ClientInfo = setting_peercert_infos( Peercert, #{ zone => default - , listener => mqtt_tcp + , listener => {tcp, default} , protocol => stomp , peerhost => PeerHost , sockport => SockPort @@ -232,8 +231,8 @@ feedvar(Override, Packet, ConnInfo, ClientInfo) -> , 'Packet' => connect_packet_to_map(Packet) }, maps:map(fun(_K, V) -> - Tokens = emqx_rule_utils:preproc_tmpl(V), - emqx_rule_utils:proc_tmpl(Tokens, Envs) + Tokens = emqx_plugin_libs_rule:preproc_tmpl(V), + emqx_plugin_libs_rule:proc_tmpl(Tokens, Envs) end, Override). connect_packet_to_map(#stomp_frame{headers = Headers}) -> @@ -393,11 +392,9 @@ handle_in(?PACKET(?CMD_SUBSCRIBE, Headers), [] -> ErrMsg = "Permission denied", handle_out(error, {receipt_id(Headers), ErrMsg}, Channel); - [MountedTopic|_] -> - NChannel1 = NChannel#channel{ - subscriptions = [{SubId, MountedTopic, Ack} - | Subs] - }, + [{MountedTopic, SubOpts}|_] -> + NSubs = [{SubId, MountedTopic, Ack, SubOpts}|Subs], + NChannel1 = NChannel#channel{subscriptions = NSubs}, handle_out(receipt, receipt_id(Headers), NChannel1) end; {error, ErrMsg, NChannel} -> @@ -415,7 +412,7 @@ handle_in(?PACKET(?CMD_UNSUBSCRIBE, Headers), SubId = header(<<"id">>, Headers), {ok, NChannel} = case lists:keyfind(SubId, 1, Subs) of - {SubId, MountedTopic, _Ack} -> + {SubId, MountedTopic, _Ack, _SubOpts} -> Topic = emqx_mountpoint:unmount(Mountpoint, MountedTopic), %% XXX: eval the return topics? _ = run_hooks(Ctx, 'client.unsubscribe', @@ -539,29 +536,30 @@ trans_pipeline([{Func, Args}|More], Outgoings, Channel) -> %% Subs parse_topic_filter({SubId, Topic}, Channel) -> - TopicFilter = emqx_topic:parse(Topic), - {ok, {SubId, TopicFilter}, Channel}. + {ParsedTopic, SubOpts} = emqx_topic:parse(Topic), + NSubOpts = SubOpts#{sub_props => #{subid => SubId}}, + {ok, {SubId, {ParsedTopic, NSubOpts}}, Channel}. -check_subscribed_status({SubId, TopicFilter}, +check_subscribed_status({SubId, {ParsedTopic, _SubOpts}}, #channel{ subscriptions = Subs, clientinfo = #{mountpoint := Mountpoint} }) -> - MountedTopic = emqx_mountpoint:mount(Mountpoint, TopicFilter), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), case lists:keyfind(SubId, 1, Subs) of - {SubId, MountedTopic, _Ack} -> + {SubId, MountedTopic, _Ack, _} -> ok; - {SubId, _OtherTopic, _Ack} -> + {SubId, _OtherTopic, _Ack, _} -> {error, "Conflict subscribe id"}; false -> ok end. -check_sub_acl({_SubId, TopicFilter}, +check_sub_acl({_SubId, {ParsedTopic, _SubOpts}}, #channel{ ctx = Ctx, clientinfo = ClientInfo}) -> - case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, TopicFilter) of + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, ParsedTopic) of deny -> {error, "ACL Deny"}; allow -> ok end. @@ -571,27 +569,27 @@ do_subscribe(TopicFilters, Channel) -> do_subscribe([], _Channel, Acc) -> lists:reverse(Acc); -do_subscribe([{TopicFilter, Option}|More], +do_subscribe([{ParsedTopic, SubOpts0}|More], Channel = #channel{ ctx = Ctx, clientinfo = ClientInfo = #{clientid := ClientId, mountpoint := Mountpoint}}, Acc) -> - SubOpts = maps:merge(emqx_gateway_utils:default_subopts(), Option), - MountedTopic = emqx_mountpoint:mount(Mountpoint, TopicFilter), + SubOpts = maps:merge(emqx_gateway_utils:default_subopts(), SubOpts0), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), _ = emqx_broker:subscribe(MountedTopic, ClientId, SubOpts), run_hooks(Ctx, 'session.subscribed', [ClientInfo, MountedTopic, SubOpts]), - do_subscribe(More, Channel, [MountedTopic|Acc]). + do_subscribe(More, Channel, [{MountedTopic, SubOpts}|Acc]). %%-------------------------------------------------------------------- %% Handle outgoing packet %%-------------------------------------------------------------------- -spec(handle_out(atom(), term(), channel()) - -> {ok, channel()} - | {ok, replies(), channel()} - | {shutdown, Reason :: term(), channel()} - | {shutdown, Reason :: term(), replies(), channel()}). + -> {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()} + | {shutdown, Reason :: term(), replies(), channel()}). handle_out(connerr, {Headers, ReceiptId, ErrMsg}, Channel) -> Frame = error_frame(Headers, ReceiptId, ErrMsg), @@ -622,24 +620,78 @@ handle_out(receipt, ReceiptId, Channel) -> %% Handle call %%-------------------------------------------------------------------- --spec(handle_call(Req :: term(), channel()) - -> {reply, Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), stomp_frame(), channel()}). -handle_call(kick, Channel) -> +-spec(handle_call(Req :: term(), From :: term(), channel()) + -> {reply, Reply :: term(), channel()} + | {reply, Reply :: term(), replies(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), stomp_frame(), channel()}). +handle_call({subscribe, Topic, SubOpts}, _From, + Channel = #channel{ + subscriptions = Subs + }) -> + case maps:get(subid, + maps:get(sub_props, SubOpts, #{}), + undefined) of + undefined -> + reply({error, no_subid}, Channel); + SubId -> + case emqx_misc:pipeline( + [ fun parse_topic_filter/2 + , fun check_subscribed_status/2 + ], {SubId, {Topic, SubOpts}}, Channel) of + {ok, {_, TopicFilter}, NChannel} -> + [{MountedTopic, NSubOpts}] = do_subscribe( + [TopicFilter], + NChannel + ), + NSubs = [{SubId, MountedTopic, <<"auto">>, NSubOpts}|Subs], + NChannel1 = NChannel#channel{subscriptions = NSubs}, + reply(ok, NChannel1); + {error, ErrMsg, NChannel} -> + ?LOG(error, "Failed to subscribe topic ~s, reason: ~s", + [Topic, ErrMsg]), + reply({error, ErrMsg}, NChannel) + end + end; + +handle_call({unsubscribe, Topic}, _From, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo = #{mountpoint := Mountpoint}, + subscriptions = Subs + }) -> + {ParsedTopic, _SubOpts} = emqx_topic:parse(Topic), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), + ok = emqx_broker:unsubscribe(MountedTopic), + _ = run_hooks(Ctx, 'session.unsubscribe', + [ClientInfo, MountedTopic, #{}]), + reply(ok, + Channel#channel{ + subscriptions = lists:keydelete(MountedTopic, 2, Subs)} + ); + +%% Reply :: [{emqx_types:topic(), emqx_types:subopts()}] +handle_call(subscriptions, _From, Channel = #channel{subscriptions = Subs}) -> + Reply = lists:map( + fun({_SubId, Topic, _Ack, SubOpts}) -> + {Topic, SubOpts} + end, Subs), + reply(Reply, Channel); + +handle_call(kick, _From, Channel) -> NChannel = ensure_disconnected(kicked, Channel), Frame = error_frame(undefined, <<"Kicked out">>), shutdown_and_reply(kicked, ok, Frame, NChannel); -handle_call(discard, Channel) -> +handle_call(discard, _From, Channel) -> Frame = error_frame(undefined, <<"Discarded">>), shutdown_and_reply(discarded, ok, Frame, Channel); %% XXX: No Session Takeover -%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> +%handle_call({takeover, 'begin'}, _From, Channel = #channel{session = Session}) -> % reply(Session, Channel#channel{takeover = true}); % -%handle_call({takeover, 'end'}, Channel = #channel{session = Session, +%handle_call({takeover, 'end'}, _From, Channel = #channel{session = Session, % pendings = Pendings}) -> % ok = emqx_session:takeover(Session), % %% TODO: Should not drain deliver here (side effect) @@ -647,9 +699,9 @@ handle_call(discard, Channel) -> % AllPendings = lists:append(Delivers, Pendings), % shutdown_and_reply(takeovered, AllPendings, Channel); -handle_call(list_authz_cache, Channel) -> +handle_call(list_authz_cache, _From, Channel) -> %% This won't work - {reply, emqx_authz_cache:list_authz_cache(default), Channel}; + {reply, emqx_authz_cache:list_authz_cache(), Channel}; %% XXX: No Quota Now % handle_call({quota, Policy}, Channel) -> @@ -657,11 +709,10 @@ handle_call(list_authz_cache, Channel) -> % Quota = emqx_limiter:init(Zone, Policy), % reply(ok, Channel#channel{quota = Quota}); -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), reply(ignored, Channel). - %%-------------------------------------------------------------------- %% Handle cast %%-------------------------------------------------------------------- @@ -678,18 +729,6 @@ handle_cast(_Req, Channel) -> -spec(handle_info(Info :: term(), channel()) -> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}). -%% XXX: Received from the emqx-management ??? -%handle_info({subscribe, TopicFilters}, Channel ) -> -% {_, NChannel} = lists:foldl( -% fun({TopicFilter, SubOpts}, {_, ChannelAcc}) -> -% do_subscribe(TopicFilter, SubOpts, ChannelAcc) -% end, {[], Channel}, parse_topic_filters(TopicFilters)), -% {ok, NChannel}; -% -%handle_info({unsubscribe, TopicFilters}, Channel) -> -% {_RC, NChannel} = process_unsubscribe(TopicFilters, #{}, Channel), -% {ok, NChannel}; - handle_info({sock_closed, Reason}, Channel = #channel{conn_state = idle}) -> shutdown(Reason, Channel); @@ -755,7 +794,7 @@ handle_deliver(Delivers, Frames0 = lists:foldl(fun({_, _, Message}, Acc) -> Topic0 = emqx_message:topic(Message), case lists:keyfind(Topic0, 2, Subs) of - {Id, Topic, Ack} -> + {Id, Topic, Ack, _SubOpts} -> %% XXX: refactor later metrics_inc('messages.delivered', Channel), NMessage = run_hooks_without_metrics( diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 95183ad5e..9599ef6e3 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -19,14 +19,13 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). -include_lib("emqx_gateway/include/emqx_gateway.hrl"). @@ -36,79 +35,75 @@ %% APIs %%-------------------------------------------------------------------- --spec load() -> ok | {error, any()}. -load() -> +-spec reg() -> ok | {error, any()}. +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - emqx_gateway_registry:load(stomp, RegistryOptions, []). + emqx_gateway_registry:reg(stomp, RegistryOptions). --spec unload() -> ok | {error, any()}. -unload() -> - emqx_gateway_registry:unload(stomp). - -init(_) -> - GwState = #{}, - {ok, GwState}. +-spec unreg() -> ok | {error, any()}. +unreg() -> + emqx_gateway_registry:unreg(stomp). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_insta_create(_Insta = #{ id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> - %% Step1. Fold the rawconfs to listeners - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), +on_gateway_load(_Gateway = #{ name := GwName, + config := Config + }, Ctx) -> + %% Step1. Fold the config to listeners + Listeners = emqx_gateway_utils:normalize_config(Config), %% Step2. Start listeners or escokd:specs ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), %% FIXME: How to throw an exception to interrupt the restart logic ? - %% FIXME: Assign ctx to InstaState - {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. + %% FIXME: Assign ctx to GwState + {ok, ListenerPids, _GwState = #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, Gateway), try %% XXX: 1. How hot-upgrade the changes ??? - %% XXX: 2. Check the New confs first before destroy old instance ??? - on_insta_destroy(OldInsta, GwInstaState, GwState), - on_insta_create(NewInsta, Ctx, GwState) + %% XXX: 2. Check the New confs first before destroy old state??? + on_gateway_unload(Gateway, GwState), + on_gateway_load(Gateway#{config => Config}, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update stomp instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, _GwInstaState, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), +on_gateway_unload(_Gateway = #{ name := GwName, + config := Config + }, _GwState) -> + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) + stop_listener(GwName, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start stomp ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start stomp ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, Type), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_stomp_frame, @@ -117,9 +112,6 @@ start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> esockd:open(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). - merge_default(Options) -> Default = emqx_gateway_utils:default_tcp_options(), case lists:keytake(tcp_options, 1, Options) of @@ -130,18 +122,18 @@ merge_default(Options) -> [{tcp_options, Default} | Options] end. -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop stomp ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop stomp ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl new file mode 100644 index 000000000..83521f5cd --- /dev/null +++ b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl @@ -0,0 +1,200 @@ +%%-------------------------------------------------------------------- +%% Copyright (C) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_coap_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<" +gateway.coap { + idle_timeout = 30s + enable_stats = false + mountpoint = \"\" + notify_type = qos + connection_required = true + subscribe_qos = qos1 + publish_qos = qos1 + authentication = undefined + + listeners.udp.default { + bind = 5683 + } + } + ">>). + +-define(HOST, "127.0.0.1"). +-define(PORT, 5683). +-define(CONN_URI, "coap://127.0.0.1/mqtt/connection?clientid=client1&username=admin&password=public"). + +-define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + emqx_mgmt_api_test_util:init_suite([emqx_gateway]), + Config. + +end_per_suite(Config) -> + emqx_mgmt_api_test_util:end_suite([emqx_gateway]), + Config. + +%%-------------------------------------------------------------------- +%% Cases +%%-------------------------------------------------------------------- +t_send_request_api(_) -> + ClientId = start_client(), + timer:sleep(200), + Path = emqx_mgmt_api_test_util:api_path(["gateway/coap/client1/request"]), + Token = <<"atoken">>, + Payload = <<"simple echo this">>, + Req = #{token => Token, + payload => Payload, + timeout => 10, + content_type => <<"text/plain">>, + method => <<"get">>}, + Auth = emqx_mgmt_api_test_util:auth_header_(), + {ok, Response} = emqx_mgmt_api_test_util:request_api(post, + Path, + "method=get", + Auth, + Req + ), + #{<<"token">> := RToken, <<"payload">> := RPayload} = + emqx_json:decode(Response, [return_maps]), + ?assertEqual(Token, RToken), + ?assertEqual(Payload, RPayload), + erlang:exit(ClientId, kill), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Internal Functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +start_client() -> + spawn(fun coap_client/0). + +coap_client() -> + {ok, CSock} = gen_udp:open(0, [binary, {active, false}]), + test_send_coap_request(CSock, post, <<>>, [], 1), + Response = test_recv_coap_response(CSock), + ?assertEqual({ok, created}, Response#coap_message.method), + echo_loop(CSock). + +echo_loop(CSock) -> + #coap_message{payload = Payload} = Req = test_recv_coap_request(CSock), + test_send_coap_response(CSock, ?HOST, ?PORT, {ok, content}, Payload, Req), + echo_loop(CSock). + +test_send_coap_request(UdpSock, Method, Content, Options, MsgId) -> + is_list(Options) orelse error("Options must be a list"), + case resolve_uri(?CONN_URI) of + {coap, {IpAddr, Port}, Path, Query} -> + Request0 = emqx_coap_message:request(con, Method, Content, + [{uri_path, Path}, + {uri_query, Query} | Options]), + Request = Request0#coap_message{id = MsgId}, + ?LOGT("send_coap_request Request=~p", [Request]), + RequestBinary = emqx_coap_frame:serialize_pkt(Request, undefined), + ?LOGT("test udp socket send to ~p:~p, data=~p", [IpAddr, Port, RequestBinary]), + ok = gen_udp:send(UdpSock, IpAddr, Port, RequestBinary); + {SchemeDiff, ChIdDiff, _, _} -> + error(lists:flatten(io_lib:format("scheme ~s or ChId ~s does not match with socket", [SchemeDiff, ChIdDiff]))) + end. + +test_recv_coap_response(UdpSock) -> + {ok, {Address, Port, Packet}} = gen_udp:recv(UdpSock, 0, 2000), + {ok, Response, _, _} = emqx_coap_frame:parse(Packet, undefined), + ?LOGT("test udp receive from ~p:~p, data1=~p, Response=~p", [Address, Port, Packet, Response]), + #coap_message{type = ack, method = Method, id=Id, token = Token, options = Options, payload = Payload} = Response, + ?LOGT("receive coap response Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Response. + +test_recv_coap_request(UdpSock) -> + case gen_udp:recv(UdpSock, 0) of + {ok, {_Address, _Port, Packet}} -> + {ok, Request, _, _} = emqx_coap_frame:parse(Packet, undefined), + #coap_message{type = con, method = Method, id=Id, token = Token, payload = Payload, options = Options} = Request, + ?LOGT("receive coap request Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Request; + {error, Reason} -> + ?LOGT("test_recv_coap_request failed, Reason=~p", [Reason]), + timeout_test_recv_coap_request + end. + +test_send_coap_response(UdpSock, Host, Port, Code, Content, Request) -> + is_list(Host) orelse error("Host is not a string"), + {ok, IpAddr} = inet:getaddr(Host, inet), + Response = emqx_coap_message:piggyback(Code, Content, Request), + ?LOGT("test_send_coap_response Response=~p", [Response]), + Binary = emqx_coap_frame:serialize_pkt(Response, undefined), + ok = gen_udp:send(UdpSock, IpAddr, Port, Binary). + +resolve_uri(Uri) -> + {ok, #{scheme := Scheme, + host := Host, + port := PortNo, + path := Path} = URIMap} = emqx_http_lib:uri_parse(Uri), + Query = maps:get(query, URIMap, undefined), + {ok, PeerIP} = inet:getaddr(Host, inet), + {Scheme, {PeerIP, PortNo}, split_path(Path), split_query(Query)}. + +split_path([]) -> []; +split_path([$/]) -> []; +split_path([$/ | Path]) -> split_segments(Path, $/, []). + +split_query(undefined) -> #{}; +split_query(Path) -> + split_segments(Path, $&, []). + +split_segments(Path, Char, Acc) -> + case string:rchr(Path, Char) of + 0 -> + [make_segment(Path) | Acc]; + N when N > 0 -> + split_segments(string:substr(Path, 1, N-1), Char, + [make_segment(string:substr(Path, N+1)) | Acc]) + end. + +make_segment(Seg) -> + list_to_binary(emqx_http_lib:uri_decode(Seg)). + +get_path([], Acc) -> + %?LOGT("get_path Acc=~p", [Acc]), + Acc; +get_path([{uri_path, Path1}|T], Acc) -> + %?LOGT("Path=~p, Acc=~p", [Path1, Acc]), + get_path(T, join_path(Path1, Acc)); +get_path([{_, _}|T], Acc) -> + get_path(T, Acc). + +join_path([], Acc) -> Acc; +join_path([<<"/">>|T], Acc) -> + join_path(T, Acc); +join_path([H|T], Acc) -> + join_path(T, <>). + +sprintf(Format, Args) -> + lists:flatten(io_lib:format(Format, Args)). diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 6e4faa40a..b91cd03b9 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -67,18 +67,16 @@ set_special_cfg(emqx_gateway) -> LisType = get(grpname), emqx_config:put( [gateway, exproto], - #{'1' => - #{authenticator => allow_anonymous, - server => #{bind => 9100}, - handler => #{address => "http://127.0.0.1:9001"}, - listener => listener_confs(LisType) - }}); + #{server => #{bind => 9100}, + handler => #{address => "http://127.0.0.1:9001"}, + listeners => listener_confs(LisType) + }); set_special_cfg(_App) -> ok. listener_confs(Type) -> Default = #{bind => 7993, acceptors => 8}, - #{Type => #{'1' => maps:merge(Default, maps:from_list(socketopts(Type)))}}. + #{Type => #{'default' => maps:merge(Default, socketopts(Type))}}. %%-------------------------------------------------------------------- %% Tests cases @@ -362,11 +360,11 @@ open(udp) -> {ok, Sock} = gen_udp:open(0, ?TCPOPTS), {udp, Sock}; open(ssl) -> - SslOpts = client_ssl_opts(), + SslOpts = maps:to_list(client_ssl_opts()), {ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?TCPOPTS ++ SslOpts), {ssl, SslSock}; open(dtls) -> - SslOpts = client_ssl_opts(), + SslOpts = maps:to_list(client_ssl_opts()), {ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?DTLSOPTS ++ SslOpts), {dtls, SslSock}. @@ -402,51 +400,56 @@ close({dtls, Sock}) -> %% Server-Opts socketopts(tcp) -> - [{tcp_options, tcp_opts()}]; + #{tcp => tcp_opts()}; socketopts(ssl) -> - [{tcp_options, tcp_opts()}, - {ssl_options, ssl_opts()}]; + #{tcp => tcp_opts(), + ssl => ssl_opts()}; socketopts(udp) -> - [{udp_options, udp_opts()}]; + #{udp => udp_opts()}; socketopts(dtls) -> - [{udp_options, udp_opts()}, - {dtls_options, dtls_opts()}]. + #{udp => udp_opts(), + dtls => dtls_opts()}. tcp_opts() -> - [{send_timeout, 15000}, - {send_timeout_close, true}, - {backlog, 100}, - {nodelay, true} | udp_opts()]. + maps:merge( + udp_opts(), + #{send_timeout => 15000, + send_timeout_close => true, + backlog => 100, + nodelay => true} + ). udp_opts() -> - [{recbuf, 1024}, - {sndbuf, 1024}, - {buffer, 1024}, - {reuseaddr, true}]. + #{recbuf => 1024, + sndbuf => 1024, + buffer => 1024, + reuseaddr => true}. ssl_opts() -> Certs = certs("key.pem", "cert.pem", "cacert.pem"), - [{versions, emqx_tls_lib:default_versions()}, - {ciphers, emqx_tls_lib:default_ciphers()}, - {verify, verify_peer}, - {fail_if_no_peer_cert, true}, - {secure_renegotiate, false}, - {reuse_sessions, true}, - {honor_cipher_order, true}]++Certs. + maps:merge( + Certs, + #{versions => emqx_tls_lib:default_versions(), + ciphers => emqx_tls_lib:default_ciphers(), + verify => verify_peer, + fail_if_no_peer_cert => true, + secure_renegotiate => false, + reuse_sessions => true, + honor_cipher_order => true} + ). dtls_opts() -> - Opts = ssl_opts(), - lists:keyreplace(versions, 1, Opts, {versions, ['dtlsv1.2', 'dtlsv1']}). + maps:merge(ssl_opts(), #{versions => ['dtlsv1.2', 'dtlsv1']}). %%-------------------------------------------------------------------- %% Client-Opts client_ssl_opts() -> - certs( "client-key.pem", "client-cert.pem", "cacert.pem" ). + certs("client-key.pem", "client-cert.pem", "cacert.pem"). -certs( Key, Cert, CACert ) -> +certs(Key, Cert, CACert) -> CertsPath = emqx_ct_helpers:deps_path(emqx, "etc/certs"), - [ { keyfile, filename:join([ CertsPath, Key ]) }, - { certfile, filename:join([ CertsPath, Cert ]) }, - { cacertfile, filename:join([ CertsPath, CACert ]) } ]. + #{keyfile => filename:join([ CertsPath, Key ]), + certfile => filename:join([ CertsPath, Cert ]), + cacertfile => filename:join([ CertsPath, CACert])}. diff --git a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl index 33d577a46..56776957f 100644 --- a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl @@ -23,7 +23,7 @@ -define(CONF_DEFAULT, <<""" gateway: { - stomp.1 {} + stomp {} } """>>). @@ -49,21 +49,15 @@ end_per_suite(_Cfg) -> t_load_unload(_) -> OldCnt = length(emqx_gateway_registry:list()), RgOpts = [{cbkmod, ?MODULE}], - GwOpts = [paramsin], - ok = emqx_gateway_registry:load(test, RgOpts, GwOpts), + ok = emqx_gateway_registry:reg(test, RgOpts), ?assertEqual(OldCnt+1, length(emqx_gateway_registry:list())), #{cbkmod := ?MODULE, - rgopts := RgOpts, - gwopts := GwOpts, - state := #{gwstate := 1}} = emqx_gateway_registry:lookup(test), + rgopts := RgOpts} = emqx_gateway_registry:lookup(test), - {error, already_existed} = emqx_gateway_registry:load(test, [{cbkmod, ?MODULE}], GwOpts), + {error, already_existed} = emqx_gateway_registry:reg(test, [{cbkmod, ?MODULE}]), - ok = emqx_gateway_registry:unload(test), + ok = emqx_gateway_registry:unreg(test), undefined = emqx_gateway_registry:lookup(test), OldCnt = length(emqx_gateway_registry:list()), ok. - -init([paramsin]) -> - {ok, _GwState = #{gwstate => 1}}. diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index 0e19d9b4f..28edda7ef 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (C) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -23,32 +23,30 @@ -define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). --include("src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). -include_lib("lwm2m_coap/include/coap.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). -define(CONF_DEFAULT, <<" -gateway: { - lwm2m_xml_dir: \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" - lwm2m.1: { - lifetime_min: 1s - lifetime_max: 86400s - qmode_time_windonw: 22 - auto_observe: false - mountpoint: \"lwm2m/%e/\" - update_msg_publish_condition: contains_object_list - translators: { - command: \"dn/#\" - response: \"up/resp\" - notify: \"up/notify\" - register: \"up/resp\" - update: \"up/resp\" - } - listener.udp.1 { - bind: 5783 - } - } +gateway.lwm2m { + xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" + lifetime_min = 1s + lifetime_max = 86400s + qmode_time_windonw = 22 + auto_observe = false + mountpoint = \"lwm2m/%u\" + update_msg_publish_condition = contains_object_list + translators { + command = {topic = \"/dn/#\", qos = 0} + response = {topic = \"/up/resp\", qos = 0} + notify = {topic = \"/up/notify\", qos = 0} + register = {topic = \"/up/resp\", qos = 0} + update = {topic = \"/up/resp\", qos = 0} + } + listeners.udp.default { + bind = 5783 + } } ">>). @@ -60,11 +58,15 @@ all() -> [ {group, test_grp_0_register} , {group, test_grp_1_read} , {group, test_grp_2_write} + , {group, test_grp_create} + , {group, test_grp_delete} , {group, test_grp_3_execute} , {group, test_grp_4_discover} , {group, test_grp_5_write_attr} , {group, test_grp_6_observe} - , {group, test_grp_8_object_19} + + %% {group, test_grp_8_object_19} + , {group, test_grp_9_psm_queue_mode} ]. suite() -> [{timetrap, {seconds, 90}}]. @@ -72,74 +74,86 @@ suite() -> [{timetrap, {seconds, 90}}]. groups() -> RepeatOpt = {repeat_until_all_ok, 1}, [ - {test_grp_0_register, [RepeatOpt], [ - case01_register, - case01_register_additional_opts, - case01_register_incorrect_opts, - case01_register_report, - case02_update_deregister, - case03_register_wrong_version, - case04_register_and_lifetime_timeout, - case05_register_wrong_epn, - case06_register_wrong_lifetime, - case07_register_alternate_path_01, - case07_register_alternate_path_02, - case08_reregister - ]}, - {test_grp_1_read, [RepeatOpt], [ - case10_read, - case10_read_separate_ack, - case11_read_object_tlv, - case11_read_object_json, - case12_read_resource_opaque, - case13_read_no_xml - ]}, - {test_grp_2_write, [RepeatOpt], [ - case20_write, - case21_write_object, - case22_write_error, - case20_single_write - ]}, - {test_grp_create, [RepeatOpt], [ - case_create_basic - ]}, - {test_grp_delete, [RepeatOpt], [ - case_delete_basic - ]}, - {test_grp_3_execute, [RepeatOpt], [ - case30_execute, case31_execute_error - ]}, - {test_grp_4_discover, [RepeatOpt], [ - case40_discover - ]}, - {test_grp_5_write_attr, [RepeatOpt], [ - case50_write_attribute - ]}, - {test_grp_6_observe, [RepeatOpt], [ - case60_observe - ]}, - {test_grp_7_block_wize_transfer, [RepeatOpt], [ - case70_read_large, case70_write_large - ]}, - {test_grp_8_object_19, [RepeatOpt], [ - case80_specail_object_19_1_0_write, - case80_specail_object_19_0_0_notify - %case80_specail_object_19_0_0_response, - %case80_normal_object_19_0_0_read - ]}, - {test_grp_9_psm_queue_mode, [RepeatOpt], [ - case90_psm_mode, - case90_queue_mode - ]} + {test_grp_0_register, [RepeatOpt], + [ + case01_register, + case01_register_additional_opts, + %% case01_register_incorrect_opts, %% TODO now we can't handle partial decode packet + case01_register_report, + case02_update_deregister, + case03_register_wrong_version, + case04_register_and_lifetime_timeout, + case05_register_wrong_epn, + %% case06_register_wrong_lifetime, %% now, will ignore wrong lifetime + case07_register_alternate_path_01, + case07_register_alternate_path_02, + case08_reregister + ]}, + {test_grp_1_read, [RepeatOpt], + [ + case10_read, + case10_read_separate_ack, + case11_read_object_tlv, + case11_read_object_json, + case12_read_resource_opaque, + case13_read_no_xml + ]}, + {test_grp_2_write, [RepeatOpt], + [ + case20_write, + case21_write_object, + case22_write_error, + case20_single_write + ]}, + {test_grp_create, [RepeatOpt], + [ + case_create_basic + ]}, + {test_grp_delete, [RepeatOpt], + [ + case_delete_basic + ]}, + {test_grp_3_execute, [RepeatOpt], + [ + case30_execute, case31_execute_error + ]}, + {test_grp_4_discover, [RepeatOpt], + [ + case40_discover + ]}, + {test_grp_5_write_attr, [RepeatOpt], + [ + case50_write_attribute + ]}, + {test_grp_6_observe, [RepeatOpt], + [ + case60_observe + ]}, + {test_grp_7_block_wize_transfer, [RepeatOpt], + [ + case70_read_large, case70_write_large + ]}, + {test_grp_8_object_19, [RepeatOpt], + [ + case80_specail_object_19_1_0_write, + case80_specail_object_19_0_0_notify, + case80_specail_object_19_0_0_response, + case80_normal_object_19_0_0_read + ]}, + {test_grp_9_psm_queue_mode, [RepeatOpt], + [ + case90_psm_mode, + case90_queue_mode + ]} ]. init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx]), + emqx_ct_helpers:start_apps([]), Config. end_per_suite(Config) -> timer:sleep(300), - emqx_ct_helpers:stop_apps([emqx]), + emqx_ct_helpers:stop_apps([]), Config. init_per_testcase(_AllTestCase, Config) -> @@ -164,9 +178,9 @@ end_per_testcase(_AllTestCase, Config) -> %%-------------------------------------------------------------------- case01_register(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -189,13 +203,13 @@ case01_register(Config) -> ?assertNotEqual(undefined, Location), %% checkpoint 2 - verify subscribed topics - timer:sleep(50), + timer:sleep(100), ?LOGT("all topics: ~p", [test_mqtt_broker:get_subscrbied_topics()]), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -211,9 +225,9 @@ case01_register(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case01_register_additional_opts(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -241,9 +255,9 @@ case01_register_additional_opts(Config) -> true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -259,9 +273,9 @@ case01_register_additional_opts(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case01_register_incorrect_opts(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -281,9 +295,9 @@ case01_register_incorrect_opts(Config) -> ?assertEqual({error,bad_request}, Method). case01_register_report(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -322,9 +336,9 @@ case01_register_report(Config) -> }), ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -340,9 +354,9 @@ case01_register_report(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case02_update_deregister(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -375,9 +389,9 @@ case02_update_deregister(Config) -> }), ?assertEqual(Register, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % UPDATE command - % ---------------------------------------- + %%---------------------------------------- + %% UPDATE command + %%---------------------------------------- ?LOGT("start to send UPDATE command", []), MsgId2 = 27, test_send_coap_request( UdpSock, @@ -401,9 +415,9 @@ case02_update_deregister(Config) -> }), ?assertEqual(Update, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -420,9 +434,9 @@ case02_update_deregister(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case03_register_wrong_version(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -434,15 +448,15 @@ case03_register_wrong_version(Config) -> [], MsgId), #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), - ?assertEqual({error,precondition_failed}, Method), + ?assertEqual({error, bad_request}, Method), timer:sleep(50), false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case04_register_and_lifetime_timeout(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -460,17 +474,17 @@ case04_register_and_lifetime_timeout(Config) -> true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % lifetime timeout - % ---------------------------------------- + %%---------------------------------------- + %% lifetime timeout + %%---------------------------------------- timer:sleep(4000), false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case05_register_wrong_epn(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- MsgId = 12, UdpSock = ?config(sock, Config), @@ -483,29 +497,29 @@ case05_register_wrong_epn(Config) -> #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), ?assertEqual({error,bad_request}, Method). -case06_register_wrong_lifetime(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- - UdpSock = ?config(sock, Config), - Epn = "urn:oma:lwm2m:oma:3", - MsgId = 12, +%% case06_register_wrong_lifetime(Config) -> +%% %%---------------------------------------- +%% %% REGISTER command +%% %%---------------------------------------- +%% UdpSock = ?config(sock, Config), +%% Epn = "urn:oma:lwm2m:oma:3", +%% MsgId = 12, - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s&lwm2m=1", [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId), - #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), - ?assertEqual({error,bad_request}, Method), - timer:sleep(50), - ?assertEqual([], test_mqtt_broker:get_subscrbied_topics()). +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s&lwm2m=1", [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId), +%% #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), +%% ?assertEqual({error,bad_request}, Method), +%% timer:sleep(50), +%% ?assertEqual([], test_mqtt_broker:get_subscrbied_topics()). case07_register_alternate_path_01(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -518,16 +532,16 @@ case07_register_alternate_path_01(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case07_register_alternate_path_02(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -540,16 +554,16 @@ case07_register_alternate_path_02(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case08_reregister(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -562,24 +576,24 @@ case08_reregister(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), ReadResult = emqx_json:encode( - #{ - <<"msgType">> => <<"register">>, - <<"data">> => #{ - <<"alternatePath">> => <<"/lwm2m">>, - <<"ep">> => list_to_binary(Epn), - <<"lt">> => 345, - <<"lwm2m">> => <<"1">>, - <<"objectList">> => [<<"/1/0">>, <<"/2/0">>, <<"/3/0">>] - } - } - ), + #{ + <<"msgType">> => <<"register">>, + <<"data">> => #{ + <<"alternatePath">> => <<"/lwm2m">>, + <<"ep">> => list_to_binary(Epn), + <<"lt">> => 345, + <<"lwm2m">> => <<"1">>, + <<"objectList">> => [<<"/1/0">>, <<"/2/0">>, <<"/3/0">>] + } + } + ), ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)), timer:sleep(1000), @@ -588,9 +602,10 @@ case08_reregister(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId + 1), + %% verify the lwm2m client is still online ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)). @@ -601,28 +616,28 @@ case10_read(Config) -> RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), timer:sleep(200), - % step 1, device register ... + %% step 1, device register ... test_send_coap_request( UdpSock, post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId1), #coap_message{method = Method1} = test_recv_coap_response(UdpSock), ?assertEqual({ok,created}, Method1), test_recv_mqtt_response(RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -640,17 +655,17 @@ case10_read(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/0">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/0">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case10_read_separate_ack(Config) -> @@ -663,19 +678,19 @@ case10_read_separate_ack(Config) -> emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), timer:sleep(200), - % step 1, device register ... + %% step 1, device register ... std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -690,12 +705,12 @@ case10_read_separate_ack(Config) -> test_send_empty_ack(UdpSock, "127.0.0.1", ?PORT, Request2), ReadResultACK = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"ack">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"ack">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }), ?assertEqual(ReadResultACK, test_recv_mqtt_response(RespTopic)), timer:sleep(100), @@ -703,21 +718,21 @@ case10_read_separate_ack(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/0">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/0">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case11_read_object_tlv(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -728,16 +743,16 @@ case11_read_object_tlv(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 207, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -754,31 +769,31 @@ case11_read_object_tlv(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0">>, - <<"content">> => [ - #{ - path => <<"/3/0/0">>, - value => <<"Open Mobile Alliance">> - }, - #{ - path => <<"/3/0/1">>, - value => <<"Lightweight M2M Client">> - }, - #{ - path => <<"/3/0/2">>, - value => <<"345000123">> - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0">>, + <<"content">> => [ + #{ + path => <<"/3/0/0">>, + value => <<"Open Mobile Alliance">> + }, + #{ + path => <<"/3/0/1">>, + value => <<"Lightweight M2M Client">> + }, + #{ + path => <<"/3/0/2">>, + value => <<"345000123">> + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case11_read_object_json(Config) -> - % step 1, device register ... + %% step 1, device register ... UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -790,16 +805,16 @@ case11_read_object_json(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -816,31 +831,31 @@ case11_read_object_json(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0">>, - <<"content">> => [ - #{ - path => <<"/3/0/0">>, - value => <<"Open Mobile Alliance">> - }, - #{ - path => <<"/3/0/1">>, - value => <<"Lightweight M2M Client">> - }, - #{ - path => <<"/3/0/2">>, - value => <<"345000123">> - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0">>, + <<"content">> => [ + #{ + path => <<"/3/0/0">>, + value => <<"Open Mobile Alliance">> + }, + #{ + path => <<"/3/0/1">>, + value => <<"Lightweight M2M Client">> + }, + #{ + path => <<"/3/0/2">>, + value => <<"345000123">> + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case12_read_resource_opaque(Config) -> - % step 1, device register ... + %% step 1, device register ... UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -851,16 +866,16 @@ case12_read_resource_opaque(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/8">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/8">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -877,23 +892,23 @@ case12_read_resource_opaque(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/8">>, - <<"content">> => [ - #{ - path => <<"/3/0/8">>, - value => base64:encode(Opaque) - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/8">>, + <<"content">> => [ + #{ + path => <<"/3/0/8">>, + value => base64:encode(Opaque) + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case13_read_no_xml(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -904,16 +919,16 @@ case13_read_no_xml(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/9723/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/9723/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -929,17 +944,17 @@ case13_read_no_xml(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"reqPath">> => <<"/9723/0/0">>, - <<"code">> => <<"4.00">>, - <<"codeMsg">> => <<"bad_request">> - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"reqPath">> => <<"/9723/0/0">>, + <<"code">> => <<"4.00">>, + <<"codeMsg">> => <<"bad_request">> + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case20_single_write(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -950,16 +965,16 @@ case20_single_write(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"path">> => <<"/3/0/13">>, - <<"type">> => <<"Integer">>, - <<"value">> => <<"12345">> - } + <<"path">> => <<"/3/0/13">>, + <<"type">> => <<"Integer">>, + <<"value">> => <<"12345">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -977,18 +992,18 @@ case20_single_write(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/13">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/13">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case20_write(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -999,18 +1014,18 @@ case20_write(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/13">>, - <<"content">> => [#{ - type => <<"Float">>, - value => <<"12345.0">> - }] - } + <<"basePath">> => <<"/3/0/13">>, + <<"content">> => [#{ + type => <<"Float">>, + value => <<"12345.0">> + }] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1028,18 +1043,18 @@ case20_write(Config) -> timer:sleep(100), WriteResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/13">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/13">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(WriteResult, test_recv_mqtt_response(RespTopic)). case21_write_object(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1050,23 +1065,23 @@ case21_write_object(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/">>, - <<"content">> => [#{ - path => <<"13">>, - type => <<"Integer">>, - value => <<"12345">> - },#{ - path => <<"14">>, - type => <<"String">>, - value => <<"87x">> - }] - } + <<"basePath">> => <<"/3/0/">>, + <<"content">> => [#{ + path => <<"13">>, + type => <<"Integer">>, + value => <<"12345">> + },#{ + path => <<"14">>, + type => <<"String">>, + value => <<"87x">> + }] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1086,18 +1101,18 @@ case21_write_object(Config) -> ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"write">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"write">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case22_write_error(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1108,20 +1123,20 @@ case22_write_error(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/1">>, - <<"content">> => [ - #{ - type => <<"Integer">>, - value => <<"12345">> - } - ] - } + <<"basePath">> => <<"/3/0/1">>, + <<"content">> => [ + #{ + type => <<"Integer">>, + value => <<"12345">> + } + ] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1137,18 +1152,18 @@ case22_write_error(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/1">>, - <<"code">> => <<"4.00">>, - <<"codeMsg">> => <<"bad_request">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/1">>, + <<"code">> => <<"4.00">>, + <<"codeMsg">> => <<"bad_request">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case_create_basic(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1159,15 +1174,14 @@ case_create_basic(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a CREATE command to device + %% step2, send a CREATE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"create">>, - <<"data">> => #{ - <<"path">> => <<"/5">> - } - }, + Command = #{<<"msgType">> => <<"create">>, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{<<"content">> => [], + <<"basePath">> => <<"/5">> + }}, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1183,18 +1197,18 @@ case_create_basic(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/5">>, - <<"code">> => <<"2.01">>, - <<"codeMsg">> => <<"created">> - }, - <<"msgType">> => <<"create">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/5">>, + <<"code">> => <<"2.01">>, + <<"codeMsg">> => <<"created">> + }, + <<"msgType">> => <<"create">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case_delete_basic(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1205,14 +1219,14 @@ case_delete_basic(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a CREATE command to device + %% step2, send a CREATE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"delete">>, <<"data">> => #{ - <<"path">> => <<"/5/0">> - } + <<"path">> => <<"/5/0">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1229,18 +1243,18 @@ case_delete_basic(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/5/0">>, - <<"code">> => <<"2.02">>, - <<"codeMsg">> => <<"deleted">> - }, - <<"msgType">> => <<"delete">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/5/0">>, + <<"code">> => <<"2.02">>, + <<"codeMsg">> => <<"deleted">> + }, + <<"msgType">> => <<"delete">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case30_execute(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1251,16 +1265,16 @@ case30_execute(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"execute">>, <<"data">> => #{ - <<"path">> => <<"/3/0/4">>, - %% "args" should not be present for "/3/0/4", only for testing the encoding here - <<"args">> => <<"2,7">> - } + <<"path">> => <<"/3/0/4">>, + %% "args" should not be present for "/3/0/4", only for testing the encoding here + <<"args">> => <<"2,7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1277,18 +1291,18 @@ case30_execute(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/4">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"execute">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/4">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"execute">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case31_execute_error(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1299,15 +1313,15 @@ case31_execute_error(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"execute">>, <<"data">> => #{ - <<"path">> => <<"/3/0/4">>, - <<"args">> => <<"2,7">> - } + <<"path">> => <<"/3/0/4">>, + <<"args">> => <<"2,7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1324,18 +1338,18 @@ case31_execute_error(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/4">>, - <<"code">> => <<"4.01">>, - <<"codeMsg">> => <<"uauthorized">> - }, - <<"msgType">> => <<"execute">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/4">>, + <<"code">> => <<"4.01">>, + <<"codeMsg">> => <<"unauthorized">> + }, + <<"msgType">> => <<"execute">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case40_discover(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1346,14 +1360,14 @@ case40_discover(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"discover">>, <<"data">> => #{ - <<"path">> => <<"/3/0/7">> - } }, + <<"path">> => <<"/3/0/7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1376,20 +1390,20 @@ case40_discover(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"discover">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/7">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => - [<<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2">>, <<"">>] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"discover">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/7">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => + [<<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2">>, <<"">>] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case50_write_attribute(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1400,17 +1414,17 @@ case50_write_attribute(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write-attr">>, <<"data">> => #{ - <<"path">> => <<"/3/0/9">>, - <<"pmin">> => <<"1">>, - <<"pmax">> => <<"5">>, - <<"lt">> => <<"5">> - } }, + <<"path">> => <<"/3/0/9">>, + <<"pmin">> => <<"1">>, + <<"pmax">> => <<"5">>, + <<"lt">> => <<"5">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(100), @@ -1435,18 +1449,18 @@ case50_write_attribute(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/9">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write-attr">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/9">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write-attr">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case60_observe(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1459,15 +1473,15 @@ case60_observe(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a OBSERVE command to device + %% step2, send a OBSERVE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"observe">>, <<"data">> => #{ - <<"path">> => <<"/3/0/10">> - } - }, + <<"path">> => <<"/3/0/10">> + } + }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1490,18 +1504,18 @@ case60_observe(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"observe">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 2048 - }] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"observe">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 2048 + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), %% step3 the notifications @@ -1517,29 +1531,29 @@ case60_observe(Config) -> #coap_message{} = test_recv_coap_response(UdpSock), ReadResult2 = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"notify">>, - <<"seqNum">> => ObSeq, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 4096 - }] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"notify">>, + <<"seqNum">> => ObSeq, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 4096 + }] + } + }), ?assertEqual(ReadResult2, test_recv_mqtt_response(RespTopicAD)), %% Step3. cancel observe CmdId3 = 308, Command3 = #{<<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, - <<"msgType">> => <<"cancel-observe">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/10">> - } - }, + <<"msgType">> => <<"cancel-observe">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/10">> + } + }, CommandJson3 = emqx_json:encode(Command3), test_mqtt_broker:publish(CommandTopic, CommandJson3, 0), timer:sleep(50), @@ -1562,143 +1576,143 @@ case60_observe(Config) -> timer:sleep(100), ReadResult3 = emqx_json:encode(#{ - <<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, - <<"msgType">> => <<"cancel-observe">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 1150 - }] - } - }), + <<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, + <<"msgType">> => <<"cancel-observe">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 1150 + }] + } + }), ?assertEqual(ReadResult3, test_recv_mqtt_response(RespTopic)). -case80_specail_object_19_0_0_notify(Config) -> - % step 1, device register, with extra register options - Epn = "urn:oma:lwm2m:oma:3", - RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", - MsgId1 = 15, - UdpSock = ?config(sock, Config), +%% case80_specail_object_19_0_0_notify(Config) -> +%% %% step 1, device register, with extra register options +%% Epn = "urn:oma:lwm2m:oma:3", +%% RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", +%% MsgId1 = 15, +%% UdpSock = ?config(sock, Config), - RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), - emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), - timer:sleep(200), +%% RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), +%% emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), +%% timer:sleep(200), - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId1), - #coap_message{method = Method1} = test_recv_coap_response(UdpSock), - ?assertEqual({ok,created}, Method1), - ReadResult = emqx_json:encode(#{ - <<"msgType">> => <<"register">>, - <<"data">> => #{ - <<"alternatePath">> => <<"/">>, - <<"ep">> => list_to_binary(Epn), - <<"lt">> => 345, - <<"lwm2m">> => <<"1">>, - <<"objectList">> => [<<"/1">>, <<"/2">>, <<"/3">>, <<"/4">>, <<"/5">>], - <<"apn">> => <<"psmA.eDRX0.ctnb">>, - <<"im">> => <<"13456">>, - <<"ct">> => <<"2.0">>, - <<"mt">> => <<"MDM9206">>, - <<"mv">> => <<"4.0">> - } - }), - ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId1), +%% #coap_message{method = Method1} = test_recv_coap_response(UdpSock), +%% ?assertEqual({ok,created}, Method1), +%% ReadResult = emqx_json:encode(#{ +%% <<"msgType">> => <<"register">>, +%% <<"data">> => #{ +%% <<"alternatePath">> => <<"/">>, +%% <<"ep">> => list_to_binary(Epn), +%% <<"lt">> => 345, +%% <<"lwm2m">> => <<"1">>, +%% <<"objectList">> => [<<"/1">>, <<"/2">>, <<"/3">>, <<"/4">>, <<"/5">>], +%% <<"apn">> => <<"psmA.eDRX0.ctnb">>, +%% <<"im">> => <<"13456">>, +%% <<"ct">> => <<"2.0">>, +%% <<"mt">> => <<"MDM9206">>, +%% <<"mv">> => <<"4.0">> +%% } +%% }), +%% ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), - % step2, send a OBSERVE command to device - CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, - CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"observe">>, - <<"data">> => #{ - <<"path">> => <<"/19/0/0">> - } - }, - CommandJson = emqx_json:encode(Command), - test_mqtt_broker:publish(CommandTopic, CommandJson, 0), - timer:sleep(50), - Request2 = test_recv_coap_request(UdpSock), - #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, - Path2 = get_coap_path(Options2), - Observe = get_coap_observe(Options2), - ?assertEqual(get, Method2), - ?assertEqual(<<"/19/0/0">>, Path2), - ?assertEqual(Observe, 0), - ?assertEqual(<<>>, Payload2), - timer:sleep(50), +%% %% step2, send a OBSERVE command to device +%% CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, +%% CmdId = 307, +%% Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"msgType">> => <<"observe">>, +%% <<"data">> => #{ +%% <<"path">> => <<"/19/0/0">> +%% } +%% }, +%% CommandJson = emqx_json:encode(Command), +%% test_mqtt_broker:publish(CommandTopic, CommandJson, 0), +%% timer:sleep(50), +%% Request2 = test_recv_coap_request(UdpSock), +%% #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, +%% Path2 = get_coap_path(Options2), +%% Observe = get_coap_observe(Options2), +%% ?assertEqual(get, Method2), +%% ?assertEqual(<<"/19/0/0">>, Path2), +%% ?assertEqual(Observe, 0), +%% ?assertEqual(<<>>, Payload2), +%% timer:sleep(50), - test_send_coap_observe_ack( UdpSock, - "127.0.0.1", - ?PORT, - {ok, content}, - #coap_content{content_format = <<"text/plain">>, payload = <<"2048">>}, - Request2), - timer:sleep(100). +%% test_send_coap_observe_ack( UdpSock, +%% "127.0.0.1", +%% ?PORT, +%% {ok, content}, +%% #coap_content{content_format = <<"text/plain">>, payload = <<"2048">>}, +%% Request2), +%% timer:sleep(100). - %% step 3, device send uplink data notifications +%% step 3, device send uplink data notifications -case80_specail_object_19_1_0_write(Config) -> - Epn = "urn:oma:lwm2m:oma:3", - RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", - MsgId1 = 15, - UdpSock = ?config(sock, Config), - RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), - emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), - timer:sleep(200), +%% case80_specail_object_19_1_0_write(Config) -> +%% Epn = "urn:oma:lwm2m:oma:3", +%% RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", +%% MsgId1 = 15, +%% UdpSock = ?config(sock, Config), +%% RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), +%% emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), +%% timer:sleep(200), - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId1), - #coap_message{method = Method1} = test_recv_coap_response(UdpSock), - ?assertEqual({ok,created}, Method1), - test_recv_mqtt_response(RespTopic), +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId1), +%% #coap_message{method = Method1} = test_recv_coap_response(UdpSock), +%% ?assertEqual({ok,created}, Method1), +%% test_recv_mqtt_response(RespTopic), - % step2, send a WRITE command to device - CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, - CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"write">>, - <<"data">> => #{ - <<"path">> => <<"/19/1/0">>, - <<"type">> => <<"Opaque">>, - <<"value">> => base64:encode(<<12345:32>>) - } - }, +%% %% step2, send a WRITE command to device +%% CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, +%% CmdId = 307, +%% Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"msgType">> => <<"write">>, +%% <<"data">> => #{ +%% <<"path">> => <<"/19/1/0">>, +%% <<"type">> => <<"Opaque">>, +%% <<"value">> => base64:encode(<<12345:32>>) +%% } +%% }, - CommandJson = emqx_json:encode(Command), - test_mqtt_broker:publish(CommandTopic, CommandJson, 0), - timer:sleep(50), - Request2 = test_recv_coap_request(UdpSock), - #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, - Path2 = get_coap_path(Options2), - ?assertEqual(put, Method2), - ?assertEqual(<<"/19/1/0">>, Path2), - ?assertEqual(<<3:2, 0:1, 0:2, 4:3, 0, 12345:32>>, Payload2), - timer:sleep(50), +%% CommandJson = emqx_json:encode(Command), +%% test_mqtt_broker:publish(CommandTopic, CommandJson, 0), +%% timer:sleep(50), +%% Request2 = test_recv_coap_request(UdpSock), +%% #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, +%% Path2 = get_coap_path(Options2), +%% ?assertEqual(put, Method2), +%% ?assertEqual(<<"/19/1/0">>, Path2), +%% ?assertEqual(<<3:2, 0:1, 0:2, 4:3, 0, 12345:32>>, Payload2), +%% timer:sleep(50), - test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, changed}, #coap_content{}, Request2, true), - timer:sleep(100), +%% test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, changed}, #coap_content{}, Request2, true), +%% timer:sleep(100), - ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/19/1/0">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), - ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). +%% ReadResult = emqx_json:encode(#{ +%% <<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"data">> => #{ +%% <<"reqPath">> => <<"/19/1/0">>, +%% <<"code">> => <<"2.04">>, +%% <<"codeMsg">> => <<"changed">> +%% }, +%% <<"msgType">> => <<"write">> +%% }), +%% ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case90_psm_mode(Config) -> server_cache_mode(Config, "ep=~s<=345&lwm2m=1&apn=psmA.eDRX0.ctnb"). @@ -1707,9 +1721,10 @@ case90_queue_mode(Config) -> server_cache_mode(Config, "ep=~s<=345&lwm2m=1&b=UQ"). server_cache_mode(Config, RegOption) -> - application:set_env(?APP, qmode_time_window, 2), - - % step 1, device register, with apn indicates "PSM" mode + #{lwm2m := LwM2M} = Gateway = emqx:get_config([gateway]), + Gateway2 = Gateway#{lwm2m := LwM2M#{qmode_time_window => 2}}, + emqx_config:put([gateway], Gateway2), + %% step 1, device register, with apn indicates "PSM" mode Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -1735,7 +1750,7 @@ server_cache_mode(Config, RegOption) -> verify_read_response_1(0, UdpSock), %% server inters into PSM mode - timer:sleep(2), + timer:sleep(2500), %% verify server caches downlink commands send_read_command_1(1, UdpSock), @@ -1758,12 +1773,12 @@ send_read_command_1(CmdId, _UdpSock) -> Epn = "urn:oma:lwm2m:oma:3", CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50). @@ -1780,16 +1795,17 @@ verify_read_response_1(CmdId, UdpSock) -> test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, content}, #coap_content{content_format = <<"text/plain">>, payload = <<"EMQ">>}, Request, true), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/0">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). device_update_1(UdpSock, Location) -> diff --git a/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl new file mode 100644 index 000000000..cb2ccf3f8 --- /dev/null +++ b/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl @@ -0,0 +1,317 @@ +%%-------------------------------------------------------------------- +%% Copyright (C) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_lwm2m_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-define(PORT, 5783). + +-define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). + +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("lwm2m_coap/include/coap.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<" +gateway.lwm2m { + xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" + lifetime_min = 1s + lifetime_max = 86400s + qmode_time_windonw = 22 + auto_observe = false + mountpoint = \"lwm2m/%u\" + update_msg_publish_condition = contains_object_list + translators { + command = {topic = \"/dn/#\", qos = 0} + response = {topic = \"/up/resp\", qos = 0} + notify = {topic = \"/up/notify\", qos = 0} + register = {topic = \"/up/resp\", qos = 0} + update = {topic = \"/up/resp\", qos = 0} + } + listeners.udp.default { + bind = 5783 + } +} +">>). + +-define(assertExists(Map, Key), + ?assertNotEqual(maps:get(Key, Map, undefined), undefined)). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + emqx_mgmt_api_test_util:init_suite([emqx_gateway]), + Config. + +end_per_suite(Config) -> + timer:sleep(300), + emqx_mgmt_api_test_util:end_suite([emqx_gateway]), + Config. + +init_per_testcase(_AllTestCase, Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + {ok, _} = application:ensure_all_started(emqx_gateway), + {ok, ClientUdpSock} = gen_udp:open(0, [binary, {active, false}]), + + {ok, C} = emqtt:start_link([{host, "localhost"},{port, 1883},{clientid, <<"c1">>}]), + {ok, _} = emqtt:connect(C), + timer:sleep(100), + + [{sock, ClientUdpSock}, {emqx_c, C} | Config]. + +end_per_testcase(_AllTestCase, Config) -> + timer:sleep(300), + gen_udp:close(?config(sock, Config)), + emqtt:disconnect(?config(emqx_c, Config)), + ok = application:stop(emqx_gateway). + +%%-------------------------------------------------------------------- +%% Cases +%%-------------------------------------------------------------------- +t_lookup_cmd_read(Config) -> + UdpSock = ?config(sock, Config), + Epn = "urn:oma:lwm2m:oma:3", + MsgId1 = 15, + RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), + emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), + timer:sleep(200), + %% step 1, device register ... + test_send_coap_request( UdpSock, + post, + sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), + #coap_content{content_format = <<"text/plain">>, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + [], + MsgId1), + #coap_message{method = Method1} = test_recv_coap_response(UdpSock), + ?assertEqual({ok,created}, Method1), + test_recv_mqtt_response(RespTopic), + + %% step2, send a READ command to device + CmdId = 206, + CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, + Command = #{ + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, + CommandJson = emqx_json:encode(Command), + ?LOGT("CommandJson=~p", [CommandJson]), + test_mqtt_broker:publish(CommandTopic, CommandJson, 0), + timer:sleep(50), + + no_received_request(Epn, <<"/3/0/0">>, <<"read">>), + + Request2 = test_recv_coap_request(UdpSock), + ?LOGT("LwM2M client got ~p", [Request2]), + timer:sleep(50), + + test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, content}, #coap_content{content_format = <<"text/plain">>, payload = <<"EMQ">>}, Request2, true), + timer:sleep(100), + + normal_received_request(Epn, <<"/3/0/0">>, <<"read">>). + +t_lookup_cmd_discover(Config) -> + %% step 1, device register ... + Epn = "urn:oma:lwm2m:oma:3", + MsgId1 = 15, + UdpSock = ?config(sock, Config), + ObjectList = <<", , , , ">>, + RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), + emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), + timer:sleep(200), + + std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), + + %% step2, send a WRITE command to device + CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, + CmdId = 307, + Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"discover">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/7">> + } }, + CommandJson = emqx_json:encode(Command), + test_mqtt_broker:publish(CommandTopic, CommandJson, 0), + + no_received_request(Epn, <<"/3/0/7">>, <<"discover">>), + + timer:sleep(50), + Request2 = test_recv_coap_request(UdpSock), + timer:sleep(50), + + PayloadDiscover = <<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2,">>, + test_send_coap_response(UdpSock, + "127.0.0.1", + ?PORT, + {ok, content}, + #coap_content{content_format = <<"application/link-format">>, payload = PayloadDiscover}, + Request2, + true), + timer:sleep(100), + discover_received_request(Epn, <<"/3/0/7">>, <<"discover">>). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Internal Functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +send_request(ClientId, Path, Action) -> + ApiPath = emqx_mgmt_api_test_util:api_path(["gateway/lwm2m", ClientId, "lookup_cmd"]), + Auth = emqx_mgmt_api_test_util:auth_header_(), + Query = io_lib:format("path=~s&action=~s", [Path, Action]), + {ok, Response} = emqx_mgmt_api_test_util:request_api(get, ApiPath, Query, Auth), + ?LOGT("rest api response:~s~n", [Response]), + Response. + +no_received_request(ClientId, Path, Action) -> + Response = send_request(ClientId, Path, Action), + NotReceived = #{<<"clientid">> => list_to_binary(ClientId), + <<"action">> => Action, + <<"code">> => <<"6.01">>, + <<"codeMsg">> => <<"reply_not_received">>, + <<"path">> => Path}, + ?assertEqual(NotReceived, emqx_json:decode(Response, [return_maps])). +normal_received_request(ClientId, Path, Action) -> + Response = send_request(ClientId, Path, Action), + RCont = emqx_json:decode(Response, [return_maps]), + ?assertEqual(list_to_binary(ClientId), maps:get(<<"clientid">>, RCont, undefined)), + ?assertEqual(Path, maps:get(<<"path">>, RCont, undefined)), + ?assertEqual(Action, maps:get(<<"action">>, RCont, undefined)), + ?assertExists(RCont, <<"code">>), + ?assertExists(RCont, <<"codeMsg">>), + ?assertExists(RCont, <<"content">>), + RCont. + +discover_received_request(ClientId, Path, Action) -> + RCont = normal_received_request(ClientId, Path, Action), + [Res | _] = maps:get(<<"content">>, RCont), + ?assertExists(Res, <<"path">>), + ?assertExists(Res, <<"name">>), + ?assertExists(Res, <<"operations">>). + +test_recv_mqtt_response(RespTopic) -> + receive + {publish, #{topic := RespTopic, payload := RM}} -> + ?LOGT("test_recv_mqtt_response Response=~p", [RM]), + RM + after 1000 -> timeout_test_recv_mqtt_response + end. + +test_send_coap_request(UdpSock, Method, Uri, Content, Options, MsgId) -> + is_record(Content, coap_content) orelse error("Content must be a #coap_content!"), + is_list(Options) orelse error("Options must be a list"), + case resolve_uri(Uri) of + {coap, {IpAddr, Port}, Path, Query} -> + Request0 = lwm2m_coap_message:request(con, Method, Content, [{uri_path, Path}, {uri_query, Query} | Options]), + Request = Request0#coap_message{id = MsgId}, + ?LOGT("send_coap_request Request=~p", [Request]), + RequestBinary = lwm2m_coap_message_parser:encode(Request), + ?LOGT("test udp socket send to ~p:~p, data=~p", [IpAddr, Port, RequestBinary]), + ok = gen_udp:send(UdpSock, IpAddr, Port, RequestBinary); + {SchemeDiff, ChIdDiff, _, _} -> + error(lists:flatten(io_lib:format("scheme ~s or ChId ~s does not match with socket", [SchemeDiff, ChIdDiff]))) + end. + +test_recv_coap_response(UdpSock) -> + {ok, {Address, Port, Packet}} = gen_udp:recv(UdpSock, 0, 2000), + Response = lwm2m_coap_message_parser:decode(Packet), + ?LOGT("test udp receive from ~p:~p, data1=~p, Response=~p", [Address, Port, Packet, Response]), + #coap_message{type = ack, method = Method, id=Id, token = Token, options = Options, payload = Payload} = Response, + ?LOGT("receive coap response Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Response. + +test_recv_coap_request(UdpSock) -> + case gen_udp:recv(UdpSock, 0, 2000) of + {ok, {_Address, _Port, Packet}} -> + Request = lwm2m_coap_message_parser:decode(Packet), + #coap_message{type = con, method = Method, id=Id, token = Token, payload = Payload, options = Options} = Request, + ?LOGT("receive coap request Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Request; + {error, Reason} -> + ?LOGT("test_recv_coap_request failed, Reason=~p", [Reason]), + timeout_test_recv_coap_request + end. + +test_send_coap_response(UdpSock, Host, Port, Code, Content, Request, Ack) -> + is_record(Content, coap_content) orelse error("Content must be a #coap_content!"), + is_list(Host) orelse error("Host is not a string"), + + {ok, IpAddr} = inet:getaddr(Host, inet), + Response = lwm2m_coap_message:response(Code, Content, Request), + Response2 = case Ack of + true -> Response#coap_message{type = ack}; + false -> Response + end, + ?LOGT("test_send_coap_response Response=~p", [Response2]), + ok = gen_udp:send(UdpSock, IpAddr, Port, lwm2m_coap_message_parser:encode(Response2)). + +std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic) -> + test_send_coap_request( UdpSock, + post, + sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), + #coap_content{content_format = <<"text/plain">>, payload = ObjectList}, + [], + MsgId1), + #coap_message{method = {ok,created}} = test_recv_coap_response(UdpSock), + test_recv_mqtt_response(RespTopic), + timer:sleep(100). + +resolve_uri(Uri) -> + {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)}. + +split_path([]) -> []; +split_path([$/]) -> []; +split_path([$/ | Path]) -> split_segments(Path, $/, []). + +split_query([]) -> []; +split_query(Path) -> split_segments(Path, $&, []). + +split_segments(Path, Char, Acc) -> + case string:rchr(Path, Char) of + 0 -> + [make_segment(Path) | Acc]; + N when N > 0 -> + split_segments(string:substr(Path, 1, N-1), Char, + [make_segment(string:substr(Path, N+1)) | Acc]) + end. + +make_segment(Seg) -> + list_to_binary(emqx_http_lib:uri_decode(Seg)). + +join_path([], Acc) -> Acc; +join_path([<<"/">>|T], Acc) -> + join_path(T, Acc); +join_path([H|T], Acc) -> + join_path(T, <>). + +sprintf(Format, Args) -> + lists:flatten(io_lib:format(Format, Args)). diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index 0c60d964f..23fb691d9 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -51,29 +51,28 @@ -define(CLIENTID, iolist_to_binary([atom_to_list(?FUNCTION_NAME), "-", integer_to_list(erlang:system_time())])). --define(CONF_DEFAULT, <<""" -gateway: { - mqttsn.1: { - gateway_id: 1 - broadcast: true - enable_stats: true - enable_qos3: true - predefined: [ - {id: 1, topic: \"/predefined/topic/name/hello\"}, - {id: 2, topic: \"/predefined/topic/name/nice\"} - ] - clientinfo_override: { - username: \"user1\" - password: \"pw123\" - } - listener.udp.1: { - bind: 1884 - max_connections: 10240000 - max_conn_rate: 1000 - } +-define(CONF_DEFAULT, <<" +gateway.mqttsn { + gateway_id = 1 + broadcast = true + enable_qos3 = true + predefined = [ + { id = 1, + topic = \"/predefined/topic/name/hello\" + }, + { id = 2, + topic = \"/predefined/topic/name/nice\" } + ] + clientinfo_override { + username = \"user1\" + password = \"pw123\" + } + listeners.udp.default { + bind = 1884 + } } -""">>). +">>). %%-------------------------------------------------------------------- %% Setups @@ -90,35 +89,6 @@ init_per_suite(Config) -> end_per_suite(_) -> emqx_ct_helpers:stop_apps([emqx_gateway]). -set_special_confs(emqx_gateway) -> - emqx_config:put( - [gateway], - #{ mqttsn => - #{'1' => - #{broadcast => true, - clientinfo_override => - #{password => "pw123", - username => "user1" - }, - enable_qos3 => true, - enable_stats => true, - gateway_id => 1, - idle_timeout => 30000, - listener => - #{udp => - #{'1' => - #{acceptors => 8,active_n => 100,backlog => 1024,bind => 1884, - high_watermark => 1048576,max_conn_rate => 1000, - max_connections => 10240000,send_timeout => 15000, - send_timeout_close => true}}}, - predefined => - [#{id => ?PREDEF_TOPIC_ID1, topic => ?PREDEF_TOPIC_NAME1}, - #{id => ?PREDEF_TOPIC_ID2, topic => ?PREDEF_TOPIC_NAME2}]}} - }); - -set_special_confs(_App) -> - ok. - %%-------------------------------------------------------------------- %% Test cases %%-------------------------------------------------------------------- @@ -127,7 +97,7 @@ set_special_confs(_App) -> %% Connect t_connect(_) -> - SockName = {'mqttsn#1:udp', 1884}, + SockName = {'mqttsn:udp:default', 1884}, ?assertEqual(true, lists:keymember(SockName, 1, esockd:listeners())), {ok, Socket} = gen_udp:open(0, [binary]), @@ -1024,7 +994,7 @@ t_will_case06(_) -> receive {deliver, WillTopic, #message{payload = WillMsg}} -> ok; - Msg -> ct:print("recevived --- unex: ~p", [Msg]) + Msg -> ct:print("received --- unex: ~p", [Msg]) after 1000 -> ct:fail(wait_willmsg_timeout) end, diff --git a/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl index 6161687f2..9aebfe791 100644 --- a/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl @@ -26,8 +26,6 @@ -define(PREDEF_TOPICS, [#{id => 1, topic => <<"/predefined/topic/name/hello">>}, #{id => 2, topic => <<"/predefined/topic/name/nice">>}]). --define(INSTA_ID, 'mqttsn#1'). - %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- @@ -45,7 +43,7 @@ end_per_suite(_Config) -> ok. init_per_testcase(_TestCase, Config) -> - {ok, Pid} = ?REGISTRY:start_link(?INSTA_ID, ?PREDEF_TOPICS), + {ok, Pid} = ?REGISTRY:start_link('mqttsn', ?PREDEF_TOPICS), {Tab, Pid} = ?REGISTRY:lookup_name(Pid), [{reg, {Tab, Pid}} | Config]. diff --git a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl index b2e9bd84e..9c3f1090f 100644 --- a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -23,19 +23,17 @@ -define(HEARTBEAT, <<$\n>>). --define(CONF_DEFAULT, <<""" -gateway: { - stomp.1: { - clientinfo_override: { - username: \"${Packet.headers.login}\" - password: \"${Packet.headers.passcode}\" - } - listener.tcp.1: { - bind: 61613 - } - } +-define(CONF_DEFAULT, <<" +gateway.stomp { + clientinfo_override { + username = \"${Packet.headers.login}\" + password = \"${Packet.headers.passcode}\" + } + listeners.tcp.default { + bind = 61613 + } } -""">>). +">>). all() -> emqx_ct:all(?MODULE). diff --git a/apps/emqx_machine/etc/emqx_machine.conf b/apps/emqx_machine/etc/emqx_machine.conf index 989665f97..3ec09f2d4 100644 --- a/apps/emqx_machine/etc/emqx_machine.conf +++ b/apps/emqx_machine/etc/emqx_machine.conf @@ -11,35 +11,35 @@ node { ## @doc node.name ## ValueType: NodeName ## Default: emqx@127.0.0.1 - name: "emqx@127.0.0.1" + name = "emqx@127.0.0.1" ## Cookie for distributed node communication. ## ## @doc node.cookie ## ValueType: String ## Default: emqxsecretcookie - cookie: emqxsecretcookie + cookie = emqxsecretcookie ## Data dir for the node ## ## @doc node.data_dir ## ValueType: Folder ## Default: "{{ platform_data_dir }}/" - data_dir: "{{ platform_data_dir }}/" + data_dir = "{{ platform_data_dir }}/" ## Dir of crash dump file. ## ## @doc node.crash_dump_dir ## ValueType: Folder ## Default: "{{ platform_log_dir }}/" - crash_dump_dir: "{{ platform_log_dir }}/" + crash_dump_dir = "{{ platform_log_dir }}/" ## Global GC Interval. ## ## @doc node.global_gc_interval ## ValueType: Duration ## Default: 15m - global_gc_interval: 15m + global_gc_interval = 15m ## Sets the net_kernel tick time in seconds. ## Notice that all communicating nodes are to have the same @@ -50,7 +50,7 @@ node { ## @doc node.dist_net_ticktime ## ValueType: Number ## Default: 2m - dist_net_ticktime: 2m + dist_net_ticktime = 2m ## Sets the port range for the listener socket of a distributed ## Erlang node. @@ -63,7 +63,7 @@ node { ## ValueType: Integer ## Range: [1024,65535] ## Default: 6369 - dist_listen_min: 6369 + dist_listen_min = 6369 ## Sets the port range for the listener socket of a distributed ## Erlang node. @@ -76,7 +76,7 @@ node { ## ValueType: Integer ## Range: [1024,65535] ## Default: 6369 - dist_listen_max: 6369 + dist_listen_max = 6369 ## Sets the maximum depth of call stack back-traces in the exit ## reason element of 'EXIT' tuples. @@ -87,7 +87,30 @@ node { ## ValueType: Integer ## Range: [0,1024] ## Default: 23 - backtrace_depth: 23 + backtrace_depth = 23 + + cluster_call { + ## Time interval to retry after a failed call + ## + ## @doc node.cluster_call.retry_interval + ## ValueType: Duration + ## Default: 1s + retry_interval = 1s + ## Retain the maximum number of completed transactions (for queries) + ## + ## @doc node.cluster_call.max_history + ## ValueType: Integer + ## Range: [1, 500] + ## Default: 100 + max_history = 100 + ## Time interval to clear completed but stale transactions. + ## Ensure that the number of completed transactions is less than the max_history + ## + ## @doc node.cluster_call.cleanup_interval + ## ValueType: Duration + ## Default: 5m + cleanup_interval = 5m + } } @@ -100,14 +123,14 @@ cluster { ## @doc cluster.name ## ValueType: String ## Default: emqxcl - name: emqxcl + name = emqxcl ## Enable cluster autoheal from network partition. ## ## @doc cluster.autoheal ## ValueType: Boolean ## Default: true - autoheal: true + autoheal = true ## Autoclean down node. A down node will be removed from the cluster ## if this value > 0. @@ -115,7 +138,7 @@ cluster { ## @doc cluster.autoclean ## ValueType: Duration ## Default: 5m - autoclean: 5m + autoclean = 5m ## Node discovery strategy to join the cluster. ## @@ -129,7 +152,7 @@ cluster { ## - k8s: Kubernetes ## ## Default: manual - discovery_strategy: manual + discovery_strategy = manual ##---------------------------------------------------------------- ## Cluster using static node list @@ -140,7 +163,7 @@ cluster { ## @doc cluster.static.seeds ## ValueType: Array ## Default: [] - seeds: ["emqx1@127.0.0.1", "emqx2@127.0.0.1"] + seeds = ["emqx1@127.0.0.1", "emqx2@127.0.0.1"] } ##---------------------------------------------------------------- @@ -152,21 +175,21 @@ cluster { ## @doc cluster.mcast.addr ## ValueType: IPAddress ## Default: "239.192.0.1" - addr: "239.192.0.1" + addr = "239.192.0.1" ## Multicast Ports. ## ## @doc cluster.mcast.ports ## ValueType: Array ## Default: [4369, 4370] - ports: [4369, 4370] + ports = [4369, 4370] ## Multicast Iface. ## ## @doc cluster.mcast.iface ## ValueType: IPAddress ## Default: "0.0.0.0" - iface: "0.0.0.0" + iface = "0.0.0.0" ## Multicast Ttl. ## @@ -174,14 +197,14 @@ cluster { ## ValueType: Integer ## Range: [0,255] ## Default: 255 - ttl: 255 + ttl = 255 ## Multicast loop. ## ## @doc cluster.mcast.loop ## ValueType: Boolean ## Default: true - loop: true + loop = true } ##---------------------------------------------------------------- @@ -193,14 +216,14 @@ cluster { ## @doc cluster.dns.name ## ValueType: String ## Default: localhost - name: localhost + name = localhost ## The App name is used to build 'node.name' with IP address. ## ## @doc cluster.dns.app ## ValueType: String ## Default: emqx - app: emqx + app = emqx } ##---------------------------------------------------------------- @@ -212,7 +235,7 @@ cluster { ## @doc cluster.etcd.server ## ValueType: URL ## Required: true - server: "http://127.0.0.1:2379" + server = "http://127.0.0.1:2379" ## The prefix helps build nodes path in etcd. Each node in the cluster ## will create a path in etcd: v2/keys/// @@ -220,28 +243,28 @@ cluster { ## @doc cluster.etcd.prefix ## ValueType: String ## Default: emqxcl - prefix: emqxcl + prefix = emqxcl ## The TTL for node's path in etcd. ## ## @doc cluster.etcd.node_ttl ## ValueType: Duration ## Default: 1m - node_ttl: 1m + node_ttl = 1m ## Path to the file containing the user's private PEM-encoded key. ## ## @doc cluster.etcd.ssl.keyfile ## ValueType: File ## Default: "{{ platform_etc_dir }}/certs/key.pem" - ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" + ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ## Path to a file containing the user certificate. ## ## @doc cluster.etcd.ssl.certfile ## ValueType: File ## Default: "{{ platform_etc_dir }}/certs/cert.pem" - ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" + ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" ## Path to the file containing PEM-encoded CA certificates. The CA certificates ## are used during server authentication and when building the client certificate chain. @@ -249,7 +272,7 @@ cluster { ## @doc cluster.etcd.ssl.cacertfile ## ValueType: File ## Default: "{{ platform_etc_dir }}/certs/cacert.pem" - ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" } ##---------------------------------------------------------------- @@ -261,47 +284,47 @@ cluster { ## @doc cluster.k8s.apiserver ## ValueType: URL ## Required: true - apiserver: "http://10.110.111.204:8080" + apiserver = "http://10.110.111.204:8080" ## The service name helps lookup EMQ nodes in the cluster. ## ## @doc cluster.k8s.service_name ## ValueType: String ## Default: emqx - service_name: emqx + service_name = emqx ## The address type is used to extract host from k8s service. ## ## @doc cluster.k8s.address_type ## ValueType: ip | dns | hostname ## Default: ip - address_type: ip + address_type = ip ## The app name helps build 'node.name'. ## ## @doc cluster.k8s.app_name ## ValueType: String ## Default: emqx - app_name: emqx + app_name = emqx ## The suffix added to dns and hostname get from k8s service ## ## @doc cluster.k8s.suffix ## ValueType: String ## Default: "pod.local" - suffix: "pod.local" + suffix = "pod.local" ## Kubernetes Namespace ## ## @doc cluster.k8s.namespace ## ValueType: String ## Default: default - namespace: default + namespace = default } - db_backend: mnesia + db_backend = mnesia - rlog: { + rlog { # role: core # core_nodes: [] } @@ -312,45 +335,183 @@ cluster { ## Log ##================================================================== log { - ## The primary log level - ## - ## - all the log messages with levels lower than this level will - ## be dropped. - ## - all the log messages with levels higher than this level will - ## go into the log handlers. The handlers then decide to log it - ## out or drop it according to the level setting of the handler. - ## - ## Note: Only the messages with severity level higher than or - ## equal to this level will be logged. - ## - ## @doc log.primary_level - ## ValueType: debug | info | notice | warning | error | critical | alert | emergency - ## Default: warning - primary_level: warning - ##---------------------------------------------------------------- ## The console log handler send log messages to emqx console ##---------------------------------------------------------------- - ## Log to single line - ## @doc log.console_handler.enable - ## ValueType: Boolean - ## Default: false - console_handler.enable: false + console_handler { + ## Log to single line + ## @doc log.console_handler..enable + ## ValueType: Boolean + ## Default: false + enable = false - ## The log level of this handler - ## All the log messages with levels lower than this level will - ## be dropped. - ## - ## @doc log.console_handler.level - ## ValueType: debug | info | notice | warning | error | critical | alert | emergency - ## Default: warning - console_handler.level: warning + ## The log level of this handler + ## All the log messages with levels lower than this level will + ## be dropped. + ## + ## @doc log.console_handler..level + ## ValueType: debug | info | notice | warning | error | critical | alert | emergency + ## Default: warning + level = warning + + ## Timezone offset to display in logs + ## + ## @doc log.console_handler..time_offset + ## ValueType: system | utc | String + ## - "system" use system zone + ## - "utc" for Universal Coordinated Time (UTC) + ## - "+hh:mm" or "-hh:mm" for a specified offset + ## Default: system + time_offset = system + + ## Limits the total number of characters printed for each log event. + ## + ## @doc log.console_handler..chars_limit + ## ValueType: unlimited | Integer + ## Range: [0, +Inf) + ## Default: unlimited + chars_limit = unlimited + + ## Maximum depth for Erlang term log formatting + ## and Erlang process message queue inspection. + ## + ## @doc log.console_handler..max_depth + ## ValueType: unlimited | Integer + ## Default: 100 + max_depth = 100 + + ## Log formatter + ## @doc log.console_handler..formatter + ## ValueType: text | json + ## Default: text + formatter = text + + ## Log to single line + ## @doc log.console_handler..single_line + ## ValueType: Boolean + ## Default: true + single_line = true + + ## The max allowed queue length before switching to sync mode. + ## + ## Log overload protection parameter. If the message queue grows + ## larger than this value the handler switches from anync to sync mode. + ## + ## @doc log.console_handler..sync_mode_qlen + ## ValueType: Integer + ## Range: [0, ${log.console_handler..drop_mode_qlen}] + ## Default: 100 + sync_mode_qlen = 100 + + ## The max allowed queue length before switching to drop mode. + ## + ## Log overload protection parameter. When the message queue grows + ## larger than this threshold, the handler switches to a mode in which + ## it drops all new events that senders want to log. + ## + ## @doc log.console_handler..drop_mode_qlen + ## ValueType: Integer + ## Range: [${log.console_handler..sync_mode_qlen}, ${log.console_handler..flush_qlen}] + ## Default: 3000 + drop_mode_qlen = 3000 + + ## The max allowed queue length before switching to flush mode. + ## + ## Log overload protection parameter. If the length of the message queue + ## grows larger than this threshold, a flush (delete) operation takes place. + ## To flush events, the handler discards the messages in the message queue + ## by receiving them in a loop without logging. + ## + ## @doc log.console_handler..flush_qlen + ## ValueType: Integer + ## Range: [${log.console_handler..drop_mode_qlen}, infinity) + ## Default: 8000 + flush_qlen = 8000 + + ## Kill the log handler when it gets overloaded. + ## + ## Log overload protection parameter. It is possible that a handler, + ## even if it can successfully manage peaks of high load without crashing, + ## can build up a large message queue, or use a large amount of memory. + ## We could kill the log handler in these cases and restart it after a + ## few seconds. + ## + ## @doc log.console_handler..overload_kill.enable + ## ValueType: Boolean + ## Default: true + overload_kill.enable = true + + ## The max allowed queue length before killing the log hanlder. + ## + ## Log overload protection parameter. This is the maximum allowed queue + ## length. If the message queue grows larger than this, the handler + ## process is terminated. + ## + ## @doc log.console_handler..overload_kill.qlen + ## ValueType: Integer + ## Range: [0, 1048576] + ## Default: 20000 + overload_kill.qlen = 20000 + + ## The max allowed memory size before killing the log hanlder. + ## + ## Log overload protection parameter. This is the maximum memory size + ## that the handler process is allowed to use. If the handler grows + ## larger than this, the process is terminated. + ## + ## @doc log.console_handler..overload_kill.mem_size + ## ValueType: Size + ## Default: 30MB + overload_kill.mem_size = 30MB + + ## Restart the log hanlder after some seconds. + ## + ## Log overload protection parameter. If the handler is terminated, + ## it restarts automatically after a delay specified in seconds. + ## + ## @doc log.console_handler..overload_kill.restart_after + ## ValueType: Duration + ## Default: 5s + overload_kill.restart_after = 5s + + ## Controlling Bursts of Log Requests. + ## + ## Log overload protection parameter. Large bursts of log events - many + ## events received by the handler under a short period of time - can + ## potentially cause problems. By specifying the maximum number of events + ## to be handled within a certain time frame, the handler can avoid + ## choking the log with massive amounts of printouts. + ## + ## Note that there would be no warning if any messages were + ## dropped because of burst control. + ## + ## @doc log.console_handler..burst_limit.enable + ## ValueType: Boolean + ## Default: false + burst_limit.enable = false + + ## This config controls the maximum number of events to handle within + ## a time frame. After the limit is reached, successive events are + ## dropped until the end of the time frame defined by `window_time`. + ## + ## @doc log.console_handler..burst_limit.max_count + ## ValueType: Integer + ## Default: 10000 + burst_limit.max_count = 10000 + + ## See the previous description of burst_limit_max_count. + ## + ## @doc log.console_handler..burst_limit.window_time + ## ValueType: duration + ## Default: 1s + burst_limit.window_time = 1s + } ##---------------------------------------------------------------- ## The file log handlers send log messages to files ##---------------------------------------------------------------- ## file_handlers. - file_handlers.emqx_log: { + file_handlers.default { ## The log level filter of this handler ## All the log messages with levels lower than this level will ## be dropped. @@ -358,7 +519,7 @@ log { ## @doc log.file_handlers..level ## ValueType: debug | info | notice | warning | error | critical | alert | emergency ## Default: warning - level: warning + level = warning ## The log file for specified level. ## @@ -373,7 +534,7 @@ log { ## @doc log.file_handlers..file ## ValueType: File ## Required: true - file: "{{ platform_log_dir }}/emqx.log" + file = "{{ platform_log_dir }}/emqx.log" ## Enables the log rotation. ## With this enabled, new log files will be created when the current @@ -382,7 +543,7 @@ log { ## @doc log.file_handlers..rotation.enable ## ValueType: Boolean ## Default: true - rotation.enable: true + rotation.enable = true ## Maximum rotation count of log files. ## @@ -390,7 +551,7 @@ log { ## ValueType: Integer ## Range: [1, 2048] ## Default: 10 - rotation.count: 10 + rotation.count = 10 ## Maximum size of each log file. ## @@ -401,169 +562,160 @@ log { ## @doc log.file_handlers..max_size ## ValueType: Size | infinity ## Default: 10MB - max_size: 10MB + max_size = 10MB + + ## Timezone offset to display in logs + ## + ## @doc log.file_handlers..time_offset + ## ValueType: system | utc | String + ## - "system" use system zone + ## - "utc" for Universal Coordinated Time (UTC) + ## - "+hh:mm" or "-hh:mm" for a specified offset + ## Default: system + time_offset = system + + ## Limits the total number of characters printed for each log event. + ## + ## @doc log.file_handlers..chars_limit + ## ValueType: unlimited | Integer + ## Range: [0, +Inf) + ## Default: unlimited + chars_limit = unlimited + + ## Maximum depth for Erlang term log formatting + ## and Erlang process message queue inspection. + ## + ## @doc log.file_handlers..max_depth + ## ValueType: unlimited | Integer + ## Default: 100 + max_depth = 100 + + ## Log formatter + ## @doc log.file_handlers..formatter + ## ValueType: text | json + ## Default: text + formatter = text + + ## Log to single line + ## @doc log.file_handlers..single_line + ## ValueType: Boolean + ## Default: true + single_line = true + + ## The max allowed queue length before switching to sync mode. + ## + ## Log overload protection parameter. If the message queue grows + ## larger than this value the handler switches from anync to sync mode. + ## + ## @doc log.file_handlers..sync_mode_qlen + ## ValueType: Integer + ## Range: [0, ${log.file_handlers..drop_mode_qlen}] + ## Default: 100 + sync_mode_qlen = 100 + + ## The max allowed queue length before switching to drop mode. + ## + ## Log overload protection parameter. When the message queue grows + ## larger than this threshold, the handler switches to a mode in which + ## it drops all new events that senders want to log. + ## + ## @doc log.file_handlers..drop_mode_qlen + ## ValueType: Integer + ## Range: [${log.file_handlers..sync_mode_qlen}, ${log.file_handlers..flush_qlen}] + ## Default: 3000 + drop_mode_qlen = 3000 + + ## The max allowed queue length before switching to flush mode. + ## + ## Log overload protection parameter. If the length of the message queue + ## grows larger than this threshold, a flush (delete) operation takes place. + ## To flush events, the handler discards the messages in the message queue + ## by receiving them in a loop without logging. + ## + ## @doc log.file_handlers..flush_qlen + ## ValueType: Integer + ## Range: [${log.file_handlers..drop_mode_qlen}, infinity) + ## Default: 8000 + flush_qlen = 8000 + + ## Kill the log handler when it gets overloaded. + ## + ## Log overload protection parameter. It is possible that a handler, + ## even if it can successfully manage peaks of high load without crashing, + ## can build up a large message queue, or use a large amount of memory. + ## We could kill the log handler in these cases and restart it after a + ## few seconds. + ## + ## @doc log.file_handlers..overload_kill.enable + ## ValueType: Boolean + ## Default: true + overload_kill.enable = true + + ## The max allowed queue length before killing the log hanlder. + ## + ## Log overload protection parameter. This is the maximum allowed queue + ## length. If the message queue grows larger than this, the handler + ## process is terminated. + ## + ## @doc log.file_handlers..overload_kill.qlen + ## ValueType: Integer + ## Range: [0, 1048576] + ## Default: 20000 + overload_kill.qlen = 20000 + + ## The max allowed memory size before killing the log hanlder. + ## + ## Log overload protection parameter. This is the maximum memory size + ## that the handler process is allowed to use. If the handler grows + ## larger than this, the process is terminated. + ## + ## @doc log.file_handlers..overload_kill.mem_size + ## ValueType: Size + ## Default: 30MB + overload_kill.mem_size = 30MB + + ## Restart the log hanlder after some seconds. + ## + ## Log overload protection parameter. If the handler is terminated, + ## it restarts automatically after a delay specified in seconds. + ## + ## @doc log.file_handlers..overload_kill.restart_after + ## ValueType: Duration + ## Default: 5s + overload_kill.restart_after = 5s + + ## Controlling Bursts of Log Requests. + ## + ## Log overload protection parameter. Large bursts of log events - many + ## events received by the handler under a short period of time - can + ## potentially cause problems. By specifying the maximum number of events + ## to be handled within a certain time frame, the handler can avoid + ## choking the log with massive amounts of printouts. + ## + ## Note that there would be no warning if any messages were + ## dropped because of burst control. + ## + ## @doc log.file_handlers..burst_limit.enable + ## ValueType: Boolean + ## Default: false + burst_limit.enable = false + + ## This config controls the maximum number of events to handle within + ## a time frame. After the limit is reached, successive events are + ## dropped until the end of the time frame defined by `window_time`. + ## + ## @doc log.file_handlers..burst_limit.max_count + ## ValueType: Integer + ## Default: 10000 + burst_limit.max_count = 10000 + + ## See the previous description of burst_limit_max_count. + ## + ## @doc log.file_handlers..burst_limit.window_time + ## ValueType: duration + ## Default: 1s + burst_limit.window_time = 1s } - - ## file_handlers. - ## - ## You could also create multiple file handlers for different - ## log level for example: - file_handlers.emqx_error_log: { - level: error - file: "{{ platform_log_dir }}/error.log" - } - - ## Timezone offset to display in logs - ## - ## @doc log.time_offset - ## ValueType: system | utc | String - ## - "system" use system zone - ## - "utc" for Universal Coordinated Time (UTC) - ## - "+hh:mm" or "-hh:mm" for a specified offset - ## Default: system - time_offset: system - - ## Limits the total number of characters printed for each log event. - ## - ## @doc log.chars_limit - ## ValueType: unlimited | Integer - ## Range: [0, +Inf) - ## Default: unlimited - chars_limit: unlimited - - ## Maximum depth for Erlang term log formatting - ## and Erlang process message queue inspection. - ## - ## @doc log.max_depth - ## ValueType: unlimited | Integer - ## Default: 80 - max_depth: 80 - - ## Log formatter - ## @doc log.formatter - ## ValueType: text | json - ## Default: text - formatter: text - - ## Log to single line - ## @doc log.single_line - ## ValueType: Boolean - ## Default: true - single_line: true - - ## The max allowed queue length before switching to sync mode. - ## - ## Log overload protection parameter. If the message queue grows - ## larger than this value the handler switches from anync to sync mode. - ## - ## @doc log.sync_mode_qlen - ## ValueType: Integer - ## Range: [0, ${log.drop_mode_qlen}] - ## Default: 100 - sync_mode_qlen: 100 - - ## The max allowed queue length before switching to drop mode. - ## - ## Log overload protection parameter. When the message queue grows - ## larger than this threshold, the handler switches to a mode in which - ## it drops all new events that senders want to log. - ## - ## @doc log.drop_mode_qlen - ## ValueType: Integer - ## Range: [${log.sync_mode_qlen}, ${log.flush_qlen}] - ## Default: 3000 - drop_mode_qlen: 3000 - - ## The max allowed queue length before switching to flush mode. - ## - ## Log overload protection parameter. If the length of the message queue - ## grows larger than this threshold, a flush (delete) operation takes place. - ## To flush events, the handler discards the messages in the message queue - ## by receiving them in a loop without logging. - ## - ## @doc log.flush_qlen - ## ValueType: Integer - ## Range: [${log.drop_mode_qlen}, infinity) - ## Default: 8000 - flush_qlen: 8000 - - ## Kill the log handler when it gets overloaded. - ## - ## Log overload protection parameter. It is possible that a handler, - ## even if it can successfully manage peaks of high load without crashing, - ## can build up a large message queue, or use a large amount of memory. - ## We could kill the log handler in these cases and restart it after a - ## few seconds. - ## - ## @doc log.overload_kill.enable - ## ValueType: Boolean - ## Default: true - overload_kill.enable: true - - ## The max allowed queue length before killing the log hanlder. - ## - ## Log overload protection parameter. This is the maximum allowed queue - ## length. If the message queue grows larger than this, the handler - ## process is terminated. - ## - ## @doc log.overload_kill.qlen - ## ValueType: Integer - ## Range: [0, 1048576] - ## Default: 20000 - overload_kill.qlen: 20000 - - ## The max allowed memory size before killing the log hanlder. - ## - ## Log overload protection parameter. This is the maximum memory size - ## that the handler process is allowed to use. If the handler grows - ## larger than this, the process is terminated. - ## - ## @doc log.overload_kill.mem_size - ## ValueType: Size - ## Default: 30MB - overload_kill.mem_size: 30MB - - ## Restart the log hanlder after some seconds. - ## - ## Log overload protection parameter. If the handler is terminated, - ## it restarts automatically after a delay specified in seconds. - ## - ## @doc log.overload_kill.restart_after - ## ValueType: Duration - ## Default: 5s - overload_kill.restart_after: 5s - - ## Controlling Bursts of Log Requests. - ## - ## Log overload protection parameter. Large bursts of log events - many - ## events received by the handler under a short period of time - can - ## potentially cause problems. By specifying the maximum number of events - ## to be handled within a certain time frame, the handler can avoid - ## choking the log with massive amounts of printouts. - ## - ## Note that there would be no warning if any messages were - ## dropped because of burst control. - ## - ## @doc log.burst_limit.enable - ## ValueType: Boolean - ## Default: false - burst_limit.enable: false - - ## This config controls the maximum number of events to handle within - ## a time frame. After the limit is reached, successive events are - ## dropped until the end of the time frame defined by `window_time`. - ## - ## @doc log.burst_limit.max_count - ## ValueType: Integer - ## Default: 10000 - burst_limit.max_count: 10000 - - ## See the previous description of burst_limit_max_count. - ## - ## @doc log.burst_limit.window_time - ## ValueType: duration - ## Default: 1s - burst_limit.window_time: 1s } ##================================================================== @@ -575,7 +727,7 @@ rpc { ## @doc rpc.mode ## ValueType: sync | async ## Default: async - mode: async + mode = async ## Max batch size of async RPC requests. ## @@ -586,7 +738,7 @@ rpc { ## ValueType: Integer ## Range: [0, 1048576] ## Default: 0 - async_batch_size: 256 + async_batch_size = 256 ## RPC port discovery ## @@ -601,7 +753,7 @@ rpc { ## an integer, then the listening port will be `5370 + ` ## ## Default: `stateless`. - port_discovery: stateless + port_discovery = stateless ## TCP server port for RPC. ## @@ -611,7 +763,7 @@ rpc { ## ValueType: Integer ## Range: [1024-65535] ## Defaults: 5369 - tcp_server_port: 5369 + tcp_server_port = 5369 ## Number of outgoing RPC connections. ## @@ -622,75 +774,75 @@ rpc { ## ValueType: Integer ## Range: [1, 256] ## Defaults: 1 - tcp_client_num: 1 + tcp_client_num = 1 ## RCP Client connect timeout. ## ## @doc rpc.connect_timeout ## ValueType: Duration ## Default: 5s - connect_timeout: 5s + connect_timeout = 5s ## TCP send timeout of RPC client and server. ## ## @doc rpc.send_timeout ## ValueType: Duration ## Default: 5s - send_timeout: 5s + send_timeout = 5s ## Authentication timeout ## ## @doc rpc.authentication_timeout ## ValueType: Duration ## Default: 5s - authentication_timeout: 5s + authentication_timeout = 5s ## Default receive timeout for call() functions ## ## @doc rpc.call_receive_timeout ## ValueType: Duration ## Default: 15s - call_receive_timeout: 15s + call_receive_timeout = 15s ## Socket idle keepalive. ## ## @doc rpc.socket_keepalive_idle ## ValueType: Duration ## Default: 900s - socket_keepalive_idle: 900s + socket_keepalive_idle = 900s ## TCP Keepalive probes interval. ## ## @doc rpc.socket_keepalive_interval ## ValueType: Duration ## Default: 75s - socket_keepalive_interval: 75s + socket_keepalive_interval = 75s ## Probes lost to close the connection ## ## @doc rpc.socket_keepalive_count ## ValueType: Integer ## Default: 9 - socket_keepalive_count: 9 + socket_keepalive_count = 9 ## Size of TCP send buffer. ## ## @doc rpc.socket_sndbuf ## ValueType: Size ## Default: 1MB - socket_sndbuf: 1MB + socket_sndbuf = 1MB ## Size of TCP receive buffer. ## ## @doc rpc.socket_recbuf ## ValueType: Size ## Default: 1MB - socket_recbuf: 1MB + socket_recbuf = 1MB ## Size of user-level software socket buffer. ## ## @doc rpc.socket_buffer ## ValueType: Size ## Default: 1MB - socket_buffer: 1MB + socket_buffer = 1MB } diff --git a/apps/emqx_gateway/src/coap/emqx_coap_resource.erl b/apps/emqx_machine/include/emqx_cluster_rpc.hrl similarity index 53% rename from apps/emqx_gateway/src/coap/emqx_coap_resource.erl rename to apps/emqx_machine/include/emqx_cluster_rpc.hrl index 8383c23b0..5c04346b7 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_resource.erl +++ b/apps/emqx_machine/include/emqx_cluster_rpc.hrl @@ -14,24 +14,22 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_coap_resource). +-ifndef(EMQ_X_CLUSTER_RPC_HRL). +-define(EMQ_X_CLUSTER_RPC_HRL, true). --include("emqx_coap.hrl"). +-define(CLUSTER_MFA, cluster_rpc_mfa). +-define(CLUSTER_COMMIT, cluster_rpc_commit). --type context() :: any(). --type topic() :: binary(). --type token() :: token(). +-record(cluster_rpc_mfa, { + tnx_id :: pos_integer(), + mfa :: mfa(), + created_at :: calendar:datetime(), + initiator :: node() +}). --type register() :: {topic(), token()} - | topic() - | undefined. +-record(cluster_rpc_commit, { + node :: node(), + tnx_id :: pos_integer() +}). --type result() :: emqx_coap_message() - | {has_sub, emqx_coap_message(), register()}. - --callback init(hocon:confg()) -> context(). --callback stop(context()) -> ok. --callback get(emqx_coap_message(), hocon:config()) -> result(). --callback put(emqx_coap_message(), hocon:config()) -> result(). --callback post(emqx_coap_message(), hocon:config()) -> result(). --callback delete(emqx_coap_message(), hocon:config()) -> result(). +-endif. diff --git a/apps/emqx_machine/src/emqx_cluster_rpc.erl b/apps/emqx_machine/src/emqx_cluster_rpc.erl new file mode 100644 index 000000000..346ca5025 --- /dev/null +++ b/apps/emqx_machine/src/emqx_cluster_rpc.erl @@ -0,0 +1,298 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_cluster_rpc). +-behaviour(gen_server). + +%% API +-export([start_link/0, mnesia/1]). +-export([multicall/3, multicall/4, query/1, reset/0, status/0]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + handle_continue/2, code_change/3]). + +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-export([start_link/3]). +-endif. + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include("emqx_cluster_rpc.hrl"). + +-rlog_shard({?COMMON_SHARD, ?CLUSTER_MFA}). +-rlog_shard({?COMMON_SHARD, ?CLUSTER_COMMIT}). + +-define(CATCH_UP, catch_up). +-define(TIMEOUT, timer:minutes(1)). + +%%%=================================================================== +%%% API +%%%=================================================================== +mnesia(boot) -> + ok = ekka_mnesia:create_table(?CLUSTER_MFA, [ + {type, ordered_set}, + {rlog_shard, ?COMMON_SHARD}, + {disc_copies, [node()]}, + {record_name, cluster_rpc_mfa}, + {attributes, record_info(fields, cluster_rpc_mfa)}]), + ok = ekka_mnesia:create_table(?CLUSTER_COMMIT, [ + {type, set}, + {rlog_shard, ?COMMON_SHARD}, + {disc_copies, [node()]}, + {record_name, cluster_rpc_commit}, + {attributes, record_info(fields, cluster_rpc_commit)}]); +mnesia(copy) -> + ok = ekka_mnesia:copy_table(cluster_rpc_mfa, disc_copies), + ok = ekka_mnesia:copy_table(cluster_rpc_commit, disc_copies). + +start_link() -> + RetryMs = application:get_env(emqx_machine, cluster_call_retry_interval, 1000), + start_link(node(), ?MODULE, RetryMs). + +start_link(Node, Name, RetryMs) -> + gen_server:start_link({local, Name}, ?MODULE, [Node, RetryMs], []). + +-spec multicall(Module, Function, Args) -> {ok, TnxId, term()} | {error, Reason} when + Module :: module(), + Function :: atom(), + Args :: [term()], + TnxId :: pos_integer(), + Reason :: string(). +multicall(M, F, A) -> + multicall(M, F, A, timer:minutes(2)). + +-spec multicall(Module, Function, Args, Timeout) -> {ok, TnxId, term()} |{error, Reason} when + Module :: module(), + Function :: atom(), + Args :: [term()], + TnxId :: pos_integer(), + Timeout :: timeout(), + Reason :: string(). +multicall(M, F, A, Timeout) -> + MFA = {initiate, {M, F, A}}, + case ekka_rlog:role() of + core -> gen_server:call(?MODULE, MFA, Timeout); + replicant -> + %% the initiate transaction must happened on core node + %% make sure MFA(in the transaction) and the transaction on the same node + %% don't need rpc again inside transaction. + case ekka_rlog_status:upstream_node(?COMMON_SHARD) of + {ok, Node} -> gen_server:call({?MODULE, Node}, MFA, Timeout); + disconnected -> {error, disconnected} + end + end. + +-spec query(pos_integer()) -> {'atomic', map()} | {'aborted', Reason :: term()}. +query(TnxId) -> + transaction(fun trans_query/1, [TnxId]). + +-spec reset() -> reset. +reset() -> gen_server:call(?MODULE, reset). + +-spec status() -> {'atomic', [map()]} | {'aborted', Reason :: term()}. +status() -> + transaction(fun trans_status/0, []). + +%%%=================================================================== +%%% gen_statem callbacks +%%%=================================================================== + +%% @private +init([Node, RetryMs]) -> + {ok, _} = mnesia:subscribe({table, ?CLUSTER_MFA, simple}), + {ok, #{node => Node, retry_interval => RetryMs}, {continue, ?CATCH_UP}}. + +%% @private +handle_continue(?CATCH_UP, State) -> + {noreply, State, catch_up(State)}. + +handle_call(reset, _From, State) -> + _ = ekka_mnesia:clear_table(?CLUSTER_COMMIT), + _ = ekka_mnesia:clear_table(?CLUSTER_MFA), + {reply, ok, State, {continue, ?CATCH_UP}}; + +handle_call({initiate, MFA}, _From, State = #{node := Node}) -> + case transaction(fun init_mfa/2, [Node, MFA]) of + {atomic, {ok, TnxId, Result}} -> + {reply, {ok, TnxId, Result}, State, {continue, ?CATCH_UP}}; + {aborted, Reason} -> + {reply, {error, Reason}, State, {continue, ?CATCH_UP}} + end; +handle_call(_, _From, State) -> + {reply, ok, State, catch_up(State)}. + +handle_cast(_, State) -> + {noreply, State, catch_up(State)}. + +handle_info({mnesia_table_event, _}, State) -> + {noreply, State, catch_up(State)}; +handle_info(_, State) -> + {noreply, State, catch_up(State)}. + +terminate(_Reason, _Data) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +catch_up(#{node := Node, retry_interval := RetryMs} = State) -> + case transaction(fun read_next_mfa/1, [Node]) of + {atomic, caught_up} -> ?TIMEOUT; + {atomic, {still_lagging, NextId, MFA}} -> + {Succeed, _} = apply_mfa(NextId, MFA), + case Succeed of + true -> + case transaction(fun commit/2, [Node, NextId]) of + {atomic, ok} -> catch_up(State); + Error -> + ?SLOG(error, #{ + msg => "failed to commit applied call", + applied_id => NextId, + error => Error}), + RetryMs + end; + false -> RetryMs + end; + {aborted, Reason} -> + ?SLOG(error, #{msg => "read_next_mfa transaction failed", error => Reason}), + RetryMs + end. + +read_next_mfa(Node) -> + NextId = + case mnesia:wread({?CLUSTER_COMMIT, Node}) of + [] -> + LatestId = get_latest_id(), + TnxId = max(LatestId - 1, 0), + commit(Node, TnxId), + ?SLOG(notice, #{ + msg => "New node first catch up and start commit.", + node => Node, tnx_id => TnxId}), + TnxId; + [#cluster_rpc_commit{tnx_id = LastAppliedID}] -> LastAppliedID + 1 + end, + case mnesia:read(?CLUSTER_MFA, NextId) of + [] -> caught_up; + [#cluster_rpc_mfa{mfa = MFA}] -> {still_lagging, NextId, MFA} + end. + +do_catch_up(ToTnxId, Node) -> + case mnesia:wread({?CLUSTER_COMMIT, Node}) of + [] -> + commit(Node, ToTnxId), + caught_up; + [#cluster_rpc_commit{tnx_id = LastAppliedId}] when ToTnxId =:= LastAppliedId -> + caught_up; + [#cluster_rpc_commit{tnx_id = LastAppliedId}] when ToTnxId > LastAppliedId -> + CurTnxId = LastAppliedId + 1, + [#cluster_rpc_mfa{mfa = MFA}] = mnesia:read(?CLUSTER_MFA, CurTnxId), + case apply_mfa(CurTnxId, MFA) of + {true, _Result} -> ok = commit(Node, CurTnxId); + {false, Error} -> mnesia:abort(Error) + end; + [#cluster_rpc_commit{tnx_id = LastAppliedId}] -> + Reason = lists:flatten(io_lib:format("~p catch up failed by LastAppliedId(~p) > ToTnxId(~p)", + [Node, LastAppliedId, ToTnxId])), + ?SLOG(error, #{ + msg => "catch up failed!", + last_applied_id => LastAppliedId, + to_tnx_id => ToTnxId + }), + mnesia:abort(Reason) + end. + +commit(Node, TnxId) -> + ok = mnesia:write(?CLUSTER_COMMIT, #cluster_rpc_commit{node = Node, tnx_id = TnxId}, write). + +get_latest_id() -> + case mnesia:last(?CLUSTER_MFA) of + '$end_of_table' -> 0; + Id -> Id + end. + +init_mfa(Node, MFA) -> + mnesia:write_lock_table(?CLUSTER_MFA), + LatestId = get_latest_id(), + ok = do_catch_up_in_one_trans(LatestId, Node), + TnxId = LatestId + 1, + MFARec = #cluster_rpc_mfa{tnx_id = TnxId, mfa = MFA, initiator = Node, created_at = erlang:localtime()}, + ok = mnesia:write(?CLUSTER_MFA, MFARec, write), + ok = commit(Node, TnxId), + case apply_mfa(TnxId, MFA) of + {true, Result} -> {ok, TnxId, Result}; + {false, Error} -> mnesia:abort(Error) + end. + +do_catch_up_in_one_trans(LatestId, Node) -> + case do_catch_up(LatestId, Node) of + caught_up -> ok; + ok -> do_catch_up_in_one_trans(LatestId, Node) + end. + +transaction(Func, Args) -> + ekka_mnesia:transaction(?COMMON_SHARD, Func, Args). + +trans_status() -> + mnesia:foldl(fun(Rec, Acc) -> + #cluster_rpc_commit{node = Node, tnx_id = TnxId} = Rec, + case mnesia:read(?CLUSTER_MFA, TnxId) of + [MFARec] -> + #cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt} = MFARec, + [#{ + node => Node, + tnx_id => TnxId, + initiator => InitNode, + mfa => MFA, + created_at => CreatedAt + } | Acc]; + [] -> Acc + end end, [], ?CLUSTER_COMMIT). + +trans_query(TnxId) -> + case mnesia:read(?CLUSTER_MFA, TnxId) of + [] -> mnesia:abort(not_found); + [#cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt}] -> + #{tnx_id => TnxId, mfa => MFA, initiator => InitNode, created_at => CreatedAt} + end. + +apply_mfa(TnxId, {M, F, A} = MFA) -> + try + Res = erlang:apply(M, F, A), + Succeed = + case Res of + ok -> + ?SLOG(notice, #{msg => "succeeded to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}), + true; + {ok, _} -> + ?SLOG(notice, #{msg => "succeeded to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}), + true; + _ -> + ?SLOG(error, #{msg => "failed to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}), + false + end, + {Succeed, Res} + catch + C : E -> + ?SLOG(critical, #{msg => "crash to apply MFA", tnx_id => TnxId, mfa => MFA, exception => C, reason => E}), + {false, lists:flatten(io_lib:format("TnxId(~p) apply MFA(~p) crash", [TnxId, MFA]))} + end. diff --git a/apps/emqx_machine/src/emqx_cluster_rpc_handler.erl b/apps/emqx_machine/src/emqx_cluster_rpc_handler.erl new file mode 100644 index 000000000..803b7f9fc --- /dev/null +++ b/apps/emqx_machine/src/emqx_cluster_rpc_handler.erl @@ -0,0 +1,89 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_cluster_rpc_handler). + +-behaviour(gen_server). + +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include("emqx_cluster_rpc.hrl"). + +-export([start_link/0, start_link/2]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +start_link() -> + MaxHistory = application:get_env(emqx_machine, cluster_call_max_history, 100), + CleanupMs = application:get_env(emqx_machine, cluster_call_cleanup_interval, 5*60*1000), + start_link(MaxHistory, CleanupMs). + +start_link(MaxHistory, CleanupMs) -> + State = #{max_history => MaxHistory, cleanup_ms => CleanupMs, timer => undefined}, + gen_server:start_link(?MODULE, [State], []). + +%%%=================================================================== +%%% Spawning and gen_server implementation +%%%=================================================================== + +init([State]) -> + {ok, ensure_timer(State)}. + +handle_call(Req, _From, State) -> + ?LOG(error, "unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + ?LOG(error, "unexpected msg: ~p", [Msg]), + {noreply, State}. + +handle_info({timeout, TRef, del_stale_mfa}, State = #{timer := TRef, max_history := MaxHistory}) -> + case ekka_mnesia:transaction(?COMMON_SHARD, fun del_stale_mfa/1, [MaxHistory]) of + {atomic, ok} -> ok; + Error -> ?LOG(error, "del_stale_cluster_rpc_mfa error:~p", [Error]) + end, + {noreply, ensure_timer(State), hibernate}; + +handle_info(Info, State) -> + ?LOG(error, "unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #{timer := TRef}) -> + emqx_misc:cancel_timer(TRef). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +ensure_timer(State = #{cleanup_ms := Ms}) -> + State#{timer := emqx_misc:start_timer(Ms, del_stale_mfa)}. + +%% @doc Keep the latest completed 100 records for querying and troubleshooting. +del_stale_mfa(MaxHistory) -> + DoneId = + mnesia:foldl(fun(Rec, Min) -> min(Rec#cluster_rpc_commit.tnx_id, Min) end, + infinity, ?CLUSTER_COMMIT), + delete_stale_mfa(mnesia:last(?CLUSTER_MFA), DoneId, MaxHistory). + +delete_stale_mfa('$end_of_table', _DoneId, _Count) -> ok; +delete_stale_mfa(CurrId, DoneId, Count) when CurrId > DoneId -> + delete_stale_mfa(mnesia:prev(?CLUSTER_MFA, CurrId), DoneId, Count); +delete_stale_mfa(CurrId, DoneId, Count) when Count > 0 -> + delete_stale_mfa(mnesia:prev(?CLUSTER_MFA, CurrId), DoneId, Count - 1); +delete_stale_mfa(CurrId, DoneId, Count) when Count =< 0 -> + mnesia:delete(?CLUSTER_MFA, CurrId, write), + delete_stale_mfa(mnesia:prev(?CLUSTER_MFA, CurrId), DoneId, Count - 1). diff --git a/apps/emqx_machine/src/emqx_machine.erl b/apps/emqx_machine/src/emqx_machine.erl index 76a51fc3b..97125d79f 100644 --- a/apps/emqx_machine/src/emqx_machine.erl +++ b/apps/emqx_machine/src/emqx_machine.erl @@ -40,9 +40,6 @@ start() -> ok = set_backtrace_depth(), ok = print_otp_version_warning(), - %% need to load some app envs - %% TODO delete it once emqx boot does not depend on modules envs - _ = load_modules(), ok = load_config_files(), ok = ensure_apps_started(), @@ -80,14 +77,6 @@ print_vsn() -> ?ULOG("~s ~s is running now!~n", [emqx_app:get_description(), emqx_app:get_release()]). -endif. % TEST --ifndef(EMQX_ENTERPRISE). -load_modules() -> - application:load(emqx_modules). --else. -load_modules() -> - ok. --endif. - load_config_files() -> %% the app env 'config_files' for 'emqx` app should be set %% in app.time.config by boot script before starting Erlang VM @@ -131,7 +120,7 @@ start_one_app(App) -> ?SLOG(debug, #{msg => "started_apps", apps => Apps}); {error, Reason} -> ?SLOG(critical, #{msg => "failed_to_start_app", app => App, reason => Reason}), - error({faile_to_start_app, App, Reason}) + error({failed_to_start_app, App, Reason}) end. %% list of app names which should be rebooted when: @@ -142,7 +131,6 @@ reboot_apps() -> , esockd , ranch , cowboy - , ekka , emqx , emqx_prometheus , emqx_modules @@ -152,12 +140,10 @@ reboot_apps() -> , emqx_statsd , emqx_resource , emqx_rule_engine - , emqx_data_bridge + , emqx_bridge , emqx_bridge_mqtt , emqx_plugin_libs - , emqx_config_helper , emqx_management - , emqx_release_helper , emqx_retainer , emqx_exhook , emqx_rule_actions diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index bf695bb19..ae2bec50a 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -23,6 +23,7 @@ -dialyzer(no_fail_call). -include_lib("typerefl/include/types.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). -type log_level() :: debug | info | notice | warning | error | critical | alert | emergency | all. -type file() :: string(). @@ -34,8 +35,7 @@ file/0, cipher/0]). --export([structs/0, fields/1, translations/0, translation/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0, fields/1, translations/0, translation/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). %% Static apps which merge their configs into the merged emqx.conf @@ -43,184 +43,425 @@ %% by nodetool to generate app.