diff --git a/backend/src/core/environment/environment.spec.ts b/backend/src/core/environment/environment.spec.ts index 933f2aa6..7cb1a22a 100644 --- a/backend/src/core/environment/environment.spec.ts +++ b/backend/src/core/environment/environment.spec.ts @@ -31,8 +31,6 @@ describe('Environment', () => { process.env.SOCKET_ROOT = '/socket.io'; - process.env.CHARSET = 'utf8'; - const env = await getFreshEnvironmentInstance(); const { Environment } = await import('./environment'); @@ -47,15 +45,11 @@ describe('Environment', () => { process.env.SOCKET_ROOT = '/socket.io'; - process.env.CHARSET = 'utf8'; - const env = await getFreshEnvironmentInstance(); expect(env.telnetHost).toBe('localhost'); expect(env.telnetPort).toBe(3000); - - expect(env.charset).toBe('utf8'); }); it('should handle optional TLS configuration', async () => { @@ -65,8 +59,6 @@ describe('Environment', () => { process.env.SOCKET_ROOT = '/socket.io'; - process.env.CHARSET = 'utf8'; - process.env.TELNET_TLS = 'true'; const env = await getFreshEnvironmentInstance(); @@ -81,8 +73,6 @@ describe('Environment', () => { process.env.SOCKET_ROOT = '/socket.io'; - process.env.CHARSET = 'utf8'; - process.env.TELNET_TLS = 'TRUE'; const env = await getFreshEnvironmentInstance(); @@ -97,31 +87,17 @@ describe('Environment', () => { process.env.SOCKET_ROOT = '/socket.io'; - process.env.CHARSET = 'utf8'; - const env = await getFreshEnvironmentInstance(); expect(env.telnetTLS).toBe(false); }); - it('should use default charset if not set', async () => { - process.env.TELNET_HOST = 'localhost'; - - process.env.TELNET_PORT = '3000'; - - process.env.SOCKET_ROOT = '/socket.io'; - - const env = await getFreshEnvironmentInstance(); - - expect(env.charset).toBe('utf8'); - }); - it('should set projectRoot correctly', async () => { process.env.TELNET_HOST = 'localhost'; process.env.TELNET_PORT = '3000'; - process.env.CHARSET = 'utf8'; + process.env.CHARSET = 'utf-8'; process.env.SOCKET_ROOT = '/socket.io'; diff --git a/backend/src/core/environment/environment.ts b/backend/src/core/environment/environment.ts index 2ec7e5c4..4178ed7f 100644 --- a/backend/src/core/environment/environment.ts +++ b/backend/src/core/environment/environment.ts @@ -17,10 +17,10 @@ export class Environment implements IEnvironment { public readonly telnetHost: string; public readonly telnetPort: number; public readonly telnetTLS: boolean; - public readonly charset: string; public readonly projectRoot: string; public readonly socketRoot: string; public readonly socketTimeout: number; + public readonly environment: 'production' | 'development'; /** * Private constructor to enforce singleton pattern. @@ -46,12 +46,22 @@ export class Environment implements IEnvironment { this.socketRoot = String(getEnvironmentVariable('SOCKET_ROOT')); - this.charset = String(getEnvironmentVariable('CHARSET', false, 'utf8')); - this.socketTimeout = Number( getEnvironmentVariable('SOCKET_TIMEOUT', false, '900000'), ); + const environment = String( + getEnvironmentVariable('ENVIRONMENT', false, 'production'), + ).toLocaleLowerCase(); + + if (environment !== 'production' && environment !== 'development') { + throw new Error( + 'Environment variable "ENVIRONMENT" must be either "production" or "development" or unset.', + ); + } + + this.environment = environment; + this.projectRoot = resolveModulePath('../../../main.js'); logger.info('[Environment] initialized', this); diff --git a/backend/src/core/environment/types/environment-keys.ts b/backend/src/core/environment/types/environment-keys.ts index f7680bf1..14e35fe2 100644 --- a/backend/src/core/environment/types/environment-keys.ts +++ b/backend/src/core/environment/types/environment-keys.ts @@ -5,5 +5,5 @@ export type EnvironmentKeys = | 'TELNET_PORT' // Required. Example '23' | 'TELNET_TLS' // Optional. Defaults to 'false' | 'SOCKET_TIMEOUT' // in milliseconds | default: 900000 (15 min) | determines how long messages are buffed for the disconnected frontend and when the telnet connection is closed - | 'SOCKET_ROOT' // Required. Example '/socket.io' - | 'CHARSET'; // Optional. Defaults to 'utf8'; + | 'SOCKET_ROOT' + | 'ENVIRONMENT'; diff --git a/backend/src/core/environment/types/environment.ts b/backend/src/core/environment/types/environment.ts index 21f4653f..2ee550d4 100644 --- a/backend/src/core/environment/types/environment.ts +++ b/backend/src/core/environment/types/environment.ts @@ -6,27 +6,7 @@ export interface IEnvironment { readonly projectRoot: string; readonly socketRoot: string; - readonly charset: string; - readonly socketTimeout: number; - // backend: { - // host: string; - // port: number; - // }; - // frontend: { - // host: string; - // port: number; - // }; - // mudrpc: { - // socketfile: string; - // }; - // webmud: { - // mudname: string; - // autoConnect: boolean; - // autoLogin: boolean; - // autoUser: string; - // autoToken: string; - // localEcho: boolean; - // }; + readonly environment: 'production' | 'development'; } diff --git a/backend/src/core/middleware/use-rest-endpoints.ts b/backend/src/core/middleware/use-rest-endpoints.ts new file mode 100644 index 00000000..dd195718 --- /dev/null +++ b/backend/src/core/middleware/use-rest-endpoints.ts @@ -0,0 +1,53 @@ +import { Express, Request, Response } from 'express'; + +import { TelnetControlSequences } from '../../features/telnet/types/telnet-control-sequences.js'; +import { TelnetOptions } from '../../features/telnet/types/telnet-options.js'; +import { logger } from '../../shared/utils/logger.js'; +import { SocketManager } from '../sockets/socket-manager.js'; + +export const useRestEndpoints = ( + app: Express, + socketManager: SocketManager, +) => { + app.use('/api/info', (req: Request, res: Response) => { + logger.info(`[Middleware] [Rest] requested /api/info`); + + const connections = Object.entries(socketManager.mudConnections).flatMap( + ([connectionKey, con]) => { + const negotiations = Object.entries(con.telnet?.negotiations || {}).map( + ([negotiationKey, negotiations]) => { + const key = Number(negotiationKey); + + return { + code: TelnetOptions[key], + ...{ + server: negotiations?.server + ? TelnetControlSequences[negotiations?.server] + : {}, + }, + ...{ + client: negotiations?.client + ? TelnetControlSequences[negotiations?.client] + : {}, + }, + ...negotiations?.subnegotiation, + }; + }, + ); + + return { + connection: connectionKey, + telnet: { + connected: + con.telnet?.isConnected === undefined + ? 'no instance' + : con.telnet.isConnected, + negotiations, + }, + }; + }, + ); + + res.send(connections); + }); +}; diff --git a/backend/src/core/middleware/use-sockets.ts b/backend/src/core/middleware/use-sockets.ts index b62d5e01..5790ff5d 100644 --- a/backend/src/core/middleware/use-sockets.ts +++ b/backend/src/core/middleware/use-sockets.ts @@ -8,7 +8,7 @@ export const useSockets = ( httpServer: HttpServer | HttpsServer, environment: Environment, ) => { - new SocketManager(httpServer, { + return new SocketManager(httpServer, { telnetHost: environment.telnetHost, telnetPort: environment.telnetPort, useTelnetTls: environment.telnetTLS, diff --git a/backend/src/core/sockets/socket-manager.ts b/backend/src/core/sockets/socket-manager.ts index 536d438f..472ca7f3 100644 --- a/backend/src/core/sockets/socket-manager.ts +++ b/backend/src/core/sockets/socket-manager.ts @@ -6,6 +6,7 @@ import { TelnetClient } from '../../features/telnet/telnet-client.js'; import { TelnetControlSequences } from '../../features/telnet/types/telnet-control-sequences.js'; import { TelnetOptions } from '../../features/telnet/types/telnet-options.js'; import { logger } from '../../shared/utils/logger.js'; +import { mapToServerEncodings } from '../../shared/utils/supported-encodings.js'; import { Environment } from '../environment/environment.js'; import { ClientToServerEvents } from './types/client-to-server-events.js'; import { InterServerEvents } from './types/inter-server-events.js'; @@ -17,7 +18,7 @@ export class SocketManager extends Server< ServerToClientEvents, InterServerEvents > { - private readonly mudConnections: MudConnections = {}; + public readonly mudConnections: MudConnections = {}; public constructor( server: HttpServer | HttpsServer, @@ -126,10 +127,7 @@ export class SocketManager extends Server< return; } - telnetClient.sendMessage( - // data.toString(Environment.getInstance().charset) + '\r\n', - `${data}\r\n`, - ); + telnetClient.sendMessage(`${data}\r\n`); }); socket.on('mudConnect', () => { @@ -149,7 +147,34 @@ export class SocketManager extends Server< ); telnetClient.on('data', (data: string | Buffer) => { - socket.emit('mudOutput', data.toString('utf8')); + const mudCharset = + this.mudConnections[socket.id].telnet?.negotiations[ + TelnetOptions.TELOPT_CHARSET + ]?.subnegotiation?.clientOption; + + if (mudCharset === undefined) { + logger.warn( + '[Socket-Manager] [Client] no charset negotiated before sending data. Default to utf-8', + ); + + socket.emit('mudOutput', data.toString('utf-8')); + + return; + } + + const charset = mapToServerEncodings(mudCharset); + + if (charset !== null) { + socket.emit('mudOutput', data.toString(charset)); + + return; + } + + logger.warn( + `[Socket-Manager] [Client] unknown charset ${mudCharset}. Default to utf-8`, + ); + + socket.emit('mudOutput', data.toString('utf-8')); }); telnetClient.on('close', () => { @@ -163,7 +188,7 @@ export class SocketManager extends Server< }); telnetClient.on('negotiationChanged', (negotiation) => { - logger.info( + logger.verbose( `[Socket-Manager] [Client] ${socket.id} telnet negotiation changed. Emitting 'negotiationChanged'`, negotiation, ); diff --git a/backend/src/features/telnet/telnet-client.ts b/backend/src/features/telnet/telnet-client.ts index 7ab749db..ba096aa3 100644 --- a/backend/src/features/telnet/telnet-client.ts +++ b/backend/src/features/telnet/telnet-client.ts @@ -11,7 +11,10 @@ import tls from 'tls'; import { logger } from '../../shared/utils/logger.js'; import { TelnetControlSequences } from './types/telnet-control-sequences.js'; import { TelnetNegotiations } from './types/telnet-negotiations.js'; +import { TelnetOptionHandler } from './types/telnet-option-handler.js'; import { TelnetOptions } from './types/telnet-options.js'; +import { handleCharsetOption } from './utils/handle-charset-option.js'; +import { handleEchoOption } from './utils/handle-echo-option.js'; import { TelnetSocketWrapper } from './utils/telnet-socket-wrapper.js'; type TelnetClientEvents = { @@ -20,8 +23,8 @@ type TelnetClientEvents = { negotiationChanged: [ { option: TelnetOptions; - server: TelnetControlSequences; - client: TelnetControlSequences; + server?: TelnetControlSequences; + client?: TelnetControlSequences; }, ]; }; @@ -30,18 +33,20 @@ type TelnetClientEvents = { * Represents a client for handling telnet communication tailored for MUD games. */ export class TelnetClient extends EventEmitter { - private negotiations: TelnetNegotiations = {}; + private _negotiations: TelnetNegotiations = {}; private readonly telnetSocket: TelnetSocket; private connected: boolean = false; + private optionsHandler: Map; + public get isConnected(): boolean { return this.connected; } - public get getNegotiations(): TelnetNegotiations { - return { ...this.negotiations }; + public get negotiations(): TelnetNegotiations { + return { ...this._negotiations }; } /** @@ -65,6 +70,13 @@ export class TelnetClient extends EventEmitter { bufferSize: 65536, }); + this.optionsHandler = new Map([ + [TelnetOptions.TELOPT_CHARSET, handleCharsetOption(this.telnetSocket)], + [TelnetOptions.TELOPT_ECHO, handleEchoOption(this.telnetSocket)], + ]); + + this.telnetSocket.on('connect', () => this.handleConnect()); + this.telnetSocket.on('close', (hadErrors) => this.handleClose(hadErrors)); this.telnetSocket.on('do', (option) => this.handleDo(option)); @@ -83,8 +95,6 @@ export class TelnetClient extends EventEmitter { this.emit('data', chunkData); }); - logger.info(`[Telnet-Client] Created`); - this.connected = true; } @@ -100,6 +110,10 @@ export class TelnetClient extends EventEmitter { this.connected = false; } + private handleConnect(): void { + logger.info(`[Telnet-Client] Connected`); + } + private handleClose(hadErrors: boolean): void { this.connected = false; @@ -107,216 +121,202 @@ export class TelnetClient extends EventEmitter { } private handleDo(option: TelnetOptions): void { - switch (option) { - case TelnetOptions.TELOPT_CHARSET: { - this.telnetSocket.writeWill(option); + this.updateNegotiations(option, { + server: TelnetControlSequences.DO, + }); - this.updateNegotiations(option, { - server: TelnetControlSequences.DO, - client: TelnetControlSequences.WILL, - }); + const handler = this.optionsHandler.get(option); - return; - } + const handlerResult = handler?.handleDo(); - case TelnetOptions.TELOPT_TM: { - this.updateNegotiations(option, { - server: TelnetControlSequences.DO, - client: TelnetControlSequences.WILL, - }); + if (handlerResult !== undefined) { + this.updateNegotiations(option, { + client: handlerResult, + }); + } else { + this.telnetSocket.writeWont(option); - this.telnetSocket.writeWill(option); + this.updateNegotiations(option, { + client: TelnetControlSequences.WONT, // we answer negatively but we should WILL everything possible + }); + } - return; - } + // switch (option) { - case TelnetOptions.TELOPT_NAWS: { - this.updateNegotiations(option, { - server: TelnetControlSequences.DO, - client: TelnetControlSequences.WILL, - }); + // case TelnetOptions.TELOPT_TM: { + // this.updateNegotiations(option, { + // server: TelnetControlSequences.DO, + // client: TelnetControlSequences.WILL, + // }); - this.telnetSocket.writeWill(option); + // this.telnetSocket.writeWill(option); - return; - } - } + // return; + // } - this.updateNegotiations(option, { - server: TelnetControlSequences.DO, - client: TelnetControlSequences.WONT, - }); + // case TelnetOptions.TELOPT_NAWS: { + // this.updateNegotiations(option, { + // server: TelnetControlSequences.DO, + // client: TelnetControlSequences.WILL, + // }); + + // this.telnetSocket.writeWill(option); - this.telnetSocket.writeWont(option); + // return; + // } + // } return; } private handleDont(option: TelnetOptions): void { - this.telnetSocket.writeWont(option); - this.updateNegotiations(option, { server: TelnetControlSequences.DONT, - client: TelnetControlSequences.WONT, }); - } - - private handleWill(option: TelnetOptions): void { - switch (option) { - case TelnetOptions.TELOPT_CHARSET: { - this.telnetSocket.writeDo(option); - this.updateNegotiations(option, { - server: TelnetControlSequences.WILL, - client: TelnetControlSequences.DO, - }); + const handler = this.optionsHandler.get(option); - return; - } + const handlerResult = handler?.handleDont(); - case TelnetOptions.TELOPT_ECHO: { - this.telnetSocket.writeDo(option); + if (handlerResult !== undefined) { + this.updateNegotiations(option, { + client: handlerResult, + }); + } else { + this.telnetSocket.writeWont(option); - this.updateNegotiations(option, { - server: TelnetControlSequences.WILL, - client: TelnetControlSequences.DO, - }); - - // socket_io.emit('mud-signal', { - // signal: 'NOECHO-START', - // id: this.mudOptions?.id, - // }); - - return; - } - - case TelnetOptions.TELOPT_GMCP: { - this.telnetSocket.writeDo(option); - - this.updateNegotiations(option, { - server: TelnetControlSequences.WILL, - client: TelnetControlSequences.DO, - }); - - return; - } + this.updateNegotiations(option, { + client: TelnetControlSequences.WONT, + }); } + } + private handleWill(option: TelnetOptions): void { this.updateNegotiations(option, { server: TelnetControlSequences.WILL, - client: TelnetControlSequences.DONT, }); - this.telnetSocket.writeDont(option); - } + const handler = this.optionsHandler.get(option); - private handleWont(option: TelnetOptions): void { - switch (option) { - case TelnetOptions.TELOPT_ECHO: { - this.telnetSocket.writeDont(option); - - this.updateNegotiations(option, { - server: TelnetControlSequences.WONT, - client: TelnetControlSequences.DONT, - }); - - // socket_io.emit('mud-signal', { - // signal: 'NOECHO-END', - // id: this.mudOptions?.id, - // }); - - return; - } - } + const handlerResult = handler?.handleWill(); - this.telnetSocket.writeDo(option); + if (handlerResult !== undefined) { + this.updateNegotiations(option, { + client: handlerResult, + }); + } else { + this.telnetSocket.writeDont(option); - this.updateNegotiations(option, { - server: TelnetControlSequences.WONT, - client: TelnetControlSequences.DO, - }); - } - - private handleSub(option: TelnetOptions, chunkData: Buffer): void { - // Todo[myst] save this as well in the this.negotiations object to see what is currently sub-negotiated - - if ( - option === TelnetOptions.TELOPT_TTYPE && - new Uint8Array(chunkData)[0] === 1 - ) { - const nullBuf = Buffer.alloc(1, 0); // TELQUAL_IS + this.updateNegotiations(option, { + client: TelnetControlSequences.DONT, // we answer negatively but we should DO everything possible + }); + } - const buf = Buffer.from('WebMud3a'); + // switch (option) { - const sendBuf = Buffer.concat([nullBuf, buf], buf.length + 1); + // case TelnetOptions.TELOPT_GMCP: { + // this.telnetSocket.writeDo(option); - this.telnetSocket.writeSub(option, sendBuf); + // this.updateNegotiations(option, { + // server: TelnetControlSequences.WILL, + // client: TelnetControlSequences.DO, + // }); - return; - } + // return; + // } + // } + } - if ( - option === TelnetOptions.TELOPT_CHARSET && - new Uint8Array(chunkData)[0] === 1 - ) { - const nullBuf = Buffer.alloc(1, 2); // ACCEPTED + private handleWont(option: TelnetOptions): void { + this.updateNegotiations(option, { + server: TelnetControlSequences.WONT, + }); - const buf = Buffer.from('UTF-8'); + const handler = this.optionsHandler.get(option); - const sendBuf = Buffer.concat([nullBuf, buf], buf.length + 1); + const handlerResult = handler?.handleWont(); - this.telnetSocket.writeSub(option, sendBuf); + if (handlerResult !== undefined) { + this.updateNegotiations(option, { + client: handlerResult, + }); + } else { + this.telnetSocket.writeDont(option); - return; + this.updateNegotiations(option, { + client: TelnetControlSequences.DONT, // we answer negatively but we should DO everything possible + }); } + } - if (option === TelnetOptions.TELOPT_GMCP) { - const tmpstr = chunkData.toString(); - - const ix = tmpstr.indexOf(' '); - - // const jx = tmpstr.indexOf('.'); + private handleSub(option: TelnetOptions, serverChunk: Buffer): void { + this.updateNegotiations(option, { + serverChunk, + }); - let jsdata = tmpstr.substr(ix + 1); - if (ix < 0 || jsdata === '') jsdata = '{}'; + const handler = this.optionsHandler.get(option); - // socket_io.emit( - // 'mud-gmcp-incoming', - // this.mudOptions?.id, - // tmpstr.substr(0, jx), - // tmpstr.substr(jx + 1, ix - jx), - // JSON.parse(jsdata), - // ); + const handlerResult = handler?.handleSub?.(serverChunk); - return; + if (handlerResult !== undefined && handlerResult !== null) { + this.updateNegotiations(option, handlerResult); } } /** - * Updates the negotiations for a given Telnet option with the provided server and client control sequences. - * This function is special because it maps all enum values to its keys, making it easier to observe. + * Updates the negotiations and subnegotiations for a given Telnet option. + * This method allows updating both the control sequences and the subnegotiations. * - * @param {TelnetOptions} option - The Telnet option to update the negotiations for. - * @param {Object} negotiations - An object containing the server and client control sequences for the option. - * @param {TelnetControlSequences} negotiations.server - The server control sequence for the option. - * @param {TelnetControlSequences} negotiations.client - The client control sequence for the option. + * @param {TelnetOptions} option - The Telnet option to update. + * @param {Object} negotiations - An object containing the server and client control sequences, as well as subnegotiations. + * @param {TelnetControlSequences} [negotiations.server] - The server control sequence for the option. + * @param {TelnetControlSequences} [negotiations.client] - The client control sequence for the option. + * @param {Buffer} [negotiations.serverChunk] - The server chunk for subnegotiations (optional). + * @param {Buffer} [negotiations.clientChunk] - The client chunk for subnegotiations (optional). + * @param {string} [negotiations.clientOption] - The client option for subnegotiations (optional). */ private updateNegotiations( option: TelnetOptions, negotiations: { - server: TelnetControlSequences; - client: TelnetControlSequences; + server?: TelnetControlSequences; + client?: TelnetControlSequences; + serverChunk?: Buffer; + clientChunk?: Buffer; + clientOption?: string; }, ): void { - this.negotiations[TelnetOptions[option] as keyof typeof TelnetOptions] = { - server: TelnetControlSequences[ - negotiations.server - ] as keyof typeof TelnetControlSequences, - client: TelnetControlSequences[ - negotiations.client - ] as keyof typeof TelnetControlSequences, + const existing = this._negotiations[option] || {}; + + // Update the control sequences for server and client + const updatedNegotiation = { + ...existing, + ...{ + server: negotiations.server ?? existing.server, + client: negotiations.client ?? existing.client, + }, }; + // Update the subnegotiation properties if they exist + if (existing) { + updatedNegotiation.subnegotiation = { + ...existing.subnegotiation, + ...{ + serverChunk: negotiations.serverChunk + ? negotiations.serverChunk.toString() + : existing.subnegotiation?.serverChunk, + clientChunk: negotiations.clientChunk + ? negotiations.clientChunk.toString() + : existing.subnegotiation?.clientChunk, + clientOption: + negotiations.clientOption ?? existing.subnegotiation?.clientOption, + }, + }; + } + + // Update the negotiations object + this._negotiations[option] = updatedNegotiation; + + // Emit event with the updated negotiation values this.emit('negotiationChanged', { option, client: negotiations.client, diff --git a/backend/src/features/telnet/types/telnet-negotiations.ts b/backend/src/features/telnet/types/telnet-negotiations.ts index 23b610fb..1f589ba6 100644 --- a/backend/src/features/telnet/types/telnet-negotiations.ts +++ b/backend/src/features/telnet/types/telnet-negotiations.ts @@ -2,15 +2,39 @@ import { TelnetControlSequences } from './telnet-control-sequences.js'; import { TelnetOptions } from './telnet-options.js'; /** - * Represents the negotiation status of Telnet options between a server and a client. - * Each key in the object represents a Telnet option and its corresponding negotiation on the server and client sides. - * But the keys are the string representation of the enum values for ease of observation. - * The value of each key is an object with `server` and `client` properties, which are of type `TelnetControlSequences`. - * The optional `?` in the type definition indicates that not all Telnet options may be negotiated. + * Represents the state of negotiations for Telnet options, including both server + * and client control sequences, as well as any subnegotiation data. */ export type TelnetNegotiations = { -readonly [key in keyof typeof TelnetOptions]?: { - server: keyof typeof TelnetControlSequences; - client: keyof typeof TelnetControlSequences; + /** + * The control sequence received from the server (DO, DON'T, WILL, WON'T). + */ + server?: TelnetControlSequences; + + /** + * The control sequence sent by the client (DO, DON'T, WILL, WON'T). + */ + client?: TelnetControlSequences; + + /** + * Optional subnegotiation data exchanged between the server and client. + */ + subnegotiation?: { + /** + * The data chunk sent by the server during subnegotiation. + */ + serverChunk?: string; + + /** + * The data chunk sent by the client during subnegotiation. + */ + clientChunk?: string; + + /** + * The client option used during subnegotiation (e.g., a charset or mode). + */ + clientOption?: string; + }; }; }; diff --git a/backend/src/features/telnet/types/telnet-option-handler.ts b/backend/src/features/telnet/types/telnet-option-handler.ts new file mode 100644 index 00000000..9887c833 --- /dev/null +++ b/backend/src/features/telnet/types/telnet-option-handler.ts @@ -0,0 +1,49 @@ +import { TelnetControlSequences } from './telnet-control-sequences.js'; +import { TelnetSubnegotiationResult } from './telnet-subnegotiation-result.js'; + +/** + * A set of handler functions for managing Telnet option negotiation. + * Each handler responds to a specific Telnet negotiation command (DO, DON'T, WILL, WON'T), + * and optionally handles subnegotiation. + */ +export type TelnetOptionHandler = { + /** + * Handles the "DO" command sent by the server, indicating that the server + * wants the client to enable a particular option. + * + * @returns {TelnetControlSequences} The control sequence (WILL, WONT) sent back to the server. + */ + handleDo: () => TelnetControlSequences; + + /** + * Handles the "DON'T" command sent by the server, indicating that the server + * wants the client to disable a particular option. + * + * @returns {TelnetControlSequences} The control sequence (WILL, WONT) sent back to the server. + */ + handleDont: () => TelnetControlSequences; + + /** + * Handles the "WILL" command sent by the server, indicating that the server + * is willing to enable a particular option. + * + * @returns {TelnetControlSequences} The control sequence (DO, DONT) sent back to the server. + */ + handleWill: () => TelnetControlSequences; + + /** + * Handles the "WON'T" command sent by the server, indicating that the server + * is unwilling to enable a particular option. + * + * @returns {TelnetControlSequences} The control sequence (DO, DONT) sent back to the server. + */ + handleWont: () => TelnetControlSequences; + + /** + * Handles the subnegotiation message sent by the server. + * + * @param {Buffer} serverChunk - The data chunk sent by the server during subnegotiation. + * @returns {TelnetSubnegotiationResult | null} The subnegotiation result, or null if not applicable. + */ + handleSub?: (serverChunk: Buffer) => TelnetSubnegotiationResult; +}; diff --git a/backend/src/features/telnet/types/telnet-options.ts b/backend/src/features/telnet/types/telnet-options.ts index 307093b8..03912724 100644 --- a/backend/src/features/telnet/types/telnet-options.ts +++ b/backend/src/features/telnet/types/telnet-options.ts @@ -2,7 +2,8 @@ * Telnet options that can be negotiated. * @todo * Todo[myst]: Remove all special options that are not negotiated. Leave only the true ones and special ones, that - * are negociated. See https://www.iana.org/assignments/telnet-options/telnet-options.xhtml for all the options + * are negociated. See https://www.iana.org/assignments/telnet-options/telnet-options.xhtml for all the options. + * Rename this to "SupportedTelnetOptions" after that. */ export enum TelnetOptions { /** @@ -292,6 +293,11 @@ export enum TelnetOptions { */ TELOPT_COMPRESS = 85, + /** + * MCCP (Mud Client Compression Protocol). + */ + TELOPT_MCCP = 86, + /** * MSP option. */ diff --git a/backend/src/features/telnet/types/telnet-subnegotiation-result.ts b/backend/src/features/telnet/types/telnet-subnegotiation-result.ts new file mode 100644 index 00000000..ca4203c7 --- /dev/null +++ b/backend/src/features/telnet/types/telnet-subnegotiation-result.ts @@ -0,0 +1,15 @@ +/** + * Represents the result of a Telnet subnegotiation. + * It contains the data that the client sends back to the server during subnegotiation. + * The client option may return null if the subnegotiation is not applicable. + */ +export type TelnetSubnegotiationResult = { + /** + * The chunk of data that the client sends during subnegotiation. + */ + clientChunk: Buffer; + /** + * The client option used during subnegotiation (e.g., a charset or mode). + */ + clientOption: string; +} | null; diff --git a/backend/src/features/telnet/utils/handle-charset-option.ts b/backend/src/features/telnet/utils/handle-charset-option.ts new file mode 100644 index 00000000..9c162317 --- /dev/null +++ b/backend/src/features/telnet/utils/handle-charset-option.ts @@ -0,0 +1,89 @@ +import { TelnetSocket } from 'telnet-stream'; + +import { logger } from '../../../shared/utils/logger.js'; +import { TelnetControlSequences } from '../types/telnet-control-sequences.js'; +import { TelnetOptionHandler } from '../types/telnet-option-handler.js'; +import { TelnetOptions } from '../types/telnet-options.js'; +import { TelnetSubnegotiationResult } from '../types/telnet-subnegotiation-result.js'; + +enum TelnetCharsetSubnogiation { + CHARSET_REJECTED = 0, + CHARSET_REQUEST = 1, + CHARSET_ACCEPTED = 2, +} + +const handleCharsetDo = + (socket: TelnetSocket) => (): TelnetControlSequences => { + socket.writeWill(TelnetOptions.TELOPT_CHARSET); + + return TelnetControlSequences.WILL; + }; + +const handleCharsetDont = + (socket: TelnetSocket) => (): TelnetControlSequences => { + socket.writeWont(TelnetOptions.TELOPT_CHARSET); + + return TelnetControlSequences.WONT; + }; + +const handleCharsetWill = + (socket: TelnetSocket) => (): TelnetControlSequences => { + socket.writeDo(TelnetOptions.TELOPT_CHARSET); + + return TelnetControlSequences.DO; + }; + +const handleCharsetWont = + (socket: TelnetSocket) => (): TelnetControlSequences => { + socket.writeDont(TelnetOptions.TELOPT_CHARSET); + + return TelnetControlSequences.DONT; + }; + +const handleCharsetSub = + (socket: TelnetSocket) => + (serverChunk: Buffer): TelnetSubnegotiationResult => { + if ( + new Uint8Array(serverChunk)[0] !== + TelnetCharsetSubnogiation.CHARSET_REQUEST + ) { + return null; + } + + const clientOption = 'UTF-8'; + + const serverCharsets = serverChunk.toString().split(' '); + + if (serverCharsets.includes(clientOption) === false) { + logger.error( + `[Telnet-Client] [Charset-Option] charset ${clientOption} is not supported by the MUD server. Only ${serverCharsets.join( + ', ', + )} are supported.`, + ); + } + + const command = Buffer.alloc(1, TelnetCharsetSubnogiation.CHARSET_ACCEPTED); + + const data = Buffer.from('UTF-8'); + + const message = Buffer.concat([command, data], data.length + 1); + + socket.writeSub(TelnetOptions.TELOPT_CHARSET, message); + + return { + clientChunk: message, + clientOption, + }; + }; + +export const handleCharsetOption = ( + socket: TelnetSocket, +): TelnetOptionHandler => { + return { + handleDo: handleCharsetDo(socket), + handleDont: handleCharsetDont(socket), + handleWill: handleCharsetWill(socket), + handleWont: handleCharsetWont(socket), + handleSub: handleCharsetSub(socket), + }; +}; diff --git a/backend/src/features/telnet/utils/handle-echo-option.ts b/backend/src/features/telnet/utils/handle-echo-option.ts new file mode 100644 index 00000000..340f7907 --- /dev/null +++ b/backend/src/features/telnet/utils/handle-echo-option.ts @@ -0,0 +1,38 @@ +import { TelnetSocket } from 'telnet-stream'; + +import { TelnetControlSequences } from '../types/telnet-control-sequences.js'; +import { TelnetOptionHandler } from '../types/telnet-option-handler.js'; +import { TelnetOptions } from '../types/telnet-options.js'; + +const handleEchoDo = (socket: TelnetSocket) => (): TelnetControlSequences => { + socket.writeWill(TelnetOptions.TELOPT_ECHO); + + return TelnetControlSequences.WILL; +}; + +const handleEchoDont = (socket: TelnetSocket) => (): TelnetControlSequences => { + socket.writeWont(TelnetOptions.TELOPT_ECHO); + + return TelnetControlSequences.WONT; +}; + +const handleEchoWill = (socket: TelnetSocket) => (): TelnetControlSequences => { + socket.writeDo(TelnetOptions.TELOPT_ECHO); + + return TelnetControlSequences.DO; +}; + +const handleEchoWont = (socket: TelnetSocket) => (): TelnetControlSequences => { + socket.writeDont(TelnetOptions.TELOPT_ECHO); + + return TelnetControlSequences.DONT; +}; + +export const handleEchoOption = (socket: TelnetSocket): TelnetOptionHandler => { + return { + handleDo: handleEchoDo(socket), + handleDont: handleEchoDont(socket), + handleWill: handleEchoWill(socket), + handleWont: handleEchoWont(socket), + }; +}; diff --git a/backend/src/main.ts b/backend/src/main.ts index 7cd2fda4..7c8d6e07 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { Environment } from './core/environment/environment.js'; import { useBodyParser } from './core/middleware/use-body-parser.js'; +import { useRestEndpoints } from './core/middleware/use-rest-endpoints.js'; import { useSockets } from './core/middleware/use-sockets.js'; import { useStaticFiles } from './core/middleware/use-static-files.js'; import { useRoutes } from './core/routes/routes.js'; @@ -27,9 +28,14 @@ useBodyParser(app); useStaticFiles(app, 'wwwroot'); -useRoutes(app); +const socketManager = useSockets(httpServer, environment); + +// Enable Debug Rest Endpoints in Development Mode +if (environment.environment === 'development') { + useRestEndpoints(app, socketManager); +} -useSockets(httpServer, environment); +useRoutes(app); // function myCleanup() { // console.log('Cleanup starts.'); diff --git a/backend/src/shared/utils/create-http-server.ts b/backend/src/shared/utils/create-http-server.ts index a62c3ec8..d3017fb8 100644 --- a/backend/src/shared/utils/create-http-server.ts +++ b/backend/src/shared/utils/create-http-server.ts @@ -5,6 +5,7 @@ import { Server as HttpsServer } from 'https'; import { logger } from './logger.js'; +// Todo[myst]: Ich will das eigentlich nicht hier haben, da man nicht beliebig in der Anwendung einfach mal http Server spawnen können sollte export function createHttpServer( app: Express, settings: { tls?: { cert: string; key: string } }, @@ -15,8 +16,6 @@ export function createHttpServer( cert: fs.readFileSync(settings.tls.cert), }; - console.log('INIT: https active'); - logger.debug('SRV://5000 : INIT: https active'); return new HttpsServer(options, app); diff --git a/backend/src/shared/utils/is-buffer-encoding.spec.ts b/backend/src/shared/utils/is-buffer-encoding.spec.ts new file mode 100644 index 00000000..bd81fac8 --- /dev/null +++ b/backend/src/shared/utils/is-buffer-encoding.spec.ts @@ -0,0 +1,77 @@ +import { isBufferEncoding } from './is-buffer-encoding'; + +describe('isBufferEncoding', () => { + // Test für gültige BufferEncodings + test('should return true for valid BufferEncodings', () => { + expect(isBufferEncoding('ascii')).toBe(true); + + expect(isBufferEncoding('utf8')).toBe(true); + + expect(isBufferEncoding('utf-8')).toBe(true); + + expect(isBufferEncoding('utf16le')).toBe(true); + + expect(isBufferEncoding('utf-16le')).toBe(true); + + expect(isBufferEncoding('ucs2')).toBe(true); + + expect(isBufferEncoding('ucs-2')).toBe(true); + + expect(isBufferEncoding('base64')).toBe(true); + + expect(isBufferEncoding('base64url')).toBe(true); + + expect(isBufferEncoding('latin1')).toBe(true); + + expect(isBufferEncoding('binary')).toBe(true); + + expect(isBufferEncoding('hex')).toBe(true); + }); + + // Test für ungültige BufferEncodings + test('should return false for invalid BufferEncodings', () => { + expect(isBufferEncoding('UTF8')).toBe(false); // Großbuchstaben + + expect(isBufferEncoding('utf 8')).toBe(false); // Leerzeichen + + expect(isBufferEncoding('UTF-16')).toBe(false); // ungültige Kodierung + + expect(isBufferEncoding('utf-32')).toBe(false); // nicht unterstütztes Encoding + + expect(isBufferEncoding('')).toBe(false); // leerer String + + expect(isBufferEncoding('randomString')).toBe(false); // zufälliger String + }); + + // Edge Case Test: Unterschied zwischen ähnlichen Kodierungen + test('should handle similar but invalid encodings', () => { + expect(isBufferEncoding('utf16')).toBe(false); // Kein 'le' Suffix + + expect(isBufferEncoding('ucs')).toBe(false); // kein '-2' + + expect(isBufferEncoding('utf_8')).toBe(false); // falsches Zeichen (Unterstrich statt Bindestrich) + }); + + // Test für sehr lange Strings (Edge Case) + test('should return false for excessively long strings', () => { + const longString = 'a'.repeat(1000); // Sehr langer String + + expect(isBufferEncoding(longString)).toBe(false); + }); + + // Test für Strings mit Sonderzeichen (Edge Case) + test('should return false for strings with special characters', () => { + expect(isBufferEncoding('utf8!')).toBe(false); // Ungültiges Sonderzeichen + + expect(isBufferEncoding('base64$')).toBe(false); // Ungültiges Sonderzeichen + + expect(isBufferEncoding('latin@1')).toBe(false); // Ungültiges Sonderzeichen + }); + + // Test für undefined oder null (Edge Case) + test('should return false for undefined or null', () => { + expect(isBufferEncoding(undefined as unknown as string)).toBe(false); + + expect(isBufferEncoding(null as unknown as string)).toBe(false); + }); +}); diff --git a/backend/src/shared/utils/is-buffer-encoding.ts b/backend/src/shared/utils/is-buffer-encoding.ts new file mode 100644 index 00000000..8a19da27 --- /dev/null +++ b/backend/src/shared/utils/is-buffer-encoding.ts @@ -0,0 +1,16 @@ +export function isBufferEncoding(encoding: string): encoding is BufferEncoding { + return [ + 'ascii', + 'utf8', + 'utf-8', + 'utf16le', + 'utf-16le', + 'ucs2', + 'ucs-2', + 'base64', + 'base64url', + 'latin1', + 'binary', + 'hex', + ].includes(encoding); +} diff --git a/backend/src/shared/utils/supported-encodings.ts b/backend/src/shared/utils/supported-encodings.ts new file mode 100644 index 00000000..07980b46 --- /dev/null +++ b/backend/src/shared/utils/supported-encodings.ts @@ -0,0 +1,11 @@ +export function mapToServerEncodings(charset: string): BufferEncoding | null { + switch (charset) { + case 'UTF-8': + case 'UTF8': + case 'utf8': + case 'utf-8': + return 'utf-8'; + default: + return null; + } +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index a5744f14..e33d34af 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -10,6 +10,10 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] -} + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + ] +} \ No newline at end of file diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 486bb076..fb92c79d 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -7,7 +7,7 @@ import { Environment } from './environment.interface'; export const environment: Environment = { production: false, // Change this to your local IP if you want to test on a mobile device in the same network - backendUrl: () => window.location.origin, + backendUrl: () => "http://localhost:5000", }; /*