2nd
This commit is contained in:
+290
@@ -0,0 +1,290 @@
|
||||
const crypto = require('crypto')
|
||||
const gatewayActions = require('./actions')
|
||||
|
||||
module.exports = class WssConnexion {
|
||||
|
||||
constructor(options){
|
||||
Object.assign(this, gatewayActions.methods)
|
||||
|
||||
this.config = options.config;
|
||||
this.socket = options.socket;
|
||||
this.req = options.req;
|
||||
this.uuid = options.uuid;
|
||||
this.wssSrv = options.wssSrv;
|
||||
this.debug = options.debug;
|
||||
this.rediscnx = options.rediscnx;
|
||||
this.roles = []
|
||||
this.accessRights = options.accessRights;
|
||||
this.userId = '';
|
||||
this.sessionID = null // null until login
|
||||
|
||||
this.subscriptions = [];
|
||||
this.usersWatched = [];
|
||||
|
||||
this.socket.on('close', this.onClose.bind(this));
|
||||
this.socket.on('message', this.receive.bind(this));
|
||||
|
||||
if(this.debug) console.log(`Spawned new WSS connection to client: ${this.req.socket.remoteAddress}:${this.req.socket.remotePort}`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
doLogin(){
|
||||
this.cnxState = 'LOGIN' // then CONNECTED
|
||||
this.challenge = crypto.randomUUID()
|
||||
this.challengeTimeout = setTimeout(() => {
|
||||
if(this.debug) console.warn(`Timeout waiting for login response for UUID ${this.uuid}, closing connection !`);
|
||||
this.close()
|
||||
}, this.config.server.challengeExpiration*1000)
|
||||
this.send(JSON.stringify({
|
||||
'action': 'LOGIN',
|
||||
'challenge': this.challenge
|
||||
}));
|
||||
if(this.debug) console.log(`Sent LOGIN for UUID ${this.uuid} ==> challenge=${this.challenge}`)
|
||||
return(new Promise((resolve, reject) => {
|
||||
this.resolveLogin = resolve
|
||||
}))
|
||||
}
|
||||
|
||||
welcome(){
|
||||
clearTimeout(this.challengeTimeout)
|
||||
this.challengeTimeout = null
|
||||
this.cnxState = 'CONNECTED'
|
||||
if(this.debug) console.log(`Welcome to UUID ${this.uuid}`)
|
||||
this.resolveLogin()
|
||||
}
|
||||
|
||||
async checkLogin(userInfo, otp){
|
||||
if(!this.config.server.devotpToken){
|
||||
let rawPayload = await this.rediscnx.redisGet(userInfo, this.config.redis.authTokenPrefix)
|
||||
let payload = JSON.parse(rawPayload)
|
||||
if(this.debug) console.log(`Got a token from Redis for ${userInfo} => ${JSON.stringify(payload)}`)
|
||||
if((!payload) || (!payload.token) || (!payload.roles)) return(false) // Redis/sessions issues : don't crash the daemon !
|
||||
this.token = payload.token
|
||||
this.roles = payload.roles
|
||||
this.sessionID = payload.sessionID || '!!no sessionID in the token key in Redis!!'
|
||||
} else {
|
||||
this.token = this.config.server.devotpToken
|
||||
this.roles = ['EIC_Dev']
|
||||
this.sessionID = this.config.server.devotpToken
|
||||
}
|
||||
|
||||
let data = new TextEncoder().encode(this.token+this.challenge)
|
||||
let bytesBuf = await crypto.subtle.digest("SHA-512", data)
|
||||
let arrayBuf = Array.from(new Uint8Array(bytesBuf))
|
||||
let goodOTP = arrayBuf.map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||
if(this.debug) console.log(`Checking challenge-response (token=${this.token}): ${otp} ?? ${goodOTP}`)
|
||||
return(otp == goodOTP)
|
||||
}
|
||||
|
||||
startKeepAlive() {
|
||||
if(this.config.server.keepAliveInterval>0) {
|
||||
if(this.config.server.keepAliveInterval >= (1.5*this.config.server.keepAliveTimeout)) {
|
||||
this.keepAliveNextTimeout = setTimeout(this.keepAlive.bind(this),this.config.server.keepAliveInterval*1000);
|
||||
} else {
|
||||
console.warn('keepAliveTimeout should be at least 1.5 x keepAliveInterval ! Ignoring Keepalive!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async receive(data) { //receive from Websocket
|
||||
try{
|
||||
var pdata = JSON.parse(data);
|
||||
var action = pdata.action;
|
||||
var payload = ('payload' in pdata) ? pdata.payload : null;
|
||||
var reqid = ('reqid' in pdata) ? pdata.reqid.substr(0,50) : null;
|
||||
} catch (err){
|
||||
if(this.debug) console.warn(`Malformed json for uuid ${this.uuid}\n${data}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.cnxState == 'LOGIN'){
|
||||
if((action=='LOGIN') && pdata.userInfo && pdata.otp) {
|
||||
if(this.debug) console.log(`received login response : user=${pdata.userInfo} otp=${pdata.otp}`)
|
||||
if(await this.checkLogin(pdata.userInfo, pdata.otp)) {
|
||||
this.userId = pdata.userInfo
|
||||
this.welcome()
|
||||
} else {
|
||||
if(this.debug) console.warn(`Bad OTP response to login request for uuid ${this.uuid}`);
|
||||
this.send(JSON.stringify({
|
||||
'action': 'LOGIN',
|
||||
'logged': false
|
||||
}));
|
||||
this.close()
|
||||
}
|
||||
} else {
|
||||
if(this.debug) console.warn(`Invalid response to login request for uuid ${this.uuid}`,pdata);
|
||||
}
|
||||
} else {
|
||||
if(typeof this['action_'+action] == "function") {
|
||||
if((this.debug) && (action != 'PONG')) console.warn(`${action} for uuid ${this.uuid}`);
|
||||
this['action_'+action](action, payload, reqid);
|
||||
} else {
|
||||
if(this.debug) console.warn(`Unknown action ${action} for UUID ${this.uuid}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send(data) { // Send to Websocket
|
||||
this.socket.send(data);
|
||||
}
|
||||
|
||||
|
||||
keepAlive(){
|
||||
this.send(JSON.stringify({
|
||||
'action': 'PING',
|
||||
}));
|
||||
this.keepAliveBomb = setTimeout(this.MissedKeepAlive.bind(this), (this.config.server.keepAliveTimeout+1)*1000);
|
||||
}
|
||||
|
||||
MissedKeepAlive(){
|
||||
if(this.debug) console.warn(`Keep-alive timeout for UUID ${this.uuid}, closing connection !`);
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
close() {
|
||||
if(this.socket) this.socket.close()
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if(this.debug) console.log(`UUID ${this.uuid} disconnected !`);
|
||||
clearTimeout(this.keepAliveNextTimeout);
|
||||
this.clearAllSubscriptions();
|
||||
this.wssSrv.cleanupConnexion(this.uuid, this.userId);
|
||||
// Suicide and leave my body to GC (Carefull if you added other references to me from outside !!)
|
||||
// Also think about all possibly active bind(this), which -I guess- also make references preventing GC.
|
||||
}
|
||||
|
||||
async getAwaitingNotifs(){
|
||||
//TODO : AWAIT this from either Redis and/or ML
|
||||
|
||||
// Key: notif destination module, value: either KV with V=nb of notifs, or Array whose length is nb of notifs
|
||||
let notifs = { // TEST EXAMPLE
|
||||
"unreadChats": {
|
||||
"chan001" : 2,
|
||||
"chan002" : 10,
|
||||
"chan003" : 7,
|
||||
},
|
||||
"OTS": [ "fallimi", "infosca" ],
|
||||
"OtherNotifDest" : []
|
||||
};
|
||||
|
||||
return(notifs);
|
||||
}
|
||||
|
||||
subscribeMandatoryChans(){
|
||||
let mandaChans = this.accessRights.mustSubscribe(this.userId, this.roles)
|
||||
mandaChans.push('userchans:'+this.userId); // Add user private chan
|
||||
|
||||
mandaChans = mandaChans.map(item=>this.config.redis.basePrefix+item)
|
||||
|
||||
for(var chan of mandaChans){
|
||||
if(!(chan in this.rediscnx.subscriptions)) this.rediscnx.subscriptions[chan] = [];
|
||||
if(this.subscriptions.indexOf(chan)<0) {
|
||||
this.subscriptions.push(chan);
|
||||
this.rediscnx.subscriptions[chan].push(this.uuid);
|
||||
}
|
||||
}
|
||||
this.action_SUBLST('SUBLST', null, '');
|
||||
}
|
||||
|
||||
clearAllSubscriptions(){
|
||||
for(var chan of this.subscriptions){
|
||||
if(chan in this.rediscnx.subscriptions) {
|
||||
this.rediscnx.subscriptions[chan].splice(this.rediscnx.subscriptions[chan].indexOf(this.uuid), 1) ;
|
||||
}
|
||||
}
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
sendErr(action, msg, reqid){
|
||||
this.send(JSON.stringify({
|
||||
'action': action,
|
||||
'success': false,
|
||||
'err': msg,
|
||||
'reqid': reqid,
|
||||
}));
|
||||
}
|
||||
|
||||
updateOnlineUsers(onlineUsers){ //Helper for our parent, triggered from above on new/lost connexion
|
||||
onlineUsers = Object.keys(onlineUsers);
|
||||
let reply = {
|
||||
'action': 'ISONLINE',
|
||||
'payload': onlineUsers.filter((x) => (this.usersWatched.indexOf(x)>-1)),
|
||||
'success': true,
|
||||
};
|
||||
this.send(JSON.stringify(reply));
|
||||
}
|
||||
}
|
||||
|
||||
/* PROTOCOL
|
||||
Application-level payloads are always JSON and always either an action, or an event :
|
||||
|
||||
1. ACTIONS : are made for request-reply.
|
||||
They are aimed at the dialogue between the FE (mainly messageBus core modules) and WSSGateway.
|
||||
These messages are identified by the fact there is an "action" key, top level.
|
||||
|
||||
Example : The FE asks WSSGateway to subscribe to Redis chans :
|
||||
Request:
|
||||
{ "action" : "SUB", // Must be a valid wssGateway action.
|
||||
"payload": ["chan1", "chan2"], // Any type required by the action
|
||||
"reqid": "987654321-abcdef-123456"
|
||||
}
|
||||
|
||||
Reply:
|
||||
{ "action" : "SUB",
|
||||
"payload": ["chan1", "chan9"], // probably you were already subscribed to chan9,
|
||||
"reqid": "987654321-abcdef-123456" // don't have to right to chan2, but succeeded subscribing to chan1
|
||||
}
|
||||
|
||||
Newton principle applied to WSSG:
|
||||
When there is an action in one direction (request),
|
||||
there is the same action in the opposite direction (reply).
|
||||
|
||||
When doing a request, the FE can optionally include a "reqid", with a uuid.
|
||||
It then has the guarnatee that the corresponding reply will contain the same reqid.
|
||||
As you can receive a reply on a particular action in any number, at any time,
|
||||
this allows the FE to match one specific action request with its specific reply.
|
||||
This, in turn, allows this module to provide action-promise and action-timeouts.
|
||||
|
||||
2. EVENTS : are any other events circulating on the bus, thus on a REDIS channel.
|
||||
They are triggered by another actor on the bus, and have nothing to do with FE-WssGW dialog .
|
||||
These messages are identified by the fact there is an "event" key, top level.
|
||||
So far, this core-module has no use of bus-events, they are considered as applicative-level-use only.
|
||||
Therefore, this module just triggers a corresponding (javascript) event, for any potential listener in the app.
|
||||
|
||||
Example :
|
||||
{ "event" : "PropaSubmitted", // Any applicative thing
|
||||
"payload": { // Any type depending on applicative convention for this event
|
||||
"propaNumber": "123456",
|
||||
"propaAcronym": "Tintin"
|
||||
}
|
||||
}
|
||||
|
||||
Will trigger a "MessageBus.PropaSubmitted" javascript event, with
|
||||
"detail":
|
||||
{ msg: {
|
||||
event: "PropaSubmitted",
|
||||
payload: { "propaNumber": "123456",
|
||||
"propaAcronym": "Tintin"
|
||||
}
|
||||
},
|
||||
chan: "wssGateway:chan1:subchan2",
|
||||
}
|
||||
|
||||
--------------- Low-level, WEBSOCKET ---------------
|
||||
{ "event":"REDISMSG", // low level
|
||||
"payload":{ // low level
|
||||
"msg":{ // low level
|
||||
"eventType":"PropaSubmitted", // APP LEVEL MESSAGE = Redis payload
|
||||
"payload":{ // APP LEVEL MESSAGE = Redis payload
|
||||
"propaNumber": "123456", // APP LEVEL MESSAGE = Redis payload
|
||||
"propaAcronym": "Tintin" // APP LEVEL MESSAGE = Redis payload
|
||||
}, // APP LEVEL MESSAGE = Redis payload
|
||||
sender: "N007xyz" // APP LEVEL MESSAGE = Redis payload => added by gateways !
|
||||
}, // low level
|
||||
"chan":"wssGateway:chan1:subchan2" // low level = Redis channel
|
||||
},
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user