coap
Version:
A CoAP library for node modelled after 'http'
643 lines • 26.4 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 events_1 = require("events");
const net_1 = require("net");
const cache_1 = __importDefault(require("./cache"));
const outgoing_message_1 = __importDefault(require("./outgoing_message"));
const dgram_1 = require("dgram");
const lru_cache_1 = require("lru-cache");
const os_1 = __importDefault(require("os"));
const incoming_message_1 = __importDefault(require("./incoming_message"));
const observe_write_stream_1 = __importDefault(require("./observe_write_stream"));
const retry_send_1 = __importDefault(require("./retry_send"));
const middlewares_1 = require("./middlewares");
const block_1 = require("./block");
const coap_packet_1 = require("coap-packet");
const helpers_1 = require("./helpers");
const parameters_1 = require("./parameters");
const fastseries_1 = __importDefault(require("fastseries"));
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)('CoAP Server');
function handleEnding(err) {
if (err != null) {
this.server._sendError(Buffer.from(err.message), this.rsinfo, this.packet);
}
}
function removeProxyOptions(packet) {
const cleanOptions = [];
if (packet.options == null) {
packet.options = [];
}
for (let i = 0; i < packet.options.length; i++) {
const optionName = packet.options[i].name;
if (typeof optionName === 'string' &&
optionName.toLowerCase() !== 'proxy-uri' &&
optionName.toLowerCase() !== 'proxy-scheme') {
cleanOptions.push(packet.options[i]);
}
}
packet.options = cleanOptions;
return packet;
}
function allAddresses(type) {
var _a;
let family = 'IPv4';
if (type === 'udp6') {
family = 'IPv6';
}
const addresses = [];
const macs = [];
const interfaces = os_1.default.networkInterfaces();
for (const ifname in interfaces) {
if (ifname in interfaces) {
(_a = interfaces[ifname]) === null || _a === void 0 ? void 0 : _a.forEach((a) => {
// Checking for repeating MAC address to avoid trying to listen on same interface twice
if (a.family === family && !macs.includes(a.mac)) {
addresses.push(a.address);
macs.push(a.mac);
}
});
}
}
return addresses;
}
// eslint-disable-next-line @typescript-eslint/ban-types
class CoapLRUCache extends lru_cache_1.LRUCache {
}
class CoAPServer extends events_1.EventEmitter {
constructor(serverOptions, listener) {
var _a, _b;
super();
this._options = {};
this._proxiedRequests = new Map();
this._options = {};
if (typeof serverOptions === 'function') {
listener = serverOptions;
}
else if (serverOptions != null) {
this._options = serverOptions;
}
this._middlewares = [middlewares_1.parseRequest];
if (this._options.proxy === true) {
this._middlewares.push(middlewares_1.proxyRequest);
this._middlewares.push(middlewares_1.handleProxyResponse);
}
if (typeof this._options.clientIdentifier !== 'function') {
this._options.clientIdentifier = (request) => {
return `${request.rsinfo.address}:${request.rsinfo.port}`;
};
}
this._clientIdentifier = this._options.clientIdentifier;
if ((this._options.piggybackReplyMs == null) ||
!(0, helpers_1.isNumeric)(this._options.piggybackReplyMs)) {
this._options.piggybackReplyMs = parameters_1.parameters.piggybackReplyMs;
}
if (!(0, helpers_1.isBoolean)(this._options.sendAcksForNonConfirmablePackets)) {
this._options.sendAcksForNonConfirmablePackets =
parameters_1.parameters.sendAcksForNonConfirmablePackets;
}
this._middlewares.push(middlewares_1.handleServerRequest);
// Multicast settings
this._multicastAddress = (_a = this._options.multicastAddress) !== null && _a !== void 0 ? _a : null;
this._multicastInterface = (_b = this._options.multicastInterface) !== null && _b !== void 0 ? _b : null;
// We use an LRU cache for the responses to avoid
// DDOS problems.
// total cache size is 32MiB
// max message size is 1152 bytes
// 32 MiB / 1152 = 29127 messages total
// The max lifetime is roughly 200s per message.
// Which gave us 145 messages/second guarantee
let maxSize = 32768 * 1024; // Maximum cache size is 32 MiB
if (typeof this._options.cacheSize === 'number' && this._options.cacheSize >= 0) {
maxSize = this._options.cacheSize;
}
this._lru = new CoapLRUCache({
maxSize,
sizeCalculation: (n, key) => {
return n.buffer.byteLength;
},
ttl: parameters_1.parameters.exchangeLifetime * 1000,
dispose: (value, key) => {
if (value.sender != null) {
value.sender.reset();
}
}
});
this._series = (0, fastseries_1.default)();
this._block1Cache = new cache_1.default(parameters_1.parameters.exchangeLifetime * 1000, () => {
return {};
});
this._block2Cache = new cache_1.default(parameters_1.parameters.exchangeLifetime * 1000, () => {
return null;
});
if (listener != null) {
this.on('request', listener);
}
debug('initialized');
}
handleRequest() {
return (msg, rsinfo) => {
const request = {
raw: msg,
rsinfo,
server: this
};
// eslint-disable-next-line @typescript-eslint/ban-types
const activeMiddlewares = [];
for (let i = 0; i < this._middlewares.length; i++) {
activeMiddlewares.push(this._middlewares[i]);
}
this._series(request, activeMiddlewares, request, handleEnding);
};
}
_sendError(payload, rsinfo, packet, code = '5.00') {
const message = (0, coap_packet_1.generate)({
code,
payload,
messageId: packet != null ? packet.messageId : undefined,
token: packet != null ? packet.token : undefined
}, parameters_1.parameters.maxMessageSize);
if (this._sock instanceof dgram_1.Socket) {
this._sock.send(message, 0, message.length, rsinfo.port);
}
}
_sendProxied(packet, proxyUri, callback) {
const url = new URL(proxyUri);
const host = url.hostname;
const port = parseInt(url.port);
const message = (0, coap_packet_1.generate)(removeProxyOptions(packet), parameters_1.parameters.maxMessageSize);
if (this._sock instanceof dgram_1.Socket) {
this._sock.send(message, port, host, callback);
}
}
_sendReverseProxied(packet, rsinfo, callback) {
const host = rsinfo.address;
const port = rsinfo.port;
const message = (0, coap_packet_1.generate)(packet, parameters_1.parameters.maxMessageSize);
if (this._sock instanceof dgram_1.Socket) {
this._sock.send(message, port, host, callback);
}
}
generateSocket(address, port, done) {
var _a;
const socketOptions = {
type: (_a = this._options.type) !== null && _a !== void 0 ? _a : 'udp4',
reuseAddr: this._options.reuseAddr
};
const sock = (0, dgram_1.createSocket)(socketOptions);
sock.bind(port, address, () => {
try {
if (this._multicastAddress != null) {
const multicastAddress = this._multicastAddress;
sock.setMulticastLoopback(true);
if (this._multicastInterface != null) {
sock.addMembership(multicastAddress, this._multicastInterface);
}
else if (this._options.type === 'udp4') {
allAddresses(this._options.type).forEach((_interface) => {
sock.addMembership(multicastAddress, _interface);
});
}
else {
// FIXME: Iterating over all network interfaces does not
// work for IPv6 at the moment
sock.addMembership(multicastAddress);
}
}
}
catch (err) {
if (done != null) {
done(err);
return;
}
else {
throw err;
}
}
if (done != null) {
done();
}
});
return sock;
}
listen(portOrCallback, addressOrCallback, done) {
let port = parameters_1.parameters.coapPort;
if (typeof portOrCallback === 'function') {
done = portOrCallback;
port = parameters_1.parameters.coapPort;
}
else if (typeof portOrCallback === 'number') {
port = portOrCallback;
}
let address;
if (typeof addressOrCallback === 'function') {
done = addressOrCallback;
}
else if (typeof addressOrCallback === 'string') {
address = addressOrCallback;
}
if (this._sock != null) {
if (done != null) {
done(new Error('Already listening'));
}
else {
throw new Error('Already listening');
}
return this;
}
if (address != null && (0, net_1.isIPv6)(address)) {
this._options.type = 'udp6';
}
if (this._options.type == null) {
this._options.type = 'udp4';
}
if (this._options.reuseAddr !== false) {
this._options.reuseAddr = true;
}
if (portOrCallback instanceof events_1.EventEmitter) {
this._sock = portOrCallback;
if (done != null) {
setImmediate(done);
}
}
else {
this._internal_socket = true;
this._sock = this.generateSocket(address, port, done);
}
this._sock.on('message', this.handleRequest());
this._sock.on('error', (error) => {
this.emit('error', error);
});
if (parameters_1.parameters.pruneTimerPeriod != null) {
// Start LRU pruning timer
this._lru.pruneTimer = setInterval(() => {
this._lru.purgeStale();
}, parameters_1.parameters.pruneTimerPeriod * 1000);
if (this._lru.pruneTimer.unref != null) {
this._lru.pruneTimer.unref();
}
}
return this;
}
close(done) {
if (done != null) {
setImmediate(done);
}
if (this._lru.pruneTimer != null) {
clearInterval(this._lru.pruneTimer);
}
if (this._sock != null) {
if (this._internal_socket && this._sock instanceof dgram_1.Socket) {
this._sock.close();
}
this._lru.clear();
this._sock = null;
this.emit('close');
}
else {
this._lru.clear();
}
this._block2Cache.reset();
this._block1Cache.reset();
return this;
}
/**
* Entry point for a new datagram from the client.
* @param packet The packet that was sent from the client.
* @param rsinfo Connection info
*/
_handle(packet, rsinfo) {
var _a, _b, _c, _d, _e, _f, _g, _h;
if (packet.code == null || packet.code[0] !== '0') {
// According to RFC7252 Section 4.2 receiving a confirmable messages
// that can't be processed, should be rejected by ignoring it AND
// sending a reset. In this case confirmable response message would
// be silently ignored, which is not exactly as stated in the standard.
// However, sending a reset would interfere with a coap client which is
// re-using a socket (see pull-request #131).
return;
}
const sock = this._sock;
const lru = this._lru;
let Message = OutMessage;
const request = new incoming_message_1.default(packet, rsinfo);
const cached = lru.peek(this._toKey(request, packet, true));
if (cached != null && !((_a = packet.ack) !== null && _a !== void 0 ? _a : false) && !((_b = packet.reset) !== null && _b !== void 0 ? _b : false) && sock instanceof dgram_1.Socket) {
sock.send(cached, 0, cached.length, rsinfo.port, rsinfo.address);
return;
}
else if (cached != null && (((_c = packet.ack) !== null && _c !== void 0 ? _c : false) || ((_d = packet.reset) !== null && _d !== void 0 ? _d : false))) {
if (cached.response != null && ((_e = packet.reset) !== null && _e !== void 0 ? _e : false)) {
cached.response.end();
}
lru.delete(this._toKey(request, packet, false));
return;
}
else if ((_g = (_f = packet.ack) !== null && _f !== void 0 ? _f : packet.reset) !== null && _g !== void 0 ? _g : false) {
return; // nothing to do, ignoring silently
}
if (request.headers.Observe === 0) {
Message = observe_write_stream_1.default;
if (packet.code !== '0.01' && packet.code !== '0.05') {
// it is neither a GET nor a FETCH
this._sendError(Buffer.from('Observe can only be present with a GET or a FETCH'), rsinfo);
return;
}
}
if (packet.code === '0.05' && request.headers['Content-Format'] == null) {
this._sendError(Buffer.from('FETCH requests must contain a Content-Format option'), rsinfo, undefined, '4.15' /* TODO: Check if this is the correct error code */);
return;
}
const cacheKey = this._toCacheKey(request, packet);
packet.piggybackReplyMs = this._options.piggybackReplyMs;
const generateResponse = () => {
const response = new Message(packet, (response, packet) => {
/**
* Extended `Buffer` with additional fields for caching.
*
* TODO: Find a more elegant solution for this type.
*/
let buf;
const sender = new retry_send_1.default(sock, rsinfo.port, rsinfo.address);
try {
buf = (0, coap_packet_1.generate)(packet, parameters_1.parameters.maxMessageSize);
}
catch (err) {
response.emit('error', err);
return;
}
if (Message === OutMessage) {
sender.on('error', response.emit.bind(response, 'error'));
}
else {
buf.response = response;
sender.on('error', () => {
response.end();
});
}
const key = this._toKey(request, packet, packet.ack || !packet.confirmable);
lru.set(key, buf);
buf.sender = sender;
if (this._options.sendAcksForNonConfirmablePackets === true ||
packet.confirmable) {
sender.send(buf, packet.ack || packet.reset || !packet.confirmable);
}
else {
debug('OMIT ACK PACKAGE');
}
});
response.statusCode = '2.05';
response._request = request._packet;
if (cacheKey != null) {
response._cachekey = cacheKey;
}
// inject this function so the response can add an entry to the cache
response._addCacheEntry = this._block2Cache.add.bind(this._block2Cache);
return response;
};
const response = generateResponse();
request.rsinfo = rsinfo;
if (packet.token != null && packet.token.length > 0) {
// return cached value only if this request is not the first block request
const block2Buff = (0, helpers_1.getOption)(packet.options, 'Block2');
let requestedBlockOption = { num: 0, more: 0, size: 0 };
if (block2Buff instanceof Buffer) {
requestedBlockOption = (_h = (0, helpers_1.parseBlock2)(block2Buff)) !== null && _h !== void 0 ? _h : requestedBlockOption;
}
if (cacheKey == null) {
return;
}
else if (requestedBlockOption.num < 1) {
if (this._block2Cache.remove(cacheKey)) {
debug('first block2 request, removed old entry from cache');
}
}
else {
debug('check if packet token is in cache, key:', cacheKey);
if (this._block2Cache.contains(cacheKey)) {
debug('found cached payload, key:', cacheKey);
if (response != null) {
const cacheEntry = this._block2Cache.get(cacheKey);
cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.options.forEach((option) => { var _a; return (_a = response._packet.options) === null || _a === void 0 ? void 0 : _a.push(option); });
response.end(cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.buffer);
}
return;
}
}
}
const block1Buff = (0, helpers_1.getOption)(packet.options, 'Block1');
if (block1Buff instanceof Buffer) {
const blockState = (0, block_1.parseBlockOption)(block1Buff);
if (blockState != null) {
const cachedData = this._block1Cache.getWithDefaultInsert(cacheKey);
const blockByteSize = Math.pow(2, 4 + blockState.size);
const incomingByteIndex = blockState.num * blockByteSize;
// Store in the cache object, use the byte index as the key
cachedData[incomingByteIndex] = request.payload;
if (blockState.more === 0) {
// Last block
const byteOffsets = Object.keys(cachedData)
.map((str) => {
return parseInt(str);
})
.sort((a, b) => {
return a - b;
});
const byteTotalSum = incomingByteIndex + request.payload.length;
let next = 0;
const concat = Buffer.alloc(byteTotalSum);
for (let i = 0; i < byteOffsets.length; i++) {
if (byteOffsets[i] === next) {
const buff = cachedData[byteOffsets[i]];
if (!(buff instanceof Buffer)) {
continue;
}
buff.copy(concat, next, 0, buff.length);
next += buff.length;
}
else {
throw new Error('Byte offset not the next in line...');
}
}
if (cacheKey != null) {
this._block1Cache.remove(cacheKey);
}
if (next === concat.length) {
request.payload = concat;
}
else {
throw new Error('Last byte index is not equal to the concat buffer length!');
}
}
else {
// More blocks to come. ACK this block
if (response != null) {
response.code = '2.31';
response.setOption('Block1', block1Buff);
response.end();
}
return;
}
}
else {
throw new Error('Invalid block state');
}
}
this.emit('request', request, response);
this.saveAdditionalBlock2Options(cacheKey, response);
}
saveAdditionalBlock2Options(cacheKey, response) {
var _a;
if (cacheKey != null) {
const cacheEntry = this._block2Cache.get(cacheKey);
(_a = response === null || response === void 0 ? void 0 : response._packet.options) === null || _a === void 0 ? void 0 : _a.forEach((option) => cacheEntry === null || cacheEntry === void 0 ? void 0 : cacheEntry.options.push(option));
}
}
/**
*
* @param request
* @param packet
* @returns
*/
_toCacheKey(request, packet) {
if (packet.token != null && packet.token.length > 0) {
return `${packet.token.toString('hex')}/${this._clientIdentifier(request)}`;
}
return null;
}
/**
*
* @param request
* @param packet
* @param appendToken
* @returns
*/
_toKey(request, packet, appendToken) {
let result = this._clientIdentifier(request);
if (packet.messageId != null) {
result += `/${packet.messageId}`;
}
if (appendToken && packet.token != null) {
result += packet.token.toString('hex');
}
return result;
}
}
// Max block size defined in the protocol is 2^(6+4) = 1024
let maxBlock2 = 1024;
// Some network stacks (e.g. 6LowPAN/Thread) might have a lower IP MTU.
// In those cases the maxPayloadSize parameter can be adjusted
if (parameters_1.parameters.maxPayloadSize < 1024) {
// CoAP Block2 header only has sizes of 2^(i+4) for i in 0 to 6 inclusive,
// so pick the next size down that is supported
let exponent = Math.log2(parameters_1.parameters.maxPayloadSize);
exponent = Math.floor(exponent);
maxBlock2 = Math.pow(2, exponent);
}
/*
new out message
inherit from OutgoingMessage
to handle cached answer and blockwise (2)
*/
class OutMessage extends outgoing_message_1.default {
/**
* Entry point for a response from the server
*
* @param payload A buffer-like object containing data to send back to the client.
* @returns
*/
end(payload) {
// removeOption(this._request.options, 'Block1');
// add logic for Block1 sending
const block2Buff = (0, helpers_1.getOption)(this._request.options, 'Block2');
let requestedBlockOption = null;
// if we got blockwise (2) request
if (block2Buff != null) {
if (block2Buff instanceof Buffer) {
requestedBlockOption = (0, helpers_1.parseBlock2)(block2Buff);
}
// bad option
if (requestedBlockOption == null) {
this.statusCode = '4.02';
return super.end();
}
}
// if payload is suitable for ONE message, shoot it out
if (payload == null ||
(requestedBlockOption == null && payload.length < maxBlock2)) {
return super.end(payload);
}
// for the first request, block2 option may be missed
if (requestedBlockOption == null) {
requestedBlockOption = {
size: maxBlock2,
more: 1,
num: 0
};
}
// block2 size should not bigger than maxBlock2
if (requestedBlockOption.size > maxBlock2) {
requestedBlockOption.size = maxBlock2;
}
// block number should have limit
const lastBlockNum = Math.ceil(payload.length / requestedBlockOption.size) - 1;
if (requestedBlockOption.num > lastBlockNum) {
// precondition fail, may request for out of range block
this.statusCode = '4.02';
return super.end();
}
// check if requested block is the last
const more = requestedBlockOption.num < lastBlockNum ? 1 : 0;
const block2 = (0, helpers_1.createBlock2)({
more,
num: requestedBlockOption.num,
size: requestedBlockOption.size
});
if (block2 == null) {
// this catch never be match,
// since we're gentleman, just handle it
this.statusCode = '4.02';
return super.end();
}
this.setOption('Block2', block2);
this.setOption('ETag', _toETag(payload));
const size2 = (0, helpers_1.getOption)(this._request.options, 'Size2');
if (size2 === 0) {
this.setOption('Size2', payload.length);
}
// cache it
if (this._request.token != null && this._request.token.length > 0) {
this._addCacheEntry(this._cachekey, { buffer: payload, options: [] });
}
super.end(payload.slice(requestedBlockOption.num * requestedBlockOption.size, (requestedBlockOption.num + 1) * requestedBlockOption.size));
return this;
}
}
/*
calculate id of a payload by xor each 2-byte-block from it
use to generate etag
payload an input buffer, represent payload need to generate id (hash)
id return var, is a buffer(2)
*/
function _toETag(payload) {
const id = Buffer.of(0, 0);
let i = 0;
do {
id[0] ^= payload[i];
id[1] ^= payload[i + 1];
i += 2;
} while (i < payload.length);
return id;
}
exports.default = CoAPServer;
//# sourceMappingURL=server.js.map