This commit is contained in:
2025-06-16 15:14:23 +02:00
committed by devbeni
parent 60fe4620ff
commit 4ff561284f
3174 changed files with 428263 additions and 0 deletions

View 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": ""
}

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a7a134b4d9eef45239a2d7caf7f52c3e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8ae237a052b29fc4b8d000f48e545bb7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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")]

View File

@ -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

View 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))

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5faa957b8d9fc314ab7596ccf14750d9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a9c19d05220a87c4cbbe4d1e422da0aa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7142349d566213c4abc763afaf4d91a1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}
}

View File

@ -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

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1999985791b91b9458059e88404885a7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 564d2cd3eee5b21419553c0528739d1b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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})");
}
}
}

View File

@ -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

View File

@ -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();
}
}
}
}

View File

@ -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

View File

@ -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' };
}
}

View File

@ -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

View File

@ -0,0 +1,10 @@
namespace Mirror.SimpleWeb
{
public enum EventType
{
Connected,
Data,
Disconnected,
Error
}
}

View File

@ -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

View 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);
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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");
}
}
}

View File

@ -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

View File

@ -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) { }
}
}

View File

@ -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

View File

@ -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];
}
}

View File

@ -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

View File

@ -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());
}
}
}

View File

@ -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

View 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;
}
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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

View 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);
}
}
}

View File

@ -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

View 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.

View 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

View 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

View 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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0e599e92544d43344a9a9060052add28
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);
}
}
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -0,0 +1,14 @@
{
"name": "SimpleWebTransport",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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

View 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
}
}

View File

@ -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