node-unifiapi
Version:
Provides API to manage Ubiquiti Unifi Controller, ver 4 and 5
451 lines (411 loc) • 16.9 kB
JavaScript
//let wrtc = require('electron-webrtc')({ headless: true });
//let wrtc = undefined;
let debug = require('debug')('WebRTCRequest');
let merge = require('merge');
let pako = require('pako');
let wait = require('./wait');
let defaultOptions = {
waiter: 10,
webrtc: undefined, // wrtc or electron-webrtc
messageId: 1
};
class WRTC {
constructor(options) {
merge(this, defaultOptions, options);
this._q = {};
this._channel = {};
this.log = require('debug')(this.debugName || 'WRTCRequest');
if (this.debug) this.log.enabled = true;
if (this.webrtc && this.webrtc.on) {
this.webrtc.on('error', error => {
console.log('ERROR', error);
});
} else {
console.log('WebRTC module is not provided as parameter! As a result, all WebRTC operations will be disabled!');
}
}
debugging(enabled) {
this.debug = enabled;
this.log.enabled = this.debug ? true : false;
this.log('Debug is', this.debug ? 'Enabled' : 'Disabled');
}
registerQ(q, fn) {
if (typeof this._q[q] !== 'object') this._q[q] = [];
if (this._q[q].indexOf(fn) < 0) this._q[q].push(fn);
}
deregisterQ(q, fn) {
if (this._q[q].indexOf(fn) >= 0) this._q[q].splice(this._q[q].indexOf(fn), 1);
}
fireQ(q, msg) {
if (this._q[q]) this._q[q].forEach(n => n(msg));
}
dropQ(q) {
delete this._q[q];
}
RTCPeerConnection(options, conditions) {
if (!this.webrtc) {
return console.log('WebRTC has not been provided as parameter! This operation is ignored!');
}
this.log('WRTC_PEER_OPEN', options, conditions);
this.peer = new this.webrtc.RTCPeerConnection(options, conditions);
[
'onicecandidate', 'onsignalingstatechange', 'oniceconnectionstatechange',
'onicegatheringstatechange', 'ondatachannel', 'onnegotiationneeded',
'onaddstream'
].forEach((n) => {
this.peer[n] = (event) => {
this.log(n, event);
this.fireQ(n, event);
};
});
this._icecandidates = [];
this.registerQ('onicecandidate', (candidate) => {
this._icecandidates.push(candidate ? candidate.candidate || null : null);
});
this.registerQ('onsignalingstatechange', () => {
this.log('SIGNALING STATE', this.peer.signalingState);
});
this.registerQ('onicegatheringstatechange', () => {
this.log('ICEGATHERING STATE', this.peer.iceGatheringState);
});
this.registerQ('oniceconnectionstatechange', () => {
this.log('ICECONNECTION STATE', this.peer.iceConnectionState);
});
this.registerQ('ondatachannel', (event) => {
this.log('We have channel open', event.channel);
});
}
setCallback(event, fn) {
var c = (msg) => {
fn(msg);
this.deregisterQ(event, c);
};
this.registerQ(event, c);
}
setLocalDescription(desc) {
return new Promise((resolve, reject) => {
if (!this.webrtc) {
console.log('WebRTC has not been provided! This operation is ignored!');
return reject('No WebRTC module provided!');
}
this.log('WEBRTC_SET_LOCALDESCR', desc);
wait(this.waiter).then(() => {
this.peer.setLocalDescription(
new this.webrtc.RTCSessionDescription(desc),
(data) => {
resolve(data || desc); // Data is always null
},
reject
);
});
});
}
setRemoteDescription(desc) {
return new Promise((resolve, reject) => {
if (!this.webrtc) {
console.log('WebRTC has not been provided! This operation is ignored!');
return reject('No WebRTC module provided!');
}
this.log('WEBRTC_SET_REMOTEDESC', desc);
let w = new this.webrtc.RTCSessionDescription(desc);
wait(this.waiter).then(() => {
this.peer.setRemoteDescription(
w,
(data) => { // Create Answer
resolve(data /*|| desc*/ );
},
reject
);
});
});
}
createAnswer( /*desc*/ ) {
return new Promise((resolve, reject) => {
this.log('WEBRTC_CREATE_ANSWER' /*, desc*/ );
wait(this.waiter).then(() => {
this.peer.createAnswer(
(data) => { // Set my local description
resolve(data /*|| desc*/ );
},
reject
);
});
});
}
createOffer(config) {
return new Promise((resolve, reject) => {
this.log('WEBRTC_CREATE_OFFER');
wait(this.waiter).then(() => {
this.peer.createOffer(resolve, reject, config);
});
});
}
waitForIceCandidates(data, timeout) {
timeout = timeout || 30;
let d = new Date();
return new Promise((resolve, reject) => {
this.log('WEBRTC_WAIT_ICECANDIDATES', data);
let test = () => {
if (this._icecandidates.indexOf(null) >= 0) return resolve(this._icecandidates);
if (new Date() - d > timeout * 1000) return reject();
setTimeout(test, 100); // Retry again in 1 second
};
test();
});
}
collectIceCandidates(data) {
return new Promise((resolve, reject) => {
// Here we have to implement waiting for iceServers
this.log('WEBRTC_WAIT_ICECANDIDATES', data);
let sdp = data.sdp.replace(/\r/, '');
this.registerQ('onicecandidate', (candidate) => {
this.log('Ive got', candidate, sdp);
//if (candidate && candidate.candidate && candidate.candidate.candidate) candidate = candidate.candidate;
if (candidate === null || candidate.candidate === null ||
(typeof candidate == 'object' && typeof candidate.candidate == 'undefined')) {
this.log('Candidate is empty, terminate the gathering');
data.sdp = sdp;
return resolve(data);
} // TODO: implement reject
if (candidate && candidate.candidate) {
let cand = candidate.candidate;
// We have to add this candidate to the data (SDP)
if (cand.candidate.match(/tcp/)) return; // Ignore TCP candidates
//if (cand.candidate.match(/fd13/)) return; // Ignore IPv6 for the moment
sdp = sdp.replace(/\r\n/g, '\n');
for (var b, c, d = sdp, e = 0, f = 0, g = /m=[^\n]*\n/g; null !== (b = g.exec(d));) {
if (f === cand.sdpMLineIndex) {
return c = b.index,
b = g.exec(d),
e = null !== b ? b.index : -1,
e >= 0 ? sdp = [d.slice(0, e), "a=" + cand.candidate + "\n", d.slice(e)].join("") : sdp = d + "a=" + cand.candidate + "\n",
sdp;
}
f++;
}
}
});
});
}
addIcePeer(peer) {
if (peer) this.peer.addIceCandidate(peer);
}
addIcePeers(list) {
return new Promise((resolve, reject) => {
this.log('Add ICE peers');
list.forEach((n) => this.addIcePeer(n));
resolve(list);
});
}
openDataChannel(name, half) {
return new Promise((resolve, reject) => {
this.log('WEBRTC_OPEN_CHANNEL', name);
let channel = this.peer.createDataChannel(name, { reliable: true });
this._channel[name] = channel;
let me = this;
function clean() {
me.deregisterQ(name + '_onopen', onopen);
me.deregisterQ('iceconnectionstatechange', iceconn);
}
function myResolve(data) {
clean();
resolve(data);
}
function myReject(data) {
clean();
reject(data);
}
function iceconn() {
if (me.peer.iceConnectionState == 'failed' ||
me.peer.iceConnectionState == 'disconnected') {
me.log('Cannot open ICE, reject');
myReject('Failed to open ICE');
}
}
this.registerQ('iceconnectionstatechange', iceconn);
function onopen(event) {
me.log('CHANNEL IS OPEN', name, event, channel.readyState);
if (channel.readyState === "open") {
me.log('Channel is open', name);
if (half) resolve(channel);
}
}
this.registerQ(name + '_onopen', onopen);
['onopen', 'onmessage', 'onerror', 'onclose'].forEach((n) => {
channel[n] = (event) => {
this.log('CHANNEL', name, n, event);
this.fireQ(name + '_' + n, event);
};
});
if (!half) myResolve(channel); // If half is not clear, wait
});
}
buildApiMessage(id, uri, content) {
this.log('BUILD Message', uri, content);
let data = Object.assign({}, content); // Ensure we use a copy
if (uri.match(/^\/upload/)) {
data.data = null; // do I need it?
delete data.data;
}
let method = data.type || data.method;
let obj = {
path: uri,
method: method ? method.toUpperCase() : "GET",
contentType: content.contentType,
'Accept-Encoding': 'gzip'
};
if (uri.indexOf("?") >= 0) {
obj.path = uri.split("?")[0];
obj.queryString = uri.split("?")[1];
}
if (method == 'GET' || method == 'DELETE') {
if (content.data) obj.queryString = content.data; // Encoding check
return this.encodeApiMessage(id, obj);
} else {
return this.encodeApiMessage(id, obj, content.data);
}
}
convertNumToUint8(num, bytes) {
let a = new ArrayBuffer(bytes);
let u = new Uint8Array(a);
for (bytes--; num; bytes--) {
u[bytes] = num % 256;
num = parseInt(num / 256);
}
return u;
}
convertUint8ToNum(u, start, count) {
let o = 0;
for (let i = 0; i < count; i++) o = o * 256 + u[start + i];
return o;
}
encodeApiMessage(id, req, data) { // TODO: implement multiple chunks
// Format, 16 bytes in front of the message
// 4 bytes total length, 8 bytes ID, 4 bytes request length, charcode
this.log('Encode Message', id, req, data);
let reqS = JSON.stringify(req);
let reqSLen = reqS.length;
data = data ? data : "";
let dataS = "";
let dataSLen;
if (typeof data == 'string') {
dataS = data;
dataSLen = dataS.length;
}
if (typeof data == 'object') {
dataS = JSON.stringify(data);
dataSLen = dataS.length;
}
if (data instanceof ArrayBuffer) {
dataSLen = data.byteLength;
}
let a = new ArrayBuffer(16 + reqSLen + dataSLen);
let u = new Uint8Array(a);
let totalLen = 12 + reqSLen + dataSLen; // Total len without itself
u.set(this.convertNumToUint8(totalLen, 4), 0); // Data have to be fixed
u.set(this.convertNumToUint8(id, 8), 4);
u.set(this.convertNumToUint8(reqSLen, 4), 12);
let i;
for (i = 0; i < reqSLen; i++) u[16 + i] = reqS.charCodeAt(i);
//this.log('Encoded', u);
if (data instanceof ArrayBuffer) {
let uu = new Uint8Array(data);
for (let j = 0; j < uu.byteLength; j++) u[16 + i++] = data[j];
} else {
for (let j = 0; j < dataSLen; j++) u[16 + i++] = dataS.charCodeAt(j);
}
return a;
}
sendMsgToChannel(name, msg) {
return new Promise((resolve, reject) => {
if (typeof this._channel[name] === 'undefined') return reject('No such channel');
this._channel[name].send(msg);
resolve(this._channel[name]);
});
}
sendApiMsg(uri, content, nowait, channelName, timeout) {
if (typeof channelName == 'undefined') channelName = 'api';
if (typeof timeout == 'undefined') timeout = 30000; // Wait for no more than 30 sec for response
let id = this.messageId++;
let channel = this._channel[channelName];
if (nowait)
return this.sendMsgToChannel(channelName, this.buildApiMessage(id, uri, content));
return new Promise((resolve, reject) => {
let kill;
if (typeof channel == 'undefined') return reject('No such channel');
channel._queue[id] = (event) => {
clearTimeout(kill);
resolve(event);
};
let wait = () => {
if (timeout < 0) return reject('Timeout');
if (channel.readyState != 'open') {
this.log('The channel', channelName, 'is not yet open. Wait...');
timeout -= 1000;
return setTimeout(wait, 1000);
}
this.sendMsgToChannel(channelName, this.buildApiMessage(id, uri, content))
.then(() => {
kill = setTimeout(() => {
delete channel._queue[id];
reject('Timeout');
}, timeout);
}) // the message is sent, wait for reply
.catch(reject);
};
wait();
});
}
openApiChannel() {
return new Promise((resolve, reject) => {
this.openDataChannel('api').then((channel) => {
channel._queue = {};
this.registerQ('api_onmessage', (event) => {
if (event.data && event.data instanceof ArrayBuffer) {
let data = new Uint8Array(event.data);
let totalLen = this.convertUint8ToNum(data, 0, 4);
let id = this.convertUint8ToNum(data, 4, 8);
let reqLen = this.convertUint8ToNum(data, 12, 4);
let obj = {
id: id,
totalLen: totalLen,
reqLen: reqLen,
request: "",
data: ""
};
let i = 0;
for (i = 0; i < reqLen; i++)
obj.request += String.fromCharCode(data[16 + i]);
obj.request = JSON.parse(obj.request);
let content = data.slice(16 + i, totalLen + 4);
if (obj.request['Content-Encoding'] == 'gzip') {
this.log('The message is GZIP compressed, decompress');
content = pako.inflate(content);
}
if (obj.request.contentType == 'application/octet-stream') {
obj.data = content;
} else {
if (obj.request.contentType == 'application/json') {
for (i = 0; i < content.byteLength; i++)
obj.data += String.fromCharCode(content[i]);
if (obj.data) obj.data = JSON.parse(obj.data);
} else {
obj.data = new Blob([content], obj.request.contentType);
}
}
this.log('API ONMESSAGE', obj);
if (channel._queue[obj.id]) {
channel._queue[obj.id](obj);
delete channel._queue[obj.id]; // free memory
}
}
});
resolve(channel);
}).catch(reject);
});
}
close() {
this.peer.close();
}
}
module.exports = WRTC;