import * as Http from 'http'; import * as SocketIO from 'socket.io'; import { DyFM_AnyError, DyFM_Array, DyFM_Async, DyFM_Error, DyFM_Error_Settings, DyFM_ErrorLevel, DyFM_Log, } from '@futdevpro/fsm-dynamo'; import { DyNTS_defaultSocketPath } from '../../_collections/default-socket-path.const'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_RouteSecurity } from '../../_enums/route-security.enum'; import { DyNTS_AppExtSysControls } from '../../_models/control-models/app-ext-system-controls.control-model'; import { DyNTS_App } from '../../_services/server/app.server'; import { DyNTS_SocketSecurity } from './_enums/socket-security.enum'; import { DyNTS_SocketServerService } from './_services/socket-server.service'; /** * This will be the MAIN service of our server project, * follow the types and type instructions while setting up your project * * In this service, there are abstract functions that you will need to implement, * where you need to set up the main params for your application. * * The extended App is containing socket server tools * * You need to add socketService definitions to setupRoutingModules * * @example * export class App extends DyNTS_AppExtended { * * ... * * // Setting up App params, and preparing project global settings * setupAppParams(): void { * this.params = new DyNTS_AppParams({ * name: 'Warbots Server', * title: warbotsTitleLog, * version: version, * dbName: 'warbots', * }); * * // dynamoNTS_GlobalSettings.logRequestsContent = false; * } * * ... * * // Setting up DBServices * setGlobalServiceCollection(): void { * DyNTS_GlobalService.setServices({ * authService: AuthService.getInstance(), * emailServiceCollection: EmailServiceCollectionService.getInstance(), * dbModels: [ * userModelParams, * userDataModelParams, * userOptionsModelParams, * userStatisticsModelParams, * userAchievementsModelParams, * userNotificationsModelParams, * * matchStatisticsModelParams, * matchDataModelParams, * DyFM_usageSession_dataParams, * DyFM_customData_dataParams, * ] * }); * } * * ... * * // Setting up Routes * setupRoutingModules(): void { * this.httpPort = env.port; * this.routingModules = [ * new DyNTS_RoutingModule({ * route: '/user', * controllers: [ * UserController.getInstance(), * UserDataController.getInstance(), * UserOptionsController.getInstance(), * UserStatisticsController.getInstance(), * UserAchievementsController.getInstance(), * UserNotificationsController.getInstance() * ] * }), * new DyNTS_RoutingModule({ * route: '/match', * controllers: [ * MatchController.getInstance(), * MatchDistributionController.getInstance(), * MatchStatisticsController.getInstance(), * ] * }), * new DyNTS_RoutingModule({ * route: '/server', * controllers: [ * ServerController.getInstance(), * ] * }), * getTestRoutingModule(), * getUsageRoutingModule() * ]; * * ... * * // Setting up Sockets (port opcionális – ha nincs megadva, a HTTP/HTTPS portot használjuk, same port) * this.socketServices = [ * NotificationService.getInstance(), * ChatService.getInstance(), * ... * ]; * } * } */ export abstract class DyNTS_AppExtended extends DyNTS_App { private readonly systemControlsExt: DyNTS_AppExtSysControls = new DyNTS_AppExtSysControls(); override get started(): boolean { return this.systemControlsExt.appExtended.started; } private httpsSocketSettingUpCount: number = 0; private httpSocketSettingUpCount: number = 0; private socketSecurity: DyNTS_RouteSecurity; /** Csak akkor van értékadva, ha nincs base httpServer (pl. nincs open route) és open socket van. */ private httpSocketServer: Http.Server | undefined; /** * Socket szolgáltatások. A port opcionális: ha nincs megadva, az extended app * a getPortSettings() httpPort/httpsPort értékét használja (WebSocket és HTTP ugyanazon a porton). * @example * // Setting up Sockets * this.socketServices = [ * NotificationService.getInstance(), * ChatService.getInstance(), * ... * ]; */ protected socketServices: DyNTS_SocketServerService[]; private readonly allSocketServers: SocketIO.Server[] = []; constructor(){ super(/* true */); /* this.asyncConstructExt().catch((error: DyFM_AnyError): void => { if ( DyNTS_global_settings.log_settings.highDetailedLogs || !(error instanceof DyFM_Error) ) { DyFM_Log.H_error( `Extended Application: ${this.params?.name} start failed. ` + `(DyNTS_AppExtended constructor catch)`, `\n ERROR:`, error ); } else { error.logSimple( `Extended Application: "${this.params?.name}" start failed. ` + `(DyNTS_AppExtended constructor catch)` ); } }); */ } protected override async asyncConstruct(extended = true): Promise { if (this.fnLogs && this.deepLog) console.log('\nfn:. asyncConstruct-extended'); try { await super.asyncConstruct(true); this.systemControlsExt.appExtended.init = true; this.socketServices = this.getSocketServices(); if (this.socketServices?.length) { if (this.logSetup) DyFM_Log.log('\nsetting up socket servers...'); this.setSocketSecurity(); await this.setupSocketServerServices(); if (this.logSetup) console.log( `\nAll sockets setted up.... sockets using security: ${this.socketSecurity}` ); } else { DyFM_Log.testWarn( 'No socketServices setted up while using Extended Application.', '\nYou should use DyNTS_App if you don`t need socket services.' ); } await this.ready(); if (this.params.title) { console.log(this.params.title); console.log(`Version: ${this.params.version}`); } DyFM_Log.H_success(`"${this.params.name}" started successfully.`); } catch (error) { /* if ( DyNTS_global_settings.log_settings.highDetailedLogs || !(error instanceof DyFM_Error) ) { DyFM_Log.H_error( `Extended Application: ${this.params?.name} start failed. (asyncConstructExt catch)` + `\n ERROR:`, error ); } else { error.logSimple( `Extended Application: "${this.params?.name}" start failed. (asyncConstructExt catch)` ); } */ throw new DyFM_Error({ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AES-001`, error: error, additionalContent: { constructErrors: this.constructErrors, systemControls: this.systemControls, systemControlsExt: this.systemControlsExt, systemReadies: { app: this.systemControls.app.getIsReady(), appExtended: this.systemControlsExt.appExtended.getIsReady(), mongoose: this.systemControls.mongoose.getIsReady(), httpServer: this.systemControls.httpServer.getIsReady(), httpsServer: this.systemControls.httpsServer.getIsReady(), httpSocketServer: this.systemControlsExt.httpSocketServer.getIsReady(), httpsSocketServer: this.systemControlsExt.httpsSocketServer.getIsReady(), }, }, }); } } override async ready(timeout: number = this.defaultReadyTimeout): Promise { if (this.fnLogs) console.log('\nfn:. ready-extended'); try { await super.ready(timeout); let ready: boolean = false; const start: number = +new Date(); if (this.constructErrors.length) { if (this.deepLog) { if (DyNTS_global_settings.log_settings.highDetailedLogs) { DyFM_Log.H_error( `Extended Application: "${this.params.name}" start failed. ` + `(ready; constructErrors check +1)`, `\n construct ERRORS:`, this.constructErrors ); } else { DyFM_Log.H_error( `Extended Application: "${this.params.name}" start failed. ` + `(ready; constructErrors check +1)`, `\n construct ERRORS:`, this.getSimplifiedConstructErrors(), ); } } throw new DyFM_Error({ message: `${this.params?.name} start failed.`, additionalContent: this.constructErrors, error: this.constructErrors?.[0] ?? new Error(), }); } while (!ready && +new Date() - start < timeout) { if (this.systemControlsExt.appExtended.init) { ready = ( this.superStarted && this.systemControlsExt.httpSocketServer.getIsReady() && this.systemControlsExt.httpsSocketServer.getIsReady() ); } else { DyFM_Log.error(`"${this.params.name}" APP NOT INITIALIZED while trying to get ready.`); } if (!ready) { await DyFM_Async.wait(100); } } if (this.constructErrors.length) { if (this.deepLog) { if (DyNTS_global_settings.log_settings.highDetailedLogs) { DyFM_Log.H_error( `Extended Application: "${this.params.name}" start failed. (ready; TIMEOUT check +2)`, `\n construct ERRORS:`, this.constructErrors ); } else { DyFM_Log.H_error( `Extended Application: "${this.params.name}" start failed. (ready; TIMEOUT check +2)`, `\n construct ERRORS:`, this.getSimplifiedConstructErrors(), ); } } throw new DyFM_Error({ message: `${this.params?.name} start failed. TIMEOUT`, additionalContent: this.constructErrors, error: this.constructErrors?.[0] ?? new Error(), }); } if (ready) { this.systemControlsExt.appExtended.started = true; if (this.fnLogs && this.deepLog) console.log('\nfn:. ready-extended: return'); return; } else { this.systemControlsExt.appExtended.started = false; let msg: string = `${this.params?.name} start failed. UNKNOWN`; if ( this.systemControlsExt.httpSocketServer.init && !this.systemControlsExt.httpSocketServer.started ) { msg += '\nhttpSocketServer start failed.'; } if ( this.systemControlsExt.httpsSocketServer.init && !this.systemControlsExt.httpsSocketServer.started ) { msg += '\nhttpsSocketServer start failed.'; } DyFM_Log.error(msg, this.constructErrors, '\n'); throw new DyFM_Error({ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AES-110`, message: msg, additionalContent: { constructErrors: this.constructErrors, systemControls: this.systemControls, systemControlsExt: this.systemControlsExt, systemReadies: { app: this.systemControls.app.getIsReady(), appExtended: this.systemControlsExt.appExtended.getIsReady(), mongoose: this.systemControls.mongoose.getIsReady(), httpServer: this.systemControls.httpServer.getIsReady(), httpsServer: this.systemControls.httpsServer.getIsReady(), httpSocketServer: this.systemControlsExt.httpSocketServer.getIsReady(), httpsSocketServer: this.systemControlsExt.httpsSocketServer.getIsReady(), }, }, error: this.constructErrors?.[0] ?? new Error(), }); } } catch (error) { throw new DyFM_Error({ ...this.__getDefaultErrorSettings('ready', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AES-111`, error: error, }); } } override async stop(): Promise { if (this.fnLogs) console.log('\nfn:. stop-extended'); try { if (this.started) { await super.stop(true); this.systemControlsExt.httpSocketServer.started = false; this.systemControlsExt.httpsSocketServer.started = false; await DyFM_Array.asyncForEach( this.allSocketServers, async (socketServer: SocketIO.Server): Promise => { await new Promise((resolve, reject): void => { socketServer.disconnectSockets(true); socketServer.close((err): void => { if (err) { DyFM_Log.error(`\nHTTP socket server close error`, err); reject( new DyFM_Error({ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AES-120`, message: `SocketServer close failed.`, error: err, }) ); } else { resolve(); } }); }); } ); this.systemControlsExt.httpSocketServer.init = false; this.systemControlsExt.httpsSocketServer.init = false; await DyFM_Async.delay(100); DyFM_Log.H_info(`"${this.params.name}" stopped successfully.`); } } catch (error) { throw new DyFM_Error({ ...this.__getDefaultErrorSettings('stop', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-AES-121`, }); } } /** * */ private setSocketSecurity(): void { if (this.fnLogs) console.log('\nfn:. setSocketSecurity'); this.socketServices.forEach((service: DyNTS_SocketServerService): void => { if (!this.socketSecurity) { this.socketSecurity = DyNTS_RouteSecurity[service.security]; } else if ( this.socketSecurity !== DyNTS_RouteSecurity.both && this.socketSecurity !== DyNTS_RouteSecurity[service.security] ) { this.socketSecurity = DyNTS_RouteSecurity.both; } }); } /** * Socket szerverek felállítása. By default a socket a HTTP/HTTPS porton fut (same port); * ha a service.port nincs megadva, a getPortSettings() httpPort/httpsPort értékét használjuk. * Ha a base app már listenel (httpServer/httpsServer), arra attacholjuk a Socket.IO-t, * különben külön httpSocketServer-t hozunk létre és listenelünk. * A Socket.IO path a portSettings.socketPath-ből jön (default: DyNTS_defaultSocketPath); a kliensnek ugyanazt kell használnia. */ private async setupSocketServerServices(): Promise { try { if (this.fnLogs) console.log('\nfn:. setupSocketServerServices'); const useBaseHttpServer: boolean = !!this.httpServer; const useBaseHttpsServer: boolean = !!this.httpsServer; if (this.socketSecurity !== DyNTS_RouteSecurity.secure) { if (!this.openExpress) { await this.initOpenExpress(); } if (!useBaseHttpServer && !this.httpSocketServer) { this.httpSocketServer = Http.createServer(this.openExpress); } } if (this.socketSecurity !== DyNTS_RouteSecurity.open) { if (!this.httpsServer) { if (this.cert) { await this.initSecureExpress(); } else { let errorMsg: string = `\nYou have secure socket service, but the certification paths are not set!` + `\nset...` + `\n cert: {` + `\n keyPath: FileSystem.PathLike,` + `\n certPath: FileSystem.PathLike,` + `\n }` + `\nin DynamoNTSServer - setupRoutingModules() to enable secure routes.`; errorMsg += '\n\nThe socket services setted to use secure server:'; this.socketServices.forEach((service: DyNTS_SocketServerService): void => { if (service.security !== DyNTS_SocketSecurity.open) { const effPort: number | undefined = service.port ?? this.portSettings.httpsPort; errorMsg += `\n ${service?.name} (port: ${effPort})`; } }); const error = new Error('Secure routes cannot be established!'); const errorStack: string[] = error.stack.split('\n'); errorStack.splice(1, 2); error.stack = errorStack.join('\n'); DyFM_Log.error(errorMsg); throw error; } } } await DyFM_Array.asyncForEach( this.socketServices, async (service: DyNTS_SocketServerService): Promise => { try { const effectivePort: number | undefined = service.security === DyNTS_SocketSecurity.open ? (service.port ?? this.portSettings.httpPort) : (service.port ?? this.portSettings.httpsPort); if (effectivePort === undefined) { throw new Error( `PORT NOT SET: "${service.name}". ` + `Set port in getServiceParams() or ensure getPortSettings() returns httpPort/httpsPort.` ); } if (service.security === DyNTS_SocketSecurity.open) { this.systemControlsExt.httpSocketServer.init = true; // BFR-FDPTOKENSERVICE-007 (fleet-wide realtime 502): ha a service SAJÁT (dedikált) portot // kér, ami ELTÉR a fő HTTP-porttól, a socket a SAJÁT portjára listen-el — NEM a fő szerverre // attacholódik. Enélkül base-server mellett a dedikált notif-port SOSEM kap listenert (a // socket a fő portra attacholt) → a gateway a notif-porton 502-t ad. A fő-portot MEGOSZTÓ // socket (port nincs, v. == httpPort) viselkedése VÁLTOZATLAN. A dedikált-ág a már bizonyított // non-base útvonalat tükrözi (saját Http.Server + listen). const sharesMainHttpPort: boolean = service.port === undefined || service.port === this.portSettings.httpPort; const attachToMain: boolean = useBaseHttpServer && sharesMainHttpPort; const serverForOpen: Http.Server = attachToMain ? this.httpServer : (useBaseHttpServer ? Http.createServer(this.openExpress) : this.httpSocketServer!); if (this.logSetup) console.log( `\nsocket setup (open): ${service?.name}:${effectivePort}` + (attachToMain ? ' (shared with HTTP)' : ' (dedicated port)') ); this.httpSocketSettingUpCount++; this.systemControlsExt.httpSocketServer.started = false; const socketPath: string = this.portSettings.socketPath ?? DyNTS_defaultSocketPath; this.allSocketServers.push( await service.setupSocketServer( new SocketIO.Server(serverForOpen, { path: socketPath }), DyNTS_SocketSecurity.open, (): void => { this.httpSocketSettingUpCount--; if (this.httpSocketSettingUpCount === 0) { this.systemControlsExt.httpSocketServer.started = true; } }, { attachedToExistingServer: attachToMain, effectivePort: effectivePort, } ) ); } else if (service.security === DyNTS_SocketSecurity.secure) { this.systemControlsExt.httpsSocketServer.init = true; if (this.logSetup) console.log( `\nsocket setup (secure): ${service?.name}:${effectivePort}` + (useBaseHttpsServer ? ' (shared with HTTPS)' : '') ); this.httpsSocketSettingUpCount++; this.systemControlsExt.httpsSocketServer.started = false; const socketPathSecure: string = this.portSettings.socketPath ?? DyNTS_defaultSocketPath; this.allSocketServers.push( await service.setupSocketServer( new SocketIO.Server(this.httpsServer, { path: socketPathSecure }), DyNTS_SocketSecurity.secure, (): void => { this.httpsSocketSettingUpCount--; if (this.httpsSocketSettingUpCount === 0) { this.systemControlsExt.httpsSocketServer.started = true; } }, { attachedToExistingServer: useBaseHttpsServer, effectivePort: effectivePort, } ) ); } else { const error = new Error( `INVALID Socket Service security: ${service.security} on ${service?.name}` ); const errorStack: string[] = error.stack?.split('\n'); errorStack.splice(1, 4); error.stack = errorStack.join('\n'); DyFM_Log.error(`\n${error.message}`); throw error; } } catch (error) { if (error instanceof DyFM_Error) { error.logSimple(`\nSocket Server service setup failed. (${service?.name})`) } else { DyFM_Log.error( `\nSocket Server service setup failed. (${service?.name})`, error, ); } throw error; } } ); } catch (error) { DyFM_Log.error( `\nSocket Server services setup failed. (${this.socketServices?.length} services)\n`, error, '\n' ); throw error; } } /** * MISSING Description (TODO) */ abstract getSocketServices(): DyNTS_SocketServerService[]; private __getDefaultErrorSettings( fnName: string, error: DyFM_AnyError ): DyFM_Error_Settings { return { status: (error as DyFM_Error)?.___status ?? 500, message: (error as Error)?.message ?? (error as DyFM_Error)?._message ?? `${fnName} was UNSUCCESSFUL (NTS)`, userMessage: (error as DyFM_Error)?.__userMessage ?? this.defaultErrorUserMsg, addECToUserMsg: !(error as DyFM_Error)?.__userMessage, issuerService: `${this?.constructor?.name}-DyNTS_AppExtended`, level: DyFM_ErrorLevel.fatal, error: error, }; } }