├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── build-dev-artifacts.yml │ ├── build-release-artifacts.yml │ ├── deploy.yml │ └── docs.yml ├── .gitignore ├── .travis.yml ├── App.toml.sample ├── LICENSE ├── Makefile ├── README.md ├── chart ├── .helmignore ├── Chart.yaml ├── templates │ ├── _helpers.tpl │ ├── app-cm.yaml │ ├── headless-svc.yaml │ ├── network │ │ ├── cluster-svc.yaml │ │ ├── external-svc.yaml │ │ ├── ing.yaml │ │ └── internal-svc.yaml │ ├── pdb.yaml │ ├── rbac │ │ ├── role.yaml │ │ ├── rolebinding.yaml │ │ └── sa.yaml │ ├── sts.yaml │ └── util │ │ └── servicemonitor.yaml └── values.yaml ├── data └── keys │ ├── iam.private_key.pem.sample │ ├── iam.public_key.pem.sample │ ├── svc.private_key.pem.sample │ └── svc.public_key.pem.sample ├── deploy.init.sh ├── docker ├── Dockerfile └── vernemq.conf ├── docs ├── book.toml └── src │ ├── SUMMARY.md │ ├── datatypes.account_id.md │ ├── datatypes.agent_id.md │ ├── datatypes.md │ ├── datatypes.session_id.md │ ├── datatypes.tracking_id.md │ ├── datsatypes.md │ ├── message-properties.application.md │ ├── message-properties.broker.md │ ├── message-properties.md │ ├── messaging-patterns.broadcast.event.md │ ├── messaging-patterns.broadcast.md │ ├── messaging-patterns.md │ ├── messaging-patterns.multicast.event.md │ ├── messaging-patterns.multicast.md │ ├── messaging-patterns.multicast.request.md │ ├── messaging-patterns.unicast.md │ ├── messaging-patterns.unicast.request.md │ ├── messaging-patterns.unicast.response.md │ └── overview.md ├── elvis ├── elvis.config ├── erlang.mk ├── rel ├── sys.config └── vm.args ├── relx.config ├── skaffold.yaml └── src ├── mqttgw.erl ├── mqttgw_app.erl ├── mqttgw_authn.erl ├── mqttgw_authz.erl ├── mqttgw_broker.erl ├── mqttgw_config.erl ├── mqttgw_dyn_srv.erl ├── mqttgw_dynsub.erl ├── mqttgw_http.erl ├── mqttgw_http_subscription.erl ├── mqttgw_id.erl ├── mqttgw_ratelimit.erl ├── mqttgw_ratelimitstate.erl ├── mqttgw_stat.erl ├── mqttgw_state.erl └── mqttgw_sup.erl /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !Makefile 3 | !erlang.mk 4 | !relx.config 5 | !rel 6 | !src 7 | !docker/entrypoint.sh 8 | !docker/vernemq.conf 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [**.{yml,yaml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.github/workflows/build-dev-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Build pre-release artifacts 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | - master 8 | 9 | jobs: 10 | build-chart: 11 | uses: foxford/reusable-workflows/.github/workflows/build-pre-release-chart.yml@master 12 | secrets: 13 | helm_registry_username: ${{ secrets.YANDEX_HELM_USERNAME }} 14 | helm_registry_password: ${{ secrets.YANDEX_HELM_PASSWORD }} 15 | 16 | build-image: 17 | uses: foxford/reusable-workflows/.github/workflows/build-pre-release-image.yml@master 18 | secrets: 19 | docker_registry_username: ${{ secrets.YANDEX_DOCKER_USERNAME }} 20 | docker_registry_password: ${{ secrets.YANDEX_DOCKER_PASSWORD }} 21 | -------------------------------------------------------------------------------- /.github/workflows/build-release-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Build release version of charts and images and push into registry 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | tags: 9 | - '*.*.*' 10 | 11 | jobs: 12 | build-chart: 13 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' 14 | uses: foxford/reusable-workflows/.github/workflows/build-release-chart.yml@master 15 | secrets: 16 | helm_registry_username: ${{ secrets.YANDEX_HELM_USERNAME }} 17 | helm_registry_password: ${{ secrets.YANDEX_HELM_PASSWORD }} 18 | 19 | build-image: 20 | if: startsWith(github.ref, 'refs/tags/') 21 | uses: foxford/reusable-workflows/.github/workflows/build-release-image.yml@master 22 | secrets: 23 | docker_registry_username: ${{ secrets.YANDEX_DOCKER_USERNAME }} 24 | docker_registry_password: ${{ secrets.YANDEX_DOCKER_PASSWORD }} 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | cluster: 7 | description: 'Select cluster' 8 | required: false 9 | default: 'dev' 10 | namespace: 11 | type: choice 12 | description: 'Select namespace' 13 | options: 14 | - s01-classroom-foxford 15 | - s01-minigroup-foxford 16 | - s01-minigroup-b2g 17 | - s01-webinar-foxford 18 | - s01-webinar-b2g 19 | - s01-webinar-tt 20 | - t01 21 | - t02 22 | - t03 23 | version: 24 | description: 'Commit/tag/branch' 25 | required: false 26 | default: 'master' 27 | 28 | jobs: 29 | deploy: 30 | uses: foxford/reusable-workflows/.github/workflows/deploy-via-flux.yml@master 31 | with: 32 | cluster: ${{ inputs.cluster }} 33 | namespace: ${{ inputs.namespace }} 34 | version: ${{ inputs.version }} 35 | secrets: 36 | gh_token: ${{ secrets._GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install awscli 15 | run: sudo apt update && sudo apt install -y awscli 16 | - name: Install mdbook 17 | run : curl -fsSL https://github.com/rust-lang/mdBook/releases/download/v0.4.10/mdbook-v0.4.10-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory $HOME 18 | - name: Upload mdbook 19 | run: ./deploy.init.sh && $HOME/mdbook build docs && ./deploy/ci-mdbook.sh 20 | env: 21 | GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} 22 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 23 | AWS_ENDPOINT: ${{ secrets.AWS_ENDPOINT }} 24 | AWS_REGION: ${{ secrets.AWS_REGION }} 25 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 26 | MDBOOK_BUCKET: docs.netology-group.services.website.yandexcloud.net 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | 7 | # Vim 8 | .*.sw[a-z] 9 | *.un~ 10 | Session.vim 11 | 12 | # Erlang 13 | deps 14 | logs 15 | ebin 16 | doc 17 | log 18 | _rel 19 | relx 20 | erl_crash.dump 21 | .erlang.mk 22 | *.beam 23 | *.plt 24 | *.d 25 | 26 | # Project 27 | App.toml 28 | deploy 29 | /docs/book 30 | .vscode 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 20.3 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - awscli 9 | 10 | services: 11 | - docker 12 | 13 | git: 14 | depth: 1 15 | 16 | jobs: 17 | include: 18 | - stage: check 19 | name: Tests 20 | install: make elvis deps plt 21 | script: make check EUNIT_OPTS=verbose 22 | - stage: build 23 | name: Docs 24 | install: 25 | - curl https://sh.rustup.rs -sSf | sh -s -- -y 26 | - ${HOME}/.cargo/bin/cargo install mdbook --vers ^0.4 27 | script: 28 | - ${TRAVIS_HOME}/.cargo/bin/mdbook build docs 29 | - ./deploy.init.sh 30 | - ./deploy/ci-mdbook.sh 31 | - stage: build 32 | name: Build 33 | script: 34 | - ./deploy.init.sh 35 | - ./deploy/ci-install-tools.sh 36 | - ./deploy/ci-build.sh 37 | 38 | stages: 39 | - name: check 40 | - name: build 41 | if: branch = master AND type = push 42 | 43 | notifications: 44 | email: false 45 | -------------------------------------------------------------------------------- /App.toml.sample: -------------------------------------------------------------------------------- 1 | id = "mqtt-gateway.svc.example.org" 2 | agent_label = "alpha" 3 | 4 | [rate_limit] 5 | message_count = 10 6 | byte_count = 10240 7 | 8 | [authn."iam.svc.example.net"] 9 | audience = ["usr.example.net"] 10 | algorithm = "ES256" 11 | key = "/app/data/keys/iam.public_key.pem.sample" 12 | 13 | [authn."svc.example.org"] 14 | audience = ["svc.example.org"] 15 | algorithm = "ES256" 16 | key = "/app/data/keys/svc.public_key.pem.sample" 17 | 18 | [authz."svc.example.org"] 19 | type = "local" 20 | trusted = ["app.svc.example.org", "devops.svc.example.org"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018-2019 Andrei Nesterov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = mqttgw 2 | PROJECT_DESCRIPTION = Authentication and authorization plugin for VerneMQ 3 | 4 | define PROJECT_ENV 5 | [ 6 | {vmq_plugin_hooks, [ 7 | {mqttgw, auth_on_register, 5, []}, 8 | {mqttgw, auth_on_register_m5, 6, []}, 9 | {mqttgw, auth_on_publish, 6, []}, 10 | {mqttgw, auth_on_publish_m5, 7, []}, 11 | {mqttgw, on_deliver, 6, []}, 12 | {mqttgw, on_deliver_m5, 7, []}, 13 | {mqttgw, auth_on_subscribe, 3, []}, 14 | {mqttgw, auth_on_subscribe_m5, 4, []}, 15 | {mqttgw, on_topic_unsubscribed, 2, []}, 16 | {mqttgw, on_client_offline, 1, []}, 17 | {mqttgw, on_client_gone, 1, []} 18 | ]} 19 | ] 20 | endef 21 | 22 | DEPS = \ 23 | vernemq_dev \ 24 | toml \ 25 | jose \ 26 | uuid \ 27 | cowboy 28 | 29 | dep_vernemq_dev = git https://github.com/erlio/vernemq_dev.git 6d622aa8c901ae7777433aef2bd049e380c474a6 30 | dep_toml = git https://github.com/dozzie/toml.git v0.3.0 31 | dep_jose = git https://github.com/manifest/jose-erlang v0.1.2 32 | dep_uuid = git https://github.com/okeuday/uuid.git v1.7.5 33 | dep_cowboy = git https://github.com/ninenines/cowboy.git 04ca4c5d31a92d4d3de087bbd7d6021dc4a6d409 34 | 35 | DEP_PLUGINS = version.mk 36 | BUILD_DEPS = version.mk 37 | dep_version.mk = git https://github.com/manifest/version.mk.git v0.2.0 38 | 39 | TEST_DEPS = proper 40 | 41 | SHELL_DEPS = tddreloader 42 | SHELL_OPTS = \ 43 | -eval 'application:ensure_all_started($(PROJECT), permanent)' \ 44 | -s tddreloader start \ 45 | -config rel/sys 46 | 47 | include erlang.mk 48 | 49 | .PHONY: elvis 50 | elvis: 51 | ./elvis rock -c elvis.config 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MQTT Gateway 2 | 3 | [![Build Status][travis-img]][travis] 4 | 5 | MQTT Gateway is a VerneMQ plugin with token based (OAuth2 Bearer Token) authentication. 6 | Authorization for publish/subscribe operations is based conventions and dynamic rules. 7 | 8 | 9 | 10 | ### Overview 11 | 12 | #### Authentication 13 | 14 | | Name | Type | Default | Description | 15 | | -------------- | ------ | -------- | ---------------------------------------------------------------- | 16 | | MQTT_CLIENT_ID | String | required | `${AGENT_LABEL}.${ACCOUNT_LABEL}.${AUDIENCE}` | 17 | | MQTT_PASSWORD | String | required | JSON Web Token. Token is required if auhentification is enabled. | 18 | | MQTT_USERNAME | String | optional | The value is ignored | 19 | 20 | 21 | 22 | ### How To Use 23 | 24 | To build and start playing with the application, 25 | execute following shell commands within different terminal tabs: 26 | 27 | ```bash 28 | ## To build container locally 29 | docker build -t netology-group/mqtt-gateway -f docker/Dockerfile . 30 | ## Running a container with VerneMQ and the plugin 31 | docker run -ti --rm \ 32 | -e APP_AUTHN_ENABLED=0 \ 33 | -e APP_AUTHZ_ENABLED=0 \ 34 | -e APP_DYNSUB_ENABLED=0 \ 35 | -e APP_STAT_ENABLED=0 \ 36 | -e APP_RATE_LIMIT_ENABLED=0 \ 37 | -e APP_ACCOUNT_ID=mqtt-gateway.svc.example.org \ 38 | -e APP_AGENT_LABEL=alpha \ 39 | -p 1883:1883 \ 40 | netology-group/mqtt-gateway 41 | 42 | ## Subscribing to messages 43 | mosquitto_sub \ 44 | -i 'test-sub.john-doe.usr.example.net' \ 45 | -u 'v2::default' \ 46 | -t 'foo' | jq '.' 47 | 48 | ## Publishing a message 49 | mosquitto_pub -V 5 \ 50 | -i 'test-pub.john-doe.usr.example.net' \ 51 | -t 'foo' \ 52 | -D connect user-property 'connection_version' 'v2' \ 53 | -D connect user-property 'connection_mode' 'default' \ 54 | -D publish user-property 'label' 'ping' \ 55 | -D publish user-property 'local_timestamp' "$(date +%s000)" \ 56 | -m '{}' 57 | ``` 58 | 59 | 60 | 61 | ### Authentication using Json Web Tokens 62 | 63 | ```bash 64 | ## Authnentication should be enabled in 'App.toml' 65 | docker run -ti --rm \ 66 | -v "$(pwd)/App.toml.sample:/app/App.toml" \ 67 | -v "$(pwd)/data/keys/iam.public_key.pem.sample:/app/data/keys/iam.public_key.pem.sample" \ 68 | -v "$(pwd)/data/keys/svc.public_key.pem.sample:/app/data/keys/svc.public_key.pem.sample" \ 69 | -e APP_CONFIG='/app/App.toml' \ 70 | -p 1883:1883 \ 71 | netology-group/mqtt-gateway 72 | 73 | ## Subscribing to messages 74 | ACCESS_TOKEN='eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJzdmMuZXhhbXBsZS5vcmciLCJpc3MiOiJzdmMuZXhhbXBsZS5vcmciLCJzdWIiOiJhcHAifQ.zevlp8zOKY12Wjm8GBpdF5vvbsMRYYEutJelODi_Fj0yRI8pHk2xTkVtM8Cl5KcxOtJtHIshgqsWoUxrTvrdvA' \ 75 | APP='app.svc.example.org' \ 76 | && mosquitto_sub \ 77 | -i "test.${APP}" \ 78 | -P "${ACCESS_TOKEN}" \ 79 | -u 'v2::service' \ 80 | -t "agents/+/api/v1/out/${APP}" | jq '.' 81 | 82 | ## Publishing a message 83 | ACCESS_TOKEN='eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ1c3IuZXhhbXBsZS5uZXQiLCJpc3MiOiJpYW0uc3ZjLmV4YW1wbGUubmV0Iiwic3ViIjoiam9obi1kb2UifQ.CjwC4qMT9nGt9oJALiGS6FtpZy3-nhX3L3HyM34Q1sL0P73-7X111A56UlbpQmuu5tGte9-Iu0iMJEYlD5XuGA' \ 84 | USER='john-doe.usr.example.net' \ 85 | APP='app.svc.example.org' \ 86 | && mosquitto_pub -V 5 \ 87 | -i "test.${USER}" \ 88 | -P "${ACCESS_TOKEN}" \ 89 | -u 'ignore' \ 90 | -t "agents/test.${USER}/api/v1/out/${APP}" \ 91 | -D connect user-property 'connection_version' 'v2' \ 92 | -D connect user-property 'connection_mode' 'default' \ 93 | -D publish user-property 'label' 'ping' \ 94 | -D publish user-property 'local_timestamp' "$(date +%s000)" \ 95 | -m '{}' 96 | ``` 97 | 98 | 99 | 100 | ### Agent's enter & leave notifications 101 | 102 | ```bash 103 | ## Running a container with VerneMQ and the plugin 104 | docker run -ti --rm \ 105 | -v "$(pwd)/App.toml.sample:/app/App.toml" \ 106 | -e APP_CONFIG='/app/App.toml' \ 107 | -e APP_AUTHN_ENABLED=0 \ 108 | -e APP_AUTHZ_ENABLED=0 \ 109 | -e APP_DYNSUB_ENABLED=0 \ 110 | -e APP_RATE_LIMIT_ENABLED=0 \ 111 | -p 1883:1883 \ 112 | netology-group/mqtt-gateway 113 | 114 | ## Subscribing to messages 115 | OBSERVER='devops.svc.example.org' \ 116 | BROKER='mqtt-gateway.svc.example.org' \ 117 | && mosquitto_sub \ 118 | -i "test-1.${OBSERVER}" \ 119 | -t "apps/${BROKER}/api/v2/audiences/+/events" \ 120 | -P "${ACCESS_TOKEN}" \ 121 | -u 'v2::observer' \ 122 | | jq '.' 123 | 124 | ## Publishing a message 125 | mosquitto_pub -V 5 \ 126 | -i 'test-pub.john-doe.usr.example.net' \ 127 | -t 'foo' \ 128 | -D connect user-property 'connection_version' 'v2' \ 129 | -D connect user-property 'connection_mode' 'default' \ 130 | -D publish user-property 'label' 'ping' \ 131 | -D publish user-property 'local_timestamp' "$(date +%s000)" \ 132 | -n 133 | ``` 134 | 135 | 136 | 137 | ### Dynamic subcriptions to app's events 138 | 139 | ```bash 140 | ## Authorization should be enabled in 'App.toml' 141 | docker run -ti --rm \ 142 | -v "$(pwd)/App.toml.sample:/app/App.toml" \ 143 | -e APP_CONFIG='/app/App.toml' \ 144 | -e APP_AUTHN_ENABLED=0 \ 145 | -e APP_STAT_ENABLED=0 \ 146 | -e APP_RATE_LIMIT_ENABLED=0 \ 147 | -e APP_ACCOUNT_ID=mqtt-gateway.svc.example.org \ 148 | -e APP_AGENT_LABEL=alpha \ 149 | -p 1883:1883 \ 150 | netology-group/mqtt-gateway 151 | 152 | ## Subscribing to the topic of user's incoming messages 153 | USER='john.usr.example.net' \ 154 | APP='app.svc.example.org' \ 155 | && mosquitto_sub \ 156 | -i "test.${USER}" \ 157 | -t "agents/test.${USER}/api/v1/in/${APP}" \ 158 | -u 'v2::default' \ 159 | | jq '.' 160 | 161 | ## Subscribing to the topic of app's incoming responses 162 | OBSERVER='devops.svc.example.org' \ 163 | APP='app.svc.example.org' \ 164 | && mosquitto_sub \ 165 | -i "test-1.${OBSERVER}" \ 166 | -t "agents/alpha.${APP}/api/v1/in/+" \ 167 | -D connect user-property 'connection_version' 'v2' \ 168 | -D connect user-property 'connection_mode' 'observer' \ 169 | | jq '.' 170 | 171 | ## Subscribing to the topic of app's incoming multicast events 172 | OBSERVER='devops.svc.example.org' \ 173 | APP='app.svc.example.org' \ 174 | && mosquitto_sub \ 175 | -i "test-2.${OBSERVER}" \ 176 | -t "agents/+/api/v1/out/${APP}" \ 177 | -D connect user-property 'connection_version' 'v2' \ 178 | -D connect user-property 'connection_mode' 'observer' \ 179 | | jq '.' 180 | 181 | ## Subscribing to the topic of broker's incoming multicast requests 182 | OBSERVER='devops.svc.example.org' \ 183 | BROKER='mqtt-gateway.svc.example.org' \ 184 | && mosquitto_sub \ 185 | -i "test-3.${OBSERVER}" \ 186 | -t "agents/+/api/v1/out/${BROKER}" \ 187 | -D connect user-property 'connection_version' 'v2' \ 188 | -D connect user-property 'connection_mode' 'observer' \ 189 | | jq '.' 190 | 191 | ## Creating a dynamic subscription 192 | APP='app.svc.example.org' \ 193 | USER='john.usr.example.net' \ 194 | BROKER='mqtt-gateway.svc.example.org' \ 195 | && mosquitto_pub -V 5 \ 196 | -i "test.${APP}" \ 197 | -t "agents/test.${APP}/api/v1/out/${BROKER}" \ 198 | -D connect user-property 'connection_version' 'v2' \ 199 | -D connect user-property 'connection_mode' 'service' \ 200 | -D publish user-property 'type' 'request' \ 201 | -D publish user-property 'method' 'subscription.create' \ 202 | -D publish response-topic "agents/alpha.${APP}/api/v1/in/${BROKER}" \ 203 | -D publish correlation-data 'foobar' \ 204 | -m "{\"object\": [\"rooms\", \"ROOM_ID\", \"events\"], \"subject\": \"test.${USER}\"}" 205 | 206 | ## Publishing an event 207 | APP='app.svc.example.org' \ 208 | && mosquitto_pub -V 5 \ 209 | -i "test.${APP}" \ 210 | -t "apps/${APP}/api/v1/rooms/ROOM_ID/events" \ 211 | -D connect user-property 'connection_version' 'v2' \ 212 | -D connect user-property 'connection_mode' 'service' \ 213 | -D publish user-property 'type' 'event' \ 214 | -D publish user-property 'label' 'room.create' \ 215 | -m '{}' 216 | 217 | ## Deleting the dynamic subscription 218 | APP='app.svc.example.org' \ 219 | USER='john.usr.example.net' \ 220 | BROKER='mqtt-gateway.svc.example.org' \ 221 | && mosquitto_pub -V 5 \ 222 | -i "test.${APP}" \ 223 | -t "agents/test.${APP}/api/v1/out/${BROKER}" \ 224 | -D connect user-property 'connection_version' 'v2' \ 225 | -D connect user-property 'connection_mode' 'service' \ 226 | -D publish user-property 'type' 'request' \ 227 | -D publish user-property 'method' 'subscription.delete' \ 228 | -D publish response-topic "agents/test.${USER}/api/v1/in/${APP}" \ 229 | -D publish correlation-data 'foobar' \ 230 | -m "{\"object\": [\"rooms\", \"ROOM_ID\", \"events\"], \"subject\": \"test.${USER}\"}" 231 | ``` 232 | 233 | ```erlang 234 | %% We can verify that the subscription was created from the VerneMQ terminal 235 | mqttgw_dynsub:list(<<"test.john.usr.example.net">>). 236 | 237 | %% [{[<<"apps">>,<<"app.svc.example.org">>,<<"api">>,<<"v1">>, 238 | %% <<"rooms">>,<<"ROOM_ID">>,<<"events">>], 239 | %% #{app => <<"app.svc.example.org">>, 240 | %% object => [<<"rooms">>,<<"ROOM_ID">>,<<"events">>], 241 | %% version => <<"v1">>}}] 242 | ``` 243 | 244 | 245 | 246 | 247 | ## Troubleshooting 248 | 249 | MQTT Gateway should be built using the same release version of Erlang/OTP as VerneMQ. 250 | 251 | 252 | 253 | ## License 254 | 255 | The source code is provided under the terms of [the MIT license][license]. 256 | 257 | [travis]:https://travis-ci.com/netology-group/mqtt-gateway?branch=master 258 | [travis-img]:https://travis-ci.com/netology-group/mqtt-gateway.png?branch=master 259 | [license]:http://www.opensource.org/licenses/MIT 260 | -------------------------------------------------------------------------------- /chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: mqtt-gateway 3 | description: mqtt-gateway broker 4 | home: https://github.com/foxford/mqtt-gateway 5 | 6 | # A chart can be either an 'application' or a 'library' chart. 7 | # 8 | # Application charts are a collection of templates that can be packaged into versioned archives 9 | # to be deployed. 10 | # 11 | # Library charts provide useful utilities or functions for the chart developer. They're included as 12 | # a dependency of application charts to inject those utilities and functions into the rendering 13 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 14 | type: application 15 | 16 | # This is the chart version. This version number should be incremented each time you make changes 17 | # to the chart and its templates, including the app version. 18 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 19 | version: 0.3.0 20 | 21 | # This is the version number of the application being deployed. This version number should be 22 | # incremented each time you make changes to the application. Versions are not expected to 23 | # follow Semantic Versioning. They should reflect the version the application is using. 24 | # It is recommended to use it with quotes. 25 | appVersion: "v0.13.15" 26 | -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "mqtt-gateway.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "mqtt-gateway.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "mqtt-gateway.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "mqtt-gateway.labels" -}} 37 | helm.sh/chart: {{ include "mqtt-gateway.chart" . }} 38 | {{ include "mqtt-gateway.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "mqtt-gateway.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "mqtt-gateway.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Short namespace. 55 | */}} 56 | {{- define "mqtt-gateway.shortNamespace" -}} 57 | {{- $shortns := regexSplit "-" .Release.Namespace -1 | first }} 58 | {{- if has $shortns (list "production" "p") }} 59 | {{- else }} 60 | {{- $shortns }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* 65 | Namespace in ingress path. 66 | converts as follows: 67 | - testing01 -> t01 68 | - staging01-classroom-ng -> s01/classroom-foxford 69 | - production-webinar-ng -> webinar-foxford 70 | */}} 71 | {{- define "mqtt-gateway.ingressPathNamespace" -}} 72 | {{- $ns_head := regexSplit "-" .Release.Namespace -1 | first }} 73 | {{- $ns_tail := regexSplit "-" .Release.Namespace -1 | rest | join "-" | replace "ng" "foxford" }} 74 | {{- if has $ns_head (list "production" "p") }} 75 | {{- $ns_tail }} 76 | {{- else }} 77 | {{- list (regexReplaceAll "(.)[^\\d]*(.+)" $ns_head "${1}${2}") $ns_tail | compact | join "/" }} 78 | {{- end }} 79 | {{- end }} 80 | 81 | {{/* 82 | Ingress path. 83 | */}} 84 | {{- define "mqtt-gateway.ingressPath" -}} 85 | {{- list "" (include "mqtt-gateway.ingressPathNamespace" .) (include "mqtt-gateway.fullname" .) | join "/" }} 86 | {{- end }} 87 | 88 | {{/* 89 | Create volumeMount name from audience and secret name 90 | */}} 91 | {{- define "mqtt-gateway.volumeMountName" -}} 92 | {{- $audience := index . 0 -}} 93 | {{- $secret := index . 1 -}} 94 | {{- printf "%s-%s-secret" $audience $secret | replace "." "-" | trunc 63 | trimSuffix "-" }} 95 | {{- end }} 96 | -------------------------------------------------------------------------------- /chart/templates/app-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: 5 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 6 | name: {{ include "mqtt-gateway.fullname" . }}-app 7 | data: 8 | App.toml: | 9 | {{- $id := list (include "mqtt-gateway.name" . ) .Values.app.svc.audience | compact | join "." }} 10 | id = {{ $id | quote }} 11 | 12 | [rate_limit] 13 | message_count = 100 14 | byte_count = 102400 15 | 16 | {{- println "" }} 17 | 18 | {{- with .Values.app.svc }} 19 | ## 20 | ## SVC 21 | ## 22 | {{- $svc_audience := list .audience | compact | join "." }} 23 | {{- with .authn }} 24 | [authn.{{ $svc_audience | quote }}] 25 | audience = [{{ $svc_audience | quote }}] 26 | algorithm = "ES256" 27 | key = {{ "key" | get . | quote }} 28 | {{- end }} 29 | 30 | {{- println "" }} 31 | 32 | {{- with .authz }} 33 | [authz.{{ $svc_audience | quote }}] 34 | type = {{ .type | quote }} 35 | {{- if eq "local" .type }} 36 | trusted = [ 37 | {{- range $account_label := .trusted }} 38 | {{ $svc_audience | list $account_label | join "." | quote }}, 39 | {{- end }} 40 | ] 41 | {{- end }} 42 | {{- end }} 43 | {{- end }} 44 | 45 | 46 | {{- println "" }} 47 | 48 | {{- range .Values.app.audiences }} 49 | ## 50 | ## {{ .audience }} 51 | ## 52 | {{- $svc_audience := list "svc" .audience | compact | join "." }} 53 | {{- $usr_audience := list "usr" .audience | compact | join "." }} 54 | {{- with "authn" | get . }} 55 | [authn.{{ list "iam" $svc_audience | join "." | quote }}] 56 | audience = [{{ $svc_audience | quote }}, {{ $usr_audience | quote }}] 57 | algorithm = "ES256" 58 | key = {{ "key" | get . | quote }} 59 | {{- end}} 60 | 61 | {{- $ns_audience := list .audience | compact | join "." }} 62 | {{- println "" }} 63 | 64 | {{- with "authz" | get . }} 65 | [authz.{{ $ns_audience | quote }}] 66 | type = {{ .type | quote }} 67 | {{- if eq "local" .type }} 68 | trusted = [ 69 | {{- range $account_label := .trusted }} 70 | {{ $ns_audience | list $account_label | join "." | quote }}, 71 | {{- end }} 72 | ] 73 | {{- end }} 74 | {{- if eq "localwhitelist" .type }} 75 | [[authz.{{ $ns_audience | quote }}.records]] 76 | {{- range $record := .records }} 77 | subject_account_id = {{ get $record "subject_account_id" | quote }} 78 | object = [ 79 | {{- range $o := get $record "object" }} 80 | {{ $o | quote }}, 81 | {{- end}} 82 | ] 83 | action = {{ get $record "action" | quote }} 84 | {{- end }} 85 | {{- end }} 86 | {{- end }} 87 | {{- println "" }} 88 | {{- end }} 89 | vernemq.conf: | 90 | allow_anonymous = off 91 | allow_register_during_netsplit = off 92 | allow_publish_during_netsplit = off 93 | allow_subscribe_during_netsplit = off 94 | allow_unsubscribe_during_netsplit = off 95 | allow_multiple_sessions = off 96 | max_client_id_size = 150 97 | persistent_client_expiration = never 98 | retry_interval = 5 99 | max_inflight_messages = 0 100 | max_online_messages = -1 101 | max_offline_messages = -1 102 | max_message_size = 0 103 | upgrade_outgoing_qos = off 104 | metadata_plugin = vmq_plumtree 105 | leveldb.maximum_memory.percent = 70 106 | listener.tcp.buffer_sizes = 4096,16384,32768 107 | listener.tcp.my_publisher_listener.buffer_sizes=4096,16384,32768 108 | listener.tcp.my_subscriber_listener.buffer_sizes=4096,16384,32768 109 | listener.max_connections = 10000 110 | listener.nr_of_acceptors = 100 111 | listener.tcp.default = 0.0.0.0:1883 112 | listener.ssl.default = 0.0.0.0:8883 113 | listener.ws.default = 0.0.0.0:8080 114 | listener.wss.default = 0.0.0.0:8443 115 | listener.tcp.allowed_protocol_versions = 3,4,5 116 | listener.ssl.allowed_protocol_versions = 3,4,5 117 | listener.ws.allowed_protocol_versions = 3,4,5 118 | listener.wss.allowed_protocol_versions = 3,4,5 119 | listener.vmq.clustering = 0.0.0.0:44053 120 | listener.mountpoint = off 121 | listener.ssl.certfile = /tls/tls.crt 122 | listener.wss.certfile = /tls/tls.crt 123 | listener.ssl.keyfile = /tls/tls.key 124 | listener.wss.keyfile = /tls/tls.key 125 | systree_enabled = off 126 | systree_interval = 0 127 | shared_subscription_policy = prefer_local 128 | plugins.mqttgw = on 129 | plugins.mqttgw.path = /app/mqttgw 130 | plugins.vmq_passwd = off 131 | plugins.vmq_acl = off 132 | plugins.vmq_diversity = off 133 | plugins.vmq_webhooks = off 134 | plugins.vmq_bridge = off 135 | vmq_acl.acl_file = /etc/vernemq/vmq.acl 136 | vmq_acl.acl_reload_interval = 10 137 | vmq_passwd.password_file = /etc/vernemq/vmq.passwd 138 | vmq_passwd.password_reload_interval = 10 139 | vmq_diversity.script_dir = /usr/share/vernemq/lua 140 | vmq_diversity.auth_postgres.enabled = off 141 | vmq_diversity.auth_mysql.enabled = off 142 | vmq_diversity.auth_mongodb.enabled = off 143 | vmq_diversity.auth_redis.enabled = off 144 | log.console = console 145 | log.console.level = info 146 | log.syslog = off 147 | log.crash = on 148 | log.crash.file = /var/log/vernemq/crash.log 149 | log.crash.maximum_message_size = 64KB 150 | log.crash.size = 10MB 151 | log.crash.rotation = $D0 152 | log.crash.rotation.keep = 5 153 | nodename = VerneMQ@127.0.0.1 154 | erlang.async_threads = 64 155 | erlang.max_ports = 262144 156 | erlang.distribution_buffer_size = 32MB 157 | -------------------------------------------------------------------------------- /chart/templates/headless-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }}-headless 5 | labels: 6 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | {{- include "mqtt-gateway.selectorLabels" . | nindent 4 }} 10 | clusterIP: None 11 | -------------------------------------------------------------------------------- /chart/templates/network/cluster-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }}-cluster 5 | labels: 6 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 7 | annotations: 8 | {{- with .Values.clusterService.annotations }} 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | spec: 12 | type: ClusterIP 13 | ports: 14 | {{- if .Values.clusterService.ports.mqtts }} 15 | - name: mqtts 16 | port: {{ .Values.clusterService.ports.mqtts }} 17 | targetPort: 8883 18 | protocol: TCP 19 | {{- end }} 20 | {{- if .Values.clusterService.ports.mqtt }} 21 | - name: mqtt 22 | port: {{ .Values.clusterService.ports.mqtt }} 23 | targetPort: 1883 24 | protocol: TCP 25 | {{- end }} 26 | {{- if .Values.clusterService.ports.metrics }} 27 | - name: metrics 28 | port: {{ .Values.clusterService.ports.metrics }} 29 | targetPort: 8888 30 | protocol: TCP 31 | {{- end }} 32 | {{- if .Values.clusterService.ports.wss }} 33 | - name: wss 34 | port: {{ .Values.clusterService.ports.wss }} 35 | targetPort: 8443 36 | protocol: TCP 37 | {{- end }} 38 | {{- if .Values.clusterService.ports.ws }} 39 | - name: ws 40 | port: {{ .Values.clusterService.ports.ws }} 41 | targetPort: 8080 42 | protocol: TCP 43 | {{- end }} 44 | {{- if .Values.clusterService.ports.http }} 45 | - name: http 46 | port: {{ .Values.clusterService.ports.http }} 47 | targetPort: 8081 48 | protocol: TCP 49 | {{- end }} 50 | selector: 51 | {{- include "mqtt-gateway.selectorLabels" . | nindent 4 }} 52 | -------------------------------------------------------------------------------- /chart/templates/network/external-svc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.externalService }} 2 | 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "mqtt-gateway.fullname" . }}-external 7 | labels: 8 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 9 | annotations: 10 | {{- with .Values.externalService.annotations }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | type: LoadBalancer 15 | externalTrafficPolicy: Cluster 16 | sessionAffinity: None 17 | {{- if .Values.externalService.ip }} 18 | loadBalancerIP: {{ .Values.externalService.ip }} 19 | {{- end }} 20 | ports: 21 | {{- if .Values.externalService.ports.mqtt }} 22 | - name: mqtt 23 | port: {{ .Values.externalService.ports.mqtt }} 24 | targetPort: 1883 25 | protocol: TCP 26 | {{- end }} 27 | {{- if .Values.externalService.ports.ws }} 28 | - name: ws 29 | port: {{ .Values.externalService.ports.ws }} 30 | targetPort: 8080 31 | protocol: TCP 32 | {{- end }} 33 | selector: 34 | {{- include "mqtt-gateway.selectorLabels" . | nindent 4 }} 35 | 36 | {{- end }} 37 | -------------------------------------------------------------------------------- /chart/templates/network/ing.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | nginx.ingress.kubernetes.io/rewrite-target: /$2 8 | spec: 9 | tls: 10 | - hosts: 11 | - {{ .Values.ingress.host | quote }} 12 | secretName: {{ .Values.tls.secretName }} 13 | rules: 14 | - host: {{ .Values.ingress.host | quote }} 15 | http: 16 | paths: 17 | {{- if .Values.clusterService.ports.mqtt }} 18 | - path: {{ include "mqtt-gateway.ingressPath" . }}(/|$)(mqtt) 19 | pathType: Prefix 20 | backend: 21 | service: 22 | name: {{ include "mqtt-gateway.fullname" . }}-cluster 23 | port: 24 | number: {{ .Values.clusterService.ports.ws }} 25 | {{- end }} 26 | {{- if .Values.clusterService.ports.metrics }} 27 | - path: {{ include "mqtt-gateway.ingressPath" . }}(/|$)(status) 28 | pathType: Prefix 29 | backend: 30 | service: 31 | name: {{ include "mqtt-gateway.fullname" . }}-cluster 32 | port: 33 | number: {{ .Values.clusterService.ports.metrics }} 34 | {{- end }} 35 | -------------------------------------------------------------------------------- /chart/templates/network/internal-svc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.internalService }} 2 | 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "mqtt-gateway.fullname" . }}-internal 7 | labels: 8 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 9 | annotations: 10 | {{- with .Values.internalService.annotations }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | type: LoadBalancer 15 | externalTrafficPolicy: Cluster 16 | sessionAffinity: None 17 | ports: 18 | {{- if .Values.internalService.ports.mqtt }} 19 | - name: mqtt 20 | port: {{ .Values.internalService.ports.mqtt }} 21 | targetPort: 1883 22 | protocol: TCP 23 | {{- end }} 24 | {{- if .Values.internalService.ports.ws }} 25 | - name: ws 26 | port: {{ .Values.internalService.ports.ws }} 27 | targetPort: 8080 28 | protocol: TCP 29 | {{- end }} 30 | {{- if .Values.clusterService.ports.http }} 31 | - name: http 32 | port: {{ .Values.clusterService.ports.http }} 33 | targetPort: 8081 34 | protocol: TCP 35 | {{- end }} 36 | selector: 37 | {{- include "mqtt-gateway.selectorLabels" . | nindent 4 }} 38 | 39 | {{- end }} 40 | -------------------------------------------------------------------------------- /chart/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | labels: 6 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 7 | spec: 8 | minAvailable: 1 9 | selector: 10 | matchLabels: 11 | {{- include "mqtt-gateway.selectorLabels" . | nindent 6 }} 12 | -------------------------------------------------------------------------------- /chart/templates/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | kind: Role 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | rules: 6 | - apiGroups: [""] # "" indicates the core API group 7 | resources: ["pods"] 8 | verbs: ["get", "watch", "list"] 9 | -------------------------------------------------------------------------------- /chart/templates/rbac/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: RoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | subjects: 6 | - kind: ServiceAccount 7 | name: {{ include "mqtt-gateway.fullname" . }} 8 | roleRef: 9 | kind: Role 10 | name: {{ include "mqtt-gateway.fullname" . }} 11 | apiGroup: rbac.authorization.k8s.io 12 | -------------------------------------------------------------------------------- /chart/templates/rbac/sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | -------------------------------------------------------------------------------- /chart/templates/sts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | labels: 6 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount | default 2 }} 9 | serviceName: {{ include "mqtt-gateway.fullname" . }}-headless 10 | selector: 11 | matchLabels: 12 | {{- include "mqtt-gateway.selectorLabels" . | nindent 6 }} 13 | template: 14 | metadata: 15 | annotations: 16 | checksum/app-cm: {{ include (print $.Template.BasePath "/app-cm.yaml") . | sha256sum }} 17 | labels: 18 | {{- include "mqtt-gateway.selectorLabels" . | nindent 8 }} 19 | spec: 20 | imagePullSecrets: 21 | - name: regcred 22 | initContainers: 23 | - name: copy-config-from-volumes 24 | image: busybox 25 | command: 26 | - "sh" 27 | - "-c" 28 | - "cp /config-tmp/* /config" 29 | volumeMounts: 30 | - name: config-tmp 31 | mountPath: /config-tmp/vernemq.conf 32 | subPath: vernemq.conf 33 | - name: config-tmp 34 | mountPath: /config-tmp/App.toml 35 | subPath: App.toml 36 | - name: config 37 | mountPath: /config 38 | imagePullPolicy: IfNotPresent 39 | resources: 40 | limits: 41 | cpu: 0.1 42 | memory: 100Mi 43 | requests: 44 | cpu: 0.1 45 | memory: 100Mi 46 | containers: 47 | - name: {{ .Chart.Name }} 48 | image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag | default .Chart.AppVersion }}" 49 | imagePullPolicy: IfNotPresent 50 | env: 51 | {{- range $key, $value := .Values.env }} 52 | - name: {{ $key }} 53 | value: {{ $value | quote }} 54 | {{- end }} 55 | - name: MY_POD_NAME 56 | valueFrom: 57 | fieldRef: 58 | fieldPath: metadata.name 59 | - name: APP_AGENT_LABEL 60 | valueFrom: 61 | fieldRef: 62 | fieldPath: metadata.name 63 | - name: DOCKER_VERNEMQ_DISTRIBUTED_COOKIE 64 | valueFrom: 65 | secretKeyRef: 66 | name: mqtt-gateway-distributed-cookie 67 | key: DOCKER_VERNEMQ_DISTRIBUTED_COOKIE 68 | volumeMounts: 69 | - name: config 70 | mountPath: /vernemq/etc/vernemq.conf 71 | subPath: vernemq.conf 72 | - name: data 73 | mountPath: /data 74 | - name: config 75 | mountPath: /app/App.toml 76 | subPath: App.toml 77 | - name: tls 78 | mountPath: /tls 79 | {{- with .Values.app.svc }} 80 | {{- $audience := .audience }} 81 | {{- range $secret, $mounts := .credentials }} 82 | {{- range $mounts }} 83 | - name: {{ include "mqtt-gateway.volumeMountName" (list $audience $secret) }} 84 | mountPath: {{ .mountPath }} 85 | subPath: {{ .subPath }} 86 | {{- end }} 87 | {{- end }} 88 | {{- end }} 89 | {{- range .Values.app.audiences }} 90 | {{- $audience := .audience }} 91 | {{- range $secret, $mounts := .credentials }} 92 | {{- range $mounts }} 93 | - name: {{ include "mqtt-gateway.volumeMountName" (list $audience $secret) }} 94 | mountPath: {{ .mountPath }} 95 | subPath: {{ .subPath }} 96 | {{- end }} 97 | {{- end }} 98 | {{- end }} 99 | resources: 100 | {{- toYaml .Values.app.resources | nindent 12 }} 101 | volumes: 102 | - name: config 103 | emptyDir: {} 104 | - name: data 105 | emptyDir: {} 106 | - name: config-tmp 107 | configMap: 108 | name: {{ include "mqtt-gateway.fullname" . }}-app 109 | - name: tls 110 | secret: 111 | secretName: tls-certificates 112 | {{- with .Values.app.svc }} 113 | {{- $audience := .audience }} 114 | {{- range $secret, $mounts := .credentials }} 115 | - name: {{ include "mqtt-gateway.volumeMountName" (list $audience $secret) }} 116 | secret: 117 | secretName: {{ $secret }} 118 | {{- end }} 119 | {{- end }} 120 | {{- range .Values.app.audiences }} 121 | {{- $audience := .audience }} 122 | {{- range $secret, $mounts := .credentials }} 123 | - name: {{ include "mqtt-gateway.volumeMountName" (list $audience $secret) }} 124 | secret: 125 | secretName: {{ $secret }} 126 | {{- end }} 127 | {{- end }} 128 | -------------------------------------------------------------------------------- /chart/templates/util/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | labels: 6 | {{- toYaml .Values.serviceMonitor.labels | nindent 4 }} 7 | spec: 8 | endpoints: 9 | - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 10 | honorLabels: true 11 | interval: 30s 12 | port: metrics 13 | scheme: http 14 | tlsConfig: 15 | insecureSkipVerify: true 16 | jobLabel: {{ include "mqtt-gateway.name" . }} 17 | selector: 18 | matchLabels: 19 | {{- include "mqtt-gateway.selectorLabels" . | nindent 6 }} 20 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for dispatcher. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | app: 8 | image: 9 | repository: cr.yandex/crp1of6bddata8ain3q5/mqtt-gateway 10 | tag: "" 11 | 12 | resources: 13 | limits: 14 | cpu: 10 15 | memory: 10Gi 16 | requests: 17 | cpu: 0.2 18 | memory: 400Mi 19 | 20 | svc: 21 | audience: foobar 22 | credentials: 23 | # foobar-secret-name: 24 | # - subPath: private-key 25 | # mountPath: /path/to/foobar/private/key 26 | # - subPath: public-key 27 | # mountPath: /path/to/foobar/public/key 28 | authz: 29 | # type: local 30 | # trusted: 31 | # - some-service 32 | authn: 33 | # key: /path/to/foobar/public/key 34 | 35 | audiences: 36 | # - audience: foobar 37 | # credentials: 38 | # foobar-pem-secret-name: 39 | # - subPath: foobar-public-key 40 | # mountPath: /path/to/foobar/public/key 41 | # authn: 42 | # key: /path/to/foobar/public/key 43 | 44 | env: 45 | APP_CONFIG: /app/App.toml 46 | APP_AUTHN_ENABLED: 1 47 | APP_AUTHZ_ENABLED: 1 48 | APP_DYNSUB_ENABLED: 1 49 | APP_STAT_ENABLED: 1 50 | APP_RATE_LIMIT_ENABLED: 1 51 | DOCKER_VERNEMQ_DISCOVERY_KUBERNETES: 0 52 | DOCKER_VERNEMQ_KUBERNETES_LABEL_SELECTOR: app.kubernetes.io/name=mqtt-gateway 53 | 54 | clusterService: 55 | ports: 56 | mqtts: 58883 57 | mqtt: 51883 58 | metrics: 58888 59 | wss: 443 60 | ws: 80 61 | http: 8081 62 | 63 | ingress: 64 | host: example.org 65 | 66 | tls: 67 | secretName: tls-certificates 68 | 69 | serviceMonitor: 70 | labels: 71 | release: kube-prometheus-stack 72 | -------------------------------------------------------------------------------- /data/keys/iam.private_key.pem.sample: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIGrdZDgyouytRYS7lPJTEEnovv+YEPBSFEjK2HMBI0FHoAoGCCqGSM49 3 | AwEHoUQDQgAEWWwNQde0SgLFERWZ1EX+1yKBQHWjDoYc0yVOerzcnh5BmSi2YXtM 4 | 4OUmbBoTZM7atDBOmG9iVnSnr+vY1EYPfA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /data/keys/iam.public_key.pem.sample: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWWwNQde0SgLFERWZ1EX+1yKBQHWj 3 | DoYc0yVOerzcnh5BmSi2YXtM4OUmbBoTZM7atDBOmG9iVnSnr+vY1EYPfA== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /data/keys/svc.private_key.pem.sample: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIGrdZDgyouytRYS7lPJTEEnovv+YEPBSFEjK2HMBI0FHoAoGCCqGSM49 3 | AwEHoUQDQgAEWWwNQde0SgLFERWZ1EX+1yKBQHWjDoYc0yVOerzcnh5BmSi2YXtM 4 | 4OUmbBoTZM7atDBOmG9iVnSnr+vY1EYPfA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /data/keys/svc.public_key.pem.sample: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWWwNQde0SgLFERWZ1EX+1yKBQHWj 3 | DoYc0yVOerzcnh5BmSi2YXtM4OUmbBoTZM7atDBOmG9iVnSnr+vY1EYPfA== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /deploy.init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! ${GITHUB_TOKEN} ]]; then echo "GITHUB_TOKEN is required" 1>&2; exit 1; fi 4 | 5 | PROJECT="${PROJECT:-mqtt-gateway}" 6 | SOURCE=${SOURCE:-"https://api.github.com/repos/netology-group/ulms-env/contents/k8s"} 7 | APPS_SOURCE="https://api.github.com/repos/foxford/ulms-env/contents/apps" 8 | BRANCH="${BRANCH:-master}" 9 | 10 | function FILE_FROM_GITHUB() { 11 | local DEST_DIR="${1}"; if [[ ! "${DEST_DIR}" ]]; then echo "${FUNCNAME[0]}:DEST_DIR is required" 1>&2; exit 1; fi 12 | local URI="${2}"; if [[ ! "${URI}" ]]; then echo "${FUNCNAME[0]}:URI is required" 1>&2; exit 1; fi 13 | if [[ "${3}" != "optional" ]]; then 14 | local FLAGS="-fsSL" 15 | else 16 | local FLAGS="-sSL" 17 | fi 18 | 19 | mkdir -p "${DEST_DIR}" 20 | curl ${FLAGS} \ 21 | -H "authorization: token ${GITHUB_TOKEN}" \ 22 | -H 'accept: application/vnd.github.v3.raw' \ 23 | -o "${DEST_DIR}/$(basename $URI)" \ 24 | "${URI}?ref=${BRANCH}" 25 | } 26 | 27 | function ADD_PROJECT() { 28 | local _PATH="${1}"; if [[ ! "${_PATH}" ]]; then echo "${FUNCNAME[0]}:_PATH is required" 1>&2; exit 1; fi 29 | local _PROJECT="${2}"; if [[ ! "${_PROJECT}" ]]; then echo "${FUNCNAME[0]}:PROJECT is required" 1>&2; exit 1; fi 30 | 31 | tee "${_PATH}" < ["src"], 5 | filter => "*.erl", 6 | rules => [{elvis_style, dont_repeat_yourself, #{min_complexity => 35}}], 7 | ruleset => erl_files}, 8 | #{dirs => ["."], 9 | filter => "Makefile", 10 | ruleset => makefiles}, 11 | #{dirs => ["."], 12 | filter => "elvis.config", 13 | ruleset => elvis_config} 14 | ]} 15 | ]} 16 | ]. 17 | -------------------------------------------------------------------------------- /rel/sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {mqttgw, [ 3 | ]} 4 | ]. 5 | 6 | -------------------------------------------------------------------------------- /rel/vm.args: -------------------------------------------------------------------------------- 1 | -name mqttgw@127.0.0.1 2 | -setcookie mqttgw 3 | -heart 4 | -------------------------------------------------------------------------------- /relx.config: -------------------------------------------------------------------------------- 1 | {release, {mqttgw, "1"}, [mqttgw]}. 2 | {extended_start_script, true}. 3 | {sys_config, "rel/sys.config"}. 4 | {vm_args, "rel/vm.args"}. 5 | 6 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta11 2 | kind: Config 3 | build: 4 | artifacts: 5 | - image: cr.yandex/crp1of6bddata8ain3q5/mqtt-gateway 6 | docker: 7 | dockerfile: docker/Dockerfile 8 | tagPolicy: 9 | gitCommit: {} 10 | local: 11 | push: true 12 | useDockerCLI: true 13 | tryImportMissing: true 14 | deploy: 15 | helm: 16 | releases: 17 | - name: mqtt-gateway 18 | chartPath: chart 19 | artifactOverrides: 20 | app.image: cr.yandex/crp1of6bddata8ain3q5/mqtt-gateway 21 | imageStrategy: 22 | helm: {} 23 | valuesFiles: 24 | - deploy/values.yaml 25 | -------------------------------------------------------------------------------- /src/mqttgw.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw). 2 | 3 | -behaviour(auth_on_register_hook). 4 | -behaviour(auth_on_register_m5_hook). 5 | -behaviour(auth_on_publish_hook). 6 | -behaviour(auth_on_publish_m5_hook). 7 | -behaviour(on_deliver_hook). 8 | -behaviour(on_deliver_m5_hook). 9 | -behaviour(auth_on_subscribe_hook). 10 | -behaviour(auth_on_subscribe_m5_hook). 11 | -behaviour(on_topic_unsubscribed_hook). 12 | -behaviour(on_client_offline_hook). 13 | -behaviour(on_client_gone_hook). 14 | 15 | -ifdef(TEST). 16 | -include_lib("proper/include/proper.hrl"). 17 | -include_lib("eunit/include/eunit.hrl"). 18 | -endif. 19 | 20 | %% API 21 | -export([ 22 | handle_connect/5, 23 | handle_publish_mqtt3/4, 24 | handle_publish_mqtt5/5, 25 | handle_publish_authz/4, 26 | handle_deliver_mqtt3/3, 27 | handle_deliver_mqtt5/4, 28 | handle_subscribe_authz/3 29 | ]). 30 | 31 | %% Plugin callbacks 32 | -export([ 33 | start/0, 34 | stop/0, 35 | auth_on_register/5, 36 | auth_on_register_m5/6, 37 | auth_on_publish/6, 38 | auth_on_publish_m5/7, 39 | on_deliver/6, 40 | on_deliver_m5/7, 41 | auth_on_subscribe/3, 42 | auth_on_subscribe_m5/4, 43 | on_topic_unsubscribed/2, 44 | on_client_offline/1, 45 | on_client_gone/1 46 | ]). 47 | 48 | -export([send_dynsub_response/7, send_dynsub_response/9, send_dynsub_multicast_event/8]). 49 | -export([ 50 | parse_agent_id/1, parse_connection_mode/1, format_session_id/2, 51 | validate_message_properties/3, 52 | update_message_properties/7 53 | ]). 54 | 55 | %% Definitions 56 | -define(APP, ?MODULE). 57 | -define(VER_2, <<"v2">>). 58 | -define(BROKER_CONNECTION, #connection{mode=service, version=?VER_2}). 59 | 60 | %% Types 61 | -type qos() :: 0..2. 62 | -type topic() :: [binary()]. 63 | -type subscription() :: {topic(), qos()}. 64 | -type connection_mode() :: default | service | observer | bridge. 65 | 66 | -record(connection, { 67 | version :: binary(), 68 | mode :: connection_mode() 69 | }). 70 | -type connection() :: #connection{}. 71 | 72 | -record(session, { 73 | id :: binary(), 74 | parent_id :: binary(), 75 | connection :: connection(), 76 | created_at :: non_neg_integer() 77 | }). 78 | -type session() :: #session{}. 79 | 80 | -record(config, { 81 | id :: mqttgw_id:agent_id(), 82 | authn :: disabled | {enabled, mqttgw_authn:config()}, 83 | authz :: disabled | {enabled, mqttgw_authz:config()}, 84 | dynsub :: disabled | enabled, 85 | stat :: disabled | enabled, 86 | rate_limit :: disabled | {enabled, mqttgw_ratelimit:config()} 87 | }). 88 | -type config() :: #config{}. 89 | 90 | -record(initial_state, { 91 | config :: config(), 92 | time :: non_neg_integer() 93 | }). 94 | -type initial_state() :: #initial_state{}. 95 | 96 | -record(state, { 97 | config :: config(), 98 | session :: session(), 99 | unique_id :: binary(), 100 | time :: non_neg_integer() 101 | }). 102 | -type state() :: #state{}. 103 | 104 | -record (message, { 105 | payload :: binary(), 106 | properties = #{} :: map() 107 | }). 108 | -type message() :: #message{}. 109 | 110 | -type error() :: #{reason_code := atom()}. 111 | 112 | -export_types([qos/0, topic/0, subscription/0]). 113 | 114 | %% ============================================================================= 115 | %% API: Connect 116 | %% ============================================================================= 117 | 118 | -spec handle_connect(binary(), binary(), boolean(), map(), initial_state()) 119 | -> ok | {error, error()}. 120 | handle_connect(ClientId, Password, CleanSession, Properties, State) -> 121 | try validate_connection_params(parse_connection_params(ClientId, Properties)) of 122 | {Conn, AgentId} -> 123 | handle_connect_constraints(Conn, AgentId, Password, CleanSession, State) 124 | catch 125 | T:R -> 126 | error_logger:warning_msg( 127 | "Error on connect: an invalid client_id = ~p, " 128 | "exception_type = ~p, exception_reason = ~p", 129 | [ClientId, T, R]), 130 | {error, #{reason_code => client_identifier_not_valid}} 131 | end. 132 | 133 | -spec handle_connect_constraints( 134 | connection(), mqttgw_id:agent_id(), binary(), boolean(), initial_state()) 135 | -> ok | {error, error()}. 136 | handle_connect_constraints(Conn, AgentId, Password, CleanSession, State) -> 137 | try verify_connect_constraints(CleanSession, Conn#connection.mode) of 138 | _ -> 139 | handle_connect_authn_config(Conn, AgentId, Password, State) 140 | catch 141 | T:R -> 142 | error_logger:warning_msg( 143 | "Error on connect: invalid constraints check, clean_session = ~p, " 144 | "exception_type = ~p, exception_reason = ~p", 145 | [CleanSession, T, R]), 146 | {error, #{reason_code => impl_specific_error}} 147 | end. 148 | 149 | -spec handle_connect_authn_config(connection(), mqttgw_id:agent_id(), binary(), initial_state()) 150 | -> ok | {error, error()}. 151 | handle_connect_authn_config(Conn, AgentId, Password, State) -> 152 | case State#initial_state.config#config.authn of 153 | disabled -> 154 | DirtyAccountId = mqttgw_id:account_id(AgentId), 155 | handle_connect_authz_config(Conn, AgentId, DirtyAccountId, State); 156 | {enabled, Config} -> 157 | handle_connect_authn(Conn, AgentId, Password, Config, State) 158 | end. 159 | 160 | -spec handle_connect_authn( 161 | connection(), mqttgw_id:agent_id(), binary(), mqttgw_authn:config(), initial_state()) 162 | -> ok | {error, error()}. 163 | handle_connect_authn(Conn, AgentId, Password, Config, State) -> 164 | DirtyAccountId = mqttgw_id:account_id(AgentId), 165 | try mqttgw_authn:authenticate(Password, Config) of 166 | AccountId when AccountId =:= DirtyAccountId -> 167 | handle_connect_authz_config(Conn, AgentId, AccountId, State); 168 | AccountId -> 169 | error_logger:warning_msg( 170 | "Error on connect: account_id = '~s' in the password is " 171 | " different from the account_id = '~s' in the client_id", 172 | [ mqttgw_authn:format_account_id(AccountId), 173 | mqttgw_authn:format_account_id(DirtyAccountId) ]), 174 | {error, #{reason_code => not_authorized}} 175 | catch 176 | T:R -> 177 | error_logger:warning_msg( 178 | "Error on connect: an invalid password " 179 | "for the agent = '~s', " 180 | "exception_type = ~p, exception_reason = ~p", 181 | [mqttgw_id:format_agent_id(AgentId), T, R]), 182 | {error, #{reason_code => bad_username_or_password}} 183 | end. 184 | 185 | -spec handle_connect_authz_config( 186 | connection(), mqttgw_id:agent_id(), mqttgw_authn:account_id(), initial_state()) 187 | -> ok | {error, error()}. 188 | handle_connect_authz_config(Conn, AgentId, AccountId, State) -> 189 | case State#initial_state.config#config.authz of 190 | disabled -> 191 | handle_connect_success(Conn, AgentId, State); 192 | {enabled, Config} -> 193 | BrokerId = State#initial_state.config#config.id, 194 | handle_connect_authz(Conn, AgentId, AccountId, BrokerId, Config, State) 195 | end. 196 | 197 | -spec handle_connect_authz( 198 | connection(), mqttgw_id:agent_id(), mqttgw_authn:account_id(), 199 | mqttgw_id:agent_id(), mqttgw_authz:config(), initial_state()) 200 | -> ok | {error, error()}. 201 | handle_connect_authz( 202 | #connection{mode=default} =Conn, AgentId, _AccountId, _BrokerId, _Config, State) -> 203 | handle_connect_success(Conn, AgentId, State); 204 | handle_connect_authz(Conn, AgentId, AccountId, BrokerId, Config, State) -> 205 | #connection{mode=Mode} = Conn, 206 | 207 | try mqttgw_authz:authorize(mqttgw_id:audience(BrokerId), AccountId, Config) of 208 | _ -> 209 | handle_connect_success(Conn, AgentId, State) 210 | catch 211 | T:R -> 212 | error_logger:warning_msg( 213 | "Error on connect: connecting in mode = '~s' isn't allowed " 214 | "for the agent = ~p, " 215 | "exception_type = ~p, exception_reason = ~p", 216 | [Mode, mqttgw_id:format_agent_id(AgentId), T, R]), 217 | {error, #{reason_code => not_authorized}} 218 | end. 219 | 220 | -spec handle_connect_success( 221 | connection(), mqttgw_id:agent_id(), initial_state()) 222 | -> ok | {error, error()}. 223 | handle_connect_success(Conn, AgentId, State) -> 224 | #connection{mode=Mode, version=Ver} = Conn, 225 | 226 | %% Get the broker session id 227 | Config = mqttgw_state:get(config), 228 | BrokerId = Config#config.id, 229 | #session{id=ParentSessionId} = mqttgw_state:get(BrokerId), 230 | 231 | %% Create an agent session 232 | Session = 233 | #session{ 234 | id=make_uuid(), 235 | parent_id=ParentSessionId, 236 | connection=Conn, 237 | created_at=State#initial_state.time}, 238 | mqttgw_state:put(AgentId, Session), 239 | 240 | handle_connect_success_stat_config( 241 | AgentId, 242 | broker_state( 243 | State#initial_state.config, 244 | Session, 245 | State#initial_state.time)), 246 | 247 | error_logger:info_msg( 248 | "Agent = '~s' connected: mode = '~s', version = '~s'", 249 | [mqttgw_id:format_agent_id(AgentId), Mode, Ver]), 250 | ok. 251 | 252 | -spec handle_connect_success_stat_config(mqttgw_id:agent_id(), state()) -> ok. 253 | handle_connect_success_stat_config(AgentId, State) -> 254 | case State#state.config#config.stat of 255 | disabled -> 256 | ok; 257 | enabled -> 258 | #state{ 259 | time=Time, 260 | unique_id=UniqueId, 261 | session=#session{ 262 | id=SessionId, 263 | parent_id=ParentSessionId, 264 | created_at=Ts}, 265 | config=#config{ 266 | id=BrokerId}} = State, 267 | SessionPairId = format_session_id(SessionId, ParentSessionId), 268 | 269 | send_audience_broadcast_event( 270 | #{id => mqttgw_id:format_agent_id(AgentId)}, 271 | [ {<<"type">>, <<"event">>}, 272 | {<<"label">>, <<"agent.enter">>}, 273 | {<<"timestamp">>, integer_to_binary(Ts)} ], 274 | ?BROKER_CONNECTION, 275 | BrokerId, 276 | AgentId, 277 | UniqueId, 278 | SessionPairId, 279 | Time), 280 | ok 281 | end. 282 | 283 | -spec verify_connect_constraints(boolean(), connection_mode()) -> ok. 284 | verify_connect_constraints(CleanSession, Mode) -> 285 | ok = verify_connect_clean_session_constraint(CleanSession, Mode), 286 | ok. 287 | 288 | -spec verify_connect_clean_session_constraint(boolean(), connection_mode()) -> ok. 289 | %% Any for trusted modes 290 | verify_connect_clean_session_constraint(_IsRetain, Mode) 291 | when (Mode =:= service) or (Mode =:= bridge) or (Mode =:= observer) 292 | -> ok; 293 | %% Only 'false' for anyone else 294 | verify_connect_clean_session_constraint(true, _Mode) -> 295 | ok; 296 | verify_connect_clean_session_constraint(IsRetain, _Mode) -> 297 | error({bad_retain, IsRetain}). 298 | 299 | %% ============================================================================= 300 | %% API: Subscription Topics 301 | %% ============================================================================= 302 | 303 | -spec handle_topic_unsubscribed(binary(), on_topic_unsubscribed_hook:maybe_topics(), state()) -> ok. 304 | handle_topic_unsubscribed(ClientId, Topics, State) when Topics =:= all_topics -> 305 | #state{ 306 | time=Time, 307 | unique_id=UniqueId, 308 | session=#session{id=SessionId, parent_id=ParentSessionId}, 309 | config=#config{id=BrokerId}} = State, 310 | SessionPairId = format_session_id(SessionId, ParentSessionId), 311 | 312 | delete_client_dynsubs( 313 | ClientId, ?BROKER_CONNECTION, 314 | BrokerId, UniqueId, SessionPairId, Time), 315 | 316 | ok; 317 | handle_topic_unsubscribed(_ClientId, _Topics, _State) -> 318 | %% NOTE: nothing to implement here because 319 | %% user agents cannot delete their dynsubs. 320 | %% Service agents can delete user's dynsubs, 321 | %% we handle that within subscription request handlers. 322 | 323 | ok. 324 | 325 | %% ============================================================================= 326 | %% API: Disconnect 327 | %% ============================================================================= 328 | 329 | -spec handle_disconnect(mqttgw_id:agent_id(), state()) -> ok. 330 | handle_disconnect(AgentId, State) -> 331 | handle_disconnect_stat_config(AgentId, State). 332 | 333 | -spec handle_disconnect_stat_config(mqttgw_id:agent_id(), state()) -> ok | {error, error()}. 334 | handle_disconnect_stat_config(AgentId, State) -> 335 | case State#state.config#config.stat of 336 | disabled -> 337 | handle_disconnect_success(AgentId, State); 338 | enabled -> 339 | #state{ 340 | time=Time, 341 | unique_id=UniqueId, 342 | session=#session{ 343 | id=SessionId, 344 | parent_id=ParentSessionId}, 345 | config=#config{ 346 | id=BrokerId}} = State, 347 | SessionPairId = format_session_id(SessionId, ParentSessionId), 348 | 349 | send_audience_broadcast_event( 350 | #{id => mqttgw_id:format_agent_id(AgentId)}, 351 | [ {<<"type">>, <<"event">>}, 352 | {<<"label">>, <<"agent.leave">>}, 353 | {<<"timestamp">>, integer_to_binary(Time)} ], 354 | ?BROKER_CONNECTION, 355 | BrokerId, 356 | AgentId, 357 | UniqueId, 358 | SessionPairId, 359 | Time), 360 | handle_disconnect_success(AgentId, State) 361 | end. 362 | 363 | -spec handle_disconnect_success(mqttgw_id:agent_id(), state()) -> ok. 364 | handle_disconnect_success(AgentId, State) -> 365 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 366 | 367 | error_logger:info_msg( 368 | "Agent = '~s' disconnected: mode = '~s'", 369 | [mqttgw_id:format_agent_id(AgentId), Mode]), 370 | ok. 371 | 372 | %% ============================================================================= 373 | %% API: Publish 374 | %% ============================================================================= 375 | 376 | -spec handle_publish_mqtt3_constraints( 377 | topic(), binary(), qos(), boolean(), mqttgw_id:agent_id(), state()) 378 | -> {ok, list()} | {error, error()}. 379 | handle_publish_mqtt3_constraints(Topic, InputPayload, QoS, IsRetain, AgentId, State) -> 380 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 381 | 382 | try verify_publish_constraints(QoS, IsRetain, Mode) of 383 | %% Rate-limit agents with mode=default. 384 | _ when Mode =:= default -> 385 | handle_publish_mqtt3_ratelimits(Topic, InputPayload, AgentId, State); 386 | _ -> 387 | handle_publish_mqtt3(Topic, InputPayload, AgentId, State) 388 | catch 389 | T:R -> 390 | error_logger:error_msg( 391 | "Error on publish: invalid constraints check, qos = ~p, retain = ~p " 392 | "from the agent = '~s' using mode = '~s', " 393 | "exception_type = ~p, exception_reason = ~p", 394 | [QoS, IsRetain, mqttgw_id:format_agent_id(AgentId), Mode, T, R]), 395 | {error, #{reason_code => impl_specific_error}} 396 | end. 397 | 398 | -spec handle_publish_mqtt3_ratelimits(topic(), binary(), mqttgw_id:agent_id(), state()) 399 | -> {ok, list()} | {error, error()}. 400 | handle_publish_mqtt3_ratelimits(Topic, InputPayload, AgentId, State) -> 401 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 402 | 403 | case State#state.config#config.rate_limit of 404 | {enabled, Constraints} -> 405 | Now = State#state.time, 406 | %% 1 second range. 407 | RangeStartTime = Now - 1000, 408 | Bytes = byte_size(InputPayload), 409 | Data = #{bytes => Bytes, time => Now}, 410 | case mqttgw_ratelimitstate:put(AgentId, Data, RangeStartTime, Constraints) of 411 | ok -> 412 | handle_publish_mqtt3(Topic, InputPayload, AgentId, State); 413 | {error, Reason} -> 414 | error_logger:error_msg( 415 | "Error on publish: rate limit exceeded " 416 | "for the agent = '~s' using mode = '~s', " 417 | "reason = ~p", 418 | [mqttgw_id:format_agent_id(AgentId), Mode, Reason]), 419 | {error, #{reason_code => impl_specific_error}} 420 | end; 421 | _ -> 422 | handle_publish_mqtt3(Topic, InputPayload, AgentId, State) 423 | end. 424 | 425 | -spec handle_publish_mqtt3(topic(), binary(), mqttgw_id:agent_id(), state()) 426 | -> {ok, list()} | {error, error()}. 427 | handle_publish_mqtt3(Topic, InputPayload, AgentId, State) -> 428 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 429 | 430 | BrokerId = State#state.config#config.id, 431 | try handle_message_properties( 432 | handle_mqtt3_envelope_properties(validate_envelope(parse_envelope(InputPayload))), 433 | AgentId, 434 | BrokerId, 435 | State) of 436 | Message -> 437 | case handle_publish_authz(Topic, Message, AgentId, State) of 438 | ok -> 439 | Changes = [{payload, envelope(Message)}], 440 | {ok, Changes}; 441 | {error, #{reason_code := Reason}} -> 442 | {error, Reason} 443 | end 444 | catch 445 | T:R -> 446 | error_logger:error_msg( 447 | "Error on publish: an invalid message = ~p " 448 | "from the agent = '~s' using mode = '~s', " 449 | "exception_type = ~p, exception_reason = ~p", 450 | [InputPayload, mqttgw_id:format_agent_id(AgentId), Mode, T, R]), 451 | {error, #{reason_code => impl_specific_error}} 452 | end. 453 | 454 | -spec handle_publish_mqtt5_constraints( 455 | topic(), binary(), map(), qos(), boolean(), mqttgw_id:agent_id(), state()) 456 | -> {ok, map()} | {error, error()}. 457 | handle_publish_mqtt5_constraints( 458 | Topic, InputPayload, InputProperties, QoS, IsRetain, AgentId, State) -> 459 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 460 | 461 | try verify_publish_constraints(QoS, IsRetain, Mode) of 462 | %% Rate-limit agents with mode=default. 463 | _ when Mode =:= default -> 464 | handle_publish_mqtt5_ratelimits(Topic, InputPayload, InputProperties, AgentId, State); 465 | _ -> 466 | handle_publish_mqtt5(Topic, InputPayload, InputProperties, AgentId, State) 467 | catch 468 | T:R -> 469 | error_logger:error_msg( 470 | "Error on publish: invalid constraints check, qos = ~p, retain = ~p " 471 | "from the agent = '~s' using mode = '~s', " 472 | "exception_type = ~p, exception_reason = ~p", 473 | [QoS, IsRetain, mqttgw_id:format_agent_id(AgentId), Mode, T, R]), 474 | {error, #{reason_code => impl_specific_error}} 475 | end. 476 | 477 | -spec handle_publish_mqtt5_ratelimits(topic(), binary(), map(), mqttgw_id:agent_id(), state()) 478 | -> {ok, map()} | {error, error()}. 479 | handle_publish_mqtt5_ratelimits(Topic, InputPayload, InputProperties, AgentId, State) -> 480 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 481 | 482 | case State#state.config#config.rate_limit of 483 | {enabled, Constraints} -> 484 | Now = State#state.time, 485 | %% 1 second range. 486 | RangeStartTime = Now - 1000, 487 | Bytes = byte_size(InputPayload), 488 | Data = #{bytes => Bytes, time => Now}, 489 | case mqttgw_ratelimitstate:put(AgentId, Data, RangeStartTime, Constraints) of 490 | ok -> 491 | handle_publish_mqtt5(Topic, InputPayload, InputProperties, AgentId, State); 492 | {error, Reason} -> 493 | error_logger:error_msg( 494 | "Error on publish: rate limit exceeded " 495 | "for the agent = '~s' using mode = '~s', " 496 | "reason = ~p", 497 | [mqttgw_id:format_agent_id(AgentId), Mode, Reason]), 498 | {error, #{reason_code => impl_specific_error}} 499 | end; 500 | _ -> 501 | handle_publish_mqtt5(Topic, InputPayload, InputProperties, AgentId, State) 502 | end. 503 | 504 | -spec handle_publish_mqtt5(topic(), binary(), map(), mqttgw_id:agent_id(), state()) 505 | -> {ok, map()} | {error, error()}. 506 | handle_publish_mqtt5(Topic, InputPayload, InputProperties, AgentId, State) -> 507 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 508 | 509 | BrokerId = State#state.config#config.id, 510 | InputMessage = #message{payload = InputPayload, properties = InputProperties}, 511 | try handle_message_properties(InputMessage, AgentId, BrokerId, State) of 512 | Message -> 513 | case handle_publish_authz(Topic, Message, AgentId, State) of 514 | ok -> 515 | %% TODO: don't modify message payload on publish (only properties) 516 | %% 517 | %% We can't use the following code now 518 | %% because currently there is no way to process 519 | %% MQTT Properties from within 'on_deliver' hook 520 | %% that is triggered for clients connected via MQTT v3. 521 | %% 522 | %% #message{payload=Payload, properties=Properties} = Message, 523 | %% Changes = 524 | %% #{payload => Payload, 525 | %% properties => InputProperties}, 526 | Changes = 527 | #{payload => envelope(Message), 528 | properties => InputProperties#{p_user_property => []}}, 529 | {ok, Changes}; 530 | Error -> 531 | Error 532 | end 533 | catch 534 | T:R -> 535 | error_logger:error_msg( 536 | "Error on publish: an invalid message = ~p with properties = ~p " 537 | "from the agent = '~s' using mode = '~s', " 538 | "exception_type = ~p, exception_reason = ~p", 539 | [InputPayload, InputProperties, mqttgw_id:format_agent_id(AgentId), Mode, T, R]), 540 | {error, #{reason_code => impl_specific_error}} 541 | end. 542 | 543 | -spec handle_publish_authz(topic(), message(), mqttgw_id:agent_id(), state()) 544 | -> ok | {error, error()}. 545 | handle_publish_authz(Topic, Message, AgentId, State) -> 546 | handle_publish_authz_config(Topic, Message, AgentId, State). 547 | 548 | -spec handle_publish_authz_config(topic(), message(), mqttgw_id:agent_id(), state()) 549 | -> ok | {error, error()}. 550 | handle_publish_authz_config(Topic, Message, AgentId, State) -> 551 | case {State#state.config#config.authz, State#state.config#config.dynsub} of 552 | {disabled, disabled} -> 553 | ok; 554 | {disabled, enabled} -> 555 | BrokerId = State#state.config#config.id, 556 | handle_publish_authz_broker_request( 557 | Topic, Message, mqttgw_id:format_account_id(BrokerId), 558 | mqttgw_id:format_agent_id(BrokerId), BrokerId, AgentId, State); 559 | {{enabled, _Config}, _} -> 560 | BrokerId = State#state.config#config.id, 561 | handle_publish_authz_topic( 562 | Topic, Message, BrokerId, AgentId, State) 563 | end. 564 | 565 | -spec handle_publish_authz_topic( 566 | topic(), message(), mqttgw_id:agent_id(), mqttgw_id:agent_id(), state()) 567 | -> ok | {error, error()}. 568 | handle_publish_authz_topic(Topic, Message, BrokerId, AgentId, State) -> 569 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 570 | 571 | try {verify_publish_topic( 572 | Topic, mqttgw_id:format_account_id(AgentId), mqttgw_id:format_agent_id(AgentId), Mode), 573 | State#state.config#config.dynsub} of 574 | {_, disabled} -> 575 | ok; 576 | {_, _} -> 577 | handle_publish_authz_broker_request( 578 | Topic, Message, mqttgw_id:format_account_id(BrokerId), 579 | mqttgw_id:format_agent_id(BrokerId), BrokerId, AgentId, State) 580 | catch 581 | T:R -> 582 | error_logger:error_msg( 583 | "Error on publish: publishing to the topic = ~p isn't allowed " 584 | "for the agent = '~s' using mode = '~s', " 585 | "exception_type = ~p, exception_reason = ~p", 586 | [Topic, mqttgw_id:format_agent_id(AgentId), Mode, T, R]), 587 | {error, #{reason_code => not_authorized}} 588 | end. 589 | 590 | -spec handle_publish_authz_broker_request( 591 | topic(), message(), binary(), binary(), mqttgw_id:agent_id(), mqttgw_id:agent_id(), state()) 592 | -> ok | {error, error()}. 593 | handle_publish_authz_broker_request( 594 | [<<"agents">>, _, <<"api">>, Version, <<"out">>, BrokerAccoundId], 595 | Message, BrokerAccoundId, _BrokerAgentId, BrokerId, AgentId, State) -> 596 | handle_publish_authz_broker_request_payload(Version, Message, BrokerId, AgentId, State); 597 | handle_publish_authz_broker_request( 598 | _Topic, _Message, _BrokerAccoundId, _BrokerAgentId, _BrokerId, _AgentId, _State) -> 599 | ok. 600 | 601 | -spec handle_publish_authz_broker_request_payload( 602 | binary(), message(), mqttgw_id:agent_id(), mqttgw_id:agent_id(), state()) 603 | -> ok | {error, error()}. 604 | handle_publish_authz_broker_request_payload( 605 | Version, #message{payload = Payload, properties = Properties}, BrokerId, AgentId, State) -> 606 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 607 | 608 | try {Mode, jsx:decode(Payload, [return_maps]), parse_broker_request_properties(Properties)} of 609 | {service, 610 | #{<<"object">> := Object, 611 | <<"subject">> := Subject}, 612 | #{type := <<"request">>, 613 | method := <<"subscription.create">>, 614 | correlation_data := CorrData, 615 | response_topic := RespTopic}} -> 616 | handle_publish_broker_dynsub_create_request( 617 | Version, Object, Subject, CorrData, RespTopic, BrokerId, AgentId, State, false); 618 | {service, 619 | #{<<"object">> := Object, 620 | <<"subject">> := Subject}, 621 | #{type := <<"request">>, 622 | method := <<"broadcast_subscription.create">>, 623 | correlation_data := CorrData, 624 | response_topic := RespTopic}} -> 625 | handle_publish_broker_dynsub_create_request( 626 | Version, Object, Subject, CorrData, RespTopic, BrokerId, AgentId, State, true); 627 | {service, 628 | #{<<"object">> := Object, 629 | <<"subject">> := Subject}, 630 | #{type := <<"request">>, 631 | method := <<"subscription.delete">>, 632 | correlation_data := CorrData, 633 | response_topic := RespTopic}} -> 634 | handle_publish_broker_dynsub_delete_request( 635 | Version, Object, Subject, CorrData, RespTopic, BrokerId, AgentId, State, false); 636 | {service, 637 | #{<<"object">> := Object, 638 | <<"subject">> := Subject}, 639 | #{type := <<"request">>, 640 | method := <<"broadcast_subscription.delete">>, 641 | correlation_data := CorrData, 642 | response_topic := RespTopic}} -> 643 | handle_publish_broker_dynsub_delete_request( 644 | Version, Object, Subject, CorrData, RespTopic, BrokerId, AgentId, State, true); 645 | _ -> 646 | error_logger:error_msg( 647 | "Error on publish: unsupported broker request = ~p with properties = ~p " 648 | "from the agent = '~s' using mode = '~s', ", 649 | [Payload, Properties, mqttgw_id:format_agent_id(AgentId), Mode]), 650 | {error, #{reason_code => impl_specific_error}} 651 | catch 652 | T:R -> 653 | error_logger:error_msg( 654 | "Error on publish: an invalid broker request = ~p with properties = ~p " 655 | "from the agent = '~s' using mode = '~s', " 656 | "exception_type = ~p, exception_reason = ~p", 657 | [Payload, Properties, mqttgw_id:format_agent_id(AgentId), Mode, T, R]), 658 | {error, #{reason_code => impl_specific_error}} 659 | end. 660 | 661 | -spec handle_publish_broker_dynsub_create_request( 662 | binary(), mqttgw_dynsub:object(), mqttgw_dynsub:subject(), 663 | binary(), binary(), mqttgw_id:agent_id(), mqttgw_id:agent_id(), state(), boolean()) 664 | -> ok | {error, error()}. 665 | handle_publish_broker_dynsub_create_request( 666 | Version, Object, Subject, CorrData, RespTopic, BrokerId, AgentId, State, IsBroadcast) -> 667 | #state{ 668 | time=Time, 669 | unique_id=UniqueId, 670 | session=#session{id=SessionId, parent_id=ParentSessionId}} = State, 671 | SessionPairId = format_session_id(SessionId, ParentSessionId), 672 | 673 | %% Subscribe the agent to the app's topic and send a success response 674 | App = mqttgw_id:format_account_id(AgentId), 675 | Data = #{app => App, object => Object, version => Version}, 676 | {Topic, QoS} = case IsBroadcast of 677 | false -> 678 | mqttgw_dyn_srv:authz_subscription_topic(Data); 679 | true -> 680 | mqttgw_dyn_srv:authz_broadcast_subscription_topic(Data) 681 | end, 682 | DynsubRespData = {CorrData, RespTopic, ?BROKER_CONNECTION, 683 | BrokerId, UniqueId, SessionPairId, Time}, 684 | mqttgw_dyn_srv:create_dynsub(Subject, Topic, QoS, DynsubRespData). 685 | 686 | -spec handle_publish_broker_dynsub_delete_request( 687 | binary(), mqttgw_dynsub:object(), mqttgw_dynsub:subject(), 688 | binary(), binary(), mqttgw_id:agent_id(), mqttgw_id:agent_id(), state(), boolean()) 689 | -> ok | {error, error()}. 690 | handle_publish_broker_dynsub_delete_request( 691 | Version, Object, Subject, CorrData, RespTopic, BrokerId, AgentId, State, IsBroadcast) -> 692 | #state{ 693 | time=Time, 694 | unique_id=UniqueId, 695 | session=#session{id=SessionId, parent_id=ParentSessionId}} = State, 696 | SessionPairId = format_session_id(SessionId, ParentSessionId), 697 | 698 | %% Unsubscribe the agent from the app's topic and send a success response 699 | App = mqttgw_id:format_account_id(AgentId), 700 | Data = #{app => App, object => Object, version => Version}, 701 | Topic = case IsBroadcast of 702 | false -> mqttgw_dyn_srv:authz_subscription_topic(Data); 703 | true -> mqttgw_dyn_srv:authz_broadcast_subscription_topic(Data) 704 | end, 705 | DynsubRespData = {CorrData, RespTopic, ?BROKER_CONNECTION, 706 | BrokerId, UniqueId, SessionPairId, Time}, 707 | mqttgw_dyn_srv:delete_dynsub(Subject, Data, DynsubRespData, Topic). 708 | 709 | -spec handle_message_properties(message(), mqttgw_id:agent_id(), mqttgw_id:agent_id(), state()) 710 | -> message(). 711 | handle_message_properties(Message, AgentId, BrokerId, State) -> 712 | #state{ 713 | time=Time, 714 | unique_id=UniqueId, 715 | session=#session{id=SessionId, parent_id=ParentSessionId, connection=Conn}} = State, 716 | SessionPairId = format_session_id(SessionId, ParentSessionId), 717 | 718 | UpdatedProperties = 719 | validate_message_properties( 720 | update_message_properties( 721 | Message#message.properties, 722 | Conn, 723 | AgentId, 724 | BrokerId, 725 | UniqueId, 726 | SessionPairId, 727 | Time), 728 | Conn, 729 | AgentId), 730 | 731 | Message#message{ 732 | properties = UpdatedProperties}. 733 | 734 | -spec validate_message_properties(map(), connection(), mqttgw_id:agent_id()) -> map(). 735 | validate_message_properties(Properties, Conn, AgentId) -> 736 | #connection{mode=Mode} = Conn, 737 | UserProperties = maps:from_list(maps:get(p_user_property, Properties, [])), 738 | 739 | %% Type of the value for user property is always an utf8 string 740 | IsUtf8String = fun(Val) -> 741 | is_binary(catch unicode:characters_to_binary(Val, utf8, utf8)) 742 | end, 743 | IsUtf8Pair = fun({Key, Val}, Acc) -> 744 | IsUtf8String(Key) andalso IsUtf8String(Val) andalso Acc 745 | end, 746 | case lists:foldl(IsUtf8Pair, true, maps:to_list(UserProperties)) of 747 | false -> error({bad_user_property, Properties}); 748 | _ -> ok 749 | end, 750 | 751 | %% Required properties for p_user_property(type)=request|response 752 | case maps:find(<<"type">>, UserProperties) of 753 | {ok, <<"request">>} -> 754 | %% Required properties: 755 | %% - p_user_property(method) 756 | %% - p_correlation_data 757 | %% - p_response_topic 758 | case 759 | { maps:find(<<"method">>, UserProperties), 760 | maps:find(p_correlation_data, Properties), 761 | maps:find(p_response_topic, Properties) } of 762 | 763 | {error, _, _} -> error({missing_method_user_property, Properties}); 764 | {_, error, _} -> error({missing_correlation_data_property, Properties}); 765 | {_, _, error} -> error({missing_response_topic_property, Properties}); 766 | %% Only services can specify a response topic that is not assosiated 767 | %% with their account 768 | {_, _, {ok, _}} when Mode =:= service -> ok; 769 | {_, _, {ok, RT}} -> 770 | verify_response_topic( 771 | binary:split(RT, <<$/>>, [global]), 772 | mqttgw_id:format_agent_id(AgentId)) 773 | end; 774 | {ok, <<"response">>} -> 775 | %% Required properties: 776 | %% - p_user_property(status) 777 | %% - p_correlation_data 778 | case 779 | { maps:find(<<"status">>, UserProperties), 780 | maps:find(p_correlation_data, Properties) } of 781 | 782 | {error, _} -> error({missing_status_user_property, Properties}); 783 | {_, error} -> error({missing_correlation_data_property, Properties}); 784 | _ -> ok 785 | end; 786 | %% TODO[2]: enable checking the constraint 787 | % {ok, <<"event">>} -> 788 | % %% Required properties: 789 | % %% - p_user_property(label) 790 | % case maps:find(<<"label">>, UserProperties) of 791 | % error -> error({missing_label_user_property, Properties}); 792 | % _ -> ok 793 | % end; 794 | _ -> 795 | ok 796 | end, 797 | 798 | %% Required properties for mode=default 799 | %% NOTE: default agent must always pass 'local_timestamp' property 800 | case {Mode, maps:find(<<"local_initial_timediff">>, UserProperties)} of 801 | {default, error} -> error({missing_local_initial_timediff_user_property, Properties}); 802 | _ -> ok 803 | end, 804 | 805 | Properties. 806 | 807 | -spec verify_response_topic(topic(), binary()) -> ok. 808 | verify_response_topic([<<"agents">>, Me, <<"api">>, _, <<"in">>, _], Me) -> 809 | ok; 810 | verify_response_topic(Topic, AgentId) -> 811 | error({bad_response_topic, Topic, AgentId}). 812 | 813 | -spec update_message_properties( 814 | map(), connection(), mqttgw_id:agent_id(), mqttgw_id:agent_id(), 815 | binary(), binary(), non_neg_integer()) 816 | -> map(). 817 | update_message_properties(Properties, Conn, AgentId, BrokerId, UniqueId, SessionPairId, Time) -> 818 | #connection{mode=Mode, version=Ver} = Conn, 819 | 820 | TimeB = integer_to_binary(Time), 821 | UserProperties0 = maps:from_list(maps:get(p_user_property, Properties, [])), 822 | 823 | %% Everything is "event" by default 824 | UserProperties1 = 825 | case maps:find(<<"type">>, UserProperties0) of 826 | error -> UserProperties0#{<<"type">> => <<"event">>}; 827 | _ -> UserProperties0 828 | end, 829 | 830 | %% Override authn properties 831 | UserProperties2 = 832 | case Mode of 833 | bridge -> 834 | %% We do not override authn properties for 'bridge' mode, 835 | %% but verify that they are exist 836 | validate_authn_properties(UserProperties1); 837 | _ -> 838 | UserProperties1#{<<"agent_id">> => mqttgw_id:format_agent_id(AgentId)} 839 | end, 840 | 841 | %% Additional connection properties 842 | UserProperties3 = 843 | UserProperties2#{ 844 | <<"connection_version">> => Ver, 845 | <<"connection_mode">> => format_connection_mode(Mode)}, 846 | 847 | %% Additional broker properties 848 | UserProperties4 = 849 | UserProperties3#{<<"broker_agent_id">> => mqttgw_id:format_agent_id(BrokerId)}, 850 | UserProperties5 = 851 | case maps:find(<<"broker_initial_processing_timestamp">>, UserProperties4) of 852 | {ok, _BrokerInitProcTs} -> 853 | UserProperties4#{ 854 | <<"broker_processing_timestamp">> => TimeB}; 855 | _ -> 856 | UserProperties4#{ 857 | <<"broker_processing_timestamp">> => TimeB, 858 | <<"broker_initial_processing_timestamp">> => TimeB} 859 | end, 860 | 861 | %% Additional svc properties 862 | UserProperties6 = 863 | case { 864 | maps:find(<<"timestamp">>, UserProperties5), 865 | maps:find(<<"initial_timestamp">>, UserProperties5)} of 866 | {{ok, Timestamp}, error} -> 867 | UserProperties5#{ 868 | <<"initial_timestamp">> => Timestamp}; 869 | _ -> 870 | UserProperties5 871 | end, 872 | 873 | %% Additional usr properties 874 | UserProperties7 = 875 | case { 876 | maps:take(<<"local_timestamp">>, UserProperties6), 877 | maps:find(<<"local_initial_timediff">>, UserProperties6)} of 878 | %% NOTE: remove 'local_initial_timediff' if it was sent by an agent in 'default' mode 879 | {error, {ok, _}} when Mode =:= default -> 880 | remove_property(<<"local_initial_timediff">>, UserProperties6); 881 | {{LocalTs, Prop7}, error} -> 882 | LocalTimeDiff = integer_to_binary(Time - binary_to_integer(LocalTs)), 883 | Prop7#{<<"local_initial_timediff">> => LocalTimeDiff}; 884 | _ -> 885 | UserProperties6 886 | end, 887 | 888 | %% Tracking properties 889 | UserProperties8 = 890 | case Mode of 891 | %% NOTE: remove 'tracking_id' if it was sent by an agent in 'default' mode 892 | default -> remove_property(<<"tracking_id">>, UserProperties7); 893 | _ -> UserProperties7 894 | end, 895 | UserProperties9 = 896 | update_session_tracking_label_property( 897 | UniqueId, 898 | SessionPairId, 899 | UserProperties8), 900 | 901 | Properties#{p_user_property => maps:to_list(UserProperties9)}. 902 | 903 | -spec update_session_tracking_label_property(binary(), binary(), map()) -> map(). 904 | update_session_tracking_label_property(UniqueId, SessionPairId, UserProperties) -> 905 | case maps:find(<<"session_tracking_label">>, UserProperties) of 906 | {ok, SessionTrackingLabel} -> 907 | L0 = binary:split(SessionTrackingLabel, <<$\s>>, [global]), 908 | L1 = gb_sets:to_list(gb_sets:add(SessionPairId, gb_sets:from_list(L0))), 909 | UserProperties#{ 910 | <<"session_tracking_label">> => binary_join(L1, <<$\s>>)}; 911 | _ -> 912 | TrackingId = format_tracking_id(UniqueId, SessionPairId), 913 | UserProperties#{ 914 | <<"tracking_id">> => TrackingId, 915 | <<"session_tracking_label">> => SessionPairId} 916 | end. 917 | 918 | -spec remove_property(binary(), map()) -> map(). 919 | remove_property(Name, UserProperties) -> 920 | case maps:take(Name, UserProperties) of 921 | {ok, M} -> M; 922 | _ -> UserProperties 923 | end. 924 | 925 | -spec handle_mqtt3_envelope_properties(message()) -> message(). 926 | handle_mqtt3_envelope_properties(Message) -> 927 | Message#message{ 928 | properties = to_mqtt5_properties(Message#message.properties, #{})}. 929 | 930 | -spec to_mqtt3_envelope_properties(map(), map()) -> map(). 931 | to_mqtt3_envelope_properties(Properties, Acc0) -> 932 | Acc1 = 933 | case maps:find(p_user_property, Properties) of 934 | {ok, UserL} -> maps:merge(Acc0, maps:from_list(UserL)); 935 | error -> Acc0 936 | end, 937 | 938 | Acc2 = 939 | case maps:find(p_correlation_data, Properties) of 940 | {ok, CorrData} -> Acc1#{<<"correlation_data">> => CorrData}; 941 | error -> Acc1 942 | end, 943 | 944 | Acc3 = 945 | case maps:find(p_response_topic, Properties) of 946 | {ok, RespTopic} -> Acc2#{<<"response_topic">> => RespTopic}; 947 | error -> Acc2 948 | end, 949 | 950 | Acc3. 951 | 952 | -spec to_mqtt5_properties(map(), map()) -> map(). 953 | to_mqtt5_properties(Rest0, Acc0) -> 954 | {Rest1, Acc1} = 955 | case maps:take(<<"response_topic">>, Rest0) of 956 | {RespTopic, M1} -> {M1, Acc0#{p_response_topic => RespTopic}}; 957 | error -> {Rest0, Acc0} 958 | end, 959 | 960 | {Rest2, Acc2} = 961 | case maps:take(<<"correlation_data">>, Rest1) of 962 | {CorrData, M2} -> {M2, Acc1#{p_correlation_data => CorrData}}; 963 | error -> {Rest1, Acc1} 964 | end, 965 | 966 | UserProperties = 967 | maps:to_list( 968 | maps:merge( 969 | maps:from_list(maps:get(p_user_property, Acc2, [])), 970 | Rest2)), 971 | case length(UserProperties) of 972 | 0 -> Acc2; 973 | _ -> Acc2#{p_user_property => UserProperties} 974 | end. 975 | 976 | -spec verify_publish_constraints(qos(), boolean(), connection_mode()) -> ok. 977 | verify_publish_constraints(QoS, IsRetain, Mode) -> 978 | ok = verify_publish_qos_constraint(QoS, Mode), 979 | ok = verify_publish_retain_constraint(IsRetain, Mode), 980 | ok. 981 | 982 | -spec verify_publish_qos_constraint(qos(), connection_mode()) -> ok. 983 | %% Any for anyone 984 | verify_publish_qos_constraint(_QoS, _Mode) -> 985 | ok. 986 | 987 | -spec verify_publish_retain_constraint(boolean(), connection_mode()) -> ok. 988 | %% Any for 'service' mode 989 | verify_publish_retain_constraint(_IsRetain, service) -> 990 | ok; 991 | %% Only 'false' for anyone else 992 | verify_publish_retain_constraint(false, _Mode) -> 993 | ok; 994 | verify_publish_retain_constraint(IsRetain, _Mode) -> 995 | error({bad_retain, IsRetain}). 996 | 997 | -spec verify_publish_topic(topic(), binary(), binary(), connection_mode()) -> ok. 998 | %% Broadcast: 999 | %% -> event(app-to-any): apps/ACCOUNT_ID(ME)/api/v1/BROADCAST_URI 1000 | verify_publish_topic([<<"apps">>, Me, <<"api">>, _ | _], Me, _AgentId, Mode) 1001 | when (Mode =:= service) or (Mode =:= observer) or (Mode =:= bridge) 1002 | -> ok; 1003 | %% Multicast: 1004 | %% -> request(one-to-app): agents/AGENT_ID(ME)/api/v1/out/ACCOUNT_ID 1005 | %% -> event(one-to-app): agents/AGENT_ID(ME)/api/v1/out/ACCOUNT_ID 1006 | verify_publish_topic([<<"agents">>, Me, <<"api">>, _, <<"out">>, _], _AccountId, Me, _Mode) 1007 | -> ok; 1008 | %% Unicast: 1009 | %% -> request(one-to-one): agents/AGENT_ID/api/v1/in/ACCOUNT_ID(ME) 1010 | %% -> response(one-to-one): agents/AGENT_ID/api/v1/in/ACCOUNT_ID(ME) 1011 | verify_publish_topic([<<"agents">>, _, <<"api">>, _, <<"in">>, Me], Me, _AgentId, Mode) 1012 | when (Mode =:= service) or (Mode =:= observer) or (Mode =:= bridge) 1013 | -> ok; 1014 | %% Broadcast dynsub: 1015 | %% -> event(agent-to-many): broadcasts/APP_ACCOUNT_ID/api/v1/BROADCAST_URI 1016 | verify_publish_topic([<<"broadcasts">>, _AppAccountId, <<"api">>, _ | _] = Topic, 1017 | _AccountId, AgentId, Mode) 1018 | -> case vmq_subscriber_db:read({"", AgentId}) of 1019 | undefined -> error({no_agent_in_subscriber_db, Topic, AgentId, Mode}); 1020 | NodeSubs when length(NodeSubs) > 1 -> 1021 | error({forbidden_multinode_sub, Topic, AgentId, Mode}); 1022 | %% If agent is subscribed to this topic - let him publish 1023 | [{_Node, _, Subs}] -> 1024 | case lists:search(fun({T, _}) -> T == Topic end, Subs) of 1025 | false -> error({missing_broadcast_dynsub, Topic, AgentId, Mode}); 1026 | {value, _} -> ok 1027 | end 1028 | end; 1029 | %% Forbidding publishing to any other topics 1030 | verify_publish_topic(Topic, _AccountId, AgentId, Mode) 1031 | -> error({nomatch_publish_topic, Topic, AgentId, Mode}). 1032 | 1033 | %% ============================================================================= 1034 | %% API: Deliver 1035 | %% ============================================================================= 1036 | 1037 | -spec handle_deliver_mqtt3(binary(), mqttgw_id:agent_id(), state()) 1038 | -> ok | {ok, list()} | {error, error()}. 1039 | handle_deliver_mqtt3(InputPayload, RecvId, State) -> 1040 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 1041 | 1042 | try handle_mqtt3_envelope_properties(validate_envelope(parse_envelope(InputPayload))) of 1043 | InputMessage -> 1044 | handle_deliver_mqtt3_changes(InputMessage, State) 1045 | catch 1046 | T:R -> 1047 | error_logger:error_msg( 1048 | "Error on deliver: an invalid message = ~p " 1049 | "from the agent = '~s' using mode = '~s', " 1050 | "exception_type = ~p, exception_reason = ~p", 1051 | [InputPayload, mqttgw_id:format_agent_id(RecvId), Mode, T, R]), 1052 | {error, #{reason_code => impl_specific_error}} 1053 | end. 1054 | 1055 | -spec handle_deliver_mqtt3_changes(message(), state()) -> ok | {ok, list()}. 1056 | handle_deliver_mqtt3_changes(Message, State) -> 1057 | #message{properties=Properties} = Message, 1058 | #state{ 1059 | time=Time, 1060 | unique_id=UniqueId, 1061 | session=#session{id=SessionId, parent_id=ParentSessionId}} = State, 1062 | SessionPairId = format_session_id(SessionId, ParentSessionId), 1063 | 1064 | ModifiedMessage = Message#message{ 1065 | properties=update_deliver_message_properties( 1066 | Properties, UniqueId, SessionPairId, Time)}, 1067 | 1068 | {ok, [{payload, envelope(ModifiedMessage)}]}. 1069 | 1070 | -spec handle_deliver_mqtt5( 1071 | binary(), map(), mqttgw_id:agent_id(), state()) 1072 | -> ok | {ok, map()} | {error, error()}. 1073 | handle_deliver_mqtt5(InputPayload, _InputProperties, RecvId, State) -> 1074 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 1075 | 1076 | %% TODO: don't modify message payload on publish (only properties) 1077 | % InputMessage = #message{payload = InputPayload, properties = InputProperties}, 1078 | % handle_deliver_mqtt5_changes(Mode, InputMessage). 1079 | 1080 | try handle_mqtt3_envelope_properties(validate_envelope(parse_envelope(InputPayload))) of 1081 | InputMessage -> 1082 | handle_deliver_mqtt5_changes(Mode, InputMessage, State) 1083 | catch 1084 | T:R -> 1085 | error_logger:error_msg( 1086 | "Error on deliver: an invalid message = ~p " 1087 | "from the agent = '~s' using mode = '~s', " 1088 | "exception_type = ~p, exception_reason = ~p", 1089 | [InputPayload, mqttgw_id:format_agent_id(RecvId), Mode, T, R]), 1090 | {error, #{reason_code => impl_specific_error}} 1091 | end. 1092 | 1093 | -spec handle_deliver_mqtt5_changes(connection_mode(), message(), state()) 1094 | -> ok | {ok, map()}. 1095 | handle_deliver_mqtt5_changes(_Mode, Message, State) -> 1096 | #message{payload=Payload, properties=Properties} = Message, 1097 | #state{ 1098 | time=Time, 1099 | unique_id=UniqueId, 1100 | session=#session{id=SessionId, parent_id=ParentSessionId}} = State, 1101 | SessionPairId = format_session_id(SessionId, ParentSessionId), 1102 | 1103 | Changes = 1104 | #{payload => Payload, 1105 | properties => update_deliver_message_properties( 1106 | Properties, UniqueId, SessionPairId, Time)}, 1107 | 1108 | {ok, Changes}. 1109 | 1110 | -spec update_deliver_message_properties( 1111 | map(), binary(), binary(), non_neg_integer()) 1112 | -> map(). 1113 | update_deliver_message_properties(Properties, UniqueId, SessionPairId, Time) -> 1114 | UserProperties0 = maps:from_list(maps:get(p_user_property, Properties, [])), 1115 | 1116 | %% Additional broker properties 1117 | UserProperties1 = 1118 | UserProperties0#{ 1119 | <<"broker_timestamp">> => integer_to_binary(Time)}, 1120 | 1121 | %% Tracking properties 1122 | UserProperties2 = 1123 | update_session_tracking_label_property( 1124 | UniqueId, 1125 | SessionPairId, 1126 | UserProperties1), 1127 | 1128 | Properties#{p_user_property => maps:to_list(UserProperties2)}. 1129 | 1130 | %% ============================================================================= 1131 | %% API: Subscribe 1132 | %% ============================================================================= 1133 | 1134 | -spec handle_subscribe_authz([subscription()], mqttgw_id:agent_id(), state()) 1135 | -> ok | {error, error()}. 1136 | handle_subscribe_authz(Subscriptions, AgentId, State) -> 1137 | handle_subscribe_authz_config(Subscriptions, AgentId, State). 1138 | 1139 | -spec handle_subscribe_authz_config([subscription()], mqttgw_id:agent_id(), state()) 1140 | -> ok | {error, error()}. 1141 | handle_subscribe_authz_config(Subscriptions, AgentId, State) -> 1142 | case State#state.config#config.authz of 1143 | disabled -> 1144 | handle_subscribe_success(Subscriptions, AgentId, State); 1145 | {enabled, _Config} -> 1146 | handle_subscribe_authz_topic(Subscriptions, AgentId, State) 1147 | end. 1148 | 1149 | -spec handle_subscribe_authz_topic([subscription()], mqttgw_id:agent_id(), state()) 1150 | -> ok | {error, error()}. 1151 | handle_subscribe_authz_topic(Subscriptions, AgentId, State) -> 1152 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 1153 | 1154 | try [verify_subscribtion( 1155 | Topic, 1156 | mqttgw_id:format_account_id(AgentId), 1157 | mqttgw_id:format_agent_id(AgentId), 1158 | Mode) 1159 | || {Topic, _QoS} <- Subscriptions] of 1160 | _ -> 1161 | handle_subscribe_success(Subscriptions, AgentId, State) 1162 | catch 1163 | T:R -> 1164 | error_logger:error_msg( 1165 | "Error on subscribe: one of the subscriptions = ~p isn't allowed " 1166 | "for the agent = '~s' using mode = '~s', " 1167 | "exception_type = ~p, exception_reason = ~p", 1168 | [Subscriptions, mqttgw_id:format_agent_id(AgentId), Mode, T, R]), 1169 | {error, #{reason_code => not_authorized}} 1170 | end. 1171 | 1172 | -spec handle_subscribe_success([subscription()], mqttgw_id:agent_id(), state()) 1173 | -> ok | {error, error()}. 1174 | handle_subscribe_success(Topics, AgentId, State) -> 1175 | #state{session=#session{connection=#connection{mode=Mode}}} = State, 1176 | 1177 | error_logger:info_msg( 1178 | "Agent = '~s' subscribed: mode = '~s', topics = ~p", 1179 | [mqttgw_id:format_agent_id(AgentId), Mode, Topics]), 1180 | 1181 | ok. 1182 | 1183 | -spec verify_subscribtion(topic(), binary(), binary(), connection_mode()) -> ok. 1184 | verify_subscribtion([<<"$share">>, _Group | Topic], AccountId, AgentId, Mode) -> 1185 | verify_subscribe_topic(Topic, AccountId, AgentId, Mode); 1186 | verify_subscribtion(Topic, AccountId, AgentId, Mode) -> 1187 | verify_subscribe_topic(Topic, AccountId, AgentId, Mode). 1188 | 1189 | -spec verify_subscribe_topic(topic(), binary(), binary(), connection_mode()) -> ok. 1190 | %% Observer can subscribe to any topic 1191 | verify_subscribe_topic(_Topic, _AccountId, _AgentId, observer) 1192 | -> ok; 1193 | %% Broadcast: 1194 | %% <- event(any-from-app): apps/ACCOUNT_ID/api/v1/BROADCAST_URI 1195 | verify_subscribe_topic([<<"apps">>, _, <<"api">>, _ | _], _AccountId, _AgentId, Mode) 1196 | when (Mode =:= service) or (Mode =:= bridge) 1197 | -> ok; 1198 | %% Multicast: 1199 | %% <- request(app-from-any): agents/+/api/v1/out/ACCOUNT_ID(ME) 1200 | verify_subscribe_topic([<<"agents">>, _, <<"api">>, _, <<"out">>, Me], Me, _AgentId, Mode) 1201 | when (Mode =:= service) or (Mode =:= bridge) 1202 | -> ok; 1203 | %% Unicast: 1204 | %% <- request(one-from-one): agents/AGENT_ID(ME)/api/v1/in/ACCOUNT_ID 1205 | %% <- request(one-from-any): agents/AGENT_ID(ME)/api/v1/in/+ 1206 | %% <- response(one-from-one): agents/AGENT_ID(ME)/api/v1/in/ACCOUNT_ID 1207 | %% <- response(one-from-any): agents/AGENT_ID(ME)/api/v1/in/+ 1208 | verify_subscribe_topic([<<"agents">>, Me, <<"api">>, _, <<"in">>, _], _AccountId, Me, _Mode) 1209 | -> ok; 1210 | %% Forbidding subscribing to any other topics 1211 | verify_subscribe_topic(Topic, _AccountId, AgentId, Mode) 1212 | -> error({nomatch_subscribe_topic, Topic, AgentId, Mode}). 1213 | 1214 | %% ============================================================================= 1215 | %% API: Broker Start 1216 | %% ============================================================================= 1217 | 1218 | -spec handle_broker_start(state()) -> ok. 1219 | handle_broker_start(State) -> 1220 | handle_broker_start_stat_config(State). 1221 | 1222 | -spec handle_broker_start_stat_config(state()) -> ok. 1223 | handle_broker_start_stat_config(State) -> 1224 | case State#state.config#config.stat of 1225 | enabled -> 1226 | #state{ 1227 | time=Time, 1228 | unique_id=UniqueId, 1229 | session=#session{ 1230 | id=SessionId, 1231 | parent_id=ParentSessionId, 1232 | created_at=Ts}, 1233 | config=#config{ 1234 | id=BrokerId}} = State, 1235 | SessionPairId = format_session_id(SessionId, ParentSessionId), 1236 | 1237 | send_audience_broadcast_event( 1238 | #{id => mqttgw_id:format_agent_id(BrokerId)}, 1239 | [ {<<"type">>, <<"event">>}, 1240 | {<<"label">>, <<"agent.enter">>}, 1241 | {<<"timestamp">>, integer_to_binary(Ts)} ], 1242 | ?BROKER_CONNECTION, 1243 | BrokerId, 1244 | BrokerId, 1245 | UniqueId, 1246 | SessionPairId, 1247 | Time), 1248 | handle_broker_start_success(); 1249 | _ -> 1250 | handle_broker_start_success() 1251 | end. 1252 | 1253 | handle_broker_start_success() -> 1254 | ok. 1255 | 1256 | %% ============================================================================= 1257 | %% API: Broker Stop 1258 | %% ============================================================================= 1259 | 1260 | -spec handle_broker_stop(state()) -> ok. 1261 | handle_broker_stop(State) -> 1262 | handle_broker_stop_stat_config(State). 1263 | 1264 | -spec handle_broker_stop_stat_config(state()) -> ok. 1265 | handle_broker_stop_stat_config(State) -> 1266 | case State#state.config#config.stat of 1267 | enabled -> 1268 | #state{ 1269 | time=Time, 1270 | unique_id=UniqueId, 1271 | session=#session{ 1272 | id=SessionId, 1273 | parent_id=ParentSessionId}, 1274 | config=#config{ 1275 | id=BrokerId}} = State, 1276 | SessionPairId = format_session_id(SessionId, ParentSessionId), 1277 | 1278 | send_audience_broadcast_event( 1279 | #{id => mqttgw_id:format_agent_id(BrokerId)}, 1280 | [ {<<"type">>, <<"event">>}, 1281 | {<<"label">>, <<"agent.leave">>}, 1282 | {<<"timestamp">>, integer_to_binary(Time)} ], 1283 | ?BROKER_CONNECTION, 1284 | BrokerId, 1285 | BrokerId, 1286 | UniqueId, 1287 | SessionPairId, 1288 | Time), 1289 | handle_broker_stop_success(); 1290 | _ -> 1291 | handle_broker_stop_success() 1292 | end. 1293 | 1294 | handle_broker_stop_success() -> 1295 | ok. 1296 | 1297 | %% ============================================================================= 1298 | %% Plugin Callbacks 1299 | %% ============================================================================= 1300 | 1301 | -spec start() -> ok. 1302 | start() -> 1303 | {ok, _} = application:ensure_all_started(?APP), 1304 | 1305 | %% Create the broker config 1306 | BrokerId = mqttgw_id:read_config(), 1307 | Config = 1308 | #config{ 1309 | id=BrokerId, 1310 | authn=mqttgw_authn:read_config(), 1311 | authz=mqttgw_authz:read_config(), 1312 | dynsub=mqttgw_dynsub:read_config(), 1313 | stat=mqttgw_stat:read_config(), 1314 | rate_limit=mqttgw_ratelimit:read_config()}, 1315 | mqttgw_state:put(config, Config), 1316 | 1317 | %% Create the broker session 1318 | Conn = ?BROKER_CONNECTION, 1319 | Time = os:system_time(millisecond), 1320 | Session = 1321 | #session{ 1322 | id=make_uuid(), 1323 | parent_id=make_uuid(), 1324 | connection=Conn, 1325 | created_at=Time}, 1326 | mqttgw_state:put(BrokerId, Session), 1327 | 1328 | handle_broker_start(broker_state(Config, Session, Time)), 1329 | ok. 1330 | 1331 | -spec stop() -> ok. 1332 | stop() -> 1333 | Config = mqttgw_state:get(config), 1334 | Time = os:system_time(millisecond), 1335 | AgentId = Config#config.id, 1336 | handle_broker_stop(broker_state(Config, mqttgw_state:get(AgentId), Time)). 1337 | 1338 | auth_on_register( 1339 | _Peer, {_MountPoint, Conn} = _SubscriberId, Username, 1340 | Password, CleanSession) -> 1341 | State = broker_initial_state(mqttgw_state:get(config), os:system_time(millisecond)), 1342 | Properties = parse_connect_properties_from_username(Username, #{}), 1343 | case handle_connect(Conn, Password, CleanSession, Properties, State) of 1344 | ok -> 1345 | ok; 1346 | {error, #{reason_code := Reason}} -> 1347 | {error, Reason} 1348 | end. 1349 | 1350 | auth_on_register_m5( 1351 | _Peer, {_MountPoint, Conn} = _SubscriberId, Username, 1352 | Password, CleanSession, Properties0) -> 1353 | State = broker_initial_state(mqttgw_state:get(config), os:system_time(millisecond)), 1354 | Properties1 = parse_connect_properties_from_username(Username, Properties0), 1355 | handle_connect(Conn, Password, CleanSession, Properties1, State). 1356 | 1357 | auth_on_publish( 1358 | _Username, {_MountPoint, Conn} = _SubscriberId, 1359 | QoS, Topic, Payload, IsRetain) -> 1360 | AgentId = parse_agent_id(Conn), 1361 | State = broker_state( 1362 | mqttgw_state:get(config), 1363 | mqttgw_state:get(AgentId), 1364 | os:system_time(millisecond)), 1365 | handle_publish_mqtt3_constraints( 1366 | Topic, Payload, QoS, IsRetain, AgentId, State). 1367 | 1368 | auth_on_publish_m5( 1369 | _Username, {_MountPoint, Conn} = _SubscriberId, 1370 | QoS, Topic, Payload, IsRetain, Properties) -> 1371 | AgentId = parse_agent_id(Conn), 1372 | State = broker_state( 1373 | mqttgw_state:get(config), 1374 | mqttgw_state:get(AgentId), 1375 | os:system_time(millisecond)), 1376 | handle_publish_mqtt5_constraints( 1377 | Topic, Payload, Properties, QoS, IsRetain, AgentId, State). 1378 | 1379 | on_deliver( 1380 | _Username, {_MountPoint, Conn} = _SubscriberId, 1381 | _QoS, _Topic, Payload, _IsRetain) -> 1382 | AgentId = parse_agent_id(Conn), 1383 | State = broker_state( 1384 | mqttgw_state:get(config), 1385 | mqttgw_state:get(AgentId), 1386 | os:system_time(millisecond)), 1387 | handle_deliver_mqtt3(Payload, AgentId, State). 1388 | 1389 | on_deliver_m5( 1390 | _Username, {_MountPoint, Conn} = _SubscriberId, 1391 | _QoS, _Topic, Payload, _IsRetain, Properties) -> 1392 | AgentId = parse_agent_id(Conn), 1393 | State = broker_state( 1394 | mqttgw_state:get(config), 1395 | mqttgw_state:get(AgentId), 1396 | os:system_time(millisecond)), 1397 | handle_deliver_mqtt5(Payload, Properties, AgentId, State). 1398 | 1399 | auth_on_subscribe( 1400 | _Username, {_MountPoint, Conn} = _SubscriberId, 1401 | Subscriptions) -> 1402 | AgentId = parse_agent_id(Conn), 1403 | State = broker_state( 1404 | mqttgw_state:get(config), 1405 | mqttgw_state:get(AgentId), 1406 | os:system_time(millisecond)), 1407 | case handle_subscribe_authz(Subscriptions, AgentId, State) of 1408 | ok -> 1409 | ok; 1410 | {error, #{reason_code := Reason}} -> 1411 | {error, Reason} 1412 | end. 1413 | 1414 | auth_on_subscribe_m5( 1415 | _Username, {_MountPoint, Conn} = _SubscriberId, 1416 | Subscriptions, _Properties) -> 1417 | AgentId = parse_agent_id(Conn), 1418 | State = broker_state( 1419 | mqttgw_state:get(config), 1420 | mqttgw_state:get(AgentId), 1421 | os:system_time(millisecond)), 1422 | handle_subscribe_authz(Subscriptions, AgentId, State). 1423 | 1424 | on_topic_unsubscribed( 1425 | {_MountPoint, Conn} = _SubscriberId, 1426 | Topics) -> 1427 | AgentId = parse_agent_id(Conn), 1428 | State = broker_state( 1429 | mqttgw_state:get(config), 1430 | mqttgw_state:get(AgentId), 1431 | os:system_time(millisecond)), 1432 | handle_topic_unsubscribed(Conn, Topics, State). 1433 | 1434 | on_client_offline({_MountPoint, Conn} = _SubscriberId) -> 1435 | AgentId = parse_agent_id(Conn), 1436 | State = broker_state( 1437 | mqttgw_state:get(config), 1438 | mqttgw_state:get(AgentId), 1439 | os:system_time(millisecond)), 1440 | handle_disconnect(AgentId, State). 1441 | 1442 | on_client_gone({_MountPoint, Conn} = _SubscriberId) -> 1443 | AgentId = parse_agent_id(Conn), 1444 | State = broker_state( 1445 | mqttgw_state:get(config), 1446 | mqttgw_state:get(AgentId), 1447 | os:system_time(millisecond)), 1448 | handle_disconnect(AgentId, State). 1449 | 1450 | %% ============================================================================= 1451 | %% Internal functions 1452 | %% ============================================================================= 1453 | 1454 | -spec broker_initial_state(config(), non_neg_integer()) -> initial_state(). 1455 | broker_initial_state(Config, Time) -> 1456 | #initial_state{config=Config, time=Time}. 1457 | 1458 | -spec broker_state(config(), session(), non_neg_integer()) -> state(). 1459 | broker_state(Config, Session, Time) -> 1460 | #state{config=Config, session=Session, unique_id=make_uuid(), time=Time}. 1461 | 1462 | -spec format_tracking_id(binary(), binary()) -> binary(). 1463 | format_tracking_id(Label, SessionId) -> 1464 | <