diff --git a/MBBSEmu.Tests/ExportedModules/Majorbbs/MajorbbsTestBase.cs b/MBBSEmu.Tests/ExportedModules/Majorbbs/MajorbbsTestBase.cs index 0abb6887..f0824dce 100644 --- a/MBBSEmu.Tests/ExportedModules/Majorbbs/MajorbbsTestBase.cs +++ b/MBBSEmu.Tests/ExportedModules/Majorbbs/MajorbbsTestBase.cs @@ -4,6 +4,7 @@ using MBBSEmu.IO; using MBBSEmu.Memory; using MBBSEmu.Module; +using MBBSEmu.Session; using Microsoft.Extensions.Configuration; using NLog; using System; @@ -38,7 +39,7 @@ protected MajorbbsTestBase() _serviceResolver.GetService(), _serviceResolver.GetService(), mbbsModule, - new PointerDictionary()); + new PointerDictionary()); mbbsEmuCpuCore.Reset(mbbsEmuMemoryCore, mbbsEmuCpuRegisters, MajorbbsFunctionDelegate); } @@ -63,7 +64,7 @@ protected void Reset() _serviceResolver.GetService(), _serviceResolver.GetService(), mbbsModule, - new PointerDictionary()); + new PointerDictionary()); } /// diff --git a/MBBSEmu.Tests/Session/Telnet/IacFilter_Tests.cs b/MBBSEmu.Tests/Session/Telnet/IacFilter_Tests.cs new file mode 100644 index 00000000..109e1caf --- /dev/null +++ b/MBBSEmu.Tests/Session/Telnet/IacFilter_Tests.cs @@ -0,0 +1,122 @@ +using MBBSEmu.DependencyInjection; +using MBBSEmu.Session.Telnet; +using NLog; +using System; +using System.IO; +using System.Text; +using Xunit; + +namespace MBBSEmu.Tests.Session.Telnet +{ + public class IacFilter_Tests + { + private readonly IacFilter iacFilter = new IacFilter(new ServiceResolver(ServiceResolver.GetTestDefaults()).GetService()); + + [Fact] + public void PassThroughNoIAC() + { + var str = "testing 1234"; + var bytes = Encoding.ASCII.GetBytes(str); + var (outBytes, len) = iacFilter.ProcessIncomingClientData(bytes, bytes.Length); + + Assert.Equal(bytes, new ReadOnlySpan(outBytes).Slice(0, len).ToArray()); + } + + [Fact] + public void PassThroughNoIACDoubleProcess() { + var str = "testing 1234"; + var bytes = Encoding.ASCII.GetBytes(str); + + var (outBytes, len) = iacFilter.ProcessIncomingClientData(bytes, bytes.Length); + + Assert.Equal(bytes, new ReadOnlySpan(outBytes).Slice(0, len).ToArray()); + + (outBytes, len) = iacFilter.ProcessIncomingClientData(bytes, bytes.Length); + + Assert.Equal(bytes, new ReadOnlySpan(outBytes).Slice(0, len).ToArray()); + } + + [Fact] + public void BasicTelnetStripping() + { + byte[] iac = { + 0xFF, 0xFB, 0x01, + 0xFF, 0xFC, 0x01, + 0xFF, 0xFD, 0x01, + 0xFF, 0xFE, 0x01, + }; + var expectedString = "This is a test of the emergency system"; + + var bytes = Concat( + Encoding.ASCII.GetBytes("This is a test"), + iac, + Encoding.ASCII.GetBytes(" of the emergency system"), + iac); + + var (outBytes, len) = iacFilter.ProcessIncomingClientData(bytes, bytes.Length); + + Assert.Equal(Encoding.ASCII.GetBytes(expectedString), new ReadOnlySpan(outBytes).Slice(0, len).ToArray()); + } + + [Fact] + public void BasicTelnetOptionsStripping() + { + byte[] iacWithOptions = { + 0xFF, 0xFB, 0x01, + 0xFF, 0xFA, 0x1F, 0x00, 0x50, 0x00, 0x18, 0xFF, 0xF0 + }; + var expectedString = "This is a test of the emergency system"; + + var bytes = Concat( + Encoding.ASCII.GetBytes("This is a test"), + iacWithOptions, + Encoding.ASCII.GetBytes(" of the emergency system"), + iacWithOptions); + + var (outBytes, len) = iacFilter.ProcessIncomingClientData(bytes, bytes.Length); + + Assert.Equal(Encoding.ASCII.GetBytes(expectedString), new ReadOnlySpan(outBytes).Slice(0, len).ToArray()); + } + + [Fact] + public void BasicTelnetStrippingOverPackets() { + var stream = new MemoryStream(); + byte[] start_iac = {0xFF}; + byte[] end_iac = {0xFB, 0x01}; + + var b = Concat(Encoding.ASCII.GetBytes("This is a test"), start_iac); + var (bytes, length) = iacFilter.ProcessIncomingClientData(b, b.Length); + stream.Write(bytes, 0, length); + + Assert.Equal( + Encoding.ASCII.GetBytes("This is a test"), + stream.ToArray()); + + b = Concat(end_iac, Encoding.ASCII.GetBytes(" of the emergency system")); + (bytes, length) = iacFilter.ProcessIncomingClientData(b, b.Length); + stream.Write(bytes, 0, length); + + Assert.Equal( + Encoding.ASCII.GetBytes("This is a test of the emergency system"), + stream.ToArray()); + } + + private static byte[] Concat(params byte[][] arrays) { + var length = 0; + foreach(var a in arrays) + { + length += a.Length; + } + + var ret = new byte[length]; + length = 0; + foreach(var a in arrays) + { + Array.Copy(a, 0, ret, length, a.Length); + length += a.Length; + } + + return ret; + } + } +} diff --git a/MBBSEmu/Session/SessionBase.cs b/MBBSEmu/Session/SessionBase.cs index 89cdd9b5..3a49b095 100644 --- a/MBBSEmu/Session/SessionBase.cs +++ b/MBBSEmu/Session/SessionBase.cs @@ -4,7 +4,6 @@ using MBBSEmu.Module; using MBBSEmu.Server; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; diff --git a/MBBSEmu/Session/Telnet/EnumIacOptions.cs b/MBBSEmu/Session/Telnet/EnumIacOptions.cs index 1ac18091..df1b3202 100644 --- a/MBBSEmu/Session/Telnet/EnumIacOptions.cs +++ b/MBBSEmu/Session/Telnet/EnumIacOptions.cs @@ -2,20 +2,64 @@ { /// /// Enumerator for IAC Options that MBBSEmu will handle + /// + /// https://www.iana.org/assignments/telnet-options/telnet-options.xhtml /// public enum EnumIacOptions : byte { BinaryTransmission = 0, Echo = 1, + Reconnection = 2, SuppressGoAhead = 3, + ApproximateMessageSizeNegotiation = 4, Status = 5, TimingMark = 6, + RemoteControlledTransAndEcho = 7, + OutputLineWidth = 8, + OutputPageSize = 9, + OutputCarrigeReturnDisposition = 10, + OutputHorizontalTabStops = 11, + OutputHorizontalTabDisposition = 12, + OutputFormfeedDisposition = 13, + OutputVerticalTabStops = 14, + OutputVerticalTabDisposition = 15, + OutputLineFeedDisposition = 16, + ExtendedAscii = 17, + Logout = 18, + ByteMacro = 19, + DataEntryTerminal = 20, + SUPDUP = 21, + SUPDUPOutput = 22, + SendLocation = 23, TerminalType = 24, + EndOfRecord = 25, + TACACSUserIdentification = 26, + OutputMarking = 27, + TerminalLocationNumber = 28, + Telnet3270Regime = 29, + X_3Pad = 30, NegotiateAboutWindowSize = 31, TerminalSpeed = 32, RemoteFlowControl = 33, Linemode = 34, + XDisplayLocation = 35, EnvironmentOption = 36, + AuthenticationOption = 37, + EncryptionOption = 38, + NewEnvironmentOption = 39, + TN3270E = 40, + XAUTH = 41, + CHARSET = 42, + TelnetRemoteSerialPort = 43, + ComPortControlOption = 44, + TelnetSuppressLocalEcho = 45, + TelnetStartTLS = 46, + KERMIT = 47, + SEND_URL = 48, + FORWARD_X = 49, + TELOPTPragmaLogon = 138, + TELOPT_SSPI_Logon = 139, + TELOPTPragmaHeartbeat = 140, None = 0xFF } } diff --git a/MBBSEmu/Session/Telnet/IacFilter.cs b/MBBSEmu/Session/Telnet/IacFilter.cs new file mode 100644 index 00000000..d22e0efa --- /dev/null +++ b/MBBSEmu/Session/Telnet/IacFilter.cs @@ -0,0 +1,108 @@ +using NLog; +using System.IO; +using System; + +namespace MBBSEmu.Session.Telnet +{ + /// + /// Parses incoming client Telnet data and invokes the IacVerbReceived + /// event when a verb is received. + /// + public class IacFilter + { + private readonly ILogger _logger; + + private const byte IAC = 0xFF; + + private const byte SB = 0xFA; + private const byte SE = 0xF0; + + private enum ParseState { + Normal, + FoundIAC, + IACCommand, + SBStart, + SBValue, + SBIAC + } + + // for jumbo frame size + private readonly MemoryStream _memoryStream = new MemoryStream(10000); + private ParseState _parseState = ParseState.Normal; + private EnumIacVerbs _currentVerb; + + public class IacVerbReceivedEventArgs : EventArgs + { + public EnumIacVerbs Verb { get; set; } + public EnumIacOptions Option { get; set; } + } + + public event EventHandler IacVerbReceived; + + public IacFilter(ILogger logger) + { + _logger = logger; + } + + public (byte[], int) ProcessIncomingClientData(byte[] clientData, int bytesReceived) + { + _memoryStream.SetLength(0); + + for (var i = 0; i < bytesReceived; ++i) + { + Process(clientData[i]); + } + + return (_memoryStream.GetBuffer(), (int)_memoryStream.Length); + } + + private void Process(byte b) + { + switch (_parseState) { + case ParseState.Normal when b == IAC: + _parseState = ParseState.FoundIAC; + break; + case ParseState.Normal: + _memoryStream.WriteByte(b); + break; + case ParseState.FoundIAC when b == SB: + _parseState = ParseState.SBStart; + break; + case ParseState.FoundIAC when b == (byte)EnumIacVerbs.WILL: + case ParseState.FoundIAC when b == (byte)EnumIacVerbs.WONT: + case ParseState.FoundIAC when b == (byte)EnumIacVerbs.DO: + case ParseState.FoundIAC when b == (byte)EnumIacVerbs.DONT: + _currentVerb = (EnumIacVerbs) b; + _parseState = ParseState.IACCommand; + break; + case ParseState.FoundIAC when b == IAC: + // special escape sequence + _memoryStream.WriteByte(b); + break; + case ParseState.FoundIAC: + _parseState = ParseState.Normal; + break; + case ParseState.IACCommand: + IacVerbReceived?.Invoke(this, new IacVerbReceivedEventArgs() + { + Verb = _currentVerb, + Option = (EnumIacOptions)b + }); + _parseState = ParseState.Normal; + break; + case ParseState.SBStart: + _parseState = ParseState.SBValue; + break; + case ParseState.SBValue when b == IAC: + _parseState = ParseState.SBIAC; + break; + case ParseState.SBIAC when b == SE: + _parseState = ParseState.Normal; + break; + case ParseState.SBIAC: + _parseState = ParseState.SBValue; + break; + } + } + } +} diff --git a/MBBSEmu/Session/Telnet/IacResponse.cs b/MBBSEmu/Session/Telnet/IacResponse.cs index fcd35290..2f484b6e 100644 --- a/MBBSEmu/Session/Telnet/IacResponse.cs +++ b/MBBSEmu/Session/Telnet/IacResponse.cs @@ -27,5 +27,23 @@ public byte[] ToArray() msOutput.WriteByte((byte)Option); return msOutput.ToArray(); } + + public override bool Equals(object obj) + { + return Equals(obj as IacResponse); + } + + public bool Equals(IacResponse other) + { + if (other == null) + return false; + + return (Verb == other.Verb && Option == other.Option); + } + + public override int GetHashCode() + { + return Verb.GetHashCode() ^ Option.GetHashCode(); + } } } diff --git a/MBBSEmu/Session/Telnet/TelnetSession.cs b/MBBSEmu/Session/Telnet/TelnetSession.cs index b807ff0c..277b4213 100644 --- a/MBBSEmu/Session/Telnet/TelnetSession.cs +++ b/MBBSEmu/Session/Telnet/TelnetSession.cs @@ -20,12 +20,34 @@ public class TelnetSession : SocketSession private static readonly byte[] ANSI_RESET_CURSOR = {0x1B, 0x5B, 0x48}; //Tracks Responses We've already sent -- prevents looping - private readonly List _iacSentResponses = new List(); + private readonly HashSet _iacSentResponses = new HashSet(); + + private class TelnetOptionsValue { + public bool Local { get; set; } + public bool Remote { get; set; } + + public EnumIacVerbs GetLocalStatusVerb() => Local ? EnumIacVerbs.WILL : EnumIacVerbs.WONT; + + public EnumIacVerbs GetRemoteCommandVerb() => Remote ? EnumIacVerbs.DO : EnumIacVerbs.DONT; + } + + private readonly Dictionary _localOptions = + new Dictionary() + { + {EnumIacOptions.BinaryTransmission, new TelnetOptionsValue { Local = true, Remote = true}}, + {EnumIacOptions.Echo, new TelnetOptionsValue { Local = true, Remote = false}}, + {EnumIacOptions.SuppressGoAhead, new TelnetOptionsValue { Local = true, Remote = true}}, + }; + + private readonly IacFilter _iacFilter; public TelnetSession(ILogger logger, Socket telnetConnection) : base(logger, telnetConnection) { SessionType = EnumSessionType.Telnet; SessionState = EnumSessionState.Unauthenticated; + + _iacFilter = new IacFilter(logger); + _iacFilter.IacVerbReceived += OnIacVerbReceived; } public override void Start() @@ -94,14 +116,7 @@ protected override void PreSend() protected override (byte[], int) ProcessIncomingClientData(byte[] clientData, int bytesReceived) { - //Process if it's an IAC command - if (clientData[0] == 0xFF) - { - ParseIAC(_socketReceiveBuffer); - return (null, 0); - } - - return (clientData, bytesReceived); + return _iacFilter.ProcessIncomingClientData(clientData, bytesReceived); } /// @@ -112,177 +127,64 @@ protected override (byte[], int) ProcessIncomingClientData(byte[] clientData, in base.Send(new IacResponse(EnumIacVerbs.DO, EnumIacOptions.BinaryTransmission).ToArray()); } - /// - /// Parses IAC Commands Received by - /// - /// - private void ParseIAC(ReadOnlySpan iacResponse) + private void AddInitialNegotiations(HashSet responses) { - var _iacResponses = new List(); - - for (var i = 0; i < iacResponse.Length; i += 3) + foreach(var entry in _localOptions) { - if (iacResponse[i] == 0) - break; - - if (iacResponse[i] != 0xFF) - throw new Exception("Invalid IAC?"); + responses.Add(new IacResponse(entry.Value.GetLocalStatusVerb(), entry.Key)); + responses.Add(new IacResponse(entry.Value.GetRemoteCommandVerb(), entry.Key)); + } + } - var iacVerb = (EnumIacVerbs)iacResponse[i + 1]; - var iacOption = (EnumIacOptions)iacResponse[i + 2]; + private void OnIacVerbReceived(object sender, IacFilter.IacVerbReceivedEventArgs args) + { + var iacResponses = new HashSet(); - _logger.Info($">> Channel {Channel}: IAC {iacVerb} {iacOption}"); + _logger.Debug($">> Channel {Channel}: IAC {args.Verb} {args.Option}"); - switch (iacOption) + if (_localOptions.TryGetValue(args.Option, out var localOptionsValue)) + { + // we support this, and we're hard coded, so tell client to back off and listen to + // us + iacResponses.Add(new IacResponse(localOptionsValue.GetLocalStatusVerb(), args.Option)); + if (args.Verb == EnumIacVerbs.WILL || args.Verb == EnumIacVerbs.WONT) { - case EnumIacOptions.BinaryTransmission: - { - switch (iacVerb) - { - case EnumIacVerbs.WILL: - _iacResponses.Add(new IacResponse(EnumIacVerbs.DO, EnumIacOptions.BinaryTransmission)); - break; - case EnumIacVerbs.DO: - _iacResponses.Add(new IacResponse(EnumIacVerbs.WILL, EnumIacOptions.BinaryTransmission)); - break; - default: - _logger.Warn($"Unhandled IAC Verb fpr {iacOption}: {iacVerb}"); - break; - } - - break; - } - case EnumIacOptions.Echo: - { - switch (iacVerb) - { - case EnumIacVerbs.DO: - _iacResponses.Add(new IacResponse(EnumIacVerbs.DONT, EnumIacOptions.Echo)); - _iacResponses.Add(new IacResponse(EnumIacVerbs.WILL, EnumIacOptions.Echo)); - break; - default: - _logger.Warn($"Unhandled IAC Verb fpr {iacOption}: {iacVerb}"); - break; - } - - break; - } - case EnumIacOptions.NegotiateAboutWindowSize: - { - switch (iacVerb) - { - case EnumIacVerbs.WILL: - _iacResponses.Add(new IacResponse(EnumIacVerbs.WONT, - EnumIacOptions.NegotiateAboutWindowSize)); - break; - default: - _logger.Warn($"Unhandled IAC Verb fpr {iacOption}: {iacVerb}"); - break; - } - - break; - } - case EnumIacOptions.TerminalSpeed: - { - switch (iacVerb) - { - case EnumIacVerbs.WILL: - _iacResponses.Add(new IacResponse(EnumIacVerbs.WONT, EnumIacOptions.TerminalSpeed)); - break; - default: - _logger.Warn($"Unhandled IAC Verb fpr {iacOption}: {iacVerb}"); - break; - } - - break; - } - case EnumIacOptions.TerminalType: - { - switch (iacVerb) - { - case EnumIacVerbs.WILL: - _iacResponses.Add(new IacResponse(EnumIacVerbs.WONT, EnumIacOptions.TerminalType)); - break; - default: - _logger.Warn($"Unhandled IAC Verb fpr {iacOption}: {iacVerb}"); - break; - } - - break; - } - case EnumIacOptions.EnvironmentOption: - { - switch (iacVerb) - { - case EnumIacVerbs.WILL: - _iacResponses.Add(new IacResponse(EnumIacVerbs.WONT, EnumIacOptions.EnvironmentOption)); - break; - default: - _logger.Warn($"Unhandled IAC Verb fpr {iacOption}: {iacVerb}"); - break; - } - - break; - } - case EnumIacOptions.SuppressGoAhead: - { - switch (iacVerb) - { - case EnumIacVerbs.WILL: - _iacResponses.Add(new IacResponse(EnumIacVerbs.DO, EnumIacOptions.SuppressGoAhead)); - break; - case EnumIacVerbs.DO: - _iacResponses.Add(new IacResponse(EnumIacVerbs.WILL, EnumIacOptions.SuppressGoAhead)); - break; - default: - _logger.Warn($"Unhandled IAC Verb fpr {iacOption}: {iacVerb}"); - break; - } - - break; - } + iacResponses.Add(new IacResponse(localOptionsValue.GetRemoteCommandVerb(), args.Option)); } } + else + { + // not supported, just return that we WONT do this + iacResponses.Add(new IacResponse(EnumIacVerbs.WONT, args.Option)); + } //In the 1st phase of IAC negotiation, ensure the required items are being negotiated if (_iacPhase == 0) { - if (_iacResponses.All(x => x.Option != EnumIacOptions.BinaryTransmission)) - { - _iacResponses.Add(new IacResponse(EnumIacVerbs.DO, EnumIacOptions.BinaryTransmission)); - _iacResponses.Add(new IacResponse(EnumIacVerbs.WILL, EnumIacOptions.BinaryTransmission)); - } - - if (_iacResponses.All(x => x.Option != EnumIacOptions.Echo)) - { - _iacResponses.Add(new IacResponse(EnumIacVerbs.DONT, EnumIacOptions.Echo)); - _iacResponses.Add(new IacResponse(EnumIacVerbs.WILL, EnumIacOptions.Echo)); - } - + AddInitialNegotiations(iacResponses); } _iacPhase++; + if (iacResponses.Count == 0) + { + return; + } + using var msIacToSend = new MemoryStream(); - foreach (var resp in _iacResponses) + foreach (var resp in iacResponses) { - //Prevent Duplicate Responses - if (!_iacSentResponses.Any(x => x.Verb == resp.Verb && x.Option == resp.Option)) + if (_iacSentResponses.Add(resp)) { - _iacSentResponses.Add(resp); - _logger.Info($"<< Channel {Channel}: IAC {resp.Verb} {resp.Option}"); + _logger.Debug($"<< Channel {Channel}: IAC {resp.Verb} {resp.Option}"); msIacToSend.Write(resp.ToArray()); } - else - { - _logger.Info($"<< Channel {Channel}: IAC {resp.Verb} {resp.Option} (Ignored, Duplicate)"); - } } - if (msIacToSend.Length == 0) - return; - - base.Send(msIacToSend.ToArray()); + if (msIacToSend.Length > 0) + { + base.Send(msIacToSend.ToArray()); + } } } }