This project provides a library that implements telnet functionality, and as many of its RFCs are viable, in a testable manner. This is done with an eye on MUDs at this time, but may improve to support more terminal capabilities as time permits and if there is ask for it.
At this time, this repository is in a rough state and does not yet implement some common modern code standards, but is constantly evolving.
This library is in a state where breaking changes to the interface are expected.
RFC | Description | Supported | Comments |
---|---|---|---|
RFC 855 | Telnet Option Specification | Full | |
RFC 1091 | Terminal Type Negotiation | Full | |
MTTS | MTTS Negotiation (Extends TTYPE) | Full | |
RFC 1073 | Window Size Negotiation (NAWS) | Full | |
GMCP | Generic Mud Communication Protocol | Full | |
MSSP | MSSP Negotiation | Full | Untested |
RFC 885 | End Of Record Negotiation | Full | Untested |
EOR | End Of Record Negotiation | Full | Untested |
MSDP | Mud Server Data Protocol | Partial | Partial Tested |
RFC 2066 | Charset Negotiation | Partial | No TTABLE support |
RFC 858 | Suppress GOAHEAD Negotiation | Full | Untested |
RFC 1572 | New Environment Negotiation | No | Planned |
MNES | Mud New Environment Negotiation | No | Planned |
MCCP | Mud Client Compression Protocol | No | Rejects |
RFC 1950 | ZLIB Compression | No | Rejects |
RFC 857 | Echo Negotiation | No | Rejects |
RFC 1079 | Terminal Speed Negotiation | No | Rejects |
RFC 1372 | Flow Control Negotiation | No | Rejects |
RFC 1184 | Line Mode Negotiation | No | Rejects |
RFC 1096 | X-Display Negotiation | No | Rejects |
RFC 1408 | Environment Negotiation | No | Rejects |
RFC 2941 | Authentication Negotiation | No | Rejects |
RFC 2946 | Encryption Negotiation | No | Rejects |
Being a Telnet Negotiation Library, this library doesn't give support for extensions like ANSI, Pueblo, MXP, etc at this time.
A documented example exists in the TestClient Project.
Initiate a logger. A Serilog logger is required by this library at this time.
var log = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(new CompactJsonFormatter(), "LogResult.log")
.MinimumLevel.LogDebug()
.CreateLogger();
Log.Logger = log;
Create functions that implement your desired behavior on getting a signal.
private async ValueTask WriteToOutputStreamAsync(byte[] arg, StreamWriter writer)
{
try
{
await writer.BaseStream.WriteAsync(arg, CancellationToken.None);
}
catch(ObjectDisposedException ode)
{
_Logger.LogInformation("Stream has been closed", ode);
}
}
public static ValueTask WriteBackAsync(byte[] writeback, Encoding encoding) =>
Task.Run(() => Console.WriteLine(encoding.GetString(writeback)));
public ValueTask SignalGMCPAsync((string module, string writeback) val, Encoding encoding) =>
Task.Run(() => _Logger.LogDebug("GMCP Signal: {Module}: {WriteBack}", val.module, val.writeback));
public ValueTask SignalMSSPAsync(MSSPConfig val) =>
Task.Run(() => _Logger.LogDebug("New MSSP: {@MSSP}", val));
public ValueTask SignalPromptAsync() =>
Task.Run(() => _Logger.LogDebug("Prompt"));
public ValueTask SignalNAWSAsync(int height, int width) =>
Task.Run(() => _Logger.LogDebug("Client Height and Width updated: {Height}x{Width}", height, width));
Initialize the Interpreter.
var telnet = new TelnetInterpreter(TelnetInterpreter.TelnetMode.Client, _Logger.ForContext<TelnetInterpreter>())
{
CallbackOnSubmitAsync = WriteBackAsync,
CallbackNegotiationAsync = (x) => WriteToOutputStreamAsync(x, output),
SignalOnGMCPAsync = SignalGMCPAsync,
SignalOnMSSPAsync = SignalMSSPAsync,
SignalOnNAWSAsync = SignalNAWSAsync,
SignalOnPromptingAsync = SignalPromptAsync,
CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") }
}.BuildAsync();
Start interpreting.
for (int currentByte = 0; currentByte != -1; currentByte = input.BaseStream.ReadByte())
{
telnet.InterpretAsync((byte)currentByte).GetAwaiter().GetResult();
}
A documented example exists in the TestServer Project. This uses a Kestrel server to make the TCP handling easier.
public class KestrelMockServer : ConnectionHandler
{
private readonly ILogger _Logger;
public KestrelMockServer(ILogger<KestrelMockServer> logger) : base()
{
Console.OutputEncoding = Encoding.UTF8;
_Logger = logger;
}
private async ValueTask WriteToOutputStreamAsync(byte[] arg, PipeWriter writer)
{
try
{
await writer.WriteAsync(new ReadOnlyMemory<byte>(arg), CancellationToken.None);
}
catch (ObjectDisposedException ode)
{
_Logger.LogError(ode, "Stream has been closed");
}
}
public ValueTask SignalGMCPAsync((string module, string writeback) val)
{
_Logger.LogDebug("GMCP Signal: {Module}: {WriteBack}", val.module, val.writeback);
return ValueTask.CompletedTask;
}
public ValueTask SignalMSSPAsync(MSSPConfig val)
{
_Logger.LogDebug("New MSSP: {@MSSPConfig}", val);
return ValueTask.CompletedTask;
}
public ValueTask SignalNAWSAsync(int height, int width)
{
_Logger.LogDebug("Client Height and Width updated: {Height}x{Width}", height, width);
return ValueTask.CompletedTask;
}
private static async ValueTask SignalMSDPAsync(MSDPServerHandler handler, TelnetInterpreter telnet, string config) =>
await handler.HandleAsync(telnet, config);
public static async ValueTask WriteBackAsync(byte[] writeback, Encoding encoding, TelnetInterpreter telnet)
{
var str = encoding.GetString(writeback);
if (str.StartsWith("echo"))
{
await telnet.SendAsync(encoding.GetBytes($"We heard: {str}" + Environment.NewLine));
}
Console.WriteLine(encoding.GetString(writeback));
}
private async ValueTask MSDPUpdateBehavior(string resetVariable)
{
_Logger.LogDebug("MSDP Reset Request: {@Reset}", resetVariable);
await ValueTask.CompletedTask;
}
public async override ValueTask OnConnectedAsync(ConnectionContext connection)
{
using (_Logger.BeginScope(new Dictionary<string, object> { { "ConnectionId", connection.ConnectionId } }))
{
_Logger.LogInformation("{ConnectionId} connected", connection.ConnectionId);
var MSDPHandler = new MSDPServerHandler(new MSDPServerModel(MSDPUpdateBehavior)
{
Commands = () => ["help", "stats", "info"],
Configurable_Variables = () => ["CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID"],
Reportable_Variables = () => ["ROOM"],
Sendable_Variables = () => ["ROOM"],
});
var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, _Logger)
{
CallbackOnSubmitAsync = WriteBackAsync,
SignalOnGMCPAsync = SignalGMCPAsync,
SignalOnMSSPAsync = SignalMSSPAsync,
SignalOnNAWSAsync = SignalNAWSAsync,
SignalOnMSDPAsync = (telnet, config) => SignalMSDPAsync(MSDPHandler, telnet, config),
CallbackNegotiationAsync = (x) => WriteToOutputStreamAsync(x, connection.Transport.Output),
CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") }
}
.RegisterMSSPConfig(() => new MSSPConfig
{
Name = "My Telnet Negotiated Server",
UTF_8 = true,
Gameplay = ["ABC", "DEF"],
Extended = new Dictionary<string, dynamic>
{
{ "Foo", "Bar"},
{ "Baz", (string[])["Moo", "Meow"] }
}
})
.BuildAsync();
while (true)
{
var result = await connection.Transport.Input.ReadAsync();
var buffer = result.Buffer;
foreach (var segment in buffer)
{
await telnet.InterpretByteArrayAsync(segment.Span.ToImmutableArray());
}
if (result.IsCompleted)
{
break;
}
connection.Transport.Input.AdvanceTo(buffer.End);
}
_Logger.LogInformation("{ConnectionId} disconnected", connection.ConnectionId);
}
}
}