├── .cargo └── config.toml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── release.yml ├── release.yml └── workflows │ ├── check-labels.yml │ ├── ci.yml │ ├── release.yml │ └── update-release-project.yml ├── .gitignore ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── DEFAULT_CONFIG.json5 ├── LICENSE ├── NOTICE.md ├── README.md ├── rust-toolchain.toml ├── zenoh-bridge-mqtt ├── .deb │ ├── postinst │ └── postrm ├── .service │ └── zenoh-bridge-mqtt.service ├── Cargo.toml └── src │ └── main.rs └── zenoh-plugin-mqtt ├── Cargo.toml ├── README.md ├── build.rs ├── src ├── config.rs ├── lib.rs ├── mqtt_helpers.rs └── mqtt_session_state.rs └── tests └── test.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-musl] 2 | rustflags = "-Ctarget-feature=-crt-static" 3 | 4 | [target.aarch64-unknown-linux-musl] 5 | rustflags = "-Ctarget-feature=-crt-static" 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Report a bug 2 | description: | 3 | Create a bug report to help us improve Zenoh. 4 | title: "[Bug] " 5 | labels: ["bug"] 6 | body: 7 | - type: textarea 8 | id: summary 9 | attributes: 10 | label: "Describe the bug" 11 | description: | 12 | A clear and concise description of the expected behaviour and what the bug is. 13 | placeholder: | 14 | E.g. zenoh peers can not automatically establish a connection. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: reproduce 19 | attributes: 20 | label: To reproduce 21 | description: "Steps to reproduce the behavior:" 22 | placeholder: | 23 | 1. Start a subscriber "..." 24 | 2. Start a publisher "...." 25 | 3. See error 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: system 30 | attributes: 31 | label: System info 32 | description: "Please complete the following information:" 33 | placeholder: | 34 | - Platform: [e.g. Ubuntu 20.04 64-bit] 35 | - CPU [e.g. AMD Ryzen 3800X] 36 | - Zenoh version/commit [e.g. 6f172ea985d42d20d423a192a2d0d46bb0ce0d11] 37 | validations: 38 | required: true 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/eclipse-zenoh/roadmap/discussions/categories/zenoh 5 | about: Open to the Zenoh community. Share your feedback with the Zenoh team. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Request a feature 2 | description: | 3 | Suggest a new feature specific to this repository. NOTE: for generic Zenoh ideas use "Ask a question". 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Guidelines for a good issue** 9 | 10 | *Is your feature request related to a problem?* 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | *Describe the solution you'd like* 14 | A clear and concise description of what you want to happen. 15 | 16 | *Describe alternatives you've considered* 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | *Additional context* 20 | Add any other context about the feature request here. 21 | - type: textarea 22 | id: feature 23 | attributes: 24 | label: "Describe the feature" 25 | validations: 26 | required: true 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/release.yml: -------------------------------------------------------------------------------- 1 | name: Add an issue to the next release 2 | description: | 3 | Add an issue as part of next release. 4 | This will be added to the current release project. 5 | You must be a contributor to use this template. 6 | labels: ["release"] 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | **Guidelines for a good issue** 12 | 13 | *Is your release item related to a problem?* 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | *Describe the solution you'd like* 17 | A clear and concise description of what you want to happen. 18 | 19 | *Describe alternatives you've considered* 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | *Additional context* 23 | Add any other context about the release item request here. 24 | - type: textarea 25 | id: item 26 | attributes: 27 | label: "Describe the release item" 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2023 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | 15 | changelog: 16 | categories: 17 | - title: Breaking changes 💥 18 | labels: 19 | - breaking-change 20 | - title: New features 🎉 21 | labels: 22 | - enhancement 23 | - new feature 24 | exclude: 25 | labels: 26 | - internal 27 | - title: Bug fixes 🐞 28 | labels: 29 | - bug 30 | exclude: 31 | labels: 32 | - internal 33 | - title: Documentation 📝 34 | labels: 35 | - documentation 36 | exclude: 37 | labels: 38 | - internal 39 | - title: Dependencies 👷 40 | labels: 41 | - dependencies 42 | exclude: 43 | labels: 44 | - internal 45 | - title: Other changes 46 | labels: 47 | - "*" 48 | exclude: 49 | labels: 50 | - internal -------------------------------------------------------------------------------- /.github/workflows/check-labels.yml: -------------------------------------------------------------------------------- 1 | name: Check required labels 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize, reopened, labeled] 6 | branches: ["**"] 7 | 8 | jobs: 9 | check-labels: 10 | name: Check PR labels 11 | uses: eclipse-zenoh/ci/.github/workflows/check-labels.yml@main 12 | secrets: 13 | github-token: ${{ secrets.GITHUB_TOKEN }} 14 | permissions: 15 | pull-requests: write 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | name: CI 15 | 16 | on: 17 | push: 18 | branches: ["**"] 19 | pull_request: 20 | branches: ["**"] 21 | 22 | env: 23 | CARGO_TERM_COLOR: always 24 | 25 | jobs: 26 | build: 27 | name: Build on ${{ matrix.os }} 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: [ubuntu-latest, macOS-latest, windows-latest] 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - name: Install Rust toolchain 38 | run: | 39 | rustup show 40 | rustup component add rustfmt clippy 41 | 42 | - name: Code format check 43 | run: cargo fmt --check -- --config "unstable_features=true,imports_granularity=Crate,group_imports=StdExternalCrate" 44 | - name: Clippy 45 | run: cargo clippy --all-targets --examples -- -D warnings 46 | 47 | - name: Build zenoh-plugin-mqtt 48 | run: cargo build -p zenoh-plugin-mqtt --verbose --all-targets 49 | 50 | - name: Build zenoh-bridge-mqtt 51 | run: cargo build -p zenoh-bridge-mqtt --verbose --all-targets 52 | 53 | - name: Run tests 54 | run: cargo test --verbose 55 | 56 | markdown_lint: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: DavidAnson/markdownlint-cli2-action@v18 61 | with: 62 | config: '.markdownlint.yaml' 63 | globs: '**/README.md' 64 | 65 | check_rust: 66 | name: Check ${{ github.repository }} using Rust 1.75 67 | runs-on: ubuntu-latest 68 | strategy: 69 | fail-fast: false 70 | steps: 71 | - name: Clone this repository 72 | uses: actions/checkout@v4 73 | 74 | - name: Update Rust 1.75.0 toolchain 75 | run: rustup update 1.75.0 76 | 77 | - name: Setup rust-cache 78 | uses: Swatinem/rust-cache@v2 79 | with: 80 | cache-bin: false 81 | 82 | - name: Check ${{ github.repository }} with rust 1.75.0 83 | run: | 84 | cargo +1.75.0 check --release --bins --lib 85 | 86 | # NOTE: In GitHub repository settings, the "Require status checks to pass 87 | # before merging" branch protection rule ensures that commits are only merged 88 | # from branches where specific status checks have passed. These checks are 89 | # specified manually as a list of workflow job names. Thus we use this extra 90 | # job to signal whether all CI checks have passed. 91 | ci: 92 | name: CI status checks 93 | runs-on: ubuntu-latest 94 | needs: [check_rust, build, markdown_lint] 95 | if: always() 96 | steps: 97 | - name: Check whether all jobs pass 98 | run: echo '${{ toJson(needs) }}' | jq -e 'all(.result == "success")' 99 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | name: Release 15 | 16 | on: 17 | schedule: 18 | - cron: "0 0 * * 1-5" 19 | workflow_dispatch: 20 | inputs: 21 | live-run: 22 | type: boolean 23 | description: Live-run 24 | required: false 25 | version: 26 | type: string 27 | description: Release number 28 | required: false 29 | zenoh-version: 30 | type: string 31 | description: Release number of Zenoh 32 | required: false 33 | branch: 34 | type: string 35 | description: Release branch 36 | required: false 37 | 38 | jobs: 39 | tag: 40 | name: Branch, Bump & tag crates 41 | uses: eclipse-zenoh/ci/.github/workflows/branch-bump-tag-crates.yml@main 42 | with: 43 | repo: ${{ github.repository }} 44 | live-run: ${{ inputs.live-run || false }} 45 | version: ${{ inputs.version }} 46 | branch: ${{ inputs.branch }} 47 | bump-deps-version: ${{ inputs.zenoh-version }} 48 | bump-deps-pattern: 'zenoh.*' 49 | bump-deps-branch: ${{ inputs.zenoh-version && format('release/{0}', inputs.zenoh-version) || '' }} 50 | secrets: inherit 51 | 52 | build-debian: 53 | name: Build Debian packages 54 | needs: tag 55 | uses: eclipse-zenoh/ci/.github/workflows/build-crates-debian.yml@main 56 | with: 57 | repo: ${{ github.repository }} 58 | version: ${{ needs.tag.outputs.version }} 59 | branch: ${{ needs.tag.outputs.branch }} 60 | secrets: inherit 61 | 62 | build-standalone: 63 | name: Build executables and libraries 64 | needs: tag 65 | uses: eclipse-zenoh/ci/.github/workflows/build-crates-standalone.yml@main 66 | with: 67 | repo: ${{ github.repository }} 68 | version: ${{ needs.tag.outputs.version }} 69 | branch: ${{ needs.tag.outputs.branch }} 70 | artifact-patterns: | 71 | ^zenoh-bridge-mqtt(\.exe)?$ 72 | ^libzenoh_plugin_mqtt\.(dylib|so)$ 73 | ^zenoh_plugin_mqtt\.dll$ 74 | secrets: inherit 75 | 76 | cargo: 77 | needs: tag 78 | name: Publish Cargo crates 79 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-cargo.yml@main 80 | with: 81 | repo: ${{ github.repository }} 82 | live-run: ${{ inputs.live-run || false }} 83 | branch: ${{ needs.tag.outputs.branch }} 84 | # - In dry-run mode, we need to publish eclipse-zenoh/zenoh before this 85 | # repository, in which case the version of zenoh dependecies are left as 86 | # is and thus point to the main branch of eclipse-zenoh/zenoh. 87 | # - In live-run mode, we assume that eclipse-zenoh/zenoh is already 88 | # published as this workflow can't be responsible for publishing it 89 | unpublished-deps-patterns: ${{ !(inputs.live-run || false) && 'zenoh.*' || '' }} 90 | unpublished-deps-repos: ${{ !(inputs.live-run || false) && 'eclipse-zenoh/zenoh' || '' }} 91 | secrets: inherit 92 | 93 | debian: 94 | name: Publish Debian packages 95 | needs: [tag, build-debian] 96 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-debian.yml@main 97 | with: 98 | no-build: true 99 | live-run: ${{ inputs.live-run || false }} 100 | version: ${{ needs.tag.outputs.version }} 101 | repo: ${{ github.repository }} 102 | branch: ${{ needs.tag.outputs.branch }} 103 | installation-test: false 104 | secrets: inherit 105 | 106 | homebrew: 107 | name: Publish Homebrew formulae 108 | needs: [tag, build-standalone] 109 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-homebrew.yml@main 110 | with: 111 | no-build: true 112 | repo: ${{ github.repository }} 113 | live-run: ${{ inputs.live-run || false }} 114 | version: ${{ needs.tag.outputs.version }} 115 | branch: ${{ needs.tag.outputs.branch }} 116 | artifact-patterns: | 117 | ^zenoh-bridge-mqtt$ 118 | ^libzenoh_plugin_mqtt\.dylib$ 119 | formulae: | 120 | zenoh-bridge-mqtt 121 | zenoh-plugin-mqtt 122 | secrets: inherit 123 | 124 | eclipse: 125 | name: Publish artifacts to Eclipse downloads 126 | needs: [tag, build-standalone] 127 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-eclipse.yml@main 128 | with: 129 | no-build: true 130 | live-run: ${{ inputs.live-run || false }} 131 | version: ${{ needs.tag.outputs.version }} 132 | repo: ${{ github.repository }} 133 | branch: ${{ needs.tag.outputs.branch }} 134 | artifact-patterns: | 135 | ^zenoh-bridge-mqtt(\.exe)?$ 136 | ^libzenoh_plugin_mqtt\.(dylib|so)$ 137 | ^zenoh_plugin_mqtt\.dll$ 138 | name: zenoh-plugin-mqtt 139 | secrets: inherit 140 | 141 | github: 142 | name: Publish artifacts to GitHub Releases 143 | needs: [tag, build-standalone] 144 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-github.yml@main 145 | with: 146 | no-build: true 147 | live-run: ${{ inputs.live-run || false }} 148 | version: ${{ needs.tag.outputs.version }} 149 | repo: ${{ github.repository }} 150 | branch: ${{ needs.tag.outputs.branch }} 151 | artifact-patterns: | 152 | ^zenoh-bridge-mqtt(\.exe)?$ 153 | ^libzenoh_plugin_mqtt\.(dylib|so)$ 154 | ^zenoh_plugin_mqtt\.dll$ 155 | secrets: inherit 156 | 157 | dockerhub: 158 | name: Publish container image to DockerHub 159 | needs: [tag, build-standalone] 160 | uses: eclipse-zenoh/ci/.github/workflows/release-crates-dockerhub.yml@main 161 | with: 162 | no-build: true 163 | live-run: ${{ inputs.live-run || false }} 164 | version: ${{ needs.tag.outputs.version }} 165 | repo: ${{ github.repository }} 166 | branch: ${{ needs.tag.outputs.branch }} 167 | image: "eclipse/zenoh-bridge-mqtt" 168 | binary: zenoh-bridge-mqtt 169 | files: | 170 | zenoh-bridge-mqtt 171 | libzenoh_plugin_mqtt.so 172 | platforms: | 173 | linux/arm64 174 | linux/amd64 175 | licenses: EPL-2.0 OR Apache-2.0 176 | secrets: inherit 177 | -------------------------------------------------------------------------------- /.github/workflows/update-release-project.yml: -------------------------------------------------------------------------------- 1 | name: Update release project 2 | 3 | on: 4 | issues: 5 | types: [opened, edited, labeled] 6 | pull_request_target: 7 | types: [closed] 8 | branches: 9 | - main 10 | 11 | jobs: 12 | main: 13 | uses: eclipse-zenoh/zenoh/.github/workflows/update-release-project.yml@main 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # CLion project directory 9 | .idea 10 | 11 | # Emacs temps 12 | *~ 13 | 14 | # MacOS Related 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, # Line length limitation 3 | "MD033": false, # Enable Inline HTML 4 | "MD041": false, # Allow first line heading 5 | "MD045": false, # Allow Images have no alternate text 6 | } -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: fmt 5 | name: fmt 6 | entry: cargo fmt -- --config "unstable_features=true,imports_granularity=Crate,group_imports=StdExternalCrate" 7 | language: system 8 | types: [rust] 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Eclipse zenoh 2 | 3 | Thanks for your interest in this project. 4 | 5 | ## Project description 6 | 7 | Eclipse zenoh provides is a stack designed to 8 | 1. minimize network overhead, 9 | 2. support extremely constrained devices, 10 | 3. supports devices with low duty-cycle by allowing the negotiation of data exchange modes and schedules, 11 | 4. provide a rich set of abstraction for distributing, querying and storing data along the entire system, and 12 | 5. provide extremely low latency and high throughput. 13 | 14 | * https://projects.eclipse.org/projects/iot.zenoh 15 | 16 | ## Developer resources 17 | 18 | Information regarding source code management, builds, coding standards, and 19 | more. 20 | 21 | * https://projects.eclipse.org/projects/iot.zenoh/developer 22 | 23 | The project maintains the following source code repositories 24 | 25 | * https://github.com/eclipse-zenoh 26 | 27 | ## Eclipse Contributor Agreement 28 | 29 | Before your contribution can be accepted by the project team contributors must 30 | electronically sign the Eclipse Contributor Agreement (ECA). 31 | 32 | * http://www.eclipse.org/legal/ECA.php 33 | 34 | Commits that are provided by non-committers must have a Signed-off-by field in 35 | the footer indicating that the author is aware of the terms by which the 36 | contribution has been provided to the project. The non-committer must 37 | additionally have an Eclipse Foundation account and must have a signed Eclipse 38 | Contributor Agreement (ECA) on file. 39 | 40 | For more information, please see the Eclipse Committer Handbook: 41 | https://www.eclipse.org/projects/handbook/#resources-commit 42 | 43 | ## Contact 44 | 45 | Contact the project developers via the project's "dev" list. 46 | 47 | * https://accounts.eclipse.org/mailing-list/zenoh-dev 48 | 49 | Or via the Discord server. 50 | 51 | * https://discord.gg/vSDSpqnbkm 52 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors to Eclipse zenoh 2 | 3 | These are the contributors to Eclipse zenoh (the initial contributors and the contributors listed in the Git log). 4 | 5 | 6 | | GitHub username | Name | 7 | | --------------- | -----------------------------| 8 | | JEnoch | Julien Enoch (ZettaScale) | 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | [workspace] 15 | members = ["zenoh-bridge-mqtt", "zenoh-plugin-mqtt"] 16 | resolver = "2" 17 | 18 | [workspace.package] 19 | authors = ["Julien Enoch "] 20 | categories = ["network-programming"] 21 | edition = "2021" 22 | homepage = "http://zenoh.io" 23 | license = "EPL-2.0 OR Apache-2.0" 24 | repository = "https://github.com/eclipse-zenoh/zenoh-plugin-mqtt" 25 | version = "1.4.0" 26 | 27 | [workspace.dependencies] 28 | async-channel = "2.3.1" 29 | async-trait = "0.1.83" 30 | base64 = "0.22.1" 31 | clap = "3.2.23" 32 | derivative = "2.2.0" 33 | flume = "0.11" 34 | futures = "0.3.26" 35 | git-version = "0.3.5" 36 | hex = "0.4.3" 37 | lazy_static = "1.4.0" 38 | ntex = { version = "2.6.0", features = ["tokio", "rustls"] } 39 | ntex-mqtt = "3.1.0" 40 | ntex-tls = "2.2.0" 41 | regex = "1.7.1" 42 | rustc_version = "0.4" 43 | rustls = { version = "0.23.13", default-features = false, features = ["ring"] } 44 | rustls-pemfile = "2.0.0" 45 | rustls-pki-types = "1.1.0" 46 | secrecy = { version = "0.8.0", features = ["alloc", "serde"] } 47 | serde = "1.0.210" 48 | serde_json = "1.0.128" 49 | tokio = { version = "1.40.0", default-features = false } # Default features are disabled due to some crates' requirements 50 | tracing = "0.1" 51 | zenoh = { version = "1.4.0", features = [ 52 | "plugins", 53 | "unstable", 54 | "internal", 55 | "internal_config", 56 | 57 | ] , git = "https://github.com/eclipse-zenoh/zenoh.git" , branch = "main" } 58 | zenoh-config = { version = "1.4.0", default-features = false , git = "https://github.com/eclipse-zenoh/zenoh.git" , branch = "main" } 59 | zenoh-ext = { version = "1.4.0", features = [ 60 | "unstable", 61 | ] , git = "https://github.com/eclipse-zenoh/zenoh.git" , branch = "main" } 62 | zenoh-plugin-mqtt = { version = "1.4.0", path = "zenoh-plugin-mqtt/", default-features = false } 63 | zenoh-plugin-rest = { version = "1.4.0", default-features = false, features = [ 64 | "static_plugin", 65 | ] , git = "https://github.com/eclipse-zenoh/zenoh.git" , branch = "main" } 66 | zenoh-plugin-trait = { version = "1.4.0", default-features = false , git = "https://github.com/eclipse-zenoh/zenoh.git" , branch = "main" } 67 | 68 | 69 | [profile.release] 70 | codegen-units = 1 71 | debug = false 72 | lto = "fat" 73 | opt-level = 3 74 | panic = "abort" 75 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-musl] 2 | image = "jenoch/rust-cross:x86_64-unknown-linux-musl" 3 | 4 | [target.arm-unknown-linux-gnueabi] 5 | image = "jenoch/rust-cross:arm-unknown-linux-gnueabi" 6 | 7 | [target.arm-unknown-linux-gnueabihf] 8 | image = "jenoch/rust-cross:arm-unknown-linux-gnueabihf" 9 | 10 | [target.armv7-unknown-linux-gnueabihf] 11 | image = "jenoch/rust-cross:armv7-unknown-linux-gnueabihf" 12 | 13 | [target.aarch64-unknown-linux-gnu] 14 | image = "jenoch/rust-cross:aarch64-unknown-linux-gnu" 15 | 16 | [target.aarch64-unknown-linux-musl] 17 | image = "jenoch/rust-cross:aarch64-unknown-linux-musl" 18 | 19 | -------------------------------------------------------------------------------- /DEFAULT_CONFIG.json5: -------------------------------------------------------------------------------- 1 | //// 2 | //// This file presents the default configuration used by both the `zenoh-plugin-mqtt` plugin and the `zenoh-bridge-mqtt` standalone executable. 3 | //// The "mqtt" JSON5 object below can be used as such in the "plugins" part of a config file for the zenoh router (zenohd). 4 | //// 5 | { 6 | plugins: { 7 | //// 8 | //// MQTT related configuration 9 | //// All settings are optional and are unset by default - uncomment the ones you want to set 10 | //// 11 | mqtt: { 12 | //// 13 | //// port: The address to bind the MQTT server. Default: "0.0.0.0:1883". Accepted values:' 14 | //// - a port number ("0.0.0.0" will be used as IP to bind, meaning any interface of the host) 15 | //// - a string with format `:` (to bind the MQTT server to a specific interface). 16 | //// 17 | // port: "0.0.0.0:1883", 18 | 19 | //// 20 | //// scope: A string added as prefix to all routed MQTT topics when mapped to a zenoh resource. 21 | //// This should be used to avoid conflicts when several distinct MQTT systems using 22 | //// the same topics names are routed via zenoh. 23 | //// 24 | // scope: "home-1", 25 | 26 | //// 27 | //// allow: A regular expression matching the MQTT topic name that must be routed via zenoh. By default topics are allowed. 28 | //// If both '--allow' and '--deny' are set a topic will be allowed if it matches only the 'allow' expression. 29 | //// 30 | // allow: "zigbee2mqtt|home-1/room-2", 31 | 32 | //// 33 | //// deny: A regular expression matching the MQTT topic name that must not be routed via zenoh. By default no topics are denied. 34 | //// If both '--allow' and '--deny' are set a topic will be allowed if it matches only the 'allow' expression. 35 | //// 36 | // deny: "zigbee2mqtt|home-1/room-2", 37 | 38 | //// 39 | //// generalise_subs: A list of key expression to use for generalising subscriptions. 40 | //// 41 | // generalise_subs: ["SUB1", "SUB2"], 42 | 43 | //// 44 | //// generalise_subs: A list of key expression to use for generalising publications. 45 | //// 46 | // generalise_subs: ["PUB1", "PUB2"], 47 | 48 | //// 49 | //// tx_channel_size: Size of the MQTT transmit channel (default: 65536). 50 | //// The channel buffers messages from Zenoh until they can be sent to MQTT clients. 51 | //// If the channel capacity is reached new messages from Zenoh will be dropped until space becomes available. 52 | //// 53 | // tx_channel_size: 65536, 54 | 55 | //// 56 | //// TLS related configuration (MQTTS active only if this part is defined). 57 | //// 58 | // tls: { 59 | // //// 60 | // //// server_private_key: TLS private key provided as either a file or base 64 encoded string. 61 | // //// One of the values below must be provided. 62 | // //// 63 | // // server_private_key: "/path/to/private-key.pem", 64 | // // server_private_key_base64: "base64-private-key", 65 | // 66 | // //// 67 | // //// server_certificate: TLS public certificate provided as either a file or base 64 encoded string. 68 | // //// One of the values below must be provided. 69 | // //// 70 | // // server_certificate: "/path/to/certificate.pem", 71 | // // server_certificate_base64: "base64-certificate", 72 | // 73 | // //// 74 | // //// root_ca_certificate: Certificate of the certificate authority used to validate clients connecting to the MQTT server. 75 | // //// Provided as either a file or base 64 encoded string. 76 | // //// This setting is optional and enables mutual TLS (mTLS) support if provided. 77 | // //// 78 | // // root_ca_certificate: "/path/to/root-ca-certificate.pem", 79 | // // root_ca_certificate_base64: "base64-root-ca-certificate", 80 | // }, 81 | 82 | //// 83 | //// This plugin uses Tokio (https://tokio.rs/) for asynchronous programming. 84 | //// When running as a plugin within a Zenoh router, the plugin creates its own Runtime managing 2 pools of threads: 85 | //// - worker threads for non-blocking tasks. Those threads are spawn at Runtime creation. 86 | //// - blocking threads for blocking tasks (e.g. I/O). Those threads are spawn when needed. 87 | //// For more details see https://github.com/tokio-rs/tokio/discussions/3858#discussioncomment-869878 88 | //// When running as a standalone bridge the Zenoh Session's Runtime is used and can be configured via the 89 | //// `ZENOH_RUNTIME` environment variable. See https://docs.rs/zenoh-runtime/latest/zenoh_runtime/enum.ZRuntime.html 90 | //// 91 | 92 | //// work_thread_num: The number of worker thread in the asynchronous runtime will use. (default: 2) 93 | //// Only for a plugin, no effect on a bridge. 94 | // work_thread_num: 2, 95 | 96 | //// max_block_thread_num: The number of blocking thread in the asynchronous runtime will use. (default: 50) 97 | //// Only for a plugin, no effect on a bridge. 98 | // max_block_thread_num: 50, 99 | 100 | //// 101 | //// MQTT client authentication related configuration. 102 | //// 103 | // auth: { 104 | // //// 105 | // //// dictionary_file: Path to a file containing the MQTT client username/password dictionary. 106 | // //// 107 | // dictionary_file: "/path/to/dictionary-file", 108 | // }, 109 | 110 | }, 111 | 112 | //// 113 | //// REST API configuration (active only if this part is defined) 114 | //// 115 | // rest: { 116 | // //// 117 | // //// The HTTP port number (for all network interfaces). 118 | // //// You can bind on a specific interface setting a ":" string. 119 | // //// 120 | // http_port: 8000, 121 | // }, 122 | }, 123 | 124 | //// 125 | //// zenoh related configuration (see zenoh documentation for more details) 126 | //// 127 | 128 | //// 129 | //// id: The identifier (as hex-string) that zenoh-bridge-mqtt must use. If not set, a random UUIDv4 will be used. 130 | //// WARNING: this id must be unique in your zenoh network. 131 | // id: "A00001", 132 | 133 | //// 134 | //// mode: The bridge's mode (peer or client) 135 | //// 136 | //mode: "client", 137 | 138 | //// 139 | //// Which endpoints to connect to. E.g. tcp/localhost:7447. 140 | //// By configuring the endpoints, it is possible to tell zenoh which router/peer to connect to at startup. 141 | //// 142 | connect: { 143 | endpoints: [ 144 | // "/:" 145 | ] 146 | }, 147 | 148 | //// 149 | //// Which endpoints to listen on. E.g. tcp/localhost:7447. 150 | //// By configuring the endpoints, it is possible to tell zenoh which are the endpoints that other routers, 151 | //// peers, or client can use to establish a zenoh session. 152 | //// 153 | listen: { 154 | endpoints: [ 155 | // "/:" 156 | ] 157 | }, 158 | } 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | apache-2.0 2 | epl-2.0 3 | 4 | 5 | Apache License 6 | Version 2.0, January 2004 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, 14 | and distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by 17 | the copyright owner that is granting the License. 18 | 19 | "Legal Entity" shall mean the union of the acting entity and all 20 | other entities that control, are controlled by, or are under common 21 | control with that entity. For the purposes of this definition, 22 | "control" means (i) the power, direct or indirect, to cause the 23 | direction or management of such entity, whether by contract or 24 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 25 | outstanding shares, or (iii) beneficial ownership of such entity. 26 | 27 | "You" (or "Your") shall mean an individual or Legal Entity 28 | exercising permissions granted by this License. 29 | 30 | "Source" form shall mean the preferred form for making modifications, 31 | including but not limited to software source code, documentation 32 | source, and configuration files. 33 | 34 | "Object" form shall mean any form resulting from mechanical 35 | transformation or translation of a Source form, including but 36 | not limited to compiled object code, generated documentation, 37 | and conversions to other media types. 38 | 39 | "Work" shall mean the work of authorship, whether in Source or 40 | Object form, made available under the License, as indicated by a 41 | copyright notice that is included in or attached to the work 42 | (an example is provided in the Appendix below). 43 | 44 | "Derivative Works" shall mean any work, whether in Source or Object 45 | form, that is based on (or derived from) the Work and for which the 46 | editorial revisions, annotations, elaborations, or other modifications 47 | represent, as a whole, an original work of authorship. For the purposes 48 | of this License, Derivative Works shall not include works that remain 49 | separable from, or merely link (or bind by name) to the interfaces of, 50 | the Work and Derivative Works thereof. 51 | 52 | "Contribution" shall mean any work of authorship, including 53 | the original version of the Work and any modifications or additions 54 | to that Work or Derivative Works thereof, that is intentionally 55 | submitted to Licensor for inclusion in the Work by the copyright owner 56 | or by an individual or Legal Entity authorized to submit on behalf of 57 | the copyright owner. For the purposes of this definition, "submitted" 58 | means any form of electronic, verbal, or written communication sent 59 | to the Licensor or its representatives, including but not limited to 60 | communication on electronic mailing lists, source code control systems, 61 | and issue tracking systems that are managed by, or on behalf of, the 62 | Licensor for the purpose of discussing and improving the Work, but 63 | excluding communication that is conspicuously marked or otherwise 64 | designated in writing by the copyright owner as "Not a Contribution." 65 | 66 | "Contributor" shall mean Licensor and any individual or Legal Entity 67 | on behalf of whom a Contribution has been received by Licensor and 68 | subsequently incorporated within the Work. 69 | 70 | 2. Grant of Copyright License. Subject to the terms and conditions of 71 | this License, each Contributor hereby grants to You a perpetual, 72 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 73 | copyright license to reproduce, prepare Derivative Works of, 74 | publicly display, publicly perform, sublicense, and distribute the 75 | Work and such Derivative Works in Source or Object form. 76 | 77 | 3. Grant of Patent License. Subject to the terms and conditions of 78 | this License, each Contributor hereby grants to You a perpetual, 79 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 80 | (except as stated in this section) patent license to make, have made, 81 | use, offer to sell, sell, import, and otherwise transfer the Work, 82 | where such license applies only to those patent claims licensable 83 | by such Contributor that are necessarily infringed by their 84 | Contribution(s) alone or by combination of their Contribution(s) 85 | with the Work to which such Contribution(s) was submitted. If You 86 | institute patent litigation against any entity (including a 87 | cross-claim or counterclaim in a lawsuit) alleging that the Work 88 | or a Contribution incorporated within the Work constitutes direct 89 | or contributory patent infringement, then any patent licenses 90 | granted to You under this License for that Work shall terminate 91 | as of the date such litigation is filed. 92 | 93 | 4. Redistribution. You may reproduce and distribute copies of the 94 | Work or Derivative Works thereof in any medium, with or without 95 | modifications, and in Source or Object form, provided that You 96 | meet the following conditions: 97 | 98 | (a) You must give any other recipients of the Work or 99 | Derivative Works a copy of this License; and 100 | 101 | (b) You must cause any modified files to carry prominent notices 102 | stating that You changed the files; and 103 | 104 | (c) You must retain, in the Source form of any Derivative Works 105 | that You distribute, all copyright, patent, trademark, and 106 | attribution notices from the Source form of the Work, 107 | excluding those notices that do not pertain to any part of 108 | the Derivative Works; and 109 | 110 | (d) If the Work includes a "NOTICE" text file as part of its 111 | distribution, then any Derivative Works that You distribute must 112 | include a readable copy of the attribution notices contained 113 | within such NOTICE file, excluding those notices that do not 114 | pertain to any part of the Derivative Works, in at least one 115 | of the following places: within a NOTICE text file distributed 116 | as part of the Derivative Works; within the Source form or 117 | documentation, if provided along with the Derivative Works; or, 118 | within a display generated by the Derivative Works, if and 119 | wherever such third-party notices normally appear. The contents 120 | of the NOTICE file are for informational purposes only and 121 | do not modify the License. You may add Your own attribution 122 | notices within Derivative Works that You distribute, alongside 123 | or as an addendum to the NOTICE text from the Work, provided 124 | that such additional attribution notices cannot be construed 125 | as modifying the License. 126 | 127 | You may add Your own copyright statement to Your modifications and 128 | may provide additional or different license terms and conditions 129 | for use, reproduction, or distribution of Your modifications, or 130 | for any such Derivative Works as a whole, provided Your use, 131 | reproduction, and distribution of the Work otherwise complies with 132 | the conditions stated in this License. 133 | 134 | 5. Submission of Contributions. Unless You explicitly state otherwise, 135 | any Contribution intentionally submitted for inclusion in the Work 136 | by You to the Licensor shall be under the terms and conditions of 137 | this License, without any additional terms or conditions. 138 | Notwithstanding the above, nothing herein shall supersede or modify 139 | the terms of any separate license agreement you may have executed 140 | with Licensor regarding such Contributions. 141 | 142 | 6. Trademarks. This License does not grant permission to use the trade 143 | names, trademarks, service marks, or product names of the Licensor, 144 | except as required for reasonable and customary use in describing the 145 | origin of the Work and reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. Unless required by applicable law or 148 | agreed to in writing, Licensor provides the Work (and each 149 | Contributor provides its Contributions) on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 151 | implied, including, without limitation, any warranties or conditions 152 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 153 | PARTICULAR PURPOSE. You are solely responsible for determining the 154 | appropriateness of using or redistributing the Work and assume any 155 | risks associated with Your exercise of permissions under this License. 156 | 157 | 8. Limitation of Liability. In no event and under no legal theory, 158 | whether in tort (including negligence), contract, or otherwise, 159 | unless required by applicable law (such as deliberate and grossly 160 | negligent acts) or agreed to in writing, shall any Contributor be 161 | liable to You for damages, including any direct, indirect, special, 162 | incidental, or consequential damages of any character arising as a 163 | result of this License or out of the use or inability to use the 164 | Work (including but not limited to damages for loss of goodwill, 165 | work stoppage, computer failure or malfunction, or any and all 166 | other commercial damages or losses), even if such Contributor 167 | has been advised of the possibility of such damages. 168 | 169 | 9. Accepting Warranty or Additional Liability. While redistributing 170 | the Work or Derivative Works thereof, You may choose to offer, 171 | and charge a fee for, acceptance of support, warranty, indemnity, 172 | or other liability obligations and/or rights consistent with this 173 | License. However, in accepting such obligations, You may act only 174 | on Your own behalf and on Your sole responsibility, not on behalf 175 | of any other Contributor, and only if You agree to indemnify, 176 | defend, and hold each Contributor harmless for any liability 177 | incurred by, or claims asserted against, such Contributor by reason 178 | of your accepting any such warranty or additional liability. 179 | 180 | OR 181 | 182 | Eclipse Public License - v 2.0 183 | 184 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 185 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 186 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 187 | 188 | 1. DEFINITIONS 189 | 190 | "Contribution" means: 191 | 192 | a) in the case of the initial Contributor, the initial content 193 | Distributed under this Agreement, and 194 | 195 | b) in the case of each subsequent Contributor: 196 | i) changes to the Program, and 197 | ii) additions to the Program; 198 | where such changes and/or additions to the Program originate from 199 | and are Distributed by that particular Contributor. A Contribution 200 | "originates" from a Contributor if it was added to the Program by 201 | such Contributor itself or anyone acting on such Contributor's behalf. 202 | Contributions do not include changes or additions to the Program that 203 | are not Modified Works. 204 | 205 | "Contributor" means any person or entity that Distributes the Program. 206 | 207 | "Licensed Patents" mean patent claims licensable by a Contributor which 208 | are necessarily infringed by the use or sale of its Contribution alone 209 | or when combined with the Program. 210 | 211 | "Program" means the Contributions Distributed in accordance with this 212 | Agreement. 213 | 214 | "Recipient" means anyone who receives the Program under this Agreement 215 | or any Secondary License (as applicable), including Contributors. 216 | 217 | "Derivative Works" shall mean any work, whether in Source Code or other 218 | form, that is based on (or derived from) the Program and for which the 219 | editorial revisions, annotations, elaborations, or other modifications 220 | represent, as a whole, an original work of authorship. 221 | 222 | "Modified Works" shall mean any work in Source Code or other form that 223 | results from an addition to, deletion from, or modification of the 224 | contents of the Program, including, for purposes of clarity any new file 225 | in Source Code form that contains any contents of the Program. Modified 226 | Works shall not include works that contain only declarations, 227 | interfaces, types, classes, structures, or files of the Program solely 228 | in each case in order to link to, bind by name, or subclass the Program 229 | or Modified Works thereof. 230 | 231 | "Distribute" means the acts of a) distributing or b) making available 232 | in any manner that enables the transfer of a copy. 233 | 234 | "Source Code" means the form of a Program preferred for making 235 | modifications, including but not limited to software source code, 236 | documentation source, and configuration files. 237 | 238 | "Secondary License" means either the GNU General Public License, 239 | Version 2.0, or any later versions of that license, including any 240 | exceptions or additional permissions as identified by the initial 241 | Contributor. 242 | 243 | 2. GRANT OF RIGHTS 244 | 245 | a) Subject to the terms of this Agreement, each Contributor hereby 246 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 247 | license to reproduce, prepare Derivative Works of, publicly display, 248 | publicly perform, Distribute and sublicense the Contribution of such 249 | Contributor, if any, and such Derivative Works. 250 | 251 | b) Subject to the terms of this Agreement, each Contributor hereby 252 | grants Recipient a non-exclusive, worldwide, royalty-free patent 253 | license under Licensed Patents to make, use, sell, offer to sell, 254 | import and otherwise transfer the Contribution of such Contributor, 255 | if any, in Source Code or other form. This patent license shall 256 | apply to the combination of the Contribution and the Program if, at 257 | the time the Contribution is added by the Contributor, such addition 258 | of the Contribution causes such combination to be covered by the 259 | Licensed Patents. The patent license shall not apply to any other 260 | combinations which include the Contribution. No hardware per se is 261 | licensed hereunder. 262 | 263 | c) Recipient understands that although each Contributor grants the 264 | licenses to its Contributions set forth herein, no assurances are 265 | provided by any Contributor that the Program does not infringe the 266 | patent or other intellectual property rights of any other entity. 267 | Each Contributor disclaims any liability to Recipient for claims 268 | brought by any other entity based on infringement of intellectual 269 | property rights or otherwise. As a condition to exercising the 270 | rights and licenses granted hereunder, each Recipient hereby 271 | assumes sole responsibility to secure any other intellectual 272 | property rights needed, if any. For example, if a third party 273 | patent license is required to allow Recipient to Distribute the 274 | Program, it is Recipient's responsibility to acquire that license 275 | before distributing the Program. 276 | 277 | d) Each Contributor represents that to its knowledge it has 278 | sufficient copyright rights in its Contribution, if any, to grant 279 | the copyright license set forth in this Agreement. 280 | 281 | e) Notwithstanding the terms of any Secondary License, no 282 | Contributor makes additional grants to any Recipient (other than 283 | those set forth in this Agreement) as a result of such Recipient's 284 | receipt of the Program under the terms of a Secondary License 285 | (if permitted under the terms of Section 3). 286 | 287 | 3. REQUIREMENTS 288 | 289 | 3.1 If a Contributor Distributes the Program in any form, then: 290 | 291 | a) the Program must also be made available as Source Code, in 292 | accordance with section 3.2, and the Contributor must accompany 293 | the Program with a statement that the Source Code for the Program 294 | is available under this Agreement, and informs Recipients how to 295 | obtain it in a reasonable manner on or through a medium customarily 296 | used for software exchange; and 297 | 298 | b) the Contributor may Distribute the Program under a license 299 | different than this Agreement, provided that such license: 300 | i) effectively disclaims on behalf of all other Contributors all 301 | warranties and conditions, express and implied, including 302 | warranties or conditions of title and non-infringement, and 303 | implied warranties or conditions of merchantability and fitness 304 | for a particular purpose; 305 | 306 | ii) effectively excludes on behalf of all other Contributors all 307 | liability for damages, including direct, indirect, special, 308 | incidental and consequential damages, such as lost profits; 309 | 310 | iii) does not attempt to limit or alter the recipients' rights 311 | in the Source Code under section 3.2; and 312 | 313 | iv) requires any subsequent distribution of the Program by any 314 | party to be under a license that satisfies the requirements 315 | of this section 3. 316 | 317 | 3.2 When the Program is Distributed as Source Code: 318 | 319 | a) it must be made available under this Agreement, or if the 320 | Program (i) is combined with other material in a separate file or 321 | files made available under a Secondary License, and (ii) the initial 322 | Contributor attached to the Source Code the notice described in 323 | Exhibit A of this Agreement, then the Program may be made available 324 | under the terms of such Secondary Licenses, and 325 | 326 | b) a copy of this Agreement must be included with each copy of 327 | the Program. 328 | 329 | 3.3 Contributors may not remove or alter any copyright, patent, 330 | trademark, attribution notices, disclaimers of warranty, or limitations 331 | of liability ("notices") contained within the Program from any copy of 332 | the Program which they Distribute, provided that Contributors may add 333 | their own appropriate notices. 334 | 335 | 4. COMMERCIAL DISTRIBUTION 336 | 337 | Commercial distributors of software may accept certain responsibilities 338 | with respect to end users, business partners and the like. While this 339 | license is intended to facilitate the commercial use of the Program, 340 | the Contributor who includes the Program in a commercial product 341 | offering should do so in a manner which does not create potential 342 | liability for other Contributors. Therefore, if a Contributor includes 343 | the Program in a commercial product offering, such Contributor 344 | ("Commercial Contributor") hereby agrees to defend and indemnify every 345 | other Contributor ("Indemnified Contributor") against any losses, 346 | damages and costs (collectively "Losses") arising from claims, lawsuits 347 | and other legal actions brought by a third party against the Indemnified 348 | Contributor to the extent caused by the acts or omissions of such 349 | Commercial Contributor in connection with its distribution of the Program 350 | in a commercial product offering. The obligations in this section do not 351 | apply to any claims or Losses relating to any actual or alleged 352 | intellectual property infringement. In order to qualify, an Indemnified 353 | Contributor must: a) promptly notify the Commercial Contributor in 354 | writing of such claim, and b) allow the Commercial Contributor to control, 355 | and cooperate with the Commercial Contributor in, the defense and any 356 | related settlement negotiations. The Indemnified Contributor may 357 | participate in any such claim at its own expense. 358 | 359 | For example, a Contributor might include the Program in a commercial 360 | product offering, Product X. That Contributor is then a Commercial 361 | Contributor. If that Commercial Contributor then makes performance 362 | claims, or offers warranties related to Product X, those performance 363 | claims and warranties are such Commercial Contributor's responsibility 364 | alone. Under this section, the Commercial Contributor would have to 365 | defend claims against the other Contributors related to those performance 366 | claims and warranties, and if a court requires any other Contributor to 367 | pay any damages as a result, the Commercial Contributor must pay 368 | those damages. 369 | 370 | 5. NO WARRANTY 371 | 372 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 373 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 374 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 375 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 376 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 377 | PURPOSE. Each Recipient is solely responsible for determining the 378 | appropriateness of using and distributing the Program and assumes all 379 | risks associated with its exercise of rights under this Agreement, 380 | including but not limited to the risks and costs of program errors, 381 | compliance with applicable laws, damage to or loss of data, programs 382 | or equipment, and unavailability or interruption of operations. 383 | 384 | 6. DISCLAIMER OF LIABILITY 385 | 386 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 387 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 388 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 389 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 390 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 391 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 392 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 393 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 394 | POSSIBILITY OF SUCH DAMAGES. 395 | 396 | 7. GENERAL 397 | 398 | If any provision of this Agreement is invalid or unenforceable under 399 | applicable law, it shall not affect the validity or enforceability of 400 | the remainder of the terms of this Agreement, and without further 401 | action by the parties hereto, such provision shall be reformed to the 402 | minimum extent necessary to make such provision valid and enforceable. 403 | 404 | If Recipient institutes patent litigation against any entity 405 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 406 | Program itself (excluding combinations of the Program with other software 407 | or hardware) infringes such Recipient's patent(s), then such Recipient's 408 | rights granted under Section 2(b) shall terminate as of the date such 409 | litigation is filed. 410 | 411 | All Recipient's rights under this Agreement shall terminate if it 412 | fails to comply with any of the material terms or conditions of this 413 | Agreement and does not cure such failure in a reasonable period of 414 | time after becoming aware of such noncompliance. If all Recipient's 415 | rights under this Agreement terminate, Recipient agrees to cease use 416 | and distribution of the Program as soon as reasonably practicable. 417 | However, Recipient's obligations under this Agreement and any licenses 418 | granted by Recipient relating to the Program shall continue and survive. 419 | 420 | Everyone is permitted to copy and distribute copies of this Agreement, 421 | but in order to avoid inconsistency the Agreement is copyrighted and 422 | may only be modified in the following manner. The Agreement Steward 423 | reserves the right to publish new versions (including revisions) of 424 | this Agreement from time to time. No one other than the Agreement 425 | Steward has the right to modify this Agreement. The Eclipse Foundation 426 | is the initial Agreement Steward. The Eclipse Foundation may assign the 427 | responsibility to serve as the Agreement Steward to a suitable separate 428 | entity. Each new version of the Agreement will be given a distinguishing 429 | version number. The Program (including Contributions) may always be 430 | Distributed subject to the version of the Agreement under which it was 431 | received. In addition, after a new version of the Agreement is published, 432 | Contributor may elect to Distribute the Program (including its 433 | Contributions) under the new version. 434 | 435 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 436 | receives no rights or licenses to the intellectual property of any 437 | Contributor under this Agreement, whether expressly, by implication, 438 | estoppel or otherwise. All rights in the Program not expressly granted 439 | under this Agreement are reserved. Nothing in this Agreement is intended 440 | to be enforceable by any entity that is not a Contributor or Recipient. 441 | No third-party beneficiary rights are created under this Agreement. 442 | 443 | Exhibit A - Form of Secondary Licenses Notice 444 | 445 | "This Source Code may also be made available under the following 446 | Secondary Licenses when the conditions for such availability set forth 447 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 448 | version(s), and exceptions or additional permissions here}." 449 | 450 | Simply including a copy of this Agreement, including this Exhibit A 451 | is not sufficient to license the Source Code under Secondary Licenses. 452 | 453 | If it is not possible or desirable to put the notice in a particular 454 | file, then You may include the notice in a location (such as a LICENSE 455 | file in a relevant directory) where a recipient would be likely to 456 | look for such a notice. 457 | 458 | You may add additional accurate notices of copyright ownership. 459 | 460 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | # Notices for Eclipse zenoh 2 | 3 | This content is produced and maintained by the Eclipse zenoh project. 4 | 5 | * Project home: https://projects.eclipse.org/projects/iot.zenoh 6 | 7 | ## Trademarks 8 | 9 | Eclipse zenoh is trademark of the Eclipse Foundation. 10 | Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. 11 | 12 | ## Copyright 13 | 14 | All content is the property of the respective authors or their employers. 15 | For more information regarding authorship of content, please consult the 16 | listed source code repository logs. 17 | 18 | ## Declared Project Licenses 19 | 20 | This program and the accompanying materials are made available under the 21 | terms of the Eclipse Public License 2.0 which is available at 22 | http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 23 | which is available at https://www.apache.org/licenses/LICENSE-2.0. 24 | 25 | SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 26 | 27 | ## Source Code 28 | 29 | The project maintains the following source code repositories: 30 | 31 | * https://github.com/eclipse-zenoh/zenoh.git 32 | * https://github.com/eclipse-zenoh/zenoh-c.git 33 | * https://github.com/eclipse-zenoh/zenoh-java.git 34 | * https://github.com/eclipse-zenoh/zenoh-go.git 35 | * https://github.com/eclipse-zenoh/zenoh-python.git 36 | 37 | ## Third-party Content 38 | 39 | *To be completed...* 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![CI](https://github.com/eclipse-zenoh/zenoh-plugin-mqtt/workflows/Rust/badge.svg)](https://github.com/eclipse-zenoh/zenoh-plugin-mqtt/actions?query=workflow%3ARust) 4 | [![Discussion](https://img.shields.io/badge/discussion-on%20github-blue)](https://github.com/eclipse-zenoh/roadmap/discussions) 5 | [![Discord](https://img.shields.io/badge/chat-on%20discord-blue)](https://discord.gg/2GJ958VuHs) 6 | [![License](https://img.shields.io/badge/License-EPL%202.0-blue)](https://choosealicense.com/licenses/epl-2.0/) 7 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 8 | 9 | # Eclipse Zenoh 10 | 11 | The Eclipse Zenoh: Zero Overhead Pub/sub, Store/Query and Compute. 12 | 13 | Zenoh (pronounce _/zeno/_) unifies data in motion, data at rest and computations. It carefully blends traditional pub/sub with geo-distributed storages, queries and computations, while retaining a level of time and space efficiency that is well beyond any of the mainstream stacks. 14 | 15 | Check the website [zenoh.io](http://zenoh.io) and the [roadmap](https://github.com/eclipse-zenoh/roadmap) for more detailed information. 16 | 17 | ------------------------------- 18 | 19 | # MQTT plugin and standalone `zenoh-bridge-mqtt` 20 | 21 | :point_right: **Install latest release:** see [below](#how-to-install-it) 22 | 23 | :point_right: **Docker image:** see [below](#docker-image) 24 | 25 | :point_right: **Build "main" branch:** see [below](#how-to-build-it) 26 | 27 | ## Background 28 | 29 | [MQTT](https://mqtt.org/) is a pub/sub protocol leveraging a broker to route the messages between the MQTT clients. 30 | The MQTT plugin for Eclipse Zenoh acts as a MQTT broker, accepting connections from MQTT clients (V3 and V5) and translating the MQTT pub/sub into a Zenoh pub/sub. 31 | I.e.: 32 | 33 | - a MQTT publication on topic `device/123/temperature` is routed as a Zenoh publication on key expression `device/123/temperature` 34 | - a MQTT subscription on topic `device/#` is mapped to a Zenoh subscription on key expression `device/**` 35 | 36 | This allows a close intergration of any MQTT system with Zenoh, but also brings to MQTT systems the benefits of a Zenoh routing infrastructure. 37 | Some examples of use cases: 38 | 39 | - Routing MQTT from the device to the Edge and to the Cloud 40 | - Bridging 2 distinct MQTT systems across the Internet, with NAT traversal 41 | - Pub/sub to MQTT via the Zenoh REST API 42 | - MQTT-ROS2 (robot) communication 43 | - Store MQTT publications in any Zenoh storage (RocksDB, InfluxDB, file system...) 44 | - MQTT record/replay with InfluxDB as a storage 45 | 46 | The MQTT plugin for Eclipse Zenoh is available either as a dynamic library to be loaded by the Zenoh router (`zenohd`), either as a standalone executable (`zenoh-bridge-mqtt`) that can acts as a Zenoh client or peer. 47 | 48 | ## Configuration 49 | 50 | `zenoh-bridge-mqtt` can be configured via a JSON5 file passed via the `-c`argument. You can see a commented example of such configuration file: [`DEFAULT_CONFIG.json5`](DEFAULT_CONFIG.json5). 51 | 52 | The `"mqtt"` part of this same configuration file can also be used in the configuration file for the zenoh router (within its `"plugins"` part). The router will automatically try to load the plugin library (`zenoh_plugin_mqtt`) at startup and apply its configuration. 53 | 54 | `zenoh-bridge-mqtt` also accepts the following arguments. If set, each argument will override the similar setting from the configuration file: 55 | 56 | - zenoh-related arguments: 57 | - **`-c, --config `** : a config file 58 | - **`-m, --mode `** : The zenoh session mode. Default: `peer` Possible values: `peer` or `client`. 59 | See [zenoh documentation](https://zenoh.io/docs/getting-started/key-concepts/#deployment-units) for more details. 60 | - **`-l, --listen `** : A locator on which this router will listen for incoming sessions. Repeat this option to open several listeners. Example of locator: `tcp/localhost:7447`. 61 | - **`-e, --peer `** : A peer locator this router will try to connect to (typically another bridge or a zenoh router). Repeat this option to connect to several peers. Example of locator: `tcp/:7447`. 62 | - **`--no-multicast-scouting`** : disable the zenoh scouting protocol that allows automatic discovery of zenoh peers and routers. 63 | - **`-i, --id `** : The identifier (as an hexadecimal string - e.g.: 0A0B23...) that the zenoh bridge must use. **WARNING: this identifier must be unique in the system!** If not set, a random UUIDv4 will be used. 64 | - **`--rest-http-port [PORT | IP:PORT]`** : Configures HTTP interface for the REST API (disabled by default, setting this option enables it). Accepted values: 65 | - a port number 66 | - a string with format `:` (to bind the HTTP server to a specific interface). 67 | - MQTT-related arguments: 68 | - **`-p, --port [PORT | IP:PORT]`** : The address to bind the MQTT server. Default: `"0.0.0.0:1883"`. Accepted values: 69 | - a port number (`"0.0.0.0"` will be used as IP to bind, meaning any interface of the host) 70 | - a string with format `:` (to bind the MQTT server to a specific interface). 71 | - **`-s, --scope `** : A string added as prefix to all routed MQTT topics when mapped to a zenoh key expression. This should be used to avoid conflicts when several distinct MQTT systems using the same topics names are routed via Zenoh. 72 | - **`-a, --allow `** : A regular expression matching the MQTT topic name that must be routed via zenoh. By default all topics are allowed. If both `--allow` and `--deny` are set a topic will be allowed if it matches only the 'allow' expression. 73 | - **`--deny `** : A regular expression matching the MQTT topic name that must not be routed via zenoh. By default no topics are denied. If both `--allow` and `--deny` are set a topic will be allowed if it matches only the 'allow' expression. 74 | - **`-w, --generalise-pub `** : A list of key expressions to use for generalising the declaration of 75 | the zenoh publications, and thus minimizing the discovery traffic (usable multiple times). 76 | See [this blog](https://zenoh.io/blog/2021-03-23-discovery/#leveraging-resource-generalisation) for more details. 77 | - **`-r, --generalise-sub `** : A list of key expressions to use for generalising the declaration of 78 | the zenoh subscriptions, and thus minimizing the discovery traffic (usable multiple times). 79 | See [this blog](https://zenoh.io/blog/2021-03-23-discovery/#leveraging-resource-generalisation) for more details. 80 | - **`--tx-channel-size `** : Size of the MQTT transmit channel (default: 65536). The channel buffers messages from Zenoh until they can be sent to MQTT clients. If the channel capacity is reached new messages from Zenoh will be dropped until space becomes available. 81 | - **`--dictionary-file `** : Path to a file containing the MQTT client username/password dictionary. 82 | - **`--server-private-key `** : Path to the TLS private key for the MQTT server. If specified a valid certificate for the server must also be provided. 83 | - **`--server-certificate `** : Path to the TLS public certificate for the MQTT server. If specified a valid private key for the server must also be provided. 84 | - **`--root-ca-certificate `** : Path to the certificate of the certificate authority used to validate clients connecting to the MQTT server. If specified a valid private key and certificate for the server must also be provided. 85 | 86 | ## Admin space 87 | 88 | The zenoh bridge for MQTT exposes an administration space allowing to get some information on its status and configuration. 89 | This administration space is accessible via any zenoh API, including the REST API that you can activate at `zenoh-bridge-mqtt` startup using the `--rest-http-port` argument. 90 | 91 | The `zenoh-bridge-mqtt` exposes this administration space with paths prefixed by `@/service//mqtt` (where `` is the unique identifier of the bridge instance). The informations are then organized with such paths: 92 | 93 | - `@/service//mqtt/version` : the bridge version 94 | - `@/service//mqtt/config` : the bridge configuration 95 | 96 | Example of queries on administration space using the REST API with the `curl` command line tool (don't forget to activate the REST API with `--rest-http-port 8000` argument): 97 | 98 | ```bash 99 | curl http://localhost:8000:/@/service/** 100 | ``` 101 | 102 | > _Pro tip: pipe the result into [**jq**](https://stedolan.github.io/jq/) command for JSON pretty print or transformation._ 103 | 104 | ## MQTTS support 105 | 106 | The MQTT plugin and standalone bridge for Eclipse Zenoh supports MQTTS. MQTTS can be configured in two ways: 107 | 108 | - server side authentication: MQTT clients validate the servers TLS certificate but not the other way around. 109 | - mutual authentication (mTLS): where both server and clients validate each other. 110 | 111 | MQTTS can be configured via the configuration file or, if using the standalone bridge, via command line arguments. 112 | 113 | ### Server side authentication configuration 114 | 115 | Server side authentication requires both a private key and certificate for the server. These can be provided as either a file or as a base 64 encoded string. 116 | 117 | In the configuration file, the required **tls** fields when using files are **server_private_key** and **server_certificate**. When using base 64 encoded strings the required **tls** fields are **server_private_key_base64** and **server_certificate_base64**. 118 | 119 | An example configuration file supporting server side authentication would be: 120 | 121 | ```json 122 | { 123 | "plugins": { 124 | "mqtt": { 125 | "tls": { 126 | "server_private_key": "/path/to/private-key.pem", 127 | "server_certificate": "/path/to/certificate.pem" 128 | } 129 | } 130 | } 131 | } 132 | ``` 133 | 134 | The standalone bridge (`zenoh-bridge-mqtt`) also allows the required files to be provided through the **`--server-private-key`** and **`--server-certificate`** command line arguments. 135 | 136 | ### Mutual authentication (mTLS) configuration 137 | 138 | In order to enable mutual authentication a certificate for the certificate authority used to validate clients connecting to the MQTT server must also be provided. This can be provided as either a file or a base 64 encoded string. 139 | 140 | In the configuration file, the required **tls** field when using a file is **root_ca_certificate**. When using base 64 encoded strings the required **tls** field when using a file is **root_ca_certificate_base64**. 141 | 142 | An example configuration file supporting server side authentication would be: 143 | 144 | ```json 145 | { 146 | "plugins": { 147 | "mqtt": { 148 | "tls": { 149 | "server_private_key": "/path/to/private-key.pem", 150 | "server_certificate": "/path/to/certificate.pem", 151 | "root_ca_certificate": "/path/to/root-ca-certificate.pem" 152 | } 153 | } 154 | } 155 | } 156 | ``` 157 | 158 | The standalone bridge (`zenoh-bridge-mqtt`) also allows the required file to be provided through the **`--root-ca-certificate`** command line argument. 159 | 160 | ## Username/password authentication 161 | 162 | The MQTT plugin and standalone bridge for Eclipse Zenoh supports basic username/password authentication of MQTT clients. Credentials are provided via a dictionary file with each line containing the username and password for a single user in the following format: 163 | 164 | ```raw 165 | username:password 166 | ``` 167 | 168 | Username/passord authentication can be configured via the configuration file or, if using the standalone bridge, via command line arguments. 169 | 170 | In the configuration file, the required **auth** field for configuring the dictionary file is **dictionary_file**. 171 | 172 | An example configuration file supporting username/password authentication would be: 173 | 174 | ```json 175 | { 176 | "plugins": { 177 | "mqtt": { 178 | "auth": { 179 | "dictionary_file": "/path/to/dictionary-file", 180 | } 181 | } 182 | } 183 | } 184 | ``` 185 | 186 | The standalone bridge (`zenoh-bridge-mqtt`) also allows the required file to be provided through the **`--dictionary-file`** command line argument. 187 | 188 | ### Security considerations 189 | 190 | Usernames and passwords are sent as part of the MQTT `CONNECT` message in clear text. As such, they can potentially be viewed using tools such as [Wireshark](https://www.wireshark.org/). 191 | 192 | To prevent this, it is highly recommended that this feature is used in conjunction with the MQTTS feature to ensure credentials are encrypted on the wire. 193 | 194 | ## How to install it 195 | 196 | To install the latest release of either the MQTT plugin for the Zenoh router, either the `zenoh-bridge-mqtt` standalone executable, you can do as follows: 197 | 198 | ### Manual installation (all platforms) 199 | 200 | All release packages can be downloaded from: 201 | 202 | - [https://download.eclipse.org/zenoh/zenoh-plugin-mqtt/latest/](https://download.eclipse.org/zenoh/zenoh-plugin-mqtt/latest/) 203 | 204 | Each subdirectory has the name of the Rust target. See the platforms each target corresponds to on [https://doc.rust-lang.org/stable/rustc/platform-support.html](https://doc.rust-lang.org/stable/rustc/platform-support.html) 205 | 206 | Choose your platform and download: 207 | 208 | - the `zenoh-plugin-mqtt--.zip` file for the plugin. 209 | Then unzip it in the same directory than `zenohd` or to any directory where it can find the plugin library (e.g. /usr/lib) 210 | - the `zenoh-bridge-mqtt--.zip` file for the standalone executable. 211 | Then unzip it where you want, and run the extracted `zenoh-bridge-mqtt` binary. 212 | 213 | ### Linux Debian 214 | 215 | Add Eclipse Zenoh private repository to the sources list: 216 | 217 | ```bash 218 | echo "deb [trusted=yes] https://download.eclipse.org/zenoh/debian-repo/ /" | sudo tee -a /etc/apt/sources.list > /dev/null 219 | sudo apt update 220 | ``` 221 | 222 | Then either: 223 | 224 | - install the plugin with: `sudo apt install zenoh-plugin-mqtt`. 225 | - install the standalone executable with: `sudo apt install zenoh-bridge-mqtt`. 226 | 227 | ## Docker image 228 | 229 | The **`zenoh-bridge-mqtt`** standalone executable is also available as a [Docker images](https://hub.docker.com/r/eclipse/zenoh-bridge-mqtt/tags?page=1&ordering=last_updated) for both amd64 and arm64. To get it, do: 230 | 231 | - `docker pull eclipse/zenoh-bridge-mqtt:latest` for the latest release 232 | - `docker pull eclipse/zenoh-bridge-mqtt:main` for the main branch version (nightly build) 233 | 234 | Usage: **`docker run --init -p 1883:1883 eclipse/zenoh-bridge-mqtt`** 235 | It supports the same command line arguments than the `zenoh-bridge-mqtt` (see above or check with `-h` argument). 236 | 237 | ## How to build it 238 | 239 | > :warning: **WARNING** :warning: : As Rust doesn't have a stable ABI, the plugins should be 240 | built with the exact same Rust version than `zenohd`, and using for `zenoh` dependency the same version (or commit number) than 'zenohd'. 241 | Otherwise, incompatibilities in memory mapping of shared types between `zenohd` and the library can lead to a `"SIGSEV"` crash. 242 | 243 | In order to build the zenoh bridge for MQTT you only need to install [Rust](https://www.rust-lang.org/tools/install). If you already have the Rust toolchain installed, make sure it is up-to-date with: 244 | 245 | ```bash 246 | rustup update 247 | ``` 248 | 249 | Then, you may clone the repository on your machine: 250 | 251 | ```bash 252 | git clone https://github.com/eclipse-zenoh/zenoh-plugin-mqtt.git 253 | cd zenoh-plugin-mqtt 254 | cargo build --release 255 | ``` 256 | 257 | The standalone executable binary `zenoh-bridge-mqtt` and a plugin shared library (`*.so` on Linux, `*.dylib` on Mac OS, `*.dll` on Windows) to be dynamically 258 | loaded by the zenoh router `zenohd` will be generated in the `target/release` subdirectory. 259 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.0" 3 | -------------------------------------------------------------------------------- /zenoh-bridge-mqtt/.deb/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for Eclipse Zenoh bridge for MQTT 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `configure' 10 | # * `abort-upgrade' 11 | # * `abort-remove' `in-favour' 12 | # 13 | # * `abort-remove' 14 | # * `abort-deconfigure' `in-favour' 15 | # `removing' 16 | # 17 | # for details, see https://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | configure) 23 | if ! command -v systemctl &> /dev/null 24 | then 25 | echo "WARNING: 'systemctl' not found - cannot install zenoh-bridge-mqtt as a service." 26 | exit 0 27 | fi 28 | id -u zenoh-bridge-mqtt &> /dev/null || useradd -r -s /bin/false zenoh-bridge-mqtt 29 | systemctl daemon-reload 30 | systemctl disable zenoh-bridge-mqtt 31 | ;; 32 | 33 | abort-upgrade|abort-remove|abort-deconfigure) 34 | ;; 35 | 36 | *) 37 | echo "postinst called with unknown argument \`$1'" >&2 38 | exit 1 39 | ;; 40 | esac 41 | 42 | # dh_installdeb will replace this with shell code automatically 43 | # generated by other debhelper scripts. 44 | 45 | #DEBHELPER# 46 | 47 | exit 0 48 | -------------------------------------------------------------------------------- /zenoh-bridge-mqtt/.deb/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postrm script for Eclipse Zenoh bridge for MQTT 3 | # see: dh_installdeb(1) 4 | 5 | set -e 6 | 7 | # summary of how this script can be called: 8 | # * `remove' 9 | # * `purge' 10 | # * `upgrade' 11 | # * `failed-upgrade' 12 | # * `abort-install' 13 | # * `abort-install' 14 | # * `abort-upgrade' 15 | # * `disappear' 16 | # 17 | # for details, see https://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | purge|remove|abort-install|disappear) 23 | userdel zenoh-bridge-mqtt > /dev/null 2>&1 || true 24 | rm -rf /etc/zenoh-bridge-mqtt 25 | ;; 26 | 27 | *) 28 | echo "postrm called with unknown argument \`$1'" >&2 29 | exit 1 30 | ;; 31 | esac 32 | 33 | # dh_installdeb will replace this with shell code automatically 34 | # generated by other debhelper scripts. 35 | 36 | #DEBHELPER# 37 | 38 | exit 0 39 | 40 | -------------------------------------------------------------------------------- /zenoh-bridge-mqtt/.service/zenoh-bridge-mqtt.service: -------------------------------------------------------------------------------- 1 | 2 | [Unit] 3 | Description = Eclipse Zenoh Bridge for MQTT 4 | Documentation=https://github.com/eclipse-zenoh/zenoh-plugin-mqtt 5 | After=network-online.target 6 | Wants=network-online.target 7 | 8 | 9 | [Service] 10 | Type=simple 11 | Environment=RUST_LOG=info 12 | ExecStart = /usr/bin/zenoh-bridge-mqtt -c /etc/zenoh-bridge-mqtt/conf.json5 13 | KillMode=mixed 14 | KillSignal=SIGINT 15 | RestartKillSignal=SIGINT 16 | Restart=on-failure 17 | RestartSec=2 18 | PermissionsStartOnly=true 19 | User=zenoh-bridge-mqtt 20 | StandardOutput=syslog 21 | StandardError=syslog 22 | SyslogIdentifier=zenoh-bridge-mqtt 23 | [Install] 24 | WantedBy=multi-user.target 25 | 26 | -------------------------------------------------------------------------------- /zenoh-bridge-mqtt/Cargo.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | [package] 15 | name = "zenoh-bridge-mqtt" 16 | version = { workspace = true } 17 | authors = { workspace = true } 18 | edition = { workspace = true } 19 | repository = { workspace = true } 20 | homepage = { workspace = true } 21 | license = { workspace = true } 22 | categories = { workspace = true } 23 | description = "Zenoh bridge for MQTT" 24 | publish = false 25 | 26 | [dependencies] 27 | clap = { workspace = true } 28 | futures = { workspace = true } 29 | lazy_static = { workspace = true } 30 | tokio = { workspace = true } 31 | tracing = { workspace = true } 32 | serde_json = { workspace = true } 33 | zenoh = { workspace = true } 34 | zenoh-config = { workspace = true } 35 | zenoh-plugin-rest = { workspace = true } 36 | zenoh-plugin-trait = { workspace = true } 37 | zenoh-plugin-mqtt = { workspace = true } 38 | 39 | [[bin]] 40 | name = "zenoh-bridge-mqtt" 41 | path = "src/main.rs" 42 | 43 | [package.metadata.deb] 44 | name = "zenoh-bridge-mqtt" 45 | maintainer = "zenoh-dev@eclipse.org" 46 | copyright = "2017, 2022 ZettaScale Technology Inc." 47 | section = "net" 48 | license-file = ["../LICENSE", "0"] 49 | depends = "$auto" 50 | maintainer-scripts = ".deb" 51 | assets = [ 52 | # binary 53 | [ 54 | "target/release/zenoh-bridge-mqtt", 55 | "/usr/bin/", 56 | "755", 57 | ], 58 | # config file 59 | [ 60 | "../DEFAULT_CONFIG.json5", 61 | "/etc/zenoh-bridge-mqtt/conf.json5", 62 | "644", 63 | ], 64 | # service 65 | [ 66 | ".service/zenoh-bridge-mqtt.service", 67 | "/lib/systemd/system/zenoh-bridge-mqtt.service", 68 | "644", 69 | ], 70 | ] 71 | conf-files = ["/etc/zenoh-bridge-mqtt/conf.json5"] 72 | -------------------------------------------------------------------------------- /zenoh-bridge-mqtt/src/main.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::str::FromStr; 15 | 16 | use clap::{App, Arg}; 17 | use zenoh::{ 18 | config::Config, 19 | init_log_from_env_or, 20 | internal::{plugins::PluginsManager, runtime::RuntimeBuilder}, 21 | session::ZenohId, 22 | }; 23 | use zenoh_config::ModeDependentValue; 24 | use zenoh_plugin_trait::Plugin; 25 | 26 | macro_rules! insert_json5 { 27 | ($config: expr, $args: expr, $key: expr, if $name: expr) => { 28 | if $args.occurrences_of($name) > 0 { 29 | $config.insert_json5($key, "true").unwrap(); 30 | } 31 | }; 32 | ($config: expr, $args: expr, $key: expr, if $name: expr, $($t: tt)*) => { 33 | if $args.occurrences_of($name) > 0 { 34 | $config.insert_json5($key, &serde_json::to_string(&$args.value_of($name).unwrap()$($t)*).unwrap()).unwrap(); 35 | } 36 | }; 37 | ($config: expr, $args: expr, $key: expr, for $name: expr, $($t: tt)*) => { 38 | if let Some(value) = $args.values_of($name) { 39 | $config.insert_json5($key, &serde_json::to_string(&value$($t)*).unwrap()).unwrap(); 40 | } 41 | }; 42 | } 43 | 44 | fn parse_args() -> Config { 45 | let app = App::new("zenoh bridge for MQTT") 46 | .version(zenoh_plugin_mqtt::MqttPlugin::PLUGIN_VERSION) 47 | .long_version(zenoh_plugin_mqtt::MqttPlugin::PLUGIN_LONG_VERSION) 48 | // 49 | // zenoh related arguments: 50 | // 51 | .arg(Arg::from_usage( 52 | r"-i, --id=[HEX_STRING] \ 53 | 'The identifier (as an hexadecimal string, with odd number of chars - e.g.: 0A0B23...) that zenohd must use. 54 | WARNING: this identifier must be unique in the system and must be 16 bytes maximum (32 chars)! 55 | If not set, a random UUIDv4 will be used.'", 56 | )) 57 | .arg(Arg::from_usage( 58 | r#"-m, --mode=[MODE] 'The zenoh session mode.'"#) 59 | .possible_values(["peer", "client"]) 60 | .default_value("peer") 61 | ) 62 | .arg(Arg::from_usage( 63 | r"-c, --config=[FILE] \ 64 | 'The configuration file. Currently, this file must be a valid JSON5 file.'", 65 | )) 66 | .arg(Arg::from_usage( 67 | r"-l, --listen=[ENDPOINT]... \ 68 | 'A locator on which this router will listen for incoming sessions. 69 | Repeat this option to open several listeners.'", 70 | ), 71 | ) 72 | .arg(Arg::from_usage( 73 | r"-e, --connect=[ENDPOINT]... \ 74 | 'A peer locator this router will try to connect to. 75 | Repeat this option to connect to several peers.'", 76 | )) 77 | .arg(Arg::from_usage( 78 | r"--no-multicast-scouting \ 79 | 'By default the zenoh bridge listens and replies to UDP multicast scouting messages for being discovered by peers and routers. 80 | This option disables this feature.'" 81 | )) 82 | .arg(Arg::from_usage( 83 | r"--rest-http-port=[PORT | IP:PORT] \ 84 | 'Configures HTTP interface for the REST API (disabled by default, setting this option enables it). Accepted values:' 85 | - a port number 86 | - a string with format `:` (to bind the HTTP server to a specific interface)." 87 | )) 88 | // 89 | // MQTT related arguments: 90 | // 91 | .arg(Arg::from_usage( 92 | r#"-p, --port=[PORT | IP:PORT] \ 93 | 'The address to bind the MQTT server. Default: "0.0.0.0:1883". Accepted values:' 94 | - a port number ("0.0.0.0" will be used as IP to bind, meaning any interface of the host) 95 | - a string with format `:` (to bind the MQTT server to a specific interface)."# 96 | )) 97 | .arg(Arg::from_usage( 98 | r#"-s, --scope=[String] 'A string added as prefix to all routed MQTT topics when mapped to a zenoh key expression. This should be used to avoid conflicts when several distinct MQTT systems using the same topics names are routed via zenoh'"# 99 | )) 100 | .arg(Arg::from_usage( 101 | r#"-a, --allow=[String] 'A regular expression matching the MQTT topic name that must be routed via zenoh. By default topics are allowed. 102 | If both '--allow' and '--deny' are set a topic will be allowed if it matches only the 'allow' expression."# 103 | )) 104 | .arg(Arg::from_usage( 105 | r#"--deny=[String] 'A regular expression matching the MQTT topic name that must not be routed via zenoh. By default no topics are denied. 106 | If both '--allow' and '--deny' are set a topic will be allowed if it matches only the 'allow' expression."# 107 | )) 108 | .arg(Arg::from_usage( 109 | r#"-r, --generalise-sub=[String]... 'A list of key expression to use for generalising subscriptions (usable multiple times).'"# 110 | )) 111 | .arg(Arg::from_usage( 112 | r#"-w, --generalise-pub=[String]... 'A list of key expression to use for generalising publications (usable multiple times).'"# 113 | )) 114 | .arg(Arg::from_usage( 115 | r#"--tx-channel-size=[Unsigned Integer] 'Size of the MQTT transmit channel (default: 65536). The channel buffers messages from Zenoh until they can be sent to MQTT clients. If the channel capacity is reached new messages from Zenoh will be dropped until space becomes available.'"# 116 | )) 117 | .arg(Arg::from_usage( 118 | r#"--dictionary-file=[FILE] 'Path to the file containing the MQTT client username/password dictionary.'"# 119 | )) 120 | .arg(Arg::from_usage( 121 | r#"--server-private-key=[FILE] 'Path to the TLS private key for the MQTT server. If specified a valid certificate for the server must also be provided.'"# 122 | ) 123 | .requires("server-certificate")) 124 | .arg(Arg::from_usage( 125 | r#"--server-certificate=[FILE] 'Path to the TLS public certificate for the MQTT server. If specified a valid private key for the server must also be provided.'"# 126 | ) 127 | .requires("server-private-key")) 128 | .arg(Arg::from_usage( 129 | r#"--root-ca-certificate=[FILE] 'Path to the certificate of the certificate authority used to validate clients connecting to the MQTT server. If specified a valid private key and certificate for the server must also be provided.'"# 130 | ) 131 | .requires_all(&["server-certificate", "server-private-key"])); 132 | let args = app.get_matches(); 133 | 134 | // load config file at first 135 | let mut config = match args.value_of("config") { 136 | Some(conf_file) => Config::from_file(conf_file).unwrap(), 137 | None => Config::default(), 138 | }; 139 | // if "mqtt" plugin conf is not present, add it (empty to use default config) 140 | if config.plugin("mqtt").is_none() { 141 | config.insert_json5("plugins/mqtt", "{}").unwrap(); 142 | } 143 | 144 | // apply zenoh related arguments over config 145 | // NOTE: only if args.occurrences_of()>0 to avoid overriding config with the default arg value 146 | if args.occurrences_of("id") > 0 { 147 | config 148 | .set_id(ZenohId::from_str(args.value_of("id").unwrap()).unwrap()) 149 | .unwrap(); 150 | } 151 | if args.occurrences_of("mode") > 0 { 152 | config 153 | .set_mode(Some(args.value_of("mode").unwrap().parse().unwrap())) 154 | .unwrap(); 155 | } 156 | if let Some(endpoints) = args.values_of("connect") { 157 | config 158 | .connect 159 | .endpoints 160 | .set(endpoints.map(|p| p.parse().unwrap()).collect()) 161 | .unwrap(); 162 | } 163 | if let Some(endpoints) = args.values_of("listen") { 164 | config 165 | .listen 166 | .endpoints 167 | .set(endpoints.map(|p| p.parse().unwrap()).collect()) 168 | .unwrap(); 169 | } 170 | if args.is_present("no-multicast-scouting") { 171 | config.scouting.multicast.set_enabled(Some(false)).unwrap(); 172 | } 173 | if let Some(port) = args.value_of("rest-http-port") { 174 | config 175 | .insert_json5("plugins/rest/http_port", &format!(r#""{port}""#)) 176 | .unwrap(); 177 | } 178 | 179 | // Always add timestamps to publications (required for PublicationCache used in case of TRANSIENT_LOCAL topics) 180 | config 181 | .timestamping 182 | .set_enabled(Some(ModeDependentValue::Unique(true))) 183 | .unwrap(); 184 | 185 | // Enable admin space 186 | config.adminspace.set_enabled(true).unwrap(); 187 | // Enable loading plugins 188 | config.plugins_loading.set_enabled(true).unwrap(); 189 | 190 | // apply MQTT related arguments over config 191 | insert_json5!(config, args, "plugins/mqtt/port", if "port",); 192 | insert_json5!(config, args, "plugins/mqtt/scope", if "scope",); 193 | insert_json5!(config, args, "plugins/mqtt/allow", if "allow", ); 194 | insert_json5!(config, args, "plugins/mqtt/deny", if "deny", ); 195 | insert_json5!(config, args, "plugins/mqtt/generalise_pubs", for "generalise-pub", .collect::>()); 196 | insert_json5!(config, args, "plugins/mqtt/generalise_subs", for "generalise-sub", .collect::>()); 197 | insert_json5!(config, args, "plugins/mqtt/tx_channel_size", if "tx-channel-size", .parse::().unwrap()); 198 | insert_json5!(config, args, "plugins/mqtt/auth/dictionary_file", if "dictionary-file", ); 199 | insert_json5!(config, args, "plugins/mqtt/tls/server_private_key", if "server-private-key", ); 200 | insert_json5!(config, args, "plugins/mqtt/tls/server_certificate", if "server-certificate", ); 201 | insert_json5!(config, args, "plugins/mqtt/tls/root_ca_certificate", if "root-ca-certificate", ); 202 | config 203 | } 204 | 205 | #[tokio::main] 206 | async fn main() { 207 | init_log_from_env_or("z=info"); 208 | 209 | tracing::info!( 210 | "zenoh-bridge-mqtt {}", 211 | zenoh_plugin_mqtt::MqttPlugin::PLUGIN_LONG_VERSION 212 | ); 213 | 214 | let config = parse_args(); 215 | tracing::info!("Zenoh {config:?}"); 216 | 217 | let mut plugins_mgr = PluginsManager::static_plugins_only(); 218 | 219 | // declare REST plugin if specified in conf 220 | if config.plugin("rest").is_some() { 221 | plugins_mgr.declare_static_plugin::("rest", true); 222 | } 223 | 224 | // declare MQTT plugin 225 | plugins_mgr.declare_static_plugin::("mqtt", true); 226 | 227 | // create a zenoh Runtime. 228 | let mut runtime = match RuntimeBuilder::new(config) 229 | .plugins_manager(plugins_mgr) 230 | .build() 231 | .await 232 | { 233 | Ok(runtime) => runtime, 234 | Err(e) => { 235 | println!("{e}. Exiting..."); 236 | std::process::exit(-1); 237 | } 238 | }; 239 | if let Err(e) = runtime.start().await { 240 | println!("Failed to start Zenoh runtime: {e}. Exiting..."); 241 | std::process::exit(-1); 242 | } 243 | 244 | futures::future::pending::<()>().await; 245 | } 246 | -------------------------------------------------------------------------------- /zenoh-plugin-mqtt/Cargo.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2017, 2020 ADLINK Technology Inc. 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ADLINK zenoh team, 13 | # 14 | [package] 15 | name = "zenoh-plugin-mqtt" 16 | version = { workspace = true } 17 | authors = { workspace = true } 18 | edition = { workspace = true } 19 | repository = { workspace = true } 20 | homepage = { workspace = true } 21 | license = { workspace = true } 22 | categories = { workspace = true } 23 | description = "Zenoh plugin for MQTT" 24 | 25 | [lib] 26 | name = "zenoh_plugin_mqtt" 27 | crate-type = ["cdylib", "rlib"] 28 | 29 | [features] 30 | default = ["dynamic_plugin"] 31 | dynamic_plugin = [] 32 | stats = ["zenoh/stats"] 33 | 34 | [dependencies] 35 | async-channel = { workspace = true } 36 | async-trait = { workspace = true } 37 | base64 = { workspace = true } 38 | derivative = { workspace = true } 39 | flume = { workspace = true } 40 | futures = { workspace = true } 41 | git-version = { workspace = true } 42 | hex = { workspace = true } 43 | lazy_static = { workspace = true } 44 | tokio = { workspace = true } 45 | tracing = { workspace = true } 46 | ntex = { workspace = true } 47 | ntex-mqtt = { workspace = true } 48 | ntex-tls = { workspace = true } 49 | regex = { workspace = true } 50 | rustls = { workspace = true } 51 | rustls-pemfile = { workspace = true } 52 | secrecy = { workspace = true } 53 | serde = { workspace = true } 54 | serde_json = { workspace = true } 55 | zenoh = { workspace = true } 56 | zenoh-config = { workspace = true } 57 | zenoh-plugin-trait = { workspace = true } 58 | 59 | [build-dependencies] 60 | rustc_version = { workspace = true } 61 | 62 | [package.metadata.deb] 63 | name = "zenoh-plugin-mqtt" 64 | maintainer = "zenoh-dev@eclipse.org" 65 | copyright = "2017, 2020 ADLINK Technology Inc." 66 | section = "net" 67 | license-file = ["../LICENSE", "0"] 68 | depends = "zenohd (=1.4.0)" 69 | -------------------------------------------------------------------------------- /zenoh-plugin-mqtt/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /zenoh-plugin-mqtt/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 2020 ADLINK Technology Inc. 2 | // 3 | // This program and the accompanying materials are made available under the 4 | // terms of the Eclipse Public License 2.0 which is available at 5 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 6 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 7 | // 8 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 9 | // 10 | // Contributors: 11 | // ADLINK zenoh team, 12 | // 13 | fn main() { 14 | // Add rustc version to zenohd 15 | let version_meta = rustc_version::version_meta().unwrap(); 16 | println!( 17 | "cargo:rustc-env=RUSTC_VERSION={}", 18 | version_meta.short_version_string 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /zenoh-plugin-mqtt/src/config.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::fmt; 15 | 16 | use regex::Regex; 17 | use serde::{ 18 | de, 19 | de::{Unexpected, Visitor}, 20 | Deserialize, Deserializer, Serialize, Serializer, 21 | }; 22 | use zenoh::key_expr::OwnedKeyExpr; 23 | use zenoh_config::SecretValue; 24 | 25 | const DEFAULT_MQTT_INTERFACE: &str = "0.0.0.0"; 26 | const DEFAULT_MQTT_PORT: &str = "1883"; 27 | const DEFAULT_MQTT_TX_CHANNEL_SIZE: usize = 65536; 28 | pub const DEFAULT_WORK_THREAD_NUM: usize = 2; 29 | pub const DEFAULT_MAX_BLOCK_THREAD_NUM: usize = 50; 30 | 31 | #[derive(Deserialize, Serialize, Debug, Clone)] 32 | #[serde(deny_unknown_fields)] 33 | pub struct Config { 34 | #[serde( 35 | default = "default_mqtt_port", 36 | deserialize_with = "deserialize_mqtt_port" 37 | )] 38 | pub port: String, 39 | #[serde(default)] 40 | pub scope: Option, 41 | #[serde( 42 | default, 43 | deserialize_with = "deserialize_regex", 44 | serialize_with = "serialize_allow" 45 | )] 46 | pub allow: Option, 47 | #[serde( 48 | default, 49 | deserialize_with = "deserialize_regex", 50 | serialize_with = "serialize_deny" 51 | )] 52 | pub deny: Option, 53 | #[serde(default)] 54 | pub generalise_subs: Vec, 55 | #[serde(default)] 56 | pub generalise_pubs: Vec, 57 | #[serde(default = "default_mqtt_tx_channel_size")] 58 | pub tx_channel_size: usize, 59 | #[serde(default)] 60 | pub tls: Option, 61 | #[serde(default = "default_work_thread_num")] 62 | pub work_thread_num: usize, 63 | #[serde(default = "default_max_block_thread_num")] 64 | pub max_block_thread_num: usize, 65 | __required__: Option, 66 | #[serde(default)] 67 | pub auth: Option, 68 | #[serde(default, deserialize_with = "deserialize_path")] 69 | __path__: Option>, 70 | } 71 | 72 | #[derive(Deserialize, Serialize, Debug, Clone)] 73 | #[serde(deny_unknown_fields)] 74 | pub struct TLSConfig { 75 | pub server_private_key: Option, 76 | #[serde(skip_serializing)] 77 | pub server_private_key_base64: Option, 78 | pub server_certificate: Option, 79 | #[serde(skip_serializing)] 80 | pub server_certificate_base64: Option, 81 | pub root_ca_certificate: Option, 82 | #[serde(skip_serializing)] 83 | pub root_ca_certificate_base64: Option, 84 | } 85 | 86 | #[derive(Deserialize, Serialize, Debug, Clone)] 87 | #[serde(deny_unknown_fields)] 88 | pub struct AuthConfig { 89 | pub dictionary_file: String, 90 | } 91 | 92 | fn default_mqtt_port() -> String { 93 | format!("{DEFAULT_MQTT_INTERFACE}:{DEFAULT_MQTT_PORT}") 94 | } 95 | 96 | fn deserialize_mqtt_port<'de, D>(deserializer: D) -> Result 97 | where 98 | D: Deserializer<'de>, 99 | { 100 | deserializer.deserialize_any(MqttPortVisitor) 101 | } 102 | 103 | fn deserialize_path<'de, D>(deserializer: D) -> Result>, D::Error> 104 | where 105 | D: Deserializer<'de>, 106 | { 107 | deserializer.deserialize_option(OptPathVisitor) 108 | } 109 | 110 | fn default_mqtt_tx_channel_size() -> usize { 111 | DEFAULT_MQTT_TX_CHANNEL_SIZE 112 | } 113 | 114 | fn default_work_thread_num() -> usize { 115 | DEFAULT_WORK_THREAD_NUM 116 | } 117 | 118 | fn default_max_block_thread_num() -> usize { 119 | DEFAULT_MAX_BLOCK_THREAD_NUM 120 | } 121 | 122 | struct OptPathVisitor; 123 | 124 | impl<'de> serde::de::Visitor<'de> for OptPathVisitor { 125 | type Value = Option>; 126 | 127 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 128 | write!(formatter, "none or a string or an array of strings") 129 | } 130 | 131 | fn visit_none(self) -> Result 132 | where 133 | E: de::Error, 134 | { 135 | Ok(None) 136 | } 137 | 138 | fn visit_some(self, deserializer: D) -> Result 139 | where 140 | D: Deserializer<'de>, 141 | { 142 | deserializer.deserialize_any(PathVisitor).map(Some) 143 | } 144 | } 145 | 146 | struct PathVisitor; 147 | 148 | impl<'de> serde::de::Visitor<'de> for PathVisitor { 149 | type Value = Vec; 150 | 151 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 152 | write!(formatter, "a string or an array of strings") 153 | } 154 | 155 | fn visit_str(self, v: &str) -> Result 156 | where 157 | E: de::Error, 158 | { 159 | Ok(vec![v.into()]) 160 | } 161 | 162 | fn visit_seq(self, mut seq: A) -> Result 163 | where 164 | A: de::SeqAccess<'de>, 165 | { 166 | let mut v = if let Some(l) = seq.size_hint() { 167 | Vec::with_capacity(l) 168 | } else { 169 | Vec::new() 170 | }; 171 | while let Some(s) = seq.next_element()? { 172 | v.push(s); 173 | } 174 | Ok(v) 175 | } 176 | } 177 | 178 | fn deserialize_regex<'de, D>(deserializer: D) -> Result, D::Error> 179 | where 180 | D: Deserializer<'de>, 181 | { 182 | let s: Option = Deserialize::deserialize(deserializer)?; 183 | 184 | match s { 185 | Some(s) => Regex::new(&s).map(Some).map_err(|e| { 186 | de::Error::custom(format!( 187 | r#"Invalid regex for 'allow' or 'deny': "{s}" - {e}"# 188 | )) 189 | }), 190 | 191 | None => Ok(None), 192 | } 193 | } 194 | 195 | fn serialize_allow(v: &Option, serializer: S) -> Result 196 | where 197 | S: Serializer, 198 | { 199 | serializer.serialize_str( 200 | &v.as_ref() 201 | .map_or_else(|| ".*".to_string(), |re| re.to_string()), 202 | ) 203 | } 204 | 205 | fn serialize_deny(v: &Option, serializer: S) -> Result 206 | where 207 | S: Serializer, 208 | { 209 | serializer.serialize_str( 210 | &v.as_ref() 211 | .map_or_else(|| "".to_string(), |re| re.to_string()), 212 | ) 213 | } 214 | 215 | struct MqttPortVisitor; 216 | 217 | impl Visitor<'_> for MqttPortVisitor { 218 | type Value = String; 219 | 220 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 221 | formatter.write_str(r#"either a port number as an integer or a string, either a string with format ":""#) 222 | } 223 | 224 | fn visit_u64(self, value: u64) -> Result 225 | where 226 | E: de::Error, 227 | { 228 | Ok(format!("{DEFAULT_MQTT_INTERFACE}:{value}")) 229 | } 230 | 231 | fn visit_str(self, value: &str) -> Result 232 | where 233 | E: de::Error, 234 | { 235 | let parts: Vec<&str> = value.split(':').collect(); 236 | if parts.len() > 2 { 237 | return Err(E::invalid_value(Unexpected::Str(value), &self)); 238 | } 239 | let (interface, port) = if parts.len() == 1 { 240 | (DEFAULT_MQTT_INTERFACE, parts[0]) 241 | } else { 242 | (parts[0], parts[1]) 243 | }; 244 | if port.parse::().is_err() { 245 | return Err(E::invalid_value(Unexpected::Str(port), &self)); 246 | } 247 | Ok(format!("{interface}:{port}")) 248 | } 249 | } 250 | 251 | #[cfg(test)] 252 | mod tests { 253 | use super::Config; 254 | 255 | #[test] 256 | fn test_path_field() { 257 | // See: https://github.com/eclipse-zenoh/zenoh-plugin-webserver/issues/19 258 | let config = serde_json::from_str::(r#"{"__path__": "/example/path"}"#); 259 | 260 | assert!(config.is_ok()); 261 | let Config { 262 | __required__, 263 | __path__, 264 | .. 265 | } = config.unwrap(); 266 | 267 | assert_eq!(__path__, Some(vec![String::from("/example/path")])); 268 | assert_eq!(__required__, None); 269 | } 270 | 271 | #[test] 272 | fn test_required_field() { 273 | // See: https://github.com/eclipse-zenoh/zenoh-plugin-webserver/issues/19 274 | let config = serde_json::from_str::(r#"{"__required__": true}"#); 275 | assert!(config.is_ok()); 276 | let Config { 277 | __required__, 278 | __path__, 279 | .. 280 | } = config.unwrap(); 281 | 282 | assert_eq!(__path__, None); 283 | assert_eq!(__required__, Some(true)); 284 | } 285 | 286 | #[test] 287 | fn test_path_field_and_required_field() { 288 | // See: https://github.com/eclipse-zenoh/zenoh-plugin-webserver/issues/19 289 | let config = serde_json::from_str::( 290 | r#"{"__path__": "/example/path", "__required__": true}"#, 291 | ); 292 | 293 | assert!(config.is_ok()); 294 | let Config { 295 | __required__, 296 | __path__, 297 | .. 298 | } = config.unwrap(); 299 | 300 | assert_eq!(__path__, Some(vec![String::from("/example/path")])); 301 | assert_eq!(__required__, Some(true)); 302 | } 303 | 304 | #[test] 305 | fn test_no_path_field_and_no_required_field() { 306 | // See: https://github.com/eclipse-zenoh/zenoh-plugin-webserver/issues/19 307 | let config = serde_json::from_str::("{}"); 308 | 309 | assert!(config.is_ok()); 310 | let Config { 311 | __required__, 312 | __path__, 313 | .. 314 | } = config.unwrap(); 315 | 316 | assert_eq!(__path__, None); 317 | assert_eq!(__required__, None); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /zenoh-plugin-mqtt/src/lib.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017, 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::{ 15 | collections::HashMap, 16 | env, 17 | future::Future, 18 | io::BufReader, 19 | sync::{ 20 | atomic::{AtomicUsize, Ordering}, 21 | Arc, 22 | }, 23 | }; 24 | 25 | use ntex::{ 26 | io::IoBoxed, 27 | service::{chain_factory, fn_factory_with_config, fn_service}, 28 | util::{ByteString, Bytes, Ready}, 29 | ServiceFactory, 30 | }; 31 | use ntex_mqtt::{v3, v5, MqttError, MqttServer}; 32 | use ntex_tls::rustls::TlsAcceptor; 33 | use rustls::{ 34 | pki_types::{CertificateDer, PrivateKeyDer}, 35 | server::WebPkiClientVerifier, 36 | RootCertStore, ServerConfig, 37 | }; 38 | use secrecy::ExposeSecret; 39 | use serde_json::Value; 40 | use tokio::task::JoinHandle; 41 | use zenoh::{ 42 | bytes::{Encoding, ZBytes}, 43 | internal::{ 44 | plugins::{RunningPluginTrait, ZenohPlugin}, 45 | runtime::Runtime, 46 | zerror, 47 | }, 48 | key_expr::keyexpr, 49 | query::Query, 50 | try_init_log_from_env, Error as ZError, Result as ZResult, Session, Wait, 51 | }; 52 | use zenoh_plugin_trait::{plugin_long_version, plugin_version, Plugin, PluginControl}; 53 | 54 | pub mod config; 55 | mod mqtt_helpers; 56 | mod mqtt_session_state; 57 | use config::{AuthConfig, Config, TLSConfig}; 58 | use mqtt_session_state::MqttSessionState; 59 | 60 | lazy_static::lazy_static! { 61 | static ref WORK_THREAD_NUM: AtomicUsize = AtomicUsize::new(config::DEFAULT_WORK_THREAD_NUM); 62 | static ref MAX_BLOCK_THREAD_NUM: AtomicUsize = AtomicUsize::new(config::DEFAULT_MAX_BLOCK_THREAD_NUM); 63 | // The global runtime is used in the dynamic plugins, which we can't get the current runtime 64 | static ref TOKIO_RUNTIME: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread() 65 | .worker_threads(WORK_THREAD_NUM.load(Ordering::SeqCst)) 66 | .max_blocking_threads(MAX_BLOCK_THREAD_NUM.load(Ordering::SeqCst)) 67 | .enable_all() 68 | .build() 69 | .expect("Unable to create runtime"); 70 | } 71 | #[inline(always)] 72 | pub(crate) fn spawn_runtime(task: F) -> JoinHandle 73 | where 74 | F: Future + Send + 'static, 75 | F::Output: Send + 'static, 76 | { 77 | // Check whether able to get the current runtime 78 | match tokio::runtime::Handle::try_current() { 79 | Ok(rt) => { 80 | // Able to get the current runtime (standalone binary), spawn on the current runtime 81 | rt.spawn(task) 82 | } 83 | Err(_) => { 84 | // Unable to get the current runtime (dynamic plugins), spawn on the global runtime 85 | TOKIO_RUNTIME.spawn(task) 86 | } 87 | } 88 | } 89 | 90 | lazy_static::lazy_static! { 91 | static ref KE_PREFIX_ADMIN_SPACE: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("@") }; 92 | static ref KE_PREFIX_MQTT: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("mqtt") }; 93 | static ref ADMIN_SPACE_KE_VERSION: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("version") }; 94 | static ref ADMIN_SPACE_KE_CONFIG: &'static keyexpr = unsafe { keyexpr::from_str_unchecked("config") }; 95 | } 96 | 97 | #[cfg(feature = "dynamic_plugin")] 98 | zenoh_plugin_trait::declare_plugin!(MqttPlugin); 99 | 100 | pub struct MqttPlugin { 101 | _drop: flume::Sender<()>, 102 | } 103 | 104 | // Authentication types 105 | type User = Vec; 106 | type Password = Vec; 107 | 108 | impl ZenohPlugin for MqttPlugin {} 109 | impl Plugin for MqttPlugin { 110 | type StartArgs = Runtime; 111 | type Instance = zenoh::internal::plugins::RunningPlugin; 112 | 113 | const DEFAULT_NAME: &'static str = "mqtt"; 114 | const PLUGIN_LONG_VERSION: &'static str = plugin_long_version!(); 115 | const PLUGIN_VERSION: &'static str = plugin_version!(); 116 | 117 | fn start( 118 | name: &str, 119 | runtime: &Self::StartArgs, 120 | ) -> ZResult { 121 | // Try to initiate login. 122 | // Required in case of dynamic lib, otherwise no logs. 123 | // But cannot be done twice in case of static link. 124 | try_init_log_from_env(); 125 | 126 | let runtime_conf = runtime.config().lock(); 127 | let plugin_conf = runtime_conf 128 | .plugin(name) 129 | .ok_or_else(|| zerror!("Plugin `{}`: missing config", name))?; 130 | let config: Config = serde_json::from_value(plugin_conf.clone()) 131 | .map_err(|e| zerror!("Plugin `{}` configuration error: {}", name, e))?; 132 | 133 | let tls_config = match config.tls.as_ref() { 134 | Some(tls) => Some(create_tls_config(tls)?), 135 | None => None, 136 | }; 137 | 138 | let auth_dictionary = match config.auth.as_ref() { 139 | Some(auth) => Some(create_auth_dictionary(auth)?), 140 | None => None, 141 | }; 142 | WORK_THREAD_NUM.store(config.work_thread_num, Ordering::SeqCst); 143 | MAX_BLOCK_THREAD_NUM.store(config.max_block_thread_num, Ordering::SeqCst); 144 | 145 | let (tx, rx) = flume::bounded(0); 146 | 147 | spawn_runtime(run( 148 | runtime.clone(), 149 | config, 150 | tls_config, 151 | auth_dictionary, 152 | rx, 153 | )); 154 | 155 | Ok(Box::new(MqttPlugin { _drop: tx })) 156 | } 157 | } 158 | 159 | impl PluginControl for MqttPlugin {} 160 | impl RunningPluginTrait for MqttPlugin {} 161 | 162 | async fn run( 163 | runtime: Runtime, 164 | config: Config, 165 | tls_config: Option>, 166 | auth_dictionary: Option>, 167 | rx: flume::Receiver<()>, 168 | ) { 169 | // Try to initiate login. 170 | // Required in case of dynamic lib, otherwise no logs. 171 | // But cannot be done twice in case of static link. 172 | try_init_log_from_env(); 173 | tracing::debug!("MQTT plugin {}", MqttPlugin::PLUGIN_LONG_VERSION); 174 | tracing::info!("MQTT plugin {:?}", config); 175 | 176 | // init Zenoh Session with provided Runtime 177 | let zsession = match zenoh::session::init(runtime) 178 | .aggregated_subscribers(config.generalise_subs.clone()) 179 | .aggregated_publishers(config.generalise_pubs.clone()) 180 | .await 181 | { 182 | Ok(session) => Arc::new(session), 183 | Err(e) => { 184 | tracing::error!("Unable to init zenoh session for MQTT plugin : {:?}", e); 185 | return; 186 | } 187 | }; 188 | 189 | // declare admin space queryable 190 | let admin_keyexpr_prefix = 191 | *KE_PREFIX_ADMIN_SPACE / &zsession.zid().into_keyexpr() / *KE_PREFIX_MQTT; 192 | let admin_keyexpr_expr = (&admin_keyexpr_prefix) / unsafe { keyexpr::from_str_unchecked("**") }; 193 | tracing::debug!("Declare admin space on {}", admin_keyexpr_expr); 194 | let config2 = config.clone(); 195 | let _admin_queryable = zsession 196 | .declare_queryable(admin_keyexpr_expr) 197 | .callback(move |query| treat_admin_query(query, &admin_keyexpr_prefix, &config2)) 198 | .await 199 | .expect("Failed to create AdminSpace queryable"); 200 | 201 | if auth_dictionary.is_some() && tls_config.is_none() { 202 | tracing::warn!( 203 | "Warning: MQTT client username/password authentication enabled without TLS!" 204 | ); 205 | } 206 | 207 | // Start MQTT Server task 208 | let config = Arc::new(config); 209 | let auth_dictionary = Arc::new(auth_dictionary); 210 | 211 | // The future inside the `run_local` is !SEND, so we can't spawn it directly in tokio runtime. 212 | // Therefore, we dedicate a blocking thread to `block_on` ntex server. 213 | tokio::task::spawn_blocking(|| { 214 | // The ntex server is using `LocalSet` to spawn async task, but Zenoh doesn't allow using tokio current_thread runtime / LocalSet. 215 | // Zenoh use `block_in_place` to drop the session 216 | // https://github.com/eclipse-zenoh/zenoh/blob/658cdd9bc419c03e6757c4d34da530b951b980e4/commons/zenoh-runtime/src/lib.rs#L131 217 | // tokio `block_in_place` doesn't allow using current_thread runtime / LocalSet 218 | // https://github.com/tokio-rs/tokio/blob/91169992b2ed0cf8844dbaa0b4024f9db588a629/tokio/src/runtime/scheduler/multi_thread/worker.rs#L387 219 | // To have a workaround, we should avoid Zenoh session drop inside the ntex server 220 | // (This might happen if MQTT fails to bind the port and it will drop the Session directly) 221 | // Therefore, we keep a Arc outside and only drop after ntex server dies. 222 | let session = zsession.clone(); 223 | let rt = tokio::runtime::Handle::try_current() 224 | .expect("Unable to get the current runtime, which should not happen."); 225 | 226 | let fut = ntex::rt::System::new(MqttPlugin::DEFAULT_NAME).run_local(async move { 227 | let server = match tls_config { 228 | Some(tls) => { 229 | ntex::server::Server::build().bind("mqtt", config.port.clone(), move |_| { 230 | chain_factory(TlsAcceptor::new(tls.clone())) 231 | .map_err(|err| MqttError::Service(MqttPluginError::from(err))) 232 | .and_then(create_mqtt_server( 233 | zsession.clone(), 234 | config.clone(), 235 | auth_dictionary.clone(), 236 | )) 237 | })? 238 | } 239 | None => { 240 | ntex::server::Server::build().bind("mqtt", config.port.clone(), move |_| { 241 | create_mqtt_server( 242 | zsession.clone(), 243 | config.clone(), 244 | auth_dictionary.clone(), 245 | ) 246 | })? 247 | } 248 | }; 249 | // Disable catching the signal inside the ntex, or we can't stop the plugin. 250 | tokio::select! { 251 | _ = server.workers(1).disable_signals().run() => { 252 | tracing::debug!("Server done"); 253 | std::io::Result::Ok(()) 254 | } 255 | _ = rx.recv_async() => { 256 | tracing::debug!("Shutting down..."); 257 | std::io::Result::Ok(()) 258 | } 259 | } 260 | }); 261 | 262 | if let Err(e) = rt.block_on(fut) { 263 | tracing::error!("Unable to start MQTT server: {:?}", e); 264 | } 265 | // Drop the the session explicitly 266 | drop(session); 267 | }); 268 | } 269 | 270 | fn create_tls_config(config: &TLSConfig) -> ZResult> { 271 | let key_bytes = match ( 272 | config.server_private_key.as_ref(), 273 | config.server_private_key_base64.as_ref(), 274 | ) { 275 | (Some(file), None) => { 276 | std::fs::read(file).map_err(|e| zerror!("Invalid private key file: {e:?}"))? 277 | } 278 | (None, Some(base64)) => base64_decode(base64.expose_secret())?, 279 | (None, None) => { 280 | return Err(zerror!( 281 | "Either 'server_private_key' or 'server_private_key_base64' must be present!" 282 | ) 283 | .into()); 284 | } 285 | _ => { 286 | return Err(zerror!( 287 | "Only one of 'server_private_key' and 'server_private_key_base64' can be present!" 288 | ) 289 | .into()); 290 | } 291 | }; 292 | let key = load_private_key(key_bytes)?; 293 | 294 | let certs_bytes = match ( 295 | config.server_certificate.as_ref(), 296 | config.server_certificate_base64.as_ref(), 297 | ) { 298 | (Some(file), None) => { 299 | std::fs::read(file).map_err(|e| zerror!("Invalid certificate file: {e:?}"))? 300 | } 301 | (None, Some(base64)) => base64_decode(base64.expose_secret())?, 302 | (None, None) => { 303 | return Err(zerror!( 304 | "Either 'server_certificate' or 'server_certificate_base64' must be present!" 305 | ) 306 | .into()); 307 | } 308 | _ => { 309 | return Err(zerror!( 310 | "Only one of 'server_certificate' and 'server_certificate_base64' can be present!" 311 | ) 312 | .into()); 313 | } 314 | }; 315 | let certs = load_certs(certs_bytes)?; 316 | 317 | // Providing a root CA certificate is optional - when provided clients will be verified 318 | let rootca_bytes = match ( 319 | config.root_ca_certificate.as_ref(), 320 | config.root_ca_certificate_base64.as_ref(), 321 | ) { 322 | (Some(file), None) => { 323 | Some(std::fs::read(file).map_err(|e| zerror!("Invalid root certificate file: {e:?}"))?) 324 | } 325 | (None, Some(base64)) => Some(base64_decode(base64.expose_secret())?), 326 | (None, None) => None, 327 | _ => { 328 | return Err(zerror!("Only one of 'root_ca_certificate' and 'root_ca_certificate_base64' can be present!").into()); 329 | } 330 | }; 331 | 332 | let tls_config = match rootca_bytes { 333 | Some(bytes) => { 334 | let root_cert_store = load_trust_anchors(bytes)?; 335 | 336 | ServerConfig::builder() 337 | .with_client_cert_verifier( 338 | WebPkiClientVerifier::builder(root_cert_store.into()).build()?, 339 | ) 340 | .with_single_cert(certs, key)? 341 | } 342 | None => ServerConfig::builder() 343 | .with_no_client_auth() 344 | .with_single_cert(certs, key)?, 345 | }; 346 | Ok(Arc::new(tls_config)) 347 | } 348 | 349 | pub fn base64_decode(data: &str) -> ZResult> { 350 | use base64::{engine::general_purpose, Engine}; 351 | Ok(general_purpose::STANDARD 352 | .decode(data) 353 | .map_err(|e| zerror!("Unable to perform base64 decoding: {e:?}"))?) 354 | } 355 | 356 | fn load_private_key(bytes: Vec) -> ZResult> { 357 | let mut reader = BufReader::new(bytes.as_slice()); 358 | 359 | loop { 360 | match rustls_pemfile::read_one(&mut reader) { 361 | Ok(item) => match item { 362 | Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(key.into()), 363 | Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(key.into()), 364 | Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(key.into()), 365 | None => break, 366 | _ => continue, 367 | }, 368 | Err(e) => return Err(zerror!("Cannot parse private key: {e:?}").into()), 369 | } 370 | } 371 | Err(zerror!("No supported private keys found").into()) 372 | } 373 | 374 | fn load_certs(bytes: Vec) -> ZResult>> { 375 | let mut reader = BufReader::new(bytes.as_slice()); 376 | 377 | let certs: Vec> = rustls_pemfile::certs(&mut reader) 378 | .collect::>() 379 | .map_err(|err| zerror!("Error processing client certificate: {err}."))?; 380 | 381 | match certs.is_empty() { 382 | true => Err(zerror!("No certificates found").into()), 383 | false => Ok(certs), 384 | } 385 | } 386 | 387 | fn load_trust_anchors(bytes: Vec) -> ZResult { 388 | let mut root_cert_store = RootCertStore::empty(); 389 | let roots = load_certs(bytes)?; 390 | for root in roots { 391 | root_cert_store.add(root)?; 392 | } 393 | Ok(root_cert_store) 394 | } 395 | 396 | fn create_auth_dictionary(config: &AuthConfig) -> ZResult> { 397 | let mut dictionary: HashMap = HashMap::new(); 398 | let content = std::fs::read_to_string(config.dictionary_file.as_str()) 399 | .map_err(|e| zerror!("Invalid user/password dictionary file: {}", e))?; 400 | 401 | // Populate the user/password dictionary 402 | // The dictionary file is expected to be in the form of: 403 | // usr1:pwd1 404 | // usr2:pwd2 405 | // usr3:pwd3 406 | for line in content.lines() { 407 | let idx = line 408 | .find(':') 409 | .ok_or_else(|| zerror!("Invalid user/password dictionary file: invalid format"))?; 410 | let user = line[..idx].as_bytes().to_owned(); 411 | if user.is_empty() { 412 | return Err(zerror!("Invalid user/password dictionary file: empty user").into()); 413 | } 414 | let password = line[idx + 1..].as_bytes().to_owned(); 415 | if password.is_empty() { 416 | return Err(zerror!("Invalid user/password dictionary file: empty password").into()); 417 | } 418 | dictionary.insert(user, password); 419 | } 420 | Ok(dictionary) 421 | } 422 | 423 | fn is_authorized( 424 | dictionary: Option<&HashMap>, 425 | usr: Option<&ByteString>, 426 | pwd: Option<&Bytes>, 427 | ) -> Result<(), String> { 428 | match (dictionary, usr, pwd) { 429 | // No user/password dictionary - all clients authorized to connect 430 | (None, _, _) => Ok(()), 431 | // User/password dictionary provided - clients must provide credentials to connect 432 | (Some(dictionary), Some(usr), Some(pwd)) => { 433 | match dictionary.get(&usr.as_bytes().to_vec()) { 434 | Some(expected_pwd) => { 435 | if pwd == expected_pwd { 436 | Ok(()) 437 | } else { 438 | Err(format!("Incorrect password for user {usr:?}")) 439 | } 440 | } 441 | None => Err(format!("Unknown user {usr:?}")), 442 | } 443 | } 444 | (Some(_), Some(usr), None) => Err(format!("Missing password for user {usr:?}")), 445 | (Some(_), None, Some(_)) => Err(("Missing user name").to_string()), 446 | (Some(_), None, None) => Err(("Missing user credentials").to_string()), 447 | } 448 | } 449 | 450 | #[allow(clippy::type_complexity)] 451 | fn create_mqtt_server( 452 | session: Arc, 453 | config: Arc, 454 | auth_dictionary: Arc>>, 455 | ) -> MqttServer< 456 | impl ServiceFactory, InitError = ()>, 457 | impl ServiceFactory, InitError = ()>, 458 | MqttPluginError, 459 | (), 460 | > { 461 | let zs_v3 = session.clone(); 462 | let zs_v5 = session.clone(); 463 | let config_v3 = config.clone(); 464 | let config_v5 = config.clone(); 465 | let auth_dictionary_v3 = auth_dictionary.clone(); 466 | let auth_dictionary_v5 = auth_dictionary.clone(); 467 | 468 | MqttServer::new() 469 | .v3(v3::MqttServer::new(fn_factory_with_config(move |_| { 470 | let zs = zs_v3.clone(); 471 | let config = config_v3.clone(); 472 | let auth_dictionary = auth_dictionary_v3.clone(); 473 | Ready::Ok::<_, ()>(fn_service(move |h| { 474 | handshake_v3(h, zs.clone(), config.clone(), auth_dictionary.clone()) 475 | })) 476 | })) 477 | .publish(fn_factory_with_config( 478 | |session: v3::Session| { 479 | Ready::Ok::<_, MqttPluginError>(fn_service(move |req| { 480 | publish_v3(session.clone(), req) 481 | })) 482 | }, 483 | )) 484 | .control(fn_factory_with_config( 485 | |session: v3::Session| { 486 | Ready::Ok::<_, MqttPluginError>(fn_service(move |req| { 487 | control_v3(session.clone(), req) 488 | })) 489 | }, 490 | ))) 491 | .v5(v5::MqttServer::new(fn_factory_with_config(move |_| { 492 | let zs = zs_v5.clone(); 493 | let config = config_v5.clone(); 494 | let auth_dictionary = auth_dictionary_v5.clone(); 495 | Ready::Ok::<_, ()>(fn_service(move |h| { 496 | handshake_v5(h, zs.clone(), config.clone(), auth_dictionary.clone()) 497 | })) 498 | })) 499 | .publish(fn_factory_with_config( 500 | |session: v5::Session| { 501 | Ready::Ok::<_, MqttPluginError>(fn_service(move |req| { 502 | publish_v5(session.clone(), req) 503 | })) 504 | }, 505 | )) 506 | .control(fn_factory_with_config( 507 | |session: v5::Session| { 508 | Ready::Ok::<_, MqttPluginError>(fn_service(move |req| { 509 | control_v5(session.clone(), req) 510 | })) 511 | }, 512 | ))) 513 | } 514 | 515 | fn treat_admin_query(query: Query, admin_keyexpr_prefix: &keyexpr, config: &Config) { 516 | let selector = query.selector(); 517 | tracing::debug!("Query on admin space: {:?}", selector); 518 | 519 | // get the list of sub-key expressions that will match the same stored keys than 520 | // the selector, if those keys had the admin_keyexpr_prefix. 521 | let sub_kes = selector.key_expr().strip_prefix(admin_keyexpr_prefix); 522 | if sub_kes.is_empty() { 523 | tracing::error!("Received query for admin space: '{}' - but it's not prefixed by admin_keyexpr_prefix='{}'", selector, admin_keyexpr_prefix); 524 | return; 525 | } 526 | 527 | // Get all matching keys/values 528 | let mut kvs: Vec<(&keyexpr, Value)> = Vec::with_capacity(sub_kes.len()); 529 | for sub_ke in sub_kes { 530 | if sub_ke.intersects(&ADMIN_SPACE_KE_VERSION) { 531 | kvs.push(( 532 | &ADMIN_SPACE_KE_VERSION, 533 | Value::String(MqttPlugin::PLUGIN_LONG_VERSION.to_string()), 534 | )); 535 | } 536 | if sub_ke.intersects(&ADMIN_SPACE_KE_CONFIG) { 537 | kvs.push(( 538 | &ADMIN_SPACE_KE_CONFIG, 539 | serde_json::to_value(config).unwrap(), 540 | )); 541 | } 542 | } 543 | 544 | // send replies 545 | for (ke, v) in kvs.drain(..) { 546 | let admin_keyexpr = admin_keyexpr_prefix / ke; 547 | match serde_json::to_vec(&v) { 548 | Ok(bytes) => { 549 | if let Err(e) = query 550 | .reply(admin_keyexpr, ZBytes::from(bytes)) 551 | .encoding(Encoding::APPLICATION_JSON) 552 | .wait() 553 | { 554 | tracing::warn!("Error replying to admin query {:?}: {}", query, e); 555 | } 556 | } 557 | Err(err) => { 558 | tracing::error!("Could not Serialize serde_json::Value to ZBytes {}", err) 559 | } 560 | } 561 | } 562 | } 563 | 564 | // NOTE: this types exists just because we can't implement TryFrom> for v5::PublishAck 565 | // (required for MQTT V5 negative acks) 566 | #[derive(Debug)] 567 | struct MqttPluginError { 568 | err: Box, 569 | } 570 | 571 | impl From for MqttPluginError { 572 | fn from(e: ZError) -> Self { 573 | MqttPluginError { err: e } 574 | } 575 | } 576 | 577 | impl From for MqttPluginError { 578 | fn from(e: std::io::Error) -> Self { 579 | MqttPluginError { err: e.into() } 580 | } 581 | } 582 | 583 | impl From for MqttPluginError { 584 | fn from(e: rustls::Error) -> Self { 585 | MqttPluginError { err: e.into() } 586 | } 587 | } 588 | 589 | impl From for MqttPluginError { 590 | fn from(e: String) -> Self { 591 | MqttPluginError { err: e.into() } 592 | } 593 | } 594 | 595 | // mqtt5 supports negative acks, so service error could be converted to PublishAck 596 | // (weird way to do it, but that's how it's done in ntex-mqtt examples...) 597 | impl std::convert::TryFrom for v5::PublishAck { 598 | type Error = MqttPluginError; 599 | fn try_from(err: MqttPluginError) -> Result { 600 | Err(err) 601 | } 602 | } 603 | 604 | async fn handshake_v3( 605 | handshake: v3::Handshake, 606 | zsession: Arc, 607 | config: Arc, 608 | auth_dictionary: Arc>>, 609 | ) -> Result, MqttPluginError> { 610 | let client_id = handshake.packet().client_id.to_string(); 611 | 612 | match is_authorized( 613 | (*auth_dictionary).as_ref(), 614 | handshake.packet().username.as_ref(), 615 | handshake.packet().password.as_ref(), 616 | ) { 617 | Ok(_) => { 618 | tracing::info!("MQTT client {} connects using v3", client_id); 619 | let session = 620 | MqttSessionState::new(client_id, zsession, config, handshake.sink().into()); 621 | Ok(handshake.ack(session, false)) 622 | } 623 | Err(err) => { 624 | tracing::warn!( 625 | "MQTT client {} connect using v3 rejected: {}", 626 | client_id, 627 | err 628 | ); 629 | Ok(handshake.not_authorized()) 630 | } 631 | } 632 | } 633 | 634 | async fn publish_v3( 635 | session: v3::Session, 636 | publish: v3::Publish, 637 | ) -> Result<(), MqttPluginError> { 638 | session 639 | .state() 640 | .route_mqtt_to_zenoh(publish.topic(), publish.payload()) 641 | .await 642 | .map_err(MqttPluginError::from) 643 | } 644 | 645 | async fn control_v3( 646 | session: v3::Session, 647 | control: v3::Control, 648 | ) -> Result { 649 | tracing::trace!( 650 | "MQTT client {} sent control: {:?}", 651 | session.client_id, 652 | control 653 | ); 654 | 655 | match control { 656 | v3::Control::Ping(ref msg) => Ok(msg.ack()), 657 | v3::Control::Disconnect(msg) => { 658 | tracing::debug!("MQTT client {} disconnected", session.client_id); 659 | session.sink().close(); 660 | Ok(msg.ack()) 661 | } 662 | v3::Control::Subscribe(mut msg) => { 663 | for mut s in msg.iter_mut() { 664 | let topic = s.topic().as_str(); 665 | tracing::debug!( 666 | "MQTT client {} subscribes to '{}'", 667 | session.client_id, 668 | topic 669 | ); 670 | match session.state().map_mqtt_subscription(topic).await { 671 | Ok(()) => s.confirm(v3::QoS::AtMostOnce), 672 | Err(e) => { 673 | tracing::error!("Subscription to '{}' failed: {}", topic, e); 674 | s.fail() 675 | } 676 | } 677 | } 678 | Ok(msg.ack()) 679 | } 680 | v3::Control::Unsubscribe(msg) => { 681 | for topic in msg.iter() { 682 | tracing::debug!( 683 | "MQTT client {} unsubscribes from '{}'", 684 | session.client_id, 685 | topic.as_str() 686 | ); 687 | } 688 | Ok(msg.ack()) 689 | } 690 | v3::Control::WrBackpressure(msg) => { 691 | tracing::debug!( 692 | "MQTT client {} WrBackpressure received: {}", 693 | session.client_id, 694 | msg.enabled() 695 | ); 696 | Ok(msg.ack()) 697 | } 698 | v3::Control::Closed(msg) => { 699 | tracing::debug!("MQTT client {} closed connection", session.client_id); 700 | session.sink().force_close(); 701 | Ok(msg.ack()) 702 | } 703 | v3::Control::Error(msg) => { 704 | tracing::warn!( 705 | "MQTT client {} Error received: {}", 706 | session.client_id, 707 | msg.get_ref().err 708 | ); 709 | Ok(msg.ack()) 710 | } 711 | v3::Control::ProtocolError(ref msg) => { 712 | tracing::warn!( 713 | "MQTT client {}: ProtocolError received: {} => disconnect it", 714 | session.client_id, 715 | msg.get_ref() 716 | ); 717 | Ok(control.disconnect()) 718 | } 719 | v3::Control::PeerGone(msg) => { 720 | tracing::debug!( 721 | "MQTT client {}: PeerGone => close connection", 722 | session.client_id 723 | ); 724 | session.sink().close(); 725 | Ok(msg.ack()) 726 | } 727 | } 728 | } 729 | 730 | async fn handshake_v5( 731 | handshake: v5::Handshake, 732 | zsession: Arc, 733 | config: Arc, 734 | auth_dictionary: Arc>>, 735 | ) -> Result, MqttPluginError> { 736 | let client_id = handshake.packet().client_id.to_string(); 737 | 738 | match is_authorized( 739 | (*auth_dictionary).as_ref(), 740 | handshake.packet().username.as_ref(), 741 | handshake.packet().password.as_ref(), 742 | ) { 743 | Ok(_) => { 744 | tracing::info!("MQTT client {} connects using v5", client_id); 745 | let session = 746 | MqttSessionState::new(client_id, zsession, config, handshake.sink().into()); 747 | Ok(handshake.ack(session)) 748 | } 749 | Err(err) => { 750 | tracing::warn!( 751 | "MQTT client {} connect using v5 rejected: {}", 752 | client_id, 753 | err 754 | ); 755 | Ok(handshake.failed(ntex_mqtt::v5::codec::ConnectAckReason::NotAuthorized)) 756 | } 757 | } 758 | } 759 | 760 | async fn publish_v5( 761 | session: v5::Session, 762 | publish: v5::Publish, 763 | ) -> Result { 764 | session 765 | .state() 766 | .route_mqtt_to_zenoh(publish.topic(), publish.payload()) 767 | .await 768 | .map(|()| publish.ack()) 769 | .map_err(MqttPluginError::from) 770 | } 771 | 772 | async fn control_v5( 773 | session: v5::Session, 774 | control: v5::Control, 775 | ) -> Result { 776 | tracing::trace!( 777 | "MQTT client {} sent control: {:?}", 778 | session.client_id, 779 | control 780 | ); 781 | 782 | use v5::codec::{Disconnect, DisconnectReasonCode}; 783 | match control { 784 | v5::Control::Auth(_) => { 785 | tracing::debug!( 786 | "MQTT client {} wants to authenticate... not yet supported!", 787 | session.client_id 788 | ); 789 | Ok(control.disconnect_with(Disconnect::new( 790 | DisconnectReasonCode::ImplementationSpecificError, 791 | ))) 792 | } 793 | v5::Control::Ping(msg) => Ok(msg.ack()), 794 | v5::Control::Disconnect(msg) => { 795 | tracing::debug!("MQTT client {} disconnected", session.client_id); 796 | session.sink().close(); 797 | Ok(msg.ack()) 798 | } 799 | v5::Control::Subscribe(mut msg) => { 800 | for mut s in msg.iter_mut() { 801 | let topic = s.topic().as_str(); 802 | tracing::debug!( 803 | "MQTT client {} subscribes to '{}'", 804 | session.client_id, 805 | topic 806 | ); 807 | match session.state().map_mqtt_subscription(topic).await { 808 | Ok(()) => s.confirm(v5::QoS::AtMostOnce), 809 | Err(e) => { 810 | tracing::error!("Subscription to '{}' failed: {}", topic, e); 811 | s.fail(v5::codec::SubscribeAckReason::ImplementationSpecificError) 812 | } 813 | } 814 | } 815 | Ok(msg.ack()) 816 | } 817 | v5::Control::Unsubscribe(msg) => { 818 | for topic in msg.iter() { 819 | tracing::debug!( 820 | "MQTT client {} unsubscribes from '{}'", 821 | session.client_id, 822 | topic.as_str() 823 | ); 824 | } 825 | Ok(msg.ack()) 826 | } 827 | v5::Control::WrBackpressure(msg) => { 828 | tracing::debug!( 829 | "MQTT client {} WrBackpressure received: {}", 830 | session.client_id, 831 | msg.enabled() 832 | ); 833 | Ok(msg.ack()) 834 | } 835 | v5::Control::Closed(msg) => { 836 | tracing::debug!("MQTT client {} closed connection", session.client_id); 837 | session.sink().close(); 838 | Ok(msg.ack()) 839 | } 840 | v5::Control::Error(msg) => { 841 | tracing::warn!( 842 | "MQTT client {} Error received: {}", 843 | session.client_id, 844 | msg.get_ref().err 845 | ); 846 | Ok(msg.ack(DisconnectReasonCode::UnspecifiedError)) 847 | } 848 | v5::Control::ProtocolError(msg) => { 849 | tracing::warn!( 850 | "MQTT client {}: ProtocolError received: {}", 851 | session.client_id, 852 | msg.get_ref() 853 | ); 854 | session.sink().close(); 855 | Ok(msg.reason_code(DisconnectReasonCode::ProtocolError).ack()) 856 | } 857 | v5::Control::PeerGone(msg) => { 858 | tracing::debug!( 859 | "MQTT client {}: PeerGone => close connection", 860 | session.client_id 861 | ); 862 | session.sink().close(); 863 | Ok(msg.ack()) 864 | } 865 | } 866 | } 867 | -------------------------------------------------------------------------------- /zenoh-plugin-mqtt/src/mqtt_helpers.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use std::convert::TryInto; 16 | 17 | use ntex::util::{ByteString, Bytes}; 18 | use ntex_mqtt::{error::SendPacketError, v3, v5}; 19 | use zenoh::{ 20 | internal::bail, 21 | key_expr::{KeyExpr, OwnedKeyExpr}, 22 | Result as ZResult, 23 | }; 24 | 25 | use crate::config::Config; 26 | 27 | const MQTT_SEPARATOR: char = '/'; 28 | const MQTT_EMPTY_LEVEL: &str = "//"; 29 | const MQTT_SINGLE_WILD: char = '+'; 30 | const MQTT_MULTI_WILD: char = '#'; 31 | 32 | pub(crate) fn mqtt_topic_to_ke<'a>( 33 | topic: &'a str, 34 | scope: &Option, 35 | ) -> ZResult> { 36 | if topic.starts_with(MQTT_SEPARATOR) { 37 | bail!( 38 | "MQTT topic with empty level not-supported: '{}' (starts with {})", 39 | topic, 40 | MQTT_SEPARATOR 41 | ); 42 | } 43 | if topic.ends_with(MQTT_SEPARATOR) { 44 | bail!( 45 | "MQTT topic with empty level not-supported: '{}' (ends with {})", 46 | topic, 47 | MQTT_SEPARATOR 48 | ); 49 | } 50 | if topic.contains(MQTT_EMPTY_LEVEL) { 51 | bail!( 52 | "MQTT topic with empty level not-supported: '{}' (contains {})", 53 | topic, 54 | MQTT_EMPTY_LEVEL 55 | ); 56 | } 57 | 58 | let ke: KeyExpr = if !topic.contains([MQTT_SINGLE_WILD, MQTT_MULTI_WILD]) { 59 | topic.try_into()? 60 | } else { 61 | topic 62 | .replace(MQTT_SINGLE_WILD, "*") 63 | .replace(MQTT_MULTI_WILD, "**") 64 | .try_into()? 65 | }; 66 | 67 | match scope { 68 | Some(scope) => Ok((scope / &ke).into()), 69 | None => Ok(ke), 70 | } 71 | } 72 | 73 | pub(crate) fn ke_to_mqtt_topic_publish( 74 | ke: &KeyExpr<'_>, 75 | scope: &Option, 76 | ) -> ZResult { 77 | if ke.is_wild() { 78 | bail!("Zenoh KeyExpr '{}' contains wildcards and cannot be converted to MQTT topic for publications", ke); 79 | } 80 | match scope { 81 | Some(scope) => { 82 | let after_scope_idx = scope.as_str().len(); 83 | if ke.starts_with(scope.as_str()) && ke.chars().nth(after_scope_idx) == Some('/') { 84 | Ok(ke[after_scope_idx + 1..].into()) 85 | } else { 86 | bail!( 87 | "Zenoh KeyExpr '{}' doesn't start with the expected scope '{}'", 88 | ke, 89 | scope 90 | ); 91 | } 92 | } 93 | None => Ok(ke.as_str().into()), 94 | } 95 | } 96 | 97 | pub(crate) fn is_allowed(mqtt_topic: &str, config: &Config) -> bool { 98 | match (&config.allow, &config.deny) { 99 | (Some(allow), None) => allow.is_match(mqtt_topic), 100 | (None, Some(deny)) => !deny.is_match(mqtt_topic), 101 | (Some(allow), Some(deny)) => allow.is_match(mqtt_topic) && !deny.is_match(mqtt_topic), 102 | (None, None) => true, 103 | } 104 | } 105 | 106 | #[derive(Clone, Debug)] 107 | pub(crate) enum MqttSink { 108 | V3(v3::MqttSink), 109 | V5(v5::MqttSink), 110 | } 111 | 112 | impl MqttSink { 113 | pub(crate) fn publish_at_most_once( 114 | &self, 115 | topic: U, 116 | payload: Bytes, 117 | ) -> Result<(), SendPacketError> 118 | where 119 | ByteString: From, 120 | { 121 | match self { 122 | MqttSink::V3(sink) => sink.publish(topic, payload).send_at_most_once(), 123 | MqttSink::V5(sink) => sink.publish(topic, payload).send_at_most_once(), 124 | } 125 | } 126 | 127 | pub(crate) fn close(&self) { 128 | match self { 129 | MqttSink::V3(sink) => { 130 | sink.close(); 131 | } 132 | MqttSink::V5(sink) => { 133 | sink.close(); 134 | } 135 | } 136 | } 137 | 138 | pub(crate) fn is_open(&self) -> bool { 139 | match self { 140 | MqttSink::V3(sink) => sink.is_open(), 141 | MqttSink::V5(sink) => sink.is_open(), 142 | } 143 | } 144 | } 145 | 146 | impl From for MqttSink { 147 | fn from(s: v3::MqttSink) -> Self { 148 | MqttSink::V3(s) 149 | } 150 | } 151 | 152 | impl From for MqttSink { 153 | fn from(s: v5::MqttSink) -> Self { 154 | MqttSink::V5(s) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /zenoh-plugin-mqtt/src/mqtt_session_state.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::{collections::HashMap, convert::TryInto, sync::Arc}; 15 | 16 | use flume::{Receiver, Sender}; 17 | use lazy_static::__Deref; 18 | use ntex::util::{ByteString, Bytes}; 19 | use tokio::sync::RwLock; 20 | use zenoh::{ 21 | internal::zerror, 22 | key_expr::KeyExpr, 23 | pubsub::Subscriber, 24 | sample::{Locality, Sample}, 25 | Result as ZResult, Session, 26 | }; 27 | 28 | use crate::{config::Config, mqtt_helpers::*}; 29 | 30 | #[derive(Debug)] 31 | pub(crate) struct MqttSessionState { 32 | pub(crate) client_id: String, 33 | pub(crate) zsession: Arc, 34 | pub(crate) config: Arc, 35 | pub(crate) subs: RwLock>>, 36 | pub(crate) tx: Sender<(ByteString, Bytes)>, 37 | } 38 | 39 | impl MqttSessionState { 40 | pub(crate) fn new( 41 | client_id: String, 42 | zsession: Arc, 43 | config: Arc, 44 | sink: MqttSink, 45 | ) -> MqttSessionState { 46 | let (tx, rx) = flume::bounded::<(ByteString, Bytes)>(config.tx_channel_size); 47 | spawn_mqtt_publisher(client_id.clone(), rx, sink); 48 | 49 | MqttSessionState { 50 | client_id, 51 | zsession, 52 | config, 53 | subs: RwLock::new(HashMap::new()), 54 | tx, 55 | } 56 | } 57 | 58 | pub(crate) async fn map_mqtt_subscription(&self, topic: &str) -> ZResult<()> { 59 | let sub_origin = if is_allowed(topic, &self.config) { 60 | // if topic is allowed, subscribe to publications coming from anywhere 61 | Locality::Any 62 | } else { 63 | // if topic is NOT allowed, subscribe to publications coming only from this plugin (for MQTT-to-MQTT routing only) 64 | tracing::debug!( 65 | "MQTT Client {}: topic '{}' is not allowed to be routed over Zenoh (see your 'allow' or 'deny' configuration) - re-publish only from MQTT publishers", 66 | self.client_id, 67 | topic 68 | ); 69 | Locality::SessionLocal 70 | }; 71 | 72 | let mut subs = self.subs.write().await; 73 | if !subs.contains_key(topic) { 74 | let ke = mqtt_topic_to_ke(topic, &self.config.scope)?; 75 | let client_id = self.client_id.clone(); 76 | let config = self.config.clone(); 77 | let tx = self.tx.clone(); 78 | let sub = self 79 | .zsession 80 | .declare_subscriber(ke) 81 | .callback(move |sample| { 82 | if let Err(e) = route_zenoh_to_mqtt(sample, &client_id, &config, &tx) { 83 | tracing::warn!("{}", e); 84 | } 85 | }) 86 | .allowed_origin(sub_origin) 87 | .await?; 88 | subs.insert(topic.into(), sub); 89 | Ok(()) 90 | } else { 91 | tracing::debug!( 92 | "MQTT Client {} already subscribes to {} => ignore", 93 | self.client_id, 94 | topic 95 | ); 96 | Ok(()) 97 | } 98 | } 99 | 100 | pub(crate) async fn route_mqtt_to_zenoh( 101 | &self, 102 | mqtt_topic: &ntex::router::Path, 103 | payload: &Bytes, 104 | ) -> ZResult<()> { 105 | let topic = mqtt_topic.get_ref().as_str(); 106 | let destination = if is_allowed(topic, &self.config) { 107 | // if topic is allowed, publish to anywhere 108 | Locality::Any 109 | } else { 110 | // if topic is NOT allowed, publish only to this plugin (for MQTT-to-MQTT routing only) 111 | tracing::trace!( 112 | "MQTT Client {}: topic '{}' is not allowed to be routed over Zenoh (see your 'allow' or 'deny' configuration) - re-publish only to MQTT subscriber", 113 | self.client_id, 114 | topic 115 | ); 116 | Locality::SessionLocal 117 | }; 118 | 119 | let ke: KeyExpr = if let Some(scope) = &self.config.scope { 120 | (scope / topic.try_into()?).into() 121 | } else { 122 | topic.try_into()? 123 | }; 124 | // TODO: check allow/deny 125 | tracing::trace!( 126 | "MQTT client {}: route from MQTT '{}' to Zenoh '{}'", 127 | self.client_id, 128 | topic, 129 | ke, 130 | ); 131 | self.zsession 132 | .put(ke, payload.deref()) 133 | .allowed_destination(destination) 134 | .await 135 | } 136 | } 137 | 138 | fn route_zenoh_to_mqtt( 139 | sample: Sample, 140 | client_id: &str, 141 | config: &Config, 142 | tx: &Sender<(ByteString, Bytes)>, 143 | ) -> ZResult<()> { 144 | let topic = ke_to_mqtt_topic_publish(sample.key_expr(), &config.scope)?; 145 | tracing::trace!( 146 | "MQTT client {}: route from Zenoh '{}' to MQTT '{}'", 147 | client_id, 148 | sample.key_expr(), 149 | topic 150 | ); 151 | let v: Vec<_> = sample.payload().to_bytes().to_vec(); 152 | tx.try_send((topic, v.into())).map_err(|e| { 153 | zerror!( 154 | "MQTT client {}: error re-publishing on MQTT a Zenoh publication on {}: {}", 155 | client_id, 156 | sample.key_expr(), 157 | e 158 | ) 159 | .into() 160 | }) 161 | } 162 | 163 | fn spawn_mqtt_publisher(client_id: String, rx: Receiver<(ByteString, Bytes)>, sink: MqttSink) { 164 | ntex::rt::spawn(async move { 165 | loop { 166 | match rx.recv_async().await { 167 | Ok((topic, payload)) => { 168 | if sink.is_open() { 169 | if let Err(e) = sink.publish_at_most_once(topic, payload) { 170 | tracing::trace!( 171 | "Failed to send MQTT message for client {} - {}", 172 | client_id, 173 | e 174 | ); 175 | sink.close(); 176 | break; 177 | } 178 | } else { 179 | tracing::trace!("MQTT sink closed for client {}", client_id); 180 | break; 181 | } 182 | } 183 | Err(_) => { 184 | tracing::trace!("MPSC Channel closed for client {}", client_id); 185 | break; 186 | } 187 | } 188 | } 189 | }); 190 | } 191 | -------------------------------------------------------------------------------- /zenoh-plugin-mqtt/tests/test.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::{ 15 | sync::mpsc::{channel, Sender}, 16 | time::Duration, 17 | }; 18 | 19 | use ntex::{ 20 | service::fn_service, 21 | time::{sleep, Millis}, 22 | util::Ready, 23 | }; 24 | use ntex_mqtt::v5; 25 | use zenoh::{ 26 | config::Config, 27 | internal::{plugins::PluginsManager, runtime::RuntimeBuilder}, 28 | Wait, 29 | }; 30 | use zenoh_config::ModeDependentValue; 31 | 32 | // The test topic 33 | const TEST_TOPIC: &str = "test-topic"; 34 | // The test payload 35 | const TEST_PAYLOAD: &str = "Hello World"; 36 | 37 | #[derive(Debug)] 38 | struct Error; 39 | 40 | impl std::convert::TryFrom for v5::PublishAck { 41 | type Error = Error; 42 | 43 | fn try_from(err: Error) -> Result { 44 | Err(err) 45 | } 46 | } 47 | 48 | async fn create_mqtt_server() { 49 | let mut plugins_mgr = PluginsManager::static_plugins_only(); 50 | plugins_mgr.declare_static_plugin::("mqtt", true); 51 | let mut config = Config::default(); 52 | config.insert_json5("plugins/mqtt", "{}").unwrap(); 53 | config 54 | .timestamping 55 | .set_enabled(Some(ModeDependentValue::Unique(true))) 56 | .unwrap(); 57 | config.adminspace.set_enabled(true).unwrap(); 58 | config.plugins_loading.set_enabled(true).unwrap(); 59 | let mut runtime = RuntimeBuilder::new(config) 60 | .plugins_manager(plugins_mgr) 61 | .build() 62 | .await 63 | .unwrap(); 64 | runtime.start().await.unwrap(); 65 | } 66 | 67 | async fn create_mqtt_subscriber(tx: Sender) { 68 | let client = v5::client::MqttConnector::new("127.0.0.1:1883") 69 | .client_id("mqtt-sub-id") 70 | .connect() 71 | .await 72 | .unwrap(); 73 | 74 | let sink = client.sink(); 75 | 76 | // handle incoming publishes 77 | ntex::rt::spawn(client.start(fn_service( 78 | move |control: v5::client::Control| match control { 79 | v5::client::Control::Publish(publish) => { 80 | println!( 81 | "incoming publish: {:?} -> {:?} payload {:?}", 82 | publish.packet().packet_id, 83 | publish.packet().topic, 84 | publish.packet().payload 85 | ); 86 | let payload = std::str::from_utf8(&publish.packet().payload) 87 | .unwrap() 88 | .to_owned(); 89 | tx.send(payload).unwrap(); 90 | Ready::Ok(publish.ack(v5::codec::PublishAckReason::Success)) 91 | } 92 | v5::client::Control::Disconnect(msg) => { 93 | println!("Server disconnecting: {:?}", msg); 94 | Ready::Ok(msg.ack()) 95 | } 96 | v5::client::Control::Error(msg) => { 97 | println!("Codec error: {:?}", msg); 98 | Ready::Ok(msg.ack(v5::codec::DisconnectReasonCode::UnspecifiedError)) 99 | } 100 | v5::client::Control::ProtocolError(msg) => { 101 | println!("Protocol error: {:?}", msg); 102 | Ready::Ok(msg.ack()) 103 | } 104 | v5::client::Control::PeerGone(msg) => { 105 | println!("Peer closed connection: {:?}", msg.error()); 106 | Ready::Ok(msg.ack()) 107 | } 108 | v5::client::Control::Closed(msg) => { 109 | println!("Server closed connection: {:?}", msg); 110 | Ready::Ok(msg.ack()) 111 | } 112 | }, 113 | ))); 114 | 115 | // subscribe to topic 116 | sink.subscribe(None) 117 | .topic_filter( 118 | TEST_TOPIC.into(), 119 | v5::codec::SubscriptionOptions { 120 | qos: v5::codec::QoS::AtLeastOnce, 121 | no_local: false, 122 | retain_as_published: false, 123 | retain_handling: v5::codec::RetainHandling::AtSubscribe, 124 | }, 125 | ) 126 | .send() 127 | .await 128 | .unwrap(); 129 | // Ensure the data is received 130 | sleep(Millis(3_000)).await; 131 | } 132 | 133 | async fn create_mqtt_publisher() { 134 | let client = v5::client::MqttConnector::new("127.0.0.1:1883") 135 | .client_id("mqtt-pub-id") 136 | .connect() 137 | .await 138 | .unwrap(); 139 | 140 | let sink = client.sink(); 141 | 142 | // handle incoming publishes 143 | ntex::rt::spawn(client.start(fn_service( 144 | |control: v5::client::Control| match control { 145 | v5::client::Control::Publish(publish) => { 146 | println!( 147 | "incoming publish: {:?} -> {:?} payload {:?}", 148 | publish.packet().packet_id, 149 | publish.packet().topic, 150 | publish.packet().payload 151 | ); 152 | Ready::Ok(publish.ack(v5::codec::PublishAckReason::Success)) 153 | } 154 | v5::client::Control::Disconnect(msg) => { 155 | println!("Server disconnecting: {:?}", msg); 156 | Ready::Ok(msg.ack()) 157 | } 158 | v5::client::Control::Error(msg) => { 159 | println!("Codec error: {:?}", msg); 160 | Ready::Ok(msg.ack(v5::codec::DisconnectReasonCode::UnspecifiedError)) 161 | } 162 | v5::client::Control::ProtocolError(msg) => { 163 | println!("Protocol error: {:?}", msg); 164 | Ready::Ok(msg.ack()) 165 | } 166 | v5::client::Control::PeerGone(msg) => { 167 | println!("Peer closed connection: {:?}", msg.error()); 168 | Ready::Ok(msg.ack()) 169 | } 170 | v5::client::Control::Closed(msg) => { 171 | println!("Server closed connection: {:?}", msg); 172 | Ready::Ok(msg.ack()) 173 | } 174 | }, 175 | ))); 176 | 177 | // send client publish 178 | let ack = sink 179 | .publish(TEST_TOPIC, TEST_PAYLOAD.into()) 180 | .send_at_least_once() 181 | .await 182 | .unwrap(); 183 | // Ensure the data is sent 184 | println!("ack received: {:?}", ack); 185 | } 186 | 187 | #[test] 188 | fn test_mqtt_pub_mqtt_sub() { 189 | // Run the bridge for MQTT and Zenoh 190 | let rt = tokio::runtime::Runtime::new().unwrap(); 191 | rt.spawn(create_mqtt_server()); 192 | // Wait for the bridge to be ready 193 | std::thread::sleep(Duration::from_secs(2)); 194 | 195 | // MQTT subscriber 196 | let (tx, rx) = channel(); 197 | rt.spawn_blocking(move || { 198 | ntex::rt::System::new("mqtt_sub").block_on(create_mqtt_subscriber(tx)) 199 | }); 200 | std::thread::sleep(Duration::from_secs(1)); 201 | 202 | // MQTT publisher 203 | rt.spawn_blocking(|| ntex::rt::System::new("mqtt_pub").block_on(create_mqtt_publisher())); 204 | 205 | // Wait for the test to complete 206 | let result = rx.recv_timeout(Duration::from_secs(3)); 207 | 208 | // Stop the tokio runtime 209 | // Since ntex server is running in blocking thread, we need to force shutdown the runtime while completing the test 210 | // Note that we should shutdown the runtime before doing any check that might panic the test. 211 | // Otherwise, there is no way to shutdown ntex server 212 | rt.shutdown_background(); 213 | 214 | let payload = result.expect("Receiver timeout"); 215 | assert_eq!(payload, TEST_PAYLOAD); 216 | } 217 | 218 | #[test] 219 | fn test_mqtt_pub_zenoh_sub() { 220 | // Run the bridge for MQTT and Zenoh 221 | let rt = tokio::runtime::Runtime::new().unwrap(); 222 | rt.spawn(create_mqtt_server()); 223 | // Wait for the bridge to be ready 224 | std::thread::sleep(Duration::from_secs(2)); 225 | 226 | // Zenoh subscriber 227 | let (tx, rx) = channel(); 228 | let session = zenoh::open(zenoh::Config::default()).wait().unwrap(); 229 | let _subscriber = session 230 | .declare_subscriber(TEST_TOPIC) 231 | .callback_mut(move |sample| { 232 | let data = sample 233 | .payload() 234 | .try_to_string() 235 | .to_owned() 236 | .unwrap() 237 | .into_owned(); 238 | tx.send(data).unwrap(); 239 | }) 240 | .wait() 241 | .unwrap(); 242 | std::thread::sleep(Duration::from_secs(1)); 243 | 244 | // MQTT publisher 245 | rt.spawn_blocking(|| ntex::rt::System::new("mqtt_pub").block_on(create_mqtt_publisher())); 246 | 247 | // Wait for the test to complete 248 | let result = rx.recv_timeout(Duration::from_secs(3)); 249 | 250 | // Stop the tokio runtime 251 | // Since ntex server is running in blocking thread, we need to force shutdown the runtime while completing the test 252 | // Note that we should shutdown the runtime before doing any check that might panic the test. 253 | // Otherwise, there is no way to shutdown ntex server 254 | rt.shutdown_background(); 255 | 256 | let payload = result.expect("Receiver timeout"); 257 | assert_eq!(payload, TEST_PAYLOAD); 258 | } 259 | 260 | #[test] 261 | fn test_zenoh_pub_mqtt_sub() { 262 | // Run the bridge for MQTT and Zenoh 263 | let rt = tokio::runtime::Runtime::new().unwrap(); 264 | rt.spawn(create_mqtt_server()); 265 | // Wait for the bridge to be ready 266 | std::thread::sleep(Duration::from_secs(2)); 267 | 268 | // MQTT subscriber 269 | let (tx, rx) = channel(); 270 | rt.spawn_blocking(move || { 271 | ntex::rt::System::new("mqtt_sub").block_on(create_mqtt_subscriber(tx)) 272 | }); 273 | std::thread::sleep(Duration::from_secs(1)); 274 | 275 | // Zenoh publisher 276 | let session = zenoh::open(zenoh::Config::default()).wait().unwrap(); 277 | let publisher = session.declare_publisher(TEST_TOPIC).wait().unwrap(); 278 | publisher.put(TEST_PAYLOAD).wait().unwrap(); 279 | 280 | // Wait for the test to complete 281 | let result = rx.recv_timeout(Duration::from_secs(3)); 282 | 283 | // Stop the tokio runtime 284 | // Since ntex server is running in blocking thread, we need to force shutdown the runtime while completing the test 285 | // Note that we should shutdown the runtime before doing any check that might panic the test. 286 | // Otherwise, there is no way to shutdown ntex server 287 | rt.shutdown_background(); 288 | 289 | let payload = result.expect("Receiver timeout"); 290 | assert_eq!(payload, TEST_PAYLOAD); 291 | } 292 | --------------------------------------------------------------------------------