Skip to content

Commit

Permalink
Basic GMCP support. Tested in Server Mode using BeipMU.
Browse files Browse the repository at this point in the history
  • Loading branch information
HarryCordewener committed Jan 3, 2024
1 parent 8d27ded commit 434fa01
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ At this time, this repository is in a rough state and does not yet implement som
| https://tintin.mudhalla.net/protocols/msdp | Mud Server Data Protocol | No | Planned |
| https://tintin.mudhalla.net/rfc/rfc1950/ | ZLIB Compression | No | Planned |
| https://tintin.mudhalla.net/protocols/mccp | Mud Client Compression Protocol | No | Planned |
| https://tintin.mudhalla.net/protocols/gmcp | Generic Mud Communication Protocol | No | Planned |
| https://tintin.mudhalla.net/protocols/gmcp | Generic Mud Communication Protocol | Partial | MSDP Planned |
| https://tintin.mudhalla.net/protocols/mccp | Mud Client Compression Protocol | No | Planned |
| http://www.faqs.org/rfcs/rfc857.html | Echo Negotiation | No | Rejects |
| http://www.faqs.org/rfcs/rfc1079.html | Terminal Speed Negotiation | No | Rejects |
Expand Down
8 changes: 8 additions & 0 deletions TelnetNegotiationCore.TestClient/MockClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ public Task WriteBack(byte[] writeback, Encoding encoding)
return Task.CompletedTask;
}

public Task WriteBackGMCP((string module, byte[] writeback) val, Encoding encoding)
{
string str = encoding.GetString(val.writeback);
_Logger.Information("Writeback: {module}: {writeBack}", val.module, str);
return Task.CompletedTask;
}

public Task SignalNAWS(int height, int width)
{
_Logger.Information("Client Height and Width updated: {height}x{width}", height, width);
Expand All @@ -52,6 +59,7 @@ public void Handle(TcpClient client)
{
CallbackOnSubmit = WriteBack,
CallbackNegotiation = (x) => WriteToOutputStream(x, output),
CallbackOnGMCP = WriteBackGMCP,
NAWSCallback = SignalNAWS,
CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") }
}.Validate()
Expand Down
8 changes: 8 additions & 0 deletions TelnetNegotiationCore.TestServer/MockServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ public void StartListener()

private async Task WriteToOutputStream(byte[] arg, StreamWriter writer) => await writer.BaseStream.WriteAsync(arg);

public Task WriteBackGMCP((string module, byte[] writeback) val, Encoding encoding)
{
string str = encoding.GetString(val.writeback);
_Logger.Information("Writeback: {module}: {writeBack}", val.module, str);
return Task.CompletedTask;
}

public Task WriteBack(byte[] writeback, Encoding encoding)
{
string str = encoding.GetString(writeback);
Expand Down Expand Up @@ -81,6 +88,7 @@ public void HandleDevice(object obj)
telnet = new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, _Logger.ForContext<TelnetInterpreter>())
{
CallbackOnSubmit = WriteBack,
CallbackOnGMCP = WriteBackGMCP,
CallbackNegotiation = (x) => WriteToOutputStream(x, output),
NAWSCallback = SignalNAWS,
CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") }
Expand Down
4 changes: 4 additions & 0 deletions TelnetNegotiationCore.UnitTests/CHARSETTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class CHARSETTests

private Task WriteBackToOutput(byte[] arg1, Encoding arg2) => throw new NotImplementedException();

private Task WriteBackToGMCP((string module, byte[] writeback) arg1, Encoding arg2) => throw new NotImplementedException();

private Task ClientWriteBackToNegotiate(byte[] arg1)
{
_negotiationOutput = arg1;
Expand Down Expand Up @@ -50,6 +52,7 @@ public async Task ServerEvaluationCheck(IEnumerable<byte[]> clientSends, IEnumer
{
CallbackNegotiation = ServerWriteBackToNegotiate,
CallbackOnSubmit = WriteBackToOutput,
CallbackOnGMCP = WriteBackToGMCP,
CallbackOnByte = (x, y) => Task.CompletedTask,
}.RegisterMSSPConfig(() => new MSSPConfig
{
Expand Down Expand Up @@ -86,6 +89,7 @@ public async Task ClientEvaluationCheck(IEnumerable<byte[]> serverSends, IEnumer
{
CallbackNegotiation = ClientWriteBackToNegotiate,
CallbackOnSubmit = WriteBackToOutput,
CallbackOnGMCP = WriteBackToGMCP,
CallbackOnByte = (x, y) => Task.CompletedTask,
CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") }
}.RegisterMSSPConfig(() => new MSSPConfig
Expand Down
7 changes: 5 additions & 2 deletions TelnetNegotiationCore.UnitTests/CreateDotGraph.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using NUnit.Framework;
using Serilog;
using Stateless.Graph;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
Expand Down Expand Up @@ -29,6 +28,7 @@ public async Task WriteClientDotGraph()
var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Client, _Logger.ForContext<TelnetInterpreter>())
{
CallbackOnSubmit = WriteBack,
CallbackOnGMCP = WriteBackToGMCP,
CallbackNegotiation = WriteToOutputStream,
NAWSCallback = SignalNAWS,
CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") }
Expand All @@ -44,6 +44,7 @@ public async Task WriteServerDotGraph()
var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, _Logger.ForContext<TelnetInterpreter>())
{
CallbackOnSubmit = WriteBack,
CallbackOnGMCP = WriteBackToGMCP,
CallbackNegotiation = WriteToOutputStream,
NAWSCallback = SignalNAWS,
CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") }
Expand All @@ -67,7 +68,9 @@ public async Task WriteServerDotGraph()
private async Task WriteToOutputStream(byte[] arg) => await Task.CompletedTask;

private async Task SignalNAWS(int arg1, int arg2) => await Task.CompletedTask;

private async Task WriteBack(byte[] arg1, Encoding encoding) => await Task.CompletedTask;

private async Task WriteBackToGMCP((string module, byte[] writeback) arg1, Encoding arg2) => await Task.CompletedTask;
}
}
7 changes: 7 additions & 0 deletions TelnetNegotiationCore.UnitTests/TTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ private Task WriteBackToNegotiate(byte[] arg1)
return Task.CompletedTask;
}

private Task WriteBackToGMCP((string Package, byte[] Info) tuple, Encoding encoding)
{
throw new NotImplementedException();
}

[SetUp]
public async Task Setup()
{
Expand All @@ -40,6 +45,7 @@ public async Task Setup()
{
CallbackNegotiation = WriteBackToNegotiate,
CallbackOnSubmit = WriteBackToOutput,
CallbackOnGMCP = WriteBackToGMCP,
CallbackOnByte = (x, y) => Task.CompletedTask,
}.RegisterMSSPConfig(() => new MSSPConfig
{
Expand All @@ -57,6 +63,7 @@ public async Task Setup()
{
CallbackNegotiation = WriteBackToNegotiate,
CallbackOnSubmit = WriteBackToOutput,
CallbackOnGMCP = WriteBackToGMCP,
CallbackOnByte = (x, y) => Task.CompletedTask,
}.RegisterMSSPConfig(() => new MSSPConfig
{
Expand Down
122 changes: 122 additions & 0 deletions TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using Stateless;
using System.Text;
using System.Threading.Tasks;
using System;
using TelnetNegotiationCore.Models;
using System.Collections.Generic;
using OneOf;
using MoreLinq;
using System.Linq;

namespace TelnetNegotiationCore.Interpreters
{
public partial class TelnetInterpreter
{
private List<byte> _gmcpBytes = new();

public Func<(string Package, byte[] Info), Encoding, Task> CallbackOnGMCP { get; init; }

private StateMachine<State, Trigger> SetupGMCPNegotiation(StateMachine<State, Trigger> tsm)
{
if(Mode == TelnetMode.Server)
{
tsm.Configure(State.Do)
.Permit(Trigger.GMCP, State.DoGMCP);

tsm.Configure(State.Dont)
.Permit(Trigger.GMCP, State.DontGMCP);

tsm.Configure(State.DoGMCP)
.SubstateOf(State.Accepting)
.OnEntry(() => _Logger.Debug("Connection: {connectionStatus}", "Client will do GMCP"));

tsm.Configure(State.DontGMCP)
.SubstateOf(State.Accepting)
.OnEntry(() => _Logger.Debug("Connection: {connectionStatus}", "Client will not GMCP"));
}
else if (Mode == TelnetMode.Client)
{
tsm.Configure(State.Willing)
.Permit(Trigger.GMCP, State.WillGMCP);

tsm.Configure(State.Refusing)
.Permit(Trigger.GMCP, State.WontGMCP);

tsm.Configure(State.WillGMCP)
.SubstateOf(State.Accepting)
.OnEntry(() => _Logger.Debug("Connection: {connectionStatus}", "Server will do GMCP"));

tsm.Configure(State.WontGMCP)
.SubstateOf(State.Accepting)
.OnEntry(() => _Logger.Debug("Connection: {connectionStatus}", "Client will GMCP"));
}

tsm.Configure(State.SubNegotiation)
.Permit(Trigger.GMCP, State.AlmostNegotiatingGMCP)
.OnEntry(() => _gmcpBytes.Clear());

TriggerHelper.ForAllTriggersButIAC(t => tsm
.Configure(State.EvaluatingGMCPValue)
.PermitReentry(t)
.OnEntryFrom(ParametarizedTrigger(t), RegisterGMCPValue));

TriggerHelper.ForAllTriggersButIAC(t => tsm
.Configure(State.AlmostNegotiatingGMCP)
.Permit(t, State.EvaluatingGMCPValue)
.OnEntryFrom(ParametarizedTrigger(t), RegisterGMCPValue));

tsm.Configure(State.EvaluatingGMCPValue)
.Permit(Trigger.IAC, State.EscapingGMCPValue);

tsm.Configure(State.AlmostNegotiatingGMCP)
.Permit(Trigger.IAC, State.EscapingGMCPValue);

tsm.Configure(State.EscapingGMCPValue)
.Permit(Trigger.IAC, State.EvaluatingGMCPValue)
.Permit(Trigger.SE, State.CompletingGMCPValue);

tsm.Configure(State.CompletingGMCPValue)
.SubstateOf(State.Accepting)
.OnEntryAsync(CompleteGMCPNegotiation);

RegisterInitialWilling(async () => await WillGMCPAsync(null));

return tsm;
}

/// <summary>
/// Adds a byte to the register.
/// </summary>
/// <param name="b">Byte.</param>
private void RegisterGMCPValue(OneOf<byte, Trigger> b)
{
_gmcpBytes.Add(b.AsT0);
}

/// <summary>
/// Completes the GMCP Negotiation. This is currently assuming a golden path.
/// </summary>
/// <param name="_">Transition, ignored.</param>
/// <returns>Task</returns>
private async Task CompleteGMCPNegotiation(StateMachine<State, Trigger>.Transition _)
{
var space = CurrentEncoding.GetBytes(" ").First();
var firstSpace = _gmcpBytes.FindIndex(x => x == space);
var packageBytes = _gmcpBytes.Skip(1).Take(firstSpace-1).ToArray();
var rest = _gmcpBytes.Skip(firstSpace + 1).ToArray();
await CallbackOnGMCP((Package: CurrentEncoding.GetString(packageBytes), Info: rest), CurrentEncoding);
}

/// <summary>
/// Announces the Server will GMCP.
/// </summary>
/// <param name="_">Transition, ignored.</param>
/// <returns>Task</returns>
private async Task WillGMCPAsync(StateMachine<State, Trigger>.Transition _)
{
_Logger.Debug("Connection: {connectionStatus}", "Announcing the server will GMCP");

await CallbackNegotiation(new byte[] { (byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.GMCP });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public TelnetInterpreter(TelnetMode mode, ILogger logger = null)
SupportedCharacterSets = new Lazy<byte[]>(CharacterSets, true);

var li = new List<Func<StateMachine<State, Trigger>, StateMachine<State, Trigger>>> {
SetupSafeNegotiation, SetupEORNegotiation, SetupMSSPNegotiation, SetupTelnetTerminalType, SetupCharsetNegotiation, SetupNAWS, SetupStandardProtocol
SetupSafeNegotiation, SetupEORNegotiation, SetupMSSPNegotiation, SetupGMCPNegotiation, SetupTelnetTerminalType, SetupCharsetNegotiation, SetupNAWS, SetupStandardProtocol
}.AggregateRight(TelnetStateMachine, (func, statemachine) => func(statemachine));

if (_Logger.IsEnabled(Serilog.Events.LogEventLevel.Verbose))
Expand Down Expand Up @@ -212,6 +212,10 @@ public TelnetInterpreter Validate()
{
throw new ApplicationException($"{CallbackNegotiation} is null and has not been registered.");
}
if (CallbackOnGMCP == null)
{
throw new ApplicationException($"{CallbackOnGMCP} is null and has not been registered.");
}

return this;
}
Expand Down
10 changes: 10 additions & 0 deletions TelnetNegotiationCore/Models/State.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,15 @@ public enum State : sbyte
WontEOR,
WillEOR,
#endregion End of Record Negotiation
#region GMCP Negotiation
DoGMCP,
DontGMCP,
WontGMCP,
WillGMCP,
AlmostNegotiatingGMCP,
EvaluatingGMCPValue,
EscapingGMCPValue,
CompletingGMCPValue
#endregion GMCP Negotiation
}
}

0 comments on commit 434fa01

Please sign in to comment.