Skip to content

Commit

Permalink
feat(backend): implemented TELNET_CHARSET
Browse files Browse the repository at this point in the history
- added CHARSET environment variable. supported values: utf-8, latin1 (ISO-8859-1), ascii (US-ASCII)
- added charset subnegotiaton with fallback to utf-8
  • Loading branch information
mystiker committed Sep 13, 2024
1 parent cb295b7 commit e3a640c
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 35 deletions.
130 changes: 121 additions & 9 deletions backend/src/core/environment/environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('Environment', () => {

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'utf8';
process.env.CHARSET = 'utf-8';

const env = await getFreshEnvironmentInstance();

Expand All @@ -47,15 +47,15 @@ describe('Environment', () => {

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'utf8';
process.env.CHARSET = 'utf-8';

const env = await getFreshEnvironmentInstance();

expect(env.telnetHost).toBe('localhost');

expect(env.telnetPort).toBe(3000);

expect(env.charset).toBe('utf8');
expect(env.charset).toBe('utf-8');
});

it('should handle optional TLS configuration', async () => {
Expand All @@ -65,7 +65,7 @@ describe('Environment', () => {

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'utf8';
process.env.CHARSET = 'utf-8';

process.env.TELNET_TLS = 'true';

Expand All @@ -81,7 +81,7 @@ describe('Environment', () => {

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'utf8';
process.env.CHARSET = 'utf-8';

process.env.TELNET_TLS = 'TRUE';

Expand All @@ -97,31 +97,143 @@ describe('Environment', () => {

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'utf8';
process.env.CHARSET = 'utf-8';

const env = await getFreshEnvironmentInstance();

expect(env.telnetTLS).toBe(false);
});

it('should use default charset if not set', async () => {
it('should use utf-8 charset when set to utf8', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'utf8';

const env = await getFreshEnvironmentInstance();

expect(env.charset).toBe('utf-8');
});

it('should use utf-8 charset when set to utf-8', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'utf-8';

const env = await getFreshEnvironmentInstance();

expect(env.charset).toBe('utf-8');
});

it('should use latin1 charset when set to latin1', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'latin1';

const env = await getFreshEnvironmentInstance();

expect(env.charset).toBe('latin1');
});

it('should use latin1 charset when set to iso-8859-1', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'iso-8859-1';

const env = await getFreshEnvironmentInstance();

expect(env.charset).toBe('latin1');
});

it('should use ascii charset when set to ascii', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'ascii';

const env = await getFreshEnvironmentInstance();

expect(env.charset).toBe('ascii');
});

it('should use ascii charset when set to us-ascii', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'us-ascii';

const env = await getFreshEnvironmentInstance();

expect(env.charset).toBe('utf8');
expect(env.charset).toBe('ascii');
});

it('should throw an error for invalid charset', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = 'invalid-charset';

await expect(getFreshEnvironmentInstance()).rejects.toThrow();
});

it('should use utf-8 as the default charset when CHARSET is not set', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

delete process.env.CHARSET; // Remove CHARSET from environment

const env = await getFreshEnvironmentInstance();

expect(env.charset).toBe('utf-8'); // Default value
});

it('should default to utf-8 when CHARSET is empty', async () => {
process.env.TELNET_HOST = 'localhost';

process.env.TELNET_PORT = '3000';

process.env.SOCKET_ROOT = '/socket.io';

process.env.CHARSET = ''; // Set CHARSET to an empty string

const env = await getFreshEnvironmentInstance();

expect(env.charset).toBe('utf-8'); // Default value
});

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';

Expand Down
26 changes: 24 additions & 2 deletions backend/src/core/environment/environment.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { config as configureEnvironment } from 'dotenv';

import { logger } from '../../shared/utils/logger.js';
import { mapToServerEncodings } from '../../shared/utils/supported-encodings.js';
import { IEnvironment } from './types/environment.js';
import { getEnvironmentVariable } from './utils/get-environment-variable.js';
import { resolveModulePath } from './utils/resolve-modulepath.js';

const ALLOWED_CHARSETS = [
'utf8',
'utf-8',
'latin1',
'iso-8859-1',
'ascii',
'us-ascii',
];

/**
* Environment class to handle environment variables and application settings.
* Reads the environment variables once oppon initialisation and provides them as properties.
Expand All @@ -17,7 +27,7 @@ export class Environment implements IEnvironment {
public readonly telnetHost: string;
public readonly telnetPort: number;
public readonly telnetTLS: boolean;
public readonly charset: string;
public readonly charset: BufferEncoding;
public readonly projectRoot: string;
public readonly socketRoot: string;
public readonly socketTimeout: number;
Expand Down Expand Up @@ -47,7 +57,19 @@ export class Environment implements IEnvironment {

this.socketRoot = String(getEnvironmentVariable('SOCKET_ROOT'));

this.charset = String(getEnvironmentVariable('CHARSET', false, 'utf8'));
const charset = String(
getEnvironmentVariable('CHARSET', false, 'utf-8'),
).toLocaleLowerCase();

const mappedCharset = mapToServerEncodings(charset);

if (mappedCharset === null) {
throw new Error(
`Environment variable "CHARSET" must be one of ${ALLOWED_CHARSETS.join(', ')}.`,
);
}

this.charset = mappedCharset;

this.socketTimeout = Number(
getEnvironmentVariable('SOCKET_TIMEOUT', false, '900000'),
Expand Down
2 changes: 1 addition & 1 deletion backend/src/core/environment/types/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface IEnvironment {
readonly projectRoot: string;
readonly socketRoot: string;

readonly charset: string;
readonly charset: BufferEncoding;

readonly socketTimeout: number;

Expand Down
31 changes: 30 additions & 1 deletion backend/src/core/sockets/socket-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -146,10 +147,38 @@ export class SocketManager extends Server<
this.managerOptions.telnetHost,
this.managerOptions.telnetPort,
this.managerOptions.useTelnetTls,
Environment.getInstance().charset,
);

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', () => {
Expand Down
Loading

0 comments on commit e3a640c

Please sign in to comment.