-
Notifications
You must be signed in to change notification settings - Fork 338
/
SingleMessageTcpListener.cs
166 lines (141 loc) · 6.98 KB
/
SingleMessageTcpListener.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
using Microsoft.Identity.Client.UI;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace NetCoreTestApp.Experimental
{
/// <summary>
/// This object is responsible for listening to a single TCP request, on localhost:port,
/// extracting the uri, parsing
/// </summary>
/// <remarks>
/// The underlying TCP listener might capture multiple requests, but only the first one is handled.
/// </remarks>
internal class SingleMessageTcpListener : IDisposable
{
private readonly int _port;
private readonly System.Net.Sockets.TcpListener _tcpListener;
public SingleMessageTcpListener(int port)
{
if (port < 1 || port == 80)
{
throw new ArgumentOutOfRangeException("Expected a valid port number, > 0, not 80");
}
_port = port;
_tcpListener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, _port);
}
public async Task ListenToSingleRequestAndRespondAsync(
Func<Uri, string> responseProducer,
CancellationToken cancellationToken)
{
cancellationToken.Register(() => _tcpListener.Stop());
_tcpListener.Start();
TcpClient tcpClient = null;
try
{
tcpClient =
await AcceptTcpClientAsync(cancellationToken)
.ConfigureAwait(false);
await ExtractUriAndRespondAsync(tcpClient, responseProducer, cancellationToken).ConfigureAwait(false);
}
finally
{
tcpClient?.Close();
}
}
/// <summary>
/// AcceptTcpClientAsync does not natively support cancellation, so use this wrapper. Make sure
/// the cancellation token is registered to stop the listener.
/// </summary>
/// <remarks>See https://stackoverflow.com/questions/19220957/tcplistener-how-to-stop-listening-while-awaiting-accepttcpclientasync</remarks>
private async Task<TcpClient> AcceptTcpClientAsync(CancellationToken token)
{
try
{
return await _tcpListener.AcceptTcpClientAsync().ConfigureAwait(false);
}
catch (Exception ex) when (token.IsCancellationRequested)
{
throw new OperationCanceledException("Cancellation was requested while awaiting TCP client connection.", ex);
}
}
private async Task ExtractUriAndRespondAsync(
TcpClient tcpClient,
Func<Uri, string> responseProducer,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
string httpRequest = await GetTcpResponseAsync(tcpClient, cancellationToken).ConfigureAwait(false);
Uri uri = ExtractUriFromHttpRequest(httpRequest);
// write an "OK, please close the browser message"
await WriteResponseAsync(responseProducer(uri), tcpClient.GetStream(), cancellationToken)
.ConfigureAwait(false);
}
#pragma warning disable CS1570 // XML comment has badly formed XML
/// <summary>
/// Example TCP response:
///
/// {GET /?code=OAQABAAIAAAC5una0EUFgTIF8ElaxtWjTl5wse5YHycjcaO_qJukUUexKz660btJtJSiQKz1h4b5DalmXspKis-bS6Inu8lNs4CpoE4FITrLv00Mr3MEYEQzgrn6JiNoIwDFSl4HBzHG8Kjd4Ho65QGUMVNyTjhWyQDf_12E8Gw9sll_sbOU51FIreZlVuvsqIWBMIJ8mfmExZBSckofV6LbcKJTeEZKaqjC09x3k1dpsCNJAtYTQIus5g1DyhAW8viDpWDpQJlT55_0W4rrNKY3CSD5AhKd3Ng4_ePPd7iC6qObfmMBlCcldX688vR2IghV0GoA0qNalzwqP7lov-yf38uVZ3ir6VlDNpbzCoV-drw0zhlMKgSq6LXT7QQYmuA4RVy_7TE9gjQpW-P0_ZXUHirpgdsblaa3JUq4cXpbMU8YCLQm7I2L0oCkBTupYXKLoM2gHSYPJ5HChhj1x0pWXRzXdqbx_TPTujBLsAo4Skr_XiLQ4QPJZpkscmXezpPa5Z87gDenUBRBI9ppROhOksekMbvPataF0qBaM38QzcnzeOCFyih1OjIKsq3GeryChrEtfY9CL9lBZ6alIIQB4thD__Tc24OUmr04hX34PjMyt1Z9Qvr76Pw0r7A52JvqQLWupx8bqok6AyCwqUGfLCPjwylSLA7NYD7vScAbfkOOszfoCC3ff14Dqm3IAB1tUJfCZoab61c6Mozls74c2Ujr3roHw4NdPuo-re5fbpSw5RVu8MffWYwXrO3GdmgcvIMkli2uperucLldNVIp6Pc3MatMYSBeAikuhtaZiZAhhl3uQxzoMhU-MO9WXuG2oIkqSvKjghxi1NUhfTK4-du7I5h1r0lFh9b3h8kvE1WBhAIxLdSAA&state=b380f309-7d24-4793-b938-e4a512b2c7f6&session_state=a442c3cd-a25e-4b88-8b33-36d194ba11b2 HTTP/1.1
/// Host: localhost:9001
/// Accept-Language: en-GB,en;q=0.9,en-US;q=0.8,ro;q=0.7,fr;q=0.6
/// Connection: keep-alive
/// Upgrade-Insecure-Requests: 1
/// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
/// Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
/// Accept-Encoding: gzip, deflate, br
/// </summary>
/// <returns>http://localhost:9001/?code=foo&session_state=bar</returns>
private Uri ExtractUriFromHttpRequest(string httpRequest)
#pragma warning restore CS1570 // XML comment has badly formed XML
{
string regexp = @"GET \/\?(.*) HTTP";
string getQuery = null;
Regex r1 = new Regex(regexp);
Match match = r1.Match(httpRequest);
if (!match.Success)
{
throw new InvalidOperationException("Not a GET query");
}
getQuery = match.Groups[1].Value;
UriBuilder uriBuilder = new UriBuilder();
uriBuilder.Query = getQuery;
uriBuilder.Port = _port;
return uriBuilder.Uri;
}
private static async Task<string> GetTcpResponseAsync(TcpClient client, CancellationToken cancellationToken)
{
NetworkStream networkStream = client.GetStream();
byte[] readBuffer = new byte[1024];
StringBuilder stringBuilder = new StringBuilder();
int numberOfBytesRead = 0;
// Incoming message may be larger than the buffer size.
do
{
numberOfBytesRead = await networkStream.ReadAsync(readBuffer, 0, readBuffer.Length, cancellationToken)
.ConfigureAwait(false);
string s = Encoding.ASCII.GetString(readBuffer, 0, numberOfBytesRead);
stringBuilder.Append(s);
}
while (networkStream.DataAvailable);
return stringBuilder.ToString();
}
private async Task WriteResponseAsync(
string message,
NetworkStream stream,
CancellationToken cancellationToken)
{
string fullResponse = $"HTTP/1.1 200 OK\r\n\r\n{message}";
var response = Encoding.ASCII.GetBytes(fullResponse);
await stream.WriteAsync(response, 0, response.Length, cancellationToken).ConfigureAwait(false);
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
public void Dispose()
{
_tcpListener?.Stop();
}
}
}