librecast-auth
Version:
Library to access Librecast authentication over multicast from the browser
404 lines (361 loc) • 11.6 kB
JavaScript
/*
* librecast.js - librecast helper functions
*
* this file is part of LIBRECAST
*
* Copyright (c) 2017-2021 Brett Sheffield <brett@librecast.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program (see the file COPYING in the distribution).
* If not, see <http://www.gnu.org/licenses/>.
*/
const UINT32_MAX = 4294967295;
/*
* librecast.js - librecast helper functions
*
* this file is part of LIBRECAST
*
* Copyright (c) 2017-2021 Brett Sheffield <brett@librecast.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program (see the file COPYING in the distribution).
* If not, see <http://www.gnu.org/licenses/>.
*/
const LIBRECAST = (function () {
"use strict";
var lc = {};
// default op callback timeout in ms
lc.DEFAULT_TIMEOUT = 5000;
lc.NO_TIMEOUT = -1;
lc.WS_CONNECTING = 0;
lc.WS_OPEN = 1;
lc.WS_CLOSING = 2;
lc.WS_CLOSED = 3;
lc.OP_NOOP = 0x01;
lc.OP_SETOPT = 0x02;
lc.OP_SOCKET_NEW = 0x03;
lc.OP_SOCKET_GETOPT = 0x04;
lc.OP_SOCKET_SETOPT = 0x05;
lc.OP_SOCKET_LISTEN = 0x06;
lc.OP_SOCKET_IGNORE = 0x07;
lc.OP_SOCKET_CLOSE = 0x08;
lc.OP_SOCKET_MSG = 0x09;
lc.OP_CHANNEL_NEW = 0x0a;
lc.OP_CHANNEL_GETMSG = 0x0b;
lc.OP_CHANNEL_GETOPT = 0x0c;
lc.OP_CHANNEL_SETOPT = 0x0d;
lc.OP_CHANNEL_GETVAL = 0x0e;
lc.OP_CHANNEL_SETVAL = 0x0f;
lc.OP_CHANNEL_BIND = 0x10;
lc.OP_CHANNEL_UNBIND = 0x11;
lc.OP_CHANNEL_JOIN = 0x12;
lc.OP_CHANNEL_PART = 0x13;
lc.OP_CHANNEL_SEND = 0x14;
lc.HEADER_LENGTH = 25;
lc.ERR_SUCCESS = 0;
lc.ERR_FAILURE = 1;
lc.ERR_WEBSOCKET_UNSUPPORTED = 2;
lc.ERR_WEBSOCKET_NOTREADY = 3;
lc.ERR_CALLBACK_NOT_FUNCTION = 4;
lc.ERR_MISSING_ARG = 5;
lc.ErrorMsg = {};
lc.ErrorMsg[lc.ERR_SUCCESS] = "Success";
lc.ErrorMsg[lc.ERR_FAILURE] = "Failure";
lc.ErrorMsg[lc.ERR_WEBSOCKET_UNSUPPORTED] = "Browser does not support websockets";
lc.ErrorMsg[lc.ERR_WEBSOCKET_NOTREADY] = "Websocket not ready";
lc.ErrorMsg[lc.ERR_CALLBACK_NOT_FUNCTION] = "Callback not a function";
lc.ErrorMsg[lc.ERR_MISSING_ARGUMENT] = "Required argument is missing";
function LibrecastException(errorCode) {
this.code = errorCode;
this.name = lc.ErrorMsg[errorCode];
this.errormsg = "ERROR (" + this.code + ") " + this.name;
}
lc.Message = class {
constructor(data) {
this.opcode = lc.OP_NOOP;
this.data = data;
if (data instanceof ArrayBuffer) {
this.len = data.byteLength;
}
else {
this.len = (this.data === undefined) ? 0 : data.length;
}
this.id = 0;
this.id2 = 0;
this.token = 0;
};
get utf8() {
if (this.data !== undefined) {
const sv = new StringView(this.data, "UTF-8", lc.HEADER_LENGTH, this.len);
return sv.toString();
}
}
};
//
// Librecast.Context -----------------------------------------------------------
//
lc.Context = class {
constructor() {
console.log("Librecast context constructor");
this.url = (location.protocol == 'https:') ? "wss://" : "ws://";
this.url += document.location.host + "/";
this.callstack = [];
this.onconnect = new Promise(resolve => { this.resolveconnect = resolve; });
this.connect();
};
connect() {
console.log("Librecast.connect()");
if (window.WebSocket) {
console.log("websockets supported");
}
else {
console.log("websockets unsupported");
throw new LibrecastException(lc.ERR_WEBSOCKET_UNSUPPORTED);
}
this.websocket = new WebSocket(this.url, "librecast");
this.websocket.binaryType = 'arraybuffer';
this.websocket.onclose = (e) => { this.wsClose(e); };
this.websocket.onerror = (e) => { this.wsError(e); };
this.websocket.onmessage = (e) => { this.wsMessage(e); };
this.websocket.onopen = (e) => { this.wsOpen(e); };
};
close() {
console.log("Librecast close()");
this.websocket.close();
};
get token() {
return Math.floor(Math.random() * UINT32_MAX);
};
callback(resolve, reject, timeout, repeat) {
const token = this.token;
const cb = {};
cb.resolve = resolve;
cb.reject = reject;
cb.created = Date.now();
cb.repeat = repeat;
this.callstack[token] = cb;
console.log("callback created with token = " + token);
if (timeout !== lc.NO_TIMEOUT) {
if (timeout === undefined) { timeout = lc.DEFAULT_TIMEOUT; };
console.log("setting callback timer " + token);
cb.timeout = setTimeout( () => {
reject("callback (" + token + ") timeout");
delete this.callstack[token];
}, timeout);
}
return token;
};
cancelCallback(token) {
console.log("cancelling callback token " + token);
if (this.callstack[token] !== undefined) {
if (this.callstack[token].timeout !== undefined) {
clearTimeout(this.callstack[token].timeout);
}
}
delete this.callstack[token];
}
send(msg) {
let buffer, dataview, idx;
buffer = new ArrayBuffer(lc.HEADER_LENGTH + msg.len * 4);
dataview = new DataView(buffer);
if (msg.data !== undefined && msg.len > 0) {
if (typeof msg.data === 'object') {
// copy ArrayBuffer into new buffer with space for header data
idx = lc.HEADER_LENGTH + msg.len;
const tmp = new Uint8Array(idx);
tmp.set(new Uint8Array(msg.data), lc.HEADER_LENGTH);
buffer = tmp.buffer;
dataview = new DataView(buffer);
}
else {
// string data, convert to UTF-8
idx = util.convertUTF16toUTF8(lc.HEADER_LENGTH, msg.data, msg.len, dataview);
}
}
// write headers
dataview.setUint8(0, msg.opcode);
dataview.setUint32(1, idx - lc.HEADER_LENGTH);
dataview.setUint32(5, msg.id);
dataview.setUint32(9, msg.id2);
dataview.setUint32(13, msg.token);
this.websocket.send(buffer);
};
wsClose(e) {
console.log("websocket close: (" + e.code + ") " + e.reason);
console.log("websocket.readyState: " + this.websocket.readyState);
console.log("reinitializing websocket");
this.connect();
};
wsError(e) {
console.log("websocket error" + e.message);
console.log("websocket.readyState: " + this.websocket.readyState);
};
wsMessage(msg) {
console.log("websocket message received (type=" + msg.type +")");
if (typeof(msg) === 'object' && msg.data instanceof ArrayBuffer) {
const dataview = new DataView(msg.data);
const cmsg = new lc.Message(msg.data);
cmsg.opcode = dataview.getUint8(0);
cmsg.len = dataview.getUint32(1);
cmsg.id = dataview.getUint32(5);
cmsg.id2 = dataview.getUint32(9);
cmsg.token = dataview.getUint32(13);
cmsg.payload = msg.data.slice(lc.HEADER_LENGTH);
console.log("opcode: " + cmsg.opcode);
console.log("len: " + cmsg.len);
console.log("id: " + cmsg.id);
console.log("id2: " + cmsg.id2);
console.log("token: " + cmsg.token);
if (this.callstack[cmsg.token] !== undefined) {
this.callstack[cmsg.token].updated = Date.now()
cmsg.sent = this.callstack[cmsg.token].created;
cmsg.recv = this.callstack[cmsg.token].updated;
cmsg.delay = cmsg.recv - cmsg.sent;
console.log("message reponse took " + cmsg.delay + " ms");
if (this.callstack[cmsg.token].timeout !== undefined) {
console.log("clearing callback timer " + cmsg.token);
clearTimeout(this.callstack[cmsg.token].timeout);
this.callstack[cmsg.token].timeout = undefined;
}
if (this.callstack[cmsg.token].resolve !== undefined) {
this.callstack[cmsg.token].resolve(cmsg);
if (this.callstack[cmsg.token] !== undefined) {
if (!this.callstack[cmsg.token].repeat) {
this.cancelCallback(cmsg.token);
}
}
}
}
}
}
wsOpen(e) {
console.log("websocket open");
console.log("websocket.readyState: " + this.websocket.readyState);
this.resolveconnect();
};
};
lc.Socket = class {
constructor(lctx) {
console.log("Socket constructor");
if (lctx === undefined) throw new Error("Librecast.Context required");
this.lctx = lctx;
this.id = undefined;
this.oncreate = new Promise((resolve, reject) => {
const msg = new lc.Message();
msg.opcode = lc.OP_SOCKET_NEW;
msg.token = this.lctx.callback(resolve, reject);
this.lctx.send(msg);
}).
then((msg) => {
this.id = msg.id;
});
};
op(opcode, data, timeout, callback) {
return new Promise((resolve, reject) => {
if (this.lctx.websocket.readyState == lc.WS_OPEN) {
const msg = new lc.Message(data);
msg.opcode = opcode;
msg.id = this.id;
if (callback !== false) {
msg.token = this.lctx.callback(resolve, reject, timeout);
}
this.lctx.send(msg);
}
else {
reject(LibrecastException(lc.ERR_WEBSOCKET_NOTREADY));
}
});
}
close() {
this.lctx.cancelCallback(this.token);
return this.op(lc.OP_SOCKET_CLOSE, undefined, undefined, false);
}
listen(onmessage, onerror) {
console.log("listening on socket " + this.id);
if (this.lctx.websocket.readyState == lc.WS_OPEN) {
const msg = new lc.Message();
msg.opcode = lc.OP_SOCKET_LISTEN;
msg.id = this.id;
msg.token = this.lctx.callback(onmessage, onerror, lc.NO_TIMEOUT, true);
this.token = msg.token;
this.lctx.send(msg);
}
else {
reject(LibrecastException(lc.ERR_WEBSOCKET_NOTREADY));
}
}
};
lc.Channel = class {
constructor(lctx, channelName) {
console.log("Channel constructor");
if (lctx === undefined) throw new Error("Librecast.Context required");
this.lctx = lctx;
this.id = undefined;
this.id2 = undefined;
this.name = channelName;
this.oncreate = new Promise((resolve, reject) => {
const msg = new lc.Message(channelName);
msg.opcode = lc.OP_CHANNEL_NEW;
if (msg.len == 0) { reject("channel name required"); };
msg.token = this.lctx.callback(resolve, reject);
this.lctx.send(msg);
})
.then((msg) => {
this.id = msg.id;
});
};
op(opcode, data, timeout) {
return new Promise((resolve, reject) => {
if (this.lctx.websocket.readyState == lc.WS_OPEN) {
const msg = new lc.Message(data);
msg.opcode = opcode;
msg.id = this.id;
msg.id2 = this.id2;
msg.token = this.lctx.callback(resolve, reject, timeout);
console.log("opcode = " + opcode + ", token = " + msg.token);
this.lctx.send(msg);
}
else {
reject(LibrecastException(lc.ERR_WEBSOCKET_NOTREADY));
}
});
}
bind(sock) {
console.log("binding channel " + this.name + "(" + this.id + ") to socket " + sock.id);
this.id2 = sock.id;
return this.op(lc.OP_CHANNEL_BIND);
}
join() {
console.log('joining channel "' + this.name + '"');
return this.op(lc.OP_CHANNEL_JOIN);
}
part() {
console.log('parting channel "' + this.name + '"');
return this.op(lc.OP_CHANNEL_PART);
}
send(data) {
return this.op(lc.OP_CHANNEL_SEND, data, lc.NO_TIMEOUT);
}
};
return lc;
}());