-
Notifications
You must be signed in to change notification settings - Fork 50
/
ByteStreamHandler.cs
393 lines (369 loc) · 12.1 KB
/
ByteStreamHandler.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
namespace PrimS.Telnet
{
using System;
using System.Collections.Generic;
using System.Text;
#if ASYNC
using System.Threading.Tasks;
#endif
/// <summary>
/// Provides core functionality for interacting with the ByteStream.
/// </summary>
public partial class ByteStreamHandler : IByteStreamHandler
{
private readonly IByteStream byteStream;
private bool IsResponsePending
{
get
{
return byteStream.Available > 0;
}
}
internal int MillisecondReadDelay { get; set; } = 16;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
byteStream.Dispose();
if (isCancellationTokenOwned)
{
internalCancellation.Dispose();
}
#if ASYNC
SendCancel();
#endif
}
}
private static DateTime ExtendRollingTimeout(TimeSpan timeout)
{
return DateTime.Now.Add(TimeSpan.FromMilliseconds(timeout.TotalMilliseconds / 100));
}
private static bool IsWaitForInitialResponse(DateTime endInitialTimeout, bool isInitialResponseReceived)
{
return !isInitialResponseReceived && DateTime.Now < endInitialTimeout;
}
private static bool IsTimeoutExpired(DateTime timeout)
{
return DateTime.Now >= timeout;
}
private static bool IsInitialResponseReceived(StringBuilder sb)
{
return sb.Length > 0;
}
#if ASYNC
private async Task<bool> IsWaitForIncrementalResponse(DateTime rollingTimeout)
#else
private bool IsWaitForIncrementalResponse(DateTime rollingTimeout)
#endif
{
var result = DateTime.Now < rollingTimeout;
#if ASYNC
await Task.Delay(MillisecondReadDelay, internalCancellation.Token).ConfigureAwait(false);
#else
System.Threading.Thread.Sleep(MillisecondReadDelay);
#endif
return result;
}
/// <summary>
/// Separate TELNET commands from text. Handle non-printable characters.
/// </summary>
/// <param name="sb">The incoming message.</param>
/// <returns>True if response is pending.</returns>
private
#if ASYNC
async Task<bool>
#else
bool
#endif
RetrieveAndParseResponse(StringBuilder sb)
{
if (IsResponsePending)
{
var input = byteStream.ReadByte();
switch (input)
{
case -1:
break;
case (int)Commands.InterpretAsCommand:
var inputVerb = byteStream.ReadByte();
if (inputVerb == -1)
{
// do nothing
}
else if (inputVerb == 255)
{
// literal IAC = 255 escaped, so append char 255 to string
sb.Append(inputVerb);
}
else
{
#if ASYNC
await
#endif
PreprocessorAsyncAdapter.ExecuteWithConfigureAwait(() => InterpretNextAsCommand(inputVerb));
}
break;
case 1: // Start of Heading
sb.Append("\n \n");
break;
case 2: // Start of Text
sb.Append("\t");
break;
case 3: // End of Text or "break" CTRL+C
sb.Append("^C");
System.Diagnostics.Debug.WriteLine("^C");
break;
case 4: // End of Transmission
sb.Append("^D");
break;
case 5: // Enquiry
#if ASYNC
await byteStream.WriteByteAsync(6, internalCancellation.Token); // Send ACK
#else
byteStream.WriteByte(6); // Send ACK
#endif
break;
case 6: // Acknowledge
// We got an ACK
break;
case 7: // Bell character
Console.Beep();
break;
case 8: // Backspace
// We could delete a character from sb, or just swallow the char here.
break;
case 11: // Vertical TAB
case 12: // Form Feed
sb.Append(Environment.NewLine);
break;
case 21:
sb.Append("NAK: Retransmit last message.");
System.Diagnostics.Debug.WriteLine("ERROR NAK: Retransmit last message.");
break;
case 31: // Unit Separator
sb.Append(",");
break;
default:
sb.Append((char)input);
break;
}
return true;
}
return false;
}
/// <summary>
/// We received a TELNET command. Handle it.
/// </summary>
/// <param name="inputVerb">The command we received.</param>
#if ASYNC
private Task InterpretNextAsCommand(int inputVerb)
#else
private void InterpretNextAsCommand(int inputVerb)
#endif
{
System.Diagnostics.Debug.Write(Enum.GetName(typeof(Commands), inputVerb));
switch (inputVerb)
{
case (int)Commands.InterruptProcess:
System.Diagnostics.Debug.WriteLine("Interrupt Process (IP) received.");
#if ASYNC
SendCancel();
return Task.CompletedTask;
#else
break;
#endif
case (int)Commands.Dont:
case (int)Commands.Wont:
// 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
#if ASYNC
return Task.CompletedTask;
#else
break;
#endif
case (int)Commands.Do:
case (int)Commands.Will:
#if ASYNC
return
#endif
PreprocessorAsyncAdapter.Execute(() => ReplyToCommand(inputVerb));
#if !ASYNC
break;
#endif
case (int)Commands.Subnegotiation:
#if ASYNC
return
#endif
PreprocessorAsyncAdapter.Execute(() => PerformNegotiation());
#if !ASYNC
break;
#endif
default:
#if ASYNC
return Task.CompletedTask;
#else
break;
#endif
}
}
/// <summary>
/// We received a request to perform sub negotiation on a TELNET option.
/// <see cref="Client.TerminalType"/> and <see cref="Client.TerminalSpeed"/> can be configured via static properties on the <see cref="Client"/> class.
/// </summary>
#if ASYNC
private async Task PerformNegotiation()
#else
private void PerformNegotiation()
#endif
{
var inputOption = byteStream.ReadByte();
var subCommand = byteStream.ReadByte();
// ISSUE: We should loop here until IAC-SE but what is the exit condition if that never comes?
var shouldIAC = byteStream.ReadByte();
var shouldSE = byteStream.ReadByte();
if (subCommand == 1 && // Sub-negotiation SEND command.
shouldIAC == (int)Commands.InterpretAsCommand &&
shouldSE == (int)Commands.SubnegotiationEnd)
{
switch (inputOption)
{
case (int)Options.TerminalType:
#if ASYNC
await
#endif
PreprocessorAsyncAdapter.ExecuteWithConfigureAwait(() => SendNegotiation(inputOption, Client.TerminalType));
break;
case (int)Options.TerminalSpeed:
#if ASYNC
await
#endif
PreprocessorAsyncAdapter.ExecuteWithConfigureAwait(() => SendNegotiation(inputOption, Client.TerminalSpeed));
break;
default:
// We don't handle other sub negotiation options yet.
System.Diagnostics.Debug.WriteLine("Request to negotiate: " + Enum.GetName(typeof(Options), inputOption));
break;
}
}
else
{
// If we get lost just send WONT to end the negotiation
var outBuffer = new byte[3];
outBuffer[0] = (byte)Commands.InterpretAsCommand;
outBuffer[1] = (byte)Commands.Wont;
outBuffer[2] = (byte)inputOption;
#if ASYNC
await byteStream.WriteAsync(outBuffer, 0, outBuffer.Length, internalCancellation.Token).ConfigureAwait(false);
#else
byteStream.Write(outBuffer, 0, outBuffer.Length);
#endif
}
}
/// <summary>
/// Send the sub negotiation response to the server.
/// </summary>
/// <param name="inputOption">The option we are negotiating.</param>
/// <param name="optionMessage">The setting for <paramref name="inputOption"/>.</param>
private
#if ASYNC
Task
#else
void
#endif
SendNegotiation(int inputOption, string optionMessage)
{
System.Diagnostics.Debug.WriteLine("Sending: " + Enum.GetName(typeof(Options), inputOption) + " Setting: " + optionMessage);
var outBuffer = BuildSendNegotiationOutBuffer(inputOption, optionMessage);
#if ASYNC
return byteStream.WriteAsync(outBuffer.ToArray(), 0, outBuffer.Count, internalCancellation.Token);
#else
byteStream.Write(outBuffer.ToArray(), 0, outBuffer.Count);
#endif
}
private static List<byte> BuildSendNegotiationOutBuffer(int inputOption, string optionMessage)
{
var outBuffer = new List<byte>();
outBuffer.Add((byte)Commands.InterpretAsCommand);
outBuffer.Add((byte)Commands.Subnegotiation);
outBuffer.Add((byte)inputOption);
outBuffer.Add(0); // "IS"
outBuffer.AddRange(Encoding.ASCII.GetBytes(optionMessage));
outBuffer.Add((byte)Commands.InterpretAsCommand);
outBuffer.Add((byte)Commands.SubnegotiationEnd);
return outBuffer;
}
/// <summary>
/// Send TELNET command response to the server.
/// Replies to all commands with <see cref="Commands.Wont"/>||<see cref="Commands.Dont"/> unless it is <see cref="Options.SuppressGoAhead"/>, <see cref="Options.TerminalType"/>, or <see cref="Options.TerminalSpeed"/>.
/// </summary>
/// <param name="inputVerb">The TELNET command we received.</param>
private
#if ASYNC
async Task
#else
void
#endif
ReplyToCommand(int inputVerb)
{
var inputOption = byteStream.ReadByte();
if (IsCommand(inputOption))
{
System.Diagnostics.Debug.WriteLine(Enum.GetName(typeof(Options), inputOption));
var outBuffer = new byte[3];
outBuffer[0] = (byte)Commands.InterpretAsCommand;
outBuffer[1] = inputOption switch
{
(int)Options.SuppressGoAhead => inputVerb == (int)Commands.Do ? (byte)Commands.Will : (byte)Commands.Do,
(int)Options.TerminalType => inputVerb == (int)Commands.Do ? (byte)Commands.Will : (byte)Commands.Do,
(int)Options.TerminalSpeed => inputVerb == (int)Commands.Do ? (byte)Commands.Will : (byte)Commands.Do,
(int)Options.WindowSize => inputVerb == (int)Commands.Do ? (byte)Commands.Will : (byte)Commands.Do,
_ => inputVerb == (int)Commands.Do ? (byte)Commands.Wont : (byte)Commands.Dont,
};
outBuffer[2] = (byte)inputOption;
#if ASYNC
await byteStream.WriteAsync(outBuffer, 0, outBuffer.Length, internalCancellation.Token).ConfigureAwait(false);
#else
byteStream.Write(outBuffer, 0, outBuffer.Length);
#endif
if (inputOption == (int)Options.WindowSize)
{ // NAWS needs to be sent immediately because the server doesn't request subnegotiation.
var clientNAWS = ((char)132 + (char)0 + (char)24).ToString(); // This could be an environment variable
#if ASYNC
await
#endif
SendNegotiation(inputOption, clientNAWS);
}
}
}
private static bool IsCommand(int inputOption)
{
return inputOption != -1;
}
#if ASYNC
private async Task<bool> IsResponseAnticipated(bool isInitialResponseReceived, DateTime endInitialTimeout, DateTime rollingTimeout)
#else
private bool IsResponseAnticipated(bool isInitialResponseReceived, DateTime endInitialTimeout, DateTime rollingTimeout)
#endif
{
return IsResponsePending || IsWaitForInitialResponse(endInitialTimeout, isInitialResponseReceived) ||
#if ASYNC
await IsWaitForIncrementalResponse(rollingTimeout).ConfigureAwait(false);
#else
IsWaitForIncrementalResponse(rollingTimeout);
#endif
}
}
}