├── .npmrc
├── .mocharc.json
├── .github
├── FUNDING.yml
├── workflows
│ ├── lint.yml
│ └── npm-test.yml
└── stale.yml
├── .npmignore
├── examples
├── client.js
├── observe_client.js
├── server.js
├── client_and_server.js
├── observe_server.js
├── req_with_payload.js
├── observe_eclipse.js
├── multicast_client_server.js
├── blockwise.js
├── json.js
├── delayed_response.js
├── proxy.js
└── blockwise_put.js
├── test
├── mocha.opts
├── common.ts
├── retry_send.ts
├── parameters.ts
├── cache.ts
├── ipv6.ts
├── segmentation.ts
├── helpers.ts
├── proxy.ts
├── end-to-end.ts
├── share-socket.ts
└── agent.ts
├── .eslintrc
├── tsconfig.json
├── .gitignore
├── LICENSE.md
├── CONTRIBUTING.md
├── lib
├── incoming_message.ts
├── observe_read_stream.ts
├── block.ts
├── observe_write_stream.ts
├── retry_send.ts
├── cache.ts
├── middlewares.ts
├── segmentation.ts
├── outgoing_message.ts
├── parameters.ts
├── option_converter.ts
├── helpers.ts
└── agent.ts
├── package.json
├── models
└── models.ts
├── index.ts
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeout": 0,
3 | "exit": true,
4 | "reporter": "spec"
5 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [Apollon77, JKRhb]
2 | custom: ['https://paypal.me/jkrhb', 'https://paypal.me/Apollon77']
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | .vscode
4 | *code-workspace
5 | .idea
6 | .nyc_output
7 | coverage
8 | *.tgz
9 | .github
10 | .mocharc.json
11 | .nycrc.json
12 | dist/test
13 | test
14 |
--------------------------------------------------------------------------------
/examples/client.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 | const req = coap.request('coap://localhost/Matteo')
3 |
4 | req.on('response', (res) => {
5 | res.pipe(process.stdout)
6 | })
7 |
8 | req.end()
9 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter dot
2 | --require test/common
3 | --ui bdd
4 | --growl
5 | --check-leaks
6 | --colors
7 | --require ts-node/register
8 | --require source-map-support/register
9 | --recursive
10 |
--------------------------------------------------------------------------------
/examples/observe_client.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 | const req = coap.request({
3 | observe: true
4 | })
5 |
6 | req.on('response', (res) => {
7 | res.pipe(process.stdout)
8 | })
9 |
10 | req.end()
11 |
--------------------------------------------------------------------------------
/examples/server.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 | const server = coap.createServer()
3 |
4 | server.on('request', (req, res) => {
5 | res.end('Hello ' + req.url.split('/')[1] + '\n')
6 | })
7 |
8 | server.listen(() => {
9 | console.log('server started')
10 | })
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard-with-typescript",
3 | "parserOptions": {
4 | "project": "./tsconfig.json"
5 | },
6 | "rules": {
7 | "indent": "off",
8 | "@typescript-eslint/indent": ["error", 4]
9 | },
10 | "env": {
11 | "mocha": true
12 | },
13 | "ignorePatterns": ["dist", "coverage", ".nyc_output", "examples"]
14 | }
15 |
--------------------------------------------------------------------------------
/test/common.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | let portCounter = 9042
10 | export function nextPort (): number {
11 | return ++portCounter
12 | }
13 |
--------------------------------------------------------------------------------
/examples/client_and_server.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 |
3 | coap.createServer((req, res) => {
4 | res.end('Hello ' + req.url.split('/')[1] + '\n')
5 | }).listen(() => {
6 | const req = coap.request('coap://localhost/Matteo')
7 |
8 | req.on('response', (res) => {
9 | res.pipe(process.stdout)
10 | res.on('end', () => {
11 | process.exit(0)
12 | })
13 | })
14 |
15 | req.end()
16 | })
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "allowJs": true,
5 | "checkJs": true,
6 | "noEmitOnError": false,
7 | "sourceMap": true,
8 | "strictNullChecks": true,
9 | "esModuleInterop": true,
10 | "alwaysStrict": true,
11 | "target": "es6",
12 | "module": "commonjs",
13 | "outDir": "dist",
14 | "lib": [
15 | "es6",
16 | "dom"
17 | ]
18 | },
19 | "files": ["index.ts"],
20 | "include": ["./lib", "./test", "./models"]
21 | }
22 |
--------------------------------------------------------------------------------
/examples/observe_server.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 | const server = coap.createServer()
3 |
4 | server.on('request', (req, res) => {
5 | if (req.headers.Observe !== 0) {
6 | return res.end(new Date().toISOString() + '\n')
7 | }
8 |
9 | const interval = setInterval(() => {
10 | res.write(new Date().toISOString() + '\n')
11 | }, 1000)
12 |
13 | res.on('finish', () => {
14 | clearInterval(interval)
15 | })
16 | })
17 |
18 | server.listen(() => {
19 | console.log('server started')
20 | })
21 |
--------------------------------------------------------------------------------
/examples/req_with_payload.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 |
3 | coap.createServer((req, res) => {
4 | res.end('Hello ' + req.url.split('/')[1] + '\nMessage payload:\n' + req.payload + '\n')
5 | }).listen(() => {
6 | const req = coap.request('coap://localhost/Matteo')
7 |
8 | const payload = {
9 | title: 'this is a test payload',
10 | body: 'containing nothing useful'
11 | }
12 |
13 | req.write(JSON.stringify(payload))
14 |
15 | req.on('response', (res) => {
16 | res.pipe(process.stdout)
17 | res.on('end', () => {
18 | process.exit(0)
19 | })
20 | })
21 |
22 | req.end()
23 | })
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependency directories
2 | node_modules
3 | jspm_packages
4 |
5 | # mac files
6 | .DS_Store
7 |
8 | # vim swap files
9 | *.swp
10 |
11 | # webstorm
12 | .idea
13 |
14 | # vscode
15 | .vscode
16 | *code-workspace
17 |
18 | # Logs
19 | logs
20 | *.log
21 | npm-debug.log*
22 | results
23 |
24 | # Runtime data
25 | pids
26 | *.pid
27 | *.seed
28 |
29 | # lock files
30 | yarn.lock
31 | package-lock.json
32 |
33 | # Coverage directory used by tools like istanbul
34 | coverage
35 |
36 | # nyc test coverage
37 | .nyc_output
38 |
39 | # Directory for instrumented libs generated by jscoverage/JSCover
40 | lib-cov
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | #local builds
46 | *.tgz
47 |
48 | # typescript output
49 | dist
50 |
--------------------------------------------------------------------------------
/examples/observe_eclipse.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 |
3 | const statusRequest = coap.request({
4 | method: 'GET',
5 | host: 'californium.eclipseprojects.io',
6 | pathname: 'obs',
7 | observe: true,
8 | confirmable: true
9 | })
10 |
11 | let responseCounter = 0
12 |
13 | statusRequest.on('response', res => {
14 | res.on('error', err => {
15 | console.error('Error by receiving: ' + err)
16 | this.emit('error', 'Error by receiving: ' + err)
17 | res.close()
18 | })
19 |
20 | res.on('data', chunk => {
21 | console.log(`Server time: ${chunk.toString()}`)
22 | responseCounter++
23 | if (responseCounter >= 5) {
24 | console.log('Successfully received five responses. Closing the ObserveStream.')
25 | res.close()
26 | }
27 | })
28 | })
29 | statusRequest.end()
30 |
--------------------------------------------------------------------------------
/examples/multicast_client_server.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 | const server = coap.createServer({
3 | multicastAddress: '224.0.1.186'
4 | })
5 | const server2 = coap.createServer({
6 | multicastAddress: '224.0.1.186'
7 | })
8 |
9 | // Create servers
10 | server.listen(5683, () => {
11 | console.log('Server 1 is listening')
12 | })
13 |
14 | server2.listen(5683, () => {
15 | console.log('Server 2 is listening')
16 | })
17 |
18 | server.on('request', (msg, res) => {
19 | console.log('Server 1 has received message')
20 | res.end('Ok')
21 |
22 | server.close()
23 | })
24 |
25 | server2.on('request', (msg, res) => {
26 | console.log('Server 2 has received message')
27 | res.end('Ok')
28 |
29 | server2.close()
30 | })
31 |
32 | // Send multicast message
33 | coap.request({
34 | host: '224.0.1.186',
35 | multicast: true,
36 | multicastTimeout: 2000
37 | }).end()
38 |
--------------------------------------------------------------------------------
/examples/blockwise.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 |
3 | coap.createServer((req, res) => {
4 | // FIXME: This has became a bit ugly due to the
5 | // replacement of the depracated url.parse
6 | // with the URL constructor.
7 | // This part of the exmample should be replaced
8 | // once a nicer solution has been found.
9 |
10 | const splitURL = req.url.split('?')
11 | const time = parseInt(splitURL[1].split('=')[1])
12 | const pathname = splitURL[0].split('/')[1]
13 |
14 | res.end(new Array(time + 1).join(pathname + ' '))
15 | }).listen(() => {
16 | const req = coap.request('coap://localhost/repeat-me?t=400')
17 |
18 | // edit this to adjust max packet
19 | req.setOption('Block2', Buffer.of(0x2))
20 |
21 | req.on('response', (res) => {
22 | res.pipe(process.stdout)
23 | res.on('end', () => {
24 | process.exit(0)
25 | })
26 | })
27 |
28 | req.end()
29 | })
30 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: StandardJS Linter
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | lint:
11 |
12 | runs-on: ${{ matrix.os }}
13 |
14 | strategy:
15 | matrix:
16 | node-version: [18.x]
17 | os: [ubuntu-latest]
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | # Caches NPM and the node_modules folder for faster builds
23 | - name: Cache node modules
24 | uses: actions/cache@v3
25 | with:
26 | path: ~/.npm
27 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
28 | restore-keys: |
29 | ${{ runner.os }}-node-
30 |
31 | - name: Use Node.js ${{ matrix.node-version }}
32 | uses: actions/setup-node@v1
33 | with:
34 | node-version: ${{ matrix.node-version }}
35 |
36 | - name: Install dependencies
37 | run: npm install
38 |
39 | - name: Use linter
40 | run: npm run lint
41 |
--------------------------------------------------------------------------------
/examples/json.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 | const bl = require('bl')
3 |
4 | coap.createServer((req, res) => {
5 | if (req.headers.Accept !== 'application/json') {
6 | res.code = '4.06'
7 | return res.end()
8 | }
9 |
10 | res.setOption('Content-Format', 'application/json')
11 |
12 | res.end(JSON.stringify({ hello: 'world' }))
13 | }).listen(() => {
14 | coap
15 | .request({
16 | pathname: '/Matteo',
17 | options: {
18 | Accept: 'application/json'
19 | }
20 | })
21 | .on('response', (res) => {
22 | console.log('response code', res.code)
23 | if (res.code !== '2.05') {
24 | return process.exit(1)
25 | }
26 |
27 | res.pipe(bl((err, data) => {
28 | if (err != null) {
29 | process.exit(1)
30 | } else {
31 | const json = JSON.parse(data)
32 | console.log(json)
33 | process.exit(0)
34 | }
35 | }))
36 | })
37 | .end()
38 | })
39 |
--------------------------------------------------------------------------------
/examples/delayed_response.js:
--------------------------------------------------------------------------------
1 | const coap = require('../') // or coap
2 |
3 | coap.createServer((req, res) => {
4 | // simulate delayed response
5 | setTimeout(() => {
6 | res.setOption('Block2', Buffer.of(2))
7 | res.end('Hello ' + req.url.split('/')[1] + '\nMessage payload:\n' + req.payload + '\n')
8 | }, 1500)
9 | }).listen(() => {
10 | const coapConnection = {
11 | host: 'localhost',
12 | pathname: '/yo',
13 | method: 'GET',
14 | confirmable: true
15 | }
16 | const req = coap.request(coapConnection)
17 | req.write('182374238472637846278346827346827346827346827346827346782346287346872346283746283462837462873468273462873462387462387182374238472637846278346827346827346827346827346827346782346287346872346283746283462837462873468273462873462387462387')
18 |
19 | req.on('response', (res) => {
20 | res.pipe(process.stdout)
21 | res.on('end', () => {
22 | process.exit(0)
23 | })
24 | })
25 |
26 | req.end()
27 | })
28 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright (c) 2013-2022 node-coap contributors
5 | ----------------------------------------------
6 |
7 | 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:
8 |
9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10 |
11 | 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.
12 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # node-coap is an OPEN Open Source Project
2 |
3 | -----------------------------------------
4 |
5 | ## What?
6 |
7 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project.
8 |
9 | ## Rules
10 |
11 | There are a few basic ground-rules for contributors:
12 |
13 | 1. **No `--force` pushes** or modifying the Git history in any way.
14 | 1. **Non-master branches** ought to be used for ongoing work.
15 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors.
16 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor.
17 | 1. Contributors should attempt to adhere to the prevailing code-style.
18 |
19 | ## Releases
20 |
21 | Declaring formal releases remains the prerogative of the project maintainer.
22 |
23 | ## Changes to this arrangement
24 |
25 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change.
26 |
27 | -----------------------------------------
28 |
--------------------------------------------------------------------------------
/test/retry_send.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { parameters } from '../index'
10 | import RetrySend from '../lib/retry_send'
11 | import { expect } from 'chai'
12 |
13 | describe('RetrySend', function () {
14 | it('should use the default retry count', function () {
15 | const result = new RetrySend({}, 1234, 'localhost')
16 | expect(result._maxRetransmit).to.eql(parameters.maxRetransmit)
17 | })
18 |
19 | it('should use a custom retry count', function () {
20 | const result = new RetrySend({}, 1234, 'localhost', 55)
21 | expect(result._maxRetransmit).to.eql(55)
22 | })
23 |
24 | it('should use default retry count, using the retry_send factory method', function () {
25 | const result = new RetrySend({}, 1234, 'localhost')
26 | expect(result._maxRetransmit).to.eql(parameters.maxRetransmit)
27 | })
28 |
29 | it('should use a custom retry count, using the retry_send factory method', function () {
30 | const result = new RetrySend({}, 1234, 'localhost', 55)
31 | expect(result._maxRetransmit).to.eql(55)
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/.github/workflows/npm-test.yml:
--------------------------------------------------------------------------------
1 | name: Build Status
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | # Cancel previous PR/branch runs when a new commit is pushed
10 | concurrency:
11 | group: ${{ github.ref }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | build:
16 |
17 | runs-on: ${{ matrix.os }}
18 |
19 | strategy:
20 | matrix:
21 | node-version: [18.x, 20.x, 22.x]
22 | os: [ubuntu-latest, windows-latest, macos-latest]
23 |
24 | steps:
25 | - uses: actions/checkout@v2
26 |
27 | # Caches NPM and the node_modules folder for faster builds
28 | - name: Cache node modules
29 | uses: actions/cache@v3
30 | with:
31 | path: ~/.npm
32 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
33 | restore-keys: |
34 | ${{ runner.os }}-node-
35 |
36 | - name: Use Node.js ${{ matrix.node-version }}
37 | uses: actions/setup-node@v1
38 | with:
39 | node-version: ${{ matrix.node-version }}
40 |
41 | - name: Install dependencies
42 | run: npm install
43 |
44 | - name: Perform tests and generate coverage report
45 | run: npm run coverage
46 |
47 | - name: Coveralls
48 | uses: coverallsapp/github-action@master
49 | with:
50 | github-token: ${{ secrets.GITHUB_TOKEN }}
51 | continue-on-error: true
52 |
--------------------------------------------------------------------------------
/lib/incoming_message.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import type { CoapMethod, OptionName } from 'coap-packet'
10 | import type { AddressInfo } from 'net'
11 | import { Readable } from 'readable-stream'
12 | import type { ReadableOptions } from 'readable-stream'
13 | import type { CoapPacket, OptionValue } from '../models/models'
14 | import { packetToMessage } from './helpers'
15 |
16 | class IncomingMessage extends Readable {
17 | rsinfo: AddressInfo
18 | outSocket?: AddressInfo
19 | _packet: CoapPacket
20 | _payloadIndex: number
21 | url: string
22 | payload: Buffer
23 | headers: Partial>
24 | method: CoapMethod
25 | code: string
26 |
27 | constructor (packet: CoapPacket, rsinfo: AddressInfo, outSocket?: AddressInfo, options?: ReadableOptions) {
28 | super(options)
29 |
30 | packetToMessage(this, packet)
31 |
32 | this.rsinfo = rsinfo
33 | this.outSocket = outSocket
34 |
35 | this._packet = packet
36 | this._payloadIndex = 0
37 | }
38 |
39 | _read (size: number): void {
40 | const end = this._payloadIndex + size
41 | const start = this._payloadIndex
42 | const payload = this._packet.payload
43 | let buf: any = null
44 |
45 | if (payload != null && start < payload.length) {
46 | buf = payload.slice(start, end)
47 | }
48 |
49 | this._payloadIndex = end
50 | this.push(buf)
51 | }
52 | }
53 |
54 | export default IncomingMessage
55 |
--------------------------------------------------------------------------------
/test/parameters.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { parameters, defaultTiming, updateTiming } from '../index'
10 | import { ParametersUpdate } from '../models/models'
11 | import { expect } from 'chai'
12 |
13 | describe('Parameters', function () {
14 | afterEach(function () {
15 | defaultTiming()
16 | })
17 |
18 | it('should ignore empty parameter', function () {
19 | // WHEN
20 | updateTiming()
21 |
22 | // THEN
23 | expect(parameters.maxRTT).to.eql(202)
24 | expect(parameters.exchangeLifetime).to.eql(247)
25 | expect(parameters.maxTransmitSpan).to.eql(45)
26 | expect(parameters.maxTransmitWait).to.eql(93)
27 | })
28 |
29 | it('should verify custom timings', function () {
30 | // GIVEN
31 | const coapTiming: ParametersUpdate = {
32 | ackTimeout: 1,
33 | ackRandomFactor: 2,
34 | maxRetransmit: 3,
35 | maxLatency: 5,
36 | piggybackReplyMs: 6
37 | }
38 |
39 | // WHEN
40 | updateTiming(coapTiming)
41 |
42 | // THEN
43 | expect(parameters.maxRTT).to.eql(11)
44 | expect(parameters.exchangeLifetime).to.eql(25)
45 | expect(parameters.maxTransmitSpan).to.eql(14)
46 | expect(parameters.maxTransmitWait).to.eql(30)
47 | })
48 |
49 | it('should verify default timings', function () {
50 | // THEN
51 | expect(parameters.maxRTT).to.eql(202)
52 | expect(parameters.exchangeLifetime).to.eql(247)
53 | expect(parameters.maxTransmitSpan).to.eql(45)
54 | expect(parameters.maxTransmitWait).to.eql(93)
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coap",
3 | "version": "1.4.2",
4 | "description": "A CoAP library for node modelled after 'http'",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc -b",
9 | "pretest": "npm run build",
10 | "prepublishOnly": "npm run build",
11 | "test": "mocha ./dist/test --exit",
12 | "coverage": "c8 -a --reporter=lcov --reporter=text --reporter=html npm run test",
13 | "lint": "eslint *.ts",
14 | "lint:fix": "eslint *.ts --fix"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/coapjs/node-coap.git"
19 | },
20 | "keywords": [
21 | "coap",
22 | "m2m",
23 | "iot",
24 | "client",
25 | "server",
26 | "udp",
27 | "observe",
28 | "internet of things",
29 | "messaging"
30 | ],
31 | "author": "Matteo Collina ",
32 | "license": "MIT",
33 | "devDependencies": {
34 | "@types/capitalize": "^2.0.2",
35 | "@types/chai": "^4.3.16",
36 | "@types/debug": "^4.1.12",
37 | "@types/mocha": "^10.0.6",
38 | "@types/node": "^20.14.6",
39 | "@types/sinon": "^17.0.3",
40 | "@typescript-eslint/eslint-plugin": "^6.4.0",
41 | "@typescript-eslint/parser": "^6.0.0",
42 | "chai": "^4.4.1",
43 | "eslint": "^8.56.0",
44 | "eslint-config-standard-with-typescript": "^40.0.0",
45 | "eslint-plugin-import": "^2.29.1",
46 | "eslint-plugin-n": "^16.0.0",
47 | "eslint-plugin-promise": "^6.2.0",
48 | "mocha": "^10.4.0",
49 | "c8": "^10.1.2",
50 | "sinon": "^18.0.0",
51 | "source-map-support": "^0.5.21",
52 | "timekeeper": "^2.3.1",
53 | "ts-node": "^10.9.2",
54 | "typescript": "^5.4.5"
55 | },
56 | "dependencies": {
57 | "bl": "^6.0.12",
58 | "@types/readable-stream": "^4.0.14",
59 | "capitalize": "^2.0.4",
60 | "coap-packet": "^1.1.1",
61 | "debug": "^4.3.5",
62 | "fastseries": "^2.0.0",
63 | "lru-cache": "^11.0.2",
64 | "readable-stream": "^4.5.2"
65 | },
66 | "engines": {
67 | "node": ">=18"
68 | },
69 | "files": [
70 | "dist/index.d.ts",
71 | "dist/index.js",
72 | "dist/index.js.map",
73 | "dist/models",
74 | "dist/lib",
75 | "examples/"
76 | ]
77 | }
78 |
--------------------------------------------------------------------------------
/lib/observe_read_stream.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { AddressInfo } from 'net'
10 | import IncomingMessage from './incoming_message'
11 | import { packetToMessage } from './helpers'
12 | import { CoapPacket } from '../models/models'
13 |
14 | export default class ObserveReadStream extends IncomingMessage {
15 | _lastId: number | undefined
16 | _lastMessageId: number | undefined
17 | _lastTime: number
18 | _disableFiltering: boolean
19 | constructor (packet: CoapPacket, rsinfo: AddressInfo, outSocket: AddressInfo) {
20 | super(packet, rsinfo, outSocket, { objectMode: true })
21 |
22 | this._lastId = undefined
23 | this._lastMessageId = undefined
24 | this._lastTime = 0
25 | this._disableFiltering = false
26 | this.append(packet, true)
27 | }
28 |
29 | get lastMessageId(): number | undefined {
30 | return this._lastMessageId;
31 | }
32 |
33 | append (packet: CoapPacket, firstPacket: boolean): void {
34 | if (!this.readable) {
35 | return
36 | }
37 |
38 | if (!firstPacket) {
39 | packetToMessage(this, packet)
40 | }
41 |
42 | let observe: number
43 |
44 | if (typeof this.headers.Observe !== 'number') {
45 | observe = 0
46 | } else {
47 | observe = this.headers.Observe
48 | }
49 |
50 | // First notification
51 | if (this._lastId === undefined) {
52 | this._lastId = observe - 1
53 | }
54 |
55 | const dseq = (observe - this._lastId) & 0xffffff
56 | const dtime = Date.now() - this._lastTime
57 |
58 | if (this._disableFiltering || (dseq > 0 && dseq < (1 << 23)) || dtime > 128 * 1000) {
59 | this._lastId = observe
60 | this._lastMessageId = packet.messageId
61 | this._lastTime = Date.now()
62 | this.push(packet.payload)
63 | }
64 | }
65 |
66 | close (eagerDeregister?: boolean): void {
67 | this.push(null)
68 | this.emit('close')
69 | if (eagerDeregister === true) {
70 | this.emit('deregister')
71 | }
72 | }
73 |
74 | // nothing to do, data will be pushed from the server
75 | _read (): void {}
76 | }
77 |
--------------------------------------------------------------------------------
/lib/block.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { Block } from '../models/models'
10 |
11 | const TwoPowTwenty = 1048575
12 |
13 | /**
14 | *
15 | * @param numOrBlockState The block sequence number or a block state object.
16 | * @param more Can indicate if more blocks are to follow.
17 | * @param size The block size.
18 | */
19 | export function generateBlockOption (numOrBlockState: Block | number, more?: number, size?: number): Buffer {
20 | let num
21 | if (typeof numOrBlockState === 'object') {
22 | num = numOrBlockState.num
23 | more = numOrBlockState.more
24 | size = numOrBlockState.size
25 | } else {
26 | num = numOrBlockState
27 | }
28 |
29 | if (num == null || more == null || size == null) {
30 | throw new Error('Invalid parameters')
31 | }
32 |
33 | if (num > TwoPowTwenty) {
34 | throw new Error('Sequence number out of range')
35 | }
36 |
37 | let buff = Buffer.alloc(4)
38 |
39 | const value = (num << 4) | (more << 3) | (size & 7)
40 | buff.writeInt32BE(value)
41 |
42 | if (num >= 4096) {
43 | buff = buff.slice(1, 4)
44 | } else if (num >= 16) {
45 | buff = buff.slice(2, 4)
46 | } else {
47 | buff = buff.slice(3, 4)
48 | }
49 |
50 | return buff
51 | }
52 |
53 | export function parseBlockOption (buff: Buffer): Block {
54 | if (buff.length === 1) {
55 | buff = Buffer.concat([Buffer.alloc(3), buff])
56 | } else if (buff.length === 2) {
57 | buff = Buffer.concat([Buffer.alloc(2), buff])
58 | } else if (buff.length === 3) {
59 | buff = Buffer.concat([Buffer.alloc(1), buff])
60 | } else {
61 | throw new Error(`Invalid block option buffer length. Must be 1, 2 or 3. It is ${buff.length}`)
62 | }
63 |
64 | const value = buff.readInt32BE()
65 |
66 | const num = (value >> 4) & TwoPowTwenty
67 | const more = (value & 8) === 8 ? 1 : 0
68 | const size = value & 7
69 |
70 | return {
71 | num,
72 | more,
73 | size
74 | }
75 | }
76 |
77 | export function exponentToByteSize (expo: number): number {
78 | return Math.pow(2, expo + 4)
79 | }
80 |
81 | export function byteSizeToExponent (byteSize: number): number {
82 | return Math.round(Math.log(byteSize) / Math.log(2) - 4)
83 | }
84 |
--------------------------------------------------------------------------------
/examples/proxy.js:
--------------------------------------------------------------------------------
1 | const coap = require('../')
2 | const async = require('async')
3 | const Readable = require('stream').Readable
4 | const requestNumber = 10
5 |
6 | let targetServer
7 | let proxy
8 |
9 | function formatTitle (msg) {
10 | return '\n\n' + msg + '\n-------------------------------------'
11 | }
12 |
13 | function requestHandler (req, res) {
14 | console.log('Target receives [%s] in port [8976] from port [%s]', req.payload, req.rsinfo.port)
15 | res.end('RES_' + req.payload)
16 | }
17 |
18 | function createTargetServer (callback) {
19 | console.log('Creating target server at port 8976')
20 |
21 | targetServer = coap.createServer(requestHandler)
22 |
23 | targetServer.listen(8976, '0.0.0.0', callback)
24 | }
25 |
26 | function proxyHandler (req, res) {
27 | console.log('Proxy handled [%s]', req.payload)
28 | res.end('RES_' + req.payload)
29 | }
30 |
31 | function createProxy (callback) {
32 | console.log('Creating proxy at port 6780')
33 |
34 | proxy = coap.createServer({ proxy: true }, proxyHandler)
35 |
36 | proxy.listen(6780, '0.0.0.0', callback)
37 | }
38 |
39 | function sendRequest (proxied) {
40 | return (n, callback) => {
41 | const req = {
42 | host: 'localhost',
43 | port: 8976,
44 | agent: false
45 | }
46 | const rs = new Readable()
47 |
48 | if (proxied) {
49 | req.port = 6780
50 | req.proxyUri = 'coap://localhost:8976'
51 | }
52 |
53 | const request = coap.request(req)
54 |
55 | request.on('response', (res) => {
56 | console.log('Client receives [%s] in port [%s] from [%s]', res.payload, res.outSocket.port, res.rsinfo.port)
57 | callback()
58 | })
59 |
60 | rs.push('MSG_' + n)
61 | rs.push(null)
62 | rs.pipe(request)
63 | }
64 | }
65 |
66 | function executeTest (proxied) {
67 | return (callback) => {
68 | if (proxied) {
69 | console.log(formatTitle('Executing tests with proxy'))
70 | } else {
71 | console.log(formatTitle('Executing tests without proxy'))
72 | }
73 |
74 | async.times(requestNumber, sendRequest(proxied), callback)
75 | }
76 | }
77 |
78 | function cleanUp (callback) {
79 | targetServer.close(() => {
80 | proxy.close(callback)
81 | })
82 | }
83 |
84 | function checkResults (callback) {
85 | console.log(formatTitle('Finish'))
86 | }
87 |
88 | async.series([
89 | createTargetServer,
90 | createProxy,
91 | executeTest(false),
92 | executeTest(true),
93 | cleanUp
94 | ], checkResults)
95 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-stale - https://github.com/probot/stale
2 |
3 | # Number of days of inactivity before an Issue or Pull Request becomes stale
4 | daysUntilStale: 14
5 |
6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
8 | daysUntilClose: 7
9 |
10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
11 | onlyLabels: []
12 |
13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
14 | exemptLabels:
15 | - enhancement
16 | - bug
17 |
18 | # Set to true to ignore issues in a project (defaults to false)
19 | exemptProjects: true
20 |
21 | # Set to true to ignore issues in a milestone (defaults to false)
22 | exemptMilestones: true
23 |
24 | # Set to true to ignore issues with an assignee (defaults to false)
25 | exemptAssignees: false
26 |
27 | # Label to use when marking as stale
28 | staleLabel: wontfix
29 |
30 | # Comment to post when marking as stale. Set to `false` to disable
31 | markComment: >
32 | This issue has been automatically marked as stale because it has not had
33 | recent activity. It will be closed if no further activity occurs within the next 7 days.
34 | Please check if the issue is still relevant in the most current version of the adapter
35 | and tell us. Also check that all relevant details, logs and reproduction steps
36 | are included and update them if needed.
37 | Thank you for your contributions.
38 |
39 | # Comment to post when removing the stale label.
40 | # unmarkComment: >
41 | # Your comment here.
42 |
43 | # Comment to post when closing a stale Issue or Pull Request.
44 | closeComment: >
45 | This issue has been automatically closed because of inactivity. Please open a new
46 | issue if still relevant and make sure to include all relevant details, logs and
47 | reproduction steps.
48 | Thank you for your contributions.
49 |
50 | # Limit the number of actions per hour, from 1-30. Default is 30
51 | limitPerRun: 30
52 |
53 | # Limit to only `issues` or `pulls`
54 | only: issues
55 |
56 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
57 | # pulls:
58 | # daysUntilStale: 30
59 | # markComment: >
60 | # This pull request has been automatically marked as stale because it has not had
61 | # recent activity. It will be closed if no further activity occurs. Thank you
62 | # for your contributions.
63 |
64 | # issues:
65 | # exemptLabels:
66 | # - confirmed
67 |
--------------------------------------------------------------------------------
/lib/observe_write_stream.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { OptionName, Packet } from 'coap-packet'
10 | import { Writable } from 'stream'
11 | import { CoapPacket, OptionValue } from '../models/models'
12 | import { setOption } from './helpers'
13 |
14 | export default class ObserveWriteStream extends Writable {
15 | _packet: Packet
16 | _request: Packet
17 | _send: (message: ObserveWriteStream, packet: Packet) => void
18 | code: string
19 | statusCode: string
20 | _counter: number
21 | _cachekey: string
22 | _addCacheEntry: Function
23 | constructor (request: Packet, send: (message: ObserveWriteStream, packet: CoapPacket) => void) {
24 | super()
25 |
26 | this._packet = {
27 | token: request.token,
28 | messageId: request.messageId,
29 | options: [],
30 | confirmable: false,
31 | ack: request.confirmable,
32 | reset: false
33 | }
34 |
35 | this._request = request
36 | this._send = send
37 |
38 | this._counter = 0
39 |
40 | this.on('finish', () => {
41 | if (this._counter === 0) { // we have sent no messages
42 | this._doSend()
43 | }
44 | })
45 | }
46 |
47 | _write (data: Buffer, encoding: string, done: () => void): void {
48 | this.setOption('Observe', ++this._counter)
49 |
50 | if (this._counter === 16777215) {
51 | this._counter = 1
52 | }
53 |
54 | this._doSend(data)
55 |
56 | done()
57 | }
58 |
59 | _doSend (data?: Buffer): void {
60 | const packet = this._packet
61 | packet.code = this.statusCode
62 | packet.payload = data
63 | this._send(this, packet)
64 |
65 | this._packet.confirmable = this._request.confirmable
66 | this._packet.ack = this._request.confirmable === false
67 | delete this._packet.messageId
68 | delete this._packet.payload
69 | }
70 |
71 | reset (): void {
72 | const packet = this._packet
73 | packet.code = '0.00'
74 | packet.payload = Buffer.alloc(0)
75 | packet.reset = true
76 | packet.ack = false
77 | packet.token = Buffer.alloc(0)
78 |
79 | this._send(this, packet)
80 |
81 | this._packet.confirmable = this._request.confirmable
82 | delete this._packet.messageId
83 | delete this._packet.payload
84 | }
85 |
86 | setOption (name: OptionName, values: OptionValue): this {
87 | setOption(this._packet, name, values)
88 | return this
89 | }
90 |
91 | setHeader (name: OptionName, values: OptionValue): this {
92 | return this.setOption(name, values)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/retry_send.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { EventEmitter } from 'events'
10 | import { parse } from 'coap-packet'
11 | import { parameters } from './parameters'
12 | import { Socket } from 'dgram'
13 |
14 | class RetrySendError extends Error {
15 | retransmitTimeout: number
16 | constructor (retransmitTimeout: number) {
17 | super(`No reply in ${retransmitTimeout} seconds.`)
18 | this.retransmitTimeout = retransmitTimeout
19 | }
20 | }
21 |
22 | export default class RetrySend extends EventEmitter {
23 | _sock: Socket
24 | _port: number
25 | _host?: string
26 | _maxRetransmit: number
27 | _sendAttemp: number
28 | _lastMessageId: number
29 | _currentTime: number
30 | _bOff: () => void
31 | _message: Buffer
32 | _timer: NodeJS.Timeout
33 | _bOffTimer: NodeJS.Timeout
34 | constructor (sock: any, port: number, host?: string, maxRetransmit?: number) {
35 | super()
36 |
37 | this._sock = sock
38 |
39 | this._port = port ?? parameters.coapPort
40 |
41 | this._host = host
42 |
43 | this._maxRetransmit = maxRetransmit ?? parameters.maxRetransmit
44 | this._sendAttemp = 0
45 | this._lastMessageId = -1
46 | this._currentTime = parameters.ackTimeout * (1 + (parameters.ackRandomFactor - 1) * Math.random()) * 1000
47 |
48 | this._bOff = () => {
49 | this._currentTime = this._currentTime * 2
50 | this._send()
51 | }
52 | }
53 |
54 | _send (avoidBackoff?: boolean): void {
55 | this._sock.send(this._message, 0, this._message.length,
56 | this._port, this._host, (err: Error, bytes: number): void => {
57 | this.emit('sent', err, bytes)
58 | if (err != null) {
59 | this.emit('error', err)
60 | }
61 | })
62 |
63 | const messageId = parse(this._message).messageId
64 | if (messageId !== this._lastMessageId) {
65 | this._lastMessageId = messageId
66 | this._sendAttemp = 0
67 | }
68 |
69 | if (avoidBackoff !== true && ++this._sendAttemp <= this._maxRetransmit) {
70 | this._bOffTimer = setTimeout(this._bOff, this._currentTime)
71 | }
72 |
73 | this.emit('sending', this._message)
74 | }
75 |
76 | send (message: Buffer, avoidBackoff?: boolean): void {
77 | this._message = message
78 | this._send(avoidBackoff)
79 |
80 | const timeout = avoidBackoff === true ? parameters.maxRTT : parameters.exchangeLifetime
81 | this._timer = setTimeout(() => {
82 | const err = new RetrySendError(timeout)
83 | if (avoidBackoff === false) {
84 | this.emit('error', err)
85 | }
86 | this.emit('timeout', err)
87 | }, timeout * 1000)
88 | }
89 |
90 | reset (): void {
91 | clearTimeout(this._timer)
92 | clearTimeout(this._bOffTimer)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/cache.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { parameters } from './parameters'
10 | import { BlockCacheMap } from '../models/models'
11 | import Debug from 'debug'
12 | const debug = Debug('Block Cache')
13 |
14 | function expiry (cache: BlockCacheMap, k: string): void {
15 | debug('delete expired cache entry, key:', k)
16 | cache.delete(k)
17 | }
18 |
19 | class BlockCache {
20 | _retentionPeriod: number
21 | _cache: BlockCacheMap
22 | _factory: () => T
23 |
24 | /**
25 | *
26 | * @param retentionPeriod
27 | * @param factory Function which returns new cache objects
28 | */
29 | constructor (retentionPeriod: number | null | undefined, factory: () => T) {
30 | this._cache = new Map()
31 | if (retentionPeriod != null) {
32 | this._retentionPeriod = retentionPeriod
33 | } else {
34 | this._retentionPeriod = parameters.exchangeLifetime * 1000
35 | }
36 |
37 | debug(`Created cache with ${this._retentionPeriod / 1000} s retention period`)
38 | this._factory = factory
39 | }
40 |
41 | private clearTimeout (key: string): void {
42 | const timeoutId = this._cache.get(key)?.timeoutId
43 | if (timeoutId != null) {
44 | clearTimeout(timeoutId)
45 | }
46 | }
47 |
48 | reset (): void {
49 | for (const key of this._cache.keys()) {
50 | debug('clean-up cache expiry timer, key:', key)
51 | this.remove(key)
52 | }
53 | }
54 |
55 | /**
56 | * @param key
57 | * @param payload
58 | */
59 | add (key: string, payload: T): void {
60 | const entry = this._cache.get(key)
61 | if (entry != null) {
62 | debug('reuse old cache entry, key:', key)
63 | clearTimeout(entry.timeoutId)
64 | } else {
65 | debug('add payload to cache, key:', key)
66 | }
67 | // setup new expiry timer
68 | const timeoutId = setTimeout(expiry, this._retentionPeriod, this._cache, key)
69 | this._cache.set(key, { payload, timeoutId })
70 | }
71 |
72 | remove (key: string): boolean {
73 | if (this.contains(key)) {
74 | debug('remove cache entry, key:', key)
75 | this.clearTimeout(key)
76 | }
77 | return this._cache.delete(key)
78 | }
79 |
80 | contains (key: string): boolean {
81 | return this._cache.has(key)
82 | }
83 |
84 | get (key: string): T | undefined {
85 | return this._cache.get(key)?.payload
86 | }
87 |
88 | getWithDefaultInsert (key: string | null): T {
89 | if (key == null) {
90 | return this._factory()
91 | }
92 | const value = this.get(key)
93 | if (value != null) {
94 | return value
95 | } else {
96 | const def = this._factory()
97 | this.add(key, def)
98 | return def
99 | }
100 | }
101 | }
102 |
103 | export default BlockCache
104 |
--------------------------------------------------------------------------------
/lib/middlewares.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import crypto from 'crypto'
10 | import { parse, ParsedPacket } from 'coap-packet'
11 | import { or, isOption } from './helpers'
12 | import { MiddlewareParameters } from '../models/models'
13 |
14 | type middlewareCallback = (nullOrError: null | Error) => void
15 |
16 | class MiddleWareError extends Error {
17 | /**
18 | * Creates a new `MiddleWareError`.
19 | *
20 | * @param middlewareName The middleware function throwing this error.
21 | */
22 | constructor (middlewareName: string) {
23 | super(`${middlewareName}: No CoAP Packet found!`)
24 | }
25 | }
26 |
27 | export function parseRequest (request: MiddlewareParameters, next: middlewareCallback): void {
28 | try {
29 | request.packet = parse(request.raw)
30 | next(null)
31 | } catch (err) {
32 | next(err)
33 | }
34 | }
35 |
36 | export function handleServerRequest (request: MiddlewareParameters, next: middlewareCallback): void {
37 | if (request.proxy != null) {
38 | return next(null)
39 | }
40 |
41 | if (request.packet == null) {
42 | return next(new MiddleWareError('handleServerRequest'))
43 | }
44 |
45 | try {
46 | request.server._handle(request.packet, request.rsinfo)
47 | next(null)
48 | } catch (err) {
49 | next(err)
50 | }
51 | }
52 |
53 | export function proxyRequest (request: MiddlewareParameters, next: middlewareCallback): void {
54 | if (request.packet == null) {
55 | return next(new MiddleWareError('proxyRequest'))
56 | }
57 |
58 | for (let i = 0; i < request.packet.options.length; i++) {
59 | const option = request.packet.options[i]
60 | if (typeof option.name !== 'string') {
61 | continue
62 | } else if (option.name.toLowerCase() === 'proxy-uri') {
63 | request.proxy = option.value.toString()
64 | }
65 | }
66 |
67 | if (request.proxy != null) {
68 | if (request.packet.token.length === 0) {
69 | request.packet.token = crypto.randomBytes(8)
70 | }
71 |
72 | request.server._proxiedRequests.set(request.packet.token.toString('hex'), request)
73 | request.server._sendProxied(request.packet, request.proxy, next)
74 | } else {
75 | next(null)
76 | }
77 | }
78 |
79 | function isObserve (packet: ParsedPacket): boolean {
80 | return packet.options.map(isOption('Observe')).reduce(or, false)
81 | }
82 |
83 | export function handleProxyResponse (request: MiddlewareParameters, next: middlewareCallback): void {
84 | if (request.proxy != null) {
85 | return next(null)
86 | }
87 |
88 | if (request.packet == null) {
89 | return next(new MiddleWareError('handleProxyResponse'))
90 | }
91 |
92 | const originalProxiedRequest = request.server._proxiedRequests.get(request.packet.token.toString('hex'))
93 | if (originalProxiedRequest != null) {
94 | request.server._sendReverseProxied(request.packet, originalProxiedRequest.rsinfo)
95 |
96 | if (!isObserve(request.packet)) {
97 | request.server._proxiedRequests.delete(request.packet.token.toString('hex'))
98 | }
99 | }
100 |
101 | next(null)
102 | }
103 |
--------------------------------------------------------------------------------
/test/cache.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { expect } from 'chai'
10 | import BlockCache from '../lib/cache'
11 |
12 | describe('Cache', () => {
13 | describe('Block Cache', () => {
14 | it('Should set up empty cache object', (done) => {
15 | const b = new BlockCache<{}>(10000, () => { return {} })
16 | expect(b._cache.size).to.eql(0)
17 | setImmediate(done)
18 | })
19 | })
20 |
21 | describe('Reset', () => {
22 | it('Should reset all caches', (done) => {
23 | const b = new BlockCache<{ payload: string } | null>(10000, () => { return null })
24 | b.add('test', { payload: 'test' })
25 | b.reset()
26 | expect(b._cache.size).to.eql(0)
27 | setImmediate(done)
28 | })
29 | })
30 |
31 | describe('Add', () => {
32 | it('Should add to cache', (done) => {
33 | const b = new BlockCache<{ payload: string } | null>(10000, () => { return null })
34 | b.add('test', { payload: 'test' })
35 | expect(b.contains('test')).to.equal(true)
36 | setImmediate(done)
37 | })
38 | // reuse old cache entry
39 | })
40 |
41 | describe('Remove', () => {
42 | it('Should from cache', (done) => {
43 | const b = new BlockCache<{ payload: string } | null>(10000, () => { return null })
44 | b.add('test', { payload: 'test' })
45 | b.add('test2', { payload: 'test2' })
46 | b.remove('test')
47 | expect(b.contains('test')).to.equal(false)
48 | setImmediate(done)
49 | })
50 | })
51 |
52 | describe('Contains', () => {
53 | it('Should check if value exists & return true', (done) => {
54 | const b = new BlockCache<{ payload: string } | null>(10000, () => { return null })
55 | b.add('test', { payload: 'test' })
56 | expect(b.contains('test')).to.eql(true)
57 | setImmediate(done)
58 | })
59 |
60 | it('Should check if value exists & return false', (done) => {
61 | const b = new BlockCache<{ payload: string } | null>(10000, () => { return null })
62 | b.add('test', { payload: 'test' })
63 | expect(b.contains('test2')).to.eql(false)
64 | setImmediate(done)
65 | })
66 | })
67 |
68 | describe('Get', () => {
69 | it('Should return payload from cache', (done) => {
70 | const b = new BlockCache<{ payload: string } | null>(10000, () => { return null })
71 | b.add('test', { payload: 'test' })
72 | expect(b.get('test')).to.eql({ payload: 'test' })
73 | setImmediate(done)
74 | })
75 | })
76 |
77 | describe('Get with default insert', () => {
78 | it('Should return payload from cache if it exists', (done) => {
79 | const b = new BlockCache<{ payload: string } | null>(10000, () => { return null })
80 | b.add('test', { payload: 'test' })
81 | expect(b.getWithDefaultInsert('test')).to.eql({ payload: 'test' })
82 | setImmediate(done)
83 | })
84 |
85 | it('Should add to cache if it doesnt exist', (done) => {
86 | const b = new BlockCache<{ payload: string } | null>(10000, () => { return null })
87 | b.getWithDefaultInsert('test')
88 | expect(b.contains('test')).to.equal(true)
89 | setImmediate(done)
90 | })
91 | })
92 | })
93 |
--------------------------------------------------------------------------------
/models/models.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 - 2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { CoapMethod, OptionName, Packet, ParsedPacket } from 'coap-packet'
10 | import { Socket } from 'dgram'
11 | import { AddressInfo } from 'net'
12 | import Agent from '../lib/agent'
13 | import IncomingMessage from '../lib/incoming_message'
14 | import OutgoingMessage from '../lib/outgoing_message'
15 | import CoAPServer from '../lib/server'
16 |
17 | export declare function requestListener (req: IncomingMessage, res: OutgoingMessage): void
18 |
19 | export type OptionValue = null | string | number | Buffer | Buffer[]
20 | export type BlockCacheMap = Map
21 | export type CoapOptions = Partial>
22 |
23 | export interface Option {
24 | name: number | OptionName | string
25 | value: OptionValue
26 | }
27 |
28 | export interface Block {
29 | more: number
30 | num: number
31 | size: number
32 | }
33 |
34 | export interface MiddlewareParameters {
35 | raw: Buffer
36 | rsinfo: AddressInfo
37 | server: CoAPServer
38 | packet?: ParsedPacket
39 | proxy?: string
40 | }
41 |
42 | export interface CoapPacket extends Packet {
43 | piggybackReplyMs?: number
44 | url?: string
45 | }
46 |
47 | export interface ParametersUpdate {
48 | ackTimeout?: number
49 | ackRandomFactor?: number
50 | maxRetransmit?: number
51 | nstart?: number
52 | defaultLeisure?: number
53 | probingRate?: number
54 | maxLatency?: number
55 | piggybackReplyMs?: number
56 | maxPayloadSize?: number
57 | maxMessageSize?: number
58 | sendAcksForNonConfirmablePackets?: boolean
59 | pruneTimerPeriod?: number
60 | }
61 |
62 | export interface Parameters {
63 | ackTimeout: number
64 | ackRandomFactor: number
65 | maxRetransmit: number
66 | nstart: number
67 | defaultLeisure: number
68 | probingRate: number
69 | maxLatency: number
70 | piggybackReplyMs: number
71 | nonLifetime: number
72 | coapPort: number
73 | maxPayloadSize: number
74 | maxMessageSize: number
75 | sendAcksForNonConfirmablePackets: boolean
76 | pruneTimerPeriod: number
77 | maxTransmitSpan: number
78 | maxTransmitWait: number
79 | processingDelay: number
80 | exchangeLifetime: number
81 | maxRTT: number
82 | defaultTiming?: () => void
83 | refreshTiming?: (parameters?: Parameters) => void
84 | }
85 |
86 | export interface CoapRequestParams {
87 | host?: string
88 | hostname?: string
89 | port?: number
90 | method?: CoapMethod
91 | confirmable?: boolean
92 | observe?: 0 | 1 | boolean | string
93 | pathname?: string
94 | query?: string
95 | options?: Partial>
96 | headers?: Partial>
97 | agent?: Agent | false
98 | proxyUri?: string
99 | multicast?: boolean
100 | multicastTimeout?: number
101 | retrySend?: number
102 | token?: Buffer
103 | contentFormat?: string | number
104 | accept?: string | number
105 | }
106 |
107 | export interface CoapServerOptions {
108 | type?: 'udp4' | 'udp6'
109 | proxy?: boolean
110 | multicastAddress?: string
111 | multicastInterface?: string
112 | piggybackReplyMs?: number
113 | sendAcksForNonConfirmablePackets?: boolean
114 | clientIdentifier?: (request: IncomingMessage) => string
115 | reuseAddr?: boolean
116 | cacheSize?: number
117 | }
118 |
119 | export interface AgentOptions {
120 | type?: 'udp4' | 'udp6'
121 | socket?: Socket
122 | port?: number
123 | }
124 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 - 2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import Agent from './lib/agent'
10 | import Server from './lib/server'
11 | import IncomingMessage from './lib/incoming_message'
12 | import OutgoingMessage from './lib/outgoing_message'
13 | import ObserveReadStream from './lib/observe_read_stream'
14 | import ObserveWriteStream from './lib/observe_write_stream'
15 | import { parameters, refreshTiming, defaultTiming } from './lib/parameters'
16 | import { isIPv6 } from 'net'
17 | import { registerOption, registerFormat, ignoreOption } from './lib/option_converter'
18 | import type { CoapServerOptions, requestListener, CoapRequestParams, ParametersUpdate, AgentOptions, CoapPacket, Option, OptionValue } from './models/models'
19 |
20 | export let globalAgent = new Agent({ type: 'udp4' })
21 | export let globalAgentIPv6 = new Agent({ type: 'udp6' })
22 |
23 | export function setGlobalAgent (agent: Agent): void {
24 | globalAgent = agent
25 | }
26 |
27 | export function setGlobalAgentV6 (agent: Agent): void {
28 | globalAgentIPv6 = agent
29 | }
30 |
31 | export function createServer (options?: CoapServerOptions | typeof requestListener, listener?: typeof requestListener): Server {
32 | return new Server(options, listener)
33 | }
34 |
35 | function _getHostname (url: URL): string {
36 | const hostname = url.hostname
37 | // Remove brackets from literal IPv6 addresses
38 | if (hostname.startsWith('[') && hostname.endsWith(']')) {
39 | return hostname.substring(1, hostname.length - 1)
40 | }
41 | return hostname
42 | }
43 |
44 | function _getQueryParamsFromSearch (url: URL): string | undefined {
45 | if (url.search != null) {
46 | return url.search.substring(1)
47 | }
48 | }
49 |
50 | function _getPort (url: URL): number {
51 | if (url.port !== '') {
52 | return parseInt(url.port)
53 | } else {
54 | return parameters.coapPort
55 | }
56 | }
57 |
58 | function _parseUrl (url: string): CoapRequestParams {
59 | const requestParams: CoapRequestParams = {}
60 | const parsedUrl = new URL(url)
61 | requestParams.hostname = _getHostname(parsedUrl)
62 | requestParams.query = _getQueryParamsFromSearch(parsedUrl)
63 | requestParams.port = _getPort(parsedUrl)
64 | requestParams.pathname = parsedUrl.pathname
65 | return requestParams
66 | }
67 |
68 | export function request (requestParams: CoapRequestParams | string): OutgoingMessage {
69 | let agent: Agent
70 | if (typeof requestParams === 'string') {
71 | requestParams = _parseUrl(requestParams)
72 | }
73 | const ipv6 = isIPv6(requestParams.hostname ?? requestParams.host ?? '')
74 | if (requestParams.agent != null && requestParams.agent !== false) {
75 | agent = requestParams.agent
76 | } else if (requestParams.agent === false && !ipv6) {
77 | agent = new Agent({ type: 'udp4' })
78 | } else if (requestParams.agent === false && ipv6) {
79 | agent = new Agent({ type: 'udp6' })
80 | } else if (ipv6) {
81 | agent = globalAgentIPv6
82 | } else {
83 | agent = globalAgent
84 | }
85 | return agent.request(requestParams)
86 | }
87 |
88 | export {
89 | parameters,
90 | refreshTiming as updateTiming,
91 | defaultTiming,
92 | registerOption,
93 | registerFormat,
94 | ignoreOption,
95 | IncomingMessage,
96 | OutgoingMessage,
97 | ObserveReadStream,
98 | ObserveWriteStream,
99 | Agent,
100 | Server,
101 | type ParametersUpdate,
102 | type CoapRequestParams,
103 | type AgentOptions,
104 | type CoapPacket,
105 | type Option,
106 | type OptionValue,
107 | type CoapServerOptions
108 | }
109 |
--------------------------------------------------------------------------------
/lib/segmentation.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { Block } from '../models/models'
10 | import OutgoingMessage from './outgoing_message'
11 | import { Packet, generate } from 'coap-packet'
12 | import { generateBlockOption, exponentToByteSize } from './block'
13 |
14 | export class SegmentedTransmission {
15 | totalLength: number
16 | currentByte: number
17 | lastByte: number
18 | req: OutgoingMessage
19 | payload: Buffer
20 | packet: Packet
21 | resendCount: number
22 | blockState: Block
23 | byteSize: number
24 | constructor (blockSize: number, req: OutgoingMessage, packet: Packet) {
25 | if (blockSize < 0 || blockSize > 6) {
26 | throw new Error(`invalid block size ${blockSize}`)
27 | }
28 |
29 | this.blockState = {
30 | num: 0,
31 | more: 0,
32 | size: 0
33 | }
34 |
35 | this.setBlockSizeExp(blockSize)
36 |
37 | this.totalLength = packet.payload?.length ?? 0
38 | this.currentByte = 0
39 | this.lastByte = 0
40 |
41 | this.req = req
42 | this.payload = packet.payload ?? Buffer.alloc(0)
43 | this.packet = packet
44 |
45 | this.packet.payload = undefined
46 | this.resendCount = 0
47 | }
48 |
49 | setBlockSizeExp (blockSizeExp: number): void {
50 | this.blockState.size = blockSizeExp
51 | this.byteSize = exponentToByteSize(blockSizeExp)
52 | }
53 |
54 | updateBlockState (): void {
55 | this.blockState.num = this.currentByte / this.byteSize
56 | this.blockState.more = ((this.currentByte + this.byteSize) < this.totalLength) ? 1 : 0
57 |
58 | this.req.setOption('Block1', generateBlockOption(this.blockState))
59 | }
60 |
61 | isCorrectACK (retBlockState: Block): boolean {
62 | return retBlockState.num === this.blockState.num// && packet.code == "2.31"
63 | }
64 |
65 | resendPreviousPacket (): void {
66 | if (this.resendCount < 5) {
67 | this.currentByte = this.lastByte
68 | if (this.remaining() > 0) {
69 | this.sendNext()
70 | }
71 | this.resendCount++
72 | } else {
73 | throw new Error('Too many block re-transfers')
74 | }
75 | }
76 |
77 | /**
78 | *
79 | * @param retBlockState The received block state from the other end
80 | */
81 | receiveACK (retBlockState: Block): void {
82 | if (this.blockState.size !== retBlockState.size) {
83 | this.setBlockSizeExp(retBlockState.size)
84 | }
85 |
86 | if (this.remaining() > 0) {
87 | this.sendNext()
88 | }
89 | this.resendCount = 0
90 | }
91 |
92 | remaining (): number {
93 | return this.totalLength - this.currentByte
94 | }
95 |
96 | sendNext (): void {
97 | const blockLength = Math.min(this.totalLength - this.currentByte, this.byteSize)
98 | const subBuffer = this.payload.slice(this.currentByte, this.currentByte + blockLength)
99 | this.updateBlockState()
100 |
101 | this.packet.ack = false
102 | this.packet.reset = false
103 | this.packet.confirmable = true
104 |
105 | this.packet.payload = subBuffer
106 |
107 | this.lastByte = this.currentByte
108 | this.currentByte += blockLength
109 | let buf
110 |
111 | try {
112 | buf = generate(this.packet)
113 | } catch (err) {
114 | this.req.sender.reset()
115 | this.req.emit('error', err)
116 | return
117 | }
118 | this.req.sender.send(buf, !this.packet.confirmable)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/examples/blockwise_put.js:
--------------------------------------------------------------------------------
1 | const coap = require('../')
2 |
3 | const bufferSize = 25000
4 | const testBuffer = Buffer.alloc(bufferSize)
5 | const containedData = 'This is a test buffer with a lot of nothing and a bit of something'
6 | testBuffer.fill('X', 'utf-8')
7 | testBuffer.write(containedData, 'utf-8')
8 | testBuffer.write(containedData, testBuffer.length - containedData.length, containedData.length, 'utf-8')
9 |
10 | /**
11 | * Formula for chunk/block sizes is:
12 | * ByteSize = 2^(chunkSize+4)
13 | * Hence
14 | * chunkSize = 0 => ByteSize = 16
15 | * chunkSize = 6 => ByteSize = 1024
16 | * chunkSize = 7 => Reserved. Don't do it.
17 | */
18 |
19 | /**
20 | * Tests the GET Block2 method transfer. Sends data in 1024 byte chunks
21 | */
22 | function TestGet () { // eslint-disable-line no-unused-vars
23 | coap.createServer((req, res) => {
24 | // Respond with the test buffer.
25 | res.end(testBuffer)
26 | }).listen(() => {
27 | // GET Request resources /test with block transfer with 1024 byte size
28 | const req = coap.request('/test')
29 |
30 | req.setOption('Block2', Buffer.from(0x6))
31 |
32 | req.on('response', (res) => {
33 | console.log('Client Received ' + res.payload.length + ' bytes')
34 | process.exit(0)
35 | })
36 |
37 | req.end()
38 | })
39 | }
40 | /**
41 | * Tests the PUT Block1 method transfer. Sends data in 1024 byte chunks
42 | */
43 | function TestPut () {
44 | coap.createServer((req, res) => {
45 | setTimeout(() => {
46 | console.log('Server Received ' + req.payload.length + ' bytes')
47 | console.log(req.payload.slice(0, containedData.length * 2).toString('utf-8'))
48 | console.log(req.payload.slice(-containedData.length).toString('utf-8'))
49 | console.log('Sending back pleasantries')
50 | res.statusCode = '2.04'
51 | res.end('Congratulations!')
52 | console.log('Sent back')
53 | }, 500)
54 | }).listen(() => {
55 | const request = coap.request({
56 | hostname: this.hostname,
57 | port: this.port,
58 | pathname: '/test',
59 | method: 'PUT'
60 | })
61 | request.setOption('Block1', Buffer.alloc(0x6))
62 |
63 | request.on('response', (res) => {
64 | console.log('Client Received Response: ' + res.payload.toString('utf-8'))
65 | console.log('Client Received Response: ' + res.code)
66 | process.exit(0)
67 | })
68 | console.log('Sending large data from client...')
69 | request.end(testBuffer)
70 | console.log('Sent to server')
71 | })
72 | }
73 | /**
74 | * Creates a CoAP server which listens for connections from outside.
75 | * Start up an external CoAP client and try it out.
76 | */
77 | function TestServer () { // eslint-disable-line no-unused-vars
78 | coap.createServer((req, res) => {
79 | console.log('Got request. Waiting 500ms')
80 | setTimeout(() => {
81 | res.setOption('Block2', Buffer.from(0x6))
82 | console.log('Sending Back Test Buffer')
83 | res.end(testBuffer)
84 | console.log('Sent Back')
85 | }, 500)
86 | }).listen()
87 | }
88 |
89 | /**
90 | * Connects to another end point located on this machine. Setup a coap server somewhere else and try it out.
91 | */
92 | function TestClient () { // eslint-disable-line no-unused-vars
93 | const request = coap.request({
94 | hostname: 'localhost',
95 | port: 5683,
96 | pathname: '/test',
97 | method: 'PUT'
98 | })
99 | request.setOption('Block1', Buffer.from(0))
100 |
101 | request.on('response', (res) => {
102 | console.log('Client Received ' + res.payload.length + ' bytes in response')
103 | process.exit(0)
104 | })
105 |
106 | console.log('Sending ' + testBuffer.length + ' bytes from client...')
107 | request.end(testBuffer)
108 | console.log('Sent to server')
109 | }
110 | // Choose yer poison
111 |
112 | TestPut()
113 | // TestGet()
114 | // TestServer()
115 | // TestClient()
116 |
--------------------------------------------------------------------------------
/lib/outgoing_message.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { BufferListStream } from 'bl'
10 | import { CoapPacket, CoapRequestParams, OptionValue } from '../models/models'
11 | import { genAck, toCode, setOption } from './helpers'
12 | import RetrySend from './retry_send'
13 | import { SegmentedTransmission } from './segmentation'
14 | import IncomingMessage from './incoming_message'
15 | import { OptionName, Packet } from 'coap-packet'
16 |
17 |
18 | export default class OutgoingMessage extends BufferListStream {
19 | _packet: Packet
20 | _ackTimer: NodeJS.Timeout | null
21 | _send: (req: OutgoingMessage, packet: Packet) => void
22 | statusCode: string
23 | code: string
24 | multicast: boolean
25 | _request: CoapPacket
26 | url: CoapRequestParams
27 | sender: RetrySend
28 | _totalPayload: Buffer
29 | multicastTimer: NodeJS.Timeout
30 | segmentedSender?: SegmentedTransmission
31 | response: IncomingMessage
32 | constructor (request: CoapPacket, send: (req: OutgoingMessage, packet: CoapPacket) => void) {
33 | super()
34 |
35 | this._packet = {
36 | messageId: request.messageId,
37 | token: request.token,
38 | options: [],
39 | confirmable: false,
40 | ack: false,
41 | reset: false
42 | }
43 |
44 | if (request.confirmable === true) {
45 | // replying in piggyback
46 | this._packet.ack = true
47 |
48 | this._ackTimer = setTimeout(() => {
49 | send(this, genAck(request))
50 |
51 | // we are no more in piggyback
52 | this._packet.confirmable = true
53 | this._packet.ack = false
54 |
55 | // we need a new messageId for the CON
56 | // reply
57 | delete this._packet.messageId
58 |
59 | this._ackTimer = null
60 | }, request.piggybackReplyMs)
61 | }
62 |
63 | this._send = send
64 |
65 | this.statusCode = ''
66 | this.code = ''
67 | }
68 |
69 | end (a?: any, b?: any): this {
70 | super.end(a, b)
71 |
72 | const packet = this._packet
73 |
74 | const code = this.code !== '' ? this.code : this.statusCode
75 | packet.code = toCode(code)
76 | packet.payload = this as unknown as Buffer
77 |
78 | if (this._ackTimer != null) {
79 | clearTimeout(this._ackTimer)
80 | }
81 |
82 | this._send(this, packet)
83 |
84 | // easy clean up after generating the packet
85 | delete this._packet.payload
86 |
87 | return this
88 | }
89 |
90 | reset (): this {
91 | super.end()
92 |
93 | const packet = this._packet
94 |
95 | packet.code = '0.00'
96 | packet.payload = Buffer.alloc(0)
97 | packet.reset = true
98 | packet.ack = false
99 | packet.token = Buffer.alloc(0)
100 |
101 | if (this._ackTimer != null) {
102 | clearTimeout(this._ackTimer)
103 | }
104 |
105 | this._send(this, packet)
106 |
107 | // easy clean up after generating the packet
108 | delete this._packet.payload
109 |
110 | return this
111 | }
112 |
113 | /**
114 | * @param {OptionName | number} code
115 | * @param {Partial>} headers
116 | */
117 | writeHead (code: OptionName | number, headers: Partial>): void {
118 | const packet = this._packet
119 | packet.code = String(code).replace(/(^\d[^.])/, '$1.')
120 | for (const [header, value] of Object.entries(headers)) {
121 | this.setOption(header as OptionName, value)
122 | }
123 | }
124 |
125 | setOption (name: OptionName | string, values: OptionValue): this {
126 | setOption(this._packet, name, values)
127 | return this
128 | }
129 |
130 | setHeader (name: OptionName, values: OptionValue): this {
131 | return this.setOption(name, values)
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/test/ipv6.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { nextPort } from './common'
10 | import { expect } from 'chai'
11 | import { parse, generate } from 'coap-packet'
12 | import { createSocket, Socket } from 'dgram'
13 | import { createServer, request, Server } from '../index'
14 |
15 | describe('IPv6', function () {
16 | describe('server', function () {
17 | let server: Server
18 | let port: number
19 | let clientPort: number
20 | let client: Socket
21 |
22 | beforeEach(function (done) {
23 | port = nextPort()
24 | clientPort = nextPort()
25 | client = createSocket('udp6')
26 | client.bind(clientPort, done)
27 | })
28 |
29 | afterEach(function () {
30 | client.close()
31 | server.close()
32 | })
33 |
34 | function send (message): void {
35 | client.send(message, 0, message.length, port, '::1')
36 | }
37 |
38 | it('should receive a CoAP message specifying the type', function (done) {
39 | server = createServer({ type: 'udp6' })
40 | server.listen(port, () => {
41 | send(generate({}))
42 | server.on('request', (req, res) => {
43 | done()
44 | })
45 | })
46 | })
47 |
48 | it('should automatically discover the type based on the host', function (done) {
49 | server = createServer()
50 | server.listen(port, '::1', () => {
51 | send(generate({}))
52 | server.on('request', (req, res) => {
53 | done()
54 | })
55 | })
56 | })
57 | })
58 |
59 | describe('request', function () {
60 | let server: Socket
61 | let port: number
62 |
63 | beforeEach(function (done) {
64 | port = nextPort()
65 | server = createSocket('udp6')
66 | server.bind(port, done)
67 | })
68 |
69 | afterEach(function () {
70 | server.close()
71 | })
72 |
73 | function createTest (createUrl) {
74 | return function (done) {
75 | const req = request(createUrl())
76 | req.end(Buffer.from('hello world'))
77 |
78 | server.on('message', (msg, rsinfo) => {
79 | const packet = parse(msg)
80 | const toSend = generate({
81 | messageId: packet.messageId,
82 | token: packet.token,
83 | payload: Buffer.from('42'),
84 | ack: true,
85 | code: '2.00'
86 | })
87 | server.send(toSend, 0, toSend.length, rsinfo.port, rsinfo.address)
88 |
89 | expect(parse(msg).payload.toString()).to.eql('hello world')
90 | done()
91 | })
92 | }
93 | }
94 |
95 | it('should send the data to the server (URL param)', createTest(function () {
96 | return `coap://[::1]:${port}`
97 | }))
98 |
99 | it('should send the data to the server (hostname + port in object)', createTest(function () {
100 | return { hostname: '::1', port }
101 | }))
102 |
103 | it('should send the data to the server (host + port in object)', createTest(function () {
104 | return { host: '::1', port }
105 | }))
106 | })
107 |
108 | describe('end-to-end', function () {
109 | let server: Server
110 | let port: number
111 |
112 | beforeEach(function (done) {
113 | port = nextPort()
114 | server = createServer({ type: 'udp6' })
115 | server.listen(port, done)
116 | })
117 |
118 | it('should receive a request at a path with some query', function (done) {
119 | request(`coap://[::1]:${port}/abcd/ef/gh/?foo=bar&beep=bop`).end()
120 | server.on('request', (req) => {
121 | expect(req.url).to.eql('/abcd/ef/gh?foo=bar&beep=bop')
122 | done()
123 | })
124 | })
125 | })
126 | })
127 |
--------------------------------------------------------------------------------
/test/segmentation.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { expect } from 'chai'
10 | import { SegmentedTransmission } from '../lib/segmentation'
11 | import OutgoingMessage from '../lib/outgoing_message'
12 | import RetrySend from '../lib/retry_send'
13 | import { createSocket } from 'dgram'
14 |
15 | describe('Segmentation', () => {
16 | describe('Segmented Transmission', () => {
17 | it('Should throw invalid block size error', (done) => {
18 | const req = new OutgoingMessage({}, (req, packet) => {})
19 | expect(() => {
20 | new SegmentedTransmission(-1, req, {}) // eslint-disable-line no-new
21 | }).to.throw('invalid block size -1')
22 | expect(() => {
23 | new SegmentedTransmission(7, req, {}) // eslint-disable-line no-new
24 | }).to.throw('invalid block size 7')
25 | setImmediate(done)
26 | })
27 | })
28 |
29 | describe('Set Block Size Exponent', () => {
30 | it('should set bytesize to value', (done) => {
31 | const req = new OutgoingMessage({}, (req, packet) => {})
32 | const v = new SegmentedTransmission(1, req, { payload: Buffer.from([0x1]) })
33 | v.setBlockSizeExp(6)
34 | expect(v.blockState.size).to.eql(6)
35 | expect(v.byteSize).to.eql(1024)
36 | setImmediate(done)
37 | })
38 | })
39 |
40 | // Update Block State
41 |
42 | describe('Is Correct Acknowledgement', () => {
43 | it('Should return true', (done) => {
44 | const req = new OutgoingMessage({}, (req, packet) => {})
45 | const v = new SegmentedTransmission(1, req, { payload: Buffer.from([0x1]) })
46 | const value = v.isCorrectACK({ num: 0, more: 0, size: 8 })
47 | expect(value).to.eql(true)
48 | setImmediate(done)
49 | })
50 | it('Should return false', (done) => {
51 | const req = new OutgoingMessage({}, (req, packet) => {})
52 | const v = new SegmentedTransmission(1, req, { payload: Buffer.from([0x1]) })
53 | const value = v.isCorrectACK({ num: 1, more: 0, size: 8 })
54 | expect(value).to.eql(false)
55 | setImmediate(done)
56 | })
57 | })
58 |
59 | describe('Resend Previous Packet', () => {
60 | it('Should increment resend count', (done) => {
61 | const req = new OutgoingMessage({}, (req, packet) => {})
62 | const v = new SegmentedTransmission(1, req, { payload: Buffer.from([0x1]) })
63 | v.resendCount = 2
64 | v.totalLength = 0
65 | v.currentByte = 1
66 | v.resendPreviousPacket()
67 | expect(v.resendCount).to.eql(3)
68 | setImmediate(done)
69 | })
70 | // should send next packet
71 | it('Should throw error', (done) => {
72 | const req = new OutgoingMessage({}, (req, packet) => {})
73 | const v = new SegmentedTransmission(1, req, { payload: Buffer.from([0x1]) })
74 | v.resendCount = 6
75 | expect(() => {
76 | v.resendPreviousPacket()
77 | }).throw('Too many block re-transfers')
78 | setImmediate(done)
79 | })
80 | })
81 |
82 | describe('Recieve Acknowledgement', () => {
83 | // Should re-set block size exp
84 | // should send next packet
85 | it('Should set resend count to 0', (done) => {
86 | const req = new OutgoingMessage({}, (req, packet) => {})
87 | req.sender = new RetrySend(createSocket('udp4'), 5683, 'localhost')
88 | const v = new SegmentedTransmission(1, req, { payload: Buffer.from([0x1]) })
89 | v.receiveACK({ size: 1, more: 0, num: 0 })
90 | v.totalLength = 0
91 | v.currentByte = 0
92 | expect(v.resendCount).to.eql(0)
93 | setImmediate(done)
94 | })
95 | })
96 |
97 | describe('Remaining', () => {
98 | it('Should return a value', (done) => {
99 | const req = new OutgoingMessage({}, (req, packet) => {})
100 | const v = new SegmentedTransmission(1, req, { payload: Buffer.from([0x1]) })
101 | v.totalLength = 0
102 | v.currentByte = 0
103 | expect(v.remaining()).to.eql(0)
104 | setImmediate(done)
105 | })
106 | })
107 |
108 | // Send Next
109 | })
110 |
--------------------------------------------------------------------------------
/test/helpers.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { expect } from 'chai'
10 |
11 | import { toCode, getOption, hasOption, removeOption, parseBlock2, createBlock2 } from '../lib/helpers'
12 |
13 | describe('Helpers', () => {
14 | describe('Has Options', () => {
15 | it('Should return true', (done) => {
16 | const options = [
17 | { name: 'test', value: 'hello' },
18 | { name: 'test2', value: 'world' }
19 | ]
20 | expect(hasOption(options, 'test')).to.eql(true)
21 | setImmediate(done)
22 | })
23 |
24 | it('Should return null', (done) => {
25 | const options = [
26 | { name: 'test2', value: 'world' }
27 | ]
28 | expect(hasOption(options, 'test')).to.eql(null)
29 | setImmediate(done)
30 | })
31 | })
32 |
33 | describe('Get Options', () => {
34 | it('Should return option value', (done) => {
35 | const options = [
36 | { name: 'test', value: 'hello' },
37 | { name: 'test2', value: 'world' }
38 | ]
39 | expect(getOption(options, 'test')).to.eql('hello')
40 | setImmediate(done)
41 | })
42 |
43 | it('Should return null', (done) => {
44 | const options = [
45 | { name: 'test2', value: 'world' }
46 | ]
47 | expect(getOption(options, 'test')).to.eql(null)
48 | setImmediate(done)
49 | })
50 | })
51 |
52 | describe('Remove Options', () => {
53 | it('Should return true', (done) => {
54 | const options = [
55 | { name: 'test', value: 'hello' },
56 | { name: 'test2', value: 'world' }
57 | ]
58 | expect(removeOption(options, 'test')).to.eql(true)
59 | setImmediate(done)
60 | })
61 |
62 | it('Should return false', (done) => {
63 | const options = [
64 | { name: 'test2', value: 'world' }
65 | ]
66 | expect(removeOption(options, 'test')).to.eql(false)
67 | setImmediate(done)
68 | })
69 | })
70 |
71 | describe('Parse Block2', () => {
72 | it('Should have case 3 equal 4128', (done) => {
73 | const buff = Buffer.from([0x01, 0x02, 0x03])
74 | const res = parseBlock2(buff)
75 | if (res != null) {
76 | expect(res.num).to.eql(4128)
77 | setImmediate(done)
78 | }
79 | })
80 |
81 | it('Should return null', (done) => {
82 | const buff = Buffer.from([0x01, 0x02, 0x03, 0x04])
83 | const res = parseBlock2(buff)
84 | expect(res).to.eql(null)
85 | setImmediate(done)
86 | })
87 |
88 | it('Should parse a zero length buffer', (done) => {
89 | const buff = Buffer.alloc(0)
90 | const res = parseBlock2(buff)
91 | expect(res).to.eql({ more: 0, num: 0, size: 0 })
92 | setImmediate(done)
93 | })
94 | })
95 |
96 | describe('Create Block2', () => {
97 | it('Should return a buffer carrying a block 2 value', (done) => {
98 | const buff = Buffer.from([0xff, 0xff, 0xe9])
99 | const block = { more: 1, num: 1048574, size: 32 }
100 | const res = createBlock2(block)
101 | expect(res).to.eql(buff)
102 | setImmediate(done)
103 | })
104 |
105 | it('Should return null', (done) => {
106 | const block = { more: 1, num: 1048576, size: 32 }
107 | const res = createBlock2(block)
108 | expect(res).to.eql(null)
109 | setImmediate(done)
110 | })
111 | })
112 |
113 | describe('Convert Codes', () => {
114 | it('Should keep codes with type string', (done) => {
115 | expect(toCode('2.05')).to.eql('2.05')
116 | setImmediate(done)
117 | })
118 |
119 | it('Should convert numeric codes with zeros inbetween', (done) => {
120 | expect(toCode(404)).to.eql('4.04')
121 | setImmediate(done)
122 | })
123 |
124 | it('Should convert numeric codes', (done) => {
125 | expect(toCode(415)).to.eql('4.15')
126 | setImmediate(done)
127 | })
128 | })
129 |
130 | // genAck
131 |
132 | // packetToMessage
133 |
134 | // removeOption
135 |
136 | // or
137 |
138 | // isOptions
139 |
140 | // isNumeric
141 |
142 | // isBoolean
143 | })
144 |
--------------------------------------------------------------------------------
/lib/parameters.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { Parameters, ParametersUpdate } from '../models/models'
10 |
11 | /**
12 | * Timeout in seconds for a response to a confirmable request.
13 | */
14 | const ACK_TIMEOUT = 2
15 |
16 | /**
17 | * Used to calculate upper bound for timeout.
18 | */
19 | const ACK_RANDOM_FACTOR = 1.5
20 |
21 | /**
22 | * Maximum number of retransmissions for a confirmable request.
23 | * Defaults to 4.
24 | */
25 | const MAX_RETRANSMIT = 4
26 |
27 | /**
28 | * Allowed number of simultaneous outstanding interactions.
29 | * Defaults to 1.
30 | *
31 | */
32 | const NSTART = 1
33 |
34 | /**
35 | * Maximum timespan (in seconds) that is waited by default before
36 | * responding to a multicast request. Defaults to 5 seconds.
37 | *
38 | * The exact point of time for responding is chosen randomly.
39 | */
40 | const DEFAULT_LEISURE = 5
41 |
42 | /**
43 | * Indicates the average data rate (in bytes) that must not be exceeded
44 | * by a CoAP endpoint in sending to a peer endpoint that does not respond.
45 | *
46 | * Defaults to 1 byte / second.
47 | */
48 | const PROBING_RATE = 1
49 |
50 | /**
51 | * Maximum time from the first transmission of a Confirmable message
52 | * to its last retransmission.
53 | *
54 | * It is calculated as follows:
55 | *
56 | * ```
57 | * ACK_TIMEOUT * ((2 ** MAX_RETRANSMIT) - 1) * ACK_RANDOM_FACTOR
58 | * ```
59 | */
60 | const MAX_TRANSMIT_SPAN = 45
61 |
62 | /**
63 | * MAX_TRANSMIT_WAIT is the maximum time (in seconds) from the first
64 | * transmission of a Confirmable message to the time when the sender
65 | * gives up on receiving an acknowledgement or reset.
66 | *
67 | * It is calculated as follows:
68 | *
69 | * ```
70 | * ACK_TIMEOUT * ((2 ** (MAX_RETRANSMIT + 1)) - 1) * ACK_RANDOM_FACTOR
71 | * ```
72 | */
73 | const MAX_TRANSMIT_WAIT = 93
74 |
75 | /**
76 | * Maximum time (in seconds) a datagram is expected to
77 | * take from the start of its transmission to the
78 | * completion of its reception.
79 | *
80 | * Arbitrarily set to 100 seconds as the default value.
81 | */
82 | const MAX_LATENCY = 100
83 |
84 | /**
85 | * The time a node takes to turn around a Confirmable message
86 | * into an acknowledgement. Uses `ACK_TIMEOUT` (two seconds) as
87 | * the default value.
88 | */
89 | const PROCESSING_DELAY = 2
90 |
91 | /**
92 | * Maximum round-trip time. Defaults to 202 seconds.
93 | *
94 | * It is calculated as follows:
95 | *
96 | * ```
97 | * (2 * MAX_LATENCY) + PROCESSING_DELAY
98 | * ```
99 | */
100 | const MAX_RTT = 202
101 |
102 | /**
103 | * Time from starting to send a confirmable message to the time when an
104 | * acknowledgement is no longer expected, i.e. message layer information
105 | * about the message exchange can be purged. Defaults to 247 seconds.
106 | *
107 | * It is calculated as follows:
108 | *
109 | * ```
110 | * MAX_TRANSMIT_SPAN + (2 * MAX_LATENCY) + PROCESSING_DELAY
111 | * ```
112 | */
113 | const EXCHANGE_LIFETIME = 247
114 |
115 | /**
116 | * Time from sending a Non-confirmable message to
117 | * the time its Message ID can be safely reused.
118 | * Defaults to 145 seconds.
119 | *
120 | * It is calculated as follows:
121 | *
122 | * ```
123 | * MAX_TRANSMIT_SPAN + MAX_LATENCY
124 | * ```
125 | */
126 | const NON_LIFETIME = 145
127 |
128 | /**
129 | * Default UDP port used by CoAP.
130 | */
131 | const COAP_PORT = 5683
132 |
133 | /**
134 | * Maximum total size of the CoAP application layer message, as
135 | * recommended by the CoAP specification
136 | */
137 | const MAX_MESSAGE_SIZE = 1152
138 |
139 | /**
140 | * Default max payload size recommended in the CoAP specification
141 | * For more info see RFC 7252 Section 4.6
142 | */
143 | const MAX_PAYLOAD_SIZE = 1024
144 |
145 | /* Custom default parameters */
146 |
147 | /**
148 | * Indicates if ACK messages should be sent for non-confirmable packages.
149 | *
150 | * `true`: always send CoAP ACK messages, even for non confirmabe packets.
151 | * `false`: only send CoAP ACK messages for confirmabe packets.
152 | */
153 | const sendAcksForNonConfirmablePackets = true
154 |
155 | /**
156 | * Number of milliseconds to wait for a piggyback response.
157 | */
158 | const piggybackReplyMs = 50
159 |
160 | /**
161 | * LRU prune timer period.
162 | *
163 | * In order to reduce unnecessary heap usage on low-traffic servers the
164 | * LRU cache is periodically pruned to remove old, expired packets. This
165 | * is a fairly low-intensity task, but the period can be altered here
166 | * or the timer disabled by setting the value to zero.
167 | * By default the value is set to `0.5 * EXCHANGE_LIFETIME` (~120s).
168 | */
169 | const pruneTimerPeriod = 0.5 * EXCHANGE_LIFETIME
170 |
171 | const p: Parameters = {
172 | ackTimeout: ACK_TIMEOUT,
173 | ackRandomFactor: ACK_RANDOM_FACTOR,
174 | maxRetransmit: MAX_RETRANSMIT,
175 | nstart: NSTART,
176 | defaultLeisure: DEFAULT_LEISURE,
177 | probingRate: PROBING_RATE,
178 | exchangeLifetime: EXCHANGE_LIFETIME,
179 | maxRTT: MAX_RTT,
180 | maxTransmitSpan: MAX_TRANSMIT_SPAN,
181 | maxTransmitWait: MAX_TRANSMIT_WAIT,
182 | processingDelay: PROCESSING_DELAY,
183 | maxLatency: MAX_LATENCY,
184 | nonLifetime: NON_LIFETIME,
185 | coapPort: COAP_PORT,
186 | maxPayloadSize: MAX_PAYLOAD_SIZE,
187 | maxMessageSize: MAX_MESSAGE_SIZE,
188 | sendAcksForNonConfirmablePackets,
189 | piggybackReplyMs,
190 | pruneTimerPeriod
191 | }
192 |
193 | const defaultParameters: Parameters = JSON.parse(JSON.stringify(p))
194 |
195 | function refreshTiming (values?: ParametersUpdate): void {
196 | for (const key in values) {
197 | if (p[key] != null) {
198 | p[key] = values[key]
199 | }
200 | }
201 |
202 | p.maxTransmitSpan = p.ackTimeout * ((Math.pow(2, p.maxRetransmit)) - 1) * p.ackRandomFactor
203 |
204 | p.maxTransmitWait = p.ackTimeout * (Math.pow(2, p.maxRetransmit + 1) - 1) * p.ackRandomFactor
205 |
206 | p.processingDelay = p.ackTimeout
207 |
208 | p.maxRTT = 2 * p.maxLatency + p.processingDelay
209 |
210 | p.exchangeLifetime = p.maxTransmitSpan + p.maxRTT
211 |
212 | if (values != null && typeof values.pruneTimerPeriod === 'number') {
213 | p.pruneTimerPeriod = values.pruneTimerPeriod
214 | } else {
215 | p.pruneTimerPeriod = (0.5 * p.exchangeLifetime)
216 | }
217 | }
218 |
219 | function defaultTiming (): void {
220 | refreshTiming(defaultParameters)
221 | }
222 |
223 | p.defaultTiming = defaultTiming
224 | p.refreshTiming = refreshTiming
225 |
226 | export { p as parameters, refreshTiming, defaultTiming }
227 |
--------------------------------------------------------------------------------
/lib/option_converter.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { OptionValue } from '../models/models'
10 |
11 | // Generic toBinary and fromBinary definitions
12 | const optionToBinaryFunctions = {}
13 | const optionFromBinaryFunctions = {}
14 |
15 | // list of options silently ignored
16 | const ignoredOptions = {}
17 |
18 | export function toBinary (name: string, value: Buffer | any): Buffer {
19 | if (Buffer.isBuffer(value)) {
20 | return value
21 | }
22 |
23 | if (optionToBinaryFunctions[name] == null) {
24 | throw new Error('Unknown string to Buffer converter for option: ' + name)
25 | }
26 |
27 | return optionToBinaryFunctions[name](value)
28 | }
29 |
30 | export function fromBinary (name: string, value: Buffer): any {
31 | const convert = optionFromBinaryFunctions[name]
32 |
33 | if (convert == null) {
34 | return value
35 | }
36 |
37 | return convert(value)
38 | }
39 |
40 | export function registerOption (name: string, toBinary: (value: OptionValue) => Buffer | null, fromBinary: (value: Buffer) => OptionValue | null): void {
41 | optionFromBinaryFunctions[name] = fromBinary
42 | optionToBinaryFunctions[name] = toBinary
43 | }
44 |
45 | export function ignoreOption (name: string): void {
46 | ignoredOptions[name] = true
47 | }
48 |
49 | ignoreOption('Cache-Control')
50 | ignoreOption('Content-Length')
51 | ignoreOption('Accept-Ranges')
52 |
53 | export function isIgnored (name: string): boolean {
54 | return ignoredOptions[name] != null
55 | }
56 |
57 | // ETag option registration
58 | const fromString = (result: string): Buffer => {
59 | return Buffer.from(result)
60 | }
61 |
62 | const toString = (value: Buffer): string => {
63 | return value.toString()
64 | }
65 |
66 | registerOption('ETag', fromString, toString)
67 | registerOption('Location-Path', fromString, toString)
68 | registerOption('Location-Query', fromString, toString)
69 | registerOption('Proxy-Uri', fromString, toString)
70 |
71 | const fromUint = (result: number): Buffer => {
72 | let uint = Number(result)
73 | if (!isFinite(uint) || Math.floor(uint) !== uint || uint < 0) {
74 | throw TypeError(`Expected uint, got ${result}`)
75 | }
76 | const parts: number[] = []
77 | while (uint > 0) {
78 | parts.unshift(uint % 256)
79 | uint = Math.floor(uint / 256)
80 | }
81 | return Buffer.from(parts)
82 | }
83 |
84 | const toUint = (value: Buffer): number => {
85 | let result = 0
86 | for (let i = 0; i < value.length; ++i) {
87 | result = 256 * result + value[i]
88 | }
89 | return result
90 | }
91 |
92 | registerOption('Max-Age', fromUint, toUint)
93 | registerOption('Size2', fromUint, toUint)
94 | registerOption('Size1', fromUint, toUint)
95 |
96 | // Content-Format and Accept options registration
97 | const formatsString = {}
98 | const formatsBinaries = {}
99 |
100 | /**
101 | * Registers a new Content-Format.
102 | *
103 | * @param name Media-Type and parameters.
104 | * @param value The numeric code of the Content-Format.
105 | */
106 | export function registerFormat (name: string, value: number): void {
107 | const bytes = numberToBuffer(value)
108 |
109 | formatsString[name] = bytes
110 | formatsBinaries[value] = name
111 | }
112 |
113 | module.exports.registerFormat = registerFormat
114 |
115 | // See https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats
116 | // for a list of all registered content-formats
117 | const supportedContentFormats = {
118 | 'text/plain': 0,
119 | 'application/cose; cose-type="cose-encrypt0"': 16,
120 | 'application/cose; cose-type="cose-mac0"': 17,
121 | 'application/cose; cose-type="cose-sign1"': 18,
122 | 'application/link-format': 40,
123 | 'application/xml': 41,
124 | 'application/octet-stream': 42,
125 | 'application/exi': 47,
126 | 'application/json': 50,
127 | 'application/json-patch+json': 51,
128 | 'application/merge-patch+json': 52,
129 | 'application/cbor': 60,
130 | 'application/cwt': 61,
131 | 'application/multipart-core': 62,
132 | 'application/cbor-seq': 63,
133 | 'application/cose-key': 101,
134 | 'application/cose-key-set': 102,
135 | 'application/senml+json': 110,
136 | 'application/sensml+json': 111,
137 | 'application/senml+cbor': 112,
138 | 'application/sensml+cbor': 113,
139 | 'application/senml-exi': 114,
140 | 'application/sensml-exi': 115,
141 | 'application/coap-group+json': 256,
142 | 'application/dots+cbor': 271,
143 | 'application/missing-blocks+cbor-seq': 272,
144 | 'application/pkcs7-mime; smime-type=server-generated-key': 280,
145 | 'application/pkcs7-mime; smime-type=certs-only': 281,
146 | 'application/pkcs8': 284,
147 | 'application/csrattrs': 285,
148 | 'application/pkcs10': 286,
149 | 'application/pkix-cert': 287,
150 | 'application/senml+xml': 310,
151 | 'application/sensml+xml': 311,
152 | 'application/senml-etch+json': 320,
153 | 'application/senml-etch+cbor': 322,
154 | 'application/td+json': 432,
155 | 'application/vnd.ocf+cbor': 10000,
156 | 'application/oscore': 10001,
157 | 'application/javascript': 10002,
158 | 'application/vnd.oma.lwm2m+tlv': 11542,
159 | 'application/vnd.oma.lwm2m+json': 11543,
160 | 'application/vnd.oma.lwm2m+cbor': 11544,
161 | 'text/css': 20000,
162 | 'image/svg+xml': 30000
163 | }
164 |
165 | for (const [name, value] of Object.entries(supportedContentFormats)) {
166 | registerFormat(name, value)
167 | }
168 |
169 | function contentFormatToBinary (value: string | number): Buffer {
170 | if (typeof value === 'number') {
171 | return numberToBuffer(value)
172 | }
173 |
174 | if (formatsString[value] != null) {
175 | return formatsString[value]
176 | }
177 |
178 | const result = formatsString[value.split(';')[0]]
179 | if (result == null) {
180 | throw new Error('Unknown Content-Format: ' + value)
181 | }
182 |
183 | return result
184 | }
185 |
186 | function contentFormatToString (value: Buffer): string | number | null {
187 | if (value.length === 0) {
188 | return formatsBinaries[0]
189 | }
190 |
191 | let numericValue: number
192 | if (value.length === 1) {
193 | numericValue = value.readUInt8(0)
194 | } else if (value.length === 2) {
195 | numericValue = value.readUInt16BE(0)
196 | } else {
197 | return null
198 | }
199 |
200 | const result = formatsBinaries[numericValue]
201 |
202 | if (result == null) {
203 | return numericValue
204 | }
205 |
206 | return result
207 | }
208 |
209 | registerOption('Content-Format', contentFormatToBinary, contentFormatToString)
210 | registerOption('Accept', contentFormatToBinary, contentFormatToString)
211 | registerOption('Observe', (sequence: number) => {
212 | let buf: Buffer
213 |
214 | if (sequence == null) {
215 | buf = Buffer.alloc(0)
216 | } else if (sequence <= 0xff) {
217 | buf = Buffer.of(sequence)
218 | } else if (sequence <= 0xffff) {
219 | buf = Buffer.of(sequence >> 8, sequence)
220 | } else {
221 | buf = Buffer.of(sequence >> 16, sequence >> 8, sequence)
222 | }
223 |
224 | return buf
225 | }, (buf) => {
226 | let result = 0
227 |
228 | if (buf.length === 1) {
229 | result = buf.readUInt8(0)
230 | } else if (buf.length === 2) {
231 | result = buf.readUInt16BE(0)
232 | } else if (buf.length === 3) {
233 | result = (buf.readUInt8(0) << 16) | buf.readUInt16BE(1)
234 | }
235 |
236 | return result
237 | })
238 |
239 | function numberToBuffer (value: number): Buffer {
240 | let buffer: Buffer
241 |
242 | if (value > 255) {
243 | buffer = Buffer.alloc(2)
244 | buffer.writeUInt16BE(value, 0)
245 | } else {
246 | buffer = Buffer.of(value)
247 | }
248 |
249 | return buffer
250 | }
251 |
--------------------------------------------------------------------------------
/lib/helpers.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2021 node-coap contributors.
3 | *
4 | * node-coap is licensed under an MIT +no-false-attribs license.
5 | * All rights not explicitly granted in the MIT license are reserved.
6 | * See the included LICENSE file for more details.
7 | */
8 |
9 | import { OptionName, NamedOption, Packet } from 'coap-packet'
10 | import { CoapPacket, Option, OptionValue, Block } from '../models/models'
11 | import { toBinary, fromBinary, isIgnored } from './option_converter'
12 | import capitalize from 'capitalize'
13 |
14 | const codes = {
15 | 0.01: 'GET',
16 | 0.02: 'POST',
17 | 0.03: 'PUT',
18 | 0.04: 'DELETE',
19 | 0.05: 'FETCH',
20 | 0.06: 'PATCH',
21 | 0.07: 'iPATCH'
22 | }
23 |
24 | export function genAck (request: Packet): Packet {
25 | return {
26 | messageId: request.messageId,
27 | code: '0.00',
28 | options: [],
29 | confirmable: false,
30 | ack: true,
31 | reset: false
32 | }
33 | }
34 |
35 | const optionAliases = {
36 | 'Content-Type': 'Content-Format',
37 | Etag: 'ETag'
38 | }
39 |
40 | export function setOption (packet: Packet, name: OptionName | string, values: OptionValue): void {
41 | name = capitalize.words(name)
42 | name = optionAliases[name] ?? name
43 | const optionName: OptionName = name as OptionName
44 |
45 | if (isIgnored(name)) {
46 | return
47 | }
48 |
49 | packet.options = packet.options?.filter((option) => {
50 | return option.name !== name
51 | })
52 |
53 | if (!Array.isArray(values)) {
54 | packet.options?.push({
55 | name: optionName,
56 | value: toBinary(name, values)
57 | })
58 | } else {
59 | for (const value of values) {
60 | packet.options?.push({ name: optionName, value })
61 | }
62 | }
63 | }
64 |
65 | export function toCode (code: string | number): string {
66 | if (typeof code === 'string') {
67 | return code
68 | }
69 |
70 | const codeClass = Math.floor(code / 100)
71 | const codeDetail = String(code - codeClass * 100).padStart(2, "0")
72 |
73 | return `${codeClass}.${codeDetail}`
74 | }
75 |
76 | export function packetToMessage (dest: any, packet: CoapPacket): void {
77 | const options = packet.options ?? []
78 | const paths: Buffer[] = []
79 | const queries: Buffer[] = []
80 | let query = ''
81 |
82 | dest.payload = packet.payload
83 | dest.options = packet.options
84 | dest.code = packet.code
85 | dest.method = codes[dest.code]
86 | dest.headers = {}
87 |
88 | for (let i = 0; i < options.length; i++) {
89 | const option = options[i]
90 |
91 | if (typeof option.name !== 'string') {
92 | continue
93 | }
94 |
95 | if (option.name === 'Uri-Path') {
96 | paths.push(option.value)
97 | }
98 |
99 | if (option.name === 'Uri-Query') {
100 | queries.push(option.value)
101 | }
102 |
103 | option.value = fromBinary(option.name, option.value)
104 |
105 | if (option.value != null && !Buffer.isBuffer(option.value)) {
106 | dest.headers[option.name] = option.value
107 | }
108 | }
109 |
110 | if (dest.headers['Content-Format'] != null) {
111 | dest.headers['Content-Type'] = dest.headers['Content-Format']
112 | }
113 |
114 | query = queries.join('&')
115 | let url = '/' + paths.join('/')
116 | if (query !== '') {
117 | url += '?' + query
118 | }
119 | dest.url = url
120 | }
121 |
122 | export function hasOption (options: Array