├── .dockerignore ├── .github └── workflows │ ├── build_packages.yaml │ ├── issue-translator.yaml │ ├── run_static_checks.yaml │ ├── run_test_case.yaml │ └── script │ └── build.sh ├── .gitignore ├── Dockerfile.test ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── bin ├── appup_test.sh ├── emqtt └── generate_relup.escript ├── changelog.md ├── include ├── emqtt.hrl └── logger.hrl ├── packages ├── Makefile ├── deb │ ├── Makefile │ └── debian │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ ├── copyright │ │ ├── postinst │ │ └── rules └── rpm │ ├── Makefile │ └── emqtt.spec ├── pre-compile ├── rebar.config ├── rebar.config.script ├── rebar3.cmd ├── src ├── emqtt.app.src ├── emqtt.appup.src ├── emqtt.erl ├── emqtt_cli.erl ├── emqtt_frame.erl ├── emqtt_inflight.erl ├── emqtt_internal.hrl ├── emqtt_props.erl ├── emqtt_quic.erl ├── emqtt_quic_connection.erl ├── emqtt_quic_stream.erl ├── emqtt_secret.erl ├── emqtt_sock.erl └── emqtt_ws.erl ├── test ├── certs │ └── .keep ├── emqtt_SUITE.erl ├── emqtt_connect_SUITE.erl ├── emqtt_frame_SUITE.erl ├── emqtt_kerberos_auth_SUITE.erl ├── emqtt_props_SUITE.erl ├── emqtt_quic_SUITE.erl ├── emqtt_request_handler.erl ├── emqtt_request_response_SUITE.erl ├── emqtt_request_sender.erl ├── emqtt_scram_auth_SUITE.erl ├── emqtt_sock_SUITE.erl ├── emqtt_test_lib.erl ├── quic_server.erl ├── ssl_server.erl └── tcp_server.erl ├── vars-pkg.config └── vars.config /.dockerignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | rebar.lock 3 | Dockerfile 4 | -------------------------------------------------------------------------------- /.github/workflows/build_packages.yaml: -------------------------------------------------------------------------------- 1 | name: Build packages 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: 8 | - published 9 | - prereleased 10 | 11 | jobs: 12 | 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | builder: 20 | - 5.2-7:1.15.7-26.1.2-1 21 | os: 22 | - ubuntu20.04 23 | - ubuntu22.04 24 | - debian11 25 | - debian12 26 | - el9 27 | - amzn2 28 | - amzn2023 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | with: 33 | fetch-depth: 0 34 | - name: build emqx packages 35 | env: 36 | BUILDER_IMAGE: "ghcr.io/emqx/emqx-builder/${{ matrix.builder }}-${{ matrix.os }}" 37 | # NOTE 38 | # Doing it manually instead of just setting `container` for the job because of 39 | # discrepancies in the images' environments affecting even basic things like 40 | # repo checkouts. 41 | run: | 42 | docker run --rm --name emqtt-${{ matrix.os }}-build -v $(pwd):/emqtt --workdir /emqtt ${BUILDER_IMAGE} /bin/bash -c ' 43 | git config --global --add safe.directory "${PWD}" 44 | .github/workflows/script/build.sh 45 | ' 46 | sudo bash -c ' 47 | cd _packages 48 | for var in $(ls); do echo $(sha256sum $var | awk "{print $1}") > $var.sha256; done 49 | ' 50 | - uses: actions/upload-artifact@v4 51 | if: ${{ github.event_name == 'release' }} 52 | with: 53 | name: packages 54 | path: _packages/* 55 | 56 | build-on-mac: 57 | runs-on: macos-latest 58 | 59 | strategy: 60 | fail-fast: false 61 | matrix: 62 | otp: 63 | - 24 64 | - 25 65 | 66 | steps: 67 | - uses: actions/checkout@v3 68 | - name: prepare 69 | run: | 70 | # See https://github.com/actions/setup-python/issues/577 71 | # brew update 72 | brew install curl zip unzip gnu-sed erlang@${{ matrix.otp }} coreutils 73 | echo "$(brew --prefix)/opt/erlang@${{ matrix.otp }}/bin" >> $GITHUB_PATH 74 | echo "$(brew --prefix)/opt/unzip/bin" >> $GITHUB_PATH 75 | echo "$(brew --prefix)/bin" >> $GITHUB_PATH 76 | - name: build 77 | run: | 78 | echo "${PATH}" 79 | erl -noshell -eval 'io:format(erlang:system_info(otp_release)), halt(0).' 80 | .github/workflows/script/build.sh 81 | pkg=emqtt-macos-$(git describe --tags --always).zip 82 | openssl dgst -sha256 _packages/$pkg | awk '{print $2}' > _packages/$pkg.sha256 83 | - uses: actions/upload-artifact@v4 84 | if: ${{ github.event_name == 'release' }} 85 | with: 86 | name: packages-mac 87 | path: _packages/* 88 | 89 | release: 90 | runs-on: ubuntu-latest 91 | 92 | needs: [build, build-on-mac] 93 | if: ${{ github.event_name == 'release' }} 94 | 95 | steps: 96 | - uses: actions/checkout@v3 97 | - uses: actions/download-artifact@v3 98 | with: 99 | name: packages 100 | path: _packages 101 | - uses: actions/download-artifact@v3 102 | with: 103 | name: packages-mac 104 | path: _packages 105 | - name: set aws 106 | uses: aws-actions/configure-aws-credentials@v1-node16 107 | with: 108 | aws-access-key-id: ${{ secrets.AwsAccessKeyId }} 109 | aws-secret-access-key: ${{ secrets.AwsSecretAccessKey }} 110 | aws-region: us-west-2 111 | - name: verify packages 112 | working-directory: _packages 113 | run: | 114 | for var in $(ls | grep emqtt | grep -v sha256); do 115 | echo "$(cat $var.sha256) $var" | sha256sum -c || exit 1 116 | done 117 | - name: upload aws 118 | run: | 119 | version=$(echo ${{ github.ref }} | sed -r "s .*/.*/(.*) \1 g") 120 | aws s3 cp --recursive ./_packages s3://packages.emqx.io/emqtt/$version 121 | aws cloudfront create-invalidation --distribution-id E3TYD0WSP4S14P --paths "/emqtt/$version/*" 122 | - name: upload github 123 | uses: softprops/action-gh-release@v1 124 | with: 125 | files: "_packages/*" 126 | -------------------------------------------------------------------------------- /.github/workflows/issue-translator.yaml: -------------------------------------------------------------------------------- 1 | name: Translate Issue from Chinese to English 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | issue_number: 7 | description: 'The issue number to translate' 8 | required: true 9 | type: string 10 | issues: 11 | types: [opened] 12 | 13 | jobs: 14 | translate: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write # Grant permission to edit issues 18 | steps: 19 | - uses: emqx/translate-issue-action@ee63ec619dfc5808ee2093dba93dfd7ac3beb437 # v1.0.2 20 | with: 21 | issue_number: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.issue_number || github.event.issue.number }} 22 | gemini_api_key: ${{ secrets.GEMINI_API_KEY }} 23 | -------------------------------------------------------------------------------- /.github/workflows/run_static_checks.yaml: -------------------------------------------------------------------------------- 1 | name: Run static checks 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: 8 | - published 9 | - prereleased 10 | 11 | jobs: 12 | 13 | run_test_case: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | builder: 19 | - "5.1-4:1.14.5-25.3.2-2-ubuntu20.04" 20 | 21 | container: "ghcr.io/emqx/emqx-builder/${{ matrix.builder }}" 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | with: 27 | submodules: recursive 28 | - name: work around https://github.com/actions/checkout/issues/766 29 | run: git config --global --add safe.directory "${GITHUB_WORKSPACE}" 30 | - name: Run tests 31 | run: | 32 | set -e 33 | make xref 34 | make dialyzer 35 | -------------------------------------------------------------------------------- /.github/workflows/run_test_case.yaml: -------------------------------------------------------------------------------- 1 | name: Run test case 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: 8 | - published 9 | - prereleased 10 | 11 | jobs: 12 | 13 | run_test_case: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | docker_image: 20 | - "ghcr.io/emqx/emqx-builder/5.3-13:1.15.7-26.2.5.2-1-ubuntu22.04" 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | submodules: recursive 27 | - name: work around https://github.com/actions/checkout/issues/766 28 | run: git config --global --add safe.directory "${GITHUB_WORKSPACE}" 29 | - name: Run tests 30 | env: 31 | DOCKER_IMAGE: "${{ matrix.docker_image }}" 32 | shell: bash 33 | run: | 34 | set -e 35 | docker build -t testimage --build-arg BUILD_FROM=${DOCKER_IMAGE} -f Dockerfile.test . 36 | docker network create --ipv6 --subnet 2001:0DB8::/112 testnet 37 | docker run -d --net testnet --name testcontainer testimage bash -c "tail -f /dev/null" 38 | docker exec testcontainer bash -c 'make eunit' 39 | docker exec testcontainer bash -c 'make ct' 40 | docker exec testcontainer bash -c 'make cover' 41 | # @TODO rebar3 tar does not include appup of dep apps 42 | # docker exec testcontainer bash -c 'make relup-test' 43 | # copy the build dir to host working dir for following build steps 44 | docker cp testcontainer:/_w/_build . 45 | docker rm -f testcontainer 46 | - uses: actions/upload-artifact@v4 47 | if: always() 48 | with: 49 | name: logs 50 | path: _build/test/logs 51 | - uses: actions/upload-artifact@v4 52 | with: 53 | name: cover 54 | path: _build/test/cover 55 | -------------------------------------------------------------------------------- /.github/workflows/script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | export PACKAGE_PATH="_packages" 5 | 6 | build_pkg(){ 7 | if [ "$(uname)" = "Linux" ]; then 8 | make pkg 9 | elif [ "$(uname)" = "Darwin" ]; then 10 | pkg=emqtt-macos-$(git describe --tags --always).zip 11 | make emqtt 12 | mkdir -p "${PACKAGE_PATH}" 13 | 14 | cd _build/emqtt/rel && zip -rq "${pkg}" emqtt && cd - 15 | mv "_build/emqtt/rel/${pkg}" "${PACKAGE_PATH}" 16 | fi 17 | } 18 | 19 | test_pkg(){ 20 | for var in ${PACKAGE_PATH}/emqtt-*; do 21 | case ${var##*.} in 22 | "zip") 23 | unzip -q "${var}" 24 | ./emqtt/bin/emqtt pub -t 'hello' --payload 'hello world' -h broker.emqx.io 25 | rm -rf emqtt 26 | ;; 27 | "deb") 28 | dpkg -i "${var}" 29 | if [ "$(dpkg -l | grep emqtt | awk '{print $1}')" != "ii" ]; then 30 | echo 'package install error' && exit 1 31 | fi 32 | emqtt pub -t 'hello' --payload 'hello world' -h broker.emqx.io 33 | dpkg -P emqtt 34 | if [ -n "$(dpkg -l | grep emqtt)" ]; then 35 | echo 'package uninstall error' && exit 1 36 | fi 37 | ;; 38 | "rpm") 39 | rpm -ivh "${var}" 40 | emqtt pub -t 'hello' --payload 'hello world' -h broker.emqx.io 41 | rpm -e emqtt 42 | ;; 43 | esac 44 | done 45 | } 46 | 47 | build_pkg 48 | test_pkg 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | ebin 8 | rel/example_project 9 | .concrete/DEV_MODE 10 | .rebar 11 | .idea 12 | .idea/ 13 | test/ct.cover.spec 14 | ct.coverdata 15 | eunit.coverdata 16 | logs/ 17 | cover/ 18 | .DS_Store 19 | _build/ 20 | _packages/ 21 | rebar3.crashdump 22 | *.swp 23 | rebar.lock 24 | emqtt.app.src 25 | emqtt_cli 26 | rebar3 27 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM=public.ecr.aws/docker/library/erlang:26 2 | FROM ${BUILD_FROM} 3 | 4 | ADD . /_w 5 | 6 | WORKDIR /_w 7 | 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | RUN apt update -y && apt install -y cmake 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2019, Anonymous . 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | 192 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CT_NODE_NAME = ct@127.0.0.1 2 | CT_EMQX_PROFILE = emqx 3 | 4 | REBAR ?= $(or $(shell which rebar3 2>/dev/null),$(CURDIR)/rebar3) 5 | REBAR_TEST = env PROFILE=$(CT_EMQX_PROFILE) $(REBAR) 6 | 7 | REBAR_URL := https://github.com/erlang/rebar3/releases/download/3.19.0/rebar3 8 | 9 | export REBAR_GIT_CLONE_OPTIONS += --depth=1 10 | 11 | all: emqtt 12 | 13 | $(REBAR): 14 | curl -fsSL "$(REBAR_URL)" -o $@ 15 | chmod +x $@ 16 | 17 | emqtt: $(REBAR) escript 18 | $(REBAR) as emqtt release 19 | 20 | pkg: escript 21 | $(REBAR) as emqtt_pkg release 22 | make -C packages 23 | 24 | compile: $(REBAR) 25 | $(REBAR) compile 26 | 27 | unlock: 28 | $(REBAR) unlock 29 | 30 | clean: distclean 31 | 32 | distclean: 33 | @rm -rf _build _packages erl_crash.dump rebar3.crashdump rebar.lock emqtt_cli rebar3 34 | 35 | xref: 36 | $(REBAR) xref 37 | 38 | eunit: compile 39 | $(REBAR_TEST) eunit --verbose 40 | 41 | ct: compile 42 | $(REBAR_TEST) as test ct --verbose --name $(CT_NODE_NAME) 43 | 44 | cover: 45 | $(REBAR_TEST) cover 46 | 47 | test: eunit ct cover 48 | 49 | dialyzer: 50 | $(REBAR) dialyzer 51 | 52 | escript: $(REBAR) compile 53 | $(REBAR) as escript escriptize 54 | 55 | relup-test: ${REBAR} 56 | bin/appup_test.sh 57 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 0. app.src 2 | 3 | Update app.src 4 | 5 | 1. Tests 6 | 7 | Sock 8 | Frame 9 | Props 10 | 11 | 2. README 12 | 13 | 3. Docs 14 | 15 | 4. Improve ws support: 16 | #{ connect_timeout => Timeout, 17 | %% http_opts => http_opts(), 18 | %% http2_opts => http2_opts(), 19 | %% protocols => [http | http2], 20 | retry => 3, 21 | retry_timeout => 3000 22 | %% TODO... 23 | %% supervise => false 24 | %% tcp_opts => [gen_tcp:connect_option()], 25 | %% tls_handshake_timeout => timeout(), 26 | %% tls_opts => [ssl:connect_option()], 27 | %% trace => boolean(), 28 | %% transport => tcp | tls | ssl, 29 | %% ws_opts => ws_opts() 30 | }, 31 | 32 | -------------------------------------------------------------------------------- /bin/appup_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | BASEDIR=$(dirname $(realpath "$0")) 4 | 5 | ## @DOC 6 | ## The script is used to test the appup.src file with following steps, 7 | ## 1) It builds all release tar files of *current* vsn and old vsns that they are defined in ${supported_rel_vsns} 8 | ## 2) It untar all the tar files to one tmp dir then build the *relup* file from there. see make_relup() 9 | ## 3) It then append the relup file to the tar file of current release, see make_relup() 10 | ## 4) It then test the old vsn upgrade and downgrade, see test_relup() 11 | 12 | # GLOBAL 13 | app=emqtt 14 | supported_rel_vsns="1.4.6" 15 | 16 | die() 17 | { 18 | echo "$1" 19 | exit 1; 20 | } 21 | 22 | build_and_save_tar() { 23 | dest_dir="$1" 24 | 25 | if rebar3 plugins list |grep relup_helper; then 26 | rebar3 as emqtt_relup_test do relup_helper gen_appups,tar; 27 | else 28 | rebar3 as emqtt_relup_test do tar 29 | fi 30 | 31 | mv _build/emqtt_relup_test/rel/emqtt/emqtt-*.tar.gz "${dest_dir}" 32 | } 33 | 34 | build_legacy() { 35 | dest_dir="$1" 36 | for vsn in ${supported_rel_vsns}; 37 | do 38 | echo "building rel tar for $vsn" 39 | vsn_dir="$dest_dir/$app-$vsn" 40 | # FIXME, this is temp repo for test 41 | git clone https://github.com/emqx/emqtt.git -b "$vsn" --recursive --depth 1 "$vsn_dir" 42 | pushd ./ 43 | cd ${vsn_dir} 44 | build_and_save_tar "$dest_dir"; 45 | popd 46 | done 47 | } 48 | 49 | untar_all_pkgs() { 50 | local dir="$1/" 51 | local appdir="$dir/$app" 52 | mkdir -p "$appdir" 53 | for f in ${dir}/*.tar.gz; 54 | do 55 | tar zxf "$f" -C "$appdir"; 56 | done 57 | } 58 | 59 | prepare_releases() { 60 | local dest_dir="$1" 61 | build_and_save_tar "$dest_dir" 62 | build_legacy "$dest_dir" 63 | untar_all_pkgs "$dest_dir" 64 | } 65 | 66 | erl_eval() { 67 | local node_cmd="$1" 68 | local cmd="$2" 69 | local expected_res="$3" 70 | [ ! -f $node_cmd ] && die "$node_cmd not found" 71 | res=$($node_cmd eval "$2") 72 | if [[ $expected_res != $res ]]; then 73 | die "Failed: eval: $cmd\n but returns $res" 74 | fi 75 | } 76 | 77 | test_relup() { 78 | local tar_dir="$1" 79 | local target_vsn="$2" 80 | 81 | for vsn in ${supported_rel_vsns}; 82 | do 83 | echo "unpack" 84 | appdir="${tar_dir}/${vsn}/" 85 | rm --preserve-root -rf "${appdir}/" 86 | mkdir -p ${appdir} 87 | tar zxf "$tar_dir/$app-$vsn.tar.gz" -C "$appdir" 88 | 89 | ## 90 | ## Start Old Version of EMQTT 91 | ## 92 | echo "starting $vsn" 93 | appscript="${appdir}/bin/emqtt" 94 | trap "timeout 3 $appscript stop || echo ok" EXIT 95 | $appscript daemon -- -mode interactive 96 | $appscript ping 97 | $appscript versions 98 | 99 | ## 100 | ## Deploy NEW Target Version 101 | ## 102 | echo "deploy $target_vsn" 103 | cp "$tar_dir/$app-$target_vsn.tar.gz" "$appdir/releases/" 104 | $appscript versions 105 | 106 | $appscript eval 'spawn(fun() -> process_flag(trap_exit, true), {ok, Pid}=emqtt:start_link(), ets:insert(ac_tab,{{application_master, emqtt}, Pid}), true=register(test_client1, Pid), receive stop -> ok end end).' 107 | erl_eval "$appscript" 'true = is_process_alive(whereis(test_client1)).' 'true' 108 | 109 | ## 110 | ## Trigger UPGRADE and check results 111 | ## 112 | echo "Upgrade test" 113 | $appscript upgrade --no-permanent "$target_vsn" 114 | $appscript ping 115 | $appscript versions 116 | erl_eval "$appscript" 'false = erlang:check_process_code(whereis(test_client1),emqtt).' 'false' 117 | erl_eval "$appscript" 'ok = gen_statem:stop(test_client1).' 'ok' 118 | echo "Upgrade test done and success" 119 | 120 | 121 | $appscript eval 'spawn(fun() -> process_flag(trap_exit, true), {ok, Pid}=emqtt:start_link(), ets:insert(ac_tab,{{application_master, emqtt},Pid}), true=register(test_client1, Pid), receive stop -> ok end end).' 122 | erl_eval "$appscript" 'true = is_process_alive(whereis(test_client1)).' 'true' 123 | 124 | ## 125 | ## Trigger DOWNGRADE and check results 126 | ## note, downgrade isn't supported yet 127 | echo "Start downgrade test" 128 | $appscript downgrade "$vsn" 129 | erl_eval "$appscript" 'false = erlang:check_process_code(whereis(test_client1),emqtt).' 'false' 130 | erl_eval "$appscript" 'ok = gen_statem:stop(test_client1).' 'ok' 131 | echo "Downgrade test done and success" 132 | 133 | done; 134 | } 135 | 136 | make_relup() { 137 | local tmpdir="$1" 138 | local current_vsn="$2" 139 | local appdir="$1/$app" 140 | 141 | untar_all_pkgs "$tmpdir" 142 | pushd ./ 143 | 144 | cd "${appdir}" 145 | for vsn in $supported_rel_vsns $current_vsn; 146 | do 147 | [ -e "${vsn}.rel" ] || ln -s "releases/$vsn/emqtt.rel" "$vsn.rel" 148 | done 149 | 150 | ${BASEDIR}/generate_relup.escript "$current_vsn" "${supported_rel_vsns/ /,}" "$PWD" "lib/" "$PWD/releases/${current_vsn}" 151 | popd 152 | 153 | gzip -d "${tmpdir}/emqtt-${current_vsn}.tar.gz" 154 | tar rvf "${tmpdir}/emqtt-${current_vsn}.tar" -C "$appdir" "releases/${current_vsn}/relup" 155 | gzip "${tmpdir}/emqtt-${current_vsn}.tar" 156 | } 157 | 158 | current_vsn() { 159 | git describe --tags --always 160 | } 161 | 162 | main() { 163 | tmpdir=$(realpath $(mktemp -d -p . --suffix '.relup_test')) 164 | current_vsn=$(current_vsn) 165 | echo "Using temp dir: $tmpdir" 166 | prepare_releases "$tmpdir" 167 | untar_all_pkgs "$tmpdir" 168 | make_relup "$tmpdir" "$current_vsn" 169 | test_relup "$tmpdir" "$current_vsn" 170 | } 171 | 172 | 173 | cmd=${1:-"main"} 174 | 175 | [ $# -gt 0 ] && shift 1 176 | "$cmd" $@ 177 | exit 0; 178 | -------------------------------------------------------------------------------- /bin/emqtt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ## constants from relx template 5 | RUNNER_ROOT_DIR="{{ runner_root_dir }}" 6 | RUNNER_BIN_DIR="{{ runner_bin_dir }}" 7 | ERTS_VSN="{{ erts_vsn }}" 8 | 9 | ERTS_PATH=$RUNNER_ROOT_DIR/erts-$ERTS_VSN/bin 10 | 11 | exec ${ERTS_PATH}/escript ${RUNNER_BIN_DIR}/emqtt_cli "$@" 12 | -------------------------------------------------------------------------------- /bin/generate_relup.escript: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -noinput 3 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 4 | %% ex: ft=erlang ts=4 sw=4 et 5 | 6 | main([NewVsn, OldVsns, RelDir, LibDir, OutDir]) -> 7 | Paths=[ RelDir | filelib:wildcard(LibDir ++ "*/ebin/")], 8 | UpFrom = string:tokens(OldVsns, ","), 9 | DownTo = UpFrom, 10 | case systools:make_relup(NewVsn, UpFrom, DownTo, [{path,Paths} ,{outdir, OutDir}]) of 11 | ok -> 12 | io:format("success! relup in outdir: ~p ~n", [OutDir]); 13 | Error -> 14 | io:format("Failed: ~p !~n", [Error]), 15 | halt(1) 16 | end. 17 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 1.14.2 2 | 3 | - WebSocket: Correctly adopt Gun 2.x interfaces and semantics. 4 | 5 | # 1.14.1 6 | 7 | - QUIC: Correctly handle peer stream recv abort. 8 | - TLS1.3: Fix crash after recv session ticket 9 | 10 | # 1.14.0 11 | 12 | - Switch to using `gun` 2.1.0 as WebSocket client. 13 | 14 | # 1.13.6 15 | 16 | - Fix a typo in MQTT v3 reason code. 17 | - Support QoE tcp latency tracking in SSL case. 18 | 19 | # 1.13.5 20 | 21 | - Avoid OTP crash reports by wrapping exit reason in the format `{shutdown,Reason}`. 22 | - Avoid `badmatch` error when `websocket` connection timeout. 23 | - Reformat some code for readability. 24 | - Fix `emqtt` was not trying to reconnect in certain error scenarios. 25 | - Fix function specs for `emqtt:connect/1` and `emqtt:ws_connect/1`. 26 | - Add missing opts to `README.md`. 27 | 28 | # 1.13.4 29 | 30 | - Handle CONNECT packet send error asynchronously so to allow a retry. 31 | - Handle `tcp_error` and `ssl_error` at `waiting_for_connack` state so to allow a retry. 32 | - Change log level for `reconnect_due_to_connection_error` from `error` to `info`. 33 | - Fix compile warnings on OTP 27. 34 | 35 | # 1.13.3 36 | 37 | - Fix compile issues on OTP 27. 38 | 39 | # 1.13.2 40 | 41 | - Support fine-tuneing QUIC transport options. 42 | 43 | # 1.13.1 44 | 45 | - Support QUIC stream ID. 46 | - Improve exception context when broker did not assign client ID as expected. 47 | Changed `bad_client_id` to `no_client_id_assigned_by_broker` so we know it's broker to blame but not client. 48 | 49 | # 1.13.0 50 | 51 | - Support Kerberos authentication callbacks. 52 | 53 | # 1.12.0 54 | 55 | - Add `max_inflight` option. 56 | - Respect Receive-Maximum if advertised by server. 57 | - Support SCRAM authentication callbacks. 58 | 59 | # 1.11.0 60 | 61 | - Add `connect` command which only establishes connection, 62 | but does not publish or subscribe. 63 | - Add `--log-level` option to CLI. 64 | - Add timestamp to CLI logs. 65 | - Exit with non-zero code when CLI stops due to error. 66 | 67 | # 1.10.0 68 | 69 | - Export emqtt:qos/0, emqtt:topic/0 and emqtt:packet_id/0 as public types. 70 | - Release packages on OTP 26 71 | - Stopped releasing on 72 | - EL 8 73 | - Ubuntu 18 74 | - Debian 10 75 | - Newly supported distros 76 | - EL 9 77 | - Debian 12 78 | - Ubuntu 22 79 | - Amazon Linux 2023 80 | 81 | # 1.9.6 82 | 83 | - Add `{auto_ack, never}` option fully disabling automatic QoS2 flow. 84 | 85 | # 1.9.5 86 | 87 | - Fix compilation warning. 88 | 89 | # 1.9.4 90 | 91 | - Respect reconnect option more robustly, attempting to reconnect in more cases. 92 | 93 | # 1.9.3 94 | 95 | - Attempt to reconnect when server sends a `DISCONNECT` packet, if reconnects are enabled. 96 | 97 | # 1.9.2 98 | 99 | - Allow external wrapped secrets as passwords. 100 | 101 | # 1.9.1 102 | 103 | - Removed 'maybe' type. 104 | - Fix websocket transport options. 105 | - Support OTP 26. 106 | - Drop `reuse_sessions` and `secure_renegotiate` options when TLS 1.3 is the only version in use. 107 | 108 | # 1.9.0 109 | 110 | - Upgrade `quicer` lib 111 | 112 | # 1.8.7 113 | 114 | - Fix a race-condition caused crash when changing control process after SSL upgrade. 115 | The race-condition is from OTP's `ssl` lib, this fix only avoids `emqtt` process to crash. 116 | 117 | # 1.8.6 118 | 119 | - Sensitive data obfuscation in debug logs. 120 | 121 | # 1.8.5 122 | 123 | - Fix ssl error messages handeling. 124 | 125 | # 1.8.4 126 | 127 | - Support MacOS build for QUIC 128 | 129 | # 1.8.3 130 | 131 | - Support `binary()` hostname. 132 | 133 | # 1.8.0-1.8.2 134 | 135 | - Support QUIC Multi-stream 136 | 137 | # 1.7.0 138 | 139 | - Hide password in an anonymous function to prevent it from leaking into the (crash) logs [#168](https://github.com/emqx/emqtt/pull/168) 140 | - Added `publish_async` APIs to support asynchronous publishing. [#165](https://github.com/emqx/emqtt/pull/165) 141 | Note that an incompatible update has been included, where the return format 142 | of the `publish` function has been changed to `ok | {ok, publish_reply()} | {error, Reason}` 143 | 144 | - Fixed inflight message retry after reconnect [#166](https://github.com/emqx/emqtt/pull/166) 145 | - Respect connect_timeout [#169](https://github.com/emqx/emqtt/pull/169) 146 | -------------------------------------------------------------------------------- /include/emqtt.hrl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -ifndef(EMQTT_HRL). 18 | -define(EMQTT_HRL, true). 19 | 20 | %%-------------------------------------------------------------------- 21 | %% MQTT Protocol Version and Names 22 | %%-------------------------------------------------------------------- 23 | 24 | -define(MQTT_PROTO_V3, 3). 25 | -define(MQTT_PROTO_V4, 4). 26 | -define(MQTT_PROTO_V5, 5). 27 | 28 | -define(PROTOCOL_NAMES, [ 29 | {?MQTT_PROTO_V3, <<"MQIsdp">>}, 30 | {?MQTT_PROTO_V4, <<"MQTT">>}, 31 | {?MQTT_PROTO_V5, <<"MQTT">>}]). 32 | 33 | %%-------------------------------------------------------------------- 34 | %% MQTT QoS Levels 35 | %%-------------------------------------------------------------------- 36 | 37 | -define(QOS_0, 0). %% At most once 38 | -define(QOS_1, 1). %% At least once 39 | -define(QOS_2, 2). %% Exactly once 40 | 41 | -define(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)). 42 | 43 | -define(IS_QOS_NAME(I), 44 | (I =:= qos0 orelse I =:= at_most_once orelse 45 | I =:= qos1 orelse I =:= at_least_once orelse 46 | I =:= qos2 orelse I =:= exactly_once)). 47 | 48 | %%-------------------------------------------------------------------- 49 | %% Maximum ClientId Length. 50 | %%-------------------------------------------------------------------- 51 | 52 | -define(MAX_CLIENTID_LEN, 65535). 53 | 54 | %%-------------------------------------------------------------------- 55 | %% MQTT Control Packet Types 56 | %%-------------------------------------------------------------------- 57 | 58 | -define(RESERVED, 0). %% Reserved 59 | -define(CONNECT, 1). %% Client request to connect to Server 60 | -define(CONNACK, 2). %% Server to Client: Connect acknowledgment 61 | -define(PUBLISH, 3). %% Publish message 62 | -define(PUBACK, 4). %% Publish acknowledgment 63 | -define(PUBREC, 5). %% Publish received (assured delivery part 1) 64 | -define(PUBREL, 6). %% Publish release (assured delivery part 2) 65 | -define(PUBCOMP, 7). %% Publish complete (assured delivery part 3) 66 | -define(SUBSCRIBE, 8). %% Client subscribe request 67 | -define(SUBACK, 9). %% Server Subscribe acknowledgment 68 | -define(UNSUBSCRIBE, 10). %% Unsubscribe request 69 | -define(UNSUBACK, 11). %% Unsubscribe acknowledgment 70 | -define(PINGREQ, 12). %% PING request 71 | -define(PINGRESP, 13). %% PING response 72 | -define(DISCONNECT, 14). %% Client or Server is disconnecting 73 | -define(AUTH, 15). %% Authentication exchange 74 | 75 | -define(TYPE_NAMES, [ 76 | 'CONNECT', 77 | 'CONNACK', 78 | 'PUBLISH', 79 | 'PUBACK', 80 | 'PUBREC', 81 | 'PUBREL', 82 | 'PUBCOMP', 83 | 'SUBSCRIBE', 84 | 'SUBACK', 85 | 'UNSUBSCRIBE', 86 | 'UNSUBACK', 87 | 'PINGREQ', 88 | 'PINGRESP', 89 | 'DISCONNECT', 90 | 'AUTH']). 91 | 92 | %%-------------------------------------------------------------------- 93 | %% MQTT V3.1.1 Connect Return Codes 94 | %%-------------------------------------------------------------------- 95 | 96 | -define(CONNACK_ACCEPT, 0). %% Connection accepted 97 | -define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version 98 | -define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server 99 | -define(CONNACK_SERVER, 3). %% Server unavailable 100 | -define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed 101 | -define(CONNACK_AUTH, 5). %% Client is not authorized to connect 102 | 103 | %%-------------------------------------------------------------------- 104 | %% MQTT V5.0 Reason Codes 105 | %%-------------------------------------------------------------------- 106 | 107 | -define(RC_SUCCESS, 16#00). 108 | -define(RC_NORMAL_DISCONNECTION, 16#00). 109 | -define(RC_GRANTED_QOS_0, 16#00). 110 | -define(RC_GRANTED_QOS_1, 16#01). 111 | -define(RC_GRANTED_QOS_2, 16#02). 112 | -define(RC_DISCONNECT_WITH_WILL_MESSAGE, 16#04). 113 | -define(RC_NO_MATCHING_SUBSCRIBERS, 16#10). 114 | -define(RC_NO_SUBSCRIPTION_EXISTED, 16#11). 115 | -define(RC_CONTINUE_AUTHENTICATION, 16#18). 116 | -define(RC_RE_AUTHENTICATE, 16#19). 117 | -define(RC_UNSPECIFIED_ERROR, 16#80). 118 | -define(RC_MALFORMED_PACKET, 16#81). 119 | -define(RC_PROTOCOL_ERROR, 16#82). 120 | -define(RC_IMPLEMENTATION_SPECIFIC_ERROR, 16#83). 121 | -define(RC_UNSUPPORTED_PROTOCOL_VERSION, 16#84). 122 | -define(RC_CLIENT_IDENTIFIER_NOT_VALID, 16#85). 123 | -define(RC_BAD_USER_NAME_OR_PASSWORD, 16#86). 124 | -define(RC_NOT_AUTHORIZED, 16#87). 125 | -define(RC_SERVER_UNAVAILABLE, 16#88). 126 | -define(RC_SERVER_BUSY, 16#89). 127 | -define(RC_BANNED, 16#8A). 128 | -define(RC_SERVER_SHUTTING_DOWN, 16#8B). 129 | -define(RC_BAD_AUTHENTICATION_METHOD, 16#8C). 130 | -define(RC_KEEP_ALIVE_TIMEOUT, 16#8D). 131 | -define(RC_SESSION_TAKEN_OVER, 16#8E). 132 | -define(RC_TOPIC_FILTER_INVALID, 16#8F). 133 | -define(RC_TOPIC_NAME_INVALID, 16#90). 134 | -define(RC_PACKET_IDENTIFIER_IN_USE, 16#91). 135 | -define(RC_PACKET_IDENTIFIER_NOT_FOUND, 16#92). 136 | -define(RC_RECEIVE_MAXIMUM_EXCEEDED, 16#93). 137 | -define(RC_TOPIC_ALIAS_INVALID, 16#94). 138 | -define(RC_PACKET_TOO_LARGE, 16#95). 139 | -define(RC_MESSAGE_RATE_TOO_HIGH, 16#96). 140 | -define(RC_QUOTA_EXCEEDED, 16#97). 141 | -define(RC_ADMINISTRATIVE_ACTION, 16#98). 142 | -define(RC_PAYLOAD_FORMAT_INVALID, 16#99). 143 | -define(RC_RETAIN_NOT_SUPPORTED, 16#9A). 144 | -define(RC_QOS_NOT_SUPPORTED, 16#9B). 145 | -define(RC_USE_ANOTHER_SERVER, 16#9C). 146 | -define(RC_SERVER_MOVED, 16#9D). 147 | -define(RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED, 16#9E). 148 | -define(RC_CONNECTION_RATE_EXCEEDED, 16#9F). 149 | -define(RC_MAXIMUM_CONNECT_TIME, 16#A0). 150 | -define(RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED, 16#A1). 151 | -define(RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED, 16#A2). 152 | 153 | %%-------------------------------------------------------------------- 154 | %% Maximum MQTT Packet ID and Length 155 | %%-------------------------------------------------------------------- 156 | 157 | -define(MAX_PACKET_ID, 16#ffff). 158 | -define(MAX_PACKET_SIZE, 16#fffffff). 159 | 160 | %%-------------------------------------------------------------------- 161 | %% MQTT Frame Mask 162 | %%-------------------------------------------------------------------- 163 | 164 | -define(HIGHBIT, 2#10000000). 165 | -define(LOWBITS, 2#01111111). 166 | 167 | %%-------------------------------------------------------------------- 168 | %% MQTT Packet Fixed Header 169 | %%-------------------------------------------------------------------- 170 | 171 | -record(mqtt_packet_header, { 172 | type = ?RESERVED, 173 | dup = false, 174 | qos = ?QOS_0, 175 | retain = false 176 | }). 177 | 178 | %%-------------------------------------------------------------------- 179 | %% MQTT Packets 180 | %%-------------------------------------------------------------------- 181 | 182 | -define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling 183 | rap => 0, %% Retain as Publish 184 | nl => 0, %% No Local 185 | qos => 0 %% QoS 186 | }). 187 | 188 | -record(mqtt_packet_connect, { 189 | proto_name = <<"MQTT">>, 190 | proto_ver = ?MQTT_PROTO_V4, 191 | is_bridge = false, 192 | clean_start = true, 193 | will_flag = false, 194 | will_qos = ?QOS_0, 195 | will_retain = false, 196 | keepalive = 0, 197 | properties = undefined, 198 | clientid = <<>>, 199 | will_props = undefined, 200 | will_topic = undefined, 201 | will_payload = undefined, 202 | username = undefined, 203 | password = undefined 204 | }). 205 | 206 | -record(mqtt_packet_connack, { 207 | ack_flags, 208 | reason_code, 209 | properties 210 | }). 211 | 212 | -record(mqtt_packet_publish, { 213 | topic_name, 214 | packet_id, 215 | properties 216 | }). 217 | 218 | -record(mqtt_packet_puback, { 219 | packet_id, 220 | reason_code, 221 | properties 222 | }). 223 | 224 | -record(mqtt_packet_subscribe, { 225 | packet_id, 226 | properties, 227 | topic_filters 228 | }). 229 | 230 | -record(mqtt_packet_suback, { 231 | packet_id, 232 | properties, 233 | reason_codes 234 | }). 235 | 236 | -record(mqtt_packet_unsubscribe, { 237 | packet_id, 238 | properties, 239 | topic_filters 240 | }). 241 | 242 | -record(mqtt_packet_unsuback, { 243 | packet_id, 244 | properties, 245 | reason_codes 246 | }). 247 | 248 | -record(mqtt_packet_disconnect, { 249 | reason_code, 250 | properties 251 | }). 252 | 253 | -record(mqtt_packet_auth, { 254 | reason_code, 255 | properties 256 | }). 257 | 258 | %%-------------------------------------------------------------------- 259 | %% MQTT Message 260 | %%-------------------------------------------------------------------- 261 | 262 | -record(mqtt_msg, { 263 | qos = ?QOS_0 :: emqtt:qos(), 264 | retain = false :: boolean(), 265 | dup = false :: boolean(), 266 | packet_id :: undefined | emqtt:packet_id(), 267 | topic :: emqtt:topic(), 268 | props :: emqtt:properties() | undefined, 269 | payload :: binary() 270 | }). 271 | 272 | %%-------------------------------------------------------------------- 273 | %% MQTT Control Packet 274 | %%-------------------------------------------------------------------- 275 | 276 | -record(mqtt_packet, { 277 | header :: #mqtt_packet_header{}, 278 | variable :: #mqtt_packet_connect{} 279 | | #mqtt_packet_connack{} 280 | | #mqtt_packet_publish{} 281 | | #mqtt_packet_puback{} 282 | | #mqtt_packet_subscribe{} 283 | | #mqtt_packet_suback{} 284 | | #mqtt_packet_unsubscribe{} 285 | | #mqtt_packet_unsuback{} 286 | | #mqtt_packet_disconnect{} 287 | | #mqtt_packet_auth{} 288 | | pos_integer() 289 | | undefined, 290 | payload :: binary() | undefined 291 | }). 292 | 293 | %%-------------------------------------------------------------------- 294 | %% MQTT Packet Match 295 | %%-------------------------------------------------------------------- 296 | 297 | -define(CONNECT_PACKET(Var), 298 | #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, 299 | variable = Var}). 300 | 301 | -define(CONNACK_PACKET(ReasonCode), 302 | #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, 303 | variable = #mqtt_packet_connack{ack_flags = 0, 304 | reason_code = ReasonCode} 305 | }). 306 | 307 | -define(CONNACK_PACKET(ReasonCode, SessPresent), 308 | #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, 309 | variable = #mqtt_packet_connack{ack_flags = SessPresent, 310 | reason_code = ReasonCode} 311 | }). 312 | 313 | -define(CONNACK_PACKET(ReasonCode, SessPresent, Properties), 314 | #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, 315 | variable = #mqtt_packet_connack{ack_flags = SessPresent, 316 | reason_code = ReasonCode, 317 | properties = Properties} 318 | }). 319 | 320 | -define(AUTH_PACKET(), 321 | #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, 322 | variable = #mqtt_packet_auth{reason_code = 0} 323 | }). 324 | 325 | -define(AUTH_PACKET(ReasonCode), 326 | #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, 327 | variable = #mqtt_packet_auth{reason_code = ReasonCode} 328 | }). 329 | 330 | -define(AUTH_PACKET(ReasonCode, Properties), 331 | #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, 332 | variable = #mqtt_packet_auth{reason_code = ReasonCode, 333 | properties = Properties} 334 | }). 335 | 336 | -define(PUBLISH_PACKET(QoS), 337 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}}). 338 | 339 | -define(PUBLISH_PACKET(QoS, PacketId), 340 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, 341 | qos = QoS}, 342 | variable = #mqtt_packet_publish{packet_id = PacketId} 343 | }). 344 | 345 | -define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), 346 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, 347 | qos = QoS}, 348 | variable = #mqtt_packet_publish{topic_name = Topic, 349 | packet_id = PacketId}, 350 | payload = Payload 351 | }). 352 | 353 | -define(PUBLISH_PACKET(QoS, Topic, PacketId, Properties, Payload), 354 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, 355 | qos = QoS}, 356 | variable = #mqtt_packet_publish{topic_name = Topic, 357 | packet_id = PacketId, 358 | properties = Properties}, 359 | payload = Payload 360 | }). 361 | 362 | -define(PUBACK_PACKET(PacketId), ?PUBACK_PACKET(PacketId, 0)). 363 | 364 | -define(PUBACK_PACKET(PacketId, ReasonCode), 365 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, 366 | variable = #mqtt_packet_puback{packet_id = PacketId, 367 | reason_code = ReasonCode} 368 | }). 369 | 370 | -define(PUBACK_PACKET(PacketId, ReasonCode, Properties), 371 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, 372 | variable = #mqtt_packet_puback{packet_id = PacketId, 373 | reason_code = ReasonCode, 374 | properties = Properties} 375 | }). 376 | 377 | -define(PUBREC_PACKET(PacketId), ?PUBREC_PACKET(PacketId, 0)). 378 | 379 | -define(PUBREC_PACKET(PacketId, ReasonCode), 380 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, 381 | variable = #mqtt_packet_puback{packet_id = PacketId, 382 | reason_code = ReasonCode} 383 | }). 384 | 385 | -define(PUBREC_PACKET(PacketId, ReasonCode, Properties), 386 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, 387 | variable = #mqtt_packet_puback{packet_id = PacketId, 388 | reason_code = ReasonCode, 389 | properties = Properties} 390 | }). 391 | 392 | -define(PUBREL_PACKET(PacketId), ?PUBREL_PACKET(PacketId, 0)). 393 | 394 | -define(PUBREL_PACKET(PacketId, ReasonCode), 395 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, 396 | qos = ?QOS_1}, 397 | variable = #mqtt_packet_puback{packet_id = PacketId, 398 | reason_code = ReasonCode} 399 | }). 400 | 401 | -define(PUBREL_PACKET(PacketId, ReasonCode, Properties), 402 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, 403 | qos = ?QOS_1}, 404 | variable = #mqtt_packet_puback{packet_id = PacketId, 405 | reason_code = ReasonCode, 406 | properties = Properties} 407 | }). 408 | 409 | -define(PUBCOMP_PACKET(PacketId), ?PUBCOMP_PACKET(PacketId, 0)). 410 | 411 | -define(PUBCOMP_PACKET(PacketId, ReasonCode), 412 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, 413 | variable = #mqtt_packet_puback{packet_id = PacketId, 414 | reason_code = ReasonCode} 415 | }). 416 | 417 | -define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties), 418 | #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, 419 | variable = #mqtt_packet_puback{packet_id = PacketId, 420 | reason_code = ReasonCode, 421 | properties = Properties} 422 | }). 423 | 424 | -define(SUBSCRIBE_PACKET(PacketId, TopicFilters), 425 | #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, 426 | qos = ?QOS_1}, 427 | variable = #mqtt_packet_subscribe{packet_id = PacketId, 428 | topic_filters = TopicFilters} 429 | }). 430 | 431 | -define(SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), 432 | #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, 433 | qos = ?QOS_1}, 434 | variable = #mqtt_packet_subscribe{packet_id = PacketId, 435 | properties = Properties, 436 | topic_filters = TopicFilters} 437 | }). 438 | 439 | -define(SUBACK_PACKET(PacketId, ReasonCodes), 440 | #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, 441 | variable = #mqtt_packet_suback{packet_id = PacketId, 442 | reason_codes = ReasonCodes} 443 | }). 444 | 445 | -define(SUBACK_PACKET(PacketId, Properties, ReasonCodes), 446 | #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, 447 | variable = #mqtt_packet_suback{packet_id = PacketId, 448 | properties = Properties, 449 | reason_codes = ReasonCodes} 450 | }). 451 | 452 | -define(UNSUBSCRIBE_PACKET(PacketId, TopicFilters), 453 | #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, 454 | qos = ?QOS_1}, 455 | variable = #mqtt_packet_unsubscribe{packet_id = PacketId, 456 | topic_filters = TopicFilters} 457 | }). 458 | 459 | -define(UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), 460 | #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, 461 | qos = ?QOS_1}, 462 | variable = #mqtt_packet_unsubscribe{packet_id = PacketId, 463 | properties = Properties, 464 | topic_filters = TopicFilters} 465 | }). 466 | 467 | -define(UNSUBACK_PACKET(PacketId), 468 | #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, 469 | variable = #mqtt_packet_unsuback{packet_id = PacketId} 470 | }). 471 | 472 | -define(UNSUBACK_PACKET(PacketId, ReasonCodes), 473 | #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, 474 | variable = #mqtt_packet_unsuback{packet_id = PacketId, 475 | reason_codes = ReasonCodes} 476 | }). 477 | 478 | -define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), 479 | #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, 480 | variable = #mqtt_packet_unsuback{packet_id = PacketId, 481 | properties = Properties, 482 | reason_codes = ReasonCodes} 483 | }). 484 | 485 | -define(DISCONNECT_PACKET(), 486 | #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, 487 | variable = #mqtt_packet_disconnect{reason_code = 0} 488 | }). 489 | 490 | -define(DISCONNECT_PACKET(ReasonCode), 491 | #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, 492 | variable = #mqtt_packet_disconnect{reason_code = ReasonCode} 493 | }). 494 | 495 | -define(DISCONNECT_PACKET(ReasonCode, Properties), 496 | #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, 497 | variable = #mqtt_packet_disconnect{reason_code = ReasonCode, 498 | properties = Properties} 499 | }). 500 | 501 | -define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). 502 | 503 | -define(catch_error(Error, Exp), 504 | try (Exp) catch error:Error -> ok end). 505 | 506 | -endif. 507 | -------------------------------------------------------------------------------- /include/logger.hrl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -ifndef(EMQTT_LOGGER_HRL). 18 | -define(EMQTT_LOGGER_HRL, true). 19 | 20 | -define(SLOG(Level, Data, Meta), 21 | %% check 'allow' here, only evaluate Data and Meta when necessary 22 | case logger:allow(Level, ?MODULE) of 23 | true -> 24 | logger:log(Level, (Data), (begin Meta end)#{mfa => {?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY}, line => ?LINE}); 25 | false -> 26 | ok 27 | end). 28 | 29 | -endif. 30 | -------------------------------------------------------------------------------- /packages/Makefile: -------------------------------------------------------------------------------- 1 | export 2 | 3 | ifneq ($(shell uname -s),Linux) 4 | $(shell echo "Please execute this script under Linux") 5 | exit 6 | endif 7 | 8 | MKFILE := $(abspath $(lastword $(MAKEFILE_LIST))) 9 | SOURCE_PATH := $(shell dirname $(MKFILE))/.. 10 | REL_PATH := $(SOURCE_PATH)/_build/emqtt/rel 11 | PACKAGES_PATH := $(SOURCE_PATH)/_packages 12 | 13 | # The version-release used for package 14 | PKG_VSN := $(shell git describe --tags --always) 15 | 16 | ifneq ($(shell cat /etc/*-release |grep -o -i centos),) 17 | ID := centos 18 | VERSION_ID := $(shell rpm --eval '%{centos_ver}') 19 | else 20 | ID := $(shell sed -n '/^ID=/p' /etc/os-release | sed -r 's/ID=(.*)/\1/g' | sed 's/"//g' ) 21 | VERSION_ID := $(shell sed -n '/^VERSION_ID=/p' /etc/os-release | sed -r 's/VERSION_ID=(.*)/\1/g' | sed 's/"//g') 22 | endif 23 | SYSTEM := $(shell echo $(ID)$(VERSION_ID) | sed -r "s/([a-zA-Z]*)-.*/\1/g") 24 | ## 25 | ## Support RPM and Debian based linux systems 26 | ## 27 | ifeq ($(ID),ubuntu) 28 | PKGERDIR := deb 29 | else ifeq ($(ID),debian) 30 | PKGERDIR := deb 31 | else ifeq ($(ID),raspbian) 32 | PKGERDIR := deb 33 | else 34 | PKGERDIR := rpm 35 | endif 36 | 37 | .PHONY: all 38 | all: zip 39 | $(if $(PKGERDIR),,$(error "Operating system '$(OS)' not supported")) 40 | $(MAKE) -C $(PKGERDIR) 41 | 42 | .PHONY: zip 43 | zip: 44 | $(MAKE) -C ../ emqtt 45 | mkdir -p $(PACKAGES_PATH) 46 | cd $(REL_PATH) && zip -rq emqtt-$(SYSTEM)-$(PKG_VSN).zip emqtt 47 | mv $(REL_PATH)/emqtt-$(SYSTEM)-$(PKG_VSN).zip $(PACKAGES_PATH) 48 | 49 | .PHONY: deb 50 | deb: 51 | make -C deb 52 | 53 | .PHONY: rpm 54 | rpm: 55 | make -C rpm 56 | 57 | -------------------------------------------------------------------------------- /packages/deb/Makefile: -------------------------------------------------------------------------------- 1 | # Keep this short to avoid bloating beam files with long file path info 2 | TOPDIR := /tmp/emqx 3 | SRCDIR := $(TOPDIR)/$(PKG_VSN) 4 | DEB_VSN := $(PKG_VSN:v%=%) 5 | 6 | ARCH := $(shell dpkg --print-architecture) 7 | SOURCE_PKG := emqtt_$(DEB_VSN)_$(ARCH) 8 | TARGET_PKG := emqtt-$(SYSTEM)-$(PKG_VSN)_$(ARCH) 9 | 10 | .PHONY: all 11 | all: prepare 12 | cp -r debian $(SRCDIR)/ 13 | sed -i "s##$(shell date -u '+%a, %d %b %Y %T %z')#g" $(SRCDIR)/debian/changelog 14 | sed -i "s##$(DEB_VSN)#g" $(SRCDIR)/debian/changelog 15 | cd $(SRCDIR) && dpkg-buildpackage -us -uc 16 | mkdir -p $(PACKAGES_PATH) 17 | cp $(SRCDIR)/../$(SOURCE_PKG).deb $(PACKAGES_PATH)/$(TARGET_PKG).deb 18 | 19 | prepare: 20 | rm -rf $(SRCDIR) 21 | mkdir -p $(TOPDIR) $(SRCDIR) 22 | cp -r $(REL_PATH)/emqtt $(SRCDIR) 23 | -------------------------------------------------------------------------------- /packages/deb/debian/changelog: -------------------------------------------------------------------------------- 1 | emqtt () unstable; urgency=medium 2 | 3 | * See github commit history: https://github.com/emqx/emqx 4 | 5 | -- emqx 6 | -------------------------------------------------------------------------------- /packages/deb/debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /packages/deb/debian/control: -------------------------------------------------------------------------------- 1 | Source: emqtt 2 | Section: unknown 3 | Priority: optional 4 | Maintainer: emqx 5 | Build-Depends: debhelper (>=9) 6 | Standards-Version: 3.9.6 7 | Homepage: https://www.emqx.io 8 | 9 | Package: emqtt 10 | Architecture: any 11 | Depends: ${shlibs:Depends}, ${misc:Depends} 12 | Description: Erlang MQTT v5.0 Client compatible with MQTT v3.0 13 | -------------------------------------------------------------------------------- /packages/deb/debian/copyright: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /packages/deb/debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for emqtt 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | chmod -R 0755 /usr/lib/emqtt/bin 9 | [ -f /usr/bin/emqtt ] && rm /usr/bin/emqtt 10 | [ -f /usr/bin/emqtt_cli ] && rm /usr/bin/emqtt_cli 11 | ln -s /usr/lib/emqtt/bin/emqtt /usr/bin/emqtt 12 | ln -s /usr/lib/emqtt/bin/emqtt_cli /usr/bin/emqtt_cli 13 | 14 | exit 0 15 | -------------------------------------------------------------------------------- /packages/deb/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # modified for node_package by dizzyd@basho.com and jared@basho.com 10 | 11 | # Uncomment this to turn on verbose mode. 12 | export DH_VERBOSE=1 13 | 14 | 15 | ## Clear variables that may confound our build of sub-projects; also 16 | ## note that it is necessary to use overlay_vars relative to .. as 17 | ## the generate command EXECUTES in rel/ 18 | build: 19 | 20 | clean: 21 | dh_clean 22 | rm -f build 23 | # make clean 24 | 25 | ## dh_shlibdeps was added to figure out the dependencies on shared libraries 26 | ## and will populate the ${shlibs:Depends} callout in the control file 27 | install: build 28 | dh_testdir 29 | dh_testroot 30 | dh_prep 31 | dh_installdirs 32 | 33 | mkdir -p debian/emqtt/usr/lib/emqtt 34 | cp -R emqtt/* debian/emqtt/usr/lib/emqtt 35 | 36 | dh_shlibdeps 37 | 38 | # We have nothing to do by default. 39 | binary-indep: install build-stamp 40 | build-stamp: 41 | 42 | # Build architecture-dependent files here. 43 | binary-arch: install 44 | dh_strip -a 45 | dh_compress -a 46 | dh_installdeb 47 | dh_gencontrol 48 | dh_builddeb 49 | 50 | binary: binary-indep binary-arch 51 | -------------------------------------------------------------------------------- /packages/rpm/Makefile: -------------------------------------------------------------------------------- 1 | # Keep this short to avoid bloating beam files with long file path info 2 | TOPDIR := $(shell rpm --eval "%{_topdir}") 3 | RPM_VSN ?= $(shell echo $(PKG_VSN) | grep -oE "[0-9]+\.[0-9]+(\.[0-9]+)?") 4 | RPM_REL ?= $(shell echo $(PKG_VSN) | grep -oE "(alpha|beta|rc)\.[0-9]") 5 | 6 | ARCH := $(shell arch) 7 | TARGET_PKG := emqtt-$(SYSTEM)-$(PKG_VSN).$(ARCH) 8 | ifeq ($(RPM_REL),) 9 | # no tail 10 | RPM_REL := 1 11 | endif 12 | SOURCE_PKG := emqtt-$(SYSTEM)-v$(RPM_VSN)-$(RPM_REL).$(ARCH) 13 | 14 | .PHONY: all 15 | all: prepare 16 | rpmbuild -v -bb \ 17 | --define "_package_name emqtt" \ 18 | --define "_name emqtt" \ 19 | --define "_builddir $(TOPDIR)/$(PKG_VSN)/BUILD" \ 20 | --define "_version $(RPM_VSN)" \ 21 | --define "_release $(RPM_REL)" \ 22 | --define "_ostype -$(SYSTEM)" \ 23 | emqtt.spec 24 | mkdir -p $(PACKAGES_PATH) 25 | cp $(TOPDIR)/RPMS/$(ARCH)/$(SOURCE_PKG).rpm $(PACKAGES_PATH)/$(TARGET_PKG).rpm 26 | 27 | prepare: 28 | rm -rf $(TOPDIR) 29 | mkdir -p $(TOPDIR)/$(PKG_VSN)/BUILD 30 | cp -r $(REL_PATH)/emqtt $(TOPDIR)/$(PKG_VSN)/BUILD 31 | -------------------------------------------------------------------------------- /packages/rpm/emqtt.spec: -------------------------------------------------------------------------------- 1 | %define debug_package %{nil} 2 | %define _user %{_name} 3 | %define _group %{_name} 4 | %define _lib_home /usr/lib/%{_name} 5 | %define _build_name_fmt %{_arch}/%{_name}%{?_ostype}-v%{_version}-%{_release}.%{_arch}.rpm 6 | 7 | Name: %{_package_name} 8 | Version: %{_version} 9 | Release: %{_release}%{?dist} 10 | Summary: emqtt 11 | Group: System Environment/Daemons 12 | License: Apache License Version 2.0 13 | URL: https://www.emqx.io 14 | BuildRoot: %{_tmppath}/%{_name}-%{_version}-root 15 | Provides: %{_name} 16 | 17 | %description 18 | Erlang MQTT v5.0 Client compatible with MQTT v3.0. 19 | 20 | %prep 21 | 22 | %build 23 | 24 | %install 25 | if [ -d %{buildroot} ]; then 26 | rm -rf %{buildroot} 27 | fi 28 | mkdir -p %{buildroot}%{_lib_home} 29 | cp -r %{_builddir}/%{_name}/* %{buildroot}%{_lib_home}/ 30 | 31 | %pre 32 | 33 | %post 34 | if [ $1 = 1 ]; then 35 | ln -s %{_lib_home}/bin/emqtt %{_bindir}/emqtt 36 | ln -s %{_lib_home}/bin/emqtt_cli %{_bindir}/emqtt_cli 37 | fi 38 | 39 | %preun 40 | if [ $1 = 0 ]; then 41 | rm -f %{_bindir}/emqtt 42 | rm -f %{_bindir}/emqtt_cli 43 | fi 44 | exit 0 45 | 46 | %files 47 | %defattr(-,root,root) 48 | %{_lib_home} 49 | 50 | %clean 51 | rm -rf %{buildroot} 52 | 53 | %changelog 54 | 55 | -------------------------------------------------------------------------------- /pre-compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | 3 | -mode(compile). 4 | 5 | main(_Args) -> 6 | update_apps(os:getenv("WITH_WS")). 7 | 8 | add_app(Apps, NewApp) -> 9 | Apps ++ [NewApp]. 10 | 11 | del_app(Apps, OldApp) -> 12 | Apps -- [OldApp]. 13 | 14 | update_apps("true") -> 15 | do_update_apps(fun add_app/2); 16 | update_apps(_false) -> 17 | do_update_apps(fun del_app/2). 18 | 19 | do_update_apps(Operator) -> 20 | FilePath = filename:join(["src", "emqtt.app.src"]), 21 | {ok, [{application, emqtt, PropLists0}]} = file:consult(FilePath), 22 | Applications0 = proplists:get_value(applications, PropLists0), 23 | Applications = Operator(Applications0, gun), 24 | PropLists = [{applications, Applications} | proplists:delete(applications, PropLists0)], 25 | NewAppSrc = {application, emqtt, PropLists}, 26 | ok = file:write_file(FilePath, [io_lib:format("~p.\n", [NewAppSrc])]). 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {minimum_otp_vsn, "21.0"}. 2 | 3 | {erl_opts, [debug_info, 4 | warn_export_all, 5 | warn_unused_vars, 6 | warn_shadow_vars, 7 | warn_unused_import, 8 | warn_obsolete_guard 9 | ]}. 10 | 11 | {deps, [{cowlib, "2.13.0"}, 12 | {gun, "2.1.0"}, 13 | {getopt, "1.0.3"} 14 | ]}. 15 | 16 | {escript_name, emqtt_cli}. 17 | {escript_main_app, emqtt}. 18 | {escript_incl_apps, [getopt,gun,cowlib]}. 19 | {escript_emu_args, "%%! -smp true +K true +A 16 +P 200000 -env ERL_MAX_PORTS 100000 -env ERTS_MAX_PORTS 100000\n"}. 20 | {escript_shebang, "#!/usr/bin/env escript\n"}. 21 | 22 | {profiles, 23 | [{test, 24 | [{deps, 25 | [ {meck, "1.0.0"} 26 | , {emqx, {git_subdir, "https://github.com/emqx/emqx", {branch, "master"}, "apps/emqx"}} 27 | , {emqx_utils, {git_subdir, "https://github.com/emqx/emqx", {branch, "master"}, "apps/emqx_utils"}} 28 | , {emqx_limiter, {git_subdir, "https://github.com/emqx/emqx", {branch, "master"}, "apps/emqx/src/emqx_limiter"}} 29 | , {emqx_durable_storage, {git_subdir, "https://github.com/emqx/emqx", {branch, "master"}, "apps/emqx_durable_storage"}} 30 | , {emqx_ds_backends, {git_subdir, "https://github.com/emqx/emqx", {branch, "master"}, "apps/emqx_ds_backends"}} 31 | , {emqx_ds_builtin_local, {git_subdir, "https://github.com/emqx/emqx", {branch, "master"}, "apps/emqx_ds_builtin_local"}} 32 | , {emqx_ds_builtin_raft, {git_subdir, "https://github.com/emqx/emqx", {branch, "master"}, "apps/emqx_ds_builtin_raft"}} 33 | , {proper, "1.4.0"} 34 | , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.1"}}} 35 | , {sasl_auth, {git, "https://github.com/kafka4beam/sasl_auth.git", {tag, "v2.2.0"}}} 36 | ]}, 37 | {erl_opts, [debug_info]}, 38 | %% Define `TEST' in emqx to get empty `foreign_refereced_schema_apps' 39 | {overrides, [{add, emqx, [{erl_opts, [{d, 'TEST'}]}]}]} 40 | ]}, 41 | {escript, []}, 42 | {emqtt, 43 | [{relx, 44 | [{release, {emqtt, git_describe}, [ 45 | kernel, 46 | sasl, 47 | crypto, 48 | public_key, 49 | asn1, 50 | syntax_tools, 51 | ssl, 52 | os_mon, 53 | inets, 54 | compiler, 55 | runtime_tools 56 | ]}, 57 | {overlay_vars,["vars.config"]}, 58 | {overlay, [ 59 | {copy, "_build/escript/bin/emqtt_cli", "bin/emqtt_cli"}, 60 | {copy,"bin/emqtt","bin/emqtt"}, 61 | {template,"bin/emqtt","bin/emqtt"} 62 | ]}, 63 | {include_src, false}, 64 | {extended_start_script, false}, 65 | {generate_start_script, false}, 66 | {sys_config, false}, 67 | {vm_args, false}, 68 | {include_erts, true} 69 | ]} 70 | ]}, 71 | {emqtt_pkg, 72 | [{relx, 73 | [{release, {emqtt, git_describe}, [ 74 | kernel, 75 | sasl, 76 | crypto, 77 | public_key, 78 | asn1, 79 | syntax_tools, 80 | ssl, 81 | os_mon, 82 | inets, 83 | compiler, 84 | runtime_tools, 85 | quicer 86 | ]}, 87 | {overlay_vars,["vars-pkg.config"]}, 88 | {overlay, [ 89 | {copy, "_build/escript/bin/emqtt_cli", "bin/emqtt_cli"}, 90 | {copy,"bin/emqtt","bin/emqtt"}, 91 | {template,"bin/emqtt","bin/emqtt"} 92 | ]}, 93 | {include_src, false}, 94 | {extended_start_script, false}, 95 | {generate_start_script, false}, 96 | {sys_config, false}, 97 | {vm_args, false}, 98 | {include_erts, true} 99 | ] 100 | 101 | } 102 | ]}, 103 | %% test relup when emqtt is used as one dep app 104 | {emqtt_relup_test, 105 | [{relx, 106 | [{release, {emqtt, git_describe}, [ 107 | kernel, 108 | sasl, 109 | crypto, 110 | public_key, 111 | asn1, 112 | syntax_tools, 113 | ssl, 114 | os_mon, 115 | inets, 116 | compiler, 117 | runtime_tools, 118 | emqtt 119 | ]}, 120 | {include_src, false}, 121 | {include_erts, true}, 122 | {dev_mode, false}, 123 | {overlay, [{copy, "{{ output_dir }}/ebin/emqtt.appup", "lib/{{ release }}/ebin/"}]} 124 | ] 125 | }, 126 | {erl_opts, [{d, 'UPGRADE_TEST_CHEAT'}]} 127 | ]} 128 | 129 | ]}. 130 | 131 | {cover_enabled, true}. 132 | {cover_opts, [verbose]}. 133 | {cover_export_enabled, true}. 134 | 135 | {xref_checks, [undefined_function_calls]}. 136 | 137 | {dialyzer, [ 138 | {warnings, [unmatched_returns, error_handling]}, 139 | {plt_location, "."}, 140 | {plt_prefix, "emqtt_dialyzer"}, 141 | {plt_apps, all_apps}, 142 | {statistics, true}, 143 | {plt_extra_apps, [quicer, getopt]} 144 | ]}. 145 | 146 | {project_plugins, [{relup_helper,{git,"https://github.com/emqx/relup_helper", {tag, "2.0.0"}}}]}. 147 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | 3 | %% ============================================================================== 4 | %% Relx configs 5 | %% ============================================================================== 6 | Keyfind = fun(K, L) -> {K, V} = lists:keyfind(K, 1, L), V end, 7 | 8 | IsCentos6 = fun() -> 9 | case file:read_file("/etc/centos-release") of 10 | {ok, <<"CentOS release 6", _/binary >>} -> 11 | true; 12 | _ -> 13 | false 14 | end 15 | end, 16 | 17 | IsWin32 = fun() -> 18 | win32 =:= element(1, os:type()) 19 | end, 20 | 21 | IsQuicSupp = fun() -> 22 | not (IsCentos6() orelse IsWin32() 23 | orelse false =/= os:getenv("BUILD_WITHOUT_QUIC") 24 | ) 25 | end, 26 | 27 | NewRelx = 28 | fun(Name, Profiles) -> 29 | EMQTT = Keyfind(Name, Profiles), 30 | Relx = Keyfind(relx, EMQTT), 31 | {release, {emqtt, Vsn}, Apps} = lists:keyfind(release, 1, Relx), 32 | GitDescribe = lists:last(string:tokens(os:cmd("git describe --tags --always"), "\n")), 33 | lists:keystore(release, 1, Relx, {release, {emqtt, GitDescribe}, 34 | Apps ++ [ {quicer, load} || IsQuicSupp() ]}) 35 | end, 36 | 37 | Profiles = Keyfind(profiles, CONFIG), 38 | NewProfiles = lists:foldl(fun(Key, Acc) -> 39 | {Key, Old} = lists:keyfind(Key, 1, Acc), 40 | New = lists:keystore(relx, 1, Old, {relx, NewRelx(Key, Acc)}), 41 | lists:keystore(Key, 1, Acc, {Key, New}) 42 | end, Profiles, [emqtt, emqtt_pkg, emqtt_relup_test]), 43 | 44 | NewConfig = lists:keystore(profiles, 1, CONFIG, {profiles, NewProfiles}), 45 | 46 | Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.2.4"}}}, 47 | KillQuicer = fun(C) -> 48 | {deps, Deps0} = lists:keyfind(deps, 1, C), 49 | {erl_opts, ErlOpts0} = lists:keyfind(erl_opts, 1, C), 50 | IsQuic = IsQuicSupp(), 51 | New = [ {deps, Deps0 ++ [ Quicer || IsQuic ]} 52 | , {erl_opts, ErlOpts0 ++ [ {d, 'BUILD_WITHOUT_QUIC'} || not IsQuic ]} 53 | ], 54 | lists:foldl(fun({Key, _Val} = KV, Acc) -> 55 | lists:keystore(Key, 1, Acc, KV) 56 | end, C, New) 57 | end, 58 | KillQuicer(NewConfig). 59 | -------------------------------------------------------------------------------- /rebar3.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | set rebarscript=%~f0 4 | escript.exe "%rebarscript:.cmd=%" %* 5 | -------------------------------------------------------------------------------- /src/emqtt.app.src: -------------------------------------------------------------------------------- 1 | {application, emqtt, 2 | [{description, "Erlang MQTT v5.0 Client"}, 3 | {id, "emqtt"}, 4 | {vsn, {cmd, "git describe --tags --always"}}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications, [kernel, stdlib, gun]}, 8 | {env, []}, 9 | {licenses, ["Apache-2.0"]}, 10 | {maintainers, ["EMQ X Team "]}, 11 | {links, [{"Homepage", "https://emqx.io/"}, 12 | {"Github", "https://github.com/emqx/emqtt"} 13 | ]} 14 | ]}. 15 | -------------------------------------------------------------------------------- /src/emqtt.appup.src: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | {"1.5.1", 3 | [ 4 | {"1.5.0", [ 5 | ]}, 6 | {"1.4.8", [ 7 | {load_module,emqtt_cli,brutal_purge,soft_purge,[]}, 8 | {load_module,emqtt_sock,brutal_purge,soft_purge,[]} 9 | ]}, 10 | {"1.4.7", [ 11 | {load_module,emqtt,brutal_purge,soft_purge,[]}, 12 | {load_module,emqtt_cli,brutal_purge,soft_purge,[]}, 13 | {load_module,emqtt_frame,brutal_purge,soft_purge,[]}, 14 | {load_module,emqtt_props,brutal_purge,soft_purge,[]}, 15 | {load_module,emqtt_quic,brutal_purge,soft_purge,[]}, 16 | {load_module,emqtt_sock,brutal_purge,soft_purge,[]}, 17 | {load_module,emqtt_ws,brutal_purge,soft_purge,[]} 18 | ]}, 19 | {<<"1\\.4\\.[4-6]">>, [ 20 | {load_module,emqtt_cli,brutal_purge,soft_purge,[]}, 21 | {load_module,emqtt_frame,brutal_purge,soft_purge,[]}, 22 | {load_module,emqtt_props,brutal_purge,soft_purge,[]}, 23 | {load_module,emqtt_quic,brutal_purge,soft_purge,[]}, 24 | {load_module,emqtt_sock,brutal_purge,soft_purge,[]}, 25 | {load_module,emqtt_ws,brutal_purge,soft_purge,[]}, 26 | {update, emqtt, {advanced, []}} 27 | ]}, 28 | {<<"1\\.4\\.[0-3]">>, []} %% No appup maintained for these versions 29 | ], 30 | [ 31 | {"1.4.8", [ 32 | {load_module,emqtt_cli,brutal_purge,soft_purge,[]}, 33 | {load_module,emqtt_sock,brutal_purge,soft_purge,[]} 34 | ]}, 35 | {"1.4.7", [ 36 | {load_module,emqtt,brutal_purge,soft_purge,[]}, 37 | {load_module,emqtt_cli,brutal_purge,soft_purge,[]}, 38 | {load_module,emqtt_frame,brutal_purge,soft_purge,[]}, 39 | {load_module,emqtt_props,brutal_purge,soft_purge,[]}, 40 | {load_module,emqtt_quic,brutal_purge,soft_purge,[]}, 41 | {load_module,emqtt_sock,brutal_purge,soft_purge,[]}, 42 | {load_module,emqtt_ws,brutal_purge,soft_purge,[]} 43 | ]}, 44 | {<<"1\\.4\\.[4-6]">>, [ 45 | {load_module,emqtt_cli,brutal_purge,soft_purge,[]}, 46 | {load_module,emqtt_frame,brutal_purge,soft_purge,[]}, 47 | {load_module,emqtt_props,brutal_purge,soft_purge,[]}, 48 | {load_module,emqtt_quic,brutal_purge,soft_purge,[]}, 49 | {load_module,emqtt_sock,brutal_purge,soft_purge,[]}, 50 | {load_module,emqtt_ws,brutal_purge,soft_purge,[]}, 51 | {update, emqtt, {advanced, []}} 52 | ]}, 53 | {<<"1\\.4\\.[0-3]">>, []} %% No appup maintained for these versions 54 | ] 55 | }. 56 | -------------------------------------------------------------------------------- /src/emqtt_cli.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------- 2 | %% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%------------------------------------------------------------------------- 16 | 17 | -module(emqtt_cli). 18 | 19 | -include("emqtt.hrl"). 20 | 21 | -export([ main/1 22 | ]). 23 | 24 | -import(proplists, [get_value/2]). 25 | 26 | -define(CMD_NAME, "emqtt"). 27 | 28 | -define(HELP_OPT, 29 | [{help, undefined, "help", boolean, 30 | "Help information"} 31 | ]). 32 | 33 | -define(CONN_SHORT_OPTS, 34 | [{host, $h, "host", {string, "localhost"}, 35 | "mqtt server hostname or IP address"}, 36 | {port, $p, "port", integer, 37 | "mqtt server port number"}, 38 | {iface, $I, "iface", string, 39 | "specify the network interface or ip address to use"}, 40 | {protocol_version, $V, "protocol-version", {atom, 'v5'}, 41 | "mqtt protocol version: v3.1 | v3.1.1 | v5"}, 42 | {username, $u, "username", string, 43 | "username for connecting to server"}, 44 | {password, $P, "password", string, 45 | "password for connecting to server"}, 46 | {clientid, $C, "clientid", string, 47 | "client identifier"}, 48 | {keepalive, $k, "keepalive", {integer, 300}, 49 | "keep alive in seconds"} 50 | ]). 51 | 52 | -define(CONN_LONG_OPTS, 53 | [{max_inflight, undefined, "max-inflight", {integer, infinity}, 54 | "Maximum number of yet unacked QoS>0 messages allowed to be in-flight"}, 55 | {will_topic, undefined, "will-topic", string, 56 | "Topic for will message"}, 57 | {will_payload, undefined, "will-payload", string, 58 | "Payload in will message"}, 59 | {will_qos, undefined, "will-qos", {integer, 0}, 60 | "QoS for will message"}, 61 | {will_retain, undefined, "will-retain", {boolean, false}, 62 | "Retain in will message"}, 63 | {enable_websocket, undefined, "enable-websocket", {boolean, false}, 64 | "Enable websocket transport or not"}, 65 | {enable_quic, undefined, "enable-quic", {boolean, false}, 66 | "Enable quic transport or not"}, 67 | {enable_ssl, undefined, "enable-ssl", {boolean, false}, 68 | "Enable ssl/tls or not"}, 69 | {tls_version, undefined, "tls-version", {atom, 'tlsv1.2'}, 70 | "TLS protocol version used when the client connects to the broker"}, 71 | {cafile, undefined, "CAfile", string, 72 | "Path to a file containing pem-encoded ca certificates"}, 73 | {cert, undefined, "cert", string, 74 | "Path to a file containing the user certificate on pem format"}, 75 | {key, undefined, "key", string, 76 | "Path to the file containing the user's private pem-encoded key"}, 77 | {sni, undefined, "sni", string, 78 | "Applicable when '--enable_ssl' is in use. " 79 | "Use '--sni true' to apply the host name from '-h|--host' option " 80 | "as SNI, therwise use the host name to which the server's SSL " 81 | "certificate is issued"}, 82 | {verify, undefined, "verify", {boolean, false}, 83 | "TLS verify option, default: false " 84 | }, 85 | {log_level, undefined, "log-level", {atom, warning}, 86 | "Log level: debug | info | warning | error"} 87 | ]). 88 | 89 | -define(CONNECT_OPTS, ?CONN_SHORT_OPTS ++ ?HELP_OPT ++ ?CONN_LONG_OPTS). 90 | 91 | -define(PUB_OPTS, ?CONN_SHORT_OPTS ++ 92 | [{topic, $t, "topic", string, 93 | "mqtt topic on which to publish the message"}, 94 | {qos, $q, "qos", {integer, 0}, 95 | "qos level of assurance for delivery of an application message"}, 96 | {retain, $r, "retain", {boolean, false}, 97 | "retain message or not"} 98 | ] ++ ?HELP_OPT ++ ?CONN_LONG_OPTS ++ 99 | [{payload, undefined, "payload", string, 100 | "application message that is being published"}, 101 | {file, undefined, "file", string, "file content to publish"}, 102 | {repeat, undefined, "repeat", {integer, 1}, 103 | "the number of times the message will be repeatedly published"}, 104 | {repeat_delay, undefined, "repeat-delay", {integer, 0}, 105 | "the number of seconds to wait after the previous message was delivered before publishing the next"} 106 | ]). 107 | 108 | -define(SUB_OPTS, ?CONN_SHORT_OPTS ++ 109 | [{topic, $t, "topic", string, 110 | "mqtt topic to subscribe to"}, 111 | {qos, $q, "qos", {integer, 0}, 112 | "maximum qos level at which the server can send application messages to the client"} 113 | ] ++ ?HELP_OPT ++ ?CONN_LONG_OPTS ++ 114 | [{retain_as_publish, undefined, "retain-as-publish", {boolean, false}, 115 | "retain as publih option in subscription options"}, 116 | {retain_handling, undefined, "retain-handling", {integer, 0}, 117 | "retain handling option in subscription options"}, 118 | {print, undefined, "print", string, 119 | "'size' to print payload size, 'as-string' to print payload as string"} 120 | ]). 121 | 122 | main(["connect" | Argv]) -> 123 | {ok, {Opts, _Args}} = getopt:parse(?CONNECT_OPTS, Argv), 124 | ok = maybe_help(connect, Opts), 125 | main(connect, Opts); 126 | main(["sub" | Argv]) -> 127 | {ok, {Opts, _Args}} = getopt:parse(?SUB_OPTS, Argv), 128 | ok = maybe_help(sub, Opts), 129 | ok = check_required_args(sub, [topic], Opts), 130 | main(sub, Opts); 131 | main(["pub" | Argv]) -> 132 | {ok, {Opts, _Args}} = getopt:parse(?PUB_OPTS, Argv), 133 | ok = maybe_help(pub, Opts), 134 | ok = check_required_args(pub, [topic], Opts), 135 | Payload = get_value(payload, Opts), 136 | File = get_value(file, Opts), 137 | case {Payload, File} of 138 | {undefined, undefined} -> 139 | log_halt("Error: missing --payload or --file~n", []); 140 | _ -> 141 | ok 142 | end, 143 | main(pub, Opts); 144 | 145 | main(_Argv) -> 146 | io:format("Usage: ~s pub | sub | connect [--help]~n", [?CMD_NAME]). 147 | 148 | main(PubSubOrJustConnect, Opts0) -> 149 | _ = process_flag(trap_exit, true), 150 | _ = application:ensure_all_started(quicer), 151 | _ = application:ensure_all_started(emqtt), 152 | Print = proplists:get_value(print, Opts0), 153 | Opts = proplists:delete(print, Opts0), 154 | NOpts = enrich_opts(parse_cmd_opts(Opts)), 155 | case proplists:get_value(log_level, Opts0) of 156 | undefined -> 157 | ok; 158 | Level -> 159 | ok = logger:set_primary_config(level, Level) 160 | end, 161 | {ok, Client} = emqtt:start_link(NOpts), 162 | ConnRet = case {proplists:get_bool(enable_websocket, NOpts), 163 | proplists:get_bool(enable_quic, NOpts)} of 164 | {false, false} -> emqtt:connect(Client); 165 | {true, false} -> emqtt:ws_connect(Client); 166 | {false, true} -> emqtt:quic_connect(Client) 167 | end, 168 | case ConnRet of 169 | {ok, Properties} -> 170 | log("Connected:~n~p~n", [Properties]), 171 | case PubSubOrJustConnect of 172 | connect -> 173 | %% only connect, keep running 174 | receive_loop(Client, Print); 175 | pub -> 176 | publish(Client, NOpts, proplists:get_value(repeat, Opts)), 177 | disconnect(Client); 178 | sub -> 179 | subscribe(Client, NOpts), 180 | KeepAlive = case Properties of 181 | #{'Server-Keep-Alive' := I} -> I; 182 | _ -> get_value(keepalive, NOpts) 183 | end, 184 | _ = timer:send_interval(KeepAlive * 1000, ping), 185 | receive_loop(Client, Print) 186 | end; 187 | {error, Reason} -> 188 | log_halt("Failed to send CONNECT due to: ~p~n", [Reason]) 189 | end. 190 | 191 | publish(Client, Opts, 1) -> 192 | do_publish(Client, Opts); 193 | publish(Client, Opts, Repeat) -> 194 | do_publish(Client, Opts), 195 | case proplists:get_value(repeat_delay, Opts) of 196 | 0 -> ok; 197 | RepeatDelay -> timer:sleep(RepeatDelay * 1000) 198 | end, 199 | publish(Client, Opts, Repeat - 1). 200 | 201 | do_publish(Client, Opts) -> 202 | case get_value(payload, Opts) of 203 | undefined -> 204 | File = get_value(file, Opts), 205 | case file:read_file(File) of 206 | {ok, Bin} -> do_publish(Client, Opts, Bin); 207 | {error, Reason} -> 208 | log_halt("Failed to read ~s:~nreason: ~p", [File, Reason]) 209 | end; 210 | Bin -> 211 | do_publish(Client, Opts, Bin) 212 | end. 213 | 214 | do_publish(Client, Opts, Payload) -> 215 | case emqtt:publish(Client, get_value(topic, Opts), Payload, Opts) of 216 | {error, Reason} -> 217 | log_halt("Failed to send PUBLISH due to: ~p~n", [Reason]); 218 | _ -> 219 | log("Sent PUBLISH (Q~p, R~p, D0, Topic=~s, Payload=...(~p bytes))~n", 220 | [get_value(qos, Opts), i(get_value(retain, Opts)), 221 | get_value(topic, Opts), iolist_size(Payload)]) 222 | end. 223 | 224 | subscribe(Client, Opts) -> 225 | case emqtt:subscribe(Client, get_value(topic, Opts), Opts) of 226 | {ok, _, [ReasonCode]} when 0 =< ReasonCode andalso ReasonCode =< 2 -> 227 | log("Subscribed to: ~s~n", [get_value(topic, Opts)]); 228 | {ok, _, [ReasonCode]} -> 229 | log_halt("Failed to subscribe to ~s due to: ~s~n", [get_value(topic, Opts), emqtt:reason_code_name(ReasonCode)]); 230 | {error, Reason} -> 231 | log_halt("Failed to send SUBSCRIBE due to: ~p~n", [Reason]) 232 | end. 233 | 234 | disconnect(Client) -> 235 | case emqtt:disconnect(Client) of 236 | ok -> 237 | log("Sent DISCONNECT~n", []); 238 | {error, Reason} -> 239 | log_halt("Failed to send DISCONNECT due to: ~p~n", [Reason]) 240 | end. 241 | 242 | maybe_help(PubSubOrConnect, Opts) -> 243 | case proplists:get_value(help, Opts) of 244 | true -> 245 | usage(PubSubOrConnect), 246 | halt(0); 247 | _ -> ok 248 | end. 249 | 250 | usage(PubSubOrConnect) -> 251 | Opts = case PubSubOrConnect of 252 | pub -> ?PUB_OPTS; 253 | sub -> ?SUB_OPTS; 254 | connect -> ?CONNECT_OPTS 255 | end, 256 | getopt:usage(Opts, ?CMD_NAME ++ " " ++ atom_to_list(PubSubOrConnect)). 257 | 258 | check_required_args(PubSub, Keys, Opts) -> 259 | lists:foreach(fun(Key) -> 260 | case lists:keyfind(Key, 1, Opts) of 261 | false -> 262 | log("Error: '~s' required~n", [Key]), 263 | usage(PubSub), 264 | halt(1); 265 | _ -> ok 266 | end 267 | end, Keys). 268 | 269 | parse_cmd_opts(Opts) -> 270 | parse_cmd_opts(Opts, []). 271 | 272 | parse_cmd_opts([], Acc) -> 273 | Acc; 274 | parse_cmd_opts([{host, Host} | Opts], Acc) -> 275 | parse_cmd_opts(Opts, [{host, Host} | Acc]); 276 | parse_cmd_opts([{port, Port} | Opts], Acc) -> 277 | parse_cmd_opts(Opts, [{port, Port} | Acc]); 278 | parse_cmd_opts([{iface, Interface} | Opts], Acc) -> 279 | NAcc = case inet:parse_address(Interface) of 280 | {ok, IPAddress0} -> 281 | maybe_append(tcp_opts, {ifaddr, IPAddress0}, Acc); 282 | _ -> 283 | case inet:getifaddrs() of 284 | {ok, IfAddrs} -> 285 | case lists:filter(fun({addr, {_, _, _, _}}) -> true; 286 | (_) -> false 287 | end, proplists:get_value(Interface, IfAddrs, [])) of 288 | [{addr, IPAddress0}] -> maybe_append(tcp_opts, {ifaddr, IPAddress0}, Acc); 289 | _ -> Acc 290 | end; 291 | _ -> Acc 292 | end 293 | end, 294 | parse_cmd_opts(Opts, NAcc); 295 | parse_cmd_opts([{protocol_version, 'v3.1'} | Opts], Acc) -> 296 | parse_cmd_opts(Opts, [{proto_ver, v3} | Acc]); 297 | parse_cmd_opts([{protocol_version, 'v3.1.1'} | Opts], Acc) -> 298 | parse_cmd_opts(Opts, [{proto_ver, v4} | Acc]); 299 | parse_cmd_opts([{protocol_version, 'v5'} | Opts], Acc) -> 300 | parse_cmd_opts(Opts, [{proto_ver, v5} | Acc]); 301 | parse_cmd_opts([{username, Username} | Opts], Acc) -> 302 | parse_cmd_opts(Opts, [{username, list_to_binary(Username)} | Acc]); 303 | parse_cmd_opts([{password, Password} | Opts], Acc) -> 304 | parse_cmd_opts(Opts, [{password, list_to_binary(Password)} | Acc]); 305 | parse_cmd_opts([{clientid, Clientid} | Opts], Acc) -> 306 | parse_cmd_opts(Opts, [{clientid, list_to_binary(Clientid)} | Acc]); 307 | parse_cmd_opts([{max_inflight, infinity} | Opts], Acc) -> 308 | parse_cmd_opts(Opts, Acc); 309 | parse_cmd_opts([{max_inflight, N} | Opts], Acc) -> 310 | parse_cmd_opts(Opts, [{max_inflight, N} | Acc]); 311 | parse_cmd_opts([{will_topic, Topic} | Opts], Acc) -> 312 | parse_cmd_opts(Opts, [{will_topic, list_to_binary(Topic)} | Acc]); 313 | parse_cmd_opts([{will_payload, Payload} | Opts], Acc) -> 314 | parse_cmd_opts(Opts, [{will_payload, list_to_binary(Payload)} | Acc]); 315 | parse_cmd_opts([{will_qos, Qos} | Opts], Acc) -> 316 | parse_cmd_opts(Opts, [{will_qos, Qos} | Acc]); 317 | parse_cmd_opts([{will_retain, Retain} | Opts], Acc) -> 318 | parse_cmd_opts(Opts, [{will_retain, Retain} | Acc]); 319 | parse_cmd_opts([{keepalive, I} | Opts], Acc) -> 320 | parse_cmd_opts(Opts, [{keepalive, I} | Acc]); 321 | parse_cmd_opts([{enable_websocket, Enable} | Opts], Acc) -> 322 | parse_cmd_opts(Opts, [{enable_websocket, Enable} | Acc]); 323 | parse_cmd_opts([{enable_quic, Enable} | Opts], Acc) -> 324 | parse_cmd_opts(Opts, [{enable_quic, Enable} | Acc]); 325 | parse_cmd_opts([{enable_ssl, Enable} | Opts], Acc) -> 326 | parse_cmd_opts(Opts, [{ssl, Enable} | Acc]); 327 | parse_cmd_opts([{tls_version, Version} | Opts], Acc) 328 | when Version =:= 'tlsv1' orelse Version =:= 'tlsv1.1'orelse 329 | Version =:= 'tlsv1.2' orelse Version =:= 'tlsv1.3' -> 330 | parse_cmd_opts(Opts, maybe_append(ssl_opts, {versions, [Version]}, Acc)); 331 | parse_cmd_opts([{cafile, CAFile} | Opts], Acc) -> 332 | parse_cmd_opts(Opts, maybe_append(ssl_opts, {cacertfile, CAFile}, Acc)); 333 | parse_cmd_opts([{cert, Cert} | Opts], Acc) -> 334 | parse_cmd_opts(Opts, maybe_append(ssl_opts, {certfile, Cert}, Acc)); 335 | parse_cmd_opts([{key, Key} | Opts], Acc) -> 336 | parse_cmd_opts(Opts, maybe_append(ssl_opts, {keyfile, Key}, Acc)); 337 | parse_cmd_opts([{sni, SNI} | Opts], Acc) -> 338 | parse_cmd_opts(Opts, maybe_append(ssl_opts, {server_name_indication, SNI}, Acc)); 339 | parse_cmd_opts([{qos, QoS} | Opts], Acc) -> 340 | parse_cmd_opts(Opts, [{qos, QoS} | Acc]); 341 | parse_cmd_opts([{retain_as_publish, RetainAsPublish} | Opts], Acc) -> 342 | parse_cmd_opts(Opts, [{rap, RetainAsPublish} | Acc]); 343 | parse_cmd_opts([{retain_handling, RetainHandling} | Opts], Acc) -> 344 | parse_cmd_opts(Opts, [{rh, RetainHandling} | Acc]); 345 | parse_cmd_opts([{retain, Retain} | Opts], Acc) -> 346 | parse_cmd_opts(Opts, [{retain, Retain} | Acc]); 347 | parse_cmd_opts([{topic, Topic} | Opts], Acc) -> 348 | parse_cmd_opts(Opts, [{topic, list_to_binary(Topic)} | Acc]); 349 | parse_cmd_opts([{payload, Payload} | Opts], Acc) -> 350 | parse_cmd_opts(Opts, [{payload, list_to_binary(Payload)} | Acc]); 351 | parse_cmd_opts([{file, File} | Opts], Acc) -> 352 | parse_cmd_opts(Opts, [{file, File} | Acc]); 353 | parse_cmd_opts([{repeat, Repeat} | Opts], Acc) -> 354 | parse_cmd_opts(Opts, [{repeat, Repeat} | Acc]); 355 | parse_cmd_opts([{repeat_delay, RepeatDelay} | Opts], Acc) -> 356 | parse_cmd_opts(Opts, [{repeat_delay, RepeatDelay} | Acc]); 357 | parse_cmd_opts([{print, WhatToPrint} | Opts], Acc) -> 358 | parse_cmd_opts(Opts, [{print, WhatToPrint} | Acc]); 359 | parse_cmd_opts([{verify, IsVerify} | Opts], Acc) -> 360 | V = case IsVerify of 361 | true -> verify_peer; 362 | false -> verify_none 363 | end, 364 | parse_cmd_opts(Opts, maybe_append(ssl_opts, {verify, V}, Acc)); 365 | parse_cmd_opts([_ | Opts], Acc) -> 366 | parse_cmd_opts(Opts, Acc). 367 | 368 | maybe_append(Key, Value, TupleList) -> 369 | case lists:keytake(Key, 1, TupleList) of 370 | {value, {Key, OldValue}, NewTupleList} -> 371 | [{Key, [Value | OldValue]} | NewTupleList]; 372 | false -> 373 | [{Key, [Value]} | TupleList] 374 | end. 375 | 376 | enrich_opts(Opts) -> 377 | pipeline([fun enrich_clientid_opt/1, 378 | fun enrich_port_opt/1], Opts). 379 | 380 | enrich_clientid_opt(Opts) -> 381 | case lists:keyfind(clientid, 1, Opts) of 382 | false -> 383 | ClientId = emqtt:random_client_id(), 384 | log("Generated clientid: ~s~n", [ClientId]), 385 | [{clientid, ClientId} | Opts]; 386 | _ -> 387 | Opts 388 | end. 389 | 390 | enrich_port_opt(Opts) -> 391 | case proplists:get_value(port, Opts) of 392 | undefined -> 393 | Port = case proplists:get_value(ssl, Opts) of 394 | true -> 8883; 395 | false -> 1883 396 | end, 397 | [{port, Port} | Opts]; 398 | _ -> Opts 399 | end. 400 | 401 | pipeline([], Input) -> 402 | Input; 403 | 404 | pipeline([Fun|More], Input) -> 405 | pipeline(More, erlang:apply(Fun, [Input])). 406 | 407 | receive_loop(Client, Print) -> 408 | receive 409 | {'EXIT', Client, Reason} -> 410 | log_halt("Client down: ~p~n", [Reason]); 411 | {publish, #{payload := Payload}} -> 412 | case Print of 413 | "size" -> log("Received ~p bytes~n", [size(Payload)]); 414 | _ -> log("~s~n", [Payload]) 415 | end, 416 | receive_loop(Client, Print); 417 | ping -> 418 | emqtt:ping(Client), 419 | receive_loop(Client, Print); 420 | _Other -> 421 | receive_loop(Client, Print) 422 | end. 423 | 424 | i(true) -> 1; 425 | i(false) -> 0. 426 | 427 | log(Fmt, Args) -> 428 | io:format("~s " ++ Fmt, [ts() | Args]). 429 | 430 | -spec log_halt(_, _) -> no_return(). 431 | log_halt(Fmt, Args) -> 432 | log(Fmt, Args), 433 | halt(1). 434 | 435 | ts() -> 436 | SystemTime = erlang:system_time(millisecond), 437 | calendar:system_time_to_rfc3339(SystemTime, [{unit, millisecond}, {time_designator, $T}]). 438 | -------------------------------------------------------------------------------- /src/emqtt_inflight.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%------------------------------------------------------------------------- 16 | 17 | -module(emqtt_inflight). 18 | 19 | -include("emqtt.hrl"). 20 | 21 | -ifdef(TEST). 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -endif. 24 | 25 | -export([ new/1 26 | , empty/1 27 | , insert/3 28 | , update/3 29 | , delete/2 30 | , size/1 31 | , maxsize/1 32 | , capacity/1 33 | , is_full/1 34 | , is_empty/1 35 | , limit/2 36 | , trim_overflow/1 37 | , foreach/2 38 | , map/2 39 | , to_retry_list/2 40 | ]). 41 | 42 | -type(inflight() :: inflight(req())). 43 | 44 | -type(inflight(Req) :: #{max_inflight := maximum() | {maximum(), _Limit :: maximum()}, 45 | sent := sent(Req), 46 | seq := seq_no() 47 | }). 48 | 49 | -type(maximum() :: infinity | pos_integer()). 50 | 51 | -type(sent(Req) :: #{id() => {seq_no(), Req}}). 52 | 53 | -type(id() :: term()). 54 | 55 | -type(seq_no() :: pos_integer()). 56 | 57 | -type(req() :: term()). 58 | 59 | -export_type([inflight/1, inflight/0]). 60 | 61 | %%-------------------------------------------------------------------- 62 | %% APIs 63 | 64 | -spec(new(infinity | pos_integer()) -> inflight()). 65 | new(MaxInflight) -> 66 | #{max_inflight => MaxInflight, sent => #{}, seq => 1}. 67 | 68 | -spec(empty(inflight()) -> inflight()). 69 | empty(Inflight) -> 70 | Inflight#{sent := #{}, seq := 1}. 71 | 72 | -spec(insert(id(), req(), inflight()) -> error | {ok, inflight()}). 73 | insert(Id, Req, Inflight = #{max_inflight := MI, sent := Sent, seq := Seq}) -> 74 | case capacity(MI, Sent) of 75 | C when C =< 0 -> 76 | error; 77 | _ -> 78 | {ok, Inflight#{sent := maps:put(Id, {Seq, Req}, Sent), 79 | seq := Seq + 1}} 80 | end. 81 | 82 | -spec(update(id(), req(), inflight()) -> error | {ok, inflight()}). 83 | update(Id, Req, Inflight = #{sent := Sent}) -> 84 | case maps:find(Id, Sent) of 85 | error -> error; 86 | {ok, {No, _OldReq}} -> 87 | {ok, Inflight#{sent := maps:put(Id, {No, Req}, Sent)}} 88 | end. 89 | 90 | -spec(delete(id(), inflight()) -> error | {{value, req()}, inflight()}). 91 | delete(Id, Inflight = #{sent := Sent}) -> 92 | case maps:take(Id, Sent) of 93 | error -> error; 94 | {{_, Req}, Sent1} -> 95 | {{value, Req}, maybe_reset_seq(Inflight#{sent := Sent1})} 96 | end. 97 | 98 | -spec(size(inflight()) -> non_neg_integer()). 99 | size(#{sent := Sent}) -> 100 | maps:size(Sent). 101 | 102 | -spec(maxsize(inflight()) -> pos_integer() | infinity). 103 | maxsize(#{max_inflight := MI}) -> 104 | effective_max_inflight(MI). 105 | 106 | -spec(is_full(inflight()) -> boolean()). 107 | is_full(Inflight) -> 108 | Capacity = capacity(Inflight), 109 | Capacity =/= infinity andalso Capacity =< 0. 110 | 111 | -spec(limit(_Limit :: pos_integer() | infinity, inflight()) -> inflight()). 112 | limit(Limit, Inflight = #{max_inflight := {MI, _LimitWas}}) -> 113 | Inflight#{max_inflight := {MI, Limit}}; 114 | limit(Limit, Inflight = #{max_inflight := MI}) -> 115 | Inflight#{max_inflight := {MI, Limit}}. 116 | 117 | -spec(capacity(inflight()) -> integer()). 118 | capacity(#{max_inflight := MI, sent := Sent}) -> 119 | capacity(MI, Sent). 120 | 121 | capacity(infinity, _Sent) -> 122 | infinity; 123 | capacity(MI, Sent) when is_tuple(MI) -> 124 | capacity(effective_max_inflight(MI), Sent); 125 | capacity(Max, Sent) -> 126 | Max - maps:size(Sent). 127 | 128 | effective_max_inflight({MI, Limit}) -> 129 | min(MI, Limit); 130 | effective_max_inflight(MI) -> 131 | MI. 132 | 133 | -spec(trim_overflow(inflight()) -> {_Overflow :: [{id(), req()}], inflight()}). 134 | trim_overflow(Inflight = #{max_inflight := MI, sent := Sent}) -> 135 | case capacity(MI, Sent) of 136 | Negative when Negative < 0 -> 137 | {Overflow, Sent1} = trim_overflow(Sent, -Negative), 138 | {Overflow, Inflight#{sent := Sent1}}; 139 | _ -> 140 | {[], Inflight} 141 | end. 142 | 143 | trim_overflow(Sent, Overflow) -> 144 | Reqs = take_requests(maps:iterator(Sent), Overflow), 145 | Sent1 = lists:foldl(fun delete_request/2, Sent, Reqs), 146 | {Reqs, Sent1}. 147 | 148 | delete_request({Id, _Req}, Sent) -> 149 | maps:remove(Id, Sent). 150 | 151 | take_requests(_It, 0) -> 152 | []; 153 | take_requests(It, N) -> 154 | case maps:next(It) of 155 | {Id, {_SeqNo, Req}, It1} -> 156 | [{Id, Req} | take_requests(It1, N - 1)]; 157 | none -> 158 | [] 159 | end. 160 | 161 | -spec(is_empty(inflight()) -> boolean()). 162 | is_empty(#{sent := Sent}) -> 163 | maps:size(Sent) =< 0. 164 | 165 | %% @doc first in first evaluate 166 | -spec(foreach(F, inflight()) -> ok when 167 | F :: fun((id(), req()) -> ok)). 168 | foreach(F, #{sent := Sent}) -> 169 | lists:foreach( 170 | fun({Id, {_SeqNo, Req}}) -> F(Id, Req) end, 171 | sort_sent(Sent) 172 | ). 173 | 174 | -spec(map(F, inflight()) -> inflight() when 175 | F :: fun((id(), req()) -> {id(), req()})). 176 | map(F, Inflight = #{sent := Sent}) -> 177 | Sent1 = maps:fold( 178 | fun(Id, {SeqNo, Req}, Acc) -> 179 | {Id1, Req1} = F(Id, Req), 180 | Acc#{Id1 => {SeqNo, Req1}} 181 | end, #{}, Sent), 182 | Inflight#{sent := Sent1}. 183 | 184 | %% @doc Return a sorted list of Pred returned true 185 | -spec(to_retry_list(Pred, inflight()) -> list({id(), req()}) when 186 | Pred :: fun((id(), req()) -> boolean())). 187 | to_retry_list(Pred, #{sent := Sent}) -> 188 | Need = sort_sent(filter_sent(fun(Id, Req) -> Pred(Id, Req) end, Sent)), 189 | lists:map(fun({Id, {_SeqNo, Req}}) -> {Id, Req} end, Need). 190 | 191 | %%-------------------------------------------------------------------- 192 | %% Internal funcs 193 | 194 | filter_sent(F, Sent) -> 195 | maps:filter(fun(Id, {_SeqNo, Req}) -> F(Id, Req) end, Sent). 196 | 197 | %% @doc sort with seq 198 | sort_sent(Sent) -> 199 | Sort = fun({_Id1, {SeqNo1, _Req1}}, 200 | {_Id2, {SeqNo2, _Req2}}) -> 201 | SeqNo1 < SeqNo2 202 | end, 203 | lists:sort(Sort, maps:to_list(Sent)). 204 | 205 | %% @doc avoid integer overflows 206 | maybe_reset_seq(Inflight) -> 207 | case is_empty(Inflight) of 208 | true -> 209 | Inflight#{seq := 1}; 210 | false -> 211 | Inflight 212 | end. 213 | 214 | %%-------------------------------------------------------------------- 215 | %% tests 216 | 217 | -ifdef(TEST). 218 | 219 | insert_delete_test() -> 220 | Inflight = emqtt_inflight:new(2), 221 | {ok, Inflight1} = emqtt_inflight:insert(1, req1, Inflight), 222 | {ok, Inflight2} = emqtt_inflight:insert(2, req2, Inflight1), 223 | error = emqtt_inflight:insert(3, req3, Inflight2), 224 | error = emqtt_inflight:delete(3, Inflight), 225 | {{value, req2}, _} = emqtt_inflight:delete(2, Inflight2). 226 | 227 | update_test() -> 228 | Inflight = emqtt_inflight:new(2), 229 | {ok, Inflight1} = emqtt_inflight:insert(1, req1, Inflight), 230 | error = emqtt_inflight:update(2, req2, Inflight1), 231 | 232 | {ok, Inflight11} = emqtt_inflight:update(1, req11, Inflight1), 233 | {{value, req11}, _} = emqtt_inflight:delete(1, Inflight11). 234 | 235 | size_full_empty_test() -> 236 | Inflight = emqtt_inflight:new(1), 237 | 0 = emqtt_inflight:size(Inflight), 238 | true = emqtt_inflight:is_empty(Inflight), 239 | false = emqtt_inflight:is_full(Inflight), 240 | 241 | {ok, Inflight1} = emqtt_inflight:insert(1, req1, Inflight), 242 | 1 = emqtt_inflight:size(Inflight1), 243 | false = emqtt_inflight:is_empty(Inflight1), 244 | true = emqtt_inflight:is_full(Inflight1), 245 | 246 | false = emqtt_inflight:is_full(emqtt_inflight:new(infinity)), 247 | true = emqtt_inflight:is_empty(emqtt_inflight:new(infinity)). 248 | 249 | limit_full_empty_test() -> 250 | Inflight = emqtt_inflight:new(10), 251 | 0 = emqtt_inflight:size(Inflight), 252 | 10 = emqtt_inflight:maxsize(Inflight), 253 | true = emqtt_inflight:is_empty(Inflight), 254 | false = emqtt_inflight:is_full(Inflight), 255 | 256 | {ok, Inflight1} = emqtt_inflight:insert(1, req1, Inflight), 257 | 1 = emqtt_inflight:size(Inflight1), 258 | false = emqtt_inflight:is_empty(Inflight1), 259 | false = emqtt_inflight:is_full(Inflight1), 260 | 261 | Inflight2 = emqtt_inflight:limit(1, Inflight1), 262 | 1 = emqtt_inflight:maxsize(Inflight2), 263 | false = emqtt_inflight:is_empty(Inflight2), 264 | true = emqtt_inflight:is_full(Inflight2), 265 | error = emqtt_inflight:insert(2, req2, Inflight2), 266 | 267 | Inflight3 = emqtt_inflight:limit(infinity, Inflight2), 268 | false = emqtt_inflight:is_full(Inflight3), 269 | {ok, Inflight4} = emqtt_inflight:insert(2, req2, Inflight3), 270 | 8 = emqtt_inflight:capacity(Inflight4). 271 | 272 | limit_trim_test() -> 273 | Inflight = emqtt_inflight:new(infinity), 274 | false = emqtt_inflight:is_full(Inflight), 275 | 276 | {ok, Inflight1} = emqtt_inflight:insert(1, req1, Inflight), 277 | {ok, Inflight2} = emqtt_inflight:insert(2, req2, Inflight1), 278 | {ok, Inflight3} = emqtt_inflight:insert(3, req3, Inflight2), 279 | {ok, Inflight4} = emqtt_inflight:insert(4, req4, Inflight3), 280 | Inflight5 = emqtt_inflight:limit(2, Inflight4), 281 | true = emqtt_inflight:is_full(Inflight5), 282 | -2 = emqtt_inflight:capacity(Inflight5), 283 | {Overflow, Inflight6} = emqtt_inflight:trim_overflow(Inflight5), 284 | [_, _] = Overflow, 285 | true = emqtt_inflight:is_full(Inflight6), 286 | 0 = emqtt_inflight:capacity(Inflight6). 287 | 288 | foreach_test() -> 289 | emqtt_inflight:foreach( 290 | fun(Id, Req) -> 291 | true = (Id =:= Req) 292 | end, inflight_example()). 293 | 294 | map_test() -> 295 | Inflight1 = emqtt_inflight:map( 296 | fun(Id, Req) -> {Id + 1, Req + 1} end, 297 | inflight_example() 298 | ), 299 | [{2, 2}, {3, 3}] = emqtt_inflight:to_retry_list(fun(_, _) -> true end, Inflight1). 300 | 301 | retry_test() -> 302 | [{"sorted by insert sequence", 303 | [{1, 1}, {2, 2}] = emqtt_inflight:to_retry_list( 304 | fun(Id, Req) -> Id =:= Req end, 305 | inflight_example() 306 | ) 307 | }, 308 | {"filter", 309 | [{2, 2}] = emqtt_inflight:to_retry_list( 310 | fun(Id, _Req) -> Id =:= 2 end, 311 | inflight_example()) 312 | }]. 313 | 314 | reset_seq_test() -> 315 | Inflight = emqtt_inflight:new(infinity), 316 | #{seq := 1} = Inflight, 317 | 318 | {ok, Inflight1} = emqtt_inflight:insert(1, req1, Inflight), 319 | #{seq := 2} = Inflight1, 320 | 321 | {_, Inflight2} = emqtt_inflight:delete(1, Inflight1), 322 | 323 | %% reset seq to 1 once inflight is empty 324 | true = emqtt_inflight:is_empty(Inflight2), 325 | #{seq := 1} = Inflight2. 326 | 327 | inflight_example() -> 328 | {ok, Inflight} = emqtt_inflight:insert(1, 1, emqtt_inflight:new(infinity)), 329 | {ok, Inflight1} = emqtt_inflight:insert(2, 2, Inflight), 330 | Inflight1. 331 | 332 | -endif. 333 | -------------------------------------------------------------------------------- /src/emqtt_internal.hrl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------- 2 | %% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%------------------------------------------------------------------------- 16 | 17 | -record(ssl_socket, {tcp, ssl}). 18 | 19 | -define(IS_QoE, (get(qoe) =/= undefined)). 20 | -------------------------------------------------------------------------------- /src/emqtt_props.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------- 2 | %% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%------------------------------------------------------------------------- 16 | 17 | %% @doc MQTT5 Properties 18 | -module(emqtt_props). 19 | 20 | -include("emqtt.hrl"). 21 | 22 | -export([ id/1 23 | , name/1 24 | , filter/2 25 | , validate/1 26 | ]). 27 | 28 | %% For tests 29 | -export([all/0]). 30 | 31 | -type(prop_name() :: atom()). 32 | -type(prop_id() :: pos_integer()). 33 | 34 | -define(PROPS_TABLE, 35 | #{16#01 => {'Payload-Format-Indicator', 'Byte', [?PUBLISH]}, 36 | 16#02 => {'Message-Expiry-Interval', 'Four-Byte-Integer', [?PUBLISH]}, 37 | 16#03 => {'Content-Type', 'UTF8-Encoded-String', [?PUBLISH]}, 38 | 16#08 => {'Response-Topic', 'UTF8-Encoded-String', [?PUBLISH]}, 39 | 16#09 => {'Correlation-Data', 'Binary-Data', [?PUBLISH]}, 40 | 16#0B => {'Subscription-Identifier', 'Variable-Byte-Integer', [?PUBLISH, ?SUBSCRIBE]}, 41 | 16#11 => {'Session-Expiry-Interval', 'Four-Byte-Integer', [?CONNECT, ?CONNACK, ?DISCONNECT]}, 42 | 16#12 => {'Assigned-Client-Identifier', 'UTF8-Encoded-String', [?CONNACK]}, 43 | 16#13 => {'Server-Keep-Alive', 'Two-Byte-Integer', [?CONNACK]}, 44 | 16#15 => {'Authentication-Method', 'UTF8-Encoded-String', [?CONNECT, ?CONNACK, ?AUTH]}, 45 | 16#16 => {'Authentication-Data', 'Binary-Data', [?CONNECT, ?CONNACK, ?AUTH]}, 46 | 16#17 => {'Request-Problem-Information', 'Byte', [?CONNECT]}, 47 | 16#18 => {'Will-Delay-Interval', 'Four-Byte-Integer', ['WILL']}, 48 | 16#19 => {'Request-Response-Information', 'Byte', [?CONNECT]}, 49 | 16#1A => {'Response-Information', 'UTF8-Encoded-String', [?CONNACK]}, 50 | 16#1C => {'Server-Reference', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT]}, 51 | 16#1F => {'Reason-String', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT, ?PUBACK, 52 | ?PUBREC, ?PUBREL, ?PUBCOMP, 53 | ?SUBACK, ?UNSUBACK, ?AUTH]}, 54 | 16#21 => {'Receive-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, 55 | 16#22 => {'Topic-Alias-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, 56 | 16#23 => {'Topic-Alias', 'Two-Byte-Integer', [?PUBLISH]}, 57 | 16#24 => {'Maximum-QoS', 'Byte', [?CONNACK]}, 58 | 16#25 => {'Retain-Available', 'Byte', [?CONNACK]}, 59 | 16#26 => {'User-Property', 'UTF8-String-Pair', 'ALL'}, 60 | 16#27 => {'Maximum-Packet-Size', 'Four-Byte-Integer', [?CONNECT, ?CONNACK]}, 61 | 16#28 => {'Wildcard-Subscription-Available', 'Byte', [?CONNACK]}, 62 | 16#29 => {'Subscription-Identifier-Available', 'Byte', [?CONNACK]}, 63 | 16#2A => {'Shared-Subscription-Available', 'Byte', [?CONNACK]} 64 | }). 65 | 66 | -spec(id(prop_name()) -> prop_id()). 67 | id('Payload-Format-Indicator') -> 16#01; 68 | id('Message-Expiry-Interval') -> 16#02; 69 | id('Content-Type') -> 16#03; 70 | id('Response-Topic') -> 16#08; 71 | id('Correlation-Data') -> 16#09; 72 | id('Subscription-Identifier') -> 16#0B; 73 | id('Session-Expiry-Interval') -> 16#11; 74 | id('Assigned-Client-Identifier') -> 16#12; 75 | id('Server-Keep-Alive') -> 16#13; 76 | id('Authentication-Method') -> 16#15; 77 | id('Authentication-Data') -> 16#16; 78 | id('Request-Problem-Information') -> 16#17; 79 | id('Will-Delay-Interval') -> 16#18; 80 | id('Request-Response-Information') -> 16#19; 81 | id('Response-Information') -> 16#1A; 82 | id('Server-Reference') -> 16#1C; 83 | id('Reason-String') -> 16#1F; 84 | id('Receive-Maximum') -> 16#21; 85 | id('Topic-Alias-Maximum') -> 16#22; 86 | id('Topic-Alias') -> 16#23; 87 | id('Maximum-QoS') -> 16#24; 88 | id('Retain-Available') -> 16#25; 89 | id('User-Property') -> 16#26; 90 | id('Maximum-Packet-Size') -> 16#27; 91 | id('Wildcard-Subscription-Available') -> 16#28; 92 | id('Subscription-Identifier-Available') -> 16#29; 93 | id('Shared-Subscription-Available') -> 16#2A; 94 | id(Name) -> error({bad_property, Name}). 95 | 96 | -spec(name(prop_id()) -> prop_name()). 97 | name(16#01) -> 'Payload-Format-Indicator'; 98 | name(16#02) -> 'Message-Expiry-Interval'; 99 | name(16#03) -> 'Content-Type'; 100 | name(16#08) -> 'Response-Topic'; 101 | name(16#09) -> 'Correlation-Data'; 102 | name(16#0B) -> 'Subscription-Identifier'; 103 | name(16#11) -> 'Session-Expiry-Interval'; 104 | name(16#12) -> 'Assigned-Client-Identifier'; 105 | name(16#13) -> 'Server-Keep-Alive'; 106 | name(16#15) -> 'Authentication-Method'; 107 | name(16#16) -> 'Authentication-Data'; 108 | name(16#17) -> 'Request-Problem-Information'; 109 | name(16#18) -> 'Will-Delay-Interval'; 110 | name(16#19) -> 'Request-Response-Information'; 111 | name(16#1A) -> 'Response-Information'; 112 | name(16#1C) -> 'Server-Reference'; 113 | name(16#1F) -> 'Reason-String'; 114 | name(16#21) -> 'Receive-Maximum'; 115 | name(16#22) -> 'Topic-Alias-Maximum'; 116 | name(16#23) -> 'Topic-Alias'; 117 | name(16#24) -> 'Maximum-QoS'; 118 | name(16#25) -> 'Retain-Available'; 119 | name(16#26) -> 'User-Property'; 120 | name(16#27) -> 'Maximum-Packet-Size'; 121 | name(16#28) -> 'Wildcard-Subscription-Available'; 122 | name(16#29) -> 'Subscription-Identifier-Available'; 123 | name(16#2A) -> 'Shared-Subscription-Available'; 124 | name(Id) -> error({unsupported_property, Id}). 125 | 126 | filter(PacketType, Props) when is_map(Props) -> 127 | maps:from_list(filter(PacketType, maps:to_list(Props))); 128 | 129 | filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_list(Props) -> 130 | Filter = fun(Name) -> 131 | case maps:find(id(Name), ?PROPS_TABLE) of 132 | {ok, {Name, _Type, 'ALL'}} -> 133 | true; 134 | {ok, {Name, _Type, AllowedTypes}} -> 135 | lists:member(PacketType, AllowedTypes); 136 | error -> false 137 | end 138 | end, 139 | [Prop || Prop = {Name, _} <- Props, Filter(Name)]. 140 | 141 | validate(Props) when is_map(Props) -> 142 | lists:foreach(fun validate_prop/1, maps:to_list(Props)). 143 | 144 | validate_prop(Prop = {Name, Val}) -> 145 | case maps:find(id(Name), ?PROPS_TABLE) of 146 | {ok, {Name, Type, _}} -> 147 | validate_value(Type, Val) 148 | orelse error(bad_property, Prop); 149 | error -> 150 | error({bad_property, Prop}) 151 | end. 152 | 153 | validate_value('Byte', Val) -> 154 | is_integer(Val) andalso Val =< 16#FF; 155 | validate_value('Two-Byte-Integer', Val) -> 156 | is_integer(Val) andalso 0 =< Val andalso Val =< 16#FFFF; 157 | validate_value('Four-Byte-Integer', Val) -> 158 | is_integer(Val) andalso 0 =< Val andalso Val =< 16#FFFFFFFF; 159 | validate_value('Variable-Byte-Integer', Val) -> 160 | is_integer(Val) andalso 0 =< Val andalso Val =< 16#7FFFFFFF; 161 | validate_value('UTF8-String-Pair', {Name, Val}) -> 162 | validate_value('UTF8-Encoded-String', Name) 163 | andalso validate_value('UTF8-Encoded-String', Val); 164 | validate_value('UTF8-String-Pair', Pairs) when is_list(Pairs) -> 165 | lists:foldl(fun(Pair, OK) -> 166 | OK andalso validate_value('UTF8-String-Pair', Pair) 167 | end, true, Pairs); 168 | validate_value('UTF8-Encoded-String', Val) -> 169 | is_binary(Val); 170 | validate_value('Binary-Data', Val) -> 171 | is_binary(Val); 172 | validate_value(_Type, _Val) -> false. 173 | 174 | -spec(all() -> map()). 175 | all() -> ?PROPS_TABLE. 176 | 177 | -------------------------------------------------------------------------------- /src/emqtt_quic.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------- 2 | %% Copyright (c) 2021-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%------------------------------------------------------------------------- 16 | 17 | -module(emqtt_quic). 18 | -ifndef(BUILD_WITHOUT_QUIC). 19 | -include("logger.hrl"). 20 | -include("emqtt.hrl"). 21 | -include_lib("quicer/include/quicer.hrl"). 22 | 23 | 24 | -define(LOG(Level, Msg, Meta, State), 25 | ?SLOG(Level, Meta#{msg => Msg, clientid => maps:get(clientid, State)}, #{})). 26 | 27 | -export([ connect/4 28 | , send/2 29 | , recv/2 30 | , close/1 31 | , open_connection/0 32 | ]). 33 | 34 | -export([ setopts/2 35 | , getstat/2 36 | , sockname/1 37 | ]). 38 | 39 | -export([init_state/1]). 40 | 41 | %% state machine callback1 42 | -export([handle_info/3]). 43 | 44 | -export_type([ mqtt_packets/0 45 | , quic_msg/0 46 | , quic_sock/0 47 | , cb_data/0 48 | ]). 49 | 50 | -type cb_data() :: #{ clientid := binary() 51 | , connection_parse_state := emqtt_frame:parse_state() 52 | , stream_parse_state := #{ quic_sock() => emqtt_frame:parse_state() } 53 | , data_stream_socks := [quic_sock()] 54 | , control_stream_sock := undefined | quic_sock() 55 | , stream_opts := map() 56 | , state_name := atom() 57 | , is_local => boolean() 58 | , is_unidir => boolean() 59 | , quic_conn_cb => module() 60 | , quic_stream_cb => module() 61 | , reconnect => boolean() 62 | , peer_bidi_stream_count => non_neg_integer() 63 | , peer_unidi_stream_count => non_neg_integer() 64 | }. 65 | -type mqtt_packets() :: [#mqtt_packet{}] | []. 66 | -type quic_sock() :: {quic, quicer:connection_handle(), quicer:stream_handle()}. 67 | -type quic_msg() :: {quic, atom() | binary(), Resource::any(), Props::any()}. 68 | 69 | 70 | -spec init_state(map()) -> cb_data(). 71 | init_state(#{ state_name := _S} = OldData) -> 72 | OldData; 73 | init_state(Data) when is_map(Data) -> 74 | Data#{ quic_conn_cb => emqtt_quic_connection 75 | , quic_stream_cb => emqtt_quic_stream 76 | , state_name => init 77 | , is_local => true %% @TODO per stream 78 | , is_unidir => false %% @TODO per stream 79 | }. 80 | 81 | -spec handle_info(quic_msg(), atom(), cb_data()) -> gen_statem:handle_event_result(). 82 | %% Handle Quic Data 83 | handle_info({quic, Data, Stream, Props}, StateName, #{quic_stream_cb := StreamCB} = CBState) 84 | when is_binary(Data) -> 85 | StreamCB:handle_stream_data(Stream, Data, Props, CBState#{state_name := StateName} ); 86 | handle_info({quic, Event, Connection, Props}, StateName, #{quic_conn_cb := ConnCB} = CBState) 87 | when connected =:= Event orelse 88 | transport_shutdown =:= Event orelse 89 | shutdown =:= Event orelse 90 | closed =:= Event orelse 91 | local_address_changed =:= Event orelse 92 | peer_address_changed =:= Event orelse 93 | streams_available =:= Event orelse 94 | peer_needs_streams =:= Event orelse 95 | dgram_state_changed =:= Event orelse 96 | nst_received =:= Event -> 97 | ConnCB:Event(Connection, Props, CBState#{state_name := StateName}); 98 | handle_info({quic, Event, Stream, Props}, StateName, #{quic_stream_cb := StreamCB} = CBState) 99 | when start_completed =:= Event orelse 100 | send_complete =:= Event orelse 101 | peer_send_shutdown =:= Event orelse 102 | peer_send_aborted =:= Event orelse 103 | peer_receive_aborted =:= Event orelse 104 | send_shutdown_complete =:= Event orelse 105 | stream_closed =:= Event orelse 106 | peer_accepted =:= Event orelse 107 | passive =:= Event -> 108 | StreamCB:Event(Stream, Props, CBState#{state_name := StateName}). 109 | 110 | open_connection() -> 111 | quicer:open_connection(). 112 | 113 | connect(Host, Port, Opts, Timeout) -> 114 | {ConnOpts, StreamOpts} = from_sockopts(Opts), 115 | case maps:is_key(nst, ConnOpts) of 116 | true -> do_0rtt_connect(Host, Port, ConnOpts, StreamOpts); 117 | false -> do_1rtt_connect(Host, Port, ConnOpts, StreamOpts, Timeout) 118 | end. 119 | 120 | do_0rtt_connect(Host, Port, ConnOpts, StreamOpts) -> 121 | IsConnOpened = maps:is_key(handle, ConnOpts), 122 | case quicer:async_connect(Host, Port, ConnOpts) of 123 | {ok, Conn} when not IsConnOpened -> 124 | case quicer:start_stream(Conn, StreamOpts) of 125 | {ok, Stream} -> 126 | {ok, {quic, Conn, Stream}}; 127 | {error, Type, Info} -> 128 | {error, {Type, Info}}; 129 | Error -> 130 | Error 131 | end; 132 | {ok, _Conn} -> 133 | skip; 134 | {error, _} = Error -> 135 | Error 136 | end. 137 | 138 | do_1rtt_connect(Host, Port, ConnOpts, StreamOpts, Timeout) -> 139 | IsConnOpened = maps:is_key(handle, ConnOpts), 140 | case quicer:connect(Host, Port, ConnOpts, Timeout) of 141 | {ok, Conn} when not IsConnOpened -> 142 | case quicer:start_stream(Conn, StreamOpts) of 143 | {ok, Stream} -> 144 | {ok, {quic, Conn, Stream}}; 145 | {error, Type, Info} -> 146 | {error, {Type, Info}}; 147 | Error -> 148 | Error 149 | end; 150 | {ok, _Conn} -> 151 | skip; 152 | {error, transport_down, Reason} -> 153 | {error, {transport_down, Reason}}; 154 | {error, _} = Error -> 155 | Error 156 | end. 157 | 158 | send({quic, _Conn, Stream}, Bin) -> 159 | send(Stream, Bin); 160 | send(Stream, Bin) -> 161 | %% Use async here because we could send before start the connection. 162 | case quicer:async_send(Stream, Bin) of 163 | {ok, _Len} -> 164 | ok; 165 | {error, ErrorType, Reason} -> 166 | {error, {ErrorType, Reason}}; 167 | Other -> 168 | Other 169 | end. 170 | 171 | recv({quic, _Conn, Stream}, Count) -> 172 | quicer:recv(Stream, Count). 173 | 174 | getstat({quic, Conn, _Stream}, Options) -> 175 | quicer:getstat(Conn, Options). 176 | 177 | setopts({quic, _Conn, Stream}, Opts) -> 178 | [ ok = quicer:setopt(Stream, Opt, OptV) 179 | || {Opt, OptV} <- Opts ], 180 | ok. 181 | 182 | close({quic, Conn, Stream}) -> 183 | %% gracefully shutdown the stream to flush all the msg in sndbuf. 184 | _ = quicer:shutdown_stream(Stream, 500), 185 | quicer:close_connection(Conn, ?QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0, 500). 186 | 187 | sockname({quic, Conn, _Stream}) -> 188 | quicer:sockname(Conn). 189 | 190 | local_addr(SOpts) -> 191 | case { proplists:get_value(port, SOpts, 0), 192 | proplists:get_value(ip, SOpts, undefined)} of 193 | {0, undefined} -> 194 | []; 195 | {Port, undefined} -> 196 | [{param_conn_local_address, ":" ++ integer_to_list(Port)}]; 197 | {Port, IpAddr} when is_tuple(IpAddr) -> 198 | [{param_conn_local_address, inet:ntoa(IpAddr) ++ ":" ++integer_to_list(Port)}] 199 | end. 200 | 201 | ssl_opts(SOpts) -> 202 | proplists:get_value(ssl_opts, SOpts, []). 203 | 204 | -spec from_sockopts(proplists:proplist()) -> {ConnOpts::map(), StreamOpts::map()}. 205 | from_sockopts(SockOpts) -> 206 | {UserConnOpts0, UserStreamOpts0} = proplists:get_value(quic_opts, SockOpts, {[], []}), 207 | %% Mandatory defaults 208 | KeepAlive = proplists:get_value(keepalive, SockOpts, 60), 209 | DefConnOpts = [ {alpn, ["mqtt"]} 210 | , {idle_timeout_ms, timer:seconds(KeepAlive * 3)} 211 | , {peer_unidi_stream_count, 1} 212 | , {peer_bidi_stream_count, 3} 213 | , {verify, proplists:get_value(verify, SockOpts, verify_none)} 214 | , {quic_event_mask, ?QUICER_CONNECTION_EVENT_MASK_NST}], 215 | %% Deprecated but for backward compatibility 216 | OptionalOpts = lists:filter(fun({handle, _}) -> true; 217 | ({nst, _}) -> true; 218 | (_) -> false 219 | end, SockOpts), 220 | QuicConnOpts = maps:from_list(DefConnOpts 221 | ++ ssl_opts(SockOpts) 222 | ++ local_addr(SockOpts) 223 | ++ OptionalOpts 224 | ++ UserConnOpts0), 225 | QuicStremOpts = maps:from_list([{active, 1} | UserStreamOpts0]), 226 | {QuicConnOpts, QuicStremOpts}. 227 | 228 | -else. 229 | %% BUILD_WITHOUT_QUIC 230 | -endif. 231 | -------------------------------------------------------------------------------- /src/emqtt_quic_connection.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(emqtt_quic_connection). 17 | 18 | -ifndef(BUILD_WITHOUT_QUIC). 19 | -include("emqtt.hrl"). 20 | -include("logger.hrl"). 21 | 22 | 23 | -include_lib("quicer/include/quicer.hrl"). 24 | -include_lib("snabbkaffe/include/snabbkaffe.hrl"). 25 | 26 | 27 | %% Callback init 28 | -export([ init/1 ]). 29 | 30 | %% Connection Callbacks 31 | -export([ new_conn/3 32 | , connected/3 33 | , transport_shutdown/3 34 | , shutdown/3 35 | , closed/3 36 | , local_address_changed/3 37 | , peer_address_changed/3 38 | , streams_available/3 39 | , peer_needs_streams/3 40 | , nst_received/3 41 | , new_stream/3 42 | , dgram_state_changed/3 43 | ]). 44 | 45 | -define(LOG(Level, Msg, Meta, State), 46 | ?SLOG(Level, Meta#{msg => Msg, clientid => maps:get(clientid, State)}, #{})). 47 | 48 | -type cb_ret() :: gen_statem:handle_event_result(). 49 | -type cb_data() :: emqtt_quic:cb_data(). 50 | -type connection_handle() :: quicer:connection_handle(). 51 | 52 | %% Not in use. 53 | init(ConnOpts) when is_list(ConnOpts) -> 54 | init(maps:from_list(ConnOpts)); 55 | init(#{stream_opts := SOpts} = S) when is_list(SOpts) -> 56 | init(S#{stream_opts := maps:from_list(SOpts)}); 57 | init(ConnOpts) when is_map(ConnOpts) -> 58 | {ok, ConnOpts}. 59 | 60 | -spec closed(connection_handle(), quicer:conn_closed_props(), cb_data()) -> cb_ret(). 61 | closed(_Conn, #{} = _Flags, #{state_name := waiting_for_connack, reconnect := true} = S) -> 62 | ?LOG(error, "quic_connection_closed_reconnect", #{}, S), 63 | keep_state_and_data; 64 | closed(_Conn, #{} = _Flags, #{state_name := _Other} = S)-> 65 | ?LOG(error, "quic_connection_closed", #{}, S), 66 | %% @TODO why not stop? 67 | {stop, {shutdown, conn_closed}, S}. 68 | 69 | -spec new_conn(connection_handle(), quicer:new_conn_props(), cb_data()) -> cb_ret(). 70 | new_conn(_Conn, #{version := _Vsn}, #{stream_opts := _SOpts} = _S) -> 71 | {stop, not_server}. 72 | 73 | -spec nst_received(connection_handle(), binary(), cb_data()) -> cb_ret(). 74 | nst_received(_Conn, Ticket, #{clientid := Cid} = S) when is_binary(Ticket) -> 75 | catch ets:insert(quic_clients_nsts, {Cid, Ticket}), 76 | {keep_state, S#{nst => Ticket}}. 77 | 78 | -spec new_stream(quicer:stream_handle(), quicer:new_stream_props(), quicer:connection_handle()) 79 | -> cb_ret(). 80 | %% handles stream when there is no stream acceptors. 81 | new_stream(_Stream, #{is_orphan := true} = _StreamProps, 82 | %% @TODO put conn ? 83 | _Handle) -> 84 | %% Remote stream from the broker, unsupported. 85 | {stop, unsupp}. 86 | 87 | -spec shutdown(connection_handle(), quicer:error_code(), cb_data()) -> cb_ret(). 88 | shutdown(_Conn, _ErrorCode, #{state_name := waiting_for_connack, reconnect := true} = _S) -> 89 | keep_state_and_data; 90 | shutdown(Conn, _ErrorCode, #{reconnect := true}) -> 91 | _ = quicer:async_shutdown_connection(Conn, 0, 0), 92 | %% @TODO how to reconnect here? 93 | {keep_state_and_data, {next_event, info, {quic_closed, Conn}}}; 94 | shutdown(Conn, ErrorCode, S) -> 95 | ok = quicer:async_close_connection(Conn), 96 | ?LOG(info, "quic_peer_conn_shutdown", #{error_code => ErrorCode}, S), 97 | %% TCP return {shutdown, closed} 98 | case ErrorCode of 99 | success -> 100 | %% @TODO we should expect quicer/EMQX to send a custom error code instead. 101 | %% {stop, normal, S}. 102 | {stop, {shutdown, normal}, S}; 103 | _Other -> 104 | {stop, {shutdown, ErrorCode}, S} 105 | end. 106 | 107 | -spec transport_shutdown(connection_handle(), quicer:transport_shutdown_props(), cb_data()) 108 | -> cb_ret(). 109 | transport_shutdown(_C, DownInfo, S) -> 110 | ?LOG(error, "quic_transport_shutdown", #{down_info => DownInfo}, S), 111 | keep_state_and_data. 112 | 113 | -spec peer_address_changed(connection_handle(), quicer:quicer_addr(), cb_data()) -> cb_ret(). 114 | peer_address_changed(_C, _NewAddr, _S) -> 115 | keep_state_and_data. 116 | 117 | -spec local_address_changed(connection_handle(), quicer:quicer_addr(), cb_data()) -> cb_ret(). 118 | local_address_changed(_C, _NewAddr, _S) -> 119 | keep_state_and_data. 120 | 121 | -spec streams_available(connection_handle(), quicer:streams_available_props(), cb_data()) 122 | -> cb_ret(). 123 | streams_available(_C, #{ unidi_streams := UnidirCnt 124 | , bidi_streams := BidirCnt }, S) -> 125 | {keep_state, S#{ unidi_streams => UnidirCnt 126 | , bidi_streams => BidirCnt}}. 127 | 128 | %% @doc May integrate with App flow control 129 | -spec peer_needs_streams(connection_handle(), undefined, cb_data()) -> cb_ret(). 130 | peer_needs_streams(_C, undefined, _S) -> 131 | keep_state_and_data. 132 | 133 | -spec connected(connection_handle(), quicer:connected_props(), cb_data()) -> cb_ret(). 134 | %% handles async 0-RTT connect 135 | connected(_Connecion, #{ is_resumed := true }, #{state_name := waiting_for_connack} = _S) -> 136 | keep_state_and_data; 137 | connected(_Connecion, _Props, _S) -> 138 | keep_state_and_data. 139 | 140 | -spec dgram_state_changed(connection_handle(), quicer:dgram_state(), cb_data()) -> cb_ret(). 141 | dgram_state_changed(_C, _State, _S) -> 142 | keep_state_and_data. 143 | -else. 144 | %% BUILD_WITHOUT_QUIC 145 | -endif. 146 | -------------------------------------------------------------------------------- /src/emqtt_quic_stream.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------- 2 | %% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%------------------------------------------------------------------------- 16 | -module(emqtt_quic_stream). 17 | 18 | -ifndef(BUILD_WITHOUT_QUIC). 19 | -include_lib("quicer/include/quicer.hrl"). 20 | -export([ init_handoff/4 21 | , new_stream/3 22 | , start_completed/3 23 | , send_complete/3 24 | , peer_send_shutdown/3 25 | , peer_send_aborted/3 26 | , peer_receive_aborted/3 27 | , send_shutdown_complete/3 28 | , stream_closed/3 29 | , peer_accepted/3 30 | , passive/3 31 | , handle_call/4 32 | ]). 33 | 34 | -export([handle_stream_data/4]). 35 | 36 | -include_lib("quicer/include/quicer_types.hrl"). 37 | -include_lib("snabbkaffe/include/snabbkaffe.hrl"). 38 | -include("emqtt.hrl"). 39 | -include("logger.hrl"). 40 | 41 | 42 | -define(LOG(Level, Msg, Meta, State), 43 | ?SLOG(Level, Meta#{msg => Msg, clientid => maps:get(clientid, State)}, #{})). 44 | 45 | -type cb_ret() :: gen_statem:handle_event_result(). 46 | -type cb_data() :: emqtt_quic:cb_data(). 47 | 48 | -spec init_handoff(stream_handle(), #{}, quicer:connection_handle(), #{}) -> cb_ret(). 49 | init_handoff(_Stream, _StreamOpts, _Conn, _Flags) -> 50 | %% stream owner already set while starts. 51 | {stop, unimpl}. 52 | 53 | -spec new_stream(stream_handle(), quicer:new_stream_props(), connection_handle()) -> cb_ret(). 54 | new_stream(_Stream, #{flags := _Flags, is_orphan := _IsOrphan}, _Conn) -> 55 | {stop, unimpl}. 56 | 57 | -spec peer_accepted(stream_handle(), undefined, cb_data()) -> cb_ret(). 58 | peer_accepted(_Stream, undefined, _S) -> 59 | %% We just ignore it 60 | keep_state_and_data. 61 | 62 | -spec peer_receive_aborted(stream_handle(), non_neg_integer(), cb_data()) -> cb_ret(). 63 | peer_receive_aborted(Stream, ErrorCode, #{is_unidir := false} = _S) -> 64 | %% we abort send with same reason 65 | _ = quicer:async_shutdown_stream(Stream, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND, ErrorCode), 66 | keep_state_and_data; 67 | peer_receive_aborted(Stream, ErrorCode, #{is_unidir := true, is_local := true} = _S) -> 68 | _ = quicer:async_shutdown_stream(Stream, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND, ErrorCode), 69 | keep_state_and_data. 70 | 71 | -spec peer_send_aborted(stream_handle(), non_neg_integer(), cb_data()) -> cb_ret(). 72 | peer_send_aborted(Stream, ErrorCode, #{is_unidir := false} = _S) -> 73 | %% we abort receive with same reason 74 | _ = quicer:async_shutdown_stream(Stream, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE, ErrorCode), 75 | keep_state_and_data; 76 | peer_send_aborted(Stream, ErrorCode, #{is_unidir := true, is_local := false} = _S) -> 77 | _ = quicer:async_shutdown_stream(Stream, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE, ErrorCode), 78 | keep_state_and_data. 79 | 80 | -spec peer_send_shutdown(stream_handle(), undefined, cb_data()) -> cb_ret(). 81 | peer_send_shutdown(Stream, undefined, _S) -> 82 | _ = quicer:async_shutdown_stream(Stream, ?QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0), 83 | keep_state_and_data. 84 | 85 | -spec send_complete(stream_handle(), boolean(), cb_data()) -> cb_ret(). 86 | send_complete(_Stream, false, _S) -> 87 | keep_state_and_data; 88 | send_complete(_Stream, true = _IsCanceled, S) -> 89 | %% @TODO bump counter 90 | ?LOG(error, "quic_stream_send_canceled", #{}, S), 91 | keep_state_and_data. 92 | 93 | -spec send_shutdown_complete(stream_handle(), boolean(), cb_data()) -> cb_ret(). 94 | send_shutdown_complete(_Stream, _IsGraceful, _S) -> 95 | keep_state_and_data. 96 | 97 | -spec start_completed(stream_handle(), quicer:stream_start_completed_props(), cb_data()) 98 | -> cb_ret(). 99 | start_completed(_Stream, #{status := success, stream_id := StreamId} = Prop, S) -> 100 | ?LOG(debug, "quic_stream_start_completed", Prop, S), 101 | {ok, S#{stream_id => StreamId}}; 102 | start_completed(_Stream, #{status := stream_limit_reached, stream_id := _StreamId} = Prop, S) -> 103 | ?LOG(error, "quic_stream_start_failed", Prop, S), 104 | {stop, stream_limit_reached}; 105 | start_completed(_Stream, #{status := Other } = Prop, S) -> 106 | ?LOG(error, "quic_stream_start_failed", Prop, S), 107 | %% or we could retry? 108 | {stop, {start_fail, Other}, S}. 109 | 110 | %% Local stream, Unidir 111 | -spec handle_stream_data(stream_handle(), binary(), quicer:recv_data_props(), cb_data()) 112 | -> cb_ret(). 113 | handle_stream_data(Stream, Bin, _Flags, #{ is_local := true 114 | , control_stream_sock := {quic, Conn, _ControlStream} 115 | , stream_parse_state := PSS} = S) -> 116 | ?LOG(debug, "recv_data", #{data => Bin}, S), 117 | Via = {quic, Conn, Stream}, 118 | case maps:get(Via, PSS, undefined) of 119 | undefined -> 120 | ?LOG(warning, "unknown stream data", #{stream => Stream}, S), 121 | {stop, {unknown_stream_data, Stream}, S}; 122 | PS -> 123 | case parse(Bin, PS, []) of 124 | {keep_state, NewPS, Packets} -> 125 | {keep_state, S#{stream_parse_state := maps:update(Via, NewPS, PSS)}, 126 | [{next_event, cast, {P, Via} } 127 | || P <- lists:reverse(Packets)]}; 128 | {stop, _} = Stop -> 129 | Stop 130 | end 131 | end; 132 | %% Remote stream 133 | handle_stream_data(_Stream, _Bin, _Flags, 134 | #{is_local := false, is_unidir := true, conn := _Conn} = _S) -> 135 | {stop, unimpl}. 136 | 137 | 138 | -spec passive(stream_handle(), undefined, cb_data()) -> cb_ret(). 139 | passive(Stream, undefined, _S)-> 140 | %% @TODO Should be called only once during the whole connecion 141 | _ = quicer:setopt(Stream, active, true), 142 | keep_state_and_data. 143 | 144 | -spec stream_closed(stream_handle(), stream_closed_props(), cb_data()) -> cb_ret(). 145 | stream_closed(CtrlStream, #{ is_conn_shutdown := _ }, 146 | #{reconnect := true, control_stream_sock := {quic, Conn, CtrlStream}}) -> 147 | {keep_state_and_data, {next_event, info, {quic_closed, Conn}}}; 148 | stream_closed(_Stream, #{ is_conn_shutdown := _ }, #{reconnect := true}) -> 149 | keep_state_and_data; 150 | stream_closed(Stream, P = #{ is_conn_shutdown := IsConnShutdown 151 | , is_app_closing := IsAppClosing 152 | , is_shutdown_by_app := IsAppShutdown 153 | , is_closed_remotely := IsRemote 154 | , status := Status 155 | , error := Code 156 | }, #{ data_stream_socks := DataStreams, 157 | stream_parse_state := PSS, 158 | control_stream_sock := {quic, Conn, CtrlStream} 159 | } = S) 160 | when is_boolean(IsConnShutdown) andalso 161 | is_boolean(IsAppClosing) andalso 162 | is_boolean(IsAppShutdown) andalso 163 | is_boolean(IsRemote) andalso 164 | is_atom(Status) andalso 165 | is_integer(Code) -> 166 | Via = {quic, Conn, Stream}, 167 | case lists:member(Via, DataStreams) of 168 | true -> 169 | %% is data stream 170 | {keep_state, S#{ data_stream_socks := lists:delete(Via, DataStreams) 171 | , stream_parse_state := maps:remove(Via, PSS) 172 | }}; 173 | false when Stream == CtrlStream -> 174 | {stop, normal}; 175 | false -> 176 | %% Other unknown streams: New stream from remote. 177 | ?LOG(warning, "unknown stream closed", P#{stream => Stream}, S), 178 | keep_state_and_data 179 | end. 180 | 181 | handle_call(_Stream, _Request, _Opts, S) -> 182 | {error, unimpl, S}. 183 | 184 | %%% Internals 185 | -spec parse(binary(), emqtt_frame:parse_state(), emqtt_quic:mqtt_packets()) 186 | -> cb_ret(). 187 | parse(<<>>, PS, Packets) -> 188 | {keep_state, PS, Packets}; 189 | parse(Bin, PS, Packets) -> 190 | try emqtt_frame:parse(Bin, PS) of 191 | {ok, MQTT, BinLeft, NewPS} -> 192 | parse(BinLeft, NewPS, [MQTT | Packets]); 193 | {more, NewPS} -> 194 | {keep_state, NewPS, Packets} 195 | catch 196 | error:Error:ST -> 197 | {stop, {Error, ST}} 198 | end. 199 | 200 | -else. 201 | %% BUILD_WITHOUT_QUIC 202 | -endif. 203 | -------------------------------------------------------------------------------- /src/emqtt_secret.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | %% Note: this module CAN'T be hot-patched to avoid invalidating the 18 | %% closures, so it must not be changed. 19 | -module(emqtt_secret). 20 | 21 | %% API: 22 | -export([wrap/1, unwrap/1]). 23 | 24 | -export_type([t/1]). 25 | 26 | -type t(T) :: fun(() -> T). 27 | 28 | %%================================================================================ 29 | %% API funcions 30 | %%================================================================================ 31 | 32 | -spec wrap(T) -> t(T). 33 | wrap(Term) -> 34 | fun() -> 35 | Term 36 | end. 37 | 38 | -spec unwrap(t(T) | T) -> T. 39 | unwrap(Term) when is_function(Term, 0) -> 40 | %% Handle potentially nested funs 41 | unwrap(Term()); 42 | unwrap(Term) -> 43 | Term. 44 | -------------------------------------------------------------------------------- /src/emqtt_sock.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------- 2 | %% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%------------------------------------------------------------------------- 16 | 17 | -module(emqtt_sock). 18 | 19 | -export([ connect/4 20 | , send/2 21 | , recv/2 22 | , close/1 23 | ]). 24 | 25 | -export([ sockname/1 26 | , setopts/2 27 | , getstat/2 28 | ]). 29 | 30 | -include("emqtt_internal.hrl"). 31 | 32 | -type(socket() :: inet:socket() | #ssl_socket{}). 33 | 34 | -type(sockname() :: {inet:ip_address(), inet:port_number()}). 35 | 36 | -type(option() :: gen_tcp:connect_option() | {ssl_opts, [ssl:tls_client_option()]}). 37 | 38 | -export_type([socket/0, option/0]). 39 | 40 | -define(DEFAULT_TCP_OPTIONS, [binary, {packet, raw}, {active, false}, 41 | {nodelay, true}]). 42 | 43 | -spec(connect(inet:ip_address() | inet:hostname(), 44 | inet:port_number(), [option()], timeout()) 45 | -> {ok, socket()} | {error, term()}). 46 | connect(Host, Port, SockOpts, Timeout) -> 47 | TcpOpts = merge_opts(?DEFAULT_TCP_OPTIONS, 48 | lists:keydelete(ssl_opts, 1, SockOpts)), 49 | case gen_tcp:connect(Host, Port, TcpOpts, Timeout) of 50 | {ok, Sock} -> 51 | case lists:keyfind(ssl_opts, 1, SockOpts) of 52 | {ssl_opts, SslOpts} -> 53 | ?IS_QoE andalso put(tcp_connected_at, erlang:monotonic_time(millisecond)), 54 | ssl_upgrade(Host, Sock, SslOpts, Timeout); 55 | false -> {ok, Sock} 56 | end; 57 | {error, Reason} -> 58 | {error, Reason} 59 | end. 60 | 61 | ssl_upgrade(Host, Sock, SslOpts0, Timeout) -> 62 | TlsVersions = proplists:get_value(versions, SslOpts0, []), 63 | Ciphers = proplists:get_value(ciphers, SslOpts0, default_ciphers(TlsVersions)), 64 | SslOpts1 = merge_opts(SslOpts0, [{ciphers, Ciphers}]), 65 | SslOpts2 = apply_sni(SslOpts1, Host), 66 | SslOpts3 = apply_host_check_fun(SslOpts2), 67 | SslOpts = maybe_drop_incompatible_options(TlsVersions, SslOpts3), 68 | case ssl:connect(Sock, SslOpts, Timeout) of 69 | {ok, SslSock} -> 70 | {ok, #ssl_socket{tcp = Sock, ssl = SslSock}}; 71 | {error, Reason} -> 72 | {error, Reason} 73 | end. 74 | 75 | -spec(send(socket(), iodata()) -> ok | {error, einval | closed}). 76 | send(Sock, Data) when is_port(Sock) -> 77 | send_tcp_data(Sock, Data); 78 | send(#ssl_socket{ssl = SslSock}, Data) -> 79 | case ssl:send(SslSock, Data) of 80 | ok -> 81 | ok; 82 | {error, closed}-> 83 | %% We attempt to grab an async exception with more information, if 84 | %% available; otherwise, bail out. 85 | receive 86 | {ssl_error, _Sock, DetailedReason} -> 87 | {error, DetailedReason}; 88 | {ssl_closed, _Sock} -> 89 | {error, closed} 90 | after 1 -> 91 | {error, closed} 92 | end; 93 | {error, Reason}-> 94 | {error, Reason} 95 | end; 96 | send(QuicStream, Data) when is_reference(QuicStream) -> 97 | case quicer:send(QuicStream, Data) of 98 | {ok, _Len} -> 99 | ok; 100 | Other -> 101 | Other 102 | end. 103 | 104 | -if(?OTP_RELEASE >= 26). 105 | send_tcp_data(Sock, Data) -> 106 | gen_tcp:send(Sock, Data). 107 | -else. 108 | send_tcp_data(Sock, Data) -> 109 | try erlang:port_command(Sock, Data) of 110 | true -> ok 111 | catch 112 | error:badarg -> {error, einval} 113 | end. 114 | -endif. 115 | 116 | -spec(recv(socket(), non_neg_integer()) 117 | -> {ok, iodata()} | {error, closed | inet:posix()}). 118 | recv(Sock, Length) when is_port(Sock) -> 119 | gen_tcp:recv(Sock, Length); 120 | recv(#ssl_socket{ssl = SslSock}, Length) -> 121 | ssl:recv(SslSock, Length); 122 | recv(QuicStream, Length) when is_reference(QuicStream) -> 123 | quicer:recv(QuicStream, Length). 124 | 125 | -spec(close(socket()) -> ok). 126 | close(Sock) when is_port(Sock) -> 127 | gen_tcp:close(Sock); 128 | close(#ssl_socket{ssl = SslSock}) -> 129 | ssl:close(SslSock). 130 | 131 | -spec(setopts(socket(), [gen_tcp:option() | ssl:tls_client_option()]) -> ok | {error, any()}). 132 | setopts(Sock, Opts) when is_port(Sock) -> 133 | inet:setopts(Sock, Opts); 134 | setopts(#ssl_socket{ssl = SslSock}, Opts) -> 135 | ssl:setopts(SslSock, Opts). 136 | 137 | -spec(getstat(socket(), [atom()]) 138 | -> {ok, [{atom(), integer()}]} | {error, term()}). 139 | getstat(Sock, Options) when is_port(Sock) -> 140 | inet:getstat(Sock, Options); 141 | getstat(#ssl_socket{tcp = Sock}, Options) -> 142 | inet:getstat(Sock, Options). 143 | 144 | -spec(sockname(socket()) -> {ok, sockname()} | {error, term()}). 145 | sockname(Sock) when is_port(Sock) -> 146 | inet:sockname(Sock); 147 | sockname(#ssl_socket{ssl = SslSock}) -> 148 | ssl:sockname(SslSock); 149 | sockname(Sock) when is_reference(Sock)-> 150 | quicer:sockname(Sock). 151 | 152 | -spec(merge_opts(list(), list()) -> list()). 153 | merge_opts(Defaults, Options) -> 154 | lists:foldl( 155 | fun({Opt, Val}, Acc) -> 156 | lists:keystore(Opt, 1, Acc, {Opt, Val}); 157 | (Opt, Acc) -> 158 | lists:usort([Opt | Acc]) 159 | end, Defaults, Options). 160 | 161 | default_ciphers(TlsVersions) -> 162 | lists:foldl( 163 | fun(TlsVer, Ciphers) -> 164 | Ciphers ++ ssl:cipher_suites(all, TlsVer) 165 | end, [], TlsVersions). 166 | 167 | apply_sni(Opts, Host) -> 168 | case lists:keyfind(server_name_indication, 1, Opts) of 169 | {_, SNI} when SNI =:= "true" orelse 170 | SNI =:= <<"true">> orelse 171 | SNI =:= true -> 172 | lists:keystore(server_name_indication, 1, Opts, 173 | {server_name_indication, Host}); 174 | _ -> 175 | Opts 176 | end. 177 | 178 | apply_host_check_fun(Opts) -> 179 | case proplists:is_defined(customize_hostname_check, Opts) of 180 | true -> 181 | Opts; 182 | false -> 183 | %% Default Support wildcard cert 184 | DefHostCheck = {customize_hostname_check, 185 | [{match_fun, 186 | public_key:pkix_verify_hostname_match_fun(https)}]}, 187 | [DefHostCheck | Opts] 188 | end. 189 | 190 | maybe_drop_incompatible_options(['tlsv1.3'], SslOpts) -> 191 | Incompatible = [reuse_sessions, secure_renegotiate], 192 | lists:filter(fun({K, _V}) -> not lists:member(K, Incompatible) end, SslOpts); 193 | maybe_drop_incompatible_options(_, SslOpts) -> 194 | SslOpts. 195 | -------------------------------------------------------------------------------- /src/emqtt_ws.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------- 2 | %% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%------------------------------------------------------------------------- 16 | 17 | -module(emqtt_ws). 18 | 19 | -export([ connect/4 20 | , send/2 21 | , close/1 22 | ]). 23 | 24 | -export([ setopts/2 25 | , getstat/2 26 | ]). 27 | 28 | -type option() :: {ws_path, string()}. 29 | -type connection() :: {_Conn :: pid(), gun:stream_ref()}. 30 | 31 | -export_type([option/0, connection/0]). 32 | 33 | -define(WS_OPTS, #{compress => false, 34 | protocols => [{<<"mqtt">>, gun_ws_h}] 35 | }). 36 | 37 | -define(WS_HEADERS, [{<<"cache-control">>, <<"no-cache">>}]). 38 | 39 | connect(Host0, Port, Opts, Timeout) -> 40 | Host1 = convert_host(Host0), 41 | {ok, _} = application:ensure_all_started(gun), 42 | %% 1. open connection 43 | TransportOptions = proplists:get_value(ws_transport_options, Opts, []), 44 | ConnOpts = opts(TransportOptions, #{connect_timeout => Timeout, 45 | retry => 0}), 46 | case gun:open(Host1, Port, ConnOpts) of 47 | {ok, ConnPid} -> 48 | case gun:await_up(ConnPid, Timeout) of 49 | {ok, _} -> 50 | case upgrade(ConnPid, Opts, Timeout) of 51 | {ok, StreamRef} -> {ok, {ConnPid, StreamRef}}; 52 | Error -> Error 53 | end; 54 | Error -> 55 | Error 56 | end; 57 | Error -> 58 | Error 59 | end. 60 | 61 | opts([], Acc) -> Acc; 62 | opts([{Name, Value} | More], Acc) -> 63 | opts(More, Acc#{Name => Value}). 64 | 65 | -spec(upgrade(pid(), list(), timeout()) 66 | -> {ok, Headers :: list()} | {error, Reason :: term()}). 67 | upgrade(ConnPid, Opts, Timeout) -> 68 | %% 2. websocket upgrade 69 | Path = proplists:get_value(ws_path, Opts, "/mqtt"), 70 | CustomHeaders = proplists:get_value(ws_headers, Opts, []), 71 | StreamRef = gun:ws_upgrade(ConnPid, Path, ?WS_HEADERS ++ CustomHeaders, ?WS_OPTS), 72 | receive 73 | {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _Headers} -> 74 | {ok, StreamRef}; 75 | {gun_response, ConnPid, _, _, Status, Headers} -> 76 | {error, {ws_upgrade_failed, Status, Headers}}; 77 | {gun_error, ConnPid, StreamRef, Reason} -> 78 | {error, {ws_upgrade_failed, Reason}}; 79 | {gun_down, ConnPid, _, Reason, _} -> 80 | {error, {ws_upgrade_failed, Reason}} 81 | after Timeout -> 82 | {error, timeout} 83 | end. 84 | 85 | %% fake stats:) 86 | getstat(_WsPid, Options) -> 87 | {ok, [{Opt, 0} || Opt <- Options]}. 88 | 89 | setopts(_WsPid, _Opts) -> 90 | ok. 91 | 92 | -spec(send({pid(), gun:stream_ref()}, iodata()) -> ok). 93 | send({WsPid, StreamRef}, Data) -> 94 | gun:ws_send(WsPid, StreamRef, {binary, Data}). 95 | 96 | -spec(close({pid(), gun:stream_ref()}) -> ok). 97 | close({WsPid, _StreamRef}) -> 98 | gun:shutdown(WsPid). 99 | 100 | -spec convert_host(inet:ip_address() | inet:hostname()) -> inet:hostname(). 101 | convert_host(Host) -> 102 | case Host of 103 | %% `inet:is_ip_address/1` is available since OTP 25 104 | Ip4 when is_tuple(Ip4) andalso tuple_size(Ip4) =:= 4 -> inet:ntoa(Host); 105 | Ip6 when is_tuple(Ip6) andalso tuple_size(Ip6) =:= 8 -> inet:ntoa(Host); 106 | _ -> Host 107 | end. 108 | -------------------------------------------------------------------------------- /test/certs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/emqtt/bf52b86e673af0d1a4ae9fcdd61454a6d06d4ab4/test/certs/.keep -------------------------------------------------------------------------------- /test/emqtt_connect_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqtt_connect_SUITE). 18 | 19 | -compile(nowarn_export_all). 20 | -compile(export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | 25 | -define(ALL_IF_IP6, {0, 0, 0, 0, 0, 0, 0, 0}). 26 | 27 | -define(HOSTS, 28 | [{ip4, [<<"127.0.0.1">>, "127.0.0.1", {127, 0, 0, 1}]}, 29 | {ip6, ["localhost"]} 30 | ]). 31 | 32 | -define(PORTS, #{ 33 | {ip4, connect} => {1883, []}, 34 | {ip4, ws_connect} => {8083, []}, 35 | {ip4, quic_connect} => {14567, []}, 36 | {ip6, connect} => {21883, [{tcp_opts, [{tcp_module, inet6_tcp}]}]}, 37 | {ip6, ws_connect} => {28083, 38 | [{ws_transport_options, [{protocols, [http]}, {tcp_opts, [inet6]}]}, 39 | {ws_headers, [{<<"host">>, <<"[::1]:28083">>}]} 40 | ]}, 41 | {ip6, quic_connect} => {34567, []} 42 | }). 43 | 44 | 45 | all() -> 46 | [{group, ip4}] ++ 47 | [{group, ip6} || is_ip6_available()]. 48 | 49 | connect_groups() -> 50 | [{group, tcp}, 51 | {group, ws} 52 | ] ++ 53 | [{group, quic} || emqtt_test_lib:has_quic()]. 54 | 55 | groups() -> 56 | [{ip4, [], connect_groups()}] ++ 57 | [{ip6, [], connect_groups()} || is_ip6_available()] ++ 58 | [{tcp, [], [t_connect, t_reconnect]}, 59 | {ws, [], [t_connect, t_reconnect]}, 60 | {quic, [], [t_connect, t_reconnect]} 61 | ]. 62 | 63 | suite() -> 64 | [{timetrap, {seconds, 15}}]. 65 | 66 | init_per_suite(Config) -> 67 | ok = emqtt_test_lib:start_emqx(), 68 | Config. 69 | 70 | end_per_suite(_Config) -> 71 | emqtt_test_lib:stop_emqx(). 72 | 73 | init_per_group(ip4, Config) -> 74 | [{ip_type, ip4} | Config]; 75 | init_per_group(ip6, Config) -> 76 | ok = emqtt_test_lib:ensure_listener(tcp, mqtt_ip6, ?ALL_IF_IP6, 21883), 77 | ok = emqtt_test_lib:ensure_listener(ws, mqtt_ip6, ?ALL_IF_IP6, 28083), 78 | case emqtt_test_lib:has_quic() of 79 | true -> 80 | ok = emqtt_test_lib:ensure_listener(quic, mqtt_ip6, ?ALL_IF_IP6, 34567); 81 | false -> 82 | ok 83 | end, 84 | [{ip_type, ip6}, {listener, mqtt_ip6} | Config]; 85 | init_per_group(tcp, Config) -> 86 | [{conn_fun, connect}, {transport, tcp} | Config]; 87 | init_per_group(ws, Config) -> 88 | [{conn_fun, ws_connect}, {transport, ws} | Config]; 89 | init_per_group(quic, Config) -> 90 | [{conn_fun, quic_connect}, {transport, quic}, {listener, mqtt} | Config]. 91 | 92 | end_per_group(_, Config) -> 93 | Config. 94 | 95 | t_connect(Config) -> 96 | IpType = ?config(ip_type, Config), 97 | ConnFun = ?config(conn_fun, Config), 98 | Hosts = ?config(IpType, ?HOSTS), 99 | {Port, Opts} = maps:get({IpType, ConnFun}, ?PORTS), 100 | lists:foreach( 101 | fun(Host) -> 102 | ct:pal("Connecting to ~p at port ~p via ~p", [Host, Port, ConnFun]), 103 | {ok, C} = emqtt:start_link([{host, Host}, {port, Port}] ++ Opts), 104 | {ok, _} = emqtt:ConnFun(C), 105 | ct:pal("Connected to ~p at port ~p via ~p", [Host, Port, ConnFun]), 106 | pong = emqtt:ping(C), 107 | connected = emqtt:status(C), 108 | ok = emqtt:disconnect(C) 109 | end, 110 | Hosts). 111 | 112 | t_reconnect(Config) -> 113 | %% Outermost group defines listener name: 114 | ListenerName = lists:last([default] ++ [L || {listener, L} <- Config]), 115 | ListenerId = atom_to_list(?config(transport, Config)) ++ ":" ++ atom_to_list(ListenerName), 116 | ConnFun = ?config(conn_fun, Config), 117 | [Host | _] = ?config(ip4, ?HOSTS), 118 | {Port, Opts} = maps:get({ip4, ConnFun}, ?PORTS), 119 | ct:pal("Connecting to ~p at port ~p via ~p", [Host, Port, ConnFun]), 120 | {ok, C} = emqtt:start_link([{host, Host}, 121 | {port, Port}, 122 | {reconnect, 3}, 123 | {reconnect_timeout, 1}] ++ Opts), 124 | {ok, _} = emqtt:ConnFun(C), 125 | ct:pal("Connected to ~p at port ~p via ~p", [Host, Port, ConnFun]), 126 | ok = emqx_listeners:restart_listener(ListenerId), 127 | pong = emqtt:ping(C), 128 | connected = emqtt:status(C), 129 | ct:pal("Reconnected to ~p at port ~p via ~p", [Host, Port, ConnFun]), 130 | ok = emqtt:disconnect(C). 131 | 132 | is_ip6_available() -> 133 | is_ip6_available(30000). 134 | 135 | is_ip6_available(Port) -> 136 | Opts = [inet6, {ip, {0,0,0,0,0,0,0,0}}], 137 | case gen_tcp:listen(Port, Opts) of 138 | {ok, Sock} -> 139 | gen_tcp:close(Sock), 140 | true; 141 | {error, eaddrinuse} -> 142 | is_ip6_available(Port + 1); 143 | {error, Reason} -> 144 | ct:pal("Cannot listen on IPv6: ~p", [Reason]), 145 | false 146 | end. 147 | -------------------------------------------------------------------------------- /test/emqtt_kerberos_auth_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | %% To run this test suite. 18 | %% You will need: 19 | %% - EMQX running with kerberos auth enabled, serving MQTT at port 3883 20 | %% - KDC is up and running. 21 | %% 22 | %% Set up test environment from EMQX CI docker-compose files: 23 | %% - Update .ci/docker-compose-file/docker-compose.yaml to make sure erlang container is exposing port 3883:1883 24 | %% - Command to start KDC: docker-compose -f ./.ci/docker-compose-file/docker-compose.yaml -f ./.ci/docker-compose-file/docker-compose-kdc.yaml up -d 25 | %% - Run EMQX in the container 'erlang.emqx.net' 26 | %% - Configure EMQX default tcp listener with kerberos auth enabled. 27 | %% - Copy client keytab file '/var/lib/secret/krb_authn_cli.keytab' from container 'kdc.emqx.net' to `/tmp` 28 | %% 29 | -module(emqtt_kerberos_auth_SUITE). 30 | 31 | -compile(nowarn_export_all). 32 | -compile(export_all). 33 | 34 | -include_lib("eunit/include/eunit.hrl"). 35 | -include_lib("common_test/include/ct.hrl"). 36 | -include("emqtt.hrl"). 37 | 38 | %%------------------------------------------------------------------------------ 39 | %% CT boilerplate 40 | %%------------------------------------------------------------------------------ 41 | 42 | all() -> 43 | emqtt_test_lib:all(?MODULE). 44 | 45 | init_per_suite(Config) -> 46 | Host = os:getenv("EMQX_HOST", "localhost"), 47 | Port = list_to_integer(os:getenv("EMQX_PORT", "3883")), 48 | case emqtt_test_lib:is_tcp_server_available(Host, Port) of 49 | true -> 50 | [ {host, Host} 51 | , {port, Port} 52 | | Config]; 53 | false -> 54 | {skip, no_emqx} 55 | end. 56 | 57 | end_per_suite(_Config) -> 58 | ok. 59 | 60 | %%------------------------------------------------------------------------------ 61 | %% Helper fns 62 | %%------------------------------------------------------------------------------ 63 | 64 | %% This must match the server principal 65 | %% For this test, the server principal is "mqtt/erlang.emqx.net@KDC.EMQX.NET" 66 | server_fqdn() -> <<"erlang.emqx.net">>. 67 | 68 | realm() -> <<"KDC.EMQX.NET">>. 69 | 70 | bin(X) -> iolist_to_binary(X). 71 | 72 | server_principal() -> 73 | bin(["mqtt/", server_fqdn(), "@", realm()]). 74 | 75 | client_principal() -> 76 | bin(["krb_authn_cli@", realm()]). 77 | 78 | client_keytab() -> 79 | <<"/tmp/krb_authn_cli.keytab">>. 80 | 81 | auth_init(#{client_keytab := KeytabFile, 82 | client_principal := ClientPrincipal, 83 | server_fqdn := ServerFQDN, 84 | server_principal := ServerPrincipal}) -> 85 | ok = sasl_auth:kinit(KeytabFile, ClientPrincipal), 86 | {ok, ClientHandle} = sasl_auth:client_new(<<"mqtt">>, ServerFQDN, ServerPrincipal, <<"krb_authn_cli">>), 87 | {ok, {sasl_continue, FirstClientToken}} = sasl_auth:client_start(ClientHandle), 88 | InitialProps = props(FirstClientToken), 89 | State = #{client_handle => ClientHandle, step => 1}, 90 | {InitialProps, State}. 91 | 92 | auth_handle(#{step := 1, 93 | client_handle := ClientHandle 94 | } = AuthState, Reason, Props) -> 95 | ct:pal("step-1: auth packet received:\n rc: ~p\n props:\n ~p", [Reason, Props]), 96 | case {Reason, Props} of 97 | {continue_authentication, 98 | #{'Authentication-Data' := ServerToken}} -> 99 | {ok, {sasl_continue, ClientToken}} = 100 | sasl_auth:client_step(ClientHandle, ServerToken), 101 | OutProps = props(ClientToken), 102 | NewState = AuthState#{step := 2}, 103 | {continue, {?RC_CONTINUE_AUTHENTICATION, OutProps}, NewState}; 104 | _ -> 105 | {stop, protocol_error} 106 | end; 107 | auth_handle(#{step := 2, 108 | client_handle := ClientHandle 109 | }, Reason, Props) -> 110 | ct:pal("step-2: auth packet received:\n rc: ~p\n props:\n ~p", [Reason, Props]), 111 | case {Reason, Props} of 112 | {continue_authentication, 113 | #{'Authentication-Data' := ServerToken}} -> 114 | {ok, {sasl_ok, ClientToken}} = 115 | sasl_auth:client_step(ClientHandle, ServerToken), 116 | OutProps = props(ClientToken), 117 | NewState = #{done => erlang:system_time()}, 118 | {continue, {?RC_CONTINUE_AUTHENTICATION, OutProps}, NewState}; 119 | _ -> 120 | {stop, protocol_error} 121 | end. 122 | 123 | props(Data) -> 124 | #{'Authentication-Method' => <<"GSSAPI-KERBEROS">>, 125 | 'Authentication-Data' => Data 126 | }. 127 | 128 | %%------------------------------------------------------------------------------ 129 | %% Testcases 130 | %%------------------------------------------------------------------------------ 131 | 132 | t_basic(Config) -> 133 | ct:timetrap({seconds, 5}), 134 | Host = ?config(host, Config), 135 | Port = ?config(port, Config), 136 | InitArgs = #{client_keytab => client_keytab(), 137 | client_principal => client_principal(), 138 | server_fqdn => server_fqdn(), 139 | server_principal => server_principal() 140 | }, 141 | {ok, C} = emqtt:start_link( 142 | #{ host => Host 143 | , port => Port 144 | , username => <<"myuser">> 145 | , proto_ver => v5 146 | , custom_auth_callbacks => 147 | #{ init => {fun ?MODULE:auth_init/1, [InitArgs]} 148 | , handle_auth => fun ?MODULE:auth_handle/3 149 | } 150 | }), 151 | ?assertMatch({ok, _}, emqtt:connect(C)), 152 | {ok, _, [0]} = emqtt:subscribe(C, <<"t/#">>), 153 | ok. 154 | 155 | t_bad_method_name(Config) -> 156 | ct:timetrap({seconds, 5}), 157 | Host = ?config(host, Config), 158 | Port = ?config(port, Config), 159 | InitFn = fun() -> 160 | KeytabFile = client_keytab(), 161 | ClientPrincipal = client_principal(), 162 | ServerFQDN = server_fqdn(), 163 | ServerPrincipal = server_principal(), 164 | ok = sasl_auth:kinit(KeytabFile, ClientPrincipal), 165 | {ok, ClientHandle} = sasl_auth:client_new(<<"mqtt">>, ServerFQDN, ServerPrincipal, <<"krb_authn_cli">>), 166 | {ok, {sasl_continue, FirstClientToken}} = sasl_auth:client_start(ClientHandle), 167 | InitialProps0 = props(FirstClientToken), 168 | %% the expected method is GSSAPI-KERBEROS, using "KERBEROS" should immediately result in a rejection 169 | InitialProps = InitialProps0#{'Authentication-Method' => <<"KERBEROS">>}, 170 | State = #{client_handle => ClientHandle, step => 1}, 171 | {InitialProps, State} 172 | end, 173 | {ok, C} = emqtt:start_link( 174 | #{ host => Host 175 | , port => Port 176 | , username => <<"myuser">> 177 | , proto_ver => v5 178 | , custom_auth_callbacks => 179 | #{init => {InitFn, []}, 180 | handle_auth => fun ?MODULE:auth_handle/3 181 | } 182 | }), 183 | _ = monitor(process, C), 184 | unlink(C), 185 | _ = emqtt:connect(C), 186 | receive 187 | {'DOWN', _, process, C, Reason} -> 188 | ?assertEqual({shutdown, not_authorized}, Reason); 189 | Msg -> 190 | ct:fail({unexpected, Msg}) 191 | end, 192 | ok. 193 | -------------------------------------------------------------------------------- /test/emqtt_props_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqtt_props_SUITE). 18 | 19 | -compile(nowarn_export_all). 20 | -compile(export_all). 21 | 22 | -include("emqtt.hrl"). 23 | -include_lib("eunit/include/eunit.hrl"). 24 | 25 | all() -> emqtt_test_lib:all(?MODULE). 26 | 27 | init_per_suite(Config) -> 28 | code:add_patha(filename:join(code:lib_dir(emqx), "ebin/")), 29 | emqtt_test_lib:ensure_test_module(emqx_common_test_helpers), 30 | Config. 31 | 32 | end_per_suite(_Config) -> 33 | ok. 34 | 35 | t_id(_) -> 36 | foreach_prop( 37 | fun({Id, Prop}) -> 38 | ?assertEqual(Id, emqtt_props:id(element(1, Prop))) 39 | end), 40 | ?catch_error({bad_property, 'Bad-Property'}, emqtt_props:id('Bad-Property')). 41 | 42 | t_name(_) -> 43 | foreach_prop( 44 | fun({Id, Prop}) -> 45 | ?assertEqual(emqtt_props:name(Id), element(1, Prop)) 46 | end), 47 | ?catch_error({unsupported_property, 16#FF}, emqtt_props:name(16#FF)). 48 | 49 | t_filter(_) -> 50 | PubProps = #{'Payload-Format-Indicator' => 6, 51 | 'Message-Expiry-Interval' => 300, 52 | 'Session-Expiry-Interval' => 300, 53 | 'User-Property' => {<<"username">>, <<"test">>} 54 | }, 55 | ?assertEqual(#{'Payload-Format-Indicator' => 6, 56 | 'Message-Expiry-Interval' => 300, 57 | 'User-Property' => {<<"username">>, <<"test">>} 58 | }, 59 | emqtt_props:filter(?PUBLISH, PubProps)), 60 | 61 | BadProps = #{'Unknown-Property' => 10}, 62 | ?catch_error({bad_property,'Unknown-Property'}, 63 | emqtt_props:filter(?PUBLISH, BadProps)). 64 | 65 | t_validate(_) -> 66 | ConnProps = #{'Payload-Format-Indicator' => 1, 67 | 'Server-Keep-Alive' => 20, 68 | 'Session-Expiry-Interval' => 1, 69 | 'Subscription-Identifier' => 1, 70 | 'Correlation-Data' => <<"test">>, 71 | 'Maximum-Packet-Size' => 255, 72 | 'User-Property' => [{<<"username">>, <<"test">>}] 73 | }, 74 | ok = emqtt_props:validate(ConnProps), 75 | 76 | BadProps = #{'Unknown-Property' => 10}, 77 | ?catch_error({bad_property,'Unknown-Property'}, 78 | emqtt_props:validate(BadProps)). 79 | 80 | foreach_prop(Fun) -> 81 | lists:foreach(Fun, maps:to_list(emqtt_props:all())). 82 | 83 | -------------------------------------------------------------------------------- /test/emqtt_request_handler.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | %% @doc This module implements a request handler based on emqtt. 18 | %% A request handler is a MQTT client which subscribes to a request topic, 19 | %% processes the requests then send response to another topic which is 20 | %% subscribed by the request sender. 21 | %% This code is in test directory because request and response are pure 22 | %% client-side behaviours. 23 | 24 | -module(emqtt_request_handler). 25 | 26 | -export([start_link/4, stop/1]). 27 | 28 | -include("emqtt.hrl"). 29 | 30 | -type qos() :: emqx_mqtt_types:qos_name() | emqx_mqtt_types:qos(). 31 | -type topic() :: emqx_topic:topic(). 32 | -type handler() :: fun((CorrData :: binary(), ReqPayload :: binary()) -> RspPayload :: binary()). 33 | 34 | -spec start_link(topic(), qos(), handler(), emqtt:options()) -> 35 | {ok, pid()} | {error, any()}. 36 | start_link(RequestTopic, QoS, RequestHandler, Options0) -> 37 | Parent = self(), 38 | MsgHandler = make_msg_handler(RequestHandler, Parent), 39 | Options = [{msg_handler, MsgHandler} | Options0], 40 | case emqtt:start_link(Options) of 41 | {ok, Pid} -> 42 | {ok, _} = emqtt:connect(Pid), 43 | try subscribe(Pid, RequestTopic, QoS) of 44 | ok -> {ok, Pid}; 45 | {error, _} = Error -> Error 46 | catch 47 | C : E : S -> 48 | emqtt:stop(Pid), 49 | {error, {C, E, S}} 50 | end; 51 | {error, _} = Error -> Error 52 | end. 53 | 54 | stop(Pid) -> 55 | emqtt:disconnect(Pid). 56 | 57 | make_msg_handler(RequestHandler, Parent) -> 58 | #{publish => fun(Msg) -> handle_msg(Msg, RequestHandler, Parent) end, 59 | puback => fun(_Ack) -> ok end, 60 | disconnected => fun(_Reason) -> ok end 61 | }. 62 | 63 | handle_msg(ReqMsg, RequestHandler, Parent) -> 64 | #{qos := QoS, properties := Props, payload := ReqPayload} = ReqMsg, 65 | case maps:find('Response-Topic', Props) of 66 | {ok, RspTopic} when RspTopic =/= <<>> -> 67 | CorrData = maps:get('Correlation-Data', Props), 68 | RspProps = maps:without(['Response-Topic'], Props), 69 | RspPayload = RequestHandler(CorrData, ReqPayload), 70 | RspMsg = #mqtt_msg{qos = QoS, 71 | topic = RspTopic, 72 | props = RspProps, 73 | payload = RspPayload 74 | }, 75 | emqx_logger:debug("~p sending response msg to topic ~s with~n" 76 | "corr-data=~p~npayload=~p", 77 | [?MODULE, RspTopic, CorrData, RspPayload]), 78 | ok = send_response(RspMsg); 79 | _ -> 80 | Parent ! {discarded, ReqPayload}, 81 | ok 82 | end. 83 | 84 | send_response(Msg) -> 85 | %% This function is evaluated by emqtt itself. 86 | %% hence delegate to another temp process for the loopback gen_statem call. 87 | Client = self(), 88 | _ = spawn_link(fun() -> 89 | case emqtt:publish(Client, Msg) of 90 | ok -> ok; 91 | {ok, _} -> ok; 92 | {error, Reason} -> exit({failed_to_publish_response, Reason}) 93 | end 94 | end), 95 | ok. 96 | 97 | subscribe(Client, Topic, QoS) -> 98 | {ok, _Props, _QoS} = 99 | emqtt:subscribe(Client, [{Topic, [{rh, 2}, {rap, false}, 100 | {nl, true}, {qos, QoS}]}]), 101 | ok. 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /test/emqtt_request_response_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqtt_request_response_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include("emqtt.hrl"). 23 | -include_lib("eunit/include/eunit.hrl"). 24 | -include_lib("common_test/include/ct.hrl"). 25 | 26 | init_per_suite(Config) -> 27 | ok = emqtt_test_lib:start_emqx(), 28 | Config. 29 | 30 | end_per_suite(_Config) -> 31 | emqtt_test_lib:stop_emqx(). 32 | 33 | all() -> 34 | [request_response]. 35 | 36 | request_response(_Config) -> 37 | request_response_per_qos(?QOS_0), 38 | request_response_per_qos(?QOS_1), 39 | request_response_per_qos(?QOS_2). 40 | 41 | request_response_per_qos(QoS) -> 42 | ReqTopic = <<"request_topic">>, 43 | RspTopic = <<"response_topic">>, 44 | {ok, Requester} = emqtt_request_sender:start_link(RspTopic, QoS, 45 | [{proto_ver, v5}, 46 | {clientid, <<"requester">>}, 47 | {properties, #{'Request-Response-Information' => 1}}]), 48 | %% This is a square service 49 | Square = fun(_CorrData, ReqBin) -> 50 | I = b2i(ReqBin), 51 | i2b(I * I) 52 | end, 53 | {ok, Responser} = emqtt_request_handler:start_link(ReqTopic, QoS, Square, 54 | [{proto_ver, v5}, 55 | {clientid, <<"responser">>} 56 | ]), 57 | ok = emqtt_request_sender:send(Requester, ReqTopic, RspTopic, <<"corr-1">>, <<"2">>, QoS), 58 | receive 59 | {response, <<"corr-1">>, <<"4">>} -> 60 | ok; 61 | Other -> 62 | erlang:error({unexpected, Other}) 63 | after 64 | 100 -> 65 | erlang:error(timeout) 66 | end, 67 | ok = emqtt_request_sender:stop(Requester), 68 | ok = emqtt_request_handler:stop(Responser). 69 | 70 | b2i(B) -> binary_to_integer(B). 71 | i2b(I) -> integer_to_binary(I). 72 | -------------------------------------------------------------------------------- /test/emqtt_request_sender.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | %% @doc This module implements a request sender based on emqtt. 18 | %% A request sender is a MQTT client which sends messages to a request 19 | %% topic, and subscribes to another topic for responses. 20 | %% This code is in test directory because request and response are pure 21 | %% client-side behaviours. 22 | 23 | -module(emqtt_request_sender). 24 | 25 | -export([start_link/3, stop/1, send/6]). 26 | 27 | -include("emqtt.hrl"). 28 | 29 | start_link(ResponseTopic, QoS, Options0) -> 30 | Parent = self(), 31 | MsgHandler = make_msg_handler(Parent), 32 | Options = [{msg_handler, MsgHandler} | Options0], 33 | case emqtt:start_link(Options) of 34 | {ok, Pid} -> 35 | {ok, _} = emqtt:connect(Pid), 36 | try subscribe(Pid, ResponseTopic, QoS) of 37 | ok -> {ok, Pid}; 38 | {error, _} = Error -> Error 39 | catch 40 | C : E : S -> 41 | emqtt:stop(Pid), 42 | {error, {C, E, S}} 43 | end; 44 | {error, _} = Error -> Error 45 | end. 46 | 47 | %% @doc Send a message to request topic with correlation-data `CorrData'. 48 | %% Response should be delivered as a `{response, CorrData, Payload}' 49 | send(Client, ReqTopic, RspTopic, CorrData, Payload, QoS) -> 50 | Props = #{'Response-Topic' => RspTopic, 51 | 'Correlation-Data' => CorrData 52 | }, 53 | Msg = #mqtt_msg{qos = QoS, 54 | topic = ReqTopic, 55 | props = Props, 56 | payload = Payload 57 | }, 58 | case emqtt:publish(Client, Msg) of 59 | ok -> ok; %% QoS = 0 60 | {ok, _} -> ok; 61 | {error, _} = E -> E 62 | end. 63 | 64 | stop(Pid) -> 65 | emqtt:disconnect(Pid). 66 | 67 | subscribe(Client, Topic, QoS) -> 68 | case emqtt:subscribe(Client, Topic, QoS) of 69 | {ok, _, _} -> ok; 70 | {error, _} = Error -> Error 71 | end. 72 | 73 | make_msg_handler(Parent) -> 74 | #{publish => fun(Msg) -> handle_msg(Msg, Parent) end, 75 | puback => fun(_Ack) -> ok end, 76 | disconnected => fun(_Reason) -> ok end 77 | }. 78 | 79 | handle_msg(Msg, Parent) -> 80 | #{properties := Props, payload := Payload} = Msg, 81 | CorrData = maps:get('Correlation-Data', Props), 82 | Parent ! {response, CorrData, Payload}, 83 | ok. 84 | 85 | -------------------------------------------------------------------------------- /test/emqtt_scram_auth_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqtt_scram_auth_SUITE). 18 | 19 | -compile(nowarn_export_all). 20 | -compile(export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | -include("emqtt.hrl"). 25 | 26 | %%------------------------------------------------------------------------------ 27 | %% CT boilerplate 28 | %%------------------------------------------------------------------------------ 29 | 30 | all() -> 31 | emqtt_test_lib:all(?MODULE). 32 | 33 | init_per_suite(Config) -> 34 | Host = os:getenv("EMQX_HOST", "localhost"), 35 | Port = list_to_integer(os:getenv("EMQX_PORT", "2883")), 36 | case emqtt_test_lib:is_tcp_server_available(Host, Port) of 37 | true -> 38 | [ {host, Host} 39 | , {port, Port} 40 | | Config]; 41 | false -> 42 | {skip, no_emqx} 43 | end. 44 | 45 | end_per_suite(_Config) -> 46 | ok. 47 | 48 | %%------------------------------------------------------------------------------ 49 | %% Helper fns 50 | %%------------------------------------------------------------------------------ 51 | 52 | client_first_message() -> 53 | <<"n,,n=myuser,r=x15obqa'#WGRb-H">>. 54 | 55 | auth_init() -> 56 | #{step => init}. 57 | 58 | auth_handle(AuthState0, Reason, Props) -> 59 | ct:pal("auth packet received:\n rc: ~p\n props:\n ~p", [Reason, Props]), 60 | case {Reason, Props} of 61 | {continue_authentication, 62 | #{ 'Authentication-Method' := <<"SCRAM-SHA-512">> 63 | , 'Authentication-Data' := ServerFirstMesage 64 | }} -> 65 | {continue, ClientFinalMessage, ClientCache} = 66 | esasl_scram:check_server_first_message( 67 | ServerFirstMesage, 68 | #{ client_first_message => client_first_message() 69 | , password => <<"mypass">> 70 | , algorithm => sha512 71 | } 72 | ), 73 | AuthState = AuthState0#{step := final, cache => ClientCache}, 74 | OutProps = #{ 'Authentication-Method' => <<"SCRAM-SHA-512">> 75 | , 'Authentication-Data' => ClientFinalMessage 76 | }, 77 | {continue, {?RC_CONTINUE_AUTHENTICATION, OutProps}, AuthState}; 78 | _ -> 79 | {stop, protocol_error} 80 | end. 81 | 82 | %%------------------------------------------------------------------------------ 83 | %% Testcases 84 | %%------------------------------------------------------------------------------ 85 | 86 | t_scram(Config) -> 87 | ct:timetrap({seconds, 5}), 88 | Host = ?config(host, Config), 89 | Port = ?config(port, Config), 90 | {ok, C} = emqtt:start_link( 91 | #{ host => Host 92 | , port => Port 93 | , username => <<"myuser">> 94 | , proto_ver => v5 95 | , properties => 96 | #{ 'Authentication-Method' => <<"SCRAM-SHA-512">> 97 | , 'Authentication-Data' => client_first_message() 98 | } 99 | , custom_auth_callbacks => 100 | #{ init => fun ?MODULE:auth_init/0 101 | , handle_auth => fun ?MODULE:auth_handle/3 102 | } 103 | }), 104 | ?assertMatch({ok, _}, emqtt:connect(C)), 105 | ok. 106 | -------------------------------------------------------------------------------- /test/emqtt_sock_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqtt_sock_SUITE). 18 | 19 | -compile(nowarn_export_all). 20 | -compile(export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | 25 | all() -> emqtt_test_lib:all(?MODULE) ++ [{group, all}]. 26 | 27 | groups() -> 28 | [ {all, [ {group, unknown_ca} 29 | , {group, known_ca} 30 | ]} 31 | , {unknown_ca, [ {group, server} 32 | , {group, wildcard_server} 33 | ]} 34 | , {known_ca, [ {group, server} 35 | , {group, wildcard_server} 36 | ]} 37 | , {server, [ {group, server_verify_peer} 38 | , {group, server_verify_none} 39 | ]} 40 | , {wildcard_server, [ {group, server_verify_peer} 41 | , {group, server_verify_none} 42 | ]} 43 | , {server_verify_peer, [ {group, client_verify_peer} 44 | , {group, client_verify_none} 45 | ]} 46 | , {server_verify_none, [ {group, client_verify_peer} 47 | , {group, client_verify_none} 48 | ]} 49 | , {client_verify_peer, [gen_connect_test]} 50 | , {client_verify_none, [gen_connect_test]} 51 | , {tlsv13, [tlsv13_connect_test]} 52 | ]. 53 | 54 | init_per_group(all, Config) -> 55 | Config; 56 | init_per_group(unknown_ca, Config) -> 57 | [{is_same_ca, false} | Config]; 58 | init_per_group(known_ca, Config) -> 59 | [{is_same_ca, true} | Config]; 60 | init_per_group(client_verify_peer, Config) -> 61 | [{client_verify, verify_peer} | Config]; 62 | init_per_group(client_verify_none, Config) -> 63 | [{client_verify, verify_none} | Config]; 64 | init_per_group(wildcard_server, Config) -> 65 | [{is_wildcard_server, true} | Config]; 66 | init_per_group(server, Config) -> 67 | [{is_wildcard_server, false} | Config]; 68 | init_per_group(server_verify_peer, Config) -> 69 | [{server_verify, verify_peer} | Config]; 70 | init_per_group(server_verify_none, Config) -> 71 | [{server_verify, verify_none} | Config]. 72 | 73 | end_per_group(all, Config) -> 74 | Config; 75 | end_per_group(unknown_ca, Config) -> 76 | proplists:delete(is_same_ca, Config); 77 | end_per_group(known_ca, Config) -> 78 | proplists:delete(is_same_ca, Config); 79 | end_per_group(client_verify_peer, Config) -> 80 | proplists:delete(client_verify, Config); 81 | end_per_group(client_verify_none, Config) -> 82 | proplists:delete(client_verify, Config); 83 | end_per_group(wildcard_server, Config) -> 84 | proplists:delete(is_wildcard_server, Config); 85 | end_per_group(server, Config) -> 86 | proplists:delete(is_wildcard_server, Config); 87 | end_per_group(server_verify_peer, Config) -> 88 | proplists:delete(server_verify, Config); 89 | end_per_group(server_verify_none, Config) -> 90 | proplists:delete(server_verify, Config). 91 | 92 | 93 | init_per_suite(Config) -> 94 | emqtt_test_lib:ensure_test_module(emqx_common_test_helpers), 95 | DataDir = cert_dir(Config), 96 | _ = emqtt_test_lib:gen_ca(DataDir, "ca"), 97 | _ = emqtt_test_lib:gen_host_cert("wildcard.localhost", "ca", DataDir, true), 98 | _ = emqtt_test_lib:gen_host_cert("localhost", "ca", DataDir), 99 | _ = emqtt_test_lib:gen_host_cert("client", "ca", DataDir), 100 | _ = emqtt_test_lib:gen_ca(DataDir, "other-ca"), 101 | _ = emqtt_test_lib:gen_host_cert("other-client", "other-ca", DataDir), 102 | 103 | [ %% Clients 104 | {unknown_client_cert_files, [{cacertfile, emqtt_test_lib:ca_cert_name(DataDir, "other-ca")}, 105 | {keyfile, emqtt_test_lib:key_name(DataDir, "other-client")}, 106 | {certfile, emqtt_test_lib:cert_name(DataDir, "other-client")}, 107 | {server_name_indication, true} 108 | ]} 109 | , {known_client_cert_files, [{cacertfile, emqtt_test_lib:ca_cert_name(DataDir, "ca")}, 110 | {keyfile, emqtt_test_lib:key_name(DataDir, "client")}, 111 | {certfile, emqtt_test_lib:cert_name(DataDir, "client")}, 112 | {server_name_indication, true} 113 | ]} 114 | %% Servers 115 | , {wildcard_server_cert_files, [{cacertfile, emqtt_test_lib:ca_cert_name(DataDir, "ca")}, 116 | {keyfile, emqtt_test_lib:key_name(DataDir, "wildcard.localhost")}, 117 | {certfile, emqtt_test_lib:cert_name(DataDir, "wildcard.localhost")} 118 | ]} 119 | , {common_server_cert_files, [{cacertfile, emqtt_test_lib:ca_cert_name(DataDir, "ca")}, 120 | {keyfile, emqtt_test_lib:key_name(DataDir, "localhost")}, 121 | {certfile, emqtt_test_lib:cert_name(DataDir, "localhost")} 122 | ]} 123 | | Config]. 124 | 125 | end_per_suite(_Config) -> 126 | ok. 127 | 128 | init_per_testcase(gen_connect_test, Config) -> 129 | ServerVerify = ?config(server_verify, Config), 130 | ClientVerify = ?config(client_verify, Config), 131 | IsWildcard = ?config(is_wildcard_server, Config), 132 | IsSameCA = ?config(is_same_ca, Config), 133 | 134 | ServerFiles = case IsWildcard of 135 | true -> ?config(wildcard_server_cert_files , Config); 136 | false -> ?config(common_server_cert_files, Config) 137 | end, 138 | 139 | ClientFiles = case IsSameCA of 140 | true -> ?config(known_client_cert_files , Config); 141 | false -> ?config(unknown_client_cert_files, Config) 142 | end, 143 | 144 | IsPass = if ClientVerify =:= verify_none andalso ServerVerify =:= verify_none -> true; 145 | ClientVerify =:= verify_peer andalso IsSameCA -> true; 146 | ClientVerify =:= verify_none andalso not IsSameCA -> false; 147 | ServerVerify =:= verify_peer andalso IsSameCA -> true; 148 | ServerVerify =:= verify_peer andalso not IsSameCA -> false; 149 | true -> false 150 | end, 151 | [ {server_ssl_opts, [ {verify, ServerVerify} | ServerFiles]} 152 | , {client_ssl_opts, [ {verify, ClientVerify} | ClientFiles]} 153 | , {target_host, case IsWildcard of 154 | true -> "a.wildcard.localhost"; 155 | false -> "localhost" 156 | end} 157 | , {expect_pass, IsPass} 158 | | Config]; 159 | init_per_testcase(tlsv13_connect_test, Config) -> 160 | ServerFiles = ?config(common_server_cert_files, Config), 161 | ClientFiles = ?config(known_client_cert_files , Config), 162 | [ {server_ssl_opts, [{verify, verify_peer}, {versions, ['tlsv1.3']} | ServerFiles]} 163 | , {client_ssl_opts, [{verify, verify_peer}, {versions, ['tlsv1.3']} | ClientFiles]} 164 | , {target_host, "localhost"} 165 | | Config]; 166 | init_per_testcase(_, Config) -> 167 | Config. 168 | 169 | %%-------------------------------------------------------------------- 170 | %% Test cases 171 | %%-------------------------------------------------------------------- 172 | 173 | t_tcp_sock(_) -> 174 | Server = tcp_server:start_link(4001), 175 | {ok, Sock} = emqtt_sock:connect("127.0.0.1", 4001, [], 3000), 176 | send_and_recv_with(Sock), 177 | ok = emqtt_sock:close(Sock), 178 | ok = tcp_server:stop(Server). 179 | 180 | t_ssl_sock(Config) -> 181 | SslOpts = [{certfile, certfile(Config)}, 182 | {keyfile, keyfile(Config)}, 183 | {verify, verify_none} 184 | ], 185 | {Server, _} = ssl_server:start_link(4443, SslOpts), 186 | {ok, Sock} = emqtt_sock:connect("127.0.0.1", 4443, [{ssl_opts, [{verify, verify_none}]}], 3000), 187 | send_and_recv_with(Sock), 188 | ok = emqtt_sock:close(Sock), 189 | ssl_server:stop(Server). 190 | 191 | gen_connect_test(Config) -> 192 | {Server, LSock} = ssl_server:start_link(0, ?config(server_ssl_opts, Config)), 193 | {ok, {_, SPort}} = ssl:sockname(LSock), 194 | ConnRes = emqtt_sock:connect(?config(target_host, Config), SPort, 195 | [{ssl_opts, ?config(client_ssl_opts, Config)}], 3000), 196 | case ?config(expect_pass, Config) of 197 | true -> 198 | {ok, Sock} = ConnRes, 199 | send_and_recv_with(Sock); 200 | false -> 201 | case ConnRes of 202 | {ok, Sock} -> 203 | %% connect success but get disconnect later 204 | ?assertException(error, {badmatch, _}, send_and_recv_with(Sock)); 205 | _ -> 206 | ?assertMatch({error, 207 | {tls_alert, 208 | {unknown_ca, 209 | _}}}, ConnRes) 210 | end 211 | end, 212 | ssl_server:stop(Server). 213 | 214 | send_and_recv_with(Sock) -> 215 | {ok, [{send_cnt, SendCnt}, {recv_cnt, RecvCnt}]} = emqtt_sock:getstat(Sock, [send_cnt, recv_cnt]), 216 | {ok, {{127,0,0,1}, _}} = emqtt_sock:sockname(Sock), 217 | ok = emqtt_sock:send(Sock, <<"hi">>), 218 | {ok, <<"hi">>} = emqtt_sock:recv(Sock, 0), 219 | ok = emqtt_sock:setopts(Sock, [{active, 100}]), 220 | {ok, Stats} = emqtt_sock:getstat(Sock, [send_cnt, recv_cnt]), 221 | Stats = [{send_cnt, SendCnt + 1}, {recv_cnt, RecvCnt + 1}]. 222 | 223 | tlsv13_connect_test(Config) -> 224 | ServerSslOpts = ?config(server_ssl_opts, Config), 225 | ClientSslOpts = ?config(client_ssl_opts, Config), 226 | Port = 1024 + random:uniform(erlang:system_info(port_limit) - 1024), 227 | {Server, _} = ssl_server:start_link(Port, ServerSslOpts), 228 | {ok, Sock} = emqtt_sock:connect(?config(target_host, Config), Port, [{ssl_opts, ClientSslOpts}], 3000), 229 | send_and_recv_with(Sock), 230 | ok = emqtt_sock:close(Sock), 231 | ssl_server:stop(Server). 232 | 233 | %%-------------------------------------------------------------------- 234 | %% Helper functions 235 | %%-------------------------------------------------------------------- 236 | certfile(Config) -> 237 | filename:join([cert_dir(Config), "localhost.pem"]). 238 | 239 | keyfile(Config) -> 240 | filename:join([cert_dir(Config), "localhost.key"]). 241 | 242 | cert_dir(Config) -> 243 | filename:join([test_dir(Config), "certs"]). 244 | test_dir(Config) -> 245 | filename:dirname(filename:dirname(proplists:get_value(data_dir, Config))). 246 | 247 | -------------------------------------------------------------------------------- /test/emqtt_test_lib.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(emqtt_test_lib). 18 | 19 | -export([ start_emqx/0 20 | , stop_emqx/0 21 | , ensure_test_module/1 22 | , ensure_listener/4 23 | , ensure_quic_listener/2 24 | , is_tcp_server_available/2 25 | , all/1 26 | , has_quic/0 27 | ]). 28 | 29 | %% TLS helpers 30 | -export([ gen_ca/2 31 | , gen_host_cert/3 32 | , gen_host_cert/4 33 | , ca_key_name/2 34 | , ca_cert_name/2 35 | , key_name/2 36 | , cert_name/2 37 | , set_ssl_options/2 38 | ] 39 | ). 40 | 41 | -spec all(module()) -> list(string()). 42 | all(Suite) -> 43 | ensure_test_module(emqx_common_test_helpers), 44 | emqx_common_test_helpers:all(Suite). 45 | 46 | -spec start_emqx() -> ok. 47 | start_emqx() -> 48 | ensure_test_module(emqx_common_test_helpers), 49 | ensure_test_module(emqx_ratelimiter_SUITE), 50 | emqx_common_test_helpers:start_apps([]), 51 | ok = ensure_quic_listener(mqtt, 14567), 52 | ok. 53 | 54 | -spec stop_emqx() -> ok. 55 | stop_emqx() -> 56 | ensure_test_module(emqx_common_test_helpers), 57 | emqx_common_test_helpers:stop_apps([]). 58 | 59 | is_tcp_server_available(Host, Port) -> 60 | ensure_test_module(emqx_common_test_helpers), 61 | emqx_common_test_helpers:is_tcp_server_available(Host, Port). 62 | 63 | -spec ensure_test_module(M::atom()) -> ok. 64 | ensure_test_module(M) -> 65 | false == code:is_loaded(M) andalso 66 | compile_emqx_test_module(M). 67 | 68 | -spec compile_emqx_test_module(M::atom()) -> ok. 69 | compile_emqx_test_module(M) -> 70 | EmqxDir = code:lib_dir(emqx), 71 | EmqttDir = code:lib_dir(emqtt), 72 | MFilename= filename:join([EmqxDir, "test", M]), 73 | OutDir = filename:join([EmqttDir, "test"]), 74 | {ok, _} = compile:file(MFilename, [{outdir, OutDir}]), 75 | ok. 76 | 77 | -spec ensure_quic_listener(atom(), inet:port_number()) -> ok. 78 | ensure_quic_listener(Name, BindPort) -> 79 | case has_quic() of 80 | true -> 81 | ok = ensure_listener(quic, Name, {0, 0, 0, 0}, BindPort); 82 | _ -> 83 | ok 84 | end. 85 | 86 | -spec ensure_listener(atom(), atom(), inet:ip_address(), inet:port_number()) -> ok. 87 | ensure_listener(Type, Name, BindAddr, BindPort) -> 88 | Type =:= quic andalso application:ensure_all_started(quicer), 89 | BaseConf = #{ 90 | enable => true, 91 | bind => {BindAddr, BindPort}, 92 | acceptors => 16, 93 | max_connections => 1024000, 94 | limiter => #{}, 95 | mountpoint => <<>>, 96 | zone => default, 97 | proxy_protocol => false, 98 | tcp_options => #{active_n => 10}, 99 | hibernate_after => 5000 100 | }, 101 | TypeSpecificConf = listener_conf(Type), 102 | Conf = maps:merge(BaseConf, TypeSpecificConf), 103 | emqx_config:put([listeners, Type, Name], Conf), 104 | case emqx_listeners:start_listener(Type, Name, Conf) of 105 | ok -> ok; 106 | {error, {already_started, _Pid}} -> ok 107 | end. 108 | 109 | listener_conf(quic) -> 110 | CertFile = filename:join(code:lib_dir(emqx), "etc/certs/cert.pem"), 111 | KeyFile = filename:join(code:lib_dir(emqx), "etc/certs/key.pem"), 112 | SslOpts = #{ 113 | certfile => CertFile, 114 | ciphers => 115 | [ 116 | "TLS_AES_256_GCM_SHA384", 117 | "TLS_AES_128_GCM_SHA256", 118 | "TLS_CHACHA20_POLY1305_SHA256" 119 | ], 120 | keyfile => KeyFile 121 | }, 122 | #{ssl_options => SslOpts}; 123 | listener_conf(ws) -> 124 | #{ 125 | websocket => 126 | #{check_origin_enable => false, 127 | compress => false, 128 | deflate_opts => 129 | #{client_context_takeover => takeover, 130 | client_max_window_bits => 15, 131 | mem_level => 8, 132 | server_context_takeover => takeover, 133 | server_max_window_bits => 15, 134 | strategy => default 135 | }, 136 | fail_if_no_subprotocol => true, 137 | idle_timeout => 7200000, 138 | max_frame_size => infinity, 139 | mqtt_path => "/mqtt", 140 | mqtt_piggyback => multiple, 141 | proxy_address_header => "x-forwarded-for", 142 | proxy_port_header => "x-forwarded-port", 143 | supported_subprotocols => ["mqtt","mqtt-v3","mqtt-v3.1.1","mqtt-v5"], 144 | validate_utf8 => true} 145 | }; 146 | listener_conf(_) -> #{}. 147 | 148 | gen_ca(Path, Name) -> 149 | %% Generate ca.pem and ca.key which will be used to generate certs 150 | %% for hosts server and clients 151 | ECKeyFile = filename(Path, "~s-ec.key", [Name]), 152 | os:cmd("openssl ecparam -name secp256r1 > " ++ ECKeyFile), 153 | Cmd = lists:flatten( 154 | io_lib:format("openssl req -new -x509 -nodes " 155 | "-newkey ec:~s " 156 | "-keyout ~s -out ~s -days 3650 " 157 | "-subj \"/C=SE/O=Internet Widgits Pty Ltd CA\"", 158 | [ECKeyFile, ca_key_name(Path, Name), 159 | ca_cert_name(Path, Name)])), 160 | os:cmd(Cmd). 161 | 162 | ca_cert_name(Path, Name) -> 163 | cert_name(Path, Name). 164 | cert_name(Path, Name) -> 165 | filename(Path, "~s.pem", [Name]). 166 | ca_key_name(Path, Name) -> 167 | key_name(Path, Name). 168 | key_name(Path, Name) -> 169 | filename(Path, "~s.key", [Name]). 170 | 171 | gen_host_cert(H, CaName, Path) -> 172 | gen_host_cert(H, CaName, Path, false). 173 | 174 | gen_host_cert(H, CaName, Path, IsWildCard) -> 175 | ECKeyFile = filename(Path, "~s-ec.key", [CaName]), 176 | CN = maybe_wildcard(str(H), IsWildCard), 177 | HKey = filename(Path, "~s.key", [H]), 178 | HCSR = filename(Path, "~s.csr", [H]), 179 | HPEM = filename(Path, "~s.pem", [H]), 180 | HEXT = filename(Path, "~s.extfile", [H]), 181 | CSR_Cmd = 182 | lists:flatten( 183 | io_lib:format( 184 | "openssl req -new -nodes -newkey ec:~s " 185 | "-keyout ~s -out ~s " 186 | "-addext \"subjectAltName=DNS:~s\" " 187 | "-addext keyUsage=digitalSignature,keyAgreement " 188 | "-subj \"/C=SE/O=Internet Widgits Pty Ltd/CN=~s\"", 189 | [ECKeyFile, HKey, HCSR, CN, CN])), 190 | create_file(HEXT, 191 | "keyUsage=digitalSignature,keyAgreement\n" 192 | "subjectAltName=DNS:~s\n", [CN]), 193 | CERT_Cmd = 194 | lists:flatten( 195 | io_lib:format( 196 | "openssl x509 -req " 197 | "-extfile ~s " 198 | "-in ~s -CA ~s -CAkey ~s -CAcreateserial " 199 | "-out ~s -days 500", 200 | [HEXT, HCSR, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), 201 | HPEM])), 202 | os:cmd(CSR_Cmd), 203 | os:cmd(CERT_Cmd), 204 | file:delete(HEXT). 205 | 206 | filename(Path, F, A) -> 207 | filename:join(Path, str(io_lib:format(F, A))). 208 | 209 | str(Arg) -> 210 | binary_to_list(iolist_to_binary(Arg)). 211 | 212 | create_file(Filename, Fmt, Args) -> 213 | {ok, F} = file:open(Filename, [write]), 214 | try 215 | io:format(F, Fmt, Args) 216 | after 217 | file:close(F) 218 | end, 219 | ok. 220 | 221 | maybe_wildcard(Str, true) -> 222 | "*."++Str; 223 | maybe_wildcard(Str, false) -> 224 | Str. 225 | 226 | set_ssl_options(ListenerId, Opts) -> 227 | {ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(ListenerId), 228 | emqx_config:put_listener_conf(Type, Name, [ssl_options], Opts), 229 | ok = emqx_listeners:restart_listener(ListenerId). 230 | 231 | has_quic() -> 232 | false =:= os:getenv("BUILD_WITHOUT_QUIC"). 233 | -------------------------------------------------------------------------------- /test/quic_server.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2021-2023 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | -module(quic_server). 17 | 18 | -export([ start_link/2 19 | , stop/1 20 | ]). 21 | 22 | start_link(Port, Opts) -> 23 | spawn_link(fun() -> quic_server(Port, Opts) end). 24 | 25 | quic_server(Port, Opts) -> 26 | {ok, L} = quicer:listen(Port, Opts), 27 | spawn_link(fun() -> accepter_loop(L) end), 28 | spawn_link(fun() -> accepter_loop(L) end), 29 | receive 30 | stop -> 31 | quicer:close_listener(L), 32 | ok 33 | end. 34 | 35 | accepter_loop(L) -> 36 | case quicer:accept(L, [], 30000) of 37 | {ok, Conn} -> 38 | case quicer:handshake(Conn, 1000) of 39 | {ok, Conn} -> 40 | server_conn_loop(Conn); 41 | _ -> 42 | ct:pal("server handshake failed~n", []), 43 | ok 44 | end; 45 | {error, timeout} -> 46 | ct:pal("server accpet conn timeout ~n", []), 47 | ok 48 | end, 49 | accepter_loop(L). 50 | 51 | 52 | server_conn_loop(Conn) -> 53 | case quicer:accept_stream(Conn, [{active, false}]) of 54 | {ok, Stm} -> 55 | %% Assertion 56 | {ok, false} = quicer:getopt(Stm, active), 57 | server_stream_loop(Conn, Stm); 58 | {error, timeout} -> 59 | ct:pal("server accpet steam timeout ~n", []), 60 | ok 61 | end. 62 | 63 | server_stream_loop(Conn, Stm) -> 64 | quicer:setopt(Stm, active, true), 65 | receive 66 | {quic, <<"ping">>, Stm, #{}} -> 67 | quicer:send(Stm, <<"pong">>) 68 | end, 69 | receive 70 | %% graceful shutdown 71 | {quic, peer_send_shutdown, Stm, undefined} -> 72 | quicer:close_connection(Conn); 73 | %% Conn shutdown 74 | {quic, shutdown, Conn, _} -> 75 | ok 76 | end. 77 | 78 | stop(Server) -> 79 | Server ! stop. 80 | -------------------------------------------------------------------------------- /test/ssl_server.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(ssl_server). 18 | 19 | -export([start_link/2, stop/1]). 20 | 21 | %% Internal exports 22 | -export([ssl_accept/1]). 23 | 24 | start_link(Port, SslOpts) -> 25 | _ = ssl:start(), 26 | {ok, LSock} = ssl:listen(Port, [{active, false}|SslOpts]), 27 | Pid = proc_lib:spawn_link(?MODULE, ssl_accept, [LSock]), 28 | {Pid, LSock}. 29 | 30 | ssl_accept(LSock) -> 31 | {ok, Sock} = ssl:transport_accept(LSock), 32 | case ssl:handshake(Sock, 5000) of 33 | {ok, SslSock} -> 34 | ssl:setopts(SslSock, [{active, true}]), 35 | {stop, _Reason} = ssl_recvloop(SslSock), 36 | ssl:close(LSock); 37 | {error, _} -> 38 | ssl:close(LSock) 39 | end. 40 | 41 | ssl_recvloop(SslSock) -> 42 | receive 43 | {ssl, SslSock, Data} -> 44 | ssl:send(SslSock, Data), 45 | ssl_recvloop(SslSock); 46 | {ssl_error, SslSock, Reason} -> 47 | {stop, Reason}; 48 | {ssl_closed, SslSock} -> 49 | {stop, closed}; 50 | stop -> {stop, normal} 51 | end. 52 | 53 | stop(Server) -> Server ! stop, ok. 54 | 55 | -------------------------------------------------------------------------------- /test/tcp_server.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(tcp_server). 18 | 19 | -export([start_link/1, stop/1]). 20 | 21 | %% Internal exports 22 | -export([accept/1]). 23 | 24 | start_link(Port) -> 25 | {ok, LSock} = gen_tcp:listen(Port, [{active, false}]), 26 | spawn_link(?MODULE, accept, [LSock]). 27 | 28 | accept(LSock) -> 29 | {ok, Sock} = gen_tcp:accept(LSock), 30 | inet:setopts(Sock, [{active, true}]), 31 | {stop, _Reason} = recvloop(Sock), 32 | gen_tcp:close(LSock). 33 | 34 | recvloop(Sock) -> 35 | receive 36 | {tcp, Sock, Data} -> 37 | gen_tcp:send(Sock, Data), 38 | recvloop(Sock); 39 | {tcp_error, Sock, Reason} -> 40 | {stop, Reason}; 41 | {tcp_closed, Sock} -> 42 | {stop, closed}; 43 | stop -> {stop, normal} 44 | end. 45 | 46 | stop(Server) -> Server ! stop, ok. 47 | 48 | -------------------------------------------------------------------------------- /vars-pkg.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% bin/emqtt 3 | %% 4 | {runner_root_dir, "/usr/lib/emqtt"}. 5 | {runner_bin_dir, "/usr/bin"}. 6 | -------------------------------------------------------------------------------- /vars.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% bin/emqtt 3 | %% 4 | {runner_root_dir, "$(cd $(dirname $(readlink $0 || echo $0))/..; pwd -P)"}. 5 | {runner_bin_dir, "$RUNNER_ROOT_DIR/bin"}. --------------------------------------------------------------------------------