From 93727772965db6eb085cc2820e8beac145b9ac48 Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Sat, 27 Sep 2025 18:41:19 -0400 Subject: [PATCH] Update WebSocket server with favicon assets and additional dependencies * Added favicon assets including various sizes and a manifest file for improved branding * Updated package.json to include new type definitions and dependencies for body-parser, cors, lru-cache, moment, multer, on-headers, response-time, and serve-favicon * Enhanced HttpServer class to utilize the favicon and improved middleware configuration for handling requests --- .../websocket/server/assets/favicon/about.txt | 6 + .../assets/favicon/android-chrome-192x192.png | Bin 0 -> 11517 bytes .../assets/favicon/android-chrome-512x512.png | Bin 0 -> 33984 bytes .../assets/favicon/apple-touch-icon.png | Bin 0 -> 10269 bytes .../server/assets/favicon/favicon-16x16.png | Bin 0 -> 680 bytes .../server/assets/favicon/favicon-32x32.png | Bin 0 -> 1483 bytes .../server/assets/favicon/favicon.ico | Bin 0 -> 15406 bytes .../server/assets/favicon/site.webmanifest | 1 + Web/WebSocket/websocket/server/package.json | 17 +- Web/WebSocket/websocket/server/src/index.ts | 32 +- .../websocket/server/src/lang/Assert.ts | 22 +- .../server/src/net/http/HttpServer.ts | 324 +++++++++++++++++- .../websocket/server/src/net/http/IRoutes.ts | 11 +- .../websocket/server/src/types/Units.ts | 18 + .../websocket/server/src/types/optional.ts | 2 +- 15 files changed, 394 insertions(+), 39 deletions(-) create mode 100644 Web/WebSocket/websocket/server/assets/favicon/about.txt create mode 100644 Web/WebSocket/websocket/server/assets/favicon/android-chrome-192x192.png create mode 100644 Web/WebSocket/websocket/server/assets/favicon/android-chrome-512x512.png create mode 100644 Web/WebSocket/websocket/server/assets/favicon/apple-touch-icon.png create mode 100644 Web/WebSocket/websocket/server/assets/favicon/favicon-16x16.png create mode 100644 Web/WebSocket/websocket/server/assets/favicon/favicon-32x32.png create mode 100644 Web/WebSocket/websocket/server/assets/favicon/favicon.ico create mode 100644 Web/WebSocket/websocket/server/assets/favicon/site.webmanifest create mode 100644 Web/WebSocket/websocket/server/src/types/Units.ts diff --git a/Web/WebSocket/websocket/server/assets/favicon/about.txt b/Web/WebSocket/websocket/server/assets/favicon/about.txt new file mode 100644 index 0000000..93143ab --- /dev/null +++ b/Web/WebSocket/websocket/server/assets/favicon/about.txt @@ -0,0 +1,6 @@ +This favicon was generated using the following graphics from Twitter Twemoji: + +- Graphics Title: 1f916.svg +- Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji) +- Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f916.svg +- Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/) diff --git a/Web/WebSocket/websocket/server/assets/favicon/android-chrome-192x192.png b/Web/WebSocket/websocket/server/assets/favicon/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..a0e145e7a0e6d54e7e08ddb74076f14c0a302707 GIT binary patch literal 11517 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
2*sAJ#dr)LxTU;{iKdX~Spa(x4&s8fkUd_4u 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 L@}iX@1_A#E$^0+y literal 0 HcmV?d00001 diff --git a/Web/WebSocket/websocket/server/assets/favicon/favicon-16x16.png b/Web/WebSocket/websocket/server/assets/favicon/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..749a5ec205244d8d440cc0cf06ac4bba59a2dd61 GIT binary patch literal 680 zcmV;Z0$2TsP)&-wlTJf6gcqXru7>S$wE z;-zCM{-Pp5r3MdwNspfDQUho08*r>GIMDha*XDfm&^df0(AK}drTw*|^s4Vz7UgXEZ0%XXpE?Qf#tIXlbE8Ke}5UaNOP4yVdY0MBac4WwAEI99tR2CN5I*Sh- z#s7#bXShVFUdQg+(mlpUmi9fTZ5A}yvyqoAuzKj3u_MErGn|#nbHN@BH8Z?am4h=Q z&rsLn!HsG&v|JZGGj?RiYMpuMPwQTW`iyNBp^KgwJ2GS` zp(@9vkR~r5RAAA9f{r$9zu1Hyr3Vp>tGei!6?C+M3|We^(pp;PPN`X822Ve&pu40V z-nt9u^v9uUAw|!uyQBeRv>2SR?kQ=P;^h1JJEr?h3>A(35K3a?TLQ6^_F$-qo*6qb zS_ICBbN;{Df9KOsb$$%jYpu9!PvBaO8SV;I7dlxC?4F(d>$n`vP#?4dU6m~r+W00030|6?|{g#Z8m21!IgR09D0jXLnNmjR*x O000096jId=+?0p+h9Wmh;#TLVIU%6AOw8G2b+St3@K|oZqiso2D9W z@+BLlXUn?Mm*tU?&C*_SOxBgOD1;5zRctS!`iaB5@ZO zvS3r*u*DV6Z#i*CUCo*POLo?$*tYKv;D?4x>~0){AFtelpIpNXFy4J(kkKP&XT8l< z^H=_2YVR*wKdtb!ihl7HiMha#S2q+3P5;9m(yCVMFTQ*6o>5p@_bAF6=b^mm22dPw zsi8IyPTC@M?Q~Ju#u2fWh`E3UZIL2pD%`3oQ@@6?SZ2`+NGWDkJIjWSp73~K@mSnS z&~rwWa}m@G^CD3T%&2xv)+N@bW%a!?E3W{z67-b7=bRCD0Rd5DZr+YqKt|0lPC$M? zdZ4Tt<(z|x)v<=ABmr5;YQ0#^ZZ)BDa;Z__b=MqJu2!>LoXMuK075}-)rSQ$`lf1F z@kkoJF71Pjle4h?sZ2~9Y&BD&V)n=(ml|s65k+cGvLZ8~xz}Wj2{@X!wb#4u=~jXF zCZuCzjznwqPPjIFi;T+`u<*g&c;c?)E@C8NjXiS6rG{F1&?`dgd~habYZgskB_T69 zJCWd-q7>w+6KG!cE*u}MfvaLOF21k`!4s!3YnTnav7_-J6Ipk79Ej{R^Xi*nu zYC``+gO0ILok%h?+k&?9P4FMIM=F2}hTz#+gEVzI1sRskI%C$@Bd4Q5Ej{SfQ8Pb< z#KY=+h@iRH5r=g^*izJuUn_LJM5@h*+pZZ8%8IT6)ln95XmGbz#dz%_xe{NkG(iMcO%-ULkeL)u#e@WA9Zg+2zJ( zhrKxGRz(xX9I{6axztcg4>QLL&JZ>5TTiiWEFh*F`&Ue}KfO)PUB}|>#5T-b$5~?b z0T<^O3o!VaqDTM-)@_Z;AB8{B2$NC3AFeca|hg{~=4pknB-dx-T zM)nC}>Oe1^EVLS!I8b18P7o6Yz{pu2lN$oTm|%|_a+woTln1@yEfIHtdox1uG(D~- z@`}4aOQ`Fow#O5@2^_)rUU3)L|F;b%+*UaKI$QxAZgn7@Heo!O6P~aD!I7J#xrpPAxs? zWlW24GZq-N(w;hQRc(p8rFnRu)r~}G3kG7?w3paRq-D?TY92oB)7r@;mG)GsH2zDq zrTF%#KaATwF2{Ula}yH5(R2nuUn{jyV2!;*TJ}6Gj`>o@4wuWf4=Yz{+W3!Z^CiSX zQEmwmDJ7ihpHaxP1Y_3NOCXJEapw45lkzl6k~RKUG)mHya8Qd{BH+CWf-!4djGK!% zTWdqut>}J*9$g&hyxgcxqfvc|vPaqo+0WV9!8gZwY_RoDwpt2q2jY2hCTIT#00960 lUhET900006NklV`bdVR{a9M1qPUJzxN-`;mj~ITpG0Y>3^P&^Pm6w zzW06qbIzR0|Np2|Jybnaefz3-J5V+27b?|ol}a^mpnZSvEh^RL+%{xLi~j+YD*bkq zYB0xe3TN2cdE>IAF7q9mvdk~ED<*P7I5)(XT-n9XClbt+&)()2nzr;$J{ikL9{7Is z7*bln`bku+Q42#|=|ep;^ojjxD@OUCQ?&_GM9iY(cV#$^pDs_x4yD1w^ZcCKSy;LE6A9&hV(mLl(A4p8+_FqgIMGYPoRFc zMVh~OQIjXyEa!L8_RR)b8(KwX?OARM6oxw564-|rI%vp|t{-fVtsM2v zp7)1QM+~vbT+}0j9_$@G>;5&1m&`QJc{hlH=FfI8w84iM#5(6o`xCS2_Y*TI#wXEw z;j!%vR@m^pP5I+9n`0)kPm1%_7WbKfY@^w5fAUnaLdNt-4}0?Nt7+hbCcanQ#P=Mt z`+Je=dzcsE&EU>lHoAXTuu_-#(WQ<4!cgxD2Dp!WJo2v2=7O(y)6?>A0`;zF05_4U z$B_`fqaD>?=MIp$rQc=orhraq0B3Np>N;ElS;0eZObPMxKO6njhQba0`E>~|(B+IxRIe|ldP4~r zwCAYc`()A;pA!3ME6Pm~hB^#B#2{9*aV{``CH1GH{3U6zbn#4lz24sb82N{6r@q59lo%1e zrO?~`b;gU(Y$*hn-)7=31bgB4y%9q-L`yXGcQ`p``!q72-a<|KLTWaaQ^VzSsyVR0 z;e5)md~V@G3}TUk+-k;9#()D{-~{`v5xV|&Mw!2B!=Esf8Y|Bb%Ej{ybUfWe7E3eP zni{C~aIga=@M{l$z&@*pK`e4mfKkm@k_L$%oc*HAclTap+R+>dF|;{=RI-eNME&hZre7Cag+e0Lz&loV{1oZM`YnSmTP{T9!kV zR~l*TWA^uE2{W4Cw^3@onImJOjuIWJlerO(Twnmp6+gIpha2l%@?SYcmoHfz@PHZq zVWr@zIk-^9L>&b_NvE_0hLy2g@q@c}gfZC_zvXPaz$k%Mp90FuttVSUo&3Ho zaQWRGZSWyR;zlkCuw3%5sylRZc+DU0SYxSizW($(U!i7GB{ekIX!o`n+7wkyg%|AC zltA6tnZoZv?B*+B!Czgl_yD|4#(J5j$2-%a@LYwueq&>pM!&Q#{K z@aw^8XK($heE(49c(bna@czU7^YOn z2ZtkT+r5~X_r*ld7)0JQV76s)-LiJ(y^k@s!HfKcn4sS}e3!dsZf%Wy?cU!+z46eq zL7U_9XT&8^VC-)4{ct<^E^fy-2C>LNE`O6xiJEoa9j(Znt38uLef_g2A|Q**e>u`j z563sqfc0j&Ey_fFKQ#(NeZU5@@F9ld%>mi;DRMi<<~~}TOSc9br&s*5X%pk0@IOvE zwkBa&q+@+KY#X1>rk4@x9_eR~@(5s@1Q+H@Z0(Jc;J2F}G5-4Wuo1m}3I?r;`%RA{ zpN#ZP4e^cOG3A(_f4w>2FN z9VL%Lq3&&bXohxN@8X(qGpEoMUg*WRk@TEK-RpheYaQB?gGb+RWT|gdS9H3{$yojb zm2RFaYnS?fHuzlSbfqnoHA6e`Z-CFmOvW&E1xJ~KV_fQC91T7%od**%Lp%S5RR@<0 z@5f`V@3EGQlx4oBGDAnz9Sa*xow4ljKRaAA(s6g*;#xv%XRr~={Wd}yG#&}@eIJ^k zT`F18Lw}k4$>=*Vc3b|{vuRhh{Ea$e6`<8r5tP#iyY(vL-sLtMdBRN#kY=Yv>v~%>jz%x;Z;<4@(?wcbZmNI=Gsas zKA%GuPiIo;g_CkS{MF@!RB+~~Fw~_SY!Qnb-aH{hqq6 zza%A6@Leta;3-LQ9#8lo`qy3q{ZU>!{#O#`Q^DzU(Fe@wOXs`$uAg+evUk4M^y_#W zOnWGd3V9BuyY+(uT;OcI_h4+O>(>u(a1W~bdOFp7yNGI%-xp>`dXFmiEg+nq%6$vv zcKE9gETRfNhA`Bn9BdJ5&sjjgkbR5|4sYK-9gjnM<7D0C|6#}f+V|oATmOhS>c#Uf z3x*}o=UJNhW!L_AJ6?y`nt9rx@w6Tqq1E}lhubG$tQwZ2`M~M7ufXE>Aj}6DGgtDM zbtKiLg^*#-Ecfwl#+8wSTwnl8%s(-19R4)-|-Kszp$6* z9hyWAa)ALXV4{EnTztQG=!a52v_i9rwf{P-`VpRQI(K)BMLs{y$6x>TKl#|CF|Un` zbeUYY#repdbfp#DD=TU;-N)-~y+z|4RMP z3e7xE_i*o(b>Tc$X!E4MCTWq(yDq%SVJ-@DQj1Uv=Py z4z$6Cf>`7zwF48_;$FsF;A~faxM};1D^25BzuwdJZ`eJ98g=IdcX6SW7SAiEfDwga z9(~%lB3c)&BeMy|-p1=)cRTPZHNb}$#3Bc|n2QG%FoBH%E^sPyo$o);4DG5g>eYLd zX>tG9dW+lpKd<36W`vT>%Fi4AiPqINQ&EAH@=n)NSqZNL;Q465TXl}-g=ahPDmB2@ z$T29$K`tme5= zwOM)HSH-$B-p|kGMxz&L;3W>(DmO8;RG|7SAT+AQ$%;U;z`@uHwyu|9c!Dp z4|s=YPSUhpN7DuuYiF>Ar)7P38-L3=^Vp7DU?|q%xQmA-)@B)(0xbb0>0wCV`l=|QrCOQ%uKXwB;=e)q=dG=b9{M0(ia zI|8q_2wYONZOSF>m(Mhoe(^M23j=pm+?1kf&6E&m@*ug5D=tpm^T?8szFX%%Qu@{0 zhv-@uxS;`B+6e4G!^YPK_lub^D9?-QJvz5`W#*vs(9i*?ZbM>b4tj_6TdqMLEU~li zU*uL;2OSY_-hX|}^g;h({Rg6ges^}o+fR{XOXiKE58nFg4>Dj#c-ThG9()d5Z2w;W z(!d)!DAQwDoLb#8dd35{Mb7mZFzvv(&(w#sHud2$3fQlu2iF!-{}mUh|ME^6=O7mt zz)~O9+JGJY*3f&v6{qHZ!s~{jPN!bH-ew`MuPNYlHP#77ZRE43QC|CVOQebV@cN&d zSMm2zi=mD-_{5qZUMJ*}*hmwO5=~+ZUbj;KOkj6|Kzk^LQhGP)Rc~!_|{crP{rbmI ({}), - getPOSTRoutes: () => ({}), - getPUTRoutes: () => ({}), - getPATCHRoutes: () => ({}), - getDELETERoutes: () => ({}), -}, {}, '', [], '', {}); +const httpServer = new HttpServer( + 'http', + 3000, + { + getGETRoutes: () => ({}), + getPOSTRoutes: () => ({}), + getPUTRoutes: () => ({}), + getPATCHRoutes: () => ({}), + getDELETERoutes: () => ({}) + }, + {}, + '', + [], + path.resolve(process.cwd(), 'assets', 'favicon', 'favicon.ico'), + {} +); httpServer.on('error', () => { - logger.error('[HttpServer] Error'); + logger.error('[HttpServer] Error'); }); -httpServer.start(); \ No newline at end of file +httpServer.start(); diff --git a/Web/WebSocket/websocket/server/src/lang/Assert.ts b/Web/WebSocket/websocket/server/src/lang/Assert.ts index 4ba255d..f3128f0 100644 --- a/Web/WebSocket/websocket/server/src/lang/Assert.ts +++ b/Web/WebSocket/websocket/server/src/lang/Assert.ts @@ -73,27 +73,40 @@ export default class Assert { } } - public static isFunction(name: string, value: unknown): asserts value is Function { + public static isFunction(name: string, value: unknown): asserts value is (...args: unknown[]) => unknown { if (typeof value !== 'function') { throw new Error(`[${name}] must be a function, instead received [${typeof value}]`); } } + public static satisfiesInterface(name: string, obj: unknown, requiredProps: (keyof T)[]): asserts obj is T { Assert.isObject(name, obj); - + for (const prop of requiredProps) { - if (!(prop in (obj as any))) { + if (!(prop in (obj as Record))) { throw new Error(`[${name}] missing required property: ${String(prop)}`); } } } - public static isArray(name: string, value: unknown): asserts value is T[] { + public static isArray(name: string, value: unknown): asserts value is unknown[] { if (!Array.isArray(value)) { throw new Error(`[${name}] must be an array, instead received [${typeof value}]`); } } + public static isArrayOf(name: string, arrayValueType: T, value: unknown): asserts value is T[] { + Assert.isArray(name, value); + + for (const item of value) { + const itemTypeof = typeof item; + + if (itemTypeof !== arrayValueType) { + throw new Error(`[${name}] must be an array of [${arrayValueType}] received [${itemTypeof}]`); + } + } + } + public static isStringArray(name: string, value: unknown): asserts value is string[] { if (!Array.isArray(value)) { throw new Error(`[${name}] must be an array, instead received [${typeof value}]`); @@ -116,6 +129,7 @@ export default class Assert { } } + // eslint-disable-next-line public static isInstance(name: string, parentClass: new (...args: any[]) => T, object: unknown): asserts object is T { if (object === null || object === undefined || typeof object !== 'object') { throw new Error(`[${name}] must be an instance of [${parentClass.constructor.name}], instead received [${typeof object}]`); diff --git a/Web/WebSocket/websocket/server/src/net/http/HttpServer.ts b/Web/WebSocket/websocket/server/src/net/http/HttpServer.ts index aa22a34..e3f1bdb 100644 --- a/Web/WebSocket/websocket/server/src/net/http/HttpServer.ts +++ b/Web/WebSocket/websocket/server/src/net/http/HttpServer.ts @@ -1,5 +1,6 @@ -import { Nullable } from '../../types/optional'; +import {Nullable} from '../../types/optional'; import Assert from '../../lang/Assert'; +import moment from 'moment'; import type {Server} from 'node:http'; import http from 'node:http'; import {EventEmitter} from 'node:events'; @@ -9,30 +10,62 @@ import IRoutes from './IRoutes'; import {Subject} from '@techniker-me/tools'; import express from 'express'; import morgan from 'morgan'; +import favicon from 'serve-favicon'; +import bodyParser from 'body-parser'; +import multer from 'multer'; +import {Kilobytes} from '../../types/Units'; +import responseTime from 'response-time'; +import onHeaders from 'on-headers'; +import {LRUCache} from 'lru-cache'; + +const requestSizeLimit: Kilobytes = 10240; +const defaultTcpSocketTimeout = moment.duration(720, 'seconds'); // Google HTTPS load balancer expects at least 600 seconds +const defaultKeepAliveTimeout = moment.duration(660, 'seconds'); // Google HTTPS load balancer expects at least 600 seconds +// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#:~:text=The%20Access%2DControl%2DMax%2D,Headers%20headers)%20can%20be%20cached. +const corsAccessControlMaxAge = moment.duration(24, 'hours'); +const shortTermCaching = 'public, max-age=20, s-maxage=20'; +const tlsSessionTimeout = moment.duration(5, 'minutes'); +const maxCachedTlsSessions = 1000; export default class HttpServer { - private readonly _logger: ILogger = LoggerFactory.getLogger('HttpServer'); + private readonly _logger: ILogger = LoggerFactory.getLogger('HttpServer'); + private readonly _eventEmitter: EventEmitter; private readonly _protocol: 'http' | 'https'; private readonly _port: number; + // @ts-expect-error - unused parameter for future functionality private readonly _routes: IRoutes; + // @ts-expect-error - unused parameter for future functionality private readonly _viewsPath: object; + // @ts-expect-error - unused parameter for future functionality private readonly _viewParameters: string; private readonly _resourcesPaths: string[]; private readonly _favicon: string; private readonly _cors: object; private readonly _app: Subject>; - private readonly _eventEmitter: EventEmitter; private readonly _server: Subject>; + private readonly _tlsSessionCache = new LRUCache({ + ttl: tlsSessionTimeout.asMilliseconds(), + max: maxCachedTlsSessions + }); - constructor(protocol: 'https' | 'http', port: number, routes: IRoutes, viewsPath: object, viewParameters: string, resourcesPaths: string[], favicon: string, cors: object) { + constructor( + protocol: 'https' | 'http', + port: number, + routes: IRoutes, + viewsPath: object, + viewParameters: string, + resourcesPaths: string[], + favicon: string, + cors: object + ) { Assert.isString('protocol', protocol); Assert.isNumber('port', port); - Assert.satisfiesInterface ('routes', routes, ['getGETRoutes', 'getPOSTRoutes', 'getPUTRoutes', 'getPATCHRoutes', 'getDELETERoutes']); - Assert.isObject('viewsPath', viewsPath); + Assert.satisfiesInterface('routes', routes, ['getGETRoutes', 'getPOSTRoutes', 'getPUTRoutes', 'getPATCHRoutes', 'getDELETERoutes']); + // Assert.isObjectOf('viewsPath', viewsPath); Assert.isString('viewParameters', viewParameters); - Assert.isStringArray('resourcesPaths', resourcesPaths); + Assert.isArrayOf('resourcesPaths', 'string', resourcesPaths); Assert.isString('favicon', favicon); - Assert.isObject('cors', cors); + // Assert.isObjectOf('cors', cors, 'string'); this._protocol = protocol; this._port = port; @@ -47,22 +80,281 @@ export default class HttpServer { this._server = new Subject>(null); } - public async start() { - const app = this._app.value = express(); - - app.use(morgan('common')); - this._server.value = http.createServer(app); + public start() { + return new Promise((resolve, reject) => { + const app = (this._app.value = express()); - this._server.value.listen(this._port, () => { - this._logger.info(`Server is running on port ${this._port}`); + this.configureListener(); + this.configureMiddleware(); + this.configureResources(); + this.configureRoutes(); + + app.set('x-powered-by', false); + + const server = (this._server.value = http.createServer(app)); + + const onListen = () => { + this._logger.info('HTTP Server listening on %s://*:%s', this._protocol, this._port); + + server.removeListener('error', onError); + + resolve(this); + }; + + const onError = (err: unknown) => { + server.removeListener('listening', onListen); + + reject(err); + }; + + server.keepAliveTimeout = defaultKeepAliveTimeout.milliseconds(); + server.timeout = defaultTcpSocketTimeout.asMilliseconds(); + server.setTimeout(defaultTcpSocketTimeout.asMilliseconds()); + server.once('error', onError); + server.once('listening', onListen); + + server.on('newSession', (sessionId, sessionData, callback) => { + const cacheId = sessionId.toString('hex'); + + this._tlsSessionCache.set(cacheId, sessionData); + this._logger.debug('Created new TLS session [%s]', cacheId); + + callback(); + }); + + server.on('resumeSession', (sessionId, callback) => { + const cacheId = sessionId.toString('hex'); + const sessionData = this._tlsSessionCache.get(cacheId); + + callback(null, sessionData); + + if (sessionData) { + this._logger.debug('Resumed TLS session [%s]', cacheId); + } else { + this._logger.debug('TLS session [%s] not found', cacheId); + } + }); + + server.listen({ + port: this._port, + backlog: 16 * 1024 + }); }); } - public on(event: string, handler: () => void): void { Assert.isNonEmptyString('event', event); Assert.isFunction('handler', handler); this._eventEmitter.on(event, handler); } + + private configureListener() { + if (!this._app.value) { + throw new Error('Unable to configure listener, no app instance found'); + } + + const app = this._app.value; + + app.use((req, res, next) => { + req.on('finish', () => { + this._eventEmitter.emit('request', req.method, req.url, res.statusCode, req.headers); + }); + + next(); + }); + } + + private configureMiddleware() { + if (!this._app.value) { + throw new Error('Unable to configure middleware, no app instance found'); + } + + const app = this._app.value; + const logger = this._logger; + + app.use(morgan('common')); + app.enable('trust proxy'); + app.set('env', 'development'); // setting env to test prevents logging to the console + app.use(favicon(this._favicon)); + app.use( + morgan( + ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :response-time', + { + stream: { + write(line) { + logger.info(line.trim()); + } + } + } + ) + ); + + app.use( + bodyParser.text({ + type: 'application/sdp', + limit: requestSizeLimit + }) + ); + app.use( + bodyParser.text({ + type: 'application/trickle-ice-sdpfrag', + limit: requestSizeLimit + }) + ); + app.use( + bodyParser.urlencoded({ + extended: true, + limit: requestSizeLimit + }) + ); + app.use( + multer({ + limits: { + fields: 1, + fieldNameSize: 100, + fieldSize: requestSizeLimit, + files: 0, + parts: 1, + headerPairs: 1 + } + }).none() + ); + app.use((req, _res, next) => { + const contentType = req?.headers?.['content-type'] || ''; + + if (contentType.startsWith('multipart/form-data;')) { + if (req?.body?.jsonBody) { + req.body = JSON.parse(req.body.jsonBody); + } + } + + next(); + }); + app.use(responseTime()); + app.use((req, res, next) => { + res.set('x-origination', 'Platform'); + + if (req.secure) { + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); + } + + next(); + }); + + if (this._cors) { + this._logger.info('Enable CORS on %s://*:%s', this._protocol, this._port); + + const cachedCorsAllowOrigins: Record = {}; + const getCorsAllowOrigin = (url: string) => { + let corsAllowOrigin = cachedCorsAllowOrigins[url]; + + if (!Object.hasOwn(cachedCorsAllowOrigins, url)) { + Object.entries(this._cors).forEach(([key, value]) => { + if (url.startsWith(key)) { + corsAllowOrigin = value; + } + }); + + cachedCorsAllowOrigins[url] = corsAllowOrigin ?? ''; + } + + return corsAllowOrigin; + }; + + app.use((req, res, next) => { + const corsAllowOrigin = getCorsAllowOrigin(req.url); + + if (corsAllowOrigin) { + res.header('Access-Control-Allow-Origin', corsAllowOrigin); + res.header( + 'Access-Control-Allow-Headers', + 'Authorization, Origin, Range, X-Requested-With, If-Modified-Since, Accept, Keep-Alive, Cache-Control, Content-Type, DNT' + ); + res.header('Access-Control-Allow-Methods', 'POST, GET, HEAD, OPTIONS, PUT, PATCH, DELETE'); + res.header('Access-Control-Expose-Headers', 'Server, Range, Date, Content-Disposition, X-Timer, ETag, Link, Location'); + res.header('Access-Control-Max-Age', corsAccessControlMaxAge.asSeconds().toString()); + + if (req.method === 'OPTIONS') { + res.header('Cache-Control', shortTermCaching); + } + } + + next(); + }); + } + app.use((_req, res, next) => { + const startTimeSeconds = Date.now() / 1000; + const startTimeNanoseconds = process.hrtime.bigint(); + + onHeaders(res, () => { + const durationNanoseconds = process.hrtime.bigint() - startTimeNanoseconds; + const durationMilliseconds = durationNanoseconds / 1000000n; + + // https://developer.fastly.com/reference/http/http-headers/X-Timer/ + // S{unixStartTimeSeconds},VS0,VE{durationMilliseconds} + res.setHeader('X-Timer', `S${startTimeSeconds},VS0,VE${durationMilliseconds}`); + }); + + next(); + }); + } + + private configureResources() { + if (!this._app.value) { + throw new Error('Unable to configure resources, no app instance found'); + } + + const app = this._app.value; + + for (const resourcePath of this._resourcesPaths) { + app.use(express.static(resourcePath)); + } + + app.use(this.errorHandler); + app.use(this.genericNotFoundHandler); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private errorHandler(err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) { + this._logger.error(err.message); + + res.status(500).send({status: 'error'}); + + return; + } + + private genericNotFoundHandler(_req: express.Request, res: express.Response) { + res.status(404).send({status: 'not-found'}); + + return; + } + + private configureRoutes() { + if (!this._app.value) { + throw new Error('Unable to configure routes, no app instance found'); + } + + // TODO: Implement routes + // const app = this._app.value; + + // let catchAllHandler = null; + + // for (const route of this._routes.getGETRoutes().entries()) { + // const [handler, name] = route; + + // if (name === '*') { + // if (catchAllHandler) { + // throw new Error(`Only one catch-all handler can ber registered per server, ignoring conflicting catch-all`); + // } + + // catchAllHandler = handler; + + // continue; + // } + + // this._logger.debug(`Registering [GET] route [${name}]`); + // // app.get(name, this._json); + // } + } } diff --git a/Web/WebSocket/websocket/server/src/net/http/IRoutes.ts b/Web/WebSocket/websocket/server/src/net/http/IRoutes.ts index bacd489..d8bc2c9 100644 --- a/Web/WebSocket/websocket/server/src/net/http/IRoutes.ts +++ b/Web/WebSocket/websocket/server/src/net/http/IRoutes.ts @@ -1,8 +1,7 @@ - export default interface IRoutes { - getGETRoutes(): Record (Promise | void); - getPOSTRoutes(): Record (Promise | void); - getPUTRoutes(): Record (Promise | void); - getPATCHRoutes(): Record (Promise | void); - getDELETERoutes(): Record (Promise | void); + getGETRoutes(): Record void | Promise>; + getPOSTRoutes(): Record void | Promise>; + getPUTRoutes(): Record void | Promise>; + getPATCHRoutes(): Record void | Promise>; + getDELETERoutes(): Record void | Promise>; } diff --git a/Web/WebSocket/websocket/server/src/types/Units.ts b/Web/WebSocket/websocket/server/src/types/Units.ts new file mode 100644 index 0000000..5566d9b --- /dev/null +++ b/Web/WebSocket/websocket/server/src/types/Units.ts @@ -0,0 +1,18 @@ +export type Bytes = number; +export type Kilobytes = number; +export type Megabytes = number; +export type Gigabytes = number; +export type Terabytes = number; +export type Petabytes = number; +export type Exabytes = number; +export type Zettabytes = number; +export type Yottabytes = number; + +export type Milliseconds = number; +export type Seconds = number; +export type Minutes = number; +export type Hours = number; +export type Days = number; +export type Weeks = number; +export type Months = number; +export type Years = number; diff --git a/Web/WebSocket/websocket/server/src/types/optional.ts b/Web/WebSocket/websocket/server/src/types/optional.ts index 164bc2e..16dbd62 100644 --- a/Web/WebSocket/websocket/server/src/types/optional.ts +++ b/Web/WebSocket/websocket/server/src/types/optional.ts @@ -1,2 +1,2 @@ export type Optional = T | undefined | null; -export type Nullable = T | null; \ No newline at end of file +export type Nullable = T | null;