aha
This commit is contained in:
8
Assets/Mirror/Transports/SimpleWeb/.cert.example.Json
Normal file
8
Assets/Mirror/Transports/SimpleWeb/.cert.example.Json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"_readme_1": "Make a copy of this file and update the fields below. (readme fields should be deleted)",
|
||||
"_readme_2": "Include the json file and cert with your server build ONLY (put them outside of asset folder)",
|
||||
"_readme_path": "path is relative from cwd not this json file",
|
||||
"_readme_password": "password can be empty or left out",
|
||||
"path": "./certs/MirrorLocal.pfx",
|
||||
"password": ""
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
guid: 4DD9F6E6E712911717200F85FA16CCAD
|
||||
fileFormatVersion: 2
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/.cert.example.Json
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/SimpleWeb/Editor.meta
Normal file
8
Assets/Mirror/Transports/SimpleWeb/Editor.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7a134b4d9eef45239a2d7caf7f52c3e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,71 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.SimpleWeb.Editor
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
[CustomPropertyDrawer(typeof(ClientWebsocketSettings))]
|
||||
public class ClientWebsocketSettingsDrawer : PropertyDrawer
|
||||
{
|
||||
readonly string websocketPortOptionName = nameof(ClientWebsocketSettings.ClientPortOption);
|
||||
readonly string customPortName = nameof(ClientWebsocketSettings.CustomClientPort);
|
||||
readonly GUIContent portOptionLabel = new GUIContent("Client Port Option",
|
||||
"Specify what port the client websocket connection uses (default same as server port)");
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
property.isExpanded = true;
|
||||
return SumPropertyHeights(property, websocketPortOptionName, customPortName);
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
DrawPortSettings(position, property);
|
||||
}
|
||||
|
||||
void DrawPortSettings(Rect position, SerializedProperty property)
|
||||
{
|
||||
SerializedProperty portOptionProp = property.FindPropertyRelative(websocketPortOptionName);
|
||||
SerializedProperty portProp = property.FindPropertyRelative(customPortName);
|
||||
float portOptionHeight = EditorGUI.GetPropertyHeight(portOptionProp);
|
||||
float portHeight = EditorGUI.GetPropertyHeight(portProp);
|
||||
float spacing = EditorGUIUtility.standardVerticalSpacing;
|
||||
bool wasEnabled = GUI.enabled;
|
||||
|
||||
position.height = portOptionHeight;
|
||||
|
||||
EditorGUI.PropertyField(position, portOptionProp, portOptionLabel);
|
||||
position.y += spacing + portOptionHeight;
|
||||
position.height = portHeight;
|
||||
|
||||
WebsocketPortOption portOption = (WebsocketPortOption)portOptionProp.enumValueIndex;
|
||||
if (portOption == WebsocketPortOption.MatchWebpageProtocol || portOption == WebsocketPortOption.DefaultSameAsServer)
|
||||
{
|
||||
int port = 0;
|
||||
if (property.serializedObject.targetObject is SimpleWebTransport swt)
|
||||
if (portOption == WebsocketPortOption.MatchWebpageProtocol)
|
||||
port = swt.clientUseWss ? 443 : 80;
|
||||
else
|
||||
port = swt.port;
|
||||
|
||||
GUI.enabled = false;
|
||||
EditorGUI.IntField(position, new GUIContent("Client Port"), port);
|
||||
GUI.enabled = wasEnabled;
|
||||
}
|
||||
else
|
||||
EditorGUI.PropertyField(position, portProp);
|
||||
|
||||
position.y += spacing + portHeight;
|
||||
}
|
||||
|
||||
float SumPropertyHeights(SerializedProperty property, params string[] propertyNames)
|
||||
{
|
||||
float totalHeight = 0;
|
||||
foreach (var name in propertyNames)
|
||||
totalHeight += EditorGUI.GetPropertyHeight(property.FindPropertyRelative(name)) + EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
return totalHeight;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d0e5b00ac8e45c99e68ad95cf843f80
|
||||
timeCreated: 1700081340
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb.meta
Normal file
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ae237a052b29fc4b8d000f48e545bb7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,7 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: AssemblyVersion("1.6.0")]
|
||||
|
||||
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")]
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee9e76201f7665244bd6ab8ea343a83f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/AssemblyInfo.cs
|
||||
uploadId: 736421
|
48
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md
Normal file
48
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md
Normal file
@ -0,0 +1,48 @@
|
||||
# [1.3.0](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.7...v1.3.0) (2022-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Allowing max message size to be increase to int32.max ([#2](https://github.com/James-Frowen/SimpleWebTransport/issues/2)) ([4cc60fd](https://github.com/James-Frowen/SimpleWebTransport/commit/4cc60fd67f3c65d90ced0e6f9f97d15d0368076d))
|
||||
|
||||
## [1.2.7](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.6...v1.2.7) (2022-02-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixing ObjectDisposedException in toString ([426de52](https://github.com/James-Frowen/SimpleWebTransport/commit/426de52ee4e98ac6212713b2b2272e3affb8fc99))
|
||||
|
||||
## [1.2.6](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.5...v1.2.6) (2022-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixing Runtime is not defined for unity 2021 ([945b50d](https://github.com/James-Frowen/SimpleWebTransport/commit/945b50dbad5b71c43e2bdaa4033f87d3f62c5572))
|
||||
|
||||
## [1.2.5](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.4...v1.2.5) (2022-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* updating Pointer_stringify to UTF8ToString ([2f5a74b](https://github.com/James-Frowen/SimpleWebTransport/commit/2f5a74ba10865e934be8d3b54ebfdeb14ca491f6))
|
||||
|
||||
## [1.2.4](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.3...v1.2.4) (2021-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding meta file for changelog ([ba5b164](https://github.com/James-Frowen/SimpleWebTransport/commit/ba5b1647aa5cc69ca80f5b52c542a9b5ee749c7f))
|
||||
|
||||
## [1.2.3](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.2...v1.2.3) (2021-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixing compile error in assemblyInfo ([7ee8380](https://github.com/James-Frowen/SimpleWebTransport/commit/7ee8380b4daf34d4e12017de55d8be481690046f))
|
||||
|
||||
## [1.2.2](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.1...v1.2.2) (2021-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixing release with empty commit ([068af74](https://github.com/James-Frowen/SimpleWebTransport/commit/068af74f7399354081f25181f90fb060b0fa1524))
|
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0ef23ac1c6a62546bbad5529b3bfdad
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client.meta
Normal file
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5faa957b8d9fc314ab7596ccf14750d9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
[Serializable]
|
||||
public struct ClientWebsocketSettings
|
||||
{
|
||||
public WebsocketPortOption ClientPortOption;
|
||||
public ushort CustomClientPort;
|
||||
}
|
||||
public enum WebsocketPortOption
|
||||
{
|
||||
DefaultSameAsServer,
|
||||
MatchWebpageProtocol,
|
||||
SpecifyPort
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20ed869c5ba54a56b750ac9e82be069f
|
||||
timeCreated: 1700425326
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public enum ClientState
|
||||
{
|
||||
NotConnected = 0,
|
||||
Connecting = 1,
|
||||
Connected = 2,
|
||||
Disconnecting = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client used to control websockets
|
||||
/// <para>Base class used by WebSocketClientWebGl and WebSocketClientStandAlone</para>
|
||||
/// </summary>
|
||||
public abstract class SimpleWebClient
|
||||
{
|
||||
readonly int maxMessagesPerTick;
|
||||
|
||||
protected ClientState state;
|
||||
protected readonly int maxMessageSize;
|
||||
protected readonly BufferPool bufferPool;
|
||||
|
||||
public readonly ConcurrentQueue<Message> receiveQueue = new ConcurrentQueue<Message>();
|
||||
|
||||
public ClientState ConnectionState => state;
|
||||
|
||||
public event Action onConnect;
|
||||
public event Action onDisconnect;
|
||||
public event Action<ArraySegment<byte>> onData;
|
||||
public event Action<Exception> onError;
|
||||
|
||||
public abstract void Connect(Uri serverAddress);
|
||||
public abstract void Disconnect();
|
||||
public abstract void Send(ArraySegment<byte> segment);
|
||||
|
||||
public static SimpleWebClient Create(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig)
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
return new WebSocketClientWebGl(maxMessageSize, maxMessagesPerTick);
|
||||
#else
|
||||
return new WebSocketClientStandAlone(maxMessageSize, maxMessagesPerTick, tcpConfig);
|
||||
#endif
|
||||
}
|
||||
|
||||
protected SimpleWebClient(int maxMessageSize, int maxMessagesPerTick)
|
||||
{
|
||||
this.maxMessageSize = maxMessageSize;
|
||||
this.maxMessagesPerTick = maxMessagesPerTick;
|
||||
bufferPool = new BufferPool(5, 20, maxMessageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes all new messages
|
||||
/// </summary>
|
||||
public void ProcessMessageQueue()
|
||||
{
|
||||
ProcessMessageQueue(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes all messages while <paramref name="behaviour"/> is enabled
|
||||
/// </summary>
|
||||
/// <param name="behaviour"></param>
|
||||
public void ProcessMessageQueue(MonoBehaviour behaviour)
|
||||
{
|
||||
int processedCount = 0;
|
||||
bool skipEnabled = behaviour == null;
|
||||
// check enabled every time in case behaviour was disabled after data
|
||||
while (
|
||||
(skipEnabled || behaviour.enabled) &&
|
||||
processedCount < maxMessagesPerTick &&
|
||||
// Dequeue last
|
||||
receiveQueue.TryDequeue(out Message next)
|
||||
)
|
||||
{
|
||||
processedCount++;
|
||||
|
||||
switch (next.type)
|
||||
{
|
||||
case EventType.Connected:
|
||||
onConnect?.Invoke();
|
||||
break;
|
||||
case EventType.Data:
|
||||
onData?.Invoke(next.data.ToSegment());
|
||||
next.data.Release();
|
||||
break;
|
||||
case EventType.Disconnected:
|
||||
onDisconnect?.Invoke();
|
||||
break;
|
||||
case EventType.Error:
|
||||
onError?.Invoke(next.exception);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (receiveQueue.Count > 0)
|
||||
Log.Warn("[SWT-SimpleWebClient]: ProcessMessageQueue has {0} remaining.", receiveQueue.Count);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13131761a0bf5a64dadeccd700fe26e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/SimpleWebClient.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9c19d05220a87c4cbbe4d1e422da0aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles Handshake to the server when it first connects
|
||||
/// <para>The client handshake does not need buffers to reduce allocations since it only happens once</para>
|
||||
/// </summary>
|
||||
internal class ClientHandshake
|
||||
{
|
||||
public bool TryHandshake(Connection conn, Uri uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
Stream stream = conn.stream;
|
||||
|
||||
byte[] keyBuffer = new byte[16];
|
||||
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
|
||||
rng.GetBytes(keyBuffer);
|
||||
|
||||
string key = Convert.ToBase64String(keyBuffer);
|
||||
string keySum = key + Constants.HandshakeGUID;
|
||||
byte[] keySumBytes = Encoding.ASCII.GetBytes(keySum);
|
||||
Log.Verbose("[SWT-ClientHandshake]: Handshake Hashing {0}", Encoding.ASCII.GetString(keySumBytes));
|
||||
|
||||
// SHA-1 is the websocket standard:
|
||||
// https://www.rfc-editor.org/rfc/rfc6455
|
||||
// we should follow the standard, even though SHA1 is considered weak:
|
||||
// https://stackoverflow.com/questions/38038841/why-is-sha-1-considered-insecure
|
||||
byte[] keySumHash = SHA1.Create().ComputeHash(keySumBytes);
|
||||
|
||||
string expectedResponse = Convert.ToBase64String(keySumHash);
|
||||
string handshake =
|
||||
$"GET {uri.PathAndQuery} HTTP/1.1\r\n" +
|
||||
$"Host: {uri.Host}:{uri.Port}\r\n" +
|
||||
$"Upgrade: websocket\r\n" +
|
||||
$"Connection: Upgrade\r\n" +
|
||||
$"Sec-WebSocket-Key: {key}\r\n" +
|
||||
$"Sec-WebSocket-Version: 13\r\n" +
|
||||
"\r\n";
|
||||
byte[] encoded = Encoding.ASCII.GetBytes(handshake);
|
||||
stream.Write(encoded, 0, encoded.Length);
|
||||
|
||||
byte[] responseBuffer = new byte[1000];
|
||||
|
||||
int? lengthOrNull = ReadHelper.SafeReadTillMatch(stream, responseBuffer, 0, responseBuffer.Length, Constants.endOfHandshake);
|
||||
|
||||
if (!lengthOrNull.HasValue)
|
||||
{
|
||||
Log.Error("[SWT-ClientHandshake]: Connection closed before handshake");
|
||||
return false;
|
||||
}
|
||||
|
||||
string responseString = Encoding.ASCII.GetString(responseBuffer, 0, lengthOrNull.Value);
|
||||
Log.Verbose("[SWT-ClientHandshake]: Handshake Response {0}", responseString);
|
||||
|
||||
string acceptHeader = "Sec-WebSocket-Accept: ";
|
||||
int startIndex = responseString.IndexOf(acceptHeader, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
if (startIndex < 0)
|
||||
{
|
||||
Log.Error("[SWT-ClientHandshake]: Unexpected Handshake Response {0}", responseString);
|
||||
return false;
|
||||
}
|
||||
|
||||
startIndex += acceptHeader.Length;
|
||||
int endIndex = responseString.IndexOf("\r\n", startIndex);
|
||||
string responseKey = responseString.Substring(startIndex, endIndex - startIndex);
|
||||
|
||||
if (responseKey != expectedResponse)
|
||||
{
|
||||
Log.Error("[SWT-ClientHandshake]: Response key incorrect\nExpected:{0}\nResponse:{1}\nThis can happen if Websocket Protocol is not installed in Windows Server Roles.", expectedResponse, responseKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ffdcabc9e28f764a94fc4efc82d3e8b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
internal class ClientSslHelper
|
||||
{
|
||||
internal bool TryCreateStream(Connection conn, Uri uri)
|
||||
{
|
||||
NetworkStream stream = conn.client.GetStream();
|
||||
if (uri.Scheme != "wss" && uri.Scheme != "https")
|
||||
{
|
||||
conn.stream = stream;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
conn.stream = CreateStream(stream, uri);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("[SWT-ClientSslHelper]: Create SSLStream Failed: {0}\n{1}\n\n", e.Message, e.StackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Stream CreateStream(NetworkStream stream, Uri uri)
|
||||
{
|
||||
SslStream sslStream = new SslStream(stream, true, ValidateServerCertificate);
|
||||
sslStream.AuthenticateAsClient(uri.Host);
|
||||
return sslStream;
|
||||
}
|
||||
|
||||
static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
// Do not allow this client to communicate with unauthenticated servers.
|
||||
|
||||
// only accept if no errors
|
||||
return sslPolicyErrors == SslPolicyErrors.None;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46055a75559a79849a750f39a766db61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientSslHelper.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public class WebSocketClientStandAlone : SimpleWebClient
|
||||
{
|
||||
readonly ClientSslHelper sslHelper;
|
||||
readonly ClientHandshake handshake;
|
||||
readonly TcpConfig tcpConfig;
|
||||
Connection conn;
|
||||
|
||||
internal WebSocketClientStandAlone(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig) : base(maxMessageSize, maxMessagesPerTick)
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
throw new NotSupportedException();
|
||||
#else
|
||||
sslHelper = new ClientSslHelper();
|
||||
handshake = new ClientHandshake();
|
||||
this.tcpConfig = tcpConfig;
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void Connect(Uri serverAddress)
|
||||
{
|
||||
state = ClientState.Connecting;
|
||||
|
||||
// create connection here before thread so that send queue exist before connected
|
||||
TcpClient client = new TcpClient();
|
||||
tcpConfig.ApplyTo(client);
|
||||
|
||||
// create connection object here so dispose correctly disconnects on failed connect
|
||||
conn = new Connection(client, AfterConnectionDisposed);
|
||||
|
||||
Thread receiveThread = new Thread(() => ConnectAndReceiveLoop(serverAddress));
|
||||
receiveThread.IsBackground = true;
|
||||
receiveThread.Start();
|
||||
}
|
||||
|
||||
void ConnectAndReceiveLoop(Uri serverAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
// connection created above
|
||||
TcpClient client = conn.client;
|
||||
conn.receiveThread = Thread.CurrentThread;
|
||||
|
||||
try
|
||||
{
|
||||
client.Connect(serverAddress.Host, serverAddress.Port);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
client.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
|
||||
bool success = sslHelper.TryCreateStream(conn, serverAddress);
|
||||
if (!success)
|
||||
{
|
||||
Log.Warn("[SWT-WebSocketClientStandAlone]: Failed to create Stream with {0}", serverAddress);
|
||||
conn.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
success = handshake.TryHandshake(conn, serverAddress);
|
||||
if (!success)
|
||||
{
|
||||
Log.Warn("[SWT-WebSocketClientStandAlone]: Failed Handshake with {0}", serverAddress);
|
||||
conn.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info("[SWT-WebSocketClientStandAlone]: HandShake Successful with {0}", serverAddress);
|
||||
|
||||
state = ClientState.Connected;
|
||||
|
||||
receiveQueue.Enqueue(new Message(EventType.Connected));
|
||||
|
||||
Thread sendThread = new Thread(() =>
|
||||
{
|
||||
SendLoop.Config sendConfig = new SendLoop.Config(
|
||||
conn,
|
||||
bufferSize: Constants.HeaderSize + Constants.MaskSize + maxMessageSize,
|
||||
setMask: true);
|
||||
|
||||
SendLoop.Loop(sendConfig);
|
||||
});
|
||||
|
||||
conn.sendThread = sendThread;
|
||||
sendThread.IsBackground = true;
|
||||
sendThread.Start();
|
||||
|
||||
ReceiveLoop.Config config = new ReceiveLoop.Config(conn,
|
||||
maxMessageSize,
|
||||
false,
|
||||
receiveQueue,
|
||||
bufferPool);
|
||||
ReceiveLoop.Loop(config);
|
||||
}
|
||||
catch (ThreadInterruptedException e) { Log.InfoException(e); }
|
||||
catch (ThreadAbortException) { Log.Error("[SWT-WebSocketClientStandAlone]: Thread Abort Exception"); }
|
||||
catch (Exception e) { Log.Exception(e); }
|
||||
finally
|
||||
{
|
||||
// close here in case connect fails
|
||||
conn?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void AfterConnectionDisposed(Connection conn)
|
||||
{
|
||||
state = ClientState.NotConnected;
|
||||
// make sure Disconnected event is only called once
|
||||
receiveQueue.Enqueue(new Message(EventType.Disconnected));
|
||||
}
|
||||
|
||||
public override void Disconnect()
|
||||
{
|
||||
state = ClientState.Disconnecting;
|
||||
Log.Verbose("[SWT-WebSocketClientStandAlone]: Disconnect Called");
|
||||
|
||||
if (conn == null)
|
||||
state = ClientState.NotConnected;
|
||||
else
|
||||
conn?.Dispose();
|
||||
}
|
||||
|
||||
public override void Send(ArraySegment<byte> segment)
|
||||
{
|
||||
ArrayBuffer buffer = bufferPool.Take(segment.Count);
|
||||
buffer.CopyFrom(segment);
|
||||
|
||||
conn.sendQueue.Enqueue(buffer);
|
||||
conn.sendPending.Set();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05a9c87dea309e241a9185e5aa0d72ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/WebSocketClientStandAlone.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7142349d566213c4abc763afaf4d91a1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
#if UNITY_WEBGL
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
internal static class SimpleWebJSLib
|
||||
{
|
||||
#if UNITY_WEBGL
|
||||
[DllImport("__Internal")]
|
||||
internal static extern bool IsConnected(int index);
|
||||
|
||||
#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments
|
||||
[DllImport("__Internal")]
|
||||
#pragma warning restore CA2101 // Specify marshaling for P/Invoke string arguments
|
||||
internal static extern int Connect(string address, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
internal static extern void Disconnect(int index);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
internal static extern bool Send(int index, byte[] array, int offset, int length);
|
||||
#else
|
||||
internal static bool IsConnected(int index) => throw new NotSupportedException();
|
||||
|
||||
internal static int Connect(string address, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback) => throw new NotSupportedException();
|
||||
|
||||
internal static void Disconnect(int index) => throw new NotSupportedException();
|
||||
|
||||
internal static bool Send(int index, byte[] array, int offset, int length) => throw new NotSupportedException();
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97b96a0b65c104443977473323c2ff35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/SimpleWebJSLib.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AOT;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
|
||||
// Unity 2019 doesn't have ArraySegment.ToArray() yet.
|
||||
public static class Extensions
|
||||
{
|
||||
public static byte[] ToArray(this ArraySegment<byte> segment)
|
||||
{
|
||||
byte[] array = new byte[segment.Count];
|
||||
Array.Copy(segment.Array, segment.Offset, array, 0, segment.Count);
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
public class WebSocketClientWebGl : SimpleWebClient
|
||||
{
|
||||
static readonly Dictionary<int, WebSocketClientWebGl> instances = new Dictionary<int, WebSocketClientWebGl>();
|
||||
|
||||
[MonoPInvokeCallback(typeof(Action<int>))]
|
||||
static void OpenCallback(int index) => instances[index].onOpen();
|
||||
|
||||
[MonoPInvokeCallback(typeof(Action<int>))]
|
||||
static void CloseCallBack(int index) => instances[index].onClose();
|
||||
|
||||
[MonoPInvokeCallback(typeof(Action<int, IntPtr, int>))]
|
||||
static void MessageCallback(int index, IntPtr bufferPtr, int count) => instances[index].onMessage(bufferPtr, count);
|
||||
|
||||
[MonoPInvokeCallback(typeof(Action<int>))]
|
||||
static void ErrorCallback(int index) => instances[index].onErr();
|
||||
|
||||
/// <summary>
|
||||
/// key for instances sent between c# and js
|
||||
/// </summary>
|
||||
int index;
|
||||
|
||||
/// <summary>
|
||||
/// Queue for messages sent by high level while still connecting, they will be sent after onOpen is called.
|
||||
/// <para>
|
||||
/// This is a workaround for anything that calls Send immediately after Connect.
|
||||
/// Without this the JS websocket will give errors.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
Queue<byte[]> ConnectingSendQueue;
|
||||
|
||||
public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index);
|
||||
|
||||
internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base(maxMessageSize, maxMessagesPerTick)
|
||||
{
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
throw new NotSupportedException();
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void Connect(Uri serverAddress)
|
||||
{
|
||||
index = SimpleWebJSLib.Connect(serverAddress.ToString(), OpenCallback, CloseCallBack, MessageCallback, ErrorCallback);
|
||||
instances.Add(index, this);
|
||||
state = ClientState.Connecting;
|
||||
}
|
||||
|
||||
public override void Disconnect()
|
||||
{
|
||||
state = ClientState.Disconnecting;
|
||||
// disconnect should cause closeCallback and OnDisconnect to be called
|
||||
SimpleWebJSLib.Disconnect(index);
|
||||
}
|
||||
|
||||
public override void Send(ArraySegment<byte> segment)
|
||||
{
|
||||
if (segment.Count > maxMessageSize)
|
||||
{
|
||||
Log.Error("[SWT-WebSocketClientWebGl]: Cant send message with length {0} because it is over the max size of {1}", segment.Count, maxMessageSize);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == ClientState.Connected)
|
||||
{
|
||||
SimpleWebJSLib.Send(index, segment.Array, segment.Offset, segment.Count);
|
||||
}
|
||||
else if (ConnectingSendQueue == null)
|
||||
{
|
||||
ConnectingSendQueue = new Queue<byte[]>();
|
||||
ConnectingSendQueue.Enqueue(segment.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
void onOpen()
|
||||
{
|
||||
receiveQueue.Enqueue(new Message(EventType.Connected));
|
||||
state = ClientState.Connected;
|
||||
|
||||
if (ConnectingSendQueue != null)
|
||||
{
|
||||
while (ConnectingSendQueue.Count > 0)
|
||||
{
|
||||
byte[] next = ConnectingSendQueue.Dequeue();
|
||||
SimpleWebJSLib.Send(index, next, 0, next.Length);
|
||||
}
|
||||
|
||||
ConnectingSendQueue = null;
|
||||
}
|
||||
}
|
||||
|
||||
void onClose()
|
||||
{
|
||||
// this code should be last in this class
|
||||
|
||||
receiveQueue.Enqueue(new Message(EventType.Disconnected));
|
||||
state = ClientState.NotConnected;
|
||||
instances.Remove(index);
|
||||
}
|
||||
|
||||
void onMessage(IntPtr bufferPtr, int count)
|
||||
{
|
||||
try
|
||||
{
|
||||
ArrayBuffer buffer = bufferPool.Take(count);
|
||||
buffer.CopyFrom(bufferPtr, count);
|
||||
|
||||
receiveQueue.Enqueue(new Message(buffer));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("[SWT-WebSocketClientWebGl]: onMessage {0}: {1}\n{2}", e.GetType(), e.Message, e.StackTrace);
|
||||
receiveQueue.Enqueue(new Message(e));
|
||||
}
|
||||
}
|
||||
|
||||
void onErr()
|
||||
{
|
||||
receiveQueue.Enqueue(new Message(new Exception("Javascript Websocket error")));
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 015c5b1915fd1a64cbe36444d16b2f7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1999985791b91b9458059e88404885a7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,123 @@
|
||||
// this will create a global object
|
||||
const SimpleWeb =
|
||||
{
|
||||
webSockets: [],
|
||||
next: 1,
|
||||
GetWebSocket: function (index)
|
||||
{
|
||||
return SimpleWeb.webSockets[index]
|
||||
},
|
||||
AddNextSocket: function (webSocket)
|
||||
{
|
||||
var index = SimpleWeb.next;
|
||||
SimpleWeb.next++;
|
||||
SimpleWeb.webSockets[index] = webSocket;
|
||||
return index;
|
||||
},
|
||||
RemoveSocket: function (index)
|
||||
{
|
||||
SimpleWeb.webSockets[index] = undefined;
|
||||
},
|
||||
};
|
||||
|
||||
function IsConnected(index)
|
||||
{
|
||||
var webSocket = SimpleWeb.GetWebSocket(index);
|
||||
if (webSocket)
|
||||
return webSocket.readyState === webSocket.OPEN;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr)
|
||||
{
|
||||
// fix for unity 2021 because unity bug in .jslib
|
||||
if (typeof Runtime === "undefined")
|
||||
{
|
||||
// if unity doesn't create Runtime, then make it here
|
||||
// dont ask why this works, just be happy that it does
|
||||
var Runtime = { dynCall: dynCall }
|
||||
}
|
||||
|
||||
const address = UTF8ToString(addressPtr);
|
||||
console.log("Connecting to " + address);
|
||||
|
||||
// Create webSocket connection.
|
||||
var webSocket = new WebSocket(address);
|
||||
webSocket.binaryType = 'arraybuffer';
|
||||
|
||||
const index = SimpleWeb.AddNextSocket(webSocket);
|
||||
|
||||
// Connection opened
|
||||
webSocket.onopen = function(event)
|
||||
{
|
||||
console.log("Connected to " + address);
|
||||
Runtime.dynCall('vi', openCallbackPtr, [index]);
|
||||
};
|
||||
|
||||
webSocket.onclose = function(event)
|
||||
{
|
||||
console.log("Disconnected from " + address);
|
||||
Runtime.dynCall('vi', closeCallBackPtr, [index]);
|
||||
};
|
||||
|
||||
webSocket.onmessage = function(event)
|
||||
{
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
var array = new Uint8Array(event.data);
|
||||
var arrayLength = array.length;
|
||||
|
||||
var bufferPtr = _malloc(arrayLength);
|
||||
var dataBuffer = new Uint8Array(HEAPU8.buffer, bufferPtr, arrayLength);
|
||||
dataBuffer.set(array);
|
||||
|
||||
Runtime.dynCall('viii', messageCallbackPtr, [index, bufferPtr, arrayLength]);
|
||||
_free(bufferPtr);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error("message type not supported")
|
||||
}
|
||||
};
|
||||
|
||||
webSocket.onerror = function(event)
|
||||
{
|
||||
console.error('Socket Error', event);
|
||||
Runtime.dynCall('vi', errorCallbackPtr, [index]);
|
||||
};
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function Disconnect(index) {
|
||||
var webSocket = SimpleWeb.GetWebSocket(index);
|
||||
if (webSocket)
|
||||
webSocket.close(1000, "Disconnect Called by Mirror");
|
||||
|
||||
SimpleWeb.RemoveSocket(index);
|
||||
}
|
||||
|
||||
function Send(index, arrayPtr, offset, length) {
|
||||
var webSocket = SimpleWeb.GetWebSocket(index);
|
||||
if (webSocket)
|
||||
{
|
||||
const start = arrayPtr + offset;
|
||||
const end = start + length;
|
||||
const data = HEAPU8.buffer.slice(start, end);
|
||||
webSocket.send(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const SimpleWebLib =
|
||||
{
|
||||
$SimpleWeb: SimpleWeb,
|
||||
IsConnected,
|
||||
Connect,
|
||||
Disconnect,
|
||||
Send
|
||||
};
|
||||
|
||||
autoAddDeps(SimpleWebLib, '$SimpleWeb');
|
||||
mergeInto(LibraryManager.library, SimpleWebLib);
|
@ -0,0 +1,44 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54452a8c6d2ca9b49a8c79f81b50305c
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
Facebook: WebGL
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
- first:
|
||||
WebGL: WebGL
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common.meta
Normal file
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 564d2cd3eee5b21419553c0528739d1b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public interface IBufferOwner
|
||||
{
|
||||
void Return(ArrayBuffer buffer);
|
||||
}
|
||||
|
||||
public sealed class ArrayBuffer : IDisposable
|
||||
{
|
||||
readonly IBufferOwner owner;
|
||||
|
||||
public readonly byte[] array;
|
||||
|
||||
/// <summary>
|
||||
/// number of bytes written to buffer
|
||||
/// </summary>
|
||||
public int count { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many times release needs to be called before buffer is returned to pool
|
||||
/// <para>This allows the buffer to be used in multiple places at the same time</para>
|
||||
/// </summary>
|
||||
public void SetReleasesRequired(int required)
|
||||
{
|
||||
releasesRequired = required;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many times release needs to be called before buffer is returned to pool
|
||||
/// <para>This allows the buffer to be used in multiple places at the same time</para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value is normally 0, but can be changed to require release to be called multiple times
|
||||
/// </remarks>
|
||||
int releasesRequired;
|
||||
|
||||
public ArrayBuffer(IBufferOwner owner, int size)
|
||||
{
|
||||
this.owner = owner;
|
||||
array = new byte[size];
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
int newValue = Interlocked.Decrement(ref releasesRequired);
|
||||
if (newValue <= 0)
|
||||
{
|
||||
count = 0;
|
||||
owner?.Return(this);
|
||||
}
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
Release();
|
||||
}
|
||||
|
||||
public void CopyTo(byte[] target, int offset)
|
||||
{
|
||||
if (count > (target.Length + offset))
|
||||
throw new ArgumentException($"{nameof(count)} was greater than {nameof(target)}.length", nameof(target));
|
||||
|
||||
Buffer.BlockCopy(array, 0, target, offset, count);
|
||||
}
|
||||
|
||||
public void CopyFrom(ArraySegment<byte> segment)
|
||||
{
|
||||
CopyFrom(segment.Array, segment.Offset, segment.Count);
|
||||
}
|
||||
|
||||
public void CopyFrom(byte[] source, int offset, int length)
|
||||
{
|
||||
if (length > array.Length)
|
||||
throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
|
||||
|
||||
count = length;
|
||||
Buffer.BlockCopy(source, offset, array, 0, length);
|
||||
}
|
||||
|
||||
public void CopyFrom(IntPtr bufferPtr, int length)
|
||||
{
|
||||
if (length > array.Length)
|
||||
throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
|
||||
|
||||
count = length;
|
||||
Marshal.Copy(bufferPtr, array, 0, length);
|
||||
}
|
||||
|
||||
public ArraySegment<byte> ToSegment() => new ArraySegment<byte>(array, 0, count);
|
||||
|
||||
[Conditional("UNITY_ASSERTIONS")]
|
||||
internal void Validate(int arraySize)
|
||||
{
|
||||
if (array.Length != arraySize)
|
||||
Log.Error("[SWT-ArrayBuffer]: Buffer that was returned had an array of the wrong size");
|
||||
}
|
||||
}
|
||||
|
||||
internal class BufferBucket : IBufferOwner
|
||||
{
|
||||
public readonly int arraySize;
|
||||
readonly ConcurrentQueue<ArrayBuffer> buffers;
|
||||
|
||||
/// <summary>
|
||||
/// keeps track of how many arrays are taken vs returned
|
||||
/// </summary>
|
||||
internal int _current = 0;
|
||||
|
||||
public BufferBucket(int arraySize)
|
||||
{
|
||||
this.arraySize = arraySize;
|
||||
buffers = new ConcurrentQueue<ArrayBuffer>();
|
||||
}
|
||||
|
||||
public ArrayBuffer Take()
|
||||
{
|
||||
IncrementCreated();
|
||||
if (buffers.TryDequeue(out ArrayBuffer buffer))
|
||||
return buffer;
|
||||
else
|
||||
{
|
||||
Log.Flood($"[SWT-BufferBucket]: BufferBucket({arraySize}) create new");
|
||||
return new ArrayBuffer(this, arraySize);
|
||||
}
|
||||
}
|
||||
|
||||
public void Return(ArrayBuffer buffer)
|
||||
{
|
||||
DecrementCreated();
|
||||
buffer.Validate(arraySize);
|
||||
buffers.Enqueue(buffer);
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
void IncrementCreated()
|
||||
{
|
||||
int next = Interlocked.Increment(ref _current);
|
||||
Log.Flood($"[SWT-BufferBucket]: BufferBucket({arraySize}) count:{next}");
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
void DecrementCreated()
|
||||
{
|
||||
int next = Interlocked.Decrement(ref _current);
|
||||
Log.Flood($"[SWT-BufferBucket]: BufferBucket({arraySize}) count:{next}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection of different sized buffers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Problem: <br/>
|
||||
/// * Need to cached byte[] so that new ones aren't created each time <br/>
|
||||
/// * Arrays sent are multiple different sizes <br/>
|
||||
/// * Some message might be big so need buffers to cover that size <br/>
|
||||
/// * Most messages will be small compared to max message size <br/>
|
||||
/// </para>
|
||||
/// <br/>
|
||||
/// <para>
|
||||
/// Solution: <br/>
|
||||
/// * Create multiple groups of buffers covering the range of allowed sizes <br/>
|
||||
/// * Split range exponentially (using math.log) so that there are more groups for small buffers <br/>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class BufferPool
|
||||
{
|
||||
internal readonly BufferBucket[] buckets;
|
||||
readonly int bucketCount;
|
||||
readonly int smallest;
|
||||
readonly int largest;
|
||||
|
||||
public BufferPool(int bucketCount, int smallest, int largest)
|
||||
{
|
||||
if (bucketCount < 2) throw new ArgumentException("Count must be at least 2");
|
||||
if (smallest < 1) throw new ArgumentException("Smallest must be at least 1");
|
||||
if (largest < smallest) throw new ArgumentException("Largest must be greater than smallest");
|
||||
|
||||
this.bucketCount = bucketCount;
|
||||
this.smallest = smallest;
|
||||
this.largest = largest;
|
||||
|
||||
// split range over log scale (more buckets for smaller sizes)
|
||||
double minLog = Math.Log(this.smallest);
|
||||
double maxLog = Math.Log(this.largest);
|
||||
double range = maxLog - minLog;
|
||||
double each = range / (bucketCount - 1);
|
||||
|
||||
buckets = new BufferBucket[bucketCount];
|
||||
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
{
|
||||
double size = smallest * Math.Pow(Math.E, each * i);
|
||||
buckets[i] = new BufferBucket((int)Math.Ceiling(size));
|
||||
}
|
||||
|
||||
Validate();
|
||||
|
||||
// Example
|
||||
// 5 count
|
||||
// 20 smallest
|
||||
// 16400 largest
|
||||
|
||||
// 3.0 log 20
|
||||
// 9.7 log 16400
|
||||
|
||||
// 6.7 range 9.7 - 3
|
||||
// 1.675 each 6.7 / (5-1)
|
||||
|
||||
// 20 e^ (3 + 1.675 * 0)
|
||||
// 107 e^ (3 + 1.675 * 1)
|
||||
// 572 e^ (3 + 1.675 * 2)
|
||||
// 3056 e^ (3 + 1.675 * 3)
|
||||
// 16,317 e^ (3 + 1.675 * 4)
|
||||
|
||||
// precision wont be lose when using doubles
|
||||
}
|
||||
|
||||
[Conditional("UNITY_ASSERTIONS")]
|
||||
void Validate()
|
||||
{
|
||||
if (buckets[0].arraySize != smallest)
|
||||
Log.Error("[SWT-BufferPool]: BufferPool Failed to create bucket for smallest. bucket:{0} smallest:{1}", buckets[0].arraySize, smallest);
|
||||
|
||||
int largestBucket = buckets[bucketCount - 1].arraySize;
|
||||
// rounded using Ceiling, so allowed to be 1 more that largest
|
||||
if (largestBucket != largest && largestBucket != largest + 1)
|
||||
Log.Error("[SWT-BufferPool]: BufferPool Failed to create bucket for largest. bucket:{0} smallest:{1}", largestBucket, largest);
|
||||
}
|
||||
|
||||
public ArrayBuffer Take(int size)
|
||||
{
|
||||
if (size > largest)
|
||||
throw new ArgumentException($"Size ({size}) is greater than largest ({largest})");
|
||||
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
if (size <= buckets[i].arraySize)
|
||||
return buckets[i].Take();
|
||||
|
||||
throw new ArgumentException($"Size ({size}) is greater than largest ({largest})");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94ae50f3ec35667469b861b12cd72f92
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
internal sealed class Connection : IDisposable
|
||||
{
|
||||
readonly object disposedLock = new object();
|
||||
|
||||
public const int IdNotSet = -1;
|
||||
public TcpClient client;
|
||||
public int connId = IdNotSet;
|
||||
|
||||
/// <summary>
|
||||
/// Connect request, sent from client to start handshake
|
||||
/// <para>Only valid on server</para>
|
||||
/// </summary>
|
||||
public Request request;
|
||||
/// <summary>
|
||||
/// RemoteEndpoint address or address from request header
|
||||
/// <para>Only valid on server</para>
|
||||
/// </summary>
|
||||
public string remoteAddress;
|
||||
|
||||
public Stream stream;
|
||||
public Thread receiveThread;
|
||||
public Thread sendThread;
|
||||
|
||||
public ManualResetEventSlim sendPending = new ManualResetEventSlim(false);
|
||||
public ConcurrentQueue<ArrayBuffer> sendQueue = new ConcurrentQueue<ArrayBuffer>();
|
||||
|
||||
public Action<Connection> onDispose;
|
||||
volatile bool hasDisposed;
|
||||
|
||||
public Connection(TcpClient client, Action<Connection> onDispose)
|
||||
{
|
||||
this.client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
this.onDispose = onDispose;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// disposes client and stops threads
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Log.Verbose("[SWT-Connection]: Dispose {0}", ToString());
|
||||
|
||||
// check hasDisposed first to stop ThreadInterruptedException on lock
|
||||
if (hasDisposed) return;
|
||||
|
||||
Log.Verbose("[SWT-Connection]: Connection Close: {0}", ToString());
|
||||
|
||||
lock (disposedLock)
|
||||
{
|
||||
// check hasDisposed again inside lock to make sure no other object has called this
|
||||
if (hasDisposed) return;
|
||||
|
||||
hasDisposed = true;
|
||||
|
||||
// stop threads first so they don't try to use disposed objects
|
||||
receiveThread.Interrupt();
|
||||
sendThread?.Interrupt();
|
||||
|
||||
try
|
||||
{
|
||||
// stream
|
||||
stream?.Dispose();
|
||||
stream = null;
|
||||
client.Dispose();
|
||||
client = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(e);
|
||||
}
|
||||
|
||||
sendPending.Dispose();
|
||||
|
||||
// release all buffers in send queue
|
||||
while (sendQueue.TryDequeue(out ArrayBuffer buffer))
|
||||
buffer.Release();
|
||||
|
||||
onDispose.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
// remoteAddress isn't set until after handshake
|
||||
if (hasDisposed)
|
||||
return $"[Conn:{connId}, Disposed]";
|
||||
else if (!string.IsNullOrWhiteSpace(remoteAddress))
|
||||
return $"[Conn:{connId}, endPoint:{remoteAddress}]";
|
||||
else
|
||||
try
|
||||
{
|
||||
EndPoint endpoint = client?.Client?.RemoteEndPoint;
|
||||
return $"[Conn:{connId}, endPoint:{endpoint}]";
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return $"[Conn:{connId}, endPoint:n/a]";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address based on the <see cref="request"/> and RemoteEndPoint
|
||||
/// <para>Called after ServerHandShake is accepted</para>
|
||||
/// </summary>
|
||||
internal string CalculateAddress()
|
||||
{
|
||||
if (request.Headers.TryGetValue("X-Forwarded-For", out string forwardFor))
|
||||
{
|
||||
string actualClientIP = forwardFor.ToString().Split(',').First();
|
||||
// Remove the port number from the address
|
||||
return actualClientIP.Split(':').First();
|
||||
}
|
||||
else
|
||||
{
|
||||
IPEndPoint ipEndPoint = (IPEndPoint)client.Client.RemoteEndPoint;
|
||||
IPAddress ipAddress = ipEndPoint.Address;
|
||||
if (ipAddress.IsIPv4MappedToIPv6)
|
||||
ipAddress = ipAddress.MapToIPv4();
|
||||
|
||||
return ipAddress.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a13073c2b49d39943888df45174851bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Connection.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,76 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant values that should never change
|
||||
/// <para>
|
||||
/// Some values are from https://tools.ietf.org/html/rfc6455
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Header is at most 4 bytes
|
||||
/// <para>
|
||||
/// If message is less than 125 then header is 2 bytes, else header is 4 bytes
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public const int HeaderSize = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Smallest size of header
|
||||
/// <para>
|
||||
/// If message is less than 125 then header is 2 bytes, else header is 4 bytes
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public const int HeaderMinSize = 2;
|
||||
|
||||
/// <summary>
|
||||
/// bytes for short length
|
||||
/// </summary>
|
||||
public const int ShortLength = 2;
|
||||
|
||||
/// <summary>
|
||||
/// bytes for long length
|
||||
/// </summary>
|
||||
public const int LongLength = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Message mask is always 4 bytes
|
||||
/// </summary>
|
||||
public const int MaskSize = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Max size of a message for length to be 1 byte long
|
||||
/// <para>
|
||||
/// payload length between 0-125
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public const int BytePayloadLength = 125;
|
||||
|
||||
/// <summary>
|
||||
/// if payload length is 126 when next 2 bytes will be the length
|
||||
/// </summary>
|
||||
public const int UshortPayloadLength = 126;
|
||||
|
||||
/// <summary>
|
||||
/// if payload length is 127 when next 8 bytes will be the length
|
||||
/// </summary>
|
||||
public const int UlongPayloadLength = 127;
|
||||
|
||||
/// <summary>
|
||||
/// Guid used for WebSocket Protocol
|
||||
/// </summary>
|
||||
public const string HandshakeGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
public static readonly int HandshakeGUIDLength = HandshakeGUID.Length;
|
||||
|
||||
public static readonly byte[] HandshakeGUIDBytes = Encoding.ASCII.GetBytes(HandshakeGUID);
|
||||
|
||||
/// <summary>
|
||||
/// Handshake messages will end with \r\n\r\n
|
||||
/// </summary>
|
||||
public static readonly byte[] endOfHandshake = new byte[4] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85d110a089d6ad348abf2d073ebce7cd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Constants.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,10 @@
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public enum EventType
|
||||
{
|
||||
Connected,
|
||||
Data,
|
||||
Disconnected,
|
||||
Error
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d9cd7d2b5229ab42a12e82ae17d0347
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/EventType.cs
|
||||
uploadId: 736421
|
270
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs
Normal file
270
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs
Normal file
@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Conditional = System.Diagnostics.ConditionalAttribute;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public static class Log
|
||||
{
|
||||
// The.NET console color names map to the following approximate CSS color names:
|
||||
|
||||
// Black: Black
|
||||
// Blue: Blue
|
||||
// Cyan: Aqua or Cyan
|
||||
// DarkBlue: DarkBlue
|
||||
// DarkCyan: DarkCyan
|
||||
// DarkGray: DarkGray
|
||||
// DarkGreen: DarkGreen
|
||||
// DarkMagenta: DarkMagenta
|
||||
// DarkRed: DarkRed
|
||||
// DarkYellow: DarkOrange or DarkGoldenRod
|
||||
// Gray: Gray
|
||||
// Green: Green
|
||||
// Magenta: Magenta
|
||||
// Red: Red
|
||||
// White: White
|
||||
// Yellow: Yellow
|
||||
|
||||
// We can't use colors that are close to white or black because
|
||||
// they won't show up well in the server console or browser console
|
||||
|
||||
public enum Levels
|
||||
{
|
||||
Flood,
|
||||
Verbose,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
None
|
||||
}
|
||||
|
||||
public static ILogger logger = Debug.unityLogger;
|
||||
public static Levels minLogLevel = Levels.None;
|
||||
|
||||
/// <summary>
|
||||
/// Logs all exceptions to console
|
||||
/// </summary>
|
||||
/// <param name="e">Exception to log</param>
|
||||
public static void Exception(Exception e)
|
||||
{
|
||||
#if UNITY_SERVER || UNITY_WEBGL
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[SWT:Exception] {e.GetType().Name}: {e.Message}\n{e.StackTrace}\n\n");
|
||||
Console.ResetColor();
|
||||
#else
|
||||
logger.Log(LogType.Exception, $"[SWT:Exception] {e.GetType().Name}: {e.Message}\n{e.StackTrace}\n\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs flood to console if minLogLevel is set to Flood or lower
|
||||
/// </summary>
|
||||
/// <param name="msg">Message text to log</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void Flood(string msg)
|
||||
{
|
||||
if (minLogLevel > Levels.Flood) return;
|
||||
|
||||
#if UNITY_SERVER || UNITY_WEBGL
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
logger.Log(LogType.Log, msg.Trim());
|
||||
Console.ResetColor();
|
||||
#else
|
||||
logger.Log(LogType.Log, msg.Trim());
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs buffer to console if minLogLevel is set to Flood or lower
|
||||
/// <para>Debug mode requrired, e.g. Unity Editor of Develpment Build</para>
|
||||
/// </summary>
|
||||
/// <param name="label">Source of the log message</param>
|
||||
/// <param name="buffer">Byte array to be logged</param>
|
||||
/// <param name="offset">starting point of byte array</param>
|
||||
/// <param name="length">number of bytes to read</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void DumpBuffer(string label, byte[] buffer, int offset, int length)
|
||||
{
|
||||
if (minLogLevel > Levels.Flood) return;
|
||||
|
||||
#if UNITY_SERVER || UNITY_WEBGL
|
||||
Console.ForegroundColor = ConsoleColor.DarkBlue;
|
||||
logger.Log(LogType.Log, $"{label}: {BufferToString(buffer, offset, length)}");
|
||||
Console.ResetColor();
|
||||
#else
|
||||
logger.Log(LogType.Log, $"<color=cyan>{label}: {BufferToString(buffer, offset, length)}</color>");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs buffer to console if minLogLevel is set to Flood or lower
|
||||
/// <para>Debug mode requrired, e.g. Unity Editor of Develpment Build</para>
|
||||
/// </summary>
|
||||
/// <param name="label">Source of the log message</param>
|
||||
/// <param name="arrayBuffer">ArrayBuffer to show details for</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void DumpBuffer(string label, ArrayBuffer arrayBuffer)
|
||||
{
|
||||
if (minLogLevel > Levels.Flood) return;
|
||||
|
||||
#if UNITY_SERVER || UNITY_WEBGL
|
||||
Console.ForegroundColor = ConsoleColor.DarkBlue;
|
||||
logger.Log(LogType.Log, $"{label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}");
|
||||
Console.ResetColor();
|
||||
#else
|
||||
logger.Log(LogType.Log, $"<color=cyan>{label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}</color>");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs verbose to console if minLogLevel is set to Verbose or lower
|
||||
/// </summary>
|
||||
/// <param name="msg">Message text to log</param>
|
||||
public static void Verbose(string msg)
|
||||
{
|
||||
if (minLogLevel > Levels.Verbose) return;
|
||||
|
||||
#if DEBUG
|
||||
// Debug builds and Unity Editor
|
||||
logger.Log(LogType.Log, msg.Trim());
|
||||
#else
|
||||
// Server or WebGL
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine(msg.Trim());
|
||||
Console.ResetColor();
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Verbose<T>(string msg, T arg1)
|
||||
{
|
||||
if (minLogLevel > Levels.Verbose) return;
|
||||
Verbose(String.Format(msg, arg1));
|
||||
}
|
||||
|
||||
public static void Verbose<T1, T2>(string msg, T1 arg1, T2 arg2)
|
||||
{
|
||||
if (minLogLevel > Levels.Verbose) return;
|
||||
Verbose(String.Format(msg, arg1, arg2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs info to console if minLogLevel is set to Info or lower
|
||||
/// </summary>
|
||||
/// <param name="msg">Message text to log</param>
|
||||
/// <param name="consoleColor">Default Cyan works in server and browser consoles</param>
|
||||
static void Info(string msg, ConsoleColor consoleColor = ConsoleColor.Cyan)
|
||||
{
|
||||
#if DEBUG
|
||||
// Debug builds and Unity Editor
|
||||
logger.Log(LogType.Log, msg.Trim());
|
||||
#else
|
||||
// Server or WebGL
|
||||
Console.ForegroundColor = consoleColor;
|
||||
Console.WriteLine(msg.Trim());
|
||||
Console.ResetColor();
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Info<T>(string msg, T arg1, ConsoleColor consoleColor = ConsoleColor.Cyan)
|
||||
{
|
||||
if (minLogLevel > Levels.Info) return;
|
||||
Info(String.Format(msg, arg1), consoleColor);
|
||||
}
|
||||
|
||||
public static void Info<T1, T2>(string msg, T1 arg1, T2 arg2, ConsoleColor consoleColor = ConsoleColor.Cyan)
|
||||
{
|
||||
if (minLogLevel > Levels.Info) return;
|
||||
Info(String.Format(msg, arg1, arg2), consoleColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs info to console if minLogLevel is set to Info or lower
|
||||
/// </summary>
|
||||
/// <param name="e">Exception to log</param>
|
||||
public static void InfoException(Exception e)
|
||||
{
|
||||
if (minLogLevel > Levels.Info) return;
|
||||
|
||||
#if DEBUG
|
||||
// Debug builds and Unity Editor
|
||||
logger.Log(LogType.Exception, e.Message);
|
||||
#else
|
||||
// Server or WebGL
|
||||
Console.ForegroundColor = ConsoleColor.DarkRed;
|
||||
Console.WriteLine(e.Message);
|
||||
Console.ResetColor();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs info to console if minLogLevel is set to Warn or lower
|
||||
/// </summary>
|
||||
/// <param name="msg">Message text to log</param>
|
||||
public static void Warn(string msg)
|
||||
{
|
||||
if (minLogLevel > Levels.Warn) return;
|
||||
|
||||
#if DEBUG
|
||||
// Debug builds and Unity Editor
|
||||
logger.Log(LogType.Warning, msg.Trim());
|
||||
#else
|
||||
// Server or WebGL
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(msg.Trim());
|
||||
Console.ResetColor();
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Warn<T>(string msg, T arg1)
|
||||
{
|
||||
if (minLogLevel > Levels.Warn) return;
|
||||
Warn(String.Format(msg, arg1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs info to console if minLogLevel is set to Error or lower
|
||||
/// </summary>
|
||||
/// <param name="msg">Message text to log</param>
|
||||
public static void Error(string msg)
|
||||
{
|
||||
if (minLogLevel > Levels.Error) return;
|
||||
|
||||
#if DEBUG
|
||||
// Debug builds and Unity Editor
|
||||
logger.Log(LogType.Error, msg.Trim());
|
||||
#else
|
||||
// Server or WebGL
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(msg.Trim());
|
||||
Console.ResetColor();
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Error<T>(string msg, T arg1)
|
||||
{
|
||||
if (minLogLevel > Levels.Error) return;
|
||||
Error(String.Format(msg, arg1));
|
||||
}
|
||||
|
||||
public static void Error<T1, T2>(string msg, T1 arg1, T2 arg2)
|
||||
{
|
||||
if (minLogLevel > Levels.Error) return;
|
||||
Error(String.Format(msg, arg1, arg2));
|
||||
}
|
||||
|
||||
public static void Error<T1, T2, T3>(string msg, T1 arg1, T2 arg2, T3 arg3)
|
||||
{
|
||||
if (minLogLevel > Levels.Error) return;
|
||||
Error(String.Format(msg, arg1, arg2, arg3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the byte array starting from offset for length bytes
|
||||
/// </summary>
|
||||
/// <param name="buffer">Byte array to read</param>
|
||||
/// <param name="offset">starting point in the byte array</param>
|
||||
/// <param name="length">number of bytes to read from offset</param>
|
||||
/// <returns></returns>
|
||||
public static string BufferToString(byte[] buffer, int offset = 0, int? length = null) => BitConverter.ToString(buffer, offset, length ?? buffer.Length);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cf1521098e04f74fbea0fe2aa0439f8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public struct Message
|
||||
{
|
||||
public readonly int connId;
|
||||
public readonly EventType type;
|
||||
public readonly ArrayBuffer data;
|
||||
public readonly Exception exception;
|
||||
|
||||
public Message(EventType type) : this()
|
||||
{
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Message(ArrayBuffer data) : this()
|
||||
{
|
||||
type = EventType.Data;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Message(Exception exception) : this()
|
||||
{
|
||||
type = EventType.Error;
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public Message(int connId, EventType type) : this()
|
||||
{
|
||||
this.connId = connId;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Message(int connId, ArrayBuffer data) : this()
|
||||
{
|
||||
this.connId = connId;
|
||||
type = EventType.Data;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Message(int connId, Exception exception) : this()
|
||||
{
|
||||
this.connId = connId;
|
||||
type = EventType.Error;
|
||||
this.exception = exception;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5d05d71b09d2714b96ffe80bc3d2a77
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Message.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public static class MessageProcessor
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static byte FirstLengthByte(byte[] buffer) => (byte)(buffer[1] & 0b0111_1111);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NeedToReadShortLength(byte[] buffer)
|
||||
{
|
||||
byte lenByte = FirstLengthByte(buffer);
|
||||
return lenByte == Constants.UshortPayloadLength;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NeedToReadLongLength(byte[] buffer)
|
||||
{
|
||||
byte lenByte = FirstLengthByte(buffer);
|
||||
return lenByte == Constants.UlongPayloadLength;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetOpcode(byte[] buffer) => buffer[0] & 0b0000_1111;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetPayloadLength(byte[] buffer) => GetMessageLength(buffer, 0, FirstLengthByte(buffer));
|
||||
|
||||
/// <summary>
|
||||
/// Has full message been sent
|
||||
/// </summary>
|
||||
/// <param name="buffer"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Finished(byte[] buffer) => (buffer[0] & 0b1000_0000) != 0;
|
||||
|
||||
public static void ValidateHeader(byte[] buffer, int maxLength, bool expectMask, bool opCodeContinuation = false)
|
||||
{
|
||||
bool finished = Finished(buffer);
|
||||
bool hasMask = (buffer[1] & 0b1000_0000) != 0; // true from clients, false from server, "All messages from the client to the server have this bit set"
|
||||
|
||||
int opcode = buffer[0] & 0b0000_1111; // expecting 1 - text message
|
||||
byte lenByte = FirstLengthByte(buffer);
|
||||
|
||||
ThrowIfMaskNotExpected(hasMask, expectMask);
|
||||
ThrowIfBadOpCode(opcode, finished, opCodeContinuation);
|
||||
|
||||
int msglen = GetMessageLength(buffer, 0, lenByte);
|
||||
|
||||
ThrowIfLengthZero(msglen);
|
||||
ThrowIfMsgLengthTooLong(msglen, maxLength);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ToggleMask(byte[] src, int sourceOffset, int messageLength, byte[] maskBuffer, int maskOffset)
|
||||
{
|
||||
ToggleMask(src, sourceOffset, src, sourceOffset, messageLength, maskBuffer, maskOffset);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ToggleMask(byte[] src, int sourceOffset, ArrayBuffer dst, int messageLength, byte[] maskBuffer, int maskOffset)
|
||||
{
|
||||
ToggleMask(src, sourceOffset, dst.array, 0, messageLength, maskBuffer, maskOffset);
|
||||
dst.count = messageLength;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ToggleMask(byte[] src, int srcOffset, byte[] dst, int dstOffset, int messageLength, byte[] maskBuffer, int maskOffset)
|
||||
{
|
||||
for (int i = 0; i < messageLength; i++)
|
||||
{
|
||||
byte maskByte = maskBuffer[maskOffset + i % Constants.MaskSize];
|
||||
dst[dstOffset + i] = (byte)(src[srcOffset + i] ^ maskByte);
|
||||
}
|
||||
}
|
||||
|
||||
/// <exception cref="InvalidDataException"></exception>
|
||||
static int GetMessageLength(byte[] buffer, int offset, byte lenByte)
|
||||
{
|
||||
if (lenByte == Constants.UshortPayloadLength)
|
||||
{
|
||||
// header is 2 bytes
|
||||
ushort value = 0;
|
||||
value |= (ushort)(buffer[offset + 2] << 8);
|
||||
value |= buffer[offset + 3];
|
||||
|
||||
return value;
|
||||
}
|
||||
else if (lenByte == Constants.UlongPayloadLength)
|
||||
{
|
||||
// header is 8 bytes
|
||||
ulong value = 0;
|
||||
value |= ((ulong)buffer[offset + 2] << 56);
|
||||
value |= ((ulong)buffer[offset + 3] << 48);
|
||||
value |= ((ulong)buffer[offset + 4] << 40);
|
||||
value |= ((ulong)buffer[offset + 5] << 32);
|
||||
value |= ((ulong)buffer[offset + 6] << 24);
|
||||
value |= ((ulong)buffer[offset + 7] << 16);
|
||||
value |= ((ulong)buffer[offset + 8] << 8);
|
||||
value |= ((ulong)buffer[offset + 9] << 0);
|
||||
|
||||
if (value > int.MaxValue)
|
||||
throw new NotSupportedException($"Can't receive payloads larger that int.max: {int.MaxValue}");
|
||||
|
||||
return (int)value;
|
||||
}
|
||||
else // is less than 126
|
||||
{
|
||||
// header is 2 bytes long
|
||||
return lenByte;
|
||||
}
|
||||
}
|
||||
|
||||
/// <exception cref="InvalidDataException"></exception>
|
||||
static void ThrowIfMaskNotExpected(bool hasMask, bool expectMask)
|
||||
{
|
||||
if (hasMask != expectMask)
|
||||
throw new InvalidDataException($"Message expected mask to be {expectMask} but was {hasMask}");
|
||||
}
|
||||
|
||||
/// <exception cref="InvalidDataException"></exception>
|
||||
static void ThrowIfBadOpCode(int opcode, bool finished, bool opCodeContinuation)
|
||||
{
|
||||
// 0 = continuation
|
||||
// 2 = binary
|
||||
// 8 = close
|
||||
|
||||
// do we expect Continuation?
|
||||
if (opCodeContinuation)
|
||||
{
|
||||
// good it was Continuation
|
||||
if (opcode == 0)
|
||||
return;
|
||||
|
||||
// bad, wasn't Continuation
|
||||
throw new InvalidDataException("Expected opcode to be Continuation");
|
||||
}
|
||||
else if (!finished)
|
||||
{
|
||||
// fragmented message, should be binary
|
||||
if (opcode == 2)
|
||||
return;
|
||||
|
||||
throw new InvalidDataException("Expected opcode to be binary");
|
||||
}
|
||||
else
|
||||
{
|
||||
// normal message, should be binary or close
|
||||
if (opcode == 2 || opcode == 8)
|
||||
return;
|
||||
|
||||
throw new InvalidDataException("Expected opcode to be binary or close");
|
||||
}
|
||||
}
|
||||
|
||||
/// <exception cref="InvalidDataException"></exception>
|
||||
static void ThrowIfLengthZero(int msglen)
|
||||
{
|
||||
if (msglen == 0)
|
||||
throw new InvalidDataException("Message length was zero");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// need to check this so that data from previous buffer isn't used
|
||||
/// </summary>
|
||||
public static void ThrowIfMsgLengthTooLong(int msglen, int maxLength)
|
||||
{
|
||||
if (msglen > maxLength)
|
||||
throw new InvalidDataException("Message length is greater than max length");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c1f218a2b16ca846aaf23260078e549
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public static class ReadHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads exactly length from stream
|
||||
/// </summary>
|
||||
/// <returns>outOffset + length</returns>
|
||||
/// <exception cref="ReadHelperException"></exception>
|
||||
public static int Read(Stream stream, byte[] outBuffer, int outOffset, int length)
|
||||
{
|
||||
int received = 0;
|
||||
try
|
||||
{
|
||||
while (received < length)
|
||||
{
|
||||
int read = stream.Read(outBuffer, outOffset + received, length - received);
|
||||
if (read == 0)
|
||||
throw new ReadHelperException("[SWT-ReadHelper]: Read returned 0");
|
||||
|
||||
received += read;
|
||||
}
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
// if interrupt is called we don't care about Exceptions
|
||||
Utils.CheckForInterupt();
|
||||
|
||||
// rethrow
|
||||
ae.Handle(e => false);
|
||||
}
|
||||
|
||||
if (received != length)
|
||||
throw new ReadHelperException("[SWT-ReadHelper]: received not equal to length");
|
||||
|
||||
return outOffset + received;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads and returns results. This should never throw an exception
|
||||
/// </summary>
|
||||
public static bool TryRead(Stream stream, byte[] outBuffer, int outOffset, int length)
|
||||
{
|
||||
try
|
||||
{
|
||||
Read(stream, outBuffer, outOffset, length);
|
||||
return true;
|
||||
}
|
||||
catch (ReadHelperException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static int? SafeReadTillMatch(Stream stream, byte[] outBuffer, int outOffset, int maxLength, byte[] endOfHeader)
|
||||
{
|
||||
try
|
||||
{
|
||||
int read = 0;
|
||||
int endIndex = 0;
|
||||
int endLength = endOfHeader.Length;
|
||||
while (true)
|
||||
{
|
||||
int next = stream.ReadByte();
|
||||
if (next == -1) // closed
|
||||
return null;
|
||||
|
||||
if (read >= maxLength)
|
||||
{
|
||||
Log.Error("[SWT-ReadHelper]: SafeReadTillMatch exceeded maxLength");
|
||||
return null;
|
||||
}
|
||||
|
||||
outBuffer[outOffset + read] = (byte)next;
|
||||
read++;
|
||||
|
||||
// if n is match, check n+1 next
|
||||
if (endOfHeader[endIndex] == next)
|
||||
{
|
||||
endIndex++;
|
||||
// when all is match return with read length
|
||||
if (endIndex >= endLength)
|
||||
return read;
|
||||
}
|
||||
// if n not match reset to 0
|
||||
else
|
||||
endIndex = 0;
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.InfoException(e);
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ReadHelperException : Exception
|
||||
{
|
||||
public ReadHelperException(string message) : base(message) { }
|
||||
|
||||
protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context) { }
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f4fa5d324e708c46a55810a97de75bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReadHelper.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,250 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
internal static class ReceiveLoop
|
||||
{
|
||||
public struct Config
|
||||
{
|
||||
public readonly Connection conn;
|
||||
public readonly int maxMessageSize;
|
||||
public readonly bool expectMask;
|
||||
public readonly ConcurrentQueue<Message> queue;
|
||||
public readonly BufferPool bufferPool;
|
||||
|
||||
public Config(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool)
|
||||
{
|
||||
this.conn = conn ?? throw new ArgumentNullException(nameof(conn));
|
||||
this.maxMessageSize = maxMessageSize;
|
||||
this.expectMask = expectMask;
|
||||
this.queue = queue ?? throw new ArgumentNullException(nameof(queue));
|
||||
this.bufferPool = bufferPool ?? throw new ArgumentNullException(nameof(bufferPool));
|
||||
}
|
||||
|
||||
public void Deconstruct(out Connection conn, out int maxMessageSize, out bool expectMask, out ConcurrentQueue<Message> queue, out BufferPool bufferPool)
|
||||
{
|
||||
conn = this.conn;
|
||||
maxMessageSize = this.maxMessageSize;
|
||||
expectMask = this.expectMask;
|
||||
queue = this.queue;
|
||||
bufferPool = this.bufferPool;
|
||||
}
|
||||
}
|
||||
|
||||
struct Header
|
||||
{
|
||||
public int payloadLength;
|
||||
public int offset;
|
||||
public int opcode;
|
||||
public bool finished;
|
||||
}
|
||||
|
||||
public static void Loop(Config config)
|
||||
{
|
||||
(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool _) = config;
|
||||
|
||||
Profiler.BeginThreadProfiling("SimpleWeb", $"ReceiveLoop {conn.connId}");
|
||||
|
||||
byte[] readBuffer = new byte[Constants.HeaderSize + (expectMask ? Constants.MaskSize : 0) + maxMessageSize];
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
TcpClient client = conn.client;
|
||||
|
||||
while (client.Connected)
|
||||
ReadOneMessage(config, readBuffer);
|
||||
|
||||
Log.Verbose("[SWT-ReceiveLoop]: {0} Not Connected", conn);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// if interrupted we don't care about other exceptions
|
||||
Utils.CheckForInterupt();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (ThreadInterruptedException e) { Log.InfoException(e); }
|
||||
catch (ThreadAbortException) { Log.Error("[SWT-ReceiveLoop]: Thread Abort Exception"); }
|
||||
catch (ObjectDisposedException e) { Log.InfoException(e); }
|
||||
catch (ReadHelperException e) { Log.InfoException(e); }
|
||||
catch (SocketException e)
|
||||
{
|
||||
// this could happen if wss client closes stream
|
||||
Log.Warn("[SWT-ReceiveLoop]: ReceiveLoop SocketException\n{0}", e.Message);
|
||||
queue.Enqueue(new Message(conn.connId, e));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// this could happen if client disconnects
|
||||
Log.Warn("[SWT-ReceiveLoop]: ReceiveLoop IOException\n{0}", e.Message);
|
||||
queue.Enqueue(new Message(conn.connId, e));
|
||||
}
|
||||
catch (InvalidDataException e)
|
||||
{
|
||||
Log.Error("[SWT-ReceiveLoop]: Invalid data from {0}\n{1}\n{2}\n\n", conn, e.Message, e.StackTrace);
|
||||
queue.Enqueue(new Message(conn.connId, e));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(e);
|
||||
queue.Enqueue(new Message(conn.connId, e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Profiler.EndThreadProfiling();
|
||||
conn.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
static void ReadOneMessage(Config config, byte[] buffer)
|
||||
{
|
||||
(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool) = config;
|
||||
Stream stream = conn.stream;
|
||||
|
||||
Header header = ReadHeader(config, buffer);
|
||||
|
||||
int msgOffset = header.offset;
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, header.payloadLength);
|
||||
|
||||
if (header.finished)
|
||||
{
|
||||
switch (header.opcode)
|
||||
{
|
||||
case 2:
|
||||
HandleArrayMessage(config, buffer, msgOffset, header.payloadLength);
|
||||
break;
|
||||
case 8:
|
||||
HandleCloseMessage(config, buffer, msgOffset, header.payloadLength);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo cache this to avoid allocations
|
||||
Queue<ArrayBuffer> fragments = new Queue<ArrayBuffer>();
|
||||
fragments.Enqueue(CopyMessageToBuffer(bufferPool, expectMask, buffer, msgOffset, header.payloadLength));
|
||||
int totalSize = header.payloadLength;
|
||||
|
||||
while (!header.finished)
|
||||
{
|
||||
header = ReadHeader(config, buffer, opCodeContinuation: true);
|
||||
|
||||
msgOffset = header.offset;
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, header.payloadLength);
|
||||
fragments.Enqueue(CopyMessageToBuffer(bufferPool, expectMask, buffer, msgOffset, header.payloadLength));
|
||||
|
||||
totalSize += header.payloadLength;
|
||||
MessageProcessor.ThrowIfMsgLengthTooLong(totalSize, maxMessageSize);
|
||||
}
|
||||
|
||||
ArrayBuffer msg = bufferPool.Take(totalSize);
|
||||
msg.count = 0;
|
||||
while (fragments.Count > 0)
|
||||
{
|
||||
ArrayBuffer part = fragments.Dequeue();
|
||||
|
||||
part.CopyTo(msg.array, msg.count);
|
||||
msg.count += part.count;
|
||||
|
||||
part.Release();
|
||||
}
|
||||
|
||||
// dump after mask off
|
||||
Log.DumpBuffer($"[SWT-ReceiveLoop]: Message", msg);
|
||||
|
||||
queue.Enqueue(new Message(conn.connId, msg));
|
||||
}
|
||||
}
|
||||
|
||||
static Header ReadHeader(Config config, byte[] buffer, bool opCodeContinuation = false)
|
||||
{
|
||||
(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool) = config;
|
||||
Stream stream = conn.stream;
|
||||
Header header = new Header();
|
||||
|
||||
// read 2
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.HeaderMinSize);
|
||||
// log after first blocking call
|
||||
Log.Flood($"[SWT-ReceiveLoop]: Message From {conn}");
|
||||
|
||||
if (MessageProcessor.NeedToReadShortLength(buffer))
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.ShortLength);
|
||||
if (MessageProcessor.NeedToReadLongLength(buffer))
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.LongLength);
|
||||
|
||||
Log.DumpBuffer($"[SWT-ReceiveLoop]: Raw Header", buffer, 0, header.offset);
|
||||
|
||||
MessageProcessor.ValidateHeader(buffer, maxMessageSize, expectMask, opCodeContinuation);
|
||||
|
||||
if (expectMask)
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.MaskSize);
|
||||
|
||||
header.opcode = MessageProcessor.GetOpcode(buffer);
|
||||
header.payloadLength = MessageProcessor.GetPayloadLength(buffer);
|
||||
header.finished = MessageProcessor.Finished(buffer);
|
||||
|
||||
Log.Flood($"[SWT-ReceiveLoop]: Header ln:{header.payloadLength} op:{header.opcode} mask:{expectMask}");
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
static void HandleArrayMessage(Config config, byte[] buffer, int msgOffset, int payloadLength)
|
||||
{
|
||||
(Connection conn, int _, bool expectMask, ConcurrentQueue<Message> queue, BufferPool bufferPool) = config;
|
||||
|
||||
ArrayBuffer arrayBuffer = CopyMessageToBuffer(bufferPool, expectMask, buffer, msgOffset, payloadLength);
|
||||
|
||||
// dump after mask off
|
||||
Log.DumpBuffer($"[SWT-ReceiveLoop]: Message", arrayBuffer);
|
||||
|
||||
queue.Enqueue(new Message(conn.connId, arrayBuffer));
|
||||
}
|
||||
|
||||
static ArrayBuffer CopyMessageToBuffer(BufferPool bufferPool, bool expectMask, byte[] buffer, int msgOffset, int payloadLength)
|
||||
{
|
||||
ArrayBuffer arrayBuffer = bufferPool.Take(payloadLength);
|
||||
|
||||
if (expectMask)
|
||||
{
|
||||
int maskOffset = msgOffset - Constants.MaskSize;
|
||||
// write the result of toggle directly into arrayBuffer to avoid 2nd copy call
|
||||
MessageProcessor.ToggleMask(buffer, msgOffset, arrayBuffer, payloadLength, buffer, maskOffset);
|
||||
}
|
||||
else
|
||||
arrayBuffer.CopyFrom(buffer, msgOffset, payloadLength);
|
||||
|
||||
return arrayBuffer;
|
||||
}
|
||||
|
||||
static void HandleCloseMessage(Config config, byte[] buffer, int msgOffset, int payloadLength)
|
||||
{
|
||||
(Connection conn, int _, bool expectMask, ConcurrentQueue<Message> _, BufferPool _) = config;
|
||||
|
||||
if (expectMask)
|
||||
{
|
||||
int maskOffset = msgOffset - Constants.MaskSize;
|
||||
MessageProcessor.ToggleMask(buffer, msgOffset, payloadLength, buffer, maskOffset);
|
||||
}
|
||||
|
||||
// dump after mask off
|
||||
Log.DumpBuffer($"[SWT-ReceiveLoop]: Message", buffer, msgOffset, payloadLength);
|
||||
Log.Verbose("[SWT-ReceiveLoop]: Close: {0} message:{1}", GetCloseCode(buffer, msgOffset), GetCloseMessage(buffer, msgOffset, payloadLength));
|
||||
|
||||
conn.Dispose();
|
||||
}
|
||||
|
||||
static string GetCloseMessage(byte[] buffer, int msgOffset, int payloadLength)
|
||||
=> Encoding.UTF8.GetString(buffer, msgOffset + 2, payloadLength - 2);
|
||||
|
||||
static int GetCloseCode(byte[] buffer, int msgOffset)
|
||||
=> buffer[msgOffset + 0] << 8 | buffer[msgOffset + 1];
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a26c2815f58431c4a98c158c8b655ffd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReceiveLoop.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a client's request to the Websockets server, which is the first message from the client.
|
||||
/// </summary>
|
||||
public class Request
|
||||
{
|
||||
static readonly char[] lineSplitChars = new char[] { '\r', '\n' };
|
||||
static readonly char[] headerSplitChars = new char[] { ':' };
|
||||
public string RequestLine;
|
||||
public Dictionary<string, string> Headers = new Dictionary<string, string>();
|
||||
|
||||
public Request(string message)
|
||||
{
|
||||
string[] all = message.Split(lineSplitChars, StringSplitOptions.RemoveEmptyEntries);
|
||||
RequestLine = all.First();
|
||||
Headers = all.Skip(1)
|
||||
.Select(header => header.Split(headerSplitChars, 2, StringSplitOptions.RemoveEmptyEntries))
|
||||
.ToDictionary(split => split[0].Trim(), split => split[1].Trim());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50b41ad63d4956a42a073bad5158fe09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs
|
||||
uploadId: 736421
|
222
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs
Normal file
222
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs
Normal file
@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public static class SendLoopConfig
|
||||
{
|
||||
public static volatile bool batchSend = false;
|
||||
public static volatile bool sleepBeforeSend = false;
|
||||
}
|
||||
internal static class SendLoop
|
||||
{
|
||||
public struct Config
|
||||
{
|
||||
public readonly Connection conn;
|
||||
public readonly int bufferSize;
|
||||
public readonly bool setMask;
|
||||
|
||||
public Config(Connection conn, int bufferSize, bool setMask)
|
||||
{
|
||||
this.conn = conn ?? throw new ArgumentNullException(nameof(conn));
|
||||
this.bufferSize = bufferSize;
|
||||
this.setMask = setMask;
|
||||
}
|
||||
|
||||
public void Deconstruct(out Connection conn, out int bufferSize, out bool setMask)
|
||||
{
|
||||
conn = this.conn;
|
||||
bufferSize = this.bufferSize;
|
||||
setMask = this.setMask;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Loop(Config config)
|
||||
{
|
||||
(Connection conn, int bufferSize, bool setMask) = config;
|
||||
|
||||
Profiler.BeginThreadProfiling("SimpleWeb", $"SendLoop {conn.connId}");
|
||||
|
||||
// create write buffer for this thread
|
||||
byte[] writeBuffer = new byte[bufferSize];
|
||||
MaskHelper maskHelper = setMask ? new MaskHelper() : null;
|
||||
try
|
||||
{
|
||||
TcpClient client = conn.client;
|
||||
Stream stream = conn.stream;
|
||||
|
||||
// null check in case disconnect while send thread is starting
|
||||
if (client == null)
|
||||
return;
|
||||
|
||||
while (client.Connected)
|
||||
{
|
||||
// wait for message
|
||||
conn.sendPending.Wait();
|
||||
// wait for 1ms for mirror to send other messages
|
||||
if (SendLoopConfig.sleepBeforeSend)
|
||||
Thread.Sleep(1);
|
||||
|
||||
conn.sendPending.Reset();
|
||||
|
||||
if (SendLoopConfig.batchSend)
|
||||
{
|
||||
int offset = 0;
|
||||
while (conn.sendQueue.TryDequeue(out ArrayBuffer msg))
|
||||
{
|
||||
// check if connected before sending message
|
||||
if (!client.Connected)
|
||||
{
|
||||
Log.Verbose("[SWT-SendLoop]: SendLoop {0} not connected", conn);
|
||||
msg.Release();
|
||||
return;
|
||||
}
|
||||
|
||||
int maxLength = msg.count + Constants.HeaderSize + Constants.MaskSize;
|
||||
|
||||
// if next writer could overflow, write to stream and clear buffer
|
||||
if (offset + maxLength > bufferSize)
|
||||
{
|
||||
stream.Write(writeBuffer, 0, offset);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
offset = SendMessage(writeBuffer, offset, msg, setMask, maskHelper);
|
||||
msg.Release();
|
||||
}
|
||||
|
||||
// after no message in queue, send remaining messages
|
||||
// don't need to check offset > 0 because last message in queue will always be sent here
|
||||
|
||||
stream.Write(writeBuffer, 0, offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
while (conn.sendQueue.TryDequeue(out ArrayBuffer msg))
|
||||
{
|
||||
// check if connected before sending message
|
||||
if (!client.Connected)
|
||||
{
|
||||
Log.Verbose("[SWT-SendLoop]: SendLoop {0} not connected", conn);
|
||||
msg.Release();
|
||||
return;
|
||||
}
|
||||
|
||||
int length = SendMessage(writeBuffer, 0, msg, setMask, maskHelper);
|
||||
stream.Write(writeBuffer, 0, length);
|
||||
msg.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.Verbose("[SWT-SendLoop]: {0} Not Connected", conn);
|
||||
}
|
||||
catch (ThreadInterruptedException e) { Log.InfoException(e); }
|
||||
catch (ThreadAbortException) { Log.Error("[SWT-SendLoop]: Thread Abort Exception"); }
|
||||
catch (Exception e) { Log.Exception(e); }
|
||||
finally
|
||||
{
|
||||
Profiler.EndThreadProfiling();
|
||||
conn.Dispose();
|
||||
maskHelper?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>new offset in buffer</returns>
|
||||
static int SendMessage(byte[] buffer, int startOffset, ArrayBuffer msg, bool setMask, MaskHelper maskHelper)
|
||||
{
|
||||
int msgLength = msg.count;
|
||||
int offset = WriteHeader(buffer, startOffset, msgLength, setMask);
|
||||
|
||||
if (setMask)
|
||||
{
|
||||
offset = maskHelper.WriteMask(buffer, offset);
|
||||
}
|
||||
|
||||
msg.CopyTo(buffer, offset);
|
||||
offset += msgLength;
|
||||
|
||||
// dump before mask on
|
||||
Log.DumpBuffer("[SWT-SendLoop]: Send", buffer, startOffset, offset);
|
||||
|
||||
if (setMask)
|
||||
{
|
||||
int messageOffset = offset - msgLength;
|
||||
MessageProcessor.ToggleMask(buffer, messageOffset, msgLength, buffer, messageOffset - Constants.MaskSize);
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
public static int WriteHeader(byte[] buffer, int startOffset, int msgLength, bool setMask)
|
||||
{
|
||||
int sendLength = 0;
|
||||
const byte finished = 128;
|
||||
const byte byteOpCode = 2;
|
||||
|
||||
buffer[startOffset + 0] = finished | byteOpCode;
|
||||
sendLength++;
|
||||
|
||||
if (msgLength <= Constants.BytePayloadLength)
|
||||
{
|
||||
buffer[startOffset + 1] = (byte)msgLength;
|
||||
sendLength++;
|
||||
}
|
||||
else if (msgLength <= ushort.MaxValue)
|
||||
{
|
||||
buffer[startOffset + 1] = 126;
|
||||
buffer[startOffset + 2] = (byte)(msgLength >> 8);
|
||||
buffer[startOffset + 3] = (byte)msgLength;
|
||||
sendLength += 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[startOffset + 1] = 127;
|
||||
// must be 64 bytes, but we only have 32 bit length, so first 4 bits are 0
|
||||
buffer[startOffset + 2] = 0;
|
||||
buffer[startOffset + 3] = 0;
|
||||
buffer[startOffset + 4] = 0;
|
||||
buffer[startOffset + 5] = 0;
|
||||
buffer[startOffset + 6] = (byte)(msgLength >> 24);
|
||||
buffer[startOffset + 7] = (byte)(msgLength >> 16);
|
||||
buffer[startOffset + 8] = (byte)(msgLength >> 8);
|
||||
buffer[startOffset + 9] = (byte)msgLength;
|
||||
|
||||
sendLength += 9;
|
||||
}
|
||||
|
||||
if (setMask)
|
||||
buffer[startOffset + 1] |= 0b1000_0000;
|
||||
|
||||
return sendLength + startOffset;
|
||||
}
|
||||
|
||||
}
|
||||
sealed class MaskHelper : IDisposable
|
||||
{
|
||||
readonly byte[] maskBuffer;
|
||||
readonly RNGCryptoServiceProvider random;
|
||||
|
||||
public MaskHelper()
|
||||
{
|
||||
maskBuffer = new byte[4];
|
||||
random = new RNGCryptoServiceProvider();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
random.Dispose();
|
||||
}
|
||||
|
||||
public int WriteMask(byte[] buffer, int offset)
|
||||
{
|
||||
random.GetBytes(maskBuffer);
|
||||
Buffer.BlockCopy(maskBuffer, 0, buffer, offset, 4);
|
||||
|
||||
return offset + 4;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f87dd81736d9c824db67f808ac71841d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,26 @@
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct TcpConfig
|
||||
{
|
||||
public readonly bool noDelay;
|
||||
public readonly int sendTimeout;
|
||||
public readonly int receiveTimeout;
|
||||
|
||||
public TcpConfig(bool noDelay, int sendTimeout, int receiveTimeout)
|
||||
{
|
||||
this.noDelay = noDelay;
|
||||
this.sendTimeout = sendTimeout;
|
||||
this.receiveTimeout = receiveTimeout;
|
||||
}
|
||||
|
||||
public void ApplyTo(TcpClient client)
|
||||
{
|
||||
client.SendTimeout = sendTimeout;
|
||||
client.ReceiveTimeout = receiveTimeout;
|
||||
client.NoDelay = noDelay;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81ac8d35f28fab14b9edda5cd9d4fc86
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/TcpConfig.cs
|
||||
uploadId: 736421
|
13
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs
Normal file
13
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
internal static class Utils
|
||||
{
|
||||
public static void CheckForInterupt()
|
||||
{
|
||||
// sleep in order to check for ThreadInterruptedException
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4643ffb4cb0562847b1ae925d07e15b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs
|
||||
uploadId: 736421
|
21
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE
Normal file
21
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 James Frowen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
14
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE.meta
Normal file
14
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE.meta
Normal file
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a0cf751b4a201242ac60b4adbc54657
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE
|
||||
uploadId: 736421
|
19
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt
Normal file
19
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt
Normal file
@ -0,0 +1,19 @@
|
||||
SimpleWebTransport is a Transport that implements websocket for Webgl
|
||||
Can be used in High level networking solution like Mirror or Mirage
|
||||
This transport can also work on standalone builds and has support for
|
||||
encryption with websocket secure.
|
||||
|
||||
Requirements:
|
||||
Unity 2019.4 LTS
|
||||
|
||||
Documentation:
|
||||
https://mirror-networking.gitbook.io/docs/
|
||||
https://github.com/James-Frowen/SimpleWebTransport/blob/master/README.md
|
||||
|
||||
Support:
|
||||
Discord: https://discord.gg/BZTQcftBkE
|
||||
Bug Reports: https://github.com/James-Frowen/SimpleWebTransport/issues
|
||||
|
||||
|
||||
**To get most recent updates and fixes download from github**
|
||||
https://github.com/James-Frowen/SimpleWebTransport/releases
|
14
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt.meta
Normal file
14
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt.meta
Normal file
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e3971d5783109f4d9ce93c7a689d701
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server.meta
Normal file
8
Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e599e92544d43344a9a9060052add28
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles Handshakes from new clients on the server
|
||||
/// <para>The server handshake has buffers to reduce allocations when clients connect</para>
|
||||
/// </summary>
|
||||
internal class ServerHandshake
|
||||
{
|
||||
const int GetSize = 3;
|
||||
const int ResponseLength = 129;
|
||||
const int KeyLength = 24;
|
||||
const int MergedKeyLength = 60;
|
||||
const string KeyHeaderString = "\r\nSec-WebSocket-Key: ";
|
||||
// this isn't an official max, just a reasonable size for a websocket handshake
|
||||
readonly int maxHttpHeaderSize = 3000;
|
||||
|
||||
// SHA-1 is the websocket standard:
|
||||
// https://www.rfc-editor.org/rfc/rfc6455
|
||||
// we should follow the standard, even though SHA1 is considered weak:
|
||||
// https://stackoverflow.com/questions/38038841/why-is-sha-1-considered-insecure
|
||||
readonly SHA1 sha1 = SHA1.Create();
|
||||
readonly BufferPool bufferPool;
|
||||
|
||||
public ServerHandshake(BufferPool bufferPool, int handshakeMaxSize)
|
||||
{
|
||||
this.bufferPool = bufferPool;
|
||||
maxHttpHeaderSize = handshakeMaxSize;
|
||||
}
|
||||
|
||||
~ServerHandshake()
|
||||
{
|
||||
sha1.Dispose();
|
||||
}
|
||||
|
||||
public bool TryHandshake(Connection conn)
|
||||
{
|
||||
Stream stream = conn.stream;
|
||||
|
||||
using (ArrayBuffer getHeader = bufferPool.Take(GetSize))
|
||||
{
|
||||
if (!ReadHelper.TryRead(stream, getHeader.array, 0, GetSize))
|
||||
return false;
|
||||
|
||||
getHeader.count = GetSize;
|
||||
|
||||
if (!IsGet(getHeader.array))
|
||||
{
|
||||
Log.Warn("[SWT-ServerHandshake]: First bytes from client was not 'GET' for handshake, instead was {0}", Log.BufferToString(getHeader.array, 0, GetSize));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string msg = ReadToEndForHandshake(stream);
|
||||
|
||||
if (string.IsNullOrEmpty(msg))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
AcceptHandshake(stream, msg);
|
||||
|
||||
conn.request = new Request(msg);
|
||||
conn.remoteAddress = conn.CalculateAddress();
|
||||
Log.Info($"[SWT-ServerHandshake]: A client connected from {0}", conn);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
Log.InfoException(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string ReadToEndForHandshake(Stream stream)
|
||||
{
|
||||
using (ArrayBuffer readBuffer = bufferPool.Take(maxHttpHeaderSize))
|
||||
{
|
||||
int? readCountOrFail = ReadHelper.SafeReadTillMatch(stream, readBuffer.array, 0, maxHttpHeaderSize, Constants.endOfHandshake);
|
||||
if (!readCountOrFail.HasValue)
|
||||
return null;
|
||||
|
||||
int readCount = readCountOrFail.Value;
|
||||
|
||||
string msg = Encoding.ASCII.GetString(readBuffer.array, 0, readCount);
|
||||
// GET isn't in the bytes we read here, so we need to add it back
|
||||
msg = $"GET{msg}";
|
||||
Log.Verbose("[SWT-ServerHandshake]: Client Handshake Message:\r\n{0}", msg);
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsGet(byte[] getHeader)
|
||||
{
|
||||
// just check bytes here instead of using Encoding.ASCII
|
||||
return getHeader[0] == 71 && // G
|
||||
getHeader[1] == 69 && // E
|
||||
getHeader[2] == 84; // T
|
||||
}
|
||||
|
||||
void AcceptHandshake(Stream stream, string msg)
|
||||
{
|
||||
using (ArrayBuffer keyBuffer = bufferPool.Take(KeyLength + Constants.HandshakeGUIDLength),
|
||||
responseBuffer = bufferPool.Take(ResponseLength))
|
||||
{
|
||||
GetKey(msg, keyBuffer.array);
|
||||
AppendGuid(keyBuffer.array);
|
||||
byte[] keyHash = CreateHash(keyBuffer.array);
|
||||
CreateResponse(keyHash, responseBuffer.array);
|
||||
|
||||
stream.Write(responseBuffer.array, 0, ResponseLength);
|
||||
}
|
||||
}
|
||||
|
||||
static void GetKey(string msg, byte[] keyBuffer)
|
||||
{
|
||||
int start = msg.IndexOf(KeyHeaderString, StringComparison.InvariantCultureIgnoreCase) + KeyHeaderString.Length;
|
||||
|
||||
Log.Verbose("[SWT-ServerHandshake]: Handshake Key: {0}", msg.Substring(start, KeyLength));
|
||||
Encoding.ASCII.GetBytes(msg, start, KeyLength, keyBuffer, 0);
|
||||
}
|
||||
|
||||
static void AppendGuid(byte[] keyBuffer)
|
||||
{
|
||||
Buffer.BlockCopy(Constants.HandshakeGUIDBytes, 0, keyBuffer, KeyLength, Constants.HandshakeGUIDLength);
|
||||
}
|
||||
|
||||
byte[] CreateHash(byte[] keyBuffer)
|
||||
{
|
||||
Log.Verbose("[SWT-ServerHandshake]: Handshake Hashing {0}", Encoding.ASCII.GetString(keyBuffer, 0, MergedKeyLength));
|
||||
return sha1.ComputeHash(keyBuffer, 0, MergedKeyLength);
|
||||
}
|
||||
|
||||
static void CreateResponse(byte[] keyHash, byte[] responseBuffer)
|
||||
{
|
||||
string keyHashString = Convert.ToBase64String(keyHash);
|
||||
|
||||
// compiler should merge these strings into 1 string before format
|
||||
string message = string.Format(
|
||||
"HTTP/1.1 101 Switching Protocols\r\n" +
|
||||
"Connection: Upgrade\r\n" +
|
||||
"Upgrade: websocket\r\n" +
|
||||
"Sec-WebSocket-Accept: {0}\r\n\r\n",
|
||||
keyHashString);
|
||||
|
||||
Log.Verbose("[SWT-ServerHandshake]: Handshake Response length {0}, IsExpected {1}", message.Length, message.Length == ResponseLength);
|
||||
Encoding.ASCII.GetBytes(message, 0, ResponseLength, responseBuffer, 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6268509ac4fb48141b9944c03295da11
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public struct SslConfig
|
||||
{
|
||||
public readonly bool enabled;
|
||||
public readonly string certPath;
|
||||
public readonly string certPassword;
|
||||
public readonly SslProtocols sslProtocols;
|
||||
|
||||
public SslConfig(bool enabled, string certPath, string certPassword, SslProtocols sslProtocols)
|
||||
{
|
||||
this.enabled = enabled;
|
||||
this.certPath = certPath;
|
||||
this.certPassword = certPassword;
|
||||
this.sslProtocols = sslProtocols;
|
||||
}
|
||||
}
|
||||
internal class ServerSslHelper
|
||||
{
|
||||
readonly SslConfig config;
|
||||
readonly X509Certificate2 certificate;
|
||||
|
||||
public ServerSslHelper(SslConfig sslConfig)
|
||||
{
|
||||
config = sslConfig;
|
||||
if (config.enabled)
|
||||
{
|
||||
certificate = new X509Certificate2(config.certPath, config.certPassword);
|
||||
Log.Info($"[SWT-ServerSslHelper]: SSL Certificate {0} loaded with expiration of {1}", certificate.Subject, certificate.GetExpirationDateString());
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryCreateStream(Connection conn)
|
||||
{
|
||||
NetworkStream stream = conn.client.GetStream();
|
||||
if (config.enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
conn.stream = CreateStream(stream);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("[SWT-ServerSslHelper]: Create SSLStream Failed: {0}", e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
conn.stream = stream;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Stream CreateStream(NetworkStream stream)
|
||||
{
|
||||
SslStream sslStream = new SslStream(stream, true, acceptClient);
|
||||
sslStream.AuthenticateAsServer(certificate, false, config.sslProtocols, false);
|
||||
|
||||
return sslStream;
|
||||
}
|
||||
|
||||
// always accept client
|
||||
bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => true;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11061fee528ebdd43817a275b1e4a317
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerSslHelper.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public class SimpleWebServer
|
||||
{
|
||||
public event Action<int, string> onConnect;
|
||||
public event Action<int> onDisconnect;
|
||||
public event Action<int, ArraySegment<byte>> onData;
|
||||
public event Action<int, Exception> onError;
|
||||
|
||||
readonly int maxMessagesPerTick;
|
||||
readonly WebSocketServer server;
|
||||
readonly BufferPool bufferPool;
|
||||
|
||||
public bool Active { get; private set; }
|
||||
|
||||
public SimpleWebServer(int maxMessagesPerTick, TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig)
|
||||
{
|
||||
this.maxMessagesPerTick = maxMessagesPerTick;
|
||||
// use max because bufferpool is used for both messages and handshake
|
||||
int max = Math.Max(maxMessageSize, handshakeMaxSize);
|
||||
bufferPool = new BufferPool(5, 20, max);
|
||||
server = new WebSocketServer(tcpConfig, maxMessageSize, handshakeMaxSize, sslConfig, bufferPool);
|
||||
}
|
||||
|
||||
public void Start(ushort port)
|
||||
{
|
||||
server.Listen(port);
|
||||
Active = true;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
server.Stop();
|
||||
Active = false;
|
||||
}
|
||||
|
||||
public void SendAll(List<int> connectionIds, ArraySegment<byte> source)
|
||||
{
|
||||
ArrayBuffer buffer = bufferPool.Take(source.Count);
|
||||
buffer.CopyFrom(source);
|
||||
buffer.SetReleasesRequired(connectionIds.Count);
|
||||
|
||||
// make copy of array before for each, data sent to each client is the same
|
||||
foreach (int id in connectionIds)
|
||||
server.Send(id, buffer);
|
||||
}
|
||||
|
||||
public void SendOne(int connectionId, ArraySegment<byte> source)
|
||||
{
|
||||
ArrayBuffer buffer = bufferPool.Take(source.Count);
|
||||
buffer.CopyFrom(source);
|
||||
server.Send(connectionId, buffer);
|
||||
}
|
||||
|
||||
public bool KickClient(int connectionId) => server.CloseConnection(connectionId);
|
||||
|
||||
public string GetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
|
||||
|
||||
public Request GetClientRequest(int connectionId) => server.GetClientRequest(connectionId);
|
||||
|
||||
/// <summary>
|
||||
/// Processes all new messages
|
||||
/// </summary>
|
||||
public void ProcessMessageQueue()
|
||||
{
|
||||
ProcessMessageQueue(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes all messages while <paramref name="behaviour"/> is enabled
|
||||
/// </summary>
|
||||
/// <param name="behaviour"></param>
|
||||
public void ProcessMessageQueue(MonoBehaviour behaviour)
|
||||
{
|
||||
int processedCount = 0;
|
||||
bool skipEnabled = behaviour == null;
|
||||
// check enabled every time in case behaviour was disabled after data
|
||||
while (
|
||||
(skipEnabled || behaviour.enabled) &&
|
||||
processedCount < maxMessagesPerTick &&
|
||||
// Dequeue last
|
||||
server.receiveQueue.TryDequeue(out Message next)
|
||||
)
|
||||
{
|
||||
processedCount++;
|
||||
|
||||
switch (next.type)
|
||||
{
|
||||
case EventType.Connected:
|
||||
onConnect?.Invoke(next.connId, GetClientAddress(next.connId));
|
||||
break;
|
||||
case EventType.Data:
|
||||
onData?.Invoke(next.connId, next.data.ToSegment());
|
||||
next.data.Release();
|
||||
break;
|
||||
case EventType.Disconnected:
|
||||
onDisconnect?.Invoke(next.connId);
|
||||
break;
|
||||
case EventType.Error:
|
||||
onError?.Invoke(next.connId, next.exception);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (server.receiveQueue.Count > 0)
|
||||
{
|
||||
Log.Warn("[SWT-SimpleWebServer]: ProcessMessageQueue has {0} remaining.", server.receiveQueue.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd51d7896f55a5e48b41a4b526562b0e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/SimpleWebServer.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
public class WebSocketServer
|
||||
{
|
||||
public readonly ConcurrentQueue<Message> receiveQueue = new ConcurrentQueue<Message>();
|
||||
|
||||
readonly TcpConfig tcpConfig;
|
||||
readonly int maxMessageSize;
|
||||
|
||||
TcpListener listener;
|
||||
Thread acceptThread;
|
||||
bool serverStopped;
|
||||
readonly ServerHandshake handShake;
|
||||
readonly ServerSslHelper sslHelper;
|
||||
readonly BufferPool bufferPool;
|
||||
readonly ConcurrentDictionary<int, Connection> connections = new ConcurrentDictionary<int, Connection>();
|
||||
|
||||
int _idCounter = 0;
|
||||
|
||||
public WebSocketServer(TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig, BufferPool bufferPool)
|
||||
{
|
||||
this.tcpConfig = tcpConfig;
|
||||
this.maxMessageSize = maxMessageSize;
|
||||
sslHelper = new ServerSslHelper(sslConfig);
|
||||
this.bufferPool = bufferPool;
|
||||
handShake = new ServerHandshake(this.bufferPool, handshakeMaxSize);
|
||||
}
|
||||
|
||||
public void Listen(int port)
|
||||
{
|
||||
listener = TcpListener.Create(port);
|
||||
listener.Start();
|
||||
|
||||
Log.Verbose("[SWT-WebSocketServer]: Server Started on {0}", port);
|
||||
|
||||
acceptThread = new Thread(acceptLoop);
|
||||
acceptThread.IsBackground = true;
|
||||
acceptThread.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
serverStopped = true;
|
||||
|
||||
// Interrupt then stop so that Exception is handled correctly
|
||||
acceptThread?.Interrupt();
|
||||
listener?.Stop();
|
||||
acceptThread = null;
|
||||
|
||||
Log.Verbose("[SWT-WebSocketServer]: Server stopped...closing all connections.");
|
||||
|
||||
// make copy so that foreach doesn't break if values are removed
|
||||
Connection[] connectionsCopy = connections.Values.ToArray();
|
||||
foreach (Connection conn in connectionsCopy)
|
||||
conn.Dispose();
|
||||
|
||||
connections.Clear();
|
||||
}
|
||||
|
||||
void acceptLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
TcpClient client = listener.AcceptTcpClient();
|
||||
tcpConfig.ApplyTo(client);
|
||||
|
||||
// TODO keep track of connections before they are in connections dictionary
|
||||
// this might not be a problem as HandshakeAndReceiveLoop checks for stop
|
||||
// and returns/disposes before sending message to queue
|
||||
Connection conn = new Connection(client, AfterConnectionDisposed);
|
||||
Log.Verbose("[SWT-WebSocketServer]: A client connected from {0}", conn);
|
||||
|
||||
// handshake needs its own thread as it needs to wait for message from client
|
||||
Thread receiveThread = new Thread(() => HandshakeAndReceiveLoop(conn));
|
||||
|
||||
conn.receiveThread = receiveThread;
|
||||
|
||||
receiveThread.IsBackground = true;
|
||||
receiveThread.Start();
|
||||
}
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// check for Interrupted/Abort
|
||||
Utils.CheckForInterupt();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (ThreadInterruptedException e) { Log.InfoException(e); }
|
||||
catch (ThreadAbortException) { Log.Error("[SWT-WebSocketServer]: Thread Abort Exception"); }
|
||||
catch (Exception e) { Log.Exception(e); }
|
||||
}
|
||||
|
||||
void HandshakeAndReceiveLoop(Connection conn)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool success = sslHelper.TryCreateStream(conn);
|
||||
if (!success)
|
||||
{
|
||||
Log.Warn("[SWT-WebSocketServer]: Failed to create SSL Stream {0}", conn);
|
||||
conn.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
success = handShake.TryHandshake(conn);
|
||||
|
||||
if (success)
|
||||
Log.Verbose("[SWT-WebSocketServer]: Sent Handshake {0}, false", conn);
|
||||
else
|
||||
{
|
||||
Log.Warn("[SWT-WebSocketServer]: Handshake Failed {0}", conn);
|
||||
conn.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// check if Stop has been called since accepting this client
|
||||
if (serverStopped)
|
||||
{
|
||||
Log.Warn("[SWT-WebSocketServer]: Server stopped after successful handshake");
|
||||
return;
|
||||
}
|
||||
|
||||
conn.connId = Interlocked.Increment(ref _idCounter);
|
||||
connections.TryAdd(conn.connId, conn);
|
||||
|
||||
receiveQueue.Enqueue(new Message(conn.connId, EventType.Connected));
|
||||
|
||||
Thread sendThread = new Thread(() =>
|
||||
{
|
||||
SendLoop.Config sendConfig = new SendLoop.Config(
|
||||
conn,
|
||||
bufferSize: Constants.HeaderSize + maxMessageSize,
|
||||
setMask: false);
|
||||
|
||||
SendLoop.Loop(sendConfig);
|
||||
});
|
||||
|
||||
conn.sendThread = sendThread;
|
||||
sendThread.IsBackground = true;
|
||||
sendThread.Name = $"SendThread {conn.connId}";
|
||||
sendThread.Start();
|
||||
|
||||
ReceiveLoop.Config receiveConfig = new ReceiveLoop.Config(
|
||||
conn,
|
||||
maxMessageSize,
|
||||
expectMask: true,
|
||||
receiveQueue,
|
||||
bufferPool);
|
||||
|
||||
ReceiveLoop.Loop(receiveConfig);
|
||||
}
|
||||
catch (ThreadInterruptedException e) { Log.InfoException(e); }
|
||||
catch (ThreadAbortException) { Log.Error("[SWT-WebSocketServer]: Thread Abort Exception"); }
|
||||
catch (Exception e) { Log.Exception(e); }
|
||||
finally
|
||||
{
|
||||
// close here in case connect fails
|
||||
conn.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void AfterConnectionDisposed(Connection conn)
|
||||
{
|
||||
if (conn.connId != Connection.IdNotSet)
|
||||
{
|
||||
receiveQueue.Enqueue(new Message(conn.connId, EventType.Disconnected));
|
||||
connections.TryRemove(conn.connId, out Connection _);
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(int id, ArrayBuffer buffer)
|
||||
{
|
||||
if (connections.TryGetValue(id, out Connection conn))
|
||||
{
|
||||
conn.sendQueue.Enqueue(buffer);
|
||||
conn.sendPending.Set();
|
||||
}
|
||||
else
|
||||
Log.Warn("[SWT-WebSocketServer]: Cannot send message to {0} because connection was not found in dictionary. Maybe it disconnected.", id);
|
||||
}
|
||||
|
||||
public bool CloseConnection(int id)
|
||||
{
|
||||
if (connections.TryGetValue(id, out Connection conn))
|
||||
{
|
||||
Log.Info($"[SWT-WebSocketServer]: Disconnecting connection {0}", id);
|
||||
conn.Dispose();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warn("[SWT-WebSocketServer]: Failed to kick {0} because id not found.", id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetClientAddress(int id)
|
||||
{
|
||||
if (!connections.TryGetValue(id, out Connection conn))
|
||||
{
|
||||
Log.Warn("[SWT-WebSocketServer]: Cannot get address of connection {0} because connection was not found in dictionary.", id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return conn.remoteAddress;
|
||||
}
|
||||
|
||||
public Request GetClientRequest(int id)
|
||||
{
|
||||
if (!connections.TryGetValue(id, out Connection conn))
|
||||
{
|
||||
Log.Warn("[SWT-WebSocketServer]: Cannot get request of connection {0} because connection was not found in dictionary.", id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return conn.request;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c434db044777d2439bae5a57d4e8ee7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/WebSocketServer.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "SimpleWebTransport",
|
||||
"rootNamespace": "",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b5390adca4e2bb4791cb930316d6f3e
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef
|
||||
uploadId: 736421
|
@ -0,0 +1,49 @@
|
||||
using System.IO;
|
||||
using System.Security.Authentication;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
|
||||
public class SslConfigLoader
|
||||
{
|
||||
internal struct Cert
|
||||
{
|
||||
public string path;
|
||||
public string password;
|
||||
}
|
||||
public static SslConfig Load(bool sslEnabled, string sslCertJson, SslProtocols sslProtocols)
|
||||
{
|
||||
// don't need to load anything if ssl is not enabled
|
||||
if (!sslEnabled)
|
||||
return default;
|
||||
|
||||
string certJsonPath = sslCertJson;
|
||||
|
||||
Cert cert = LoadCertJson(certJsonPath);
|
||||
|
||||
return new SslConfig(
|
||||
enabled: sslEnabled,
|
||||
sslProtocols: sslProtocols,
|
||||
certPath: cert.path,
|
||||
certPassword: cert.password
|
||||
);
|
||||
}
|
||||
|
||||
internal static Cert LoadCertJson(string certJsonPath)
|
||||
{
|
||||
string json = File.ReadAllText(certJsonPath);
|
||||
Cert cert = JsonUtility.FromJson<Cert>(json);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cert.path))
|
||||
throw new InvalidDataException("Cert Json didn't not contain \"path\"");
|
||||
|
||||
// don't use IsNullOrWhiteSpace here because whitespace could be a valid password for a cert
|
||||
// password can also be empty
|
||||
if (string.IsNullOrEmpty(cert.password))
|
||||
cert.password = string.Empty;
|
||||
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfdb6b97a48a48b498e563e857342da1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SslConfigLoader.cs
|
||||
uploadId: 736421
|
389
Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs
Normal file
389
Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs
Normal file
@ -0,0 +1,389 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/websockets-transport")]
|
||||
public class SimpleWebTransport : Transport, PortTransport
|
||||
{
|
||||
public const string NormalScheme = "ws";
|
||||
public const string SecureScheme = "wss";
|
||||
|
||||
[Tooltip("Protect against allocation attacks by keeping the max message size small. Otherwise an attacker might send multiple fake packets with 2GB headers, causing the server to run out of memory after allocating multiple large packets.")]
|
||||
public int maxMessageSize = 16 * 1024;
|
||||
|
||||
[FormerlySerializedAs("handshakeMaxSize")]
|
||||
[Tooltip("Max size for http header send as handshake for websockets")]
|
||||
public int maxHandshakeSize = 16 * 1024;
|
||||
|
||||
[FormerlySerializedAs("serverMaxMessagesPerTick")]
|
||||
[Tooltip("Caps the number of messages the server will process per tick. Allows LateUpdate to finish to let the reset of unity continue in case more messages arrive before they are processed")]
|
||||
public int serverMaxMsgsPerTick = 10000;
|
||||
|
||||
[FormerlySerializedAs("clientMaxMessagesPerTick")]
|
||||
[Tooltip("Caps the number of messages the client will process per tick. Allows LateUpdate to finish to let the reset of unity continue in case more messages arrive before they are processed")]
|
||||
public int clientMaxMsgsPerTick = 1000;
|
||||
|
||||
[Tooltip("Send would stall forever if the network is cut off during a send, so we need a timeout (in milliseconds)")]
|
||||
public int sendTimeout = 5000;
|
||||
|
||||
[Tooltip("How long without a message before disconnecting (in milliseconds)")]
|
||||
public int receiveTimeout = 20000;
|
||||
|
||||
[Tooltip("disables nagle algorithm. lowers CPU% and latency but increases bandwidth")]
|
||||
public bool noDelay = true;
|
||||
|
||||
[Header("Obsolete SSL settings")]
|
||||
|
||||
[Tooltip("Requires wss connections on server, only to be used with SSL cert.json, never with reverse proxy.\nNOTE: if sslEnabled is true clientUseWss is forced true, even if not checked.")]
|
||||
public bool sslEnabled;
|
||||
|
||||
[Tooltip("Protocols that SSL certificate is created to support.")]
|
||||
public SslProtocols sslProtocols = SslProtocols.Tls12;
|
||||
|
||||
[Tooltip("Path to json file that contains path to cert and its password\nUse Json file so that cert password is not included in client builds\nSee Assets/Mirror/Transports/.cert.example.Json")]
|
||||
public string sslCertJson = "./cert.json";
|
||||
|
||||
[Header("Server settings")]
|
||||
|
||||
[Tooltip("Port to use for server")]
|
||||
public ushort port = 27777;
|
||||
public ushort Port
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_WEBGL
|
||||
if (clientWebsocketSettings.ClientPortOption == WebsocketPortOption.SpecifyPort)
|
||||
return clientWebsocketSettings.CustomClientPort;
|
||||
else
|
||||
return port;
|
||||
#else
|
||||
return port;
|
||||
#endif
|
||||
}
|
||||
set
|
||||
{
|
||||
#if UNITY_WEBGL
|
||||
if (clientWebsocketSettings.ClientPortOption == WebsocketPortOption.SpecifyPort)
|
||||
clientWebsocketSettings.CustomClientPort = value;
|
||||
else
|
||||
port = value;
|
||||
#else
|
||||
port = value;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("Groups messages in queue before calling Stream.Send")]
|
||||
public bool batchSend = true;
|
||||
|
||||
[Tooltip("Waits for 1ms before grouping and sending messages.\n" +
|
||||
"This gives time for mirror to finish adding message to queue so that less groups need to be made.\n" +
|
||||
"If WaitBeforeSend is true then BatchSend Will also be set to true")]
|
||||
public bool waitBeforeSend = true;
|
||||
|
||||
[Header("Client settings")]
|
||||
|
||||
[Tooltip("Sets connect scheme to wss. Useful when client needs to connect using wss when TLS is outside of transport.\nNOTE: if sslEnabled is true clientUseWss is also true")]
|
||||
public bool clientUseWss;
|
||||
public ClientWebsocketSettings clientWebsocketSettings = new ClientWebsocketSettings { ClientPortOption = WebsocketPortOption.DefaultSameAsServer, CustomClientPort = 7777 };
|
||||
|
||||
[Header("Logging")]
|
||||
|
||||
[Tooltip("Choose minimum severity level for logging\nFlood level requires Debug build")]
|
||||
[SerializeField] Log.Levels minimumLogLevel = Log.Levels.Warn;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets _logLevels field</para>
|
||||
/// <para>Sets _logLevels and Log.level fields</para>
|
||||
/// </summary>
|
||||
public Log.Levels LogLevels
|
||||
{
|
||||
get => minimumLogLevel;
|
||||
set
|
||||
{
|
||||
minimumLogLevel = value;
|
||||
Log.minLogLevel = minimumLogLevel;
|
||||
}
|
||||
}
|
||||
|
||||
SimpleWebClient client;
|
||||
SimpleWebServer server;
|
||||
|
||||
TcpConfig TcpConfig => new TcpConfig(noDelay, sendTimeout, receiveTimeout);
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Log.minLogLevel = minimumLogLevel;
|
||||
}
|
||||
|
||||
public override string ToString() => $"SWT [{port}]";
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
Log.minLogLevel = minimumLogLevel;
|
||||
}
|
||||
|
||||
public override bool Available() => true;
|
||||
|
||||
public override int GetMaxPacketSize(int channelId = 0) => maxMessageSize;
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
client?.Disconnect();
|
||||
client = null;
|
||||
server?.Stop();
|
||||
server = null;
|
||||
}
|
||||
|
||||
#region Client
|
||||
|
||||
string GetClientScheme() => (sslEnabled || clientUseWss) ? SecureScheme : NormalScheme;
|
||||
|
||||
public override bool IsEncrypted => ClientConnected() && (clientUseWss || sslEnabled) || ServerActive() && sslEnabled;
|
||||
|
||||
// Not technically correct, but there's no good way to get the actual cipher, especially in browser
|
||||
// When using reverse proxy, connection between proxy and server is not encrypted.
|
||||
public override string EncryptionCipher => "TLS";
|
||||
|
||||
public override bool ClientConnected()
|
||||
{
|
||||
// not null and not NotConnected (we want to return true if connecting or disconnecting)
|
||||
return client != null && client.ConnectionState != ClientState.NotConnected;
|
||||
}
|
||||
|
||||
public override void ClientConnect(string hostname)
|
||||
{
|
||||
UriBuilder builder = new UriBuilder
|
||||
{
|
||||
Scheme = GetClientScheme(),
|
||||
Host = hostname,
|
||||
};
|
||||
|
||||
switch (clientWebsocketSettings.ClientPortOption)
|
||||
{
|
||||
case WebsocketPortOption.SpecifyPort:
|
||||
builder.Port = clientWebsocketSettings.CustomClientPort;
|
||||
break;
|
||||
case WebsocketPortOption.MatchWebpageProtocol:
|
||||
// not including a port in the builder allows the webpage to drive the port
|
||||
// https://github.com/MirrorNetworking/Mirror/pull/3477
|
||||
break;
|
||||
default: // default case handles ClientWebsocketPortOption.DefaultSameAsServerPort
|
||||
builder.Port = port;
|
||||
break;
|
||||
}
|
||||
|
||||
ClientConnect(builder.Uri);
|
||||
}
|
||||
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
// connecting or connected
|
||||
if (ClientConnected())
|
||||
{
|
||||
Log.Warn("[SWT-ClientConnect]: Already Connected");
|
||||
return;
|
||||
}
|
||||
|
||||
client = SimpleWebClient.Create(maxMessageSize, clientMaxMsgsPerTick, TcpConfig);
|
||||
if (client == null)
|
||||
return;
|
||||
|
||||
client.onConnect += OnClientConnected.Invoke;
|
||||
|
||||
client.onDisconnect += () =>
|
||||
{
|
||||
OnClientDisconnected.Invoke();
|
||||
// clear client here after disconnect event has been sent
|
||||
// there should be no more messages after disconnect
|
||||
client = null;
|
||||
};
|
||||
|
||||
client.onData += (ArraySegment<byte> data) => OnClientDataReceived.Invoke(data, Channels.Reliable);
|
||||
|
||||
// We will not invoke OnClientError if minLogLevel is set to None
|
||||
// We only send the full exception if minLogLevel is set to Verbose
|
||||
switch (Log.minLogLevel)
|
||||
{
|
||||
case Log.Levels.Flood:
|
||||
case Log.Levels.Verbose:
|
||||
client.onError += (Exception e) =>
|
||||
{
|
||||
OnClientError.Invoke(TransportError.Unexpected, e.ToString());
|
||||
ClientDisconnect();
|
||||
};
|
||||
break;
|
||||
case Log.Levels.Info:
|
||||
case Log.Levels.Warn:
|
||||
case Log.Levels.Error:
|
||||
client.onError += (Exception e) =>
|
||||
{
|
||||
OnClientError.Invoke(TransportError.Unexpected, e.Message);
|
||||
ClientDisconnect();
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
client.Connect(uri);
|
||||
}
|
||||
|
||||
public override void ClientDisconnect()
|
||||
{
|
||||
// don't set client null here of messages wont be processed
|
||||
client?.Disconnect();
|
||||
}
|
||||
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
if (!ClientConnected())
|
||||
{
|
||||
Log.Error("[SWT-ClientSend]: Not Connected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (segment.Count > maxMessageSize)
|
||||
{
|
||||
Log.Error("[SWT-ClientSend]: Message greater than max size");
|
||||
return;
|
||||
}
|
||||
|
||||
if (segment.Count == 0)
|
||||
{
|
||||
Log.Error("[SWT-ClientSend]: Message count was zero");
|
||||
return;
|
||||
}
|
||||
|
||||
client.Send(segment);
|
||||
|
||||
// call event. might be null if no statistics are listening etc.
|
||||
OnClientDataSent?.Invoke(segment, Channels.Reliable);
|
||||
}
|
||||
|
||||
// messages should always be processed in early update
|
||||
public override void ClientEarlyUpdate()
|
||||
{
|
||||
client?.ProcessMessageQueue(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Server
|
||||
|
||||
string GetServerScheme() => sslEnabled ? SecureScheme : NormalScheme;
|
||||
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder
|
||||
{
|
||||
Scheme = GetServerScheme(),
|
||||
Host = Dns.GetHostName(),
|
||||
Port = port
|
||||
};
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
public override bool ServerActive()
|
||||
{
|
||||
return server != null && server.Active;
|
||||
}
|
||||
|
||||
public override void ServerStart()
|
||||
{
|
||||
if (ServerActive())
|
||||
Log.Warn("[SWT-ServerStart]: Server Already Started");
|
||||
|
||||
SslConfig config = SslConfigLoader.Load(sslEnabled, sslCertJson, sslProtocols);
|
||||
server = new SimpleWebServer(serverMaxMsgsPerTick, TcpConfig, maxMessageSize, maxHandshakeSize, config);
|
||||
|
||||
server.onConnect += OnServerConnectedWithAddress.Invoke;
|
||||
server.onDisconnect += OnServerDisconnected.Invoke;
|
||||
server.onData += (int connId, ArraySegment<byte> data) => OnServerDataReceived.Invoke(connId, data, Channels.Reliable);
|
||||
|
||||
// We will not invoke OnServerError if minLogLevel is set to None
|
||||
// We only send the full exception if minLogLevel is set to Verbose
|
||||
switch (Log.minLogLevel)
|
||||
{
|
||||
case Log.Levels.Flood:
|
||||
case Log.Levels.Verbose:
|
||||
server.onError += (connId, exception) =>
|
||||
{
|
||||
OnServerError(connId, TransportError.Unexpected, exception.ToString());
|
||||
ServerDisconnect(connId);
|
||||
};
|
||||
break;
|
||||
case Log.Levels.Info:
|
||||
case Log.Levels.Warn:
|
||||
case Log.Levels.Error:
|
||||
server.onError += (connId, exception) =>
|
||||
{
|
||||
OnServerError(connId, TransportError.Unexpected, exception.Message);
|
||||
ServerDisconnect(connId);
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
SendLoopConfig.batchSend = batchSend || waitBeforeSend;
|
||||
SendLoopConfig.sleepBeforeSend = waitBeforeSend;
|
||||
|
||||
server.Start(port);
|
||||
}
|
||||
|
||||
public override void ServerStop()
|
||||
{
|
||||
if (ServerActive())
|
||||
{
|
||||
server.Stop();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ServerDisconnect(int connectionId)
|
||||
{
|
||||
if (ServerActive())
|
||||
server.KickClient(connectionId);
|
||||
}
|
||||
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
if (!ServerActive())
|
||||
{
|
||||
Log.Error("[SWT-ServerSend]: Server Not Active");
|
||||
return;
|
||||
}
|
||||
|
||||
if (segment.Count > maxMessageSize)
|
||||
{
|
||||
Log.Error("[SWT-ServerSend]: Message greater than max size");
|
||||
return;
|
||||
}
|
||||
|
||||
if (segment.Count == 0)
|
||||
{
|
||||
Log.Error("[SWT-ServerSend]: Message count was zero");
|
||||
return;
|
||||
}
|
||||
|
||||
server.SendOne(connectionId, segment);
|
||||
|
||||
// call event. might be null if no statistics are listening etc.
|
||||
OnServerDataSent?.Invoke(connectionId, segment, Channels.Reliable);
|
||||
}
|
||||
|
||||
public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
|
||||
|
||||
public Request ServerGetClientRequest(int connectionId) => server.GetClientRequest(connectionId);
|
||||
|
||||
// messages should always be processed in early update
|
||||
public override void ServerEarlyUpdate()
|
||||
{
|
||||
server?.ProcessMessageQueue(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0110f245bfcfc7d459681f7bd9ebc590
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs
|
||||
uploadId: 736421
|
Reference in New Issue
Block a user