首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
repo_token: 0zzWBuyAWoQxHHR0JQ4jdZ12CPdvhiolr
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,6 @@
ts: false
jsx: false
flow: false
coverage: true
strict: true
check-coverage: false
+84
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,331 @@
<!-- markdownlint-disable MD013 -->
# Aedes
![ci](https://github.com/moscajs/aedes/workflows/ci/badge.svg)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/moscajs/aedes/graphs/commit-activity)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/moscajs/aedes/pulls)\
[![Coverage Status](https://coveralls.io/repos/moscajs/aedes/badge.svg?branch=main&service=github)](https://coveralls.io/github/moscajs/aedes?branch=main)
[![Known Vulnerabilities](https://snyk.io/test/github/moscajs/aedes/badge.svg)](https://snyk.io/test/github/moscajs/aedes)\
![node](https://img.shields.io/node/v/aedes)
[![NPM version](https://img.shields.io/npm/v/aedes.svg?style=flat)](https://www.npmjs.com/aedes)
[![NPM downloads](https://img.shields.io/npm/dm/aedes.svg?style=flat)](https://www.npmjs.com/aedes)
[![opencollective](https://opencollective.com/aedes/donate/button.png)](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
[![Help wanted](https://img.shields.io/github/labels/moscajs/aedes/help%20wanted)](https://github.com/moscajs/aedes/labels/help%20wanted)
[![Contributors](https://img.shields.io/github/contributors/moscajs/aedes)](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:
[![Backers](https://opencollective.com/aedes/backers.svg?avatarHeight=64&width=890&button=false)](https://opencollective.com/aedes#backers)
### Sponsors
Become a sponsor to get your logo on our README on Github
[![Sponsor](https://opencollective.com/aedes/sponsors.svg)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+865
View File
@@ -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
View File
@@ -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)
})
})
}
File diff suppressed because it is too large Load Diff
+177
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
]
}