iwebpp.io
Version:
iWebPP name-client library for node.js
1,169 lines (991 loc) • 119 kB
JavaScript
// iWebPP.IO name-client implementation
// Copyright (c) 2012-present Tom Zhou<iwebpp@gmail.com>
'use strict';
var debug = require('debug')('iwebpp.io');
// eventEmitter
var eventEmitter = require('events').EventEmitter,
util = require('util'),
httpp = require('httpp'),
httpps = require('httpps'),
https = require('https'),
UDT = require('udt'), // for hole punch, isIP
NET = require('net'), // for tunneling
os = require('os'), // for network interface check
DNS = require('dns'),
URL = require('url'),
FS = require('fs');
// MSGPACK library
var MSGPACK = require('msgpack-js');
// UUID generator
var UUID = require('uuid');
// p2p stream websocket library
var WebSocket = require('wspp');
var WebSocketServer = WebSocket.Server;
// Session establish protocol
var SEP = require('./sep');
// vURL
var vURL = require('./vurl');
// root CA for connection to ns/as
var vCA = require('ssl-root-cas').create();
vCA.unshift(FS.readFileSync(__dirname + '/../ca/ca-cert.pem'));
// helpers
function isLocalhost(host) {
return ((host === 'localhost') || (host === '127.0.0.1') ||
(host === 'ip6-localhost') || (host === '0:0:0:0:0:0:0:1') || (host === '::1'));
}
// vToken represents as /vtoken/xxx
function genvTokenStr(vtoken){
return (vtoken ? '/vtoken/'+vtoken : '');
}
// name-client pair: one primary client, another client bind on the same port and connect to alternate name-server.
// option argument consists of srvinfo,usrinfo,devinfo,conmode,secmode,vURL mode:
// - srvinfo: {
// endpoints: xxx,
// timeout: xxx, // in sec
// turn: [{ip: xxx, agent: xxx, proxy: xxx}] // TURN server endpoints with ip and proxy/agent ports
// - }
// - usrinfo: user MUST put user-specific info here to identify user globally,
// for exmaple, user name+password->usrkey, domain name, etc
// - devinfo: device identity, etc, that used to generate devkey
// - conmode: c/s or p2p connection mode
// - secmode: secure mode, SSL, etc
// - sslmode: ssl authentication mode, server only, both client and server
// - vmode: vURL mode, 'vpath' or 'vhost', default is 'vhost'
// - ipmode: IPv6 or IPv4. 4: IPv4, 6: IPv6
// - intf: {ipaddr: xxx, port: xxx} // bing on dedicated local interface
// -keephole: true - keep punched hole live, false or null - close hole after punched, default true
// - backlog: server listen backlog, default 1024
var nmCln = exports = module.exports = function(options, fn){
var self = this;
if (!(this instanceof nmCln)) return new nmCln(options, fn);
// super constructor
eventEmitter.call(self);
if (typeof options === 'function') {
fn = options;
options = {};
}
// check on version
self.version = 1;
/////////////////////////////////////////////////////////////////
// arguments check
if (options === undefined || options.usrinfo === undefined) {
console.error('please enter usrinfo parameter like: {usrinfo: {domain: "51dese.com", usrkey: "dese"}}');
this.state = 'fail';
this.emit('error', 'invalid arguments');
return;
}
// default server information
options.srvinfo = options.srvinfo || {
timeout: 20,
endpoints: [{ip: '51dese.com', port: 51686}, {ip: '51dese.com', port: 51868}],
turn: [
{ip: '51dese.com', agent: 51866, proxy: 51688} // every turn-server include proxy and agent port
]
};
// default connection mode
if (options.conmode === undefined)
options.conmode = SEP.SEP_MODE_CS;
////////////////////////////////////////////////////////////////////
var conn;
var rsdp;
var srvinfo = options.srvinfo;
var usrinfo = options.usrinfo;
var devinfo = options.devinfo;
var conmode = options.conmode;
// TBD as option and exchange between peers
var keephole = false; ///options.keephole === false ? false : true;
// TBD optimizing listening backlog
var backlog = options.backlog || 1024;
// level-based ACL
var secmode = (options.secmode === undefined) ?
SEP.SEP_SEC_SSL : options.secmode;
// SSL authentication mode
var sslmode = (options.sslmode === undefined) ?
SEP.SEP_SSL_AUTH_SRV_ONLY : options.sslmode;
// vpath or vhost
var vmode = (options.vmode === undefined) ? vURL.URL_MODE_HOST : options.vmode;
//
// State Machine as FSM:
// new->connecting->(connected/timeout/error)->
// (ready->reconnecting->connecting->(connected/timeout/error)->ready)->
// closing->closed
//
self.state = 'new';
// reconnect count
self.reconnect = 0;
// offer/answer session cache
self.offans_sdp = []; // sdp session info
///self.offans_stun = []; // stun session info
///self.offans_turn = []; // turn session info
///self.offans_user = []; // login user info
// userinfo/identity
self.usrinfo = usrinfo;
// deviceinfo/identity
// TBD... got device identity by name UUID version 1 or 3 natively
self.devinfo = devinfo || {devkey: UUID.v1()};
// connection mode / security mode / ssl auth mode: https, etc
self.conmode = conmode;
self.secmode = secmode;
self.secerts = null; // secure certification in case https/wss key/cert/ca, etc
self.sslmode = sslmode; // SSL authentication mode
self.srvsslcerts = null; // server certs
// keep hole or not
self.keephole = keephole;
// listening backlog
self.backlog = backlog;
// serverinfo
// at least connect to two name servers
self.srvinfo = srvinfo;
self.srvs = srvinfo.endpoints ||
[{ip: 'localhost', port: 51686},
{ip: 'localhost', port: 51868}];
self.ocnt = 0; // opened connection count to servers
self.conn = {}; // connections to name-server
// IP mode: IPv4 or IPv6
self.ipmode = (options.ipmode === undefined) ? 0 : options.ipmode;
// local/inner IP address info
self.port = (options.intf && options.intf.port) || 0; // all coonection MUST bind on the same port!!!
self.ipaddr = (options.intf && options.intf.ipaddr) || null; // all coonection MUST bind on the same local interface
// public/outter IP address info
self.gid = UUID.v1(); // GID of name-client
self.natype = 0; // 0: cone NAT/FW, 1: symmetric NAT/FW
self.oipaddr = ''; // the out address seen by peer. if NAT exists, it should be NAT/gw address, not local address
self.oport = 0; // the out port seen by primary name-server
self.geoip = null; // GeoIP of name-client
self.peer = {}; // connections pool to peers
self.hole = {}; // hole-keep connection to peers
// at most one business server on self.ipaddr/port for UDT or HTTPP or Websocket
self.bsrv = {}; // business server bind on self.port in c/s connection mode
// vURL info
self.vmode = vmode;
self.vpath = '';
self.vhost = '';
self.tvurl = ''; // TURN based vURL
self.svurl = ''; // STUN based vURL TBD...
self.vurl = ''; // vURL generic hard-code as tvurl by now
self.vtoken = ''; // vURL security token
self.vtokenstr = ''; // vURL security token string like /vtoken/xxx
// turn server obj cache
// TBD balance... now only connect to one turn server
if (srvinfo.turn) {
self.turnSrvs = Array.isArray(srvinfo.turn) ? srvinfo.turn : [srvinfo.turn];
self.turnagentConn = {}; // connection to turn agent server
} else {
self.turnSrvs = null;
self.turnagentReady = false;
}
// ACL cache
// notes: only allow incoming connection from turn-server agent port and
// authorized peer name-client
// like: {'v4:ip:port': true} - allow; {'v4:ip:port': null or false} - reject;
// { 'v4:ip': true} - allow; { 'v4:ip': null or false} - reject;
self.acls = {};
// Launch normal logics ->//////////////////////////////////////////////////////////
// 0.
// detect IP mode: IPv4 or IPv6
self._getIPVersion(self.srvs[0].ip, function(err, ipv) {
if (err || !ipv) {
console.error('No interface reach to Nameserver');
self.state = 'error';
self.emit('error', 'No interface reach to Nameserver');
} else {
self.ipmode = ipv;
// 1.
// get valid network IP address and setup SDP session once
self._getValidIPAddr(self.ipmode, function (err, addr) {
if (!err && addr) {
debug('outgoing network interface ip address: ' + addr);
// record local IP address
self.ipaddr = addr;
// 1.1
// setup SDP session once
self._LSM_setupSdpSession();
} else {
console.error('no outgoing network interface');
self.state = 'error';
self.emit('error', 'no outgoing network interface');
}
});
}
});
// 2.
// Launch business server once ready
self.once('ready', function(){
debug('SDP session ready, launch business server');
// launch business server
self._LSM_setupBusinessServer();
});
// 3.
// Handle reconnect
self.on('reconnect', function(event) {
debug('Reconnect by '+event+', '+self.reconnect+' times');
// 3.1
// increase reconnect count
self.reconnect += 1;
// 3.2
// clear all existing connections ???
try {
// 3.2.1
// close name-server connections
for (var k in self.conn)
if (self.conn[k] && self.conn[k].socket && self.conn[k].socket.close) {
self.conn[k].socket.close();
}
// 3.2.2
// close turn agent connection
if (self.turnagentConn && self.turnagentConn.close) {
self.turnagentConn.close();
}
// 3.2.3
// keep peer connections
// hole-keep connections
/*for (var h in self.hole)
if (self.hole[h] && self.hole[h].socket && self.hole[h].socket.close) {
self.hole[h].socket.close();
}*/
} catch (e) {
console.error('clear all sockets, ignore '+e);
}
// 3.3
// keep all business services
// 3.6
// relaunch SDP session after 2s
setTimeout(function() {
self._LSM_setupSdpSession();
}, 2000);
});
// 6.
// hook user callback once ready
if (typeof fn === 'function') {
self.once('ready', fn);
}
//<- Launch normal logics //////////////////////////////////////////////////////////
};
util.inherits(nmCln, eventEmitter);
// Internal methods for LSM
// detect Nameserver's IPVersion as IP mode
nmCln.prototype._getIPVersion = function(srvip, fn) {
var self = this;
if (self.ipmode) {
// check from argument
fn(null, self.ipmode);
} else {
var ipv = UDT.isIP(srvip);
if (ipv) {
// check if IP
fn(null, ipv);
} else {
// DNS lookup
DNS.lookup(srvip, function (err, address, family) {
if (err) {
console.error(err + ',dns lookup srvinfo failed');
fn(err + ',dns lookup srvinfo failed');
} else {
fn(null, family);
}
});
}
}
};
// get valid network interface IP address
nmCln.prototype._getValidIPAddr = function(proto, fn) {
var self = this;
// 0.
// extract local IP address interface and test connection avalabiltity to name-server
var intfs = os.networkInterfaces();
var addr4 = [], addr6 = [];
function testConnection2NS(laddr, fn){
var wsproto = self.secmode ? 'wss://' : 'ws://';
var nscon = new WebSocket(wsproto+self.srvs[0].ip+':'+self.srvs[0].port+SEP.SEP_CTRLPATH_NS,
{
httpp: true, hole: {addr: laddr},
// SSL related info
rejectUnauthorized: true,
ca: vCA
});
var nstmo = setTimeout(function(){
fn(0);
debug('connect to NS from '+laddr+' ... timeout');
}, 2000); // 2s timeout
nscon.once('open', function(){
clearTimeout(nstmo);
fn(1, laddr);
nscon.close();
debug('connect to NS from '+laddr+' ... ok');
});
}
// 0.2
// extract all network interfaces
debug('network interfaces: '+JSON.stringify(intfs));
for (var k in intfs) {
for (var kk in intfs[k]) {
if (((intfs[k])[kk].internal === (self.srvs[0].ip === 'localhost' ||
self.srvs[0].ip === '127.0.0.1')) &&
('IPv4' === (intfs[k])[kk].family)) {
// find foreign network interface
addr4.push((intfs[k])[kk].address);
}
if (((intfs[k])[kk].internal === (self.srvs[0].ip === 'ip6-localhost' ||
self.srvs[0].ip === '::1' ||
self.srvs[0].ip === '0:0:0:0:0:0:0:1')) &&
('IPv6' === (intfs[k])[kk].family)) {
// find foreign network interface
addr6.push((intfs[k])[kk].address);
}
}
}
// 0.3
// test network interface availability
var intfsaddr = (proto == 6) ? addr6 : addr4;
var intfskkok = 0;
for (var idx = 0; idx < intfsaddr.length; idx ++) {
// skip loop
if (intfskkok) break;
// test connection
testConnection2NS(intfsaddr[idx], function(yes, addr){
if (yes && !intfskkok) {
// select first available foreign network interface
intfskkok = 1;
// send back ipaddr
fn(null, addr);
} else if ((idx === (intfsaddr.length-1)) && !intfskkok) {
console.error('failed get IPv'+proto+' address');
fn('failed get IPv'+proto+' address');
}
});
}
};
// connect to TURN agent server to setup TURN/PUNCH session
nmCln.prototype._LSM_connectTurnAgent = function(fn, tmo){
var self = this;
// 0.
// callback event count
self.clntturnagentCbCnt = self.clntturnagentCbCnt || 0;
// 1.
// make websocket connection to agent port
// TBD... secure agent server
if (self.turnSrvs && self.turnSrvs.length) {
var wsproto = self.secmode ? 'wss://' : 'ws://';
debug('turn agent connection:'+wsproto+self.turnSrvs[0].ip+':'+self.turnSrvs[0].agent+SEP.SEP_CTRLPATH_AS);
self.turnagentConn = new WebSocket(wsproto+self.turnSrvs[0].ip+':'+self.turnSrvs[0].agent+SEP.SEP_CTRLPATH_AS,
{
httpp: true, hole: {port: self.port, addr: self.ipaddr},
// SSL related info
rejectUnauthorized: true,
ca: vCA
});
// initialize offer message count per client
// every time, client send one offer message, increase it by one
self.turnagentConn.offerMsgcnt = 0;
var t = setTimeout(function(){
self.removeAllListeners('clntturnagent'+self.clntturnagentCbCnt);
fn('connect TURN agent server timeout');
}, (tmo || 30)*1000); // 30s timeout in default
self.turnagentConn.once('open', function(){
debug('connected to turn agent server successfully');
// 1.1
// waiting for hole punch answer message
self.once('clntturnagent'+self.clntturnagentCbCnt, function(yes){
clearTimeout(t);
fn(null, yes);
});
// 2.
// send hole punch offer message anyway
var tom = {};
tom.opc = SEP.SEP_OPC_PUNCH_OFFER;
tom.offer = {
// protocol info
proto : 'udp',
mode : SEP.SEP_MODE_CS,
// user info
domain : (self.usrinfo && self.usrinfo.domain) || '51dese.com',
usrkey : (self.usrinfo && self.usrinfo.usrkey) || 'tomzhou',
// client info
vmode : self.vmode, // vURL mode
secmode : self.secmode, // security mode
clntgid : self.gid,
clntlocalIP : self.ipaddr,
clntlocalPort: self.port,
devkey : (self.devinfo && self.devinfo.devkey) || 'iloveyou',
// server info
srvip : self.turnSrvs[0].ip,
proxyport: self.turnSrvs[0].proxy,
agentport: self.turnSrvs[0].agent
};
tom.seqno = self.turnagentConn.offerMsgcnt++;
// !!! put callback event count
tom.evcbcnt = self.clntturnagentCbCnt++;
try {
// !!!Prefer json format
///self.turnagentConn.send(MSGPACK.encode(tom), {binary: true, mask: false}, function(err){
self.turnagentConn.send(JSON.stringify(tom), {binary: false, mask: false}, function(err){
if (err) console.error(err+',send turn agent punch offer info failed');
});
} catch (e) {
console.error(e+',send turn agent punch offer failed immediately');
}
});
// 3.
// handle agent server message
self.turnagentConn.on('message', function(message, flags){
var tdata = (flags.binary) ? MSGPACK.decode(message) : JSON.parse(message);
debug('nmclnt:new turn agent message:'+JSON.stringify(tdata));
// check if opc is valid
if ('number' === typeof tdata.opc) {
switch (tdata.opc) {
// offer/answer opc /////////////////////////////////////////////
case SEP.SEP_OPC_PUNCH_ANSWER:
debug('turn/punch session:'+JSON.stringify(tdata.answer));
// 3.1
// check offer credit
// 3.2
// check answer
if (tdata.answer && tdata.answer.ready) {
self.turnagentReady = true;
// record vURL security token
if (self.secmode > SEP.SEP_SEC_SSL) {
self.vtoken = tdata.answer.vtoken;
self.vtokenstr = genvTokenStr(self.vtoken);
} else {
self.vtoken = '';
self.vtokenstr = '';
}
} else {
self.turnagentReady = false;
}
self.emit('clntturnagent'+tdata.evcbcnt, self.turnagentReady);
break;
default:
console.error('unknown opc:'+JSON.stringify(tdata));
break;
}
} else {
console.error('unknown message:'+JSON.stringify(tdata));
}
});
// 4.
// handle close event with reconnect
self.turnagentConn.once('close', function() {
debug('turn agent client closed');
// 4.1
// trigger reconnect event
if (self.state === 'ready') {
// check if connection still broken
var turcon = self.turnagentConn;
if (!(turcon && (turcon.readyState === turcon.OPEN))) {
debug('trun agent client reconnect triggered');
self.state = 'reconnecting';
self.emit('reconnect', 'turn agent client closed');
} else {
debug('trun agent client already recoveried');
}
}
});
} else {
console.error('no TURN server');
fn('no TURN server');
}
};
// setup SDP session
nmCln.prototype._LSM_setupSdpSession = function() {
var self = this;
// initialize state
self.ocnt = 0;
// on meessage process
function onMessage(message, flags) {
// flags.binary will be set if a binary message is received
// flags.masked will be set if the message was masked
var data = (flags.binary) ? MSGPACK.decode(message) : JSON.parse(message);
debug('nmcln:new message:'+JSON.stringify(data));
// 1.
// check if opc is valid
if ('number' === typeof data.opc) {
switch (data.opc) {
case SEP.SEP_OPC_SDP_ANSWER:
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
self.offans_sdp.push(data);
// in case connected to all name-servers
debug('self.offans_sdp.length %d, self.srvs.length %d', self.offans_sdp.length, self.srvs.length);
if (self.offans_sdp.length === self.srvs.length) {
// 2.
// check if symmetric nat/firewall by answer, then extract clint's public ip/port
// TBD... sdp decision in server side
var natype = 0;
for (var idx = 0; idx < (self.offans_sdp.length - 1); idx ++) {
debug('SDP answer %d: %s', idx, JSON.stringify(self.offans_sdp[idx].answer.sdp));
debug('SDP answer %d: %s', idx+1, JSON.stringify(self.offans_sdp[idx+1].answer.sdp));
// 2.1
// check directed host
// TBD...
/*if (((self.offans_sdp[idx].answer.sdp.clntIP && self.offans_sdp[idx+1].answer.sdp.clntIP) &&
(self.offans_sdp[idx].answer.sdp.clntPort && self.offans_sdp[idx+1].answer.sdp.clntPort) &&
(self.offans_sdp[idx].answer.sdp.clntIP === self.offans_sdp[idx+1].answer.sdp.clntIP) &&
(self.offans_sdp[idx].answer.sdp.clntPort === self.offans_sdp[idx+1].answer.sdp.clntPort) &&
// === local port
(self.offans_sdp[idx].answer.sdp.clntPort === self.port))) {
natype = 2;
break;
}*/
// 2.2
// check symmetric nat/fw
if (!((self.offans_sdp[idx].answer.sdp.clntIP && self.offans_sdp[idx+1].answer.sdp.clntIP) &&
(self.offans_sdp[idx].answer.sdp.clntPort && self.offans_sdp[idx+1].answer.sdp.clntPort) &&
(self.offans_sdp[idx].answer.sdp.clntIP === self.offans_sdp[idx+1].answer.sdp.clntIP) &&
(self.offans_sdp[idx].answer.sdp.clntPort === self.offans_sdp[idx+1].answer.sdp.clntPort))) {
natype = 1;
break;
}
}
// 2.1
// record client GID, NAT type, public Ip/Port
///self.gid = self.offans_sdp[0].answer.client.gid;
self.vpath = self.offans_sdp[0].answer.client.vpath;
self.vhost = self.offans_sdp[0].answer.client.vhost;
self.natype = natype;
self.oipaddr = self.offans_sdp[0].answer.sdp.clntIP;
self.oport = self.offans_sdp[0].answer.sdp.clntPort;
self.geoip = self.offans_sdp[0].answer.client.geoip;
debug('GeoIP:'+JSON.stringify(self.geoip));
// 2.1.1
// record server's Domain Name
self.srvinfo.dn = self.offans_sdp[0].answer.server.dn;
// 2.1.2
// enable vURL if TURN ready
if (self.turnagentReady) {
var vurlproto = self.secmode ? 'https://' : 'http://';
if (self.vmode === vURL.URL_MODE_PATH) {
// vpath-based turn vURL
self.tvurl = vurlproto+
self.srvinfo.turn[0].ip+
((self.srvinfo.turn[0].proxy === 443) ? '' : (':'+self.srvinfo.turn[0].proxy));
// append vToken in secure vURL mode
self.tvurl += (self.secmode > SEP.SEP_SEC_SSL) ? self.vtokenstr : '';
// append vPath
self.tvurl += self.vpath;
} else {
// vhost-based turn vURL
// notes:
// - vhost-based vURL MUST use domain name instead of ip address
// - vhost-based vURL MUST Not use localhost
self.tvurl = vurlproto+
self.vhost+
(self.srvinfo.dn || self.srvinfo.turn[0].ip)+
((self.srvinfo.turn[0].proxy === 443) ? '' : (':'+self.srvinfo.turn[0].proxy));
// append vToken in secure vURL mode
self.tvurl += (self.secmode > SEP.SEP_SEC_SSL) ? self.vtokenstr : '';
}
// TBD... STUN session based vURL
// generic vURL hard-code as tvurl
// TBD...
self.vurl = self.tvurl;
}
// 2.1.3
// record security certification
if (self.secmode) {
// client certs
self.secerts = self.offans_sdp[0].answer.secerts;
self.secerts.ca = vCA;
// server certs
self.srvsslcerts = {};
// https certification like: {key: xxx, cert: xxx, ca: xxx}
['key', 'cert', 'ca'].forEach(function(k){
self.srvsslcerts[k] = self.secerts[k];
});
// check ssl auth mode like: {requestCert: xxx, rejectUnauthorized: xxx}
if (self.sslmode === SEP.SEP_SSL_AUTH_SRV_CLNT) {
['requestCert', 'rejectUnauthorized'].forEach(function(k){
self.srvsslcerts[k] = self.secerts[k];
});
} else {
self.srvsslcerts.requestCert = false;
self.srvsslcerts.rejectUnauthorized = false;
}
///debug('client SSL cert:'+JSON.stringify(self.secerts));
///debug('server SSL cert:'+JSON.stringify(self.srvsslcerts));
}
// 2.2
// return sdp info to user
var rsdp = {
// GID of name-client
gid: self.gid,
vpath: self.vpath,
vhost: self.vhost,
// connection/secure/vURL mode
conmode: self.conmode,
secmode: self.secmode,
vmode: self.vmode,
vtoken: self.vtoken,
// NAT/FW type
natype: natype, // 0: cone NAT/FW, 1: symmetric NAT/FW, 2: directed connecton without NAT/FW
// local binding address/port
port: self.port,
addr: self.ipaddr,
// from sdp offer/answer exchange
publicIP: self.oipaddr,
publicPort: self.oport, // useless it's symmetric NAT/FW
// GeoIP
geoip: self.geoip
};
self.emit('clntsdpanswer'+data.evcbcnt, rsdp);
debug('got SDP successfully:'+JSON.stringify(rsdp));
}
} else {
// return error info
self.emit('clntsdpanswer'+data.evcbcnt, {err: 'create sdp offer failed '+(data.answer.error ? data.answer.error : '')});
console.error('create sdp offer failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_NAT_ANSWER:
// 1.
// check answer state
if ((data.answer.state === SEP.SEP_OPC_STATE_READY) && data.answer.ready) {
// 2.
// send back stun info
self.emit('clntnatypeanswer'+data.evcbcnt);
debug('update client nat type info successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('clntnatypeanswer'+data.evcbcnt, 'update client nat type info failed '+(data.answer.error ? data.answer.error : ''));
console.error('update client nat type info failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_HEART_BEAT_ANSWER:
// 1.
// check answer state
if ((data.answer.state === SEP.SEP_OPC_STATE_READY) && data.answer.ready) {
// 2.
// send back stun info
self.emit('clntheartbeatanswer'+data.evcbcnt);
debug('update client heart-beat successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('clntheartbeatanswer'+data.evcbcnt, 'update client heart-beat failed '+(data.answer.error ? data.answer.error : ''));
console.error('update client heart-beat failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_PUNCH_OFFER:
debug('got SEP_OPC_PUNCH_OFFER');
// 1.
// check offer credits
// 2.
// punch hole
self.punchHole({
endpoint: data.offer.peer,
isInitiator: data.offer.isInitiator
},
function(err, coninfo){
if (err) console.error('punch hole failed: '+err);
// fill answer
data.opc = SEP.SEP_OPC_PUNCH_ANSWER;
data.answer = {};
if (!err && coninfo) {
data.answer.state = SEP.SEP_OPC_STATE_READY;
data.answer.ready = true;
data.answer.cinfo = coninfo; // pass connection external ip/port info
} else {
data.answer.state = SEP.SEP_OPC_STATE_FAIL;
data.answer.ready = false;
}
// 3.
// send back punch answer to name-servers
// TBD... balance among name-servers
// Algorithem:
// - 0: one directed connection, send message from nat/fw or Initiator of None-nat/fw
// - 1: both of asymmmetric NAT/FW, send message from Initiator side to name-server
// - 2: one asymmetric NAT/FW, another's symmetric, send message from symmetric side
// - 3: both of symmmetric NAT/FW, send message from Initiator side to name-server
if (self.natype===2 || data.offer.peer.natype===2) {
if (self.natype!=2 ||
(self.natype===2 && data.offer.isInitiator))
self.sendOpcMsg(data);
} else if (self.natype===0 && data.offer.peer.natype===0) {
// send message from Initiator only
if (data.offer.isInitiator)
self.sendOpcMsg(data);
} else if (self.natype===1 || data.offer.peer.natype===1) {
// send message from symmetric side only
if (self.natype===1)
self.sendOpcMsg(data);
} else {
// send message from Initiator only
if (data.offer.isInitiator)
self.sendOpcMsg(data);
}
});
break;
case SEP.SEP_OPC_STUN_ANSWER:
// 1.
// check answer state
if ((data.answer.state === SEP.SEP_OPC_STATE_READY) && data.answer.ready) {
// 2.
// send back stun info
self.emit('clntstunanswer'+data.evcbcnt, data.answer.stun);
debug('got stun info successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('clntstunanswer'+data.evcbcnt, {err: 'ask client stun session failed '+(data.answer.error ? data.answer.error : '')});
console.error('ask client stun session failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_TURN_ANSWER:
// 1.
// check answer state
if ((data.answer.state === SEP.SEP_OPC_STATE_READY) && data.answer.ready) {
// 2.
// send back turn info for initiator
if (data.answer.isInitiator) {
self.emit('clntturnanswer'+data.evcbcnt, data.answer.turn);
} else {
// 2.1
// just log in responsor client
console.log('Waiting for TURN/PROXY relay connection:'+JSON.stringify(data.answer.turn));
}
debug('got turn info successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('clntturnanswer'+data.evcbcnt, {err: 'ask client turn session failed '+(data.answer.error ? data.answer.error : '')});
console.error('ask client turn session failed:'+JSON.stringify(data));
}
break;
// user management opc -> //////////////////////////////////////////////
case SEP.SEP_OPC_CLNT_SDP_ANSWER:
// 1.
// check answer state
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
// 2.
// send back sdp info
self.emit('clntsdps'+data.evcbcnt, data.answer.sdps);
debug('got sdp info successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('clntsdps'+data.evcbcnt, {err: 'ask client sdp session failed '+(data.answer.error ? data.answer.error : '')});
console.error('ask client sdp session failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_ALL_USR_ANSWER:
// 1.
// check answer state
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
// 2.
// send back login info
self.emit('allusrs'+data.evcbcnt, data.answer.usrs);
debug('got user info successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('allusrs'+data.evcbcnt, {err: 'ask user info failed '+(data.answer.error ? data.answer.error : '')});
console.error('ask user info failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_ALL_LOGIN_ANSWER:
// 1.
// check answer state
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
// 2.
// send back login info
self.emit('alllogins'+data.evcbcnt, data.answer.logins);
debug('got user logins successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('alllogins'+data.evcbcnt, {err: 'ask user all logins failed '+(data.answer.error ? data.answer.error : '')});
console.error('ask user logins failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_USR_LOGIN_ANSWER:
// 1.
// check answer state
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
// 2.
// send back login info
self.emit('usrlogins'+data.evcbcnt, data.answer.logins);
debug('got user logins successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('usrlogins'+data.evcbcnt, {err: 'ask user logins failed '+(data.answer.error ? data.answer.error : '')});
console.error('ask user logins failed:'+JSON.stringify(data));
}
break;
// user management opc <- //////////////////////////////////////////////
// service management opc -> //////////////////////////////////////
case SEP.SEP_OPC_SRV_REPORT_ANSWER:
// 1.
// check answer state
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
// 2.
// send back service info
self.emit('reportsrv'+data.evcbcnt, data.answer.srv);
debug('report service successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('reportsrv'+data.evcbcnt, {err: 'report serivce failed '+(data.answer.error ? data.answer.error : '')});
console.error('report service failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_SRV_UPDATE_ANSWER:
// 1.
// check answer state
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
// 2.
// send back service info
self.emit('updatesrv'+data.evcbcnt, data.answer.srv);
debug('update service successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('updatesrv'+data.evcbcnt, {err: 'update serivce failed '+(data.answer.error ? data.answer.error : '')});
console.error('update service failed:'+JSON.stringify(data));
}
break;
case SEP.SEP_OPC_SRV_QUERY_ANSWER:
// 1.
// check answer state
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
// 2.
// send back service info
self.emit('querysrv'+data.evcbcnt, data.answer.srv);
debug('query service successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('querysrv'+data.evcbcnt, {err: 'query serivce failed '+(data.answer.error ? data.answer.error : '')});
console.error('query service failed:'+JSON.stringify(data));
}
break;
// service management opc <- ////////////////////////////////////
// vURL management opc -> ///////////////////////////////////////
case SEP.SEP_OPC_VURL_INFO_ANSWER:
// 1.
// check answer state
if (data.answer.state === SEP.SEP_OPC_STATE_READY) {
// 2.
// send back vURL info
self.emit('vurlinfo'+data.evcbcnt, data.answer.info);
debug('got vURL info successfully:'+JSON.stringify(data));
} else {
// return error info
self.emit('vurlinfo'+data.evcbcnt, {err: 'get vURL info failed '+(data.answer.error ? data.answer.error : '')});
console.error('get vURL info failed:'+JSON.stringify(data));
}
break;
// vURL management opc <- ///////////////////////////////////////
default:
console.error('unknown opc:'+JSON.stringify(data));
break;
}
} else {
console.error('unknown message:'+JSON.stringify(data));
}
}
// 1.
// start the first connection on random port,then waiting alternate connection
// with 10s timeout.
var wsproto = self.secmode ? 'wss://' : 'ws://';
// !!! Notes: for reconnection, use existing port
var conn = null;
if (self.reconnect) {
conn = new WebSocket(wsproto+self.srvs[0].ip+':'+self.srvs[0].port+SEP.SEP_CTRLPATH_NS,
{
httpp: true, hole: {addr: self.ipaddr, port: self.port},
// SSL related info
rejectUnauthorized: true,
ca: vCA
});
} else {
conn = new WebSocket(wsproto+self.srvs[0].ip+':'+self.srvs[0].port+SEP.SEP_CTRLPATH_NS,
{
httpp: true, hole: {addr: self.ipaddr},
// SSL related info
rejectUnauthorized: true,
ca: vCA
});
}
// initialize offer message count per client
// every time, client send one offer message, increase it by one
conn.offerMsgcnt = 0;
conn.nmsrv = self.srvs[0];
self.conn[JSON.stringify(self.srvs[0])] = {socket: conn, to: self.srvs[0]};
var t = setTimeout(function(){
self.state = 'timeout';
self.emit('timeout', 'connection timeout');
// close all connection
try {
for (var k in self.conn) {
if (self.conn[k] && self.conn[k].socket && self.conn[k].socket.close) {
self.conn[k].socket.close();
}
}
} catch (e) {
console.error('clear all sockets, ignore '+e);
}
}, (self.srvinfo.timeout || 20)*1000); // 20s timeout in default
conn.once('open', function(){
debug('connection to the first name-server');
// increase opened client count
self.ocnt ++;
// 2.
// update state to connecting
self.state = 'connecting';
// 3.
// record binding on port/ip/fd, then start alternate connections
self.fd = conn.address().fd;
self.port = conn.address().port;
debug('nmclnt binding on %s:%d with fd:%d', self.ipaddr, self.port, self.fd);
for (var i = 1; i < self.srvs.length; i ++) {
var connalt = new WebSocket(wsproto+self.srvs[i].ip+':'+self.srvs[i].port+SEP.SEP_CTRLPATH_NS,
{
httpp: true, hole: {port: self.port, addr: self.ipaddr},
// SSL related info
rejectUnauthorized: true,
ca: vCA
});
// initialize offer message count per client
// every time, client send one offer message, increase it by one
connalt.offerMsgcnt = 0;
connalt.nmsrv = self.srvs[i];
self.conn[JSON.stringify(self.srvs[i])] = {socket: connalt, to: self.srvs[i]};
// 4.
// all connection ready, then emit nmCln ready event
connalt.once('open', function(){
// increase opened connection count
self.ocnt++;
if (self.ocnt === self.srvs.length) {
clearTimeout(t);
// 5.
// emit connected event immediately
self.state = 'connected';
// 5.1
// connect TURN agent server
if (self.turnSrvs) {
self._LSM_connectTurnAgent(function(err, yes){
if (!err && yes) {
self.turnagentReady = true;
} else {
self.turnagentReady = false;
}
self.emit('connected');
});
} else {
self.emit('connected');
}
}
});
// 5.
// on message
connalt.on('message', onMessage);
// 6.
// handle close event with reconnect
connalt.once('close', function(){
debug('name-server alternate client close');
// 6
// trigger reconnect event
if (self.state === 'ready') {
// 6.1
// check if connections still broken
var nmconlive = true;
for (var nmk in self.conn) {
if (!self.conn.hasOwnProperty(nmk)) continue;
var nmcon = self.conn[nmk] && self.conn[nmk].socket;
if (!(nmcon && (nmcon.readyState == nmcon.OPEN))) {
nmconlive = false;
break;
}
}
if (!nmconlive) {
debug('alternate client reconnect tri