Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): added NAWS support #123

Merged
merged 2 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions backend/src/core/environment/types/environment-keys.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export type EnvironmentKeys =
| 'HOST' // Optional. Defaults to '0.0.0.0'
| 'PORT' // Optional. Defaults to 5000
| 'TELNET_HOST' // Required. Example '127.0.0.1'
| 'TELNET_PORT' // Required. Example '23'
| 'TELNET_TLS' // Optional. Defaults to 'false'
| 'HOST' // Optional | defaults to '0.0.0.0' | the IP the backend will listen for
| 'PORT' // Optional | defaults to 5000 | the PORT the backend will listen for
| 'TELNET_HOST' // Required | the IP of your MUD
| 'TELNET_PORT' // Required | the PORT of your MUD
| 'TELNET_TLS' // Optional | defaults to 'false' | set this to true if you want a secure connection
| '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'
| 'ENVIRONMENT';
| 'SOCKET_ROOT' // Required | the named socket for
| 'ENVIRONMENT'; // Optional | Enables Debug REST Endpoint /api/info
72 changes: 72 additions & 0 deletions backend/src/features/telnet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Telnet Negotiations

## Overview

Telnet negotiations are a crucial part of the Telnet protocol, allowing clients and servers to agree on various options that affect the communication session. This document explains how Telnet negotiations work and how we handle different options in our implementation.

## Negotiation Process

The Telnet negotiation process is a bidirectional protocol in which both the client and server can initiate and respond to requests for specific options. This process enables both parties to agree on features and settings to be used during the session. The typical negotiation commands are as follows:

- DO: Sent by one party (client or server) to request that the other party enable a specific option. This command is a request for the peer to start using the option if supported.

- DON'T: Sent by a party to instruct the peer not to enable a particular option or as a response to decline a previously sent DO request.

- WILL: Sent in response to a DO command to indicate willingness to enable the specified option. This command signifies that the sender is ready to start using the requested option.

- WON'T: Used to refuse enabling an option. If a peer sends a DO command and the recipient cannot or will not enable the option, it responds with WON'T.

- SUBNEGOTIATION: Some options require additional data to be configured beyond simply being enabled. In such cases, either party can initiate subnegotiation, sending a command with necessary data. For example, options like NAWS (Negotiate About Window Size) or CHARSET (character set) often involve subnegotiation to specify values (e.g., window size or encoding).

The TelnetClient class handles these negotiation commands, regardless of their origin, and emits events for each command received, allowing the application to respond accordingly. This event-driven structure enables dynamic handling of each negotiation state, supporting a flexible exchange of capabilities.

## Supported Options

| Telnet Option | Client Support | Client Negotiation | Remarks | Discussion |
| ---------------------------------- | ------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| NAWS (Negotiate About Window Size) | Partial | WILL (+ Sub) | We support this option to subnegotiate the window size. However, we send static values <br>for the window size (80x25) and it does look like Unitopia is ignoring these values. | https://github.com/unitopia-de/webmud3/issues/108 |
| CHARSET | Partial | WILL (+ Sub) | We support this option to subnegotiate the character set with the server. <br>However, we only accept UTF-8. If the server does not subnogitiate <br>for UTF-8, an error will be thrown and the connection will be closed. | https://github.com/unitopia-de/webmud3/issues/111 |
| ECHO | Full | Dynamic | Is used in the telnet login flow to hide user input (password) | |
| SGA (Suppress Go Ahead) | Todo | | | https://github.com/unitopia-de/webmud3/issues/115 |
| LINEMODE | Todo | | | https://github.com/unitopia-de/webmud3/issues/114 |
| STARTTLS | Intentionally Unsupported | WONT | This option allows to upgrade any existing connection to a secure one. However, we <br>don't support this intentionally and recommend you to initialize a secure <br>connection from the beginning. | https://github.com/unitopia-de/webmud3/issues/113 |
| EOR (End of Record) | Todo | | | |

## How to handle a new option

Define a new TelnetOptionHandler object for the option you want to handle.

```typescript
const newOptionHandler: TelnetOptionHandler = {
negotiate: () => {
// In this handler you can send a negotiation yourself uppon initialization.
// Use this if you want the server to enable/disable the option.
}
handleDo: () => {
// Handle the DO command for the new option
// Return a TelnetNegotiationResult object with the appropriate control sequence and subnegotiation result
},
handleDont: () => {
// Handle the DON'T command for the new option
// Return a TelnetNegotiationResult object with the appropriate control sequence and subnegotiation result
},
handleWill: () => {
// Handle the WILL command for the new option
// Return a TelnetNegotiationResult object with the appropriate control sequence and subnegotiation result
},
handleWont: () => {
// Handle the WON'T command for the new option
// Return a TelnetNegotiationResult object with the appropriate control sequence and subnegotiation result
},
handleSub: (serverChunk: Buffer) => {
// Handle the subnegotiation data for the new option
// Return a TelnetSubnegotiationResult object with the appropriate client chunk and client option
},
};
```

Add the `newOptionHandler` object to the optionsHandler map in your TelnetClient class:

```typescript
this.optionsHandler.set(TelnetOptions.TELOPT_LINEMODE, newOptionHandler);
```
30 changes: 14 additions & 16 deletions backend/src/features/telnet/telnet-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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 { handleNawsOption } from './utils/handle-naws-option.js';
import { TelnetSocketWrapper } from './utils/telnet-socket-wrapper.js';

type TelnetClientEvents = {
Expand Down Expand Up @@ -73,6 +74,7 @@ export class TelnetClient extends EventEmitter<TelnetClientEvents> {
this.optionsHandler = new Map([
[TelnetOptions.TELOPT_CHARSET, handleCharsetOption(this.telnetSocket)],
[TelnetOptions.TELOPT_ECHO, handleEchoOption(this.telnetSocket)],
[TelnetOptions.TELOPT_NAWS, handleNawsOption(this.telnetSocket)],
]);

this.telnetSocket.on('connect', () => this.handleConnect());
Expand Down Expand Up @@ -131,7 +133,9 @@ export class TelnetClient extends EventEmitter<TelnetClientEvents> {

if (handlerResult !== undefined) {
this.updateNegotiations(option, {
client: handlerResult,
client: handlerResult.controlSequence,
clientChunk: handlerResult.subNegotiationResult?.clientChunk,
clientOption: handlerResult.subNegotiationResult?.clientOption,
});
} else {
this.telnetSocket.writeWont(option);
Expand All @@ -154,18 +158,6 @@ export class TelnetClient extends EventEmitter<TelnetClientEvents> {
// return;
// }

// case TelnetOptions.TELOPT_NAWS: {
// this.updateNegotiations(option, {
// server: TelnetControlSequences.DO,
// client: TelnetControlSequences.WILL,
// });

// this.telnetSocket.writeWill(option);

// return;
// }
// }

return;
}

Expand All @@ -180,7 +172,9 @@ export class TelnetClient extends EventEmitter<TelnetClientEvents> {

if (handlerResult !== undefined) {
this.updateNegotiations(option, {
client: handlerResult,
client: handlerResult.controlSequence,
clientChunk: handlerResult.subNegotiationResult?.clientChunk,
clientOption: handlerResult.subNegotiationResult?.clientOption,
});
} else {
this.telnetSocket.writeWont(option);
Expand All @@ -202,7 +196,9 @@ export class TelnetClient extends EventEmitter<TelnetClientEvents> {

if (handlerResult !== undefined) {
this.updateNegotiations(option, {
client: handlerResult,
client: handlerResult.controlSequence,
clientChunk: handlerResult.subNegotiationResult?.clientChunk,
clientOption: handlerResult.subNegotiationResult?.clientOption,
});
} else {
this.telnetSocket.writeDont(option);
Expand Down Expand Up @@ -238,7 +234,9 @@ export class TelnetClient extends EventEmitter<TelnetClientEvents> {

if (handlerResult !== undefined) {
this.updateNegotiations(option, {
client: handlerResult,
client: handlerResult.controlSequence,
clientChunk: handlerResult.subNegotiationResult?.clientChunk,
clientOption: handlerResult.subNegotiationResult?.clientOption,
});
} else {
this.telnetSocket.writeDont(option);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { TelnetControlSequences } from './telnet-control-sequences.js';
import { TelnetSubnegotiationResult } from './telnet-subnegotiation-result.js';

export type TelnetNegotiationResult = {
subNegotiationResult?: TelnetSubnegotiationResult;

controlSequence: TelnetControlSequences;
};
22 changes: 14 additions & 8 deletions backend/src/features/telnet/types/telnet-option-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TelnetControlSequences } from './telnet-control-sequences.js';
import { TelnetNegotiationResult } from './telnet-negotiation-result.js';
import { TelnetSubnegotiationResult } from './telnet-subnegotiation-result.js';

/**
Expand All @@ -7,37 +7,43 @@ import { TelnetSubnegotiationResult } from './telnet-subnegotiation-result.js';
* and optionally handles subnegotiation.
*/
export type TelnetOptionHandler = {
/**
* Intiate a negotiation with the server.
* @returns {TelnetNegotiationResult} The control sequence (DO, DONT, WILL, WONT) sent back to the server.
*/
negotiate?: () => TelnetNegotiationResult;

/**
* 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.
* @returns {TelnetNegotiationResult} The control sequence (WILL, WONT) sent back to the server.
*/
handleDo: () => TelnetControlSequences;
handleDo: () => TelnetNegotiationResult;

/**
* 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;
handleDont: () => TelnetNegotiationResult;

/**
* 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.
* @returns {TelnetNegotiationResult} The control sequence (DO, DONT) sent back to the server.
*/
handleWill: () => TelnetControlSequences;
handleWill: () => TelnetNegotiationResult;

/**
* 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.
* @returns {TelnetNegotiationResult} The control sequence (DO, DONT) sent back to the server.
*/
handleWont: () => TelnetControlSequences;
handleWont: () => TelnetNegotiationResult;

/**
* Handles the subnegotiation message sent by the server.
Expand Down
35 changes: 22 additions & 13 deletions backend/src/features/telnet/utils/handle-charset-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,53 @@ import { TelnetSocket } from 'telnet-stream';

import { logger } from '../../../shared/utils/logger.js';
import { TelnetControlSequences } from '../types/telnet-control-sequences.js';
import { TelnetNegotiationResult } from '../types/telnet-negotiation-result.js';
import { TelnetOptionHandler } from '../types/telnet-option-handler.js';
import { TelnetOptions } from '../types/telnet-options.js';
import { TelnetSubnegotiationResult } from '../types/telnet-subnegotiation-result.js';

const DEFAULT_CLIENT_ENCODING = 'UTF-8';

enum TelnetCharsetSubnogiation {
CHARSET_REJECTED = 0,
CHARSET_REQUEST = 1,
CHARSET_ACCEPTED = 2,
}

const handleCharsetDo =
(socket: TelnetSocket) => (): TelnetControlSequences => {
(socket: TelnetSocket) => (): TelnetNegotiationResult => {
socket.writeWill(TelnetOptions.TELOPT_CHARSET);

return TelnetControlSequences.WILL;
return {
controlSequence: TelnetControlSequences.WILL,
};
};

const handleCharsetDont =
(socket: TelnetSocket) => (): TelnetControlSequences => {
(socket: TelnetSocket) => (): TelnetNegotiationResult => {
socket.writeWont(TelnetOptions.TELOPT_CHARSET);

return TelnetControlSequences.WONT;
return {
controlSequence: TelnetControlSequences.WONT,
};
};

const handleCharsetWill =
(socket: TelnetSocket) => (): TelnetControlSequences => {
(socket: TelnetSocket) => (): TelnetNegotiationResult => {
socket.writeDo(TelnetOptions.TELOPT_CHARSET);

return TelnetControlSequences.DO;
return {
controlSequence: TelnetControlSequences.DO,
};
};

const handleCharsetWont =
(socket: TelnetSocket) => (): TelnetControlSequences => {
(socket: TelnetSocket) => (): TelnetNegotiationResult => {
socket.writeDont(TelnetOptions.TELOPT_CHARSET);

return TelnetControlSequences.DONT;
return {
controlSequence: TelnetControlSequences.DONT,
};
};

const handleCharsetSub =
Expand All @@ -50,13 +61,11 @@ const handleCharsetSub =
return null;
}

const clientOption = 'UTF-8';

const serverCharsets = serverChunk.toString().split(' ');

if (serverCharsets.includes(clientOption) === false) {
if (serverCharsets.includes(DEFAULT_CLIENT_ENCODING) === false) {
logger.error(
`[Telnet-Client] [Charset-Option] charset ${clientOption} is not supported by the MUD server. Only ${serverCharsets.join(
`[Telnet-Client] [Charset-Option] charset ${DEFAULT_CLIENT_ENCODING} is not supported by the MUD server. Only ${serverCharsets.join(
', ',
)} are supported.`,
);
Expand All @@ -72,7 +81,7 @@ const handleCharsetSub =

return {
clientChunk: message,
clientOption,
clientOption: DEFAULT_CLIENT_ENCODING,
};
};

Expand Down
32 changes: 18 additions & 14 deletions backend/src/features/telnet/utils/handle-echo-option.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import { TelnetSocket } from 'telnet-stream';

import { TelnetControlSequences } from '../types/telnet-control-sequences.js';
import { TelnetNegotiationResult } from '../types/telnet-negotiation-result.js';
import { TelnetOptionHandler } from '../types/telnet-option-handler.js';
import { TelnetOptions } from '../types/telnet-options.js';

const handleEchoDo = (socket: TelnetSocket) => (): TelnetControlSequences => {
const handleEchoDo = (socket: TelnetSocket) => (): TelnetNegotiationResult => {
socket.writeWill(TelnetOptions.TELOPT_ECHO);

return TelnetControlSequences.WILL;
return { controlSequence: TelnetControlSequences.WILL };
};

const handleEchoDont = (socket: TelnetSocket) => (): TelnetControlSequences => {
socket.writeWont(TelnetOptions.TELOPT_ECHO);
const handleEchoDont =
(socket: TelnetSocket) => (): TelnetNegotiationResult => {
socket.writeWont(TelnetOptions.TELOPT_ECHO);

return TelnetControlSequences.WONT;
};
return { controlSequence: TelnetControlSequences.WONT };
};

const handleEchoWill = (socket: TelnetSocket) => (): TelnetControlSequences => {
socket.writeDo(TelnetOptions.TELOPT_ECHO);
const handleEchoWill =
(socket: TelnetSocket) => (): TelnetNegotiationResult => {
socket.writeDo(TelnetOptions.TELOPT_ECHO);

return TelnetControlSequences.DO;
};
return { controlSequence: TelnetControlSequences.DO };
};

const handleEchoWont = (socket: TelnetSocket) => (): TelnetControlSequences => {
socket.writeDont(TelnetOptions.TELOPT_ECHO);
const handleEchoWont =
(socket: TelnetSocket) => (): TelnetNegotiationResult => {
socket.writeDont(TelnetOptions.TELOPT_ECHO);

return TelnetControlSequences.DONT;
};
return { controlSequence: TelnetControlSequences.DONT };
};

export const handleEchoOption = (socket: TelnetSocket): TelnetOptionHandler => {
return {
Expand Down
Loading
Loading