TelnetNegotiationCore 1.0.7

dotnet add package TelnetNegotiationCore --version 1.0.7
NuGet\Install-Package TelnetNegotiationCore -Version 1.0.7
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TelnetNegotiationCore" Version="1.0.7" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add TelnetNegotiationCore --version 1.0.7
#r "nuget: TelnetNegotiationCore, 1.0.7"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install TelnetNegotiationCore as a Cake Addin
#addin nuget:?package=TelnetNegotiationCore&version=1.0.7

// Install TelnetNegotiationCore as a Cake Tool
#tool nuget:?package=TelnetNegotiationCore&version=1.0.7

Discord Build Status NuGet Larger Logo

Telnet Negotiation Core

Summary

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.

State

This library is in a state where breaking changes to the interface are expected.

Support

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

ANSI Support, ETC?

Being a Telnet Negotiation Library, this library doesn't give support for extensions like ANSI, Pueblo, MXP, etc at this time.

Use

Client

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 Task 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 Task WriteBackAsync(byte[] writeback, Encoding encoding) =>
  Task.Run(() => Console.WriteLine(encoding.GetString(writeback)));

public Task SignalGMCPAsync((string module, string writeback) val, Encoding encoding) =>
  Task.Run(() => _Logger.LogDebug("GMCP Signal: {Module}: {WriteBack}", val.module, val.writeback));

public Task SignalMSSPAsync(MSSPConfig val) =>
  Task.Run(() => _Logger.LogDebug("New MSSP: {@MSSP}", val));

public Task SignalPromptAsync() =>
  Task.Run(() => _Logger.LogDebug("Prompt"));

public Task 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();
}

Server

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 Task 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 Task SignalGMCPAsync((string module, string writeback) val)
  {
    _Logger.LogDebug("GMCP Signal: {Module}: {WriteBack}", val.module, val.writeback);
    return Task.CompletedTask;
  }

  public Task SignalMSSPAsync(MSSPConfig val)
  {
    _Logger.LogDebug("New MSSP: {@MSSPConfig}", val);
    return Task.CompletedTask;
  }

  public Task SignalNAWSAsync(int height, int width)
  {
    _Logger.LogDebug("Client Height and Width updated: {Height}x{Width}", height, width);
    return Task.CompletedTask;
  }

  private static async Task SignalMSDPAsync(MSDPServerHandler handler, TelnetInterpreter telnet, string config) =>
    await handler.HandleAsync(telnet, config);

  public static async Task 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 Task MSDPUpdateBehavior(string resetVariable)
  {
    _Logger.LogDebug("MSDP Reset Request: {@Reset}", resetVariable);
    await Task.CompletedTask;
  }

  public async override Task 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);
    }
  }
}
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.7 163 3/19/2024
1.0.6 95 1/9/2024
1.0.4 87 1/9/2024
1.0.3 87 1/8/2024
1.0.2 98 1/7/2024
1.0.1 93 1/4/2024
1.0.0 100 1/3/2024

# Change Log
All notable changes to this project will be documented in this file.

## [1.0.7] - 2024-03-19

### Changed
- Get NuGet to play nice about dependencies.

## [1.0.6] - 2024-01-09

### Changed
- Replaces 1.0.5, which was an invalid package update.
- Removed MoreLINQ dependency by making a copy of the function I needed and keep dependencies lower. License retained in the source file - to abide by Apache2 License.

## [1.0.5] - 2024-01-09

### Changed
- Removed MoreLINQ dependency by making a copy of the function I needed and keep dependencies lower. License retained in the source file - to abide by Apache2 License.

## [1.0.4] - 2024-01-09
 
### Changed
- Removed Serilog dependency in favor of Microsoft.Extensions.Logging.Abstractions, which allows one to inject the preferred logger.

## [1.0.3] - 2024-01-08
 
### Fixed
- Ensure that the Project Dependency on TelnetNegotiationCore.Functional is added as a DLL.

## [1.0.2] - 2024-01-07
 
### Added
- Add MSDP support.
- Added a helper function to convert strings to safe byte arrays.

### Changed
- Altered EOR functionality.

## [1.0.1] - 2024-01-03
 
### Added
- Add callback function for MSSP.

### Changed
- Target .NET 8.0.
- Change Methods to be properly async.
- Modernized TestClient example to use Pipes.
- Modernized TestServer example to use Pipes and Kestrel.

## [1.0.0] - 2024-01-03
 
Initial version.

### Added
- Initial support for RFC855 (TELOPT)
- Initial support for RFC858 (GOAHEAD)
- Initial support for RFC1091 (TTERM)
- Initial support for MTTS
- Initial support for RFC885 (EOR)
- Initial support for RFC1073 (NAWS)
- Initial support for RFC2066 (CHARSET)
- Initial support for MSSP
- Initial support for GMCP