iopa-udp
Version:
API-First User Datagram Protocol (UDP) stack for Internet of Things (IoT), based on Internet of Protocols Alliance (IOPA) specification
321 lines (264 loc) • 11.4 kB
JavaScript
/*
* Copyright (c) 2015 Internet of Protocols Alliance (IOPA)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// DEPENDENCIES
var iopa = require('iopa');
var dgram = require('dgram');
var util = require('util');
var events = require('events');
var iopaStream = require('iopa-common-stream');
var net = require('net');
const IOPA = iopa.constants.IOPA,
SERVER = iopa.constants.SERVER
const packageVersion = require('../../package.json').version;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/* ***********************************************************************
* IOPA UDP SIMPLEX SERVER AND CLIENT ON SAME PORT
* Unicast or Multicast+Unicast
* *********************************************************************** */
var seq = 0;
/**
* Representes UDP Server
*
* @class UdpSimple
* @param options (object) {currently unusued}
* @param appFunc (function(context)) delegate to which to call with all new inbound requests
* @event request function(context) alternative way to get inbound requests
* @event error function(err, args)
* @constructor
* @public
*/
function UdpSimplex(options, appFunc) {
_classCallCheck(this, UdpSimplex);
if (typeof options === 'function') {
appFunc = options;
options = {};
}
events.EventEmitter.call(this);
if (typeof options == 'string'){
this._id = options;
options = {};
options[SERVER.Id] = this._id
} else
{
options = options || {};
this._id = options[SERVER.Id] || (seq++).toString();
}
this._options = options;
this._factory = new iopa.Factory(options);
this._appFunc = appFunc;
this._connectFunc = this._appFunc.connect || function (context) { return Promise.resolve(context) };
this._createFunc = this._appFunc.create || function (context) { return context };
this._dispatchFunc = this._appFunc.dispatch || function (context) { return Promise.resolve(context) };
this._udp = null;
this._connections = {};
}
util.inherits(UdpSimplex, events.EventEmitter)
/**
* @method listen
* Create socket and bind to local port to listen for incoming requests
*
* @param {Integer} [port] Port on which to listen
* @param {String} [address] Local host address on which to listen
* @returns {Promise}
* @public
*/
UdpSimplex.prototype.listen = function UdpSimplex_listen(port, address, options) {
options = options || {};
iopa.util.shallow.merge(options, this._options);
if (port == undefined) {
address = 0;
}
if (this._udp)
return new Promise(function (resolve, reject) {
reject("Already listening");
});
if (address && net.isIPv6(address))
options[SERVER.LocalPortType] = 'udp6';
if (!options[SERVER.LocalPortType])
options[SERVER.LocalPortType] = 'udp4'
if (!options[SERVER.LocalPortReuse])
options[SERVER.LocalPortReuse] = true;
if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) > 0.11) {
//RE-USE ADDRESS IF NODE 0.12 OR LATER
this._udp = dgram.createSocket({ type: options[SERVER.LocalPortType], "reuseAddr": options[SERVER.LocalPortReuse] })
}
else {
this._udp = dgram.createSocket(this._options[SERVER.LocalPortType]);
}
this._udp.on("message", this._onMessage.bind(this));
var that = this;
return new Promise(function (resolve, reject) {
that._udp.bind(port, address || null,
function () {
that._linfo = that._udp.address();
that._port = that._linfo.port;
that._address = that._linfo.address;
var el;
if (typeof options[SERVER.MulticastAddress] == 'object' && util.isArray(options[SERVER.MulticastAddress])) {
for (var n = 0; n < options[SERVER.MulticastAddress].length; n++) {
el = options[SERVER.MulticastAddress][n];
that._udp.setBroadcast(true);
that._udp.setMulticastTTL(128);
// that._multicastUDP.setMulticastLoopback(true);
if (typeof el == 'string') {
that._udp.addMembership(el);
that._multicastAddress = el;
}
else if (typeof el == 'object' && util.isArray(el)) {
that._udp.addMembership(el[0], el[1]);
that._multicastAddress = el[0];
}
}
}
else if (typeof options[SERVER.MulticastAddress] == 'string') {
el = options[SERVER.MulticastAddress];
that._udp.addMembership(el);
that._multicastAddress = el;
}
resolve(that._linfo);
});
});
};
Object.defineProperty(UdpSimplex.prototype, SERVER.LocalPort, { get: function () { return this._port; } });
Object.defineProperty(UdpSimplex.prototype, SERVER.LocalAddress, { get: function () { return this._address; } });
Object.defineProperty(UdpSimplex.prototype, SERVER.MulticastAddress, { get: function () { return this._multicastAddress; } });
Object.defineProperty(UdpSimplex.prototype, "port", { get: function () { return this._port; } });
Object.defineProperty(UdpSimplex.prototype, "address", { get: function () { return this._address; } });
Object.defineProperty(UdpSimplex.prototype, SERVER.RawTransport, {
get: function () { return this; },
set: function(value) {
this._write = value._write.bind(value);
this.connect = value.connect.bind(value);
} });
UdpSimplex.prototype._onMessage = function UdpSimplex_onMessage(msg, rinfo) {
var context = this._factory.createContext();
context[IOPA.Method] = IOPA.METHODS.data;
context[SERVER.Id] = this._id;
context[SERVER.TLS] = false;
context[SERVER.RemoteAddress] = rinfo.address;
context[SERVER.RemotePort] = rinfo.port;
context[SERVER.LocalAddress] = this._address;
context[SERVER.LocalPort] = this._port;
context[SERVER.RawStream] = new iopaStream.IncomingMessageStream();
context[SERVER.RawStream].append(msg);
context[SERVER.IsLocalOrigin] = false;
context[SERVER.IsRequest] = true;
context[SERVER.SessionId] = context[SERVER.LocalAddress] + ":" + context[SERVER.LocalPort] + "-" + context[SERVER.RemoteAddress] + ":" + context[SERVER.RemotePort];
var response = context.response;
response[SERVER.Id] = this._id;
response[SERVER.TLS] = context[SERVER.TLS];
response[SERVER.RemoteAddress] = context[SERVER.RemoteAddress];
response[SERVER.RemotePort] = context[SERVER.RemotePort];
response[SERVER.LocalAddress] = context[SERVER.LocalAddress];
response[SERVER.LocalPort] = context[SERVER.LocalPort];
response[SERVER.RawStream] = new iopaStream.OutgoingStreamTransform(this._write.bind(this, context.response));
response[SERVER.IsLocalOrigin] = true;
response[SERVER.IsRequest] = false;
context.create = this._create.bind(this, context, response);
context.dispatch = this._dispatchFunc;
response.dispatch = this._dispatchFunc.bind(this, response);
context.using(this._appFunc);
}
/**
* Creates a new IOPA Request using a UDP Url including host and port name
*
* @method connect
* @parm {object} options not used
* @parm {string} urlStr url representation of ://127.0.0.1:8200
* @public
* @constructor
*/
UdpSimplex.prototype.connect = function UdpSimplex_connect(urlStr, defaults) {
defaults = defaults || {};
defaults[IOPA.Method] = defaults[IOPA.Method] || IOPA.METHODS.connect;
var channelContext = this._factory.createRequest(urlStr, defaults);
channelContext[SERVER.Id] = this._id;
channelContext[SERVER.LocalPort] = this._port;
channelContext[SERVER.LocalAddress] = this._address;
channelContext[SERVER.OriginalUrl] = urlStr;
channelContext.create = this._create.bind(this, channelContext, channelContext);
channelContext.dispatch = this._dispatchFunc;
channelContext[SERVER.Disconnect] = this._disconnect.bind(this, channelContext);
channelContext[SERVER.RawStream] = new iopaStream.OutgoingStreamTransform(this._write.bind(this, channelContext));
channelContext[SERVER.RawStream].on('finish', this._disconnect.bind(this, channelContext, null));
channelContext[SERVER.SessionId] = channelContext[SERVER.LocalAddress] + ":" + channelContext[SERVER.LocalPort] + "-" + channelContext[SERVER.RemoteAddress] + ":" + channelContext[SERVER.RemotePort];
this._connections[channelContext[SERVER.SessionId]] = channelContext;
var that = this;
return new Promise(function (resolve, reject) {resolve(that._connectFunc(channelContext));});
};
UdpSimplex.prototype._write = function UdpSimplex_write(context, chunk, encoding, done) {
if (typeof chunk === "string" || chunk instanceof String) {
chunk = new Buffer(chunk, encoding);
}
this._udp.send(chunk, 0, chunk.length, context[SERVER.RemotePort], context[SERVER.RemoteAddress], done);
}
/**
* Create a new IOPA Request, optionally using a UDP Url
*
* @method create
* @param path string representation of /hello
* @param options object dictionary to override defaults
* @returns context
* @public
*/
UdpSimplex.prototype._create = function UdpSimplex_create(channelContext, transportContext, path, options) {
var urlStr = channelContext[IOPA.Scheme] +
"//" +
channelContext[SERVER.RemoteAddress] + ":" + channelContext[SERVER.RemotePort] +
channelContext[IOPA.PathBase] +
channelContext[IOPA.Path];
if (path) urlStr += path;
var context = channelContext[SERVER.Factory].createRequest(urlStr, options);
channelContext[SERVER.Id] = this._id;
channelContext[SERVER.Factory].mergeCapabilities(context, channelContext);
context[SERVER.SessionId] = channelContext[SERVER.SessionId];
context[SERVER.LocalAddress] = transportContext[SERVER.LocalAddress];
context[SERVER.LocalPort] = transportContext[SERVER.LocalPort];
context[SERVER.RawStream] = transportContext[SERVER.RawStream];
context.dispatch = this._dispatchFunc.bind(this, context);
return this._createFunc(context);
};
/**
* @method _disconnect
* Close the channel context
*
* @public
*/
UdpSimplex.prototype._disconnect = function UdpSimplex_disconnect(channelContext, err) {
if (channelContext[IOPA.CancelToken].isCancelled)
return;
channelContext[IOPA.Events] = null;
channelContext[SERVER.CancelTokenSource].cancel(IOPA.EVENTS.Disconnect);
delete this._connections[channelContext[SERVER.SessionId]];
channelContext.dispose();
}
/**
* @method close
* Close the underlying socket and stop listening for data on it.
*
* @returns {Promise()}
* @public
*/
UdpSimplex.prototype.close = function UdpSimplex_close() {
for (var key in this._connections)
this._disconnect(this._connections[key], null);
this._connections = {};
this._udp.close();
this.emit("close");
this._udp = undefined;
return Promise.resolve(null);
};
module.exports = UdpSimplex;