From 434fa01016f6450ef644fdae3985b786927446db Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Tue, 2 Jan 2024 19:27:45 -0600 Subject: [PATCH] Basic GMCP support. Tested in Server Mode using BeipMU. --- README.md | 2 +- .../MockClient.cs | 8 ++ .../MockServer.cs | 8 ++ .../CHARSETTests.cs | 4 + .../CreateDotGraph.cs | 7 +- TelnetNegotiationCore.UnitTests/TTypeTests.cs | 7 + .../Interpreters/TelnetGMCPInterpreter.cs | 122 ++++++++++++++++++ .../Interpreters/TelnetStandardInterpreter.cs | 6 +- TelnetNegotiationCore/Models/State.cs | 10 ++ 9 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs diff --git a/README.md b/README.md index b60a93b..34c545c 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/TelnetNegotiationCore.TestClient/MockClient.cs b/TelnetNegotiationCore.TestClient/MockClient.cs index 2909861..eb8679d 100644 --- a/TelnetNegotiationCore.TestClient/MockClient.cs +++ b/TelnetNegotiationCore.TestClient/MockClient.cs @@ -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); @@ -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() diff --git a/TelnetNegotiationCore.TestServer/MockServer.cs b/TelnetNegotiationCore.TestServer/MockServer.cs index 8b1fe6a..df2a4cf 100644 --- a/TelnetNegotiationCore.TestServer/MockServer.cs +++ b/TelnetNegotiationCore.TestServer/MockServer.cs @@ -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); @@ -81,6 +88,7 @@ public void HandleDevice(object obj) telnet = new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, _Logger.ForContext()) { CallbackOnSubmit = WriteBack, + CallbackOnGMCP = WriteBackGMCP, CallbackNegotiation = (x) => WriteToOutputStream(x, output), NAWSCallback = SignalNAWS, CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") } diff --git a/TelnetNegotiationCore.UnitTests/CHARSETTests.cs b/TelnetNegotiationCore.UnitTests/CHARSETTests.cs index 343525d..f604bda 100644 --- a/TelnetNegotiationCore.UnitTests/CHARSETTests.cs +++ b/TelnetNegotiationCore.UnitTests/CHARSETTests.cs @@ -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; @@ -50,6 +52,7 @@ public async Task ServerEvaluationCheck(IEnumerable clientSends, IEnumer { CallbackNegotiation = ServerWriteBackToNegotiate, CallbackOnSubmit = WriteBackToOutput, + CallbackOnGMCP = WriteBackToGMCP, CallbackOnByte = (x, y) => Task.CompletedTask, }.RegisterMSSPConfig(() => new MSSPConfig { @@ -86,6 +89,7 @@ public async Task ClientEvaluationCheck(IEnumerable 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 diff --git a/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs b/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs index 4cbe791..a60b4b3 100644 --- a/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs +++ b/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs @@ -1,7 +1,6 @@ using NUnit.Framework; using Serilog; using Stateless.Graph; -using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -29,6 +28,7 @@ public async Task WriteClientDotGraph() var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Client, _Logger.ForContext()) { CallbackOnSubmit = WriteBack, + CallbackOnGMCP = WriteBackToGMCP, CallbackNegotiation = WriteToOutputStream, NAWSCallback = SignalNAWS, CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") } @@ -44,6 +44,7 @@ public async Task WriteServerDotGraph() var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, _Logger.ForContext()) { CallbackOnSubmit = WriteBack, + CallbackOnGMCP = WriteBackToGMCP, CallbackNegotiation = WriteToOutputStream, NAWSCallback = SignalNAWS, CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") } @@ -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; } } diff --git a/TelnetNegotiationCore.UnitTests/TTypeTests.cs b/TelnetNegotiationCore.UnitTests/TTypeTests.cs index b4fa122..c0c30b4 100644 --- a/TelnetNegotiationCore.UnitTests/TTypeTests.cs +++ b/TelnetNegotiationCore.UnitTests/TTypeTests.cs @@ -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() { @@ -40,6 +45,7 @@ public async Task Setup() { CallbackNegotiation = WriteBackToNegotiate, CallbackOnSubmit = WriteBackToOutput, + CallbackOnGMCP = WriteBackToGMCP, CallbackOnByte = (x, y) => Task.CompletedTask, }.RegisterMSSPConfig(() => new MSSPConfig { @@ -57,6 +63,7 @@ public async Task Setup() { CallbackNegotiation = WriteBackToNegotiate, CallbackOnSubmit = WriteBackToOutput, + CallbackOnGMCP = WriteBackToGMCP, CallbackOnByte = (x, y) => Task.CompletedTask, }.RegisterMSSPConfig(() => new MSSPConfig { diff --git a/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs new file mode 100644 index 0000000..bb38fa1 --- /dev/null +++ b/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs @@ -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 _gmcpBytes = new(); + + public Func<(string Package, byte[] Info), Encoding, Task> CallbackOnGMCP { get; init; } + + private StateMachine SetupGMCPNegotiation(StateMachine 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; + } + + /// + /// Adds a byte to the register. + /// + /// Byte. + private void RegisterGMCPValue(OneOf b) + { + _gmcpBytes.Add(b.AsT0); + } + + /// + /// Completes the GMCP Negotiation. This is currently assuming a golden path. + /// + /// Transition, ignored. + /// Task + private async Task CompleteGMCPNegotiation(StateMachine.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); + } + + /// + /// Announces the Server will GMCP. + /// + /// Transition, ignored. + /// Task + private async Task WillGMCPAsync(StateMachine.Transition _) + { + _Logger.Debug("Connection: {connectionStatus}", "Announcing the server will GMCP"); + + await CallbackNegotiation(new byte[] { (byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.GMCP }); + } + } +} diff --git a/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs index f1e59b0..cb21a4f 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs @@ -101,7 +101,7 @@ public TelnetInterpreter(TelnetMode mode, ILogger logger = null) SupportedCharacterSets = new Lazy(CharacterSets, true); var li = new List, StateMachine>> { - 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)) @@ -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; } diff --git a/TelnetNegotiationCore/Models/State.cs b/TelnetNegotiationCore/Models/State.cs index fec1ac2..73ab766 100644 --- a/TelnetNegotiationCore/Models/State.cs +++ b/TelnetNegotiationCore/Models/State.cs @@ -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 } }