├── .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