This repository has been archived by the owner on Sep 24, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathServer.cs
363 lines (312 loc) · 12.9 KB
/
Server.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
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Ninja.WebSockets;
using Ninja.WebSockets.Internal;
using UnityEngine;
namespace Mirror.Websocket
{
public class Server
{
public event Action<int> Connected;
public event Action<int, ArraySegment<byte>> ReceivedData;
public event Action<int> Disconnected;
public event Action<int, Exception> ReceivedError;
const int MaxMessageSize = 256 * 1024;
// listener
TcpListener listener;
readonly IWebSocketServerFactory webSocketServerFactory = new WebSocketServerFactory();
CancellationTokenSource cancellation;
// clients with <connectionId, TcpClient>
Dictionary<int, WebSocket> clients = new Dictionary<int, WebSocket>();
public bool NoDelay = true;
// connectionId counter
// (right now we only use it from one listener thread, but we might have
// multiple threads later in case of WebSockets etc.)
// -> static so that another server instance doesn't start at 0 again.
static int counter = 0;
// public next id function in case someone needs to reserve an id
// (e.g. if hostMode should always have 0 connection and external
// connections should start at 1, etc.)
public static int NextConnectionId()
{
int id = Interlocked.Increment(ref counter);
// it's very unlikely that we reach the uint limit of 2 billion.
// even with 1 new connection per second, this would take 68 years.
// -> but if it happens, then we should throw an exception because
// the caller probably should stop accepting clients.
// -> it's hardly worth using 'bool Next(out id)' for that case
// because it's just so unlikely.
if (id == int.MaxValue)
{
throw new Exception("connection id limit reached: " + id);
}
return id;
}
// check if the server is running
public bool Active
{
get { return listener != null; }
}
public WebSocket GetClient(int connectionId)
{
// paul: null is evil, throw exception if not found
return clients[connectionId];
}
public bool _secure = false;
public SslConfiguration _sslConfig;
public class SslConfiguration
{
public System.Security.Cryptography.X509Certificates.X509Certificate2 Certificate;
public bool ClientCertificateRequired;
public System.Security.Authentication.SslProtocols EnabledSslProtocols;
public bool CheckCertificateRevocation;
}
public async Task Listen(int port)
{
try
{
cancellation = new CancellationTokenSource();
listener = TcpListener.Create(port);
listener.Server.NoDelay = NoDelay;
listener.Start();
Debug.Log($"Websocket server started listening on port {port}");
while (true)
{
TcpClient tcpClient = await listener.AcceptTcpClientAsync();
_ = ProcessTcpClient(tcpClient, cancellation.Token);
}
}
catch (ObjectDisposedException)
{
// do nothing. This will be thrown if the Listener has been stopped
}
catch (Exception ex)
{
ReceivedError?.Invoke(0, ex);
}
}
async Task ProcessTcpClient(TcpClient tcpClient, CancellationToken token)
{
try
{
// this worker thread stays alive until either of the following happens:
// Client sends a close conection request OR
// An unhandled exception is thrown OR
// The server is disposed
// get a secure or insecure stream
Stream stream = tcpClient.GetStream();
if (_secure)
{
SslStream sslStream = new SslStream(stream, false, CertVerificationCallback);
sslStream.AuthenticateAsServer(_sslConfig.Certificate, _sslConfig.ClientCertificateRequired, _sslConfig.EnabledSslProtocols, _sslConfig.CheckCertificateRevocation);
stream = sslStream;
}
WebSocketHttpContext context = await webSocketServerFactory.ReadHttpHeaderFromStreamAsync(tcpClient, stream, token);
if (context.IsWebSocketRequest)
{
// Force KeepAliveInterval to Zero, otherwise the transport is unstable and causes random disconnects.
WebSocketServerOptions options = new WebSocketServerOptions() { KeepAliveInterval = TimeSpan.Zero, SubProtocol = "binary" };
WebSocket webSocket = await webSocketServerFactory.AcceptWebSocketAsync(context, options);
await ReceiveLoopAsync(webSocket, token);
}
else
{
Debug.Log("Http header contains no web socket upgrade request. Ignoring");
}
}
catch (IOException)
{
// do nothing. This will be thrown if the transport is closed
}
catch (ObjectDisposedException)
{
// do nothing. This will be thrown if the Listener has been stopped
}
catch (Exception ex)
{
ReceivedError?.Invoke(0, ex);
}
finally
{
try
{
tcpClient.Client.Close();
tcpClient.Close();
}
catch (Exception ex)
{
ReceivedError?.Invoke(0, ex);
}
}
}
bool CertVerificationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// Much research has been done on this. When this is initiated from a HTTPS/WSS stream,
// the certificate is null and the SslPolicyErrors is RemoteCertificateNotAvailable.
// Meaning we CAN'T verify this and this is all we can do.
return true;
}
public bool enabled;
async Task ReceiveLoopAsync(WebSocket webSocket, CancellationToken token)
{
int connectionId = NextConnectionId();
clients.Add(connectionId, webSocket);
byte[] buffer = new byte[MaxMessageSize];
try
{
// someone connected, raise event
Connected?.Invoke(connectionId);
while (true)
{
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), token);
if (!enabled)
{
await WaitForEnabledAsync();
}
if (result.MessageType == WebSocketMessageType.Close)
{
Debug.Log($"Client initiated close. Status: {result.CloseStatus} Description: {result.CloseStatusDescription}");
break;
}
ArraySegment<byte> data = await ReadFrames(connectionId, result, webSocket, buffer, token);
if (data.Count == 0)
break;
try
{
// we received some data, raise event
ReceivedData?.Invoke(connectionId, data);
}
catch (Exception exception)
{
ReceivedError?.Invoke(connectionId, exception);
}
}
}
catch (Exception exception)
{
ReceivedError?.Invoke(connectionId, exception);
}
finally
{
clients.Remove(connectionId);
Disconnected?.Invoke(connectionId);
}
}
async Task WaitForEnabledAsync()
{
while (!enabled)
{
await Task.Delay(10);
}
}
// a message might come splitted in multiple frames
// collect all frames
async Task<ArraySegment<byte>> ReadFrames(int connectionId, WebSocketReceiveResult result, WebSocket webSocket, byte[] buffer, CancellationToken token)
{
int count = result.Count;
while (!result.EndOfMessage)
{
if (count >= MaxMessageSize)
{
string closeMessage = string.Format("Maximum message size: {0} bytes.", MaxMessageSize);
await webSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, CancellationToken.None);
ReceivedError?.Invoke(connectionId, new WebSocketException(WebSocketError.HeaderError));
return new ArraySegment<byte>();
}
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, count, MaxMessageSize - count), CancellationToken.None);
count += result.Count;
}
return new ArraySegment<byte>(buffer, 0, count);
}
public void Stop()
{
// only if started
if (!Active)
return;
Debug.Log("Server: stopping...");
cancellation.Cancel();
// stop listening to connections so that no one can connect while we
// close the client connections
listener.Stop();
// clear clients list
clients.Clear();
listener = null;
}
// send message to client using socket connection or throws exception
public async void Send(int connectionId, ArraySegment<byte> segment)
{
// find the connection
if (clients.TryGetValue(connectionId, out WebSocket client))
{
try
{
await client.SendAsync(segment, WebSocketMessageType.Binary, true, cancellation.Token);
}
catch (ObjectDisposedException)
{
// connection has been closed, swallow exception
Disconnect(connectionId);
}
catch (Exception exception)
{
if (clients.ContainsKey(connectionId))
{
// paul: If someone unplugs their internet
// we can potentially get hundreds of errors here all at once
// because all the WriteAsync wake up at once and throw exceptions
// by hiding inside this if, I ensure that we only report the first error
// all other errors are swallowed.
// this prevents a log storm that freezes the server for several seconds
ReceivedError?.Invoke(connectionId, exception);
}
Disconnect(connectionId);
}
}
else
{
ReceivedError?.Invoke(connectionId, new SocketException((int)SocketError.NotConnected));
}
}
// get connection info in case it's needed (IP etc.)
// (we should never pass the TcpClient to the outside)
public string GetClientAddress(int connectionId)
{
// find the connection
if (clients.TryGetValue(connectionId, out WebSocket client))
{
WebSocketImplementation wsClient = client as WebSocketImplementation;
return wsClient.Context.Client.Client.RemoteEndPoint.ToString();
}
return null;
}
// disconnect (kick) a client
public bool Disconnect(int connectionId)
{
// find the connection
if (clients.TryGetValue(connectionId, out WebSocket client))
{
clients.Remove(connectionId);
// just close it. client thread will take care of the rest.
client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
Debug.Log("Server.Disconnect connectionId:" + connectionId);
return true;
}
return false;
}
public override string ToString()
{
if (Active)
{
return $"Websocket server {listener.LocalEndpoint}";
}
return "";
}
}
}