├── .devcontainer ├── devcontainer.json └── docker-compose.yml ├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── api-breakage.yml │ ├── ci.yml │ ├── gen-docs.yml │ └── validate.yml ├── .gitignore ├── .mailmap ├── .spi.yml ├── .swift-format ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── LICENSE ├── Package.swift ├── README.md ├── SECURITY.md ├── Sources └── MQTTNIO │ ├── AsyncAwaitSupport │ ├── MQTTClient+async.swift │ ├── MQTTClientV5+async.swift │ └── Sendable.swift │ ├── ChannelHandlers │ ├── MQTTMessageDecoder.swift │ ├── MQTTMessageHandler.swift │ ├── MQTTTaskHandler.swift │ ├── PingreqHandler.swift │ ├── WebSocketHandler.swift │ └── WebSocketInitialRequest.swift │ ├── MQTTClient.swift │ ├── MQTTClientV5.swift │ ├── MQTTConfiguration.swift │ ├── MQTTConnection.swift │ ├── MQTTCoreTypes.swift │ ├── MQTTCoreTypesV5.swift │ ├── MQTTError.swift │ ├── MQTTInflight.swift │ ├── MQTTListeners.swift │ ├── MQTTNIO.docc │ ├── MQTTClient.V5.md │ ├── MQTTClient.md │ ├── mqttnio-aws.md │ ├── mqttnio-connections.md │ ├── mqttnio-v5.md │ └── mqttnio.md │ ├── MQTTPacket.swift │ ├── MQTTProperties.swift │ ├── MQTTReason.swift │ ├── MQTTSerializer.swift │ ├── MQTTTask.swift │ └── TSTLSConfiguration.swift ├── Tests └── MQTTNIOTests │ ├── CoreMQTTTests.swift │ ├── MQTTNIOTests+async.swift │ ├── MQTTNIOTests.swift │ └── MQTTNIOv5Tests.swift ├── docker-compose.yml ├── mosquitto ├── certs │ ├── ca.der │ ├── ca.key │ ├── ca.pem │ ├── ca.srl │ ├── client.csr │ ├── client.key │ ├── client.p12 │ ├── client.pem │ ├── mosquitto.org.crt │ ├── server.csr │ ├── server.key │ └── server.pem ├── config │ ├── mosquitto.conf │ └── passwd └── socket │ └── .gitkeep └── scripts ├── build-docc-gh.sh ├── build-docc.sh ├── commit-docs.sh ├── generate-certs.sh ├── generate_contributors_list.sh ├── mosquitto.sh ├── openssl.cnf └── validate.sh /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MQTT Swift", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "app", 5 | "workspaceFolder": "/workspace", 6 | "features": { 7 | "ghcr.io/devcontainers/features/common-utils:2": { 8 | "installZsh": "false", 9 | "username": "vscode", 10 | "userUid": "1000", 11 | "userGid": "1000", 12 | "upgradePackages": "false" 13 | }, 14 | "ghcr.io/devcontainers/features/git:1": { 15 | "version": "os-provided", 16 | "ppa": "false" 17 | } 18 | }, 19 | // Configure tool-specific properties. 20 | "customizations": { 21 | // Configure properties specific to VS Code. 22 | "vscode": { 23 | "extensions": [ 24 | "sswg.swift-lang" 25 | ], 26 | "settings": { 27 | "lldb.library": "/usr/lib/liblldb.so" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # run this with: docker-compose -f docker-compose.yml run test 2 | version: "3.3" 3 | 4 | services: 5 | app: 6 | image: swift:6.0 7 | volumes: 8 | - ..:/workspace 9 | - mosquitto-socket:/workspace/mosquitto/socket 10 | depends_on: 11 | - mosquitto 12 | environment: 13 | - MOSQUITTO_SERVER=mosquitto 14 | - CI=true 15 | command: sleep infinity 16 | 17 | mosquitto: 18 | image: eclipse-mosquitto 19 | volumes: 20 | - ../mosquitto/config:/mosquitto/config 21 | - ../mosquitto/certs:/mosquitto/certs 22 | - mosquitto-socket:/mosquitto/socket 23 | ports: 24 | - "1883:1883" 25 | - "8883:8883" 26 | - "8080:8080" 27 | - "8081:8081" 28 | 29 | volumes: 30 | mosquitto-socket: 31 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .build 2 | .git -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adam-fowler 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report any Issues found while using MQTT NIO 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Context (please complete the following information):** 24 | - Platform [e.g. iOS, macOS, Linux] 25 | - Swift/Xcode Version: 26 | - Version [e.g. 1.2.0] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | ignore: 8 | - dependency-name: "codecov/codecov-action" 9 | update-types: ["version-update:semver-major"] 10 | groups: 11 | dependencies: 12 | patterns: 13 | - "*" 14 | - package-ecosystem: "swift" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | open-pull-requests-limit: 5 19 | allow: 20 | - dependency-type: all 21 | groups: 22 | all-dependencies: 23 | patterns: 24 | - "*" 25 | -------------------------------------------------------------------------------- /.github/workflows/api-breakage.yml: -------------------------------------------------------------------------------- 1 | name: API breaking changes 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | linux: 8 | runs-on: ubuntu-latest 9 | container: 10 | image: swift:5.9 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | # https://github.com/actions/checkout/issues/766 17 | - name: Mark the workspace as safe 18 | run: git config --global --add safe.directory ${GITHUB_WORKSPACE} 19 | - name: API breaking changes 20 | run: | 21 | swift package diagnose-api-breaking-changes origin/${GITHUB_BASE_REF} 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | release: 11 | types: [published] 12 | workflow_dispatch: 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }}-ci 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | macos: 19 | runs-on: macOS-latest 20 | timeout-minutes: 15 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Install and run Mosquitto 25 | run: | 26 | brew install mosquitto 27 | mosquitto -d -c mosquitto/config/mosquitto.conf 28 | - name: SPM tests 29 | run: swift test --enable-code-coverage 30 | - name: Convert coverage files 31 | run: | 32 | xcrun llvm-cov export -format "lcov" \ 33 | .build/debug/mqtt-nioPackageTests.xctest/Contents/MacOs/mqtt-nioPackageTests \ 34 | -ignore-filename-regex="\/Tests\/" \ 35 | -instr-profile=.build/debug/codecov/default.profdata > info.lcov 36 | - name: Upload to codecov.io 37 | uses: codecov/codecov-action@v4 38 | with: 39 | files: info.lcov 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | 42 | ios: 43 | runs-on: macOS-latest 44 | timeout-minutes: 15 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | - name: Xcodebuild 49 | run: | 50 | xcodebuild build -scheme mqtt-nio -destination 'platform=iOS Simulator,name=iPhone 11' 51 | 52 | linux: 53 | runs-on: ubuntu-latest 54 | timeout-minutes: 15 55 | strategy: 56 | matrix: 57 | tag: 58 | - swift:5.10 59 | - swift:6.0 60 | - swift:6.1 61 | container: 62 | image: ${{ matrix.tag }} 63 | services: 64 | mosquitto: 65 | image: eclipse-mosquitto 66 | options: --name mosquitto 67 | ports: 68 | - 1883:1883 69 | - 1884:1884 70 | - 8883:8883 71 | - 8080:8080 72 | - 8081:8081 73 | volumes: 74 | - ${{ github.workspace }}/mosquitto/config:/mosquitto/config 75 | - ${{ github.workspace }}/mosquitto/certs:/mosquitto/certs 76 | - ${{ github.workspace }}/mosquitto/socket:/mosquitto/socket 77 | 78 | steps: 79 | - name: Checkout 80 | uses: actions/checkout@v4 81 | - name: Restart Mosquitto 82 | # The mosquitto service container is started *before* mqtt-nio is checked 83 | # out. Restarting the container after the checkout step is needed for the 84 | # container to see volumes populated from the checked out workspace. 85 | uses: docker://docker 86 | with: 87 | args: docker restart mosquitto 88 | - name: Test 89 | env: 90 | MOSQUITTO_SERVER: mosquitto 91 | run: | 92 | swift test --enable-test-discovery --enable-code-coverage 93 | - name: Convert coverage files 94 | run: | 95 | llvm-cov export -format="lcov" \ 96 | .build/debug/mqtt-nioPackageTests.xctest \ 97 | -ignore-filename-regex="\/Tests\/" \ 98 | -instr-profile .build/debug/codecov/default.profdata > info.lcov 99 | - name: Upload to codecov.io 100 | uses: codecov/codecov-action@v4 101 | with: 102 | files: info.lcov 103 | token: ${{ secrets.CODECOV_TOKEN }} 104 | -------------------------------------------------------------------------------- /.github/workflows/gen-docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate Documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: macOS-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Install Dependencies 19 | run: | 20 | brew install mint 21 | mint install apple/swift-docc@main 22 | echo "$HOME/.mint/bin" >> $GITHUB_PATH 23 | - name: Build 24 | env: 25 | DOCC: docc 26 | run: | 27 | ./scripts/build-docc-gh.sh 28 | - name: Commit 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | REMOTE_BRANCH: "gh-pages" 32 | run: | 33 | REMOTE_REPO="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 34 | git config user.name "${GITHUB_ACTOR}" 35 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 36 | 37 | mv docs _docs 38 | git checkout ${REMOTE_BRANCH} 39 | rm -rf docs/ 40 | mv _docs/mqtt-nio docs 41 | 42 | git add --all docs 43 | git status 44 | git commit -m "Documentation for https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_SHA}" -m "Generated by gen-docs.yml" 45 | git push ${REMOTE_REPO} ${REMOTE_BRANCH} 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validity Check 2 | 3 | on: 4 | pull_request: 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }}-validate 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | validate: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 15 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 1 18 | - name: run script 19 | run: ./scripts/validate.sh 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /.vscode 5 | /docs 6 | /Packages 7 | /*.xcodeproj 8 | xcuserdata/ 9 | Package.resolved -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Pete Grayson Pete Grayson 2 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [MQTTNIO] 5 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version" : 1, 3 | "indentation" : { 4 | "spaces" : 4 5 | }, 6 | "tabWidth" : 4, 7 | "fileScopedDeclarationPrivacy" : { 8 | "accessLevel" : "private" 9 | }, 10 | "spacesAroundRangeFormationOperators" : false, 11 | "indentConditionalCompilationBlocks" : false, 12 | "indentSwitchCaseLabels" : false, 13 | "lineBreakAroundMultilineExpressionChainComponents" : false, 14 | "lineBreakBeforeControlFlowKeywords" : false, 15 | "lineBreakBeforeEachArgument" : true, 16 | "lineBreakBeforeEachGenericRequirement" : true, 17 | "lineLength" : 150, 18 | "maximumBlankLines" : 1, 19 | "respectsExistingLineBreaks" : true, 20 | "prioritizeKeepingFunctionOutputTogether" : true, 21 | "multiElementCollectionTrailingCommas" : true, 22 | "rules" : { 23 | "AllPublicDeclarationsHaveDocumentation" : false, 24 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 25 | "AlwaysUseLowerCamelCase" : false, 26 | "AmbiguousTrailingClosureOverload" : true, 27 | "BeginDocumentationCommentWithOneLineSummary" : false, 28 | "DoNotUseSemicolons" : true, 29 | "DontRepeatTypeInStaticProperties" : true, 30 | "FileScopedDeclarationPrivacy" : true, 31 | "FullyIndirectEnum" : true, 32 | "GroupNumericLiterals" : true, 33 | "IdentifiersMustBeASCII" : true, 34 | "NeverForceUnwrap" : false, 35 | "NeverUseForceTry" : false, 36 | "NeverUseImplicitlyUnwrappedOptionals" : false, 37 | "NoAccessLevelOnExtensionDeclaration" : true, 38 | "NoAssignmentInExpressions" : true, 39 | "NoBlockComments" : true, 40 | "NoCasesWithOnlyFallthrough" : true, 41 | "NoEmptyTrailingClosureParentheses" : true, 42 | "NoLabelsInCasePatterns" : true, 43 | "NoLeadingUnderscores" : false, 44 | "NoParensAroundConditions" : true, 45 | "NoVoidReturnOnFunctionSignature" : true, 46 | "OmitExplicitReturns" : true, 47 | "OneCasePerLine" : true, 48 | "OneVariableDeclarationPerLine" : true, 49 | "OnlyOneTrailingClosureArgument" : true, 50 | "OrderedImports" : true, 51 | "ReplaceForEachWithForLoop" : true, 52 | "ReturnVoidInsteadOfEmptyTuple" : true, 53 | "UseEarlyExits" : false, 54 | "UseExplicitNilCheckInConditions" : false, 55 | "UseLetInEveryBoundCaseVariable" : false, 56 | "UseShorthandTypeNames" : true, 57 | "UseSingleLinePropertyGetter" : false, 58 | "UseSynthesizedInitializer" : false, 59 | "UseTripleSlashForDocumentationComments" : true, 60 | "UseWhereClausesInForLoops" : false, 61 | "ValidateDocumentationComments" : false 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All developers should feel welcome and encouraged to contribute to MQTTNIO. Because of this we have adopted the code of conduct defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source 4 | communities, and we think it articulates our values well. The full text is copied below: 5 | 6 | ## Contributor Code of Conduct v1.3 7 | 8 | As contributors and maintainers of this project, and in the interest of 9 | fostering an open and welcoming community, we pledge to respect all people who 10 | contribute through reporting issues, posting feature requests, updating 11 | documentation, submitting pull requests or patches, and other activities. 12 | 13 | We are committed to making participation in this project a harassment-free 14 | experience for everyone, regardless of level of experience, gender, gender 15 | identity and expression, sexual orientation, disability, personal appearance, 16 | body size, race, ethnicity, age, religion, or nationality. 17 | 18 | Examples of unacceptable behavior by participants include: 19 | 20 | * The use of sexualized language or imagery 21 | * Personal attacks 22 | * Trolling or insulting/derogatory comments 23 | * Public or private harassment 24 | * Publishing other's private information, such as physical or electronic 25 | addresses, without explicit permission 26 | * Other unethical or unprofessional conduct 27 | 28 | Project maintainers have the right and responsibility to remove, edit, or 29 | reject comments, commits, code, wiki edits, issues, and other contributions 30 | that are not aligned to this Code of Conduct, or to ban temporarily or 31 | permanently any contributor for other behaviors that they deem inappropriate, 32 | threatening, offensive, or harmful. 33 | 34 | By adopting this Code of Conduct, project maintainers commit themselves to 35 | fairly and consistently applying these principles to every aspect of managing 36 | this project. Project maintainers who do not follow or enforce the Code of 37 | Conduct may be permanently removed from the project team. 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces 40 | when an individual is representing the project or its community. 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 43 | reported by contacting a project maintainer at [INSERT EMAIL ADDRESS]. All 44 | complaints will be reviewed and investigated and will result in a response that 45 | is deemed necessary and appropriate to the circumstances. Maintainers are 46 | obligated to maintain confidentiality with regard to the reporter of an 47 | incident. 48 | 49 | 50 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 51 | version 1.3.0, available at https://www.contributor-covenant.org/version/1/3/0/code-of-conduct.html 52 | 53 | [homepage]: https://www.contributor-covenant.org 54 | 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Legal 4 | By submitting a pull request, you represent that you have the right to license your contribution to the community, and agree by submitting the patch 5 | that your contributions are licensed under the Apache 2.0 license (see [LICENSE](LICENSE.txt)). 6 | 7 | ## Contributor Conduct 8 | All contributors are expected to adhere to the project's [Code of Conduct](CODE_OF_CONDUCT.md). 9 | 10 | ## Submitting a bug or issue 11 | Please ensure to include the following in your bug report 12 | - A concise description of the issue, what happened and what you expected. 13 | - Simple reproduction steps 14 | - Version of the library you are using 15 | - Contextual information (Swift version, OS etc) 16 | 17 | ## Submitting a Pull Request 18 | 19 | Please ensure to include the following in your Pull Request 20 | - A description of what you are trying to do. What the PR provides to the library, additional functionality, fixing a bug etc 21 | - A description of the code changes 22 | - Documentation on how these changes are being tested 23 | - Additional tests to show your code working and to ensure future changes don't break your code. 24 | 25 | Please keep your PRs to a minimal number of changes. If a PR is large try to split it up into smaller PRs. Don't move code around unnecessarily it makes comparing old with new very hard. 26 | 27 | The main development branch of the repository is `main`. 28 | 29 | ### Testing 30 | 31 | The project tests expect a local version of mosquitto to be running using the config that is in this project. You can start a local mosquitto MQTT server by running the script `scripts/mosquitto.sh`. You can test the Linux version using Docker. There is a docker-compose file in the root of this project. Run Linux tests as follows 32 | ``` 33 | docker-compose run test 34 | ``` 35 | 36 | ### Formatting 37 | 38 | We use Nick Lockwood's SwiftFormat for formatting code. PRs will not be accepted if they haven't be formatted. The current version of SwiftFormat we are using is v0.52.10. 39 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to Soto. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | ### Contributors 11 | 12 | - Adam Fowler 13 | - Benedek Kozma 14 | - Michael Housh 15 | - Michal Šrůtek <35694712+michalsrutek@users.noreply.github.com> 16 | - Pete Grayson 17 | - Sean Patrick O'Brien 18 | - Sven A. Schmidt 19 | - x_0o0 20 | 21 | **Updating this list** 22 | 23 | Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` 24 | -------------------------------------------------------------------------------- /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 | 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 2020 Adam Fowler 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "mqtt-nio", 7 | platforms: [.macOS(.v10_14), .iOS(.v12), .tvOS(.v12), .watchOS(.v6)], 8 | products: [ 9 | .library(name: "MQTTNIO", targets: ["MQTTNIO"]) 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.0"), 13 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 14 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"), 15 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.14.0"), 16 | .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.20.0"), 17 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "MQTTNIO", 22 | dependencies: [ 23 | .product(name: "Atomics", package: "swift-atomics"), 24 | .product(name: "Logging", package: "swift-log"), 25 | .product(name: "NIO", package: "swift-nio"), 26 | .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), 27 | .product(name: "NIOHTTP1", package: "swift-nio"), 28 | .product(name: "NIOWebSocket", package: "swift-nio"), 29 | .product(name: "NIOSSL", package: "swift-nio-ssl", condition: .when(platforms: [.linux, .macOS])), 30 | .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), 31 | ] 32 | ), 33 | .testTarget(name: "MQTTNIOTests", dependencies: ["MQTTNIO"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MQTT NIO 2 | 3 | [![sswg:sandbox|94x20](https://img.shields.io/badge/sswg-sandbox-lightgrey.svg)](https://github.com/swift-server/sswg/blob/master/process/incubation.md#sandbox-level) 4 | [Swift 5.7](https://swift.org) 5 | [](https://github.com/adam-fowler/mqtt-nio/workflows/CI/badge.svg) 6 | 7 | A Swift NIO based MQTT v3.1.1 and v5.0 client. 8 | 9 | MQTT (Message Queuing Telemetry Transport) is a lightweight messaging protocol that was developed by IBM and first released in 1999. It uses the pub/sub pattern and translates messages between devices, servers, and applications. It is commonly used in Internet of things (IoT) technologies. 10 | 11 | MQTTNIO is a Swift NIO based implementation of a MQTT client. It supports 12 | - MQTT versions 3.1.1 and 5.0. 13 | - Unencrypted and encrypted (via TLS) connections 14 | - WebSocket connections 15 | - Posix sockets 16 | - Apple's Network framework via [NIOTransportServices](https://github.com/apple/swift-nio-transport-services) (required for iOS). 17 | - Unix domain sockets 18 | 19 | You can find documentation for MQTTNIO 20 | [here](https://swift-server-community.github.io/mqtt-nio/documentation/mqttnio/). There is also a sample demonstrating the use MQTTNIO in an iOS app found [here](https://github.com/adam-fowler/EmCuTeeTee) 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Currently we support versions 2.x.x of MQTTNIO. These will receive security updates as and when needed. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you believe you have found a security vulnerability in MQTTNIO please do not post this in a public forum, do not create a GitHub Issue. Instead you should email [security@soto.codes](mailto:security@soto.codes) with details of the issue. 10 | 11 | #### What happens next? 12 | 13 | * A member of the team will acknowledge receipt of the report within 5 14 | working days. This may include a request for additional 15 | information about reproducing the vulnerability. 16 | * We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the 17 | vulnerability within 10 days of the report as per their [security 18 | guidelines][sswg-security]. 19 | * Once we have identified a fix we may ask you to validate it. We aim to do this 20 | within 30 days, but this may not always be possible. 21 | * We will decide on a planned release date and let you know when it is. 22 | * Once the fix has been released we will publish a security advisory on GitHub 23 | and the [SSWG][sswg] will announce the vulnerability on the [Swift 24 | forums][swift-forums-sec]. 25 | 26 | [sswg]: https://github.com/swift-server/sswg 27 | [sswg-security]: https://github.com/swift-server/sswg/blob/main/process/incubation.md#security-best-practices 28 | [swift-forums-sec]: https://forums.swift.org/c/server/security-updates/ 29 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/AsyncAwaitSupport/MQTTClient+async.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIOCore 15 | 16 | #if canImport(FoundationEssentials) 17 | import Dispatch 18 | import FoundationEssentials 19 | #else 20 | import Foundation 21 | #endif 22 | 23 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 24 | extension MQTTClient { 25 | /// Shutdown MQTTClient asynchronously. 26 | /// 27 | /// Before an `MQTTClient` is deleted you need to call this function or the synchronous 28 | /// version `syncShutdownGracefully` to do a clean shutdown of the client. It closes the 29 | /// connection, notifies everything listening for shutdown and shuts down the EventLoopGroup 30 | /// if the client created it 31 | /// 32 | /// - Parameters: 33 | /// - queue: Dispatch Queue to run shutdown on 34 | public func shutdown(queue: DispatchQueue = .global()) async throws { 35 | try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation) in 36 | self.shutdown(queue: queue) { error in 37 | if let error { 38 | cont.resume(throwing: error) 39 | } else { 40 | cont.resume() 41 | } 42 | } 43 | } 44 | } 45 | 46 | /// Connect to MQTT server 47 | /// 48 | /// If `cleanSession` is set to false the Server MUST resume communications with the Client based on 49 | /// state from the current Session (as identified by the Client identifier). If there is no Session 50 | /// associated with the Client identifier the Server MUST create a new Session. The Client and Server 51 | /// MUST store the Session after the Client and Server are disconnected. If set to true then the Client 52 | /// and Server MUST discard any previous Session and start a new one 53 | /// 54 | /// - Parameters: 55 | /// - cleanSession: should we start with a new session 56 | /// - will: Publish message to be posted as soon as connection is made 57 | /// - Returns: EventLoopFuture to be updated with whether server holds a session for this client 58 | /// - Returns: Whether server held a session for this client and has restored it. 59 | @discardableResult public func connect( 60 | cleanSession: Bool = true, 61 | will: (topicName: String, payload: ByteBuffer, qos: MQTTQoS, retain: Bool)? = nil 62 | ) async throws -> Bool { 63 | try await self.connect(cleanSession: cleanSession, will: will).get() 64 | } 65 | 66 | /// Connect to MQTT server 67 | /// 68 | /// If `cleanSession` is set to false the Server MUST resume communications with the Client based on 69 | /// state from the current Session (as identified by the Client identifier). If there is no Session 70 | /// associated with the Client identifier the Server MUST create a new Session. The Client and Server 71 | /// MUST store the Session after the Client and Server are disconnected. If set to true then the Client 72 | /// and Server MUST discard any previous Session and start a new one 73 | /// 74 | /// - Parameters: 75 | /// - cleanSession: should we start with a new session 76 | /// - will: Publish message to be posted as soon as connection is made 77 | /// - Returns: EventLoopFuture to be updated with whether server holds a session for this client 78 | /// - Returns: Whether server held a session for this client and has restored it. 79 | @discardableResult public func connect( 80 | cleanSession: Bool = true, 81 | will: (topicName: String, payload: ByteBuffer, qos: MQTTQoS, retain: Bool)? = nil, 82 | connectConfiguration: ConnectConfiguration 83 | ) async throws -> Bool { 84 | try await self.connect( 85 | cleanSession: cleanSession, 86 | will: will, 87 | connectConfiguration: 88 | connectConfiguration 89 | ).get() 90 | } 91 | 92 | /// Publish message to topic 93 | /// 94 | /// Depending on QoS completes when message is sent, when PUBACK is received or when PUBREC 95 | /// and following PUBCOMP are received 96 | /// 97 | /// Waits for publish to complete. Depending on QoS setting the future will complete 98 | /// when message is sent, when PUBACK is received or when PUBREC and following PUBCOMP are 99 | /// received 100 | /// - Parameters: 101 | /// - topicName: Topic name on which the message is published 102 | /// - payload: Message payload 103 | /// - qos: Quality of Service for message. 104 | /// - retain: Whether this is a retained message. 105 | public func publish(to topicName: String, payload: ByteBuffer, qos: MQTTQoS, retain: Bool = false) async throws { 106 | try await self.publish(to: topicName, payload: payload, qos: qos, retain: retain).get() 107 | } 108 | 109 | /// Subscribe to topic 110 | /// 111 | /// Completes when SUBACK is received 112 | /// - Parameter subscriptions: Subscription infos 113 | public func subscribe(to subscriptions: [MQTTSubscribeInfo]) async throws -> MQTTSuback { 114 | try await self.subscribe(to: subscriptions).get() 115 | } 116 | 117 | /// Unsubscribe from topic 118 | /// 119 | /// Completes when UNSUBACK is received 120 | /// - Parameter subscriptions: List of subscriptions to unsubscribe from 121 | public func unsubscribe(from subscriptions: [String]) async throws { 122 | try await self.unsubscribe(from: subscriptions).get() 123 | } 124 | 125 | /// Ping the server to test if it is still alive and to tell it you are alive. 126 | /// 127 | /// Completes when PINGRESP is received 128 | /// 129 | /// You shouldn't need to call this as the `MQTTClient` automatically sends PINGREQ messages to the server to ensure 130 | /// the connection is still live. If you initialize the client with the configuration `disablePingReq: true` then these 131 | /// are disabled and it is up to you to send the PINGREQ messages yourself 132 | public func ping() async throws { 133 | try await self.ping().get() 134 | } 135 | 136 | /// Disconnect from server 137 | public func disconnect() async throws { 138 | try await self.disconnect().get() 139 | } 140 | 141 | /// Create a publish listener AsyncSequence that yields a result whenever a PUBLISH message is received from the server 142 | /// 143 | /// To create listener and process results 144 | /// ``` 145 | /// Task { 146 | /// let listener = client.createPublishListener() 147 | /// for result in listener { 148 | /// switch result { 149 | /// case .success(let packet): 150 | /// ... 151 | /// case .failure: 152 | /// break 153 | /// } 154 | /// } 155 | /// } 156 | /// ``` 157 | public func createPublishListener() -> MQTTPublishListener { 158 | .init(self) 159 | } 160 | } 161 | 162 | /// MQTT Publish message listener AsyncSequence 163 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 164 | public class MQTTPublishListener: AsyncSequence { 165 | public typealias AsyncIterator = AsyncStream.AsyncIterator 166 | public typealias Element = Result 167 | 168 | let client: MQTTClient 169 | let stream: AsyncStream 170 | let name: String 171 | 172 | init(_ client: MQTTClient) { 173 | let name = UUID().uuidString 174 | self.client = client 175 | self.name = name 176 | let cleanSession = client.connection?.cleanSession ?? true 177 | self.stream = AsyncStream { cont in 178 | client.addPublishListener(named: name) { result in 179 | cont.yield(result) 180 | } 181 | client.addShutdownListener(named: name) { _ in 182 | cont.finish() 183 | } 184 | client.addCloseListener(named: name) { connectResult in 185 | if cleanSession { 186 | cont.finish() 187 | } 188 | } 189 | } 190 | } 191 | 192 | deinit { 193 | self.client.removePublishListener(named: self.name) 194 | self.client.removeCloseListener(named: self.name) 195 | self.client.removeShutdownListener(named: self.name) 196 | } 197 | 198 | public __consuming func makeAsyncIterator() -> AsyncStream.AsyncIterator { 199 | self.stream.makeAsyncIterator() 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/AsyncAwaitSupport/MQTTClientV5+async.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIOCore 15 | 16 | #if canImport(FoundationEssentials) 17 | import FoundationEssentials 18 | #else 19 | import Foundation 20 | #endif 21 | 22 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 23 | extension MQTTClient.V5 { 24 | /// Connect to MQTT server 25 | /// 26 | /// If `cleanStart` is set to false the Server MUST resume communications with the Client based on 27 | /// state from the current Session (as identified by the Client identifier). If there is no Session 28 | /// associated with the Client identifier the Server MUST create a new Session. The Client and Server 29 | /// MUST store the Session after the Client and Server are disconnected. If set to true then the 30 | /// Client and Server MUST discard any previous Session and start a new one 31 | /// 32 | /// The function returns an EventLoopFuture which will be updated with whether the server has restored a session for this client. 33 | /// 34 | /// - Parameters: 35 | /// - cleanStart: should we start with a new session 36 | /// - properties: properties to attach to connect message 37 | /// - will: Publish message to be posted as soon as connection is made 38 | /// - authWorkflow: The authentication workflow. This is currently unimplemented. 39 | /// - Returns: CONNACK response 40 | public func connect( 41 | cleanStart: Bool = true, 42 | properties: MQTTProperties = .init(), 43 | will: (topicName: String, payload: ByteBuffer, qos: MQTTQoS, retain: Bool, properties: MQTTProperties)? = nil, 44 | authWorkflow: ((MQTTAuthV5, EventLoop) -> EventLoopFuture)? = nil 45 | ) async throws -> MQTTConnackV5 { 46 | try await self.connect(cleanStart: cleanStart, properties: properties, will: will, authWorkflow: authWorkflow).get() 47 | } 48 | 49 | /// Connect to MQTT server 50 | /// 51 | /// If `cleanStart` is set to false the Server MUST resume communications with the Client based on 52 | /// state from the current Session (as identified by the Client identifier). If there is no Session 53 | /// associated with the Client identifier the Server MUST create a new Session. The Client and Server 54 | /// MUST store the Session after the Client and Server are disconnected. If set to true then the 55 | /// Client and Server MUST discard any previous Session and start a new one 56 | /// 57 | /// The function returns an EventLoopFuture which will be updated with whether the server has restored a session for this client. 58 | /// 59 | /// - Parameters: 60 | /// - cleanStart: should we start with a new session 61 | /// - properties: properties to attach to connect message 62 | /// - will: Publish message to be posted as soon as connection is made 63 | /// - authWorkflow: The authentication workflow. This is currently unimplemented. 64 | /// - connectConfiguration: Override client configuration during connection 65 | /// - Returns: CONNACK response 66 | public func connect( 67 | cleanStart: Bool = true, 68 | properties: MQTTProperties = .init(), 69 | will: (topicName: String, payload: ByteBuffer, qos: MQTTQoS, retain: Bool, properties: MQTTProperties)? = nil, 70 | authWorkflow: ((MQTTAuthV5, EventLoop) -> EventLoopFuture)? = nil, 71 | connectConfiguration: MQTTClient.ConnectConfiguration 72 | ) async throws -> MQTTConnackV5 { 73 | try await self.connect( 74 | cleanStart: cleanStart, 75 | properties: properties, 76 | will: will, 77 | authWorkflow: authWorkflow, 78 | connectConfiguration: connectConfiguration 79 | ).get() 80 | } 81 | 82 | /// Publish message to topic 83 | /// - Parameters: 84 | /// - topicName: Topic name on which the message is published 85 | /// - payload: Message payload 86 | /// - qos: Quality of Service for message. 87 | /// - retain: Whether this is a retained message. 88 | /// - properties: properties to attach to publish message 89 | /// - Returns: Future waiting for publish to complete. Depending on QoS setting the future will complete 90 | /// when message is sent, when PUBACK is received or when PUBREC and following PUBCOMP are 91 | /// received. QoS1 and above return an `MQTTAckV5` which contains a `reason` and `properties` 92 | public func publish( 93 | to topicName: String, 94 | payload: ByteBuffer, 95 | qos: MQTTQoS, 96 | retain: Bool = false, 97 | properties: MQTTProperties = .init() 98 | ) async throws -> MQTTAckV5? { 99 | try await self.publish(to: topicName, payload: payload, qos: qos, retain: retain, properties: properties).get() 100 | } 101 | 102 | /// Subscribe to topic 103 | /// - Parameters: 104 | /// - subscriptions: Subscription infos 105 | /// - properties: properties to attach to subscribe message 106 | /// - Returns: Future waiting for subscribe to complete. Will wait for SUBACK message from server and 107 | /// return its contents 108 | public func subscribe( 109 | to subscriptions: [MQTTSubscribeInfoV5], 110 | properties: MQTTProperties = .init() 111 | ) async throws -> MQTTSubackV5 { 112 | try await self.subscribe(to: subscriptions, properties: properties).get() 113 | } 114 | 115 | /// Unsubscribe from topic 116 | /// - Parameters: 117 | /// - subscriptions: List of subscriptions to unsubscribe from 118 | /// - properties: properties to attach to unsubscribe message 119 | /// - Returns: Future waiting for unsubscribe to complete. Will wait for UNSUBACK message from server and 120 | /// return its contents 121 | public func unsubscribe( 122 | from subscriptions: [String], 123 | properties: MQTTProperties = .init() 124 | ) async throws -> MQTTSubackV5 { 125 | try await self.unsubscribe(from: subscriptions, properties: properties).get() 126 | } 127 | 128 | /// Disconnect from server 129 | /// - Parameter properties: properties to attach to disconnect packet 130 | /// - Returns: Future waiting on disconnect message to be sent 131 | public func disconnect(properties: MQTTProperties = .init()) async throws { 132 | try await self.disconnect(properties: properties).get() 133 | } 134 | 135 | /// Re-authenticate with server 136 | /// 137 | /// - Parameters: 138 | /// - properties: properties to attach to auth packet. Must include `authenticationMethod` 139 | /// - authWorkflow: Respond to auth packets from server 140 | /// - Returns: final auth packet returned from server 141 | public func auth( 142 | properties: MQTTProperties, 143 | authWorkflow: ((MQTTAuthV5, EventLoop) -> EventLoopFuture)? = nil 144 | ) async throws -> MQTTAuthV5 { 145 | try await self.auth(properties: properties, authWorkflow: authWorkflow).get() 146 | } 147 | 148 | /// Create a publish listener AsyncSequence that yields a value whenever a PUBLISH message is received from the server 149 | /// with the specified subscription id 150 | public func createPublishListener(subscriptionId: UInt) -> MQTTPublishIdListener { 151 | .init(self, subscriptionId: subscriptionId) 152 | } 153 | } 154 | 155 | /// MQTT Publish message listener, that filters messages by subscription identifier 156 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 157 | public class MQTTPublishIdListener: AsyncSequence { 158 | public typealias AsyncIterator = AsyncStream.AsyncIterator 159 | public typealias Element = MQTTPublishInfo 160 | 161 | let client: MQTTClient 162 | let stream: AsyncStream 163 | let name: String 164 | 165 | init(_ client: MQTTClient.V5, subscriptionId: UInt) { 166 | let name = UUID().uuidString 167 | self.client = client.client 168 | self.name = name 169 | let cleanSession = client.client.connection?.cleanSession ?? true 170 | self.stream = AsyncStream { cont in 171 | client.addPublishListener(named: name, subscriptionId: subscriptionId) { result in 172 | cont.yield(result) 173 | } 174 | client.client.addShutdownListener(named: name) { _ in 175 | cont.finish() 176 | } 177 | client.client.addCloseListener(named: name) { connectResult in 178 | if cleanSession { 179 | cont.finish() 180 | } 181 | } 182 | } 183 | } 184 | 185 | deinit { 186 | self.client.removePublishListener(named: self.name) 187 | self.client.removeCloseListener(named: self.name) 188 | self.client.removeShutdownListener(named: self.name) 189 | } 190 | 191 | public __consuming func makeAsyncIterator() -> AsyncStream.AsyncIterator { 192 | self.stream.makeAsyncIterator() 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/AsyncAwaitSupport/Sendable.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | @available(*, deprecated, renamed: "Sendable") 15 | public typealias _MQTTSendable = Sendable 16 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/ChannelHandlers/MQTTMessageDecoder.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Logging 15 | import NIO 16 | 17 | /// Decode ByteBuffers into MQTT Messages 18 | struct ByteToMQTTMessageDecoder: NIOSingleStepByteToMessageDecoder { 19 | mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> MQTTPacket? { 20 | try self.decode(buffer: &buffer) 21 | } 22 | 23 | typealias InboundOut = MQTTPacket 24 | 25 | let version: MQTTClient.Version 26 | 27 | init(version: MQTTClient.Version) { 28 | self.version = version 29 | } 30 | 31 | mutating func decode(buffer: inout ByteBuffer) throws -> MQTTPacket? { 32 | let origBuffer = buffer 33 | do { 34 | let packet = try MQTTIncomingPacket.read(from: &buffer) 35 | let message: MQTTPacket 36 | switch packet.type { 37 | case .PUBLISH: 38 | message = try MQTTPublishPacket.read(version: self.version, from: packet) 39 | case .CONNACK: 40 | message = try MQTTConnAckPacket.read(version: self.version, from: packet) 41 | case .PUBACK, .PUBREC, .PUBREL, .PUBCOMP: 42 | message = try MQTTPubAckPacket.read(version: self.version, from: packet) 43 | case .SUBACK, .UNSUBACK: 44 | message = try MQTTSubAckPacket.read(version: self.version, from: packet) 45 | case .PINGRESP: 46 | message = try MQTTPingrespPacket.read(version: self.version, from: packet) 47 | case .DISCONNECT: 48 | message = try MQTTDisconnectPacket.read(version: self.version, from: packet) 49 | case .AUTH: 50 | message = try MQTTAuthPacket.read(version: self.version, from: packet) 51 | default: 52 | throw MQTTError.decodeError 53 | } 54 | return message 55 | } catch InternalError.incompletePacket { 56 | buffer = origBuffer 57 | return nil 58 | } catch { 59 | throw error 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/ChannelHandlers/MQTTMessageHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Logging 15 | import NIO 16 | 17 | class MQTTMessageHandler: ChannelDuplexHandler { 18 | typealias InboundIn = ByteBuffer 19 | typealias InboundOut = MQTTPacket 20 | typealias OutboundIn = MQTTPacket 21 | typealias OutboundOut = ByteBuffer 22 | 23 | let client: MQTTClient 24 | let pingreqHandler: PingreqHandler? 25 | var decoder: NIOSingleStepByteToMessageProcessor 26 | 27 | init(_ client: MQTTClient, pingInterval: TimeAmount) { 28 | self.client = client 29 | if client.configuration.disablePing { 30 | self.pingreqHandler = nil 31 | } else { 32 | self.pingreqHandler = .init(client: client, timeout: pingInterval) 33 | } 34 | self.decoder = .init(.init(version: client.configuration.version)) 35 | } 36 | 37 | func handlerAdded(context: ChannelHandlerContext) { 38 | if context.channel.isActive { 39 | self.pingreqHandler?.start(context: context) 40 | } 41 | } 42 | 43 | func channelActive(context: ChannelHandlerContext) { 44 | self.pingreqHandler?.start(context: context) 45 | context.fireChannelActive() 46 | } 47 | 48 | func channelInactive(context: ChannelHandlerContext) { 49 | self.pingreqHandler?.stop() 50 | context.fireChannelInactive() 51 | } 52 | 53 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 54 | let message = unwrapOutboundIn(data) 55 | self.client.logger.trace("MQTT Out", metadata: ["mqtt_message": .string("\(message)"), "mqtt_packet_id": .string("\(message.packetId)")]) 56 | var bb = context.channel.allocator.buffer(capacity: 0) 57 | do { 58 | try message.write(version: self.client.configuration.version, to: &bb) 59 | context.write(wrapOutboundOut(bb), promise: promise) 60 | } catch { 61 | promise?.fail(error) 62 | } 63 | self.pingreqHandler?.write() 64 | } 65 | 66 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 67 | let buffer = self.unwrapInboundIn(data) 68 | 69 | do { 70 | try self.decoder.process(buffer: buffer) { message in 71 | switch message.type { 72 | case .PUBLISH: 73 | let publishMessage = message as! MQTTPublishPacket 74 | // publish logging includes topic name 75 | self.client.logger.trace( 76 | "MQTT In", 77 | metadata: [ 78 | "mqtt_message": .stringConvertible(publishMessage), 79 | "mqtt_packet_id": .stringConvertible(publishMessage.packetId), 80 | "mqtt_topicName": .string(publishMessage.publish.topicName), 81 | ] 82 | ) 83 | self.respondToPublish(publishMessage) 84 | return 85 | 86 | case .CONNACK, .PUBACK, .PUBREC, .PUBCOMP, .SUBACK, .UNSUBACK, .PINGRESP, .AUTH: 87 | context.fireChannelRead(wrapInboundOut(message)) 88 | 89 | case .PUBREL: 90 | self.respondToPubrel(message) 91 | context.fireChannelRead(wrapInboundOut(message)) 92 | 93 | case .DISCONNECT: 94 | let disconnectMessage = message as! MQTTDisconnectPacket 95 | let ack = MQTTAckV5(reason: disconnectMessage.reason, properties: disconnectMessage.properties) 96 | context.fireErrorCaught(MQTTError.serverDisconnection(ack)) 97 | context.close(promise: nil) 98 | 99 | case .CONNECT, .SUBSCRIBE, .UNSUBSCRIBE, .PINGREQ: 100 | context.fireErrorCaught(MQTTError.unexpectedMessage) 101 | context.close(promise: nil) 102 | self.client.logger.error("Unexpected MQTT Message", metadata: ["mqtt_message": .string("\(message)")]) 103 | return 104 | } 105 | self.client.logger.trace( 106 | "MQTT In", 107 | metadata: ["mqtt_message": .stringConvertible(message), "mqtt_packet_id": .stringConvertible(message.packetId)] 108 | ) 109 | } 110 | } catch { 111 | context.fireErrorCaught(error) 112 | context.close(promise: nil) 113 | self.client.logger.error("Error processing MQTT message", metadata: ["mqtt_error": .string("\(error)")]) 114 | } 115 | } 116 | 117 | func updatePingreqTimeout(_ timeout: TimeAmount) { 118 | self.pingreqHandler?.updateTimeout(timeout) 119 | } 120 | 121 | /// Respond to PUBLISH message 122 | /// If QoS is `.atMostOnce` then no response is required 123 | /// If QoS is `.atLeastOnce` then send PUBACK 124 | /// If QoS is `.exactlyOnce` then send PUBREC, wait for PUBREL and then respond with PUBCOMP (in `respondToPubrel`) 125 | private func respondToPublish(_ message: MQTTPublishPacket) { 126 | guard let connection = client.connection else { return } 127 | switch message.publish.qos { 128 | case .atMostOnce: 129 | self.client.publishListeners.notify(.success(message.publish)) 130 | 131 | case .atLeastOnce: 132 | connection.sendMessageNoWait(MQTTPubAckPacket(type: .PUBACK, packetId: message.packetId)) 133 | .map { _ in message.publish } 134 | .whenComplete { self.client.publishListeners.notify($0) } 135 | 136 | case .exactlyOnce: 137 | var publish = message.publish 138 | connection.sendMessage(MQTTPubAckPacket(type: .PUBREC, packetId: message.packetId)) { newMessage in 139 | guard newMessage.packetId == message.packetId else { return false } 140 | // if we receive a publish message while waiting for a PUBREL from broker then replace data to be published and retry PUBREC 141 | if newMessage.type == .PUBLISH, let publishMessage = newMessage as? MQTTPublishPacket { 142 | publish = publishMessage.publish 143 | throw MQTTError.retrySend 144 | } 145 | // if we receive anything but a PUBREL then throw unexpected message 146 | guard newMessage.type == .PUBREL else { throw MQTTError.unexpectedMessage } 147 | // now we have received the PUBREL we can process the published message. PUBCOMP is sent by `respondToPubrel` 148 | return true 149 | } 150 | .map { _ in publish } 151 | .whenComplete { result in 152 | // do not report retrySend error 153 | if case .failure(let error) = result, case MQTTError.retrySend = error { 154 | return 155 | } 156 | self.client.publishListeners.notify(result) 157 | } 158 | } 159 | } 160 | 161 | /// Respond to PUBREL message by sending PUBCOMP. Do this separate from `responeToPublish` as the broker might send 162 | /// multiple PUBREL messages, if the client is slow to respond 163 | private func respondToPubrel(_ message: MQTTPacket) { 164 | guard let connection = client.connection else { return } 165 | _ = connection.sendMessageNoWait(MQTTPubAckPacket(type: .PUBCOMP, packetId: message.packetId)) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/ChannelHandlers/MQTTTaskHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | 16 | /// Task handler. 17 | final class MQTTTaskHandler: ChannelInboundHandler, RemovableChannelHandler { 18 | typealias InboundIn = MQTTPacket 19 | 20 | var eventLoop: EventLoop! 21 | var client: MQTTClient 22 | 23 | init(client: MQTTClient) { 24 | self.client = client 25 | self.eventLoop = nil 26 | self.tasks = [] 27 | } 28 | 29 | func addTask(_ task: MQTTTask) -> EventLoopFuture { 30 | if self.eventLoop.inEventLoop { 31 | self._addTask(task) 32 | return self.eventLoop.makeSucceededVoidFuture() 33 | } else { 34 | return self.eventLoop.submit { 35 | self.tasks.append(task) 36 | } 37 | } 38 | } 39 | 40 | private func _addTask(_ task: MQTTTask) { 41 | self.tasks.append(task) 42 | } 43 | 44 | private func _removeTask(_ task: MQTTTask) { 45 | self.tasks.removeAll { $0 === task } 46 | } 47 | 48 | private func removeTask(_ task: MQTTTask) { 49 | if self.eventLoop.inEventLoop { 50 | self._removeTask(task) 51 | } else { 52 | self.eventLoop.execute { 53 | self._removeTask(task) 54 | } 55 | } 56 | } 57 | 58 | func handlerAdded(context: ChannelHandlerContext) { 59 | self.eventLoop = context.eventLoop 60 | } 61 | 62 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 63 | let response = self.unwrapInboundIn(data) 64 | for task in self.tasks { 65 | do { 66 | // should this task respond to inbound packet 67 | if try task.checkInbound(response) { 68 | self.removeTask(task) 69 | task.succeed(response) 70 | return 71 | } 72 | } catch { 73 | self.removeTask(task) 74 | task.fail(error) 75 | return 76 | } 77 | } 78 | 79 | self.processUnhandledPacket(response) 80 | } 81 | 82 | /// process packets where no equivalent task was found 83 | func processUnhandledPacket(_ packet: MQTTPacket) { 84 | // we only send response to v5 server 85 | guard self.client.configuration.version == .v5_0 else { return } 86 | guard let connection = client.connection else { return } 87 | 88 | switch packet.type { 89 | case .PUBREC: 90 | _ = connection.sendMessageNoWait( 91 | MQTTPubAckPacket( 92 | type: .PUBREL, 93 | packetId: packet.packetId, 94 | reason: .packetIdentifierNotFound 95 | ) 96 | ) 97 | case .PUBREL: 98 | _ = connection.sendMessageNoWait( 99 | MQTTPubAckPacket( 100 | type: .PUBCOMP, 101 | packetId: packet.packetId, 102 | reason: .packetIdentifierNotFound 103 | ) 104 | ) 105 | default: 106 | break 107 | } 108 | } 109 | 110 | func channelInactive(context: ChannelHandlerContext) { 111 | // channel is inactive so we should fail or tasks in progress 112 | self.tasks.forEach { $0.fail(MQTTError.serverClosedConnection) } 113 | self.tasks.removeAll() 114 | } 115 | 116 | func errorCaught(context: ChannelHandlerContext, error: Error) { 117 | // we caught an error so we should fail all active tasks 118 | self.tasks.forEach { $0.fail(error) } 119 | self.tasks.removeAll() 120 | } 121 | 122 | var tasks: [MQTTTask] 123 | } 124 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/ChannelHandlers/PingreqHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | 16 | /// Channel handler for sending PINGREQ messages to keep connect alive 17 | final class PingreqHandler { 18 | typealias OutboundIn = MQTTPacket 19 | typealias OutboundOut = MQTTPacket 20 | typealias InboundIn = MQTTPacket 21 | typealias InboundOut = MQTTPacket 22 | 23 | let client: MQTTClient 24 | var timeout: TimeAmount 25 | var lastEventTime: NIODeadline 26 | var task: Scheduled? 27 | 28 | init(client: MQTTClient, timeout: TimeAmount) { 29 | self.client = client 30 | self.timeout = timeout 31 | self.lastEventTime = .now() 32 | self.task = nil 33 | } 34 | 35 | func updateTimeout(_ timeout: TimeAmount) { 36 | self.timeout = timeout 37 | } 38 | 39 | func start(context: ChannelHandlerContext) { 40 | guard self.task == nil else { return } 41 | self.scheduleTask(context) 42 | } 43 | 44 | func stop() { 45 | self.task?.cancel() 46 | self.task = nil 47 | } 48 | 49 | func write() { 50 | self.lastEventTime = .now() 51 | } 52 | 53 | func scheduleTask(_ context: ChannelHandlerContext) { 54 | guard context.channel.isActive else { return } 55 | 56 | self.task = context.eventLoop.scheduleTask(deadline: self.lastEventTime + self.timeout) { 57 | // if lastEventTime plus the timeout is less than now send PINGREQ 58 | // otherwise reschedule task 59 | if self.lastEventTime + self.timeout <= .now() { 60 | guard context.channel.isActive else { return } 61 | 62 | self.client.ping().whenComplete { result in 63 | switch result { 64 | case .failure(let error): 65 | context.fireErrorCaught(error) 66 | case .success: 67 | break 68 | } 69 | self.lastEventTime = .now() 70 | self.scheduleTask(context) 71 | } 72 | } else { 73 | self.scheduleTask(context) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/ChannelHandlers/WebSocketHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | import NIOWebSocket 16 | 17 | /// WebSocket channel handler. Sends WebSocket frames, receives and combines frames. 18 | /// Code inspired from vapor/websocket-kit https://github.com/vapor/websocket-kit 19 | /// and the WebSocket sample from swift-nio 20 | /// https://github.com/apple/swift-nio/tree/main/Sources/NIOWebSocketClient 21 | /// 22 | /// The WebSocket ping/pong is implemented but not used as the MQTT client already implements 23 | /// PINGREQ messages 24 | final class WebSocketHandler: ChannelDuplexHandler { 25 | typealias OutboundIn = ByteBuffer 26 | typealias OutboundOut = WebSocketFrame 27 | typealias InboundIn = WebSocketFrame 28 | typealias InboundOut = ByteBuffer 29 | 30 | static let pingData: String = "MQTTClient" 31 | 32 | var webSocketFrameSequence: WebSocketFrameSequence? 33 | var waitingOnPong: Bool = false 34 | var pingInterval: TimeAmount? 35 | 36 | /// Write bytebuffer as WebSocket frame 37 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 38 | guard context.channel.isActive else { return } 39 | 40 | let buffer = unwrapOutboundIn(data) 41 | self.send(context: context, buffer: buffer, opcode: .binary, fin: true, promise: promise) 42 | } 43 | 44 | /// Read WebSocket frame 45 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 46 | let frame = self.unwrapInboundIn(data) 47 | 48 | switch frame.opcode { 49 | case .pong: 50 | self.pong(context: context, frame: frame) 51 | case .ping: 52 | self.ping(context: context, frame: frame) 53 | case .text: 54 | if var frameSeq = self.webSocketFrameSequence { 55 | frameSeq.append(frame) 56 | self.webSocketFrameSequence = frameSeq 57 | } else { 58 | var frameSeq = WebSocketFrameSequence(type: .text) 59 | frameSeq.append(frame) 60 | self.webSocketFrameSequence = frameSeq 61 | } 62 | case .binary: 63 | if var frameSeq = self.webSocketFrameSequence { 64 | frameSeq.append(frame) 65 | self.webSocketFrameSequence = frameSeq 66 | } else { 67 | var frameSeq = WebSocketFrameSequence(type: .binary) 68 | frameSeq.append(frame) 69 | self.webSocketFrameSequence = frameSeq 70 | } 71 | case .continuation: 72 | if var frameSeq = self.webSocketFrameSequence { 73 | frameSeq.append(frame) 74 | self.webSocketFrameSequence = frameSeq 75 | } else { 76 | self.close(context: context, code: .protocolError, promise: nil) 77 | } 78 | case .connectionClose: 79 | self.receivedClose(context: context, frame: frame) 80 | default: 81 | break 82 | } 83 | 84 | if let frameSeq = self.webSocketFrameSequence, frame.fin { 85 | switch frameSeq.type { 86 | case .binary, .text: 87 | context.fireChannelRead(wrapInboundOut(frameSeq.buffer)) 88 | default: break 89 | } 90 | self.webSocketFrameSequence = nil 91 | } 92 | } 93 | 94 | /// Send web socket frame to server 95 | private func send( 96 | context: ChannelHandlerContext, 97 | buffer: ByteBuffer, 98 | opcode: WebSocketOpcode, 99 | fin: Bool = true, 100 | promise: EventLoopPromise? = nil 101 | ) { 102 | let maskKey = self.makeMaskKey() 103 | let frame = WebSocketFrame(fin: fin, opcode: opcode, maskKey: maskKey, data: buffer) 104 | context.writeAndFlush(wrapOutboundOut(frame), promise: promise) 105 | } 106 | 107 | /// Send ping and setup task to check for pong and send new ping 108 | private func sendPingAndWait(context: ChannelHandlerContext) { 109 | guard context.channel.isActive, let pingInterval else { 110 | return 111 | } 112 | if self.waitingOnPong { 113 | // We never received a pong from our last ping, so the connection has timed out 114 | let promise = context.eventLoop.makePromise(of: Void.self) 115 | self.close(context: context, code: .unknown(1006), promise: promise) 116 | promise.futureResult.whenComplete { _ in 117 | // Usually, closing a WebSocket is done by sending the close frame and waiting 118 | // for the peer to respond with their close frame. We are in a timeout situation, 119 | // so the other side likely will never send the close frame. We just close the 120 | // channel ourselves. 121 | context.channel.close(mode: .all, promise: nil) 122 | } 123 | 124 | } else { 125 | let buffer = context.channel.allocator.buffer(string: Self.pingData) 126 | self.send(context: context, buffer: buffer, opcode: .ping) 127 | _ = context.eventLoop.scheduleTask(in: pingInterval) { 128 | self.sendPingAndWait(context: context) 129 | } 130 | } 131 | } 132 | 133 | /// Respond to pong from server. Verify contents of pong and clear waitingOnPong flag 134 | private func pong(context: ChannelHandlerContext, frame: WebSocketFrame) { 135 | var frameData = frame.unmaskedData 136 | guard let frameDataString = frameData.readString(length: Self.pingData.count), 137 | frameDataString == Self.pingData 138 | else { 139 | self.close(context: context, code: .goingAway, promise: nil) 140 | return 141 | } 142 | self.waitingOnPong = false 143 | } 144 | 145 | /// Respond to ping from server 146 | private func ping(context: ChannelHandlerContext, frame: WebSocketFrame) { 147 | if frame.fin { 148 | self.send(context: context, buffer: frame.unmaskedData, opcode: .pong, fin: true, promise: nil) 149 | } else { 150 | self.close(context: context, code: .protocolError, promise: nil) 151 | } 152 | } 153 | 154 | private func receivedClose(context: ChannelHandlerContext, frame: WebSocketFrame) { 155 | // Handle a received close frame. We're just going to close. 156 | self.isClosed = true 157 | context.close(promise: nil) 158 | } 159 | 160 | /// Make mask key to be used in WebSocket frame 161 | func makeMaskKey() -> WebSocketMaskingKey? { 162 | let bytes: [UInt8] = (0...3).map { _ in UInt8.random(in: 1...255) } 163 | return WebSocketMaskingKey(bytes) 164 | } 165 | 166 | /// Close websocket connection 167 | public func close(context: ChannelHandlerContext, code: WebSocketErrorCode = .goingAway, promise: EventLoopPromise?) { 168 | guard self.isClosed == false else { 169 | promise?.succeed(()) 170 | return 171 | } 172 | self.isClosed = true 173 | 174 | let codeAsInt = UInt16(webSocketErrorCode: code) 175 | let codeToSend: WebSocketErrorCode 176 | if codeAsInt == 1005 || codeAsInt == 1006 { 177 | /// Code 1005 and 1006 are used to report errors to the application, but must never be sent over 178 | /// the wire (per https://tools.ietf.org/html/rfc6455#section-7.4) 179 | codeToSend = .normalClosure 180 | } else { 181 | codeToSend = code 182 | } 183 | 184 | var buffer = context.channel.allocator.buffer(capacity: 2) 185 | buffer.write(webSocketErrorCode: codeToSend) 186 | self.send(context: context, buffer: buffer, opcode: .connectionClose, fin: true, promise: promise) 187 | } 188 | 189 | func channelInactive(context: ChannelHandlerContext) { 190 | self.close(context: context, code: .unknown(1006), promise: nil) 191 | 192 | // We always forward the error on to let others see it. 193 | context.fireChannelInactive() 194 | } 195 | 196 | func errorCaught(context: ChannelHandlerContext, error: Error) { 197 | let errorCode: WebSocketErrorCode 198 | if let error = error as? NIOWebSocketError { 199 | errorCode = WebSocketErrorCode(error) 200 | } else { 201 | errorCode = .unexpectedServerError 202 | } 203 | self.close(context: context, code: errorCode, promise: nil) 204 | 205 | // We always forward the error on to let others see it. 206 | context.fireErrorCaught(error) 207 | } 208 | 209 | private var isClosed: Bool = false 210 | } 211 | 212 | struct WebSocketFrameSequence { 213 | var buffer: ByteBuffer 214 | var type: WebSocketOpcode 215 | 216 | init(type: WebSocketOpcode) { 217 | self.buffer = ByteBufferAllocator().buffer(capacity: 0) 218 | self.type = type 219 | } 220 | 221 | mutating func append(_ frame: WebSocketFrame) { 222 | var data = frame.unmaskedData 223 | switch self.type { 224 | case .binary, .text: 225 | self.buffer.writeBuffer(&data) 226 | default: break 227 | } 228 | } 229 | } 230 | 231 | extension WebSocketErrorCode { 232 | init(_ error: NIOWebSocketError) { 233 | switch error { 234 | case .invalidFrameLength: 235 | self = .messageTooLarge 236 | case .fragmentedControlFrame, 237 | .multiByteControlFrameLength: 238 | self = .protocolError 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/ChannelHandlers/WebSocketInitialRequest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | import NIOHTTP1 16 | 17 | // The HTTP handler to be used to initiate the request. 18 | // This initial request will be adapted by the WebSocket upgrader to contain the upgrade header parameters. 19 | // Channel read will only be called if the upgrade fails. 20 | final class WebSocketInitialRequestHandler: ChannelInboundHandler, RemovableChannelHandler, Sendable { 21 | public typealias InboundIn = HTTPClientResponsePart 22 | public typealias OutboundOut = HTTPClientRequestPart 23 | 24 | let host: String 25 | let urlPath: String 26 | let additionalHeaders: HTTPHeaders 27 | let upgradePromise: EventLoopPromise 28 | 29 | init(host: String, urlPath: String, additionalHeaders: HTTPHeaders, upgradePromise: EventLoopPromise) { 30 | self.host = host 31 | self.urlPath = urlPath 32 | self.additionalHeaders = additionalHeaders 33 | self.upgradePromise = upgradePromise 34 | } 35 | 36 | public func channelActive(context: ChannelHandlerContext) { 37 | // We are connected. It's time to send the message to the server to initialize the upgrade dance. 38 | var headers = HTTPHeaders() 39 | headers.add(name: "Content-Length", value: "0") 40 | headers.add(name: "host", value: self.host) 41 | headers.add(name: "Sec-WebSocket-Protocol", value: "mqtt") 42 | headers.add(contentsOf: self.additionalHeaders) 43 | 44 | let requestHead = HTTPRequestHead( 45 | version: HTTPVersion(major: 1, minor: 1), 46 | method: .GET, 47 | uri: urlPath, 48 | headers: headers 49 | ) 50 | 51 | context.write(self.wrapOutboundOut(.head(requestHead)), promise: nil) 52 | context.write(self.wrapOutboundOut(.body(.byteBuffer(ByteBuffer()))), promise: nil) 53 | context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) 54 | } 55 | 56 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 57 | let clientResponse = self.unwrapInboundIn(data) 58 | 59 | switch clientResponse { 60 | case .head: 61 | self.upgradePromise.fail(MQTTError.websocketUpgradeFailed) 62 | case .body: 63 | break 64 | case .end: 65 | context.close(promise: nil) 66 | } 67 | } 68 | 69 | public func errorCaught(context: ChannelHandlerContext, error: Error) { 70 | self.upgradePromise.fail(error) 71 | // As we are not really interested getting notified on success or failure 72 | // we just pass nil as promise to reduce allocations. 73 | context.close(promise: nil) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTClientV5.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | 16 | extension MQTTClient { 17 | /// Provides implementations of functions that expose MQTT Version 5.0 features 18 | public struct V5 { 19 | let client: MQTTClient 20 | 21 | /// Connect to MQTT server 22 | /// 23 | /// If `cleanStart` is set to false the Server MUST resume communications with the Client based on 24 | /// state from the current Session (as identified by the Client identifier). If there is no Session 25 | /// associated with the Client identifier the Server MUST create a new Session. The Client and Server 26 | /// MUST store the Session after the Client and Server are disconnected. If set to true then the 27 | /// Client and Server MUST discard any previous Session and start a new one 28 | /// 29 | /// The function returns an EventLoopFuture which will be updated with whether the server has restored a session for this client. 30 | /// 31 | /// - Parameters: 32 | /// - cleanStart: should we start with a new session 33 | /// - properties: properties to attach to connect message 34 | /// - will: Publish message to be posted as soon as connection is made 35 | /// - authWorkflow: The authentication workflow. This is currently unimplemented. 36 | /// - Returns: EventLoopFuture to be updated with connack 37 | public func connect( 38 | cleanStart: Bool = true, 39 | properties: MQTTProperties = .init(), 40 | will: (topicName: String, payload: ByteBuffer, qos: MQTTQoS, retain: Bool, properties: MQTTProperties)? = nil, 41 | authWorkflow: ((MQTTAuthV5, EventLoop) -> EventLoopFuture)? = nil 42 | ) -> EventLoopFuture { 43 | self.connect( 44 | cleanStart: cleanStart, 45 | properties: properties, 46 | will: will, 47 | authWorkflow: authWorkflow, 48 | connectConfiguration: .init() 49 | ) 50 | } 51 | 52 | /// Connect to MQTT server 53 | /// 54 | /// If `cleanStart` is set to false the Server MUST resume communications with the Client based on 55 | /// state from the current Session (as identified by the Client identifier). If there is no Session 56 | /// associated with the Client identifier the Server MUST create a new Session. The Client and Server 57 | /// MUST store the Session after the Client and Server are disconnected. If set to true then the 58 | /// Client and Server MUST discard any previous Session and start a new one 59 | /// 60 | /// The function returns an EventLoopFuture which will be updated with whether the server has restored a session for this client. 61 | /// 62 | /// - Parameters: 63 | /// - cleanStart: should we start with a new session 64 | /// - properties: properties to attach to connect message 65 | /// - will: Publish message to be posted as soon as connection is made 66 | /// - authWorkflow: The authentication workflow. This is currently unimplemented. 67 | /// - connectConfiguration: Override client configuration during connection 68 | /// - Returns: EventLoopFuture to be updated with connack 69 | public func connect( 70 | cleanStart: Bool = true, 71 | properties: MQTTProperties = .init(), 72 | will: (topicName: String, payload: ByteBuffer, qos: MQTTQoS, retain: Bool, properties: MQTTProperties)? = nil, 73 | authWorkflow: ((MQTTAuthV5, EventLoop) -> EventLoopFuture)? = nil, 74 | connectConfiguration: ConnectConfiguration 75 | ) -> EventLoopFuture { 76 | let publish = will.map { 77 | MQTTPublishInfo( 78 | qos: .atMostOnce, 79 | retain: $0.retain, 80 | dup: false, 81 | topicName: $0.topicName, 82 | payload: $0.payload, 83 | properties: $0.properties 84 | ) 85 | } 86 | let keepAliveInterval = connectConfiguration.keepAliveInterval ?? self.client.configuration.keepAliveInterval 87 | let packet = MQTTConnectPacket( 88 | cleanSession: cleanStart, 89 | keepAliveSeconds: UInt16(keepAliveInterval.nanoseconds / 1_000_000_000), 90 | clientIdentifier: self.client.identifier, 91 | userName: connectConfiguration.userName ?? self.client.configuration.userName, 92 | password: connectConfiguration.password ?? self.client.configuration.password, 93 | properties: properties, 94 | will: publish 95 | ) 96 | 97 | return self.client.connect(packet: packet, authWorkflow: authWorkflow).map { 98 | .init( 99 | sessionPresent: $0.sessionPresent, 100 | reason: MQTTReasonCode(rawValue: $0.returnCode) ?? .unrecognisedReason, 101 | properties: $0.properties 102 | ) 103 | } 104 | } 105 | 106 | /// Publish message to topic 107 | /// - Parameters: 108 | /// - topicName: Topic name on which the message is published 109 | /// - payload: Message payload 110 | /// - qos: Quality of Service for message. 111 | /// - retain: Whether this is a retained message. 112 | /// - properties: properties to attach to publish message 113 | /// - Returns: Future waiting for publish to complete. Depending on QoS setting the future will complete 114 | /// when message is sent, when PUBACK is received or when PUBREC and following PUBCOMP are 115 | /// received. QoS1 and above return an `MQTTAckV5` which contains a `reason` and `properties` 116 | public func publish( 117 | to topicName: String, 118 | payload: ByteBuffer, 119 | qos: MQTTQoS, 120 | retain: Bool = false, 121 | properties: MQTTProperties = .init() 122 | ) -> EventLoopFuture { 123 | let info = MQTTPublishInfo(qos: qos, retain: retain, dup: false, topicName: topicName, payload: payload, properties: properties) 124 | let packetId = self.client.updatePacketId() 125 | let packet = MQTTPublishPacket(publish: info, packetId: packetId) 126 | return self.client.publish(packet: packet) 127 | } 128 | 129 | /// Subscribe to topic 130 | /// - Parameters: 131 | /// - subscriptions: Subscription infos 132 | /// - properties: properties to attach to subscribe message 133 | /// - Returns: Future waiting for subscribe to complete. Will wait for SUBACK message from server and 134 | /// return its contents 135 | public func subscribe( 136 | to subscriptions: [MQTTSubscribeInfoV5], 137 | properties: MQTTProperties = .init() 138 | ) -> EventLoopFuture { 139 | let packetId = self.client.updatePacketId() 140 | let packet = MQTTSubscribePacket(subscriptions: subscriptions, properties: properties, packetId: packetId) 141 | return self.client.subscribe(packet: packet) 142 | .map { message in 143 | MQTTSubackV5(reasons: message.reasons, properties: message.properties) 144 | } 145 | } 146 | 147 | /// Unsubscribe from topic 148 | /// - Parameters: 149 | /// - subscriptions: List of subscriptions to unsubscribe from 150 | /// - properties: properties to attach to unsubscribe message 151 | /// - Returns: Future waiting for unsubscribe to complete. Will wait for UNSUBACK message from server and 152 | /// return its contents 153 | public func unsubscribe( 154 | from subscriptions: [String], 155 | properties: MQTTProperties = .init() 156 | ) -> EventLoopFuture { 157 | let packetId = self.client.updatePacketId() 158 | let packet = MQTTUnsubscribePacket(subscriptions: subscriptions, properties: properties, packetId: packetId) 159 | return self.client.unsubscribe(packet: packet) 160 | .map { message in 161 | MQTTSubackV5(reasons: message.reasons, properties: message.properties) 162 | } 163 | } 164 | 165 | /// Disconnect from server 166 | /// - Parameter properties: properties to attach to disconnect packet 167 | /// - Returns: Future waiting on disconnect message to be sent 168 | public func disconnect(properties: MQTTProperties = .init()) -> EventLoopFuture { 169 | self.client.disconnect(packet: MQTTDisconnectPacket(reason: .success, properties: properties)) 170 | } 171 | 172 | /// Re-authenticate with server 173 | /// 174 | /// - Parameters: 175 | /// - properties: properties to attach to auth packet. Must include `authenticationMethod` 176 | /// - authWorkflow: Respond to auth packets from server 177 | /// - Returns: final auth packet returned from server 178 | public func auth( 179 | properties: MQTTProperties, 180 | authWorkflow: ((MQTTAuthV5, EventLoop) -> EventLoopFuture)? = nil 181 | ) -> EventLoopFuture { 182 | let authPacket = MQTTAuthPacket(reason: .reAuthenticate, properties: properties) 183 | let authFuture = self.client.reAuth(packet: authPacket) 184 | let eventLoop = authFuture.eventLoop 185 | return authFuture.flatMap { response -> EventLoopFuture in 186 | guard let auth = response as? MQTTAuthPacket else { return eventLoop.makeFailedFuture(MQTTError.unexpectedMessage) } 187 | if auth.reason == .success { 188 | return eventLoop.makeSucceededFuture(auth) 189 | } 190 | guard let authWorkflow else { return eventLoop.makeFailedFuture(MQTTError.authWorkflowRequired) } 191 | return self.client.processAuth(authPacket, authWorkflow: authWorkflow, on: eventLoop) 192 | } 193 | .flatMapThrowing { response -> MQTTAuthV5 in 194 | guard let auth = response as? MQTTAuthPacket else { throw MQTTError.unexpectedMessage } 195 | return MQTTAuthV5(reason: auth.reason, properties: auth.properties) 196 | } 197 | } 198 | 199 | /// Add named publish listener. Called whenever a PUBLISH message is received from the server with the 200 | /// specified subscription id 201 | /// 202 | /// - Parameters: 203 | /// - name: Name of listener 204 | /// - subscriptionIdentifier: subscription identifier to look for 205 | /// - listener: listener function 206 | public func addPublishListener(named name: String, subscriptionId: UInt, _ listener: @escaping (MQTTPublishInfo) -> Void) { 207 | self.client.publishListeners.addListener(named: name) { result in 208 | if case .success(let info) = result { 209 | for property in info.properties { 210 | if case .subscriptionIdentifier(let id) = property, 211 | id == subscriptionId 212 | { 213 | listener(info) 214 | break 215 | } 216 | } 217 | } 218 | } 219 | } 220 | } 221 | 222 | /// access MQTT v5 functionality 223 | public var v5: V5 { 224 | precondition(self.configuration.version == .v5_0, "Cannot use v5 functions with v3.1 client") 225 | return V5(client: self) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTConfiguration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2022 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | import NIOHTTP1 16 | 17 | #if os(macOS) || os(Linux) 18 | import NIOSSL 19 | #endif 20 | 21 | extension MQTTClient { 22 | /// Version of MQTT server to connect to 23 | public enum Version { 24 | case v3_1_1 25 | case v5_0 26 | } 27 | 28 | /// Enum for different TLS Configuration types. 29 | /// 30 | /// The TLS Configuration type to use if defined by the EventLoopGroup the client is using. 31 | /// If you don't provide an EventLoopGroup then the EventLoopGroup created will be defined 32 | /// by this variable. It is recommended on iOS you use NIO Transport Services. 33 | public enum TLSConfigurationType { 34 | /// NIOSSL TLS configuration 35 | #if os(macOS) || os(Linux) 36 | case niossl(TLSConfiguration) 37 | #endif 38 | #if canImport(Network) 39 | /// NIO Transport Serviecs TLS configuration 40 | case ts(TSTLSConfiguration) 41 | #endif 42 | } 43 | 44 | public struct WebSocketConfiguration { 45 | /// Initialize MQTTClient WebSocket configuration struct 46 | /// - Parameters: 47 | /// - urlPath: WebSocket URL, defaults to "/mqtt" 48 | /// - maxFrameSize: Max frame size WebSocket client will allow 49 | /// - initialRequestHeaders: Additional headers to add to initial HTTP request 50 | public init( 51 | urlPath: String, 52 | maxFrameSize: Int = 1 << 14, 53 | initialRequestHeaders: HTTPHeaders = [:] 54 | ) { 55 | self.urlPath = urlPath 56 | self.maxFrameSize = maxFrameSize 57 | self.initialRequestHeaders = initialRequestHeaders 58 | } 59 | 60 | /// WebSocket URL, defaults to "/mqtt" 61 | public let urlPath: String 62 | /// Max frame size WebSocket client will allow 63 | public let maxFrameSize: Int 64 | /// Additional headers to add to initial HTTP request 65 | public let initialRequestHeaders: HTTPHeaders 66 | } 67 | 68 | /// Configuration for MQTTClient 69 | public struct Configuration { 70 | /// Initialize MQTTClient configuration struct 71 | /// - Parameters: 72 | /// - version: Version of MQTT server client is connecting to 73 | /// - disablePing: Disable the automatic sending of pingreq messages 74 | /// - keepAliveInterval: MQTT keep alive period. 75 | /// - pingInterval: Override calculated interval between each pingreq message 76 | /// - connectTimeout: Timeout for connecting to server 77 | /// - timeout: Timeout for server ACK responses 78 | /// - userName: MQTT user name 79 | /// - password: MQTT password 80 | /// - useSSL: Use encrypted connection to server 81 | /// - tlsConfiguration: TLS configuration, for SSL connection 82 | /// - sniServerName: Server name used by TLS. This will default to host name if not set 83 | /// - webSocketConfiguration: Set this if you want to use WebSockets 84 | public init( 85 | version: Version = .v3_1_1, 86 | disablePing: Bool = false, 87 | keepAliveInterval: TimeAmount = .seconds(90), 88 | pingInterval: TimeAmount? = nil, 89 | connectTimeout: TimeAmount = .seconds(10), 90 | timeout: TimeAmount? = nil, 91 | userName: String? = nil, 92 | password: String? = nil, 93 | useSSL: Bool = false, 94 | tlsConfiguration: TLSConfigurationType? = nil, 95 | sniServerName: String? = nil, 96 | webSocketConfiguration: WebSocketConfiguration 97 | ) { 98 | self.version = version 99 | self.disablePing = disablePing 100 | self.keepAliveInterval = keepAliveInterval 101 | self.pingInterval = pingInterval 102 | self.connectTimeout = connectTimeout 103 | self.timeout = timeout 104 | self.userName = userName 105 | self.password = password 106 | self.useSSL = useSSL 107 | self.tlsConfiguration = tlsConfiguration 108 | self.sniServerName = sniServerName 109 | self.webSocketConfiguration = webSocketConfiguration 110 | } 111 | 112 | /// Initialize MQTTClient configuration struct 113 | /// - Parameters: 114 | /// - version: Version of MQTT server client is connecting to 115 | /// - disablePing: Disable the automatic sending of pingreq messages 116 | /// - keepAliveInterval: MQTT keep alive period. 117 | /// - pingInterval: Override calculated interval between each pingreq message 118 | /// - connectTimeout: Timeout for connecting to server 119 | /// - timeout: Timeout for server ACK responses 120 | /// - userName: MQTT user name 121 | /// - password: MQTT password 122 | /// - useSSL: Use encrypted connection to server 123 | /// - useWebSockets: Use a websocket connection to server 124 | /// - tlsConfiguration: TLS configuration, for SSL connection 125 | /// - sniServerName: Server name used by TLS. This will default to host name if not set 126 | /// - webSocketURLPath: URL Path for web socket. Defaults to "/mqtt" 127 | /// - webSocketMaxFrameSize: Maximum frame size for a web socket connection 128 | public init( 129 | version: Version = .v3_1_1, 130 | disablePing: Bool = false, 131 | keepAliveInterval: TimeAmount = .seconds(90), 132 | pingInterval: TimeAmount? = nil, 133 | connectTimeout: TimeAmount = .seconds(10), 134 | timeout: TimeAmount? = nil, 135 | userName: String? = nil, 136 | password: String? = nil, 137 | useSSL: Bool = false, 138 | useWebSockets: Bool = false, 139 | tlsConfiguration: TLSConfigurationType? = nil, 140 | sniServerName: String? = nil, 141 | webSocketURLPath: String? = nil, 142 | webSocketMaxFrameSize: Int = 1 << 14 143 | ) { 144 | self.version = version 145 | self.disablePing = disablePing 146 | self.keepAliveInterval = keepAliveInterval 147 | self.pingInterval = pingInterval 148 | self.connectTimeout = connectTimeout 149 | self.timeout = timeout 150 | self.userName = userName 151 | self.password = password 152 | self.useSSL = useSSL 153 | self.tlsConfiguration = tlsConfiguration 154 | self.sniServerName = sniServerName 155 | if useWebSockets { 156 | self.webSocketConfiguration = .init(urlPath: webSocketURLPath ?? "/mqtt", maxFrameSize: webSocketMaxFrameSize) 157 | } else { 158 | self.webSocketConfiguration = nil 159 | } 160 | } 161 | 162 | /// Initialize MQTTClient configuration struct 163 | /// - Parameters: 164 | /// - version: Version of MQTT server client is connecting to 165 | /// - disablePing: Disable the automatic sending of pingreq messages 166 | /// - keepAliveInterval: MQTT keep alive period. 167 | /// - pingInterval: Override calculated interval between each pingreq message 168 | /// - connectTimeout: Timeout for connecting to server 169 | /// - timeout: Timeout for server ACK responses 170 | /// - maxRetryAttempts: Max number of times to send a message. This is deprecated 171 | /// - userName: MQTT user name 172 | /// - password: MQTT password 173 | /// - useSSL: Use encrypted connection to server 174 | /// - useWebSockets: Use a websocket connection to server 175 | /// - tlsConfiguration: TLS configuration, for SSL connection 176 | /// - sniServerName: Server name used by TLS. This will default to host name if not set 177 | /// - webSocketURLPath: URL Path for web socket. Defaults to "/mqtt" 178 | /// - webSocketMaxFrameSize: Maximum frame size for a web socket connection 179 | @available(*, deprecated, message: "maxRetryAttempts is no longer used") 180 | public init( 181 | version: Version = .v3_1_1, 182 | disablePing: Bool = false, 183 | keepAliveInterval: TimeAmount = .seconds(90), 184 | pingInterval: TimeAmount? = nil, 185 | connectTimeout: TimeAmount = .seconds(10), 186 | timeout: TimeAmount? = nil, 187 | maxRetryAttempts: Int, 188 | userName: String? = nil, 189 | password: String? = nil, 190 | useSSL: Bool = false, 191 | useWebSockets: Bool = false, 192 | tlsConfiguration: TLSConfigurationType? = nil, 193 | sniServerName: String? = nil, 194 | webSocketURLPath: String? = nil, 195 | webSocketMaxFrameSize: Int = 1 << 14 196 | ) { 197 | self.version = version 198 | self.disablePing = disablePing 199 | self.keepAliveInterval = keepAliveInterval 200 | self.pingInterval = pingInterval 201 | self.connectTimeout = connectTimeout 202 | self.timeout = timeout 203 | self.userName = userName 204 | self.password = password 205 | self.useSSL = useSSL 206 | self.tlsConfiguration = tlsConfiguration 207 | self.sniServerName = sniServerName 208 | if useWebSockets { 209 | self.webSocketConfiguration = .init(urlPath: webSocketURLPath ?? "/mqtt", maxFrameSize: webSocketMaxFrameSize) 210 | } else { 211 | self.webSocketConfiguration = nil 212 | } 213 | } 214 | 215 | /// use a websocket connection to server 216 | public var useWebSockets: Bool { 217 | self.webSocketConfiguration != nil 218 | } 219 | 220 | /// URL Path for web socket. Defaults to "/mqtt" 221 | public var webSocketURLPath: String? { 222 | self.webSocketConfiguration?.urlPath 223 | } 224 | 225 | /// Maximum frame size for a web socket connection 226 | public var webSocketMaxFrameSize: Int { 227 | self.webSocketConfiguration?.maxFrameSize ?? 1 << 14 228 | } 229 | 230 | /// Version of MQTT server client is connecting to 231 | public let version: Version 232 | /// disable the automatic sending of pingreq messages 233 | public let disablePing: Bool 234 | /// MQTT keep alive period. 235 | public let keepAliveInterval: TimeAmount 236 | /// override interval between each pingreq message 237 | public let pingInterval: TimeAmount? 238 | /// timeout for connecting to server 239 | public let connectTimeout: TimeAmount 240 | /// timeout for server response 241 | public let timeout: TimeAmount? 242 | /// MQTT user name. 243 | public let userName: String? 244 | /// MQTT password. 245 | public let password: String? 246 | /// use encrypted connection to server 247 | public let useSSL: Bool 248 | /// TLS configuration 249 | public let tlsConfiguration: TLSConfigurationType? 250 | /// server name used by TLS 251 | public let sniServerName: String? 252 | /// WebSocket configuration 253 | public let webSocketConfiguration: WebSocketConfiguration? 254 | } 255 | 256 | /// Configuration used at connection time to override values stored in the MQTTClient.Configuration 257 | public struct ConnectConfiguration { 258 | /// MQTT user name. 259 | public let userName: String? 260 | /// MQTT password. 261 | public let password: String? 262 | /// MQTT keep alive period. 263 | public let keepAliveInterval: TimeAmount? 264 | 265 | /// Initialize MQTTClient connect configuration struct 266 | /// 267 | /// - Parameters: 268 | /// - keepAliveInterval: MQTT keep alive period. 269 | /// - userName: MQTT user name 270 | /// - password: MQTT password 271 | public init( 272 | keepAliveInterval: TimeAmount? = nil, 273 | userName: String? = nil, 274 | password: String? = nil 275 | ) { 276 | self.keepAliveInterval = keepAliveInterval 277 | self.userName = userName 278 | self.password = password 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTConnection.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2022 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | import NIOHTTP1 16 | import NIOWebSocket 17 | 18 | #if canImport(FoundationEssentials) 19 | import FoundationEssentials 20 | #else 21 | import Foundation 22 | #endif 23 | #if canImport(Network) 24 | import Network 25 | import NIOTransportServices 26 | #endif 27 | #if os(macOS) || os(Linux) 28 | import NIOSSL 29 | #endif 30 | 31 | final class MQTTConnection { 32 | let channel: Channel 33 | let cleanSession: Bool 34 | let timeout: TimeAmount? 35 | let taskHandler: MQTTTaskHandler 36 | 37 | private init(channel: Channel, cleanSession: Bool, timeout: TimeAmount?, taskHandler: MQTTTaskHandler) { 38 | self.channel = channel 39 | self.cleanSession = cleanSession 40 | self.timeout = timeout 41 | self.taskHandler = taskHandler 42 | } 43 | 44 | static func create(client: MQTTClient, cleanSession: Bool, pingInterval: TimeAmount) -> EventLoopFuture { 45 | let taskHandler = MQTTTaskHandler(client: client) 46 | return self.createBootstrap(client: client, pingInterval: pingInterval, taskHandler: taskHandler) 47 | .map { MQTTConnection(channel: $0, cleanSession: cleanSession, timeout: client.configuration.timeout, taskHandler: taskHandler) } 48 | } 49 | 50 | static func createBootstrap(client: MQTTClient, pingInterval: TimeAmount, taskHandler: MQTTTaskHandler) -> EventLoopFuture { 51 | let eventLoop = client.eventLoopGroup.next() 52 | let channelPromise = eventLoop.makePromise(of: Channel.self) 53 | do { 54 | // get bootstrap based off what eventloop we are running on 55 | let bootstrap = try getBootstrap(client: client) 56 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 57 | .channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) 58 | .connectTimeout(client.configuration.connectTimeout) 59 | .channelInitializer { channel in 60 | // Work out what handlers to add 61 | let handlers: [ChannelHandler] = [ 62 | MQTTMessageHandler(client, pingInterval: pingInterval), 63 | taskHandler, 64 | ] 65 | // are we using websockets 66 | if let webSocketConfiguration = client.configuration.webSocketConfiguration { 67 | // prepare for websockets and on upgrade add handlers 68 | let promise = eventLoop.makePromise(of: Void.self) 69 | promise.futureResult.map { _ in channel } 70 | .cascade(to: channelPromise) 71 | 72 | return Self.setupChannelForWebsockets( 73 | client: client, 74 | channel: channel, 75 | webSocketConfiguration: webSocketConfiguration, 76 | upgradePromise: promise 77 | ) { 78 | try channel.pipeline.syncOperations.addHandlers(handlers) 79 | } 80 | } else { 81 | return channel.pipeline.addHandlers(handlers) 82 | } 83 | } 84 | 85 | let channelFuture: EventLoopFuture 86 | 87 | if client.port == 0 { 88 | channelFuture = bootstrap.connect(unixDomainSocketPath: client.host) 89 | } else { 90 | channelFuture = bootstrap.connect(host: client.host, port: client.port) 91 | } 92 | 93 | channelFuture 94 | .map { channel in 95 | if !client.configuration.useWebSockets { 96 | channelPromise.succeed(channel) 97 | } 98 | } 99 | .cascadeFailure(to: channelPromise) 100 | } catch { 101 | channelPromise.fail(error) 102 | } 103 | return channelPromise.futureResult 104 | } 105 | 106 | static func getBootstrap(client: MQTTClient) throws -> NIOClientTCPBootstrap { 107 | var bootstrap: NIOClientTCPBootstrap 108 | let serverName = client.configuration.sniServerName ?? client.host 109 | #if canImport(Network) 110 | // if eventLoop is compatible with NIOTransportServices create a NIOTSConnectionBootstrap 111 | if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), 112 | let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: client.eventLoopGroup) 113 | { 114 | // create NIOClientTCPBootstrap with NIOTS TLS provider 115 | let options: NWProtocolTLS.Options 116 | switch client.configuration.tlsConfiguration { 117 | case .ts(let config): 118 | options = try config.getNWProtocolTLSOptions() 119 | #if os(macOS) || os(Linux) 120 | case .niossl: 121 | throw MQTTError.wrongTLSConfig 122 | #endif 123 | default: 124 | options = NWProtocolTLS.Options() 125 | } 126 | sec_protocol_options_set_tls_server_name(options.securityProtocolOptions, serverName) 127 | let tlsProvider = NIOTSClientTLSProvider(tlsOptions: options) 128 | bootstrap = NIOClientTCPBootstrap(tsBootstrap, tls: tlsProvider) 129 | if client.configuration.useSSL { 130 | return bootstrap.enableTLS() 131 | } 132 | return bootstrap 133 | } 134 | #endif 135 | 136 | #if os(macOS) || os(Linux) // canImport(Network) 137 | if let clientBootstrap = ClientBootstrap(validatingGroup: client.eventLoopGroup) { 138 | let tlsConfiguration: TLSConfiguration 139 | switch client.configuration.tlsConfiguration { 140 | case .niossl(let config): 141 | tlsConfiguration = config 142 | default: 143 | tlsConfiguration = TLSConfiguration.makeClientConfiguration() 144 | } 145 | if client.configuration.useSSL { 146 | let sslContext = try NIOSSLContext(configuration: tlsConfiguration) 147 | let tlsProvider = try NIOSSLClientTLSProvider(context: sslContext, serverHostname: serverName) 148 | bootstrap = NIOClientTCPBootstrap(clientBootstrap, tls: tlsProvider) 149 | return bootstrap.enableTLS() 150 | } else { 151 | bootstrap = NIOClientTCPBootstrap(clientBootstrap, tls: NIOInsecureNoTLS()) 152 | } 153 | return bootstrap 154 | } 155 | #endif 156 | preconditionFailure("Cannot create bootstrap for the supplied EventLoop") 157 | } 158 | 159 | static func setupChannelForWebsockets( 160 | client: MQTTClient, 161 | channel: Channel, 162 | webSocketConfiguration: MQTTClient.WebSocketConfiguration, 163 | upgradePromise promise: EventLoopPromise, 164 | afterHandlerAdded: @escaping () throws -> Void 165 | ) -> EventLoopFuture { 166 | // initial HTTP request handler, before upgrade 167 | let httpHandler = WebSocketInitialRequestHandler( 168 | host: client.configuration.sniServerName ?? client.hostHeader, 169 | urlPath: webSocketConfiguration.urlPath, 170 | additionalHeaders: webSocketConfiguration.initialRequestHeaders, 171 | upgradePromise: promise 172 | ) 173 | // create random key for request key 174 | let requestKey = (0..<16).map { _ in UInt8.random(in: .min ..< .max) } 175 | let websocketUpgrader = NIOWebSocketClientUpgrader( 176 | requestKey: Data(requestKey).base64EncodedString(), 177 | maxFrameSize: client.configuration.webSocketMaxFrameSize 178 | ) { channel, _ in 179 | let future = channel.eventLoop.makeCompletedFuture { 180 | try channel.pipeline.syncOperations.addHandler(WebSocketHandler()) 181 | try afterHandlerAdded() 182 | } 183 | future.cascade(to: promise) 184 | return future 185 | } 186 | 187 | let config: NIOHTTPClientUpgradeConfiguration = ( 188 | upgraders: [websocketUpgrader], 189 | completionHandler: { _ in 190 | channel.pipeline.removeHandler(httpHandler, promise: nil) 191 | } 192 | ) 193 | 194 | // add HTTP handler with web socket upgrade 195 | return channel.pipeline.addHTTPClientHandlers(withClientUpgrade: config).flatMap { 196 | channel.pipeline.addHandler(httpHandler) 197 | } 198 | } 199 | 200 | func sendMessageNoWait(_ message: MQTTPacket) -> EventLoopFuture { 201 | self.channel.writeAndFlush(message) 202 | } 203 | 204 | func close() -> EventLoopFuture { 205 | if self.channel.isActive { 206 | return self.channel.close() 207 | } else { 208 | return self.channel.eventLoop.makeSucceededFuture(()) 209 | } 210 | } 211 | 212 | func sendMessage(_ message: MQTTPacket, checkInbound: @escaping (MQTTPacket) throws -> Bool) -> EventLoopFuture { 213 | let task = MQTTTask(on: channel.eventLoop, timeout: self.timeout, checkInbound: checkInbound) 214 | 215 | self.taskHandler.addTask(task) 216 | .flatMap { 217 | self.channel.writeAndFlush(message) 218 | } 219 | .whenFailure { error in 220 | task.fail(error) 221 | } 222 | return task.promise.futureResult 223 | } 224 | 225 | func updatePingreqTimeout(_ timeout: TimeAmount) { 226 | self.channel.pipeline.handler(type: MQTTMessageHandler.self).whenSuccess { handler in 227 | handler.updatePingreqTimeout(timeout) 228 | } 229 | } 230 | 231 | var closeFuture: EventLoopFuture { self.channel.closeFuture } 232 | } 233 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTCoreTypes.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIOCore 15 | 16 | /// Indicates the level of assurance for delivery of a packet. 17 | public enum MQTTQoS: UInt8, Sendable { 18 | /// fire and forget 19 | case atMostOnce = 0 20 | /// wait for PUBACK, if you don't receive it after a period of time retry sending 21 | case atLeastOnce = 1 22 | /// wait for PUBREC, send PUBREL and then wait for PUBCOMP 23 | case exactlyOnce = 2 24 | } 25 | 26 | /// MQTT Packet type enumeration 27 | public enum MQTTPacketType: UInt8, Sendable { 28 | case CONNECT = 0x10 29 | case CONNACK = 0x20 30 | case PUBLISH = 0x30 31 | case PUBACK = 0x40 32 | case PUBREC = 0x50 33 | case PUBREL = 0x62 34 | case PUBCOMP = 0x70 35 | case SUBSCRIBE = 0x82 36 | case SUBACK = 0x90 37 | case UNSUBSCRIBE = 0xA2 38 | case UNSUBACK = 0xB0 39 | case PINGREQ = 0xC0 40 | case PINGRESP = 0xD0 41 | case DISCONNECT = 0xE0 42 | case AUTH = 0xF0 43 | } 44 | 45 | /// MQTT PUBLISH packet parameters. 46 | public struct MQTTPublishInfo: Sendable { 47 | /// Quality of Service for message. 48 | public let qos: MQTTQoS 49 | 50 | /// Whether this is a retained message. 51 | public let retain: Bool 52 | 53 | /// Whether this is a duplicate publish message. 54 | public let dup: Bool 55 | 56 | /// Topic name on which the message is published. 57 | public let topicName: String 58 | 59 | /// MQTT v5 properties 60 | public let properties: MQTTProperties 61 | 62 | /// Message payload. 63 | public let payload: ByteBuffer 64 | 65 | public init( 66 | qos: MQTTQoS, 67 | retain: Bool, 68 | dup: Bool = false, 69 | topicName: String, 70 | payload: ByteBuffer, 71 | properties: MQTTProperties 72 | ) { 73 | self.qos = qos 74 | self.retain = retain 75 | self.dup = dup 76 | self.topicName = topicName 77 | self.payload = payload 78 | self.properties = properties 79 | } 80 | 81 | static let emptyByteBuffer = ByteBufferAllocator().buffer(capacity: 0) 82 | } 83 | 84 | /// MQTT SUBSCRIBE packet parameters. 85 | public struct MQTTSubscribeInfo: Sendable { 86 | /// Topic filter to subscribe to. 87 | public let topicFilter: String 88 | 89 | /// Quality of Service for subscription. 90 | public let qos: MQTTQoS 91 | 92 | public init(topicFilter: String, qos: MQTTQoS) { 93 | self.qos = qos 94 | self.topicFilter = topicFilter 95 | } 96 | } 97 | 98 | /// MQTT Sub ACK 99 | /// 100 | /// Contains data returned in subscribe ack packets 101 | public struct MQTTSuback: Sendable { 102 | public enum ReturnCode: UInt8, Sendable { 103 | case grantedQoS0 = 0 104 | case grantedQoS1 = 1 105 | case grantedQoS2 = 2 106 | case failure = 0x80 107 | } 108 | 109 | /// MQTT v5 subscribe return codes 110 | public let returnCodes: [ReturnCode] 111 | 112 | init(returnCodes: [MQTTSuback.ReturnCode]) { 113 | self.returnCodes = returnCodes 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTCoreTypesV5.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | /// MQTT v5 Connack 15 | public struct MQTTConnackV5: Sendable { 16 | /// is using session state from previous session 17 | public let sessionPresent: Bool 18 | /// connect reason code 19 | public let reason: MQTTReasonCode 20 | /// properties 21 | public let properties: MQTTProperties 22 | } 23 | 24 | /// MQTT v5 ACK information. Returned with PUBACK, PUBREL 25 | public struct MQTTAckV5: Sendable { 26 | /// MQTT v5 reason code 27 | public let reason: MQTTReasonCode 28 | /// MQTT v5 properties 29 | public let properties: MQTTProperties 30 | 31 | init(reason: MQTTReasonCode = .success, properties: MQTTProperties = .init()) { 32 | self.reason = reason 33 | self.properties = properties 34 | } 35 | } 36 | 37 | /// MQTT v5 SUBSCRIBE packet parameters. 38 | public struct MQTTSubscribeInfoV5: Sendable { 39 | /// Retain handling options 40 | public enum RetainHandling: UInt8, Sendable { 41 | /// always send retain message 42 | case sendAlways = 0 43 | /// send retain if new 44 | case sendIfNew = 1 45 | /// do not send retain message 46 | case doNotSend = 2 47 | } 48 | 49 | /// Topic filter to subscribe to. 50 | public let topicFilter: String 51 | 52 | /// Quality of Service for subscription. 53 | public let qos: MQTTQoS 54 | 55 | /// Don't forward message published by this client 56 | public let noLocal: Bool 57 | 58 | /// Keep retain flag message was published with 59 | public let retainAsPublished: Bool 60 | 61 | /// Retain handing 62 | public let retainHandling: RetainHandling 63 | 64 | public init( 65 | topicFilter: String, 66 | qos: MQTTQoS, 67 | noLocal: Bool = false, 68 | retainAsPublished: Bool = true, 69 | retainHandling: RetainHandling = .sendIfNew 70 | ) { 71 | self.qos = qos 72 | self.topicFilter = topicFilter 73 | self.noLocal = noLocal 74 | self.retainAsPublished = retainAsPublished 75 | self.retainHandling = retainHandling 76 | } 77 | } 78 | 79 | /// MQTT v5 Sub ACK packet 80 | /// 81 | /// Contains data returned in subscribe/unsubscribe ack packets 82 | public struct MQTTSubackV5: Sendable { 83 | /// MQTT v5 subscription reason code 84 | public let reasons: [MQTTReasonCode] 85 | /// MQTT v5 properties 86 | public let properties: MQTTProperties 87 | 88 | init(reasons: [MQTTReasonCode], properties: MQTTProperties = .init()) { 89 | self.reasons = reasons 90 | self.properties = properties 91 | } 92 | } 93 | 94 | /// MQTT V5 Auth packet 95 | /// 96 | /// An AUTH packet is sent from Client to Server or Server to Client as 97 | /// part of an extended authentication exchange, such as challenge / response 98 | /// authentication 99 | public struct MQTTAuthV5: Sendable { 100 | /// MQTT v5 authentication reason code 101 | public let reason: MQTTReasonCode 102 | /// MQTT v5 properties 103 | public let properties: MQTTProperties 104 | 105 | init(reason: MQTTReasonCode, properties: MQTTProperties) { 106 | self.reason = reason 107 | self.properties = properties 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTError.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | /// MQTTClient errors 15 | public enum MQTTError: Error { 16 | /// Value returned in connection error 17 | public enum ConnectionReturnValue: UInt8, Sendable { 18 | /// connection was accepted 19 | case accepted = 0 20 | /// The Server does not support the version of the MQTT protocol requested by the Client. 21 | case unacceptableProtocolVersion = 1 22 | /// The Client Identifier is a valid string but is not allowed by the Server. 23 | case identifierRejected = 2 24 | /// The MQTT Server is not available. 25 | case serverUnavailable = 3 26 | /// The Server does not accept the User Name or Password specified by the Client 27 | case badUserNameOrPassword = 4 28 | /// The client is not authorized to connect 29 | case notAuthorized = 5 30 | /// Return code was unrecognised 31 | case unrecognizedReturnValue = 0xFF 32 | } 33 | 34 | /// You called connect on a client that is already connected to the broker 35 | case alreadyConnected 36 | /// Client has already been shutdown 37 | case alreadyShutdown 38 | /// We received an unexpected message while connecting 39 | case failedToConnect 40 | /// We received an unsuccessful connection return value 41 | case connectionError(ConnectionReturnValue) 42 | /// We received an unsuccessful return value from either a connect or publish 43 | case reasonError(MQTTReasonCode) 44 | /// client in not connected 45 | case noConnection 46 | /// the server disconnected 47 | case serverDisconnection(MQTTAckV5) 48 | /// the server closed the connection. If this happens during a publish you can resend 49 | /// the publish packet by reconnecting to server with `cleanSession` set to false. 50 | case serverClosedConnection 51 | /// received unexpected message from broker 52 | case unexpectedMessage 53 | /// Decode of MQTT message failed 54 | case decodeError 55 | /// Upgrade to websocker failed 56 | case websocketUpgradeFailed 57 | /// client timed out while waiting for response from server 58 | case timeout 59 | /// Internal error, used to get the client to retry sending 60 | case retrySend 61 | /// You have provided the wrong TLS configuration for the EventLoopGroup you provided 62 | case wrongTLSConfig 63 | /// Packet received contained invalid entries 64 | case badResponse 65 | /// Failed to recognise the packet control type 66 | case unrecognisedPacketType 67 | /// Auth packets sent without authWorkflow being supplied 68 | case authWorkflowRequired 69 | } 70 | 71 | /// Errors generated by bad packets sent by the client 72 | public struct MQTTPacketError: Error, Equatable { 73 | /// Packet sent contained invalid entries 74 | public static var badParameter: MQTTPacketError { .init(error: .badParameter) } 75 | /// QoS is not accepted by this connection as it is greater than the accepted value 76 | public static var qosInvalid: MQTTPacketError { .init(error: .qosInvalid) } 77 | /// publish messages on this connection do not support the retain flag 78 | public static var retainUnavailable: MQTTPacketError { .init(error: .retainUnavailable) } 79 | /// subscribe/unsubscribe packet requires at least one topic 80 | public static var atLeastOneTopicRequired: MQTTPacketError { .init(error: .atLeastOneTopicRequired) } 81 | /// topic alias is greater than server maximum topic alias or the alias is zero 82 | public static var topicAliasOutOfRange: MQTTPacketError { .init(error: .topicAliasOutOfRange) } 83 | /// invalid topic name 84 | public static var invalidTopicName: MQTTPacketError { .init(error: .invalidTopicName) } 85 | /// client to server publish packets cannot include a subscription identifier 86 | public static var publishIncludesSubscription: MQTTPacketError { .init(error: .publishIncludesSubscription) } 87 | 88 | private enum _Error { 89 | case badParameter 90 | case qosInvalid 91 | case retainUnavailable 92 | case atLeastOneTopicRequired 93 | case topicAliasOutOfRange 94 | case invalidTopicName 95 | case publishIncludesSubscription 96 | } 97 | 98 | private let error: _Error 99 | } 100 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTInflight.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2022 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | import NIOConcurrencyHelpers 16 | 17 | /// Array of inflight packets. Used to resend packets when reconnecting to server 18 | struct MQTTInflight { 19 | init() { 20 | self.lock = NIOLock() 21 | self.packets = .init(initialCapacity: 4) 22 | } 23 | 24 | /// add packet 25 | mutating func add(packet: MQTTPacket) { 26 | self.lock.withLock { 27 | self.packets.append(packet) 28 | } 29 | } 30 | 31 | /// remove packert 32 | mutating func remove(id: UInt16) { 33 | self.lock.withLock { 34 | guard let first = packets.firstIndex(where: { $0.packetId == id }) else { return } 35 | self.packets.remove(at: first) 36 | } 37 | } 38 | 39 | /// remove all packets 40 | mutating func clear() { 41 | self.lock.withLock { 42 | self.packets = [] 43 | } 44 | } 45 | 46 | private let lock: NIOLock 47 | private(set) var packets: CircularBuffer 48 | } 49 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTListeners.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2022 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | import NIOConcurrencyHelpers 16 | 17 | final class MQTTListeners { 18 | typealias Listener = (ReturnType) -> Void 19 | 20 | func notify(_ result: ReturnType) { 21 | let listeners = self.lock.withLock { 22 | return self.listeners 23 | } 24 | for listener in listeners.values { 25 | listener(result) 26 | } 27 | } 28 | 29 | func addListener(named name: String, listener: @escaping Listener) { 30 | self.lock.withLock { 31 | self.listeners[name] = listener 32 | } 33 | } 34 | 35 | func removeListener(named name: String) { 36 | self.lock.withLock { 37 | self.listeners[name] = nil 38 | } 39 | } 40 | 41 | func removeAll() { 42 | self.lock.withLock { 43 | self.listeners = [:] 44 | } 45 | } 46 | 47 | private let lock = NIOLock() 48 | private var listeners: [String: Listener] = [:] 49 | } 50 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTNIO.docc/MQTTClient.V5.md: -------------------------------------------------------------------------------- 1 | # ``MQTTNIO/MQTTClient/V5-swift.struct`` 2 | 3 | ## Topics 4 | 5 | ### Connection 6 | 7 | - ``connect(cleanStart:properties:will:authWorkflow:)-9o7nr`` 8 | - ``connect(cleanStart:properties:will:authWorkflow:)-8fbfg`` 9 | - ``disconnect(properties:)-321k`` 10 | - ``disconnect(properties:)-6x8ku`` 11 | 12 | ### Publish 13 | 14 | - ``publish(to:payload:qos:retain:properties:)-859ab`` 15 | - ``publish(to:payload:qos:retain:properties:)-1dhge`` 16 | 17 | ### Subscriptions 18 | 19 | - ``subscribe(to:properties:)-x043`` 20 | - ``subscribe(to:properties:)-4nenw`` 21 | - ``unsubscribe(from:properties:)-43of5`` 22 | - ``unsubscribe(from:properties:)-3chfg`` 23 | - ``createPublishListener(subscriptionId:)`` 24 | - ``addPublishListener(named:subscriptionId:_:)`` 25 | 26 | ### Authentication 27 | 28 | - ``auth(properties:authWorkflow:)-5vif3`` 29 | - ``auth(properties:authWorkflow:)-64uim`` 30 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTNIO.docc/MQTTClient.md: -------------------------------------------------------------------------------- 1 | # ``MQTTNIO/MQTTClient`` 2 | 3 | ## Topics 4 | 5 | ### Creating a client 6 | 7 | - ``init(host:port:identifier:eventLoopGroupProvider:logger:configuration:)`` 8 | - ``Configuration-swift.struct`` 9 | 10 | ### Instance Properties 11 | 12 | - ``configuration-swift.property`` 13 | - ``identifier`` 14 | - ``host`` 15 | - ``port`` 16 | - ``eventLoopGroup`` 17 | - ``logger`` 18 | - ``v5-swift.property`` 19 | - ``V5-swift.struct`` 20 | 21 | ### Shutdown 22 | 23 | - ``shutdown(queue:)`` 24 | - ``shutdown(queue:_:)`` 25 | - ``syncShutdownGracefully()`` 26 | 27 | ### Connection 28 | 29 | - ``connect(cleanSession:will:)-242j6`` 30 | - ``connect(cleanSession:will:)-51e4w`` 31 | - ``isActive()`` 32 | - ``disconnect()-8tgrs`` 33 | - ``disconnect()-45iy6`` 34 | - ``ping()-8mctk`` 35 | - ``ping()-3m8i5`` 36 | 37 | ### Publish 38 | 39 | - ``publish(to:payload:qos:retain:)`` 40 | - ``publish(to:payload:qos:retain:properties:)`` 41 | 42 | ### Subscriptions 43 | 44 | - ``subscribe(to:)-2ibiy`` 45 | - ``subscribe(to:)-1y95e`` 46 | - ``unsubscribe(from:)-48i9t`` 47 | - ``unsubscribe(from:)-1wjnz`` 48 | - ``createPublishListener()`` 49 | - ``addPublishListener(named:_:)`` 50 | - ``removePublishListener(named:)`` 51 | 52 | ### Listeners 53 | 54 | - ``addCloseListener(named:_:)`` 55 | - ``addShutdownListener(named:_:)`` 56 | - ``removeCloseListener(named:)`` 57 | - ``removeShutdownListener(named:)`` 58 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTNIO.docc/mqttnio-aws.md: -------------------------------------------------------------------------------- 1 | # AWS IoT 2 | 3 | Using MQTTNIO with AWS IoT. 4 | 5 | The MQTT client can be used to connect to AWS IoT brokers. You can use both a WebSocket connection authenticated using AWS Signature V4 and a standard connection using a X.509 client certificate. If you are using a X.509 certificate make sure you update the attached role to allow your client id to connect and which topics you can subscribe, publish to. 6 | 7 | If you are using an AWS Signature V4 authenticated WebSocket connection you can use the V4 signer from [SotoCore](https://github.com/soto-project/soto) to sign your initial request as follows 8 | ```swift 9 | import SotoSignerV4 10 | 11 | let host = "MY_AWS_IOT_ENDPOINT.iot.eu-west-1.amazonaws.com" 12 | let headers = HTTPHeaders([("host", host)]) 13 | let signer = AWSSigner( 14 | credentials: StaticCredential(accessKeyId: "MYACCESSKEY", secretAccessKey: "MYSECRETKEY"), 15 | name: "iotdata", 16 | region: "eu-west-1" 17 | ) 18 | let signedURL = signer.signURL( 19 | url: URL(string: "https://\(host)/mqtt")!, 20 | method: .GET, 21 | headers: headers, 22 | body: .none, 23 | expires: .minutes(30) 24 | ) 25 | let requestURI = "/mqtt?\(signedURL.query!)" 26 | let client = MQTTClient( 27 | host: host, 28 | identifier: "MyAWSClient", 29 | eventLoopGroupProvider: .createNew, 30 | configuration: .init(useSSL: true, webSocketConfiguration: .init(urlPath: requestUri)) 31 | ) 32 | ``` 33 | You can find out more about connecting to AWS brokers [here](https://docs.aws.amazon.com/iot/latest/developerguide/protocols.html) 34 | 35 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTNIO.docc/mqttnio-connections.md: -------------------------------------------------------------------------------- 1 | # Connections 2 | 3 | Support for TLS, WebSockets, and Unix Domain Sockets. 4 | 5 | ## TLS 6 | 7 | MQTT NIO supports TLS connections. You can enable this through the `Configuration` provided at initialization. Set`Configuration.useSSL` to `true` and provide your SSL certificates via the `Configuration.tlsConfiguration` struct. For example to connect to the mosquitto test server `test.mosquitto.org` on port 8884 you need to provide their root certificate and your own certificate. They provide details on the website [https://test.mosquitto.org/](https://test.mosquitto.org/) on how to generate these. 8 | 9 | ```swift 10 | let rootCertificate = try NIOSSLCertificate.fromPEMBytes([UInt8](mosquittoCertificateText.utf8)) 11 | let myCertificate = try NIOSSLCertificate.fromPEMBytes([UInt8](myCertificateText.utf8)) 12 | let myPrivateKey = try NIOSSLPrivateKey(bytes: [UInt8](myPrivateKeyText.utf8), format: .pem) 13 | let tlsConfiguration: TLSConfiguration? = TLSConfiguration.forClient( 14 | trustRoots: .certificates(rootCertificate), 15 | certificateChain: myCertificate.map { .certificate($0) }, 16 | privateKey: .privateKey(myPrivateKey) 17 | ) 18 | let client = MQTTClient( 19 | host: "test.mosquitto.org", 20 | port: 8884, 21 | identifier: "MySSLClient", 22 | eventLoopGroupProvider: .createNew, 23 | configuration: .init(useSSL: true, tlsConfiguration: .niossl(tlsConfiguration)), 24 | ) 25 | ``` 26 | 27 | ## WebSockets 28 | 29 | MQTT also supports Web Socket connections. Provide a `WebSocketConfiguration` when initializing `MQTTClient.Configuration` to enable this. 30 | 31 | ## NIO Transport Services 32 | 33 | On macOS and iOS you can use the NIO Transport Services library (NIOTS) and Apple's `Network.framework` for communication with the MQTT broker. If you don't provide an `eventLoopGroup` or a `TLSConfigurationType` then this is the default for both platforms. If you do provide either of these then the library will base it's decision on whether to use NIOTS or NIOSSL on what you provide. Provide a `MultiThreadedEventLoopGroup` or `NIOSSL.TLSConfiguration` and the client will use NIOSSL. Provide a `NIOTSEventLoopGroup` or `TSTLSConfiguration` and the client will use NIOTS. If you provide a `MultiThreadedEventLoopGroup` and a `TSTLSConfiguration` then the client will throw an error. If you are running on iOS you should always choose NIOTS. 34 | 35 | ## Unix Domain Sockets 36 | 37 | MQTT NIO can connect to a local MQTT broker via a Unix Domain Socket. 38 | 39 | ```swift 40 | let client = MQTTClient( 41 | unixSocketPath: "/path/to/broker.socket", 42 | identifier: "UDSClient", 43 | eventLoopGroupProvider: .createNew 44 | ) 45 | ``` 46 | 47 | Under the hood, `MQTTClient.port` will be 0 and `MQTTClient.host` will be the specified unix socket path when connecting to a unix socket. 48 | 49 | Note that mosquitto supports listening on a unix domain socket. This can be enabled by adding a `listener` option to the mosquitto config. 50 | 51 | ``` 52 | listener 0 /path/to/broker.socket 53 | ``` 54 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTNIO.docc/mqttnio-v5.md: -------------------------------------------------------------------------------- 1 | # MQTT Version 5.0 2 | 3 | MQTTNIO support for MQTT v5 protocol. 4 | 5 | Version 2.0 of MQTTNIO added support for MQTT v5.0. To create a client that will connect to a v5 MQTT broker you need to set the version in the configuration as follows 6 | 7 | ```swift 8 | let client = MQTTClient( 9 | host: host, 10 | identifier: "MyAWSClient", 11 | eventLoopGroupProvider: .createNew, 12 | configuration: .init(version: .v5_0) 13 | ) 14 | ``` 15 | 16 | You can then use the same functions available to the v3.1.1 client but there are also v5.0 specific versions of `connect`, `publish`, `subscribe`, `unsubscribe` and `disconnect`. These can be accessed via the variable `MQTTClient.v5`. The v5.0 functions add support for MQTT properties in both function parameters and return types and the additional subscription parameters. For example here is a `publish` call adding the `contentType` property. 17 | 18 | ```swift 19 | _ = try await client.v5.publish( 20 | to: "JSONTest", 21 | payload: payload, 22 | qos: .atLeastOnce, 23 | properties: [.contentType("application/json")] 24 | ) 25 | ``` 26 | 27 | Whoever subscribes to the "JSONTest" topic with a v5.0 client will also receive the `.contentType` property along with the payload. 28 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTNIO.docc/mqttnio.md: -------------------------------------------------------------------------------- 1 | # ``MQTTNIO`` 2 | 3 | A Swift NIO based MQTT client 4 | 5 | MQTT (Message Queuing Telemetry Transport) is a lightweight messaging protocol that was developed by IBM and first released in 1999. It uses the pub/sub pattern and translates messages between devices, servers, and applications. It is commonly used in Internet of things (IoT) technologies. 6 | 7 | MQTTNIO is a Swift NIO based implementation of a MQTT client. It supports 8 | 9 | - MQTT versions 3.1.1 and 5.0. 10 | - Unencrypted and encrypted (via TLS) connections 11 | - WebSocket connections 12 | - Posix sockets 13 | - Apple's Network framework via [NIOTransportServices](https://github.com/apple/swift-nio-transport-services) (required for iOS). 14 | 15 | ## Usage 16 | 17 | Create a client and connect to the MQTT broker. 18 | 19 | ```swift 20 | let client = MQTTClient( 21 | host: "mqtt.eclipse.org", 22 | port: 1883, 23 | identifier: "My Client", 24 | eventLoopGroupProvider: .createNew 25 | ) 26 | do { 27 | try await client.connect() 28 | print("Succesfully connected") 29 | } catch { 30 | print("Error while connecting \(error)") 31 | } 32 | ``` 33 | 34 | Subscribe to a topic and add a publish listener to report publish messages sent from the server/broker. 35 | ```swift 36 | let subscription = MQTTSubscribeInfo(topicFilter: "my-topics", qos: .atLeastOnce) 37 | _ = try await client.subscribe(to: [subscription]) 38 | let listener = client.createPublishListener() 39 | for await result in listener { 40 | switch result { 41 | case .success(let publish): 42 | if publish.topicName == "my-topics" { 43 | var buffer = publish.payload 44 | let string = buffer.readString(length: buffer.readableBytes) 45 | print(string) 46 | } 47 | case .failure(let error): 48 | print("Error while receiving PUBLISH event") 49 | } 50 | } 51 | ``` 52 | 53 | Publish to a topic. 54 | ```swift 55 | try await client.publish( 56 | to: "my-topics", 57 | payload: ByteBuffer(string: "This is the Test payload"), 58 | qos: .atLeastOnce 59 | ) 60 | ``` 61 | 62 | MQTTClient supports both Swift concurrency and SwiftNIO `EventLoopFuture`. The above examples use Swift concurrency but there are equivalent versions of these functions that return `EventLoopFuture`s. You can find out more about Swift NIO and `EventLoopFuture` [here](https://swiftpackageindex.com/apple/swift-nio/main/documentation/niocore/eventloopfuture). 63 | 64 | ## Topics 65 | 66 | ### Articles 67 | 68 | - 69 | - 70 | - 71 | 72 | ### Client 73 | 74 | - ``MQTTClient`` 75 | 76 | ### Subscribe/Publish 77 | 78 | - ``MQTTSubscribeInfo`` 79 | - ``MQTTSuback`` 80 | - ``MQTTPublishInfo`` 81 | - ``MQTTPublishListener`` 82 | - ``MQTTQoS`` 83 | - ``MQTTPacketType`` 84 | 85 | ### Errors 86 | 87 | - ``MQTTError`` 88 | - ``MQTTPacketError`` 89 | 90 | ### V5 Connection 91 | 92 | - ``MQTTConnackV5`` 93 | 94 | ### V5 Subscribe/Publish 95 | 96 | - ``MQTTSubscribeInfoV5`` 97 | - ``MQTTProperties`` 98 | - ``MQTTReasonCode`` 99 | - ``MQTTAckV5`` 100 | - ``MQTTSubackV5`` 101 | - ``MQTTPublishIdListener`` 102 | 103 | ### V5 Authentication 104 | 105 | - ``MQTTAuthV5`` 106 | 107 | ### TLS 108 | 109 | - ``TSTLSConfiguration`` 110 | - ``TSTLSVersion`` 111 | - ``TSCertificateVerification`` 112 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTReason.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | /// MQTT v5.0 reason codes. 15 | /// 16 | /// A reason code is a one byte unsigned value that indicates the result of an operation. 17 | /// Reason codes less than 128 are considered successful. Codes greater than or equal to 128 are considered 18 | /// a failure. These are returned by CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, DISCONNECT and 19 | /// AUTH packets 20 | public enum MQTTReasonCode: UInt8, Sendable { 21 | /// Success (available for all). For SUBACK mean QoS0 is available 22 | case success = 0 23 | /// The subscription is accepted and the maximum QoS sent will be QoS 1. This might be a lower QoS than was requested. 24 | case grantedQoS1 = 1 25 | /// The subscription is accepted and any received QoS will be sent to this subscription. 26 | case grantedQoS2 = 2 27 | /// The Client wishes to disconnect but requires that the Server also publishes its Will Message. 28 | case disconnectWithWill = 4 29 | /// The PUBLISH message is accepted but there are no subscribers. This is sent only by the Server. If the Server knows that 30 | /// there are no matching subscribers, it MAY use this Reason Code instead of 0x00 (Success). 31 | case noMatchingSubscriber = 16 32 | /// No matching Topic Filter is being used by the Client. 33 | case noSubscriptionExisted = 17 34 | /// Continue the authentication with another step 35 | case continueAuthentication = 24 36 | /// Initiate a re-authentication 37 | case reAuthenticate = 25 38 | /// Unaccpeted and the Server either does not wish to reveal the reason or none of the other Reason Codes apply. 39 | case unspecifiedError = 128 40 | /// Data within the packet could not be correctly parsed. 41 | case malformedPacket = 129 42 | /// Data in the packet does not conform to this specification. 43 | case protocolError = 130 44 | /// Packet is valid but the server does not accept it 45 | case implementationSpecificError = 131 46 | /// The Server does not support the version of the MQTT protocol requested by the Client. 47 | case unsupportedProtocolVersion = 132 48 | /// The Client Identifier is a valid string but is not allowed by the Server. 49 | case clientIdentifierNotValid = 133 50 | /// The Server does not accept the User Name or Password specified by the Client 51 | case badUsernameOrPassword = 134 52 | /// The client is not authorized to do this 53 | case notAuthorized = 135 54 | /// The MQTT Server is not available. 55 | case serverUnavailable = 136 56 | /// The Server is busy. Try again later. 57 | case serverBusy = 137 58 | /// This Client has been banned by administrative action. Contact the server administrator. 59 | case banned = 138 60 | /// The Server is shutting down. 61 | case serverShuttingDown = 139 62 | /// The authentication method is not supported or does not match the authentication method currently in use. 63 | case badAuthenticationMethod = 140 64 | /// The Connection is closed because no packet has been received for 1.5 times the Keepalive time. 65 | case keepAliveTimeout = 141 66 | /// Another Connection using the same ClientID has connected causing this Connection to be closed. 67 | case sessionTakenOver = 142 68 | /// The Topic Filter is correctly formed but is not allowed for this Client. 69 | case topicFilterInvalid = 143 70 | /// The Topic Name is not malformed, but is not accepted by this Client or Server. 71 | case topicNameInvalid = 144 72 | /// The specified Packet Identifier is already in use. This might indicate a mismatch in the Session State between the Client and Server. 73 | case packetIdentifierInUse = 145 74 | /// The Packet Identifier is not known. This is not an error during recovery, but at other times indicates a mismatch between 75 | /// the Session State on the Client and Server. 76 | case packetIdentifierNotFound = 146 77 | /// The Client or Server has received more than Receive Maximum publication for which it has not sent PUBACK or PUBCOMP. 78 | case receiveMaximumExceeded = 147 79 | /// The Client or Server has received a PUBLISH packet containing a Topic Alias which is greater than the Maximum Topic Alias it 80 | /// sent in the CONNECT or CONNACK packet. 81 | case topicAliasInvalid = 148 82 | /// The packet exceeded the maximum permissible size. 83 | case packetTooLarge = 149 84 | /// The received data rate is too high. 85 | case messageRateTooHigh = 150 86 | /// An implementation or administrative imposed limit has been exceeded. 87 | case quotaExceeeded = 151 88 | /// The Connection is closed due to an administrative action. 89 | case administrativeAction = 152 90 | /// The PUBLISH payload format does not match the one specified in the Payload Format Indicator. 91 | case payloadFormatInvalid = 153 92 | /// The Server does not support retained messages, and retain was set to 1. 93 | case retainNotSupported = 154 94 | /// The Server does not support the QoS set 95 | case qosNotSupported = 155 96 | /// The Client should temporarily use another server. 97 | case useAnotherServer = 156 98 | /// The Client should permanently use another server. 99 | case serverMoved = 157 100 | /// The Server does not support Shared Subscriptions for this Client. 101 | case sharedSubscriptionsNotSupported = 158 102 | /// The connection rate limit has been exceeded. 103 | case connectionRateExceeeded = 159 104 | /// The maximum connection time authorized for this connection has been exceeded. 105 | case maximumConnectTime = 160 106 | /// The Server does not support Subscription Identifiers; the subscription is not accepted. 107 | case subscriptionIdentifiersNotSupported = 161 108 | /// The Server does not support Wildcard Subscriptions; the subscription is not accepted. 109 | case wildcardSubscriptionsNotSupported = 162 110 | /// Reason code was unrecognised 111 | case unrecognisedReason = 255 112 | } 113 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTSerializer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | 16 | enum MQTTSerializer { 17 | /// write variable length 18 | static func writeVariableLengthInteger(_ value: Int, to byteBuffer: inout ByteBuffer) { 19 | var value = value 20 | repeat { 21 | let byte = UInt8(value & 0x7F) 22 | value >>= 7 23 | if value != 0 { 24 | byteBuffer.writeInteger(byte | 0x80) 25 | } else { 26 | byteBuffer.writeInteger(byte) 27 | } 28 | } while value != 0 29 | } 30 | 31 | static func variableLengthIntegerPacketSize(_ value: Int) -> Int { 32 | var value = value 33 | var size = 0 34 | repeat { 35 | size += 1 36 | value >>= 7 37 | } while value != 0 38 | return size 39 | } 40 | 41 | /// write string to byte buffer 42 | static func writeString(_ string: String, to byteBuffer: inout ByteBuffer) throws { 43 | let length = string.utf8.count 44 | guard length < 65536 else { throw MQTTPacketError.badParameter } 45 | byteBuffer.writeInteger(UInt16(length)) 46 | byteBuffer.writeString(string) 47 | } 48 | 49 | /// write buffer to byte buffer 50 | static func writeBuffer(_ buffer: ByteBuffer, to byteBuffer: inout ByteBuffer) throws { 51 | let length = buffer.readableBytes 52 | guard length < 65536 else { throw MQTTPacketError.badParameter } 53 | var buffer = buffer 54 | byteBuffer.writeInteger(UInt16(length)) 55 | byteBuffer.writeBuffer(&buffer) 56 | } 57 | 58 | /// read variable length from bytebuffer 59 | static func readVariableLengthInteger(from byteBuffer: inout ByteBuffer) throws -> Int { 60 | var value = 0 61 | var shift = 0 62 | repeat { 63 | guard let byte: UInt8 = byteBuffer.readInteger() else { throw InternalError.incompletePacket } 64 | value += (Int(byte) & 0x7F) << shift 65 | if byte & 0x80 == 0 { 66 | break 67 | } 68 | shift += 7 69 | } while true 70 | return value 71 | } 72 | 73 | /// read string from bytebuffer 74 | static func readString(from byteBuffer: inout ByteBuffer) throws -> String { 75 | guard let length: UInt16 = byteBuffer.readInteger() else { throw MQTTError.badResponse } 76 | guard let string = byteBuffer.readString(length: Int(length)) else { throw MQTTError.badResponse } 77 | return string 78 | } 79 | 80 | /// read slice from bytebuffer 81 | static func readBuffer(from byteBuffer: inout ByteBuffer) throws -> ByteBuffer { 82 | guard let length: UInt16 = byteBuffer.readInteger() else { throw MQTTError.badResponse } 83 | guard let buffer = byteBuffer.readSlice(length: Int(length)) else { throw MQTTError.badResponse } 84 | return buffer 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/MQTTTask.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | 16 | /// Class encapsulating a single task 17 | final class MQTTTask { 18 | let promise: EventLoopPromise 19 | let checkInbound: (MQTTPacket) throws -> Bool 20 | let timeoutTask: Scheduled? 21 | 22 | init(on eventLoop: EventLoop, timeout: TimeAmount?, checkInbound: @escaping (MQTTPacket) throws -> Bool) { 23 | let promise = eventLoop.makePromise(of: MQTTPacket.self) 24 | self.promise = promise 25 | self.checkInbound = checkInbound 26 | if let timeout { 27 | self.timeoutTask = eventLoop.scheduleTask(in: timeout) { 28 | promise.fail(MQTTError.timeout) 29 | } 30 | } else { 31 | self.timeoutTask = nil 32 | } 33 | } 34 | 35 | func succeed(_ response: MQTTPacket) { 36 | self.timeoutTask?.cancel() 37 | self.promise.succeed(response) 38 | } 39 | 40 | func fail(_ error: Error) { 41 | self.timeoutTask?.cancel() 42 | self.promise.fail(error) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/MQTTNIO/TSTLSConfiguration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2022 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | #if canImport(Network) 15 | 16 | import Foundation 17 | import Network 18 | #if os(macOS) || os(Linux) 19 | import NIOSSL 20 | #endif 21 | 22 | /// TLS Version enumeration 23 | public enum TSTLSVersion { 24 | case tlsV10 25 | case tlsV11 26 | case tlsV12 27 | case tlsV13 28 | 29 | /// return `SSLProtocol` for iOS12 api 30 | var sslProtocol: SSLProtocol { 31 | switch self { 32 | case .tlsV10: 33 | return .tlsProtocol1 34 | case .tlsV11: 35 | return .tlsProtocol11 36 | case .tlsV12: 37 | return .tlsProtocol12 38 | case .tlsV13: 39 | return .tlsProtocol13 40 | } 41 | } 42 | 43 | /// return `tls_protocol_version_t` for iOS13 and later apis 44 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 45 | var tlsProtocolVersion: tls_protocol_version_t { 46 | switch self { 47 | case .tlsV10: 48 | return .TLSv10 49 | case .tlsV11: 50 | return .TLSv11 51 | case .tlsV12: 52 | return .TLSv12 53 | case .tlsV13: 54 | return .TLSv13 55 | } 56 | } 57 | } 58 | 59 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 60 | extension tls_protocol_version_t { 61 | var tsTLSVersion: TSTLSVersion { 62 | switch self { 63 | case .TLSv10: 64 | return .tlsV10 65 | case .TLSv11: 66 | return .tlsV11 67 | case .TLSv12: 68 | return .tlsV12 69 | case .TLSv13: 70 | return .tlsV13 71 | default: 72 | preconditionFailure("Invalid TLS version") 73 | } 74 | } 75 | } 76 | 77 | /// Certificate verification modes. 78 | public enum TSCertificateVerification { 79 | /// All certificate verification disabled. 80 | case none 81 | 82 | /// Certificates will be validated against the trust store and checked 83 | /// against the hostname of the service we are contacting. 84 | case fullVerification 85 | } 86 | 87 | /// TLS configuration for NIO Transport Services 88 | public struct TSTLSConfiguration { 89 | /// Error loading TLS files 90 | public enum Error: Swift.Error { 91 | case invalidData 92 | } 93 | 94 | /// Struct defining an array of certificates 95 | public struct Certificates { 96 | let certificates: [SecCertificate] 97 | 98 | /// Create certificate array from already loaded SecCertificate array 99 | public static func certificates(_ secCertificates: [SecCertificate]) -> Self { .init(certificates: secCertificates) } 100 | 101 | #if os(macOS) || os(Linux) 102 | /// Create certificate array from PEM file 103 | public static func pem(_ filename: String) throws -> Self { 104 | let certificates = try NIOSSLCertificate.fromPEMFile(filename) 105 | let secCertificates = try certificates.map { certificate -> SecCertificate in 106 | guard let certificate = try SecCertificateCreateWithData(nil, Data(certificate.toDERBytes()) as CFData) else { 107 | throw TSTLSConfiguration.Error.invalidData 108 | } 109 | return certificate 110 | } 111 | return .init(certificates: secCertificates) 112 | } 113 | #endif 114 | 115 | /// Create certificate array from DER file 116 | public static func der(_ filename: String) throws -> Self { 117 | let certificateData = try Data(contentsOf: URL(fileURLWithPath: filename)) 118 | guard let secCertificate = SecCertificateCreateWithData(nil, certificateData as CFData) else { 119 | throw TSTLSConfiguration.Error.invalidData 120 | } 121 | return .init(certificates: [secCertificate]) 122 | } 123 | } 124 | 125 | /// Struct defining identity 126 | public struct Identity { 127 | let identity: SecIdentity 128 | 129 | /// Create Identity from already loaded SecIdentity 130 | public static func secIdentity(_ secIdentity: SecIdentity) -> Self { .init(identity: secIdentity) } 131 | 132 | /// Create Identity from p12 file 133 | public static func p12(filename: String, password: String) throws -> Self { 134 | let data = try Data(contentsOf: URL(fileURLWithPath: filename)) 135 | let options: [String: String] = [kSecImportExportPassphrase as String: password] 136 | var rawItems: CFArray? 137 | guard SecPKCS12Import(data as CFData, options as CFDictionary, &rawItems) == errSecSuccess else { 138 | throw TSTLSConfiguration.Error.invalidData 139 | } 140 | let items = rawItems! as! [[String: Any]] 141 | guard let firstItem = items.first, 142 | let secIdentity = firstItem[kSecImportItemIdentity as String] as! SecIdentity? 143 | else { 144 | throw TSTLSConfiguration.Error.invalidData 145 | } 146 | return .init(identity: secIdentity) 147 | } 148 | } 149 | 150 | /// The minimum TLS version to allow in negotiation. Defaults to tlsv1. 151 | public var minimumTLSVersion: TSTLSVersion 152 | 153 | /// The maximum TLS version to allow in negotiation. If nil, there is no upper limit. Defaults to nil. 154 | public var maximumTLSVersion: TSTLSVersion? 155 | 156 | /// The trust roots to use to validate certificates. This only needs to be provided if you intend to validate 157 | /// certificates. 158 | public var trustRoots: [SecCertificate]? 159 | 160 | /// The identity associated with the leaf certificate. 161 | public var clientIdentity: SecIdentity? 162 | 163 | /// The application protocols to use in the connection. 164 | public var applicationProtocols: [String] 165 | 166 | /// Whether to verify remote certificates. 167 | public var certificateVerification: TSCertificateVerification 168 | 169 | /// Initialize TSTLSConfiguration 170 | /// - Parameters: 171 | /// - minimumTLSVersion: minimum version of TLS supported 172 | /// - maximumTLSVersion: maximum version of TLS supported 173 | /// - trustRoots: The trust roots to use to validate certificates 174 | /// - clientIdentity: Client identity 175 | /// - applicationProtocols: The application protocols to use in the connection 176 | /// - certificateVerification: Should certificates be verified 177 | public init( 178 | minimumTLSVersion: TSTLSVersion = .tlsV10, 179 | maximumTLSVersion: TSTLSVersion? = nil, 180 | trustRoots: [SecCertificate]? = nil, 181 | clientIdentity: SecIdentity? = nil, 182 | applicationProtocols: [String] = [], 183 | certificateVerification: TSCertificateVerification = .fullVerification 184 | ) { 185 | self.minimumTLSVersion = minimumTLSVersion 186 | self.maximumTLSVersion = maximumTLSVersion 187 | self.trustRoots = trustRoots 188 | self.clientIdentity = clientIdentity 189 | self.applicationProtocols = applicationProtocols 190 | self.certificateVerification = certificateVerification 191 | } 192 | 193 | /// Initialize TSTLSConfiguration 194 | /// - Parameters: 195 | /// - minimumTLSVersion: minimum version of TLS supported 196 | /// - maximumTLSVersion: maximum version of TLS supported 197 | /// - trustRoots: The trust roots to use to validate certificates 198 | /// - clientIdentity: Client identity 199 | /// - applicationProtocols: The application protocols to use in the connection 200 | /// - certificateVerification: Should certificates be verified 201 | public init( 202 | minimumTLSVersion: TSTLSVersion = .tlsV10, 203 | maximumTLSVersion: TSTLSVersion? = nil, 204 | trustRoots: Certificates, 205 | clientIdentity: Identity, 206 | applicationProtocols: [String] = [], 207 | certificateVerification: TSCertificateVerification = .fullVerification 208 | ) { 209 | self.minimumTLSVersion = minimumTLSVersion 210 | self.maximumTLSVersion = maximumTLSVersion 211 | self.trustRoots = trustRoots.certificates 212 | self.clientIdentity = clientIdentity.identity 213 | self.applicationProtocols = applicationProtocols 214 | self.certificateVerification = certificateVerification 215 | } 216 | } 217 | 218 | extension TSTLSConfiguration { 219 | func getNWProtocolTLSOptions() throws -> NWProtocolTLS.Options { 220 | let options = NWProtocolTLS.Options() 221 | 222 | // minimum TLS protocol 223 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { 224 | sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, self.minimumTLSVersion.tlsProtocolVersion) 225 | } else { 226 | sec_protocol_options_set_tls_min_version(options.securityProtocolOptions, self.minimumTLSVersion.sslProtocol) 227 | } 228 | 229 | // maximum TLS protocol 230 | if let maximumTLSVersion = self.maximumTLSVersion { 231 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { 232 | sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, maximumTLSVersion.tlsProtocolVersion) 233 | } else { 234 | sec_protocol_options_set_tls_max_version(options.securityProtocolOptions, maximumTLSVersion.sslProtocol) 235 | } 236 | } 237 | 238 | if let clientIdentity = self.clientIdentity, let secClientIdentity = sec_identity_create(clientIdentity) { 239 | sec_protocol_options_set_local_identity(options.securityProtocolOptions, secClientIdentity) 240 | } 241 | 242 | for applicationProtocol in self.applicationProtocols { 243 | sec_protocol_options_add_tls_application_protocol(options.securityProtocolOptions, applicationProtocol) 244 | } 245 | 246 | if self.certificateVerification != .fullVerification || self.trustRoots != nil { 247 | // add verify block to control certificate verification 248 | sec_protocol_options_set_verify_block( 249 | options.securityProtocolOptions, 250 | { _, sec_trust, sec_protocol_verify_complete in 251 | guard self.certificateVerification != .none else { 252 | sec_protocol_verify_complete(true) 253 | return 254 | } 255 | 256 | let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue() 257 | if let trustRootCertificates = trustRoots { 258 | SecTrustSetAnchorCertificates(trust, trustRootCertificates as CFArray) 259 | } 260 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { 261 | SecTrustEvaluateAsyncWithError(trust, Self.tlsDispatchQueue) { _, result, error in 262 | if let error { 263 | print("Trust failed: \(error.localizedDescription)") 264 | } 265 | sec_protocol_verify_complete(result) 266 | } 267 | } else { 268 | SecTrustEvaluateAsync(trust, Self.tlsDispatchQueue) { _, result in 269 | switch result { 270 | case .proceed, .unspecified: 271 | sec_protocol_verify_complete(true) 272 | default: 273 | sec_protocol_verify_complete(false) 274 | } 275 | } 276 | } 277 | }, 278 | Self.tlsDispatchQueue 279 | ) 280 | } 281 | return options 282 | } 283 | 284 | /// Dispatch queue used by Network framework TLS to control certificate verification 285 | static var tlsDispatchQueue = DispatchQueue(label: "TSTLSConfiguration") 286 | } 287 | 288 | /// Deprecated TSTLSConfiguration 289 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 290 | @available(*, deprecated, message: "Use the init using TSTLSVersion") 291 | extension TSTLSConfiguration { 292 | /// Initialize TSTLSConfiguration 293 | public init( 294 | minimumTLSVersion: tls_protocol_version_t, 295 | maximumTLSVersion: tls_protocol_version_t? = nil, 296 | trustRoots: Certificates, 297 | clientIdentity: Identity, 298 | applicationProtocols: [String] = [], 299 | certificateVerification: TSCertificateVerification = .fullVerification 300 | ) { 301 | self.minimumTLSVersion = minimumTLSVersion.tsTLSVersion 302 | self.maximumTLSVersion = maximumTLSVersion?.tsTLSVersion 303 | self.trustRoots = trustRoots.certificates 304 | self.clientIdentity = clientIdentity.identity 305 | self.applicationProtocols = applicationProtocols 306 | self.certificateVerification = certificateVerification 307 | } 308 | 309 | /// Initialize TSTLSConfiguration 310 | public init( 311 | minimumTLSVersion: tls_protocol_version_t, 312 | maximumTLSVersion: tls_protocol_version_t? = nil, 313 | trustRoots: [SecCertificate]? = nil, 314 | clientIdentity: SecIdentity? = nil, 315 | applicationProtocols: [String] = [], 316 | certificateVerification: TSCertificateVerification = .fullVerification 317 | ) { 318 | self.minimumTLSVersion = minimumTLSVersion.tsTLSVersion 319 | self.maximumTLSVersion = maximumTLSVersion?.tsTLSVersion 320 | self.trustRoots = trustRoots 321 | self.clientIdentity = clientIdentity 322 | self.applicationProtocols = applicationProtocols 323 | self.certificateVerification = certificateVerification 324 | } 325 | } 326 | 327 | #endif 328 | -------------------------------------------------------------------------------- /Tests/MQTTNIOTests/CoreMQTTTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import NIO 15 | import XCTest 16 | 17 | @testable import MQTTNIO 18 | 19 | final class CoreMQTTTests: XCTestCase { 20 | func testConnect() throws { 21 | let publish = MQTTPublishInfo( 22 | qos: .atMostOnce, 23 | retain: false, 24 | dup: false, 25 | topicName: "MyTopic", 26 | payload: ByteBufferAllocator().buffer(string: "Test payload"), 27 | properties: .init() 28 | ) 29 | var byteBuffer = ByteBufferAllocator().buffer(capacity: 1024) 30 | let connectPacket = MQTTConnectPacket( 31 | cleanSession: true, 32 | keepAliveSeconds: 15, 33 | clientIdentifier: "MyClient", 34 | userName: nil, 35 | password: nil, 36 | properties: .init(), 37 | will: publish 38 | ) 39 | try connectPacket.write(version: .v3_1_1, to: &byteBuffer) 40 | XCTAssertEqual(byteBuffer.readableBytes, 45) 41 | } 42 | 43 | func testPublish() throws { 44 | let publish = MQTTPublishInfo( 45 | qos: .atMostOnce, 46 | retain: false, 47 | dup: false, 48 | topicName: "MyTopic", 49 | payload: ByteBufferAllocator().buffer(string: "Test payload"), 50 | properties: .init() 51 | ) 52 | var byteBuffer = ByteBufferAllocator().buffer(capacity: 1024) 53 | let publishPacket = MQTTPublishPacket(publish: publish, packetId: 456) 54 | try publishPacket.write(version: .v3_1_1, to: &byteBuffer) 55 | let packet = try MQTTIncomingPacket.read(from: &byteBuffer) 56 | let publish2 = try MQTTPublishPacket.read(version: .v3_1_1, from: packet) 57 | XCTAssertEqual(publish.topicName, publish2.publish.topicName) 58 | XCTAssertEqual(publish.payload, publish2.publish.payload) 59 | } 60 | 61 | func testSubscribe() throws { 62 | let subscriptions: [MQTTSubscribeInfoV5] = [ 63 | .init(topicFilter: "topic/cars", qos: .atLeastOnce), 64 | .init(topicFilter: "topic/buses", qos: .atLeastOnce), 65 | ] 66 | var byteBuffer = ByteBufferAllocator().buffer(capacity: 1024) 67 | let subscribePacket = MQTTSubscribePacket(subscriptions: subscriptions, properties: nil, packetId: 456) 68 | try subscribePacket.write(version: .v3_1_1, to: &byteBuffer) 69 | let packet = try MQTTIncomingPacket.read(from: &byteBuffer) 70 | XCTAssertEqual(packet.remainingData.readableBytes, 29) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/MQTTNIOTests/MQTTNIOTests+async.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the MQTTNIO project 4 | // 5 | // Copyright (c) 2020-2021 Adam Fowler 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Atomics 15 | import Logging 16 | import NIO 17 | import NIOFoundationCompat 18 | import NIOHTTP1 19 | import XCTest 20 | 21 | @testable import MQTTNIO 22 | 23 | #if os(macOS) || os(Linux) 24 | import NIOSSL 25 | #endif 26 | 27 | @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) 28 | final class AsyncMQTTNIOTests: XCTestCase { 29 | static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost" 30 | static let logger: Logger = { 31 | var logger = Logger(label: "MQTTTests") 32 | logger.logLevel = .trace 33 | return logger 34 | }() 35 | 36 | func createClient(identifier: String, configuration: MQTTClient.Configuration = .init()) -> MQTTClient { 37 | MQTTClient( 38 | host: Self.hostname, 39 | port: 1883, 40 | identifier: identifier, 41 | eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup.singleton), 42 | logger: Self.logger, 43 | configuration: configuration 44 | ) 45 | } 46 | 47 | func withMQTTClient( 48 | identifier: String, 49 | configuration: MQTTClient.Configuration = .init(), 50 | operation: (MQTTClient) async throws -> Void 51 | ) async throws { 52 | let client = createClient(identifier: identifier, configuration: configuration) 53 | do { 54 | try await operation(client) 55 | } catch { 56 | try? await client.shutdown() 57 | throw error 58 | } 59 | try await client.shutdown() 60 | } 61 | 62 | func testConnect() async throws { 63 | try await withMQTTClient(identifier: "testConnect+async") { client in 64 | try await client.connect() 65 | try await client.disconnect() 66 | } 67 | } 68 | 69 | func testPublishSubscribe() async throws { 70 | try await withMQTTClient(identifier: "testPublish+async") { client in 71 | try await withMQTTClient(identifier: "testPublish+async2") { client2 in 72 | let payloadString = "Hello" 73 | try await client.connect() 74 | try await client2.connect() 75 | _ = try await client2.subscribe(to: [.init(topicFilter: "TestSubject", qos: .atLeastOnce)]) 76 | try await withThrowingTaskGroup(of: Void.self) { group in 77 | group.addTask { 78 | let listener = client2.createPublishListener() 79 | for try await event in listener { 80 | switch event { 81 | case .success(let publish): 82 | var buffer = publish.payload 83 | let string = buffer.readString(length: buffer.readableBytes) 84 | XCTAssertEqual(string, payloadString) 85 | return 86 | case .failure(let error): 87 | XCTFail("\(error)") 88 | } 89 | } 90 | } 91 | group.addTask { 92 | try await Task.sleep(nanoseconds: 5_000_000_000) 93 | XCTFail("Timeout") 94 | } 95 | try await client.publish(to: "TestSubject", payload: ByteBufferAllocator().buffer(string: payloadString), qos: .atLeastOnce) 96 | try await group.next() 97 | group.cancelAll() 98 | } 99 | 100 | try await client2.disconnect() 101 | } 102 | try await client.disconnect() 103 | } 104 | } 105 | 106 | func testPing() async throws { 107 | try await withMQTTClient(identifier: "TestPing", configuration: .init(disablePing: true)) { client in 108 | try await client.connect() 109 | try await client.ping() 110 | try await client.disconnect() 111 | } 112 | } 113 | 114 | func testAsyncSequencePublishListener() async throws { 115 | try await withMQTTClient(identifier: "testAsyncSequencePublishListener+async", configuration: .init(version: .v5_0)) { client in 116 | try await withMQTTClient(identifier: "testAsyncSequencePublishListener+async2", configuration: .init(version: .v5_0)) { client2 in 117 | 118 | try await client.connect() 119 | try await client2.connect() 120 | _ = try await client2.v5.subscribe(to: [.init(topicFilter: "TestSubject", qos: .atLeastOnce)]) 121 | try await withThrowingTaskGroup(of: Void.self) { group in 122 | group.addTask { 123 | let publishListener = client2.createPublishListener() 124 | for await result in publishListener { 125 | switch result { 126 | case .success(let publish): 127 | var buffer = publish.payload 128 | let string = buffer.readString(length: buffer.readableBytes) 129 | print("Received: \(string ?? "nothing")") 130 | return 131 | 132 | case .failure(let error): 133 | XCTFail("\(error)") 134 | } 135 | } 136 | } 137 | group.addTask { 138 | try await Task.sleep(nanoseconds: 5_000_000_000) 139 | XCTFail("Timeout") 140 | } 141 | try await client.publish(to: "TestSubject", payload: ByteBufferAllocator().buffer(string: "Hello"), qos: .atLeastOnce) 142 | try await client.publish(to: "TestSubject", payload: ByteBufferAllocator().buffer(string: "Goodbye"), qos: .atLeastOnce) 143 | 144 | try await group.next() 145 | group.cancelAll() 146 | } 147 | try await client2.disconnect() 148 | } 149 | try await client.disconnect() 150 | } 151 | } 152 | 153 | func testAsyncSequencePublishSubscriptionIdListener() async throws { 154 | try await withMQTTClient(identifier: "testAsyncSequencePublishSubscriptionIdListener+async", configuration: .init(version: .v5_0)) { client in 155 | try await withMQTTClient(identifier: "testAsyncSequencePublishSubscriptionIdListener+async2", configuration: .init(version: .v5_0)) { 156 | client2 in 157 | let payloadString = "Hello" 158 | 159 | try await client.connect() 160 | try await client2.connect() 161 | _ = try await client2.v5.subscribe( 162 | to: [.init(topicFilter: "TestSubject", qos: .atLeastOnce)], 163 | properties: [.subscriptionIdentifier(1)] 164 | ) 165 | _ = try await client2.v5.subscribe( 166 | to: [.init(topicFilter: "TestSubject2", qos: .atLeastOnce)], 167 | properties: [.subscriptionIdentifier(2)] 168 | ) 169 | try await withThrowingTaskGroup(of: Void.self) { group in 170 | group.addTask { 171 | let publishListener = client2.v5.createPublishListener(subscriptionId: 1) 172 | for await event in publishListener { 173 | XCTAssertEqual(String(buffer: event.payload), payloadString) 174 | return 175 | } 176 | } 177 | group.addTask { 178 | let publishListener = client2.v5.createPublishListener(subscriptionId: 2) 179 | for await event in publishListener { 180 | XCTAssertEqual(String(buffer: event.payload), payloadString) 181 | return 182 | } 183 | } 184 | group.addTask { 185 | try await Task.sleep(nanoseconds: 5_000_000_000) 186 | XCTFail("Timeout") 187 | } 188 | try await client.publish(to: "TestSubject", payload: ByteBufferAllocator().buffer(string: payloadString), qos: .atLeastOnce) 189 | try await client.publish(to: "TestSubject2", payload: ByteBufferAllocator().buffer(string: payloadString), qos: .atLeastOnce) 190 | 191 | try await group.next() 192 | try await group.next() 193 | group.cancelAll() 194 | } 195 | 196 | try await client2.disconnect() 197 | } 198 | try await client.disconnect() 199 | } 200 | } 201 | 202 | func testMQTTPublishRetain() async throws { 203 | let payloadString = 204 | #"{"from":1000000,"to":1234567,"type":1,"content":"I am a beginner in swift and I am studying hard!!测试\n\n test, message","timestamp":1607243024,"nonce":"pAx2EsUuXrVuiIU3GGOGHNbUjzRRdT5b","sign":"ff902e31a6a5f5343d70a3a93ac9f946adf1caccab539c6f3a6"}"# 205 | let payload = ByteBufferAllocator().buffer(string: payloadString) 206 | 207 | try await withMQTTClient(identifier: "testMQTTPublishRetain_publisher+async") { client in 208 | try await withMQTTClient(identifier: "testMQTTPublishRetain_subscriber+async") { client2 in 209 | try await client.connect() 210 | try await client.publish(to: "testAsyncMQTTPublishRetain", payload: payload, qos: .atLeastOnce, retain: true) 211 | try await client2.connect() 212 | try await withThrowingTaskGroup(of: Void.self) { group in 213 | group.addTask { 214 | let listener = client2.createPublishListener() 215 | for try await event in listener { 216 | switch event { 217 | case .success(let publish): 218 | var buffer = publish.payload 219 | let string = buffer.readString(length: buffer.readableBytes) 220 | XCTAssertEqual(string, payloadString) 221 | return 222 | case .failure(let error): 223 | XCTFail("\(error)") 224 | } 225 | } 226 | } 227 | group.addTask { 228 | try await Task.sleep(nanoseconds: 5_000_000_000) 229 | XCTFail("Timeout") 230 | } 231 | _ = try await client2.subscribe(to: [.init(topicFilter: "testAsyncMQTTPublishRetain", qos: .atLeastOnce)]) 232 | try await group.next() 233 | group.cancelAll() 234 | } 235 | 236 | try await client2.disconnect() 237 | } 238 | try await client.disconnect() 239 | } 240 | } 241 | 242 | func testPersistentSubscription() async throws { 243 | let count = ManagedAtomic(0) 244 | let (stream, cont) = AsyncStream.makeStream(of: Void.self) 245 | try await withMQTTClient(identifier: "testPublish+async") { client in 246 | try await withMQTTClient(identifier: "testPublish+async2") { client2 in 247 | let payloadString = "Hello" 248 | try await client.connect() 249 | try await client2.connect(cleanSession: false) 250 | _ = try await client2.subscribe(to: [.init(topicFilter: "TestSubject", qos: .atLeastOnce)]) 251 | try await withThrowingTaskGroup(of: Void.self) { group in 252 | group.addTask { 253 | let listener = client2.createPublishListener() 254 | cont.finish() 255 | for try await event in listener { 256 | switch event { 257 | case .success(let publish): 258 | var buffer = publish.payload 259 | let string = buffer.readString(length: buffer.readableBytes) 260 | XCTAssertEqual(string, payloadString) 261 | let value = count.wrappingIncrementThenLoad(by: 1, ordering: .relaxed) 262 | if value == 2 { 263 | return 264 | } 265 | case .failure(let error): 266 | XCTFail("\(error)") 267 | } 268 | } 269 | } 270 | group.addTask { 271 | try await Task.sleep(nanoseconds: 5_000_000_000) 272 | XCTFail("Timeout") 273 | } 274 | await stream.first { _ in true } 275 | try await client.publish(to: "TestSubject", payload: ByteBufferAllocator().buffer(string: payloadString), qos: .atLeastOnce) 276 | try await client2.disconnect() 277 | try await client2.connect(cleanSession: false) 278 | try await client.publish(to: "TestSubject", payload: ByteBufferAllocator().buffer(string: payloadString), qos: .atLeastOnce) 279 | try await group.next() 280 | group.cancelAll() 281 | } 282 | 283 | try await client2.disconnect() 284 | } 285 | try await client.disconnect() 286 | } 287 | XCTAssertEqual(count.load(ordering: .relaxed), 2) 288 | } 289 | 290 | func testSubscriptionListenerEndsOnCleanSessionDisconnect() async throws { 291 | try await withMQTTClient(identifier: "testPublish+async") { client in 292 | try await withMQTTClient(identifier: "testPublish+async2") { client2 in 293 | let payloadString = "Hello" 294 | try await client.connect() 295 | try await client2.connect(cleanSession: true) 296 | _ = try await client2.subscribe(to: [.init(topicFilter: "TestSubject", qos: .atLeastOnce)]) 297 | try await withThrowingTaskGroup(of: Void.self) { group in 298 | group.addTask { 299 | let listener = client2.createPublishListener() 300 | for try await event in listener { 301 | switch event { 302 | case .success(let publish): 303 | var buffer = publish.payload 304 | let string = buffer.readString(length: buffer.readableBytes) 305 | XCTAssertEqual(string, payloadString) 306 | case .failure(let error): 307 | XCTFail("\(error)") 308 | } 309 | } 310 | } 311 | group.addTask { 312 | try await Task.sleep(nanoseconds: 5_000_000_000) 313 | XCTFail("Timeout") 314 | } 315 | try await client.publish(to: "TestSubject", payload: ByteBufferAllocator().buffer(string: payloadString), qos: .atLeastOnce) 316 | try await client2.disconnect() 317 | try await group.next() 318 | group.cancelAll() 319 | } 320 | } 321 | try await client.disconnect() 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # run this with: docker-compose -f docker-compose.yml run test 2 | version: "3.3" 3 | 4 | services: 5 | test: 6 | image: swift:6.0 7 | working_dir: /mqtt-nio 8 | volumes: 9 | - .:/mqtt-nio 10 | - mosquitto-socket:/mqtt-nio/mosquitto/socket 11 | depends_on: 12 | - mosquitto 13 | environment: 14 | - MOSQUITTO_SERVER=mosquitto 15 | - CI=true 16 | command: /bin/bash -xcl "swift test" 17 | 18 | mosquitto: 19 | image: eclipse-mosquitto 20 | volumes: 21 | - ./mosquitto/config:/mosquitto/config 22 | - ./mosquitto/certs:/mosquitto/certs 23 | - mosquitto-socket:/mosquitto/socket 24 | ports: 25 | - "1883:1883" 26 | - "8883:8883" 27 | - "8080:8080" 28 | - "8081:8081" 29 | 30 | volumes: 31 | mosquitto-socket: 32 | -------------------------------------------------------------------------------- /mosquitto/certs/ca.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swift-server-community/mqtt-nio/493ca8bae5b3e93f9246a1a182a87efe25b5e6ba/mosquitto/certs/ca.der -------------------------------------------------------------------------------- /mosquitto/certs/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDAF1SU9K6eYp2K 3 | zfQy+uxnjZp8cI+aHaJBj5sbggvT2qNmQTvgVXoxbKM9mxTLZ6ks3L9hXzXHeW8o 4 | oLVdYKpJw7YtTvU5KW+w9okJzh1wXvgm8r0Pro8zt1N8mtJuyHdNQ6gPdghBnnGt 5 | lT62BbTqxALHDp6mBF/HfulQagvo+Kewa3sR6Fn0oxHNGy/dbauWdqxHrvY+ytbv 6 | T/xGbQVLO9hfacrUsKxAD9QO60gklMZS3sNyN3mT/87ZbtaQ0vBAGY8HJmBM5BCj 7 | BqbAoqwOkO7eJxO34UQbZ9EQlCAcCdhTL6NEz2YnvCYjTYwo5CKLy2wF6fkYZYBz 8 | gNYMFz77AgMBAAECggEAFmSh35uGn8AvTXck+Kx30rqXP9p/YyABQlNTaamHZ3Md 9 | iVYhfM16KTjY7t2dVvkGp8w0I03OHyrw4nOZsQEL2P2Px0hlHfzOoHqdDx+QHwFz 10 | PFcf4yweVZERkn0Z+wAzqDYy6VqBK+Ukq2+yl9WieZvQFxSFOiVYZRFOcalrKgVe 11 | jBvIlDzC6CCHaYnornUtpMg5W6OGZn/3EcjBl9EOmeJW8P6YZpv9CDUHag0LcBaY 12 | E0mUIH0zvTXnYbBMX4xNG1IrB1MxwAW5EhyrebGBsD0uigjzNY/iquQdKIMUtHX4 13 | IWgbW+CELfOGdQR9AOWjMzxZyXYNY2WGhRwrxVFjAQKBgQD/xJHNenajePdhAdal 14 | +SwYv5OxIMA3Tch3qEnVmK/b/mDqANdFMerRa4OeHXSZYSpAiWEFil3DXthFsBfF 15 | apdCtseAJRiKanNm29B30J1ZDXGg+d7RZ+o6/v3PMiyNYvlBdoYhWhmmjUby7LfG 16 | fUfJpj/++semD1QpUk9VjZnyUQKBgQDAQ/cCB9oEl/hfO89SH007Hf6nYxVHUUxw 17 | X/Wvo7tWgRiO08xgeAD5hFPuIhoEXxevirWEvS8VBWJbEX5E4/++b+D9rU4icVXR 18 | /j2Ae8hcU/3WXKjanzU37c/emPfiWllZOnnmkzEOMnWWpXbgkHKpBKesHJBEx5AR 19 | 4ZKC21WdiwKBgQDbRTFegIPfZ+BlCQd1aSYV3YAH1bUUdJnNg7gw52K07uM1Gh1z 20 | 0/SlL1A6KLSCnht0EpLcBiCWUuSE8g+fDt+4sSxdvu/IErT21LJnVbDf3LeysyUE 21 | T9suUtdTX4d0ewqHxc3+H9lnwSy3LJvtDhCSXvX0ahjpU7DqcAdVqDz30QKBgA2n 22 | Re1A/XyCBkNNDgX47xUZpjHg2Wv9G/6G8f2NFQqplELgS402OGt/uC4bAdn5nsb6 23 | hLVucd9+SGPLNDpULp8pdsLNAdV0UvHcqiVrpOjZlhcY8WzFDZRxMOfP2Rqb6+ho 24 | PKvhHAS0FKGkEnMUDvBtwjJ4vM4FlfePG0ZrqQLNAoGAKE8XZcLoG3u4xvlPd4Nj 25 | zpXpshvO+hafwtvjJ7HhozK6eMn9tNnRChPmf8OsBpws9Ku6sL9TTXLmBKGd9LnW 26 | W1hSjwiAXxtr8EvzcgBfjcViLj6I3Cy/uYcxFKkUnPfzOy6CnDLlP9E5u8GZtyVK 27 | FaEd2Ce+Tgj4Pet+9E5qDUw= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /mosquitto/certs/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDTjCCAjYCCQDlX9hyx6XI6zANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJV 3 | SzESMBAGA1UECAwJRWRpbmJ1cmdoMRIwEAYDVQQHDAlFZGluYnVyZ2gxEDAOBgNV 4 | BAoMB01RVFROSU8xCzAJBgNVBAsMAkNBMRMwEQYDVQQDDApzb3RvLmNvZGVzMB4X 5 | DTI0MDkxODIwMTY1OVoXDTI1MDkxODIwMTY1OVowaTELMAkGA1UEBhMCVUsxEjAQ 6 | BgNVBAgMCUVkaW5idXJnaDESMBAGA1UEBwwJRWRpbmJ1cmdoMRAwDgYDVQQKDAdN 7 | UVRUTklPMQswCQYDVQQLDAJDQTETMBEGA1UEAwwKc290by5jb2RlczCCASIwDQYJ 8 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMAXVJT0rp5inYrN9DL67GeNmnxwj5od 9 | okGPmxuCC9Pao2ZBO+BVejFsoz2bFMtnqSzcv2FfNcd5byigtV1gqknDti1O9Tkp 10 | b7D2iQnOHXBe+CbyvQ+ujzO3U3ya0m7Id01DqA92CEGeca2VPrYFtOrEAscOnqYE 11 | X8d+6VBqC+j4p7BrexHoWfSjEc0bL91tq5Z2rEeu9j7K1u9P/EZtBUs72F9pytSw 12 | rEAP1A7rSCSUxlLew3I3eZP/ztlu1pDS8EAZjwcmYEzkEKMGpsCirA6Q7t4nE7fh 13 | RBtn0RCUIBwJ2FMvo0TPZie8JiNNjCjkIovLbAXp+RhlgHOA1gwXPvsCAwEAATAN 14 | BgkqhkiG9w0BAQsFAAOCAQEAAnqIZhMwBPqDkJdPC+jI5Bx2KN/TStNXfmid1f83 15 | kRVDJAILdVbt3XnMPYA0pdyCA4KitSiNTquGcVh+YCvx640nWEtTBMi3XsCR6puT 16 | CRl2AbjB0o7BfVRTO1ovMuFSxbtIxpYmZ94DLzFt2kg3OvpFFKXJG2PJk/anuVlF 17 | vCdfzKmJasFHdPQ9UfPbgtjBRJTTah1UqR2SUvia3dEOPYk5dVszOMJ7GiMv83ic 18 | jKy5D7yaWEzJK5SH7Swuq3T8ZSEPTtDg3TMpjkmJrNVVNNZTWi9xkNzUkuP2gfA9 19 | TXahE1Czv/rB5T+3Hub1WUytycZyfw95iE+3Hlcwf3Np3w== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /mosquitto/certs/ca.srl: -------------------------------------------------------------------------------- 1 | C0BAD048C3DEF2C6 2 | -------------------------------------------------------------------------------- /mosquitto/certs/client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICsjCCAZoCAQAwbTELMAkGA1UEBhMCVUsxEjAQBgNVBAgMCUVkaW5idXJnaDES 3 | MBAGA1UEBwwJRWRpbmJ1cmdoMRAwDgYDVQQKDAdNUVRUTklPMQ8wDQYDVQQLDAZD 4 | bGllbnQxEzARBgNVBAMMCnNvdG8uY29kZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IB 5 | DwAwggEKAoIBAQCk4gTilQfyqF4a1HQ3sVAs+xzuZ9azn/sWrliacyhqo23tzmg+ 6 | ArFxbDI2DRptV8p/AZb8P2QhdgH0AZ+6RFeQksF+kXKU3ChO5TKFFZvXxNET6mc4 7 | oGysqjAmA2y2vmBbRflibwkfxCr4Bmf0iEC0xMxnx8ltJxFxLpLLEuu1VyZngnEW 8 | tNFxLoygUGM/L55FE0Bx8E5ThUAuYk7vR0dp58zopT5yH/ExH5rOd7VinYUVc9Xk 9 | yQISpQV18Hx2EC04FJQr/kKapfQ9wItgVwMqeJihczsyTZ5zbG8h21bOLa7T7eF8 10 | ZrXaIG3rvKnBuTzl9zM5TCnjY0V0MAPkB09HAgMBAAGgADANBgkqhkiG9w0BAQsF 11 | AAOCAQEAQz0B+T8WtEb2CdfhXTdAUgSScq9nWRAtf6/9x0cDjBVREpsrYLh/+AJ9 12 | G/qlzu5WhaBYQwscs3fc1quWOrBoqXiGAM5/+cfAH2GX+lGvJtEGYaxyDU6rhlEV 13 | 8K5fzVf0FWTZZb8Cxa1i+Pvq+1DXMnwfWLSZiHPOoy805LSxLnYVfCmyPWKyWEEE 14 | 7Z5uAZJQG5UGRTEosUQdG1dINICUWrddvw5jgYNBrO5UlvIasUQmrHCBumtmxake 15 | 8x7cSEctCUCmtTm7eNw4QauDd2pgNIYSoHYgFjD64+6p89WANoAjzTvwzCMDx6qg 16 | dZHUokpVRP13Ap5swwV6FXRddCFzGw== 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /mosquitto/certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCk4gTilQfyqF4a 3 | 1HQ3sVAs+xzuZ9azn/sWrliacyhqo23tzmg+ArFxbDI2DRptV8p/AZb8P2QhdgH0 4 | AZ+6RFeQksF+kXKU3ChO5TKFFZvXxNET6mc4oGysqjAmA2y2vmBbRflibwkfxCr4 5 | Bmf0iEC0xMxnx8ltJxFxLpLLEuu1VyZngnEWtNFxLoygUGM/L55FE0Bx8E5ThUAu 6 | Yk7vR0dp58zopT5yH/ExH5rOd7VinYUVc9XkyQISpQV18Hx2EC04FJQr/kKapfQ9 7 | wItgVwMqeJihczsyTZ5zbG8h21bOLa7T7eF8ZrXaIG3rvKnBuTzl9zM5TCnjY0V0 8 | MAPkB09HAgMBAAECggEAcvPtCfdzKhd+PGBggi+JwUJ1gjU899CSotZ8iXm99NLq 9 | IkCkZo9EHNqdCxgJk7AASpnWJRkg+z8lz3OOY7OgBPh8FHzdELGJHLAoj6ZoF39t 10 | cOAchNs7yQmCNg5vLdz+msPnQVw+VTpT5sW4lkCkNCN8iuI8KXBydaFN0GzpjmwM 11 | YSy1KiCSXfhd42ZgufnFdcZaKBmezTGe7g3dc5kH+XCGAYIOVYkm2p3VKmsWOpom 12 | PllnkOg96VlJL/wq4ze1HYmSVNQwnTBRiJ02vizabhlwazd9lXWeXHUnsQvRrqVv 13 | g3ijkS3vovQkStx804KgUFM2139gNK/6LM5+Yy8c4QKBgQDXCey9rtEfwhk1HCoG 14 | G08u/RBv1FYUBUMUOQfbKDDgOQQl8EL4/M5UYzECMJ+oaDh6AeQKlgeNZ5avMr4n 15 | jX1CDv74pdyrM+SfsOT+UqxJumqfX3N/oPLlYJZUdYzuU74R+wjYMv6rglqfXPJy 16 | ZaEXrJQIHkCrq5NiE09W/VzEtwKBgQDESk96dCfSb2cfNXY+Nd1bNXSUzApHn+3N 17 | oW3oe4fDLn0Lb1InHZHdJh35oOsi0G1dR1hh5j8pkeAsCyB54StQebvPsgsgiONe 18 | gcKBFWDxTdVFFrkX0D7LMVopoa4vULpJYF7uh03NovVoRxSIxOvSfLJ1g15qvI3J 19 | V9pxpaXZ8QKBgQCeTPMXi/rs6yFNZKdXCYGYMLmJ6YFYiasg1v7+ia65UZ/JIf7b 20 | dpeZrc+lMhBGlDqHLp8mX929bfWSkcNEMLd2Cr4OY2N4MOJr4Hgi9M9aEz5shoLr 21 | AJvu2dSw5jxSMhmo+OlA5wFtVq/Jw03Dgyc821G6TDMFbXA48cglXKyPLwKBgGbd 22 | KllZlaTJjJjmQ1jGkYHCuZ1gb+KpBM3F0vsKAVNfgVgEtmCZNl9WIk827QIJh8Zi 23 | JQboyiDWuUtbaWF1hmVd5fYpr3sVQVG9XliXA35w81qJVeYM01tYOY+nksho3bam 24 | Mhl9/l/NDh3fYftqdsqPXlYGyevVc9gXmfoEfK2BAoGBAKxS7yeVQu8+Xjpq4ZMr 25 | Ep/Un+w8KQhCwdmw+GaPZMuOer8S4HF5bSFr9ZcDtVgHrYCMZh/VDvFsTTcoxUpY 26 | proLaMy0kXoPVKIZtGGw29196IRnElxO2ex2kNWT07M8YBOJMJcjeGDvPkKJ9PiL 27 | yOARJoTKYbw1H/ZsSsItd3uf 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /mosquitto/certs/client.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swift-server-community/mqtt-nio/493ca8bae5b3e93f9246a1a182a87efe25b5e6ba/mosquitto/certs/client.p12 -------------------------------------------------------------------------------- /mosquitto/certs/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDUjCCAjoCCQDAutBIw97yxjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJV 3 | SzESMBAGA1UECAwJRWRpbmJ1cmdoMRIwEAYDVQQHDAlFZGluYnVyZ2gxEDAOBgNV 4 | BAoMB01RVFROSU8xCzAJBgNVBAsMAkNBMRMwEQYDVQQDDApzb3RvLmNvZGVzMB4X 5 | DTI0MDkxODIwMTcwMFoXDTI1MDkxODIwMTcwMFowbTELMAkGA1UEBhMCVUsxEjAQ 6 | BgNVBAgMCUVkaW5idXJnaDESMBAGA1UEBwwJRWRpbmJ1cmdoMRAwDgYDVQQKDAdN 7 | UVRUTklPMQ8wDQYDVQQLDAZDbGllbnQxEzARBgNVBAMMCnNvdG8uY29kZXMwggEi 8 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk4gTilQfyqF4a1HQ3sVAs+xzu 9 | Z9azn/sWrliacyhqo23tzmg+ArFxbDI2DRptV8p/AZb8P2QhdgH0AZ+6RFeQksF+ 10 | kXKU3ChO5TKFFZvXxNET6mc4oGysqjAmA2y2vmBbRflibwkfxCr4Bmf0iEC0xMxn 11 | x8ltJxFxLpLLEuu1VyZngnEWtNFxLoygUGM/L55FE0Bx8E5ThUAuYk7vR0dp58zo 12 | pT5yH/ExH5rOd7VinYUVc9XkyQISpQV18Hx2EC04FJQr/kKapfQ9wItgVwMqeJih 13 | czsyTZ5zbG8h21bOLa7T7eF8ZrXaIG3rvKnBuTzl9zM5TCnjY0V0MAPkB09HAgMB 14 | AAEwDQYJKoZIhvcNAQELBQADggEBAKs01ETIh77ejB5zWQIoiQp6LGHpyXpMgBg3 15 | fMKMHOkxDS7elfuw7H8hQ+TUZlWEkMmSCK2BrIZlMou7Ut3z46hIn9xj2CUfmL+c 16 | GzsPfGYQXrAWGFNhdGYShI1Fz/GhgHRizOhEQhBbhq6xcP8LVylVwZRDj9aHb1Yh 17 | O/nk3cPBj9K7by+SCKNSxnyUyh5jPH/Rk2SH3E9Y5zczaaWn92w5DpBWQTljhIw+ 18 | ld16KyUTWvMXJRfiwF0AosHMp3FNt4OjuE/gbVIjBcuIOMqCpdk18WvJp7fuZvzZ 19 | PHMsjIpuriAg/l2K9rXdFV5wZXBkrGmNgmBOid/YrRaCCvwmQdM= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /mosquitto/certs/mosquitto.org.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL 3 | BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG 4 | A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU 5 | BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv 6 | by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE 7 | BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES 8 | MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp 9 | dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ 10 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg 11 | UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW 12 | Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA 13 | s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH 14 | 3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo 15 | E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT 16 | MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV 17 | 6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL 18 | BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC 19 | 6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf 20 | +pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK 21 | sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839 22 | LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE 23 | m/XriWr/Cq4h/JfB7NTsezVslgkBaoU= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /mosquitto/certs/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC2jCCAcICAQAwbTELMAkGA1UEBhMCVUsxEjAQBgNVBAgMCUVkaW5idXJnaDES 3 | MBAGA1UEBwwJRWRpbmJ1cmdoMRAwDgYDVQQKDAdNUVRUTklPMQ8wDQYDVQQLDAZT 4 | ZXJ2ZXIxEzARBgNVBAMMCnNvdG8uY29kZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IB 5 | DwAwggEKAoIBAQDNCWgtm3sJjtIEFSw2GynEWZAPIftuQf77Eg37aI8ogOgqDc0t 6 | aK1iDFSQMwrCquTYQaT4+aJ+12EaPTbBy0xxw9Z5gOYXNK8ONCHB5sNXGbWbn6I8 7 | 5fonmRXifbssMx7iSWAwlyfWA5T/ug+/2S3csP1EG/ROlahoTbMF1Zj0vcPzpCXq 8 | fM5Yp9h+eMIJ7PeIogdIDaBKFy6Ko1lHXcjl11wbTG9jNDqb0Vn71O6Er4Y91nDr 9 | GU9j5DxsbUQl95hZxXZcYEQhapIsXPxseDIeSML5HLsFsgWmzyuhyKOYwPLqnFX3 10 | 4hllIqFYcjSv8I5DqQ8Fn+abkQwNdrTOzXprAgMBAAGgKDAmBgkqhkiG9w0BCQ4x 11 | GTAXMBUGA1UdEQQOMAyCCnNvdG8uY29kZXMwDQYJKoZIhvcNAQELBQADggEBAFW2 12 | bGyhAPxVzb1zzb0f8LkUOyiEUAFC2BA++mCFXmidfMnc7u30yvWgxBOSZ9NyYIde 13 | smB0IG+jw8a3vjZhAx5WN36kzMheTnxzXLpAkXpi7onZgBRxdFCwJm1NP21N1AnV 14 | Y6CHcBfO1jUs7ESccZGuUZJmQTCcs6802lgOT35T6acGPeF70NFpnnZwBLXWMhhj 15 | z83StCgZonOG4niCDWVSbN/MwWGszNfmXABhHkEkU/UBwDziF8gsl6jGbcJz7cju 16 | CWVWWAsH6BzEhfy3Y2p+KMKi37nmFELC8R0ivsDvIrBRpj7n5ALQjFHIp+DYjlSw 17 | WOvc/qansezZw5g6+I0= 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /mosquitto/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNCWgtm3sJjtIE 3 | FSw2GynEWZAPIftuQf77Eg37aI8ogOgqDc0taK1iDFSQMwrCquTYQaT4+aJ+12Ea 4 | PTbBy0xxw9Z5gOYXNK8ONCHB5sNXGbWbn6I85fonmRXifbssMx7iSWAwlyfWA5T/ 5 | ug+/2S3csP1EG/ROlahoTbMF1Zj0vcPzpCXqfM5Yp9h+eMIJ7PeIogdIDaBKFy6K 6 | o1lHXcjl11wbTG9jNDqb0Vn71O6Er4Y91nDrGU9j5DxsbUQl95hZxXZcYEQhapIs 7 | XPxseDIeSML5HLsFsgWmzyuhyKOYwPLqnFX34hllIqFYcjSv8I5DqQ8Fn+abkQwN 8 | drTOzXprAgMBAAECggEAfefs5TS41SGyrXci8wazGzO0VtaTQx2bqiloFJ4cas7d 9 | whU/jUbeUXso4nO1g9zVMkb9OzZwJluz8Rzt5wskIigUKACTSmS7qokwwZUnFvFe 10 | p/Xa9nJyrqY+3ho/OeEacfKE8tGfULhaYr6qtTB0DTVSEOTpnOghxgsQh+CmUIKt 11 | bXwXQj7ziUm0mH7V+ndKJhE/nysT9agYRLY2huJq7MRCDucKndc2D9sKy5YaDDmh 12 | glnuQutW29MPn615l5W/Q3CL0NiD7fsX8rwmDXd0/fgAfXQ7D0rYOEtPuSMkFPKP 13 | kHTz0TAfH1DQMlD1rnKUCcxUmiPW/R9Iq2cwWULcIQKBgQDxvXr2mFoGt1kooayJ 14 | U/wwkYQjd6EeSYn/MlPdN0loIz/SlsK1erPBskiMBWMken7sbjNHfbhwVlU64pas 15 | wDytE3Psc+mpJzICE12fcJyjJVzLfz90p0jUhDHhPQA3H7a5j3KinPQr0+sIFMx/ 16 | z0IyUBkRilEXe0BilZREMZmdpwKBgQDZIasM4RITodV0vYEXwF1lp+Kxz//juxzB 17 | 8+O7FSMZV4ACGxz9sAC289uiyqX3URTA0WwLZaIMrkf3uYpMwtY6hxOi4s/87Imx 18 | rFJ8f+gPykzyU7FNkpyK6O8e9OJLFrRYOhTVihQ4bez45CvM88NV158qWb5NC95T 19 | wM9opaE9nQKBgQCeOBQY/hI+Pxad32Nb5poy96ryw8OyXRNy8e+t5Bepjxigrof1 20 | 2793UUbmTkhbgck82cu6SPDEpdzW06MmohOUfBztb9hJHBxA+4fVaRE8PqIDlt9j 21 | bHHglj1HXHOdoKYpwVeYUv4FCYjVGzfVl0OORpqBvnPg2IyFeb02/Pe8FQKBgA/H 22 | xaGy/dhVa6kHWMl8Ho2TzQL1RfisEaP68LMZDyr5VAFTLSE22GZzhKPpLHSz/Nki 23 | n0KYyVU4mVxkrKt1gZJRXNj6uPj9y+gQyRHpTdlP75WxBXLI0/24fiB21bd1V/gN 24 | iJQYa+3J924DTzefA7RKbnPqf80jrq3RloFZgEV1AoGBAOMFNr1uuJ9UPUFteuuC 25 | 88rPvt6JJjXR75LveEBOhF+WAcIlBU4IYWuwuHPpiM7vMvY3O05U8XCLfreqJveE 26 | HB3E45p87eXTc9A+tYQH4+pRQZPXvtJ8pvDmuxiQOOwnqCtNMdPmuBjgh9iTSirq 27 | LPriQ244KoAoP9GFinWZSy05 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /mosquitto/certs/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDnjCCAoagAwIBAgIJAMC60EjD3vLFMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV 3 | BAYTAlVLMRIwEAYDVQQIDAlFZGluYnVyZ2gxEjAQBgNVBAcMCUVkaW5idXJnaDEQ 4 | MA4GA1UECgwHTVFUVE5JTzELMAkGA1UECwwCQ0ExEzARBgNVBAMMCnNvdG8uY29k 5 | ZXMwHhcNMjQwOTE4MjAxNjU5WhcNMjUwOTE4MjAxNjU5WjBtMQswCQYDVQQGEwJV 6 | SzESMBAGA1UECAwJRWRpbmJ1cmdoMRIwEAYDVQQHDAlFZGluYnVyZ2gxEDAOBgNV 7 | BAoMB01RVFROSU8xDzANBgNVBAsMBlNlcnZlcjETMBEGA1UEAwwKc290by5jb2Rl 8 | czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM0JaC2bewmO0gQVLDYb 9 | KcRZkA8h+25B/vsSDftojyiA6CoNzS1orWIMVJAzCsKq5NhBpPj5on7XYRo9NsHL 10 | THHD1nmA5hc0rw40IcHmw1cZtZufojzl+ieZFeJ9uywzHuJJYDCXJ9YDlP+6D7/Z 11 | Ldyw/UQb9E6VqGhNswXVmPS9w/OkJep8zlin2H54wgns94iiB0gNoEoXLoqjWUdd 12 | yOXXXBtMb2M0OpvRWfvU7oSvhj3WcOsZT2PkPGxtRCX3mFnFdlxgRCFqkixc/Gx4 13 | Mh5IwvkcuwWyBabPK6HIo5jA8uqcVffiGWUioVhyNK/wjkOpDwWf5puRDA12tM7N 14 | emsCAwEAAaNFMEMwCwYDVR0PBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr 15 | BgEFBQcDAjAVBgNVHREEDjAMggpzb3RvLmNvZGVzMA0GCSqGSIb3DQEBCwUAA4IB 16 | AQBfvpGcHGa7wHy2jA/NDGNhiPWHFLFQIS6PmuvmukooBOEP+SbpG7jwoRMrDQxb 17 | tFZQoPzHTpuXv6eMRd2jQzVDoPHjAN4ykuIZ/IPY+qBI6q9YsMctzyJzgCawim0P 18 | AdqzMjlHTe/a5Q6oEZU2PejDPVo+lcvKe4EDmvP7pE7O+tAtArgdOaC1DSLnUyW/ 19 | qU8GjUWkmgKDDbk5xtF47lNGKjLRxrwV7dI1hrtjMY4p+xCfAU+c30AsDx98jqNz 20 | SKk4DEhs5X1itnPvBS+KqcJvrWeqSLBgCnpcu7VaEazPwvUxvfdPmwlaSIr90c9E 21 | fIZ5adVZM5dudAxXSLS4LzJg 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /mosquitto/config/mosquitto.conf: -------------------------------------------------------------------------------- 1 | # Setup 2 | per_listener_settings true 3 | allow_zero_length_clientid true 4 | 5 | log_timestamp_format %H:%M:%S 6 | log_type all 7 | 8 | # Plain 9 | listener 1883 10 | protocol mqtt 11 | allow_anonymous true 12 | 13 | # Plain with password 14 | listener 1884 15 | protocol mqtt 16 | password_file ./mosquitto/config/passwd 17 | 18 | # SSL with client certificate 19 | listener 8883 20 | protocol mqtt 21 | allow_anonymous true 22 | cafile ./mosquitto/certs/ca.pem 23 | certfile ./mosquitto/certs/server.pem 24 | keyfile ./mosquitto/certs/server.key 25 | require_certificate true 26 | 27 | # WebSockets, no SSL 28 | listener 8080 29 | protocol websockets 30 | allow_anonymous true 31 | 32 | # WebSockets with SSL 33 | listener 8081 34 | protocol websockets 35 | allow_anonymous true 36 | cafile ./mosquitto/certs/ca.pem 37 | certfile ./mosquitto/certs/server.pem 38 | keyfile ./mosquitto/certs/server.key 39 | 40 | # Unix Domain Socket 41 | listener 0 ./mosquitto/socket/mosquitto.sock 42 | protocol mqtt 43 | allow_anonymous true 44 | -------------------------------------------------------------------------------- /mosquitto/config/passwd: -------------------------------------------------------------------------------- 1 | mqttnio:$7$101$V3sQoki84eYhwOzK$UqUnHA35ZpzW2s6XPANQxOWT05gS3+Er9h9uK1G7czRjQ6UpQN5yXDSU/UKyvlYMczDv3Yuc7AcrhEYoidT9bw== 2 | -------------------------------------------------------------------------------- /mosquitto/socket/.gitkeep: -------------------------------------------------------------------------------- 1 | A local mosquitto server using `mosquitto/config/mosquitto.conf` will create a `mosquitto.sock` socket in this directory. 2 | 3 | If using the `docker-compose.yml` container environment, a shared container volume will be mounted here. 4 | 5 | This allows tests that connect to mosquitto via unix domain socket to assume the socket(s) will be found in this directory and work from multiple environments. 6 | 7 | Do not remove this directory. 8 | -------------------------------------------------------------------------------- /scripts/build-docc-gh.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # GitHub action version of build-docc which does not use the plugin. While GH actions 3 | # are not available in the macOS-latest image GitHub provide this script will be necessary 4 | TEMP_DIR="$(pwd)/temp" 5 | 6 | cleanup() 7 | { 8 | if [ -n "$TEMP_DIR" ]; then 9 | rm -rf $TEMP_DIR 10 | fi 11 | } 12 | trap cleanup exit $? 13 | 14 | SG_FOLDER=.build/symbol-graphs 15 | MQTTNIO_SG_FOLDER=.build/mqtt-nio-symbol-graphs 16 | OUTPUT_PATH=docs/mqtt-nio/ 17 | 18 | BUILD_SYMBOLS=1 19 | 20 | while getopts 's' option 21 | do 22 | case $option in 23 | s) BUILD_SYMBOLS=0;; 24 | esac 25 | done 26 | 27 | if [ -z "${DOCC_HTML_DIR:-}" ]; then 28 | git clone https://github.com/apple/swift-docc-render-artifact $TEMP_DIR/swift-docc-render-artifact 29 | export DOCC_HTML_DIR="$TEMP_DIR/swift-docc-render-artifact/dist" 30 | fi 31 | 32 | if test "$BUILD_SYMBOLS" == 1; then 33 | # build symbol graphs 34 | mkdir -p $SG_FOLDER 35 | swift build \ 36 | -Xswiftc -emit-symbol-graph \ 37 | -Xswiftc -emit-symbol-graph-dir -Xswiftc $SG_FOLDER 38 | # Copy MQTTNIO symbol graph into separate folder 39 | mkdir -p $MQTTNIO_SG_FOLDER 40 | cp $SG_FOLDER/MQTTNIO* $MQTTNIO_SG_FOLDER 41 | fi 42 | 43 | # Build documentation 44 | mkdir -p $OUTPUT_PATH 45 | rm -rf $OUTPUT_PATH/* 46 | docc convert Sources/MQTTNIO/MQTTNIO.docc \ 47 | --transform-for-static-hosting \ 48 | --hosting-base-path /mqtt-nio \ 49 | --fallback-display-name MQTTNIO \ 50 | --fallback-bundle-identifier org.swift-server-community.mqtt-nio \ 51 | --fallback-bundle-version 1 \ 52 | --additional-symbol-graph-dir $MQTTNIO_SG_FOLDER \ 53 | --output-path $OUTPUT_PATH -------------------------------------------------------------------------------- /scripts/build-docc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # if plugin worked for us then would use it over everything below 4 | usePlugin() 5 | { 6 | mkdir -p ./docs/mqtt-nio 7 | swift package \ 8 | --allow-writing-to-directory ./docs \ 9 | generate-documentation \ 10 | --target MQTTNIO \ 11 | --output-path ./docs/mqtt-nio \ 12 | --transform-for-static-hosting \ 13 | --hosting-base-path /mqtt-nio 14 | } 15 | 16 | usePlugin 17 | -------------------------------------------------------------------------------- /scripts/commit-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | # stash everything that isn't in docs, store result in STASH_RESULT 6 | STASH_RESULT=$(git stash -- ":(exclude)docs") 7 | # get branch name 8 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 9 | REVISION_HASH=$(git rev-parse HEAD) 10 | 11 | rm -rf _docs 12 | mv docs _docs 13 | git checkout gh-pages 14 | # copy contents of docs to docs/current replacing the ones that are already there 15 | rm -rf docs 16 | mv _docs/mqtt-nio docs 17 | # commit 18 | git add --all docs 19 | 20 | git status 21 | git commit -m "Documentation for https://github.com/adam-fowler/mqtt-nio/tree/$REVISION_HASH" 22 | git push 23 | # return to branch 24 | git checkout $CURRENT_BRANCH 25 | 26 | if [ "$STASH_RESULT" != "No local changes to save" ]; then 27 | git stash pop 28 | fi 29 | 30 | -------------------------------------------------------------------------------- /scripts/generate-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | HOME=$(dirname "$0") 6 | FULL_HOME="$(pwd)"/"$HOME" 7 | SERVER=soto.codes 8 | 9 | function generateCA() { 10 | SUBJECT=$1 11 | openssl req \ 12 | -nodes \ 13 | -x509 \ 14 | -sha256 \ 15 | -newkey rsa:2048 \ 16 | -subj "$SUBJECT" \ 17 | -days 1825 \ 18 | -keyout ca.key \ 19 | -out ca.pem 20 | openssl x509 -in ca.pem -out ca.der -outform DER 21 | } 22 | 23 | function generateServerCertificate() { 24 | SUBJECT=$1 25 | NAME=$2 26 | openssl req \ 27 | -new \ 28 | -nodes \ 29 | -sha256 \ 30 | -subj "$SUBJECT" \ 31 | -extensions v3_req \ 32 | -reqexts SAN \ 33 | -config <(cat "$FULL_HOME"/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:$SERVER\n")) \ 34 | -keyout "$NAME".key \ 35 | -out "$NAME".csr 36 | 37 | openssl x509 \ 38 | -req \ 39 | -sha256 \ 40 | -in "$NAME".csr \ 41 | -CA ca.pem \ 42 | -CAkey ca.key \ 43 | -CAcreateserial \ 44 | -extfile <(cat "$FULL_HOME"/openssl.cnf <(printf "subjectAltName=DNS:$SERVER\n")) \ 45 | -extensions v3_req \ 46 | -out "$NAME".pem \ 47 | -days 1825 48 | } 49 | 50 | function generateClientCertificate() { 51 | SUBJECT=$1 52 | NAME=$2 53 | #PASSWORD=$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-25) 54 | PASSWORD="MQTTNIOClientCertPassword" 55 | openssl req \ 56 | -new \ 57 | -nodes \ 58 | -sha256 \ 59 | -subj "$SUBJECT" \ 60 | -keyout "$NAME".key \ 61 | -out "$NAME".csr 62 | 63 | openssl x509 \ 64 | -req \ 65 | -sha256 \ 66 | -in "$NAME".csr \ 67 | -CA ca.pem \ 68 | -CAkey ca.key \ 69 | -CAcreateserial \ 70 | -out "$NAME".pem \ 71 | -days 1825 72 | 73 | openssl pkcs12 -export -passout pass:"$PASSWORD" -out "$NAME".p12 -in "$NAME".pem -inkey "$NAME".key 74 | 75 | echo "Password: $PASSWORD" 76 | } 77 | 78 | cd "$HOME"/../mosquitto/certs/ 79 | 80 | OUTPUT_ROOT=1 81 | OUTPUT_CLIENT=1 82 | OUTPUT_SERVER=1 83 | 84 | while getopts 'sc' option 85 | do 86 | case $option in 87 | s) OUTPUT_ROOT=0;OUTPUT_SERVER=1;OUTPUT_CLIENT=0 ;; 88 | c) OUTPUT_ROOT=0;OUTPUT_SERVER=0;OUTPUT_CLIENT=1 ;; 89 | esac 90 | done 91 | 92 | if test "$OUTPUT_ROOT" == 1; then 93 | generateCA "/C=UK/ST=Edinburgh/L=Edinburgh/O=MQTTNIO/OU=CA/CN=${SERVER}" 94 | fi 95 | if test "$OUTPUT_SERVER" == 1; then 96 | generateServerCertificate "/C=UK/ST=Edinburgh/L=Edinburgh/O=MQTTNIO/OU=Server/CN=${SERVER}" server 97 | fi 98 | if test "$OUTPUT_CLIENT" == 1; then 99 | generateClientCertificate "/C=UK/ST=Edinburgh/L=Edinburgh/O=MQTTNIO/OU=Client/CN=${SERVER}" client 100 | fi 101 | -------------------------------------------------------------------------------- /scripts/generate_contributors_list.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftNIO open source project 5 | ## 6 | ## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of SwiftNIO project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | 16 | set -eu 17 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 18 | contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) 19 | 20 | cat > "$here/../CONTRIBUTORS.txt" <<- EOF 21 | For the purpose of tracking copyright, this is the list of individuals and 22 | organizations who have contributed source code to Soto. 23 | 24 | For employees of an organization/company where the copyright of work done 25 | by employees of that company is held by the company itself, only the company 26 | needs to be listed here. 27 | 28 | ## COPYRIGHT HOLDERS 29 | 30 | ### Contributors 31 | 32 | $contributors 33 | 34 | **Updating this list** 35 | 36 | Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` 37 | EOF 38 | -------------------------------------------------------------------------------- /scripts/mosquitto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run mosquitto MQTT broker with configuration needed for the test suite. 4 | # 5 | # If mosquitto is installed locally, it will be run. Otherwise, fall back to 6 | # running a containerized version of mosquitto. The --container/-C option may be 7 | # supplied to force running a containerized mosquitto. 8 | # 9 | # N.B. On MacOS, a native mosquitto executable must be run to connect to 10 | # mosquitto using a unix domain socket. 11 | 12 | set -eu -o pipefail 13 | 14 | usage() 15 | { 16 | echo "Usage: mosquitto.sh [--container|-C]" 17 | exit 2 18 | } 19 | 20 | run-installed-mosquitto() 21 | { 22 | mosquitto -c mosquitto/config/mosquitto.conf 23 | } 24 | 25 | run-containerized-mosquitto() 26 | { 27 | docker run \ 28 | -p 1883:1883 \ 29 | -p 1884:1884 \ 30 | -p 8883:8883 \ 31 | -p 8080:8080 \ 32 | -p 8081:8081 \ 33 | -v "$(pwd)"/mosquitto/config:/mosquitto/config \ 34 | -v "$(pwd)"/mosquitto/certs:/mosquitto/certs \ 35 | -v "$(pwd)"/mosquitto/socket:/mosquitto/socket \ 36 | eclipse-mosquitto 37 | } 38 | 39 | USE_CONTAINER=0 40 | 41 | if [[ $# -gt 1 ]]; then 42 | usage 43 | elif [[ $# -eq 1 ]]; then 44 | case "$1" in 45 | -C|--container) USE_CONTAINER=1 ;; 46 | *) usage ;; 47 | esac 48 | fi 49 | 50 | cd "$(dirname "$(dirname "$0")")" 51 | 52 | if [[ $USE_CONTAINER -eq 1 ]]; then 53 | if [ "$(uname)" != "Linux" ]; then 54 | echo "warning: unix domain socket connections will not work with a mosquitto container on $(uname)" 55 | fi 56 | run-containerized-mosquitto 57 | elif command -v mosquitto >/dev/null; then 58 | run-installed-mosquitto 59 | elif [ "$(uname)" = "Linux" ]; then 60 | echo "notice: mosquitto not installed; running eclipse-mosquitto container instead..." 61 | run-containerized-mosquitto 62 | else 63 | echo "error: mosquitto must be installed" 64 | if [ "$(uname)" = "Darwin" ]; then 65 | echo "mosquitto can be installed on MacOS with: brew install mosquitto" 66 | fi 67 | exit 1 68 | fi 69 | -------------------------------------------------------------------------------- /scripts/openssl.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | #default_bits = 2048 3 | #default_md = sha256 4 | #default_keyfile = privkey.pem 5 | distinguished_name = req_distinguished_name 6 | attributes = req_attributes 7 | req_extensions = v3_req 8 | 9 | [ req_distinguished_name ] 10 | countryName = Country Name (2 letter code) 11 | countryName_min = 2 12 | countryName_max = 2 13 | stateOrProvinceName = State or Province Name (full name) 14 | localityName = Locality Name (eg, city) 15 | 0.organizationName = Organization Name (eg, company) 16 | organizationalUnitName = Organizational Unit Name (eg, section) 17 | commonName = Common Name (eg, fully qualified host name) 18 | commonName_max = 64 19 | emailAddress = Email Address 20 | emailAddress_max = 64 21 | 22 | [ req_attributes ] 23 | challengePassword = A challenge password 24 | challengePassword_min = 4 25 | challengePassword_max = 20 26 | 27 | [ v3_req ] 28 | keyUsage = digitalSignature, keyEncipherment 29 | extendedKeyUsage = serverAuth, clientAuth 30 | -------------------------------------------------------------------------------- /scripts/validate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftNIO open source project 5 | ## 6 | ## Copyright (c) 2017-2019 Apple Inc. and the SwiftNIO project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of SwiftNIO project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | 16 | set -eu 17 | 18 | printf "=> Checking format... " 19 | FIRST_OUT="$(git status --porcelain)" 20 | git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place 21 | git diff --exit-code '*.swift' 22 | 23 | SECOND_OUT="$(git status --porcelain)" 24 | if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then 25 | printf "\033[0;31mformatting issues!\033[0m\n" 26 | git --no-pager diff 27 | exit 1 28 | else 29 | printf "\033[0;32mokay.\033[0m\n" 30 | fi 31 | --------------------------------------------------------------------------------