UNPKG

iwebpp.io

Version:

iWebPP name-client library for node.js

1,169 lines (991 loc) 119 kB
// 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