UNPKG

node-unifiapi

Version:

Provides API to manage Ubiquiti Unifi Controller, ver 4 and 5

292 lines (274 loc) 11.5 kB
let wrtc = require('./webrtc-request'); let Uuid = require('uuid'); let debug = require('debug')('Unifi SSH'); function SSHSession(unifi, mac, uuid, stun, turn, username, password, site, autoclose, webrtc, waiter) { this.unifi = unifi; this.mac = mac; this.uuid = uuid || Uuid.v4(); this.stun = stun; this.turn = turn; this.username = username; this.password = password; this.site = site; this.channel = undefined; this.debug = this.unifi.debug; debug.enabled = this.debug; this.buffer = ""; this.status = "closed"; this.inClosing = false; this.autoclose = autoclose || 30000; this.connectTimeout = 15000; this.webrtc = webrtc; this.waiter = waiter; this._q = {}; this._qqq = { connect: [] }; } /** * Enable or disable debugging * @param {boolean} enabled Enable or Disable debugging * @return {undefined} */ SSHSession.prototype.debugging = function(enabled) { this.debug = enabled; debug.enabled = this.debug ? true : false; debug('Debug is', this.debug ? 'enabled' : 'disabled'); }; /** * @param {number} connectTimeout How much time to wait for connection. Default 15000ms * @param {Function} closeCallBack Function which will be called in case of close * @return {Promise} */ SSHSession.prototype.connect = function(connectTimeout, closeCallBack) { return new Promise((resolve, reject) => { let timeoutChannel = null; let me = this; function myResolve(data) { clearTimeout(timeoutChannel); me._qqq.connect.forEach(n => n.resolve(data)); me._qqq.connect = []; } function myReject(data) { clearTimeout(timeoutChannel); me._qqq.connect.forEach(n => n.reject(data)); me._qqq.connect = []; if (typeof closeCallBack == 'function') closeCallBack(data); } if (this.status == "open") return resolve(this); this._qqq.connect.push({ resolve: resolve, reject: reject }); if (this._qqq.connect.length > 1) return; // Something is waiting this.inClosing = false; let firstCall; if (this.stun || this.turn) { firstCall = this.unifi.buildSSHSession(this.mac, this.uuid, "-1", this.stun, this.turn, this.username || '', this.password || '', this.site); } else { firstCall = this.unifi.getTurnCredentials().then((data) => { debug('Turn credentials are', data && data.data ? data.data : data); if (data && data.data) { let d = data.data.shift() || {}; if (d.uris) { this.stun = d.uris.filter(n => n.match(/stun:/)).map(n => n.replace(/\?.*/, '')); this.turn = d.uris.filter(n => n.match(/turn:/)).map(n => n.replace(/\?.*/, '')); this.username = d.username; this.password = d.password; } } return this.unifi.buildSSHSession(this.mac, this.uuid, "-1", (this.stun instanceof Array && this.stun[0] ? this.stun[0].replace(/stun:/, '') : this.stun), (this.turn instanceof Array && this.turn[0] ? this.turn[0].replace(/turn:/, '') : this.turn), this.username, this.password, this.site); }); } firstCall .then(() => { let o = { debug: this.debug }; if (this.waiter) o.waiter = this.waiter; if (this.webrtc) o.webrtc = this.webrtc; this.wrtc = new wrtc(o); debug('Will open peer connection with stun', this.stun, 'turn', this.turn); this.wrtc.RTCPeerConnection({ iceServers: [{ urls: this.stun, url: this.stun }, { urls: this.turn, url: this.turn, username: this.username, credential: this.password } ] }, { optional: [ { DtlsSrtpKeyAgreement: true }, { RtpDataChannels: true } ] }); // ICE Servers let connStateChange = () => { debug('CAREFUL, Connection state changed'); let state = this.wrtc.peer.iceConnectionState; if (state == 'disconnected' || state == 'failed') { debug('We are notified for session disconnection'); let rej = this.state != "open"; this.close(); if (typeof closeCallBack == 'function') closeCallBack(state); if (rej) myReject('SSH Connection fail'); } if ([ /*'open',*/ 'failed', 'error', 'disconnected', /*'connected'*/ ].indexOf(state) < 0) { // Unless we have a termination, we need to check again the state debug('Connection state will be checked again'); this.wrtc.setCallback('oniceconnectionstatechange', connStateChange); } }; this.wrtc.setCallback('oniceconnectionstatechange', connStateChange); this.wrtc.setCallback('ondatachannel', (event) => { debug('GREAT, we have the session channel', event.channel); this.channel = event.channel; this.channel.onopen = () => { debug('SSH session is open'); this.status = "open"; clearTimeout(timeoutChannel); this.fireQ('onopen'); this.touchAutoClose(); myResolve(this); }; this.channel.onclose = () => { debug('SSH session is closed'); this.fireQ('onclose'); }; this.channel.onmessage = (event) => { let u = new Uint8Array(event.data); let s = ""; for (let i = 0; i < u.byteLength; i++) s += String.fromCharCode(u[i]); debug('SSHChannel message', s); this.buffer += s; this.fireQ('onmessage', event); }; }); return this.unifi.getSDPOffer(this.mac, this.uuid, this.site); }) .then((data) => { let sdpOffer = data.data.shift().ssh_sdp_offer; debug('SSH SDP Offer is', sdpOffer); return this.wrtc.setRemoteDescription({ type: 'offer', sdp: sdpOffer }); }) .then((data) => { return this.wrtc.createAnswer(data); }) .then((data) => { return this.wrtc.setLocalDescription(data); }) .then((sdpData) => { return this.wrtc.collectIceCandidates(sdpData); }) .then((data) => { debug('LocalData to send', data); let sdp = data.sdp; let line = sdp .match(/^a=candidate:.+udp\s(\d+).+$/mig); debug('line', line); line = line .sort((a, b) => { let x = a.match(/udp\s+(\d+)\s/)[1]; let y = b.match(/udp\s+(\d+)\s/)[1]; return x > y; }).shift(); let ip = line.match(/udp\s+\d+\s+(\S+)\s/)[1]; return this.unifi.sshSDPAnswer(this.mac, this.uuid, /*sdp.replace("c=IN IP4 0.0.0.0", "c=IN IP4 " + ip) */ sdp, this.site); }) // .then((data) => { // return this.wrtc.openDataChannel('ssh'); // }) .then((data) => { debug('Channel is supposed to be open now. Lets wait'); timeoutChannel = setTimeout(() => { debug('Timeout has passed without response, the channel is not open'); this.unifi.closeSSHSession(this.mac, this.uuid, this.site) .then(() => { myReject('WebRTC Session Timeout'); }) .catch(myReject); }, connectTimeout || this.connectTimeout || 15000); }) .catch(myReject); }); }; SSHSession.prototype.touchAutoClose = function() { if (this._autoclose) clearTimeout(this._autoclose); if (this.autoclose) { this._autoclose = setTimeout(() => { debug('Closing due no activity'); this.close(); }, this.autoclose); } }; SSHSession.prototype.registerQ = function(q, fn) { if (typeof this._q[q] !== 'object') this._q[q] = []; if (this._q[q].indexOf(fn) < 0) this._q[q].push(fn); }; SSHSession.prototype.deregisterQ = function(q, fn) { if (this._q[q].indexOf(fn) >= 0) this._q[q].splice(this._q[q].indexOf(fn), 1); }; SSHSession.prototype.dropQ = function(q) { delete this._q[q]; }; SSHSession.prototype.fireQ = function(q, msg) { if (this._q[q]) this._q[q].forEach((n) => n(msg)); }; SSHSession.prototype.send = function(msg) { this.touchAutoClose(); debug('send:', msg); this.channel.send(msg); }; SSHSession.prototype.recv = function() { this.touchAutoClose(); let buf = this.buffer; this.buffer = ""; //debug('recv:', buf); return buf; }; SSHSession.prototype.close = function() { this.status = "closed"; return new Promise((resolve, reject) => { if (this.inClosing) resolve(); this.inClosing = true; if (this._autoclose) clearTimeout(this._autoclose); this.unifi.closeSSHSession(this.mac, this.uuid, this.site) .then(resolve) .catch(reject); }); }; SSHSession.prototype.wait = function(timeout) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(this); }, timeout); }); }; SSHSession.prototype.expect = function(test, timeout, errormsg) { return new Promise((resolve, reject) => { test = test instanceof Array ? test.slice() : [test]; let tList = test.map(n => n instanceof RegExp ? n : new RegExp(n)); let c = null; timeout = timeout || 10000; errormsg = errormsg || 'timeout for expect'; debug('Expecting', tList, timeout, errormsg); let check = () => { let out = tList.filter(t => t.test(this.buffer)); if (out.length > 0) { clearTimeout(c); this.deregisterQ('onmessage', check); debug('Match found', out, 'in', this.buffer); this.lastBuff = this.recv(); this.lastMatch = out; resolve(this); // Clear the buffer } }; c = setTimeout(() => { this.deregisterQ('onmessage', check); reject(errormsg); }, timeout); this.registerQ('onmessage', check); }); }; module.exports = SSHSession;