首次提交
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
repo_token: 0zzWBuyAWoQxHHR0JQ4jdZ12CPdvhiolr
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set
|
||||
* text=auto
|
||||
|
||||
# Require Unix line endings
|
||||
* text eol=lf
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# PRs that only touch the docs folder
|
||||
documentation:
|
||||
- all: ["docs/**/*"]
|
||||
|
||||
# PRs that only touch type files
|
||||
typescript:
|
||||
- all: ["**/*[.|-]d.ts"]
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
name: Dependency Review
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16, 18, 20]
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
check-latest: true
|
||||
cache: npm
|
||||
cache-dependency-path: package.json
|
||||
|
||||
- name: Install
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
|
||||
- name: Check licenses
|
||||
run: |
|
||||
npm run license-checker
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
npm run test:ci
|
||||
|
||||
- name: Coveralls Parallel
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel: true
|
||||
flag-name: run-${{ matrix.node-version }}-${{ matrix.os }}
|
||||
|
||||
coverage:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel-finished: true
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
language: [ 'javascript-typescript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
name: Pull Request Labeler
|
||||
|
||||
on: pull_request_target
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ts: false
|
||||
jsx: false
|
||||
flow: false
|
||||
coverage: true
|
||||
strict: true
|
||||
check-coverage: false
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@matteocollina.com. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
||||
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Aedes Contributors
|
||||
Copyright (c) 2015-2020 Matteo Collina, http://matteocollina.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
# Aedes
|
||||
|
||||

|
||||
[](https://standardjs.com/)
|
||||
[](https://github.com/moscajs/aedes/graphs/commit-activity)
|
||||
[](https://github.com/moscajs/aedes/pulls)\
|
||||
[](https://coveralls.io/github/moscajs/aedes?branch=main)
|
||||
[](https://snyk.io/test/github/moscajs/aedes)\
|
||||

|
||||
[](https://www.npmjs.com/aedes)
|
||||
[](https://www.npmjs.com/aedes)
|
||||
|
||||
[](https://opencollective.com/aedes/donate)
|
||||
|
||||
Barebone MQTT server that can run on any stream servers
|
||||
|
||||
- [Aedes](#aedes)
|
||||
- [Install](#install)
|
||||
- [Docker](#docker)
|
||||
- [API](#api)
|
||||
- [Features](#features)
|
||||
- [Examples](#examples)
|
||||
- [Clusters](#clusters)
|
||||
- [Extensions](#extensions)
|
||||
- [Middleware Plugins](#middleware-plugins)
|
||||
- [Persistence](#persistence)
|
||||
- [MQEmitter](#mqemitter)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [Mosca vs Aedes](#mosca-vs-aedes)
|
||||
- [Benchmark: Aedes](#benchmark-aedes)
|
||||
- [In memory - No clusters](#in-memory---no-clusters)
|
||||
- [Redis Persistence and Redis Emitter - With Clusters](#redis-persistence-and-redis-emitter---with-clusters)
|
||||
- [Mongo Persistence and Redis Emitter - With Clusters](#mongo-persistence-and-redis-emitter---with-clusters)
|
||||
- [Redis Persistence and Mongodb Emitter - With Clusters](#redis-persistence-and-mongodb-emitter---with-clusters)
|
||||
- [Benchmark: Mosca](#benchmark-mosca)
|
||||
- [Made with Aedes](#made-with-aedes)
|
||||
- [Collaborators](#collaborators)
|
||||
- [Contribution](#contribution)
|
||||
- [Support](#support)
|
||||
- [Backers](#backers)
|
||||
- [Sponsors](#sponsors)
|
||||
- [License](#license)
|
||||
|
||||
## Install
|
||||
|
||||
To install aedes, simply use npm:
|
||||
|
||||
```sh
|
||||
npm install aedes
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Check Docker docs [here](https://github.com/moscajs/aedes-cli#docker)
|
||||
|
||||
## API
|
||||
|
||||
- [Aedes object](./docs/Aedes.md)
|
||||
- [Client object](./docs/Client.md)
|
||||
|
||||
## Features
|
||||
|
||||
- Full compatible with [MQTT 3.1 and 3.1.1][ISO20922]
|
||||
- Standard TCP Support
|
||||
- SSL / TLS
|
||||
- WebSocket Support
|
||||
- Message Persistence
|
||||
- Automatic Reconnect
|
||||
- Offline Buffering
|
||||
- Backpress-support API
|
||||
- High Availability
|
||||
- Clusterable
|
||||
- Authentication and Authorization
|
||||
- `$SYS` support
|
||||
- Pluggable middlewares
|
||||
- [Dynamic Topics][dynamic_topics] Support
|
||||
- MQTT Bridge Support between aedes
|
||||
- [MQTT 5.0][mqttv5] _(not support yet)_
|
||||
- [Bridge Protocol][bridge_protocol] _(incoming connections only)_
|
||||
|
||||
## Examples
|
||||
|
||||
- [Examples](./docs/Examples.md)
|
||||
|
||||
## Clusters
|
||||
|
||||
Aedes needs on disk dbs like MongoDB and Redis in order to work with clusters. Based on our tests and users reports the best performances/stability are reached when using [aedes-persistence-mongodb] paired with [mqemitter-redis].
|
||||
|
||||
Other info:
|
||||
|
||||
- The repo [aedes-tests](https://github.com/moscajs/aedes-tests) is used to test aedes with clusters and different emitters/persistences. Check its source code to have a starting point on how to work with clusters
|
||||
|
||||
## Bridge connections
|
||||
|
||||
Normally, when publishing a message, the `retain` flag is consumed by Aedes and
|
||||
then set to `false`. This is done for two reasons:
|
||||
|
||||
- MQTT-3.3.1-9 states that it MUST set the RETAIN flag to 0 when a PUBLISH
|
||||
Packet is sent to a Client because it matches an established subscription
|
||||
regardless of how the flag was set in the message it received.
|
||||
- When operating as a cluster, only one Aedes node may store the packet
|
||||
|
||||
Brokers that support the [Bridge Protocol][bridge_protocol] can connect to
|
||||
Aedes. When connecting with this special protocol, subscriptions work as usual
|
||||
except that the `retain` flag in the packet is propagated as-is.
|
||||
|
||||
## Extensions
|
||||
|
||||
- [aedes-logging]: Logging module for Aedes, based on Pino
|
||||
- [aedes-stats]: Stats for Aedes
|
||||
- [aedes-cli]: Run Aedes MQTT Broker from the CLI
|
||||
- [aedes-protocol-decoder]: Protocol decoder for Aedes MQTT Broker
|
||||
- [aedes-server-factory]: Create a server instance such as TCP, HTTP, TLS...
|
||||
|
||||
## Middleware Plugins
|
||||
|
||||
### Persistence
|
||||
|
||||
- [aedes-persistence]: In-memory implementation of an Aedes persistence
|
||||
- [aedes-persistence-mongodb]: MongoDB persistence for Aedes
|
||||
- [aedes-persistence-redis]: Redis persistence for Aedes
|
||||
- [aedes-persistence-level]: LevelDB persistence for Aedes
|
||||
- [aedes-persistence-nedb]: NeDB persistence for Aedes
|
||||
|
||||
### MQEmitter
|
||||
|
||||
- [mqemitter]: An opinionated memory Message Queue with an emitter-style API
|
||||
- [mqemitter-redis]: Redis-powered mqemitter
|
||||
- [mqemitter-mongodb]: Mongodb based mqemitter
|
||||
- [mqemitter-child-process]: Share the same mqemitter between a hierarchy of
|
||||
child processes
|
||||
- [mqemitter-cs]: Expose a MQEmitter via a simple client/server protocol
|
||||
- [mqemitter-p2p]: A P2P implementation of MQEmitter, based on HyperEmitter and
|
||||
a Merkle DAG
|
||||
- [mqemitter-aerospike]: Aerospike mqemitter
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This library is born after a lot of discussion with all
|
||||
[Mosca](http://www.npmjs.com/mosca) users and how that was deployed in
|
||||
production. This addresses your concerns about performance and stability.
|
||||
|
||||
## Mosca vs Aedes
|
||||
|
||||
Example benchmark test with 1000 clients sending 5000 QoS 1 messsages. Used
|
||||
[mqtt-benchmark] with command:
|
||||
|
||||
```sh
|
||||
mqtt-benchmark --broker tcp://localhost:1883 --clients 1000 --qos 1 --count 5000
|
||||
```
|
||||
|
||||
CPU INFO:
|
||||
|
||||
```sh
|
||||
Architecture: x86_64
|
||||
CPU op-mode(s): 32-bit, 64-bit
|
||||
Byte Order: Little Endian
|
||||
CPU(s): 8
|
||||
On-line CPU(s) list: 0-7
|
||||
Thread(s) per core: 2
|
||||
Core(s) per socket: 4
|
||||
Socket(s): 1
|
||||
NUMA node(s): 1
|
||||
Vendor ID: GenuineIntel
|
||||
CPU family: 6
|
||||
Model: 94
|
||||
Model name: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
|
||||
Stepping: 3
|
||||
CPU MHz: 800.014
|
||||
CPU max MHz: 3500,0000
|
||||
CPU min MHz: 800,0000
|
||||
BogoMIPS: 5199.98
|
||||
Virtualization: VT-x
|
||||
L1d cache: 32K
|
||||
L1i cache: 32K
|
||||
L2 cache: 256K
|
||||
L3 cache: 6144K
|
||||
```
|
||||
|
||||
### Benchmark: Aedes
|
||||
|
||||
#### In memory - No clusters
|
||||
|
||||
```sh
|
||||
========= TOTAL (1000) =========
|
||||
Total Ratio: 1.000 (5000000/5000000)
|
||||
Total Runtime (sec): 178.495
|
||||
Average Runtime (sec): 177.845
|
||||
Msg time min (ms): 0.077
|
||||
Msg time max (ms): 199.805
|
||||
Msg time mean mean (ms): 35.403
|
||||
Msg time mean std (ms): 0.042
|
||||
Average Bandwidth (msg/sec): 28.115
|
||||
Total Bandwidth (msg/sec): 28114.678
|
||||
```
|
||||
|
||||
#### Redis Persistence and Redis Emitter - With Clusters
|
||||
|
||||
```sh
|
||||
========= TOTAL (1000) =========
|
||||
Total Ratio: 1.000 (5000000/5000000)
|
||||
Total Runtime (sec): 114.404
|
||||
Average Runtime (sec): 109.022
|
||||
Msg time min (ms): 0.065
|
||||
Msg time max (ms): 393.214
|
||||
Msg time mean mean (ms): 21.520
|
||||
Msg time mean std (ms): 0.595
|
||||
Average Bandwidth (msg/sec): 45.896
|
||||
Total Bandwidth (msg/sec): 45896.306
|
||||
```
|
||||
|
||||
#### Mongo Persistence and Redis Emitter - With Clusters
|
||||
|
||||
```sh
|
||||
========= TOTAL (1000) =========
|
||||
Total Ratio: 1.000 (5000000/5000000)
|
||||
Total Runtime (sec): 112.769
|
||||
Average Runtime (sec): 105.524
|
||||
Msg time min (ms): 0.062
|
||||
Msg time max (ms): 329.062
|
||||
Msg time mean mean (ms): 20.750
|
||||
Msg time mean std (ms): 0.878
|
||||
Average Bandwidth (msg/sec): 47.464
|
||||
Total Bandwidth (msg/sec): 47464.271
|
||||
```
|
||||
|
||||
### Redis Persistence and Mongodb Emitter - With Clusters
|
||||
|
||||
```sh
|
||||
========= TOTAL (1000) =========
|
||||
Total Ratio: 1.000 (5000000/5000000)
|
||||
Total Runtime (sec): 118.587
|
||||
Average Runtime (sec): 114.190
|
||||
Msg time min (ms): 0.080
|
||||
Msg time max (ms): 324.028
|
||||
Msg time mean mean (ms): 22.558
|
||||
Msg time mean std (ms): 0.730
|
||||
Average Bandwidth (msg/sec): 43.832
|
||||
Total Bandwidth (msg/sec): 43831.927
|
||||
```
|
||||
|
||||
### Benchmark: [Mosca](http://www.npmjs.com/mosca)
|
||||
|
||||
```sh
|
||||
========= TOTAL (1000) =========
|
||||
Total Ratio: 1.000 (5000000/5000000)
|
||||
Total Runtime (sec): 264.934
|
||||
Average Runtime (sec): 264.190
|
||||
Msg time min (ms): 0.070
|
||||
Msg time max (ms): 168.116
|
||||
Msg time mean mean (ms): 52.629
|
||||
Msg time mean std (ms): 0.074
|
||||
Average Bandwidth (msg/sec): 18.926
|
||||
Total Bandwidth (msg/sec): 18925.942
|
||||
```
|
||||
|
||||
## Made with Aedes
|
||||
|
||||
Here is a list of some interesting projects that are using Aedes as MQTT Broker. Submit a PR or an issue if you would like to add yours
|
||||
|
||||
- [node-red-contrib-aedes](https://github.com/martin-doyle/node-red-contrib-aedes): MQTT broker for Node-Red based on Aedes
|
||||
- [Mqtt2Mqtt](https://github.com/robertsLando/Mqtt2Mqtt): Mqtt Bridge between two brokers with UI
|
||||
- [Kuzzle](https://github.com/kuzzleio/kuzzle): High performance and full featured IoT backend using MQTT alongside WebSocket and Http protocols
|
||||
|
||||
## Collaborators
|
||||
|
||||
- [__Gavin D'mello__](https://github.com/GavinDmello)
|
||||
- [__Behrad Zari__](https://github.com/behrad)
|
||||
- [__Gnought__](https://github.com/gnought)
|
||||
- [__Daniel Lando__](https://github.com/robertsLando)
|
||||
- [__Getlarge__](https://github.com/getlarge)
|
||||
|
||||
## Contribution
|
||||
|
||||
[](https://github.com/moscajs/aedes/labels/help%20wanted)
|
||||
[](https://github.com/moscajs/aedes/graphs/contributors)
|
||||
|
||||
Want to contribute? Check our list of
|
||||
[features/bugs](https://github.com/moscajs/aedes/projects/1)
|
||||
|
||||
## Security notice
|
||||
|
||||
Messages sent to the broker are considered _valid_ once they pass the [`authorizePublish`](./docs/Aedes.md#handler-authorizepublish-client-packet-callback) callback.
|
||||
In other terms, if permissions for the given client are revoked after the call completes, the message is still considered valid.
|
||||
In case you are sending time-sensitive messages, make sure to use QoS 0 or connect with a clean session.
|
||||
|
||||
## Support
|
||||
|
||||
If there are bugs/leaks in production scenarios, we encourage people to send Pull Request and/or reach out maintainers for some paid support.
|
||||
|
||||
### Backers
|
||||
|
||||
Thank you to all our backers! :raised_hands:
|
||||
|
||||
[](https://opencollective.com/aedes#backers)
|
||||
|
||||
### Sponsors
|
||||
|
||||
Become a sponsor to get your logo on our README on Github
|
||||
|
||||
[](https://opencollective.com/aedes#sponsor)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under [MIT](./LICENSE).
|
||||
|
||||
[ISO20922]: https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html
|
||||
[mqttv5]: https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
|
||||
[bridge_protocol]: https://github.com/mqtt/mqtt.github.io/wiki/bridge_protocol
|
||||
[dynamic_topics]: https://github.com/mqtt/mqtt.github.io/wiki/are_topics_dynamic
|
||||
[mqtt-benchmark]: https://github.com/krylovsk/mqtt-benchmark
|
||||
|
||||
[aedes-logging]: https://www.npmjs.com/aedes-logging
|
||||
[aedes-stats]: https://www.npmjs.com/aedes-stats
|
||||
[aedes-cli]: https://www.npmjs.com/aedes-cli
|
||||
[aedes-protocol-decoder]: https://www.npmjs.com/aedes-protocol-decoder
|
||||
[aedes-server-factory]: https://www.npmjs.com/aedes-server-factory
|
||||
[aedes-persistence]: https://www.npmjs.com/aedes-persistence
|
||||
[aedes-persistence-mongodb]: https://www.npmjs.com/aedes-persistence-mongodb
|
||||
[aedes-persistence-redis]: https://www.npmjs.com/aedes-persistence-redis
|
||||
[aedes-persistence-level]: https://www.npmjs.com/aedes-persistence-level
|
||||
[aedes-persistence-nedb]: https://www.npmjs.com/aedes-persistence-nedb
|
||||
|
||||
[mqemitter]: https://www.npmjs.com/mqemitter
|
||||
[mqemitter-redis]: https://www.npmjs.com/mqemitter-redis
|
||||
[mqemitter-mongodb]: https://www.npmjs.com/mqemitter-mongodb
|
||||
[mqemitter-child-process]: https://www.npmjs.com/mqemitter-child-process
|
||||
[mqemitter-cs]: https://www.npmjs.com/mqemitter-cs
|
||||
[mqemitter-p2p]: https://www.npmjs.com/mqemitter-p2p
|
||||
[mqemitter-aerospike]: https://www.npmjs.com/mqemitter-aerospike
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report all vulnerabilities to [https://github.com/moscajs/aedes/security](https://github.com/moscajs/aedes/security).
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import Aedes, { AedesOptions } from './types/instance'
|
||||
|
||||
export declare function createBroker(options?: AedesOptions): Aedes
|
||||
|
||||
export * from './types/instance'
|
||||
export * from './types/packet'
|
||||
export * from './types/client'
|
||||
|
||||
export { default } from './types/instance'
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
'use strict'
|
||||
|
||||
const EventEmitter = require('events')
|
||||
const util = require('util')
|
||||
const parallel = require('fastparallel')
|
||||
const series = require('fastseries')
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
const reusify = require('reusify')
|
||||
const { pipeline } = require('stream')
|
||||
const Packet = require('aedes-packet')
|
||||
const memory = require('aedes-persistence')
|
||||
const mqemitter = require('mqemitter')
|
||||
const Client = require('./lib/client')
|
||||
const { $SYS_PREFIX, bulk } = require('./lib/utils')
|
||||
|
||||
module.exports = Aedes.createBroker = Aedes
|
||||
|
||||
const defaultOptions = {
|
||||
concurrency: 100,
|
||||
heartbeatInterval: 60000, // 1 minute
|
||||
connectTimeout: 30000, // 30 secs
|
||||
decodeProtocol: null,
|
||||
preConnect: defaultPreConnect,
|
||||
authenticate: defaultAuthenticate,
|
||||
authorizePublish: defaultAuthorizePublish,
|
||||
authorizeSubscribe: defaultAuthorizeSubscribe,
|
||||
authorizeForward: defaultAuthorizeForward,
|
||||
published: defaultPublished,
|
||||
trustProxy: false,
|
||||
trustedProxies: [],
|
||||
queueLimit: 42,
|
||||
maxClientsIdLength: 23,
|
||||
keepaliveLimit: 0
|
||||
}
|
||||
|
||||
function Aedes (opts) {
|
||||
const that = this
|
||||
|
||||
if (!(this instanceof Aedes)) {
|
||||
return new Aedes(opts)
|
||||
}
|
||||
|
||||
opts = Object.assign({}, defaultOptions, opts)
|
||||
|
||||
this.id = opts.id || uuidv4()
|
||||
// +1 when construct a new aedes-packet
|
||||
// internal track for last brokerCounter
|
||||
this.counter = 0
|
||||
this.queueLimit = opts.queueLimit
|
||||
this.connectTimeout = opts.connectTimeout
|
||||
this.keepaliveLimit = opts.keepaliveLimit
|
||||
this.maxClientsIdLength = opts.maxClientsIdLength
|
||||
this.mq = opts.mq || mqemitter({
|
||||
concurrency: opts.concurrency,
|
||||
matchEmptyLevels: true // [MQTT-4.7.1-3]
|
||||
})
|
||||
this.handle = function handle (conn, req) {
|
||||
conn.setMaxListeners(opts.concurrency * 2)
|
||||
// create a new Client instance for a new connection
|
||||
// return, just to please standard
|
||||
return new Client(that, conn, req)
|
||||
}
|
||||
this.persistence = opts.persistence || memory()
|
||||
this.persistence.broker = this
|
||||
this._parallel = parallel()
|
||||
this._series = series()
|
||||
this._enqueuers = reusify(DoEnqueues)
|
||||
|
||||
this.preConnect = opts.preConnect
|
||||
this.authenticate = opts.authenticate
|
||||
this.authorizePublish = opts.authorizePublish
|
||||
this.authorizeSubscribe = opts.authorizeSubscribe
|
||||
this.authorizeForward = opts.authorizeForward
|
||||
this.published = opts.published
|
||||
|
||||
this.decodeProtocol = opts.decodeProtocol
|
||||
this.trustProxy = opts.trustProxy
|
||||
this.trustedProxies = opts.trustedProxies
|
||||
|
||||
this.clients = {}
|
||||
this.brokers = {}
|
||||
|
||||
const heartbeatTopic = $SYS_PREFIX + that.id + '/heartbeat'
|
||||
const birthTopic = $SYS_PREFIX + that.id + '/birth'
|
||||
|
||||
this._heartbeatInterval = setInterval(heartbeat, opts.heartbeatInterval)
|
||||
|
||||
const bufId = Buffer.from(that.id, 'utf8')
|
||||
|
||||
// in a cluster env this is used to warn other broker instances
|
||||
// that this broker is alive
|
||||
that.publish({
|
||||
topic: birthTopic,
|
||||
payload: bufId
|
||||
}, noop)
|
||||
|
||||
function heartbeat () {
|
||||
that.publish({
|
||||
topic: heartbeatTopic,
|
||||
payload: bufId
|
||||
}, noop)
|
||||
}
|
||||
|
||||
function deleteOldBrokers (broker) {
|
||||
if (that.brokers[broker] + (3 * opts.heartbeatInterval) < Date.now()) {
|
||||
delete that.brokers[broker]
|
||||
}
|
||||
}
|
||||
|
||||
this._clearWillInterval = setInterval(function () {
|
||||
Object.keys(that.brokers).forEach(deleteOldBrokers)
|
||||
|
||||
pipeline(
|
||||
that.persistence.streamWill(that.brokers),
|
||||
bulk(receiveWills),
|
||||
function done (err) {
|
||||
if (err) {
|
||||
that.emit('error', err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}, opts.heartbeatInterval * 4)
|
||||
|
||||
function receiveWills (chunks, done) {
|
||||
that._parallel(that, checkAndPublish, chunks, done)
|
||||
}
|
||||
|
||||
function checkAndPublish (will, done) {
|
||||
const notPublish =
|
||||
that.brokers[will.brokerId] !== undefined && that.brokers[will.brokerId] + (3 * opts.heartbeatInterval) >= Date.now()
|
||||
|
||||
if (notPublish) return done()
|
||||
|
||||
// randomize this, so that multiple brokers
|
||||
// do not publish the same wills at the same time
|
||||
this.authorizePublish(that.clients[will.clientId] || null, will, function (err) {
|
||||
if (err) { return doneWill() }
|
||||
that.publish(will, doneWill)
|
||||
|
||||
function doneWill (err) {
|
||||
if (err) { return done(err) }
|
||||
that.persistence.delWill({
|
||||
id: will.clientId,
|
||||
brokerId: will.brokerId
|
||||
}, done)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.mq.on($SYS_PREFIX + '+/heartbeat', function storeBroker (packet, done) {
|
||||
that.brokers[packet.payload.toString()] = Date.now()
|
||||
done()
|
||||
})
|
||||
|
||||
this.mq.on($SYS_PREFIX + '+/birth', function brokerBorn (packet, done) {
|
||||
const brokerId = packet.payload.toString()
|
||||
|
||||
// reset duplicates counter
|
||||
if (brokerId !== that.id) {
|
||||
for (const clientId in that.clients) {
|
||||
delete that.clients[clientId].duplicates[brokerId]
|
||||
}
|
||||
}
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
this.mq.on($SYS_PREFIX + '+/new/clients', function closeSameClients (packet, done) {
|
||||
const serverId = packet.topic.split('/')[1]
|
||||
const clientId = packet.payload.toString()
|
||||
|
||||
if (that.clients[clientId] && serverId !== that.id) {
|
||||
if (that.clients[clientId].closed) {
|
||||
// remove the client from the list if it is already closed
|
||||
that.deleteClient(clientId)
|
||||
done()
|
||||
} else {
|
||||
that.clients[clientId].close(done)
|
||||
}
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
// metadata
|
||||
this.connectedClients = 0
|
||||
this.closed = false
|
||||
}
|
||||
|
||||
util.inherits(Aedes, EventEmitter)
|
||||
|
||||
function storeRetained (packet, done) {
|
||||
if (packet.retain) {
|
||||
this.broker.persistence.storeRetained(packet, done)
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
function emitPacket (packet, done) {
|
||||
if (this.client) packet.clientId = this.client.id
|
||||
this.broker.mq.emit(packet, done)
|
||||
}
|
||||
|
||||
function enqueueOffline (packet, done) {
|
||||
const enqueuer = this.broker._enqueuers.get()
|
||||
|
||||
enqueuer.complete = done
|
||||
enqueuer.packet = packet
|
||||
enqueuer.topic = packet.topic
|
||||
enqueuer.broker = this.broker
|
||||
this.broker.persistence.subscriptionsByTopic(
|
||||
packet.topic,
|
||||
enqueuer.done
|
||||
)
|
||||
}
|
||||
|
||||
function DoEnqueues () {
|
||||
this.next = null
|
||||
this.complete = null
|
||||
this.packet = null
|
||||
this.topic = null
|
||||
this.broker = null
|
||||
|
||||
const that = this
|
||||
|
||||
this.done = function doneEnqueue (err, subs) {
|
||||
const broker = that.broker
|
||||
|
||||
if (err) {
|
||||
// is this really recoverable?
|
||||
// let's just error the whole aedes
|
||||
// https://nodejs.org/api/events.html#events_error_events
|
||||
broker.emit('error', err)
|
||||
return
|
||||
}
|
||||
|
||||
if (that.topic.indexOf($SYS_PREFIX) === 0) {
|
||||
subs = subs.filter(removeSharp)
|
||||
}
|
||||
|
||||
const packet = that.packet
|
||||
const complete = that.complete
|
||||
|
||||
that.packet = null
|
||||
that.complete = null
|
||||
that.topic = null
|
||||
|
||||
broker.persistence.outgoingEnqueueCombi(subs, packet, complete)
|
||||
broker._enqueuers.release(that)
|
||||
}
|
||||
}
|
||||
|
||||
// + is 43
|
||||
// # is 35
|
||||
function removeSharp (sub) {
|
||||
const code = sub.topic.charCodeAt(0)
|
||||
return code !== 43 && code !== 35
|
||||
}
|
||||
|
||||
function callPublished (_, done) {
|
||||
this.broker.published(this.packet, this.client, done)
|
||||
this.broker.emit('publish', this.packet, this.client)
|
||||
}
|
||||
|
||||
const publishFuncsSimple = [
|
||||
storeRetained,
|
||||
emitPacket,
|
||||
callPublished
|
||||
]
|
||||
const publishFuncsQoS = [
|
||||
storeRetained,
|
||||
enqueueOffline,
|
||||
emitPacket,
|
||||
callPublished
|
||||
]
|
||||
Aedes.prototype.publish = function (packet, client, done) {
|
||||
if (typeof client === 'function') {
|
||||
done = client
|
||||
client = null
|
||||
}
|
||||
const p = new Packet(packet, this)
|
||||
const publishFuncs = p.qos > 0 ? publishFuncsQoS : publishFuncsSimple
|
||||
|
||||
this._series(new PublishState(this, client, packet), publishFuncs, p, done)
|
||||
}
|
||||
|
||||
Aedes.prototype.subscribe = function (topic, func, done) {
|
||||
this.mq.on(topic, func, done)
|
||||
}
|
||||
|
||||
Aedes.prototype.unsubscribe = function (topic, func, done) {
|
||||
this.mq.removeListener(topic, func, done)
|
||||
}
|
||||
|
||||
Aedes.prototype.registerClient = function (client) {
|
||||
const that = this
|
||||
if (this.clients[client.id]) {
|
||||
// [MQTT-3.1.4-2]
|
||||
this.clients[client.id].close(function closeClient () {
|
||||
that._finishRegisterClient(client)
|
||||
})
|
||||
} else {
|
||||
this._finishRegisterClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
Aedes.prototype._finishRegisterClient = function (client) {
|
||||
this.connectedClients++
|
||||
this.clients[client.id] = client
|
||||
this.emit('client', client)
|
||||
this.publish({
|
||||
topic: $SYS_PREFIX + this.id + '/new/clients',
|
||||
payload: Buffer.from(client.id, 'utf8')
|
||||
}, noop)
|
||||
}
|
||||
|
||||
Aedes.prototype.unregisterClient = function (client) {
|
||||
this.deleteClient(client.id)
|
||||
this.emit('clientDisconnect', client)
|
||||
this.publish({
|
||||
topic: $SYS_PREFIX + this.id + '/disconnect/clients',
|
||||
payload: Buffer.from(client.id, 'utf8')
|
||||
}, noop)
|
||||
}
|
||||
|
||||
Aedes.prototype.deleteClient = function (clientId) {
|
||||
this.connectedClients--
|
||||
delete this.clients[clientId]
|
||||
}
|
||||
|
||||
function closeClient (client, cb) {
|
||||
this.clients[client].close(cb)
|
||||
}
|
||||
|
||||
Aedes.prototype.close = function (cb = noop) {
|
||||
const that = this
|
||||
if (this.closed) {
|
||||
return cb()
|
||||
}
|
||||
this.closed = true
|
||||
clearInterval(this._heartbeatInterval)
|
||||
clearInterval(this._clearWillInterval)
|
||||
this._parallel(this, closeClient, Object.keys(this.clients), doneClose)
|
||||
function doneClose () {
|
||||
that.emit('closed')
|
||||
that.mq.close(cb)
|
||||
}
|
||||
}
|
||||
|
||||
Aedes.prototype.version = require('./package.json').version
|
||||
|
||||
function defaultPreConnect (client, packet, callback) {
|
||||
callback(null, true)
|
||||
}
|
||||
|
||||
function defaultAuthenticate (client, username, password, callback) {
|
||||
callback(null, true)
|
||||
}
|
||||
|
||||
function defaultAuthorizePublish (client, packet, callback) {
|
||||
if (packet.topic.startsWith($SYS_PREFIX)) {
|
||||
return callback(new Error($SYS_PREFIX + ' topic is reserved'))
|
||||
}
|
||||
callback(null)
|
||||
}
|
||||
|
||||
function defaultAuthorizeSubscribe (client, sub, callback) {
|
||||
callback(null, sub)
|
||||
}
|
||||
|
||||
function defaultAuthorizeForward (client, packet) {
|
||||
return packet
|
||||
}
|
||||
|
||||
function defaultPublished (packet, client, callback) {
|
||||
callback(null)
|
||||
}
|
||||
|
||||
function PublishState (broker, client, packet) {
|
||||
this.broker = broker
|
||||
this.client = client
|
||||
this.packet = packet
|
||||
}
|
||||
|
||||
function noop () {}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
const mqtt = require('mqtt')
|
||||
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 })
|
||||
const interval = 5000
|
||||
|
||||
let sent = 0
|
||||
|
||||
function count () {
|
||||
console.log('sent/s', sent / interval * 1000)
|
||||
sent = 0
|
||||
}
|
||||
|
||||
setInterval(count, interval)
|
||||
|
||||
function immediatePublish () {
|
||||
setImmediate(publish)
|
||||
}
|
||||
|
||||
function publish () {
|
||||
sent++
|
||||
client.publish('test', 'payload', immediatePublish)
|
||||
}
|
||||
|
||||
client.on('connect', publish)
|
||||
|
||||
client.on('offline', function () {
|
||||
console.log('offline')
|
||||
})
|
||||
|
||||
client.on('error', function () {
|
||||
console.log('reconnect!')
|
||||
client.stream.end()
|
||||
})
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
const mqtt = require('mqtt')
|
||||
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 })
|
||||
const interval = 5000
|
||||
|
||||
let sent = 0
|
||||
|
||||
function count () {
|
||||
console.log('sent/s', sent / interval * 1000)
|
||||
sent = 0
|
||||
}
|
||||
|
||||
setInterval(count, interval)
|
||||
|
||||
function publish () {
|
||||
sent++
|
||||
client.publish('test', 'payload', { qos: 1 }, publish)
|
||||
}
|
||||
|
||||
client.setMaxListeners(100)
|
||||
|
||||
client.on('connect', function () {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
publish()
|
||||
}
|
||||
})
|
||||
|
||||
client.on('offline', function () {
|
||||
console.log('offline')
|
||||
})
|
||||
|
||||
client.on('error', function () {
|
||||
console.log('reconnect!')
|
||||
client.stream.end()
|
||||
})
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
const mqtt = require('mqtt')
|
||||
const convertHrtime = require('convert-hrtime')
|
||||
const mode = require('compute-mode')
|
||||
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 })
|
||||
const interval = 5000
|
||||
|
||||
let sent = 0
|
||||
const latencies = []
|
||||
|
||||
function count () {
|
||||
console.log('sent/s', sent / interval * 1000)
|
||||
sent = 0
|
||||
}
|
||||
|
||||
setInterval(count, interval)
|
||||
|
||||
function publish () {
|
||||
sent++
|
||||
client.publish('test', JSON.stringify(process.hrtime()), { qos: 1 })
|
||||
}
|
||||
|
||||
function subscribe () {
|
||||
client.subscribe('test', { qos: 1 }, publish)
|
||||
}
|
||||
|
||||
client.on('connect', subscribe)
|
||||
client.on('message', publish)
|
||||
client.on('message', function (topic, payload) {
|
||||
const sentAt = JSON.parse(payload)
|
||||
const diff = process.hrtime(sentAt)
|
||||
latencies.push(convertHrtime(diff).ms)
|
||||
})
|
||||
|
||||
client.on('offline', function () {
|
||||
console.log('offline')
|
||||
})
|
||||
|
||||
client.on('error', function () {
|
||||
console.log('reconnect!')
|
||||
client.stream.end()
|
||||
})
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
const total = latencies.reduce(function (acc, num) {
|
||||
return acc + num
|
||||
})
|
||||
console.log('total', total)
|
||||
console.log('average', total / latencies.length)
|
||||
console.log('mode', mode(latencies))
|
||||
process.exit(0)
|
||||
})
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
'use strict'
|
||||
|
||||
// To be used with cpuprofilify http://npm.im/cpuprofilify
|
||||
|
||||
const aedes = require('../')()
|
||||
const server = require('net').createServer(aedes.handle)
|
||||
const port = 1883
|
||||
|
||||
server.listen(port, function () {
|
||||
console.error('server listening on port', port, 'pid', process.pid)
|
||||
})
|
||||
|
||||
aedes.on('clientError', function (client, err) {
|
||||
console.error('client error', client.id, err.message)
|
||||
})
|
||||
|
||||
// Cleanly shut down process on SIGTERM to ensure that perf-<pid>.map gets flushed
|
||||
process.on('SIGINT', onSIGINT)
|
||||
|
||||
function onSIGINT () {
|
||||
// IMPORTANT to log on stderr, to not clutter stdout which is purely for data, i.e. dtrace stacks
|
||||
console.error('Caught SIGTERM, shutting down.')
|
||||
server.close()
|
||||
process.exit(0)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
const mqtt = require('mqtt')
|
||||
|
||||
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 })
|
||||
const interval = 5000
|
||||
|
||||
let counter = 0
|
||||
|
||||
function count () {
|
||||
console.log('received/s', counter / interval * 1000)
|
||||
counter = 0
|
||||
}
|
||||
|
||||
setInterval(count, interval)
|
||||
|
||||
client.on('connect', function () {
|
||||
count()
|
||||
this.subscribe('test')
|
||||
this.on('message', function () {
|
||||
counter++
|
||||
})
|
||||
})
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
const mqtt = require('mqtt')
|
||||
|
||||
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 })
|
||||
const interval = 5000
|
||||
|
||||
let counter = 0
|
||||
|
||||
function count () {
|
||||
console.log('received/s', counter / interval * 1000)
|
||||
counter = 0
|
||||
}
|
||||
|
||||
setInterval(count, interval)
|
||||
|
||||
client.on('connect', function () {
|
||||
this.subscribe('test', { qos: 1 })
|
||||
})
|
||||
|
||||
client.handleMessage = function (packet, done) {
|
||||
counter++
|
||||
done()
|
||||
}
|
||||
|
||||
client.on('offline', function () {
|
||||
console.log('offline')
|
||||
})
|
||||
|
||||
client.on('error', function () {
|
||||
console.log('reconnect!')
|
||||
client.stream.end()
|
||||
})
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
<!-- markdownlint-disable MD013 MD024 -->
|
||||
# Aedes
|
||||
|
||||
- [Aedes](#aedes)
|
||||
- [new Aedes([options]) / new Aedes.Server([options])](#new-aedesoptions--new-aedesserveroptions)
|
||||
- [aedes.id](#aedesid)
|
||||
- [aedes.connectedClients](#aedesconnectedclients)
|
||||
- [aedes.closed](#aedesclosed)
|
||||
- [Event: client](#event-client)
|
||||
- [Event: clientReady](#event-clientready)
|
||||
- [Event: clientDisconnect](#event-clientdisconnect)
|
||||
- [Event: clientError](#event-clienterror)
|
||||
- [Event: connectionError](#event-connectionerror)
|
||||
- [Event: keepaliveTimeout](#event-keepalivetimeout)
|
||||
- [Event: publish](#event-publish)
|
||||
- [Event: ack](#event-ack)
|
||||
- [Event: ping](#event-ping)
|
||||
- [Event: subscribe](#event-subscribe)
|
||||
- [Event: unsubscribe](#event-unsubscribe)
|
||||
- [Event: connackSent](#event-connacksent)
|
||||
- [Event: closed](#event-closed)
|
||||
- [aedes.handle (stream)](#aedeshandle-stream)
|
||||
- [aedes.subscribe (topic, deliverfunc, callback)](#aedessubscribe-topic-deliverfunc-callback)
|
||||
- [aedes.unsubscribe (topic, deliverfunc, callback)](#aedesunsubscribe-topic-deliverfunc-callback)
|
||||
- [aedes.publish (packet, callback)](#aedespublish-packet-callback)
|
||||
- [aedes.close ([callback])](#aedesclose-callback)
|
||||
- [Handler: preConnect (client, packet, callback)](#handler-preconnect-client-packet-callback)
|
||||
- [Handler: authenticate (client, username, password, callback)](#handler-authenticate-client-username-password-callback)
|
||||
- [Handler: authorizePublish (client, packet, callback)](#handler-authorizepublish-client-packet-callback)
|
||||
- [Handler: authorizeSubscribe (client, subscription, callback)](#handler-authorizesubscribe-client-subscription-callback)
|
||||
- [Handler: authorizeForward (client, packet)](#handler-authorizeforward-client-packet)
|
||||
- [Handler: published (packet, client, callback)](#handler-published-packet-client-callback)
|
||||
|
||||
## new Aedes([options]) / new Aedes.Server([options])
|
||||
|
||||
- options `<object>`
|
||||
- `mq` [`<MQEmitter>`](../README.md#mqemitter) middleware used to deliver messages to subscribed clients. In a cluster environment it is used also to share messages between brokers instances. __Default__: `mqemitter`
|
||||
- `concurrency` `<number>` maximum number of concurrent messages delivered by `mq`. __Default__: `100`
|
||||
- `persistence` [`<Persistence>`](../README.md#persistence) middleware that stores _QoS > 0, retained, will_ packets and _subscriptions_. __Default__: `aedes-persistence` (_in memory_)
|
||||
- `queueLimit` `<number>` maximum number of queued messages before client session is established. If number of queued items exceeds, `connectionError` throws an error `Client queue limit reached`. __Default__: `42`
|
||||
- `maxClientsIdLength` option to override MQTT 3.1.0 clients Id length limit. __Default__: `23`
|
||||
- `heartbeatInterval` `<number>` an interval in millisconds at which server beats its health signal in `$SYS/<aedes.id>/heartbeat` topic. __Default__: `60000`
|
||||
- `id` `<string>` aedes broker unique identifier. __Default__: `uuidv4()`
|
||||
- `connectTimeout` `<number>` maximum waiting time in milliseconds waiting for a [`CONNECT`][CONNECT] packet. __Default__: `30000`
|
||||
- `keepaliveLimit` `<number>` maximum client keep alive time allowed, 0 means no limit. __Default__: `0`
|
||||
- Returns `<Aedes>`
|
||||
|
||||
Create a new Aedes server.
|
||||
|
||||
Aedes is the class and function exposed by this module. It can be created by `Aedes()` or using `new Aedes()`. An variant `aedes.Server` is for TypeScript or ES modules.
|
||||
|
||||
## aedes.id
|
||||
|
||||
- `<string>` __Default__: `uuidv4()`
|
||||
|
||||
Server unique identifier.
|
||||
|
||||
## aedes.connectedClients
|
||||
|
||||
- `<number>` __Default__: 0
|
||||
|
||||
Number of connected clients in server.
|
||||
|
||||
## aedes.closed
|
||||
|
||||
- `<boolean>` __Default__: false
|
||||
|
||||
a read-only flag indicates if server is closed or not.
|
||||
|
||||
## Event: client
|
||||
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted when the `client` registers itself to server. The `client` is not ready yet. Its [`connecting`](./Client.md##clientconnecting) state equals to `true`.
|
||||
|
||||
Server publishes a SYS topic `$SYS/<aedes.id>/new/clients` to inform it registers the client into its registration pool. `client.id` is the payload.
|
||||
|
||||
## Event: clientReady
|
||||
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted when the `client` has received all its offline messages and be initialized. The `client` [`connected`](./Client.md##clientconnected) state equals to `true` and is ready for processing incoming messages.
|
||||
|
||||
## Event: clientDisconnect
|
||||
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted when a client disconnects.
|
||||
|
||||
Server publishes a SYS topic `$SYS/<aedes.id>/disconnect/clients` to inform it deregisters the client. `client.id` is the payload.
|
||||
|
||||
## Event: clientError
|
||||
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
- `error` `<Error>`
|
||||
|
||||
Emitted when an error occurs.
|
||||
|
||||
## Event: connectionError
|
||||
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
- `error` `<Error>`
|
||||
|
||||
Emitted when an error occurs. Unlike `clientError` it raises only when `client` is uninitialized.
|
||||
|
||||
## Event: keepaliveTimeout
|
||||
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted when timeout happes in the `client` keepalive.
|
||||
|
||||
## Event: publish
|
||||
|
||||
- `packet` `<aedes-packet>` & [`PUBLISH`][PUBLISH]
|
||||
- `client` [`<Client>`](./Client.md) | `null`
|
||||
|
||||
Emitted when servers delivers the `packet` to subscribed `client`. If there are no clients subscribed to the `packet` topic, server still publish the `packet` and emit the event. `client` is `null` when `packet` is an internal message like aedes heartbeat message and LWT.
|
||||
|
||||
> _Note! `packet` belongs `aedes-packet` type. Some properties belongs to aedes internal, any changes on them will break aedes internal flow._
|
||||
|
||||
## Event: ack
|
||||
|
||||
- `packet` `<object>` [`PUBLISH`][PUBLISH] for QoS 1, [`PUBREL`][PUBREL] for QoS 2
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted an QoS 1 or 2 acknowledgement when the `packet` successfully delivered to the `client`.
|
||||
|
||||
## Event: ping
|
||||
|
||||
- `packet` `<object>` [`PINGREQ`][PINGREQ]
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted when `client` sends a `PINGREQ`.
|
||||
|
||||
## Event: subscribe
|
||||
|
||||
- `subscriptions` `<object>`
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted when `client` successfully subscribe the `subscriptions` in server.
|
||||
|
||||
`subscriptions` is an array of `{ topic: topic, qos: qos }`. The array excludes duplicated topics and includes negated subscriptions where `qos` equals to `128`. See more on [authorizeSubscribe](#handler-authorizesubscribe-client-subscription-callback)
|
||||
|
||||
Server publishes a SYS topic `$SYS/<aedes.id>/new/subscribers` to inform a client successfully subscribed to one or more topics. The payload is a JSON that has `clientId` and `subs` props, `subs` equals to `subscriptions` array.
|
||||
|
||||
## Event: unsubscribe
|
||||
|
||||
- `unsubscriptions` `Array<string>`
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted when `client` successfully unsubscribe the `subscriptions` in server.
|
||||
|
||||
`unsubscriptions` are an array of unsubscribed topics.
|
||||
|
||||
Server publishes a SYS topic `$SYS/<aedes.id>/new/unsubscribers` to inform a client successfully unsubscribed to one or more topics. The payload is a JSON that has `clientId` and `subs` props, `subs` equals to `unsubscriptions` array.
|
||||
|
||||
## Event: connackSent
|
||||
|
||||
- `packet` `<object>` [`CONNACK`][CONNACK]
|
||||
- `client` [`<Client>`](./Client.md)
|
||||
|
||||
Emitted when server sends an acknowledge to `client`. Please refer to the MQTT specification for the explanation of returnCode object property in `CONNACK`.
|
||||
|
||||
## Event: closed
|
||||
|
||||
Emitted when server is closed.
|
||||
|
||||
## aedes.handle (stream)
|
||||
|
||||
- stream: `<net.Socket>` | `<stream.Duplex>`
|
||||
- Returns: [`<Client>`](./Client.md)
|
||||
|
||||
A connection listener that pipe stream to aedes.
|
||||
|
||||
```js
|
||||
const aedes = require('./aedes')()
|
||||
const server = require('net').createServer(aedes.handle)
|
||||
```
|
||||
|
||||
## aedes.subscribe (topic, deliverfunc, callback)
|
||||
|
||||
- topic: `<string>`
|
||||
- deliverfunc: `<Function>` `(packet, cb) => void`
|
||||
- packet: `<aedes-packet>` & [`PUBLISH`][PUBLISH]
|
||||
- cb: `<Function>`
|
||||
- callback: `<Function>`
|
||||
|
||||
Directly subscribe a `topic` in server side. Bypass [`authorizeSubscribe`](#handler-authorizesubscribe-client-subscription-callback)
|
||||
|
||||
The `topic` and `deliverfunc` is a compound key to differentiate the uniqueness of its subscription pool. `topic` could be the one that is existed, in this case `deliverfunc` will be invoked as well as [`SUBSCRIBE`][SUBSCRIBE] does.
|
||||
|
||||
`deliverfunc` supports backpressue.
|
||||
|
||||
In aedes internal, `deliverfunc` is a function that delivers messages to subscribed clients.
|
||||
|
||||
> _Note! `packet` belongs `aedes-packet` type. Some properties belongs to aedes internal, any changes on them will break aedes internal flow._
|
||||
|
||||
In general you would find most properities in `packet` is same as what the incoming [`PUBLISH`][PUBLISH] is. For sure `cmd` property in `packet` structure in `deliverfunc` must be `publish`.
|
||||
|
||||
> _Note! it requires `deliverfunc` to call `cb` before the function returns, otherwise some subscribed clients with same `topic` will not receive messages._
|
||||
|
||||
`callback` is invoked when server successfully registers the subscription.
|
||||
|
||||
## aedes.unsubscribe (topic, deliverfunc, callback)
|
||||
|
||||
Reverse of [aedes.subscribe](#aedessubscribe-topic-deliverfunc-callback).
|
||||
|
||||
> _Note! the `deliverfunc` should be same as when `aedes.subscribe` does, otherwise the unsubscription will fail._
|
||||
|
||||
## aedes.publish (packet, callback)
|
||||
|
||||
- `packet` `<object>` [`PUBLISH`][PUBLISH]
|
||||
- `callback` `<Function>` `(error) => void`
|
||||
- error `<Error>` | `null`
|
||||
|
||||
Directly deliver `packet` on behalf of server to subscribed clients. Bypass [`authorizePublish`](#handler-authorizepublish-client-packet-callback).
|
||||
|
||||
`callback` will be invoked with `error` arugments after finish.
|
||||
|
||||
## aedes.close ([callback])
|
||||
|
||||
- callback: `<Function>`
|
||||
|
||||
Close aedes server and disconnects all clients.
|
||||
|
||||
`callback` will be invoked when server is closed.
|
||||
|
||||
## Handler: preConnect (client, packet, callback)
|
||||
|
||||
- client: [`<Client>`](./Client.md)
|
||||
- packet: `<object>` [`CONNECT`][CONNECT]
|
||||
- callback: `<Function>` `(error, successful) => void`
|
||||
- error `<Error>` | `null`
|
||||
- successful `<boolean>`
|
||||
|
||||
Invoked when server receives a valid [`CONNECT`][CONNECT] packet. The packet can be modified.
|
||||
|
||||
`client` object is in default state. If invoked `callback` with no errors and `successful` be `true`, server will continue to establish a session.
|
||||
|
||||
Any `error` will be raised in `connectionError` event.
|
||||
|
||||
Some Use Cases:
|
||||
|
||||
1. Rate Limit / Throttle by `client.conn.remoteAddress`
|
||||
2. Check `aedes.connectedClient` to limit maximum connections
|
||||
3. IP blacklisting
|
||||
|
||||
```js
|
||||
aedes.preConnect = function(client, packet, callback) {
|
||||
callback(null, client.conn.remoteAddress === '::1') {
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
aedes.preConnect = function(client, packet, callback) {
|
||||
callback(new Error('connection error'), client.conn.remoteAddress !== '::1') {
|
||||
}
|
||||
```
|
||||
|
||||
## Handler: authenticate (client, username, password, callback)
|
||||
|
||||
- client: [`<Client>`](./Client.md)
|
||||
- username: `<string>`
|
||||
- password: `<Buffer>`
|
||||
- callback: `<Function>` `(error, successful) => void`
|
||||
- error `<Error>` | `null`
|
||||
- successful `<boolean>`
|
||||
|
||||
Invoked after `preConnect`.
|
||||
|
||||
Server parses the [`CONNECT`][CONNECT] packet, initializes `client` object which set `client.id` to match the one in [`CONNECT`][CONNECT] packet and extract `username` and `password` as parameters for user-defined authentication flow.
|
||||
|
||||
If invoked `callback` with no errors and `successful` be `true`, server authenticates `client` and continues to setup the client session.
|
||||
|
||||
If authenticated, server acknowledges a [`CONNACK`][CONNACK] with `returnCode=0`, otherwise `returnCode=5`. Users could define the value between `2` and `5` by defining a `returnCode` property in `error` object.
|
||||
|
||||
```js
|
||||
aedes.authenticate = function (client, username, password, callback) {
|
||||
callback(null, username === 'matteo')
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
aedes.authenticate = function (client, username, password, callback) {
|
||||
var error = new Error('Auth error')
|
||||
error.returnCode = 4
|
||||
callback(error, null)
|
||||
}
|
||||
```
|
||||
|
||||
Please refer to [Connect Return Code](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_3.1_-) to see their meanings.
|
||||
|
||||
## Handler: authorizePublish (client, packet, callback)
|
||||
|
||||
- client: [`<Client>`](./Client.md) | `null`
|
||||
- packet: `<object>` [`PUBLISH`][PUBLISH]
|
||||
- callback: `<Function>` `(error) => void`
|
||||
- error `<Error>` | `null`
|
||||
|
||||
Invoked when
|
||||
|
||||
1. publish LWT to all online clients
|
||||
2. incoming client publish
|
||||
|
||||
`client` is `null` when aedes publishes obsolete LWT without connected clients
|
||||
|
||||
If invoked `callback` with no errors, server authorizes the packet otherwise emits `clientError` with `error`. If an `error` occurs the client connection will be closed, but no error is returned to the client (MQTT-3.3.5-2)
|
||||
|
||||
```js
|
||||
aedes.authorizePublish = function (client, packet, callback) {
|
||||
if (packet.topic === 'aaaa') {
|
||||
return callback(new Error('wrong topic'))
|
||||
}
|
||||
if (packet.topic === 'bbb') {
|
||||
packet.payload = Buffer.from('overwrite packet payload')
|
||||
}
|
||||
callback(null)
|
||||
}
|
||||
```
|
||||
|
||||
By default `authorizePublish` throws error in case a client publish to topics with `$SYS/` prefix to prevent possible DoS (see [#597](https://github.com/moscajs/aedes/issues/597)). If you write your own implementation of `authorizePublish` we suggest you to add a check for this. Default implementation:
|
||||
|
||||
```js
|
||||
function defaultAuthorizePublish (client, packet, callback) {
|
||||
if (packet.topic.startsWith($SYS_PREFIX)) {
|
||||
return callback(new Error($SYS_PREFIX + ' topic is reserved'))
|
||||
}
|
||||
callback(null)
|
||||
}
|
||||
```
|
||||
|
||||
## Handler: authorizeSubscribe (client, subscription, callback)
|
||||
|
||||
- client: [`<Client>`](./Client.md)
|
||||
- subscription: `<object>`
|
||||
- callback: `<Function>` `(error) => void`
|
||||
- error `<Error>` | `null`
|
||||
- subscription: `<object>` | `null`
|
||||
|
||||
Invoked when
|
||||
|
||||
1. restore subscriptions in non-clean session.
|
||||
2. incoming client [`SUBSCRIBE`][SUBSCRIBE]
|
||||
|
||||
`subscription` is a dictionary object like `{ topic: hello, qos: 0 }`.
|
||||
|
||||
If invoked `callback` with no errors, server authorizes the packet otherwise emits `clientError` with `error`.
|
||||
|
||||
In general user should not touch the `subscription` and pass to callback, but server gives an option to change the subscription on-the-fly.
|
||||
|
||||
```js
|
||||
aedes.authorizeSubscribe = function (client, sub, callback) {
|
||||
if (sub.topic === 'aaaa') {
|
||||
return callback(new Error('wrong topic'))
|
||||
}
|
||||
if (sub.topic === 'bbb') {
|
||||
// overwrites subscription
|
||||
sub.topic = 'foo'
|
||||
sub.qos = 1
|
||||
}
|
||||
callback(null, sub)
|
||||
}
|
||||
```
|
||||
|
||||
To negate a subscription, set the subscription to `null`. Aedes ignores the negated subscription and the `qos` in `SubAck` is set to `128` based on [MQTT 3.11 spec](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html#_Toc385349323):
|
||||
|
||||
```js
|
||||
aedes.authorizeSubscribe = function (client, sub, callback) {
|
||||
// prohibited to subscribe 'aaaa' and suppress error
|
||||
callback(null, sub.topic === 'aaaa' ? null : sub)
|
||||
}
|
||||
```
|
||||
|
||||
## Handler: authorizeForward (client, packet)
|
||||
|
||||
- client: [`<Client>`](./Client.md)
|
||||
- packet: `<aedes-packet>` & [`PUBLISH`][PUBLISH]
|
||||
- Returns: `<aedes-packet>` | `null`
|
||||
|
||||
Invoked when
|
||||
|
||||
1. aedes sends retained messages when client reconnects
|
||||
2. aedes pre-delivers subscribed message to clients
|
||||
|
||||
Return `null` will not forward `packet` to clients.
|
||||
|
||||
In general user should not touch the `packet` and return it what it is, but server gives an option to change the `packet` on-the-fly and forward it to clients.
|
||||
|
||||
> _Note! `packet` belongs `aedes-packet` type. Some properties belongs to aedes internal, any changes on them will break aedes internal flow._
|
||||
|
||||
```js
|
||||
aedes.authorizeForward = function (client, packet) {
|
||||
if (packet.topic === 'aaaa' && client.id === "I should not see this") {
|
||||
return
|
||||
}
|
||||
if (packet.topic === 'bbb') {
|
||||
packet.payload = new Buffer('overwrite packet payload')
|
||||
}
|
||||
return packet
|
||||
}
|
||||
```
|
||||
|
||||
## Handler: published (packet, client, callback)
|
||||
|
||||
- packet: `<aedes-packet>` & [`PUBLISH`][PUBLISH]
|
||||
- client: [`<Client>`](./Client.md)
|
||||
- callback: `<Function>`
|
||||
|
||||
same as [`Event: publish`](#event-publish), but provides a backpressure functionality. TLDR; If you are doing operations on packets that MUST require finishing operations on a packet before handling the next one use this otherwise, expecially for long running operations, you should use [`Event: publish`](#event-publish) instead.
|
||||
|
||||
[CONNECT]: https://github.com/mqttjs/mqtt-packet#connect
|
||||
[CONNACK]: https://github.com/mqttjs/mqtt-packet#connack
|
||||
[SUBSCRIBE]: https://github.com/mqttjs/mqtt-packet#subscribe
|
||||
[PINGREQ]: https://github.com/mqttjs/mqtt-packet#pingreq
|
||||
[PUBLISH]: https://github.com/mqttjs/mqtt-packet#publish
|
||||
[PUBREL]: https://github.com/mqttjs/mqtt-packet#pubrel
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
<!-- markdownlint-disable MD013 MD024 -->
|
||||
# Client
|
||||
|
||||
- [Client](#client)
|
||||
- [new Client(aedes, stream, request)](#new-clientaedes-stream-request)
|
||||
- [client.conn](#clientconn)
|
||||
- [client.req](#clientreq)
|
||||
- [client.connecting](#clientconnecting)
|
||||
- [client.connected](#clientconnected)
|
||||
- [client.closed](#clientclosed)
|
||||
- [client.id](#clientid)
|
||||
- [client.clean](#clientclean)
|
||||
- [client.version](#clientversion)
|
||||
- [Event: connected](#event-connected)
|
||||
- [Event: error](#event-error)
|
||||
- [client.publish (packet, [callback])](#clientpublish-packet-callback)
|
||||
- [client.subscribe (subscriptions, [callback])](#clientsubscribe-subscriptions-callback)
|
||||
- [client.unsubscribe (unsubscriptions, [callback])](#clientunsubscribe-unsubscriptions-callback)
|
||||
- [client.close ([callback])](#clientclose-callback)
|
||||
- [client.emptyOutgoingQueue ([callback])](#clientemptyoutgoingqueue-callback)
|
||||
|
||||
## new Client(aedes, stream, request)
|
||||
|
||||
- aedes [`<Aedes>`](./Aedes.md)
|
||||
- stream: `<net.Socket>` | `<stream.Duplex>`
|
||||
- request: `<http.IncomingMessage>`
|
||||
- Returns: `<Client>`
|
||||
|
||||
## client.conn
|
||||
|
||||
- `<net.Socket>` | `<stream.Duplex>`
|
||||
|
||||
Client connection stream object.
|
||||
|
||||
In the case of `net.createServer`, `conn` passed to the `connectionlistener` function by node's [net.createServer](https://nodejs.org/api/net.html#net_net_createserver_options_connectionlistener) API.
|
||||
|
||||
In the case of [`websocket-stream`][websocket-stream], it's the `stream` argument passed to the websocket `handle` function in [`websocket-stream #on-the-server`][websocket-stream-doc-on-the-server]].
|
||||
|
||||
## client.req
|
||||
|
||||
- `<http.IncomingMessage>`
|
||||
|
||||
only for [`websocket-stream`][websocket-stream]. It is a HTTP Websocket upgrade request object passed to websocket `handle` function in [`websocket-stream #on-the-server`][websocket-stream-doc-on-the-server]. It gives an option for accessing headers or cookies.
|
||||
|
||||
## client.connecting
|
||||
|
||||
- `<boolean>` __Default__: `false`
|
||||
|
||||
a read-only flag, it is true when Client is in CONNECT phase. Aedes emits `connackSent` event will not reset `connecting` to `false` until it received all its offline messagess to the Client.
|
||||
|
||||
## client.connected
|
||||
|
||||
- `<boolean>` __Default__: `false`
|
||||
|
||||
a read-only flag, it is `true` when `connected` event is emitted, and `false` when client is closed.
|
||||
|
||||
## client.closed
|
||||
|
||||
- `<boolean>` __Default__: `false`
|
||||
|
||||
a read-only flag indicates if client is closed or not.
|
||||
|
||||
## client.id
|
||||
|
||||
- `<string>` __Default__: `aedes_${hyperid()}`
|
||||
|
||||
Client unique identifier, specified by CONNECT packet.
|
||||
|
||||
It is available only after `CONNACK (rc=0)`, otherwise it is `null` in cases:
|
||||
|
||||
- in [`aedes.preConnect`](./Aedes.md#handler-preconnect-client-callback) stage
|
||||
- after `CONNACK (rc!=0)` response
|
||||
- `connectionError` raised by aedes
|
||||
|
||||
## client.clean
|
||||
|
||||
- `<boolean>` __Default__: `true`
|
||||
|
||||
Client clean flag, set by clean flag in `CONNECT` packet.
|
||||
|
||||
## client.version
|
||||
|
||||
- `<number>` __Default__: `null`
|
||||
|
||||
Client version, set by protocol version in `CONNECT` packet when `CONNACK (rc=0)` returns.
|
||||
|
||||
## Event: connected
|
||||
|
||||
Same as aedes [`clientReady`](./Aedes.md#event-clientready) but in client-wise.
|
||||
|
||||
## Event: error
|
||||
|
||||
- `error` `<Error>`
|
||||
|
||||
Emitted when an error occurs.
|
||||
|
||||
## client.publish (packet, [callback])
|
||||
|
||||
- `packet` `<object>` [`PUBLISH`][PUBLISH]
|
||||
- `callback` `<Function>` `(error) => void`
|
||||
- error `<Error>` | `null`
|
||||
|
||||
Publish the given `packet` to this client. QoS 1 and 2 are fully supported, while the retained flag is not.
|
||||
|
||||
`callback` will be invoked when the message has been sent, but not acked.
|
||||
|
||||
## client.subscribe (subscriptions, [callback])
|
||||
|
||||
- `subscriptions` `<object>`
|
||||
- `callback` `<Function>` `(error) => void`
|
||||
- error `<Error>` | `null`
|
||||
|
||||
Subscribe client to the list of topics.
|
||||
|
||||
`subscriptions` can be:
|
||||
|
||||
1. a single object in the format `{ topic: topic, qos: qos }`
|
||||
2. an array of the above
|
||||
3. a full [`SUBSCRIBE`][SUBSCRIBE], specifying a `messageId` will send suback to the client.
|
||||
|
||||
`callback` will be invoked when the subscription is completed.
|
||||
|
||||
## client.unsubscribe (unsubscriptions, [callback])
|
||||
|
||||
- `unsubscriptions` `<object>`
|
||||
- `callback` `<Function>` `(error) => void`
|
||||
- error `<Error>` | `null`
|
||||
|
||||
Unsubscribe client to the list of topics.
|
||||
|
||||
`unsubscriptions` can be:
|
||||
|
||||
1. a single object in the format `{ topic: topic, qos: qos }`
|
||||
2. an array of the above
|
||||
3. a full [`UNSUBSCRIBE`][UNSUBSCRIBE]
|
||||
|
||||
`callback` will be invoked when the unsubscriptions are completed.
|
||||
|
||||
## client.close ([callback])
|
||||
|
||||
Disconnect client
|
||||
|
||||
`callback` will be invoked when client is closed.
|
||||
|
||||
## client.emptyOutgoingQueue ([callback])
|
||||
|
||||
Clear all outgoing messages (QoS > 1) related to this client from persistence
|
||||
|
||||
`callback` will be invoked when the operation ends.
|
||||
|
||||
[PUBLISH]: https://github.com/mqttjs/mqtt-packet#publish
|
||||
[SUBSCRIBE]: https://github.com/mqttjs/mqtt-packet#subscribe
|
||||
[UNSUBSCRIBE]: https://github.com/mqttjs/mqtt-packet#unsubscribe
|
||||
|
||||
[websocket-stream]: https://www.npmjs.com/websocket-stream
|
||||
[websocket-stream-doc-on-the-server]: https://github.com/maxogden/websocket-stream/blob/master/readme.md#on-the-server
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<!-- markdownlint-disable MD013 MD024 -->
|
||||
# Examples
|
||||
|
||||
## Simple plain MQTT server
|
||||
|
||||
```js
|
||||
const aedes = require('aedes')()
|
||||
const server = require('net').createServer(aedes.handle)
|
||||
const port = 1883
|
||||
|
||||
server.listen(port, function () {
|
||||
console.log('server started and listening on port ', port)
|
||||
})
|
||||
```
|
||||
|
||||
## Typescript
|
||||
|
||||
```ts
|
||||
import Aedes from 'aedes'
|
||||
import { createServer } from 'net'
|
||||
|
||||
const port = 1883
|
||||
|
||||
const aedes = new Aedes()
|
||||
const server = createServer(aedes.handle)
|
||||
|
||||
server.listen(port, function () {
|
||||
console.log('server started and listening on port ', port)
|
||||
})
|
||||
```
|
||||
|
||||
## Simple plain MQTT server using server-factory
|
||||
|
||||
```js
|
||||
const aedes = require('aedes')()
|
||||
const { createServer } = require('aedes-server-factory')
|
||||
const port = 1883
|
||||
|
||||
const server = createServer(aedes)
|
||||
|
||||
server.listen(port, function () {
|
||||
console.log('server started and listening on port ', port)
|
||||
})
|
||||
```
|
||||
|
||||
## MQTT over TLS / MQTTS
|
||||
|
||||
```js
|
||||
const fs = require('fs')
|
||||
const aedes = require('aedes')()
|
||||
const port = 8883
|
||||
|
||||
const options = {
|
||||
key: fs.readFileSync('YOUR_PRIVATE_KEY_FILE.pem'),
|
||||
cert: fs.readFileSync('YOUR_PUBLIC_CERT_FILE.pem')
|
||||
}
|
||||
|
||||
const server = require('tls').createServer(options, aedes.handle)
|
||||
|
||||
server.listen(port, function () {
|
||||
console.log('server started and listening on port ', port)
|
||||
})
|
||||
```
|
||||
|
||||
## MQTT server over WebSocket
|
||||
|
||||
```js
|
||||
const aedes = require('aedes')()
|
||||
const httpServer = require('http').createServer()
|
||||
const ws = require('websocket-stream')
|
||||
const port = 8888
|
||||
|
||||
ws.createServer({ server: httpServer }, aedes.handle)
|
||||
|
||||
httpServer.listen(port, function () {
|
||||
console.log('websocket server listening on port ', port)
|
||||
})
|
||||
```
|
||||
|
||||
## MQTT server over WebSocket using server-factory
|
||||
|
||||
```js
|
||||
const aedes = require('aedes')()
|
||||
const { createServer } = require('aedes-server-factory')
|
||||
const port = 8888
|
||||
|
||||
const httpServer = createServer(aedes, { ws: true })
|
||||
|
||||
httpServer.listen(port, function () {
|
||||
console.log('websocket server listening on port ', port)
|
||||
})
|
||||
```
|
||||
|
||||
## Clusters
|
||||
|
||||
In order to use Aedes in clusters you have to choose a persistence and an mqemitter that supports clusters. Tested persistence/mqemitters that works with clusters are:
|
||||
|
||||
- [mqemitter-redis]
|
||||
- [mqemitter-child-process]
|
||||
- [mqemitter-mongodb]
|
||||
- [aedes-persistence-mongodb]
|
||||
- [aedes-persistence-redis]
|
||||
|
||||
[This](https://github.com/moscajs/aedes/blob/master/examples/clusters/index.js) is an example using [mqemitter-mongodb] and [aedes-persistence-mongodb]
|
||||
|
||||
[aedes-persistence-mongodb]: https://www.npmjs.com/aedes-persistence-mongodb
|
||||
[aedes-persistence-redis]: https://www.npmjs.com/aedes-persistence-redis
|
||||
|
||||
[mqemitter-redis]: https://www.npmjs.com/mqemitter-redis
|
||||
[mqemitter-mongodb]: https://www.npmjs.com/mqemitter-mongodb
|
||||
[mqemitter-child-process]: https://www.npmjs.com/mqemitter-child-process
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
'use strict'
|
||||
|
||||
const aedes = require('./aedes')()
|
||||
const server = require('net').createServer(aedes.handle)
|
||||
const httpServer = require('http').createServer()
|
||||
const ws = require('websocket-stream')
|
||||
const port = 1883
|
||||
const wsPort = 8888
|
||||
|
||||
server.listen(port, function () {
|
||||
console.log('server listening on port', port)
|
||||
})
|
||||
|
||||
ws.createServer({
|
||||
server: httpServer
|
||||
}, aedes.handle)
|
||||
|
||||
httpServer.listen(wsPort, function () {
|
||||
console.log('websocket server listening on port', wsPort)
|
||||
})
|
||||
|
||||
aedes.on('clientError', function (client, err) {
|
||||
console.log('client error', client.id, err.message, err.stack)
|
||||
})
|
||||
|
||||
aedes.on('connectionError', function (client, err) {
|
||||
console.log('client error', client, err.message, err.stack)
|
||||
})
|
||||
|
||||
aedes.on('publish', function (packet, client) {
|
||||
if (client) {
|
||||
console.log('message from client', client.id)
|
||||
}
|
||||
})
|
||||
|
||||
aedes.on('subscribe', function (subscriptions, client) {
|
||||
if (client) {
|
||||
console.log('subscribe from client', subscriptions, client.id)
|
||||
}
|
||||
})
|
||||
|
||||
aedes.on('client', function (client) {
|
||||
console.log('new client', client.id)
|
||||
})
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
const cluster = require('cluster')
|
||||
const Aedes = require('aedes')
|
||||
const { createServer } = require('net')
|
||||
const { cpus } = require('os')
|
||||
const MONGO_URL = 'mongodb://127.0.0.1/aedes-clusters'
|
||||
|
||||
const mq = process.env.MQ === 'redis'
|
||||
? require('mqemitter-redis')({
|
||||
port: process.env.REDIS_PORT || 6379
|
||||
})
|
||||
: require('mqemitter-mongodb')({
|
||||
url: MONGO_URL
|
||||
})
|
||||
|
||||
const persistence = process.env.PERSISTENCE === 'redis'
|
||||
? require('aedes-persistence-redis')({
|
||||
port: process.env.REDIS_PORT || 6379
|
||||
})
|
||||
: require('aedes-persistence-mongodb')({
|
||||
url: MONGO_URL
|
||||
})
|
||||
|
||||
function startAedes () {
|
||||
const port = 1883
|
||||
|
||||
const aedes = Aedes({
|
||||
id: 'BROKER_' + cluster.worker.id,
|
||||
mq,
|
||||
persistence
|
||||
})
|
||||
|
||||
const server = createServer(aedes.handle)
|
||||
|
||||
server.listen(port, '0.0.0.0', function () {
|
||||
console.log('Aedes listening on port:', port)
|
||||
aedes.publish({ topic: 'aedes/hello', payload: "I'm broker " + aedes.id })
|
||||
})
|
||||
|
||||
server.on('error', function (err) {
|
||||
console.log('Server error', err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
aedes.on('subscribe', function (subscriptions, client) {
|
||||
console.log('MQTT client \x1b[32m' + (client ? client.id : client) +
|
||||
'\x1b[0m subscribed to topics: ' + subscriptions.map(s => s.topic).join('\n'), 'from broker', aedes.id)
|
||||
})
|
||||
|
||||
aedes.on('unsubscribe', function (subscriptions, client) {
|
||||
console.log('MQTT client \x1b[32m' + (client ? client.id : client) +
|
||||
'\x1b[0m unsubscribed to topics: ' + subscriptions.join('\n'), 'from broker', aedes.id)
|
||||
})
|
||||
|
||||
// fired when a client connects
|
||||
aedes.on('client', function (client) {
|
||||
console.log('Client Connected: \x1b[33m' + (client ? client.id : client) + '\x1b[0m', 'to broker', aedes.id)
|
||||
})
|
||||
|
||||
// fired when a client disconnects
|
||||
aedes.on('clientDisconnect', function (client) {
|
||||
console.log('Client Disconnected: \x1b[31m' + (client ? client.id : client) + '\x1b[0m', 'to broker', aedes.id)
|
||||
})
|
||||
|
||||
// fired when a message is published
|
||||
aedes.on('publish', async function (packet, client) {
|
||||
console.log('Client \x1b[31m' + (client ? client.id : 'BROKER_' + aedes.id) + '\x1b[0m has published', packet.payload.toString(), 'on', packet.topic, 'to broker', aedes.id)
|
||||
})
|
||||
}
|
||||
|
||||
if (cluster.isMaster) {
|
||||
const numWorkers = cpus().length
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
cluster.fork()
|
||||
}
|
||||
|
||||
cluster.on('online', function (worker) {
|
||||
console.log('Worker ' + worker.process.pid + ' is online')
|
||||
})
|
||||
|
||||
cluster.on('exit', function (worker, code, signal) {
|
||||
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal)
|
||||
console.log('Starting a new worker')
|
||||
cluster.fork()
|
||||
})
|
||||
} else {
|
||||
startAedes()
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "aedes_clusters",
|
||||
"version": "1.0.0",
|
||||
"description": "Testing Aedes Broker with clusters",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "robertsLando",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aedes": "^0.48.1",
|
||||
"aedes-persistence-mongodb": "^9.1.0",
|
||||
"aedes-persistence-redis": "^9.0.1",
|
||||
"mqemitter-mongodb": "^8.1.0",
|
||||
"mqemitter-redis": "^5.0.0"
|
||||
}
|
||||
}
|
||||
+399
@@ -0,0 +1,399 @@
|
||||
'use strict'
|
||||
|
||||
const mqtt = require('mqtt-packet')
|
||||
const EventEmitter = require('events')
|
||||
const util = require('util')
|
||||
const eos = require('end-of-stream')
|
||||
const Packet = require('aedes-packet')
|
||||
const write = require('./write')
|
||||
const QoSPacket = require('./qos-packet')
|
||||
const handleSubscribe = require('./handlers/subscribe')
|
||||
const handleUnsubscribe = require('./handlers/unsubscribe')
|
||||
const handle = require('./handlers')
|
||||
const { pipeline } = require('stream')
|
||||
const { through } = require('./utils')
|
||||
|
||||
module.exports = Client
|
||||
|
||||
function Client (broker, conn, req) {
|
||||
const that = this
|
||||
|
||||
// metadata
|
||||
this.closed = false
|
||||
this.connecting = false
|
||||
this.connected = false
|
||||
this.connackSent = false
|
||||
this.errored = false
|
||||
|
||||
// mqtt params
|
||||
this.id = null
|
||||
this.clean = true
|
||||
this.version = null
|
||||
|
||||
this.subscriptions = {}
|
||||
this.duplicates = {}
|
||||
|
||||
this.broker = broker
|
||||
this.conn = conn
|
||||
conn.client = this
|
||||
|
||||
this._disconnected = false
|
||||
this._authorized = false
|
||||
this._parsingBatch = 1
|
||||
this._nextId = Math.ceil(Math.random() * 65535)
|
||||
|
||||
this.req = req
|
||||
this.connDetails = req ? req.connDetails : null
|
||||
|
||||
// we use two variables for the will
|
||||
// because we store in _will while
|
||||
// we are authenticating
|
||||
this.will = null
|
||||
this._will = null
|
||||
|
||||
this._parser = mqtt.parser()
|
||||
this._parser.client = this
|
||||
this._parser._queue = [] // queue packets received before client fires 'connect' event. Prevents memory leaks on 'connect' event
|
||||
this._parser.on('packet', enqueue)
|
||||
this.once('connected', dequeue)
|
||||
|
||||
function nextBatch (err) {
|
||||
if (err) {
|
||||
that.emit('error', err)
|
||||
return
|
||||
}
|
||||
|
||||
const client = that
|
||||
|
||||
if (client._paused) {
|
||||
return
|
||||
}
|
||||
|
||||
that._parsingBatch--
|
||||
if (that._parsingBatch <= 0) {
|
||||
that._parsingBatch = 0
|
||||
const buf = client.conn.read(null)
|
||||
if (buf) {
|
||||
client._parser.parse(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
this._nextBatch = nextBatch
|
||||
|
||||
conn.on('readable', nextBatch)
|
||||
|
||||
this.on('error', onError)
|
||||
conn.on('error', this.emit.bind(this, 'error'))
|
||||
this._parser.on('error', this.emit.bind(this, 'error'))
|
||||
|
||||
conn.on('end', this.close.bind(this))
|
||||
this._eos = eos(this.conn, this.close.bind(this))
|
||||
|
||||
const getToForwardPacket = (_packet) => {
|
||||
// Mqttv5 3.8.3.1: https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html#_Toc3901169
|
||||
// prevent to forward messages sent by the same client when no-local flag is set
|
||||
if (_packet.clientId === that.id && _packet.nl) return
|
||||
|
||||
const toForward = dedupe(that, _packet) &&
|
||||
that.broker.authorizeForward(that, _packet)
|
||||
|
||||
return toForward
|
||||
}
|
||||
|
||||
this.deliver0 = function deliverQoS0 (_packet, cb) {
|
||||
const toForward = getToForwardPacket(_packet)
|
||||
|
||||
if (toForward) {
|
||||
// Give nodejs some time to clear stacks, or we will see
|
||||
// "Maximum call stack size exceeded" in a very high load
|
||||
setImmediate(() => {
|
||||
const packet = new Packet(toForward, broker)
|
||||
packet.qos = 0
|
||||
write(that, packet, function (err) {
|
||||
that._onError(err)
|
||||
cb() // don't pass the error here or it will be thrown by mqemitter
|
||||
})
|
||||
})
|
||||
} else {
|
||||
setImmediate(cb)
|
||||
}
|
||||
}
|
||||
|
||||
this.deliverQoS = function deliverQoS (_packet, cb) {
|
||||
// downgrade to qos0 if requested by publish
|
||||
if (_packet.qos === 0) {
|
||||
that.deliver0(_packet, cb)
|
||||
return
|
||||
}
|
||||
const toForward = getToForwardPacket(_packet)
|
||||
|
||||
if (toForward) {
|
||||
setImmediate(() => {
|
||||
const packet = new QoSPacket(toForward, that)
|
||||
// Downgrading to client subscription qos if needed
|
||||
const clientSub = that.subscriptions[packet.topic]
|
||||
if (clientSub && (clientSub.qos || 0) < packet.qos) {
|
||||
packet.qos = clientSub.qos
|
||||
}
|
||||
packet.writeCallback = cb
|
||||
if (that.clean || packet.retain) {
|
||||
writeQoS(null, that, packet)
|
||||
} else {
|
||||
broker.persistence.outgoingUpdate(that, packet, writeQoS)
|
||||
}
|
||||
})
|
||||
} else if (that.clean === false) {
|
||||
that.broker.persistence.outgoingClearMessageId(that, _packet, noop)
|
||||
// we consider this to be an error, since the packet is undefined
|
||||
// so there's nothing to send
|
||||
setImmediate(cb)
|
||||
} else {
|
||||
setImmediate(cb)
|
||||
}
|
||||
}
|
||||
|
||||
this._keepaliveTimer = null
|
||||
this._keepaliveInterval = -1
|
||||
|
||||
this._connectTimer = setTimeout(function () {
|
||||
that.emit('error', new Error('connect did not arrive in time'))
|
||||
}, broker.connectTimeout)
|
||||
}
|
||||
|
||||
function dedupe (client, packet) {
|
||||
const id = packet.brokerId
|
||||
if (!id) {
|
||||
return true
|
||||
}
|
||||
const duplicates = client.duplicates
|
||||
const counter = packet.brokerCounter
|
||||
const result = (duplicates[id] || 0) < counter
|
||||
if (result) {
|
||||
duplicates[id] = counter
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function writeQoS (err, client, packet) {
|
||||
if (err) {
|
||||
// is this right, or we should ignore thins?
|
||||
client.emit('error', err)
|
||||
// don't pass the error here or it will be thrown by mqemitter
|
||||
packet.writeCallback()
|
||||
} else {
|
||||
write(client, packet, function (err) {
|
||||
if (err) {
|
||||
client.emit('error', err)
|
||||
}
|
||||
// don't pass the error here or it will be thrown by mqemitter
|
||||
packet.writeCallback()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function drainRequest (req) {
|
||||
req.callback()
|
||||
}
|
||||
|
||||
function onError (err) {
|
||||
if (!err) return
|
||||
|
||||
this.errored = true
|
||||
this.conn.removeAllListeners('error')
|
||||
this.conn.on('error', noop)
|
||||
// hack to clean up the write callbacks in case of error
|
||||
const state = this.conn._writableState
|
||||
const list = typeof state.getBuffer === 'function' ? state.getBuffer() : state.buffer
|
||||
list.forEach(drainRequest)
|
||||
this.broker.emit(this.id ? 'clientError' : 'connectionError', this, err)
|
||||
this.close()
|
||||
}
|
||||
|
||||
util.inherits(Client, EventEmitter)
|
||||
|
||||
Client.prototype._onError = onError
|
||||
|
||||
Client.prototype.publish = function (message, done) {
|
||||
const packet = new Packet(message, this.broker)
|
||||
const that = this
|
||||
if (packet.qos === 0) {
|
||||
// skip offline and send it as it is
|
||||
this.deliver0(packet, done)
|
||||
return
|
||||
}
|
||||
if (!this.clean && this.id) {
|
||||
this.broker.persistence.outgoingEnqueue({
|
||||
clientId: this.id
|
||||
}, packet, function deliver (err) {
|
||||
if (err) {
|
||||
return done(err)
|
||||
}
|
||||
that.deliverQoS(packet, done)
|
||||
})
|
||||
} else {
|
||||
that.deliverQoS(packet, done)
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.subscribe = function (packet, done) {
|
||||
if (!packet.subscriptions) {
|
||||
if (!Array.isArray(packet)) {
|
||||
packet = [packet]
|
||||
}
|
||||
packet = {
|
||||
subscriptions: packet
|
||||
}
|
||||
}
|
||||
handleSubscribe(this, packet, false, done)
|
||||
}
|
||||
|
||||
Client.prototype.unsubscribe = function (packet, done) {
|
||||
if (!packet.unsubscriptions) {
|
||||
if (!Array.isArray(packet)) {
|
||||
packet = [packet]
|
||||
}
|
||||
packet = {
|
||||
unsubscriptions: packet
|
||||
}
|
||||
}
|
||||
handleUnsubscribe(this, packet, done)
|
||||
}
|
||||
|
||||
Client.prototype.close = function (done) {
|
||||
if (this.closed) {
|
||||
if (typeof done === 'function') {
|
||||
done()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const that = this
|
||||
const conn = this.conn
|
||||
|
||||
this.closed = true
|
||||
|
||||
this._parser.removeAllListeners('packet')
|
||||
conn.removeAllListeners('readable')
|
||||
|
||||
this._parser._queue = null
|
||||
|
||||
if (this._keepaliveTimer) {
|
||||
this._keepaliveTimer.clear()
|
||||
this._keepaliveInterval = -1
|
||||
this._keepaliveTimer = null
|
||||
}
|
||||
|
||||
if (this._connectTimer) {
|
||||
clearTimeout(this._connectTimer)
|
||||
this._connectTimer = null
|
||||
}
|
||||
|
||||
this._eos()
|
||||
this._eos = noop
|
||||
|
||||
handleUnsubscribe(
|
||||
this,
|
||||
{
|
||||
unsubscriptions: Object.keys(this.subscriptions)
|
||||
},
|
||||
finish)
|
||||
|
||||
function finish () {
|
||||
const will = that.will
|
||||
// _disconnected is set only if client is disconnected with a valid disconnect packet
|
||||
if (!that._disconnected && will) {
|
||||
that.broker.authorizePublish(that, will, function (err) {
|
||||
if (err) { return done() }
|
||||
that.broker.publish(will, that, done)
|
||||
|
||||
function done () {
|
||||
that.broker.persistence.delWill({
|
||||
id: that.id,
|
||||
brokerId: that.broker.id
|
||||
}, noop)
|
||||
}
|
||||
})
|
||||
} else if (will) {
|
||||
// delete the persisted will even on clean disconnect https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349232
|
||||
that.broker.persistence.delWill({
|
||||
id: that.id,
|
||||
brokerId: that.broker.id
|
||||
}, noop)
|
||||
}
|
||||
|
||||
that.will = null // this function might be called twice
|
||||
that._will = null
|
||||
|
||||
that.connected = false
|
||||
that.connecting = false
|
||||
|
||||
conn.removeAllListeners('error')
|
||||
conn.on('error', noop)
|
||||
|
||||
if (that.broker.clients[that.id] && that._authorized) {
|
||||
that.broker.unregisterClient(that)
|
||||
}
|
||||
|
||||
// clear up the drain event listeners
|
||||
that.conn.emit('drain')
|
||||
that.conn.removeAllListeners('drain')
|
||||
|
||||
conn.destroy()
|
||||
|
||||
if (typeof done === 'function') {
|
||||
done()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.pause = function () {
|
||||
this._paused = true
|
||||
}
|
||||
|
||||
Client.prototype.resume = function () {
|
||||
this._paused = false
|
||||
this._nextBatch()
|
||||
}
|
||||
|
||||
function enqueue (packet) {
|
||||
const client = this.client
|
||||
client._parsingBatch++
|
||||
// already connected or it's the first packet
|
||||
if (client.connackSent || client._parsingBatch === 1) {
|
||||
handle(client, packet, client._nextBatch)
|
||||
} else {
|
||||
if (this._queue.length < client.broker.queueLimit) {
|
||||
this._queue.push(packet)
|
||||
} else {
|
||||
this.emit('error', new Error('Client queue limit reached'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dequeue () {
|
||||
const q = this._parser._queue
|
||||
if (q) {
|
||||
for (let i = 0, len = q.length; i < len; i++) {
|
||||
handle(this, q[i], this._nextBatch)
|
||||
}
|
||||
|
||||
this._parser._queue = null
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.emptyOutgoingQueue = function (done) {
|
||||
const client = this
|
||||
const persistence = client.broker.persistence
|
||||
|
||||
function filter (packet, enc, next) {
|
||||
persistence.outgoingClearMessageId(client, packet, next)
|
||||
}
|
||||
|
||||
pipeline(
|
||||
persistence.outgoingStream(client),
|
||||
through(filter),
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
function noop () {}
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
'use strict'
|
||||
|
||||
const retimer = require('retimer')
|
||||
const { pipeline } = require('stream')
|
||||
const write = require('../write')
|
||||
const QoSPacket = require('../qos-packet')
|
||||
const { through } = require('../utils')
|
||||
const handleSubscribe = require('./subscribe')
|
||||
const uniqueId = require('hyperid')()
|
||||
|
||||
function Connack (arg) {
|
||||
this.cmd = 'connack'
|
||||
this.returnCode = arg.returnCode
|
||||
this.sessionPresent = arg.sessionPresent
|
||||
}
|
||||
|
||||
function ClientPacketStatus (client, packet) {
|
||||
this.client = client
|
||||
this.packet = packet
|
||||
}
|
||||
|
||||
const connectActions = [
|
||||
authenticate,
|
||||
setKeepAlive,
|
||||
fetchSubs,
|
||||
restoreSubs,
|
||||
storeWill,
|
||||
registerClient,
|
||||
doConnack,
|
||||
emptyQueue
|
||||
]
|
||||
|
||||
const errorMessages = [
|
||||
'',
|
||||
'unacceptable protocol version',
|
||||
'identifier rejected',
|
||||
'Server unavailable',
|
||||
'bad user name or password',
|
||||
'not authorized',
|
||||
'keep alive limit exceeded'
|
||||
]
|
||||
|
||||
function handleConnect (client, packet, done) {
|
||||
clearTimeout(client._connectTimer)
|
||||
client._connectTimer = null
|
||||
client.connecting = true
|
||||
client.broker.preConnect(client, packet, negate)
|
||||
|
||||
function negate (err, successful) {
|
||||
if (!err && successful === true) {
|
||||
setImmediate(init, client, packet, done)
|
||||
} else {
|
||||
client.connecting = false
|
||||
done(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init (client, packet, done) {
|
||||
const clientId = packet.clientId
|
||||
let returnCode = 0
|
||||
// [MQTT-3.1.2-2]
|
||||
if (packet.protocolVersion < 3 || packet.protocolVersion > 4) {
|
||||
returnCode = 1
|
||||
}
|
||||
// MQTT 3.1.0 allows <= 23 client id length
|
||||
if (packet.protocolVersion === 3 && clientId.length > client.broker.maxClientsIdLength) {
|
||||
returnCode = 2
|
||||
}
|
||||
// check if the client keepalive is compatible with broker settings
|
||||
if (client.broker.keepaliveLimit && (!packet.keepalive || packet.keepalive > client.broker.keepaliveLimit)) {
|
||||
returnCode = 6
|
||||
}
|
||||
if (returnCode > 0) {
|
||||
const error = new Error(errorMessages[returnCode])
|
||||
error.errorCode = returnCode
|
||||
doConnack(
|
||||
{ client, returnCode, sessionPresent: false },
|
||||
done.bind(this, error))
|
||||
return
|
||||
}
|
||||
|
||||
client.id = clientId || 'aedes_' + uniqueId()
|
||||
client.clean = packet.clean
|
||||
client.version = packet.protocolVersion
|
||||
client._will = packet.will
|
||||
|
||||
client.broker._series(
|
||||
new ClientPacketStatus(client, packet),
|
||||
connectActions,
|
||||
{ returnCode: 0, sessionPresent: false }, // [MQTT-3.1.4-4], [MQTT-3.2.2-4]
|
||||
function (err) {
|
||||
this.client.connecting = false
|
||||
if (!err) {
|
||||
this.client.connected = true
|
||||
this.client.broker.emit('clientReady', client)
|
||||
this.client.emit('connected')
|
||||
}
|
||||
done(err)
|
||||
})
|
||||
}
|
||||
|
||||
function authenticate (arg, done) {
|
||||
const client = this.client
|
||||
client.pause()
|
||||
client.broker.authenticate(
|
||||
client,
|
||||
this.packet.username,
|
||||
this.packet.password,
|
||||
negate)
|
||||
|
||||
function negate (err, successful) {
|
||||
if (client.closed || client.broker.closed) {
|
||||
// a hack, sometimes client.close() or broker.close() happened
|
||||
// before authenticate() comes back
|
||||
// we stop here for not to register it and deregister it in write()
|
||||
return
|
||||
}
|
||||
if (!err && successful) {
|
||||
client._authorized = true
|
||||
return done()
|
||||
}
|
||||
|
||||
if (err) {
|
||||
const errCode = err.returnCode
|
||||
if (errCode && (errCode >= 2 && errCode <= 5)) {
|
||||
arg.returnCode = errCode
|
||||
} else {
|
||||
arg.returnCode = 5
|
||||
}
|
||||
if (!err.message) {
|
||||
err.message = errorMessages[arg.returnCode]
|
||||
}
|
||||
} else {
|
||||
arg.returnCode = 5
|
||||
err = new Error(errorMessages[arg.returnCode])
|
||||
}
|
||||
err.errorCode = arg.returnCode
|
||||
arg.client = client
|
||||
doConnack(arg,
|
||||
// [MQTT-3.2.2-5]
|
||||
client.close.bind(client, done.bind(this, err)))
|
||||
}
|
||||
}
|
||||
|
||||
function setKeepAlive (arg, done) {
|
||||
if (this.packet.keepalive > 0) {
|
||||
const client = this.client
|
||||
// [MQTT-3.1.2-24]
|
||||
client._keepaliveInterval = (this.packet.keepalive * 1500) + 1
|
||||
client._keepaliveTimer = retimer(function keepaliveTimeout () {
|
||||
client.broker.emit('keepaliveTimeout', client)
|
||||
client.emit('error', new Error('keep alive timeout'))
|
||||
}, client._keepaliveInterval)
|
||||
}
|
||||
done()
|
||||
}
|
||||
|
||||
function fetchSubs (arg, done) {
|
||||
const client = this.client
|
||||
if (!this.packet.clean) {
|
||||
client.broker.persistence.subscriptionsByClient({
|
||||
id: client.id,
|
||||
done,
|
||||
arg
|
||||
}, gotSubs)
|
||||
return
|
||||
}
|
||||
arg.sessionPresent = false // [MQTT-3.2.2-1]
|
||||
client.broker.persistence.cleanSubscriptions(
|
||||
client,
|
||||
done)
|
||||
}
|
||||
|
||||
function gotSubs (err, subs, client) {
|
||||
if (err) {
|
||||
return client.done(err)
|
||||
}
|
||||
client.arg.subs = subs
|
||||
client.done()
|
||||
}
|
||||
|
||||
function restoreSubs (arg, done) {
|
||||
if (arg.subs) {
|
||||
handleSubscribe(this.client, { subscriptions: arg.subs }, true, done)
|
||||
arg.sessionPresent = !!arg.subs // cast to boolean, [MQTT-3.2.2-2]
|
||||
return
|
||||
}
|
||||
arg.sessionPresent = false // [MQTT-3.2.2-1], [MQTT-3.2.2-3]
|
||||
done()
|
||||
}
|
||||
|
||||
function storeWill (arg, done) {
|
||||
const client = this.client
|
||||
client.will = client._will
|
||||
// delete any existing will messages from persistence
|
||||
client.broker.persistence.delWill(client, function () {
|
||||
if (client.will) {
|
||||
client.broker.persistence.putWill(
|
||||
client,
|
||||
client.will,
|
||||
done)
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function registerClient (arg, done) {
|
||||
const client = this.client
|
||||
client.broker.registerClient(client)
|
||||
done()
|
||||
}
|
||||
|
||||
function doConnack (arg, done) {
|
||||
const client = arg.client || this.client
|
||||
const connack = new Connack(arg)
|
||||
write(client, connack, function (err) {
|
||||
if (!err) {
|
||||
client.broker.emit('connackSent', connack, client)
|
||||
client.connackSent = true
|
||||
}
|
||||
done(err)
|
||||
})
|
||||
}
|
||||
|
||||
// push any queued messages (included retained messages) at the disconnected time
|
||||
// when QoS > 0 and session is true
|
||||
function emptyQueue (arg, done) {
|
||||
const client = this.client
|
||||
const persistence = client.broker.persistence
|
||||
const outgoing = persistence.outgoingStream(client)
|
||||
|
||||
client.resume()
|
||||
|
||||
pipeline(
|
||||
outgoing,
|
||||
through(function clearQueue (data, enc, next) {
|
||||
const packet = new QoSPacket(data, client)
|
||||
// Here we are deliberatly passing only the error
|
||||
// This is because there is no destination stream so the "client"
|
||||
// Object filled the buffer up to the highWaterMark preventing stored messages
|
||||
// being sent
|
||||
packet.writeCallback = (error, _client) => next(error)
|
||||
persistence.outgoingUpdate(client, packet, emptyQueueFilter)
|
||||
}),
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
function emptyQueueFilter (err, client, packet) {
|
||||
const next = packet.writeCallback
|
||||
|
||||
if (err) {
|
||||
client.emit('error', err)
|
||||
return next()
|
||||
}
|
||||
|
||||
const authorized = (packet.cmd === 'publish')
|
||||
? client.broker.authorizeForward(client, packet)
|
||||
: true
|
||||
|
||||
const persistence = client.broker.persistence
|
||||
|
||||
if (client.clean || !authorized) {
|
||||
persistence.outgoingClearMessageId(client, packet, next)
|
||||
} else {
|
||||
write(client, packet, next)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = handleConnect
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
'use strict'
|
||||
|
||||
const handleConnect = require('./connect')
|
||||
const handleSubscribe = require('./subscribe')
|
||||
const handleUnsubscribe = require('./unsubscribe')
|
||||
const handlePublish = require('./publish')
|
||||
const handlePuback = require('./puback')
|
||||
const handlePubrel = require('./pubrel')
|
||||
const handlePubrec = require('./pubrec')
|
||||
const handlePing = require('./ping')
|
||||
|
||||
function handle (client, packet, done) {
|
||||
if (packet.cmd === 'connect') {
|
||||
if (client.connecting || client.connected) {
|
||||
// [MQTT-3.1.0-2]
|
||||
finish(client.conn, packet, done)
|
||||
return
|
||||
}
|
||||
handleConnect(client, packet, done)
|
||||
return
|
||||
}
|
||||
|
||||
if (!client.connecting && !client.connected) {
|
||||
// [MQTT-3.1.0-1]
|
||||
finish(client.conn, packet, done)
|
||||
return
|
||||
}
|
||||
|
||||
switch (packet.cmd) {
|
||||
case 'publish':
|
||||
handlePublish(client, packet, done)
|
||||
break
|
||||
case 'subscribe':
|
||||
handleSubscribe(client, packet, false, done)
|
||||
break
|
||||
case 'unsubscribe':
|
||||
handleUnsubscribe(client, packet, done)
|
||||
break
|
||||
case 'pubcomp':
|
||||
case 'puback':
|
||||
handlePuback(client, packet, done)
|
||||
break
|
||||
case 'pubrel':
|
||||
handlePubrel(client, packet, done)
|
||||
break
|
||||
case 'pubrec':
|
||||
handlePubrec(client, packet, done)
|
||||
break
|
||||
case 'pingreq':
|
||||
handlePing(client, packet, done)
|
||||
break
|
||||
case 'disconnect':
|
||||
// [MQTT-3.14.4-3]
|
||||
client._disconnected = true
|
||||
// [MQTT-3.14.4-1] [MQTT-3.14.4-2]
|
||||
client.conn.destroy()
|
||||
done()
|
||||
return
|
||||
default:
|
||||
client.conn.destroy()
|
||||
done()
|
||||
return
|
||||
}
|
||||
|
||||
if (client._keepaliveInterval > 0) {
|
||||
client._keepaliveTimer.reschedule(client._keepaliveInterval)
|
||||
}
|
||||
}
|
||||
|
||||
function finish (conn, packet, done) {
|
||||
conn.destroy()
|
||||
const error = new Error('Invalid protocol')
|
||||
error.info = packet
|
||||
done(error)
|
||||
}
|
||||
|
||||
module.exports = handle
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const write = require('../write')
|
||||
const pingResp = {
|
||||
cmd: 'pingresp'
|
||||
}
|
||||
|
||||
function handlePing (client, packet, done) {
|
||||
client.broker.emit('ping', packet, client)
|
||||
write(client, pingResp, done)
|
||||
}
|
||||
|
||||
module.exports = handlePing
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
function handlePuback (client, packet, done) {
|
||||
const persistence = client.broker.persistence
|
||||
persistence.outgoingClearMessageId(client, packet, function (err, origPacket) {
|
||||
client.broker.emit('ack', origPacket, client)
|
||||
done(err)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = handlePuback
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
'use strict'
|
||||
|
||||
const write = require('../write')
|
||||
|
||||
function PubAck (packet) {
|
||||
this.cmd = 'puback'
|
||||
this.messageId = packet.messageId
|
||||
}
|
||||
|
||||
function PubRec (packet) {
|
||||
this.cmd = 'pubrec'
|
||||
this.messageId = packet.messageId
|
||||
}
|
||||
|
||||
const publishActions = [
|
||||
authorizePublish,
|
||||
enqueuePublish
|
||||
]
|
||||
function handlePublish (client, packet, done) {
|
||||
const topic = packet.topic
|
||||
let err
|
||||
if (topic.length === 0) {
|
||||
err = new Error('empty topic not allowed in PUBLISH')
|
||||
return done(err)
|
||||
}
|
||||
if (topic.indexOf('#') > -1) {
|
||||
err = new Error('# is not allowed in PUBLISH')
|
||||
return done(err)
|
||||
}
|
||||
if (topic.indexOf('+') > -1) {
|
||||
err = new Error('+ is not allowed in PUBLISH')
|
||||
return done(err)
|
||||
}
|
||||
client.broker._series(client, publishActions, packet, done)
|
||||
}
|
||||
|
||||
function enqueuePublish (packet, done) {
|
||||
const client = this
|
||||
|
||||
switch (packet.qos) {
|
||||
case 2:
|
||||
client.broker.persistence.incomingStorePacket(client, packet, function (err) {
|
||||
if (err) { return done(err) }
|
||||
write(client, new PubRec(packet), done)
|
||||
})
|
||||
break
|
||||
case 1:
|
||||
write(client, new PubAck(packet), function (err) {
|
||||
if (err) { return done(err) }
|
||||
client.broker.publish(packet, client, done)
|
||||
})
|
||||
break
|
||||
case 0:
|
||||
client.broker.publish(packet, client, done)
|
||||
break
|
||||
default:
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
function authorizePublish (packet, done) {
|
||||
this.broker.authorizePublish(this, packet, done)
|
||||
}
|
||||
|
||||
module.exports = handlePublish
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
'use strict'
|
||||
|
||||
const write = require('../write')
|
||||
|
||||
function PubRel (packet) {
|
||||
this.cmd = 'pubrel'
|
||||
this.messageId = packet.messageId
|
||||
}
|
||||
|
||||
function handlePubrec (client, packet, done) {
|
||||
const pubrel = new PubRel(packet)
|
||||
|
||||
if (client.clean) {
|
||||
write(client, pubrel, done)
|
||||
return
|
||||
}
|
||||
|
||||
client.broker.persistence.outgoingUpdate(
|
||||
client, pubrel, reply)
|
||||
|
||||
function reply (err) {
|
||||
if (err) {
|
||||
done(err)
|
||||
} else {
|
||||
write(client, pubrel, done)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = handlePubrec
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
'use strict'
|
||||
|
||||
const write = require('../write')
|
||||
|
||||
function ClientPacketStatus (client, packet) {
|
||||
this.client = client
|
||||
this.packet = packet
|
||||
}
|
||||
|
||||
function PubComp (packet) {
|
||||
this.cmd = 'pubcomp'
|
||||
this.messageId = packet.messageId
|
||||
}
|
||||
|
||||
const pubrelActions = [
|
||||
pubrelGet,
|
||||
pubrelPublish,
|
||||
pubrelWrite,
|
||||
pubrelDel
|
||||
]
|
||||
function handlePubrel (client, packet, done) {
|
||||
client.broker._series(
|
||||
new ClientPacketStatus(client, packet),
|
||||
pubrelActions, {}, done)
|
||||
}
|
||||
|
||||
function pubrelGet (arg, done) {
|
||||
const persistence = this.client.broker.persistence
|
||||
persistence.incomingGetPacket(this.client, this.packet, reply)
|
||||
|
||||
function reply (err, packet) {
|
||||
arg.packet = packet
|
||||
done(err)
|
||||
}
|
||||
}
|
||||
|
||||
function pubrelPublish (arg, done) {
|
||||
this.client.broker.publish(arg.packet, this.client, done)
|
||||
}
|
||||
|
||||
function pubrelWrite (arg, done) {
|
||||
write(this.client, new PubComp(arg.packet), done)
|
||||
}
|
||||
|
||||
function pubrelDel (arg, done) {
|
||||
const persistence = this.client.broker.persistence
|
||||
persistence.incomingDelPacket(this.client, arg.packet, done)
|
||||
}
|
||||
|
||||
module.exports = handlePubrel
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
'use strict'
|
||||
|
||||
const fastfall = require('fastfall')
|
||||
const Packet = require('aedes-packet')
|
||||
const { through } = require('../utils')
|
||||
const { validateTopic, $SYS_PREFIX } = require('../utils')
|
||||
const write = require('../write')
|
||||
|
||||
const subscribeTopicActions = fastfall([
|
||||
authorize,
|
||||
storeSubscriptions,
|
||||
addSubs
|
||||
])
|
||||
const restoreTopicActions = fastfall([
|
||||
authorize,
|
||||
addSubs
|
||||
])
|
||||
|
||||
function SubAck (packet, granted) {
|
||||
this.cmd = 'suback'
|
||||
this.messageId = packet.messageId
|
||||
// the qos granted
|
||||
this.granted = granted
|
||||
}
|
||||
|
||||
function Subscription (qos, func, rh, rap, nl) {
|
||||
this.qos = qos
|
||||
this.func = func
|
||||
|
||||
// retain-handling indicates how retained messages should be
|
||||
// handled when a new subscription is created
|
||||
// (see [MQTT-3.3.1-9] through [MQTT-3.3.1-11])
|
||||
this.rh = rh
|
||||
|
||||
// retain-as-published indicates whether to leave the retain flag as-is (true)
|
||||
// or to clear it before sending to subscriptions (false) default false
|
||||
// (see [MQTT-3.3.1-12] through [MQTT-3.3.1-13])
|
||||
this.rap = rap
|
||||
|
||||
// no-local indicates that a client should not receive its own
|
||||
// messages (see [MQTT-3.8.3-3])
|
||||
this.nl = nl
|
||||
}
|
||||
|
||||
function SubscribeState (client, packet, restore, finish) {
|
||||
this.client = client
|
||||
this.packet = packet
|
||||
this.actions = restore ? restoreTopicActions : subscribeTopicActions
|
||||
this.finish = finish
|
||||
this.subState = []
|
||||
}
|
||||
|
||||
function SubState (client, packet, granted, rh, rap, nl) {
|
||||
this.client = client
|
||||
this.packet = packet
|
||||
this.granted = granted
|
||||
this.rh = rh
|
||||
this.rap = rap
|
||||
this.nl = nl
|
||||
}
|
||||
|
||||
// if same subscribed topic in subs array, we pick up the last one
|
||||
function _dedupe (subs) {
|
||||
const dedupeSubs = {}
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
const sub = subs[i]
|
||||
dedupeSubs[sub.topic] = sub
|
||||
}
|
||||
const ret = []
|
||||
for (const key in dedupeSubs) {
|
||||
ret.push(dedupeSubs[key])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
function handleSubscribe (client, packet, restore, done) {
|
||||
packet.subscriptions = packet.subscriptions.length === 1 ? packet.subscriptions : _dedupe(packet.subscriptions)
|
||||
client.broker._parallel(
|
||||
new SubscribeState(client, packet, restore, done), // what will be this in the functions
|
||||
doSubscribe, // function to call
|
||||
packet.subscriptions, // first argument of the function
|
||||
restore ? done : completeSubscribe // the function to be called when the parallel ends
|
||||
)
|
||||
}
|
||||
|
||||
function doSubscribe (sub, done) {
|
||||
const s = new SubState(this.client, this.packet, sub.qos, sub.rh, sub.rap, sub.nl)
|
||||
this.subState.push(s)
|
||||
this.actions.call(s, sub, done)
|
||||
}
|
||||
|
||||
function authorize (sub, done) {
|
||||
const err = validateTopic(sub.topic, 'SUBSCRIBE')
|
||||
if (err) {
|
||||
return done(err)
|
||||
}
|
||||
const client = this.client
|
||||
client.broker.authorizeSubscribe(client, sub, done)
|
||||
}
|
||||
|
||||
function blockDollarSignTopics (func) {
|
||||
return function deliverSharp (packet, cb) {
|
||||
// '$' is 36
|
||||
if (packet.topic.charCodeAt(0) === 36) {
|
||||
cb()
|
||||
} else {
|
||||
func(packet, cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function storeSubscriptions (sub, done) {
|
||||
if (!sub || typeof sub !== 'object') {
|
||||
// means failure: MQTT 3.1.1 specs > 3.9.3 Payload
|
||||
this.granted = 128
|
||||
return done(null, null)
|
||||
}
|
||||
|
||||
const packet = this.packet
|
||||
const client = this.client
|
||||
|
||||
if (client.clean) {
|
||||
return done(null, sub)
|
||||
}
|
||||
|
||||
client.broker.persistence.addSubscriptions(client, packet.subscriptions, function addSub (err) {
|
||||
done(err, sub)
|
||||
})
|
||||
}
|
||||
|
||||
function addSubs (sub, done) {
|
||||
if (!sub || typeof sub !== 'object') {
|
||||
return done()
|
||||
}
|
||||
|
||||
const client = this.client
|
||||
const broker = client.broker
|
||||
const topic = sub.topic
|
||||
const qos = sub.qos
|
||||
const rh = this.rh
|
||||
const rap = this.rap
|
||||
const nl = this.nl
|
||||
let func = qos > 0 ? client.deliverQoS : client.deliver0
|
||||
|
||||
const deliverFunc = func
|
||||
func = function handlePacketSubscription (_packet, cb) {
|
||||
_packet = new Packet(_packet, broker)
|
||||
_packet.nl = nl
|
||||
if (!rap) _packet.retain = false
|
||||
deliverFunc(_packet, cb)
|
||||
}
|
||||
|
||||
// [MQTT-4.7.2-1]
|
||||
if (isStartsWithWildcard(topic)) {
|
||||
func = blockDollarSignTopics(func)
|
||||
}
|
||||
|
||||
if (client.closed || client.broker.closed) {
|
||||
// a hack, sometimes client.close() or broker.close() happened
|
||||
// before authenticate() comes back
|
||||
// we don't continue subscription here
|
||||
return
|
||||
}
|
||||
|
||||
if (!client.subscriptions[topic]) {
|
||||
client.subscriptions[topic] = new Subscription(qos, func, rh, rap, nl)
|
||||
broker.subscribe(topic, func, done)
|
||||
} else if (client.subscriptions[topic].qos !== qos || client.subscriptions[topic].rh !== rh || client.subscriptions[topic].rap !== rap || client.subscriptions[topic].nl !== nl) {
|
||||
broker.unsubscribe(topic, client.subscriptions[topic].func)
|
||||
client.subscriptions[topic] = new Subscription(qos, func, rh, rap, nl)
|
||||
broker.subscribe(topic, func, done)
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
// + is 43
|
||||
// # is 35
|
||||
function isStartsWithWildcard (topic) {
|
||||
const code = topic.charCodeAt(0)
|
||||
return code === 43 || code === 35
|
||||
}
|
||||
|
||||
function completeSubscribe (err) {
|
||||
const done = this.finish
|
||||
|
||||
if (err) {
|
||||
return done(err)
|
||||
}
|
||||
|
||||
const packet = this.packet
|
||||
const client = this.client
|
||||
|
||||
if (packet.messageId !== undefined) {
|
||||
// [MQTT-3.9.3-1]
|
||||
write(client,
|
||||
new SubAck(packet, this.subState.map(obj => obj.granted)),
|
||||
done)
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
|
||||
const broker = client.broker
|
||||
|
||||
// subscriptions array to return as result in 'subscribe' event and $SYS
|
||||
const subs = packet.subscriptions
|
||||
|
||||
// topics we need to retrieve retain values from
|
||||
const topics = []
|
||||
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
// skip topics that are not allowed
|
||||
if (this.subState[i].granted !== 128) {
|
||||
topics.push(subs[i].topic)
|
||||
}
|
||||
// set granted qos to subscriptions
|
||||
subs[i].qos = this.subState[i].granted
|
||||
}
|
||||
|
||||
this.subState = []
|
||||
|
||||
broker.emit('subscribe', subs, client)
|
||||
broker.publish({
|
||||
topic: $SYS_PREFIX + broker.id + '/new/subscribes',
|
||||
payload: Buffer.from(JSON.stringify({
|
||||
clientId: client.id,
|
||||
subs
|
||||
}), 'utf8')
|
||||
}, noop)
|
||||
|
||||
// Conform to MQTT 3.1.1 section 3.1.2.4
|
||||
// Restored sessions should not contain any retained message.
|
||||
// Retained message should be only fetched from SUBSCRIBE.
|
||||
|
||||
if (topics.length > 0) {
|
||||
const persistence = broker.persistence
|
||||
const stream = persistence.createRetainedStreamCombi(topics)
|
||||
stream.pipe(through(function sendRetained (packet, enc, cb) {
|
||||
packet = new Packet({
|
||||
cmd: packet.cmd,
|
||||
qos: packet.qos,
|
||||
topic: packet.topic,
|
||||
payload: packet.payload,
|
||||
retain: true
|
||||
}, broker)
|
||||
// this should not be deduped
|
||||
packet.brokerId = null
|
||||
client.deliverQoS(packet, cb)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function noop () { }
|
||||
|
||||
module.exports = handleSubscribe
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
'use strict'
|
||||
|
||||
const write = require('../write')
|
||||
const { validateTopic, $SYS_PREFIX } = require('../utils')
|
||||
|
||||
function UnSubAck (packet) {
|
||||
this.cmd = 'unsuback'
|
||||
this.messageId = packet.messageId // [MQTT-3.10.4-4]
|
||||
}
|
||||
|
||||
function UnsubscribeState (client, packet, finish) {
|
||||
this.client = client
|
||||
this.packet = packet
|
||||
this.finish = finish
|
||||
}
|
||||
|
||||
function handleUnsubscribe (client, packet, done) {
|
||||
const broker = client.broker
|
||||
const unsubscriptions = packet.unsubscriptions
|
||||
let err
|
||||
|
||||
for (let i = 0; i < unsubscriptions.length; i++) {
|
||||
err = validateTopic(unsubscriptions[i], 'UNSUBSCRIBE')
|
||||
if (err) {
|
||||
return done(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (packet.messageId !== undefined) {
|
||||
if (client.clean) {
|
||||
return actualUnsubscribe(client, packet, done)
|
||||
}
|
||||
|
||||
broker.persistence.removeSubscriptions(client, unsubscriptions, function (err) {
|
||||
if (err) {
|
||||
return done(err)
|
||||
}
|
||||
|
||||
actualUnsubscribe(client, packet, done)
|
||||
})
|
||||
} else {
|
||||
actualUnsubscribe(client, packet, done)
|
||||
}
|
||||
}
|
||||
|
||||
function actualUnsubscribe (client, packet, done) {
|
||||
const broker = client.broker
|
||||
broker._parallel(
|
||||
new UnsubscribeState(client, packet, done),
|
||||
doUnsubscribe,
|
||||
packet.unsubscriptions,
|
||||
completeUnsubscribe)
|
||||
}
|
||||
|
||||
function doUnsubscribe (sub, done) {
|
||||
const client = this.client
|
||||
const broker = client.broker
|
||||
const s = client.subscriptions[sub]
|
||||
|
||||
if (s) {
|
||||
const func = s.func
|
||||
delete client.subscriptions[sub]
|
||||
broker.unsubscribe(
|
||||
sub,
|
||||
func,
|
||||
done)
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
function completeUnsubscribe (err) {
|
||||
const client = this.client
|
||||
|
||||
if (err) {
|
||||
client.emit('error', err)
|
||||
return
|
||||
}
|
||||
|
||||
const packet = this.packet
|
||||
const done = this.finish
|
||||
|
||||
if (packet.messageId !== undefined) {
|
||||
write(client, new UnSubAck(packet),
|
||||
done)
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
|
||||
if ((!client.closed || client.clean === true) && packet.unsubscriptions.length > 0) {
|
||||
client.broker.emit('unsubscribe', packet.unsubscriptions, client)
|
||||
client.broker.publish({
|
||||
topic: $SYS_PREFIX + client.broker.id + '/new/unsubscribes',
|
||||
payload: Buffer.from(JSON.stringify({
|
||||
clientId: client.id,
|
||||
subs: packet.unsubscriptions
|
||||
}), 'utf8')
|
||||
}, noop)
|
||||
}
|
||||
}
|
||||
|
||||
function noop () { }
|
||||
|
||||
module.exports = handleUnsubscribe
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
'use strict'
|
||||
|
||||
const Packet = require('aedes-packet')
|
||||
const util = require('util')
|
||||
|
||||
function QoSPacket (original, client) {
|
||||
Packet.call(this, original, client.broker)
|
||||
|
||||
this.writeCallback = client._onError.bind(client)
|
||||
|
||||
if (!original.messageId) {
|
||||
this.messageId = client._nextId
|
||||
if (client._nextId >= 65535) {
|
||||
client._nextId = 1
|
||||
} else {
|
||||
client._nextId++
|
||||
}
|
||||
} else {
|
||||
this.messageId = original.messageId
|
||||
}
|
||||
}
|
||||
|
||||
util.inherits(QoSPacket, Packet)
|
||||
|
||||
module.exports = QoSPacket
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
'use strict'
|
||||
|
||||
const { Transform, Writable } = require('stream')
|
||||
|
||||
function validateTopic (topic, message) {
|
||||
if (!topic || topic.length === 0) { // [MQTT-3.8.3-3]
|
||||
return new Error('impossible to ' + message + ' to an empty topic')
|
||||
}
|
||||
|
||||
const end = topic.length - 1
|
||||
const endMinus = end - 1
|
||||
const slashInPreEnd = endMinus > 0 && topic.charCodeAt(endMinus) !== 47
|
||||
|
||||
for (let i = 0; i < topic.length; i++) {
|
||||
switch (topic.charCodeAt(i)) {
|
||||
case 35: { // #
|
||||
const notAtTheEnd = i !== end
|
||||
if (notAtTheEnd || slashInPreEnd) {
|
||||
return new Error('# is only allowed in ' + message + ' in the last position')
|
||||
}
|
||||
break
|
||||
}
|
||||
case 43: { // +
|
||||
const pastChar = i < end - 1 && topic.charCodeAt(i + 1) !== 47
|
||||
const preChar = i > 1 && topic.charCodeAt(i - 1) !== 47
|
||||
if (pastChar || preChar) {
|
||||
return new Error('+ is only allowed in ' + message + ' between /')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function through (transform) {
|
||||
return new Transform({
|
||||
objectMode: true,
|
||||
transform
|
||||
})
|
||||
}
|
||||
|
||||
function bulk (fn) {
|
||||
return new Writable({
|
||||
objectMode: true,
|
||||
writev: function (chunks, cb) {
|
||||
fn(chunks.map(chunk => chunk.chunk), cb)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateTopic,
|
||||
through,
|
||||
bulk,
|
||||
$SYS_PREFIX: '$SYS/'
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
'use strict'
|
||||
|
||||
const mqtt = require('mqtt-packet')
|
||||
|
||||
function write (client, packet, done) {
|
||||
let error = null
|
||||
if (client.connecting || client.connected) {
|
||||
try {
|
||||
const result = mqtt.writeToStream(packet, client.conn)
|
||||
if (!result && !client.errored) {
|
||||
client.conn.once('drain', done)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
error = new Error('packet received not valid')
|
||||
}
|
||||
} else {
|
||||
error = new Error('connection closed')
|
||||
}
|
||||
|
||||
setImmediate(done, error, client)
|
||||
}
|
||||
|
||||
module.exports = write
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"name": "aedes",
|
||||
"version": "0.51.3",
|
||||
"description": "Stream-based MQTT broker",
|
||||
"main": "aedes.js",
|
||||
"types": "aedes.d.ts",
|
||||
"scripts": {
|
||||
"lint": "npm run lint:standard && npm run lint:typescript && npm run lint:markdown",
|
||||
"lint:fix": "standard --fix && eslint -c types/.eslintrc.json --fix aedes.d.ts types/**/*.ts test/types/**/*.test-d.ts",
|
||||
"lint:standard": "standard --verbose | snazzy",
|
||||
"lint:typescript": "eslint -c types/.eslintrc.json aedes.d.ts types/**/*.ts test/types/**/*.test-d.ts",
|
||||
"lint:markdown": "markdownlint docs/*.md README.md",
|
||||
"test": "npm run lint && npm run unit && npm run test:typescript",
|
||||
"test:ci": "npm run lint && npm run unit -- --cov --no-check-coverage --coverage-report=lcovonly && npm run test:typescript",
|
||||
"test:report": "npm run lint && npm run unit:report && npm run test:typescript",
|
||||
"test:typescript": "tsd",
|
||||
"unit": "tap -J test/*.js",
|
||||
"unit:report": "tap -J test/*.js --cov --coverage-report=html --coverage-report=cobertura | tee out.tap",
|
||||
"license-checker": "license-checker --production --onlyAllow=\"MIT;ISC;BSD-3-Clause;BSD-2-Clause;0BSD\"",
|
||||
"release": "read -p 'GITHUB_TOKEN: ' GITHUB_TOKEN && export GITHUB_TOKEN=$GITHUB_TOKEN && release-it --disable-metrics"
|
||||
},
|
||||
"release-it": {
|
||||
"github": {
|
||||
"release": true
|
||||
},
|
||||
"git": {
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": [
|
||||
"npm run test:ci"
|
||||
]
|
||||
},
|
||||
"npm": {
|
||||
"publish": true
|
||||
}
|
||||
},
|
||||
"pre-commit": [
|
||||
"test"
|
||||
],
|
||||
"tsd": {
|
||||
"directory": "test/types"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"types/*",
|
||||
"test/types/*"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/moscajs/aedes.git"
|
||||
},
|
||||
"keywords": [
|
||||
"mqtt",
|
||||
"broker",
|
||||
"server",
|
||||
"mqtt-server",
|
||||
"stream",
|
||||
"streams",
|
||||
"publish",
|
||||
"subscribe",
|
||||
"pubsub",
|
||||
"messaging",
|
||||
"mosca",
|
||||
"mosquitto",
|
||||
"iot",
|
||||
"internet",
|
||||
"of",
|
||||
"things"
|
||||
],
|
||||
"author": "Matteo Collina <hello@matteocollina.com>",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Gavin D'mello",
|
||||
"url": "https://github.com/GavinDmello"
|
||||
},
|
||||
{
|
||||
"name": "Behrad Zari",
|
||||
"url": "https://github.com/behrad"
|
||||
},
|
||||
{
|
||||
"name": "Gnought",
|
||||
"url": "https://github.com/gnought"
|
||||
},
|
||||
{
|
||||
"name": "Daniel Lando",
|
||||
"url": "https://github.com/robertsLando"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/aedes"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/moscajs/aedes/issues"
|
||||
},
|
||||
"homepage": "https://github.com/moscajs/aedes#readme",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sinonjs/fake-timers": "^11.2.2",
|
||||
"@types/node": "^20.11.17",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||
"@typescript-eslint/parser": "^7.0.1",
|
||||
"concat-stream": "^2.0.0",
|
||||
"duplexify": "^4.1.2",
|
||||
"license-checker": "^25.0.1",
|
||||
"markdownlint-cli": "^0.41.0",
|
||||
"mqtt": "^5.3.5",
|
||||
"mqtt-connection": "^4.1.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"proxyquire": "^2.1.3",
|
||||
"release-it": "^17.0.5",
|
||||
"snazzy": "^9.0.0",
|
||||
"standard": "^17.1.0",
|
||||
"tap": "^16.3.10",
|
||||
"tsd": "^0.31.0",
|
||||
"typescript": "^5.3.3",
|
||||
"websocket-stream": "^5.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"aedes-packet": "^3.0.0",
|
||||
"aedes-persistence": "^9.1.2",
|
||||
"end-of-stream": "^1.4.4",
|
||||
"fastfall": "^1.5.1",
|
||||
"fastparallel": "^2.4.1",
|
||||
"fastseries": "^2.0.0",
|
||||
"hyperid": "^3.2.0",
|
||||
"mqemitter": "^6.0.0",
|
||||
"mqtt-packet": "^9.0.0",
|
||||
"retimer": "^4.0.0",
|
||||
"reusify": "^1.0.4",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
+1177
File diff suppressed because it is too large
Load Diff
+865
@@ -0,0 +1,865 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const eos = require('end-of-stream')
|
||||
const { setup, connect, subscribe, subscribeMultiple, noError } = require('./helper')
|
||||
const aedes = require('../')
|
||||
const proxyquire = require('proxyquire')
|
||||
|
||||
test('test aedes.createBroker', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes.createBroker()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
connect(setup(broker), {}, function () {
|
||||
t.pass('connected')
|
||||
})
|
||||
})
|
||||
|
||||
test('publish QoS 0', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const s = connect(setup(), { clientId: 'my-client-xyz-5' })
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
retain: false,
|
||||
dup: false,
|
||||
clientId: 'my-client-xyz-5'
|
||||
}
|
||||
|
||||
s.broker.mq.on('hello', function (packet, cb) {
|
||||
expected.brokerId = s.broker.id
|
||||
expected.brokerCounter = s.broker.counter
|
||||
t.equal(packet.messageId, undefined, 'MUST not contain a packet identifier in QoS 0')
|
||||
t.same(packet, expected, 'packet matches')
|
||||
cb()
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
})
|
||||
})
|
||||
|
||||
test('messageId shoud reset to 1 if it reached 65535', function (t) {
|
||||
t.plan(7)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const publishPacket = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
}
|
||||
let count = 0
|
||||
s.broker.on('clientReady', function (client) {
|
||||
subscribe(t, s, 'hello', 1, function () {
|
||||
client._nextId = 65535
|
||||
s.outStream.on('data', function (packet) {
|
||||
if (packet.cmd === 'puback') {
|
||||
t.equal(packet.messageId, 42)
|
||||
}
|
||||
if (packet.cmd === 'publish') {
|
||||
t.equal(packet.messageId, count++ === 0 ? 65535 : 1)
|
||||
}
|
||||
})
|
||||
s.inStream.write(publishPacket)
|
||||
s.inStream.write(publishPacket)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('publish empty topic throws error', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: '',
|
||||
payload: 'world'
|
||||
})
|
||||
|
||||
s.broker.on('clientError', function (client, err) {
|
||||
t.pass('should emit error')
|
||||
})
|
||||
})
|
||||
|
||||
test('publish to $SYS topic throws error', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: '$SYS/not/allowed',
|
||||
payload: 'world'
|
||||
})
|
||||
|
||||
s.broker.on('clientError', function (client, err) {
|
||||
t.pass('should emit error')
|
||||
})
|
||||
})
|
||||
|
||||
;[{ qos: 0, clean: false }, { qos: 0, clean: true }, { qos: 1, clean: false }, { qos: 1, clean: true }].forEach(function (ele) {
|
||||
test('subscribe a single topic in QoS ' + ele.qos + ' [clean=' + ele.clean + ']', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const s = connect(setup(), { clean: ele.clean })
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
dup: false,
|
||||
length: 12,
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
const expectedSubs = ele.clean ? null : [{ topic: 'hello', qos: ele.qos, rh: undefined, rap: undefined, nl: undefined }]
|
||||
|
||||
subscribe(t, s, 'hello', ele.qos, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet matches')
|
||||
})
|
||||
|
||||
s.broker.persistence.subscriptionsByClient(s.client, function (_, subs) {
|
||||
t.same(subs, expectedSubs)
|
||||
})
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Catch invalid packet writeToStream errors
|
||||
test('return write errors to callback', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const write = proxyquire('../lib/write.js', {
|
||||
'mqtt-packet': {
|
||||
writeToStream: () => {
|
||||
throw Error('error')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const client = {
|
||||
conn: {
|
||||
writable: true
|
||||
},
|
||||
connecting: true
|
||||
}
|
||||
|
||||
write(client, {}, function (err) {
|
||||
t.equal(err.message, 'packet received not valid', 'should return the error to callback')
|
||||
})
|
||||
})
|
||||
|
||||
;[{ qos: 0, clean: false }, { qos: 0, clean: true }, { qos: 1, clean: false }, { qos: 1, clean: true }].forEach(function (ele) {
|
||||
test('subscribe multipe topics in QoS ' + ele.qos + ' [clean=' + ele.clean + ']', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const s = connect(setup(), { clean: ele.clean })
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
dup: false,
|
||||
length: 12,
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
const subs = [
|
||||
{ topic: 'hello', qos: ele.qos, rh: undefined, rap: undefined, nl: undefined },
|
||||
{ topic: 'world', qos: ele.qos, rh: undefined, rap: undefined, nl: undefined }
|
||||
]
|
||||
const expectedSubs = ele.clean ? null : subs
|
||||
|
||||
subscribeMultiple(t, s, subs, [ele.qos, ele.qos], function () {
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected, 'packet matches')
|
||||
})
|
||||
|
||||
s.broker.persistence.subscriptionsByClient(s.client, function (_, saveSubs) {
|
||||
t.same(saveSubs, expectedSubs)
|
||||
})
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('does not die badly on connection error', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'subscribe',
|
||||
messageId: 42,
|
||||
subscriptions: [{
|
||||
topic: 'hello',
|
||||
qos: 0
|
||||
}]
|
||||
})
|
||||
|
||||
s.broker.on('clientError', function (client, err) {
|
||||
t.ok(client, 'client is passed')
|
||||
t.ok(err, 'err is passed')
|
||||
})
|
||||
|
||||
s.outStream.on('data', function (packet) {
|
||||
s.conn.destroy()
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world')
|
||||
}, function () {
|
||||
t.pass('calls the callback')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Guarded in mqtt-packet
|
||||
test('subscribe should have messageId', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'subscribe',
|
||||
subscriptions: [{
|
||||
topic: 'hello',
|
||||
qos: 0
|
||||
}]
|
||||
})
|
||||
s.broker.on('connectionError', function (client, err) {
|
||||
t.ok(err.message, 'Invalid messageId')
|
||||
})
|
||||
})
|
||||
|
||||
test('subscribe with messageId 0 should return suback', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'subscribe',
|
||||
subscriptions: [{
|
||||
topic: 'hello',
|
||||
qos: 0
|
||||
}],
|
||||
messageId: 0
|
||||
})
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'suback',
|
||||
messageId: 0,
|
||||
dup: false,
|
||||
length: 3,
|
||||
qos: 0,
|
||||
retain: false,
|
||||
granted: [
|
||||
0
|
||||
]
|
||||
}, 'packet matches')
|
||||
})
|
||||
})
|
||||
|
||||
test('unsubscribe', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const s = noError(connect(setup()), t)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
s.inStream.write({
|
||||
cmd: 'unsubscribe',
|
||||
messageId: 43,
|
||||
unsubscriptions: ['hello']
|
||||
})
|
||||
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'unsuback',
|
||||
messageId: 43,
|
||||
dup: false,
|
||||
length: 2,
|
||||
qos: 0,
|
||||
retain: false
|
||||
}, 'packet matches')
|
||||
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.fail('packet received')
|
||||
})
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
}, function () {
|
||||
t.pass('publish finished')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('unsubscribe without subscribe', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = noError(connect(setup()), t)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'unsubscribe',
|
||||
messageId: 43,
|
||||
unsubscriptions: ['hello']
|
||||
})
|
||||
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'unsuback',
|
||||
messageId: 43,
|
||||
dup: false,
|
||||
length: 2,
|
||||
qos: 0,
|
||||
retain: false
|
||||
}, 'packet matches')
|
||||
})
|
||||
})
|
||||
|
||||
test('unsubscribe on disconnect for a clean=true client', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
const opts = { clean: true }
|
||||
const s = connect(setup(), opts)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
s.conn.destroy(null, function () {
|
||||
t.pass('closed streams')
|
||||
})
|
||||
s.outStream.on('data', function () {
|
||||
t.fail('should not receive any more messages')
|
||||
})
|
||||
s.broker.once('unsubscribe', function () {
|
||||
t.pass('should emit unsubscribe')
|
||||
})
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world')
|
||||
}, function () {
|
||||
t.pass('calls the callback')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('unsubscribe on disconnect for a clean=false client', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const opts = { clean: false }
|
||||
const s = connect(setup(), opts)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
s.conn.destroy(null, function () {
|
||||
t.pass('closed streams')
|
||||
})
|
||||
s.outStream.on('data', function () {
|
||||
t.fail('should not receive any more messages')
|
||||
})
|
||||
s.broker.once('unsubscribe', function () {
|
||||
t.fail('should not emit unsubscribe')
|
||||
})
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world')
|
||||
}, function () {
|
||||
t.pass('calls the callback')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('disconnect', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = noError(connect(setup()), t)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('clientDisconnect', function () {
|
||||
t.pass('closed stream')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'disconnect'
|
||||
})
|
||||
})
|
||||
|
||||
test('disconnect client on wrong cmd', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = noError(connect(setup()), t)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('clientDisconnect', function () {
|
||||
t.pass('closed stream')
|
||||
})
|
||||
|
||||
s.broker.on('clientReady', function (c) {
|
||||
// don't use stream write here because it will throw an error on mqtt_packet genetete
|
||||
c._parser.emit('packet', { cmd: 'pippo' })
|
||||
})
|
||||
})
|
||||
|
||||
test('client closes', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
const client = noError(connect(setup(broker), { clientId: 'abcde' }))
|
||||
broker.on('clientReady', function () {
|
||||
const brokerClient = broker.clients.abcde
|
||||
t.equal(brokerClient.connected, true, 'client connected')
|
||||
eos(client.conn, t.pass.bind(t, 'client closes'))
|
||||
setImmediate(() => {
|
||||
brokerClient.close(function () {
|
||||
t.equal(broker.clients.abcde, undefined, 'client instance is removed')
|
||||
})
|
||||
t.equal(brokerClient.connected, false, 'client disconnected')
|
||||
broker.close(function (err) {
|
||||
t.error(err, 'no error')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('broker closes', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
const client = noError(connect(setup(broker), {
|
||||
clientId: 'abcde'
|
||||
}, function () {
|
||||
eos(client.conn, t.pass.bind(t, 'client closes'))
|
||||
broker.close(function (err) {
|
||||
t.error(err, 'no error')
|
||||
t.ok(broker.closed)
|
||||
t.equal(broker.clients.abcde, undefined, 'client instance is removed')
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
test('broker closes gracefully', function (t) {
|
||||
t.plan(7)
|
||||
|
||||
const broker = aedes()
|
||||
const client1 = noError(connect(setup(broker), {
|
||||
}, function () {
|
||||
const client2 = noError(connect(setup(broker), {
|
||||
}, function () {
|
||||
t.equal(broker.connectedClients, 2, '2 connected clients')
|
||||
eos(client1.conn, t.pass.bind(t, 'client1 closes'))
|
||||
eos(client2.conn, t.pass.bind(t, 'client2 closes'))
|
||||
broker.close(function (err) {
|
||||
t.error(err, 'no error')
|
||||
t.ok(broker.mq.closed, 'broker mq closes')
|
||||
t.ok(broker.closed, 'broker closes')
|
||||
t.equal(broker.connectedClients, 0, 'no connected clients')
|
||||
})
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
test('testing other event', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const client = setup(broker)
|
||||
|
||||
broker.on('connectionError', function (client, error) {
|
||||
t.notOk(client.id, null)
|
||||
})
|
||||
client.conn.emit('error', 'Connect not yet arrived')
|
||||
})
|
||||
|
||||
test('connect without a clientId for MQTT 3.1.1', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = setup()
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
keepalive: 0
|
||||
})
|
||||
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'connack',
|
||||
returnCode: 0,
|
||||
length: 2,
|
||||
qos: 0,
|
||||
retain: false,
|
||||
dup: false,
|
||||
topic: null,
|
||||
payload: null,
|
||||
sessionPresent: false
|
||||
}, 'successful connack')
|
||||
})
|
||||
})
|
||||
|
||||
test('disconnect existing client with the same clientId', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const c1 = connect(setup(broker), {
|
||||
clientId: 'abcde'
|
||||
}, function () {
|
||||
eos(c1.conn, function () {
|
||||
t.pass('first client disconnected')
|
||||
})
|
||||
|
||||
connect(setup(broker), {
|
||||
clientId: 'abcde'
|
||||
}, function () {
|
||||
t.pass('second client connected')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('disconnect if another broker connects the same clientId', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const c1 = connect(setup(broker), {
|
||||
clientId: 'abcde'
|
||||
}, function () {
|
||||
eos(c1.conn, function () {
|
||||
t.pass('disconnect first client')
|
||||
})
|
||||
|
||||
broker.publish({
|
||||
topic: '$SYS/anotherBroker/new/clients',
|
||||
payload: Buffer.from('abcde')
|
||||
}, function () {
|
||||
t.pass('second client connects to another broker')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('publish to $SYS/broker/new/clients', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.mq.on('$SYS/' + broker.id + '/new/clients', function (packet, done) {
|
||||
t.equal(packet.payload.toString(), 'abcde', 'clientId matches')
|
||||
done()
|
||||
})
|
||||
|
||||
connect(setup(broker), {
|
||||
clientId: 'abcde'
|
||||
})
|
||||
})
|
||||
|
||||
test('publish to $SYS/broker/new/subsribers and $SYS/broker/new/unsubsribers', function (t) {
|
||||
t.plan(7)
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const sub = {
|
||||
topic: 'hello',
|
||||
qos: 0
|
||||
}
|
||||
|
||||
broker.mq.on('$SYS/' + broker.id + '/new/subscribes', function (packet, done) {
|
||||
const payload = JSON.parse(packet.payload.toString())
|
||||
t.equal(payload.clientId, 'abcde', 'clientId matches')
|
||||
t.same(payload.subs, [sub], 'subscriptions matches')
|
||||
done()
|
||||
})
|
||||
|
||||
broker.mq.on('$SYS/' + broker.id + '/new/unsubscribes', function (packet, done) {
|
||||
const payload = JSON.parse(packet.payload.toString())
|
||||
t.equal(payload.clientId, 'abcde', 'clientId matches')
|
||||
t.same(payload.subs, [sub.topic], 'unsubscriptions matches')
|
||||
done()
|
||||
})
|
||||
|
||||
const subscriber = connect(setup(broker), {
|
||||
clean: false, clientId: 'abcde'
|
||||
}, function () {
|
||||
subscribe(t, subscriber, sub.topic, sub.qos, function () {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'unsubscribe',
|
||||
messageId: 43,
|
||||
unsubscriptions: ['hello']
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('restore QoS 0 subscriptions not clean', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 12,
|
||||
retain: false
|
||||
}
|
||||
|
||||
let subscriber = connect(setup(broker), {
|
||||
clean: false, clientId: 'abcde'
|
||||
}, function () {
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker), {
|
||||
}, function () {
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
|
||||
t.equal(connect.sessionPresent, true, 'session present is set to true')
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 0
|
||||
})
|
||||
})
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('do not restore QoS 0 subscriptions when clean', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), {
|
||||
clean: true, clientId: 'abcde'
|
||||
}, function () {
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.inStream.end()
|
||||
subscriber.broker.persistence.subscriptionsByClient(broker.clients.abcde, function (_, subs, client) {
|
||||
t.equal(subs, null, 'no previous subscriptions restored')
|
||||
})
|
||||
const publisher = connect(setup(broker), {
|
||||
}, function () {
|
||||
subscriber = connect(setup(broker), {
|
||||
clean: true, clientId: 'abcde'
|
||||
}, function (connect) {
|
||||
t.equal(connect.sessionPresent, false, 'session present is set to false')
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 0
|
||||
})
|
||||
})
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('packet received')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('double sub does not double deliver', function (t) {
|
||||
t.plan(7)
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
dup: false,
|
||||
length: 12,
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
const s = connect(setup(), {
|
||||
}, function () {
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet matches')
|
||||
s.outStream.on('data', function () {
|
||||
t.fail('double deliver')
|
||||
})
|
||||
})
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
})
|
||||
|
||||
test('overlapping sub does not double deliver', function (t) {
|
||||
t.plan(7)
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
dup: false,
|
||||
length: 12,
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
const s = connect(setup(), {
|
||||
}, function () {
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
subscribe(t, s, 'hello/#', 0, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet matches')
|
||||
s.outStream.on('data', function () {
|
||||
t.fail('double deliver')
|
||||
})
|
||||
})
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
})
|
||||
|
||||
test('clear drain', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const s = connect(setup(), {
|
||||
}, function () {
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
// fake a busy socket
|
||||
s.conn.write = function (chunk, enc, cb) {
|
||||
return false
|
||||
}
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
}, function () {
|
||||
t.pass('callback called')
|
||||
})
|
||||
|
||||
s.conn.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
})
|
||||
|
||||
test('id option', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker1 = aedes()
|
||||
|
||||
setup(broker1).conn.destroy()
|
||||
t.ok(broker1.id, 'broker gets random id when id option not set')
|
||||
|
||||
const broker2 = aedes({ id: 'abc' })
|
||||
setup(broker2).conn.destroy()
|
||||
t.equal(broker2.id, 'abc', 'broker id equals id option when set')
|
||||
|
||||
t.teardown(() => {
|
||||
broker1.close()
|
||||
broker2.close()
|
||||
})
|
||||
})
|
||||
|
||||
test('not duplicate client close when client error occurs', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
connect(setup(broker))
|
||||
broker.on('client', function (client) {
|
||||
client.conn.on('drain', () => {
|
||||
t.pass('client closed ok')
|
||||
})
|
||||
client.close()
|
||||
// add back to test if there is duplicated close() call
|
||||
client.conn.on('drain', () => {
|
||||
t.fail('double client close calls')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('not duplicate client close when double close() called', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
connect(setup(broker))
|
||||
broker.on('clientReady', function (client) {
|
||||
client.conn.on('drain', () => {
|
||||
t.pass('client closed ok')
|
||||
})
|
||||
client.close()
|
||||
// add back to test if there is duplicated close() call
|
||||
client.conn.on('drain', () => {
|
||||
t.fail('double execute client close function')
|
||||
})
|
||||
client.close()
|
||||
})
|
||||
})
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const { setup, connect, subscribe } = require('./helper')
|
||||
|
||||
for (const qos of [0, 1, 2]) {
|
||||
const packet = {
|
||||
qos,
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
}
|
||||
|
||||
if (qos > 0) packet.messageId = 42
|
||||
|
||||
test('normal client sends a publish message and shall receive it back, qos = ' + qos, function (t) {
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const handle = setTimeout(() => {
|
||||
t.fail('did not receive packet back')
|
||||
t.end()
|
||||
}, 1000)
|
||||
|
||||
subscribe(t, s, 'hello', qos, function () {
|
||||
s.outStream.on('data', (packet) => {
|
||||
if (packet.cmd === 'publish') {
|
||||
clearTimeout(handle)
|
||||
t.end()
|
||||
} else if (packet.cmd === 'pubrec') {
|
||||
s.inStream.write({ cmd: 'pubrel', messageId: 42 })
|
||||
}
|
||||
})
|
||||
|
||||
s.inStream.write(packet)
|
||||
})
|
||||
})
|
||||
|
||||
test('bridge client sends a publish message but shall not receive it back, qos = ' + qos, function (t) {
|
||||
// protocolVersion 128 + 4 means mqtt 3.1.1 with bridgeMode enabled
|
||||
// https://github.com/mqttjs/mqtt-packet/blob/7f7c2ed8bcb4b2c582851d120a94e0b4a731f661/parser.js#L171
|
||||
const s = connect(setup(), { clientId: 'my-client-bridge-1', protocolVersion: 128 + 4 })
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const handle = setTimeout(() => t.end(), 1000)
|
||||
|
||||
subscribe(t, s, 'hello', qos, function () {
|
||||
s.outStream.on('data', function () {
|
||||
clearTimeout(handle)
|
||||
t.fail('should not receive packet back')
|
||||
t.end()
|
||||
})
|
||||
|
||||
s.inStream.write(packet)
|
||||
})
|
||||
})
|
||||
}
|
||||
+1027
File diff suppressed because it is too large
Load Diff
+177
@@ -0,0 +1,177 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const EventEmitter = require('events')
|
||||
const { setup, connect, subscribe } = require('./helper')
|
||||
const aedes = require('../')
|
||||
|
||||
test('aedes is closed before client authenticate returns', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const evt = new EventEmitter()
|
||||
const broker = aedes({
|
||||
authenticate: (client, username, password, done) => {
|
||||
evt.emit('AuthenticateBegin', client)
|
||||
setTimeout(function () {
|
||||
done(null, true)
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
|
||||
broker.on('client', function (client) {
|
||||
t.fail('should no client registration')
|
||||
})
|
||||
broker.on('connackSent', function () {
|
||||
t.fail('should no connack be sent')
|
||||
})
|
||||
broker.on('clientError', function (client, err) {
|
||||
t.error(err)
|
||||
})
|
||||
|
||||
connect(setup(broker))
|
||||
|
||||
evt.on('AuthenticateBegin', function (client) {
|
||||
t.equal(broker.connectedClients, 0)
|
||||
broker.close()
|
||||
})
|
||||
})
|
||||
|
||||
test('client is closed before authenticate returns', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const evt = new EventEmitter()
|
||||
const broker = aedes({
|
||||
authenticate: async (client, username, password, done) => {
|
||||
evt.emit('AuthenticateBegin', client)
|
||||
setTimeout(function () {
|
||||
done(null, true)
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.on('client', function (client) {
|
||||
t.fail('should no client registration')
|
||||
})
|
||||
broker.on('connackSent', function () {
|
||||
t.fail('should no connack be sent')
|
||||
})
|
||||
broker.on('clientError', function (client, err) {
|
||||
t.error(err)
|
||||
})
|
||||
|
||||
connect(setup(broker))
|
||||
|
||||
evt.on('AuthenticateBegin', function (client) {
|
||||
t.equal(broker.connectedClients, 0)
|
||||
client.close()
|
||||
})
|
||||
})
|
||||
|
||||
test('client is closed before authorizePublish returns', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const evt = new EventEmitter()
|
||||
const broker = aedes({
|
||||
authorizePublish: (client, packet, done) => {
|
||||
evt.emit('AuthorizePublishBegin', client)
|
||||
// simulate latency writing to persistent store.
|
||||
setTimeout(function () {
|
||||
done()
|
||||
evt.emit('AuthorizePublishEnd', client)
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
|
||||
broker.on('clientError', function (client, err) {
|
||||
t.equal(err.message, 'connection closed')
|
||||
})
|
||||
|
||||
const s = connect(setup(broker))
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 10,
|
||||
retain: false
|
||||
})
|
||||
|
||||
evt.on('AuthorizePublishBegin', function (client) {
|
||||
t.equal(broker.connectedClients, 1)
|
||||
client.close()
|
||||
})
|
||||
evt.on('AuthorizePublishEnd', function (client) {
|
||||
t.equal(broker.connectedClients, 0)
|
||||
broker.close()
|
||||
})
|
||||
})
|
||||
|
||||
test('close client when its socket is closed', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const subscriber = connect(setup(broker))
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.end()
|
||||
subscriber.conn.on('close', function () {
|
||||
t.equal(broker.connectedClients, 0, 'no connected client')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('multiple clients subscribe same topic, and all clients still receive message except the closed one', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const mqtt = require('mqtt')
|
||||
const broker = aedes()
|
||||
|
||||
let client2
|
||||
|
||||
t.teardown(() => {
|
||||
client2.end()
|
||||
broker.close()
|
||||
server.close()
|
||||
})
|
||||
|
||||
const server = require('net').createServer(broker.handle)
|
||||
const port = 1883
|
||||
server.listen(port)
|
||||
broker.on('clientError', function (client, err) {
|
||||
t.error(err)
|
||||
})
|
||||
|
||||
const _sameTopic = 'hello'
|
||||
|
||||
// client 1
|
||||
const client1 = mqtt.connect('mqtt://localhost', { clientId: 'client1', resubscribe: false, reconnectPeriod: -1 })
|
||||
client1.on('message', () => {
|
||||
t.fail('client1 receives message')
|
||||
})
|
||||
|
||||
client1.subscribe(_sameTopic, { qos: 0, retain: false }, () => {
|
||||
t.pass('client1 sub callback')
|
||||
// stimulate closed socket by users
|
||||
client1.stream.destroy()
|
||||
|
||||
// client 2
|
||||
client2 = mqtt.connect('mqtt://localhost', { clientId: 'client2', resubscribe: false })
|
||||
client2.on('message', () => {
|
||||
t.pass('client2 receives message')
|
||||
t.equal(broker.connectedClients, 1)
|
||||
})
|
||||
client2.subscribe(_sameTopic, { qos: 0, retain: false }, () => {
|
||||
t.pass('client2 sub callback')
|
||||
|
||||
// pubClient
|
||||
const pubClient = mqtt.connect('mqtt://localhost', { clientId: 'pubClient' })
|
||||
pubClient.publish(_sameTopic, 'world', { qos: 0, retain: false }, () => {
|
||||
t.pass('pubClient publish event')
|
||||
pubClient.end()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+762
@@ -0,0 +1,762 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const http = require('http')
|
||||
const ws = require('websocket-stream')
|
||||
const mqtt = require('mqtt')
|
||||
const { setup, connect, delay } = require('./helper')
|
||||
const aedes = require('../')
|
||||
|
||||
;[{ ver: 3, id: 'MQIsdp' }, { ver: 4, id: 'MQTT' }].forEach(function (ele) {
|
||||
test('connect and connack (minimal)', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const s = setup()
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: ele.id,
|
||||
protocolVersion: ele.ver,
|
||||
clean: true,
|
||||
clientId: 'my-client',
|
||||
keepalive: 0
|
||||
})
|
||||
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'connack',
|
||||
returnCode: 0,
|
||||
length: 2,
|
||||
qos: 0,
|
||||
retain: false,
|
||||
dup: false,
|
||||
topic: null,
|
||||
payload: null,
|
||||
sessionPresent: false
|
||||
}, 'successful connack')
|
||||
t.equal(s.client.version, ele.ver)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.1.2-2]
|
||||
test('reject client requested for unacceptable protocol version', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQIsdp',
|
||||
protocolVersion: 5,
|
||||
clean: true,
|
||||
clientId: 'my-client',
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'connack')
|
||||
t.equal(packet.returnCode, 1, 'unacceptable protocol version')
|
||||
t.equal(broker.connectedClients, 0)
|
||||
})
|
||||
broker.on('clientError', function (client, err) {
|
||||
t.fail('should not raise clientError error')
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(err.message, 'unacceptable protocol version')
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.1.2-1], Guarded in mqtt-packet
|
||||
test('reject client requested for unsupported protocol version', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 2,
|
||||
clean: true,
|
||||
clientId: 'my-client',
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.fail('no data sent')
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(client.version, null)
|
||||
t.equal(err.message, 'Invalid protocol version')
|
||||
t.equal(broker.connectedClients, 0)
|
||||
})
|
||||
})
|
||||
|
||||
test('reject clients that exceed the keepalive limit', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes({
|
||||
keepaliveLimit: 100
|
||||
})
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
keepalive: 150
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
console.log(packet)
|
||||
t.same(packet, {
|
||||
cmd: 'connack',
|
||||
returnCode: 6,
|
||||
length: 2,
|
||||
qos: 0,
|
||||
retain: false,
|
||||
dup: false,
|
||||
topic: null,
|
||||
payload: null,
|
||||
sessionPresent: false
|
||||
}, 'unsuccessful connack, keep alive limit exceeded')
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(err.message, 'keep alive limit exceeded')
|
||||
t.equal(broker.connectedClients, 0)
|
||||
})
|
||||
})
|
||||
|
||||
// Guarded in mqtt-packet
|
||||
test('reject clients with no clientId running on MQTT 3.1.0', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQIsdp',
|
||||
protocolVersion: 3,
|
||||
clean: true,
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.fail('no data sent')
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(client.version, null)
|
||||
t.equal(err.message, 'clientId must be supplied before 3.1.1')
|
||||
t.equal(broker.connectedClients, 0)
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.1.3-7], Guarded in mqtt-packet
|
||||
test('reject clients without clientid and clean=false on MQTT 3.1.1', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: false,
|
||||
clientId: '',
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.fail('no data sent')
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(err.message, 'clientId must be given if cleanSession set to 0')
|
||||
t.equal(broker.connectedClients, 0)
|
||||
})
|
||||
})
|
||||
|
||||
test('clients without clientid and clean=true on MQTT 3.1.1 will get a generated clientId', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'connack')
|
||||
t.equal(packet.returnCode, 0)
|
||||
t.equal(broker.connectedClients, 1)
|
||||
t.equal(s.client.version, 4)
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.error(err, 'no error')
|
||||
})
|
||||
broker.on('client', function (client) {
|
||||
t.ok(client.id.startsWith('aedes_'))
|
||||
})
|
||||
})
|
||||
|
||||
test('client connect error while fetching subscriptions', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
broker.persistence.subscriptionsByClient = function (c, cb) {
|
||||
cb(new Error('error'), [], c)
|
||||
}
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: false,
|
||||
clientId: 'my-client',
|
||||
keepalive: 0
|
||||
})
|
||||
|
||||
broker.on('clientError', function (client, err) {
|
||||
t.equal(client.version, 4)
|
||||
t.pass('throws error')
|
||||
})
|
||||
})
|
||||
|
||||
test('client connect clear outgoing', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const clientId = 'abcde'
|
||||
const brokerId = 'pippo'
|
||||
|
||||
const broker = aedes({ id: brokerId })
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const subs = [{ clientId }]
|
||||
const packet = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
brokerId,
|
||||
brokerCounter: 2,
|
||||
retain: true,
|
||||
messageId: 42,
|
||||
dup: false
|
||||
}
|
||||
|
||||
broker.persistence.outgoingEnqueueCombi(subs, packet, function () {
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId,
|
||||
keepalive: 0
|
||||
})
|
||||
|
||||
broker.on('clientReady', function (client) {
|
||||
broker.persistence.outgoingUpdate(client, packet, function (err) {
|
||||
t.equal('no such packet', err.message, 'packet not found')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('clients with zero-byte clientid and clean=true on MQTT 3.1.1 will get a generated clientId', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId: '',
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'connack')
|
||||
t.equal(packet.returnCode, 0)
|
||||
t.equal(broker.connectedClients, 1)
|
||||
t.equal(s.client.version, 4)
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.error(err, 'no error')
|
||||
})
|
||||
broker.on('client', function (client) {
|
||||
t.ok(client.id.startsWith('aedes_'))
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.1.3-7]
|
||||
test('reject clients with > 23 clientId length in MQTT 3.1.0', function (t) {
|
||||
t.plan(7)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
const conn = s.client.conn
|
||||
const end = conn.end
|
||||
|
||||
conn.end = function () {
|
||||
t.fail('should not call `conn.end()`')
|
||||
end()
|
||||
}
|
||||
|
||||
function drain () {
|
||||
t.pass('should empty connection request queue')
|
||||
}
|
||||
|
||||
conn._writableState.getBuffer = () => [{ callback: drain }, { callback: drain }]
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQIsdp',
|
||||
protocolVersion: 3,
|
||||
clean: true,
|
||||
clientId: 'abcdefghijklmnopqrstuvwxyz',
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'connack')
|
||||
t.equal(packet.returnCode, 2, 'identifier rejected')
|
||||
t.equal(broker.connectedClients, 0)
|
||||
t.equal(s.client.version, null)
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(err.message, 'identifier rejected')
|
||||
})
|
||||
})
|
||||
|
||||
test('connect clients with > 23 clientId length using aedes maxClientsIdLength option in MQTT 3.1.0', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes({ maxClientsIdLength: 26 })
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 3,
|
||||
clean: true,
|
||||
clientId: 'abcdefghijklmnopqrstuvwxyz',
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'connack')
|
||||
t.equal(packet.returnCode, 0)
|
||||
t.equal(broker.connectedClients, 1)
|
||||
t.equal(s.client.version, 3)
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.error(err, 'no error')
|
||||
})
|
||||
})
|
||||
|
||||
test('connect with > 23 clientId length in MQTT 3.1.1', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId: 'abcdefghijklmnopqrstuvwxyz',
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'connack')
|
||||
t.equal(packet.returnCode, 0)
|
||||
t.equal(broker.connectedClients, 1)
|
||||
t.equal(s.client.version, 4)
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.error(err, 'no error')
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.1.0-1]
|
||||
test('the first Packet MUST be a CONNECT Packet', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const packet = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
const s = setup(broker)
|
||||
s.inStream.write(packet)
|
||||
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(err.message, 'Invalid protocol')
|
||||
})
|
||||
setImmediate(() => {
|
||||
t.ok(s.conn.destroyed, 'close connection if first packet is not a CONNECT')
|
||||
s.conn.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.1.0-2]
|
||||
test('second CONNECT Packet sent from a Client as a protocol violation and disconnect the Client', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const packet = {
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId: 'my-client',
|
||||
keepalive: 0
|
||||
}
|
||||
broker.on('clientError', function (client, err) {
|
||||
t.equal(err.message, 'Invalid protocol')
|
||||
})
|
||||
const s = connect(setup(broker), { clientId: 'abcde' })
|
||||
s.broker.on('clientReady', function () {
|
||||
t.ok(broker.clients.abcde.connected)
|
||||
// destory client when there is a 2nd cmd:connect, even the clientId is dfferent
|
||||
s.inStream.write(packet)
|
||||
setImmediate(() => {
|
||||
t.equal(broker.clients.abcde, undefined, 'client instance is removed')
|
||||
t.ok(s.conn.destroyed, 'close connection if packet is a CONNECT after network is established')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('connect handler calls done when preConnect throws error', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes({
|
||||
preConnect: function (client, packet, done) {
|
||||
done(Error('error in preconnect'))
|
||||
}
|
||||
})
|
||||
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
const handleConnect = require('../lib/handlers/connect')
|
||||
|
||||
handleConnect(s.client, {}, function done (err) {
|
||||
t.equal(err.message, 'error in preconnect', 'calls done with error')
|
||||
})
|
||||
})
|
||||
|
||||
test('handler calls done when disconnect or unknown packet cmd is received', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes()
|
||||
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
const handle = require('../lib/handlers/index')
|
||||
|
||||
handle(s.client, { cmd: 'disconnect' }, function done () {
|
||||
t.pass('calls done when disconnect cmd is received')
|
||||
})
|
||||
|
||||
handle(s.client, { cmd: 'fsfadgragae' }, function done () {
|
||||
t.pass('calls done when unknown cmd is received')
|
||||
})
|
||||
})
|
||||
|
||||
test('reject second CONNECT Packet sent while first CONNECT still in preConnect stage', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const packet1 = {
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId: 'my-client-1',
|
||||
keepalive: 0
|
||||
}
|
||||
const packet2 = {
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId: 'my-client-2',
|
||||
keepalive: 0
|
||||
}
|
||||
|
||||
let i = 0
|
||||
const broker = aedes({
|
||||
preConnect: function (client, packet, done) {
|
||||
const ms = i++ === 0 ? 2000 : 500
|
||||
setTimeout(function () {
|
||||
done(null, true)
|
||||
}, ms)
|
||||
}
|
||||
})
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(err.info.clientId, 'my-client-2')
|
||||
t.equal(err.message, 'Invalid protocol')
|
||||
})
|
||||
|
||||
const msg = async (s, ms, msg) => {
|
||||
await delay(ms)
|
||||
s.inStream.write(msg)
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
await Promise.all([msg(s, 100, packet1), msg(s, 200, packet2)])
|
||||
})().catch(
|
||||
(error) => {
|
||||
t.fail(error)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// [MQTT-3.1.2-1], Guarded in mqtt-packet
|
||||
test('reject clients with wrong protocol name', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT_hello',
|
||||
protocolVersion: 3,
|
||||
clean: true,
|
||||
clientId: 'my-client',
|
||||
keepalive: 0
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.fail('no data sent')
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
t.equal(err.message, 'Invalid protocolId')
|
||||
t.equal(broker.connectedClients, 0)
|
||||
})
|
||||
})
|
||||
|
||||
test('After first CONNECT Packet, others are queued until \'connect\' event', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const queueLimit = 50
|
||||
const broker = aedes({ queueLimit })
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publishP = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
|
||||
const connectP = {
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId: 'abcde',
|
||||
keepalive: 0
|
||||
}
|
||||
|
||||
const s = setup(broker)
|
||||
s.inStream.write(connectP)
|
||||
|
||||
process.once('warning', e => t.fail('Memory leak detected'))
|
||||
|
||||
for (let i = 0; i < queueLimit; i++) {
|
||||
s.inStream.write(publishP)
|
||||
}
|
||||
|
||||
broker.on('client', function (client) {
|
||||
t.equal(client._parser._queue.length, queueLimit, 'Packets have been queued')
|
||||
|
||||
client.once('connected', () => {
|
||||
t.equal(client._parser._queue, null, 'Queue is empty')
|
||||
s.conn.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Test queue limit', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const queueLimit = 50
|
||||
const broker = aedes({ queueLimit })
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publishP = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
|
||||
const connectP = {
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId: 'abcde',
|
||||
keepalive: 0
|
||||
}
|
||||
|
||||
const s = setup(broker)
|
||||
s.inStream.write(connectP)
|
||||
|
||||
process.once('warning', e => t.fail('Memory leak detected'))
|
||||
|
||||
for (let i = 0; i < queueLimit + 1; i++) {
|
||||
s.inStream.write(publishP)
|
||||
}
|
||||
|
||||
broker.on('connectionError', function (conn, err) {
|
||||
t.equal(err.message, 'Client queue limit reached', 'Queue error is thrown')
|
||||
s.conn.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
;[['fail with no error msg', 3, null, false], ['succeed with no error msg', 9, null, true], ['fail with error msg', 6, new Error('connection banned'), false], ['succeed with error msg', 6, new Error('connection banned'), true]].forEach(function (ele, idx) {
|
||||
const title = ele[0]
|
||||
const plan = ele[1]
|
||||
const err = ele[2]
|
||||
const ok = ele[3]
|
||||
test('preConnect handler - ' + title, function (t) {
|
||||
t.plan(plan)
|
||||
|
||||
const broker = aedes({
|
||||
preConnect: function (client, packet, done) {
|
||||
t.ok(client.connecting)
|
||||
t.notOk(client.connected)
|
||||
t.equal(client.version, null)
|
||||
return done(err, ok)
|
||||
}
|
||||
})
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = setup(broker)
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId: 'my-client-' + idx,
|
||||
keepalive: 0
|
||||
})
|
||||
broker.on('client', function (client) {
|
||||
if (ok && !err) {
|
||||
t.ok(client.connecting)
|
||||
t.notOk(client.connected)
|
||||
t.pass('register client ok')
|
||||
} else {
|
||||
t.fail('no reach here')
|
||||
}
|
||||
})
|
||||
broker.on('clientReady', function (client) {
|
||||
t.notOk(client.connecting)
|
||||
t.ok(client.connected)
|
||||
t.pass('connect ok')
|
||||
})
|
||||
broker.on('clientError', function (client, err) {
|
||||
t.fail('no client error')
|
||||
})
|
||||
broker.on('connectionError', function (client, err) {
|
||||
if (err) {
|
||||
t.notOk(client.connecting)
|
||||
t.notOk(client.connected)
|
||||
t.equal(err.message, 'connection banned')
|
||||
} else {
|
||||
t.fail('no connection error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// websocket-stream based connections
|
||||
test('websocket clients have access to the request object', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const port = 4883
|
||||
const broker = aedes()
|
||||
broker.on('client', function (client) {
|
||||
if (client.req) {
|
||||
t.pass('client request object present')
|
||||
if (client.req.headers) {
|
||||
t.equal('sample', client.req.headers['x-test-protocol'])
|
||||
}
|
||||
} else {
|
||||
t.fail('no request object present')
|
||||
}
|
||||
})
|
||||
|
||||
const server = http.createServer()
|
||||
ws.createServer({
|
||||
server
|
||||
}, broker.handle)
|
||||
|
||||
server.listen(port, function (err) {
|
||||
t.error(err, 'no error')
|
||||
})
|
||||
|
||||
const client = mqtt.connect(`ws://localhost:${port}`, {
|
||||
wsOptions: {
|
||||
headers: {
|
||||
'X-Test-Protocol': 'sample'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.teardown(() => {
|
||||
client.end(true)
|
||||
broker.close()
|
||||
server.close()
|
||||
})
|
||||
})
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const mqemitter = require('mqemitter')
|
||||
const { setup, connect, subscribe } = require('./helper')
|
||||
const aedes = require('../')
|
||||
|
||||
test('publishes an hearbeat', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes({
|
||||
heartbeatInterval: 10 // ms
|
||||
})
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.subscribe('$SYS/+/heartbeat', function (message, cb) {
|
||||
const id = message.topic.match(/\$SYS\/([^/]+)\/heartbeat/)[1]
|
||||
t.equal(id, broker.id, 'broker id matches')
|
||||
t.same(message.payload.toString(), id, 'message has id as the payload')
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
test('publishes birth', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const mq = mqemitter()
|
||||
const brokerId = 'test-broker'
|
||||
const fakeBroker = 'fake-broker'
|
||||
const clientId = 'test-client'
|
||||
|
||||
mq.on(`$SYS/${brokerId}/birth`, (message, cb) => {
|
||||
t.pass('broker birth received')
|
||||
t.same(message.payload.toString(), brokerId, 'message has id as the payload')
|
||||
cb()
|
||||
})
|
||||
|
||||
const broker = aedes({
|
||||
id: brokerId,
|
||||
mq
|
||||
})
|
||||
|
||||
broker.on('client', (client) => {
|
||||
t.equal(client.id, clientId, 'client connected')
|
||||
// set a fake counter on a fake broker
|
||||
process.nextTick(() => {
|
||||
broker.clients[clientId].duplicates[fakeBroker] = 42
|
||||
mq.emit({ topic: `$SYS/${fakeBroker}/birth`, payload: Buffer.from(fakeBroker) })
|
||||
})
|
||||
})
|
||||
|
||||
mq.on(`$SYS/${fakeBroker}/birth`, (message, cb) => {
|
||||
process.nextTick(() => {
|
||||
t.equal(!!broker.clients[clientId].duplicates[fakeBroker], false, 'client duplicates has been resetted')
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
const s = connect(setup(broker), { clientId })
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
})
|
||||
|
||||
;['$mcollina', '$SYS'].forEach(function (topic) {
|
||||
test('does not forward $ prefixed topics to # subscription - ' + topic, function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, '#', 0, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.fail('no packet should be received')
|
||||
})
|
||||
|
||||
s.broker.mq.emit({
|
||||
cmd: 'publish',
|
||||
topic: topic + '/hello',
|
||||
payload: 'world'
|
||||
}, function () {
|
||||
t.pass('nothing happened')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('does not forward $ prefixed topics to +/# subscription - ' + topic, function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, '+/#', 0, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.fail('no packet should be received')
|
||||
})
|
||||
|
||||
s.broker.mq.emit({
|
||||
cmd: 'publish',
|
||||
topic: topic + '/hello',
|
||||
payload: 'world'
|
||||
}, function () {
|
||||
t.pass('nothing happened')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('does not store $SYS topics to QoS 1 # subscription', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const opts = { clean: false, clientId: 'abcde' }
|
||||
let s = connect(setup(broker), opts)
|
||||
|
||||
subscribe(t, s, '#', 1, function () {
|
||||
s.inStream.end()
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: '$SYS/hello',
|
||||
payload: 'world',
|
||||
qos: 1
|
||||
}, function () {
|
||||
s = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.fail('no packet should be received')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Emit event when receives a ping', { timeout: 2000 }, function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.on('ping', function (packet, client) {
|
||||
if (client && client) {
|
||||
t.equal(client.id, 'abcde')
|
||||
t.equal(packet.cmd, 'pingreq')
|
||||
t.equal(packet.payload, null)
|
||||
t.equal(packet.topic, null)
|
||||
t.equal(packet.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
const s = connect(setup(broker), { clientId: 'abcde' })
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'pingreq'
|
||||
})
|
||||
})
|
||||
|
||||
test('Emit event when broker closed', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
broker.once('closed', function () {
|
||||
t.ok(true)
|
||||
})
|
||||
broker.close()
|
||||
})
|
||||
|
||||
test('Emit closed event one only when double broker.close()', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
broker.on('closed', function () {
|
||||
t.pass('closed')
|
||||
})
|
||||
t.notOk(broker.closed)
|
||||
broker.close()
|
||||
t.ok(broker.closed)
|
||||
broker.close()
|
||||
t.ok(broker.closed)
|
||||
})
|
||||
|
||||
test('Test backpressure aedes published function', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
let publishCount = 10
|
||||
let count = 0
|
||||
|
||||
const broker = aedes({
|
||||
published: function (packet, client, done) {
|
||||
if (client) {
|
||||
count++
|
||||
setTimeout(() => {
|
||||
publisher.end()
|
||||
done()
|
||||
})
|
||||
} else { done() }
|
||||
}
|
||||
})
|
||||
|
||||
const mqtt = require('mqtt')
|
||||
const server = require('net').createServer(broker.handle)
|
||||
let publisher
|
||||
|
||||
server.listen(0, function () {
|
||||
const port = server.address().port
|
||||
publisher = mqtt.connect({ port, host: 'localhost', clean: true, keepalive: 30 })
|
||||
|
||||
function next () {
|
||||
if (--publishCount > 0) { process.nextTick(publish) }
|
||||
}
|
||||
|
||||
function publish () {
|
||||
publisher.publish('test', 'payload', next)
|
||||
}
|
||||
|
||||
publisher.on('connect', publish)
|
||||
publisher.on('end', function () {
|
||||
t.ok(count > publishCount)
|
||||
t.equal(publishCount, 0)
|
||||
broker.close()
|
||||
server.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('clear closed clients when the same clientId is managed by another broker', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const clientId = 'closed-client'
|
||||
const aedesBroker = aedes()
|
||||
|
||||
// simulate a closed client on the broker
|
||||
aedesBroker.clients[clientId] = { closed: true, broker: aedesBroker }
|
||||
aedesBroker.connectedClients = 1
|
||||
|
||||
// simulate the creation of the same client on another broker of the cluster
|
||||
aedesBroker.publish({ topic: '$SYS/anotherbroker/new/clients', payload: clientId }, () => {
|
||||
t.equal(aedesBroker.clients[clientId], undefined) // check that the closed client was removed
|
||||
t.equal(aedesBroker.connectedClients, 0)
|
||||
})
|
||||
|
||||
t.teardown(aedesBroker.close.bind(aedesBroker))
|
||||
})
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
'use strict'
|
||||
|
||||
const duplexify = require('duplexify')
|
||||
const mqtt = require('mqtt-connection')
|
||||
const { through } = require('../lib/utils')
|
||||
const util = require('util')
|
||||
const aedes = require('../')
|
||||
|
||||
const parseStream = mqtt.parseStream
|
||||
const generateStream = mqtt.generateStream
|
||||
let clients = 0
|
||||
|
||||
function setup (broker) {
|
||||
const inStream = generateStream()
|
||||
const outStream = parseStream()
|
||||
const conn = duplexify(outStream, inStream)
|
||||
|
||||
broker = broker || aedes()
|
||||
|
||||
return {
|
||||
client: broker.handle(conn),
|
||||
conn,
|
||||
inStream,
|
||||
outStream,
|
||||
broker
|
||||
}
|
||||
}
|
||||
|
||||
function connect (s, opts, connected) {
|
||||
s = Object.create(s)
|
||||
s.outStream = s.outStream.pipe(through(filter))
|
||||
|
||||
opts = opts || {}
|
||||
|
||||
opts.cmd = 'connect'
|
||||
opts.protocolId = opts.protocolId || 'MQTT'
|
||||
opts.protocolVersion = opts.protocolVersion || 4
|
||||
opts.clean = !!opts.clean
|
||||
opts.clientId = opts.clientId || 'my-client-' + clients++
|
||||
opts.keepalive = opts.keepalive || 0
|
||||
|
||||
s.inStream.write(opts)
|
||||
|
||||
return s
|
||||
|
||||
function filter (packet, enc, cb) {
|
||||
if (packet.cmd !== 'publish') {
|
||||
delete packet.topic
|
||||
delete packet.payload
|
||||
}
|
||||
|
||||
// using setImmediate to wait for connected to be fired
|
||||
// setup also needs to return first
|
||||
if (packet.cmd !== 'connack') {
|
||||
setImmediate(this.push.bind(this, packet))
|
||||
} else if (connected && packet.returnCode === 0) {
|
||||
setImmediate(connected, packet)
|
||||
}
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
function noError (s, t) {
|
||||
s.broker.on('clientError', function (client, err) {
|
||||
if (err) throw err
|
||||
t.notOk(err, 'must not error')
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
function subscribe (t, subscriber, topic, qos, done) {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'subscribe',
|
||||
messageId: 24,
|
||||
subscriptions: [{
|
||||
topic,
|
||||
qos
|
||||
}]
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'suback')
|
||||
t.same(packet.granted, [qos])
|
||||
t.equal(packet.messageId, 24)
|
||||
|
||||
if (done) {
|
||||
done(null, packet)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// subs: [{topic:, qos:}]
|
||||
function subscribeMultiple (t, subscriber, subs, expectedGranted, done) {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'subscribe',
|
||||
messageId: 24,
|
||||
subscriptions: subs
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'suback')
|
||||
t.same(packet.granted, expectedGranted)
|
||||
t.equal(packet.messageId, 24)
|
||||
|
||||
if (done) {
|
||||
done(null, packet)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setup,
|
||||
connect,
|
||||
noError,
|
||||
subscribe,
|
||||
subscribeMultiple,
|
||||
delay: util.promisify(setTimeout)
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const eos = require('end-of-stream')
|
||||
const Faketimers = require('@sinonjs/fake-timers')
|
||||
const { setup, connect, noError } = require('./helper')
|
||||
const aedes = require('../')
|
||||
|
||||
test('supports pingreq/pingresp', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = noError(connect(setup()))
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('keepaliveTimeout', function (client) {
|
||||
t.fail('keep alive should not timeout')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'pingreq'
|
||||
})
|
||||
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'pingresp', 'the response is a pingresp')
|
||||
})
|
||||
})
|
||||
|
||||
test('supports keep alive disconnections', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const clock = Faketimers.install()
|
||||
const s = connect(setup(), { keepalive: 1 })
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('keepaliveTimeout', function (client) {
|
||||
t.pass('keep alive timeout')
|
||||
})
|
||||
eos(s.conn, function () {
|
||||
t.pass('waits 1 and a half the keepalive timeout')
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
clock.uninstall()
|
||||
}, 1.5)
|
||||
clock.tick(1.5)
|
||||
})
|
||||
|
||||
test('supports keep alive disconnections after a pingreq', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const clock = Faketimers.install()
|
||||
const s = connect(setup(), { keepalive: 1 })
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
eos(s.conn, function () {
|
||||
t.pass('waits 1 and a half the keepalive timeout')
|
||||
})
|
||||
s.broker.on('keepaliveTimeout', function (client) {
|
||||
t.pass('keep alive timeout')
|
||||
})
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'pingresp', 'the response is a pingresp')
|
||||
})
|
||||
setTimeout(() => {
|
||||
s.inStream.write({
|
||||
cmd: 'pingreq'
|
||||
})
|
||||
clock.uninstall()
|
||||
}, 1)
|
||||
clock.tick(3)
|
||||
})
|
||||
|
||||
test('disconnect if a connect does not arrive in time', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const clock = Faketimers.install()
|
||||
const s = setup(aedes({
|
||||
connectTimeout: 500
|
||||
}))
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.client.on('error', function (err) {
|
||||
t.equal(err.message, 'connect did not arrive in time')
|
||||
})
|
||||
eos(s.conn, function () {
|
||||
t.pass('waits waitConnectTimeout before ending')
|
||||
})
|
||||
setTimeout(() => {
|
||||
clock.uninstall()
|
||||
}, 1000)
|
||||
clock.tick(1000)
|
||||
})
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const { setup, connect, subscribe, noError } = require('./helper')
|
||||
const aedes = require('../')
|
||||
|
||||
test('count connected clients', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
t.equal(broker.connectedClients, 0, 'no connected clients')
|
||||
|
||||
connect(setup(broker), {
|
||||
}, function () {
|
||||
t.equal(broker.connectedClients, 1, 'one connected clients')
|
||||
|
||||
const last = connect(setup(broker), {
|
||||
}, function () {
|
||||
t.equal(broker.connectedClients, 2, 'two connected clients')
|
||||
|
||||
last.conn.destroy()
|
||||
|
||||
// needed because destroy() will do the trick before
|
||||
// the next tick
|
||||
setImmediate(function () {
|
||||
t.equal(broker.connectedClients, 1, 'one connected clients')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('call published method', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.published = function (packet, client, done) {
|
||||
t.equal(packet.topic, 'hello', 'topic matches')
|
||||
t.equal(packet.payload.toString(), 'world', 'payload matches')
|
||||
t.equal(client, null, 'no client')
|
||||
done()
|
||||
}
|
||||
|
||||
broker.publish({
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world')
|
||||
}, function (err) {
|
||||
t.error(err, 'no error')
|
||||
})
|
||||
})
|
||||
|
||||
test('call published method with client', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.published = function (packet, client, done) {
|
||||
// for internal messages, client will be null
|
||||
if (client) {
|
||||
t.equal(packet.topic, 'hello', 'topic matches')
|
||||
t.equal(packet.payload.toString(), 'world', 'payload matches')
|
||||
t.equal(packet.qos, 1)
|
||||
t.equal(packet.messageId, 42)
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
const s = connect(setup(broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
})
|
||||
|
||||
test('emit publish event with client - QoS 0', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.on('publish', function (packet, client) {
|
||||
// for internal messages, client will be null
|
||||
if (client) {
|
||||
t.equal(packet.qos, 0)
|
||||
t.equal(packet.topic, 'hello', 'topic matches')
|
||||
t.equal(packet.payload.toString(), 'world', 'payload matches')
|
||||
}
|
||||
})
|
||||
|
||||
const s = connect(setup(broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0
|
||||
})
|
||||
})
|
||||
|
||||
test('emit publish event with client - QoS 1', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.on('publish', function (packet, client) {
|
||||
// for internal messages, client will be null
|
||||
if (client) {
|
||||
t.equal(packet.qos, 1)
|
||||
t.equal(packet.messageId, 42)
|
||||
t.equal(packet.topic, 'hello', 'topic matches')
|
||||
t.equal(packet.payload.toString(), 'world', 'payload matches')
|
||||
}
|
||||
})
|
||||
|
||||
const s = connect(setup(broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
})
|
||||
|
||||
test('emit subscribe event', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = connect(setup(broker), { clientId: 'abcde' })
|
||||
|
||||
broker.on('subscribe', function (subscriptions, client) {
|
||||
t.same(subscriptions, [{
|
||||
topic: 'hello',
|
||||
qos: 0
|
||||
}], 'topic matches')
|
||||
t.equal(client.id, 'abcde', 'client matches')
|
||||
})
|
||||
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
t.pass('subscribe completed')
|
||||
})
|
||||
})
|
||||
|
||||
test('emit subscribe event if unrecognized params in subscribe packet structure', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = noError(connect(setup(broker)))
|
||||
const subs = [{ topic: 'hello', qos: 0 }]
|
||||
|
||||
broker.on('subscribe', function (subscriptions, client) {
|
||||
t.equal(subscriptions, subs)
|
||||
t.same(client, s.client)
|
||||
})
|
||||
|
||||
s.client.subscribe({
|
||||
subscriptions: subs,
|
||||
restore: true
|
||||
}, function (err) {
|
||||
t.error(err)
|
||||
})
|
||||
})
|
||||
|
||||
test('emit unsubscribe event', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = connect(setup(broker), { clean: true, clientId: 'abcde' })
|
||||
|
||||
broker.on('unsubscribe', function (unsubscriptions, client) {
|
||||
t.same(unsubscriptions, [
|
||||
'hello'
|
||||
], 'unsubscription matches')
|
||||
t.equal(client.id, 'abcde', 'client matches')
|
||||
})
|
||||
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
s.inStream.write({
|
||||
cmd: 'unsubscribe',
|
||||
messageId: 43,
|
||||
unsubscriptions: ['hello']
|
||||
})
|
||||
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.pass('subscribe completed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('emit unsubscribe event if unrecognized params in unsubscribe packet structure', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = noError(connect(setup(broker)))
|
||||
const unsubs = [{ topic: 'hello', qos: 0 }]
|
||||
|
||||
broker.on('unsubscribe', function (unsubscriptions, client) {
|
||||
t.equal(unsubscriptions, unsubs)
|
||||
t.same(client, s.client)
|
||||
})
|
||||
|
||||
s.client.unsubscribe({
|
||||
unsubscriptions: unsubs,
|
||||
close: true
|
||||
}, function (err) {
|
||||
t.error(err)
|
||||
})
|
||||
})
|
||||
|
||||
test('dont emit unsubscribe event on client close', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = noError(connect(setup(broker), { clientId: 'abcde' }), t)
|
||||
|
||||
broker.on('unsubscribe', function (unsubscriptions, client) {
|
||||
t.error('unsubscribe should not be emitted')
|
||||
})
|
||||
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
s.inStream.end({
|
||||
cmd: 'disconnect'
|
||||
})
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.pass('unsubscribe completed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('emit clientDisconnect event', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.on('clientDisconnect', function (client) {
|
||||
t.equal(client.id, 'abcde', 'client matches')
|
||||
})
|
||||
|
||||
const s = noError(connect(setup(broker), { clientId: 'abcde' }), t)
|
||||
|
||||
s.inStream.end({
|
||||
cmd: 'disconnect'
|
||||
})
|
||||
s.outStream.resume()
|
||||
})
|
||||
|
||||
test('emits client', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.on('client', function (client) {
|
||||
t.equal(client.id, 'abcde', 'clientId matches')
|
||||
})
|
||||
|
||||
connect(setup(broker), {
|
||||
clientId: 'abcde'
|
||||
})
|
||||
})
|
||||
|
||||
test('get aedes version', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
t.equal(broker.version, require('../package.json').version)
|
||||
})
|
||||
|
||||
test('connect and connackSent event', { timeout: 50 }, function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const s = setup()
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const clientId = 'my-client'
|
||||
|
||||
s.broker.on('connackSent', function (packet, client) {
|
||||
t.equal(packet.returnCode, 0)
|
||||
t.equal(client.id, clientId, 'connackSent event and clientId matches')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'connect',
|
||||
protocolId: 'MQTT',
|
||||
protocolVersion: 4,
|
||||
clean: true,
|
||||
clientId,
|
||||
keepalive: 0
|
||||
})
|
||||
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'connack',
|
||||
returnCode: 0,
|
||||
length: 2,
|
||||
qos: 0,
|
||||
retain: false,
|
||||
dup: false,
|
||||
topic: null,
|
||||
payload: null,
|
||||
sessionPresent: false
|
||||
}, 'successful connack')
|
||||
})
|
||||
})
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const EventEmitter = require('events')
|
||||
const mqtt = require('mqtt')
|
||||
const net = require('net')
|
||||
const Faketimers = require('@sinonjs/fake-timers')
|
||||
const aedes = require('../')
|
||||
|
||||
test('connect 500 concurrent clients', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const evt = new EventEmitter()
|
||||
const broker = aedes()
|
||||
const server = net.createServer(broker.handle)
|
||||
const total = 500
|
||||
|
||||
server.listen(0, function (err) {
|
||||
t.error(err, 'no error')
|
||||
|
||||
const clock = Faketimers.createClock()
|
||||
t.teardown(clock.reset.bind(clock))
|
||||
|
||||
const port = server.address().port
|
||||
|
||||
let connected = 0
|
||||
const clients = []
|
||||
clock.setTimeout(function () {
|
||||
t.equal(clients.length, total)
|
||||
t.equal(connected, total)
|
||||
while (clients.length) {
|
||||
clients.shift().end()
|
||||
}
|
||||
}, total)
|
||||
|
||||
evt.on('finish', function () {
|
||||
if (clients.length === 0) {
|
||||
broker.close()
|
||||
server.close()
|
||||
}
|
||||
})
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
clients[i] = mqtt.connect({
|
||||
port,
|
||||
keepalive: 0,
|
||||
reconnectPeriod: 100
|
||||
}).on('connect', function () {
|
||||
connected++
|
||||
if ((connected % (total / 10)) === 0) {
|
||||
console.log('connected', connected)
|
||||
}
|
||||
clock.tick(1)
|
||||
}).on('close', function () {
|
||||
evt.emit('finish')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('do not block after a subscription', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const evt = new EventEmitter()
|
||||
const broker = aedes()
|
||||
const server = net.createServer(broker.handle)
|
||||
const total = 10000
|
||||
let sent = 0
|
||||
let received = 0
|
||||
|
||||
server.listen(0, function (err) {
|
||||
t.error(err, 'no error')
|
||||
|
||||
const clock = Faketimers.createClock()
|
||||
t.teardown(clock.reset.bind(clock))
|
||||
|
||||
const clockId = clock.setTimeout(finish, total)
|
||||
|
||||
const port = server.address().port
|
||||
|
||||
const publisher = mqtt.connect({
|
||||
port,
|
||||
keepalive: 0
|
||||
}).on('error', function (err) {
|
||||
clock.clearTimeout(clockId)
|
||||
t.fail(err)
|
||||
})
|
||||
|
||||
let subscriber
|
||||
|
||||
function immediatePublish () {
|
||||
setImmediate(publish)
|
||||
}
|
||||
|
||||
function publish () {
|
||||
if (sent === total) {
|
||||
publisher.end()
|
||||
} else {
|
||||
sent++
|
||||
publisher.publish('test', 'payload', immediatePublish)
|
||||
}
|
||||
}
|
||||
|
||||
function startSubscriber () {
|
||||
subscriber = mqtt.connect({
|
||||
port,
|
||||
keepalive: 0
|
||||
}).on('error', function (err) {
|
||||
clock.clearTimeout(clockId)
|
||||
t.fail(err)
|
||||
})
|
||||
|
||||
subscriber.subscribe('test', publish)
|
||||
|
||||
subscriber.on('message', function () {
|
||||
if (received % (total / 10) === 0) {
|
||||
console.log('sent / received', sent, received)
|
||||
}
|
||||
received++
|
||||
clock.tick(1)
|
||||
})
|
||||
subscriber.on('close', function () {
|
||||
evt.emit('finish')
|
||||
})
|
||||
}
|
||||
|
||||
publisher.on('connect', startSubscriber)
|
||||
publisher.on('close', function () {
|
||||
evt.emit('finish')
|
||||
})
|
||||
evt.on('finish', function () {
|
||||
if (publisher.connected || subscriber.connected) { return }
|
||||
broker.close()
|
||||
server.close()
|
||||
t.equal(total, sent, 'messages sent')
|
||||
t.equal(total, received, 'messages received')
|
||||
})
|
||||
function finish () {
|
||||
subscriber.end()
|
||||
publisher.end()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('do not block with overlapping subscription', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const evt = new EventEmitter()
|
||||
const broker = aedes({ concurrency: 15 })
|
||||
const server = net.createServer(broker.handle)
|
||||
const total = 10000
|
||||
let sent = 0
|
||||
let received = 0
|
||||
|
||||
server.listen(0, function (err) {
|
||||
t.error(err, 'no error')
|
||||
|
||||
const clock = Faketimers.createClock()
|
||||
t.teardown(clock.reset.bind(clock))
|
||||
|
||||
const clockId = clock.setTimeout(finish, total)
|
||||
|
||||
const port = server.address().port
|
||||
|
||||
const publisher = mqtt.connect({
|
||||
port,
|
||||
keepalive: 0
|
||||
}).on('error', function (err) {
|
||||
clock.clearTimeout(clockId)
|
||||
t.fail(err)
|
||||
})
|
||||
|
||||
let subscriber
|
||||
|
||||
function immediatePublish (e) {
|
||||
setImmediate(publish)
|
||||
}
|
||||
|
||||
function publish () {
|
||||
if (sent === total) {
|
||||
publisher.end()
|
||||
} else {
|
||||
sent++
|
||||
publisher.publish('test', 'payload', immediatePublish)
|
||||
}
|
||||
}
|
||||
|
||||
function startSubscriber () {
|
||||
subscriber = mqtt.connect({
|
||||
port,
|
||||
keepalive: 0
|
||||
}).on('error', function (err) {
|
||||
clock.clearTimeout(clockId)
|
||||
t.fail(err)
|
||||
})
|
||||
|
||||
subscriber.subscribe('#', function () {
|
||||
subscriber.subscribe('test', function () {
|
||||
immediatePublish()
|
||||
})
|
||||
})
|
||||
|
||||
subscriber.on('message', function () {
|
||||
if (received % (total / 10) === 0) {
|
||||
console.log('sent / received', sent, received)
|
||||
}
|
||||
received++
|
||||
clock.tick(1)
|
||||
})
|
||||
subscriber.on('close', function () {
|
||||
evt.emit('finish')
|
||||
})
|
||||
}
|
||||
|
||||
publisher.on('connect', startSubscriber)
|
||||
publisher.on('close', function () {
|
||||
evt.emit('finish')
|
||||
})
|
||||
evt.on('finish', function () {
|
||||
if (publisher.connected || subscriber.connected) { return }
|
||||
broker.close()
|
||||
server.close()
|
||||
t.equal(total, sent, 'messages sent')
|
||||
t.equal(total, received, 'messages received')
|
||||
})
|
||||
function finish () {
|
||||
subscriber.end()
|
||||
publisher.end()
|
||||
}
|
||||
})
|
||||
})
|
||||
+880
@@ -0,0 +1,880 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const concat = require('concat-stream')
|
||||
const { setup, connect, subscribe } = require('./helper')
|
||||
const Faketimers = require('@sinonjs/fake-timers')
|
||||
const aedes = require('../')
|
||||
|
||||
test('publish QoS 1', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const expected = {
|
||||
cmd: 'puback',
|
||||
messageId: 42,
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 2,
|
||||
retain: false
|
||||
}
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
|
||||
test('publish QoS 1 throws error', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.persistence.subscriptionsByTopic = function (packet, done) {
|
||||
return done(new Error('Throws error'))
|
||||
}
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
|
||||
s.broker.on('error', function (err) {
|
||||
t.equal('Throws error', err.message, 'Throws error')
|
||||
})
|
||||
})
|
||||
|
||||
test('publish QoS 1 throws error on write', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('client', function (client) {
|
||||
client.connected = false
|
||||
client.connecting = false
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
})
|
||||
|
||||
s.broker.on('clientError', function (client, err) {
|
||||
t.equal(err.message, 'connection closed', 'throws error')
|
||||
})
|
||||
})
|
||||
|
||||
test('publish QoS 1 and check offline queue', function (t) {
|
||||
t.plan(13)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker), { clean: false })
|
||||
const subscriberClient = {
|
||||
id: 'abcde'
|
||||
}
|
||||
const subscriber = connect(setup(broker), { clean: false, clientId: subscriberClient.id })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
qos: 1,
|
||||
dup: false,
|
||||
retain: false
|
||||
}
|
||||
const expectedAck = {
|
||||
cmd: 'puback',
|
||||
retain: false,
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 2,
|
||||
messageId: 10
|
||||
}
|
||||
const sent = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 10,
|
||||
retain: false,
|
||||
dup: false
|
||||
}
|
||||
const queue = []
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
publisher.outStream.on('data', function (packet) {
|
||||
t.same(packet, expectedAck, 'ack packet must patch')
|
||||
})
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
queue.push(packet)
|
||||
delete packet.payload
|
||||
delete packet.length
|
||||
t.not(packet.messageId, undefined, 'messageId is assigned a value')
|
||||
t.not(packet.messageId, 10, 'messageId should be unique')
|
||||
expected.messageId = packet.messageId
|
||||
t.same(packet, expected, 'publish packet must patch')
|
||||
if (queue.length === 2) {
|
||||
setImmediate(() => {
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
broker.persistence.outgoingClearMessageId(subscriberClient, queue[i], function (_, origPacket) {
|
||||
if (origPacket) {
|
||||
delete origPacket.brokerId
|
||||
delete origPacket.brokerCounter
|
||||
delete origPacket.payload
|
||||
delete origPacket.messageId
|
||||
delete sent.payload
|
||||
delete sent.messageId
|
||||
t.same(origPacket, sent, 'origPacket must match')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
publisher.inStream.write(sent)
|
||||
sent.payload = 'world2world'
|
||||
publisher.inStream.write(sent)
|
||||
})
|
||||
})
|
||||
|
||||
test('publish QoS 1 and empty offline queue', function (t) {
|
||||
t.plan(13)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker), { clean: false })
|
||||
const subscriberClient = {
|
||||
id: 'abcde'
|
||||
}
|
||||
const subscriber = connect(setup(broker), { clean: false, clientId: subscriberClient.id })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
qos: 1,
|
||||
dup: false,
|
||||
retain: false
|
||||
}
|
||||
const expectedAck = {
|
||||
cmd: 'puback',
|
||||
retain: false,
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 2,
|
||||
messageId: 10
|
||||
}
|
||||
const sent = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 10,
|
||||
retain: false,
|
||||
dup: false
|
||||
}
|
||||
const queue = []
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
publisher.outStream.on('data', function (packet) {
|
||||
t.same(packet, expectedAck, 'ack packet must patch')
|
||||
})
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
queue.push(packet)
|
||||
delete packet.payload
|
||||
delete packet.length
|
||||
t.not(packet.messageId, undefined, 'messageId is assigned a value')
|
||||
t.not(packet.messageId, 10, 'messageId should be unique')
|
||||
expected.messageId = packet.messageId
|
||||
t.same(packet, expected, 'publish packet must patch')
|
||||
if (queue.length === 2) {
|
||||
setImmediate(() => {
|
||||
broker.clients[subscriberClient.id].emptyOutgoingQueue(function () {
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
broker.persistence.outgoingClearMessageId(subscriberClient, queue[i], function (_, origPacket) {
|
||||
t.equal(!!origPacket, false, 'Packet has been removed')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
publisher.inStream.write(sent)
|
||||
sent.payload = 'world2world'
|
||||
publisher.inStream.write(sent)
|
||||
})
|
||||
})
|
||||
|
||||
test('subscribe QoS 1', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'puback',
|
||||
messageId: packet.messageId
|
||||
})
|
||||
t.not(packet.messageId, 42, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('subscribe QoS 0, but publish QoS 1', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 12,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('restore QoS 1 subscriptions not clean', function (t) {
|
||||
t.plan(7)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
|
||||
t.equal(connect.sessionPresent, true, 'session present is set to true')
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
})
|
||||
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'puback',
|
||||
messageId: packet.messageId
|
||||
})
|
||||
t.not(packet.messageId, 42, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('restore multiple QoS 1 subscriptions not clean w/ authorizeSubscribe', function (t) {
|
||||
t.plan(11)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'foo',
|
||||
payload: Buffer.from('bar'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 10,
|
||||
retain: false
|
||||
}
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscribe(t, subscriber, 'foo', 1, function () {
|
||||
subscriber.inStream.end()
|
||||
broker.authorizeSubscribe = function (client, sub, done) {
|
||||
done(null, sub.topic === 'hello' ? 123 : sub)
|
||||
}
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
|
||||
t.equal(connect.sessionPresent, true, 'session present is set to true')
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'foo',
|
||||
payload: 'bar',
|
||||
qos: 1,
|
||||
messageId: 48
|
||||
})
|
||||
})
|
||||
publisher.outStream.on('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
})
|
||||
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'puback',
|
||||
messageId: packet.messageId
|
||||
})
|
||||
t.not(packet.messageId, 48, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('remove stored subscriptions if connected with clean=true', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
subscriber = connect(setup(broker), { clean: true, clientId: 'abcde' }, function (packet) {
|
||||
t.equal(packet.sessionPresent, false, 'session present is set to false')
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
|
||||
subscriber.inStream.end()
|
||||
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
|
||||
t.equal(connect.sessionPresent, false, 'session present is set to false')
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 43
|
||||
})
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('publish received')
|
||||
})
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('publish received')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('resend publish on non-clean reconnect QoS 1', function (t) {
|
||||
t.plan(8)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const opts = { clean: false, clientId: 'abcde' }
|
||||
let subscriber = connect(setup(broker), opts)
|
||||
const subscriberClient = {
|
||||
id: opts.clientId
|
||||
}
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
|
||||
subscriber = connect(setup(broker), opts)
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'puback',
|
||||
messageId: packet.messageId
|
||||
})
|
||||
t.not(packet.messageId, 42, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
setImmediate(() => {
|
||||
const stream = broker.persistence.outgoingStream(subscriberClient)
|
||||
stream.pipe(concat(function (list) {
|
||||
t.equal(list.length, 1, 'should remain one item in queue')
|
||||
t.same(list[0].payload, Buffer.from('world world'), 'packet must match')
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('resend many publish on non-clean reconnect QoS 1', function (t) {
|
||||
t.plan(4)
|
||||
const broker = aedes()
|
||||
const clock = Faketimers.createClock()
|
||||
|
||||
t.teardown(() => {
|
||||
broker.close.bind(broker)
|
||||
clock.reset.bind(clock)
|
||||
})
|
||||
|
||||
const opts = { clean: false, clientId: 'abcde' }
|
||||
let subscriber = connect(setup(broker), opts)
|
||||
const publisher = connect(setup(broker))
|
||||
const { through } = require('../lib/utils')
|
||||
const total = through().writableHighWaterMark * 2
|
||||
|
||||
let received = 0
|
||||
clock.setTimeout(() => {
|
||||
broker.close()
|
||||
t.equal(received, total)
|
||||
}, total)
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
for (let sent = 0; sent < total; sent++) {
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'message-' + sent,
|
||||
qos: 1,
|
||||
messageId: 42 + sent
|
||||
})
|
||||
}
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
subscriber = connect(setup(broker), opts)
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'puback',
|
||||
messageId: packet.messageId
|
||||
})
|
||||
received++
|
||||
clock.tick(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('do not resend QoS 1 packets at each reconnect', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
subscriber.inStream.end({
|
||||
cmd: 'puback',
|
||||
messageId: packet.messageId
|
||||
})
|
||||
|
||||
t.not(packet.messageId, 42, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
|
||||
const subscriber2 = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
|
||||
subscriber2.outStream.once('data', function (packet) {
|
||||
t.fail('this should never happen')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('do not resend QoS 1 packets if reconnect is clean', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
|
||||
subscriber = connect(setup(broker), { clean: true, clientId: 'abcde' })
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('this should never happen')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('do not resend QoS 1 packets at reconnect if puback was received', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
subscriber.inStream.end({
|
||||
cmd: 'puback',
|
||||
messageId: packet.messageId
|
||||
})
|
||||
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('this should never happen')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('remove stored subscriptions after unsubscribe', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'unsubscribe',
|
||||
messageId: 43,
|
||||
unsubscriptions: ['hello']
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'unsuback',
|
||||
messageId: 43,
|
||||
dup: false,
|
||||
length: 2,
|
||||
qos: 0,
|
||||
retain: false
|
||||
}, 'packet matches')
|
||||
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (packet) {
|
||||
t.equal(packet.sessionPresent, false, 'session present is set to false')
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 43
|
||||
}, function () {
|
||||
subscriber.inStream.end()
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('publish received')
|
||||
})
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('publish received')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('upgrade a QoS 0 subscription to QoS 1', function (t) {
|
||||
t.plan(8)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
length: 14,
|
||||
retain: false,
|
||||
dup: false
|
||||
}
|
||||
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
subscribe(t, s, 'hello', 1, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.ok(packet.messageId, 'has messageId')
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet matches')
|
||||
})
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('downgrade QoS 0 publish on QoS 1 subsciption', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
length: 12,
|
||||
retain: false,
|
||||
dup: false
|
||||
}
|
||||
|
||||
subscribe(t, s, 'hello', 1, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet matches')
|
||||
})
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 0
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('subscribe and publish QoS 1 in parallel', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
broker.on('clientError', function (client, err) {
|
||||
console.log(err.stack)
|
||||
// t.fail('no client error')
|
||||
})
|
||||
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
t.equal(packet.messageId, 42, 'messageId must match')
|
||||
s.outStream.on('data', function (packet) {
|
||||
if (packet.cmd === 'suback') {
|
||||
t.same(packet.granted, [1])
|
||||
t.equal(packet.messageId, 24)
|
||||
}
|
||||
if (packet.cmd === 'publish') {
|
||||
s.inStream.write({
|
||||
cmd: 'puback',
|
||||
messageId: packet.messageId
|
||||
})
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'subscribe',
|
||||
messageId: 24,
|
||||
subscriptions: [{
|
||||
topic: 'hello',
|
||||
qos: 1
|
||||
}]
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42
|
||||
})
|
||||
})
|
||||
+686
@@ -0,0 +1,686 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const concat = require('concat-stream')
|
||||
const { setup, connect, subscribe } = require('./helper')
|
||||
const aedes = require('../')
|
||||
|
||||
function publish (t, s, packet, done) {
|
||||
const msgId = packet.messageId
|
||||
|
||||
s.inStream.write(packet)
|
||||
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'pubrec',
|
||||
messageId: msgId,
|
||||
length: 2,
|
||||
dup: false,
|
||||
retain: false,
|
||||
qos: 0
|
||||
}, 'pubrec must match')
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'pubrel',
|
||||
messageId: msgId
|
||||
})
|
||||
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'pubcomp',
|
||||
messageId: msgId,
|
||||
length: 2,
|
||||
dup: false,
|
||||
retain: false,
|
||||
qos: 0
|
||||
}, 'pubcomp must match')
|
||||
|
||||
if (done) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function receive (t, subscriber, expected, done) {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.not(packet.messageId, expected.messageId, 'messageId must differ')
|
||||
|
||||
const msgId = packet.messageId
|
||||
delete packet.messageId
|
||||
delete expected.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
|
||||
subscriber.inStream.write({
|
||||
cmd: 'pubrec',
|
||||
messageId: msgId
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'pubcomp',
|
||||
messageId: msgId
|
||||
})
|
||||
t.same(packet, {
|
||||
cmd: 'pubrel',
|
||||
messageId: msgId,
|
||||
length: 2,
|
||||
qos: 1,
|
||||
retain: false,
|
||||
dup: false
|
||||
}, 'pubrel must match')
|
||||
|
||||
if (done) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test('publish QoS 2', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const packet = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 2,
|
||||
messageId: 42
|
||||
}
|
||||
publish(t, s, packet)
|
||||
})
|
||||
|
||||
test('subscribe QoS 2', function (t) {
|
||||
t.plan(8)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const toPublish = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
messageId: 42,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
publish(t, publisher, toPublish)
|
||||
|
||||
receive(t, subscriber, toPublish)
|
||||
})
|
||||
})
|
||||
|
||||
test('publish QoS 2 throws error on write', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('client', function (client) {
|
||||
client.connected = false
|
||||
client.connecting = false
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 2,
|
||||
messageId: 42
|
||||
})
|
||||
})
|
||||
|
||||
s.broker.on('clientError', function (client, err) {
|
||||
t.equal(err.message, 'connection closed', 'throws error')
|
||||
})
|
||||
})
|
||||
|
||||
test('pubrec handler calls done when outgoingUpdate fails (clean=false)', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup(), { clean: false })
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const handle = require('../lib/handlers/pubrec.js')
|
||||
|
||||
s.broker.persistence.outgoingUpdate = function (client, pubrel, done) {
|
||||
done(Error('throws error'))
|
||||
}
|
||||
|
||||
handle(s.client, { messageId: 42 }, function done () {
|
||||
t.pass('calls done on error')
|
||||
})
|
||||
})
|
||||
|
||||
test('client.publish with clean=true subscribption QoS 2', function (t) {
|
||||
t.plan(8)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const toPublish = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
messageId: 42,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
let brokerClient = null
|
||||
|
||||
broker.on('client', function (client) {
|
||||
brokerClient = client
|
||||
|
||||
brokerClient.on('error', function (err) {
|
||||
t.error(err)
|
||||
})
|
||||
})
|
||||
|
||||
const subscriber = connect(setup(broker), { clean: true })
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
t.pass('subscribed')
|
||||
receive(t, subscriber, toPublish)
|
||||
brokerClient.publish(toPublish, function (err) {
|
||||
t.error(err)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('call published method with client with QoS 2', function (t) {
|
||||
t.plan(9)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const toPublish = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
messageId: 42,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
broker.published = function (packet, client, cb) {
|
||||
// Client is null for all server publishes
|
||||
if (packet.topic.split('/')[0] !== '$SYS') {
|
||||
t.ok(client, 'client must be passed to published method')
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
publish(t, publisher, toPublish)
|
||||
|
||||
receive(t, subscriber, toPublish)
|
||||
})
|
||||
})
|
||||
|
||||
;[true, false].forEach(function (cleanSession) {
|
||||
test(`authorized forward publish packets in QoS 2 [clean=${cleanSession}]`, function (t) {
|
||||
t.plan(9)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const opts = { clean: cleanSession }
|
||||
const publisher = connect(setup(broker), { clientId: 'my-client-xyz-8' })
|
||||
const subscriber = connect(setup(broker), { ...opts, clientId: 'abcde' })
|
||||
const forwarded = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
retain: false,
|
||||
dup: false,
|
||||
messageId: undefined,
|
||||
clientId: 'my-client-xyz-8'
|
||||
}
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
retain: false,
|
||||
length: 14,
|
||||
dup: false
|
||||
}
|
||||
broker.authorizeForward = function (client, packet) {
|
||||
forwarded.brokerId = broker.id
|
||||
forwarded.brokerCounter = broker.counter
|
||||
delete packet.nl
|
||||
t.same(packet, forwarded, 'forwarded packet must match')
|
||||
return packet
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.not(packet.messageId, 42)
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
|
||||
publish(t, publisher, {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
retain: false,
|
||||
messageId: 42,
|
||||
dup: false
|
||||
}, function () {
|
||||
const stream = broker.persistence.outgoingStream({ id: 'abcde' })
|
||||
stream.pipe(concat(function (list) {
|
||||
if (cleanSession) {
|
||||
t.equal(list.length, 0, 'should have empty item in queue')
|
||||
} else {
|
||||
t.equal(list.length, 1, 'should have one item in queue')
|
||||
}
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
;[true, false].forEach(function (cleanSession) {
|
||||
test(`unauthorized forward publish packets in QoS 2 [clean=${cleanSession}]`, function (t) {
|
||||
t.plan(6)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const opts = { clean: cleanSession }
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker), { ...opts, clientId: 'abcde' })
|
||||
|
||||
broker.authorizeForward = function (client, packet) {
|
||||
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('should not receive any packets')
|
||||
})
|
||||
|
||||
publish(t, publisher, {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
retain: false,
|
||||
messageId: 42,
|
||||
dup: false
|
||||
}, function () {
|
||||
const stream = broker.persistence.outgoingStream({ id: 'abcde' })
|
||||
stream.pipe(concat(function (list) {
|
||||
t.equal(list.length, 0, 'should empty in queue')
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('subscribe QoS 0, but publish QoS 2', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 12,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
|
||||
publish(t, publisher, {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
retain: false,
|
||||
messageId: 42,
|
||||
dup: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('subscribe QoS 1, but publish QoS 2', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
|
||||
publish(t, publisher, {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
retain: false,
|
||||
messageId: 42,
|
||||
dup: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('restore QoS 2 subscriptions not clean', function (t) {
|
||||
t.plan(9)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
dup: false,
|
||||
length: 14,
|
||||
messageId: 42,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
|
||||
t.equal(connect.sessionPresent, true, 'session present is set to true')
|
||||
publish(t, publisher, expected)
|
||||
})
|
||||
|
||||
receive(t, subscriber, expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('resend publish on non-clean reconnect QoS 2', function (t) {
|
||||
t.plan(8)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const opts = { clean: false, clientId: 'abcde' }
|
||||
let subscriber = connect(setup(broker), opts)
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
dup: false,
|
||||
length: 14,
|
||||
messageId: 42,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
publish(t, publisher, expected, function () {
|
||||
subscriber = connect(setup(broker), opts)
|
||||
|
||||
receive(t, subscriber, expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('resend pubrel on non-clean reconnect QoS 2', function (t) {
|
||||
t.plan(9)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const opts = { clean: false, clientId: 'abcde' }
|
||||
let subscriber = connect(setup(broker), opts)
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
dup: false,
|
||||
length: 14,
|
||||
messageId: 42,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
subscriber.inStream.end()
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
|
||||
publish(t, publisher, expected, function () {
|
||||
subscriber = connect(setup(broker), opts)
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.not(packet.messageId, expected.messageId, 'messageId must differ')
|
||||
|
||||
const msgId = packet.messageId
|
||||
delete packet.messageId
|
||||
delete expected.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
|
||||
subscriber.inStream.write({
|
||||
cmd: 'pubrec',
|
||||
messageId: msgId
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'pubrel',
|
||||
messageId: msgId,
|
||||
length: 2,
|
||||
qos: 1,
|
||||
retain: false,
|
||||
dup: false
|
||||
}, 'pubrel must match')
|
||||
|
||||
subscriber.inStream.end()
|
||||
|
||||
subscriber = connect(setup(broker), opts)
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, {
|
||||
cmd: 'pubrel',
|
||||
messageId: msgId,
|
||||
length: 2,
|
||||
qos: 1,
|
||||
retain: false,
|
||||
dup: false
|
||||
}, 'pubrel must match')
|
||||
|
||||
subscriber.inStream.write({
|
||||
cmd: 'pubcomp',
|
||||
messageId: msgId
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('publish after disconnection', function (t) {
|
||||
t.plan(10)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const toPublish = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
messageId: 42,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
const toPublish2 = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('worl2'),
|
||||
qos: 2,
|
||||
messageId: 43,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 2, function () {
|
||||
publish(t, publisher, toPublish)
|
||||
|
||||
receive(t, subscriber, toPublish, function () {
|
||||
publish(t, publisher, toPublish2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('multiple publish and store one', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const broker = aedes()
|
||||
|
||||
const sid = {
|
||||
id: 'abcde'
|
||||
}
|
||||
const s = connect(setup(broker), { clientId: sid.id })
|
||||
const toPublish = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 2,
|
||||
retain: false,
|
||||
dup: false,
|
||||
messageId: 42
|
||||
}
|
||||
|
||||
let count = 5
|
||||
while (count--) {
|
||||
s.inStream.write(toPublish)
|
||||
}
|
||||
let recvcnt = 0
|
||||
s.outStream.on('data', function (packet) {
|
||||
if (++recvcnt < 5) return
|
||||
broker.close(function () {
|
||||
broker.persistence.incomingGetPacket(sid, toPublish, function (err, origPacket) {
|
||||
delete origPacket.brokerId
|
||||
delete origPacket.brokerCounter
|
||||
t.same(origPacket, toPublish, 'packet must match')
|
||||
t.error(err)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('packet is written to stream after being stored', function (t) {
|
||||
const s = connect(setup())
|
||||
|
||||
const broker = s.broker
|
||||
|
||||
t.teardown(broker.close.bind(s.broker))
|
||||
|
||||
let packetStored = false
|
||||
|
||||
const fn = broker.persistence.incomingStorePacket.bind(broker.persistence)
|
||||
|
||||
s.broker.persistence.incomingStorePacket = function (client, packet, done) {
|
||||
packetStored = true
|
||||
t.pass('packet stored')
|
||||
fn(client, packet, done)
|
||||
}
|
||||
|
||||
const packet = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 2,
|
||||
messageId: 42
|
||||
}
|
||||
|
||||
publish(t, s, packet)
|
||||
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'pubrec', 'pubrec received')
|
||||
t.equal(packetStored, true, 'after packet store')
|
||||
t.end()
|
||||
})
|
||||
})
|
||||
|
||||
test('not send pubrec when persistence fails to store packet', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const s = connect(setup())
|
||||
const broker = s.broker
|
||||
|
||||
t.teardown(broker.close.bind(s.broker))
|
||||
|
||||
s.broker.persistence.incomingStorePacket = function (client, packet, done) {
|
||||
t.pass('packet stored')
|
||||
done(new Error('store error'))
|
||||
}
|
||||
s.broker.on('clientError', function (client, err) {
|
||||
t.equal(err.message, 'store error')
|
||||
})
|
||||
|
||||
const packet = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 2,
|
||||
messageId: 42
|
||||
}
|
||||
|
||||
s.inStream.write(packet)
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.fail('should not have pubrec')
|
||||
})
|
||||
})
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const { setup, connect } = require('./helper')
|
||||
|
||||
test('after an error, outstanding packets are discarded', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup(), {
|
||||
keepalive: 1000
|
||||
})
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
const packet = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world'
|
||||
}
|
||||
|
||||
s.broker.mq.on('hello', function (msg, cb) {
|
||||
t.pass('first msg received')
|
||||
s.inStream.destroy(new Error('something went wrong'))
|
||||
cb()
|
||||
setImmediate(() => {
|
||||
packet.topic = 'foo'
|
||||
s.inStream.write(packet)
|
||||
s.inStream.write(packet)
|
||||
})
|
||||
})
|
||||
s.broker.mq.on('foo', function (msg, cb) {
|
||||
t.fail('msg received')
|
||||
})
|
||||
s.inStream.write(packet)
|
||||
})
|
||||
+713
@@ -0,0 +1,713 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const { through } = require('../lib/utils')
|
||||
const Faketimers = require('@sinonjs/fake-timers')
|
||||
const { setup, connect, subscribe, noError } = require('./helper')
|
||||
const aedes = require('../')
|
||||
|
||||
// [MQTT-3.3.1-9]
|
||||
test('live retain packets', function (t) {
|
||||
t.plan(5)
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
retain: false,
|
||||
dup: false,
|
||||
length: 12,
|
||||
qos: 0
|
||||
}
|
||||
|
||||
const s = noError(connect(setup()), t)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, 'hello', 0, function () {
|
||||
s.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected)
|
||||
})
|
||||
|
||||
s.broker.publish({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
retain: true,
|
||||
dup: false,
|
||||
length: 12,
|
||||
qos: 0
|
||||
}, function () {
|
||||
t.pass('publish finished')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('retain messages', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 12,
|
||||
retain: true
|
||||
}
|
||||
|
||||
broker.subscribe('hello', function (packet, cb) {
|
||||
cb()
|
||||
|
||||
// defer this or it will receive the message which
|
||||
// is being published
|
||||
setImmediate(function () {
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
publisher.inStream.write(expected)
|
||||
})
|
||||
|
||||
test('retain messages propagates through broker subscriptions', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
retain: true
|
||||
}
|
||||
|
||||
const subscriberFunc = function (packet, cb) {
|
||||
packet = Object.assign({}, packet)
|
||||
delete packet.brokerId
|
||||
delete packet.brokerCounter
|
||||
cb()
|
||||
setImmediate(function () {
|
||||
t.same(packet, expected, 'packet must not have been modified')
|
||||
})
|
||||
}
|
||||
|
||||
broker.subscribe('hello', subscriberFunc, function () {
|
||||
broker.publish(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('avoid wrong deduping of retain messages', function (t) {
|
||||
t.plan(7)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 12,
|
||||
retain: true
|
||||
}
|
||||
|
||||
broker.subscribe('hello', function (packet, cb) {
|
||||
cb()
|
||||
// subscribe and publish another topic
|
||||
subscribe(t, subscriber, 'hello2', 0, function () {
|
||||
cb()
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello2',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
publisher.inStream.write(expected)
|
||||
})
|
||||
|
||||
test('reconnected subscriber will not receive retained messages when QoS 0 and clean', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker), { clean: true })
|
||||
let subscriber = connect(setup(broker), { clean: true })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
retain: false,
|
||||
dup: false,
|
||||
length: 12
|
||||
}
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 0,
|
||||
retain: false
|
||||
})
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
subscriber.inStream.end()
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'foo',
|
||||
qos: 0,
|
||||
retain: true
|
||||
})
|
||||
subscriber = connect(setup(broker), { clean: true })
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
t.fail('should not received retain message')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('subscriber will not receive retained messages when QoS is 128', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const pubPacket = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
retain: true,
|
||||
messageId: 42
|
||||
}
|
||||
|
||||
broker.authorizeSubscribe = function (client, sub, callback) {
|
||||
if (sub.topic === pubPacket.topic) {
|
||||
callback(null, null)
|
||||
} else {
|
||||
callback(null, sub)
|
||||
}
|
||||
}
|
||||
|
||||
const publisher = connect(setup(broker), { clean: true })
|
||||
|
||||
publisher.inStream.write(pubPacket)
|
||||
|
||||
publisher.outStream.on('data', function (packet) {
|
||||
const subscriber = connect(setup(broker), { clean: true })
|
||||
subscribe(t, subscriber, pubPacket.topic, 128, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
t.fail('should not received retain message')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.3.1-6]
|
||||
test('new QoS 0 subscribers receive QoS 0 retained messages when clean', function (t) {
|
||||
t.plan(9)
|
||||
|
||||
const clock = Faketimers.createClock()
|
||||
const broker = aedes()
|
||||
t.teardown(function () {
|
||||
clock.reset()
|
||||
broker.close()
|
||||
})
|
||||
|
||||
const publisher = connect(setup(broker), { clean: true })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello/world',
|
||||
payload: Buffer.from('big big world'),
|
||||
qos: 0,
|
||||
retain: true,
|
||||
dup: false,
|
||||
length: 26
|
||||
}
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello/world',
|
||||
payload: 'big big world',
|
||||
qos: 0,
|
||||
retain: true
|
||||
})
|
||||
const subscriber1 = connect(setup(broker), { clean: true })
|
||||
subscribe(t, subscriber1, 'hello/world', 0, function () {
|
||||
subscriber1.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
clock.tick(100)
|
||||
})
|
||||
})
|
||||
const subscriber2 = connect(setup(broker), { clean: true })
|
||||
subscribe(t, subscriber2, 'hello/+', 0, function () {
|
||||
subscriber2.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
clock.tick(100)
|
||||
})
|
||||
})
|
||||
|
||||
clock.setTimeout(() => {
|
||||
t.equal(broker.counter, 9)
|
||||
}, 200)
|
||||
})
|
||||
|
||||
// [MQTT-3.3.1-5]
|
||||
test('new QoS 0 subscribers receive downgraded QoS 1 retained messages when clean', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
const broker = aedes()
|
||||
|
||||
const publisher = connect(setup(broker), { clean: true })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
retain: true,
|
||||
dup: false,
|
||||
length: 12
|
||||
}
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
retain: true,
|
||||
messageId: 42
|
||||
})
|
||||
publisher.outStream.on('data', function (packet) {
|
||||
const subscriber = connect(setup(broker), { clean: true })
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
t.not(packet.messageId, 42, 'messageId should not be the same')
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
broker.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
broker.on('closed', function () {
|
||||
t.equal(broker.counter, 8)
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.3.1-10]
|
||||
test('clean retained messages', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker), { clean: true })
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 0,
|
||||
retain: true
|
||||
})
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: '',
|
||||
qos: 0,
|
||||
retain: true
|
||||
})
|
||||
const subscriber = connect(setup(broker), { clean: true })
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('should not received retain message')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.3.1-11]
|
||||
test('broker not store zero-byte retained messages', function (t) {
|
||||
t.plan(0)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = connect(setup(broker))
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: '',
|
||||
retain: true
|
||||
})
|
||||
s.broker.on('publish', function (packet, client) {
|
||||
if (packet.topic.startsWith('$SYS/')) {
|
||||
return
|
||||
}
|
||||
const stream = s.broker.persistence.createRetainedStream(packet.topic)
|
||||
stream.pipe(through(function sendRetained (packet, enc, cb) {
|
||||
t.fail('not store zero-byte retained messages')
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
test('fail to clean retained messages without retain flag', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker), { clean: true })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
retain: true,
|
||||
dup: false,
|
||||
length: 12
|
||||
}
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 0,
|
||||
retain: true
|
||||
})
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: '',
|
||||
qos: 0,
|
||||
retain: false
|
||||
})
|
||||
const subscriber = connect(setup(broker), { clean: true })
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('only get the last retained messages in same topic', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker), { clean: true })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('foo'),
|
||||
qos: 0,
|
||||
retain: true,
|
||||
dup: false,
|
||||
length: 10
|
||||
}
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 0,
|
||||
retain: true
|
||||
})
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'foo',
|
||||
qos: 0,
|
||||
retain: true
|
||||
})
|
||||
const subscriber = connect(setup(broker), { clean: true })
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('deliver QoS 1 retained messages to new subscriptions', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: true
|
||||
}
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42,
|
||||
retain: true
|
||||
})
|
||||
|
||||
publisher.outStream.on('data', function (packet) {
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('deliver QoS 1 retained messages to established subscriptions', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42,
|
||||
retain: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('deliver QoS 0 retained message with QoS 1 subscription', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker))
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 12,
|
||||
retain: true
|
||||
}
|
||||
|
||||
broker.mq.on('hello', function (msg, cb) {
|
||||
cb()
|
||||
|
||||
// defer this or it will receive the message which
|
||||
// is being published
|
||||
setImmediate(function () {
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
messageId: 42,
|
||||
retain: true
|
||||
})
|
||||
})
|
||||
|
||||
test('disconnect and retain messages with QoS 1 [clean=false]', function (t) {
|
||||
t.plan(7)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = noError(connect(setup(broker), { clean: false, clientId: 'abcde' }), t)
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: true
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'disconnect'
|
||||
})
|
||||
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
console.log('original', packet)
|
||||
})
|
||||
|
||||
const publisher = noError(connect(setup(broker)), t)
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 42,
|
||||
retain: true
|
||||
})
|
||||
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
|
||||
t.equal(connect.sessionPresent, true, 'session present is set to true')
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
// receive any queued messages (no matter they are retained messages) at the disconnected time
|
||||
t.not(packet.messageId, 42, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
packet.length = 14
|
||||
t.same(packet, expected, 'packet must match')
|
||||
|
||||
// there should be no messages come from restored subscriptions
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.fail('should not receive any more messages')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('disconnect and two retain messages with QoS 1 [clean=false]', function (t) {
|
||||
t.plan(15)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
let subscriber = noError(connect(setup(broker), { clean: false, clientId: 'abcde' }), t)
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 14,
|
||||
retain: true
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.inStream.write({
|
||||
cmd: 'disconnect'
|
||||
})
|
||||
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
console.log('original', packet)
|
||||
})
|
||||
|
||||
const publisher = noError(connect(setup(broker)), t)
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world',
|
||||
qos: 1,
|
||||
messageId: 41,
|
||||
retain: true
|
||||
})
|
||||
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: 'world2',
|
||||
qos: 1,
|
||||
messageId: 42,
|
||||
retain: true
|
||||
})
|
||||
|
||||
publisher.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'puback')
|
||||
|
||||
subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
|
||||
t.equal(connect.sessionPresent, true, 'session present is set to true')
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
// receive any queued messages (included retained messages) at the disconnected time
|
||||
t.not(packet.messageId, 41, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
packet.length = 14
|
||||
expected.payload = Buffer.from('world')
|
||||
t.same(packet, expected, 'packet must match')
|
||||
|
||||
// receive any queued messages (included retained messages) at the disconnected time
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.not(packet.messageId, 42, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
packet.length = 14
|
||||
expected.payload = Buffer.from('world2')
|
||||
t.same(packet, expected, 'packet must match')
|
||||
|
||||
// should get the last retained message when we do a subscribe
|
||||
subscribe(t, subscriber, 'hello', 1, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
t.not(packet.messageId, 42, 'messageId must differ')
|
||||
delete packet.messageId
|
||||
packet.length = 14
|
||||
expected.payload = Buffer.from('world2')
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const { setup, connect, subscribe } = require('./helper')
|
||||
const aedes = require('../')
|
||||
const { validateTopic } = require('../lib/utils')
|
||||
|
||||
test('validation of `null` topic', function (t) {
|
||||
// issue #780
|
||||
t.plan(1)
|
||||
const err = validateTopic(null, 'SUBSCRIBE')
|
||||
t.equal(err.message, 'impossible to SUBSCRIBE to an empty topic')
|
||||
})
|
||||
|
||||
// [MQTT-4.7.1-3]
|
||||
test('Single-level wildcard should match empty level', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, 'a/+/b', 0, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.pass('ok')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'a//b',
|
||||
payload: 'world'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-4.7.3-1]
|
||||
test('publish empty topic', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const s = connect(setup())
|
||||
|
||||
subscribe(t, s, '#', 0, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.fail('no packet')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: '',
|
||||
payload: 'world'
|
||||
})
|
||||
|
||||
s.broker.close(function () {
|
||||
t.equal(s.broker.connectedClients, 0, 'no connected clients')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('publish invalid topic with #', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, '#', 0, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.fail('no packet')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello/#',
|
||||
payload: 'world'
|
||||
})
|
||||
})
|
||||
|
||||
s.broker.on('clientError', function () {
|
||||
t.pass('raise an error')
|
||||
})
|
||||
})
|
||||
|
||||
test('publish invalid topic with +', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
subscribe(t, s, '#', 0, function () {
|
||||
s.outStream.once('data', function (packet) {
|
||||
t.fail('no packet')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello/+/eee',
|
||||
payload: 'world'
|
||||
})
|
||||
})
|
||||
|
||||
s.broker.on('clientError', function () {
|
||||
t.pass('raise an error')
|
||||
})
|
||||
})
|
||||
|
||||
;['base/#/sub', 'base/#sub', 'base/sub#', 'base/xyz+/sub', 'base/+xyz/sub', ''].forEach(function (topic) {
|
||||
test('subscribe to invalid topic with "' + topic + '"', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('clientError', function () {
|
||||
t.pass('raise an error')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'subscribe',
|
||||
messageId: 24,
|
||||
subscriptions: [{
|
||||
topic,
|
||||
qos: 0
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
test('unsubscribe to invalid topic with "' + topic + '"', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const s = connect(setup())
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('clientError', function () {
|
||||
t.pass('raise an error')
|
||||
})
|
||||
|
||||
s.inStream.write({
|
||||
cmd: 'unsubscribe',
|
||||
messageId: 24,
|
||||
unsubscriptions: [topic]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('topics are case-sensitive', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const publisher = connect(setup(broker), { clean: true })
|
||||
const subscriber = connect(setup(broker), { clean: true })
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 12,
|
||||
retain: false
|
||||
}
|
||||
|
||||
subscribe(t, subscriber, 'hello', 0, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected, 'packet mush match')
|
||||
})
|
||||
;['hello', 'HELLO', 'heLLo', 'HELLO/#', 'hello/+'].forEach(function (topic) {
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic,
|
||||
payload: 'world',
|
||||
qos: 0,
|
||||
retain: false
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function subscribeMultipleTopics (t, broker, qos, subscriber, subscriptions, done) {
|
||||
const publisher = connect(setup(broker))
|
||||
subscriber.inStream.write({
|
||||
cmd: 'subscribe',
|
||||
messageId: 24,
|
||||
subscriptions
|
||||
})
|
||||
|
||||
subscriber.outStream.once('data', function (packet) {
|
||||
t.equal(packet.cmd, 'suback')
|
||||
t.same(packet.granted, subscriptions.map(obj => obj.qos))
|
||||
t.equal(packet.messageId, 24)
|
||||
|
||||
publisher.inStream.write({
|
||||
cmd: 'publish',
|
||||
topic: 'hello/world',
|
||||
payload: 'world',
|
||||
qos,
|
||||
messageId: 42
|
||||
})
|
||||
|
||||
if (done) {
|
||||
done(null, packet)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('Overlapped topics with same QoS', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello/world',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 1,
|
||||
dup: false,
|
||||
length: 20,
|
||||
retain: false
|
||||
}
|
||||
const sub = [
|
||||
{ topic: 'hello/world', qos: 1 },
|
||||
{ topic: 'hello/#', qos: 1 }]
|
||||
subscribeMultipleTopics(t, broker, 1, subscriber, sub, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.3.5-1]
|
||||
test('deliver overlapped topics respecting the maximum QoS of all the matching subscriptions - QoS 0 publish', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello/world',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 18,
|
||||
retain: false
|
||||
}
|
||||
const sub = [
|
||||
{ topic: 'hello/world', qos: 0 },
|
||||
{ topic: 'hello/#', qos: 2 }]
|
||||
subscribeMultipleTopics(t, broker, 0, subscriber, sub, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
delete packet.messageId
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// [MQTT-3.3.5-1]
|
||||
test('deliver overlapped topics respecting the maximum QoS of all the matching subscriptions - QoS 2 publish', function (t) {
|
||||
t.plan(3)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const subscriber = connect(setup(broker))
|
||||
|
||||
const sub = [
|
||||
{ topic: 'hello/world', qos: 0 },
|
||||
{ topic: 'hello/#', qos: 2 }]
|
||||
subscribeMultipleTopics(t, broker, 2, subscriber, sub, function () {
|
||||
subscriber.outStream.on('data', function () {
|
||||
t.fail('should receive messages with the maximum QoS')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Overlapped topics with QoS downgrade', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const subscriber = connect(setup(broker))
|
||||
const expected = {
|
||||
cmd: 'publish',
|
||||
topic: 'hello/world',
|
||||
payload: Buffer.from('world'),
|
||||
qos: 0,
|
||||
dup: false,
|
||||
length: 18,
|
||||
retain: false
|
||||
}
|
||||
const sub = [
|
||||
{ topic: 'hello/world', qos: 1 },
|
||||
{ topic: 'hello/#', qos: 1 }]
|
||||
subscribeMultipleTopics(t, broker, 0, subscriber, sub, function () {
|
||||
subscriber.outStream.on('data', function (packet) {
|
||||
t.same(packet, expected, 'packet must match')
|
||||
})
|
||||
})
|
||||
})
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import { IncomingMessage } from 'node:http'
|
||||
import { Socket } from 'node:net'
|
||||
import type {
|
||||
Brokers,
|
||||
AuthenticateError,
|
||||
Client,
|
||||
Connection
|
||||
} from '../../aedes'
|
||||
import Aedes, { AedesOptions, createBroker } from '../../aedes'
|
||||
import type { AedesPublishPacket, ConnackPacket, ConnectPacket, PingreqPacket, PublishPacket, PubrelPacket, Subscription, SubscribePacket, UnsubscribePacket } from '../../types/packet'
|
||||
import { expectType } from 'tsd'
|
||||
|
||||
// Test for createBroker function
|
||||
expectType<(options?: AedesOptions) => Aedes>(createBroker)
|
||||
|
||||
// Aedes server
|
||||
let broker = createBroker()
|
||||
expectType<Aedes>(broker)
|
||||
|
||||
broker = new Aedes({
|
||||
id: 'aedes',
|
||||
concurrency: 100,
|
||||
heartbeatInterval: 60000,
|
||||
connectTimeout: 30000,
|
||||
maxClientsIdLength: 23,
|
||||
keepaliveLimit: 0,
|
||||
preConnect: (client: Client, packet: ConnectPacket, callback) => {
|
||||
if (client.req) {
|
||||
callback(new Error('not websocket stream'), false)
|
||||
}
|
||||
if (client.conn instanceof Socket && client.conn.remoteAddress === '::1') {
|
||||
callback(null, true)
|
||||
} else {
|
||||
callback(new Error('connection error'), false)
|
||||
}
|
||||
},
|
||||
authenticate: (
|
||||
client: Client,
|
||||
username: Readonly<string | undefined>,
|
||||
password: Readonly<Buffer | undefined>,
|
||||
callback
|
||||
) => {
|
||||
if (
|
||||
username === 'test' &&
|
||||
password === Buffer.from('test') &&
|
||||
client.version === 4
|
||||
) {
|
||||
callback(null, true)
|
||||
} else {
|
||||
const error = new Error() as AuthenticateError
|
||||
error.returnCode = 1
|
||||
|
||||
callback(error, false)
|
||||
}
|
||||
},
|
||||
authorizePublish: (
|
||||
client: Client | null,
|
||||
packet: PublishPacket,
|
||||
callback
|
||||
) => {
|
||||
if (packet.topic === 'aaaa') {
|
||||
return callback(new Error('wrong topic'))
|
||||
}
|
||||
|
||||
if (packet.topic === 'bbb') {
|
||||
packet.payload = Buffer.from('overwrite packet payload')
|
||||
}
|
||||
|
||||
callback(null)
|
||||
},
|
||||
authorizeSubscribe: (client: Client, sub: Subscription, callback) => {
|
||||
if (sub.topic === 'aaaa') {
|
||||
return callback(new Error('wrong topic'))
|
||||
}
|
||||
|
||||
if (sub.topic === 'bbb') {
|
||||
// overwrites subscription
|
||||
sub.qos = 2
|
||||
}
|
||||
|
||||
callback(null, sub)
|
||||
},
|
||||
authorizeForward: (client: Client, packet: AedesPublishPacket) => {
|
||||
if (packet.topic === 'aaaa' && client.id === 'I should not see this') {
|
||||
return null
|
||||
// also works with return undefined
|
||||
} else if (
|
||||
packet.topic === 'aaaa' &&
|
||||
client.id === 'I should not see this either'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (packet.topic === 'bbb') {
|
||||
packet.payload = Buffer.from('overwrite packet payload')
|
||||
}
|
||||
|
||||
return packet
|
||||
},
|
||||
published: (packet: AedesPublishPacket, client: Client, callback) => {
|
||||
callback(null)
|
||||
callback(new Error())
|
||||
}
|
||||
})
|
||||
|
||||
expectType<Aedes>(broker)
|
||||
|
||||
expectType<Readonly<Brokers>>(broker.brokers)
|
||||
|
||||
expectType<Aedes>(broker.on('closed', () => {}))
|
||||
expectType<Aedes>(broker.on('client', (client: Client) => {}))
|
||||
expectType<Aedes>(broker.on('clientReady', (client: Client) => {}))
|
||||
expectType<Aedes>(broker.on('clientDisconnect', (client: Client) => {}))
|
||||
expectType<Aedes>(broker.on('keepaliveTimeout', (client: Client) => {}))
|
||||
expectType<Aedes>(
|
||||
broker.on('clientError', (client: Client, error: Error) => {})
|
||||
)
|
||||
expectType<Aedes>(
|
||||
broker.on('connectionError', (client: Client, error: Error) => {})
|
||||
)
|
||||
expectType<Aedes>(
|
||||
broker.on('connackSent', (packet: ConnackPacket, client: Client) => {})
|
||||
)
|
||||
expectType<Aedes>(
|
||||
broker.on('ping', (packet: PingreqPacket, client: Client) => {})
|
||||
)
|
||||
expectType<Aedes>(
|
||||
broker.on(
|
||||
'publish',
|
||||
(packet: AedesPublishPacket, client: Client | null) => {}
|
||||
)
|
||||
)
|
||||
expectType<Aedes>(
|
||||
broker.on('ack', (packet: PublishPacket | PubrelPacket, client: Client) => {})
|
||||
)
|
||||
expectType<Aedes>(
|
||||
broker.on('subscribe', (subscriptions: Subscription[], client: Client) => {})
|
||||
)
|
||||
expectType<Aedes>(
|
||||
broker.on('unsubscribe', (unsubscriptions: string[], client: Client) => {})
|
||||
)
|
||||
|
||||
expectType<void>(
|
||||
broker.publish({} as PublishPacket, (error?: Error) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expectType<void>(
|
||||
broker.subscribe(
|
||||
'topic',
|
||||
(packet: AedesPublishPacket, callback: () => void) => {},
|
||||
() => {}
|
||||
)
|
||||
)
|
||||
|
||||
expectType<void>(
|
||||
broker.unsubscribe(
|
||||
'topic',
|
||||
(packet: AedesPublishPacket, callback: () => void) => {},
|
||||
() => {}
|
||||
)
|
||||
)
|
||||
|
||||
expectType<void>(broker.close())
|
||||
expectType<void>(broker.close(() => {}))
|
||||
|
||||
// Aedes client
|
||||
const client = broker.handle({} as Connection, {} as IncomingMessage)
|
||||
const client2 = broker.handle({} as Connection)
|
||||
|
||||
expectType<Client>(client)
|
||||
expectType<Client>(client2)
|
||||
|
||||
expectType<Connection>(client.conn)
|
||||
expectType<IncomingMessage>(client.req!)
|
||||
|
||||
expectType<Client>(client.on('connected', () => {}))
|
||||
expectType<Client>(
|
||||
client.on('error', (error: Error) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expectType<void>(
|
||||
client.publish({} as PublishPacket, (error?: Error) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
)
|
||||
expectType<void>(client.publish({} as PublishPacket))
|
||||
|
||||
expectType<void>(
|
||||
client.subscribe({} as Subscription, (error?: Error) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
)
|
||||
expectType<void>(client.subscribe({} as Subscription))
|
||||
expectType<void>(client.subscribe([] as Subscription[]))
|
||||
expectType<void>(client.subscribe({} as SubscribePacket))
|
||||
|
||||
expectType<void>(
|
||||
client.unsubscribe({} as Subscription, (error?: Error) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
)
|
||||
expectType<void>(client.unsubscribe({} as Subscription))
|
||||
expectType<void>(client.unsubscribe([] as Subscription[]))
|
||||
expectType<void>(client.unsubscribe({} as UnsubscribePacket))
|
||||
|
||||
expectType<void>(client.emptyOutgoingQueue())
|
||||
expectType<void>(client.emptyOutgoingQueue(() => {}))
|
||||
|
||||
expectType<void>(client.close())
|
||||
expectType<void>(client.close(() => {}))
|
||||
+601
@@ -0,0 +1,601 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const memory = require('aedes-persistence')
|
||||
const Faketimers = require('@sinonjs/fake-timers')
|
||||
const { setup, connect, noError } = require('./helper')
|
||||
const aedes = require('../')
|
||||
|
||||
function willConnect (s, opts, connected) {
|
||||
opts = opts || {}
|
||||
opts.will = {
|
||||
topic: 'mywill',
|
||||
payload: Buffer.from('last will'),
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
|
||||
return connect(s, opts, connected)
|
||||
}
|
||||
|
||||
test('delivers a will', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const opts = {}
|
||||
// willConnect populates opts with a will
|
||||
const s = willConnect(setup(),
|
||||
opts,
|
||||
function () {
|
||||
s.conn.destroy()
|
||||
}
|
||||
)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.mq.on('mywill', function (packet, cb) {
|
||||
t.equal(packet.topic, opts.will.topic, 'topic matches')
|
||||
t.same(packet.payload, opts.will.payload, 'payload matches')
|
||||
t.equal(packet.qos, opts.will.qos, 'qos matches')
|
||||
t.equal(packet.retain, opts.will.retain, 'retain matches')
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
test('calling close two times should not deliver two wills', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const opts = {}
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.on('client', function (client) {
|
||||
client.close()
|
||||
client.close()
|
||||
})
|
||||
|
||||
broker.mq.on('mywill', onWill)
|
||||
|
||||
// willConnect populates opts with a will
|
||||
willConnect(setup(broker), opts)
|
||||
|
||||
function onWill (packet, cb) {
|
||||
broker.mq.removeListener('mywill', onWill, function () {
|
||||
broker.mq.on('mywill', function (packet) {
|
||||
t.fail('the will must be delivered only once')
|
||||
})
|
||||
})
|
||||
t.equal(packet.topic, opts.will.topic, 'topic matches')
|
||||
t.same(packet.payload, opts.will.payload, 'payload matches')
|
||||
t.equal(packet.qos, opts.will.qos, 'qos matches')
|
||||
t.equal(packet.retain, opts.will.retain, 'retain matches')
|
||||
cb()
|
||||
}
|
||||
})
|
||||
|
||||
test('delivers old will in case of a crash', function (t) {
|
||||
t.plan(8)
|
||||
|
||||
const persistence = memory()
|
||||
const will = {
|
||||
topic: 'mywill',
|
||||
payload: Buffer.from('last will'),
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
|
||||
persistence.broker = {
|
||||
id: 'anotherBroker'
|
||||
}
|
||||
|
||||
persistence.putWill({
|
||||
id: 'myClientId42'
|
||||
}, will, function (err) {
|
||||
t.error(err, 'no error')
|
||||
|
||||
let authorized = false
|
||||
const interval = 10 // ms, so that the will check happens fast!
|
||||
const broker = aedes({
|
||||
persistence,
|
||||
heartbeatInterval: interval,
|
||||
authorizePublish: function (client, packet, callback) {
|
||||
t.strictSame(client, null, 'client must be null')
|
||||
authorized = true
|
||||
callback(null)
|
||||
}
|
||||
})
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const start = Date.now()
|
||||
|
||||
broker.mq.on('mywill', check)
|
||||
|
||||
function check (packet, cb) {
|
||||
broker.mq.removeListener('mywill', check, function () {
|
||||
broker.mq.on('mywill', function (packet) {
|
||||
t.fail('the will must be delivered only once')
|
||||
})
|
||||
})
|
||||
t.ok(Date.now() - start >= 3 * interval, 'the will needs to be emitted after 3 heartbeats')
|
||||
t.equal(packet.topic, will.topic, 'topic matches')
|
||||
t.same(packet.payload, will.payload, 'payload matches')
|
||||
t.equal(packet.qos, will.qos, 'qos matches')
|
||||
t.equal(packet.retain, will.retain, 'retain matches')
|
||||
t.equal(authorized, true, 'authorization called')
|
||||
cb()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('deliver old will without authorization in case of a crash', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const persistence = memory()
|
||||
const will = {
|
||||
topic: 'mywill',
|
||||
payload: Buffer.from('last will'),
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
|
||||
persistence.broker = {
|
||||
id: 'anotherBroker'
|
||||
}
|
||||
|
||||
persistence.putWill({
|
||||
id: 'myClientId42'
|
||||
}, will, function (err) {
|
||||
t.error(err, 'no error')
|
||||
|
||||
const interval = 10 // ms, so that the will check happens fast!
|
||||
const broker = aedes({
|
||||
persistence,
|
||||
heartbeatInterval: interval,
|
||||
authorizePublish: function (client, packet, callback) {
|
||||
t.strictSame(client, null, 'client must be null')
|
||||
callback(new Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.mq.on('mywill', check)
|
||||
|
||||
function check (packet, cb) {
|
||||
t.fail('received will without authorization')
|
||||
cb()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('delete old broker', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const clock = Faketimers.install()
|
||||
|
||||
const heartbeatInterval = 100
|
||||
const broker = aedes({
|
||||
heartbeatInterval
|
||||
})
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const brokerId = 'dummyBroker'
|
||||
|
||||
broker.brokers[brokerId] = Date.now() - heartbeatInterval * 3.5
|
||||
|
||||
setTimeout(() => {
|
||||
t.equal(broker.brokers[brokerId], undefined, 'Broker deleted')
|
||||
}, heartbeatInterval * 4)
|
||||
|
||||
clock.tick(heartbeatInterval * 4)
|
||||
|
||||
clock.uninstall()
|
||||
})
|
||||
|
||||
test('store the will in the persistence', function (t) {
|
||||
t.plan(5)
|
||||
|
||||
const opts = {
|
||||
clientId: 'abcde'
|
||||
}
|
||||
|
||||
// willConnect populates opts with a will
|
||||
const s = willConnect(setup(), opts)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('client', function () {
|
||||
// this is connack
|
||||
s.broker.persistence.getWill({
|
||||
id: opts.clientId
|
||||
}, function (err, packet) {
|
||||
t.error(err, 'no error')
|
||||
t.same(packet.topic, opts.will.topic, 'will topic matches')
|
||||
t.same(packet.payload, opts.will.payload, 'will payload matches')
|
||||
t.same(packet.qos, opts.will.qos, 'will qos matches')
|
||||
t.same(packet.retain, opts.will.retain, 'will retain matches')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('delete the will in the persistence after publish', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const opts = {
|
||||
clientId: 'abcde'
|
||||
}
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.on('client', function (client) {
|
||||
setImmediate(function () {
|
||||
client.close()
|
||||
})
|
||||
})
|
||||
|
||||
broker.mq.on('mywill', check)
|
||||
|
||||
// willConnect populates opts with a will
|
||||
willConnect(setup(broker), opts)
|
||||
|
||||
function check (packet, cb) {
|
||||
broker.mq.removeListener('mywill', check, function () {
|
||||
broker.persistence.getWill({
|
||||
id: opts.clientId
|
||||
}, function (err, p) {
|
||||
t.error(err, 'no error')
|
||||
t.notOk(p, 'packet is empty')
|
||||
})
|
||||
})
|
||||
cb()
|
||||
}
|
||||
})
|
||||
|
||||
test('delivers a will with authorization', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
let authorized = false
|
||||
const opts = {}
|
||||
// willConnect populates opts with a will
|
||||
const s = willConnect(
|
||||
setup(aedes({
|
||||
authorizePublish: (client, packet, callback) => {
|
||||
authorized = true
|
||||
callback(null)
|
||||
}
|
||||
})),
|
||||
opts,
|
||||
function () {
|
||||
s.conn.destroy()
|
||||
}
|
||||
)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('clientDisconnect', function (client) {
|
||||
t.equal(client.connected, false)
|
||||
})
|
||||
|
||||
s.broker.mq.on('mywill', function (packet, cb) {
|
||||
t.equal(packet.topic, opts.will.topic, 'topic matches')
|
||||
t.same(packet.payload, opts.will.payload, 'payload matches')
|
||||
t.equal(packet.qos, opts.will.qos, 'qos matches')
|
||||
t.equal(packet.retain, opts.will.retain, 'retain matches')
|
||||
t.equal(authorized, true, 'authorization called')
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
test('delivers a will waits for authorization', function (t) {
|
||||
t.plan(6)
|
||||
|
||||
let authorized = false
|
||||
const opts = {}
|
||||
// willConnect populates opts with a will
|
||||
const s = willConnect(
|
||||
setup(aedes({
|
||||
authorizePublish: (client, packet, callback) => {
|
||||
authorized = true
|
||||
setImmediate(() => { callback(null) })
|
||||
}
|
||||
})),
|
||||
opts,
|
||||
function () {
|
||||
s.conn.destroy()
|
||||
}
|
||||
)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('clientDisconnect', function () {
|
||||
t.pass('client is disconnected')
|
||||
})
|
||||
|
||||
s.broker.mq.on('mywill', function (packet, cb) {
|
||||
t.equal(packet.topic, opts.will.topic, 'topic matches')
|
||||
t.same(packet.payload, opts.will.payload, 'payload matches')
|
||||
t.equal(packet.qos, opts.will.qos, 'qos matches')
|
||||
t.equal(packet.retain, opts.will.retain, 'retain matches')
|
||||
t.equal(authorized, true, 'authorization called')
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
test('does not deliver a will without authorization', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
let authorized = false
|
||||
const opts = {}
|
||||
// willConnect populates opts with a will
|
||||
const s = willConnect(
|
||||
setup(aedes({
|
||||
authorizePublish: (username, packet, callback) => {
|
||||
authorized = true
|
||||
callback(new Error())
|
||||
}
|
||||
})),
|
||||
opts,
|
||||
function () {
|
||||
s.conn.destroy()
|
||||
}
|
||||
)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('clientDisconnect', function () {
|
||||
t.equal(authorized, true, 'authorization called')
|
||||
})
|
||||
|
||||
s.broker.mq.on('mywill', function (packet, cb) {
|
||||
t.fail('received will without authorization')
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
test('does not deliver a will without authentication', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
let authenticated = false
|
||||
const opts = {}
|
||||
// willConnect populates opts with a will
|
||||
const s = willConnect(
|
||||
setup(aedes({
|
||||
authenticate: (client, username, password, callback) => {
|
||||
authenticated = true
|
||||
callback(new Error(), false)
|
||||
}
|
||||
})),
|
||||
opts
|
||||
)
|
||||
t.teardown(s.broker.close.bind(s.broker))
|
||||
|
||||
s.broker.on('clientError', function () {
|
||||
t.equal(authenticated, true, 'authentication called')
|
||||
t.end()
|
||||
})
|
||||
|
||||
s.broker.mq.on('mywill', function (packet, cb) {
|
||||
t.fail('received will without authentication')
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
test('does not deliver will if broker is closed during authentication', function (t) {
|
||||
t.plan(0)
|
||||
|
||||
const opts = { keepalive: 1 }
|
||||
|
||||
const broker = aedes({
|
||||
authenticate: function (client, username, password, callback) {
|
||||
setTimeout(function () {
|
||||
callback(null, true)
|
||||
})
|
||||
broker.close()
|
||||
}
|
||||
})
|
||||
|
||||
broker.on('keepaliveTimeout', function () {
|
||||
t.fail('keepalive timer shoud not be set')
|
||||
})
|
||||
|
||||
broker.mq.on('mywill', function (packet, cb) {
|
||||
t.fail('Received will when it was not expected')
|
||||
cb()
|
||||
})
|
||||
|
||||
willConnect(setup(broker), opts)
|
||||
})
|
||||
|
||||
// [MQTT-3.14.4-3]
|
||||
test('does not deliver will when client sends a DISCONNECT', function (t) {
|
||||
t.plan(0)
|
||||
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = noError(willConnect(setup(broker), {}, function () {
|
||||
s.inStream.end({
|
||||
cmd: 'disconnect'
|
||||
})
|
||||
}), t)
|
||||
|
||||
s.broker.mq.on('mywill', function (packet, cb) {
|
||||
t.fail(packet)
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
test('deletes from persistence on DISCONNECT', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const opts = {
|
||||
clientId: 'abcde'
|
||||
}
|
||||
const broker = aedes()
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const s = noError(willConnect(setup(broker), opts, function () {
|
||||
s.inStream.end({
|
||||
cmd: 'disconnect'
|
||||
})
|
||||
}), t)
|
||||
|
||||
s.broker.persistence.getWill({
|
||||
id: opts.clientId
|
||||
}, function (err, packet) {
|
||||
t.error(err, 'no error')
|
||||
t.notOk(packet)
|
||||
})
|
||||
})
|
||||
|
||||
test('does not store multiple will with same clientid', function (t) {
|
||||
t.plan(4)
|
||||
|
||||
const opts = { clientId: 'abcde' }
|
||||
|
||||
const broker = aedes()
|
||||
|
||||
let s = noError(willConnect(setup(broker), opts, function () {
|
||||
// gracefully close client so no will is sent
|
||||
s.inStream.end({
|
||||
cmd: 'disconnect'
|
||||
})
|
||||
}), t)
|
||||
|
||||
broker.on('clientDisconnect', function (client) {
|
||||
// reconnect same client with will
|
||||
s = willConnect(setup(broker), opts, function () {
|
||||
// check that there are not 2 will messages for the same clientid
|
||||
s.broker.persistence.delWill({ id: opts.clientId }, function (err, packet) {
|
||||
t.error(err, 'no error')
|
||||
t.equal(packet.clientId, opts.clientId, 'will packet found')
|
||||
s.broker.persistence.delWill({ id: opts.clientId }, function (err, packet) {
|
||||
t.error(err, 'no error')
|
||||
t.equal(!!packet, false, 'no duplicated packets')
|
||||
broker.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('don\'t delivers a will if broker alive', function (t) {
|
||||
const persistence = memory()
|
||||
const will = {
|
||||
topic: 'mywill',
|
||||
payload: Buffer.from('last will'),
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
|
||||
const oldBroker = 'broker1'
|
||||
|
||||
persistence.broker = {
|
||||
id: oldBroker
|
||||
}
|
||||
|
||||
persistence.putWill({
|
||||
id: 'myClientId42'
|
||||
}, will, function (err) {
|
||||
t.error(err, 'no error')
|
||||
|
||||
const opts = {
|
||||
persistence,
|
||||
heartbeatInterval: 10
|
||||
}
|
||||
|
||||
let count = 0
|
||||
|
||||
const broker = aedes(opts)
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
const streamWill = persistence.streamWill
|
||||
persistence.streamWill = function () {
|
||||
// don't pass broker.brokers to streamWill
|
||||
return streamWill.call(persistence)
|
||||
}
|
||||
|
||||
broker.mq.on('mywill', function (packet, cb) {
|
||||
t.fail('Will received')
|
||||
cb()
|
||||
})
|
||||
|
||||
broker.mq.on('$SYS/+/heartbeat', function () {
|
||||
t.pass('Heartbeat received')
|
||||
broker.brokers[oldBroker] = Date.now()
|
||||
|
||||
if (++count === 5) {
|
||||
t.end()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('handle will publish error', function (t) {
|
||||
t.plan(2)
|
||||
const persistence = memory()
|
||||
const will = {
|
||||
topic: 'mywill',
|
||||
payload: Buffer.from('last will'),
|
||||
qos: 0,
|
||||
retain: false
|
||||
}
|
||||
|
||||
persistence.broker = {
|
||||
id: 'broker1'
|
||||
}
|
||||
|
||||
persistence.putWill({
|
||||
id: 'myClientId42'
|
||||
}, will, function (err) {
|
||||
t.error(err, 'no error')
|
||||
|
||||
const opts = {
|
||||
persistence,
|
||||
heartbeatInterval: 10
|
||||
}
|
||||
|
||||
persistence.delWill = function (client, cb) {
|
||||
cb(new Error('Throws error'))
|
||||
}
|
||||
|
||||
const broker = aedes(opts)
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.once('error', function (err) {
|
||||
t.equal('Throws error', err.message, 'throws error')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('handle will publish error 2', function (t) {
|
||||
t.plan(2)
|
||||
const persistence = memory()
|
||||
const will = {
|
||||
topic: 'mywill',
|
||||
payload: Buffer.from('last will'),
|
||||
qos: 0,
|
||||
retain: true
|
||||
}
|
||||
|
||||
persistence.broker = {
|
||||
id: 'broker1'
|
||||
}
|
||||
|
||||
persistence.putWill({
|
||||
id: 'myClientId42'
|
||||
}, will, function (err) {
|
||||
t.error(err, 'no error')
|
||||
|
||||
const opts = {
|
||||
persistence,
|
||||
heartbeatInterval: 10
|
||||
}
|
||||
|
||||
persistence.storeRetained = function (packet, cb) {
|
||||
cb(new Error('Throws error'))
|
||||
}
|
||||
|
||||
const broker = aedes(opts)
|
||||
t.teardown(broker.close.bind(broker))
|
||||
|
||||
broker.once('error', function (err) {
|
||||
t.equal('Throws error', err.message, 'throws error')
|
||||
})
|
||||
})
|
||||
})
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"standard"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"env": { "node": true },
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"project": "./types/tsconfig.json",
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"rules": {
|
||||
"linebreak-style": ["warn", "unix"],
|
||||
"no-console": "off",
|
||||
"semi": ["error", "never"],
|
||||
"import/export": "off", // this errors on multiple exports (overload interfaces)
|
||||
"@typescript-eslint/indent": ["error", 2]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.d.ts", "*.test-d.ts"],
|
||||
"rules": {
|
||||
"no-dupe-class-members": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/triple-slash-reference": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.test-d.ts"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"node/handle-callback-err": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
},
|
||||
"globals": {
|
||||
"NodeJS": "readonly"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { IncomingMessage } from 'node:http'
|
||||
import {
|
||||
PublishPacket,
|
||||
SubscribePacket,
|
||||
Subscription,
|
||||
Subscriptions,
|
||||
UnsubscribePacket
|
||||
} from './packet'
|
||||
import { Connection } from './instance'
|
||||
import { EventEmitter } from 'node:events'
|
||||
|
||||
export interface Client extends EventEmitter {
|
||||
id: Readonly<string>;
|
||||
clean: Readonly<boolean>;
|
||||
version: Readonly<number>;
|
||||
conn: Connection;
|
||||
req?: IncomingMessage;
|
||||
connecting: Readonly<boolean>;
|
||||
connected: Readonly<boolean>;
|
||||
closed: Readonly<boolean>;
|
||||
|
||||
on(event: 'connected', listener: () => void): this;
|
||||
on(event: 'error', listener: (error: Error) => void): this;
|
||||
|
||||
publish(message: PublishPacket, callback?: (error?: Error) => void): void;
|
||||
subscribe(
|
||||
subscriptions:
|
||||
| Subscriptions
|
||||
| Subscription
|
||||
| Subscription[]
|
||||
| SubscribePacket,
|
||||
callback?: (error?: Error) => void
|
||||
): void;
|
||||
unsubscribe(
|
||||
topicObjects:
|
||||
| Subscriptions
|
||||
| Subscription
|
||||
| Subscription[]
|
||||
| UnsubscribePacket,
|
||||
callback?: (error?: Error) => void
|
||||
): void;
|
||||
close(callback?: () => void): void;
|
||||
emptyOutgoingQueue(callback?: () => void): void;
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { Duplex } from 'node:stream'
|
||||
import { Socket } from 'node:net'
|
||||
import { IncomingMessage } from 'http'
|
||||
import { Client } from './client'
|
||||
import type {
|
||||
AedesPublishPacket,
|
||||
ConnectPacket,
|
||||
ConnackPacket,
|
||||
Subscription,
|
||||
PingreqPacket,
|
||||
PublishPacket,
|
||||
PubrelPacket
|
||||
} from './packet'
|
||||
import { EventEmitter } from 'node:events'
|
||||
|
||||
type LastHearthbeatTimestamp = Date;
|
||||
|
||||
export interface Brokers {
|
||||
[brokerId: string]: LastHearthbeatTimestamp;
|
||||
}
|
||||
|
||||
export type Connection = Duplex | Socket;
|
||||
|
||||
/* eslint no-unused-vars: 0 */
|
||||
export const enum AuthErrorCode {
|
||||
UNNACCEPTABLE_PROTOCOL = 1,
|
||||
IDENTIFIER_REJECTED = 2,
|
||||
SERVER_UNAVAILABLE = 3,
|
||||
BAD_USERNAME_OR_PASSWORD = 4,
|
||||
NOT_AUTHORIZED = 5,
|
||||
}
|
||||
|
||||
export type AuthenticateError = Error & { returnCode: AuthErrorCode };
|
||||
|
||||
type PreConnectHandler = (
|
||||
client: Client,
|
||||
packet: ConnectPacket,
|
||||
callback: (error: Error | null, success: boolean) => void
|
||||
) => void;
|
||||
|
||||
type AuthenticateHandler = (
|
||||
client: Client,
|
||||
username: Readonly<string | undefined>,
|
||||
password: Readonly<Buffer | undefined>,
|
||||
done: (error: AuthenticateError | null, success: boolean | null) => void
|
||||
) => void;
|
||||
|
||||
type AuthorizePublishHandler = (
|
||||
client: Client | null,
|
||||
packet: PublishPacket,
|
||||
callback: (error?: Error | null) => void
|
||||
) => void;
|
||||
|
||||
type AuthorizeSubscribeHandler = (
|
||||
client: Client,
|
||||
subscription: Subscription,
|
||||
callback: (error: Error | null, subscription?: Subscription | null) => void
|
||||
) => void;
|
||||
|
||||
type AuthorizeForwardHandler = (
|
||||
client: Client,
|
||||
packet: AedesPublishPacket
|
||||
) => AedesPublishPacket | null | void;
|
||||
|
||||
type PublishedHandler = (
|
||||
packet: AedesPublishPacket,
|
||||
client: Client,
|
||||
callback: (error?: Error | null) => void
|
||||
) => void;
|
||||
|
||||
export interface AedesOptions {
|
||||
mq?: any;
|
||||
id?: string;
|
||||
persistence?: any;
|
||||
concurrency?: number;
|
||||
heartbeatInterval?: number;
|
||||
connectTimeout?: number;
|
||||
keepaliveLimit?: number;
|
||||
queueLimit?: number;
|
||||
maxClientsIdLength?: number;
|
||||
preConnect?: PreConnectHandler;
|
||||
authenticate?: AuthenticateHandler;
|
||||
authorizePublish?: AuthorizePublishHandler;
|
||||
authorizeSubscribe?: AuthorizeSubscribeHandler;
|
||||
authorizeForward?: AuthorizeForwardHandler;
|
||||
published?: PublishedHandler;
|
||||
}
|
||||
|
||||
export default class Aedes extends EventEmitter {
|
||||
id: Readonly<string>
|
||||
connectedClients: Readonly<number>
|
||||
closed: Readonly<boolean>
|
||||
brokers: Readonly<Brokers>
|
||||
|
||||
constructor(option?: AedesOptions);
|
||||
handle: (stream: Connection, request?: IncomingMessage) => Client
|
||||
|
||||
on(event: 'closed', listener: () => void): this;
|
||||
on(
|
||||
event: 'client' | 'clientReady' | 'clientDisconnect' | 'keepaliveTimeout',
|
||||
listener: (client: Client) => void
|
||||
): this;
|
||||
|
||||
on(
|
||||
event: 'clientError' | 'connectionError',
|
||||
listener: (client: Client, error: Error) => void
|
||||
): this;
|
||||
|
||||
on(
|
||||
event: 'connackSent',
|
||||
listener: (packet: ConnackPacket, client: Client) => void
|
||||
): this;
|
||||
|
||||
on(
|
||||
event: 'ping',
|
||||
listener: (packet: PingreqPacket, client: Client) => void
|
||||
): this;
|
||||
|
||||
on(
|
||||
event: 'publish',
|
||||
listener: (packet: AedesPublishPacket, client: Client | null) => void
|
||||
): this;
|
||||
|
||||
on(
|
||||
event: 'ack',
|
||||
listener: (packet: PublishPacket | PubrelPacket, client: Client) => void
|
||||
): this;
|
||||
|
||||
on(
|
||||
event: 'subscribe',
|
||||
listener: (subscriptions: Subscription[], client: Client) => void
|
||||
): this;
|
||||
|
||||
on(
|
||||
event: 'unsubscribe',
|
||||
listener: (unsubscriptions: string[], client: Client) => void
|
||||
): this;
|
||||
|
||||
publish(packet: PublishPacket, callback: (error?: Error) => void): void;
|
||||
subscribe(
|
||||
topic: string,
|
||||
deliverfunc: (packet: AedesPublishPacket, callback: () => void) => void,
|
||||
callback: () => void
|
||||
): void;
|
||||
|
||||
unsubscribe(
|
||||
topic: string,
|
||||
deliverfunc: (packet: AedesPublishPacket, callback: () => void) => void,
|
||||
callback: () => void
|
||||
): void;
|
||||
|
||||
close(callback?: () => void): void;
|
||||
|
||||
preConnect: PreConnectHandler
|
||||
authenticate: AuthenticateHandler
|
||||
authorizePublish: AuthorizePublishHandler
|
||||
authorizeSubscribe: AuthorizeSubscribeHandler
|
||||
authorizeForward: AuthorizeForwardHandler
|
||||
published: PublishedHandler
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { AedesPacket } from 'aedes-packet'
|
||||
import {
|
||||
IConnackPacket,
|
||||
IConnectPacket,
|
||||
IPingreqPacket,
|
||||
IPublishPacket,
|
||||
IPubrelPacket,
|
||||
ISubscribePacket,
|
||||
ISubscription,
|
||||
IUnsubscribePacket
|
||||
} from 'mqtt-packet'
|
||||
import { Client } from './client'
|
||||
|
||||
export type SubscribePacket = ISubscribePacket & { cmd: 'subscribe' };
|
||||
export type UnsubscribePacket = IUnsubscribePacket & { cmd: 'unsubscribe' };
|
||||
export type Subscription = ISubscription & { clientId?: Client['id'] };
|
||||
export type Subscriptions = { subscriptions: Subscription[] };
|
||||
|
||||
export type PublishPacket = IPublishPacket & { cmd: 'publish' };
|
||||
|
||||
export type ConnectPacket = IConnectPacket & { cmd: 'connect' };
|
||||
export type ConnackPacket = IConnackPacket & { cmd: 'connack' };
|
||||
|
||||
export type PubrelPacket = IPubrelPacket & { cmd: 'pubrel' };
|
||||
export type PingreqPacket = IPingreqPacket & { cmd: 'pingreq' };
|
||||
|
||||
export type AedesPublishPacket = PublishPacket & AedesPacket;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"removeComments": true,
|
||||
"typeRoots" : ["./", "../node_modules/@types/"]
|
||||
},
|
||||
"include": [
|
||||
"../test/types/*.test-d.ts",
|
||||
"./*.d.ts",
|
||||
"../aedes.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user