'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); } }