/* Copyright (c) 2020, 2023, The Unified Company. This code is part of Unify. This program is free software; you can redistribute it and/or modify it under the terms of the ESA Software Community License - Strong Copyleft LICENSE, as published by the ESA. See the ESA Software Community License - Strong Copyleft LICENSE, for more details. https://unifyjs.org */ import socketMessage from '../unify/socketMessage.js'; import uws from '../node_modules/uws/uws.js'; import http from 'http'; import https from 'https'; import socketClient from './socketClient.js'; import objectManager from './objectManager.js'; import Console from '../server/console.js'; import webConsole from '../unify/console.js'; import tools from '../unify/tools.js'; import deepclone from '../unify/clonedeep.js'; import applications from '../configs/applications.js'; import unify from '../unify/unify.js'; import defaultObject from '../unify/defaultObject.js'; import path from 'path'; import groupsFlat from './themeGroups.js'; import moduleLoader from '../server/moduleLoader.js'; import imports from './imports.js'; import { RateLimiterMemory, BurstyRateLimiter } from '../node_modules/node-rate-limiter-flexible/index.js'; global.isListening = false; global.cachePromise = new Array(); global.cacheResolve = new Array(); for (var i = 0; i < 1000000; i++) { global.cachePromise[i] = new Promise(function( resolve ) { global.cacheResolve[i] = resolve; }); } export default class socketManager{ userID = 0; port = 4437; host = "localhost"; cacheTreshold = 100; cacheSize = 50; cachePointer = 0; cacheDelay = 30; cacheBusy = false; blockedRemoteAddresses = new Array(); clients = new Array(); sessionClients = new Array(); objectManager = new objectManager(); moduleLoader = new moduleLoader(); clientID = 0; socket; core; hotSwapping; applicationID; applicationURLArray; httpsServer; application; constructor( port ){ if( port ) { this.port = port; } global.socketManager = this; this.initializeLimiter(); } async checkRateLimiter( remoteAddress ) { console.log("remoteAddress", remoteAddress); var allow = await this.rateLimiter.consume( remoteAddress, 2 ) .then(() => { return true; }) .catch(() => { return false; }); if( !allow ) { if( global.mode == "development" ) { console.log("To many connections from same ip. Connection rejected."); } } return allow; } initializeLimiter() { this.burstyLimiter = new BurstyRateLimiter( new RateLimiterMemory({ points: 50, duration: 1, }), new RateLimiterMemory({ points: 150, duration: 3, }) ); this.rateLimiter = new RateLimiterMemory({ points: 100, duration: 1, }); } setApplicationID( applicationID ) { this.applicationID = applicationID; } setApplication( application ) { this.application = application; } create() { this.cacheDelay = ( 10 - global.cacheBuildSpeed ) * 20; if( global.mode == "development" ) { this.cacheTreshold = 1; console.log( "Max number of clients ", global.maxClients, ",But set to", this.cacheTreshold, "Because the server is running in developer mode." ); //this.cacheTreshold = global.maxClients; } else { console.log( "Max number of clients ", global.maxClients ); this.cacheTreshold = global.maxClients; } console.log( "Cache build speed ", global.cacheBuildSpeed, "(" + this.cacheDelay + "ms delay per client)" ); if( !global.isListening ) { this.fileUpdater(); global.isListening = true; } var socketManager = this; //var privateKey = fs.readFileSync('./sslCertificates/privkey.pem', 'utf8'); //var certificate = fs.readFileSync('./sslCertificates/fullchain.pem', 'utf8'); var socket = { compression: 0, maxPayloadLength: 16 * 1024 * 1024, idleTimeout: 60, open: async function( ws, req ) { /* ws [ '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', 'close', 'constructor', 'cork', 'end', 'getBufferedAmount', 'getRemoteAddress', 'getRemoteAddressAsText', 'getTopics', 'getUserData', 'hasOwnProperty', 'isPrototypeOf', 'isSubscribed', 'ping', 'propertyIsEnumerable', 'publish', 'send', 'sendFirstFragment', 'sendFragment', 'sendLastFragment', 'subscribe', 'toLocaleString', 'toString', 'unsubscribe', 'valueOf' ] */ var buffer = ws.getRemoteAddressAsText(); var remoteAddress = new TextDecoder().decode( new Int8Array( buffer ) ); var allow = await socketManager.checkRateLimiter( remoteAddress ); if( !allow ) { console.log("Connection Rejected."); ws.close(); //socketManager.blockedRemoteAddresses.push( remoteAddress ); return true; } else { console.log("Connection Accepted."); } ws.socket = socket; socketManager.connection( ws ) }, message: async function( ws, buf ) { //var message = String.fromCharCode.apply( null, new Int8Array( buf ) ); /* var buffer = ws.getRemoteAddressAsText(); var remoteAddress = new TextDecoder().decode( new Int8Array( buffer ) ); if( socketManager.blockedRemoteAddresses.includes( remoteAddress ) ) { ws.close(); return true; } */ var message = new TextDecoder().decode( new Int8Array( buf ) ); if( !ws.client ) { //console.log("userID", message, socketManager.userID); var client = await socketManager.connectionCallback( ws, message ); ws.client = await client; } if( !ws.client ) { return false; } ws.client.message( message, client ) }, close: ( ws, code, message ) => { //ws.client.destroy() var client = ws.client; if( client ) { client.destroy(); } /* console.log("socket.closed on cluster:", global.clusterID ); var socketManager = client.socketManager; var userID = client.userID; console.log("socket closed", userID, Object.keys( socketManager.clients) ); var exists = socketManager.clients[ userID ]; if( exists ) { console.log("client still exists, starting timer."); client.destroyTimerCallback = function() { client.destroy(); var exists = socketManager.clients[ userID ]; console.log("Timer exceeded, Client is removed.", typeof exists); } client.destroyTimer = setTimeout( client.destroyTimerCallback, client.destroyTime ); } else { //console.log( "client is destroyed" ); } */ } } if( !global.ssl ) { var socketServer = uws.App(); this.socket = socketServer.ws( '/*', socket ).listen( this.port,function( token ) { if( token ) { console.log(`Listening to port ${socketManager.port}`); } else { console.log(`Failed to listen to port ${port}`); } }); console.log("socket server started on port: " , this.port ); } else { var socketServer = uws.SSLApp({ key_file_name: './sslCertificates/privkey.pem', cert_file_name: './sslCertificates/fullchain.pem' }); this.socket = socketServer.ws( '/*', socket ).listen( this.port,function( token ) { if( token ) { console.log(`Listening to port ${socketManager.port}`); } else { console.log(`Failed to listen to port ${port}`); } }); console.log("started Secure socket server: ", this.port); } global.applicationCache = new Array(); this.createCache( this.cacheSize ); this.createEventListeners(); } async createCache( how_many ) { if( !this.cacheBusy ) { this.cacheBusy = true; var cachedItems = this.cachePointer; var client = new Object(); client.classObjects = new Array(); client.objects = new Array(); var defaultInstance = new defaultObject(); var startTime = Date.now(); var allObjects = new Array(); var methods = tools.getAllFuncs( defaultInstance ); for ( var i = this.cachePointer; i < this.cachePointer + how_many; i++ ) { var cacheObject = new Object(); cacheObject.client = new Object(); cacheObject.client.classObjects = new Array(); cacheObject.client.objects = new Array(); //cacheObject.application = structuredClone(beginObject); cacheObject.application = new imports[ this.applicationID ](); global.core.collectObjects( cacheObject.application, cacheObject.client ); var objects = cacheObject.client.objects; for (var k = 0; k < objects.length; k++) { var object = objects[k] for (var j = 0; j < methods.length; j++) { var method = methods[j]; const ommit = new Array( '', 'setJoinID', 'reset', 'isOrExtendsClass', 'hasClass', 'getTableName', 'delete', 'appendToSelector', 'agregateDefaultObject', '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', 'valueOf', 'constructor', 'toLocaleString', 'toString' ); if( ommit.includes( method ) ) { continue; } if(!object[method]) { // this takes most time object[method] = defaultInstance[method]; } } global.core.prepareObject( object, cacheObject.client ); global.core.tableManager.parse( object ); } await tools.delay( this.cacheDelay ); //console.log("create cache", i); global.applicationCache[i] = cacheObject; global.cacheResolve[i](); } console.log("\n\n\n\n"); console.log("cached ", how_many, "client Environments.") console.log("Total cached ", Object.keys(global.applicationCache).length, "client Environments.") console.log("On Cluster ", global.clusterID); console.log("in ", (Date.now() - startTime ) / 1000, "Seconds" ); console.log("from ", this.cachePointer, "to", this.cachePointer + how_many); console.log("\n\n"); this.cachePointer = this.cachePointer + how_many; this.cacheBusy = false; if( this.cachePointer < this.cacheTreshold ) { this.createCache( this.cacheSize ); } } } /* createCacheFastest( how_many ) { var cachedItems = global.applicationCache.length; var client = new Object(); client.classObjects = new Array(); client.objects = new Array(); var beginObject = new imports[ this.applicationID ](); var removedMethods = new Array(); var collectionStorage = new Array(); var tableStorage = new Array(); global.core.getTables( beginObject, tableStorage ); global.core.prepareClone( beginObject, removedMethods ); global.core.removeCollectionObject( beginObject, false, collectionStorage ); //console.log(beginObject); var defaultInstance = new defaultObject(); var startTime = Date.now(); var allObjects = new Array(); for ( var i = cachedItems; i < cachedItems + how_many; i++ ) { var cacheObject = new Object(); cacheObject.client = new Object(); cacheObject.client.classObjects = new Array(); cacheObject.client.objects = new Array(); cacheObject.application = structuredClone(beginObject); //cacheObject.application = new imports[ this.applicationID ](); global.core.fastParse( cacheObject.application, cacheObject.client, true ); allObjects = [...allObjects, ...cacheObject.client.objects] console.log("create cache", i); global.applicationCache[i] = cacheObject; } //console.log("time taken", (Date.now() - startTime ) / 1000 ); //var startTime = Date.now(); //console.log("all objects ", allObjects.length); var methods = tools.getAllFuncs( defaultInstance ); for ( var i = 0; i < allObjects.length; i++ ) { var object = allObjects[i]; //var nodeMethodsString = object.__nodeMethods; var className = tools.getClassName( object ); if( object.type == "table" ) { object.table = tableStorage[className] } var currentRemoved = removedMethods[className]; //if(currentRemoved) //console.log("reconstructing", className, Object.keys( currentRemoved ).length); if( currentRemoved ) { var keys = Object.keys( currentRemoved ); for (var j = 0; j < keys.length; j++) { var removedMethodName = keys[j] var removedMethod = removedMethods[className][removedMethodName] object[ removedMethodName ] = removedMethod; //console.log(className, removedMethodName, removedMethod); } } switch( object.type ) { case "renderCollection": object.object = collectionStorage[ className ] if( object.collections[0] ) { object.collections[0].object = collectionStorage[ className ]; } break; case "collection": var className = tools.getClassName( object ); if( object.parent ) { var parentClassName = tools.getClassName( object.parent ); if(object.parentName) { object.object = collectionStorage[ object.parentName ]; } } break; } for (var j = 0; j < methods.length; j++) { var method = methods[j]; if(!object[method]) { object[method] = defaultInstance[method]; } //console.log(method, defaultInstance[method]); } global.core.prepareObject( object, false ); global.core.tableManager.parse( object ); } //console.log(global.applicationCache[0]); console.log("time taken", (Date.now() - startTime ) / 1000 ); console.log("cached " + how_many + " client Environments."); console.log("from ", cachedItems, "to", cachedItems + how_many); } */ createEventListeners() { var socketManager = this; //this.socket.on('connection', function( socket ){ socketManager.connection( socket ) }); } recycleClient( userID ) { //delete global.applicationCache[ userID ]; var client = this.clients[ userID ]; if( client.sessionKey ) { delete this.sessionClients[ client.sessionKey ]; } var cacheObject = global.applicationCache[ userID ]; var objects = cacheObject.client.objects; //console.log("global.applicationCache", userID, objects.length ); for (var i = 0; i < objects.length; i++) { var object = objects[i]; object.value = false; object.id = 0; if( object.type == "renderCollection" ) { object.empty( cacheObject.client ); /* var collection = this.getCollection(); if( collection ) { for (var j = 0; j < rows.length; j++) { var row = rows[j]; delete this[row.propertyName]; } } */ } object.user = false } delete global.applicationCache[ userID ]; global.applicationCache.push( cacheObject ); this.cachePointer++; console.log("recycled cacheObject", global.applicationCache.length); delete this.clients[ userID ]; } removeClient( userID ) { console.log( "RemoveClient with id", userID); console.log( "Clients:", Object.keys( this.clients ) ); var clients = this.clients; this.recycleClient( userID ); this.objectManager.removeObjectsByClientID( userID ); console.log( "Clients:", Object.keys( this.clients ) ); } getClientBySessionKey( sessionKey ) { var clients = this.clients; console.log( "getClientBySessionKey", Object.keys(this.sessionClients), sessionKey); return this.sessionClients[ sessionKey ]; } async addClient( id, socket ) { var newClient = new socketClient( this.port ); newClient.userID = id; newClient.socket = socket; newClient.socketManager = this; newClient.core = this.core; newClient.application = this.application; await newClient.createSocket( socket ); this.clients[ id ] = newClient; this.sessionClients[ newClient.sessionKey ] = newClient; // numClients = this.clientID++; if( id >= this.cachePointer - this.cacheTreshold ) { this.createCache( this.cacheSize ); } return newClient; } createInitiationMessage() { var message = new socketMessage(); message.id = 0; message.data = "connected"; message.type = "promise"; return message; } async registerClient( socket, message ) { var userID = this.userID; var client2 = await this.addClient( userID, socket ); console.log( "Client connected: ", userID ); var object = new Object(); //object.sessionKey = client.sessionKey; //var returnMessage = client.createValidMessage( message, object ); //client.send( returnMessage ); //this.userID++; return client2; } reEstablishConnection( client, socket, message ) { client.socket = socket; if( client.destroyTimerCallback ) { clearTimeout( client.destroyTimer ); console.log( "destroyTimerCallback cleared." ); } client.createEventListeners(); console.log( "Client found, Socket re-established.", client.id ); var object = new Object(); object.data = "Connection re-established"; object.sessionKey = client.sessionKey; var returnMessage = client.createValidMessage( message, object ); client.send( returnMessage ); } async reconnectClient( socket, message ) { console.log( "reconnectClient", "Cluster number:", global.clusterID, message.object ); var client = this.getClientBySessionKey( message.object ); if( client ) { this.reEstablishConnection( client, socket, message ); } } async connectionCallback( socket, json ){ var string = tools.serializeString( json ); var message = tools.serializeJSON( string ); switch( message.type ) { case "registerClient": var client = await this.registerClient( socket, message ); this.userID++; return client; break; case "reconnectClient": this.reconnectClient( socket, message ); break; default: } } connection( socket ) { if( global.consoleLog ) { console.log = global.consoleLog; } var that = this; //socket.on( 'message', async function( json ) { that.connectionCallback( socket, json ); } ); var message = this.createInitiationMessage(); socket.send( JSON.stringify( message ) ); if( global.mode == "development" ) { console.log( "Number connected clients: ", Object.keys( this.clients ).length ); } } log( parameter1, parameter2, parameter3 ) { var parameters = webConsole.parseParameters( parameter1, parameter2, parameter3 ); this.logClients( parameters ); } logClients( parameters ) { for(var c = 0; c