diff --git a/TelnetNegotiationCore.Functional/MSDPLibrary.fs b/TelnetNegotiationCore.Functional/MSDPLibrary.fs index 1b6a122..2f78545 100644 --- a/TelnetNegotiationCore.Functional/MSDPLibrary.fs +++ b/TelnetNegotiationCore.Functional/MSDPLibrary.fs @@ -15,7 +15,7 @@ module MSDPLibrary = | MSDP_ARRAY_CLOSE = 6uy [] - let private MSDPScanTailRec (root: obj, array: seq, encoding: Encoding) = + let rec private MSDPScanTailRec (root: obj, array: byte seq, encoding: Encoding) = let rec scan accRoot accArray = if Seq.length accArray = 0 then (accRoot, accArray) else @@ -42,8 +42,8 @@ module MSDPLibrary = (encoding.GetString(accArray |> Seq.takeWhile(fun x -> x > 6uy) |> Array.ofSeq), accArray |> Seq.skipWhile(fun x -> x > 6uy)) scan root array - let public MSDPScan(array: seq, encoding) = - let (result, _) = MSDPScanTailRec(Map [], array, encoding) + let public MSDPScan(array: byte seq, encoding) = + let result, _ = MSDPScanTailRec(Map [], array, encoding) result let parseJsonRoot (jsonRootNode: JsonNode, encoding: Encoding) = @@ -55,16 +55,16 @@ module MSDPLibrary = |> Seq.map (fun prop -> let key = prop.Key let value = parseJsonValue prop.Value - [(byte)Trigger.MSDP_VAR] @ (encoding.GetBytes(key) |> List.ofArray) @ [(byte)Trigger.MSDP_VAL] @ value + [byte Trigger.MSDP_VAR] @ (encoding.GetBytes(key) |> List.ofArray) @ [byte Trigger.MSDP_VAL] @ value ) |> List.concat - [(byte)Trigger.MSDP_TABLE_OPEN] @ parsedObj @ [(byte)Trigger.MSDP_TABLE_CLOSE] + [byte Trigger.MSDP_TABLE_OPEN] @ parsedObj @ [(byte)Trigger.MSDP_TABLE_CLOSE] | JsonValueKind.Array -> let parsedArr = jsonNode.AsArray() - |> Seq.map (fun prop -> [(byte)Trigger.MSDP_VAL] @ parseJsonValue(prop)) + |> Seq.map (fun prop -> [byte Trigger.MSDP_VAL] @ parseJsonValue(prop)) |> List.ofSeq |> List.concat - [(byte)Trigger.MSDP_ARRAY_OPEN] @ parsedArr @ [(byte)Trigger.MSDP_ARRAY_CLOSE] + [byte Trigger.MSDP_ARRAY_OPEN] @ parsedArr @ [byte Trigger.MSDP_ARRAY_CLOSE] | JsonValueKind.String -> encoding.GetBytes(jsonNode.AsValue().ToString()) |> List.ofArray | JsonValueKind.Number -> encoding.GetBytes(jsonNode.AsValue().ToString()) |> List.ofArray | JsonValueKind.True -> encoding.GetBytes("1") |> List.ofArray diff --git a/TelnetNegotiationCore.TestServer/TelnetNegotiationCore.TestServer.csproj b/TelnetNegotiationCore.TestServer/TelnetNegotiationCore.TestServer.csproj index 8e3dbe6..a47c4b5 100644 --- a/TelnetNegotiationCore.TestServer/TelnetNegotiationCore.TestServer.csproj +++ b/TelnetNegotiationCore.TestServer/TelnetNegotiationCore.TestServer.csproj @@ -6,11 +6,11 @@ - true + false - True + false diff --git a/TelnetNegotiationCore.UnitTests/BaseTest.cs b/TelnetNegotiationCore.UnitTests/BaseTest.cs index 6f20a9d..fa0e774 100644 --- a/TelnetNegotiationCore.UnitTests/BaseTest.cs +++ b/TelnetNegotiationCore.UnitTests/BaseTest.cs @@ -7,7 +7,7 @@ namespace TelnetNegotiationCore.UnitTests { public class BaseTest { - static internal Microsoft.Extensions.Logging.ILogger logger; + internal static readonly Microsoft.Extensions.Logging.ILogger logger; static BaseTest() { diff --git a/TelnetNegotiationCore.UnitTests/CHARSETTests.cs b/TelnetNegotiationCore.UnitTests/CHARSETTests.cs index 9e386ca..a9fef38 100644 --- a/TelnetNegotiationCore.UnitTests/CHARSETTests.cs +++ b/TelnetNegotiationCore.UnitTests/CHARSETTests.cs @@ -11,7 +11,7 @@ namespace TelnetNegotiationCore.UnitTests { [TestFixture] - public class CHARSETTests() : BaseTest + public class CharsetTests() : BaseTest { private byte[] _negotiationOutput; diff --git a/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs b/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs index ffb7267..39276a9 100644 --- a/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs +++ b/TelnetNegotiationCore.UnitTests/CreateDotGraph.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using NUnit.Framework; +using NUnit.Framework; using Stateless.Graph; using System.Collections.Generic; using System.IO; @@ -8,62 +7,62 @@ using TelnetNegotiationCore.Interpreters; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.UnitTests +namespace TelnetNegotiationCore.UnitTests; + +[TestFixture( + Category = "Tool", + Description = "Creates the DotGraph files for Server and Client forms. Some of these are combined.")] +public class CreateDotGraph : BaseTest { - [TestFixture( - Category = "Tool", - Description = "Creates the DotGraph files for Server and Client forms. Some of these are combined.")] - public class CreateDotGraph : BaseTest - { - [Test] - public async Task WriteClientDotGraph() - { - var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Client, logger) - { - CallbackOnSubmitAsync = WriteBack, - SignalOnGMCPAsync = WriteBackToGMCP, - CallbackNegotiationAsync = WriteToOutputStream, - SignalOnNAWSAsync = SignalNAWS, - CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") } - }.BuildAsync(); + [Test] + public async Task WriteClientDotGraph() + { + var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Client, logger) + { + CallbackOnSubmitAsync = WriteBack, + SignalOnGMCPAsync = WriteBackToGMCP, + CallbackNegotiationAsync = WriteToOutputStream, + SignalOnNAWSAsync = SignalNAWS, + CharsetOrder = [Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1")] + }.BuildAsync(); + + var dotGraph = UmlDotGraph.Format(telnet.TelnetStateMachine.GetInfo()); + await File.WriteAllTextAsync(Path.Combine(Directory.GetCurrentDirectory(), "..", "ClientDotGraph.dot"), dotGraph); + } - var dotGraph = UmlDotGraph.Format(telnet.TelnetStateMachine.GetInfo()); - File.WriteAllText(Path.Combine(Directory.GetCurrentDirectory(), "..", "ClientDotGraph.dot"), dotGraph); - } + [Test] + public async Task WriteServerDotGraph() + { + var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, logger) + { + CallbackOnSubmitAsync = WriteBack, + SignalOnGMCPAsync = WriteBackToGMCP, + CallbackNegotiationAsync = WriteToOutputStream, + SignalOnNAWSAsync = SignalNAWS, + CharsetOrder = [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 + { + { "Foo", "Bar" }, + { "Baz", (string[]) ["Moo", "Meow"] } + } + }).BuildAsync(); - [Test] - public async Task WriteServerDotGraph() - { - var telnet = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, logger) - { - CallbackOnSubmitAsync = WriteBack, - SignalOnGMCPAsync = WriteBackToGMCP, - CallbackNegotiationAsync = WriteToOutputStream, - SignalOnNAWSAsync = SignalNAWS, - CharsetOrder = new[] { Encoding.GetEncoding("utf-8"), Encoding.GetEncoding("iso-8859-1") } - } - .RegisterMSSPConfig(() => new MSSPConfig - { - Name = "My Telnet Negotiated Server", - UTF_8 = true, - Gameplay = new[] { "ABC", "DEF" }, - Extended = new Dictionary - { - { "Foo", "Bar"}, - { "Baz", new [] {"Moo", "Meow" }} - } - }).BuildAsync(); + var dotGraph = UmlDotGraph.Format(telnet.TelnetStateMachine.GetInfo()); + await File.WriteAllTextAsync(Path.Combine(Directory.GetCurrentDirectory(), "..", "ServerDotGraph.dot"), + dotGraph); + } - var dotGraph = UmlDotGraph.Format(telnet.TelnetStateMachine.GetInfo()); - File.WriteAllText(Path.Combine(Directory.GetCurrentDirectory(), "..", "ServerDotGraph.dot"), dotGraph); - } + private async Task WriteToOutputStream(byte[] arg) => await Task.CompletedTask; - private async Task WriteToOutputStream(byte[] arg) => await Task.CompletedTask; + private async Task SignalNAWS(int arg1, int arg2) => await Task.CompletedTask; - private async Task SignalNAWS(int arg1, int arg2) => await Task.CompletedTask; - - private async Task WriteBack(byte[] arg1, Encoding encoding, TelnetInterpreter t) => await Task.CompletedTask; + private async Task WriteBack(byte[] arg1, Encoding encoding, TelnetInterpreter t) => await Task.CompletedTask; - private async Task WriteBackToGMCP((string module, string writeback) arg1) => await Task.CompletedTask; - } -} + private async Task WriteBackToGMCP((string module, string writeback) arg1) => await Task.CompletedTask; +} \ No newline at end of file diff --git a/TelnetNegotiationCore.UnitTests/MSDPTests.cs b/TelnetNegotiationCore.UnitTests/MSDPTests.cs index 28884fc..f2cae7c 100644 --- a/TelnetNegotiationCore.UnitTests/MSDPTests.cs +++ b/TelnetNegotiationCore.UnitTests/MSDPTests.cs @@ -5,130 +5,129 @@ using System.Text.Json; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.UnitTests +namespace TelnetNegotiationCore.UnitTests; + +[TestFixture] +public class MSDPTests : BaseTest { - [TestFixture] - public class MSDPTests : BaseTest - { - static readonly Encoding encoding = Encoding.ASCII; + private static readonly Encoding Encoding = Encoding.ASCII; - [TestCaseSource(nameof(FSharpScanTestSequences))] - public void TestFSharpScan(byte[] testcase, dynamic expectedObject) - { - var result = Functional.MSDPLibrary.MSDPScan(testcase, encoding); - logger.LogInformation("Serialized: {Serialized}", JsonSerializer.Serialize(result)); - Assert.AreEqual(JsonSerializer.Serialize(expectedObject), JsonSerializer.Serialize(result)); - } + [TestCaseSource(nameof(FSharpScanTestSequences))] + public void TestFSharpScan(byte[] testcase, dynamic expectedObject) + { + var result = Functional.MSDPLibrary.MSDPScan(testcase, Encoding); + logger.LogInformation("Serialized: {Serialized}", JsonSerializer.Serialize(result)); + Assert.AreEqual(JsonSerializer.Serialize(expectedObject), JsonSerializer.Serialize(result)); + } - [TestCaseSource(nameof(FSharpReportTestSequences))] - public void TestFSharpReport(dynamic obj, byte[] expectedSequence) - { - byte[] result = Functional.MSDPLibrary.Report(JsonSerializer.Serialize(obj), encoding); - logger.LogInformation("Sequence: {@Serialized}", result); - Assert.AreEqual(expectedSequence, result); - } + [TestCaseSource(nameof(FSharpReportTestSequences))] + public void TestFSharpReport(dynamic obj, byte[] expectedSequence) + { + byte[] result = Functional.MSDPLibrary.Report(JsonSerializer.Serialize(obj), Encoding); + logger.LogInformation("Sequence: {@Serialized}", result); + Assert.AreEqual(expectedSequence, result); + } - public static IEnumerable FSharpScanTestSequences + public static IEnumerable FSharpScanTestSequences + { + get { - get - { - yield return new TestCaseData((byte[])[ + yield return new TestCaseData((byte[])[ (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("LIST"), + .. Encoding.GetBytes("LIST"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("COMMANDS")], - new { LIST = "COMMANDS" }); + .. Encoding.GetBytes("COMMANDS")], + new { LIST = "COMMANDS" }); - yield return new TestCaseData((byte[])[ + yield return new TestCaseData((byte[])[ (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("COMMANDS"), + .. Encoding.GetBytes("COMMANDS"), (byte)Trigger.MSDP_VAL, (byte)Trigger.MSDP_ARRAY_OPEN, (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("LIST"), + .. Encoding.GetBytes("LIST"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("REPORT"), + .. Encoding.GetBytes("REPORT"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("SEND"), + .. Encoding.GetBytes("SEND"), (byte)Trigger.MSDP_ARRAY_CLOSE], - new { COMMANDS = (string[])["LIST", "REPORT", "SEND"] }); + new { COMMANDS = (string[])["LIST", "REPORT", "SEND"] }); - yield return new TestCaseData((byte[])[ + yield return new TestCaseData((byte[])[ (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("ROOM"), + .. Encoding.GetBytes("ROOM"), (byte)Trigger.MSDP_VAL, (byte)Trigger.MSDP_TABLE_OPEN, (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("VNUM"), + .. Encoding.GetBytes("VNUM"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("6008"), + .. Encoding.GetBytes("6008"), (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("NAME"), + .. Encoding.GetBytes("NAME"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("The Forest clearing"), + .. Encoding.GetBytes("The Forest clearing"), (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("AREA"), + .. Encoding.GetBytes("AREA"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("Haon Dor"), + .. Encoding.GetBytes("Haon Dor"), (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("EXITS"), + .. Encoding.GetBytes("EXITS"), (byte)Trigger.MSDP_VAL, (byte)Trigger.MSDP_TABLE_OPEN, (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("n"), + .. Encoding.GetBytes("n"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("6011"), + .. Encoding.GetBytes("6011"), (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("e"), + .. Encoding.GetBytes("e"), (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("6012"), + .. Encoding.GetBytes("6012"), (byte)Trigger.MSDP_TABLE_CLOSE, (byte)Trigger.MSDP_TABLE_CLOSE ], - new { ROOM = new { AREA = "Haon Dor", EXITS = new { e = "6012", n = "6011" }, NAME = "The Forest clearing", VNUM = "6008" } }); - } + new { ROOM = new { AREA = "Haon Dor", EXITS = new { e = "6012", n = "6011" }, NAME = "The Forest clearing", VNUM = "6008" } }); } - public static IEnumerable FSharpReportTestSequences + } + public static IEnumerable FSharpReportTestSequences + { + get { - get - { - yield return new TestCaseData(new { LIST = "COMMANDS" }, (byte[])[ - (byte)Trigger.MSDP_TABLE_OPEN, - (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("LIST"), - (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("COMMANDS"), - (byte)Trigger.MSDP_TABLE_CLOSE]); + yield return new TestCaseData(new { LIST = "COMMANDS" }, (byte[])[ + (byte)Trigger.MSDP_TABLE_OPEN, + (byte)Trigger.MSDP_VAR, + .. Encoding.GetBytes("LIST"), + (byte)Trigger.MSDP_VAL, + .. Encoding.GetBytes("COMMANDS"), + (byte)Trigger.MSDP_TABLE_CLOSE]); - yield return new TestCaseData(new { LIST = (string[])["COMMANDS", "JIM"] }, (byte[])[ - (byte)Trigger.MSDP_TABLE_OPEN, - (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("LIST"), - (byte)Trigger.MSDP_VAL, - (byte)Trigger.MSDP_ARRAY_OPEN, - (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("COMMANDS"), - (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("JIM"), - (byte)Trigger.MSDP_ARRAY_CLOSE, - (byte)Trigger.MSDP_TABLE_CLOSE]); + yield return new TestCaseData(new { LIST = (string[])["COMMANDS", "JIM"] }, (byte[])[ + (byte)Trigger.MSDP_TABLE_OPEN, + (byte)Trigger.MSDP_VAR, + .. Encoding.GetBytes("LIST"), + (byte)Trigger.MSDP_VAL, + (byte)Trigger.MSDP_ARRAY_OPEN, + (byte)Trigger.MSDP_VAL, + .. Encoding.GetBytes("COMMANDS"), + (byte)Trigger.MSDP_VAL, + .. Encoding.GetBytes("JIM"), + (byte)Trigger.MSDP_ARRAY_CLOSE, + (byte)Trigger.MSDP_TABLE_CLOSE]); - yield return new TestCaseData(new { LIST = (dynamic[])["COMMANDS", (dynamic[])["JIM"]] }, (byte[])[ - (byte)Trigger.MSDP_TABLE_OPEN, - (byte)Trigger.MSDP_VAR, - .. encoding.GetBytes("LIST"), - (byte)Trigger.MSDP_VAL, - (byte)Trigger.MSDP_ARRAY_OPEN, - (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("COMMANDS"), - (byte)Trigger.MSDP_VAL, - (byte)Trigger.MSDP_ARRAY_OPEN, - (byte)Trigger.MSDP_VAL, - .. encoding.GetBytes("JIM"), - (byte)Trigger.MSDP_ARRAY_CLOSE, - (byte)Trigger.MSDP_ARRAY_CLOSE, - (byte)Trigger.MSDP_TABLE_CLOSE]); - } + yield return new TestCaseData(new { LIST = (dynamic[])["COMMANDS", (dynamic[])["JIM"]] }, (byte[])[ + (byte)Trigger.MSDP_TABLE_OPEN, + (byte)Trigger.MSDP_VAR, + .. Encoding.GetBytes("LIST"), + (byte)Trigger.MSDP_VAL, + (byte)Trigger.MSDP_ARRAY_OPEN, + (byte)Trigger.MSDP_VAL, + .. Encoding.GetBytes("COMMANDS"), + (byte)Trigger.MSDP_VAL, + (byte)Trigger.MSDP_ARRAY_OPEN, + (byte)Trigger.MSDP_VAL, + .. Encoding.GetBytes("JIM"), + (byte)Trigger.MSDP_ARRAY_CLOSE, + (byte)Trigger.MSDP_ARRAY_CLOSE, + (byte)Trigger.MSDP_TABLE_CLOSE]); } } -} +} \ No newline at end of file diff --git a/TelnetNegotiationCore.UnitTests/TTypeTests.cs b/TelnetNegotiationCore.UnitTests/TTypeTests.cs index 850b1bd..e5842fa 100644 --- a/TelnetNegotiationCore.UnitTests/TTypeTests.cs +++ b/TelnetNegotiationCore.UnitTests/TTypeTests.cs @@ -7,167 +7,166 @@ using TelnetNegotiationCore.Interpreters; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.UnitTests +namespace TelnetNegotiationCore.UnitTests; + +[TestFixture] +public class TTypeTests: BaseTest { - [TestFixture] - public class TTypeTests: BaseTest - { - private TelnetInterpreter _server_ti; - private TelnetInterpreter _client_ti; - private byte[] _negotiationOutput; + private TelnetInterpreter _server_ti; + private TelnetInterpreter _client_ti; + private byte[] _negotiationOutput; - private Task WriteBackToOutput(byte[] arg1, Encoding arg2, TelnetInterpreter t) => throw new NotImplementedException(); + private Task WriteBackToOutput(byte[] arg1, Encoding arg2, TelnetInterpreter t) => throw new NotImplementedException(); - private Task WriteBackToNegotiate(byte[] arg1) { _negotiationOutput = arg1; return Task.CompletedTask; } + private Task WriteBackToNegotiate(byte[] arg1) { _negotiationOutput = arg1; return Task.CompletedTask; } - private Task WriteBackToGMCP((string Package, string Info) tuple) => throw new NotImplementedException(); + private Task WriteBackToGMCP((string Package, string Info) tuple) => throw new NotImplementedException(); - [SetUp] - public async Task Setup() + [SetUp] + public async Task Setup() + { + _server_ti = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, logger) { - _server_ti = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Server, logger) - { - CallbackNegotiationAsync = WriteBackToNegotiate, - CallbackOnSubmitAsync = WriteBackToOutput, - SignalOnGMCPAsync = WriteBackToGMCP, - CallbackOnByteAsync = (x, y) => Task.CompletedTask, - }.RegisterMSSPConfig(() => new MSSPConfig + CallbackNegotiationAsync = WriteBackToNegotiate, + CallbackOnSubmitAsync = WriteBackToOutput, + SignalOnGMCPAsync = WriteBackToGMCP, + CallbackOnByteAsync = (x, y) => Task.CompletedTask, + }.RegisterMSSPConfig(() => new MSSPConfig + { + Name = "My Telnet Negotiated Server", + UTF_8 = true, + Gameplay = ["ABC", "DEF"], + Extended = new Dictionary { - Name = "My Telnet Negotiated Server", - UTF_8 = true, - Gameplay = new[] { "ABC", "DEF" }, - Extended = new Dictionary - { - { "Foo", "Bar"}, - { "Baz", new [] {"Moo", "Meow" }} - } - }).BuildAsync(); + { "Foo", "Bar"}, + { "Baz", (string[]) ["Moo", "Meow"] } + } + }).BuildAsync(); - _client_ti = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Client, logger) - { - CallbackNegotiationAsync = WriteBackToNegotiate, - CallbackOnSubmitAsync = WriteBackToOutput, - SignalOnGMCPAsync = WriteBackToGMCP, - CallbackOnByteAsync = (x, y) => Task.CompletedTask, - }.RegisterMSSPConfig(() => new MSSPConfig + _client_ti = await new TelnetInterpreter(TelnetInterpreter.TelnetMode.Client, logger) + { + CallbackNegotiationAsync = WriteBackToNegotiate, + CallbackOnSubmitAsync = WriteBackToOutput, + SignalOnGMCPAsync = WriteBackToGMCP, + CallbackOnByteAsync = (x, y) => Task.CompletedTask, + }.RegisterMSSPConfig(() => new MSSPConfig + { + Name = "My Telnet Negotiated Client", + UTF_8 = true, + Gameplay = ["ABC", "DEF"], + Extended = new Dictionary { - Name = "My Telnet Negotiated Client", - UTF_8 = true, - Gameplay = new[] { "ABC", "DEF" }, - Extended = new Dictionary - { - { "Foo", "Bar"}, - { "Baz", new [] {"Moo", "Meow" }} - } - }).BuildAsync(); - } + { "Foo", "Bar"}, + { "Baz", new [] {"Moo", "Meow" }} + } + }).BuildAsync(); + } - [TestCaseSource(nameof(ServerTTypeSequences), Category = nameof(TelnetInterpreter.TelnetMode.Server))] - public async Task ServerEvaluationCheck(IEnumerable clientSends, IEnumerable serverShouldRespondWith, IEnumerable RegisteredTTypes) - { - if (clientSends.Count() != serverShouldRespondWith.Count()) - throw new Exception("Invalid Testcase."); + [TestCaseSource(nameof(ServerTTypeSequences), Category = nameof(TelnetInterpreter.TelnetMode.Server))] + public async Task ServerEvaluationCheck(IEnumerable clientSends, IEnumerable serverShouldRespondWith, IEnumerable RegisteredTTypes) + { + if (clientSends.Count() != serverShouldRespondWith.Count()) + throw new Exception("Invalid Testcase."); - foreach ((var clientSend, var serverShouldRespond, var shouldHaveTTypeList) in clientSends.Zip(serverShouldRespondWith, RegisteredTTypes)) + foreach (var (clientSend, serverShouldRespond, shouldHaveTTypeList) in clientSends.Zip(serverShouldRespondWith, RegisteredTTypes)) + { + _negotiationOutput = null; + foreach (var x in clientSend ?? Enumerable.Empty()) { - _negotiationOutput = null; - foreach (var x in clientSend ?? Enumerable.Empty()) - { - await _server_ti.InterpretAsync(x); - } - CollectionAssert.AreEqual(shouldHaveTTypeList ?? Enumerable.Empty(), _server_ti.TerminalTypes); - CollectionAssert.AreEqual(serverShouldRespond, _negotiationOutput); + await _server_ti.InterpretAsync(x); } + CollectionAssert.AreEqual(shouldHaveTTypeList ?? Enumerable.Empty(), _server_ti.TerminalTypes); + CollectionAssert.AreEqual(serverShouldRespond, _negotiationOutput); } + } - [TestCaseSource(nameof(ClientTTypeSequences), Category = nameof(TelnetInterpreter.TelnetMode.Client))] - public async Task ClientEvaluationCheck(IEnumerable serverSends, IEnumerable serverShouldRespondWith) - { - if (serverSends.Count() != serverShouldRespondWith.Count()) - throw new Exception("Invalid Testcase."); + [TestCaseSource(nameof(ClientTTypeSequences), Category = nameof(TelnetInterpreter.TelnetMode.Client))] + public async Task ClientEvaluationCheck(IEnumerable serverSends, IEnumerable serverShouldRespondWith) + { + if (serverSends.Count() != serverShouldRespondWith.Count()) + throw new Exception("Invalid Testcase."); - foreach ((var serverSend, var clientShouldRespond) in serverSends.Zip(serverShouldRespondWith)) + foreach (var (serverSend, clientShouldRespond) in serverSends.Zip(serverShouldRespondWith)) + { + _negotiationOutput = null; + foreach (var x in serverSend ?? Enumerable.Empty()) { - _negotiationOutput = null; - foreach (var x in serverSend ?? Enumerable.Empty()) - { - await _client_ti.InterpretAsync(x); - } - CollectionAssert.AreEqual(clientShouldRespond, _negotiationOutput); + await _client_ti.InterpretAsync(x); } + CollectionAssert.AreEqual(clientShouldRespond, _negotiationOutput); } + } - public static IEnumerable ClientTTypeSequences + public static IEnumerable ClientTTypeSequences + { + get { - get - { - yield return new TestCaseData( - new[] - { - new [] { (byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.TTYPE } - }, - new[] - { - new [] { (byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE } - }).SetName("Basic responds to Server TType DO"); - yield return new TestCaseData( - new byte[][] - { - [(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.TTYPE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE] - }, - new byte[][] - { - [(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'T', (byte)'N', (byte)'C', (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'X', (byte)'T', (byte)'E', (byte)'R', (byte)'M', (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'M', (byte)'T', (byte)'T', (byte)'S', (byte)' ', (byte)'3', (byte)'8', (byte)'5', (byte)'3', (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'M', (byte)'T', (byte)'T', (byte)'S', (byte)' ', (byte)'3', (byte)'8', (byte)'5', (byte)'3', (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'T', (byte)'N', (byte)'C', (byte)Trigger.IAC, (byte)Trigger.SE] - }).SetName("Capable of sending a TType in a cycling manner, with a repeat for the last item"); - } + yield return new TestCaseData( + new[] + { + new [] { (byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.TTYPE } + }, + new[] + { + new [] { (byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE } + }).SetName("Basic responds to Server TType DO"); + yield return new TestCaseData( + new byte[][] + { + [(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.TTYPE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE] + }, + new byte[][] + { + [(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'T', (byte)'N', (byte)'C', (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'X', (byte)'T', (byte)'E', (byte)'R', (byte)'M', (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'M', (byte)'T', (byte)'T', (byte)'S', (byte)' ', (byte)'3', (byte)'8', (byte)'5', (byte)'3', (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'M', (byte)'T', (byte)'T', (byte)'S', (byte)' ', (byte)'3', (byte)'8', (byte)'5', (byte)'3', (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'T', (byte)'N', (byte)'C', (byte)Trigger.IAC, (byte)Trigger.SE] + }).SetName("Capable of sending a TType in a cycling manner, with a repeat for the last item"); } + } - public static IEnumerable ServerTTypeSequences + public static IEnumerable ServerTTypeSequences + { + get { - get - { - yield return new TestCaseData( - new[] { // Client Sends - new[] { (byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE }, - }, - new[] { // Server Should Respond With - new[] { (byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE }, - }, - new[] // Registered TType List After Negotiation - { - Array.Empty() - }).SetName("Basic responds to Client TType Willing"); - yield return new TestCaseData( - new byte[][] { // Client Sends - [(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'A', (byte)'N', (byte)'S', (byte)'I', (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'V', (byte)'T', (byte)'1', (byte)'0', (byte)'0', (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'V', (byte)'T', (byte)'1', (byte)'0', (byte)'0', (byte)Trigger.IAC, (byte)Trigger.SE] - }, - new byte[][] { // Server Should Respond With - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], - null - }, - new[] // Registered TType List After Negotiation - { - Array.Empty(), - ["ANSI"], - ["ANSI", "VT100"], - ["ANSI", "VT100"] - }).SetName("Long response to Client TType Willing"); - } + yield return new TestCaseData( + new[] { // Client Sends + new[] { (byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE }, + }, + new[] { // Server Should Respond With + new[] { (byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE }, + }, + new[] // Registered TType List After Negotiation + { + Array.Empty() + }).SetName("Basic responds to Client TType Willing"); + yield return new TestCaseData( + new byte[][] { // Client Sends + [(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'A', (byte)'N', (byte)'S', (byte)'I', (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'V', (byte)'T', (byte)'1', (byte)'0', (byte)'0', (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, (byte)'V', (byte)'T', (byte)'1', (byte)'0', (byte)'0', (byte)Trigger.IAC, (byte)Trigger.SE] + }, + new byte[][] { // Server Should Respond With + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE], + null + }, + new[] // Registered TType List After Negotiation + { + Array.Empty(), + ["ANSI"], + ["ANSI", "VT100"], + ["ANSI", "VT100"] + }).SetName("Long response to Client TType Willing"); } } } \ No newline at end of file diff --git a/TelnetNegotiationCore/Handlers/MSDPClientHandler.cs b/TelnetNegotiationCore/Handlers/MSDPClientHandler.cs index 4d14f68..52d9f94 100644 --- a/TelnetNegotiationCore/Handlers/MSDPClientHandler.cs +++ b/TelnetNegotiationCore/Handlers/MSDPClientHandler.cs @@ -1,20 +1,15 @@ using System; -using System.Linq; -using System.Text; using System.Threading.Tasks; -namespace TelnetNegotiationCore.Handlers +namespace TelnetNegotiationCore.Handlers; + +/// +/// A simple handler for MSDP that creates a workflow for requesting MSDP information, and storing returned information. +/// +public class MSDPClientHandler() { - /// - /// A simple handler for MSDP that creates a workflow for requesting MSDP information, and storing returned information. - /// - public class MSDPClientHandler + public Task HandleAsync(string serverJson) { - public MSDPClientHandler() { } - - public Task HandleAsync(string serverJson) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } diff --git a/TelnetNegotiationCore/Handlers/MSDPServerHandler.cs b/TelnetNegotiationCore/Handlers/MSDPServerHandler.cs index 52959e9..074887c 100644 --- a/TelnetNegotiationCore/Handlers/MSDPServerHandler.cs +++ b/TelnetNegotiationCore/Handlers/MSDPServerHandler.cs @@ -5,132 +5,131 @@ using TelnetNegotiationCore.Functional; using TelnetNegotiationCore.Interpreters; -namespace TelnetNegotiationCore.Handlers +namespace TelnetNegotiationCore.Handlers; + +/// +/// A simple handler for MSDP that creates a workflow for responding with MSDP information. +/// +/// +/// We need a way to Observe changes to the functions to properly support the REPORT action. +/// This is not currently implemented. +/// +/// +/// A dictionary that resolves Variables, commands, and lists. +/// +public class MSDPServerHandler(MSDPServerModel model) { - /// - /// A simple handler for MSDP that creates a workflow for responding with MSDP information. - /// - /// - /// We need a way to Observe changes to the functions to properly support the REPORT action. - /// This is not currently implemented. - /// - /// - /// A dictionary that resolves Variables, commands, and lists. - /// - public class MSDPServerHandler(MSDPServerModel model) + public MSDPServerModel Data { get; private init; } = model; + + public async Task HandleAsync(TelnetInterpreter telnet, string clientJson) { - public MSDPServerModel Data { get; private init; } = model; + var json = JsonSerializer.Deserialize(clientJson); - public async Task HandleAsync(TelnetInterpreter telnet, string clientJson) + if (json.LIST != null) { - var json = JsonSerializer.Deserialize(clientJson); - - if (json.LIST != null) - { - await HandleListRequestAsync(telnet, (string)json.LIST); - } - else if (json.REPORT != null) - { - await HandleReportRequestAsync(telnet, (string)json.REPORT); - } - else if (json.RESET != null) - { - await HandleResetRequestAsync((string)json.RESET); - } - else if (json.SEND != null) - { - await HandleSendRequestAsync(telnet, (string)json.SEND); - } - else if (json.UNREPORT != null) - { - await HandleUnReportRequestAsync((string)json.UNREPORT); - } - - await Task.CompletedTask; + await HandleListRequestAsync(telnet, (string)json.LIST); + } + else if (json.REPORT != null) + { + await HandleReportRequestAsync(telnet, (string)json.REPORT); } + else if (json.RESET != null) + { + await HandleResetRequestAsync((string)json.RESET); + } + else if (json.SEND != null) + { + await HandleSendRequestAsync(telnet, (string)json.SEND); + } + else if (json.UNREPORT != null) + { + await HandleUnReportRequestAsync((string)json.UNREPORT); + } + + await Task.CompletedTask; + } - /// - /// Handles a request to LIST a single item. For safety, also supports a list of lists. - /// Should at least support: - /// "COMMANDS" Request an array of commands supported by the server. - /// "LISTS" Request an array of lists supported by the server. - /// "CONFIGURABLE_VARIABLES" Request an array of variables the client can configure. - /// "REPORTABLE_VARIABLES" Request an array of variables the server will report. - /// "REPORTED_VARIABLES" Request an array of variables currently being reported. - /// "SENDABLE_VARIABLES" Request an array of variables the server will send. - /// - /// Telnet Interpreter to callback with. - /// The item to report - private async Task HandleListRequestAsync(TelnetInterpreter telnet, string item) => - await ExecuteOnAsync(item, async (val) => - await (Data.Lists.TryGetValue(val, out Func> value) - ? telnet.CallbackNegotiationAsync( - MSDPLibrary.Report(JsonSerializer.Serialize(value()), - telnet.CurrentEncoding)) - : Task.CompletedTask)); + /// + /// Handles a request to LIST a single item. For safety, also supports a list of lists. + /// Should at least support: + /// "COMMANDS" Request an array of commands supported by the server. + /// "LISTS" Request an array of lists supported by the server. + /// "CONFIGURABLE_VARIABLES" Request an array of variables the client can configure. + /// "REPORTABLE_VARIABLES" Request an array of variables the server will report. + /// "REPORTED_VARIABLES" Request an array of variables currently being reported. + /// "SENDABLE_VARIABLES" Request an array of variables the server will send. + /// + /// Telnet Interpreter to callback with. + /// The item to report + private async Task HandleListRequestAsync(TelnetInterpreter telnet, string item) => + await ExecuteOnAsync(item, async (val) => + await (Data.Lists.TryGetValue(val, out Func> value) + ? telnet.CallbackNegotiationAsync( + MSDPLibrary.Report(JsonSerializer.Serialize(value()), + telnet.CurrentEncoding)) + : Task.CompletedTask)); - private async Task HandleReportRequestAsync(TelnetInterpreter telnet, string item) => - await ExecuteOnAsync(item, async (val) => - { - await HandleSendRequestAsync(telnet, val); - Data.Report(val, async (newVal) => await HandleSendRequestAsync(telnet, newVal)); - }); + private async Task HandleReportRequestAsync(TelnetInterpreter telnet, string item) => + await ExecuteOnAsync(item, async (val) => + { + await HandleSendRequestAsync(telnet, val); + Data.Report(val, async (newVal) => await HandleSendRequestAsync(telnet, newVal)); + }); - /// - /// The RESET command works like the LIST command, and can be used to reset groups of variables to their initial state. - /// Most commonly RESET will be called with REPORTABLE_VARIABLES or REPORTED_VARIABLES as the argument, - /// though any LIST option can be used. - /// - /// Item to reset - private async Task HandleResetRequestAsync(string item) => - await ExecuteOnAsync(item, async (var) => - { - var found = Data.Reportable_Variables().TryGetValue(var, out var list); - await Task.CompletedTask; - }); + /// + /// The RESET command works like the LIST command, and can be used to reset groups of variables to their initial state. + /// Most commonly RESET will be called with REPORTABLE_VARIABLES or REPORTED_VARIABLES as the argument, + /// though any LIST option can be used. + /// + /// Item to reset + private async Task HandleResetRequestAsync(string item) => + await ExecuteOnAsync(item, async (var) => + { + var found = Data.Reportable_Variables().TryGetValue(var, out var list); + await Task.CompletedTask; + }); - /// - /// The SEND command can be used by either side, but should typically be used by the client. - /// After the client has received a list of variables, or otherwise knows which variables exist, - /// it can request the server to send those variables and their values with the SEND command. - /// The value of the SEND command should be a list of variables the client wants returned. - /// - /// Telnet interpreter to send back negotiation with - /// The item to send - private async Task HandleSendRequestAsync(TelnetInterpreter telnet, string item) => - await ExecuteOnAsync(item, async (var) => - { - var found = Data.Sendable_Variables().TryGetValue(var, out var val); - var jsonString = $"{{{var}:{(found ? val : "null")}}}"; - await telnet.CallbackNegotiationAsync(MSDPLibrary.Report(jsonString, telnet.CurrentEncoding)); - }); + /// + /// The SEND command can be used by either side, but should typically be used by the client. + /// After the client has received a list of variables, or otherwise knows which variables exist, + /// it can request the server to send those variables and their values with the SEND command. + /// The value of the SEND command should be a list of variables the client wants returned. + /// + /// Telnet interpreter to send back negotiation with + /// The item to send + private async Task HandleSendRequestAsync(TelnetInterpreter telnet, string item) => + await ExecuteOnAsync(item, async (var) => + { + var found = Data.Sendable_Variables().TryGetValue(var, out var val); + var jsonString = $"{{{var}:{(found ? val : "null")}}}"; + await telnet.CallbackNegotiationAsync(MSDPLibrary.Report(jsonString, telnet.CurrentEncoding)); + }); - /// - /// The UNREPORT command is used to remove the report status of variables after the use of the REPORT command. - /// - /// The item to stop reporting on - private async Task HandleUnReportRequestAsync(string item) => - await ExecuteOnAsync(item, async (var) => - { - Data.UnReport(var); - await Task.CompletedTask; - }); + /// + /// The UNREPORT command is used to remove the report status of variables after the use of the REPORT command. + /// + /// The item to stop reporting on + private async Task HandleUnReportRequestAsync(string item) => + await ExecuteOnAsync(item, async (var) => + { + Data.UnReport(var); + await Task.CompletedTask; + }); - private async Task ExecuteOnAsync(string item, Func function) + private async Task ExecuteOnAsync(string item, Func function) + { + string[] items; + if (item.StartsWith('[')) + { + items = JsonSerializer.Deserialize(item); + } + else + { + items = [item]; + } + foreach (var val in items) { - string[] items; - if (item.StartsWith('[')) - { - items = JsonSerializer.Deserialize(item); - } - else - { - items = [item]; - } - foreach (var val in items) - { - await function(val); - } + await function(val); } } } diff --git a/TelnetNegotiationCore/Handlers/MSDPServerModel.cs b/TelnetNegotiationCore/Handlers/MSDPServerModel.cs index 1993946..d7ea6c3 100644 --- a/TelnetNegotiationCore/Handlers/MSDPServerModel.cs +++ b/TelnetNegotiationCore/Handlers/MSDPServerModel.cs @@ -3,115 +3,114 @@ using System.Linq; using System.Threading.Tasks; -namespace TelnetNegotiationCore.Handlers +namespace TelnetNegotiationCore.Handlers; + +/// +/// https://tintin.mudhalla.net/protocols/msdp/ +/// Reportable MSDP Variables +/// These variables are mere suggestions for MUDs wanting to implement MSDP.By using these reportable variables it'll be easier to create MSDP scripts that need little or no modification to work across MUDs. If you create your own set of variables in addition to these, it's suggested to create an extended online specification that describes your variables and their behavior.The SPECIFICATION variable can be used to link people to the web page. +/// +/// General +/// "ACCOUNT_NAME" Name of the player account. +/// "CHARACTER_NAME" Name of the player character. +/// "SERVER_ID" Name of the MUD, or an otherwise unique ID. +/// "SERVER_TIME" The time on the server using either military or civilian time. +/// "SPECIFICATION" URL to the MUD's online MSDP specification, if any. +/// +/// Character +/// "AFFECTS" Current affects in array format. +/// "ALIGNMENT" Current alignment. +/// "EXPERIENCE" Current total experience points. Use 0-100 for percentages. +/// "EXPERIENCE_MAX" Current maximum experience points. Use 100 for percentages. +/// "EXPERIENCE_TNL" Current total experience points Till Next Level.Use 0-100 for percentages. +/// "EXPERIENCE_TNL_MAX" Current maximum experience points Till Next Level.Use 100 for percentages. +/// "HEALTH" Current health points. +/// "HEALTH_MAX" Current maximum health points. +/// "LEVEL" Current level. +/// "MANA" Current mana points. +/// "MANA_MAX" Current maximum mana points. +/// "MONEY" Current amount of money. +/// "MOVEMENT" Current movement points. +/// "MOVEMENT_MAX" Current maximum movement points. +/// +/// Combat +/// "OPPONENT_LEVEL" Level of opponent. +/// "OPPONENT_HEALTH" Current health points of opponent.Use 0-100 for percentages. +/// "OPPONENT_HEALTH_MAX" Current maximum health points of opponent. Use 100 for percentages. +/// "OPPONENT_NAME" Name of opponent. +/// "OPPONENT_STRENGTH" Relative strength of opponent, like the consider mud command. +/// Mapping +/// Indentation indicates the variable is nested within the parent variable using a table. +/// "ROOM" +/// "VNUM" A number uniquely identifying the room. +/// "NAME" The name of the room. +/// "AREA" The area the room is in. +/// "COORDS" +/// "X" The X coordinate of the room. +/// "Y" The Y coordinate of the room. +/// "Z" The Z coordinate of the room. +/// "TERRAIN" The terrain type of the room. Forest, Ocean, etc. +/// "EXITS" Nested abbreviated exit directions (n, e, w, etc) and corresponding destination VNUMs. +/// +/// World +/// "WORLD_TIME" The in game time on the MUD using either military or civilian time. +/// +/// Configurable MSDP Variables +/// Configurable variables are variables on the server that can be altered by the client. Implementing configurable variable support is optional. +/// General +/// "CLIENT_NAME" Name of the MUD client. +/// "CLIENT_VERSION" Version of the MUD client. +/// "PLUGIN_ID" Unique ID of the MSDP plugin/script. +/// +public class MSDPServerModel { /// - /// https://tintin.mudhalla.net/protocols/msdp/ - /// Reportable MSDP Variables - /// These variables are mere suggestions for MUDs wanting to implement MSDP.By using these reportable variables it'll be easier to create MSDP scripts that need little or no modification to work across MUDs. If you create your own set of variables in addition to these, it's suggested to create an extended online specification that describes your variables and their behavior.The SPECIFICATION variable can be used to link people to the web page. - /// - /// General - /// "ACCOUNT_NAME" Name of the player account. - /// "CHARACTER_NAME" Name of the player character. - /// "SERVER_ID" Name of the MUD, or an otherwise unique ID. - /// "SERVER_TIME" The time on the server using either military or civilian time. - /// "SPECIFICATION" URL to the MUD's online MSDP specification, if any. - /// - /// Character - /// "AFFECTS" Current affects in array format. - /// "ALIGNMENT" Current alignment. - /// "EXPERIENCE" Current total experience points. Use 0-100 for percentages. - /// "EXPERIENCE_MAX" Current maximum experience points. Use 100 for percentages. - /// "EXPERIENCE_TNL" Current total experience points Till Next Level.Use 0-100 for percentages. - /// "EXPERIENCE_TNL_MAX" Current maximum experience points Till Next Level.Use 100 for percentages. - /// "HEALTH" Current health points. - /// "HEALTH_MAX" Current maximum health points. - /// "LEVEL" Current level. - /// "MANA" Current mana points. - /// "MANA_MAX" Current maximum mana points. - /// "MONEY" Current amount of money. - /// "MOVEMENT" Current movement points. - /// "MOVEMENT_MAX" Current maximum movement points. - /// - /// Combat - /// "OPPONENT_LEVEL" Level of opponent. - /// "OPPONENT_HEALTH" Current health points of opponent.Use 0-100 for percentages. - /// "OPPONENT_HEALTH_MAX" Current maximum health points of opponent. Use 100 for percentages. - /// "OPPONENT_NAME" Name of opponent. - /// "OPPONENT_STRENGTH" Relative strength of opponent, like the consider mud command. - /// Mapping - /// Indentation indicates the variable is nested within the parent variable using a table. - /// "ROOM" - /// "VNUM" A number uniquely identifying the room. - /// "NAME" The name of the room. - /// "AREA" The area the room is in. - /// "COORDS" - /// "X" The X coordinate of the room. - /// "Y" The Y coordinate of the room. - /// "Z" The Z coordinate of the room. - /// "TERRAIN" The terrain type of the room. Forest, Ocean, etc. - /// "EXITS" Nested abbreviated exit directions (n, e, w, etc) and corresponding destination VNUMs. - /// - /// World - /// "WORLD_TIME" The in game time on the MUD using either military or civilian time. - /// - /// Configurable MSDP Variables - /// Configurable variables are variables on the server that can be altered by the client. Implementing configurable variable support is optional. - /// General - /// "CLIENT_NAME" Name of the MUD client. - /// "CLIENT_VERSION" Version of the MUD client. - /// "PLUGIN_ID" Unique ID of the MSDP plugin/script. + /// What lists we can report on. /// - public class MSDPServerModel - { - /// - /// What lists we can report on. - /// - public Dictionary>> Lists { get; private init; } + public Dictionary>> Lists { get; private init; } - public Func> Commands { get; set; } = () => []; + public Func> Commands { get; set; } = () => []; - public Func> Configurable_Variables { get; set; } = () => []; + public Func> Configurable_Variables { get; set; } = () => []; - public Func> Reportable_Variables { get; set; } = () => []; + public Func> Reportable_Variables { get; set; } = () => []; - public Dictionary> Reported_Variables => []; + public Dictionary> Reported_Variables => []; - public Func> Sendable_Variables { get; set; } = () => []; + public Func> Sendable_Variables { get; set; } = () => []; - public Func ResetCallbackAsync { get; } + public Func ResetCallbackAsync { get; } - /// - /// Creates the MSDP Server Model. - /// Define each public variable to implement MSDP. - /// - /// Function to call when a client wishes to set a server variable. - public MSDPServerModel(Func resetCallback) + /// + /// Creates the MSDP Server Model. + /// Define each public variable to implement MSDP. + /// + /// Function to call when a client wishes to set a server variable. + public MSDPServerModel(Func resetCallback) + { + Lists = new() { - Lists = new() - { - { "COMMANDS", Commands}, - { "CONFIGURABLE_VARIABLES", Configurable_Variables}, - { "REPORTABLE_VARIABLES", Reportable_Variables}, - { "REPORTED_VARIABLES", () => Reported_Variables.Select( x=> x.Key).ToHashSet() }, - { "SENDABLE_VARIABLES", Sendable_Variables} - }; - - ResetCallbackAsync = resetCallback; - } + { "COMMANDS", Commands}, + { "CONFIGURABLE_VARIABLES", Configurable_Variables}, + { "REPORTABLE_VARIABLES", Reportable_Variables}, + { "REPORTED_VARIABLES", () => Reported_Variables.Select( x=> x.Key).ToHashSet() }, + { "SENDABLE_VARIABLES", Sendable_Variables} + }; + + ResetCallbackAsync = resetCallback; + } - public async Task ResetAsync(string configurableVariable) => - await ResetCallbackAsync(configurableVariable); + public async Task ResetAsync(string configurableVariable) => + await ResetCallbackAsync(configurableVariable); - public void Report(string reportableVariable, Func function) => - Reported_Variables.Add(reportableVariable, function); + public void Report(string reportableVariable, Func function) => + Reported_Variables.Add(reportableVariable, function); - public void UnReport(string reportableVariable) => - Reported_Variables.Remove(reportableVariable); + public void UnReport(string reportableVariable) => + Reported_Variables.Remove(reportableVariable); - public async Task NotifyChangeAsync(string reportableVariable, string newValue) => - await (Reported_Variables.TryGetValue(reportableVariable, out var function) - ? function(newValue) - : Task.CompletedTask); - } + public async Task NotifyChangeAsync(string reportableVariable, string newValue) => + await (Reported_Variables.TryGetValue(reportableVariable, out var function) + ? function(newValue) + : Task.CompletedTask); } diff --git a/TelnetNegotiationCore/Interpreters/TelnetCharsetInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetCharsetInterpreter.cs index 0adffbb..790ff41 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetCharsetInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetCharsetInterpreter.cs @@ -8,291 +8,290 @@ using Stateless; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +/// +/// Implements RFC 2066: +/// +/// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html +/// +public partial class TelnetInterpreter { /// - /// Implements RFC 2066: - /// - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// Internal Charset Byte State /// - public partial class TelnetInterpreter - { - /// - /// Internal Charset Byte State - /// - private byte[] _charsetByteState; + private byte[] _charsetByteState; - /// - /// Internal Charset Byte Index Value - /// - private int _charsetByteIndex = 0; + /// + /// Internal Charset Byte Index Value + /// + private int _charsetByteIndex = 0; - /// - /// Internal Accepted Charset Byte State - /// - private byte[] _acceptedCharsetByteState; + /// + /// Internal Accepted Charset Byte State + /// + private byte[] _acceptedCharsetByteState; - /// - /// Internal Accepted Charset Byte Index Value - /// - private int _acceptedCharsetByteIndex = 0; + /// + /// Internal Accepted Charset Byte Index Value + /// + private int _acceptedCharsetByteIndex = 0; - private bool charsetOffered = false; + private bool charsetOffered = false; - private Func> AllowedEncodings { get; set; } = Encoding.GetEncodings; + private Func> AllowedEncodings { get; set; } = Encoding.GetEncodings; - private Func, IOrderedEnumerable> _charsetOrder = (x) => x.Select(y => y.GetEncoding()).OrderBy(z => z.EncodingName); + private Func, IOrderedEnumerable> _charsetOrder = (x) => x.Select(y => y.GetEncoding()).OrderBy(z => z.EncodingName); - private Func SignalCharsetChangeAsync { get; set; } + private Func SignalCharsetChangeAsync { get; set; } - public Lazy SupportedCharacterSets { get; } + public Lazy SupportedCharacterSets { get; } - /// - /// Sets the CharacterSet Order - /// - /// codepage is less than zero or greater than 65535. - /// codepage is not supported by the underlying platform. - /// codepage is not supported by the underlying platform. - public IEnumerable CharsetOrder + /// + /// Sets the CharacterSet Order + /// + /// codepage is less than zero or greater than 65535. + /// codepage is not supported by the underlying platform. + /// codepage is not supported by the underlying platform. + public IEnumerable CharsetOrder + { + init { - init - { - var ordered = value.Reverse().ToList(); - _charsetOrder = (x) => x.Select(x => x.GetEncoding()).OrderByDescending(z => ordered.IndexOf(z)); - } + var ordered = value.Reverse().ToList(); + _charsetOrder = (x) => x.Select(x => x.GetEncoding()).OrderByDescending(z => ordered.IndexOf(z)); } + } - /// - /// Character set Negotiation will set the Character Set and Character Page Server & Client have agreed to. - /// - /// The state machine. - /// Itself - private StateMachine SetupCharsetNegotiation(StateMachine tsm) - { - tsm.Configure(State.Willing) - .Permit(Trigger.CHARSET, State.WillDoCharset); + /// + /// Character set Negotiation will set the Character Set and Character Page Server & Client have agreed to. + /// + /// The state machine. + /// Itself + private StateMachine SetupCharsetNegotiation(StateMachine tsm) + { + tsm.Configure(State.Willing) + .Permit(Trigger.CHARSET, State.WillDoCharset); - tsm.Configure(State.Refusing) - .Permit(Trigger.CHARSET, State.WontDoCharset); + tsm.Configure(State.Refusing) + .Permit(Trigger.CHARSET, State.WontDoCharset); - tsm.Configure(State.Do) - .Permit(Trigger.CHARSET, State.DoCharset); + tsm.Configure(State.Do) + .Permit(Trigger.CHARSET, State.DoCharset); - tsm.Configure(State.Dont) - .Permit(Trigger.CHARSET, State.DontCharset); + tsm.Configure(State.Dont) + .Permit(Trigger.CHARSET, State.DontCharset); - tsm.Configure(State.WillDoCharset) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnWillingCharsetAsync); + tsm.Configure(State.WillDoCharset) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnWillingCharsetAsync); - tsm.Configure(State.WontDoCharset) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Won't do Character Set - do nothing")); + tsm.Configure(State.WontDoCharset) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Won't do Character Set - do nothing")); - tsm.Configure(State.DoCharset) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnDoCharsetAsync); + tsm.Configure(State.DoCharset) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnDoCharsetAsync); - tsm.Configure(State.DontCharset) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do Character Set - do nothing")); + tsm.Configure(State.DontCharset) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do Character Set - do nothing")); - tsm.Configure(State.SubNegotiation) - .Permit(Trigger.CHARSET, State.AlmostNegotiatingCharset); + tsm.Configure(State.SubNegotiation) + .Permit(Trigger.CHARSET, State.AlmostNegotiatingCharset); - tsm.Configure(State.AlmostNegotiatingCharset) - .Permit(Trigger.REQUEST, State.NegotiatingCharset) - .Permit(Trigger.REJECTED, State.EndingCharsetSubnegotiation) - .Permit(Trigger.ACCEPTED, State.NegotiatingAcceptedCharset); + tsm.Configure(State.AlmostNegotiatingCharset) + .Permit(Trigger.REQUEST, State.NegotiatingCharset) + .Permit(Trigger.REJECTED, State.EndingCharsetSubnegotiation) + .Permit(Trigger.ACCEPTED, State.NegotiatingAcceptedCharset); - tsm.Configure(State.EndingCharsetSubnegotiation) - .Permit(Trigger.IAC, State.EndSubNegotiation); + tsm.Configure(State.EndingCharsetSubnegotiation) + .Permit(Trigger.IAC, State.EndSubNegotiation); - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.NegotiatingCharset).Permit(t, State.EvaluatingCharset)); - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.NegotiatingAcceptedCharset).Permit(t, State.EvaluatingAcceptedCharsetValue)); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.NegotiatingCharset).Permit(t, State.EvaluatingCharset)); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.NegotiatingAcceptedCharset).Permit(t, State.EvaluatingAcceptedCharsetValue)); - tsm.Configure(State.EscapingCharsetValue) - .Permit(Trigger.IAC, State.EvaluatingCharset) - .Permit(Trigger.SE, State.CompletingCharset); + tsm.Configure(State.EscapingCharsetValue) + .Permit(Trigger.IAC, State.EvaluatingCharset) + .Permit(Trigger.SE, State.CompletingCharset); - tsm.Configure(State.EscapingAcceptedCharsetValue) - .Permit(Trigger.IAC, State.EvaluatingAcceptedCharsetValue) - .Permit(Trigger.SE, State.CompletingAcceptedCharset); + tsm.Configure(State.EscapingAcceptedCharsetValue) + .Permit(Trigger.IAC, State.EvaluatingAcceptedCharsetValue) + .Permit(Trigger.SE, State.CompletingAcceptedCharset); - tsm.Configure(State.NegotiatingCharset) - .Permit(Trigger.IAC, State.EscapingCharsetValue) - .OnEntry(GetCharset); + tsm.Configure(State.NegotiatingCharset) + .Permit(Trigger.IAC, State.EscapingCharsetValue) + .OnEntry(GetCharset); - tsm.Configure(State.NegotiatingAcceptedCharset) - .Permit(Trigger.IAC, State.EscapingAcceptedCharsetValue) - .OnEntry(GetAcceptedCharset); + tsm.Configure(State.NegotiatingAcceptedCharset) + .Permit(Trigger.IAC, State.EscapingAcceptedCharsetValue) + .OnEntry(GetAcceptedCharset); - tsm.Configure(State.EvaluatingCharset) - .Permit(Trigger.IAC, State.EscapingCharsetValue); + tsm.Configure(State.EvaluatingCharset) + .Permit(Trigger.IAC, State.EscapingCharsetValue); - tsm.Configure(State.EvaluatingAcceptedCharsetValue) - .Permit(Trigger.IAC, State.EscapingAcceptedCharsetValue); + tsm.Configure(State.EvaluatingAcceptedCharsetValue) + .Permit(Trigger.IAC, State.EscapingAcceptedCharsetValue); - TriggerHelper.ForAllTriggers(t => tsm.Configure(State.EvaluatingCharset).OnEntryFrom(ParameterizedTrigger(t), CaptureCharset)); - TriggerHelper.ForAllTriggers(t => tsm.Configure(State.EvaluatingAcceptedCharsetValue).OnEntryFrom(ParameterizedTrigger(t), CaptureAcceptedCharset)); + TriggerHelper.ForAllTriggers(t => tsm.Configure(State.EvaluatingCharset).OnEntryFrom(ParameterizedTrigger(t), CaptureCharset)); + TriggerHelper.ForAllTriggers(t => tsm.Configure(State.EvaluatingAcceptedCharsetValue).OnEntryFrom(ParameterizedTrigger(t), CaptureAcceptedCharset)); - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.EvaluatingCharset).PermitReentry(t)); - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.EvaluatingAcceptedCharsetValue).PermitReentry(t)); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.EvaluatingCharset).PermitReentry(t)); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.EvaluatingAcceptedCharsetValue).PermitReentry(t)); - tsm.Configure(State.CompletingAcceptedCharset) - .OnEntryAsync(CompleteAcceptedCharsetAsync) - .SubstateOf(State.Accepting); + tsm.Configure(State.CompletingAcceptedCharset) + .OnEntryAsync(CompleteAcceptedCharsetAsync) + .SubstateOf(State.Accepting); - tsm.Configure(State.CompletingCharset) - .OnEntryAsync(CompleteCharsetAsync) - .SubstateOf(State.Accepting); + tsm.Configure(State.CompletingCharset) + .OnEntryAsync(CompleteCharsetAsync) + .SubstateOf(State.Accepting); - RegisterInitialWilling(WillingCharsetAsync); + RegisterInitialWilling(WillingCharsetAsync); - return tsm; - } + return tsm; + } - /// - /// Initialize internal state values for Charset. - /// - /// Ignored - private void GetCharset(StateMachine.Transition _) - { - _charsetByteState = new byte[_buffer.Length]; - _charsetByteIndex = 0; - } + /// + /// Initialize internal state values for Charset. + /// + /// Ignored + private void GetCharset(StateMachine.Transition _) + { + _charsetByteState = new byte[_buffer.Length]; + _charsetByteIndex = 0; + } - /// - /// Initialize internal state values for Charset. - /// - /// Ignored - private void GetAcceptedCharset(StateMachine.Transition _) - { - _acceptedCharsetByteState = new byte[42]; - _acceptedCharsetByteIndex = 0; - } + /// + /// Initialize internal state values for Charset. + /// + /// Ignored + private void GetAcceptedCharset(StateMachine.Transition _) + { + _acceptedCharsetByteState = new byte[42]; + _acceptedCharsetByteIndex = 0; + } - /// - /// Read the Charset state values and finalize it and prepare to respond. - /// - /// Ignored - private void CaptureCharset(OneOf b) - { - if (_charsetByteIndex > _charsetByteState.Length) return; - _charsetByteState[_charsetByteIndex] = b.AsT0; - _charsetByteIndex++; - } + /// + /// Read the Charset state values and finalize it and prepare to respond. + /// + /// Ignored + private void CaptureCharset(OneOf b) + { + if (_charsetByteIndex > _charsetByteState.Length) return; + _charsetByteState[_charsetByteIndex] = b.AsT0; + _charsetByteIndex++; + } - /// - /// Read the Charset state values and finalize it and prepare to respond. - /// - /// Ignored - private void CaptureAcceptedCharset(OneOf b) - { - _acceptedCharsetByteState[_acceptedCharsetByteIndex] = b.AsT0; - _acceptedCharsetByteIndex++; - } + /// + /// Read the Charset state values and finalize it and prepare to respond. + /// + /// Ignored + private void CaptureAcceptedCharset(OneOf b) + { + _acceptedCharsetByteState[_acceptedCharsetByteIndex] = b.AsT0; + _acceptedCharsetByteIndex++; + } - /// - /// Finalize internal state values for Charset. - /// - /// Ignored - private async Task CompleteCharsetAsync(StateMachine.Transition _) + /// + /// Finalize internal state values for Charset. + /// + /// Ignored + private async Task CompleteCharsetAsync(StateMachine.Transition _) + { + if (charsetOffered && Mode == TelnetMode.Server) { - if (charsetOffered && Mode == TelnetMode.Server) - { - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.REJECTED, (byte)Trigger.IAC, (byte)Trigger.SE]); - return; - } + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.REJECTED, (byte)Trigger.IAC, (byte)Trigger.SE]); + return; + } - char? sep = ascii.GetString(_charsetByteState, 0, 1)?[0]; - string[] charsetsOffered = ascii.GetString(_charsetByteState, 1, _charsetByteIndex - 1).Split(sep ?? ' '); + char? sep = ascii.GetString(_charsetByteState, 0, 1)?[0]; + string[] charsetsOffered = ascii.GetString(_charsetByteState, 1, _charsetByteIndex - 1).Split(sep ?? ' '); - _Logger.LogDebug("Charsets offered to us: {@charsetResultDebug}", charsetsOffered); + _Logger.LogDebug("Charsets offered to us: {@charsetResultDebug}", charsetsOffered); - var encodingDict = AllowedEncodings().ToDictionary(x => x.GetEncoding().WebName); - var offeredEncodingInfo = charsetsOffered.Select(x => { try { return encodingDict[Encoding.GetEncoding(x).WebName]; } catch { return null; } }).Where(x => x != null); - var preferredEncoding = _charsetOrder(offeredEncodingInfo); - var chosenEncoding = preferredEncoding.FirstOrDefault(); + var encodingDict = AllowedEncodings().ToDictionary(x => x.GetEncoding().WebName); + var offeredEncodingInfo = charsetsOffered.Select(x => { try { return encodingDict[Encoding.GetEncoding(x).WebName]; } catch { return null; } }).Where(x => x != null); + var preferredEncoding = _charsetOrder(offeredEncodingInfo); + var chosenEncoding = preferredEncoding.FirstOrDefault(); - if (chosenEncoding == null) - { - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.REJECTED, (byte)Trigger.IAC, (byte)Trigger.SE]); - return; - } + if (chosenEncoding == null) + { + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.REJECTED, (byte)Trigger.IAC, (byte)Trigger.SE]); + return; + } - _Logger.LogDebug("Charsets chosen by us: {@charsetWebName} (CP: {@cp})", chosenEncoding.WebName, chosenEncoding.CodePage); + _Logger.LogDebug("Charsets chosen by us: {@charsetWebName} (CP: {@cp})", chosenEncoding.WebName, chosenEncoding.CodePage); - byte[] preamble = [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.ACCEPTED]; - byte[] charsetAscii = ascii.GetBytes(chosenEncoding.WebName); - byte[] postAmble = [ (byte)Trigger.IAC, (byte)Trigger.SE ]; + byte[] preamble = [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.ACCEPTED]; + byte[] charsetAscii = ascii.GetBytes(chosenEncoding.WebName); + byte[] postAmble = [ (byte)Trigger.IAC, (byte)Trigger.SE ]; - CurrentEncoding = chosenEncoding; - await (SignalCharsetChangeAsync?.Invoke(CurrentEncoding) ?? Task.CompletedTask); + CurrentEncoding = chosenEncoding; + await (SignalCharsetChangeAsync?.Invoke(CurrentEncoding) ?? Task.CompletedTask); - // TODO: The implementing Server or Client needs to be warned when CurrentEncoding is set! - // This would allow, for instance, the Console to ensure it displays Unicode correctly. + // TODO: The implementing Server or Client needs to be warned when CurrentEncoding is set! + // This would allow, for instance, the Console to ensure it displays Unicode correctly. - await CallbackNegotiationAsync([.. preamble, .. charsetAscii, .. postAmble]); - } + await CallbackNegotiationAsync([.. preamble, .. charsetAscii, .. postAmble]); + } - /// - /// Finalize internal state values for Accepted Charset. - /// - /// Ignored - private async Task CompleteAcceptedCharsetAsync(StateMachine.Transition _) + /// + /// Finalize internal state values for Accepted Charset. + /// + /// Ignored + private async Task CompleteAcceptedCharsetAsync(StateMachine.Transition _) + { + try { - try - { - CurrentEncoding = Encoding.GetEncoding(ascii.GetString(_acceptedCharsetByteState, 0, _acceptedCharsetByteIndex).Trim()); - } - catch (Exception ex) - { - _Logger.LogError(ex, "Unexpected error during Accepting Charset Negotiation. Could not find charset: {charset}", ascii.GetString(_acceptedCharsetByteState, 0, _acceptedCharsetByteIndex)); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.REJECTED, (byte)Trigger.IAC, (byte)Trigger.SE]); - } - _Logger.LogInformation("Connection: Accepted Charset Negotiation for: {charset}", CurrentEncoding.WebName); - charsetOffered = false; + CurrentEncoding = Encoding.GetEncoding(ascii.GetString(_acceptedCharsetByteState, 0, _acceptedCharsetByteIndex).Trim()); } - - /// - /// Announce we do charset negotiation to the client after getting a Willing. - /// - private async Task OnWillingCharsetAsync(StateMachine.Transition _) + catch (Exception ex) { - _Logger.LogDebug("Connection: {ConnectionState}", "Request charset negotiation from Client"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.CHARSET]); - charsetOffered = false; + _Logger.LogError(ex, "Unexpected error during Accepting Charset Negotiation. Could not find charset: {charset}", ascii.GetString(_acceptedCharsetByteState, 0, _acceptedCharsetByteIndex)); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.REJECTED, (byte)Trigger.IAC, (byte)Trigger.SE]); } + _Logger.LogInformation("Connection: Accepted Charset Negotiation for: {charset}", CurrentEncoding.WebName); + charsetOffered = false; + } - /// - /// Announce we do charset negotiation to the client. - /// - private async Task WillingCharsetAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to Charset!"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.CHARSET]); - } + /// + /// Announce we do charset negotiation to the client after getting a Willing. + /// + private async Task OnWillingCharsetAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Connection: {ConnectionState}", "Request charset negotiation from Client"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.CHARSET]); + charsetOffered = false; + } - /// - /// Announce the charsets we support to the client after getting a Do. - /// - private async Task OnDoCharsetAsync(StateMachine.Transition _) - { - _Logger.LogDebug("Charsets String: {CharsetList}", ";" + string.Join(";", _charsetOrder(AllowedEncodings()).Select(x => x.WebName))); - await CallbackNegotiationAsync(SupportedCharacterSets.Value); - charsetOffered = true; - } + /// + /// Announce we do charset negotiation to the client. + /// + private async Task WillingCharsetAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to Charset!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.CHARSET]); + } - /// - /// Form the Character Set output, based on the system Encodings at the time of connection startup. - /// - /// A byte array representing the charset offering. - private byte[] CharacterSets() - { - return [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.REQUEST, - .. ascii.GetBytes($";{string.Join(";", _charsetOrder(AllowedEncodings()).Select(x => x.WebName))}"), - (byte)Trigger.IAC, (byte)Trigger.SE]; - } + /// + /// Announce the charsets we support to the client after getting a Do. + /// + private async Task OnDoCharsetAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Charsets String: {CharsetList}", ";" + string.Join(";", _charsetOrder(AllowedEncodings()).Select(x => x.WebName))); + await CallbackNegotiationAsync(SupportedCharacterSets.Value); + charsetOffered = true; + } + + /// + /// Form the Character Set output, based on the system Encodings at the time of connection startup. + /// + /// A byte array representing the charset offering. + private byte[] CharacterSets() + { + return [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.CHARSET, (byte)Trigger.REQUEST, + .. ascii.GetBytes($";{string.Join(";", _charsetOrder(AllowedEncodings()).Select(x => x.WebName))}"), + (byte)Trigger.IAC, (byte)Trigger.SE]; } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs index 5203d73..0c39377 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetEORInterpreter.cs @@ -1,150 +1,148 @@ using System; -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Stateless; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +public partial class TelnetInterpreter { - public partial class TelnetInterpreter - { - private bool? _doEOR = null; + private bool? _doEOR = null; - public Func SignalOnPromptingAsync { get; init; } + public Func SignalOnPromptingAsync { get; init; } - /// - /// Character set Negotiation will set the Character Set and Character Page Server & Client have agreed to. - /// - /// The state machine. - /// Itself - private StateMachine SetupEORNegotiation(StateMachine tsm) + /// + /// Character set Negotiation will set the Character Set and Character Page Server & Client have agreed to. + /// + /// The state machine. + /// Itself + private StateMachine SetupEORNegotiation(StateMachine tsm) + { + if (Mode == TelnetMode.Server) { - if (Mode == TelnetMode.Server) - { - tsm.Configure(State.Do) - .Permit(Trigger.TELOPT_EOR, State.DoEOR); + tsm.Configure(State.Do) + .Permit(Trigger.TELOPT_EOR, State.DoEOR); - tsm.Configure(State.Dont) - .Permit(Trigger.TELOPT_EOR, State.DontEOR); + tsm.Configure(State.Dont) + .Permit(Trigger.TELOPT_EOR, State.DontEOR); - tsm.Configure(State.DoEOR) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnDoEORAsync); + tsm.Configure(State.DoEOR) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnDoEORAsync); - tsm.Configure(State.DontEOR) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnDontEORAsync); + tsm.Configure(State.DontEOR) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnDontEORAsync); - RegisterInitialWilling(WillingEORAsync); - } - else - { - tsm.Configure(State.Willing) - .Permit(Trigger.TELOPT_EOR, State.WillEOR); + RegisterInitialWilling(WillingEORAsync); + } + else + { + tsm.Configure(State.Willing) + .Permit(Trigger.TELOPT_EOR, State.WillEOR); - tsm.Configure(State.Refusing) - .Permit(Trigger.TELOPT_EOR, State.WontEOR); + tsm.Configure(State.Refusing) + .Permit(Trigger.TELOPT_EOR, State.WontEOR); - tsm.Configure(State.WontEOR) - .SubstateOf(State.Accepting) - .OnEntryAsync(WontEORAsync); + tsm.Configure(State.WontEOR) + .SubstateOf(State.Accepting) + .OnEntryAsync(WontEORAsync); - tsm.Configure(State.WillEOR) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnWillEORAsync); - } + tsm.Configure(State.WillEOR) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnWillEORAsync); + } - tsm.Configure(State.StartNegotiation) - .Permit(Trigger.EOR, State.Prompting); + tsm.Configure(State.StartNegotiation) + .Permit(Trigger.EOR, State.Prompting); - tsm.Configure(State.Prompting) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnEORPrompt); + tsm.Configure(State.Prompting) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnEORPrompt); - return tsm; - } + return tsm; + } - private async Task OnEORPrompt() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Server is prompting EOR"); - await (SignalOnPromptingAsync?.Invoke() ?? Task.CompletedTask); - } + private async Task OnEORPrompt() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Server is prompting EOR"); + await (SignalOnPromptingAsync?.Invoke() ?? Task.CompletedTask); + } - private async Task OnDontEORAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do EOR - do nothing"); - _doEOR = false; - await Task.CompletedTask; - } + private async Task OnDontEORAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do EOR - do nothing"); + _doEOR = false; + await Task.CompletedTask; + } - private async Task WontEORAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do EOR - do nothing"); - _doEOR = false; - await Task.CompletedTask; - } + private async Task WontEORAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do EOR - do nothing"); + _doEOR = false; + await Task.CompletedTask; + } - /// - /// Announce we do EOR negotiation to the client. - /// - private async Task WillingEORAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to EOR!"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TELOPT_EOR]); - } + /// + /// Announce we do EOR negotiation to the client. + /// + private async Task WillingEORAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to EOR!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TELOPT_EOR]); + } - /// - /// Store that we are now in EOR mode. - /// - private Task OnDoEORAsync(StateMachine.Transition _) - { - _Logger.LogDebug("Connection: {ConnectionState}", "Client supports End of Record."); - _doEOR = true; - return Task.CompletedTask; - } + /// + /// Store that we are now in EOR mode. + /// + private Task OnDoEORAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Connection: {ConnectionState}", "Client supports End of Record."); + _doEOR = true; + return Task.CompletedTask; + } - /// - /// Store that we are now in EOR mode. - /// - private async Task OnWillEORAsync(StateMachine.Transition _) + /// + /// Store that we are now in EOR mode. + /// + private async Task OnWillEORAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Connection: {ConnectionState}", "Server supports End of Record."); + _doEOR = true; + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.TELOPT_EOR]); + } + + /// + /// Sends a byte message as a Prompt, if supported, by not sending an EOR at the end. + /// + /// Byte array + /// A completed Task + public async Task SendPromptAsync(byte[] send) + { + await CallbackNegotiationAsync(send); + if (_doEOR is null or false) { - _Logger.LogDebug("Connection: {ConnectionState}", "Server supports End of Record."); - _doEOR = true; - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.TELOPT_EOR]); + await CallbackNegotiationAsync(CurrentEncoding.GetBytes(Environment.NewLine)); } - - /// - /// Sends a byte message as a Prompt, if supported, by not sending an EOR at the end. - /// - /// Byte array - /// A completed Task - public async Task SendPromptAsync(byte[] send) + else if(_doEOR is true) { - await CallbackNegotiationAsync(send); - if (_doEOR is null or false) - { - await CallbackNegotiationAsync(CurrentEncoding.GetBytes(Environment.NewLine)); - } - else if(_doEOR is true) - { - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.EOR]); - } - else if (_doGA is not null or false) - { - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.GA]); - } + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.EOR]); } - - /// - /// Sends a byte message, adding an EOR at the end if needed. - /// - /// Byte array - /// A completed Task - public async Task SendAsync(byte[] send) + else if (_doGA is not null or false) { - await CallbackNegotiationAsync(send); - await CallbackNegotiationAsync(CurrentEncoding.GetBytes(Environment.NewLine)); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.GA]); } } + + /// + /// Sends a byte message, adding an EOR at the end if needed. + /// + /// Byte array + /// A completed Task + public async Task SendAsync(byte[] send) + { + await CallbackNegotiationAsync(send); + await CallbackNegotiationAsync(CurrentEncoding.GetBytes(Environment.NewLine)); + } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs index abf30b9..b8b0960 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetGMCPInterpreter.cs @@ -8,150 +8,149 @@ using System.Text.Json; using Microsoft.Extensions.Logging; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +public partial class TelnetInterpreter { - public partial class TelnetInterpreter - { - private List _GMCPBytes = []; + private List _GMCPBytes = []; - public Func<(string Package, string Info), Task> SignalOnGMCPAsync { get; init; } + public Func<(string Package, string Info), Task> SignalOnGMCPAsync { get; init; } - private StateMachine SetupGMCPNegotiation(StateMachine tsm) + private StateMachine SetupGMCPNegotiation(StateMachine tsm) + { + if (Mode == TelnetMode.Server) { - if (Mode == TelnetMode.Server) - { - tsm.Configure(State.Do) - .Permit(Trigger.GMCP, State.DoGMCP); + tsm.Configure(State.Do) + .Permit(Trigger.GMCP, State.DoGMCP); - tsm.Configure(State.Dont) - .Permit(Trigger.GMCP, State.DontGMCP); + tsm.Configure(State.Dont) + .Permit(Trigger.GMCP, State.DontGMCP); - tsm.Configure(State.DoGMCP) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client will do GMCP")); + tsm.Configure(State.DoGMCP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client will do GMCP")); - tsm.Configure(State.DontGMCP) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client will not GMCP")); + tsm.Configure(State.DontGMCP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client will not GMCP")); - RegisterInitialWilling(async () => await WillGMCPAsync(null)); - } - else if (Mode == TelnetMode.Client) - { - tsm.Configure(State.Willing) - .Permit(Trigger.GMCP, State.WillGMCP); + RegisterInitialWilling(async () => await WillGMCPAsync(null)); + } + 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.Refusing) + .Permit(Trigger.GMCP, State.WontGMCP); - tsm.Configure(State.WillGMCP) - .SubstateOf(State.Accepting) - .OnEntryAsync(DoGMCPAsync); + tsm.Configure(State.WillGMCP) + .SubstateOf(State.Accepting) + .OnEntryAsync(DoGMCPAsync); - tsm.Configure(State.WontGMCP) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client will GMCP")); - } + tsm.Configure(State.WontGMCP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client will GMCP")); + } - tsm.Configure(State.SubNegotiation) - .Permit(Trigger.GMCP, State.AlmostNegotiatingGMCP) - .OnEntry(() => _GMCPBytes.Clear()); + tsm.Configure(State.SubNegotiation) + .Permit(Trigger.GMCP, State.AlmostNegotiatingGMCP) + .OnEntry(() => _GMCPBytes.Clear()); - TriggerHelper.ForAllTriggersButIAC(t => tsm - .Configure(State.EvaluatingGMCPValue) - .PermitReentry(t) - .OnEntryFrom(ParameterizedTrigger(t), RegisterGMCPValue)); + TriggerHelper.ForAllTriggersButIAC(t => tsm + .Configure(State.EvaluatingGMCPValue) + .PermitReentry(t) + .OnEntryFrom(ParameterizedTrigger(t), RegisterGMCPValue)); - TriggerHelper.ForAllTriggersButIAC(t => tsm - .Configure(State.AlmostNegotiatingGMCP) - .Permit(t, State.EvaluatingGMCPValue)); + TriggerHelper.ForAllTriggersButIAC(t => tsm + .Configure(State.AlmostNegotiatingGMCP) + .Permit(t, State.EvaluatingGMCPValue)); - tsm.Configure(State.EvaluatingGMCPValue) - .Permit(Trigger.IAC, State.EscapingGMCPValue); + tsm.Configure(State.EvaluatingGMCPValue) + .Permit(Trigger.IAC, State.EscapingGMCPValue); - tsm.Configure(State.AlmostNegotiatingGMCP) - .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.EscapingGMCPValue) + .Permit(Trigger.IAC, State.EvaluatingGMCPValue) + .Permit(Trigger.SE, State.CompletingGMCPValue); - tsm.Configure(State.CompletingGMCPValue) - .SubstateOf(State.Accepting) - .OnEntryAsync(CompleteGMCPNegotiation); + tsm.Configure(State.CompletingGMCPValue) + .SubstateOf(State.Accepting) + .OnEntryAsync(CompleteGMCPNegotiation); - return tsm; - } + return tsm; + } - /// - /// Adds a byte to the register. - /// - /// Byte. - private void RegisterGMCPValue(OneOf b) - { - _GMCPBytes.Add(b.AsT0); - } + /// + /// Adds a byte to the register. + /// + /// Byte. + private void RegisterGMCPValue(OneOf b) + { + _GMCPBytes.Add(b.AsT0); + } - public Task SendGMCPCommand(string package, string command) => - SendGMCPCommand(CurrentEncoding.GetBytes(package), CurrentEncoding.GetBytes(command)); + public Task SendGMCPCommand(string package, string command) => + SendGMCPCommand(CurrentEncoding.GetBytes(package), CurrentEncoding.GetBytes(command)); - public Task SendGMCPCommand(string package, byte[] command) => - SendGMCPCommand(CurrentEncoding.GetBytes(package), command); + public Task SendGMCPCommand(string package, byte[] command) => + SendGMCPCommand(CurrentEncoding.GetBytes(package), command); + + public async Task SendGMCPCommand(byte[] package, byte[] command) + { + await CallbackNegotiationAsync( + [ + (byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.GMCP, + .. package, + .. CurrentEncoding.GetBytes(" "), + .. command, + .. new byte[] { (byte)Trigger.IAC, (byte)Trigger.SE }, + ]); + } - public async Task SendGMCPCommand(byte[] package, byte[] command) + /// + /// 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.Take(firstSpace).ToArray(); + var rest = _GMCPBytes.Skip(firstSpace + 1).ToArray(); + + // TODO: Consideration: a version of this that sends back a Dynamic or other similar object. + var package = CurrentEncoding.GetString(packageBytes); + + if(package == "MSDP") { - await CallbackNegotiationAsync( - [ - (byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.GMCP, - .. package, - .. CurrentEncoding.GetBytes(" "), - .. command, - .. new byte[] { (byte)Trigger.IAC, (byte)Trigger.SE }, - ]); + await (SignalOnMSDPAsync?.Invoke(this, JsonSerializer.Serialize(Functional.MSDPLibrary.MSDPScan(packageBytes, CurrentEncoding))) ?? Task.CompletedTask); } - - /// - /// Completes the GMCP Negotiation. This is currently assuming a golden path. - /// - /// Transition, ignored. - /// Task - private async Task CompleteGMCPNegotiation(StateMachine.Transition _) + else { - var space = CurrentEncoding.GetBytes(" ").First(); - var firstSpace = _GMCPBytes.FindIndex(x => x == space); - var packageBytes = _GMCPBytes.Take(firstSpace).ToArray(); - var rest = _GMCPBytes.Skip(firstSpace + 1).ToArray(); - - // TODO: Consideration: a version of this that sends back a Dynamic or other similar object. - var package = CurrentEncoding.GetString(packageBytes); - - if(package == "MSDP") - { - await (SignalOnMSDPAsync?.Invoke(this, JsonSerializer.Serialize(Functional.MSDPLibrary.MSDPScan(packageBytes, CurrentEncoding))) ?? Task.CompletedTask); - } - else - { - await (SignalOnGMCPAsync?.Invoke((Package: package, Info: CurrentEncoding.GetString(packageBytes))) ?? Task.CompletedTask); - } + await (SignalOnGMCPAsync?.Invoke((Package: package, Info: CurrentEncoding.GetString(packageBytes))) ?? Task.CompletedTask); } + } - /// - /// Announces the Server will GMCP. - /// - /// Transition, ignored. - /// Task - private async Task WillGMCPAsync(StateMachine.Transition _) - { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing the server will GMCP"); + /// + /// Announces the Server will GMCP. + /// + /// Transition, ignored. + /// Task + private async Task WillGMCPAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing the server will GMCP"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.GMCP]); - } + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.GMCP]); + } - private async Task DoGMCPAsync(StateMachine.Transition _) - { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing the client can do GMCP"); + private async Task DoGMCPAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing the client can do GMCP"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.GMCP]); - } + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.GMCP]); } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs index 25d82ef..bcd2a26 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetMSDPInterpreter.cs @@ -8,120 +8,119 @@ using Stateless; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +/// +/// Implements http://www.faqs.org/rfcs/rfc1073.html +/// +/// +/// TODO: Implement Client Side +/// +public partial class TelnetInterpreter { + private List _currentMSDPInfo; + + public Func SignalOnMSDPAsync { get; init; } + /// - /// Implements http://www.faqs.org/rfcs/rfc1073.html + /// Mud Server Status Protocol will provide information to the requestee about the server's contents. /// - /// - /// TODO: Implement Client Side - /// - public partial class TelnetInterpreter + /// The state machine. + /// Itself + private StateMachine SetupMSDPNegotiation(StateMachine tsm) { - private List _currentMSDPInfo; + if (Mode == TelnetMode.Server) + { + tsm.Configure(State.Do) + .Permit(Trigger.MSDP, State.DoMSDP); - public Func SignalOnMSDPAsync { get; init; } + tsm.Configure(State.Dont) + .Permit(Trigger.MSDP, State.DontMSDP); - /// - /// Mud Server Status Protocol will provide information to the requestee about the server's contents. - /// - /// The state machine. - /// Itself - private StateMachine SetupMSDPNegotiation(StateMachine tsm) - { - if (Mode == TelnetMode.Server) - { - tsm.Configure(State.Do) - .Permit(Trigger.MSDP, State.DoMSDP); - - tsm.Configure(State.Dont) - .Permit(Trigger.MSDP, State.DontMSDP); - - tsm.Configure(State.DoMSDP) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnDoMSDPAsync); - - tsm.Configure(State.DontMSDP) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do MSDP - do nothing")); - - RegisterInitialWilling(WillingMSDPAsync); - } - else - { - tsm.Configure(State.Willing) - .Permit(Trigger.MSDP, State.WillMSDP); - - tsm.Configure(State.Refusing) - .Permit(Trigger.MSDP, State.WontMSDP); - - tsm.Configure(State.WillMSDP) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnWillMSDPAsync); - - tsm.Configure(State.WontMSDP) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do MSDP - do nothing")); - - tsm.Configure(State.SubNegotiation) - .Permit(Trigger.MSDP, State.AlmostNegotiatingMSDP) - .OnEntry(() => _currentMSDPInfo = []); - - tsm.Configure(State.AlmostNegotiatingMSDP) - .Permit(Trigger.MSDP_VAR, State.EvaluatingMSDP) - .Permit(Trigger.MSDP_VAL, State.EvaluatingMSDP) - .Permit(Trigger.MSDP_ARRAY_OPEN, State.EvaluatingMSDP) - .Permit(Trigger.MSDP_ARRAY_CLOSE, State.EvaluatingMSDP) - .Permit(Trigger.MSDP_TABLE_OPEN, State.EvaluatingMSDP) - .Permit(Trigger.MSDP_TABLE_CLOSE, State.EvaluatingMSDP); - - tsm.Configure(State.EvaluatingMSDP) - .Permit(Trigger.IAC, State.EscapingMSDP); - - tsm.Configure(State.EscapingMSDP) - .Permit(Trigger.IAC, State.EvaluatingMSDP) - .Permit(Trigger.SE, State.CompletingMSDP); - - tsm.Configure(State.CompletingMSDP) - .SubstateOf(State.Accepting) - .OnEntry(ReadMSDPValues); - - TriggerHelper.ForAllTriggersButIAC(t => - tsm.Configure(State.EvaluatingMSDP).OnEntryFrom(ParameterizedTrigger(t), CaptureMSDPValue).PermitReentry(t)); - } - - return tsm; + tsm.Configure(State.DoMSDP) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnDoMSDPAsync); + + tsm.Configure(State.DontMSDP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do MSDP - do nothing")); + + RegisterInitialWilling(WillingMSDPAsync); } + else + { + tsm.Configure(State.Willing) + .Permit(Trigger.MSDP, State.WillMSDP); - private void CaptureMSDPValue(OneOf b) => _currentMSDPInfo.Add(b.AsT0); + tsm.Configure(State.Refusing) + .Permit(Trigger.MSDP, State.WontMSDP); - private void ReadMSDPValues() => SignalOnMSDPAsync?.Invoke(this, JsonSerializer.Serialize(Functional.MSDPLibrary.MSDPScan(_currentMSDPInfo.Skip(1), CurrentEncoding))); + tsm.Configure(State.WillMSDP) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnWillMSDPAsync); - /// - /// Announce we do MSDP negotiation to the client. - /// - private async Task WillingMSDPAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to MSDP!"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.MSDP]); - } + tsm.Configure(State.WontMSDP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do MSDP - do nothing")); - /// - /// Announce the MSDP we support to the client after getting a Do. - /// - private Task OnDoMSDPAsync(StateMachine.Transition _) - { - _Logger.LogDebug("Connection: {ConnectionState}", "Client will do MSDP output"); - return Task.CompletedTask; - } + tsm.Configure(State.SubNegotiation) + .Permit(Trigger.MSDP, State.AlmostNegotiatingMSDP) + .OnEntry(() => _currentMSDPInfo = []); - /// - /// Announce we do MSDP negotiation to the server. - /// - private async Task OnWillMSDPAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to MSDP!"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.MSDP]); + tsm.Configure(State.AlmostNegotiatingMSDP) + .Permit(Trigger.MSDP_VAR, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_VAL, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_ARRAY_OPEN, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_ARRAY_CLOSE, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_TABLE_OPEN, State.EvaluatingMSDP) + .Permit(Trigger.MSDP_TABLE_CLOSE, State.EvaluatingMSDP); + + tsm.Configure(State.EvaluatingMSDP) + .Permit(Trigger.IAC, State.EscapingMSDP); + + tsm.Configure(State.EscapingMSDP) + .Permit(Trigger.IAC, State.EvaluatingMSDP) + .Permit(Trigger.SE, State.CompletingMSDP); + + tsm.Configure(State.CompletingMSDP) + .SubstateOf(State.Accepting) + .OnEntry(ReadMSDPValues); + + TriggerHelper.ForAllTriggersButIAC(t => + tsm.Configure(State.EvaluatingMSDP).OnEntryFrom(ParameterizedTrigger(t), CaptureMSDPValue).PermitReentry(t)); } + + return tsm; + } + + private void CaptureMSDPValue(OneOf b) => _currentMSDPInfo.Add(b.AsT0); + + private void ReadMSDPValues() => SignalOnMSDPAsync?.Invoke(this, JsonSerializer.Serialize(Functional.MSDPLibrary.MSDPScan(_currentMSDPInfo.Skip(1), CurrentEncoding))); + + /// + /// Announce we do MSDP negotiation to the client. + /// + private async Task WillingMSDPAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to MSDP!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.MSDP]); + } + + /// + /// Announce the MSDP we support to the client after getting a Do. + /// + private Task OnDoMSDPAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Connection: {ConnectionState}", "Client will do MSDP output"); + return Task.CompletedTask; + } + + /// + /// Announce we do MSDP negotiation to the server. + /// + private async Task OnWillMSDPAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to MSDP!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.MSDP]); } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs index e1d7552..ebcc021 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetMSSPInterpreter.cs @@ -9,315 +9,314 @@ using Stateless; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +public partial class TelnetInterpreter { - public partial class TelnetInterpreter + private Func _msspConfig = () => new(); + + private List _currentMSSPVariable; + private List> _currentMSSPValueList; + private List _currentMSSPValue; + private List> _currentMSSPVariableList; + + public Func SignalOnMSSPAsync { get; init; } + + private IImmutableDictionary MSSPAttributeMembers = typeof(MSSPConfig) + .GetMembers() + .Select(x => (Member: x, Attribute: x.GetCustomAttribute())) + .Where(x => x.Attribute != null) + .ToImmutableDictionary(x => x.Attribute.Name.ToUpper()); + + /// + /// Mud Server Status Protocol will provide information to the requestee about the server's contents. + /// + /// The state machine. + /// Itself + private StateMachine SetupMSSPNegotiation(StateMachine tsm) { - private Func _msspConfig = () => new(); - - private List _currentMSSPVariable; - private List> _currentMSSPValueList; - private List _currentMSSPValue; - private List> _currentMSSPVariableList; - - public Func SignalOnMSSPAsync { get; init; } - - private IImmutableDictionary MSSPAttributeMembers = typeof(MSSPConfig) - .GetMembers() - .Select(x => (Member: x, Attribute: x.GetCustomAttribute())) - .Where(x => x.Attribute != null) - .ToImmutableDictionary(x => x.Attribute.Name.ToUpper()); - - /// - /// Mud Server Status Protocol will provide information to the requestee about the server's contents. - /// - /// The state machine. - /// Itself - private StateMachine SetupMSSPNegotiation(StateMachine tsm) + if (Mode == TelnetMode.Server) { - if (Mode == TelnetMode.Server) - { - tsm.Configure(State.Do) - .Permit(Trigger.MSSP, State.DoMSSP); + tsm.Configure(State.Do) + .Permit(Trigger.MSSP, State.DoMSSP); - tsm.Configure(State.Dont) - .Permit(Trigger.MSSP, State.DontMSSP); + tsm.Configure(State.Dont) + .Permit(Trigger.MSSP, State.DontMSSP); - tsm.Configure(State.DoMSSP) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnDoMSSPAsync); + tsm.Configure(State.DoMSSP) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnDoMSSPAsync); - tsm.Configure(State.DontMSSP) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do MSSP - do nothing")); + tsm.Configure(State.DontMSSP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do MSSP - do nothing")); - RegisterInitialWilling(WillingMSSPAsync); - } - else - { - tsm.Configure(State.Willing) - .Permit(Trigger.MSSP, State.WillMSSP); + RegisterInitialWilling(WillingMSSPAsync); + } + else + { + tsm.Configure(State.Willing) + .Permit(Trigger.MSSP, State.WillMSSP); - tsm.Configure(State.Refusing) - .Permit(Trigger.MSSP, State.WontMSSP); + tsm.Configure(State.Refusing) + .Permit(Trigger.MSSP, State.WontMSSP); - tsm.Configure(State.WillMSSP) - .SubstateOf(State.Accepting) - .OnEntryAsync(OnWillMSSPAsync); + tsm.Configure(State.WillMSSP) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnWillMSSPAsync); - tsm.Configure(State.WontMSSP) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do MSSP - do nothing")); + tsm.Configure(State.WontMSSP) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do MSSP - do nothing")); - tsm.Configure(State.SubNegotiation) - .Permit(Trigger.MSSP, State.AlmostNegotiatingMSSP) - .OnEntry(() => - { - _currentMSSPValue = []; - _currentMSSPVariable = []; - _currentMSSPValueList = []; - _currentMSSPVariableList = []; - }); - - tsm.Configure(State.AlmostNegotiatingMSSP) - .Permit(Trigger.MSSP_VAR, State.EvaluatingMSSPVar); - - tsm.Configure(State.EvaluatingMSSPVar) - .Permit(Trigger.MSSP_VAL, State.EvaluatingMSSPVal) - .Permit(Trigger.IAC, State.EscapingMSSPVar) - .OnEntryFrom(Trigger.MSSP_VAR, RegisterMSSPVal); - - tsm.Configure(State.EscapingMSSPVar) - .Permit(Trigger.IAC, State.EvaluatingMSSPVar); - - tsm.Configure(State.EvaluatingMSSPVal) - .Permit(Trigger.MSSP_VAR, State.EvaluatingMSSPVar) - .Permit(Trigger.IAC, State.EscapingMSSPVal) - .OnEntryFrom(Trigger.MSSP_VAL, RegisterMSSPVar); - - tsm.Configure(State.EscapingMSSPVal) - .Permit(Trigger.IAC, State.EvaluatingMSSPVal) - .Permit(Trigger.SE, State.CompletingMSSP); - - tsm.Configure(State.CompletingMSSP) - .SubstateOf(State.Accepting) - .OnEntryAsync(ReadMSSPValues); - - TriggerHelper.ForAllTriggersExcept([Trigger.MSSP_VAL, Trigger.MSSP_VAR, Trigger.IAC], t => tsm.Configure(State.EvaluatingMSSPVal).OnEntryFrom(ParameterizedTrigger(t), CaptureMSSPValue)); - TriggerHelper.ForAllTriggersExcept([Trigger.MSSP_VAL, Trigger.MSSP_VAR, Trigger.IAC], t => tsm.Configure(State.EvaluatingMSSPVar).OnEntryFrom(ParameterizedTrigger(t), CaptureMSSPVariable)); - - TriggerHelper.ForAllTriggersExcept([Trigger.IAC, Trigger.MSSP_VAR], - t => tsm.Configure(State.EvaluatingMSSPVal).PermitReentry(t)); - TriggerHelper.ForAllTriggersExcept([Trigger.IAC, Trigger.MSSP_VAL], - t => tsm.Configure(State.EvaluatingMSSPVar).PermitReentry(t)); - } - - return tsm; + tsm.Configure(State.SubNegotiation) + .Permit(Trigger.MSSP, State.AlmostNegotiatingMSSP) + .OnEntry(() => + { + _currentMSSPValue = []; + _currentMSSPVariable = []; + _currentMSSPValueList = []; + _currentMSSPVariableList = []; + }); + + tsm.Configure(State.AlmostNegotiatingMSSP) + .Permit(Trigger.MSSP_VAR, State.EvaluatingMSSPVar); + + tsm.Configure(State.EvaluatingMSSPVar) + .Permit(Trigger.MSSP_VAL, State.EvaluatingMSSPVal) + .Permit(Trigger.IAC, State.EscapingMSSPVar) + .OnEntryFrom(Trigger.MSSP_VAR, RegisterMSSPVal); + + tsm.Configure(State.EscapingMSSPVar) + .Permit(Trigger.IAC, State.EvaluatingMSSPVar); + + tsm.Configure(State.EvaluatingMSSPVal) + .Permit(Trigger.MSSP_VAR, State.EvaluatingMSSPVar) + .Permit(Trigger.IAC, State.EscapingMSSPVal) + .OnEntryFrom(Trigger.MSSP_VAL, RegisterMSSPVar); + + tsm.Configure(State.EscapingMSSPVal) + .Permit(Trigger.IAC, State.EvaluatingMSSPVal) + .Permit(Trigger.SE, State.CompletingMSSP); + + tsm.Configure(State.CompletingMSSP) + .SubstateOf(State.Accepting) + .OnEntryAsync(ReadMSSPValues); + + TriggerHelper.ForAllTriggersExcept([Trigger.MSSP_VAL, Trigger.MSSP_VAR, Trigger.IAC], t => tsm.Configure(State.EvaluatingMSSPVal).OnEntryFrom(ParameterizedTrigger(t), CaptureMSSPValue)); + TriggerHelper.ForAllTriggersExcept([Trigger.MSSP_VAL, Trigger.MSSP_VAR, Trigger.IAC], t => tsm.Configure(State.EvaluatingMSSPVar).OnEntryFrom(ParameterizedTrigger(t), CaptureMSSPVariable)); + + TriggerHelper.ForAllTriggersExcept([Trigger.IAC, Trigger.MSSP_VAR], + t => tsm.Configure(State.EvaluatingMSSPVal).PermitReentry(t)); + TriggerHelper.ForAllTriggersExcept([Trigger.IAC, Trigger.MSSP_VAL], + t => tsm.Configure(State.EvaluatingMSSPVar).PermitReentry(t)); } - private void RegisterMSSPVal() - { - if (_currentMSSPValue.Count == 0) return; + return tsm; + } - _currentMSSPValueList.Add(_currentMSSPValue); - _currentMSSPValue = []; - } + private void RegisterMSSPVal() + { + if (_currentMSSPValue.Count == 0) return; - private void RegisterMSSPVar() - { - if (_currentMSSPVariable.Count == 0) return; + _currentMSSPValueList.Add(_currentMSSPValue); + _currentMSSPValue = []; + } - _currentMSSPVariableList.Add(_currentMSSPVariable); - _currentMSSPVariable = []; - } + private void RegisterMSSPVar() + { + if (_currentMSSPVariable.Count == 0) return; - private async Task ReadMSSPValues() - { - RegisterMSSPVal(); - RegisterMSSPVar(); + _currentMSSPVariableList.Add(_currentMSSPVariable); + _currentMSSPVariable = []; + } - var grouping = _currentMSSPVariableList - .Zip(_currentMSSPValueList) - .GroupBy(x => CurrentEncoding.GetString(x.First.ToArray())); + private async Task ReadMSSPValues() + { + RegisterMSSPVal(); + RegisterMSSPVar(); - foreach (var group in grouping) - { - StoreClientMSSPDetails(group.Key, group.Select(x => CurrentEncoding.GetString(x.Second.ToArray()))); - } + var grouping = _currentMSSPVariableList + .Zip(_currentMSSPValueList) + .GroupBy(x => CurrentEncoding.GetString([.. x.First])); - await (SignalOnMSSPAsync?.Invoke(_msspConfig()) ?? Task.CompletedTask); + foreach (var group in grouping) + { + StoreClientMSSPDetails(group.Key, group.Select(x => CurrentEncoding.GetString([.. x.Second]))); } - /// - /// - /// - /// - /// - private void StoreClientMSSPDetails(string variable, IEnumerable value) + await (SignalOnMSSPAsync?.Invoke(_msspConfig()) ?? Task.CompletedTask); + } + + /// + /// + /// + /// + /// + private void StoreClientMSSPDetails(string variable, IEnumerable value) + { + if (MSSPAttributeMembers.ContainsKey(variable.ToUpper())) { - if (MSSPAttributeMembers.ContainsKey(variable.ToUpper())) - { - var foundAttribute = MSSPAttributeMembers[variable.ToUpper()]; - var fieldInfo = (PropertyInfo)foundAttribute.Member; + var foundAttribute = MSSPAttributeMembers[variable.ToUpper()]; + var fieldInfo = (PropertyInfo)foundAttribute.Member; - var msspConfig = _msspConfig(); + var msspConfig = _msspConfig(); - if (fieldInfo.PropertyType == typeof(string)) - { - fieldInfo.SetValue(msspConfig, value.First()); - } - else if (fieldInfo.PropertyType == typeof(int)) - { - var val = int.Parse(value.First()); - fieldInfo.SetValue(_msspConfig(), val); - } - else if (fieldInfo.PropertyType == typeof(bool)) - { - var val = value.First() == "1"; - fieldInfo.SetValue(_msspConfig(), val); - } - else if (fieldInfo.PropertyType == typeof(IEnumerable)) - { - fieldInfo.SetValue(_msspConfig(), value); - } - _msspConfig = () => msspConfig; + if (fieldInfo.PropertyType == typeof(string)) + { + fieldInfo.SetValue(msspConfig, value.First()); } - else + else if (fieldInfo.PropertyType == typeof(int)) { - dynamic valueToSet = value.Count() > 1 ? value : value.First(); - - if (!_msspConfig().Extended.TryAdd(variable, valueToSet)) - { - _msspConfig().Extended[variable] = valueToSet; - } + var val = int.Parse(value.First()); + fieldInfo.SetValue(_msspConfig(), val); + } + else if (fieldInfo.PropertyType == typeof(bool)) + { + var val = value.First() == "1"; + fieldInfo.SetValue(_msspConfig(), val); } + else if (fieldInfo.PropertyType == typeof(IEnumerable)) + { + fieldInfo.SetValue(_msspConfig(), value); + } + _msspConfig = () => msspConfig; } - - /* - * For ease of parsing, variables and values cannot contain the MSSP_VAL, MSSP_VAR, IAC, or NUL byte. - * The value can be an empty string unless a numeric value is expected in which case the default value should be 0. - * If your Mud can't calculate one of the numeric values for the World variables you can use "-1" to indicate that the data is not available. - * If a list of responses is provided try to pick from the list, unless "Etc" is specified, which means it's open ended. - * - * TODO: Support -1 on reporting. - */ - private void CaptureMSSPVariable(OneOf b) + else { - // We could increment here based on having switched... Somehow? - // We need a better state tracking for this, to indicate the transition. - _currentMSSPVariable.Add(b.AsT0); - } + dynamic valueToSet = value.Count() > 1 ? value : value.First(); - private void CaptureMSSPValue(OneOf b) - { - // We could increment here based on having switched... Somehow? - // We need a better state tracking for this, to indicate the transition. - _currentMSSPValue.Add(b.AsT0); + if (!_msspConfig().Extended.TryAdd(variable, valueToSet)) + { + _msspConfig().Extended[variable] = valueToSet; + } } + } - /// - /// Announce we do MSSP negotiation to the client. - /// - private async Task WillingMSSPAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to MSSP!"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.MSSP]); - } + /* + * For ease of parsing, variables and values cannot contain the MSSP_VAL, MSSP_VAR, IAC, or NUL byte. + * The value can be an empty string unless a numeric value is expected in which case the default value should be 0. + * If your Mud can't calculate one of the numeric values for the World variables you can use "-1" to indicate that the data is not available. + * If a list of responses is provided try to pick from the list, unless "Etc" is specified, which means it's open ended. + * + * TODO: Support -1 on reporting. + */ + private void CaptureMSSPVariable(OneOf b) + { + // We could increment here based on having switched... Somehow? + // We need a better state tracking for this, to indicate the transition. + _currentMSSPVariable.Add(b.AsT0); + } - /// - /// Announce the MSSP we support to the client after getting a Do. - /// - private async Task OnDoMSSPAsync(StateMachine.Transition _) - { - _Logger.LogDebug("Connection: {ConnectionState}", "Writing MSSP output"); - await CallbackNegotiationAsync(ReportMSSP(_msspConfig())); - } + private void CaptureMSSPValue(OneOf b) + { + // We could increment here based on having switched... Somehow? + // We need a better state tracking for this, to indicate the transition. + _currentMSSPValue.Add(b.AsT0); + } - /// - /// Announce we do MSSP negotiation to the server. - /// - private async Task OnWillMSSPAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to MSSP!"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.MSSP]); - } + /// + /// Announce we do MSSP negotiation to the client. + /// + private async Task WillingMSSPAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to MSSP!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.MSSP]); + } - /// - /// Report the MSSP values to the client. - /// - /// MSSP Configuration. - /// The full byte array to send the client. - private byte[] ReportMSSP(MSSPConfig config) => - [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.MSSP, .. MSSPReadConfig(config), (byte)Trigger.IAC, (byte)Trigger.SE]; + /// + /// Announce the MSSP we support to the client after getting a Do. + /// + private async Task OnDoMSSPAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Connection: {ConnectionState}", "Writing MSSP output"); + await CallbackNegotiationAsync(ReportMSSP(_msspConfig())); + } - public TelnetInterpreter RegisterMSSPConfig(Func config) - { - _msspConfig = config; - _Logger.LogDebug("Registering MSSP Config. Currently evaluates to: {@MSSPConfig}", config()); - return this; - } + /// + /// Announce we do MSSP negotiation to the server. + /// + private async Task OnWillMSSPAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to MSSP!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.MSSP]); + } - private byte[] MSSPReadConfig(MSSPConfig config) - { - byte[] msspBytes = []; + /// + /// Report the MSSP values to the client. + /// + /// MSSP Configuration. + /// The full byte array to send the client. + private byte[] ReportMSSP(MSSPConfig config) => + [(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.MSSP, .. MSSPReadConfig(config), (byte)Trigger.IAC, (byte)Trigger.SE]; - var fields = typeof(MSSPConfig).GetProperties(); - var knownFields = fields.Where(field => Attribute.IsDefined(field, typeof(NameAttribute))); + public TelnetInterpreter RegisterMSSPConfig(Func config) + { + _msspConfig = config; + _Logger.LogDebug("Registering MSSP Config. Currently evaluates to: {@MSSPConfig}", config()); + return this; + } - foreach (var field in knownFields) - { - var b = field.GetValue(config); - if (b == null) continue; + private byte[] MSSPReadConfig(MSSPConfig config) + { + byte[] msspBytes = []; - var attr = Attribute.GetCustomAttribute(field, typeof(NameAttribute)) as NameAttribute; + var fields = typeof(MSSPConfig).GetProperties(); + var knownFields = fields.Where(field => Attribute.IsDefined(field, typeof(NameAttribute))); - msspBytes = [.. msspBytes, .. ConvertToMSSP(attr.Name, b)]; - } + foreach (var field in knownFields) + { + var b = field.GetValue(config); + if (b == null) continue; - foreach (var item in config.Extended ?? []) - { - if (item.Value == null) continue; - msspBytes = [.. msspBytes, .. ConvertToMSSP(item.Key, item.Value) as byte[]]; - } + var attr = Attribute.GetCustomAttribute(field, typeof(NameAttribute)) as NameAttribute; - return msspBytes; + msspBytes = [.. msspBytes, .. ConvertToMSSP(attr.Name, b)]; } - private byte[] ConvertToMSSP(string name, dynamic val) + foreach (var item in config.Extended ?? []) { - byte[] bt = [(byte)Trigger.MSSP_VAR, .. ascii.GetBytes(name)]; + if (item.Value == null) continue; + msspBytes = [.. msspBytes, .. ConvertToMSSP(item.Key, item.Value) as byte[]]; + } - switch (val) - { - case string s: - { - _Logger.LogDebug("MSSP Announcement: {MSSPKey}: {MSSPVal}", name, s); - return [..bt, (byte)Trigger.MSSP_VAL, .. ascii.GetBytes(s)]; - } - case int i: - { - _Logger.LogDebug("MSSP Announcement: {MSSPKey}: {MSSPVal}", name, i.ToString()); - return [.. bt, (byte)Trigger.MSSP_VAL, .. ascii.GetBytes(i.ToString())]; - } - case bool boolean: - { - _Logger.LogDebug("MSSP Announcement: {MSSPKey}: {MSSPVal}", name, boolean); - return [.. bt, (byte)Trigger.MSSP_VAL, .. ascii.GetBytes(boolean ? "1" : "0")]; - } - case IEnumerable list: - { - foreach (var item in list) - { - _Logger.LogDebug("MSSP Announcement: {MSSPKey}[]: {MSSPVal}", name, item); - bt = [.. bt, (byte)Trigger.MSSP_VAL, .. ascii.GetBytes(item)]; - } - return bt; - } - default: + return msspBytes; + } + + private byte[] ConvertToMSSP(string name, dynamic val) + { + byte[] bt = [(byte)Trigger.MSSP_VAR, .. ascii.GetBytes(name)]; + + switch (val) + { + case string s: + { + _Logger.LogDebug("MSSP Announcement: {MSSPKey}: {MSSPVal}", name, s); + return [..bt, (byte)Trigger.MSSP_VAL, .. ascii.GetBytes(s)]; + } + case int i: + { + _Logger.LogDebug("MSSP Announcement: {MSSPKey}: {MSSPVal}", name, i.ToString()); + return [.. bt, (byte)Trigger.MSSP_VAL, .. ascii.GetBytes(i.ToString())]; + } + case bool boolean: + { + _Logger.LogDebug("MSSP Announcement: {MSSPKey}: {MSSPVal}", name, boolean); + return [.. bt, (byte)Trigger.MSSP_VAL, .. ascii.GetBytes(boolean ? "1" : "0")]; + } + case IEnumerable list: + { + foreach (var item in list) { - return []; + _Logger.LogDebug("MSSP Announcement: {MSSPKey}[]: {MSSPVal}", name, item); + bt = [.. bt, (byte)Trigger.MSSP_VAL, .. ascii.GetBytes(item)]; } - } + return bt; + } + default: + { + return []; + } } } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetNAWSInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetNAWSInterpreter.cs index a15650f..c259336 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetNAWSInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetNAWSInterpreter.cs @@ -6,202 +6,201 @@ using Stateless; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +/// +/// Implements http://www.faqs.org/rfcs/rfc1073.html +/// +/// +/// TODO: Implement Client Side +/// +public partial class TelnetInterpreter { /// - /// Implements http://www.faqs.org/rfcs/rfc1073.html + /// Internal NAWS Byte State + /// + private byte[] _nawsByteState; + + /// + /// Internal NAWS Byte Index Value + /// + private int _nawsIndex = 0; + + /// + /// Currently known Client Height /// /// - /// TODO: Implement Client Side + /// Defaults to 24 /// - public partial class TelnetInterpreter - { - /// - /// Internal NAWS Byte State - /// - private byte[] _nawsByteState; - - /// - /// Internal NAWS Byte Index Value - /// - private int _nawsIndex = 0; - - /// - /// Currently known Client Height - /// - /// - /// Defaults to 24 - /// - public int ClientHeight { get; private set; } = 24; - - /// - /// Currently known Client Width. - /// - /// - /// Defaults to 78 - /// - public int ClientWidth { get; private set; } = 78; - - /// - /// NAWS Callback function to alert server of Width & Height negotiation - /// - public Func SignalOnNAWSAsync { get; init; } - - /// - /// This exists to avoid an infinite loop with badly conforming clients. - /// - private bool _WillingToDoNAWS = false; - - /// - /// If the server you are connected to makes use of the client window in ways that are linked to its width and height, - /// then is useful for it to be able to find out how big it is, and also to get notified when it is resized. - /// - /// NAWS can be initiated from the Client or the Server. - /// - /// The state machine. - /// Itself - private StateMachine SetupNAWS(StateMachine tsm) - { - tsm.Configure(State.Willing) - .Permit(Trigger.NAWS, State.WillDoNAWS); + public int ClientHeight { get; private set; } = 24; - tsm.Configure(State.Refusing) - .Permit(Trigger.NAWS, State.WontDoNAWS); + /// + /// Currently known Client Width. + /// + /// + /// Defaults to 78 + /// + public int ClientWidth { get; private set; } = 78; - tsm.Configure(State.Dont) - .Permit(Trigger.NAWS, State.DontNAWS); + /// + /// NAWS Callback function to alert server of Width & Height negotiation + /// + public Func SignalOnNAWSAsync { get; init; } - tsm.Configure(State.Do) - .Permit(Trigger.NAWS, State.DoNAWS); + /// + /// This exists to avoid an infinite loop with badly conforming clients. + /// + private bool _WillingToDoNAWS = false; - if (Mode == TelnetMode.Server) - { - tsm.Configure(State.DontNAWS) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do NAWS - do nothing")); + /// + /// If the server you are connected to makes use of the client window in ways that are linked to its width and height, + /// then is useful for it to be able to find out how big it is, and also to get notified when it is resized. + /// + /// NAWS can be initiated from the Client or the Server. + /// + /// The state machine. + /// Itself + private StateMachine SetupNAWS(StateMachine tsm) + { + tsm.Configure(State.Willing) + .Permit(Trigger.NAWS, State.WillDoNAWS); - tsm.Configure(State.DoNAWS) - .SubstateOf(State.Accepting) - .OnEntryAsync(ServerWontNAWSAsync); - } + tsm.Configure(State.Refusing) + .Permit(Trigger.NAWS, State.WontDoNAWS); - if (Mode == TelnetMode.Client) - { - tsm.Configure(State.DontNAWS) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do NAWS - do nothing")); + tsm.Configure(State.Dont) + .Permit(Trigger.NAWS, State.DontNAWS); - tsm.Configure(State.DoNAWS) - .SubstateOf(State.Accepting) - .OnEntry(() => _WillingToDoNAWS = true); - } + tsm.Configure(State.Do) + .Permit(Trigger.NAWS, State.DoNAWS); - tsm.Configure(State.WillDoNAWS) + if (Mode == TelnetMode.Server) + { + tsm.Configure(State.DontNAWS) .SubstateOf(State.Accepting) - .OnEntryAsync(RequestNAWSAsync); + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do NAWS - do nothing")); - tsm.Configure(State.WontDoNAWS) + tsm.Configure(State.DoNAWS) .SubstateOf(State.Accepting) - .OnEntry(() => _WillingToDoNAWS = false); + .OnEntryAsync(ServerWontNAWSAsync); + } - tsm.Configure(State.SubNegotiation) - .Permit(Trigger.NAWS, State.NegotiatingNAWS); + if (Mode == TelnetMode.Client) + { + tsm.Configure(State.DontNAWS) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do NAWS - do nothing")); - tsm.Configure(State.NegotiatingNAWS) - .Permit(Trigger.IAC, State.EscapingNAWSValue) - .OnEntry(GetNAWS); + tsm.Configure(State.DoNAWS) + .SubstateOf(State.Accepting) + .OnEntry(() => _WillingToDoNAWS = true); + } - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.NegotiatingNAWS).Permit(t, State.EvaluatingNAWS)); + tsm.Configure(State.WillDoNAWS) + .SubstateOf(State.Accepting) + .OnEntryAsync(RequestNAWSAsync); - tsm.Configure(State.EvaluatingNAWS) - .PermitDynamic(Trigger.IAC, () => _nawsIndex < 4 ? State.EscapingNAWSValue : State.CompletingNAWS); + tsm.Configure(State.WontDoNAWS) + .SubstateOf(State.Accepting) + .OnEntry(() => _WillingToDoNAWS = false); - TriggerHelper.ForAllTriggers(t => tsm.Configure(State.EvaluatingNAWS).OnEntryFrom(ParameterizedTrigger(t), CaptureNAWS)); + tsm.Configure(State.SubNegotiation) + .Permit(Trigger.NAWS, State.NegotiatingNAWS); - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.EvaluatingNAWS).PermitReentry(t)); + tsm.Configure(State.NegotiatingNAWS) + .Permit(Trigger.IAC, State.EscapingNAWSValue) + .OnEntry(GetNAWS); - tsm.Configure(State.EscapingNAWSValue) - .Permit(Trigger.IAC, State.EvaluatingNAWS); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.NegotiatingNAWS).Permit(t, State.EvaluatingNAWS)); - tsm.Configure(State.CompletingNAWS) - .SubstateOf(State.EndSubNegotiation) - .OnEntryAsync(CompleteNAWSAsync); + tsm.Configure(State.EvaluatingNAWS) + .PermitDynamic(Trigger.IAC, () => _nawsIndex < 4 ? State.EscapingNAWSValue : State.CompletingNAWS); - RegisterInitialWilling(async () => await RequestNAWSAsync(null)); + TriggerHelper.ForAllTriggers(t => tsm.Configure(State.EvaluatingNAWS).OnEntryFrom(ParameterizedTrigger(t), CaptureNAWS)); - return tsm; - } + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.EvaluatingNAWS).PermitReentry(t)); - public async Task SendNAWS(short width, short height) - { - if(!_WillingToDoNAWS) await Task.CompletedTask; - - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.NAWS, - .. BitConverter.GetBytes(width), .. BitConverter.GetBytes(height), - (byte)Trigger.IAC, (byte)Trigger.SE]); - } + tsm.Configure(State.EscapingNAWSValue) + .Permit(Trigger.IAC, State.EvaluatingNAWS); - /// - /// Request NAWS from a client - /// - public async Task RequestNAWSAsync(StateMachine.Transition _) - { - if (!_WillingToDoNAWS) - { - _Logger.LogDebug("Connection: {ConnectionState}", "Requesting NAWS details from Client"); + tsm.Configure(State.CompletingNAWS) + .SubstateOf(State.EndSubNegotiation) + .OnEntryAsync(CompleteNAWSAsync); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.NAWS]); - _WillingToDoNAWS = true; - } - } + RegisterInitialWilling(async () => await RequestNAWSAsync(null)); - /// - /// Capture a byte and write it into the NAWS buffer - /// - /// The current byte - private void CaptureNAWS(OneOf b) - { - if (_nawsIndex > _nawsByteState.Length) return; - _nawsByteState[_nawsIndex] = b.AsT0; - _nawsIndex++; - } + return tsm; + } - /// - /// Initialize internal state values for NAWS. - /// - /// Ignored - private void GetNAWS(StateMachine.Transition _) - { - _nawsByteState = new byte[4]; - _nawsIndex = 0; - } + public async Task SendNAWS(short width, short height) + { + if(!_WillingToDoNAWS) await Task.CompletedTask; + + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.NAWS, + .. BitConverter.GetBytes(width), .. BitConverter.GetBytes(height), + (byte)Trigger.IAC, (byte)Trigger.SE]); + } - private async Task ServerWontNAWSAsync() + /// + /// Request NAWS from a client + /// + public async Task RequestNAWSAsync(StateMachine.Transition _) + { + if (!_WillingToDoNAWS) { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing refusing to send NAWS, this is a Server!"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WONT, (byte)Trigger.NAWS]); + _Logger.LogDebug("Connection: {ConnectionState}", "Requesting NAWS details from Client"); + + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.NAWS]); + _WillingToDoNAWS = true; } + } - /// - /// Read the NAWS state values and finalize it into width and height values. - /// - /// Ignored - private async Task CompleteNAWSAsync(StateMachine.Transition _) - { - byte[] width = [_nawsByteState[0], _nawsByteState[1]]; - byte[] height = [_nawsByteState[2], _nawsByteState[3]]; + /// + /// Capture a byte and write it into the NAWS buffer + /// + /// The current byte + private void CaptureNAWS(OneOf b) + { + if (_nawsIndex > _nawsByteState.Length) return; + _nawsByteState[_nawsIndex] = b.AsT0; + _nawsIndex++; + } - if (BitConverter.IsLittleEndian) - { - Array.Reverse(width); - Array.Reverse(height); - } + /// + /// Initialize internal state values for NAWS. + /// + /// Ignored + private void GetNAWS(StateMachine.Transition _) + { + _nawsByteState = new byte[4]; + _nawsIndex = 0; + } - ClientWidth = BitConverter.ToInt16(width); - ClientHeight = BitConverter.ToInt16(height); + private async Task ServerWontNAWSAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing refusing to send NAWS, this is a Server!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WONT, (byte)Trigger.NAWS]); + } - _Logger.LogDebug("Negotiated for: {clientWidth} width and {clientHeight} height", ClientWidth, ClientHeight); - await (SignalOnNAWSAsync?.Invoke(ClientHeight, ClientWidth) ?? Task.CompletedTask); + /// + /// Read the NAWS state values and finalize it into width and height values. + /// + /// Ignored + private async Task CompleteNAWSAsync(StateMachine.Transition _) + { + byte[] width = [_nawsByteState[0], _nawsByteState[1]]; + byte[] height = [_nawsByteState[2], _nawsByteState[3]]; + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(width); + Array.Reverse(height); } + + ClientWidth = BitConverter.ToInt16(width); + ClientHeight = BitConverter.ToInt16(height); + + _Logger.LogDebug("Negotiated for: {clientWidth} width and {clientHeight} height", ClientWidth, ClientHeight); + await (SignalOnNAWSAsync?.Invoke(ClientHeight, ClientWidth) ?? Task.CompletedTask); } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetSafeInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetSafeInterpreter.cs index 55fd686..e084902 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetSafeInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetSafeInterpreter.cs @@ -6,144 +6,143 @@ using System.IO; using Microsoft.Extensions.Logging; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +/// +/// The Safe Interpreter, providing ways to not crash the system when we are given a STATE we were not expecting. +/// +public partial class TelnetInterpreter { /// - /// The Safe Interpreter, providing ways to not crash the system when we are given a STATE we were not expecting. + /// Create a byte[] that is safe to send over telnet by repeating 255s. /// - public partial class TelnetInterpreter + /// The string intent to be sent across the wire. + /// The new byte[] with 255s duplicated. + public byte[] TelnetSafeString(string str) { - /// - /// Create a byte[] that is safe to send over telnet by repeating 255s. - /// - /// The string intent to be sent across the wire. - /// The new byte[] with 255s duplicated. - public byte[] TelnetSafeString(string str) - { - byte[] result; - var x = CurrentEncoding.GetBytes(str).AsSpan(); + byte[] result; + var x = CurrentEncoding.GetBytes(str).AsSpan(); - using (var memStream = new MemoryStream()) + using (var memStream = new MemoryStream()) + { + foreach (byte bt in x) { - foreach (byte bt in x) - { - memStream.Write(bt == 255 ? ([255, 255]) : (byte[])[bt]); - } - result = memStream.ToArray(); + memStream.Write(bt == 255 ? ([255, 255]) : (byte[])[bt]); } - - return result; + result = memStream.ToArray(); } - /// - /// Create a byte[] that is safe to send over telnet by repeating 255s. - /// Only use this function if you do not intend to send any kind of negotiation. - /// - /// The original bytes intent to be sent. - /// The new byte[] with 255s duplicated. - public byte[] TelnetSafeBytes(byte[] str) - { - byte[] result; - var x = str; + return result; + } - using (var memStream = new MemoryStream()) + /// + /// Create a byte[] that is safe to send over telnet by repeating 255s. + /// Only use this function if you do not intend to send any kind of negotiation. + /// + /// The original bytes intent to be sent. + /// The new byte[] with 255s duplicated. + public byte[] TelnetSafeBytes(byte[] str) + { + byte[] result; + var x = str; + + using (var memStream = new MemoryStream()) + { + foreach (byte bt in x) { - foreach (byte bt in x) - { - memStream.Write(bt == 255 ? ([255, 255]) : (byte[])[bt]); - } - result = memStream.ToArray(); + memStream.Write(bt == 255 ? ([255, 255]) : (byte[])[bt]); } - - return result; + result = memStream.ToArray(); } - /// - /// Protect against State Transitions and Telnet Negotiations we do not recognize. - /// - /// - /// TODO: Log what byte was sent using TriggerWithParameter output. - /// - /// The state machine. - /// Itself - private StateMachine SetupSafeNegotiation(StateMachine tsm) + return result; + } + + /// + /// Protect against State Transitions and Telnet Negotiations we do not recognize. + /// + /// + /// TODO: Log what byte was sent using TriggerWithParameter output. + /// + /// The state machine. + /// Itself + private StateMachine SetupSafeNegotiation(StateMachine tsm) + { + var info = tsm.GetInfo(); + var triggers = Enum.GetValues(typeof(Trigger)).OfType(); + var refuseThese = new List { State.Willing, State.Refusing, State.Do, State.Dont }; + + foreach (var stateInfo in info.States.Join(refuseThese, x => x.UnderlyingState, y => y, (x, y) => x)) { - var info = tsm.GetInfo(); - var triggers = Enum.GetValues(typeof(Trigger)).OfType(); - var refuseThese = new List { State.Willing, State.Refusing, State.Do, State.Dont }; + var state = (State)stateInfo.UnderlyingState; + var outboundUnhandledTriggers = triggers.Except(stateInfo.Transitions.Select(x => (Trigger)x.Trigger.UnderlyingTrigger)); - foreach (var stateInfo in info.States.Join(refuseThese, x => x.UnderlyingState, y => y, (x, y) => x)) + foreach (var trigger in outboundUnhandledTriggers) { - var state = (State)stateInfo.UnderlyingState; - var outboundUnhandledTriggers = triggers.Except(stateInfo.Transitions.Select(x => (Trigger)x.Trigger.UnderlyingTrigger)); + tsm.Configure(state).Permit(trigger, (State)Enum.Parse(typeof(State), $"Bad{state}")); + tsm.Configure((State)Enum.Parse(typeof(State), $"Bad{state}")) + .SubstateOf(State.Accepting); - foreach (var trigger in outboundUnhandledTriggers) + if (state is State.Do) { - tsm.Configure(state).Permit(trigger, (State)Enum.Parse(typeof(State), $"Bad{state}")); tsm.Configure((State)Enum.Parse(typeof(State), $"Bad{state}")) - .SubstateOf(State.Accepting); - - if (state is State.Do) - { - tsm.Configure((State)Enum.Parse(typeof(State), $"Bad{state}")) - .OnEntryFromAsync(trigger, async () => - { - _Logger.LogDebug("Connection: {ConnectionState}", $"Telling the Client, Won't respond to the trigger: {trigger}."); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WONT, (byte)trigger]); - }); - } - else if (state is State.Willing) - { - tsm.Configure((State)Enum.Parse(typeof(State), $"Bad{state}")) - .OnEntryFromAsync(trigger, async () => - { - _Logger.LogDebug("Connection: {ConnectionState}", $"Telling the Client, Don't send {trigger}."); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DONT, (byte)trigger]); - }); - } + .OnEntryFromAsync(trigger, async () => + { + _Logger.LogDebug("Connection: {ConnectionState}", $"Telling the Client, Won't respond to the trigger: {trigger}."); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WONT, (byte)trigger]); + }); + } + else if (state is State.Willing) + { + tsm.Configure((State)Enum.Parse(typeof(State), $"Bad{state}")) + .OnEntryFromAsync(trigger, async () => + { + _Logger.LogDebug("Connection: {ConnectionState}", $"Telling the Client, Don't send {trigger}."); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DONT, (byte)trigger]); + }); } } + } - var underlyingTriggers = info.States.First(x => (State)x.UnderlyingState == State.SubNegotiation).Transitions - .Select(x => (Trigger)x.Trigger.UnderlyingTrigger); + var underlyingTriggers = info.States.First(x => (State)x.UnderlyingState == State.SubNegotiation).Transitions + .Select(x => (Trigger)x.Trigger.UnderlyingTrigger); - foreach(var trigger in triggers.Except(underlyingTriggers)) - { - tsm.Configure(State.SubNegotiation).Permit(trigger, State.BadSubNegotiation); - } + foreach(var trigger in triggers.Except(underlyingTriggers)) + { + tsm.Configure(State.SubNegotiation).Permit(trigger, State.BadSubNegotiation); + } - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.BadSubNegotiation).Permit(t, State.BadSubNegotiationEvaluating)); - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.BadSubNegotiationEvaluating).PermitReentry(t)); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.BadSubNegotiation).Permit(t, State.BadSubNegotiationEvaluating)); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.BadSubNegotiationEvaluating).PermitReentry(t)); - tsm.Configure(State.BadSubNegotiation) - .Permit(Trigger.IAC, State.BadSubNegotiationEscaping) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", $"Unsupported SubNegotiation.")); - tsm.Configure(State.BadSubNegotiationEscaping) - .Permit(Trigger.IAC, State.BadSubNegotiationEvaluating) - .Permit(Trigger.SE, State.BadSubNegotiationCompleting); - tsm.Configure(State.BadSubNegotiationCompleting) - .OnEntry(() => _Logger.LogDebug("Connection: Explicitly ignoring the SubNegotiation that was sent.")) - .SubstateOf(State.Accepting); + tsm.Configure(State.BadSubNegotiation) + .Permit(Trigger.IAC, State.BadSubNegotiationEscaping) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", $"Unsupported SubNegotiation.")); + tsm.Configure(State.BadSubNegotiationEscaping) + .Permit(Trigger.IAC, State.BadSubNegotiationEvaluating) + .Permit(Trigger.SE, State.BadSubNegotiationCompleting); + tsm.Configure(State.BadSubNegotiationCompleting) + .OnEntry(() => _Logger.LogDebug("Connection: Explicitly ignoring the SubNegotiation that was sent.")) + .SubstateOf(State.Accepting); - var states = tsm.GetInfo().States; - var acceptingStateInfo = states.Where(x => (State)x.UnderlyingState == State.Accepting); + var states = tsm.GetInfo().States; + var acceptingStateInfo = states.Where(x => (State)x.UnderlyingState == State.Accepting); - var statesAllowingForErrorTransitions = states - .Except(acceptingStateInfo); + var statesAllowingForErrorTransitions = states + .Except(acceptingStateInfo); - foreach(var state in statesAllowingForErrorTransitions) - { - tsm.Configure((State)state.UnderlyingState).Permit(Trigger.Error, State.Accepting); - } + foreach(var state in statesAllowingForErrorTransitions) + { + tsm.Configure((State)state.UnderlyingState).Permit(Trigger.Error, State.Accepting); + } - tsm.OnUnhandledTrigger(async (state, trigger, unmetGuards) => - { - _Logger.LogCritical("Bad transition from {@State} with trigger {@Trigger} due to unmet guards: {@UnmetGuards}. Cannot recover. " + - "Ignoring character and attempting to recover.", state, trigger, unmetGuards); - await tsm.FireAsync(ParameterizedTrigger(Trigger.Error), Trigger.Error); - }); + tsm.OnUnhandledTrigger(async (state, trigger, unmetGuards) => + { + _Logger.LogCritical("Bad transition from {@State} with trigger {@Trigger} due to unmet guards: {@UnmetGuards}. Cannot recover. " + + "Ignoring character and attempting to recover.", state, trigger, unmetGuards); + await tsm.FireAsync(ParameterizedTrigger(Trigger.Error), Trigger.Error); + }); - return tsm; - } + return tsm; } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs index 5102649..fc17395 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetStandardInterpreter.cs @@ -9,278 +9,277 @@ using Microsoft.Extensions.Logging; using LocalMoreLinq; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +/// +/// TODO: Telnet Interpreter should take in a simple Interface object that can Read & Write from / to a Stream! +/// Read Byte, Write Byte, and a Buffer Size. That way we can test it. +/// +public partial class TelnetInterpreter { /// - /// TODO: Telnet Interpreter should take in a simple Interface object that can Read & Write from / to a Stream! - /// Read Byte, Write Byte, and a Buffer Size. That way we can test it. + /// A list of functions to call at the start. + /// + private readonly List> _InitialCall; + + /// + /// The current Encoding used for interpreting incoming non-negotiation text, and what we should send on outbound. + /// + public Encoding CurrentEncoding { get; private set; } = Encoding.ASCII; + + /// + /// Telnet state machine + /// + public StateMachine TelnetStateMachine { get; private set; } + + /// + /// A cache of parameterized triggers. + /// + private readonly ParameterizedTriggers _parameterizedTriggers; + + /// + /// Local buffer. We only take up to 5mb in buffer space. + /// + private readonly byte[] _buffer = new byte[5242880]; + + /// + /// Buffer position where we are writing. + /// + private int _bufferPosition = 0; + + /// + /// Helper function for Byte parameterized triggers. + /// + /// The Trigger + /// A Parameterized trigger + private StateMachine.TriggerWithParameters> ParameterizedTrigger(Trigger t) + => _parameterizedTriggers.ParameterizedTrigger(TelnetStateMachine, t); + + /// + /// The Logger /// - public partial class TelnetInterpreter + private readonly ILogger _Logger; + + public enum TelnetMode { - /// - /// A list of functions to call at the start. - /// - private readonly List> _InitialCall; - - /// - /// The current Encoding used for interpreting incoming non-negotiation text, and what we should send on outbound. - /// - public Encoding CurrentEncoding { get; private set; } = Encoding.ASCII; - - /// - /// Telnet state machine - /// - public StateMachine TelnetStateMachine { get; private set; } - - /// - /// A cache of parameterized triggers. - /// - private readonly ParameterizedTriggers _parameterizedTriggers; - - /// - /// Local buffer. We only take up to 5mb in buffer space. - /// - private readonly byte[] _buffer = new byte[5242880]; - - /// - /// Buffer position where we are writing. - /// - private int _bufferPosition = 0; - - /// - /// Helper function for Byte parameterized triggers. - /// - /// The Trigger - /// A Parameterized trigger - private StateMachine.TriggerWithParameters> ParameterizedTrigger(Trigger t) - => _parameterizedTriggers.ParameterizedTrigger(TelnetStateMachine, t); - - /// - /// The Logger - /// - private readonly ILogger _Logger; - - public enum TelnetMode - { - Error = 0, - Client = 1, - Server = 2 - }; - - public readonly TelnetMode Mode; - - /// - /// Callback to run on a submission (linefeed) - /// - public Func CallbackOnSubmitAsync { get; init; } - - /// - /// Callback to the output stream directly for negotiation. - /// - public Func CallbackNegotiationAsync { get; init; } - - /// - /// Callback per byte. - /// - public Func CallbackOnByteAsync { get; init; } - - /// - /// Constructor, sets up for standard Telnet protocol with NAWS and Character Set support. - /// - /// - /// After calling this constructor, one should subscribe to the Triggers, register a Stream, and then run Process() - /// - /// A Serilog Logger. If null, we will use the default one with a Context of the Telnet Interpreter. - public TelnetInterpreter(TelnetMode mode, ILogger logger) - { - Mode = mode; - _Logger = logger; - logger.BeginScope(new Dictionary { { "TelnetMode", mode } }); - - _InitialCall = []; - TelnetStateMachine = new StateMachine(State.Accepting); - _parameterizedTriggers = new ParameterizedTriggers(); - - SupportedCharacterSets = new Lazy(CharacterSets, true); - - var li = new List, StateMachine>> { - SetupSafeNegotiation, - SetupEORNegotiation, - SetupSuppressGANegotiation, - SetupMSSPNegotiation, - SetupMSDPNegotiation, - SetupGMCPNegotiation, - SetupTelnetTerminalType, - SetupCharsetNegotiation, - SetupNAWS, - SetupStandardProtocol - }.AggregateRight(TelnetStateMachine, (func, stateMachine) => func(stateMachine)); - - if (logger.IsEnabled(LogLevel.Trace)) - { - TelnetStateMachine.OnTransitioned((transition) => _Logger.LogTrace("Telnet StateMachine: {Source} --[{Trigger}({TriggerByte})]--> {Destination}", - transition.Source, transition.Trigger, transition.Parameters[0], transition.Destination)); - } - } + Error = 0, + Client = 1, + Server = 2 + }; - /// - /// Validates the configuration, then sets up the initial calls for negotiation. - /// - /// The Telnet Interpreter - public async Task BuildAsync() - { - Validate(); + public readonly TelnetMode Mode; - foreach (var t in _InitialCall) - { - await t(); - } - return this; - } + /// + /// Callback to run on a submission (linefeed) + /// + public Func CallbackOnSubmitAsync { get; init; } - /// - /// Setup standard processes. - /// - /// The state machine. - /// Itself - private StateMachine SetupStandardProtocol(StateMachine tsm) + /// + /// Callback to the output stream directly for negotiation. + /// + public Func CallbackNegotiationAsync { get; init; } + + /// + /// Callback per byte. + /// + public Func CallbackOnByteAsync { get; init; } + + /// + /// Constructor, sets up for standard Telnet protocol with NAWS and Character Set support. + /// + /// + /// After calling this constructor, one should subscribe to the Triggers, register a Stream, and then run Process() + /// + /// A Serilog Logger. If null, we will use the default one with a Context of the Telnet Interpreter. + public TelnetInterpreter(TelnetMode mode, ILogger logger) + { + Mode = mode; + _Logger = logger; + logger.BeginScope(new Dictionary { { "TelnetMode", mode } }); + + _InitialCall = []; + TelnetStateMachine = new StateMachine(State.Accepting); + _parameterizedTriggers = new ParameterizedTriggers(); + + SupportedCharacterSets = new Lazy(CharacterSets, true); + + var li = new List, StateMachine>> { + SetupSafeNegotiation, + SetupEORNegotiation, + SetupSuppressGANegotiation, + SetupMSSPNegotiation, + SetupMSDPNegotiation, + SetupGMCPNegotiation, + SetupTelnetTerminalType, + SetupCharsetNegotiation, + SetupNAWS, + SetupStandardProtocol + }.AggregateRight(TelnetStateMachine, (func, stateMachine) => func(stateMachine)); + + if (logger.IsEnabled(LogLevel.Trace)) { - // If we are in Accepting mode, these should be interpreted as regular characters. - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.Accepting).Permit(t, State.ReadingCharacters)); - - // Standard triggers, which are fine in the Awaiting state and should just be interpreted as a character in this state. - tsm.Configure(State.ReadingCharacters) - .SubstateOf(State.Accepting) - .Permit(Trigger.NEWLINE, State.Act); - - TriggerHelper.ForAllTriggers(t => tsm.Configure(State.ReadingCharacters).OnEntryFromAsync(ParameterizedTrigger(t), WriteToBufferAndAdvanceAsync)); - - // We've gotten a newline. We interpret this as time to act and send a signal back. - tsm.Configure(State.Act) - .SubstateOf(State.Accepting) - .OnEntry(WriteToOutput); - - // SubNegotiation - tsm.Configure(State.Accepting) - .Permit(Trigger.IAC, State.StartNegotiation); - - // Escaped IAC, interpret as actual IAC - tsm.Configure(State.StartNegotiation) - .Permit(Trigger.IAC, State.ReadingCharacters) - .Permit(Trigger.WILL, State.Willing) - .Permit(Trigger.WONT, State.Refusing) - .Permit(Trigger.DO, State.Do) - .Permit(Trigger.DONT, State.Dont) - .Permit(Trigger.SB, State.SubNegotiation) - .OnEntry(x => _Logger.LogTrace("Connection: {ConnectionState}", "Starting Negotiation")); - - tsm.Configure(State.StartNegotiation) - .Permit(Trigger.NOP, State.DoNothing); - - tsm.Configure(State.DoNothing) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogTrace("Connection: {ConnectionState}", "NOP call. Do nothing.")); - - // As a general documentation, negotiation means a Do followed by a Will, or a Will followed by a Do. - // Do followed by Refusing or Will followed by Don't indicate negative negotiation. - tsm.Configure(State.Willing); - tsm.Configure(State.Refusing); - tsm.Configure(State.Do); - tsm.Configure(State.Dont); - - tsm.Configure(State.ReadingCharacters) - .OnEntryFrom(Trigger.IAC, x => _Logger.LogDebug("Connection: {ConnectionState}", "Canceling negotiation")); - - tsm.Configure(State.SubNegotiation) - .OnEntryFrom(Trigger.IAC, x => _Logger.LogDebug("Connection: {ConnectionState}", "SubNegotiation request")); - - tsm.Configure(State.EndSubNegotiation) - .Permit(Trigger.SE, State.Accepting); - - return tsm; + TelnetStateMachine.OnTransitioned((transition) => _Logger.LogTrace("Telnet StateMachine: {Source} --[{Trigger}({TriggerByte})]--> {Destination}", + transition.Source, transition.Trigger, transition.Parameters[0], transition.Destination)); } + } - /// - /// Write the character into a buffer. - /// - /// A useful byte for the Client/Server - private async Task WriteToBufferAndAdvanceAsync(OneOf b) + /// + /// Validates the configuration, then sets up the initial calls for negotiation. + /// + /// The Telnet Interpreter + public async Task BuildAsync() + { + Validate(); + + foreach (var t in _InitialCall) { - if (b.AsT0 == (byte)Trigger.CARRIAGERETURN) return; - _Logger.LogTrace("Debug: Writing into buffer: {Byte}", b.AsT0); - _buffer[_bufferPosition] = b.AsT0; - _bufferPosition++; - await (CallbackOnByteAsync?.Invoke(b.AsT0, CurrentEncoding) ?? Task.CompletedTask); + await t(); } + return this; + } + + /// + /// Setup standard processes. + /// + /// The state machine. + /// Itself + private StateMachine SetupStandardProtocol(StateMachine tsm) + { + // If we are in Accepting mode, these should be interpreted as regular characters. + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.Accepting).Permit(t, State.ReadingCharacters)); + + // Standard triggers, which are fine in the Awaiting state and should just be interpreted as a character in this state. + tsm.Configure(State.ReadingCharacters) + .SubstateOf(State.Accepting) + .Permit(Trigger.NEWLINE, State.Act); + + TriggerHelper.ForAllTriggers(t => tsm.Configure(State.ReadingCharacters).OnEntryFromAsync(ParameterizedTrigger(t), WriteToBufferAndAdvanceAsync)); + + // We've gotten a newline. We interpret this as time to act and send a signal back. + tsm.Configure(State.Act) + .SubstateOf(State.Accepting) + .OnEntry(WriteToOutput); + + // SubNegotiation + tsm.Configure(State.Accepting) + .Permit(Trigger.IAC, State.StartNegotiation); + + // Escaped IAC, interpret as actual IAC + tsm.Configure(State.StartNegotiation) + .Permit(Trigger.IAC, State.ReadingCharacters) + .Permit(Trigger.WILL, State.Willing) + .Permit(Trigger.WONT, State.Refusing) + .Permit(Trigger.DO, State.Do) + .Permit(Trigger.DONT, State.Dont) + .Permit(Trigger.SB, State.SubNegotiation) + .OnEntry(x => _Logger.LogTrace("Connection: {ConnectionState}", "Starting Negotiation")); + + tsm.Configure(State.StartNegotiation) + .Permit(Trigger.NOP, State.DoNothing); + + tsm.Configure(State.DoNothing) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogTrace("Connection: {ConnectionState}", "NOP call. Do nothing.")); + + // As a general documentation, negotiation means a Do followed by a Will, or a Will followed by a Do. + // Do followed by Refusing or Will followed by Don't indicate negative negotiation. + tsm.Configure(State.Willing); + tsm.Configure(State.Refusing); + tsm.Configure(State.Do); + tsm.Configure(State.Dont); + + tsm.Configure(State.ReadingCharacters) + .OnEntryFrom(Trigger.IAC, x => _Logger.LogDebug("Connection: {ConnectionState}", "Canceling negotiation")); + + tsm.Configure(State.SubNegotiation) + .OnEntryFrom(Trigger.IAC, x => _Logger.LogDebug("Connection: {ConnectionState}", "SubNegotiation request")); + + tsm.Configure(State.EndSubNegotiation) + .Permit(Trigger.SE, State.Accepting); + + return tsm; + } + + /// + /// Write the character into a buffer. + /// + /// A useful byte for the Client/Server + private async Task WriteToBufferAndAdvanceAsync(OneOf b) + { + if (b.AsT0 == (byte)Trigger.CARRIAGERETURN) return; + _Logger.LogTrace("Debug: Writing into buffer: {Byte}", b.AsT0); + _buffer[_bufferPosition] = b.AsT0; + _bufferPosition++; + await (CallbackOnByteAsync?.Invoke(b.AsT0, CurrentEncoding) ?? Task.CompletedTask); + } + + /// + /// Write it to output - this should become an Event. + /// + private void WriteToOutput() + { + byte[] cp = new byte[_bufferPosition]; + _buffer.AsSpan()[.._bufferPosition].CopyTo(cp); + _bufferPosition = 0; + CallbackOnSubmitAsync.Invoke(cp, CurrentEncoding, this); + } - /// - /// Write it to output - this should become an Event. - /// - private void WriteToOutput() + /// + /// Validates the object is ready to process. + /// + private TelnetInterpreter Validate() + { + if (CallbackOnSubmitAsync == null && CallbackOnByteAsync == null) { - byte[] cp = new byte[_bufferPosition]; - _buffer.AsSpan()[.._bufferPosition].CopyTo(cp); - _bufferPosition = 0; - CallbackOnSubmitAsync.Invoke(cp, CurrentEncoding, this); + throw new ApplicationException($"Writeback Functions ({CallbackOnSubmitAsync}, {CallbackOnByteAsync}) are null or have not been registered."); } - - /// - /// Validates the object is ready to process. - /// - private TelnetInterpreter Validate() + if (CallbackNegotiationAsync == null) { - if (CallbackOnSubmitAsync == null && CallbackOnByteAsync == null) - { - throw new ApplicationException($"Writeback Functions ({CallbackOnSubmitAsync}, {CallbackOnByteAsync}) are null or have not been registered."); - } - if (CallbackNegotiationAsync == null) - { - throw new ApplicationException($"{CallbackNegotiationAsync} is null and has not been registered."); - } - if (SignalOnGMCPAsync == null) - { - throw new ApplicationException($"{SignalOnGMCPAsync} is null and has not been registered."); - } - - return this; + throw new ApplicationException($"{CallbackNegotiationAsync} is null and has not been registered."); } - - private void RegisterInitialWilling(Func fun) + if (SignalOnGMCPAsync == null) { - _InitialCall.Add(fun); + throw new ApplicationException($"{SignalOnGMCPAsync} is null and has not been registered."); } - /// - /// Interprets the next byte in an asynchronous way. - /// TODO: Cache the value of IsDefined, or get a way to compile this down to a faster call that doesn't require reflection each time. - /// - /// An integer representation of a byte. - /// Task - public async Task InterpretAsync(byte bt) + return this; + } + + private void RegisterInitialWilling(Func fun) + { + _InitialCall.Add(fun); + } + + /// + /// Interprets the next byte in an asynchronous way. + /// TODO: Cache the value of IsDefined, or get a way to compile this down to a faster call that doesn't require reflection each time. + /// + /// An integer representation of a byte. + /// Task + public async Task InterpretAsync(byte bt) + { + if (Enum.IsDefined(typeof(Trigger), (short)bt)) + { + await TelnetStateMachine.FireAsync(ParameterizedTrigger((Trigger)bt), bt); + } + else { - if (Enum.IsDefined(typeof(Trigger), (short)bt)) - { - await TelnetStateMachine.FireAsync(ParameterizedTrigger((Trigger)bt), bt); - } - else - { - await TelnetStateMachine.FireAsync(ParameterizedTrigger(Trigger.ReadNextCharacter), bt); - } + await TelnetStateMachine.FireAsync(ParameterizedTrigger(Trigger.ReadNextCharacter), bt); } + } - /// - /// Interprets the next byte in an asynchronous way. - /// TODO: Cache the value of IsDefined, or get a way to compile this down to a faster call that doesn't require reflection each time. - /// - /// An integer representation of a byte. - /// Task - public async Task InterpretByteArrayAsync(ImmutableArray byteArray) + /// + /// Interprets the next byte in an asynchronous way. + /// TODO: Cache the value of IsDefined, or get a way to compile this down to a faster call that doesn't require reflection each time. + /// + /// An integer representation of a byte. + /// Task + public async Task InterpretByteArrayAsync(ImmutableArray byteArray) + { + foreach (var b in byteArray) { - foreach (var b in byteArray) - { - await InterpretAsync(b); - } + await InterpretAsync(b); } } } diff --git a/TelnetNegotiationCore/Interpreters/TelnetSuppressGAInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetSuppressGAInterpreter.cs index b085deb..34f1dd7 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetSuppressGAInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetSuppressGAInterpreter.cs @@ -3,105 +3,104 @@ using System.Threading.Tasks; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +public partial class TelnetInterpreter { - public partial class TelnetInterpreter + private bool? _doGA = true; + + /// + /// Character set Negotiation will set the Character Set and Character Page Server & Client have agreed to. + /// + /// The state machine. + /// Itself + private StateMachine SetupSuppressGANegotiation(StateMachine tsm) { - private bool? _doGA = true; - - /// - /// Character set Negotiation will set the Character Set and Character Page Server & Client have agreed to. - /// - /// The state machine. - /// Itself - private StateMachine SetupSuppressGANegotiation(StateMachine tsm) + if (Mode == TelnetMode.Server) { - 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; - } + tsm.Configure(State.Do) + .Permit(Trigger.SUPPRESSGOAHEAD, State.DoSUPPRESSGOAHEAD); + tsm.Configure(State.Dont) + .Permit(Trigger.SUPPRESSGOAHEAD, State.DontSUPPRESSGOAHEAD); - private async Task OnSUPPRESSGOAHEADPrompt() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Server is prompting SUPPRESSGOAHEAD"); - await (SignalOnPromptingAsync?.Invoke() ?? Task.CompletedTask); - } + tsm.Configure(State.DoSUPPRESSGOAHEAD) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnDoSuppressGAAsync); - private async Task OnDontSuppressGAAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do SUPPRESSGOAHEAD - do nothing"); - _doGA = true; - await Task.CompletedTask; - } + tsm.Configure(State.DontSUPPRESSGOAHEAD) + .SubstateOf(State.Accepting) + .OnEntryAsync(OnDontSuppressGAAsync); - private async Task WontSuppressGAAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Server won't do SUPPRESSGOAHEAD - do nothing"); - _doGA = true; - await Task.CompletedTask; + RegisterInitialWilling(WillingSuppressGAAsync); } - - /// - /// Announce we do SUPPRESSGOAHEAD negotiation to the client. - /// - private async Task WillingSuppressGAAsync() + else { - _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to SUPPRESSGOAHEAD!"); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.SUPPRESSGOAHEAD]); - } + tsm.Configure(State.Willing) + .Permit(Trigger.SUPPRESSGOAHEAD, State.WillSUPPRESSGOAHEAD); - /// - /// Store that we are now in SUPPRESSGOAHEAD mode. - /// - private Task OnDoSuppressGAAsync(StateMachine.Transition _) - { - _Logger.LogDebug("Connection: {ConnectionState}", "Client supports End of Record."); - _doGA =false; - return Task.CompletedTask; - } + tsm.Configure(State.Refusing) + .Permit(Trigger.SUPPRESSGOAHEAD, State.WontSUPPRESSGOAHEAD); - /// - /// Store that we are now in SUPPRESSGOAHEAD mode. - /// - private async Task OnWillSuppressGAAsync(StateMachine.Transition _) - { - _Logger.LogDebug("Connection: {ConnectionState}", "Server supports End of Record."); - _doGA = false; - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.SUPPRESSGOAHEAD]); + 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; + } + + /// + /// Announce we do SUPPRESSGOAHEAD negotiation to the client. + /// + private async Task WillingSuppressGAAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Announcing willingness to SUPPRESSGOAHEAD!"); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.SUPPRESSGOAHEAD]); + } + + /// + /// Store that we are now in SUPPRESSGOAHEAD mode. + /// + private Task OnDoSuppressGAAsync(StateMachine.Transition _) + { + _Logger.LogDebug("Connection: {ConnectionState}", "Client supports End of Record."); + _doGA =false; + return Task.CompletedTask; + } + + /// + /// Store that we are now in SUPPRESSGOAHEAD mode. + /// + private async Task OnWillSuppressGAAsync(StateMachine.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/Interpreters/TelnetTerminalTypeInterpreter.cs b/TelnetNegotiationCore/Interpreters/TelnetTerminalTypeInterpreter.cs index 6b61d8d..cd6106c 100644 --- a/TelnetNegotiationCore/Interpreters/TelnetTerminalTypeInterpreter.cs +++ b/TelnetNegotiationCore/Interpreters/TelnetTerminalTypeInterpreter.cs @@ -8,261 +8,260 @@ using System.Threading.Tasks; using TelnetNegotiationCore.Models; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +/// +/// Implements RFC 1091 and MTTS +/// https://datatracker.ietf.org/doc/html/rfc1091 +/// https://tintin.mudhalla.net/protocols/mtts/ +/// +/// TODO: Allow the end-user to set TerminalTypes in Client Mode. +/// TODO: Optimize byte array allocations that get commonly used. +/// +public partial class TelnetInterpreter { /// - /// Implements RFC 1091 and MTTS - /// https://datatracker.ietf.org/doc/html/rfc1091 - /// https://tintin.mudhalla.net/protocols/mtts/ - /// - /// TODO: Allow the end-user to set TerminalTypes in Client Mode. - /// TODO: Optimize byte array allocations that get commonly used. + /// A list of terminal types for this connection. /// - public partial class TelnetInterpreter - { - /// - /// A list of terminal types for this connection. - /// - public ImmutableList TerminalTypes { get; private set; } = []; - - /// - /// The current selected Terminal Type. Use RequestTerminalTypeAsync if you want the client to switch to the next mode. - /// - public string CurrentTerminalType => _CurrentTerminalType == -1 ? "unknown" : TerminalTypes[Math.Min(_CurrentTerminalType, TerminalTypes.Count - 1)]; - - /// - /// Currently selected Terminal Type index. - /// - private int _CurrentTerminalType = -1; - - /// - /// Internal Terminal Type Byte State - /// - private byte[] _ttypeByteState; - - /// - /// Internal Terminal Type Byte Index - /// - private int _ttypeIndex = 0; - - /// - /// A dictionary for MTTS support. - /// - private readonly Dictionary _MTTS = new() - { - {1, "ANSI"}, - {2, "VT100"}, - {4, "UTF8"}, - {8, "256 COLORS"}, - {16, "MOUSE_TRACKING"}, - {32, "OSC_COLOR_PALETTE"}, - {64, "SCREEN_READER"}, - {128, "PROXY"}, - {256, "TRUECOLOR"}, - {512, "MNES"}, - {1024, "MSLP"} - }; + public ImmutableList TerminalTypes { get; private set; } = []; - /// - /// Support for Client & Server Terminal Type negotiation - /// RFC 1091 - /// - /// The state machine. - /// Itself - private StateMachine SetupTelnetTerminalType(StateMachine tsm) - { - tsm.Configure(State.Willing) - .Permit(Trigger.TTYPE, State.WillDoTType); + /// + /// The current selected Terminal Type. Use RequestTerminalTypeAsync if you want the client to switch to the next mode. + /// + public string CurrentTerminalType => _CurrentTerminalType == -1 ? "unknown" : TerminalTypes[Math.Min(_CurrentTerminalType, TerminalTypes.Count - 1)]; - tsm.Configure(State.Refusing) - .Permit(Trigger.TTYPE, State.WontDoTType); + /// + /// Currently selected Terminal Type index. + /// + private int _CurrentTerminalType = -1; + + /// + /// Internal Terminal Type Byte State + /// + private byte[] _ttypeByteState; - tsm.Configure(State.Do) - .Permit(Trigger.TTYPE, State.DoTType); + /// + /// Internal Terminal Type Byte Index + /// + private int _ttypeIndex = 0; - tsm.Configure(State.Dont) - .Permit(Trigger.TTYPE, State.DontTType); + /// + /// A dictionary for MTTS support. + /// + private readonly Dictionary _MTTS = new() + { + {1, "ANSI"}, + {2, "VT100"}, + {4, "UTF8"}, + {8, "256 COLORS"}, + {16, "MOUSE_TRACKING"}, + {32, "OSC_COLOR_PALETTE"}, + {64, "SCREEN_READER"}, + {128, "PROXY"}, + {256, "TRUECOLOR"}, + {512, "MNES"}, + {1024, "MSLP"} + }; - return Mode switch - { - TelnetMode.Server => SetupTelnetTerminalTypeAsServer(tsm), - TelnetMode.Client => SetupTelnetTerminalTypeAsClient(tsm), - _ => throw new NotImplementedException() - }; - } + /// + /// Support for Client & Server Terminal Type negotiation + /// RFC 1091 + /// + /// The state machine. + /// Itself + private StateMachine SetupTelnetTerminalType(StateMachine tsm) + { + tsm.Configure(State.Willing) + .Permit(Trigger.TTYPE, State.WillDoTType); + + tsm.Configure(State.Refusing) + .Permit(Trigger.TTYPE, State.WontDoTType); + + tsm.Configure(State.Do) + .Permit(Trigger.TTYPE, State.DoTType); - /// - /// Sets up the Telnet Terminal Type negotiation in Client Mode. - /// As the Client, we respond to DO and DONT, but not to WILL and WONT. - /// - /// The state machine. - /// Itself - private StateMachine SetupTelnetTerminalTypeAsClient(StateMachine tsm) + tsm.Configure(State.Dont) + .Permit(Trigger.TTYPE, State.DontTType); + + return Mode switch { - _CurrentTerminalType = -1; - TerminalTypes = TerminalTypes.AddRange([ "TNC", "XTERM", "MTTS 3853" ]); + TelnetMode.Server => SetupTelnetTerminalTypeAsServer(tsm), + TelnetMode.Client => SetupTelnetTerminalTypeAsClient(tsm), + _ => throw new NotImplementedException() + }; + } - tsm.Configure(State.DoTType) - .SubstateOf(State.Accepting) - .OnEntryAsync(WillDoTerminalTypeAsync); + /// + /// Sets up the Telnet Terminal Type negotiation in Client Mode. + /// As the Client, we respond to DO and DONT, but not to WILL and WONT. + /// + /// The state machine. + /// Itself + private StateMachine SetupTelnetTerminalTypeAsClient(StateMachine tsm) + { + _CurrentTerminalType = -1; + TerminalTypes = TerminalTypes.AddRange([ "TNC", "XTERM", "MTTS 3853" ]); - tsm.Configure(State.DontTType) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Server telling us not to Terminal Type")); + tsm.Configure(State.DoTType) + .SubstateOf(State.Accepting) + .OnEntryAsync(WillDoTerminalTypeAsync); - tsm.Configure(State.SubNegotiation) - .Permit(Trigger.TTYPE, State.AlmostNegotiatingTerminalType); + tsm.Configure(State.DontTType) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Server telling us not to Terminal Type")); - tsm.Configure(State.AlmostNegotiatingTerminalType) - .Permit(Trigger.SEND, State.NegotiatingTerminalType); + tsm.Configure(State.SubNegotiation) + .Permit(Trigger.TTYPE, State.AlmostNegotiatingTerminalType); - tsm.Configure(State.NegotiatingTerminalType) - .Permit(Trigger.IAC, State.CompletingTerminalType) - .OnEntry(GetTerminalType); + tsm.Configure(State.AlmostNegotiatingTerminalType) + .Permit(Trigger.SEND, State.NegotiatingTerminalType); - tsm.Configure(State.CompletingTerminalType) - .OnEntryAsync(ReportNextAvailableTerminalTypeAsync) - .Permit(Trigger.SE, State.Accepting); + tsm.Configure(State.NegotiatingTerminalType) + .Permit(Trigger.IAC, State.CompletingTerminalType) + .OnEntry(GetTerminalType); - return tsm; - } + tsm.Configure(State.CompletingTerminalType) + .OnEntryAsync(ReportNextAvailableTerminalTypeAsync) + .Permit(Trigger.SE, State.Accepting); - /// - /// Sets up the Telnet Terminal Type negotiation in Server Mode. - /// As the server, we respond to WILL and WONT, but not to DO and DONT. - /// We initiate the request for Telnet Negotiation. - /// - /// The state machine. - /// Itself - private StateMachine SetupTelnetTerminalTypeAsServer(StateMachine tsm) - { - tsm.Configure(State.WillDoTType) - .SubstateOf(State.Accepting) - .OnEntryAsync(RequestTerminalTypeAsync); + return tsm; + } - tsm.Configure(State.WontDoTType) - .SubstateOf(State.Accepting) - .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do Terminal Type")); + /// + /// Sets up the Telnet Terminal Type negotiation in Server Mode. + /// As the server, we respond to WILL and WONT, but not to DO and DONT. + /// We initiate the request for Telnet Negotiation. + /// + /// The state machine. + /// Itself + private StateMachine SetupTelnetTerminalTypeAsServer(StateMachine tsm) + { + tsm.Configure(State.WillDoTType) + .SubstateOf(State.Accepting) + .OnEntryAsync(RequestTerminalTypeAsync); - tsm.Configure(State.SubNegotiation) - .Permit(Trigger.TTYPE, State.AlmostNegotiatingTerminalType); + tsm.Configure(State.WontDoTType) + .SubstateOf(State.Accepting) + .OnEntry(() => _Logger.LogDebug("Connection: {ConnectionState}", "Client won't do Terminal Type")); - tsm.Configure(State.AlmostNegotiatingTerminalType) - .Permit(Trigger.IS, State.NegotiatingTerminalType); + tsm.Configure(State.SubNegotiation) + .Permit(Trigger.TTYPE, State.AlmostNegotiatingTerminalType); - tsm.Configure(State.NegotiatingTerminalType) - .Permit(Trigger.IAC, State.EscapingTerminalTypeValue) - .OnEntry(GetTerminalType); + tsm.Configure(State.AlmostNegotiatingTerminalType) + .Permit(Trigger.IS, State.NegotiatingTerminalType); - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.NegotiatingTerminalType).Permit(t, State.EvaluatingTerminalType)); - TriggerHelper.ForAllTriggers(t => tsm.Configure(State.EvaluatingTerminalType).OnEntryFrom(ParameterizedTrigger(t), CaptureTerminalType)); - TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.EvaluatingTerminalType).PermitReentry(t)); + tsm.Configure(State.NegotiatingTerminalType) + .Permit(Trigger.IAC, State.EscapingTerminalTypeValue) + .OnEntry(GetTerminalType); - tsm.Configure(State.EvaluatingTerminalType) - .Permit(Trigger.IAC, State.EscapingTerminalTypeValue); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.NegotiatingTerminalType).Permit(t, State.EvaluatingTerminalType)); + TriggerHelper.ForAllTriggers(t => tsm.Configure(State.EvaluatingTerminalType).OnEntryFrom(ParameterizedTrigger(t), CaptureTerminalType)); + TriggerHelper.ForAllTriggersButIAC(t => tsm.Configure(State.EvaluatingTerminalType).PermitReentry(t)); - tsm.Configure(State.EscapingTerminalTypeValue) - .Permit(Trigger.IAC, State.EvaluatingTerminalType) - .Permit(Trigger.SE, State.CompletingTerminalType); + tsm.Configure(State.EvaluatingTerminalType) + .Permit(Trigger.IAC, State.EscapingTerminalTypeValue); - tsm.Configure(State.CompletingTerminalType) - .OnEntryAsync(CompleteTerminalTypeAsServerAsync) - .SubstateOf(State.Accepting); + tsm.Configure(State.EscapingTerminalTypeValue) + .Permit(Trigger.IAC, State.EvaluatingTerminalType) + .Permit(Trigger.SE, State.CompletingTerminalType); - RegisterInitialWilling(SendDoTerminalTypeAsync); + tsm.Configure(State.CompletingTerminalType) + .OnEntryAsync(CompleteTerminalTypeAsServerAsync) + .SubstateOf(State.Accepting); - return tsm; - } + RegisterInitialWilling(SendDoTerminalTypeAsync); - /// - /// Initialize internal state values for Terminal Type. - /// - /// Ignored - private void GetTerminalType() - { - _ttypeByteState = new byte[1024]; - _ttypeIndex = 0; - } + return tsm; + } - /// - /// Capture a byte and write it into the Terminal Type buffer - /// - /// The current byte - private void CaptureTerminalType(OneOf b) - { - if (_ttypeIndex > _ttypeByteState.Length) return; - _ttypeByteState[_ttypeIndex] = b.AsT0; - _ttypeIndex++; - } + /// + /// Initialize internal state values for Terminal Type. + /// + /// Ignored + private void GetTerminalType() + { + _ttypeByteState = new byte[1024]; + _ttypeIndex = 0; + } + + /// + /// Capture a byte and write it into the Terminal Type buffer + /// + /// The current byte + private void CaptureTerminalType(OneOf b) + { + if (_ttypeIndex > _ttypeByteState.Length) return; + _ttypeByteState[_ttypeIndex] = b.AsT0; + _ttypeIndex++; + } - /// - /// Read the Terminal Type state values and finalize it into the Terminal Types List. - /// Then, if we have not seen this Terminal Type before, request another! - /// - private async Task CompleteTerminalTypeAsServerAsync() + /// + /// Read the Terminal Type state values and finalize it into the Terminal Types List. + /// Then, if we have not seen this Terminal Type before, request another! + /// + private async Task CompleteTerminalTypeAsServerAsync() + { + var TType = ascii.GetString(_ttypeByteState, 0, _ttypeIndex); + if (TerminalTypes.Contains(TType)) { - var TType = ascii.GetString(_ttypeByteState, 0, _ttypeIndex); - if (TerminalTypes.Contains(TType)) + _CurrentTerminalType = (_CurrentTerminalType + 1) % TerminalTypes.Count; + var MTTS = TerminalTypes.FirstOrDefault(x => x.StartsWith("MTTS")); + if (MTTS != default) { - _CurrentTerminalType = (_CurrentTerminalType + 1) % TerminalTypes.Count; - var MTTS = TerminalTypes.FirstOrDefault(x => x.StartsWith("MTTS")); - if (MTTS != default) - { - var mttsVal = int.Parse(MTTS.Remove(0, 5)); + var mttsVal = int.Parse(MTTS.Remove(0, 5)); - TerminalTypes = TerminalTypes.AddRange(_MTTS.Where(x => (mttsVal & x.Key) != 0).Select(x => x.Value)); - } - - TerminalTypes = TerminalTypes.Remove(MTTS); - _Logger.LogDebug("Connection: {ConnectionState}: {@TerminalTypes}", "Completing Terminal Type negotiation. List as follows", TerminalTypes); - } - else - { - _Logger.LogTrace("Connection: {ConnectionState}: {TerminalType}", "Registering Terminal Type. Requesting the next", TType); - TerminalTypes = TerminalTypes.Add(TType); - _CurrentTerminalType++; - await RequestTerminalTypeAsync(); + TerminalTypes = TerminalTypes.AddRange(_MTTS.Where(x => (mttsVal & x.Key) != 0).Select(x => x.Value)); } - } - /// - /// Tell the Client that the Server is willing to listen to Terminal Type. - /// - /// - private async Task WillDoTerminalTypeAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Telling the other party, Willing to do Terminal Type."); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE]); + TerminalTypes = TerminalTypes.Remove(MTTS); + _Logger.LogDebug("Connection: {ConnectionState}: {@TerminalTypes}", "Completing Terminal Type negotiation. List as follows", TerminalTypes); } - - /// - /// Tell the Client to do Terminal Type. This should not happen as a Server. - /// - private async Task SendDoTerminalTypeAsync() + else { - _Logger.LogDebug("Connection: {ConnectionState}", "Telling the other party, to do Terminal Type."); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.TTYPE]); + _Logger.LogTrace("Connection: {ConnectionState}: {TerminalType}", "Registering Terminal Type. Requesting the next", TType); + TerminalTypes = TerminalTypes.Add(TType); + _CurrentTerminalType++; + await RequestTerminalTypeAsync(); } + } - /// - /// Request Terminal Type from Client. This flips to the next one. - /// - public async Task RequestTerminalTypeAsync() - { - _Logger.LogDebug("Connection: {ConnectionState}", "Telling the client, to send the next Terminal Type."); - await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE]); - } + /// + /// Tell the Client that the Server is willing to listen to Terminal Type. + /// + /// + private async Task WillDoTerminalTypeAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Telling the other party, Willing to do Terminal Type."); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.WILL, (byte)Trigger.TTYPE]); + } - private async Task ReportNextAvailableTerminalTypeAsync() - { - _CurrentTerminalType = (_CurrentTerminalType + 1) % (TerminalTypes.Count + 1); - _Logger.LogDebug("Connection: {ConnectionState}", "Reporting the next Terminal Type to the server."); - byte[] terminalType = [ - (byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, - .. ascii.GetBytes(CurrentTerminalType), - (byte)Trigger.IAC, (byte)Trigger.SE ]; - - await CallbackNegotiationAsync(terminalType); - } + /// + /// Tell the Client to do Terminal Type. This should not happen as a Server. + /// + private async Task SendDoTerminalTypeAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Telling the other party, to do Terminal Type."); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.DO, (byte)Trigger.TTYPE]); + } + + /// + /// Request Terminal Type from Client. This flips to the next one. + /// + public async Task RequestTerminalTypeAsync() + { + _Logger.LogDebug("Connection: {ConnectionState}", "Telling the client, to send the next Terminal Type."); + await CallbackNegotiationAsync([(byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.SEND, (byte)Trigger.IAC, (byte)Trigger.SE]); + } + + private async Task ReportNextAvailableTerminalTypeAsync() + { + _CurrentTerminalType = (_CurrentTerminalType + 1) % (TerminalTypes.Count + 1); + _Logger.LogDebug("Connection: {ConnectionState}", "Reporting the next Terminal Type to the server."); + byte[] terminalType = [ + (byte)Trigger.IAC, (byte)Trigger.SB, (byte)Trigger.TTYPE, (byte)Trigger.IS, + .. ascii.GetBytes(CurrentTerminalType), + (byte)Trigger.IAC, (byte)Trigger.SE ]; + + await CallbackNegotiationAsync(terminalType); } } \ No newline at end of file diff --git a/TelnetNegotiationCore/Models/MSSPConfig.cs b/TelnetNegotiationCore/Models/MSSPConfig.cs index f4c0ec9..fc3f0a7 100644 --- a/TelnetNegotiationCore/Models/MSSPConfig.cs +++ b/TelnetNegotiationCore/Models/MSSPConfig.cs @@ -1,238 +1,227 @@ using System; using System.Collections.Generic; -namespace TelnetNegotiationCore.Models +namespace TelnetNegotiationCore.Models; + +/// +/// Indicates the MSSP-safe name to send. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false)] +public class NameAttribute(string name) : Attribute +{ + public string Name { get; private set; } = name; +} + +/// +/// Indicates whether or not it's in the official MSSP definition. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false)] +public class OfficialAttribute(bool official) : Attribute +{ + public bool Official { get; private set; } = official; +} + +/// +/// The MSSP Configuration. Takes Functions for its inputs for the purpose of re-evaluation. +/// +public class MSSPConfig { - /// - /// Indicates the MSSP-safe name to send. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false)] - public class NameAttribute : Attribute - { - public string Name { get; private set; } - - public NameAttribute(string name) - { - Name = name; - } - } - - /// - /// Indicates whether or not it's in the official MSSP definition. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false)] - public class OfficialAttribute : Attribute - { - public bool Official { get; private set; } - - public OfficialAttribute(bool official) - { - Official = official; - } - } - - /// - /// The MSSP Configuration. Takes Functions for its inputs for the purpose of re-evaluation. - /// - public class MSSPConfig - { - /// NAME: Name of the MUD. - [Name("NAME"), Official(true)] - public string Name { get; set; } - - /// PLAYERS: Current number of logged in players. - [Name("PLAYERS"), Official(true)] - public int Players { get; set; } - - /// UPTIME: Unix time value of the startup time of the MUD. - [Name("UPTIME"), Official(true)] - public int Uptime { get; set; } - - /// CODEBASE: Name of the codebase, eg Merc 2.1. You can report multiple codebases using the array format, make sure to report the current codebase last. - [Name("CODEBASE"), Official(true)] - public IEnumerable Codebase { get; set; } - - /// CONTACT: Email address for contacting the MUD. - [Name("CONTACT"), Official(true)] - public string Contact { get; set; } - - /// CRAWL DELAY: Preferred minimum number of hours between crawls. Send -1 to use the crawler's default. - [Name("CRAWL DELAY"), Official(true)] - public int Crawl_Delay { get; set; } - - /// CREATED: Year the MUD was created. - [Name("CREATED"), Official(true)] - public string Created { get; set; } - - /// HOSTNAME: Current or new hostname. - [Name("HOSTNAME"), Official(true)] - public string Hostname { get; set; } - - /// ICON: URL to a square image in bmp, png, jpg, or gif format. The icon should be equal or larger than 64x64 pixels, with a filesize no larger than 256KB. - [Name("ICON"), Official(true)] - public string Icon { get; set; } - - /// IP: Current or new IP address. - [Name("IP"), Official(true)] - public string IP { get; set; } - - /// IPV6: Current or new IPv6 address. - [Name("IPV6"), Official(true)] - public string IPV6 { get; set; } - - /// LANGUAGE: English name of the language used, eg German or English - [Name("LANGUAGE"), Official(true)] - public string Language { get; set; } - - /// LOCATION: English short name of the country where the server is located, using ISO 3166. - [Name("LOCATION"), Official(true)] - public string Location { get; set; } - - /// MINIMUM AGE: Current minimum age requirement, omit if not applicable. - [Name("MINIMUM AGE"), Official(true)] - public string Minimum_Age { get; set; } - - /// PORT: Current or new port number. Can be used multiple times, most important port last. - [Name("PORT"), Official(true)] - public int Port { get; set; } - - /// REFERRAL: A list of other MSSP enabled MUDs for the crawler to check using the host port format and array notation. Adding referrals is important to make MSSP decentralized. Make sure to separate the host and port with a space rather than : because IPv6 addresses contain colons. - [Name("REFERRAL"), Official(true)] - public IEnumerable Referral { get; set; } - - /// The port number for a SSL (Secure Socket Layer) encrypted connection. - [Name("SSL"), Official(true)] - public string Ssl { get; set; } - - /// WEBSITE: URL to MUD website, this should include the http:// or https:// prefix. - [Name("WEBSITE"), Official(true)] - public string Website { get; set; } - - /// FAMILY: AberMUD, CoffeeMUD, DikuMUD, Evennia, LPMud, MajorMUD, MOO, Mordor, SocketMud, TinyMUD, TinyMUCK, TinyMUSH, Custom. - /// Report Custom unless it's a well established family. - /// - /// You can report multiple generic codebases using the array format, make sure to report the most distant codebase (aka the family) last. - /// - /// Check the MUD family tree for naming and capitalization. - [Name("FAMILY"), Official(true)] - public IEnumerable Family { get; set; } - - /// GENRE: Adult, Fantasy, Historical, Horror, Modern, Mystery, None, Romance, Science Fiction, Spiritual - [Name("GENRE"), Official(true)] - public string Genre { get; set; } - - /// GAMEPLAY: Adventure, Educational, Hack and Slash, None, Player versus Player, Player versus Environment, Questing, Roleplaying, Simulation, Social, Strategy - [Name("GAMEPLAY"), Official(true)] - public IEnumerable Gameplay { get; set; } - - - /// STATUS: Alpha, Closed Beta, Open Beta, Live - [Name("STATUS"), Official(true)] - public string Status { get; set; } - - /// GAMESYSTEM: D&D, d20 System, World of Darkness, Etc. Use Custom if using a custom game system. Use None if not available. - [Name("GAMESYSTEM"), Official(true)] - public string Gamesystem { get; set; } - - /// INTERMUD: AberChat, I3, IMC2, MudNet, Etc. Can be used multiple times if you support several protocols, most important protocol last. Leave empty or omit if no Intermud protocol is supported. - [Name("INTERMUD"), Official(true)] - public IEnumerable Intermud { get; set; } - - /// SUBGENRE: Alternate History, Anime, Cyberpunk, Detective, Discworld, Dragonlance, Christian Fiction, Classical Fantasy, - /// Crime, Dark Fantasy, Epic Fantasy, Erotic, Exploration, Forgotten Realms, Frankenstein, Gothic, High Fantasy, - /// Magical Realism, Medieval Fantasy, Multiverse, Paranormal, Post-Apocalyptic, Military Science Fiction, - /// Mythology, Pulp, Star Wars, Steampunk, Suspense, Time Travel, Weird Fiction, World War II, Urban Fantasy, Etc. - /// - /// Use None if not applicable. - [Name("SUBGENRE"), Official(true)] - public string Subgenre { get; set; } - - /// AREAS: Current number of areas. - [Name("AREAS"), Official(true)] - public int Areas { get; set; } - - /// HELPFILES: Current number of help files. - [Name("HELPFILES"), Official(true)] - public int Helpfiles { get; set; } - - /// MOBILES: Current number of unique mobiles. - [Name("MOBILES"), Official(true)] - public int Mobiles { get; set; } - - /// OBJECTS: Current number of unique objects. - [Name("OBJECTS"), Official(true)] - public int Objects { get; set; } - - /// ROOMS: Current number of unique rooms, use 0 if roomless. - [Name("ROOMS"), Official(true)] - public int Rooms { get; set; } - - /// CLASSES: Number of player classes, use 0 if classless. - [Name("CLASSES"), Official(true)] - public int Classes { get; set; } - - /// LEVELS: Number of player levels, use 0 if level-less. - [Name("LEVELS"), Official(true)] - public int Levels { get; set; } - - /// RACES: Number of player races, use 0 if raceless. - [Name("RACES"), Official(true)] - public int Races { get; set; } - - /// SKILLS: Number of player skills, use 0 if skill-less. - [Name("SKILLS"), Official(true)] - public int Skills { get; set; } - - /// ANSI: Supports ANSI colors ? 1 or 0 - [Name("ANSI"), Official(true)] - public bool Ansi { get; set; } - - /// PUEBLO: Supports Pueblo ? 1 or 0 - [Name("PUEBLO"), Official(false)] - public bool Pueblo { get; set; } - - /// MSP: Supports MSP ? 1 or 0 - [Name("MSP"), Official(true)] - public bool MSP { get; set; } - - /// UTF-8: Supports UTF-8 ? 1 or 0 - [Name("UTF-8"), Official(true)] - public bool UTF_8 { get; set; } - - /// VT100: Supports VT100 interface ? 1 or 0 - [Name("VT100"), Official(true)] - public bool VT100 { get; set; } - - /// XTERM: 256 COLORS Supports xterm 256 colors ? 1 or 0 - [Name("XTERM 256 COLORS"), Official(true)] - public bool XTerm_256_Colors { get; set; } - - /// XTERM: TRUE COLORS Supports xterm 24 bit colors ? 1 or 0 - [Name("XTERM TRUE COLORS"), Official(true)] - public bool XTerm_True_Colors { get; set; } - - /// PAY: TO PLAY Pay to play ? 1 or 0 - [Name("PAY TO PLAY"), Official(true)] - public bool Pay_To_Play { get; set; } - - /// PAY: FOR PERKS Pay for perks ? 1 or 0 - [Name("PAY FOR PERKS"), Official(true)] - public bool Pay_For_Perks { get; set; } - - /// HIRING: BUILDERS Game is hiring builders ? 1 or 0 - [Name("HIRING BUILDERS"), Official(true)] - public bool Hiring_Builders { get; set; } - - /// HIRING: CODERS Game is hiring coders ? 1 or 0 - [Name("HIRING CODERS"), Official(true)] - public bool Hiring_Coders { get; set; } - - /// Additional information. - /// Dictionary Key serves as the MSSP Key - /// Dictionary Value.obj serves as the MSSP Value - /// Dictionary Value.type serves as the MSSP Value Type for unboxing - /// We only support IEnumerable, bool, int and string at this time - [Official(false)] - public Dictionary Extended { get; set; } = new (); - } + /// NAME: Name of the MUD. + [Name("NAME"), Official(true)] + public string Name { get; set; } + + /// PLAYERS: Current number of logged in players. + [Name("PLAYERS"), Official(true)] + public int Players { get; set; } + + /// UPTIME: Unix time value of the startup time of the MUD. + [Name("UPTIME"), Official(true)] + public int Uptime { get; set; } + + /// CODEBASE: Name of the codebase, eg Merc 2.1. You can report multiple codebases using the array format, make sure to report the current codebase last. + [Name("CODEBASE"), Official(true)] + public IEnumerable Codebase { get; set; } + + /// CONTACT: Email address for contacting the MUD. + [Name("CONTACT"), Official(true)] + public string Contact { get; set; } + + /// CRAWL DELAY: Preferred minimum number of hours between crawls. Send -1 to use the crawler's default. + [Name("CRAWL DELAY"), Official(true)] + public int Crawl_Delay { get; set; } + + /// CREATED: Year the MUD was created. + [Name("CREATED"), Official(true)] + public string Created { get; set; } + + /// HOSTNAME: Current or new hostname. + [Name("HOSTNAME"), Official(true)] + public string Hostname { get; set; } + + /// ICON: URL to a square image in bmp, png, jpg, or gif format. The icon should be equal or larger than 64x64 pixels, with a filesize no larger than 256KB. + [Name("ICON"), Official(true)] + public string Icon { get; set; } + + /// IP: Current or new IP address. + [Name("IP"), Official(true)] + public string IP { get; set; } + + /// IPV6: Current or new IPv6 address. + [Name("IPV6"), Official(true)] + public string IPV6 { get; set; } + + /// LANGUAGE: English name of the language used, eg German or English + [Name("LANGUAGE"), Official(true)] + public string Language { get; set; } + + /// LOCATION: English short name of the country where the server is located, using ISO 3166. + [Name("LOCATION"), Official(true)] + public string Location { get; set; } + + /// MINIMUM AGE: Current minimum age requirement, omit if not applicable. + [Name("MINIMUM AGE"), Official(true)] + public string Minimum_Age { get; set; } + + /// PORT: Current or new port number. Can be used multiple times, most important port last. + [Name("PORT"), Official(true)] + public int Port { get; set; } + + /// REFERRAL: A list of other MSSP enabled MUDs for the crawler to check using the host port format and array notation. Adding referrals is important to make MSSP decentralized. Make sure to separate the host and port with a space rather than : because IPv6 addresses contain colons. + [Name("REFERRAL"), Official(true)] + public IEnumerable Referral { get; set; } + + /// The port number for a SSL (Secure Socket Layer) encrypted connection. + [Name("SSL"), Official(true)] + public string Ssl { get; set; } + + /// WEBSITE: URL to MUD website, this should include the http:// or https:// prefix. + [Name("WEBSITE"), Official(true)] + public string Website { get; set; } + + /// FAMILY: AberMUD, CoffeeMUD, DikuMUD, Evennia, LPMud, MajorMUD, MOO, Mordor, SocketMud, TinyMUD, TinyMUCK, TinyMUSH, Custom. + /// Report Custom unless it's a well established family. + /// + /// You can report multiple generic codebases using the array format, make sure to report the most distant codebase (aka the family) last. + /// + /// Check the MUD family tree for naming and capitalization. + [Name("FAMILY"), Official(true)] + public IEnumerable Family { get; set; } + + /// GENRE: Adult, Fantasy, Historical, Horror, Modern, Mystery, None, Romance, Science Fiction, Spiritual + [Name("GENRE"), Official(true)] + public string Genre { get; set; } + + /// GAMEPLAY: Adventure, Educational, Hack and Slash, None, Player versus Player, Player versus Environment, Questing, Roleplaying, Simulation, Social, Strategy + [Name("GAMEPLAY"), Official(true)] + public IEnumerable Gameplay { get; set; } + + + /// STATUS: Alpha, Closed Beta, Open Beta, Live + [Name("STATUS"), Official(true)] + public string Status { get; set; } + + /// GAMESYSTEM: D&D, d20 System, World of Darkness, Etc. Use Custom if using a custom game system. Use None if not available. + [Name("GAMESYSTEM"), Official(true)] + public string Gamesystem { get; set; } + + /// INTERMUD: AberChat, I3, IMC2, MudNet, Etc. Can be used multiple times if you support several protocols, most important protocol last. Leave empty or omit if no Intermud protocol is supported. + [Name("INTERMUD"), Official(true)] + public IEnumerable Intermud { get; set; } + + /// SUBGENRE: Alternate History, Anime, Cyberpunk, Detective, Discworld, Dragonlance, Christian Fiction, Classical Fantasy, + /// Crime, Dark Fantasy, Epic Fantasy, Erotic, Exploration, Forgotten Realms, Frankenstein, Gothic, High Fantasy, + /// Magical Realism, Medieval Fantasy, Multiverse, Paranormal, Post-Apocalyptic, Military Science Fiction, + /// Mythology, Pulp, Star Wars, Steampunk, Suspense, Time Travel, Weird Fiction, World War II, Urban Fantasy, Etc. + /// + /// Use None if not applicable. + [Name("SUBGENRE"), Official(true)] + public string Subgenre { get; set; } + + /// AREAS: Current number of areas. + [Name("AREAS"), Official(true)] + public int Areas { get; set; } + + /// HELPFILES: Current number of help files. + [Name("HELPFILES"), Official(true)] + public int Helpfiles { get; set; } + + /// MOBILES: Current number of unique mobiles. + [Name("MOBILES"), Official(true)] + public int Mobiles { get; set; } + + /// OBJECTS: Current number of unique objects. + [Name("OBJECTS"), Official(true)] + public int Objects { get; set; } + + /// ROOMS: Current number of unique rooms, use 0 if roomless. + [Name("ROOMS"), Official(true)] + public int Rooms { get; set; } + + /// CLASSES: Number of player classes, use 0 if classless. + [Name("CLASSES"), Official(true)] + public int Classes { get; set; } + + /// LEVELS: Number of player levels, use 0 if level-less. + [Name("LEVELS"), Official(true)] + public int Levels { get; set; } + + /// RACES: Number of player races, use 0 if raceless. + [Name("RACES"), Official(true)] + public int Races { get; set; } + + /// SKILLS: Number of player skills, use 0 if skill-less. + [Name("SKILLS"), Official(true)] + public int Skills { get; set; } + + /// ANSI: Supports ANSI colors ? 1 or 0 + [Name("ANSI"), Official(true)] + public bool Ansi { get; set; } + + /// PUEBLO: Supports Pueblo ? 1 or 0 + [Name("PUEBLO"), Official(false)] + public bool Pueblo { get; set; } + + /// MSP: Supports MSP ? 1 or 0 + [Name("MSP"), Official(true)] + public bool MSP { get; set; } + + /// UTF-8: Supports UTF-8 ? 1 or 0 + [Name("UTF-8"), Official(true)] + public bool UTF_8 { get; set; } + + /// VT100: Supports VT100 interface ? 1 or 0 + [Name("VT100"), Official(true)] + public bool VT100 { get; set; } + + /// XTERM: 256 COLORS Supports xterm 256 colors ? 1 or 0 + [Name("XTERM 256 COLORS"), Official(true)] + public bool XTerm_256_Colors { get; set; } + + /// XTERM: TRUE COLORS Supports xterm 24 bit colors ? 1 or 0 + [Name("XTERM TRUE COLORS"), Official(true)] + public bool XTerm_True_Colors { get; set; } + + /// PAY: TO PLAY Pay to play ? 1 or 0 + [Name("PAY TO PLAY"), Official(true)] + public bool Pay_To_Play { get; set; } + + /// PAY: FOR PERKS Pay for perks ? 1 or 0 + [Name("PAY FOR PERKS"), Official(true)] + public bool Pay_For_Perks { get; set; } + + /// HIRING: BUILDERS Game is hiring builders ? 1 or 0 + [Name("HIRING BUILDERS"), Official(true)] + public bool Hiring_Builders { get; set; } + + /// HIRING: CODERS Game is hiring coders ? 1 or 0 + [Name("HIRING CODERS"), Official(true)] + public bool Hiring_Coders { get; set; } + + /// Additional information. + /// Dictionary Key serves as the MSSP Key + /// Dictionary Value.obj serves as the MSSP Value + /// Dictionary Value.type serves as the MSSP Value Type for unboxing + /// We only support IEnumerable, bool, int and string at this time + [Official(false)] + public Dictionary Extended { get; set; } = []; } diff --git a/TelnetNegotiationCore/Models/State.cs b/TelnetNegotiationCore/Models/State.cs index 5574a57..1d11de6 100644 --- a/TelnetNegotiationCore/Models/State.cs +++ b/TelnetNegotiationCore/Models/State.cs @@ -1,112 +1,111 @@ -namespace TelnetNegotiationCore.Models +namespace TelnetNegotiationCore.Models; + +public enum State : sbyte { - public enum State : sbyte - { - #region Standard Negotiation - Accepting, - ReadingCharacters, - StartNegotiation, - EndNegotiation, - SubNegotiation, - EndSubNegotiation, - DoNothing, - Do, - Dont, - Willing, - Refusing, - #endregion Standard Negotiation - #region MSSP Negotiation - DoMSSP, - DontMSSP, - WontMSSP, - WillMSSP, - AlmostNegotiatingMSSP, - EvaluatingMSSPVar, - EvaluatingMSSPVal, - EscapingMSSPVar, - EscapingMSSPVal, - CompletingMSSP, - #endregion MSSP Negotiation - #region Window Size Negotiation - WillDoNAWS, - WontDoNAWS, - NegotiatingNAWS, - EvaluatingNAWS, - EscapingNAWSValue, - CompletingNAWS, - DontNAWS, - DoNAWS, - #endregion Window Size Negotiation - #region Charset Negotation - WillDoCharset, - WontDoCharset, - AlmostNegotiatingCharset, - NegotiatingCharset, - EvaluatingCharset, - EscapingCharsetValue, - CompletingCharset, - Act, - DoCharset, - DontCharset, - EndingCharsetSubnegotiation, - NegotiatingAcceptedCharset, - EvaluatingAcceptedCharsetValue, - EscapingAcceptedCharsetValue, - CompletingAcceptedCharset, - #endregion Charset Negotation - #region Terminal Type Negotiation - WillDoTType, - WontDoTType, - DoTType, - DontTType, - EndingTerminalTypeNegotiation, - AlmostNegotiatingTerminalType, - NegotiatingTerminalType, - EvaluatingTerminalType, - EscapingTerminalTypeValue, - CompletingTerminalType, - #endregion Terminal Type Negotiation - #region Safe Negotiation - BadWilling, - BadRefusing, - BadDo, - BadDont, - BadSubNegotiation, - BadSubNegotiationEscaping, - BadSubNegotiationEvaluating, - BadSubNegotiationCompleting, - #endregion - #region End of Record Negotiation - DoEOR, - DontEOR, - WontEOR, - WillEOR, - Prompting, - #endregion End of Record Negotiation - #region GMCP Negotiation - DoGMCP, - DontGMCP, - WontGMCP, - WillGMCP, - AlmostNegotiatingGMCP, - EvaluatingGMCPValue, - EscapingGMCPValue, - CompletingGMCPValue, - #endregion GMCP Negotiation - #region MSDP Negotiation - DontMSDP, - DoMSDP, - WillMSDP, - WontMSDP, - NegotiatingMSDP, - EvaluatingMSDP, - CompletingMSDP, - AlmostNegotiatingMSDP, - EscapingMSDP, - DoSUPPRESSGOAHEAD, - DontSUPPRESSGOAHEAD, - WillSUPPRESSGOAHEAD, - WontSUPPRESSGOAHEAD - #endregion MSDP Negotiation - } + #region Standard Negotiation + Accepting, + ReadingCharacters, + StartNegotiation, + EndNegotiation, + SubNegotiation, + EndSubNegotiation, + DoNothing, + Do, + Dont, + Willing, + Refusing, + #endregion Standard Negotiation + #region MSSP Negotiation + DoMSSP, + DontMSSP, + WontMSSP, + WillMSSP, + AlmostNegotiatingMSSP, + EvaluatingMSSPVar, + EvaluatingMSSPVal, + EscapingMSSPVar, + EscapingMSSPVal, + CompletingMSSP, + #endregion MSSP Negotiation + #region Window Size Negotiation + WillDoNAWS, + WontDoNAWS, + NegotiatingNAWS, + EvaluatingNAWS, + EscapingNAWSValue, + CompletingNAWS, + DontNAWS, + DoNAWS, + #endregion Window Size Negotiation + #region Charset Negotation + WillDoCharset, + WontDoCharset, + AlmostNegotiatingCharset, + NegotiatingCharset, + EvaluatingCharset, + EscapingCharsetValue, + CompletingCharset, + Act, + DoCharset, + DontCharset, + EndingCharsetSubnegotiation, + NegotiatingAcceptedCharset, + EvaluatingAcceptedCharsetValue, + EscapingAcceptedCharsetValue, + CompletingAcceptedCharset, + #endregion Charset Negotation + #region Terminal Type Negotiation + WillDoTType, + WontDoTType, + DoTType, + DontTType, + EndingTerminalTypeNegotiation, + AlmostNegotiatingTerminalType, + NegotiatingTerminalType, + EvaluatingTerminalType, + EscapingTerminalTypeValue, + CompletingTerminalType, + #endregion Terminal Type Negotiation + #region Safe Negotiation + BadWilling, + BadRefusing, + BadDo, + BadDont, + BadSubNegotiation, + BadSubNegotiationEscaping, + BadSubNegotiationEvaluating, + BadSubNegotiationCompleting, + #endregion + #region End of Record Negotiation + DoEOR, + DontEOR, + WontEOR, + WillEOR, + Prompting, + #endregion End of Record Negotiation + #region GMCP Negotiation + DoGMCP, + DontGMCP, + WontGMCP, + WillGMCP, + AlmostNegotiatingGMCP, + EvaluatingGMCPValue, + EscapingGMCPValue, + CompletingGMCPValue, + #endregion GMCP Negotiation + #region MSDP Negotiation + DontMSDP, + DoMSDP, + WillMSDP, + WontMSDP, + NegotiatingMSDP, + EvaluatingMSDP, + CompletingMSDP, + AlmostNegotiatingMSDP, + EscapingMSDP, + DoSUPPRESSGOAHEAD, + DontSUPPRESSGOAHEAD, + WillSUPPRESSGOAHEAD, + WontSUPPRESSGOAHEAD + #endregion MSDP Negotiation } diff --git a/TelnetNegotiationCore/Models/Trigger.cs b/TelnetNegotiationCore/Models/Trigger.cs index 8d5e4f4..6d9a32c 100644 --- a/TelnetNegotiationCore/Models/Trigger.cs +++ b/TelnetNegotiationCore/Models/Trigger.cs @@ -5,354 +5,354 @@ using System.Collections.Immutable; using OneOf; -namespace TelnetNegotiationCore.Models +namespace TelnetNegotiationCore.Models; + +/// +/// Helper class to create TriggerWithParameter objects. +/// +public class ParameterizedTriggers { + private readonly Dictionary.TriggerWithParameters>> _cache = []; + /// - /// Helper class to create TriggerWithParameter objects. + /// Returns a (cached) Parameterized Trigger. /// - public class ParameterizedTriggers + /// State Machine + /// The Trigger + /// One of Byte or Trigger, allowing both the 255 byte range excluding standard triggers, and Triggers above the number + public StateMachine.TriggerWithParameters> ParameterizedTrigger(StateMachine stm, Trigger t) { - private readonly Dictionary.TriggerWithParameters>> _cache = []; - /// - /// Returns a (cached) Parameterized Trigger. - /// - /// State Machine - /// The Trigger - /// One of Byte or Trigger, allowing both the 255 byte range excluding standard triggers, and Triggers above the number - public StateMachine.TriggerWithParameters> ParameterizedTrigger(StateMachine stm, Trigger t) + if (_cache.TryGetValue(t, out StateMachine.TriggerWithParameters> value)) { - - if (_cache.TryGetValue(t, out StateMachine.TriggerWithParameters> value)) - { - return value; - } - _cache.Add(t, stm.SetTriggerParameters>(t)); - return _cache[t]; + return value; } + _cache.Add(t, stm.SetTriggerParameters>(t)); + return _cache[t]; } +} - /// - /// A helper class to create state transitions for a list of triggers. - /// - public static class TriggerHelper - { - private static readonly ImmutableHashSet AllTriggers = ImmutableHashSet.Empty.Union(((IEnumerable)Enum.GetValues(typeof(Trigger))).Distinct()); - - public static void ForAllTriggers(Action f) - { - foreach(var trigger in AllTriggers) f(trigger); - } +/// +/// A helper class to create state transitions for a list of triggers. +/// +public static class TriggerHelper +{ + private static readonly ImmutableHashSet AllTriggers = ImmutableHashSet.Empty.Union(((IEnumerable)Enum.GetValues(typeof(Trigger))).Distinct()); - public static void ForAllTriggersExcept(IEnumerable except, Action f) - { - foreach (var trigger in AllTriggers.Except(except)) f(trigger); - } + public static void ForAllTriggers(Action f) + { + foreach(var trigger in AllTriggers) f(trigger); + } - public static void ForAllTriggersButIAC(Action f) - => ForAllTriggersExcept([Trigger.IAC], f); + public static void ForAllTriggersExcept(IEnumerable except, Action f) + { + foreach (var trigger in AllTriggers.Except(except)) f(trigger); } + public static void ForAllTriggersButIAC(Action f) + => ForAllTriggersExcept([Trigger.IAC], f); +} + #pragma warning disable CA1069 // Enums values should not be duplicated - public enum Trigger : short - { - /// - /// Sub-negotiation IS command. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - IS = 0, - /// - /// Sub-negotiation SEND command - /// ECHO negotiation (Unsupported) - /// Sub-negotiation MSSP_VAR - /// Sub-negotiation MSDP_VAR - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// RFC 857: http://www.faqs.org/rfcs/rfc857.html - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html - /// MSSP: https://tintin.mudhalla.net/protocols/mssp/ - /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ - /// - ECHO = 1, - MSSP_VAR = 1, - MSDP_VAR = 1, - SEND = 1, - REQUEST = 1, - /// - /// Sub-negotiation ACCEPTED command. - /// Sub-negotiation MSSP_VAL - /// Sub-negotiation MSDP_VAL - /// - /// - /// MSSP: https://tintin.mudhalla.net/protocols/mssp/ - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html - /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ - /// - MSSP_VAL = 2, - MSDP_VAL = 2, - ACCEPTED = 2, - /// - /// Sub-negotiation REJECTED command. - /// Suppress Go Ahead - /// Sub-negotiation MSDP_TABLE_OPEN - /// - /// - /// RFC 858: http://www.faqs.org/rfcs/rfc858.html - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html - /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ - /// - SUPPRESSGOAHEAD = 3, - REJECTED = 3, - MSDP_TABLE_OPEN = 3, - /// - /// Sub-negotiation TTABLE-IS command. (Unsupported) - /// Sub-negotiation MSDP_TABLE_CLOSE - /// - /// - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html - /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ - /// - TTABLE_IS = 4, - MSDP_TABLE_CLOSE = 4, - /// - /// Sub-negotiation TTABLE_REJECTED command. (Unsupported) - /// Sub-negotiation MSDP_ARRAY_OPEN - /// - /// - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html - /// - TTABLE_REJECTED = 5, - MSDP_ARRAY_OPEN = 5, - /// - /// Sub-negotiation TTABLE_ACK command. (Unsupported) - /// Sub-negotiation MSDP_ARRAY_CLOSE - /// - /// - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html - /// - TTABLE_ACK = 6, - MSDP_ARRAY_CLOSE = 6, - /// - /// Sub-negotiation TTABLE_NAK command. (Unsupported) - /// - /// - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html - /// - TTABLE_NAK = 7, - /// - /// Newline Indicator - /// - /// - /// We treat this as 'now act' - /// - NEWLINE = 10, - /// - /// Carriage Return - /// - /// - /// We ignore this, due to its relationship to Newline Indication. - /// - CARRIAGERETURN = 13, - /// - /// Terminal Type - /// - /// - /// RFC 1091: http://www.faqs.org/rfcs/rfc1091.html - /// MTTS: https://tintin.mudhalla.net/protocols/mtts/ - /// - TTYPE = 24, - /// - /// End of Record Negotiation - /// - /// - /// EOR: https://tintin.mudhalla.net/protocols/eor/ - /// RFC 885: http://www.faqs.org/rfcs/rfc885.html - /// - /// - TELOPT_EOR = 25, - /// - /// Window size option. - /// - /// - /// RFC 1073: http://www.faqs.org/rfcs/rfc1073.html - /// - NAWS = 31, - /// - /// Terminal Speed option (Unsupported) - /// - /// - /// RFC 1079: http://www.faqs.org/rfcs/rfc1079.html - /// - TSPEED = 32, - /// - /// Toggle Flow Control (Unsupported) - /// - /// - /// RFC 1372: http://www.faqs.org/rfcs/rfc1372.html - /// - FLOWCONTROL = 33, - /// - /// Linemode option (Unsupported) - /// - /// - /// RFC 1184: http://www.faqs.org/rfcs/rfc1184.html - /// - LINEMODE = 34, - /// - /// X-Display Location (Unsupported) - /// - /// - /// RFC 1096: http://www.faqs.org/rfcs/rfc1096.html - /// - XDISPLOC = 35, - /// - /// Environment (Unsupported) - /// - /// - /// RFC 1408: http://www.faqs.org/rfcs/rfc1408.html - /// - ENVIRON = 36, - /// - /// Authentication (Unsupported) - /// - /// - /// RFC 2941: http://www.faqs.org/rfcs/rfc2941.html - /// - AUTHENTICATION = 37, - /// - /// Encrypt (Unsupported) - /// - /// - /// RFC 2946: http://www.faqs.org/rfcs/rfc2946.html - /// - ENCRYPT = 38, - /// - /// New Environment (Unsupported) (Support Planned) - /// - /// - /// MNES: https://tintin.mudhalla.net/protocols/mnes/ - /// RFC 1572: http://www.faqs.org/rfcs/rfc1572.html - /// - NEWENVIRON = 39, - /// - /// Charset option - /// - /// - /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html - /// - CHARSET = 42, - /// - /// Mud Server Data Protocol (Unsupported) (Support Planned) - /// - /// - /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ - /// - MSDP = 69, - /// - /// Mud Server Status Protocol - /// - /// - /// MSSP: https://tintin.mudhalla.net/protocols/mssp/ - /// - MSSP = 70, - /// - /// Mud Client Compression Protocol (Unsupported) - /// - /// - /// MCCP: https://tintin.mudhalla.net/protocols/mccp - /// RFC 1950: https://tintin.mudhalla.net/rfc/rfc1950/ - /// - MCCP2 = 86, - MCCP3 = 87, - /// Generic Mud Communication Protocol - /// - /// - /// GMCP: https://tintin.mudhalla.net/protocols/gmcp/ - /// - GMCP = 201, - /// - /// End of Record - /// - /// - /// EOR: https://tintin.mudhalla.net/protocols/eor/ - /// RFC 885: http://www.faqs.org/rfcs/rfc885.html - /// - /// - EOR = 239, - /// - /// The end of sub-negotiation options. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - SE = 240, - /// - /// No operation. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - NOP = 241, - /// - /// Go ahead. Used, under certain circumstances, to tell the other end that it can transmit. - /// - /// - /// RFC 854: http://www.faqs.org/rfcs/rfc854.html - /// - GA = 249, - /// - /// The start of sub-negotiation options. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - SB = 250, - /// - /// Confirm willingness to negotiate. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - WILL = 251, - /// - /// Confirm unwillingness to negotiate. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - WONT = 252, - /// - /// Indicate willingness to negotiate. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - DO = 253, - /// - /// Indicate unwillingness to negotiate. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - DONT = 254, - /// - /// Marks the start of a negotiation sequence. - /// - /// - /// RFC 855: http://www.faqs.org/rfcs/rfc855.html - /// - IAC = 255, - /// - /// A generic trigger, outside of what a byte can contain, to indicate generic progression. - /// - ReadNextCharacter = 256, - /// - /// A generic bad state trigger, outside of what a byte can contain, to indicate a generic bad state transition. - /// - Error = 257 - } -#pragma warning restore CA1069 // Enums values should not be duplicated +public enum Trigger : short +{ + /// + /// Sub-negotiation IS command. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + IS = 0, + /// + /// Sub-negotiation SEND command + /// ECHO negotiation (Unsupported) + /// Sub-negotiation MSSP_VAR + /// Sub-negotiation MSDP_VAR + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// RFC 857: http://www.faqs.org/rfcs/rfc857.html + /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// MSSP: https://tintin.mudhalla.net/protocols/mssp/ + /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ + /// + ECHO = 1, + MSSP_VAR = 1, + MSDP_VAR = 1, + SEND = 1, + REQUEST = 1, + /// + /// Sub-negotiation ACCEPTED command. + /// Sub-negotiation MSSP_VAL + /// Sub-negotiation MSDP_VAL + /// + /// + /// MSSP: https://tintin.mudhalla.net/protocols/mssp/ + /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ + /// + MSSP_VAL = 2, + MSDP_VAL = 2, + ACCEPTED = 2, + /// + /// Sub-negotiation REJECTED command. + /// Suppress Go Ahead + /// Sub-negotiation MSDP_TABLE_OPEN + /// + /// + /// RFC 858: http://www.faqs.org/rfcs/rfc858.html + /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ + /// + SUPPRESSGOAHEAD = 3, + REJECTED = 3, + MSDP_TABLE_OPEN = 3, + /// + /// Sub-negotiation TTABLE-IS command. (Unsupported) + /// Sub-negotiation MSDP_TABLE_CLOSE + /// + /// + /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ + /// + TTABLE_IS = 4, + MSDP_TABLE_CLOSE = 4, + /// + /// Sub-negotiation TTABLE_REJECTED command. (Unsupported) + /// Sub-negotiation MSDP_ARRAY_OPEN + /// + /// + /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// + TTABLE_REJECTED = 5, + MSDP_ARRAY_OPEN = 5, + /// + /// Sub-negotiation TTABLE_ACK command. (Unsupported) + /// Sub-negotiation MSDP_ARRAY_CLOSE + /// + /// + /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// + TTABLE_ACK = 6, + MSDP_ARRAY_CLOSE = 6, + /// + /// Sub-negotiation TTABLE_NAK command. (Unsupported) + /// + /// + /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// + TTABLE_NAK = 7, + /// + /// Newline Indicator + /// + /// + /// We treat this as 'now act' + /// + NEWLINE = 10, + /// + /// Carriage Return + /// + /// + /// We ignore this, due to its relationship to Newline Indication. + /// + CARRIAGERETURN = 13, + /// + /// Terminal Type + /// + /// + /// RFC 1091: http://www.faqs.org/rfcs/rfc1091.html + /// MTTS: https://tintin.mudhalla.net/protocols/mtts/ + /// + TTYPE = 24, + /// + /// End of Record Negotiation + /// + /// + /// EOR: https://tintin.mudhalla.net/protocols/eor/ + /// RFC 885: http://www.faqs.org/rfcs/rfc885.html + /// + /// + TELOPT_EOR = 25, + /// + /// Window size option. + /// + /// + /// RFC 1073: http://www.faqs.org/rfcs/rfc1073.html + /// + NAWS = 31, + /// + /// Terminal Speed option (Unsupported) + /// + /// + /// RFC 1079: http://www.faqs.org/rfcs/rfc1079.html + /// + TSPEED = 32, + /// + /// Toggle Flow Control (Unsupported) + /// + /// + /// RFC 1372: http://www.faqs.org/rfcs/rfc1372.html + /// + FLOWCONTROL = 33, + /// + /// Linemode option (Unsupported) + /// + /// + /// RFC 1184: http://www.faqs.org/rfcs/rfc1184.html + /// + LINEMODE = 34, + /// + /// X-Display Location (Unsupported) + /// + /// + /// RFC 1096: http://www.faqs.org/rfcs/rfc1096.html + /// + XDISPLOC = 35, + /// + /// Environment (Unsupported) + /// + /// + /// RFC 1408: http://www.faqs.org/rfcs/rfc1408.html + /// + ENVIRON = 36, + /// + /// Authentication (Unsupported) + /// + /// + /// RFC 2941: http://www.faqs.org/rfcs/rfc2941.html + /// + AUTHENTICATION = 37, + /// + /// Encrypt (Unsupported) + /// + /// + /// RFC 2946: http://www.faqs.org/rfcs/rfc2946.html + /// + ENCRYPT = 38, + /// + /// New Environment (Unsupported) (Support Planned) + /// + /// + /// MNES: https://tintin.mudhalla.net/protocols/mnes/ + /// RFC 1572: http://www.faqs.org/rfcs/rfc1572.html + /// + NEWENVIRON = 39, + /// + /// Charset option + /// + /// + /// RFC 2066: http://www.faqs.org/rfcs/rfc2066.html + /// + CHARSET = 42, + /// + /// Mud Server Data Protocol (Unsupported) (Support Planned) + /// + /// + /// MSDP: https://tintin.mudhalla.net/protocols/msdp/ + /// + MSDP = 69, + /// + /// Mud Server Status Protocol + /// + /// + /// MSSP: https://tintin.mudhalla.net/protocols/mssp/ + /// + MSSP = 70, + /// + /// Mud Client Compression Protocol (Unsupported) + /// + /// + /// MCCP: https://tintin.mudhalla.net/protocols/mccp + /// RFC 1950: https://tintin.mudhalla.net/rfc/rfc1950/ + /// + MCCP2 = 86, + MCCP3 = 87, + /// Generic Mud Communication Protocol + /// + /// + /// GMCP: https://tintin.mudhalla.net/protocols/gmcp/ + /// + GMCP = 201, + /// + /// End of Record + /// + /// + /// EOR: https://tintin.mudhalla.net/protocols/eor/ + /// RFC 885: http://www.faqs.org/rfcs/rfc885.html + /// + /// + EOR = 239, + /// + /// The end of sub-negotiation options. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + SE = 240, + /// + /// No operation. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + NOP = 241, + /// + /// Go ahead. Used, under certain circumstances, to tell the other end that it can transmit. + /// + /// + /// RFC 854: http://www.faqs.org/rfcs/rfc854.html + /// + GA = 249, + /// + /// The start of sub-negotiation options. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + SB = 250, + /// + /// Confirm willingness to negotiate. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + WILL = 251, + /// + /// Confirm unwillingness to negotiate. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + WONT = 252, + /// + /// Indicate willingness to negotiate. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + DO = 253, + /// + /// Indicate unwillingness to negotiate. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + DONT = 254, + /// + /// Marks the start of a negotiation sequence. + /// + /// + /// RFC 855: http://www.faqs.org/rfcs/rfc855.html + /// + IAC = 255, + /// + /// A generic trigger, outside of what a byte can contain, to indicate generic progression. + /// + ReadNextCharacter = 256, + /// + /// A generic bad state trigger, outside of what a byte can contain, to indicate a generic bad state transition. + /// + Error = 257 } +#pragma warning restore CA1069 // Enums values should not be duplicated + diff --git a/TelnetNegotiationCore/TelnetHelpers.cs b/TelnetNegotiationCore/TelnetHelpers.cs index c0c2495..fb4e114 100644 --- a/TelnetNegotiationCore/TelnetHelpers.cs +++ b/TelnetNegotiationCore/TelnetHelpers.cs @@ -1,9 +1,8 @@ using System.Text; -namespace TelnetNegotiationCore.Interpreters +namespace TelnetNegotiationCore.Interpreters; + +public partial class TelnetInterpreter { - public partial class TelnetInterpreter - { - private readonly ASCIIEncoding ascii = new(); - } + private readonly ASCIIEncoding ascii = new(); } diff --git a/TelnetNegotiationCore/TelnetNegotiationCore.csproj b/TelnetNegotiationCore/TelnetNegotiationCore.csproj index 5c85fdb..222fd10 100644 --- a/TelnetNegotiationCore/TelnetNegotiationCore.csproj +++ b/TelnetNegotiationCore/TelnetNegotiationCore.csproj @@ -16,7 +16,6 @@ git telnet LICENSE - $([System.IO.File]::ReadAllText("CHANGELOG.md")) Copyright © TelnetNegotiationCore Contributors 2024-$([System.DateTime]::Now.ToString(yyyy))