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