Comparing version 0.0.1 to 0.1.0
100
index.js
const bl = require('bl') | ||
, dgram = require('dgram') | ||
, parse = require('coap-packet').parse | ||
, generate = require('coap-packet').generate | ||
, URL = require('url') | ||
, coapPort = 5683 | ||
const bl = require('bl') | ||
, dgram = require('dgram') | ||
, backoff = require('backoff') | ||
, parse = require('coap-packet').parse | ||
, generate = require('coap-packet').generate | ||
, URL = require('url') | ||
, Server = require('./lib/server') | ||
, IncomingMessage = require('./lib/incoming_message') | ||
, OutgoingMessage = require('./lib/outgoing_message') | ||
, parameters = require('./lib/parameters') | ||
, optionsConv = require('./lib/option_converter') | ||
module.exports.request = function(url) { | ||
var req = bl() | ||
, client = dgram.createSocket('udp4') | ||
, packet = { options: [] } | ||
var req | ||
, bOff = backoff.exponential({ | ||
randomisationFactor: 0.2, | ||
initialDelay: 1222, | ||
maxDelay: parameters.maxTransmitSpan * 1000 | ||
}) | ||
, timer | ||
, cleanUp = function() { | ||
client.close() | ||
bOff.reset() | ||
clearTimeout(timer) | ||
} | ||
, client = dgram.createSocket('udp4', function(msg, rsinfo) { | ||
req.emit('response', new IncomingMessage(parse(msg), rsinfo)) | ||
}) | ||
, message | ||
if (typeof url === 'string') { | ||
url = URL.parse(url) | ||
, send | ||
send = function(buf) { | ||
if (Buffer.isBuffer(buf)) | ||
message = buf | ||
client.send(message, 0, message.length, | ||
url.port, url.hostname || url.host) | ||
bOff.backoff() | ||
} | ||
packet.code = url.method || 'GET' | ||
url.port = url.port || coapPort | ||
req = new OutgoingMessage({}, send) | ||
urlPropertyToPacketOption(url, packet, 'pathname', 'Uri-Path', '/') | ||
urlPropertyToPacketOption(url, packet, 'query', 'Uri-Query', '&') | ||
if (typeof url === 'string') | ||
url = URL.parse(url) | ||
req.statusCode = url.method || 'GET' | ||
url.port = url.port || parameters.coapPort | ||
urlPropertyToPacketOption(url, req, 'pathname', 'Uri-Path', '/') | ||
urlPropertyToPacketOption(url, req, 'query', 'Uri-Query', '&') | ||
client.on('error', req.emit.bind(req, 'error')) | ||
req.on('finish', function() { | ||
packet.payload = req.slice() | ||
message = generate(packet) | ||
req.on('error', cleanUp) | ||
client.send(message, 0, message.length, url.port, url.hostname || url.host, function(err, bytes) { | ||
client.close() | ||
}) | ||
}) | ||
bOff.failAfter(parameters.maxRetransmit - 1) | ||
bOff.on('ready', send) | ||
timer = setTimeout(function() { | ||
var err = new Error('No reply in ' + parameters.exchangeLifetime + 's') | ||
req.emit('error', err) | ||
}, parameters.exchangeLifetime * 1000) | ||
return req | ||
} | ||
function urlPropertyToPacketOption(url, packet, property, option, separator) { | ||
module.exports.createServer = Server | ||
function urlPropertyToPacketOption(url, req, property, option, separator) { | ||
if (url[property]) | ||
url[property].split(separator) | ||
.filter(function(part) { return part !== '' }) | ||
.forEach(function(part) { | ||
req.setOption(option, url[property].split(separator) | ||
.filter(function(part) { return part !== '' }) | ||
.map(function(part) { | ||
var buf = new Buffer(Buffer.byteLength(part)) | ||
buf.write(part) | ||
packet.options.push({ | ||
name: option | ||
, value: buf | ||
}) | ||
}) | ||
return buf | ||
})) | ||
} | ||
module.exports.registerOption = optionsConv.registerOption | ||
module.exports.registerFormat = optionsConv.registerFormat |
{ | ||
"name": "coap", | ||
"version": "0.0.1", | ||
"version": "0.1.0", | ||
"description": "A CoAP library for node modelled after 'http'", | ||
@@ -14,5 +14,3 @@ "main": "index.js", | ||
"pre-commit": [ | ||
"test", | ||
"lint-lib", | ||
"lint-tests" | ||
"test" | ||
], | ||
@@ -32,8 +30,12 @@ "keywords": [ | ||
"chai": "~1.8.0", | ||
"mocha": "~1.13.0" | ||
"mocha": "~1.13.0", | ||
"timekeeper": "0.0.3", | ||
"sinon": "~1.7.3" | ||
}, | ||
"dependencies": { | ||
"coap-packet": "~0.1.1", | ||
"bl": "~0.4.2" | ||
"coap-packet": "~0.1.3", | ||
"bl": "~0.4.2", | ||
"backoff": "~2.3.0", | ||
"lru-cache": "~2.3.1" | ||
} | ||
} |
230
README.md
@@ -7,5 +7,14 @@ node-coap | ||
__node-coap__ is an _highly experimental_ client and (in the future) | ||
server library for CoAP modelled after the `http` module. | ||
__node-coap__ is an _highly experimental_ client and server library for CoAP modelled after the `http` module. | ||
* <a href="#intro">Introduction</a> | ||
* <a href="#install">Installation</a> | ||
* <a href="#basic">Basic Example</a> | ||
* <a href="#api">API</a> | ||
* <a href="#contributing">Contributing</a> | ||
* <a href="#licence">Licence & copyright</a> | ||
<a name="intro"></a> | ||
## Introduction | ||
What is CoAP? | ||
@@ -22,10 +31,14 @@ > Constrained Application Protocol (CoAP) is a software protocol | ||
**node-coap** is an **OPEN Open Source Project**, see the <a href="#contributing">Contributing</a> section to find out what this means. | ||
This has been tested only on node v0.10. | ||
<a name="install"></a> | ||
## Installation | ||
``` | ||
$: npm install coap --save | ||
$ npm install coap --save | ||
``` | ||
<a name="basic"></a> | ||
## Basic Example | ||
@@ -36,25 +49,34 @@ | ||
``` | ||
const dgram = require('dgram') | ||
, coapPacket = require('coap-packet') | ||
, parse = packet.parse | ||
, payload = new Buffer('Hello World') | ||
, port = 41234 | ||
, server = dgram.createSocket("udp4") | ||
, coap = require('coap') | ||
```js | ||
const coap = require('coap') | ||
, server = coap.createServer() | ||
server.bind(port, function() { | ||
coap.request('coap://localhost:' + port).end(paylaod) | ||
server.on('request', function(req, res) { | ||
res.end('Hello ' + req.url.split('/')[1] + '\n') | ||
}) | ||
server.on('message', function(data) { | ||
console.log(parse(data).payload.toString()) | ||
server.close() | ||
// the default CoAP port is 5683 | ||
server.listen(function() { | ||
var req = coap.request('coap://localhost/Matteo') | ||
req.on('response', function(res) { | ||
res.pipe(process.stdout) | ||
res.on('end', function() { | ||
process.exit(0) | ||
}) | ||
}) | ||
req.end() | ||
}) | ||
``` | ||
<a name="api"></a> | ||
## API | ||
* <a href="#parse"><code>coap.<b>request()</b></code></a> | ||
* <a href="#request"><code>coap.<b>request()</b></code></a> | ||
* <a href="#createServer"><code>coap.<b>createServer()</b></code></a> | ||
* <a href="#incoming"><code>IncomingMessage</b></code></a> | ||
* <a href="#outgoing"><code>OutgoingMessage</b></code></a> | ||
<a name="request"></a> | ||
### request(url) | ||
@@ -78,9 +100,179 @@ | ||
`coap.request()` returns an instance of `stream.Writable`. If you need | ||
`coap.request()` returns an instance of <a | ||
href='#incoming'><code>IncomingMessage</code></a>. | ||
If you need | ||
to add a payload, just `pipe` into it. | ||
Otherwise, you __must__ call `end` to submit the request. | ||
#### Event: 'response' | ||
`function (response) { }` | ||
Emitted when a response is received. | ||
`response` is | ||
an instance of <a | ||
href='#incoming'><code>IncomingMessage</code></a>. | ||
<a name="createServer"></a> | ||
### createServer([requestListener]) | ||
Returns a new CoAP Server object. | ||
The `requestListener` is a function which is automatically | ||
added to the `'request'` event. | ||
#### Event: 'request' | ||
`function (request, response) { }` | ||
Emitted each time there is a request. | ||
`request` is an instance of <a | ||
href='#incoming'><code>IncomingMessage</code></a> and `response` is | ||
an instance of <a | ||
href='#outgoing'><code>OutgoingMessage</code></a>. | ||
#### server.listen(port, [hostname], [callback]) | ||
Begin accepting connections on the specified port and hostname. If the | ||
hostname is omitted, the server will accept connections directed to any | ||
IPv4 address (`INADDR_ANY`). | ||
To listen to a unix socket, supply a filename instead of port and hostname. | ||
This function is asynchronous. | ||
#### server.close([callback]) | ||
Closes the server. | ||
This function is synchronous, but it provides an asynchronous callback | ||
for convenience. | ||
<a name="outgoing"></a> | ||
### OutgoingMessage | ||
An `OutgoingMessage` object is returned by `coap.request` or | ||
emitted by the `coap.createServer` `'response'` event. | ||
It may be used to access response status, headers and data. | ||
It implements the [Writable | ||
Stream](http://nodejs.org/api/stream.html#stream_class_stream_writable) interface, as well as the | ||
following additional methods and properties. | ||
#### message.statusCode | ||
The CoAP code ot the message. | ||
It is HTTP-compatible, as it can be passed `404`. | ||
#### message.setOption(name, value) | ||
Sets a single option value. | ||
All the options are in binary format, except for | ||
`'Content-Format'`, `'Accept'` and `'ETag'`. | ||
See <a href='#registerOption'> to know how to register more. | ||
Use an array of buffers | ||
if you need to send multiple options with the same name. | ||
If you need to pass a custom option, pass a string containing a | ||
Example: | ||
message.setOption("Content-Format", "application/json"); | ||
or | ||
message.setOption("555", [new Buffer('abcde', | ||
new Buffer('ghi')]); | ||
`setOption` is also aliased as `setHeader` for HTTP API | ||
compatibility. | ||
Also, `'Content-Type'` is aliased to `'Content-Format'` for HTTP | ||
compatibility. | ||
See the | ||
[spec](http://tools.ietf.org/html/draft-ietf-core-coap-18#section-5.4) | ||
for all the possible options. | ||
<a name="incoming"></a> | ||
### IncomingMessage | ||
An `IncomingMessage` object is created by `coap.createServer` or | ||
`coap.request` | ||
and passed as the first argument to the `'request'` and `'response'` event | ||
respectively. It may be used to access response status, headers and data. | ||
It implements the [Readable | ||
Stream](http://nodejs.org/api/stream.html#stream_class_stream_readable) interface, as well as the | ||
following additional methods and properties. | ||
#### message.payload | ||
The full payload of the message, as a Buffer. | ||
#### message.options | ||
All the CoAP options, as parsed by | ||
[CoAP-packet](http://github.com/mcollina/coap-packet). | ||
All the options are in binary format, except for | ||
`'Content-Format'`, `'Accept'` and `'ETag'`. | ||
See <a href='#registerOption'> to know how to register more. | ||
See the | ||
[spec](http://tools.ietf.org/html/draft-ietf-core-coap-18#section-5.4) | ||
for all the possible options. | ||
#### message.headers | ||
All the CoAP options that can be represented in a human-readable format. | ||
Currently they are only `'Content-Format'`, `'Accept'` and | ||
`'ETag'`. | ||
See <a href='#registerOption'> to know how to register more. | ||
Also, `'Content-Type'` is aliased to `'Content-Format'` for HTTP | ||
compatibility. | ||
#### message.code | ||
The CoAP code of the message. | ||
#### message.method | ||
The method of the message, it might be | ||
`'GET'`, `'POST'`, `'PUT'`, `'DELETE'` or `null`. | ||
It is null if the CoAP code cannot be parsed into a method, i.e. it is | ||
not in the '0.' range. | ||
#### message.url | ||
The URL of the request, e.g. | ||
`'coap://localhost:12345/hello/world?a=b&b=c'`. | ||
<a name="contributing"></a> | ||
## Contributing | ||
__node-coap__ is an **OPEN Open Source Project**. This means that: | ||
> 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. | ||
See the [CONTRIBUTING.md](https://github.com/mcollina/node-coap/blob/master/CONTRIBUTING.md) file for more details. | ||
## Limitations | ||
At the moment only non-confirmable messages are supported (NON in the | ||
CoAP spec). This means less reliability, as there is no way for a client | ||
to know if the server has received the message. | ||
Moreover, the maximum packet size is 1280, as the | ||
[blockwise](http://datatracker.ietf.org/doc/draft-ietf-core-block/) is | ||
not supported yet. | ||
The [observe](http://datatracker.ietf.org/doc/draft-ietf-core-observe/) | ||
support is planned too. | ||
## Contributors | ||
Coap-Packet is only possible due to the excellent work of the following contributors: | ||
__node-coap__ is only possible due to the excellent work of the following contributors: | ||
@@ -87,0 +279,0 @@ <table><tbody> |
var coap = require('../') | ||
, parse = require('coap-packet').parse | ||
, generate = require('coap-packet').generate | ||
, dgram = require('dgram') | ||
, bl = require('bl') | ||
, request = coap.request | ||
const coap = require('../') | ||
, parse = require('coap-packet').parse | ||
, generate = require('coap-packet').generate | ||
, dgram = require('dgram') | ||
, bl = require('bl') | ||
, sinon = require('sinon') | ||
, request = coap.request | ||
@@ -59,2 +60,12 @@ describe('request', function() { | ||
it('should error if the message is too big', function(done) { | ||
var req = request('coap://localhost:' + port) | ||
req.on('error', function() { | ||
done() | ||
}) | ||
req.end(new Buffer(1280)) | ||
}) | ||
it('should imply a default port', function(done) { | ||
@@ -143,2 +154,305 @@ server2 = dgram.createSocket('udp4') | ||
}) | ||
it('should emit a response', function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
server.on('message', function(msg, rsinfo) { | ||
var packet = parse(msg) | ||
, toSend = generate({ | ||
messageId: packet.messageId | ||
, token: packet.token | ||
, payload: new Buffer('42') | ||
}) | ||
server.send(toSend, 0, toSend.length, rsinfo.port, rsinfo.address) | ||
}) | ||
req.on('response', function(res) { | ||
res.pipe(bl(function(err, data) { | ||
expect(data).to.eql(new Buffer('42')) | ||
done() | ||
})) | ||
}) | ||
req.end() | ||
}) | ||
it('should allow to add an option', function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
, buf = new Buffer(3) | ||
req.setOption('ETag', buf) | ||
req.end() | ||
server.on('message', function(msg) { | ||
expect(parse(msg).options[0].name).to.eql('ETag') | ||
expect(parse(msg).options[0].value).to.eql(buf) | ||
done() | ||
}) | ||
}) | ||
it('should overwrite the option', function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
, buf = new Buffer(3) | ||
req.setOption('ETag', new Buffer(3)) | ||
req.setOption('ETag', buf) | ||
req.end() | ||
server.on('message', function(msg) { | ||
expect(parse(msg).options[0].value).to.eql(buf) | ||
done() | ||
}) | ||
}) | ||
it('should alias setOption to setHeader', function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
, buf = new Buffer(3) | ||
req.setHeader('ETag', buf) | ||
req.end() | ||
server.on('message', function(msg) { | ||
expect(parse(msg).options[0].value).to.eql(buf) | ||
done() | ||
}) | ||
}) | ||
it('should set multiple options', function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
, buf1 = new Buffer(3) | ||
, buf2 = new Buffer(3) | ||
req.setOption('433', [buf1, buf2]) | ||
req.end() | ||
server.on('message', function(msg) { | ||
expect(parse(msg).options[0].value).to.eql(buf1) | ||
expect(parse(msg).options[1].value).to.eql(buf2) | ||
done() | ||
}) | ||
}) | ||
it('should alias the \'Content-Format\' option to \'Content-Type\'', function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
req.setOption('Content-Type', new Buffer([0])) | ||
req.end() | ||
server.on('message', function(msg) { | ||
expect(parse(msg).options[0].name).to.eql('Content-Format') | ||
expect(parse(msg).options[0].value).to.eql(new Buffer([0])) | ||
done() | ||
}) | ||
}) | ||
var formatsString = { | ||
'text/plain': new Buffer([0]) | ||
, 'application/link-format': new Buffer([40]) | ||
, 'application/xml': new Buffer([41]) | ||
, 'application/octet-stream': new Buffer([42]) | ||
, 'application/exi': new Buffer([47]) | ||
, 'application/json': new Buffer([50]) | ||
} | ||
describe('with the \'Content-Format\' header in the outgoing message', function() { | ||
function buildTest(format, value) { | ||
it('should parse ' + format, function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
req.setOption('Content-Format', format) | ||
req.end() | ||
server.on('message', function(msg) { | ||
expect(parse(msg).options[0].value).to.eql(value) | ||
done() | ||
}) | ||
}) | ||
} | ||
for (var format in formatsString) { | ||
buildTest(format, formatsString[format]) | ||
} | ||
}) | ||
describe('with the \'Accept\' header in the outgoing message', function() { | ||
function buildTest(format, value) { | ||
it('should parse ' + format, function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
req.setHeader('Accept', format) | ||
req.end() | ||
server.on('message', function(msg) { | ||
expect(parse(msg).options[0].value).to.eql(value) | ||
done() | ||
}) | ||
}) | ||
} | ||
for (var format in formatsString) { | ||
buildTest(format, formatsString[format]) | ||
} | ||
}) | ||
describe('with the \'Content-Format\' in the response', function() { | ||
function buildResponse(value) { | ||
return function(msg, rsinfo) { | ||
var packet = parse(msg) | ||
, toSend = generate({ | ||
messageId: packet.messageId | ||
, token: packet.token | ||
, options: [{ | ||
name: 'Content-Format' | ||
, value: value | ||
}] | ||
}) | ||
server.send(toSend, 0, toSend.length, rsinfo.port, rsinfo.address) | ||
} | ||
} | ||
function buildTest(format, value) { | ||
it('should parse ' + format, function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
server.on('message', buildResponse(value)) | ||
req.on('response', function(res) { | ||
expect(res.options[0].value).to.eql(format) | ||
done() | ||
}) | ||
req.end() | ||
}) | ||
it('should include ' + format + ' in the headers', function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
server.on('message', buildResponse(value)) | ||
req.on('response', function(res) { | ||
expect(res.headers['Content-Format']).to.eql(format) | ||
expect(res.headers['Content-Type']).to.eql(format) | ||
done() | ||
}) | ||
req.end() | ||
}) | ||
} | ||
for (var format in formatsString) { | ||
buildTest(format, formatsString[format]) | ||
} | ||
}) | ||
it('should include \'ETag\' in the response headers', function(done) { | ||
var req = request({ | ||
port: port | ||
}) | ||
server.on('message', function(msg, rsinfo) { | ||
var packet = parse(msg) | ||
, toSend = generate({ | ||
messageId: packet.messageId | ||
, token: packet.token | ||
, options: [{ | ||
name: 'ETag' | ||
, value: new Buffer('abcdefgh') | ||
}] | ||
}) | ||
server.send(toSend, 0, toSend.length, rsinfo.port, rsinfo.address) | ||
}) | ||
req.on('response', function(res) { | ||
expect(res.headers).to.have.property('ETag', 'abcdefgh') | ||
done() | ||
}) | ||
req.end() | ||
}) | ||
describe('retries', function() { | ||
var clock | ||
beforeEach(function() { | ||
clock = sinon.useFakeTimers() | ||
}) | ||
afterEach(function() { | ||
clock.restore() | ||
}) | ||
function fastForward(increase, max) { | ||
clock.tick(increase) | ||
if (increase < max) | ||
setImmediate(fastForward.bind(null, increase, max - increase)) | ||
} | ||
it('should error after ~247 seconds', function(done) { | ||
var req = request('coap://localhost:' + port) | ||
req.end() | ||
req.on('error', function(err) { | ||
expect(err).to.have.property('message', 'No reply in 247s') | ||
done() | ||
}) | ||
clock.tick(247 * 1000) | ||
}) | ||
it('should retry four times before erroring', function(done) { | ||
var req = request('coap://localhost:' + port) | ||
, messages = 0 | ||
req.end() | ||
server.on('message', function(msg) { | ||
messages++ | ||
}) | ||
req.on('error', function(err) { | ||
// original one plus 4 retries | ||
expect(messages).to.eql(5) | ||
done() | ||
}) | ||
fastForward(100, 247 * 1000) | ||
}) | ||
it('should retry four times before 45s', function(done) { | ||
var req = request('coap://localhost:' + port) | ||
, messages = 0 | ||
req.end() | ||
server.on('message', function(msg) { | ||
messages++ | ||
}) | ||
setTimeout(function() { | ||
// original one plus 4 retries | ||
expect(messages).to.eql(5) | ||
done() | ||
}, 45 * 1000) | ||
fastForward(100, 45 * 1000) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 9 instances in 1 package
1183
285
5
49683
4
5
19
+ Addedbackoff@~2.3.0
+ Addedlru-cache@~2.3.1
+ Addedbackoff@2.3.0(transitive)
+ Addedlru-cache@2.3.1(transitive)
Updatedcoap-packet@~0.1.3