coap
Version:
A CoAP library for node modelled after 'http'
529 lines • 20.6 kB
JavaScript
"use strict";
/*
* Copyright (c) 2013-2021 node-coap contributors.
*
* node-coap is licensed under an MIT +no-false-attribs license.
* All rights not explicitly granted in the MIT license are reserved.
* See the included LICENSE file for more details.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const crypto = require("crypto");
const dgram_1 = require("dgram");
const events_1 = require("events");
const coap_packet_1 = require("coap-packet");
const incoming_message_1 = __importDefault(require("./incoming_message"));
const outgoing_message_1 = __importDefault(require("./outgoing_message"));
const observe_read_stream_1 = __importDefault(require("./observe_read_stream"));
const retry_send_1 = __importDefault(require("./retry_send"));
const helpers_1 = require("./helpers");
const segmentation_1 = require("./segmentation");
const block_1 = require("./block");
const parameters_1 = require("./parameters");
const maxToken = Math.pow(2, 32);
const maxMessageId = Math.pow(2, 16);
class Agent extends events_1.EventEmitter {
constructor(opts) {
super();
if (opts == null) {
opts = {};
}
if (opts.type == null) {
opts.type = 'udp4';
}
if (opts.socket != null) {
const sock = opts.socket;
opts.type = sock.type;
delete opts.port;
}
this._opts = opts;
this._init(opts.socket);
}
_init(socket) {
var _a;
this._closing = false;
if (this._sock != null) {
return;
}
this._sock = socket !== null && socket !== void 0 ? socket : (0, dgram_1.createSocket)({ type: (_a = this._opts.type) !== null && _a !== void 0 ? _a : 'udp4' });
this._sock.on('message', (msg, rsinfo) => {
let packet;
try {
packet = (0, coap_packet_1.parse)(msg);
}
catch (err) {
return;
}
if (packet.code[0] === '0' && packet.code !== '0.00') {
// ignore this packet since it's not a response.
return;
}
if (this._sock != null) {
const outSocket = this._sock.address();
this._handle(packet, rsinfo, outSocket);
}
});
if (this._opts.port != null) {
this._sock.bind(this._opts.port);
}
this._sock.on('error', (err) => {
this.emit('error', err);
});
this._msgIdToReq = new Map();
this._tkToReq = new Map();
this._tkToMulticastResAddr = new Map();
this._lastToken = Math.floor(Math.random() * (maxToken - 1));
this._lastMessageId = Math.floor(Math.random() * (maxMessageId - 1));
this._msgInFlight = 0;
this._requests = 0;
}
close(done) {
if (this._msgIdToReq.size === 0 && this._msgInFlight === 0) {
// No requests in flight, close immediately
this._doClose(done);
return this;
}
done = done !== null && done !== void 0 ? done : (() => { });
this.once('close', done);
for (const req of this._msgIdToReq.values()) {
this.abort(req);
}
return this;
}
_cleanUp() {
if (--this._requests !== 0) {
return;
}
if (this._opts.socket == null) {
this._closing = true;
}
if (this._msgInFlight > 0) {
return;
}
this._doClose();
}
_doClose(done) {
for (const req of this._msgIdToReq.values()) {
req.sender.reset();
}
if (this._opts.socket != null) {
return;
}
if (this._sock == null) {
this.emit('close');
return;
}
this._sock.close(() => {
this._sock = null;
if (done != null) {
done();
}
this.emit('close');
});
}
_handle(packet, rsinfo, outSocket) {
let buf;
let response;
let req = this._msgIdToReq.get(packet.messageId);
const ackSent = (err) => {
if (err != null && req != null) {
req.emit('error', err);
}
this._msgInFlight--;
if (this._closing && this._msgInFlight === 0) {
this._doClose();
}
};
if (req == null) {
if (packet.token.length > 0) {
req = this._tkToReq.get(packet.token.toString('hex'));
}
if ((packet.ack || packet.reset) && req == null) {
// Nothing to do on unknown or duplicate ACK/RST packet
return;
}
if (req == null) {
buf = (0, coap_packet_1.generate)({
code: '0.00',
reset: true,
messageId: packet.messageId
});
if (this._sock != null) {
this._msgInFlight++;
this._sock.send(buf, 0, buf.length, rsinfo.port, rsinfo.address, ackSent);
}
return;
}
}
if (packet.confirmable) {
buf = (0, coap_packet_1.generate)({
code: '0.00',
ack: true,
messageId: packet.messageId
});
if (this._sock != null) {
this._msgInFlight++;
this._sock.send(buf, 0, buf.length, rsinfo.port, rsinfo.address, ackSent);
}
}
if (packet.code !== '0.00' && (req._packet.token == null || req._packet.token.length !== packet.token.length || Buffer.compare(req._packet.token, packet.token) !== 0)) {
// The tokens don't match, ignore the message since it is a malformed response
return;
}
const block1Buff = (0, helpers_1.getOption)(packet.options, 'Block1');
let block1 = null;
if (block1Buff instanceof Buffer) {
block1 = (0, block_1.parseBlockOption)(block1Buff);
// check for error
if (block1 == null) {
req.sender.reset();
req.emit('error', new Error('Failed to parse block1'));
return;
}
}
req.sender.reset();
if (block1 != null) {
// If the client takes too long to respond then the retry sender will send
// another packet with the previous messageId, which we've already removed.
// Handle both piggybacked (ACK with Block1) and non-piggybacked (CON with Block1) responses
const segmentedSender = req.segmentedSender;
if (segmentedSender != null) {
// If there's more to send/receive, then carry on!
if (segmentedSender.remaining() > 0) {
if (segmentedSender.isCorrectACK(block1)) {
if (req._packet.messageId != null) {
this._msgIdToReq.delete(req._packet.messageId);
}
req._packet.messageId = this._nextMessageId();
this._msgIdToReq.set(req._packet.messageId, req);
segmentedSender.receiveACK(block1);
}
else {
segmentedSender.resendPreviousPacket();
}
return;
}
else {
// console.log("Packet received done");
if (req._packet.options != null) {
(0, helpers_1.removeOption)(req._packet.options, 'Block1');
}
delete req.segmentedSender;
}
}
}
if (!packet.confirmable && !req.multicast) {
this._msgIdToReq.delete(packet.messageId);
}
// Drop empty messages (ACKs), but process RST
if (packet.code === '0.00' && !packet.reset) {
return;
}
const block2Buff = (0, helpers_1.getOption)(packet.options, 'Block2');
let block2 = null;
// if we got blockwise (2) response
if (block2Buff instanceof Buffer) {
block2 = (0, helpers_1.parseBlock2)(block2Buff);
// check for error
if (block2 == null) {
req.sender.reset();
req.emit('error', new Error('failed to parse block2'));
return;
}
}
if (block2 != null) {
if (req.multicast) {
req = this._convertMulticastToUnicastRequest(req, rsinfo);
if (req == null) {
return;
}
}
// accumulate payload
req._totalPayload = Buffer.concat([req._totalPayload, packet.payload]);
if (block2.more === 1) {
// increase message id for next request
if (req._packet.messageId != null) {
this._msgIdToReq.delete(req._packet.messageId);
}
req._packet.messageId = this._nextMessageId();
this._msgIdToReq.set(req._packet.messageId, req);
// next block2 request
const block2Val = (0, helpers_1.createBlock2)({
more: 0,
num: block2.num + 1,
size: block2.size
});
if (block2Val == null) {
req.sender.reset();
req.emit('error', new Error('failed to create block2'));
return;
}
req.setOption('Block2', block2Val);
req._packet.payload = undefined;
req.sender.send((0, coap_packet_1.generate)(req._packet));
return;
}
else {
// get full payload
packet.payload = req._totalPayload;
// clear the payload incase of block2
req._totalPayload = Buffer.alloc(0);
}
}
const observe = req.url.observe != null && [true, 0, '0'].includes(req.url.observe);
if (req.response != null) {
const response = req.response;
if (response.append != null) {
// it is an observe request
// and we are already streaming
return response.append(packet);
}
else {
// TODO There is a previous response but is not an ObserveStream !
return;
}
}
else if (block2 != null && packet.token != null && !observe) {
this._tkToReq.delete(packet.token.toString('hex'));
}
else if (!observe && !req.multicast) {
// it is not, so delete the token
this._tkToReq.delete(packet.token.toString('hex'));
}
if (observe && packet.code !== '4.04') {
response = new observe_read_stream_1.default(packet, rsinfo, outSocket);
response.on('close', () => {
this._tkToReq.delete(packet.token.toString('hex'));
this._cleanUp();
});
response.on('deregister', () => {
const deregisterUrl = Object.assign({}, req === null || req === void 0 ? void 0 : req.url);
deregisterUrl.observe = 1;
deregisterUrl.token = req === null || req === void 0 ? void 0 : req._packet.token;
const deregisterReq = this.request(deregisterUrl);
// If the request fails, we'll deal with it with a RST message anyway.
deregisterReq.on('error', () => { });
deregisterReq.end();
});
}
else {
response = new incoming_message_1.default(packet, rsinfo, outSocket);
}
if (!req.multicast) {
req.response = response;
}
req.emit('response', response);
}
_nextToken() {
const buf = Buffer.alloc(8);
if (++this._lastToken === maxToken) {
this._lastToken = 0;
}
buf.writeUInt32BE(this._lastToken, 0);
crypto.randomBytes(4).copy(buf, 4);
return buf;
}
_nextMessageId() {
if (++this._lastMessageId === maxMessageId) {
this._lastMessageId = 0;
}
return this._lastMessageId;
}
/**
* Entry point for a new client-side request.
* @param url The parameters for the request
*/
request(url) {
var _a, _b, _c, _d;
this._init();
const options = (_a = url.options) !== null && _a !== void 0 ? _a : url.headers;
const multicastTimeout = url.multicastTimeout != null ? url.multicastTimeout : 20000;
const host = (_b = url.hostname) !== null && _b !== void 0 ? _b : url.host;
const port = (_c = url.port) !== null && _c !== void 0 ? _c : parameters_1.parameters.coapPort;
const req = new outgoing_message_1.default({}, (req, packet) => {
var _a, _b;
if (url.confirmable !== false) {
packet.confirmable = true;
}
// multicast message should be forced non-confirmable
if (url.multicast === true) {
req.multicast = true;
packet.confirmable = false;
}
if (!((_b = (_a = packet.ack) !== null && _a !== void 0 ? _a : packet.reset) !== null && _b !== void 0 ? _b : false)) {
packet.messageId = this._nextMessageId();
if ((url.token instanceof Buffer) && (url.token.length > 0)) {
if (url.token.length > 8) {
return req.emit('error', new Error('Token may be no longer than 8 bytes.'));
}
packet.token = url.token;
}
else {
packet.token = this._nextToken();
}
const token = packet.token.toString('hex');
if (req.multicast) {
this._tkToMulticastResAddr.set(token, []);
}
if (token != null) {
this._tkToReq.set(token, req);
}
}
if (packet.messageId != null) {
this._msgIdToReq.set(packet.messageId, req);
}
const block1Buff = (0, helpers_1.getOption)(packet.options, 'Block1');
if (block1Buff != null) {
// Setup for a segmented transmission
req.segmentedSender = new segmentation_1.SegmentedTransmission(block1Buff[0], req, packet);
req.segmentedSender.sendNext();
}
else {
let buf;
try {
buf = (0, coap_packet_1.generate)(packet);
}
catch (err) {
req.sender.reset();
return req.emit('error', err);
}
req.sender.send(buf, packet.confirmable === false);
}
});
req.sender = new retry_send_1.default(this._sock, port, host, url.retrySend);
req.url = url;
req.statusCode = (_d = url.method) !== null && _d !== void 0 ? _d : 'GET';
this.urlPropertyToPacketOption(url, req, 'pathname', 'Uri-Path', '/');
this.urlPropertyToPacketOption(url, req, 'query', 'Uri-Query', '&');
if (options != null) {
for (const optionName of Object.keys(options)) {
if (optionName in options) {
req.setOption(optionName, options[optionName]);
}
}
}
if (url.proxyUri != null) {
req.setOption('Proxy-Uri', url.proxyUri);
}
if (url.accept != null) {
req.setOption('Accept', url.accept);
}
if (url.contentFormat != null) {
req.setOption('Content-Format', url.contentFormat);
}
req.sender.on('error', req.emit.bind(req, 'error'));
req.sender.on('sending', () => {
this._msgInFlight++;
});
req.sender.on('timeout', (err) => {
req.emit('timeout', err);
this.abort(req);
});
req.sender.on('sent', () => {
if (req.multicast) {
return;
}
this._msgInFlight--;
if (this._closing && this._msgInFlight === 0) {
this._doClose();
}
});
// Start multicast monitoring timer in case of multicast request
if (url.multicast === true) {
req.multicastTimer = setTimeout(() => {
if (req._packet.token != null) {
const token = req._packet.token.toString('hex');
this._tkToReq.delete(token);
this._tkToMulticastResAddr.delete(token);
}
if (req._packet.messageId != null) {
this._msgIdToReq.delete(req._packet.messageId);
}
this._msgInFlight--;
if (this._msgInFlight === 0 && this._closing) {
this._doClose();
}
}, multicastTimeout);
}
this._setObserveOption(req, req.url);
this._requests++;
req._totalPayload = Buffer.alloc(0);
return req;
}
_setObserveOption(req, requestParameters) {
const observeParameter = requestParameters.observe;
if (observeParameter == null || observeParameter === false) {
req.on('response', this._cleanUp.bind(this));
return;
}
// `null` indicates an option value of zero here, encoded with zero length.
// Using `null` avoids issues with some devices that cannot process an
// option value of 0 that is encoded as a byte containing eight zeros.
let observeValue;
if (typeof observeParameter === 'number') {
observeValue = observeParameter === 0 ? null : observeParameter;
}
else if (typeof observeParameter === 'string') {
observeValue = parseInt(observeParameter);
}
else if (observeParameter) {
observeValue = null;
}
else {
return;
}
if (observeValue == null || !isNaN(observeValue)) {
req.setOption('Observe', observeValue);
}
}
abort(req) {
req.sender.removeAllListeners();
req.sender.reset();
this._msgInFlight--;
this._cleanUp();
if (req._packet.messageId != null) {
this._msgIdToReq.delete(req._packet.messageId);
}
if (req._packet.token != null) {
this._tkToReq.delete(req._packet.token.toString('hex'));
}
}
urlPropertyToPacketOption(url, req, property, option, separator) {
if (url[property] != null) {
req.setOption(option, url[property].normalize('NFC').split(separator)
.filter((part) => { return part !== ''; })
.map((part) => {
const buf = Buffer.alloc(Buffer.byteLength(part));
buf.write(part);
return buf;
}));
}
}
_convertMulticastToUnicastRequest(req, rsinfo) {
var _a;
const unicastReq = this.request(req.url);
const unicastAddress = rsinfo.address.split('%')[0];
const token = req._packet.token.toString('hex');
const addressArray = (_a = this._tkToMulticastResAddr.get(token)) !== null && _a !== void 0 ? _a : [];
if (addressArray.includes(unicastAddress)) {
return undefined;
}
unicastReq.url.host = unicastAddress;
unicastReq.sender._host = unicastAddress;
clearTimeout(unicastReq.multicastTimer);
unicastReq.url.multicast = false;
req.eventNames().forEach(eventName => {
req.listeners(eventName).forEach(listener => {
unicastReq.on(eventName, listener);
});
});
addressArray.push(unicastAddress);
unicastReq._packet.token = this._nextToken();
this._requests++;
return unicastReq;
}
}
exports.default = Agent;
//# sourceMappingURL=agent.js.map