├── .eslintrc ├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ ├── lint.yml │ └── npm-test.yml ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── examples ├── blockwise.js ├── blockwise_put.js ├── client.js ├── client_and_server.js ├── delayed_response.js ├── json.js ├── multicast_client_server.js ├── observe_client.js ├── observe_eclipse.js ├── observe_server.js ├── proxy.js ├── req_with_payload.js └── server.js ├── index.ts ├── lib ├── agent.ts ├── block.ts ├── cache.ts ├── helpers.ts ├── incoming_message.ts ├── middlewares.ts ├── observe_read_stream.ts ├── observe_write_stream.ts ├── option_converter.ts ├── outgoing_message.ts ├── parameters.ts ├── retry_send.ts ├── segmentation.ts └── server.ts ├── models └── models.ts ├── package.json ├── test ├── agent.ts ├── blockwise.ts ├── cache.ts ├── common.ts ├── end-to-end.ts ├── helpers.ts ├── ipv6.ts ├── mocha.opts ├── parameters.ts ├── proxy.ts ├── request.ts ├── retry_send.ts ├── segmentation.ts ├── server.ts └── share-socket.ts └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Apollon77, JKRhb] 2 | custom: ['https://paypal.me/jkrhb', 'https://paypal.me/Apollon77'] 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 0, 3 | "exit": true, 4 | "reporter": "spec" 5 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/agent.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 = require('crypto') 10 | import { Socket, createSocket } from 'dgram' 11 | import { AgentOptions, CoapRequestParams, Block } from '../models/models' 12 | import { EventEmitter } from 'events' 13 | import { parse, generate, ParsedPacket } from 'coap-packet' 14 | import IncomingMessage from './incoming_message' 15 | import OutgoingMessage from './outgoing_message' 16 | import ObserveStream from './observe_read_stream' 17 | import RetrySend from './retry_send' 18 | import { parseBlock2, createBlock2, getOption, removeOption } from './helpers' 19 | import { SegmentedTransmission } from './segmentation' 20 | import { parseBlockOption } from './block' 21 | import { AddressInfo } from 'net' 22 | import { parameters } from './parameters' 23 | 24 | const maxToken = Math.pow(2, 32) 25 | const maxMessageId = Math.pow(2, 16) 26 | 27 | class Agent extends EventEmitter { 28 | _opts: AgentOptions 29 | _closing: boolean 30 | _sock: Socket | null 31 | _msgIdToReq: Map 32 | _tkToReq: Map 33 | _tkToMulticastResAddr: Map 34 | private _lastToken: number 35 | _lastMessageId: number 36 | private _msgInFlight: number 37 | _requests: number 38 | constructor (opts?: AgentOptions) { 39 | super() 40 | 41 | if (opts == null) { 42 | opts = {} 43 | } 44 | 45 | if (opts.type == null) { 46 | opts.type = 'udp4' 47 | } 48 | 49 | if (opts.socket != null) { 50 | const sock = opts.socket as any 51 | opts.type = sock.type 52 | delete opts.port 53 | } 54 | 55 | this._opts = opts 56 | 57 | this._init(opts.socket) 58 | } 59 | 60 | _init (socket?: Socket): void { 61 | this._closing = false 62 | 63 | if (this._sock != null) { 64 | return 65 | } 66 | 67 | this._sock = socket ?? createSocket({ type: this._opts.type ?? 'udp4' }) 68 | this._sock.on('message', (msg, rsinfo) => { 69 | let packet: ParsedPacket 70 | try { 71 | packet = parse(msg) 72 | } catch (err) { 73 | return 74 | } 75 | 76 | if (packet.code[0] === '0' && packet.code !== '0.00') { 77 | // ignore this packet since it's not a response. 78 | return 79 | } 80 | 81 | if (this._sock != null) { 82 | const outSocket = this._sock.address() 83 | this._handle(packet, rsinfo, outSocket) 84 | } 85 | }) 86 | 87 | if (this._opts.port != null) { 88 | this._sock.bind(this._opts.port) 89 | } 90 | 91 | this._sock.on('error', (err) => { 92 | this.emit('error', err) 93 | }) 94 | 95 | this._msgIdToReq = new Map() 96 | this._tkToReq = new Map() 97 | this._tkToMulticastResAddr = new Map() 98 | 99 | this._lastToken = Math.floor(Math.random() * (maxToken - 1)) 100 | this._lastMessageId = Math.floor(Math.random() * (maxMessageId - 1)) 101 | 102 | this._msgInFlight = 0 103 | this._requests = 0 104 | } 105 | 106 | close (done?: (err?: Error) => void): this { 107 | if (this._msgIdToReq.size === 0 && this._msgInFlight === 0) { 108 | // No requests in flight, close immediately 109 | this._doClose(done) 110 | return this 111 | } 112 | 113 | done = done ?? (() => {}) 114 | this.once('close', done) 115 | for (const req of this._msgIdToReq.values()) { 116 | this.abort(req) 117 | } 118 | return this 119 | } 120 | 121 | _cleanUp (): void { 122 | if (--this._requests !== 0) { 123 | return 124 | } 125 | 126 | if (this._opts.socket == null) { 127 | this._closing = true 128 | } 129 | 130 | if (this._msgInFlight > 0) { 131 | return 132 | } 133 | 134 | this._doClose() 135 | } 136 | 137 | _doClose (done?: (err?: Error) => void): void { 138 | for (const req of this._msgIdToReq.values()) { 139 | req.sender.reset() 140 | } 141 | 142 | if (this._opts.socket != null) { 143 | return 144 | } 145 | 146 | if (this._sock == null) { 147 | this.emit('close') 148 | return 149 | } 150 | 151 | this._sock.close(() => { 152 | this._sock = null 153 | if (done != null) { 154 | done() 155 | } 156 | this.emit('close') 157 | }) 158 | } 159 | 160 | _handle (packet: ParsedPacket, rsinfo: AddressInfo, outSocket: AddressInfo): void { 161 | let buf: Buffer 162 | let response: IncomingMessage 163 | let req: OutgoingMessage | undefined = this._msgIdToReq.get(packet.messageId) 164 | const ackSent = (err: Error): void => { 165 | if (err != null && req != null) { 166 | req.emit('error', err) 167 | } 168 | 169 | this._msgInFlight-- 170 | if (this._closing && this._msgInFlight === 0) { 171 | this._doClose() 172 | } 173 | } 174 | if (req == null) { 175 | if (packet.token.length > 0) { 176 | req = this._tkToReq.get(packet.token.toString('hex')) 177 | } 178 | 179 | if ((packet.ack || packet.reset) && req == null) { 180 | // Nothing to do on unknown or duplicate ACK/RST packet 181 | return 182 | } 183 | 184 | if (req == null) { 185 | buf = generate({ 186 | code: '0.00', 187 | reset: true, 188 | messageId: packet.messageId 189 | }) 190 | 191 | if (this._sock != null) { 192 | this._msgInFlight++ 193 | this._sock.send(buf, 0, buf.length, rsinfo.port, rsinfo.address, ackSent) 194 | } 195 | return 196 | } 197 | } 198 | 199 | if (packet.confirmable) { 200 | buf = generate({ 201 | code: '0.00', 202 | ack: true, 203 | messageId: packet.messageId 204 | }) 205 | 206 | if (this._sock != null) { 207 | this._msgInFlight++ 208 | this._sock.send(buf, 0, buf.length, rsinfo.port, rsinfo.address, ackSent) 209 | } 210 | } 211 | 212 | if (packet.code !== '0.00' && (req._packet.token == null || req._packet.token.length !== packet.token.length || Buffer.compare(req._packet.token, packet.token) !== 0)) { 213 | // The tokens don't match, ignore the message since it is a malformed response 214 | return 215 | } 216 | 217 | const block1Buff = getOption(packet.options, 'Block1') 218 | let block1: Block | null = null 219 | if (block1Buff instanceof Buffer) { 220 | block1 = parseBlockOption(block1Buff) 221 | // check for error 222 | if (block1 == null) { 223 | req.sender.reset() 224 | req.emit('error', new Error('Failed to parse block1')) 225 | return 226 | } 227 | } 228 | 229 | req.sender.reset() 230 | 231 | if (block1 != null && packet.ack) { 232 | // If the client takes too long to respond then the retry sender will send 233 | // another packet with the previous messageId, which we've already removed. 234 | const segmentedSender = req.segmentedSender 235 | if (segmentedSender != null) { 236 | // If there's more to send/receive, then carry on! 237 | if (segmentedSender.remaining() > 0) { 238 | if (segmentedSender.isCorrectACK(block1)) { 239 | if (req._packet.messageId != null) { 240 | this._msgIdToReq.delete(req._packet.messageId) 241 | } 242 | req._packet.messageId = this._nextMessageId() 243 | this._msgIdToReq.set(req._packet.messageId, req) 244 | segmentedSender.receiveACK(block1) 245 | } else { 246 | segmentedSender.resendPreviousPacket() 247 | } 248 | return 249 | } else { 250 | // console.log("Packet received done"); 251 | if (req._packet.options != null) { 252 | removeOption(req._packet.options, 'Block1') 253 | } 254 | delete req.segmentedSender 255 | } 256 | } 257 | } 258 | 259 | if (!packet.confirmable && !req.multicast) { 260 | this._msgIdToReq.delete(packet.messageId) 261 | } 262 | 263 | // Drop empty messages (ACKs), but process RST 264 | if (packet.code === '0.00' && !packet.reset) { 265 | return 266 | } 267 | 268 | const block2Buff = getOption(packet.options, 'Block2') 269 | let block2: Block | null = null 270 | // if we got blockwise (2) response 271 | if (block2Buff instanceof Buffer) { 272 | block2 = parseBlock2(block2Buff) 273 | // check for error 274 | if (block2 == null) { 275 | req.sender.reset() 276 | req.emit('error', new Error('failed to parse block2')) 277 | return 278 | } 279 | } 280 | if (block2 != null) { 281 | if (req.multicast) { 282 | req = this._convertMulticastToUnicastRequest(req, rsinfo) 283 | if (req == null) { 284 | return 285 | } 286 | } 287 | 288 | // accumulate payload 289 | req._totalPayload = Buffer.concat([req._totalPayload, packet.payload]) 290 | 291 | if (block2.more === 1) { 292 | // increase message id for next request 293 | if (req._packet.messageId != null) { 294 | this._msgIdToReq.delete(req._packet.messageId) 295 | } 296 | req._packet.messageId = this._nextMessageId() 297 | this._msgIdToReq.set(req._packet.messageId, req) 298 | 299 | // next block2 request 300 | const block2Val = createBlock2({ 301 | more: 0, 302 | num: block2.num + 1, 303 | size: block2.size 304 | }) 305 | if (block2Val == null) { 306 | req.sender.reset() 307 | req.emit('error', new Error('failed to create block2')) 308 | return 309 | } 310 | req.setOption('Block2', block2Val) 311 | req._packet.payload = undefined 312 | req.sender.send(generate(req._packet)) 313 | 314 | return 315 | } else { 316 | // get full payload 317 | packet.payload = req._totalPayload 318 | // clear the payload incase of block2 319 | req._totalPayload = Buffer.alloc(0) 320 | } 321 | } 322 | 323 | const observe = req.url.observe != null && [true, 0, '0'].includes(req.url.observe) 324 | 325 | if (req.response != null) { 326 | const response: any = req.response 327 | if (response.append != null) { 328 | // it is an observe request 329 | // and we are already streaming 330 | return response.append(packet) 331 | } else { 332 | // TODO There is a previous response but is not an ObserveStream ! 333 | return 334 | } 335 | } else if (block2 != null && packet.token != null && !observe) { 336 | this._tkToReq.delete(packet.token.toString('hex')) 337 | } else if (!observe && !req.multicast) { 338 | // it is not, so delete the token 339 | this._tkToReq.delete(packet.token.toString('hex')) 340 | } 341 | 342 | if (observe && packet.code !== '4.04') { 343 | response = new ObserveStream(packet, rsinfo, outSocket) 344 | response.on('close', () => { 345 | this._tkToReq.delete(packet.token.toString('hex')) 346 | this._cleanUp() 347 | }) 348 | response.on('deregister', () => { 349 | const deregisterUrl = Object.assign({}, req?.url) 350 | deregisterUrl.observe = 1 351 | deregisterUrl.token = req?._packet.token 352 | 353 | const deregisterReq = this.request(deregisterUrl) 354 | // If the request fails, we'll deal with it with a RST message anyway. 355 | deregisterReq.on('error', () => {}) 356 | deregisterReq.end() 357 | }) 358 | } else { 359 | response = new IncomingMessage(packet, rsinfo, outSocket) 360 | } 361 | 362 | if (!req.multicast) { 363 | req.response = response 364 | } 365 | 366 | req.emit('response', response) 367 | } 368 | 369 | _nextToken (): Buffer { 370 | const buf = Buffer.alloc(8) 371 | 372 | if (++this._lastToken === maxToken) { 373 | this._lastToken = 0 374 | } 375 | 376 | buf.writeUInt32BE(this._lastToken, 0) 377 | crypto.randomBytes(4).copy(buf, 4) 378 | 379 | return buf 380 | } 381 | 382 | _nextMessageId (): number { 383 | if (++this._lastMessageId === maxMessageId) { 384 | this._lastMessageId = 0 385 | } 386 | 387 | return this._lastMessageId 388 | } 389 | 390 | /** 391 | * Entry point for a new client-side request. 392 | * @param url The parameters for the request 393 | */ 394 | request (url: CoapRequestParams): OutgoingMessage { 395 | this._init() 396 | 397 | const options = url.options ?? url.headers 398 | const multicastTimeout = url.multicastTimeout != null ? url.multicastTimeout : 20000 399 | const host = url.hostname ?? url.host 400 | const port = url.port ?? parameters.coapPort 401 | 402 | const req = new OutgoingMessage({}, (req, packet) => { 403 | if (url.confirmable !== false) { 404 | packet.confirmable = true 405 | } 406 | 407 | // multicast message should be forced non-confirmable 408 | if (url.multicast === true) { 409 | req.multicast = true 410 | packet.confirmable = false 411 | } 412 | 413 | if (!(packet.ack ?? packet.reset ?? false)) { 414 | packet.messageId = this._nextMessageId() 415 | if ((url.token instanceof Buffer) && (url.token.length > 0)) { 416 | if (url.token.length > 8) { 417 | return req.emit('error', new Error('Token may be no longer than 8 bytes.')) 418 | } 419 | packet.token = url.token 420 | } else { 421 | packet.token = this._nextToken() 422 | } 423 | const token = packet.token.toString('hex') 424 | if (req.multicast) { 425 | this._tkToMulticastResAddr.set(token, []) 426 | } 427 | if (token != null) { 428 | this._tkToReq.set(token, req) 429 | } 430 | } 431 | 432 | if (packet.messageId != null) { 433 | this._msgIdToReq.set(packet.messageId, req) 434 | } 435 | 436 | const block1Buff = getOption(packet.options, 'Block1') 437 | if (block1Buff != null) { 438 | // Setup for a segmented transmission 439 | req.segmentedSender = new SegmentedTransmission(block1Buff[0], req, packet) 440 | req.segmentedSender.sendNext() 441 | } else { 442 | let buf: Buffer 443 | try { 444 | buf = generate(packet) 445 | } catch (err) { 446 | req.sender.reset() 447 | return req.emit('error', err) 448 | } 449 | req.sender.send(buf, packet.confirmable === false) 450 | } 451 | }) 452 | 453 | req.sender = new RetrySend(this._sock, port, host, url.retrySend) 454 | 455 | req.url = url 456 | 457 | req.statusCode = url.method ?? 'GET' 458 | 459 | this.urlPropertyToPacketOption(url, req, 'pathname', 'Uri-Path', '/') 460 | this.urlPropertyToPacketOption(url, req, 'query', 'Uri-Query', '&') 461 | 462 | if (options != null) { 463 | for (const optionName of Object.keys(options)) { 464 | if (optionName in options) { 465 | req.setOption(optionName, options[optionName]) 466 | } 467 | } 468 | } 469 | 470 | if (url.proxyUri != null) { 471 | req.setOption('Proxy-Uri', url.proxyUri) 472 | } 473 | 474 | if (url.accept != null) { 475 | req.setOption('Accept', url.accept) 476 | } 477 | 478 | if (url.contentFormat != null) { 479 | req.setOption('Content-Format', url.contentFormat) 480 | } 481 | 482 | req.sender.on('error', req.emit.bind(req, 'error')) 483 | 484 | req.sender.on('sending', () => { 485 | this._msgInFlight++ 486 | }) 487 | 488 | req.sender.on('timeout', (err) => { 489 | req.emit('timeout', err) 490 | this.abort(req) 491 | }) 492 | 493 | req.sender.on('sent', () => { 494 | if (req.multicast) { 495 | return 496 | } 497 | 498 | this._msgInFlight-- 499 | if (this._closing && this._msgInFlight === 0) { 500 | this._doClose() 501 | } 502 | }) 503 | 504 | // Start multicast monitoring timer in case of multicast request 505 | if (url.multicast === true) { 506 | req.multicastTimer = setTimeout(() => { 507 | if (req._packet.token != null) { 508 | const token = req._packet.token.toString('hex') 509 | this._tkToReq.delete(token) 510 | this._tkToMulticastResAddr.delete(token) 511 | } 512 | if (req._packet.messageId != null) { 513 | this._msgIdToReq.delete(req._packet.messageId) 514 | } 515 | this._msgInFlight-- 516 | if (this._msgInFlight === 0 && this._closing) { 517 | this._doClose() 518 | } 519 | }, multicastTimeout) 520 | } 521 | 522 | this._setObserveOption(req, req.url) 523 | 524 | this._requests++ 525 | 526 | req._totalPayload = Buffer.alloc(0) 527 | 528 | return req 529 | } 530 | 531 | _setObserveOption (req: OutgoingMessage, requestParameters: CoapRequestParams): void { 532 | const observeParameter = requestParameters.observe 533 | 534 | if (observeParameter == null || observeParameter === false) { 535 | req.on('response', this._cleanUp.bind(this)) 536 | return 537 | } 538 | 539 | // `null` indicates an option value of zero here, encoded with zero length. 540 | // Using `null` avoids issues with some devices that cannot process an 541 | // option value of 0 that is encoded as a byte containing eight zeros. 542 | let observeValue: null | number 543 | 544 | if (typeof observeParameter === 'number') { 545 | observeValue = observeParameter === 0 ? null : observeParameter 546 | } else if (typeof observeParameter === 'string') { 547 | observeValue = parseInt(observeParameter) 548 | } else if (observeParameter) { 549 | observeValue = null 550 | } else { 551 | return 552 | } 553 | 554 | if (observeValue == null || !isNaN(observeValue)) { 555 | req.setOption('Observe', observeValue) 556 | } 557 | } 558 | 559 | abort (req: OutgoingMessage): void { 560 | req.sender.removeAllListeners() 561 | req.sender.reset() 562 | this._msgInFlight-- 563 | this._cleanUp() 564 | if (req._packet.messageId != null) { 565 | this._msgIdToReq.delete(req._packet.messageId) 566 | } 567 | if (req._packet.token != null) { 568 | this._tkToReq.delete(req._packet.token.toString('hex')) 569 | } 570 | } 571 | 572 | urlPropertyToPacketOption (url: CoapRequestParams, req: OutgoingMessage, property: string, option: string, separator: string): void { 573 | if (url[property] != null) { 574 | req.setOption(option, url[property].normalize('NFC').split(separator) 575 | .filter((part) => { return part !== '' }) 576 | .map((part) => { 577 | const buf = Buffer.alloc(Buffer.byteLength(part)) 578 | buf.write(part) 579 | return buf 580 | })) 581 | } 582 | } 583 | 584 | _convertMulticastToUnicastRequest (req: any, rsinfo: AddressInfo): OutgoingMessage | undefined { 585 | const unicastReq = this.request(req.url) 586 | const unicastAddress = rsinfo.address.split('%')[0] 587 | const token = req._packet.token.toString('hex') 588 | const addressArray = this._tkToMulticastResAddr.get(token) ?? [] 589 | 590 | if (addressArray.includes(unicastAddress)) { 591 | return undefined 592 | } 593 | 594 | unicastReq.url.host = unicastAddress 595 | unicastReq.sender._host = unicastAddress 596 | clearTimeout(unicastReq.multicastTimer) 597 | unicastReq.url.multicast = false 598 | req.eventNames().forEach(eventName => { 599 | req.listeners(eventName).forEach(listener => { 600 | unicastReq.on(eventName, listener) 601 | }) 602 | }) 603 | addressArray.push(unicastAddress) 604 | unicastReq._packet.token = this._nextToken() 605 | this._requests++ 606 | return unicastReq 607 | } 608 | } 609 | 610 | export default Agent 611 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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