From 2c0c6bb203f16f240ca4477b42faabd52ef38984 Mon Sep 17 00:00:00 2001
From: Harry Cordewener <admin@twilightdays.org>
Date: Mon, 29 Jan 2024 23:59:19 -0600
Subject: [PATCH] Implement SUPPRESS GO-AHEAD.

---
 README.md                                     |   4 +-
 .../Interpreters/TelnetEORInterpreter.cs      |   5 +
 .../Interpreters/TelnetStandardInterpreter.cs |  11 +-
 .../TelnetSuppressGAInterpreter.cs            | 107 ++++++++++++++++++
 TelnetNegotiationCore/Models/State.cs         |   6 +-
 TelnetNegotiationCore/Models/Trigger.cs       |   7 ++
 6 files changed, 136 insertions(+), 4 deletions(-)
 create mode 100644 TelnetNegotiationCore/Interpreters/TelnetSuppressGAInterpreter.cs

diff --git a/README.md b/README.md
index 029299d..88e9a86 100644
--- a/README.md
+++ b/README.md
@@ -22,9 +22,9 @@ This library is in a state where breaking changes to the interface are expected.
 | [MSSP](https://tintin.mudhalla.net/protocols/mssp)  | MSSP Negotiation                   | Full       | Untested           |
 | [RFC 885](http://www.faqs.org/rfcs/rfc885.html)     | End Of Record Negotiation          | Full       | Untested           | 
 | [EOR](https://tintin.mudhalla.net/protocols/eor)    | End Of Record Negotiation          | Full       | Untested           |
-| [MSDP](https://tintin.mudhalla.net/protocols/msdp)  | Mud Server Data Protocol           | Partial    | Planned            |
+| [MSDP](https://tintin.mudhalla.net/protocols/msdp)  | Mud Server Data Protocol           | Partial    | Partial Tested     |
 | [RFC 2066](http://www.faqs.org/rfcs/rfc2066.html)   | Charset Negotiation                | Partial    | No TTABLE support  |
-| [RFC 858](http://www.faqs.org/rfcs/rfc858.html)     | Suppress GOAHEAD Negotiation       | No         | Planned            |
+| [RFC 858](http://www.faqs.org/rfcs/rfc858.html)     | Suppress GOAHEAD Negotiation       | Full       | Untested           |
 | [RFC 1572](http://www.faqs.org/rfcs/rfc1572.html)   | New Environment Negotiation        | No         | Planned            |
 | [MNES](https://tintin.mudhalla.net/protocols/mnes)  | Mud New Environment Negotiation    | No         | Planned            |
 | [MCCP](https://tintin.mudhalla.net/protocols/mccp)  | Mud Client Compression Protocol    | No         | Rejects            |
diff --git a/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs
index b365791..7698d44 100644
--- a/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs
+++ b/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs
@@ -130,6 +130,11 @@ public async Task SendPromptAsync(byte[] send)
 			{
 				await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.EOR]);
 			}
+			// TODO: Tie into _doGA
+			if(_doEOR is not null or false)
+			{
+				await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.GA]);
+			}
 		}
 
 		/// <summary>
diff --git a/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs
index f8bbccf..5102649 100644
--- a/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs
+++ b/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs
@@ -104,7 +104,16 @@ public TelnetInterpreter(TelnetMode mode, ILogger logger)
 			SupportedCharacterSets = new Lazy<byte[]>(CharacterSets, true);
 
 			var li = new List<Func<StateMachine<State, Trigger>, StateMachine<State, Trigger>>> {
-				SetupSafeNegotiation, SetupEORNegotiation, SetupMSSPNegotiation, SetupMSDPNegotiation, SetupGMCPNegotiation, SetupTelnetTerminalType, SetupCharsetNegotiation, SetupNAWS, SetupStandardProtocol
+				SetupSafeNegotiation, 
+				SetupEORNegotiation, 
+				SetupSuppressGANegotiation, 
+				SetupMSSPNegotiation, 
+				SetupMSDPNegotiation, 
+				SetupGMCPNegotiation, 
+				SetupTelnetTerminalType, 
+				SetupCharsetNegotiation, 
+				SetupNAWS, 
+				SetupStandardProtocol
 			}.AggregateRight(TelnetStateMachine, (func, stateMachine) => func(stateMachine));
 
 			if (logger.IsEnabled(LogLevel.Trace))
diff --git a/TelnetNegotiationCore/Interpreters/TelnetSuppressGAInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetSuppressGAInterpreter.cs
new file mode 100644
index 0000000..b085deb
--- /dev/null
+++ b/TelnetNegotiationCore/Interpreters/TelnetSuppressGAInterpreter.cs
@@ -0,0 +1,107 @@
+using Microsoft.Extensions.Logging;
+using Stateless;
+using System.Threading.Tasks;
+using TelnetNegotiationCore.Models;
+
+namespace TelnetNegotiationCore.Interpreters
+{
+	public partial class TelnetInterpreter
+	{
+		private bool? _doGA = true;
+
+		/// <summary>
+		/// Character set Negotiation will set the Character Set and Character Page Server & Client have agreed to.
+		/// </summary>
+		/// <param name="tsm">The state machine.</param>
+		/// <returns>Itself</returns>
+		private StateMachine<State, Trigger> SetupSuppressGANegotiation(StateMachine<State, Trigger> tsm)
+		{
+			if (Mode == TelnetMode.Server)
+			{
+				tsm.Configure(State.Do)
+					.Permit(Trigger.SUPPRESSGOAHEAD, State.DoSUPPRESSGOAHEAD);
+
+				tsm.Configure(State.Dont)
+					.Permit(Trigger.SUPPRESSGOAHEAD, State.DontSUPPRESSGOAHEAD);
+
+				tsm.Configure(State.DoSUPPRESSGOAHEAD)
+					.SubstateOf(State.Accepting)
+					.OnEntryAsync(OnDoSuppressGAAsync);
+
+				tsm.Configure(State.DontSUPPRESSGOAHEAD)
+					.SubstateOf(State.Accepting)
+					.OnEntryAsync(OnDontSuppressGAAsync);
+
+				RegisterInitialWilling(WillingSuppressGAAsync);
+			}
+			else
+			{
+				tsm.Configure(State.Willing)
+					.Permit(Trigger.SUPPRESSGOAHEAD, State.WillSUPPRESSGOAHEAD);
+
+				tsm.Configure(State.Refusing)
+					.Permit(Trigger.SUPPRESSGOAHEAD, State.WontSUPPRESSGOAHEAD);
+
+				tsm.Configure(State.WontSUPPRESSGOAHEAD)
+					.SubstateOf(State.Accepting)
+					.OnEntryAsync(WontSuppressGAAsync);
+
+				tsm.Configure(State.WillSUPPRESSGOAHEAD)
+					.SubstateOf(State.Accepting)
+					.OnEntryAsync(OnWillSuppressGAAsync);
+			}
+
+			return tsm;
+		}
+
+
+		private async Task OnSUPPRESSGOAHEADPrompt()
+		{
+			_Logger.LogDebug("Connection: {ConnectionState}", "Server is prompting SUPPRESSGOAHEAD");
+			await (SignalOnPromptingAsync?.Invoke() ?? Task.CompletedTask);
+		}
+
+		private async Task OnDontSuppressGAAsync()
+		{
+			_Logger.LogDebug("Connection: {ConnectionState}", "Client won't do SUPPRESSGOAHEAD - do nothing");
+			_doGA = true;
+			await Task.CompletedTask;
+		}
+
+		private async Task WontSuppressGAAsync()
+		{
+			_Logger.LogDebug("Connection: {ConnectionState}", "Server won't do SUPPRESSGOAHEAD - do nothing");
+			_doGA = true;
+			await Task.CompletedTask;
+		}
+
+		/// <summary>
+		/// Announce we do SUPPRESSGOAHEAD negotiation to the client.
+		/// </summary>
+		private async Task WillingSuppressGAAsync()
+		{
+			_Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to SUPPRESSGOAHEAD!");
+			await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.SUPPRESSGOAHEAD]);
+		}
+
+		/// <summary>
+		/// Store that we are now in SUPPRESSGOAHEAD mode.
+		/// </summary>
+		private Task OnDoSuppressGAAsync(StateMachine<State, Trigger>.Transition _)
+		{
+			_Logger.LogDebug("Connection: {ConnectionState}", "Client supports End of Record.");
+			_doGA =false;
+			return Task.CompletedTask;
+		}
+
+		/// <summary>
+		/// Store that we are now in SUPPRESSGOAHEAD mode.
+		/// </summary>
+		private async Task OnWillSuppressGAAsync(StateMachine<State, Trigger>.Transition _)
+		{
+			_Logger.LogDebug("Connection: {ConnectionState}", "Server supports End of Record.");
+			_doGA = false;
+			await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.SUPPRESSGOAHEAD]);
+		}
+	}
+}
diff --git a/TelnetNegotiationCore/Models/State.cs b/TelnetNegotiationCore/Models/State.cs
index 6b00673..5574a57 100644
--- a/TelnetNegotiationCore/Models/State.cs
+++ b/TelnetNegotiationCore/Models/State.cs
@@ -102,7 +102,11 @@ public enum State : sbyte
 		EvaluatingMSDP,
 		CompletingMSDP,
 		AlmostNegotiatingMSDP,
-		EscapingMSDP
+		EscapingMSDP,
+		DoSUPPRESSGOAHEAD,
+		DontSUPPRESSGOAHEAD,
+		WillSUPPRESSGOAHEAD,
+		WontSUPPRESSGOAHEAD
 		#endregion MSDP Negotiation
 	}
 }
diff --git a/TelnetNegotiationCore/Models/Trigger.cs b/TelnetNegotiationCore/Models/Trigger.cs
index 5aef643..8d5e4f4 100644
--- a/TelnetNegotiationCore/Models/Trigger.cs
+++ b/TelnetNegotiationCore/Models/Trigger.cs
@@ -297,6 +297,13 @@ public enum Trigger : short
 		/// </remarks>
 		NOP = 241,
 		/// <summary>
+		/// Go ahead. Used, under certain circumstances, to tell the other end that it can transmit.
+		/// </summary>
+		/// <remarks>
+		/// RFC 854: http://www.faqs.org/rfcs/rfc854.html
+		/// </remarks>
+		GA = 249,
+		/// <summary>
 		/// The start of sub-negotiation options.	
 		/// </summary>
 		/// <remarks>