224 lines
7.8 KiB
JavaScript
Executable File
224 lines
7.8 KiB
JavaScript
Executable File
'use strict'
|
|
// Remember : the whole app context is in another parallel & inacessible universe !
|
|
|
|
if(typeof(crypto.randomUUID)!='function'){
|
|
crypto.randomUUID = ()=>{ var buf = new Uint8Array(14);
|
|
crypto.getRandomValues(buf);
|
|
var uuid = Array.from(buf, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
|
return(uuid.substr(0,8)+'-'+uuid.substr(10,4)+'-'+uuid.substr(14,4)+'-'+uuid.substr(18,4)+'-'+uuid.substr(22));
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @author Nicolas Stein
|
|
* @category Core
|
|
* @subcategory Libraries
|
|
* @requires MessageBus
|
|
*/
|
|
class MessageBusWorker {
|
|
|
|
/**
|
|
*
|
|
* @param {*} config
|
|
* @param {*} userInfo
|
|
*/
|
|
constructor(config, userInfo){
|
|
this.config = config;
|
|
this.userInfo = userInfo;
|
|
this.wsurl = this.config.protocol+this.config.hostname;
|
|
if(('port' in this.config) && (this.config.port!='')) this.wsurl += ':'+this.config.port;
|
|
this.wsurl += this.config.path ;
|
|
this.keepAlive = true;
|
|
this.curReconnectTime = 0;
|
|
this.ConnectTimeout = null
|
|
this.token = false
|
|
this.stateMachine = 'DISCONNECTED'
|
|
this.noReconnect = false
|
|
// 'DISCONNECTED'
|
|
// -> 'LOGIN' (receive challenge & answer to it)
|
|
// -> 'READY' (received logged=true)
|
|
this.getToken()
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
getToken() {
|
|
if(!this.config.devotpToken){
|
|
fetch(this.config.tokenUrl+'?'+crypto.randomUUID(),{
|
|
credentials: 'include'
|
|
})
|
|
.then(response => response.json(), (err => {console.log('ERROR IN FETCH:',err)}))
|
|
.then(data => {
|
|
if(data.success && data.payload && data.payload.token) {
|
|
this.token = data.payload.token
|
|
if(this.config.debug && ''=='sensitive-even-for-debug') console.log(`Received Token : ${this.token}`)
|
|
this.connect();
|
|
} else {
|
|
console.warn('Could not get messagebus token !')
|
|
//TODO retry once in a while / integrate in the whole connect process
|
|
// to be part of retrials...
|
|
}
|
|
})
|
|
} else {
|
|
console.warn('!!! Using dev token for bus !!!')
|
|
this.token = this.config.devotpToken
|
|
this.connect();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
connect(){
|
|
this.socket = new WebSocket(this.wsurl);
|
|
this.ConnectTimeout = setTimeout(() => {
|
|
if((this.socket) && (close in this.socket)) this.socket.close(null);
|
|
}, this.config.connectTimeout*1000);
|
|
this.socket.onopen = this.WSonOpen.bind(this);
|
|
this.socket.onmessage = this.WSonMessage.bind(this);
|
|
this.socket.onclose = this.WSonClose.bind(this);
|
|
this.socket.onerror = this.WSonError.bind(this);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} data
|
|
*/
|
|
clientActionDispatch(data){
|
|
if(this.socket.readyState != 1) {
|
|
var state = [ 'Connecting', '', 'Closing', 'Closed'];
|
|
console.warn(`Attempt to send to ${state[this.socket.readyState]} Websocket !`);
|
|
return;
|
|
}
|
|
if(typeof(data)!='string') data=JSON.stringify(data);
|
|
this.socket.send(data);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} e
|
|
*/
|
|
WSonOpen(e){
|
|
this.stateMachine = 'LOGIN'
|
|
clearTimeout( this.ConnectTimeout);
|
|
console.log('Websocket connection established');
|
|
this.curReconnectTime = 0;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} challenge
|
|
*/
|
|
async login(challenge) {
|
|
let data = new TextEncoder().encode(this.token+challenge)
|
|
let bytesBuf = await crypto.subtle.digest("SHA-512", data)
|
|
let arrayBuf = Array.from(new Uint8Array(bytesBuf))
|
|
let response = arrayBuf.map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
if(this.config.debug && ''=='sensitive-even-for-debug') console.log(`Answering to challenge, with userinfo:`, response, this.userInfo)
|
|
this.clientActionDispatch({'action':'LOGIN', 'userInfo': this.userInfo , 'otp': response});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} e
|
|
*/
|
|
WSonMessage(e){
|
|
if(e.data.toLowerCase()=='unauthorized'){ // Do not spam if session is lost
|
|
this.noReconnect = true
|
|
if(this.config.debug) console.log(`Received MSG unauthorized !?`)
|
|
return;
|
|
}
|
|
|
|
// We're supposed to receive JSON only !
|
|
try{
|
|
var data = JSON.parse(e.data);
|
|
} catch(e){
|
|
console.warn('WSS: Received garbage :'+e.data);
|
|
return;
|
|
}
|
|
|
|
//if(this.config.debug) console.log(`Received MSG (in state:${this.stateMachine}) :`, data, this.stateMachine)
|
|
// LOGIN messages
|
|
if(this.stateMachine == 'LOGIN'){
|
|
if(data.action!='LOGIN') { // Non LOGIN messages in a LOGIN state are garbage
|
|
console.warn('WSS: Non-login message in a LOGIN state',data.action)
|
|
return
|
|
}
|
|
if(data.challenge) { // step1: challenge to reply
|
|
if(this.config.debug && ''=='sensitive-even-for-debug') console.log(`Got challenge ${data.challenge}...`)
|
|
this.login(data.challenge)
|
|
return
|
|
} else if(data.logged===true){ // step2 logged !
|
|
if(this.config.debug) console.log(`Logged !`)
|
|
this.stateMachine = 'READY'
|
|
postMessage({'event': 'connected' });
|
|
return
|
|
} else if(data.logged===false){ // step2 bad login !
|
|
this.noReconnect = true
|
|
console.warn('WSS-Login: challenge-response refused. (session lost?)')
|
|
return
|
|
}
|
|
}
|
|
|
|
if((data.action=='PING') && this.keepAlive){ // Keep Alive is managed here
|
|
this.clientActionDispatch({'action':'PONG'});
|
|
return
|
|
}
|
|
|
|
// All other messages are the upper-layer's business !
|
|
postMessage({'event': 'ReceiveFromServer', 'data':e.data});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} e
|
|
*/
|
|
WSonClose(e){
|
|
clearTimeout( this.ConnectTimeout);
|
|
console.warn(`Websocket connection has closed ! [${(new Date()).toISOString()}]`);
|
|
postMessage({'event': 'closed' });
|
|
this.socket.close();
|
|
if(this.noReconnect) return
|
|
|
|
var reconnectTime = parseFloat(this.config.autoReconnect);
|
|
var reconnectTimeFactor = parseFloat(this.config.autoReconnectTimeFactor);
|
|
var reconnectTimeMax = parseFloat(this.config.autoReconnectTimeMax);
|
|
var reconnectJitterPercent = parseFloat(this.config.autoReconnectJitterPercent);
|
|
if( (!isNaN(reconnectTime)) && (!isNaN(reconnectTimeFactor)) && (!isNaN(reconnectTimeMax)) && (!isNaN(reconnectJitterPercent)) ) {
|
|
if(this.curReconnectTime==0) this.curReconnectTime = reconnectTime;
|
|
else {
|
|
this.curReconnectTime *= reconnectTimeFactor;
|
|
if(this.curReconnectTime>reconnectTimeMax) this.curReconnectTime = reconnectTimeMax;
|
|
}
|
|
var rjit = (Math.random()*reconnectJitterPercent)-(reconnectJitterPercent/2);
|
|
this.curReconnectTime += (this.curReconnectTime*(rjit/100));
|
|
// Reconnect in curReconnectTime (=>getToken THEN connect)
|
|
setTimeout(this.getToken.bind(this), Math.floor(1000*this.curReconnectTime));
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} e
|
|
*/
|
|
WSonError(e){
|
|
//console.warn('Websocket error:', e.message);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
var msgbus = null;
|
|
onmessage = (e) => { // message from client
|
|
if (e.data.action=='start') {
|
|
if(!msgbus) msgbus = new MessageBusWorker(e.data.config, e.data.userInfo);
|
|
} else {
|
|
if(msgbus) msgbus.clientActionDispatch(e.data);
|
|
}
|
|
}
|
|
|