Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions PrimS.Telnet.NetStandard/BaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public abstract partial class BaseClient : IBaseClient
/// <inheritdoc/>
public int MillisecondReadDelay { get; set; } = DefaultMillisecondReadDelay;

/// <summary>
/// Gets or sets the telnet option negotiation mode.
/// </summary>
public NegotiationMode NegotiationMode { get; set; } = NegotiationMode.Simple;

/// <inheritdoc/>
public bool IsConnected
{
Expand Down
34 changes: 34 additions & 0 deletions PrimS.Telnet.NetStandard/ByteStreamHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
public partial class ByteStreamHandler : IByteStreamHandler
{
private readonly IByteStream byteStream;
private readonly Rfc1143OptionNegotiator? rfc1143Negotiator;

private bool IsResponsePending
{
Expand Down Expand Up @@ -201,6 +202,17 @@ private void InterpretNextAsCommand(int inputVerb)
#endif
case (int)Commands.Dont:
case (int)Commands.Wont:
if (rfc1143Negotiator != null)
{
#if ASYNC
return
#endif
PreprocessorAsyncAdapter.Execute(() => rfc1143Negotiator.HandleCommand(inputVerb));
#if !ASYNC
break;
#endif
}

// We should ignore Don't\Won't because that is the default state.
// Only reply on state change. This helps avoid loops.
// See RFC1143: https://tools.ietf.org/html/rfc1143
Expand All @@ -211,14 +223,36 @@ private void InterpretNextAsCommand(int inputVerb)
#endif
case (int)Commands.Do:
case (int)Commands.Will:
if (rfc1143Negotiator != null)
{
#if ASYNC
return
#endif
PreprocessorAsyncAdapter.Execute(() => rfc1143Negotiator.HandleCommand(inputVerb));
#if !ASYNC
break;
#endif
}
#if ASYNC
return
#endif
PreprocessorAsyncAdapter.Execute(() => ReplyToCommand(inputVerb));
#if !ASYNC
break;
#endif

case (int)Commands.Subnegotiation:
if (rfc1143Negotiator != null)
{
#if ASYNC
return
#endif
PreprocessorAsyncAdapter.Execute(() => rfc1143Negotiator.ProcessSubnegotiation(cmdByte => InterpretNextAsCommand(cmdByte)));
#if !ASYNC
break;
#endif
}

#if ASYNC
return
#endif
Expand Down
20 changes: 20 additions & 0 deletions PrimS.Telnet.NetStandard/ByteStreamHandlerCancellable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,30 @@ public ByteStreamHandler(IByteStream byteStream, CancellationTokenSource interna
/// <param name="internalCancellation">A cancellation token.</param>
/// <param name="millisecondReadDelay">Time to delay between reads from the stream.</param>
public ByteStreamHandler(IByteStream byteStream, CancellationTokenSource internalCancellation, int millisecondReadDelay)
: this(byteStream, internalCancellation, millisecondReadDelay, NegotiationMode.Simple)
{ }

/// <summary>
/// Initialises a new instance of the <see cref="ByteStreamHandler"/> class.
/// </summary>
/// <param name="byteStream">The byteStream to handle.</param>
/// <param name="internalCancellation">A cancellation token.</param>
/// <param name="millisecondReadDelay">Time to delay between reads from the stream.</param>
/// <param name="negotiationMode">The negotiation mode to use for this instance.</param>
public ByteStreamHandler(IByteStream byteStream, CancellationTokenSource internalCancellation, int millisecondReadDelay, NegotiationMode negotiationMode)
{
this.byteStream = byteStream;
this.internalCancellation = internalCancellation;
MillisecondReadDelay = millisecondReadDelay;

if (negotiationMode == NegotiationMode.Rfc1143)
{
rfc1143Negotiator = new Rfc1143OptionNegotiator(byteStream, internalCancellation);
}
else
{
rfc1143Negotiator = null;
}
}

private bool IsCancellationRequested
Expand Down
2 changes: 1 addition & 1 deletion PrimS.Telnet.NetStandard/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public Task<string> ReadAsync()
public Task<string> ReadAsync(TimeSpan timeout)
{
#pragma warning disable CA2000 // Dispose objects before losing scope
var handler = new ByteStreamHandler(ByteStream, InternalCancellation, MillisecondReadDelay);
var handler = new ByteStreamHandler(ByteStream, InternalCancellation, MillisecondReadDelay, NegotiationMode);
#pragma warning restore CA2000 // Dispose objects before losing scope
return handler.ReadAsync(timeout);
}
Expand Down
6 changes: 6 additions & 0 deletions PrimS.Telnet.NetStandard/Client_Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ internal static byte[] SuppressGoAheadBuffer
#endif
ProactiveOptionNegotiation()
{
if (NegotiationMode == NegotiationMode.Rfc1143)
{
var negotiator = new Rfc1143OptionNegotiator(ByteStream, InternalCancellation);
negotiator.EnableOptionOnServer(Options.SuppressGoAhead);
}

var supressGoAhead = SuppressGoAheadBuffer;
#if ASYNC
return ByteStream.WriteAsync(supressGoAhead, 0, supressGoAhead.Length, InternalCancellation.Token);
Expand Down
5 changes: 5 additions & 0 deletions PrimS.Telnet.NetStandard/IBaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@ public interface IBaseClient : IDisposable
/// The read delay ms.
/// </summary>
int MillisecondReadDelay { get; set; }

/// <summary>
/// Gets or sets the telnet option negotiation mode.
/// </summary>
NegotiationMode NegotiationMode { get; set; }
}
}
20 changes: 20 additions & 0 deletions PrimS.Telnet.NetStandard/NegotiationMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace PrimS.Telnet
{
/// <summary>
/// Specifies the telnet option negotiation behavior.
/// </summary>
public enum NegotiationMode
{
/// <summary>
/// Simple negotiation - faster, compatible with most servers.
/// Uses basic accept/reject logic without state tracking.
/// </summary>
Simple = 0,

/// <summary>
/// RFC 1143 compliant negotiation - prevents loops and follows telnet standards.
/// Implements Q-method state machine for robust option negotiation.
/// </summary>
Rfc1143 = 1
}
}
57 changes: 57 additions & 0 deletions PrimS.Telnet.NetStandard/NegotiationStateManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace PrimS.Telnet
{
using System.Collections.Generic;
using System.Runtime.CompilerServices;

/// <summary>
/// Manages negotiation states across multiple ByteStreamHandler instances for the same connection.
/// This ensures FSM state continuity when ByteStreamHandlers are recreated.
/// Uses ConditionalWeakTable to automatically clean up when IByteStream is garbage collected.
/// </summary>
public static class NegotiationStateManager
{
private static readonly ConditionalWeakTable<IByteStream, Dictionary<Options, Rfc1143OptionNegotiator.NegotiationState>> optionStatesByStream = new ConditionalWeakTable<IByteStream, Dictionary<Options, Rfc1143OptionNegotiator.NegotiationState>>();

/// <summary>
/// Gets the negotiation state for a specific connection and option.
/// </summary>
/// <param name="byteStream">The byte stream representing the connection</param>
/// <param name="option">The telnet option</param>
/// <returns>The state for the option, or a new state if none exists</returns>
public static Rfc1143OptionNegotiator.NegotiationState GetOptionState(IByteStream byteStream, Options option)
{
var states = optionStatesByStream.GetOrCreateValue(byteStream);

if (!states.TryGetValue(option, out var state))
{
state = new Rfc1143OptionNegotiator.NegotiationState();
states[option] = state;
}

return state;
}

/// <summary>
/// Sets the negotiation state for a specific connection and option.
/// </summary>
/// <param name="byteStream">The byte stream representing the connection</param>
/// <param name="option">The telnet option</param>
/// <param name="state">The state to set</param>
public static void SetOptionState(IByteStream byteStream, Options option, Rfc1143OptionNegotiator.NegotiationState state)
{
var states = optionStatesByStream.GetOrCreateValue(byteStream);
states[option] = state;
}


/// <summary>
/// Gets all option states for a connection.
/// </summary>
/// <param name="byteStream">The byte stream representing the connection</param>
/// <returns>Dictionary of option states</returns>
public static Dictionary<Options, Rfc1143OptionNegotiator.NegotiationState> GetAllStates(IByteStream byteStream)
{
return optionStatesByStream.GetOrCreateValue(byteStream);
}
}
}
Loading