├── .github └── workflows │ └── main.yml ├── .gitignore ├── .releaserc.json ├── Benchmark~ ├── CopyBufferBenchmark.cs ├── CopyBufferResults.md ├── FakeMirror.js └── Readme.md ├── DebugScripts ├── DebugScene.unity ├── DebugScene.unity.meta ├── DebugServer.cs ├── DebugServer.cs.meta ├── README.md ├── README.md.meta ├── SimpleWebTransport.DebugScripts.asmdef ├── SimpleWebTransport.DebugScripts.asmdef.meta └── node~ │ ├── .gitignore │ ├── DebugClient.js │ ├── DebugServer.js │ ├── index.html │ ├── package-lock.json │ ├── package.json │ └── pfxTestServer.js ├── HowToCreateSSLCert.md ├── LICENSE ├── README.md ├── source ├── .cert.example.Json ├── AssemblyInfo.cs ├── AssemblyInfo.cs.meta ├── Client.meta ├── Client │ ├── SimpleWebClient.cs │ ├── SimpleWebClient.cs.meta │ ├── StandAlone.meta │ ├── StandAlone │ │ ├── ClientHandshake.cs │ │ ├── ClientHandshake.cs.meta │ │ ├── ClientSslHelper.cs │ │ ├── ClientSslHelper.cs.meta │ │ ├── WebSocketClientStandAlone.cs │ │ └── WebSocketClientStandAlone.cs.meta │ ├── Webgl.meta │ └── Webgl │ │ ├── SimpleWebJSLib.cs │ │ ├── SimpleWebJSLib.cs.meta │ │ ├── WebSocketClientWebGl.cs │ │ ├── WebSocketClientWebGl.cs.meta │ │ ├── plugin.meta │ │ └── plugin │ │ ├── SimpleWeb.jslib │ │ └── SimpleWeb.jslib.meta ├── Common.meta ├── Common │ ├── BufferPool.cs │ ├── BufferPool.cs.meta │ ├── Connection.cs │ ├── Connection.cs.meta │ ├── Constants.cs │ ├── Constants.cs.meta │ ├── EventType.cs │ ├── EventType.cs.meta │ ├── Log.cs │ ├── Log.cs.meta │ ├── Message.cs │ ├── Message.cs.meta │ ├── MessageProcessor.cs │ ├── MessageProcessor.cs.meta │ ├── ReadHelper.cs │ ├── ReadHelper.cs.meta │ ├── ReceiveLoop.cs │ ├── ReceiveLoop.cs.meta │ ├── SendLoop.cs │ ├── SendLoop.cs.meta │ ├── TcpConfig.cs │ ├── TcpConfig.cs.meta │ ├── Utils.cs │ └── Utils.cs.meta ├── README.txt ├── README.txt.meta ├── Server.meta ├── Server │ ├── ServerHandshake.cs │ ├── ServerHandshake.cs.meta │ ├── ServerSslHelper.cs │ ├── ServerSslHelper.cs.meta │ ├── SimpleWebServer.cs │ ├── SimpleWebServer.cs.meta │ ├── WebSocketServer.cs │ └── WebSocketServer.cs.meta ├── SimpleWebTransport.asmdef ├── SimpleWebTransport.asmdef.meta ├── SimpleWebTransport.cs ├── SimpleWebTransport.cs.meta ├── SslConfigLoader.cs └── SslConfigLoader.cs.meta └── tests ├── Common.meta ├── Common ├── RunNodeTest.cs ├── RunNodeTest.cs.meta ├── SimpleWebTransport.Tests.Common.asmdef └── SimpleWebTransport.Tests.Common.asmdef.meta ├── Editor.meta ├── Editor ├── .Bad1.Json ├── .Bad2.Json ├── .Good1.Json ├── .Good2.Json ├── BufferPoolTests.cs ├── BufferPoolTests.cs.meta ├── CheckNodeTest.cs ├── CheckNodeTest.cs.meta ├── LogTest.cs ├── LogTest.cs.meta ├── MessageProcessorTests.cs ├── MessageProcessorTests.cs.meta ├── SimpleWebTransport.Tests.Editor.asmdef ├── SimpleWebTransport.Tests.Editor.asmdef.meta ├── SslConfigLoaderTest.cs └── SslConfigLoaderTest.cs.meta ├── Runtime.meta ├── Runtime ├── Client.meta ├── Client │ ├── ClientTests.cs │ ├── ClientTests.cs.meta │ ├── ClientTestsMultiple.cs │ ├── ClientTestsMultiple.cs.meta │ ├── ClientWssTest.cs │ └── ClientWssTest.cs.meta ├── Server.meta ├── Server │ ├── BadHandshake.cs │ ├── BadHandshake.cs.meta │ ├── ConnectAndCloseTest.cs │ ├── ConnectAndCloseTest.cs.meta │ ├── ManyClientTest.cs │ ├── ManyClientTest.cs.meta │ ├── MultiBadHandshake.cs │ ├── MultiBadHandshake.cs.meta │ ├── ReceiveMessageTest.cs │ ├── ReceiveMessageTest.cs.meta │ ├── SendMessageTest.cs │ ├── SendMessageTest.cs.meta │ ├── ServerDisconnectTest.cs │ ├── ServerDisconnectTest.cs.meta │ ├── SslServerTest.cs │ ├── SslServerTest.cs.meta │ ├── StartAndStopTest.cs │ └── StartAndStopTest.cs.meta ├── SimpleWebTestBase.cs ├── SimpleWebTestBase.cs.meta ├── SimpleWebTransport.Tests.Runtime.asmdef ├── SimpleWebTransport.Tests.Runtime.asmdef.meta ├── TestInstances.cs └── TestInstances.cs.meta └── node~ ├── .gitignore ├── Connect.js ├── ConnectAndClose.js ├── ConnectAndCloseMany.js ├── Disconnect.js ├── HelloError.js ├── HelloError2.js ├── HelloWorld.js ├── HelloWorld2.js ├── Ping.js ├── README.md ├── README.md.meta ├── ReceiveManyMessages.js ├── ReceiveMessages.js ├── RunMany.js ├── SendAlmostLargeMessages.js ├── SendLargeMessages.js ├── SendManyLargeMessages.js ├── SendManyMessages.js ├── SendMessages.js ├── SendTooLargeMessages.js ├── Timeout.js ├── WssConnectAndClose.js ├── package-lock.json └── package.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'doc/**' 7 | - '**/*.md' 8 | 9 | jobs: 10 | 11 | requestActivationFile: 12 | runs-on: ubuntu-latest 13 | if: false 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Request manual activation file 19 | uses: MirrorNG/unity-runner@2.0.0 20 | id: getManualLicenseFile 21 | with: 22 | entrypoint: /request_activation.sh 23 | 24 | - name: Expose as artifact 25 | uses: actions/upload-artifact@v1 26 | with: 27 | name: Manual Activation File 28 | path: ${{ steps.getManualLicenseFile.outputs.filePath }} 29 | 30 | Tests: 31 | name: Test 32 | runs-on: ubuntu-latest 33 | env: 34 | UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} 35 | if: true 36 | steps: 37 | 38 | # Checkout repository (required to test local actions) 39 | - name: Checkout Mirror 40 | uses: actions/checkout@v2 41 | with: 42 | repository: vis2k/Mirror 43 | fetch-depth: 0 44 | 45 | - name: Delete old SimpleWebTransport folder 46 | run: rm -rf Assets/Mirror/Runtime/Transport/SimpleWebTransport 47 | 48 | - name: Checkout SimpleWebTransport 49 | uses: actions/checkout@v2 50 | with: 51 | fetch-depth: 0 52 | path: Assets/SimpleWebTransport 53 | 54 | - name: Add Coverage Plugin 55 | uses: canastro/copy-action@0.0.2 56 | with: 57 | source: Packages/manifest-coverage.json 58 | target: Packages/manifest.json 59 | 60 | - name: Activate license 61 | uses: MirrorNG/unity-runner@2.0.0 62 | with: 63 | entrypoint: /activate.sh 64 | 65 | - name: Generate Solution 66 | uses: MirrorNG/unity-runner@2.0.0 67 | with: 68 | # Arguments to pass to unity 69 | args: -buildTarget StandaloneWindows64 -customBuildName MirrorNG -customBuildPath ./build/StandaloneWindows64 -projectPath . -executeMethod UnityEditor.SyncVS.SyncSolution -quit 70 | 71 | # Install node packages before running tests 72 | - name: Install Node Packages 73 | run: npm i 74 | working-directory: Assets/SimpleWebTransport/tests/node~/ 75 | 76 | # Configure test runner 77 | - name: Run editor Tests 78 | uses: MirrorNG/unity-runner@2.0.0 79 | with: 80 | args: -runTests -testPlatform editmode -testResults Tests/editmode-results.xml -enableCodeCoverage -coverageResultsPath Tests -editorTestsCategories SimpleWebTransport 81 | 82 | - name: Run play Tests 83 | uses: MirrorNG/unity-runner@2.0.0 84 | with: 85 | args: -runTests -testPlatform playmode -testResults Tests/playmode-results.xml -enableCodeCoverage -coverageResultsPath Tests -editorTestsCategories SimpleWebTransport 86 | 87 | # Upload artifacts 88 | - name: Archive test results 89 | uses: actions/upload-artifact@v1 90 | if: always() 91 | with: 92 | name: Test results (editor mode) 93 | path: Tests 94 | 95 | - name: Publish test results 96 | uses: MirrorNG/nunit-reporter@v1.0.9 97 | if: always() 98 | with: 99 | path: "Tests/*.xml" 100 | access-token: ${{ secrets.GITHUB_TOKEN }} 101 | 102 | - name: SonarQube analysis 103 | if: always() 104 | uses: MirrorNG/unity-runner@2.0.0 105 | with: 106 | entrypoint: /sonar-scanner.sh 107 | projectKey: MirrorNetworking_SimpleWebTransport 108 | projectName: SimpleWebTransport 109 | sonarOrganisation: mirrornetworking 110 | solution: ./SimpleWebTransport.csproj 111 | beginArguments: >- 112 | /d:sonar.verbose="true" 113 | /d:sonar.cs.nunit.reportsPaths=Tests/editmode-results.xml,Tests/playimode-results.xml 114 | /d:sonar.cs.opencover.reportsPaths=Tests/workspace-opencov/EditMode/TestCoverageResults_0000.xml,Tests/workspace-opencov/PlayMode/TestCoverageResults_0000.xml 115 | /d:sonar.exclusions=Assets/Mirror/**/* 116 | /d:sonar.inclusions=Assets/SimpleWebTransport/source/**/* 117 | /d:sonar.scm.exclusions.disabled=true 118 | env: 119 | FrameworkPathOverride: /opt/Unity/Editor/Data/MonoBleedingEdge/ 120 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 121 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 122 | 123 | Release: 124 | runs-on: windows-latest 125 | if: github.ref == 'refs/heads/master' 126 | needs: Tests 127 | steps: 128 | - name: Checkout repository 129 | uses: actions/checkout@v2 130 | with: 131 | fetch-depth: 0 132 | 133 | - name: Setup dotnet 134 | uses: actions/setup-dotnet@v1 135 | with: 136 | dotnet-version: '3.1.100' 137 | 138 | - name: Install unity-packer 139 | run: dotnet tool install -g unity-packer 140 | 141 | - name: Package 142 | run: | 143 | unity-packer pack SimpleWebTransport.unitypackage source Assets/SimpleWebTransport LICENSE Assets/SimpleWebTransport/LICENSE 144 | 145 | - uses: actions/upload-artifact@v1 146 | with: 147 | name: SimpleWebTransport.unitypackage 148 | path: SimpleWebTransport.unitypackage 149 | 150 | - name: Release 151 | uses: cycjimmy/semantic-release-action@v2 152 | with: 153 | extra_plugins: | 154 | @semantic-release/exec 155 | @semantic-release/changelog 156 | @semantic-release/git 157 | branch: master 158 | env: 159 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 160 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | DebugScripts.meta 2 | LICENSE.meta 3 | README.md.meta 4 | source.meta 5 | tests.meta 6 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "verifyConditions": [ 3 | "@semantic-release/github" 4 | ], 5 | "prepare": [ 6 | { 7 | "path": "@semantic-release/exec", 8 | "prepareCmd": "echo ${nextRelease.version} > source/version.txt" 9 | } 10 | ], 11 | "publish": [ 12 | { 13 | "path": "@semantic-release/github", 14 | "assets": [ 15 | { 16 | "path": "SimpleWebTransport.unitypackage", 17 | "label": "SimpleWebTransport Unity Package", 18 | "name": "SimpleWebTransport-${nextRelease.version}.unitypackage" 19 | } 20 | ] 21 | } 22 | ], 23 | "plugins": [ 24 | [ 25 | "@semantic-release/commit-analyzer", 26 | { 27 | "preset": "angular", 28 | "releaseRules": [ 29 | { 30 | "type": "breaking", 31 | "release": "major" 32 | }, 33 | { 34 | "type": "feature", 35 | "release": "minor" 36 | } 37 | ] 38 | } 39 | ], 40 | "@semantic-release/release-notes-generator" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Benchmark~/CopyBufferBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace Mirror.SimpleWeb.Benchmark 7 | { 8 | public class CopyBufferBenchmark : MonoBehaviour 9 | { 10 | private void Start() 11 | { 12 | //int[] sizes = new int[] { 10, 100, 800, 2000, 16000, ushort.MaxValue }; 13 | int[] smallSizes = new int[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, ushort.MaxValue }; 14 | int minIterations = 10000; 15 | testOne(nameof(copyMethod), copyMethod, smallSizes, minIterations); 16 | testOne(nameof(forLoop), forLoop, smallSizes, minIterations); 17 | testOne(nameof(ArrayCopy), ArrayCopy, smallSizes, minIterations); 18 | testOne(nameof(BufferCopy), BufferCopy, smallSizes, minIterations); 19 | 20 | Application.Quit(); 21 | } 22 | 23 | void testOne(string name, Action action, int[] sizes, int minIterations) 24 | { 25 | int max = sizes.Max(); 26 | int itterSize = minIterations * max; 27 | foreach (int size in sizes) 28 | { 29 | byte[] src = new byte[size]; 30 | byte[] dst = new byte[size]; 31 | 32 | int itt = itterSize / size; 33 | int warmup = itt / 10; 34 | 35 | for (int i = 0; i < warmup; i++) 36 | { 37 | action.Invoke(src, dst, size); 38 | } 39 | Stopwatch sw = Stopwatch.StartNew(); 40 | for (int i = 0; i < itt; i++) 41 | { 42 | action.Invoke(src, dst, size); 43 | } 44 | sw.Stop(); 45 | Console.WriteLine($"{name,-20}{size,-20}{sw.ElapsedMilliseconds}"); 46 | } 47 | } 48 | 49 | void copyMethod(byte[] src, byte[] dst, int length) 50 | { 51 | forLoop(src, dst, length); 52 | } 53 | void forLoop(byte[] src, byte[] dst, int length) 54 | { 55 | for (int i = 0; i < length; i++) 56 | { 57 | dst[i] = src[i]; 58 | } 59 | } 60 | void ArrayCopy(byte[] src, byte[] dst, int length) 61 | { 62 | Array.Copy(src, 0, dst, 0, length); 63 | } 64 | void BufferCopy(byte[] src, byte[] dst, int length) 65 | { 66 | Buffer.BlockCopy(src, 0, dst, 0, length); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Benchmark~/CopyBufferResults.md: -------------------------------------------------------------------------------- 1 | Uses [CopyBufferBenchmark](./CopyBufferBenchmark.cs) to test the speed of different copy methods for byte[] 2 | 3 | *Smaller array count has more iteration* 4 | 5 | | Method | Array Size | ElapsedMilliseconds | 6 | |--------------|--------------|----------------------| 7 | | forInline | 10 | 691 | 8 | | forMethod | 10 | 721 | 9 | | ArrayCopy | 10 | 1529 | 10 | | BufferCopy | 10 | 1117 | 11 | | | | | 12 | | forInline | 20 | 589 | 13 | | forMethod | 20 | 628 | 14 | | ArrayCopy | 20 | 710 | 15 | | BufferCopy | 20 | 534 | 16 | | | | | 17 | | forInline | 100 | 527 | 18 | | ArrayCopy | 100 | 169 | 19 | | BufferCopy | 100 | 124 | 20 | | | | | 21 | | forInline | 800 | 499 | 22 | | ArrayCopy | 800 | 54 | 23 | | BufferCopy | 800 | 28 | 24 | | | | | 25 | | forInline | 2000 | 470 | 26 | | ArrayCopy | 2000 | 39 | 27 | | BufferCopy | 2000 | 14 | 28 | | | | | 29 | | forInline | 16000 | 463 | 30 | | ArrayCopy | 16000 | 32 | 31 | | BufferCopy | 16000 | 9 | 32 | | | | | 33 | | forInline | 65535 | 463 | 34 | | ArrayCopy | 65535 | 33 | 35 | | BufferCopy | 65535 | 19 | 36 | 37 | **Summary** 38 | 39 | - for loop (method or inline) for small size ~10 40 | - Buffer.BlockCopy for other 20+ 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Benchmark~/FakeMirror.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | function ByteArray(count, hash1, hash2) { 4 | var buffer = new ArrayBuffer(count); 5 | var view = new Uint8Array(buffer); 6 | view[0] = hash1; 7 | view[1] = hash2; 8 | return buffer; 9 | } 10 | function NetworkPingMessage() { 11 | return ByteArray(10, 0x7F, 0x81); 12 | } 13 | function ReadyMessage() { 14 | return ByteArray(2, 0x3C, 0x9D) 15 | } 16 | function AddPlayerMessage() { 17 | return ByteArray(2, 0x1D, 0x33); 18 | } 19 | function Connect(address) { 20 | // set MirrorLocal in host file to point to ip of pc on local networks 21 | let webSocket = new WebSocket(address); 22 | webSocket.binaryType = 'arraybuffer'; 23 | 24 | const pingInterval = 2000; 25 | let intervalHandle; 26 | 27 | webSocket.addEventListener('error', function (event) { 28 | console.error('Socket Error', event); 29 | }); 30 | 31 | webSocket.addEventListener('open', function (event) { 32 | console.log('Connected to ' + address); 33 | var pingMsg = NetworkPingMessage(); 34 | intervalHandle = setInterval(() => { 35 | webSocket.send(pingMsg); 36 | }, pingInterval); 37 | 38 | webSocket.send(ReadyMessage()); 39 | webSocket.send(AddPlayerMessage()); 40 | }); 41 | 42 | webSocket.addEventListener('close', function (event) { 43 | console.log('Closed'); 44 | clearInterval(intervalHandle); 45 | }); 46 | 47 | webSocket.addEventListener('message', function (event) { 48 | var buffer = event.data; 49 | if (buffer instanceof ArrayBuffer) { 50 | // console.log(`length: ${buffer.byteLength} msg: ${buf2hex(buffer)}`); 51 | } 52 | else { 53 | console.error("Message not array buffer"); 54 | } 55 | }); 56 | 57 | function buf2hex(buffer) { // buffer is an ArrayBuffer 58 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('-'); 59 | } 60 | } 61 | 62 | function OpenMany(address, count) { 63 | for (let i = 0; i < count; i++) { 64 | setTimeout(() => { 65 | Connect(address); 66 | }, 10000 * i); 67 | } 68 | } 69 | 70 | const args = process.argv.slice(2); 71 | const runCount = parseInt(args[0]); 72 | OpenMany("ws://localhost:7776", runCount); 73 | -------------------------------------------------------------------------------- /Benchmark~/Readme.md: -------------------------------------------------------------------------------- 1 | This folder contains note and benchmarks for things related to this transport -------------------------------------------------------------------------------- /DebugScripts/DebugScene.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9a8ab93d35506724eacf20248ab85d66 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /DebugScripts/DebugServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace Mirror.SimpleWeb 5 | { 6 | [RequireComponent(typeof(SimpleWebTransport))] 7 | public class DebugServer : MonoBehaviour 8 | { 9 | public Vector2 guiPosition; 10 | private SimpleWebTransport transport; 11 | 12 | private void Start() 13 | { 14 | 15 | transport = GetComponent(); 16 | #if MIRROR_29_0_OR_NEWER 17 | transport.OnServerConnected = onConnect; 18 | transport.OnServerDataReceived = onData; 19 | transport.OnServerDisconnected = onDisconnect; 20 | transport.OnServerError = onError; 21 | #else 22 | transport.OnServerConnected.AddListener(onConnect); 23 | transport.OnServerDataReceived.AddListener(onData); 24 | transport.OnServerDisconnected.AddListener(onDisconnect); 25 | transport.OnServerError.AddListener(onError); 26 | #endif 27 | transport.ServerStart(); 28 | } 29 | 30 | private void onConnect(int connId) 31 | { 32 | Debug.Log($"onConnect:{connId}"); 33 | } 34 | 35 | private void onData(int connId, ArraySegment data, int channel) 36 | { 37 | string str = BitConverter.ToString(data.Array, data.Offset, data.Count); 38 | Debug.Log($"onData:{connId} data:{str}"); 39 | } 40 | 41 | private void onDisconnect(int connId) 42 | { 43 | Debug.LogWarning($"onDisconnect:{connId}"); 44 | } 45 | 46 | private void onError(int connId, Exception e) 47 | { 48 | Debug.LogError($"onError:{connId}, {e}"); 49 | } 50 | 51 | 52 | public void OnGUI() 53 | { 54 | using (new GUILayout.AreaScope(new Rect(guiPosition, new Vector2(500, 500)))) 55 | { 56 | if (GUILayout.Button("Send Message")) 57 | { 58 | #if MIRROR_26_0_OR_NEWER 59 | transport.ServerSend(1, Channels.DefaultReliable, new ArraySegment(new byte[] { 1, 2, 4, 8 })); 60 | #else 61 | transport.ServerSend(new System.Collections.Generic.List() { 1 }, Channels.DefaultReliable, new ArraySegment(new byte[] { 1, 2, 4, 8 })); 62 | #endif 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DebugScripts/DebugServer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13c0f6c6800c8e249b0088a4e6d4897e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /DebugScripts/README.md: -------------------------------------------------------------------------------- 1 | # DebugScripts 2 | 3 | Scripts in this folder are used to debug the transport 4 | 5 | 6 | ## For developers 7 | 8 | To use these scripts locally update the cert paths to keys that you have locally. 9 | 10 | Certs for unity need to be signed by a CA, to make yourself a CA see https://stackoverflow.com/a/60516812/8479976 11 | 12 | It also helps to set `MirrorLocal` your host file to point to an ip address of another pc on your local networks 13 | -------------------------------------------------------------------------------- /DebugScripts/README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b4d13f6190e9e714bbb9ca71e64cd4c1 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /DebugScripts/SimpleWebTransport.DebugScripts.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleWebTransport.DebugScripts", 3 | "references": [ 4 | "Mirror", 5 | "SimpleWebTransport" 6 | ], 7 | "optionalUnityReferences": [], 8 | "includePlatforms": [], 9 | "excludePlatforms": [], 10 | "allowUnsafeCode": false, 11 | "overrideReferences": false, 12 | "precompiledReferences": [], 13 | "autoReferenced": true, 14 | "defineConstraints": [] 15 | } -------------------------------------------------------------------------------- /DebugScripts/SimpleWebTransport.DebugScripts.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e4f71182715ba004287a5d47b92cf733 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /DebugScripts/node~/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /DebugScripts/node~/DebugClient.js: -------------------------------------------------------------------------------- 1 | // copy paste this to console in chrome new tab 2 | (() => { 3 | // set MirrorLocal in host file to point to ip of pc on local networks 4 | let webSocket = new WebSocket("ws://MirrorLocal:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | const pingInterval = 1000; 8 | let intervalHandle; 9 | 10 | webSocket.addEventListener('error', function (event) { 11 | console.error('Socket Error', event); 12 | }); 13 | 14 | webSocket.addEventListener('open', function (event) { 15 | console.log('Open event'); 16 | intervalHandle = setInterval(() => { 17 | var buffer = new ArrayBuffer(4); 18 | var view = new Uint8Array(buffer); 19 | for (let i = 0; i < view.length; i++) { 20 | view[i] = i + 10; 21 | } 22 | console.log('Send Ping'); 23 | webSocket.send(buffer); 24 | }, pingInterval); 25 | }); 26 | 27 | webSocket.addEventListener('close', function (event) { 28 | console.log('Close event'); 29 | clearInterval(intervalHandle); 30 | }); 31 | 32 | webSocket.addEventListener('message', function (event) { 33 | var buffer = event.data; 34 | if (buffer instanceof ArrayBuffer) { 35 | console.log(`length: ${buffer.byteLength} msg: ${buf2hex(buffer)}`); 36 | } 37 | else { 38 | console.error("Message not array buffer"); 39 | } 40 | }); 41 | 42 | function buf2hex(buffer) { // buffer is an ArrayBuffer 43 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('-'); 44 | } 45 | })(); 46 | -------------------------------------------------------------------------------- /DebugScripts/node~/DebugServer.js: -------------------------------------------------------------------------------- 1 | const WebSocketServer = require('websocket').server; 2 | const https = require('https'); 3 | const fs = require('fs'); 4 | 5 | const options = { 6 | key: fs.readFileSync('./certs/MirrorLocal.key'), 7 | cert: fs.readFileSync('./certs/MirrorLocal.crt') 8 | }; 9 | 10 | var server = https.createServer(options, function (request, response) { 11 | console.log((new Date()) + ' Received request for ' + request.url); 12 | response.writeHead(200); 13 | response.end("hello world\n"); 14 | }); 15 | server.listen(7776, function () { 16 | console.log('Server is listening on port 7776'); 17 | }); 18 | 19 | wsServer = new WebSocketServer({ 20 | httpServer: server, 21 | 22 | // You should not use autoAcceptConnections for production 23 | // applications, as it defeats all standard cross-origin protection 24 | // facilities built into the protocol and the browser. You should 25 | // *always* verify the connection's origin and decide whether or not 26 | // to accept it. 27 | autoAcceptConnections: false 28 | }); 29 | 30 | wsServer.on('request', function (request) { 31 | console.log("request:", request); 32 | var connection = request.accept(); 33 | console.log((new Date()) + ' Connection accepted.'); 34 | 35 | connection.on('message', function (message) { 36 | console.log(`Received Message: ${message.type}`); 37 | if (message.type === 'utf8') { 38 | console.log('Received Message: ' + message.utf8Data); 39 | connection.sendUTF(message.utf8Data); 40 | } 41 | else if (message.type === 'binary') { 42 | console.log('Received Binary Message of ' + message.binaryData.length + ' bytes'); 43 | connection.sendBytes(message.binaryData); 44 | } 45 | }); 46 | connection.on('close', function (reasonCode, description) { 47 | console.log("close", reasonCode, description); 48 | }); 49 | connection.on('error', function (err) { 50 | console.log("Error", err); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /DebugScripts/node~/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /DebugScripts/node~/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-web-transport-debug", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bufferutil": { 8 | "version": "4.0.1", 9 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.1.tgz", 10 | "integrity": "sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==", 11 | "requires": { 12 | "node-gyp-build": "~3.7.0" 13 | } 14 | }, 15 | "d": { 16 | "version": "1.0.1", 17 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", 18 | "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", 19 | "requires": { 20 | "es5-ext": "^0.10.50", 21 | "type": "^1.0.1" 22 | } 23 | }, 24 | "debug": { 25 | "version": "2.6.9", 26 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 27 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 28 | "requires": { 29 | "ms": "2.0.0" 30 | } 31 | }, 32 | "es5-ext": { 33 | "version": "0.10.53", 34 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", 35 | "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", 36 | "requires": { 37 | "es6-iterator": "~2.0.3", 38 | "es6-symbol": "~3.1.3", 39 | "next-tick": "~1.0.0" 40 | } 41 | }, 42 | "es6-iterator": { 43 | "version": "2.0.3", 44 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", 45 | "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", 46 | "requires": { 47 | "d": "1", 48 | "es5-ext": "^0.10.35", 49 | "es6-symbol": "^3.1.1" 50 | } 51 | }, 52 | "es6-symbol": { 53 | "version": "3.1.3", 54 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", 55 | "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", 56 | "requires": { 57 | "d": "^1.0.1", 58 | "ext": "^1.1.2" 59 | } 60 | }, 61 | "ext": { 62 | "version": "1.4.0", 63 | "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", 64 | "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", 65 | "requires": { 66 | "type": "^2.0.0" 67 | }, 68 | "dependencies": { 69 | "type": { 70 | "version": "2.1.0", 71 | "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", 72 | "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" 73 | } 74 | } 75 | }, 76 | "is-typedarray": { 77 | "version": "1.0.0", 78 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 79 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 80 | }, 81 | "ms": { 82 | "version": "2.0.0", 83 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 84 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 85 | }, 86 | "next-tick": { 87 | "version": "1.0.0", 88 | "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", 89 | "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" 90 | }, 91 | "node-gyp-build": { 92 | "version": "3.7.0", 93 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz", 94 | "integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==" 95 | }, 96 | "type": { 97 | "version": "1.2.0", 98 | "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", 99 | "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" 100 | }, 101 | "typedarray-to-buffer": { 102 | "version": "3.1.5", 103 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 104 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 105 | "requires": { 106 | "is-typedarray": "^1.0.0" 107 | } 108 | }, 109 | "utf-8-validate": { 110 | "version": "5.0.2", 111 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.2.tgz", 112 | "integrity": "sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==", 113 | "requires": { 114 | "node-gyp-build": "~3.7.0" 115 | } 116 | }, 117 | "websocket": { 118 | "version": "1.0.32", 119 | "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.32.tgz", 120 | "integrity": "sha512-i4yhcllSP4wrpoPMU2N0TQ/q0O94LRG/eUQjEAamRltjQ1oT1PFFKOG4i877OlJgCG8rw6LrrowJp+TYCEWF7Q==", 121 | "requires": { 122 | "bufferutil": "^4.0.1", 123 | "debug": "^2.2.0", 124 | "es5-ext": "^0.10.50", 125 | "typedarray-to-buffer": "^3.1.5", 126 | "utf-8-validate": "^5.0.2", 127 | "yaeti": "^0.0.6" 128 | } 129 | }, 130 | "yaeti": { 131 | "version": "0.0.6", 132 | "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", 133 | "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /DebugScripts/node~/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-web-transport-debug", 3 | "version": "1.0.0", 4 | "description": "use nodejs websocket client to do unit tests", 5 | "author": "", 6 | "license": "ISC", 7 | "dependencies": { 8 | "websocket": "^1.0.32" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /DebugScripts/node~/pfxTestServer.js: -------------------------------------------------------------------------------- 1 | // use this script to test if your cert is working 2 | 3 | const https = require('https'); 4 | const fs = require('fs'); 5 | const port = 8000; 6 | 7 | console.log("Listening on " + port); 8 | 9 | const options = { 10 | pfx: fs.readFileSync('testCert.pfx') 11 | }; 12 | 13 | https.createServer(options, (req, res) => { 14 | console.log("Request from " + (req.socket ? req.socket.remoteAddress : "NULL")); 15 | res.writeHead(200); 16 | res.end('hello world\n'); 17 | }).listen(port); 18 | -------------------------------------------------------------------------------- /HowToCreateSSLCert.md: -------------------------------------------------------------------------------- 1 | # How to create and setup an SSL Cert 2 | 3 | If you host your webgl build on a https domain you will need to use wss which will require a ssl cert. 4 | 5 | ## pre-setup 6 | 7 | - You need a domain name 8 | - With dns record pointing at cloud server 9 | - Set up cloud server: [How to set up google cloud server](https://mirror-networking.com/docs/Articles/Guides/DevServer/gcloud/index.html) 10 | 11 | > note: You may need to open port 80 for certbot 12 | 13 | ## Get Cert 14 | 15 | Follows guides here: 16 | 17 | https://letsencrypt.org/getting-started/ 18 | https://certbot.eff.org/instructions 19 | 20 | Find the instructions for your server version, below is link for `Ubuntu 18.04 LTS (bionic)` 21 | 22 | https://certbot.eff.org/lets-encrypt/ubuntubionic-other 23 | 24 | For instruction 7 25 | 26 | ``` 27 | sudo certbot certonly --standalone 28 | ``` 29 | 30 | After filling in details you will get a result like this 31 | 32 | ``` 33 | IMPORTANT NOTES: 34 | - Congratulations! Your certificate and chain have been saved at: 35 | /etc/letsencrypt/live/simpleweb.example.com/fullchain.pem 36 | Your key file has been saved at: 37 | /etc/letsencrypt/live/simpleweb.example.com/privkey.pem 38 | Your cert will expire on 2021-01-07. To obtain a new or tweaked 39 | version of this certificate in the future, simply run certbot 40 | again. To non-interactively renew *all* of your certificates, run 41 | "certbot renew" 42 | - If you like Certbot, please consider supporting our work by: 43 | 44 | Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate 45 | Donating to EFF: https://eff.org/donate-le 46 | ``` 47 | 48 | `simpleweb.example.com` should be your domain 49 | 50 | ## Create cert.pfx 51 | 52 | To create a pfx file that SimpleWebTransport can use run this command in the `/etc/letsencrypt/live/simpleweb.example.com/` folder 53 | 54 | ```sh 55 | openssl pkcs12 -export -out cert.pfx -inkey privkey.pem -in cert.pem -certfile chain.pem 56 | ``` 57 | You will be asked for a password, you can set a password or leave it blank. 58 | 59 | You might need to be super user in order to do this: 60 | 61 | ``` 62 | su 63 | 64 | cd /etc/letsencrypt/live/simpleweb.example.com/ 65 | ``` 66 | 67 | ## Using cert.pfx 68 | 69 | You can either copy the cert.pfx file to your server folder or create a symbolic link 70 | 71 | Move 72 | ```sh 73 | mv /etc/letsencrypt/live/simpleweb.example.com/cert.pfx ~/path/to/server/cert.pfx 74 | ``` 75 | 76 | Symbolic link 77 | ```sh 78 | ln -s /etc/letsencrypt/live/simpleweb.example.com/cert.pfx ~/path/to/server/cert.pfx 79 | ``` 80 | 81 | ### create cert.json file 82 | 83 | Create a `cert.json` that SimpleWebTransport can read 84 | 85 | Run this command in the `~/path/to/server/` folder 86 | 87 | If you left the password blank at cert creation: 88 | ```sh 89 | echo '{ "path":"./cert.pfx", "password": "" }' > cert.json 90 | ``` 91 | 92 | If you set up a password "yourPassword" at cert creation: 93 | ```sh 94 | echo '{ "path":"./cert.pfx", "password": "yourPassword" }' > cert.json 95 | ``` 96 | 97 | ### Run your server 98 | 99 | After the `cert.json` and `cert.pfx` are in the server folder like this 100 | ``` 101 | ServerFolder 102 | |- demo_server.x86_64 103 | |- cert.json 104 | |- cert.pfx 105 | ``` 106 | 107 | Then make the server file executable 108 | ``` 109 | chmod +x demo_server.x86_64 110 | ``` 111 | 112 | To run in the active terminal use 113 | ``` 114 | ./demo_server.x86_64 115 | ``` 116 | 117 | 118 | To run in background use 119 | ``` 120 | nohup ./demo_server.x86_64 & 121 | ``` 122 | > `nohup` means: the executable will keep running after you close your ssh session 123 | the `&` sign means: that your server will run in background 124 | 125 | 126 | > you may need to use `sudo` to run if you created a symbolic link 127 | 128 | ### Connect to your game 129 | 130 | Test everything is working by connection using the editor or a build 131 | 132 | set your domain (eg `simpleweb.example.com`) in the hostname field and then start a client 133 | 134 | # Debug 135 | 136 | To check if your pfx file is working outside of unity you can use [pfxTestServer.js](DebugScripts/node\~/pfxTestServer.js). 137 | 138 | To use this install `nodejs` then set the pfx path and run it with `node pfxTestServer.js` 139 | 140 | You should then be able to visit `https://simpleweb.example.com:8000` and have the server response (change port and domain to fit your needs) 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mirror Networking 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=MirrorNetworking_SimpleWebTransport&metric=coverage)](https://sonarcloud.io/dashboard?id=MirrorNetworking_SimpleWebTransport) 2 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=MirrorNetworking_SimpleWebTransport&metric=alert_status)](https://sonarcloud.io/dashboard?id=MirrorNetworking_SimpleWebTransport) 3 | [![Discord](https://img.shields.io/discord/343440455738064897.svg)](https://discordapp.com/invite/N9QVxbM) 4 | [![release](https://img.shields.io/github/release/MirrorNetworking/SimpleWebTransport.svg)](https://github.com/MirrorNetworking/SimpleWebTransport/releases/latest) 5 | 6 | # Simple Web Transport 7 | 8 | Websocket Transport for [Mirror](https://github.com/vis2k/Mirror) 9 | 10 | This Transport uses the websocket protocol. This allows this transport to be used in WebGL builds of unity. 11 | 12 | ## Usage 13 | 14 | 1) Download the code from the source folder or package on [Release](https://github.com/MirrorNetworking/SimpleWebTransport/releases) page. 15 | 2) Put the code somewhere in your Assets folder 16 | 3) Replace your existing Transport with SimpleWebTransport on your NetworkManager 17 | 18 | 19 | ## Bugs? 20 | 21 | Please report any bugs or issues [Here](https://github.com/MirrorNetworking/SimpleWebTransport/issues) 22 | 23 | 24 | # Websocket Secure 25 | 26 | This transport supports the wss protocol which is required for https pages. 27 | 28 | ## How to create and setup an SSL Cert 29 | 30 | If you host your webgl build on a https domain you will need to use wss which will require a ssl cert. 31 | 32 | [See this page](./HowToCreateSSLCert.md) 33 | 34 | 35 | # Logging 36 | 37 | Log levels can be set using the dropdown on the transport or or setting `Mirror.SimpleWeb.Log.level`. 38 | 39 | The transport applies the dropdown value in its `Awake` and `OnValidate` methods. 40 | 41 | ### Log methods 42 | 43 | Log methods in this transport use the [ConditionalAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.conditionalattribute?view=netstandard-2.0) so they are removed depending on the preprocessor defines. 44 | 45 | These preprocessor defines effect the logging 46 | - `DEBUG` allows warn/error logs 47 | - `SIMPLEWEB_LOG_ENABLED` allows all logs 48 | 49 | Without `SIMPLEWEB_LOG_ENABLED` info or verbose logging will never happen even if log level allows it. 50 | 51 | See the [Unity docs](https://docs.unity3d.com/Manual/PlatformDependentCompilation.html) on how set custom defines. 52 | -------------------------------------------------------------------------------- /source/.cert.example.Json: -------------------------------------------------------------------------------- 1 | { 2 | "_readme_1": "Make a copy of this file and update the fields below. (readme fields should be deleted)", 3 | "_readme_2": "Include the json file and cert with your server build ONLY (put them outside of asset folder)", 4 | "_readme_path": "path is relative from cwd not this json file", 5 | "_readme_password": "password can be empty or left out", 6 | "path": "./certs/MirrorLocal.pfx", 7 | "password": "" 8 | } -------------------------------------------------------------------------------- /source/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")] 4 | [assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")] 5 | -------------------------------------------------------------------------------- /source/AssemblyInfo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ee9e76201f7665244bd6ab8ea343a83f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Client.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5faa957b8d9fc314ab7596ccf14750d9 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /source/Client/SimpleWebClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using UnityEngine; 4 | 5 | namespace Mirror.SimpleWeb 6 | { 7 | public enum ClientState 8 | { 9 | NotConnected = 0, 10 | Connecting = 1, 11 | Connected = 2, 12 | Disconnecting = 3, 13 | } 14 | /// 15 | /// Client used to control websockets 16 | /// Base class used by WebSocketClientWebGl and WebSocketClientStandAlone 17 | /// 18 | public abstract class SimpleWebClient 19 | { 20 | public static SimpleWebClient Create(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig) 21 | { 22 | #if UNITY_WEBGL && !UNITY_EDITOR 23 | return new WebSocketClientWebGl(maxMessageSize, maxMessagesPerTick); 24 | #else 25 | return new WebSocketClientStandAlone(maxMessageSize, maxMessagesPerTick, tcpConfig); 26 | #endif 27 | } 28 | 29 | readonly int maxMessagesPerTick; 30 | protected readonly int maxMessageSize; 31 | protected readonly ConcurrentQueue receiveQueue = new ConcurrentQueue(); 32 | protected readonly BufferPool bufferPool; 33 | 34 | protected ClientState state; 35 | 36 | protected SimpleWebClient(int maxMessageSize, int maxMessagesPerTick) 37 | { 38 | this.maxMessageSize = maxMessageSize; 39 | this.maxMessagesPerTick = maxMessagesPerTick; 40 | bufferPool = new BufferPool(5, 20, maxMessageSize); 41 | } 42 | 43 | public ClientState ConnectionState => state; 44 | 45 | public event Action onConnect; 46 | public event Action onDisconnect; 47 | public event Action> onData; 48 | public event Action onError; 49 | 50 | public void ProcessMessageQueue(MonoBehaviour behaviour) 51 | { 52 | int processedCount = 0; 53 | // check enabled every time incase behaviour was disabled after data 54 | while ( 55 | behaviour.enabled && 56 | processedCount < maxMessagesPerTick && 57 | // Dequeue last 58 | receiveQueue.TryDequeue(out Message next) 59 | ) 60 | { 61 | processedCount++; 62 | 63 | switch (next.type) 64 | { 65 | case EventType.Connected: 66 | onConnect?.Invoke(); 67 | break; 68 | case EventType.Data: 69 | onData?.Invoke(next.data.ToSegment()); 70 | next.data.Release(); 71 | break; 72 | case EventType.Disconnected: 73 | onDisconnect?.Invoke(); 74 | break; 75 | case EventType.Error: 76 | onError?.Invoke(next.exception); 77 | break; 78 | } 79 | } 80 | } 81 | 82 | public abstract void Connect(Uri serverAddress); 83 | public abstract void Disconnect(); 84 | public abstract void Send(ArraySegment segment); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /source/Client/SimpleWebClient.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13131761a0bf5a64dadeccd700fe26e5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Client/StandAlone.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a9c19d05220a87c4cbbe4d1e422da0aa 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /source/Client/StandAlone/ClientHandshake.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace Mirror.SimpleWeb 7 | { 8 | /// 9 | /// Handles Handshake to the server when it first connects 10 | /// The client handshake does not need buffers to reduce allocations since it only happens once 11 | /// 12 | internal class ClientHandshake 13 | { 14 | public bool TryHandshake(Connection conn, Uri uri) 15 | { 16 | try 17 | { 18 | Stream stream = conn.stream; 19 | 20 | byte[] keyBuffer = new byte[16]; 21 | using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) 22 | { 23 | rng.GetBytes(keyBuffer); 24 | } 25 | 26 | string key = Convert.ToBase64String(keyBuffer); 27 | string keySum = key + Constants.HandshakeGUID; 28 | byte[] keySumBytes = Encoding.ASCII.GetBytes(keySum); 29 | Log.Verbose($"Handshake Hashing {Encoding.ASCII.GetString(keySumBytes)}"); 30 | 31 | byte[] keySumHash = SHA1.Create().ComputeHash(keySumBytes); 32 | 33 | string expectedResponse = Convert.ToBase64String(keySumHash); 34 | string handshake = 35 | $"GET {uri.PathAndQuery} HTTP/1.1\r\n" + 36 | $"Host: {uri.Host}:{uri.Port}\r\n" + 37 | $"Upgrade: websocket\r\n" + 38 | $"Connection: Upgrade\r\n" + 39 | $"Sec-WebSocket-Key: {key}\r\n" + 40 | $"Sec-WebSocket-Version: 13\r\n" + 41 | "\r\n"; 42 | byte[] encoded = Encoding.ASCII.GetBytes(handshake); 43 | stream.Write(encoded, 0, encoded.Length); 44 | 45 | byte[] responseBuffer = new byte[1000]; 46 | 47 | int? lengthOrNull = ReadHelper.SafeReadTillMatch(stream, responseBuffer, 0, responseBuffer.Length, Constants.endOfHandshake); 48 | 49 | if (!lengthOrNull.HasValue) 50 | { 51 | Log.Error("Connected closed before handshake"); 52 | return false; 53 | } 54 | 55 | string responseString = Encoding.ASCII.GetString(responseBuffer, 0, lengthOrNull.Value); 56 | 57 | string acceptHeader = "Sec-WebSocket-Accept: "; 58 | int startIndex = responseString.IndexOf(acceptHeader) + acceptHeader.Length; 59 | int endIndex = responseString.IndexOf("\r\n", startIndex); 60 | string responseKey = responseString.Substring(startIndex, endIndex - startIndex); 61 | 62 | if (responseKey != expectedResponse) 63 | { 64 | Log.Error($"Response key incorrect, Response:{responseKey} Expected:{expectedResponse}"); 65 | return false; 66 | } 67 | 68 | return true; 69 | } 70 | catch (Exception e) 71 | { 72 | Log.Exception(e); 73 | return false; 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /source/Client/StandAlone/ClientHandshake.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3ffdcabc9e28f764a94fc4efc82d3e8b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Client/StandAlone/ClientSslHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Security; 4 | using System.Net.Sockets; 5 | using System.Security.Cryptography.X509Certificates; 6 | 7 | namespace Mirror.SimpleWeb 8 | { 9 | internal class ClientSslHelper 10 | { 11 | internal bool TryCreateStream(Connection conn, Uri uri) 12 | { 13 | NetworkStream stream = conn.client.GetStream(); 14 | if (uri.Scheme != "wss") 15 | { 16 | conn.stream = stream; 17 | return true; 18 | } 19 | 20 | try 21 | { 22 | conn.stream = CreateStream(stream, uri); 23 | return true; 24 | } 25 | catch (Exception e) 26 | { 27 | Log.Error($"Create SSLStream Failed: {e}", false); 28 | return false; 29 | } 30 | } 31 | 32 | Stream CreateStream(NetworkStream stream, Uri uri) 33 | { 34 | SslStream sslStream = new SslStream(stream, true, ValidateServerCertificate); 35 | sslStream.AuthenticateAsClient(uri.Host); 36 | return sslStream; 37 | } 38 | 39 | static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) 40 | { 41 | // Do not allow this client to communicate with unauthenticated servers. 42 | 43 | // only accept if no errors 44 | return sslPolicyErrors == SslPolicyErrors.None; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /source/Client/StandAlone/ClientSslHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 46055a75559a79849a750f39a766db61 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Client/StandAlone/WebSocketClientStandAlone.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Threading; 4 | 5 | namespace Mirror.SimpleWeb 6 | { 7 | public class WebSocketClientStandAlone : SimpleWebClient 8 | { 9 | readonly ClientSslHelper sslHelper; 10 | readonly ClientHandshake handshake; 11 | readonly TcpConfig tcpConfig; 12 | Connection conn; 13 | 14 | 15 | internal WebSocketClientStandAlone(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig) : base(maxMessageSize, maxMessagesPerTick) 16 | { 17 | #if UNITY_WEBGL && !UNITY_EDITOR 18 | throw new NotSupportedException(); 19 | #else 20 | sslHelper = new ClientSslHelper(); 21 | handshake = new ClientHandshake(); 22 | this.tcpConfig = tcpConfig; 23 | #endif 24 | } 25 | 26 | public override void Connect(Uri serverAddress) 27 | { 28 | state = ClientState.Connecting; 29 | Thread receiveThread = new Thread(() => ConnectAndReceiveLoop(serverAddress)); 30 | receiveThread.IsBackground = true; 31 | receiveThread.Start(); 32 | } 33 | 34 | void ConnectAndReceiveLoop(Uri serverAddress) 35 | { 36 | try 37 | { 38 | TcpClient client = new TcpClient(); 39 | tcpConfig.ApplyTo(client); 40 | 41 | // create connection object here so dispose correctly disconnects on failed connect 42 | conn = new Connection(client, AfterConnectionDisposed); 43 | conn.receiveThread = Thread.CurrentThread; 44 | 45 | try 46 | { 47 | client.Connect(serverAddress.Host, serverAddress.Port); 48 | } 49 | catch (SocketException) 50 | { 51 | client.Dispose(); 52 | throw; 53 | } 54 | 55 | 56 | bool success = sslHelper.TryCreateStream(conn, serverAddress); 57 | if (!success) 58 | { 59 | Log.Warn("Failed to create Stream"); 60 | conn.Dispose(); 61 | return; 62 | } 63 | 64 | success = handshake.TryHandshake(conn, serverAddress); 65 | if (!success) 66 | { 67 | Log.Warn("Failed Handshake"); 68 | conn.Dispose(); 69 | return; 70 | } 71 | 72 | Log.Info("HandShake Successful"); 73 | 74 | state = ClientState.Connected; 75 | 76 | receiveQueue.Enqueue(new Message(EventType.Connected)); 77 | 78 | Thread sendThread = new Thread(() => 79 | { 80 | SendLoop.Config sendConfig = new SendLoop.Config( 81 | conn, 82 | bufferSize: Constants.HeaderSize + Constants.MaskSize + maxMessageSize, 83 | setMask: true); 84 | 85 | SendLoop.Loop(sendConfig); 86 | }); 87 | 88 | conn.sendThread = sendThread; 89 | sendThread.IsBackground = true; 90 | sendThread.Start(); 91 | 92 | ReceiveLoop.Config config = new ReceiveLoop.Config(conn, 93 | maxMessageSize, 94 | false, 95 | receiveQueue, 96 | bufferPool); 97 | ReceiveLoop.Loop(config); 98 | } 99 | catch (ThreadInterruptedException e) { Log.InfoException(e); } 100 | catch (ThreadAbortException e) { Log.InfoException(e); } 101 | catch (Exception e) { Log.Exception(e); } 102 | finally 103 | { 104 | // close here incase connect fails 105 | conn?.Dispose(); 106 | } 107 | } 108 | 109 | void AfterConnectionDisposed(Connection conn) 110 | { 111 | state = ClientState.NotConnected; 112 | // make sure Disconnected event is only called once 113 | receiveQueue.Enqueue(new Message(EventType.Disconnected)); 114 | } 115 | 116 | public override void Disconnect() 117 | { 118 | state = ClientState.Disconnecting; 119 | Log.Info("Disconnect Called"); 120 | if (conn == null) 121 | { 122 | state = ClientState.NotConnected; 123 | } 124 | else 125 | { 126 | conn?.Dispose(); 127 | } 128 | } 129 | 130 | public override void Send(ArraySegment segment) 131 | { 132 | ArrayBuffer buffer = bufferPool.Take(segment.Count); 133 | buffer.CopyFrom(segment); 134 | 135 | conn.sendQueue.Enqueue(buffer); 136 | conn.sendPending.Set(); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /source/Client/StandAlone/WebSocketClientStandAlone.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 05a9c87dea309e241a9185e5aa0d72ab 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Client/Webgl.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7142349d566213c4abc763afaf4d91a1 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /source/Client/Webgl/SimpleWebJSLib.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | #if UNITY_WEBGL 3 | using System.Runtime.InteropServices; 4 | #endif 5 | 6 | namespace Mirror.SimpleWeb 7 | { 8 | internal static class SimpleWebJSLib 9 | { 10 | #if UNITY_WEBGL 11 | [DllImport("__Internal")] 12 | internal static extern bool IsConnected(int index); 13 | 14 | #pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments 15 | [DllImport("__Internal")] 16 | #pragma warning restore CA2101 // Specify marshaling for P/Invoke string arguments 17 | internal static extern int Connect(string address, Action openCallback, Action closeCallBack, Action messageCallback, Action errorCallback); 18 | 19 | [DllImport("__Internal")] 20 | internal static extern void Disconnect(int index); 21 | 22 | [DllImport("__Internal")] 23 | internal static extern bool Send(int index, byte[] array, int offset, int length); 24 | #else 25 | internal static bool IsConnected(int index) => throw new NotSupportedException(); 26 | 27 | internal static int Connect(string address, Action openCallback, Action closeCallBack, Action messageCallback, Action errorCallback) => throw new NotSupportedException(); 28 | 29 | internal static void Disconnect(int index) => throw new NotSupportedException(); 30 | 31 | internal static bool Send(int index, byte[] array, int offset, int length) => throw new NotSupportedException(); 32 | #endif 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/Client/Webgl/SimpleWebJSLib.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 97b96a0b65c104443977473323c2ff35 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Client/Webgl/WebSocketClientWebGl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using AOT; 4 | 5 | namespace Mirror.SimpleWeb 6 | { 7 | public class WebSocketClientWebGl : SimpleWebClient 8 | { 9 | static readonly Dictionary instances = new Dictionary(); 10 | 11 | /// 12 | /// key for instances sent between c# and js 13 | /// 14 | int index; 15 | 16 | internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base(maxMessageSize, maxMessagesPerTick) 17 | { 18 | #if !UNITY_WEBGL || UNITY_EDITOR 19 | throw new NotSupportedException(); 20 | #endif 21 | } 22 | 23 | public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index); 24 | 25 | public override void Connect(Uri serverAddress) 26 | { 27 | index = SimpleWebJSLib.Connect(serverAddress.ToString(), OpenCallback, CloseCallBack, MessageCallback, ErrorCallback); 28 | instances.Add(index, this); 29 | state = ClientState.Connecting; 30 | } 31 | 32 | public override void Disconnect() 33 | { 34 | state = ClientState.Disconnecting; 35 | // disconnect should cause closeCallback and OnDisconnect to be called 36 | SimpleWebJSLib.Disconnect(index); 37 | } 38 | 39 | public override void Send(ArraySegment segment) 40 | { 41 | if (segment.Count > maxMessageSize) 42 | { 43 | Log.Error($"Cant send message with length {segment.Count} because it is over the max size of {maxMessageSize}"); 44 | return; 45 | } 46 | 47 | SimpleWebJSLib.Send(index, segment.Array, 0, segment.Count); 48 | } 49 | 50 | void onOpen() 51 | { 52 | receiveQueue.Enqueue(new Message(EventType.Connected)); 53 | state = ClientState.Connected; 54 | } 55 | 56 | void onClose() 57 | { 58 | // this code should be last in this class 59 | 60 | receiveQueue.Enqueue(new Message(EventType.Disconnected)); 61 | state = ClientState.NotConnected; 62 | instances.Remove(index); 63 | } 64 | 65 | void onMessage(IntPtr bufferPtr, int count) 66 | { 67 | try 68 | { 69 | ArrayBuffer buffer = bufferPool.Take(count); 70 | buffer.CopyFrom(bufferPtr, count); 71 | 72 | receiveQueue.Enqueue(new Message(buffer)); 73 | } 74 | catch (Exception e) 75 | { 76 | Log.Error($"onData {e.GetType()}: {e.Message}\n{e.StackTrace}"); 77 | receiveQueue.Enqueue(new Message(e)); 78 | } 79 | } 80 | 81 | void onErr() 82 | { 83 | receiveQueue.Enqueue(new Message(new Exception("Javascript Websocket error"))); 84 | Disconnect(); 85 | } 86 | 87 | [MonoPInvokeCallback(typeof(Action))] 88 | static void OpenCallback(int index) => instances[index].onOpen(); 89 | 90 | [MonoPInvokeCallback(typeof(Action))] 91 | static void CloseCallBack(int index) => instances[index].onClose(); 92 | 93 | [MonoPInvokeCallback(typeof(Action))] 94 | static void MessageCallback(int index, IntPtr bufferPtr, int count) => instances[index].onMessage(bufferPtr, count); 95 | 96 | [MonoPInvokeCallback(typeof(Action))] 97 | static void ErrorCallback(int index) => instances[index].onErr(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /source/Client/Webgl/WebSocketClientWebGl.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 015c5b1915fd1a64cbe36444d16b2f7d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Client/Webgl/plugin.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1999985791b91b9458059e88404885a7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /source/Client/Webgl/plugin/SimpleWeb.jslib: -------------------------------------------------------------------------------- 1 | // this will create a global object 2 | const SimpleWeb = { 3 | webSockets: [], 4 | next: 1, 5 | GetWebSocket: function (index) { 6 | return SimpleWeb.webSockets[index] 7 | }, 8 | AddNextSocket: function (webSocket) { 9 | var index = SimpleWeb.next; 10 | SimpleWeb.next++; 11 | SimpleWeb.webSockets[index] = webSocket; 12 | return index; 13 | }, 14 | RemoveSocket: function (index) { 15 | SimpleWeb.webSockets[index] = undefined; 16 | }, 17 | }; 18 | 19 | function IsConnected(index) { 20 | var webSocket = SimpleWeb.GetWebSocket(index); 21 | if (webSocket) { 22 | return webSocket.readyState === webSocket.OPEN; 23 | } 24 | else { 25 | return false; 26 | } 27 | } 28 | 29 | function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr) { 30 | const address = Pointer_stringify(addressPtr); 31 | console.log("Connecting to " + address); 32 | // Create webSocket connection. 33 | webSocket = new WebSocket(address); 34 | webSocket.binaryType = 'arraybuffer'; 35 | const index = SimpleWeb.AddNextSocket(webSocket); 36 | 37 | // Connection opened 38 | webSocket.addEventListener('open', function (event) { 39 | console.log("Connected to " + address); 40 | Runtime.dynCall('vi', openCallbackPtr, [index]); 41 | }); 42 | webSocket.addEventListener('close', function (event) { 43 | console.log("Disconnected from " + address); 44 | Runtime.dynCall('vi', closeCallBackPtr, [index]); 45 | }); 46 | 47 | // Listen for messages 48 | webSocket.addEventListener('message', function (event) { 49 | if (event.data instanceof ArrayBuffer) { 50 | // TODO dont alloc each time 51 | var array = new Uint8Array(event.data); 52 | var arrayLength = array.length; 53 | 54 | var bufferPtr = _malloc(arrayLength); 55 | var dataBuffer = new Uint8Array(HEAPU8.buffer, bufferPtr, arrayLength); 56 | dataBuffer.set(array); 57 | 58 | Runtime.dynCall('viii', messageCallbackPtr, [index, bufferPtr, arrayLength]); 59 | _free(bufferPtr); 60 | } 61 | else { 62 | console.error("message type not supported") 63 | } 64 | }); 65 | 66 | webSocket.addEventListener('error', function (event) { 67 | console.error('Socket Error', event); 68 | 69 | Runtime.dynCall('vi', errorCallbackPtr, [index]); 70 | }); 71 | 72 | return index; 73 | } 74 | 75 | function Disconnect(index) { 76 | var webSocket = SimpleWeb.GetWebSocket(index); 77 | if (webSocket) { 78 | webSocket.close(1000, "Disconnect Called by Mirror"); 79 | } 80 | 81 | SimpleWeb.RemoveSocket(index); 82 | } 83 | 84 | function Send(index, arrayPtr, offset, length) { 85 | var webSocket = SimpleWeb.GetWebSocket(index); 86 | if (webSocket) { 87 | const start = arrayPtr + offset; 88 | const end = start + length; 89 | const data = HEAPU8.buffer.slice(start, end); 90 | webSocket.send(data); 91 | return true; 92 | } 93 | return false; 94 | } 95 | 96 | 97 | const SimpleWebLib = { 98 | $SimpleWeb: SimpleWeb, 99 | IsConnected, 100 | Connect, 101 | Disconnect, 102 | Send 103 | }; 104 | autoAddDeps(SimpleWebLib, '$SimpleWeb'); 105 | mergeInto(LibraryManager.library, SimpleWebLib); -------------------------------------------------------------------------------- /source/Client/Webgl/plugin/SimpleWeb.jslib.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 54452a8c6d2ca9b49a8c79f81b50305c 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | Any: 16 | second: 17 | enabled: 0 18 | settings: {} 19 | - first: 20 | Editor: Editor 21 | second: 22 | enabled: 0 23 | settings: 24 | DefaultValueInitialized: true 25 | - first: 26 | Facebook: WebGL 27 | second: 28 | enabled: 1 29 | settings: {} 30 | - first: 31 | WebGL: WebGL 32 | second: 33 | enabled: 1 34 | settings: {} 35 | userData: 36 | assetBundleName: 37 | assetBundleVariant: 38 | -------------------------------------------------------------------------------- /source/Common.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 564d2cd3eee5b21419553c0528739d1b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /source/Common/BufferPool.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 94ae50f3ec35667469b861b12cd72f92 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/Connection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.IO; 4 | using System.Net.Sockets; 5 | using System.Threading; 6 | 7 | namespace Mirror.SimpleWeb 8 | { 9 | internal sealed class Connection : IDisposable 10 | { 11 | public const int IdNotSet = -1; 12 | 13 | readonly object disposedLock = new object(); 14 | 15 | public TcpClient client; 16 | 17 | public int connId = IdNotSet; 18 | public Stream stream; 19 | public Thread receiveThread; 20 | public Thread sendThread; 21 | 22 | public ManualResetEventSlim sendPending = new ManualResetEventSlim(false); 23 | public ConcurrentQueue sendQueue = new ConcurrentQueue(); 24 | 25 | public Action onDispose; 26 | 27 | volatile bool hasDisposed; 28 | 29 | public Connection(TcpClient client, Action onDispose) 30 | { 31 | this.client = client ?? throw new ArgumentNullException(nameof(client)); 32 | this.onDispose = onDispose; 33 | } 34 | 35 | 36 | /// 37 | /// disposes client and stops threads 38 | /// 39 | public void Dispose() 40 | { 41 | Log.Verbose($"Dispose {ToString()}"); 42 | 43 | // check hasDisposed first to stop ThreadInterruptedException on lock 44 | if (hasDisposed) { return; } 45 | 46 | Log.Info($"Connection Close: {ToString()}"); 47 | 48 | 49 | lock (disposedLock) 50 | { 51 | // check hasDisposed again inside lock to make sure no other object has called this 52 | if (hasDisposed) { return; } 53 | hasDisposed = true; 54 | 55 | // stop threads first so they dont try to use disposed objects 56 | receiveThread.Interrupt(); 57 | sendThread?.Interrupt(); 58 | 59 | try 60 | { 61 | // stream 62 | stream?.Dispose(); 63 | stream = null; 64 | client.Dispose(); 65 | client = null; 66 | } 67 | catch (Exception e) 68 | { 69 | Log.Exception(e); 70 | } 71 | 72 | sendPending.Dispose(); 73 | 74 | // release all buffers in send queue 75 | while (sendQueue.TryDequeue(out ArrayBuffer buffer)) 76 | { 77 | buffer.Release(); 78 | } 79 | 80 | onDispose.Invoke(this); 81 | } 82 | } 83 | 84 | public override string ToString() 85 | { 86 | System.Net.EndPoint endpoint = client?.Client?.RemoteEndPoint; 87 | return $"[Conn:{connId}, endPoint:{endpoint}]"; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /source/Common/Connection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a13073c2b49d39943888df45174851bd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/Constants.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Mirror.SimpleWeb 4 | { 5 | /// 6 | /// Constant values that should never change 7 | /// 8 | /// Some values are from https://tools.ietf.org/html/rfc6455 9 | /// 10 | /// 11 | internal static class Constants 12 | { 13 | /// 14 | /// Header is at most 4 bytes 15 | /// 16 | /// If message is less than 125 then header is 2 bytes, else header is 4 bytes 17 | /// 18 | /// 19 | public const int HeaderSize = 4; 20 | 21 | /// 22 | /// Smallest size of header 23 | /// 24 | /// If message is less than 125 then header is 2 bytes, else header is 4 bytes 25 | /// 26 | /// 27 | public const int HeaderMinSize = 2; 28 | 29 | /// 30 | /// bytes for short length 31 | /// 32 | public const int ShortLength = 2; 33 | 34 | /// 35 | /// Message mask is always 4 bytes 36 | /// 37 | public const int MaskSize = 4; 38 | 39 | /// 40 | /// Max size of a message for length to be 1 byte long 41 | /// 42 | /// payload length between 0-125 43 | /// 44 | /// 45 | public const int BytePayloadLength = 125; 46 | 47 | /// 48 | /// if payload length is 126 when next 2 bytes will be the length 49 | /// 50 | public const int UshortPayloadLength = 126; 51 | 52 | /// 53 | /// if payload length is 127 when next 8 bytes will be the length 54 | /// 55 | public const int UlongPayloadLength = 127; 56 | 57 | 58 | /// 59 | /// Guid used for WebSocket Protocol 60 | /// 61 | public const string HandshakeGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 62 | 63 | public static readonly int HandshakeGUIDLength = HandshakeGUID.Length; 64 | 65 | public static readonly byte[] HandshakeGUIDBytes = Encoding.ASCII.GetBytes(HandshakeGUID); 66 | 67 | /// 68 | /// Handshake messages will end with \r\n\r\n 69 | /// 70 | public static readonly byte[] endOfHandshake = new byte[4] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /source/Common/Constants.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85d110a089d6ad348abf2d073ebce7cd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/EventType.cs: -------------------------------------------------------------------------------- 1 | namespace Mirror.SimpleWeb 2 | { 3 | public enum EventType 4 | { 5 | Connected, 6 | Data, 7 | Disconnected, 8 | Error 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /source/Common/EventType.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2d9cd7d2b5229ab42a12e82ae17d0347 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using Conditional = System.Diagnostics.ConditionalAttribute; 4 | 5 | namespace Mirror.SimpleWeb 6 | { 7 | public static class Log 8 | { 9 | // used for Conditional 10 | const string SIMPLEWEB_LOG_ENABLED = nameof(SIMPLEWEB_LOG_ENABLED); 11 | const string DEBUG = nameof(DEBUG); 12 | 13 | public enum Levels 14 | { 15 | none = 0, 16 | error = 1, 17 | warn = 2, 18 | info = 3, 19 | verbose = 4, 20 | } 21 | 22 | public static ILogger logger = Debug.unityLogger; 23 | public static Levels level = Levels.none; 24 | 25 | public static string BufferToString(byte[] buffer, int offset = 0, int? length = null) 26 | { 27 | return BitConverter.ToString(buffer, offset, length ?? buffer.Length); 28 | } 29 | 30 | [Conditional(SIMPLEWEB_LOG_ENABLED)] 31 | public static void DumpBuffer(string label, byte[] buffer, int offset, int length) 32 | { 33 | if (level < Levels.verbose) 34 | return; 35 | 36 | logger.Log(LogType.Log, $"VERBOSE: {label}: {BufferToString(buffer, offset, length)}"); 37 | } 38 | 39 | [Conditional(SIMPLEWEB_LOG_ENABLED)] 40 | public static void DumpBuffer(string label, ArrayBuffer arrayBuffer) 41 | { 42 | if (level < Levels.verbose) 43 | return; 44 | 45 | logger.Log(LogType.Log, $"VERBOSE: {label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}"); 46 | } 47 | 48 | [Conditional(SIMPLEWEB_LOG_ENABLED)] 49 | public static void Verbose(string msg, bool showColor = true) 50 | { 51 | if (level < Levels.verbose) 52 | return; 53 | 54 | if (showColor) 55 | logger.Log(LogType.Log, $"VERBOSE: {msg}"); 56 | else 57 | logger.Log(LogType.Log, $"VERBOSE: {msg}"); 58 | } 59 | 60 | [Conditional(SIMPLEWEB_LOG_ENABLED)] 61 | public static void Info(string msg, bool showColor = true) 62 | { 63 | if (level < Levels.info) 64 | return; 65 | 66 | if (showColor) 67 | logger.Log(LogType.Log, $"INFO: {msg}"); 68 | else 69 | logger.Log(LogType.Log, $"INFO: {msg}"); 70 | } 71 | 72 | /// 73 | /// An expected Exception was caught, useful for debugging but not important 74 | /// 75 | /// 76 | /// 77 | [Conditional(SIMPLEWEB_LOG_ENABLED)] 78 | public static void InfoException(Exception e) 79 | { 80 | if (level < Levels.info) 81 | return; 82 | 83 | logger.Log(LogType.Log, $"INFO_EXCEPTION: {e.GetType().Name} Message: {e.Message}"); 84 | } 85 | 86 | [Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)] 87 | public static void Warn(string msg, bool showColor = true) 88 | { 89 | if (level < Levels.warn) 90 | return; 91 | 92 | if (showColor) 93 | logger.Log(LogType.Warning, $"WARN: {msg}"); 94 | else 95 | logger.Log(LogType.Warning, $"WARN: {msg}"); 96 | } 97 | 98 | [Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)] 99 | public static void Error(string msg, bool showColor = true) 100 | { 101 | if (level < Levels.error) 102 | return; 103 | 104 | if (showColor) 105 | logger.Log(LogType.Error, $"ERROR: {msg}"); 106 | else 107 | logger.Log(LogType.Error, $"ERROR: {msg}"); 108 | } 109 | 110 | public static void Exception(Exception e) 111 | { 112 | // always log Exceptions 113 | logger.Log(LogType.Error, $"EXCEPTION: {e.GetType().Name} Message: {e.Message}"); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /source/Common/Log.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3cf1521098e04f74fbea0fe2aa0439f8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mirror.SimpleWeb 4 | { 5 | public struct Message 6 | { 7 | public readonly int connId; 8 | public readonly EventType type; 9 | public readonly ArrayBuffer data; 10 | public readonly Exception exception; 11 | 12 | public Message(EventType type) : this() 13 | { 14 | this.type = type; 15 | } 16 | 17 | public Message(ArrayBuffer data) : this() 18 | { 19 | type = EventType.Data; 20 | this.data = data; 21 | } 22 | 23 | public Message(Exception exception) : this() 24 | { 25 | type = EventType.Error; 26 | this.exception = exception; 27 | } 28 | 29 | public Message(int connId, EventType type) : this() 30 | { 31 | this.connId = connId; 32 | this.type = type; 33 | } 34 | 35 | public Message(int connId, ArrayBuffer data) : this() 36 | { 37 | this.connId = connId; 38 | type = EventType.Data; 39 | this.data = data; 40 | } 41 | 42 | public Message(int connId, Exception exception) : this() 43 | { 44 | this.connId = connId; 45 | type = EventType.Error; 46 | this.exception = exception; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /source/Common/Message.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f5d05d71b09d2714b96ffe80bc3d2a77 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/MessageProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Mirror.SimpleWeb 5 | { 6 | public static class MessageProcessor 7 | { 8 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 9 | static byte FirstLengthByte(byte[] buffer) => (byte)(buffer[1] & 0b0111_1111); 10 | 11 | public static bool NeedToReadShortLength(byte[] buffer) 12 | { 13 | byte lenByte = FirstLengthByte(buffer); 14 | 15 | return lenByte >= Constants.UshortPayloadLength; 16 | } 17 | 18 | public static int GetOpcode(byte[] buffer) 19 | { 20 | return buffer[0] & 0b0000_1111; 21 | } 22 | 23 | public static int GetPayloadLength(byte[] buffer) 24 | { 25 | byte lenByte = FirstLengthByte(buffer); 26 | return GetMessageLength(buffer, 0, lenByte); 27 | } 28 | 29 | public static void ValidateHeader(byte[] buffer, int maxLength, bool expectMask) 30 | { 31 | bool finished = (buffer[0] & 0b1000_0000) != 0; // has full message been sent 32 | 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" 33 | 34 | int opcode = buffer[0] & 0b0000_1111; // expecting 1 - text message 35 | byte lenByte = FirstLengthByte(buffer); 36 | 37 | ThrowIfNotFinished(finished); 38 | ThrowIfMaskNotExpected(hasMask, expectMask); 39 | ThrowIfBadOpCode(opcode); 40 | 41 | int msglen = GetMessageLength(buffer, 0, lenByte); 42 | 43 | ThrowIfLengthZero(msglen); 44 | ThrowIfMsgLengthTooLong(msglen, maxLength); 45 | } 46 | 47 | public static void ToggleMask(byte[] src, int sourceOffset, int messageLength, byte[] maskBuffer, int maskOffset) 48 | { 49 | ToggleMask(src, sourceOffset, src, sourceOffset, messageLength, maskBuffer, maskOffset); 50 | } 51 | 52 | public static void ToggleMask(byte[] src, int sourceOffset, ArrayBuffer dst, int messageLength, byte[] maskBuffer, int maskOffset) 53 | { 54 | ToggleMask(src, sourceOffset, dst.array, 0, messageLength, maskBuffer, maskOffset); 55 | dst.count = messageLength; 56 | } 57 | 58 | public static void ToggleMask(byte[] src, int srcOffset, byte[] dst, int dstOffset, int messageLength, byte[] maskBuffer, int maskOffset) 59 | { 60 | for (int i = 0; i < messageLength; i++) 61 | { 62 | byte maskByte = maskBuffer[maskOffset + i % Constants.MaskSize]; 63 | dst[dstOffset + i] = (byte)(src[srcOffset + i] ^ maskByte); 64 | } 65 | } 66 | 67 | /// 68 | static int GetMessageLength(byte[] buffer, int offset, byte lenByte) 69 | { 70 | if (lenByte == Constants.UshortPayloadLength) 71 | { 72 | // header is 4 bytes long 73 | ushort value = 0; 74 | value |= (ushort)(buffer[offset + 2] << 8); 75 | value |= buffer[offset + 3]; 76 | 77 | return value; 78 | } 79 | else if (lenByte == Constants.UlongPayloadLength) 80 | { 81 | throw new InvalidDataException("Max length is longer than allowed in a single message"); 82 | } 83 | else // is less than 126 84 | { 85 | // header is 2 bytes long 86 | return lenByte; 87 | } 88 | } 89 | 90 | /// 91 | static void ThrowIfNotFinished(bool finished) 92 | { 93 | if (!finished) 94 | { 95 | throw new InvalidDataException("Full message should have been sent, if the full message wasn't sent it wasn't sent from this trasnport"); 96 | } 97 | } 98 | 99 | /// 100 | static void ThrowIfMaskNotExpected(bool hasMask, bool expectMask) 101 | { 102 | if (hasMask != expectMask) 103 | { 104 | throw new InvalidDataException($"Message expected mask to be {expectMask} but was {hasMask}"); 105 | } 106 | } 107 | 108 | /// 109 | static void ThrowIfBadOpCode(int opcode) 110 | { 111 | // 2 = binary 112 | // 8 = close 113 | if (opcode != 2 && opcode != 8) 114 | { 115 | throw new InvalidDataException("Expected opcode to be binary or close"); 116 | } 117 | } 118 | 119 | /// 120 | static void ThrowIfLengthZero(int msglen) 121 | { 122 | if (msglen == 0) 123 | { 124 | throw new InvalidDataException("Message length was zero"); 125 | } 126 | } 127 | 128 | /// 129 | /// need to check this so that data from previous buffer isnt used 130 | /// 131 | /// 132 | static void ThrowIfMsgLengthTooLong(int msglen, int maxLength) 133 | { 134 | if (msglen > maxLength) 135 | { 136 | throw new InvalidDataException("Message length is greater than max length"); 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /source/Common/MessageProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4c1f218a2b16ca846aaf23260078e549 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/ReadHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.Serialization; 4 | 5 | namespace Mirror.SimpleWeb 6 | { 7 | public static class ReadHelper 8 | { 9 | /// 10 | /// Reads exactly length from stream 11 | /// 12 | /// outOffset + length 13 | /// 14 | public static int Read(Stream stream, byte[] outBuffer, int outOffset, int length) 15 | { 16 | int received = 0; 17 | try 18 | { 19 | while (received < length) 20 | { 21 | int read = stream.Read(outBuffer, outOffset + received, length - received); 22 | if (read == 0) 23 | { 24 | throw new ReadHelperException("returned 0"); 25 | } 26 | received += read; 27 | } 28 | } 29 | catch (AggregateException ae) 30 | { 31 | // if interupt is called we dont care about Exceptions 32 | Utils.CheckForInterupt(); 33 | 34 | // rethrow 35 | ae.Handle(e => false); 36 | } 37 | 38 | if (received != length) 39 | { 40 | throw new ReadHelperException("returned not equal to length"); 41 | } 42 | 43 | return outOffset + received; 44 | } 45 | 46 | /// 47 | /// Reads and returns results. This should never throw an exception 48 | /// 49 | public static bool TryRead(Stream stream, byte[] outBuffer, int outOffset, int length) 50 | { 51 | try 52 | { 53 | Read(stream, outBuffer, outOffset, length); 54 | return true; 55 | } 56 | catch (ReadHelperException) 57 | { 58 | return false; 59 | } 60 | catch (IOException) 61 | { 62 | return false; 63 | } 64 | catch (Exception e) 65 | { 66 | Log.Exception(e); 67 | return false; 68 | } 69 | } 70 | 71 | public static int? SafeReadTillMatch(Stream stream, byte[] outBuffer, int outOffset, int maxLength, byte[] endOfHeader) 72 | { 73 | try 74 | { 75 | int read = 0; 76 | int endIndex = 0; 77 | int endLength = endOfHeader.Length; 78 | while (true) 79 | { 80 | int next = stream.ReadByte(); 81 | if (next == -1) // closed 82 | return null; 83 | 84 | if (read >= maxLength) 85 | { 86 | Log.Error("SafeReadTillMatch exceeded maxLength"); 87 | return null; 88 | } 89 | 90 | outBuffer[outOffset + read] = (byte)next; 91 | read++; 92 | 93 | // if n is match, check n+1 next 94 | if (endOfHeader[endIndex] == next) 95 | { 96 | endIndex++; 97 | // when all is match return with read length 98 | if (endIndex >= endLength) 99 | { 100 | return read; 101 | } 102 | } 103 | // if n not match reset to 0 104 | else 105 | { 106 | endIndex = 0; 107 | } 108 | } 109 | } 110 | catch (IOException e) 111 | { 112 | Log.InfoException(e); 113 | return null; 114 | } 115 | catch (Exception e) 116 | { 117 | Log.Exception(e); 118 | return null; 119 | } 120 | } 121 | } 122 | 123 | [Serializable] 124 | public class ReadHelperException : Exception 125 | { 126 | public ReadHelperException(string message) : base(message) { } 127 | 128 | protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context) 129 | { 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /source/Common/ReadHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9f4fa5d324e708c46a55810a97de75bc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/ReceiveLoop.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a26c2815f58431c4a98c158c8b655ffd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/SendLoop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Sockets; 4 | using System.Security.Cryptography; 5 | using System.Threading; 6 | using UnityEngine.Profiling; 7 | 8 | namespace Mirror.SimpleWeb 9 | { 10 | public static class SendLoopConfig 11 | { 12 | public static volatile bool batchSend = false; 13 | public static volatile bool sleepBeforeSend = false; 14 | } 15 | internal static class SendLoop 16 | { 17 | public struct Config 18 | { 19 | public readonly Connection conn; 20 | public readonly int bufferSize; 21 | public readonly bool setMask; 22 | 23 | public Config(Connection conn, int bufferSize, bool setMask) 24 | { 25 | this.conn = conn ?? throw new ArgumentNullException(nameof(conn)); 26 | this.bufferSize = bufferSize; 27 | this.setMask = setMask; 28 | } 29 | 30 | public void Deconstruct(out Connection conn, out int bufferSize, out bool setMask) 31 | { 32 | conn = this.conn; 33 | bufferSize = this.bufferSize; 34 | setMask = this.setMask; 35 | } 36 | } 37 | 38 | public static void Loop(Config config) 39 | { 40 | (Connection conn, int bufferSize, bool setMask) = config; 41 | 42 | Profiler.BeginThreadProfiling("SimpleWeb", $"SendLoop {conn.connId}"); 43 | 44 | // create write buffer for this thread 45 | byte[] writeBuffer = new byte[bufferSize]; 46 | MaskHelper maskHelper = setMask ? new MaskHelper() : null; 47 | try 48 | { 49 | TcpClient client = conn.client; 50 | Stream stream = conn.stream; 51 | 52 | // null check incase disconnect while send thread is starting 53 | if (client == null) 54 | return; 55 | 56 | while (client.Connected) 57 | { 58 | // wait for message 59 | conn.sendPending.Wait(); 60 | // wait for 1ms for mirror to send other messages 61 | if (SendLoopConfig.sleepBeforeSend) 62 | { 63 | Thread.Sleep(1); 64 | } 65 | conn.sendPending.Reset(); 66 | 67 | if (SendLoopConfig.batchSend) 68 | { 69 | int offset = 0; 70 | while (conn.sendQueue.TryDequeue(out ArrayBuffer msg)) 71 | { 72 | // check if connected before sending message 73 | if (!client.Connected) { Log.Info($"SendLoop {conn} not connected"); return; } 74 | 75 | int maxLength = msg.count + Constants.HeaderSize + Constants.MaskSize; 76 | 77 | // if next writer could overflow, write to stream and clear buffer 78 | if (offset + maxLength > bufferSize) 79 | { 80 | stream.Write(writeBuffer, 0, offset); 81 | offset = 0; 82 | } 83 | 84 | offset = SendMessage(writeBuffer, offset, msg, setMask, maskHelper); 85 | msg.Release(); 86 | } 87 | 88 | // after no message in queue, send remaining messages 89 | // dont need to check offset > 0 because last message in queue will always be sent here 90 | 91 | stream.Write(writeBuffer, 0, offset); 92 | } 93 | else 94 | { 95 | while (conn.sendQueue.TryDequeue(out ArrayBuffer msg)) 96 | { 97 | // check if connected before sending message 98 | if (!client.Connected) { Log.Info($"SendLoop {conn} not connected"); return; } 99 | 100 | int length = SendMessage(writeBuffer, 0, msg, setMask, maskHelper); 101 | stream.Write(writeBuffer, 0, length); 102 | msg.Release(); 103 | } 104 | } 105 | } 106 | 107 | Log.Info($"{conn} Not Connected"); 108 | } 109 | catch (ThreadInterruptedException e) { Log.InfoException(e); } 110 | catch (ThreadAbortException e) { Log.InfoException(e); } 111 | catch (Exception e) 112 | { 113 | Log.Exception(e); 114 | } 115 | finally 116 | { 117 | Profiler.EndThreadProfiling(); 118 | conn.Dispose(); 119 | maskHelper?.Dispose(); 120 | } 121 | } 122 | 123 | /// new offset in buffer 124 | static int SendMessage(byte[] buffer, int startOffset, ArrayBuffer msg, bool setMask, MaskHelper maskHelper) 125 | { 126 | int msgLength = msg.count; 127 | int offset = WriteHeader(buffer, startOffset, msgLength, setMask); 128 | 129 | if (setMask) 130 | { 131 | offset = maskHelper.WriteMask(buffer, offset); 132 | } 133 | 134 | msg.CopyTo(buffer, offset); 135 | offset += msgLength; 136 | 137 | // dump before mask on 138 | Log.DumpBuffer("Send", buffer, startOffset, offset); 139 | 140 | if (setMask) 141 | { 142 | int messageOffset = offset - msgLength; 143 | MessageProcessor.ToggleMask(buffer, messageOffset, msgLength, buffer, messageOffset - Constants.MaskSize); 144 | } 145 | 146 | return offset; 147 | } 148 | 149 | static int WriteHeader(byte[] buffer, int startOffset, int msgLength, bool setMask) 150 | { 151 | int sendLength = 0; 152 | const byte finished = 128; 153 | const byte byteOpCode = 2; 154 | 155 | buffer[startOffset + 0] = finished | byteOpCode; 156 | sendLength++; 157 | 158 | if (msgLength <= Constants.BytePayloadLength) 159 | { 160 | buffer[startOffset + 1] = (byte)msgLength; 161 | sendLength++; 162 | } 163 | else if (msgLength <= ushort.MaxValue) 164 | { 165 | buffer[startOffset + 1] = 126; 166 | buffer[startOffset + 2] = (byte)(msgLength >> 8); 167 | buffer[startOffset + 3] = (byte)msgLength; 168 | sendLength += 3; 169 | } 170 | else 171 | { 172 | throw new InvalidDataException($"Trying to send a message larger than {ushort.MaxValue} bytes"); 173 | } 174 | 175 | if (setMask) 176 | { 177 | buffer[startOffset + 1] |= 0b1000_0000; 178 | } 179 | 180 | return sendLength + startOffset; 181 | } 182 | 183 | sealed class MaskHelper : IDisposable 184 | { 185 | readonly byte[] maskBuffer; 186 | readonly RNGCryptoServiceProvider random; 187 | 188 | public MaskHelper() 189 | { 190 | maskBuffer = new byte[4]; 191 | random = new RNGCryptoServiceProvider(); 192 | } 193 | public void Dispose() 194 | { 195 | random.Dispose(); 196 | } 197 | 198 | public int WriteMask(byte[] buffer, int offset) 199 | { 200 | random.GetBytes(maskBuffer); 201 | Buffer.BlockCopy(maskBuffer, 0, buffer, offset, 4); 202 | 203 | return offset + 4; 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /source/Common/SendLoop.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f87dd81736d9c824db67f808ac71841d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/TcpConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Sockets; 2 | 3 | namespace Mirror.SimpleWeb 4 | { 5 | public struct TcpConfig 6 | { 7 | public readonly bool noDelay; 8 | public readonly int sendTimeout; 9 | public readonly int receiveTimeout; 10 | 11 | public TcpConfig(bool noDelay, int sendTimeout, int receiveTimeout) 12 | { 13 | this.noDelay = noDelay; 14 | this.sendTimeout = sendTimeout; 15 | this.receiveTimeout = receiveTimeout; 16 | } 17 | 18 | public void ApplyTo(TcpClient client) 19 | { 20 | client.SendTimeout = sendTimeout; 21 | client.ReceiveTimeout = receiveTimeout; 22 | client.NoDelay = noDelay; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /source/Common/TcpConfig.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 81ac8d35f28fab14b9edda5cd9d4fc86 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Common/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace Mirror.SimpleWeb 4 | { 5 | internal static class Utils 6 | { 7 | public static void CheckForInterupt() 8 | { 9 | // sleep in order to check for ThreadInterruptedException 10 | Thread.Sleep(1); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/Common/Utils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4643ffb4cb0562847b1ae925d07e15b6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/README.txt: -------------------------------------------------------------------------------- 1 | SimpleWebTransport is a Transport that implements websocket for Webgl builds of 2 | mirror. This transport can also work on standalone builds and has support for 3 | encryption with websocket secure. 4 | 5 | How to use: 6 | Replace your existing Transport with SimpleWebTransport on your NetworkManager 7 | 8 | Requirements: 9 | Unity 2018.4 LTS 10 | Mirror v18.0.0 11 | 12 | Documentation: 13 | https://mirror-networking.com/docs/ 14 | https://github.com/MirrorNetworking/SimpleWebTransport/blob/master/README.md 15 | 16 | Support: 17 | Discord: https://discordapp.com/invite/N9QVxbM 18 | Bug Reports: https://github.com/MirrorNetworking/SimpleWebTransport/issues 19 | 20 | 21 | **To get most recent updates and fixes download from github** 22 | https://github.com/MirrorNetworking/SimpleWebTransport/releases 23 | -------------------------------------------------------------------------------- /source/README.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0e3971d5783109f4d9ce93c7a689d701 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /source/Server.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0e599e92544d43344a9a9060052add28 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /source/Server/ServerHandshake.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace Mirror.SimpleWeb 7 | { 8 | /// 9 | /// Handles Handshakes from new clients on the server 10 | /// The server handshake has buffers to reduce allocations when clients connect 11 | /// 12 | internal class ServerHandshake 13 | { 14 | const int GetSize = 3; 15 | const int ResponseLength = 129; 16 | const int KeyLength = 24; 17 | const int MergedKeyLength = 60; 18 | const string KeyHeaderString = "Sec-WebSocket-Key: "; 19 | // this isnt an offical max, just a reasonable size for a websocket handshake 20 | readonly int maxHttpHeaderSize = 3000; 21 | 22 | readonly SHA1 sha1 = SHA1.Create(); 23 | readonly BufferPool bufferPool; 24 | 25 | public ServerHandshake(BufferPool bufferPool, int handshakeMaxSize) 26 | { 27 | this.bufferPool = bufferPool; 28 | this.maxHttpHeaderSize = handshakeMaxSize; 29 | } 30 | 31 | ~ServerHandshake() 32 | { 33 | sha1.Dispose(); 34 | } 35 | 36 | public bool TryHandshake(Connection conn) 37 | { 38 | Stream stream = conn.stream; 39 | 40 | using (ArrayBuffer getHeader = bufferPool.Take(GetSize)) 41 | { 42 | if (!ReadHelper.TryRead(stream, getHeader.array, 0, GetSize)) 43 | return false; 44 | getHeader.count = GetSize; 45 | 46 | 47 | if (!IsGet(getHeader.array)) 48 | { 49 | Log.Warn($"First bytes from client was not 'GET' for handshake, instead was {Log.BufferToString(getHeader.array, 0, GetSize)}"); 50 | return false; 51 | } 52 | } 53 | 54 | 55 | string msg = ReadToEndForHandshake(stream); 56 | 57 | if (string.IsNullOrEmpty(msg)) 58 | return false; 59 | 60 | try 61 | { 62 | AcceptHandshake(stream, msg); 63 | return true; 64 | } 65 | catch (ArgumentException e) 66 | { 67 | Log.InfoException(e); 68 | return false; 69 | } 70 | } 71 | 72 | string ReadToEndForHandshake(Stream stream) 73 | { 74 | using (ArrayBuffer readBuffer = bufferPool.Take(maxHttpHeaderSize)) 75 | { 76 | int? readCountOrFail = ReadHelper.SafeReadTillMatch(stream, readBuffer.array, 0, maxHttpHeaderSize, Constants.endOfHandshake); 77 | if (!readCountOrFail.HasValue) 78 | return null; 79 | 80 | int readCount = readCountOrFail.Value; 81 | 82 | string msg = Encoding.ASCII.GetString(readBuffer.array, 0, readCount); 83 | Log.Verbose(msg); 84 | 85 | return msg; 86 | } 87 | } 88 | 89 | static bool IsGet(byte[] getHeader) 90 | { 91 | // just check bytes here instead of using Encoding.ASCII 92 | return getHeader[0] == 71 && // G 93 | getHeader[1] == 69 && // E 94 | getHeader[2] == 84; // T 95 | } 96 | 97 | void AcceptHandshake(Stream stream, string msg) 98 | { 99 | using ( 100 | ArrayBuffer keyBuffer = bufferPool.Take(KeyLength), 101 | responseBuffer = bufferPool.Take(ResponseLength)) 102 | { 103 | GetKey(msg, keyBuffer.array); 104 | AppendGuid(keyBuffer.array); 105 | byte[] keyHash = CreateHash(keyBuffer.array); 106 | CreateResponse(keyHash, responseBuffer.array); 107 | 108 | stream.Write(responseBuffer.array, 0, ResponseLength); 109 | } 110 | } 111 | 112 | 113 | static void GetKey(string msg, byte[] keyBuffer) 114 | { 115 | int start = msg.IndexOf(KeyHeaderString) + KeyHeaderString.Length; 116 | 117 | Log.Verbose($"Handshake Key: {msg.Substring(start, KeyLength)}"); 118 | Encoding.ASCII.GetBytes(msg, start, KeyLength, keyBuffer, 0); 119 | } 120 | 121 | static void AppendGuid(byte[] keyBuffer) 122 | { 123 | Buffer.BlockCopy(Constants.HandshakeGUIDBytes, 0, keyBuffer, KeyLength, Constants.HandshakeGUID.Length); 124 | } 125 | 126 | byte[] CreateHash(byte[] keyBuffer) 127 | { 128 | Log.Verbose($"Handshake Hashing {Encoding.ASCII.GetString(keyBuffer, 0, MergedKeyLength)}"); 129 | 130 | return sha1.ComputeHash(keyBuffer, 0, MergedKeyLength); 131 | } 132 | 133 | static void CreateResponse(byte[] keyHash, byte[] responseBuffer) 134 | { 135 | string keyHashString = Convert.ToBase64String(keyHash); 136 | 137 | // compiler should merge these strings into 1 string before format 138 | string message = string.Format( 139 | "HTTP/1.1 101 Switching Protocols\r\n" + 140 | "Connection: Upgrade\r\n" + 141 | "Upgrade: websocket\r\n" + 142 | "Sec-WebSocket-Accept: {0}\r\n\r\n", 143 | keyHashString); 144 | 145 | Log.Verbose($"Handshake Response length {message.Length}, IsExpected {message.Length == ResponseLength}"); 146 | Encoding.ASCII.GetBytes(message, 0, ResponseLength, responseBuffer, 0); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /source/Server/ServerHandshake.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6268509ac4fb48141b9944c03295da11 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Server/ServerSslHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Security; 4 | using System.Net.Sockets; 5 | using System.Security.Authentication; 6 | using System.Security.Cryptography.X509Certificates; 7 | 8 | namespace Mirror.SimpleWeb 9 | { 10 | public struct SslConfig 11 | { 12 | public readonly bool enabled; 13 | public readonly string certPath; 14 | public readonly string certPassword; 15 | public readonly SslProtocols sslProtocols; 16 | 17 | public SslConfig(bool enabled, string certPath, string certPassword, SslProtocols sslProtocols) 18 | { 19 | this.enabled = enabled; 20 | this.certPath = certPath; 21 | this.certPassword = certPassword; 22 | this.sslProtocols = sslProtocols; 23 | } 24 | } 25 | internal class ServerSslHelper 26 | { 27 | readonly SslConfig config; 28 | readonly X509Certificate2 certificate; 29 | 30 | public ServerSslHelper(SslConfig sslConfig) 31 | { 32 | config = sslConfig; 33 | if (config.enabled) 34 | certificate = new X509Certificate2(config.certPath, config.certPassword); 35 | } 36 | 37 | internal bool TryCreateStream(Connection conn) 38 | { 39 | NetworkStream stream = conn.client.GetStream(); 40 | if (config.enabled) 41 | { 42 | try 43 | { 44 | conn.stream = CreateStream(stream); 45 | return true; 46 | } 47 | catch (Exception e) 48 | { 49 | Log.Error($"Create SSLStream Failed: {e}", false); 50 | return false; 51 | } 52 | } 53 | else 54 | { 55 | conn.stream = stream; 56 | return true; 57 | } 58 | } 59 | 60 | Stream CreateStream(NetworkStream stream) 61 | { 62 | SslStream sslStream = new SslStream(stream, true, acceptClient); 63 | sslStream.AuthenticateAsServer(certificate, false, config.sslProtocols, false); 64 | 65 | return sslStream; 66 | } 67 | 68 | bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) 69 | { 70 | // always accept client 71 | return true; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /source/Server/ServerSslHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 11061fee528ebdd43817a275b1e4a317 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Server/SimpleWebServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace Mirror.SimpleWeb 6 | { 7 | public class SimpleWebServer 8 | { 9 | readonly int maxMessagesPerTick; 10 | 11 | readonly WebSocketServer server; 12 | readonly BufferPool bufferPool; 13 | 14 | public SimpleWebServer(int maxMessagesPerTick, TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig) 15 | { 16 | this.maxMessagesPerTick = maxMessagesPerTick; 17 | // use max because bufferpool is used for both messages and handshake 18 | int max = Math.Max(maxMessageSize, handshakeMaxSize); 19 | bufferPool = new BufferPool(5, 20, max); 20 | 21 | server = new WebSocketServer(tcpConfig, maxMessageSize, handshakeMaxSize, sslConfig, bufferPool); 22 | } 23 | 24 | public bool Active { get; private set; } 25 | 26 | public event Action onConnect; 27 | public event Action onDisconnect; 28 | public event Action> onData; 29 | public event Action onError; 30 | 31 | public void Start(ushort port) 32 | { 33 | server.Listen(port); 34 | Active = true; 35 | } 36 | 37 | public void Stop() 38 | { 39 | server.Stop(); 40 | Active = false; 41 | } 42 | 43 | public void SendAll(List connectionIds, ArraySegment source) 44 | { 45 | ArrayBuffer buffer = bufferPool.Take(source.Count); 46 | buffer.CopyFrom(source); 47 | buffer.SetReleasesRequired(connectionIds.Count); 48 | 49 | // make copy of array before for each, data sent to each client is the same 50 | foreach (int id in connectionIds) 51 | { 52 | server.Send(id, buffer); 53 | } 54 | } 55 | public void SendOne(int connectionId, ArraySegment source) 56 | { 57 | ArrayBuffer buffer = bufferPool.Take(source.Count); 58 | buffer.CopyFrom(source); 59 | 60 | server.Send(connectionId, buffer); 61 | } 62 | 63 | public bool KickClient(int connectionId) 64 | { 65 | return server.CloseConnection(connectionId); 66 | } 67 | 68 | public string GetClientAddress(int connectionId) 69 | { 70 | return server.GetClientAddress(connectionId); 71 | } 72 | 73 | public void ProcessMessageQueue(MonoBehaviour behaviour) 74 | { 75 | int processedCount = 0; 76 | // check enabled every time incase behaviour was disabled after data 77 | while ( 78 | behaviour.enabled && 79 | processedCount < maxMessagesPerTick && 80 | // Dequeue last 81 | server.receiveQueue.TryDequeue(out Message next) 82 | ) 83 | { 84 | processedCount++; 85 | 86 | switch (next.type) 87 | { 88 | case EventType.Connected: 89 | onConnect?.Invoke(next.connId); 90 | break; 91 | case EventType.Data: 92 | onData?.Invoke(next.connId, next.data.ToSegment()); 93 | next.data.Release(); 94 | break; 95 | case EventType.Disconnected: 96 | onDisconnect?.Invoke(next.connId); 97 | break; 98 | case EventType.Error: 99 | onError?.Invoke(next.connId, next.exception); 100 | break; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /source/Server/SimpleWebServer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bd51d7896f55a5e48b41a4b526562b0e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/Server/WebSocketServer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5c434db044777d2439bae5a57d4e8ee7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/SimpleWebTransport.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleWebTransport", 3 | "references": [ 4 | "Mirror" 5 | ], 6 | "optionalUnityReferences": [], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [] 14 | } -------------------------------------------------------------------------------- /source/SimpleWebTransport.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3b5390adca4e2bb4791cb930316d6f3e 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /source/SimpleWebTransport.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0110f245bfcfc7d459681f7bd9ebc590 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /source/SslConfigLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using UnityEngine; 3 | 4 | namespace Mirror.SimpleWeb 5 | { 6 | internal class SslConfigLoader 7 | { 8 | internal struct Cert 9 | { 10 | public string path; 11 | public string password; 12 | } 13 | internal static SslConfig Load(SimpleWebTransport transport) 14 | { 15 | // dont need to load anything if ssl is not enabled 16 | if (!transport.sslEnabled) 17 | return default; 18 | 19 | string certJsonPath = transport.sslCertJson; 20 | 21 | Cert cert = LoadCertJson(certJsonPath); 22 | 23 | return new SslConfig( 24 | enabled: transport.sslEnabled, 25 | sslProtocols: transport.sslProtocols, 26 | certPath: cert.path, 27 | certPassword: cert.password 28 | ); 29 | } 30 | 31 | internal static Cert LoadCertJson(string certJsonPath) 32 | { 33 | string json = File.ReadAllText(certJsonPath); 34 | Cert cert = JsonUtility.FromJson(json); 35 | 36 | if (string.IsNullOrEmpty(cert.path)) 37 | { 38 | throw new InvalidDataException("Cert Json didnt not contain \"path\""); 39 | } 40 | if (string.IsNullOrEmpty(cert.password)) 41 | { 42 | // password can be empty 43 | cert.password = string.Empty; 44 | } 45 | 46 | return cert; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /source/SslConfigLoader.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dfdb6b97a48a48b498e563e857342da1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Common.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b0f36e701a9ed89479dc07a5412700e8 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /tests/Common/RunNodeTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e49080df259f6e5459b35ae46576702d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Common/SimpleWebTransport.Tests.Common.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleWebTransport.Tests.Common", 3 | "references": [ 4 | "Mirror" 5 | ], 6 | "optionalUnityReferences": [ 7 | "TestAssemblies" 8 | ], 9 | "includePlatforms": [], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [] 16 | } -------------------------------------------------------------------------------- /tests/Common/SimpleWebTransport.Tests.Common.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 60cf1f77e340def4b8ad3349fc18a3c4 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /tests/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 25b70f3f2edcd4142ab2a64175f0d504 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /tests/Editor/.Bad1.Json: -------------------------------------------------------------------------------- 1 | { 2 | "_readme_1": "Bad json that doesn't have path or password" 3 | } -------------------------------------------------------------------------------- /tests/Editor/.Bad2.Json: -------------------------------------------------------------------------------- 1 | { 2 | "_readme_1": "Bad json that doesn't have path", 3 | "password": "some password" 4 | } -------------------------------------------------------------------------------- /tests/Editor/.Good1.Json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "Some path", 3 | "password": "Some password" 4 | } -------------------------------------------------------------------------------- /tests/Editor/.Good2.Json: -------------------------------------------------------------------------------- 1 | { 2 | "_readme_1": "Json doesn't have to have password", 3 | "path": "Some path" 4 | } -------------------------------------------------------------------------------- /tests/Editor/BufferPoolTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | 6 | namespace Mirror.SimpleWeb.Tests 7 | { 8 | [Category("SimpleWebTransport")] 9 | public class BufferPoolTests 10 | { 11 | [Test] 12 | [TestCase(0)] 13 | [TestCase(-10)] 14 | public void ErrorWhenSmallestBelow1(int smallest) 15 | { 16 | ArgumentException exception = Assert.Throws(() => 17 | { 18 | BufferPool bufferPool = new BufferPool(5, smallest, 16384); 19 | }); 20 | 21 | Assert.That(exception.Message, Is.EqualTo("Smallest must be atleast 1")); 22 | } 23 | 24 | [Test] 25 | [TestCase(10, 1)] 26 | [TestCase(100, 99)] 27 | [TestCase(1, -1)] 28 | [TestCase(1, 0)] 29 | public void ErrorWhenLargestBelowSmallest(int smallest, int largest) 30 | { 31 | ArgumentException exception = Assert.Throws(() => 32 | { 33 | BufferPool bufferPool = new BufferPool(5, smallest, largest); 34 | }); 35 | 36 | Assert.That(exception.Message, Is.EqualTo("Largest must be greater than smallest")); 37 | } 38 | 39 | [Test] 40 | [TestCase(0)] 41 | [TestCase(1)] 42 | [TestCase(-10)] 43 | public void ErrorWhenCountIsBelow2(int count) 44 | { 45 | ArgumentException exception = Assert.Throws(() => 46 | { 47 | BufferPool bufferPool = new BufferPool(count, 2, 16384); 48 | }); 49 | 50 | Assert.That(exception.Message, Is.EqualTo("Count must be atleast 2")); 51 | } 52 | 53 | static IEnumerable CreatesCorrectCountSource => Enumerable.Range(2, 100); 54 | [Test] 55 | [TestCaseSource(nameof(CreatesCorrectCountSource))] 56 | public void CreatesCorrectCount(int count) 57 | { 58 | BufferPool bufferPool = new BufferPool(count, 2, 16384); 59 | 60 | Assert.That(bufferPool.buckets.Length, Is.EqualTo(count)); 61 | } 62 | 63 | [Test] 64 | [TestCase(2, 2, 100)] 65 | [TestCase(5, 2, 16384)] 66 | [TestCase(8, 2, 16384)] 67 | [TestCase(15, 2, 16384)] 68 | [TestCase(15, 100, 200)] 69 | public void SmallestAndLargestGroupsHavecorrectvalues(int count, int smallest, int largest) 70 | { 71 | BufferPool bufferPool = new BufferPool(count, smallest, largest); 72 | 73 | BufferBucket smallestGroup = bufferPool.buckets[0]; 74 | BufferBucket largestGroup = bufferPool.buckets[count - 1]; 75 | Assert.That(smallestGroup.arraySize, Is.EqualTo(smallest)); 76 | // largest can be 1 greater because caclatations should round up 77 | Assert.That(largestGroup.arraySize, Is.EqualTo(largest).Or.EqualTo(largest + 1), "largest should be equal to larget or large + 1"); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Editor/BufferPoolTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e9f64d29724a7b04e841abde7dd124da 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Editor/CheckNodeTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using NUnit.Framework; 4 | 5 | namespace Mirror.SimpleWeb.Tests 6 | { 7 | [Category("SimpleWebTransport")] 8 | public class CheckNodeTest 9 | { 10 | /// 11 | /// This test will fail if SimpleWebTransport isnt in root of project 12 | /// 13 | [Test] 14 | public void FindFullPath() 15 | { 16 | string actual = RunNode.ResolvePath("HelloWorld.js"); 17 | string expected = "./Assets/SimpleWebTransport/tests/node~/HelloWorld.js"; 18 | 19 | Assert.That(Path.GetFullPath(actual), Is.EqualTo(Path.GetFullPath(expected))); 20 | } 21 | 22 | [Test] 23 | public void ShouldReturnHelloWorld() 24 | { 25 | RunNode.Result result = RunNode.Run("HelloWorld.js", false); 26 | 27 | result.AssetTimeout(false); 28 | 29 | result.AssetOutput( 30 | "Hello World!" 31 | ); 32 | result.AssetErrors(); 33 | } 34 | 35 | [Test] 36 | public void ShouldReturnHelloWorld2() 37 | { 38 | RunNode.Result result = RunNode.Run("HelloWorld2.js", false); 39 | 40 | result.AssetTimeout(false); 41 | result.AssetOutput( 42 | "Hello World!", 43 | "Hello again World!" 44 | ); 45 | result.AssetErrors(); 46 | } 47 | 48 | [Test] 49 | public void ShouldReturnHelloError() 50 | { 51 | RunNode.Result result = RunNode.Run("HelloError.js", false); 52 | 53 | result.AssetTimeout(false); 54 | result.AssetOutput(); 55 | result.AssetErrors( 56 | "Hello Error!" 57 | ); 58 | } 59 | 60 | [Test] 61 | public void ShouldReturnHelloError2() 62 | { 63 | RunNode.Result result = RunNode.Run("HelloError2.js", false); 64 | 65 | result.AssetTimeout(false); 66 | result.AssetOutput(); 67 | result.AssetErrors( 68 | "Hello Error!", 69 | "Hello again Error!" 70 | ); 71 | } 72 | 73 | [Test] 74 | public void ShouldFinishBeforeTimeout() 75 | { 76 | Stopwatch stopwatch = new Stopwatch(); 77 | stopwatch.Start(); 78 | RunNode.Result result = RunNode.Run("HelloWorld.js", false); 79 | 80 | stopwatch.Stop(); 81 | double seconds = stopwatch.Elapsed.TotalSeconds; 82 | // hello script is fast and should finish faster than 1 second 83 | Assert.That(seconds, Is.LessThan(2.0)); 84 | } 85 | 86 | [Test] 87 | public void ShouldStopAfterTimeout() 88 | { 89 | RunNode.Result result = RunNode.Run("Timeout.js", false); 90 | 91 | result.AssetTimeout(true); 92 | 93 | result.AssetTimeout(true); 94 | result.AssetOutput( 95 | "Should be running" 96 | ); 97 | result.AssetErrors(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/Editor/CheckNodeTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5a1a692eedb228946bd041f4a3b24dc2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Editor/LogTest.cs: -------------------------------------------------------------------------------- 1 | // define this to make sure log level works 2 | #define SIMPLEWEB_LOG_ENABLED 3 | using System; 4 | using System.Collections; 5 | using System.IO; 6 | using System.Linq; 7 | using NUnit.Framework; 8 | using UnityEngine.TestTools; 9 | 10 | namespace Mirror.SimpleWeb.Tests 11 | { 12 | [Category("SimpleWebTransport")] 13 | public class LogTest 14 | { 15 | static IEnumerable BufferToStringTestCases 16 | { 17 | get 18 | { 19 | yield return new TestCaseData(new byte[5] { 10, 11, 12, 13, 14 }, null, null).Returns("0A-0B-0C-0D-0E"); 20 | yield return new TestCaseData(new byte[5] { 10, 11, 12, 13, 14 }, 1, 3).Returns("0B-0C-0D"); 21 | yield return new TestCaseData(new byte[5] { 255, 0, 128, 13, 14 }, null, 3).Returns("FF-00-80"); 22 | yield return new TestCaseData(new byte[5] { 255, 0, 128, 13, 14 }, null, 5).Returns("FF-00-80-0D-0E"); 23 | 24 | byte[] data = Enumerable.Range(0, 255).Select(x => (byte)x).ToArray(); 25 | // charaters are split by '-' 26 | // charaters are in hex 27 | // charaters are padded (eg 01 instead of 1) 28 | string expected = string.Join("-", data.Select(x => x.ToString("X").PadLeft(2, '0'))); 29 | yield return new TestCaseData(data, null, null).Returns(expected); 30 | } 31 | } 32 | [Test] 33 | [TestCaseSource(nameof(BufferToStringTestCases))] 34 | public string BufferToString(byte[] buffer, int? offset, int? length) 35 | { 36 | return Log.BufferToString(buffer, offset ?? 0, length); 37 | } 38 | 39 | 40 | const string SomeMessage = "Some Message"; 41 | 42 | static IEnumerable TestCases 43 | { 44 | get 45 | { 46 | for (int i = 0; i < 2; i++) 47 | { 48 | foreach (Log.Levels level in (Log.Levels[])Enum.GetValues(typeof(Log.Levels))) 49 | { 50 | yield return new object[] { i == 1, level }; 51 | } 52 | } 53 | } 54 | } 55 | 56 | [Test] 57 | [TestCaseSource(nameof(TestCases))] 58 | public void DumpMessageTest(bool asArrayBuffer, Log.Levels levels) 59 | { 60 | Log.level = levels; 61 | const string Label = "fun label"; 62 | 63 | byte[] data = new byte[5] { 10, 11, 12, 13, 14 }; 64 | string expected = "0B-0C-0D"; 65 | 66 | if (levels >= Log.Levels.verbose) 67 | { 68 | LogAssert.Expect(UnityEngine.LogType.Log, $"VERBOSE: {Label}: {expected}"); 69 | } 70 | 71 | if (asArrayBuffer) 72 | { 73 | ArrayBuffer buffer = new ArrayBuffer(null, 10); 74 | buffer.CopyFrom(data, 1, 3); 75 | Log.DumpBuffer(Label, buffer); 76 | } 77 | else 78 | { 79 | Log.DumpBuffer(Label, data, 1, 3); 80 | } 81 | 82 | LogAssert.NoUnexpectedReceived(); 83 | } 84 | 85 | [Test] 86 | [TestCaseSource(nameof(TestCases))] 87 | public void ExceptionTest(bool showColor, Log.Levels levels) 88 | { 89 | Log.level = levels; 90 | 91 | Exception myException = new IOException(SomeMessage); 92 | // Exception isnt effected by log level 93 | LogAssert.Expect(UnityEngine.LogType.Error, $"EXCEPTION: {nameof(IOException)} Message: {SomeMessage}"); 94 | Log.Exception(myException); 95 | 96 | LogAssert.NoUnexpectedReceived(); 97 | } 98 | 99 | [Test] 100 | [TestCaseSource(nameof(TestCases))] 101 | public void ErrorTest(bool showColor, Log.Levels levels) 102 | { 103 | Log.level = levels; 104 | 105 | if (levels >= Log.Levels.error) 106 | { 107 | if (showColor) 108 | LogAssert.Expect(UnityEngine.LogType.Error, $"ERROR: {SomeMessage}"); 109 | else 110 | LogAssert.Expect(UnityEngine.LogType.Error, $"ERROR: {SomeMessage}"); 111 | } 112 | Log.Error(SomeMessage, showColor); 113 | 114 | LogAssert.NoUnexpectedReceived(); 115 | } 116 | 117 | [Test] 118 | [TestCaseSource(nameof(TestCases))] 119 | public void InfoExceptionTest(bool showColor, Log.Levels levels) 120 | { 121 | Log.level = levels; 122 | 123 | Exception myException = new IOException(SomeMessage); 124 | 125 | if (levels >= Log.Levels.info) 126 | { 127 | LogAssert.Expect(UnityEngine.LogType.Log, $"INFO_EXCEPTION: {nameof(IOException)} Message: {SomeMessage}"); 128 | } 129 | Log.InfoException(myException); 130 | 131 | LogAssert.NoUnexpectedReceived(); 132 | } 133 | 134 | [Test] 135 | [TestCaseSource(nameof(TestCases))] 136 | public void InfoTest(bool showColor, Log.Levels levels) 137 | { 138 | Log.level = levels; 139 | 140 | if (levels >= Log.Levels.info) 141 | { 142 | if (showColor) 143 | LogAssert.Expect(UnityEngine.LogType.Log, $"INFO: {SomeMessage}"); 144 | else 145 | LogAssert.Expect(UnityEngine.LogType.Log, $"INFO: {SomeMessage}"); 146 | } 147 | Log.Info(SomeMessage, showColor); 148 | 149 | LogAssert.NoUnexpectedReceived(); 150 | } 151 | 152 | [Test] 153 | [TestCaseSource(nameof(TestCases))] 154 | public void VerboseTest(bool showColor, Log.Levels levels) 155 | { 156 | Log.level = levels; 157 | 158 | if (levels >= Log.Levels.verbose) 159 | { 160 | if (showColor) 161 | LogAssert.Expect(UnityEngine.LogType.Log, $"VERBOSE: {SomeMessage}"); 162 | else 163 | LogAssert.Expect(UnityEngine.LogType.Log, $"VERBOSE: {SomeMessage}"); 164 | } 165 | Log.Verbose(SomeMessage, showColor); 166 | 167 | LogAssert.NoUnexpectedReceived(); 168 | } 169 | 170 | [Test] 171 | [TestCaseSource(nameof(TestCases))] 172 | public void WarnTest(bool showColor, Log.Levels levels) 173 | { 174 | Log.level = levels; 175 | 176 | if (levels >= Log.Levels.warn) 177 | { 178 | if (showColor) 179 | LogAssert.Expect(UnityEngine.LogType.Warning, $"WARN: {SomeMessage}"); 180 | else 181 | LogAssert.Expect(UnityEngine.LogType.Warning, $"WARN: {SomeMessage}"); 182 | } 183 | Log.Warn(SomeMessage, showColor); 184 | 185 | LogAssert.NoUnexpectedReceived(); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /tests/Editor/LogTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f58422d1a2e5d2e4c9625e2a80d48e03 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Editor/MessageProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using NUnit.Framework; 3 | 4 | namespace Mirror.SimpleWeb.Tests 5 | { 6 | [Category("SimpleWebTransport")] 7 | public class MessageProcessorTests 8 | { 9 | // TODO Needs updating for new Message Processor methods 10 | private byte[] CreateMessage(bool finished = true, bool hasMask = true, int opcode = 2, int length = 10) 11 | { 12 | byte[] buffer = new byte[20]; 13 | 14 | buffer[0] |= (byte)(finished ? 0b1000_0000 : 0); 15 | buffer[1] |= (byte)(hasMask ? 0b1000_0000 : 0); 16 | 17 | buffer[0] |= (byte)(opcode & 0b0000_1111); 18 | 19 | if (length < 126) 20 | { 21 | buffer[1] |= (byte)(length & 0b0111_1111); 22 | } 23 | else if (126 <= length && length <= ushort.MaxValue) 24 | { 25 | buffer[1] |= 126 & 0b0111_1111; 26 | buffer[2] = (byte)(length >> 8); 27 | buffer[3] = (byte)length; 28 | } 29 | else if (ushort.MaxValue < length && (ulong)length <= ulong.MaxValue) 30 | { 31 | buffer[1] |= 127 & 0b0111_1111; 32 | } 33 | 34 | return buffer; 35 | } 36 | 37 | [Test] 38 | [TestCase(2, 10)] 39 | [TestCase(2, 100)] 40 | [TestCase(2, 125)] 41 | [TestCase(2, 126)] 42 | [TestCase(2, 127)] 43 | [TestCase(2, 128)] 44 | [TestCase(2, 1000)] 45 | [TestCase(8, 10)] 46 | [TestCase(8, 100)] 47 | [TestCase(8, 125)] 48 | [TestCase(8, 126)] 49 | [TestCase(8, 127)] 50 | [TestCase(8, 128)] 51 | [TestCase(8, 1000)] 52 | [Ignore("Broken")] 53 | public void HasCorrectValues(int opCode, int length) 54 | { 55 | //byte[] buffer = CreateMessage(opcode: opCode, length: length); 56 | 57 | //MessageProcessor.Result result = MessageProcessor.ValidateHeader(buffer, length, true); 58 | 59 | //Assert.That(result.opcode, Is.EqualTo(opCode)); 60 | //Assert.That(result.offset, Is.EqualTo(length < 126 ? 2 : 4)); 61 | //Assert.That(result.msgLength, Is.EqualTo(length)); 62 | } 63 | 64 | [Test] 65 | public void ThrowsWhenNotFinished() 66 | { 67 | byte[] buffer = CreateMessage(finished: false); 68 | 69 | InvalidDataException expection = Assert.Throws(() => 70 | { 71 | MessageProcessor.ValidateHeader(buffer, 10 * 1024, true); 72 | }); 73 | 74 | Assert.That(expection.Message, Is.EqualTo("Full message should have been sent, if the full message wasn't sent it wasn't sent from this trasnport")); 75 | } 76 | 77 | [Test] 78 | [TestCase(true)] 79 | [TestCase(false)] 80 | public void ThrowsWhenMaskIsWrong(bool mask) 81 | { 82 | byte[] buffer = CreateMessage(hasMask: !mask); 83 | 84 | InvalidDataException expection = Assert.Throws(() => 85 | { 86 | MessageProcessor.ValidateHeader(buffer, 10 * 1024, expectMask: mask); 87 | }); 88 | 89 | Assert.That(expection.Message, Is.EqualTo($"Message expected mask to be {mask} but was {!mask}")); 90 | } 91 | 92 | [Test] 93 | [TestCase(0)] 94 | [TestCase(1)] 95 | [TestCase(3)] 96 | [TestCase(4)] 97 | [TestCase(5)] 98 | [TestCase(6)] 99 | [TestCase(7)] 100 | [TestCase(9)] 101 | [TestCase(10)] 102 | [TestCase(11)] 103 | [TestCase(12)] 104 | [TestCase(13)] 105 | [TestCase(14)] 106 | [TestCase(15)] 107 | public void ThrowsForBadOpCode(int opcode) 108 | { 109 | byte[] buffer = CreateMessage(opcode: opcode); 110 | 111 | InvalidDataException expection = Assert.Throws(() => 112 | { 113 | MessageProcessor.ValidateHeader(buffer, 10 * 1024, true); 114 | }); 115 | 116 | Assert.That(expection.Message, Is.EqualTo("Expected opcode to be binary or close")); 117 | } 118 | 119 | [Test] 120 | public void ThrowsWhenLengthIsZero() 121 | { 122 | byte[] buffer = CreateMessage(length: 0); 123 | 124 | InvalidDataException expection = Assert.Throws(() => 125 | { 126 | MessageProcessor.ValidateHeader(buffer, 10 * 1024, true); 127 | }); 128 | 129 | Assert.That(expection.Message, Is.EqualTo("Message length was zero")); 130 | } 131 | 132 | [Test] 133 | public void ThrowsWhenLengthWasGreaterThanBuffer() 134 | { 135 | byte[] buffer = CreateMessage(length: 10 * 1024 + 1); 136 | 137 | InvalidDataException expection = Assert.Throws(() => 138 | { 139 | MessageProcessor.ValidateHeader(buffer, 10 * 1024, true); 140 | }); 141 | 142 | Assert.That(expection.Message, Is.EqualTo("Message length is greater than max length")); 143 | } 144 | 145 | [Test] 146 | [Ignore("Broken")] 147 | public void MessageAtMaxLengthIsOk() 148 | { 149 | //byte[] buffer = CreateMessage(length: 10 * 1024); 150 | 151 | //MessageProcessor.Result result = MessageProcessor.ValidateHeader(buffer, 10 * 1024, true); 152 | 153 | //Assert.That(result.opcode, Is.EqualTo(2)); 154 | //Assert.That(result.offset, Is.EqualTo(4)); 155 | //Assert.That(result.msgLength, Is.EqualTo(10 * 1024)); 156 | } 157 | 158 | [Test] 159 | public void ThrowsWhenMessageIsCreaterThanUshortMax() 160 | { 161 | byte[] buffer = CreateMessage(length: ushort.MaxValue * 1000); 162 | 163 | InvalidDataException expection = Assert.Throws(() => 164 | { 165 | MessageProcessor.ValidateHeader(buffer, 10 * 1024, true); 166 | }); 167 | 168 | Assert.That(expection.Message, Is.EqualTo("Max length is longer than allowed in a single message")); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/Editor/MessageProcessorTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 00ecffe2234894a4fa05995fbbe863b1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Editor/SimpleWebTransport.Tests.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleWebTransport.Tests.Editor", 3 | "references": [ 4 | "Mirror", 5 | "SimpleWebTransport", 6 | "SimpleWebTransport.Tests.Common" 7 | ], 8 | "optionalUnityReferences": [ 9 | "TestAssemblies" 10 | ], 11 | "includePlatforms": [ 12 | "Editor" 13 | ], 14 | "excludePlatforms": [], 15 | "allowUnsafeCode": false, 16 | "overrideReferences": false, 17 | "precompiledReferences": [], 18 | "autoReferenced": true, 19 | "defineConstraints": [] 20 | } -------------------------------------------------------------------------------- /tests/Editor/SimpleWebTransport.Tests.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0da7abed0ccf5ab4f984be0ccd5db4e0 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /tests/Editor/SslConfigLoaderTest.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using NUnit.Framework; 3 | using UnityEditor; 4 | 5 | namespace Mirror.SimpleWeb.Tests 6 | { 7 | [Category("SimpleWebTransport")] 8 | public class SslConfigLoaderTest 9 | { 10 | [Test] 11 | public void ExampleIsValid() 12 | { 13 | SslConfigLoader.Cert result = SslConfigLoader.LoadCertJson(Path.Combine(TransportDir, ".cert.example.Json")); 14 | 15 | Assert.That(result.path, Is.EqualTo("./certs/MirrorLocal.pfx")); 16 | Assert.That(result.password, Is.EqualTo("")); 17 | } 18 | 19 | [Test] 20 | public void ThrowsIfCantFindJson() 21 | { 22 | FileNotFoundException exception = Assert.Throws(() => 23 | { 24 | SslConfigLoader.LoadCertJson(Path.Combine(TestDir, "NotARealFile.Json")); 25 | }); 26 | 27 | Assert.That(exception.Message, Does.StartWith("Could not find file ")); 28 | } 29 | [Test] 30 | [TestCase(".Bad1.Json")] 31 | [TestCase(".Bad2.Json")] 32 | public void ThrowsIfBadJson(string path) 33 | { 34 | InvalidDataException exception = Assert.Throws(() => 35 | { 36 | SslConfigLoader.LoadCertJson(Path.Combine(TestDir, path)); 37 | }); 38 | 39 | Assert.That(exception.Message, Does.StartWith("Cert Json didnt not contain \"path\"")); 40 | } 41 | 42 | [Test] 43 | [TestCase(".Good1.Json", "Some path", "Some password")] 44 | [TestCase(".Good2.Json", "Some path", "")] 45 | public void ValidConfig(string jsonPath, string expectedPath, string expectedPassowrd) 46 | { 47 | SslConfigLoader.Cert cert = SslConfigLoader.LoadCertJson(Path.Combine(TestDir, jsonPath)); 48 | Assert.That(cert.path, Is.EqualTo(expectedPath)); 49 | Assert.That(cert.password, Is.EqualTo(expectedPassowrd)); 50 | } 51 | 52 | 53 | 54 | static string _testDir; 55 | static string TestDir 56 | { 57 | get 58 | { 59 | findDir(ref _testDir, nameof(SslConfigLoaderTest)); 60 | return _testDir; 61 | } 62 | } 63 | 64 | static string _transportDir; 65 | static string TransportDir 66 | { 67 | get 68 | { 69 | findDir(ref _transportDir, nameof(SimpleWebTransport)); 70 | return _transportDir; 71 | } 72 | } 73 | 74 | private static void findDir(ref string field, string file) 75 | { 76 | if (string.IsNullOrEmpty(field)) 77 | { 78 | string[] guidsFound = AssetDatabase.FindAssets($"t:Script " + file); 79 | if (guidsFound.Length == 1 && !string.IsNullOrEmpty(guidsFound[0])) 80 | { 81 | string script = AssetDatabase.GUIDToAssetPath(guidsFound[0]); 82 | string dir = Path.GetDirectoryName(script); 83 | field = dir; 84 | } 85 | else 86 | { 87 | UnityEngine.Debug.LogError("Could not find path of dir"); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Editor/SslConfigLoaderTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 586585a0b4f68ce4ca9b543ca2bef769 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fa1d359fe2e474c45be80b584b638412 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /tests/Runtime/Client.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8b11d6894bd1fed4eb2f177175f42c62 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /tests/Runtime/Client/ClientTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af6b99cbca3147249b6b8b16073990a5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Client/ClientTestsMultiple.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using NUnit.Framework; 3 | using UnityEngine; 4 | using UnityEngine.TestTools; 5 | 6 | namespace Mirror.SimpleWeb.Tests.Client 7 | { 8 | [Category("SimpleWebTransport")] 9 | public class ClientTestsMultiple : SimpleWebTestBase 10 | { 11 | protected override bool StartServer => true; 12 | 13 | [UnityTest] 14 | public IEnumerator CanCreateMultipleClientInstances() 15 | { 16 | int count = 10; 17 | SimpleWebTransport[] transports = new SimpleWebTransport[count]; 18 | 19 | for (int i = 0; i < count; i++) 20 | { 21 | transports[i] = CreateTransport(); 22 | 23 | transports[i].ClientConnect("localhost"); 24 | yield return new WaitForSeconds(0.2f); 25 | } 26 | 27 | yield return new WaitForSeconds(0.5f); 28 | 29 | 30 | Assert.That(server.onConnect, Has.Count.EqualTo(count), $"Connect should have been calle {count} times"); 31 | 32 | for (int i = 0; i < count; i++) 33 | { 34 | transports[i].ClientDisconnect(); 35 | yield return new WaitForSeconds(0.2f); 36 | } 37 | 38 | // wait for disconnect 39 | yield return new WaitForSeconds(1); 40 | 41 | Assert.That(server.onDisconnect, Has.Count.EqualTo(count), $"disconnect should have been calle {count} times"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Runtime/Client/ClientTestsMultiple.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0f0ac16f3777c6848ab35bc51d40125b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Client/ClientWssTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using NUnit.Framework; 5 | using UnityEngine; 6 | using UnityEngine.TestTools; 7 | 8 | namespace Mirror.SimpleWeb.Tests.Client 9 | { 10 | [Ignore("Needs a CA key to work, see bottom of setup")] 11 | public class ClientWssTest : SimpleWebTestBase 12 | { 13 | protected override bool StartServer => false; 14 | 15 | [SetUp] 16 | public override void SetUp() 17 | { 18 | base.SetUp(); 19 | 20 | server.sslEnabled = true; 21 | server.sslCertJson = "./Assets/SimpleWebTransport/source/.cert.example.Json"; 22 | server.ServerStart(); 23 | 24 | client.sslEnabled = true; 25 | } 26 | 27 | [UnityTest] 28 | public IEnumerator Wss_CanConnectAndDisconnectFromServer() 29 | { 30 | client.ClientConnect("localhost"); 31 | 32 | // wait for connect 33 | yield return new WaitForSeconds(1); 34 | 35 | Assert.That(client.onConnect, Is.EqualTo(1), "Connect should be called once"); 36 | Assert.That(server.onConnect, Has.Count.EqualTo(1), "server Connect should be called once"); 37 | 38 | client.ClientDisconnect(); 39 | 40 | // wait for disconnect 41 | yield return new WaitForSeconds(1); 42 | 43 | Assert.That(client.onDisconnect, Is.EqualTo(1), "Disconnect should be called once"); 44 | Assert.That(server.onDisconnect, Has.Count.EqualTo(1), "server Disconnect should be called once"); 45 | } 46 | 47 | [UnityTest] 48 | public IEnumerator Wss_CanPingAndStayConnectedForTime() 49 | { 50 | // server gets message and sends reply 51 | 52 | #if MIRROR_29_0_OR_NEWER 53 | server.OnServerDataReceived = 54 | 55 | #else 56 | server.OnServerDataReceived.AddListener 57 | #endif 58 | ((i, data, __) => 59 | { 60 | Assert.That(i, Is.EqualTo(1), "Conenction Id should be 1"); 61 | 62 | byte[] expectedBytes = new byte[4] { 11, 12, 13, 14 }; 63 | CollectionAssert.AreEqual(expectedBytes, data, "data should match"); 64 | 65 | byte[] relyBytes = new byte[4] { 1, 2, 3, 4 }; 66 | server.ServerSend(new List { i }, 0, new ArraySegment(relyBytes)); 67 | }); 68 | 69 | #if MIRROR_29_0_OR_NEWER 70 | server.OnClientDataReceived = 71 | 72 | #else 73 | server.OnClientDataReceived.AddListener 74 | #endif 75 | ((data, __) => 76 | { 77 | byte[] expectedBytes = new byte[4] { 1, 2, 3, 4 }; 78 | 79 | CollectionAssert.AreEqual(expectedBytes, data, "data should match"); 80 | }); 81 | 82 | client.ClientConnect("localhost"); 83 | 84 | // wait for connect 85 | yield return new WaitForSeconds(1); 86 | 87 | for (int i = 0; i < 100; i++) 88 | { 89 | byte[] sendBytes = new byte[4] { 11, 12, 13, 14 }; 90 | client.ClientSend(0, new ArraySegment(sendBytes)); 91 | yield return new WaitForSeconds(0.1f); 92 | } 93 | 94 | // wait for message 95 | yield return new WaitForSeconds(0.25f); 96 | 97 | 98 | Assert.That(client.onData, Has.Count.EqualTo(100), "client should have 100 messages"); 99 | Assert.That(server.onData, Has.Count.EqualTo(100), "server should have 100 messages"); 100 | } 101 | 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/Runtime/Client/ClientWssTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d0be36357a4ad64408fcc1b864f8000f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3cc93aababa754847a221edf0db20095 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /tests/Runtime/Server/BadHandshake.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Linq; 3 | using System.Net.Sockets; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using NUnit.Framework; 7 | using UnityEngine; 8 | using UnityEngine.TestTools; 9 | 10 | namespace Mirror.SimpleWeb.Tests.Server 11 | { 12 | [Category("SimpleWebTransport")] 13 | public class BadHandshake : SimpleWebTestBase 14 | { 15 | protected override bool StartServer => true; 16 | 17 | TcpClient tcpClient; 18 | 19 | [TearDown] 20 | public override void TearDown() 21 | { 22 | base.TearDown(); 23 | 24 | tcpClient?.Dispose(); 25 | } 26 | 27 | [UnityTest] 28 | public IEnumerator ClosesConnectionIfBadData() 29 | { 30 | ExpectHandshakeFailedError(); 31 | 32 | Task createTask = CreateBadClient(); 33 | while (!createTask.IsCompleted) { yield return null; } 34 | tcpClient = createTask.Result; 35 | Assert.That(tcpClient.Connected, Is.True, "Client should have connected"); 36 | 37 | WriteBadData(tcpClient); 38 | 39 | // wait for message to be processed 40 | yield return new WaitForSeconds(1f); 41 | 42 | Assert.That(HasDisconnected(tcpClient), Is.True, "Client should have been disconnected"); 43 | 44 | Assert.That(server.onConnect, Has.Count.EqualTo(0), "Connect should not be called"); 45 | Assert.That(server.onDisconnect, Has.Count.EqualTo(0), "Disconnect should not be called"); 46 | Assert.That(server.onData, Has.Count.EqualTo(0), "Data should not be called"); 47 | } 48 | 49 | [UnityTest] 50 | public IEnumerator ClosesConnectionIfGetThenBadData() 51 | { 52 | ExpectHandshakeFailedError(); 53 | 54 | Task createTask = CreateBadClient(); 55 | while (!createTask.IsCompleted) { yield return null; } 56 | tcpClient = createTask.Result; 57 | Assert.That(tcpClient.Connected, Is.True, "Client should have connected"); 58 | 59 | byte[] badData = Enumerable.Range(1, 20).Select(x => (byte)x).ToArray(); 60 | badData[0] = (byte)'G'; 61 | badData[1] = (byte)'E'; 62 | badData[2] = (byte)'T'; 63 | badData[badData.Length - 4] = (byte)'\r'; 64 | badData[badData.Length - 3] = (byte)'\n'; 65 | badData[badData.Length - 2] = (byte)'\r'; 66 | badData[badData.Length - 1] = (byte)'\n'; 67 | WriteBadData(tcpClient, badData); 68 | 69 | // wait for message to be processed 70 | yield return new WaitForSeconds(1f); 71 | 72 | Assert.That(HasDisconnected(tcpClient), Is.True, "Client should have been disconnected"); 73 | 74 | Assert.That(server.onConnect, Has.Count.EqualTo(0), "Connect should not be called"); 75 | Assert.That(server.onDisconnect, Has.Count.EqualTo(0), "Disconnect should not be called"); 76 | Assert.That(server.onData, Has.Count.EqualTo(0), "Data should not be called"); 77 | } 78 | 79 | [UnityTest] 80 | public IEnumerator ClosesConnectionIfGetWithKeyThenBadData() 81 | { 82 | ExpectHandshakeFailedError(); 83 | 84 | Task createTask = CreateBadClient(); 85 | while (!createTask.IsCompleted) { yield return null; } 86 | tcpClient = createTask.Result; 87 | Assert.That(tcpClient.Connected, Is.True, "Client should have connected"); 88 | 89 | string badMessage = 90 | $"GET /chat HTTP/1.1\r\n" + 91 | $"Sec-WebSocket-Key: bad-key\r\n" + 92 | "\r\n"; 93 | byte[] badData = Encoding.ASCII.GetBytes(badMessage); 94 | WriteBadData(tcpClient, badData); 95 | 96 | // wait for message to be processed 97 | yield return new WaitForSeconds(1f); 98 | 99 | Assert.That(HasDisconnected(tcpClient), Is.True, "Client should have been disconnected"); 100 | 101 | Assert.That(server.onConnect, Has.Count.EqualTo(0), "Connect should not be called"); 102 | Assert.That(server.onDisconnect, Has.Count.EqualTo(0), "Disconnect should not be called"); 103 | Assert.That(server.onData, Has.Count.EqualTo(0), "Data should not be called"); 104 | } 105 | 106 | [UnityTest] 107 | public IEnumerator ClosesConnectionIfNoHandShakeInTimeout() 108 | { 109 | ExpectHandshakeFailedError(); 110 | 111 | Task createTask = CreateBadClient(); 112 | while (!createTask.IsCompleted) { yield return null; } 113 | tcpClient = createTask.Result; 114 | Assert.That(tcpClient.Connected, Is.True, "Client should have connected"); 115 | 116 | // wait for timeout 117 | yield return new WaitForSeconds(timeout / 1000); 118 | // wait for time to process timeout 119 | yield return new WaitForSeconds(1); 120 | 121 | Assert.That(HasDisconnected(tcpClient), Is.True, "Client should have been disconnected"); 122 | 123 | Assert.That(server.onConnect, Has.Count.EqualTo(0), "Connect should not be called"); 124 | Assert.That(server.onDisconnect, Has.Count.EqualTo(0), "Disconnect should not be called"); 125 | Assert.That(server.onData, Has.Count.EqualTo(0), "Data should not be called"); 126 | } 127 | 128 | [UnityTest] 129 | public IEnumerator OtherClientsCanConnectWhileWaitingForBadClient() 130 | { 131 | ExpectHandshakeFailedError(); 132 | 133 | // connect bad client 134 | Task createTask = CreateBadClient(); 135 | while (!createTask.IsCompleted) { yield return null; } 136 | tcpClient = createTask.Result; 137 | Assert.That(tcpClient.Connected, Is.True, "Client should have connected"); 138 | 139 | // connect good client 140 | Task task = RunNode.RunAsync("ConnectAndClose.js"); 141 | while (!task.IsCompleted) 142 | { 143 | yield return null; 144 | } 145 | 146 | // check good client connected and then closed by itself 147 | RunNode.Result result = task.Result; 148 | 149 | result.AssetTimeout(false); 150 | result.AssetOutput( 151 | "Connection opened", 152 | "Closed after 2000ms" 153 | ); 154 | result.AssetErrors(); 155 | 156 | // check server events 157 | Assert.That(server.onConnect, Has.Count.EqualTo(1), "Connect should have been called once"); 158 | Assert.That(server.onDisconnect, Has.Count.EqualTo(1), "Disconnect should have been called once"); 159 | Assert.That(server.onData, Has.Count.EqualTo(0), "Data should not be called"); 160 | 161 | 162 | // wait for timeout 163 | yield return new WaitForSeconds(timeout / 1000); 164 | 165 | Assert.That(HasDisconnected(tcpClient), Is.True, "Client should have been disconnected"); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /tests/Runtime/Server/BadHandshake.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 31c9fb7bde0566240bec0aecc124b074 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server/ConnectAndCloseTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | using UnityEngine; 5 | using UnityEngine.TestTools; 6 | 7 | namespace Mirror.SimpleWeb.Tests.Server 8 | { 9 | [Category("SimpleWebTransport")] 10 | public class ConnectAndCloseTest : SimpleWebTestBase 11 | { 12 | protected override bool StartServer => true; 13 | 14 | [UnityTest] 15 | public IEnumerator AcceptsConnection() 16 | { 17 | Task task = RunNode.RunAsync("ConnectAndClose.js"); 18 | while (!task.IsCompleted) 19 | { 20 | yield return null; 21 | } 22 | RunNode.Result result = task.Result; 23 | 24 | result.AssetTimeout(false); 25 | result.AssetOutput( 26 | "Connection opened", 27 | "Closed after 2000ms" 28 | ); 29 | result.AssetErrors(); 30 | 31 | // wait for message to be processed 32 | yield return new WaitForSeconds(0.2f); 33 | 34 | Assert.That(server.onConnect, Has.Count.EqualTo(1), "Connect should be called once"); 35 | } 36 | 37 | [UnityTest] 38 | public IEnumerator ReactsToClose() 39 | { 40 | Task task = RunNode.RunAsync("ConnectAndClose.js"); 41 | while (!task.IsCompleted) 42 | { 43 | yield return null; 44 | } 45 | RunNode.Result result = task.Result; 46 | 47 | result.AssetTimeout(false); 48 | result.AssetOutput( 49 | "Connection opened", 50 | "Closed after 2000ms" 51 | ); 52 | result.AssetErrors(); 53 | 54 | // wait for message to be processed 55 | yield return new WaitForSeconds(0.2f); 56 | 57 | Assert.That(server.onConnect, Has.Count.EqualTo(1), "Connect should be called once"); 58 | Assert.That(server.onDisconnect, Has.Count.EqualTo(1), "Disconnected should be called once"); 59 | } 60 | 61 | [UnityTest] 62 | public IEnumerator ShouldTimeoutClientAfterClientProcessIsKilled() 63 | { 64 | // kill js early so it doesn't send close message 65 | Task task = RunNode.RunAsync("Connect.js", 2000); 66 | while (!task.IsCompleted) 67 | { 68 | yield return null; 69 | } 70 | RunNode.Result result = task.Result; 71 | 72 | result.AssetTimeout(true); 73 | result.AssetOutput( 74 | "Connection opened" 75 | ); 76 | result.AssetErrors(); 77 | 78 | // wait for timeout 79 | yield return new WaitForSeconds(2 * timeout / 1000); 80 | 81 | Assert.That(server.onConnect, Has.Count.EqualTo(1), "Connect should be called once"); 82 | Assert.That(server.onDisconnect, Has.Count.EqualTo(1), "Disconnected should be called once"); 83 | } 84 | 85 | [UnityTest] 86 | public IEnumerator ShouldTimeoutClientAfterNoMessage() 87 | { 88 | // make sure doesn't timeout 89 | Task task = RunNode.RunAsync("Connect.js", timeout * 10); 90 | 91 | // wait for timeout 92 | yield return new WaitForSeconds(2 * timeout / 1000); 93 | 94 | Assert.That(server.onConnect, Has.Count.EqualTo(1), "Connect should be called once"); 95 | Assert.That(server.onDisconnect, Has.Count.EqualTo(1), "Disconnected should be called once"); 96 | 97 | Assert.That(task.IsCompleted, Is.True, "Connect.js should have stopped after connection was closed by timeout"); 98 | RunNode.Result result = task.Result; 99 | 100 | result.AssetTimeout(false); 101 | result.AssetOutput( 102 | "Connection opened", 103 | $"Connection closed" 104 | ); 105 | result.AssetErrors(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Runtime/Server/ConnectAndCloseTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 77a3ec142b49dce439b54f0ad3869dad 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server/ManyClientTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f9caad761d95d4c499366e2ce4bbd2ab 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server/MultiBadHandshake.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Net.Sockets; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using UnityEngine; 7 | using UnityEngine.TestTools; 8 | 9 | namespace Mirror.SimpleWeb.Tests.Server 10 | { 11 | [Category("SimpleWebTransport")] 12 | public class MultiBadHandshake : SimpleWebTestBase 13 | { 14 | protected override bool StartServer => true; 15 | 16 | List badClients = new List(); 17 | 18 | [TearDown] 19 | public override void TearDown() 20 | { 21 | base.TearDown(); 22 | 23 | foreach (TcpClient bad in badClients) 24 | { 25 | bad.Dispose(); 26 | } 27 | } 28 | 29 | [UnityTest] 30 | public IEnumerator MultipleGoodAndBadClients() 31 | { 32 | int connectIndex = 1; 33 | #if MIRROR_29_0_OR_NEWER 34 | server.OnServerConnected = 35 | #else 36 | server.OnServerConnected.AddListener 37 | #endif 38 | ((connId) => 39 | { 40 | Assert.That(connId == connectIndex, "Clients should be connected in order with the next index"); 41 | connectIndex++; 42 | }); 43 | const int clientCount = 10; 44 | for (int i = 0; i < clientCount; i++) 45 | { 46 | ExpectHandshakeFailedError(); 47 | 48 | // alternate between good and bad clients 49 | Task createTask = CreateBadClient(); 50 | while (!createTask.IsCompleted) { yield return null; } 51 | TcpClient client = createTask.Result; 52 | Assert.That(client.Connected, Is.True, "Client should have connected"); 53 | badClients.Add(client); 54 | } 55 | Task task = RunNode.RunAsync("ConnectAndCloseMany.js", arg0: clientCount.ToString()); 56 | 57 | // wait for timeout + extra so bad clients disconnect 58 | yield return new WaitForSeconds(2 * timeout / 1000); 59 | 60 | Assert.That(server.onConnect, Has.Count.EqualTo(clientCount), "Connect should not be called"); 61 | Assert.That(server.onDisconnect, Has.Count.EqualTo(clientCount), "Disconnect should not be called"); 62 | Assert.That(server.onData, Has.Count.EqualTo(0), "Data should not be called"); 63 | 64 | Assert.That(task.IsCompleted, Is.True, "Take should have been completed"); 65 | RunNode.Result result = task.Result; 66 | 67 | result.AssetTimeout(false); 68 | result.AssetErrors(); 69 | List expected = new List(); 70 | for (int i = 0; i < clientCount; i++) 71 | { 72 | expected.Add($"{i}: Connection opened"); 73 | expected.Add($"{i}: Closed after 2000ms"); 74 | } 75 | result.AssetOutputUnordered(expected.ToArray()); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Runtime/Server/MultiBadHandshake.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 417b3ee1333946046a1e114441f4c74c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server/ReceiveMessageTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d869a02d922b49a43b44052b1c22fc5a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server/SendMessageTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using NUnit.Framework; 7 | using UnityEngine; 8 | using UnityEngine.TestTools; 9 | 10 | namespace Mirror.SimpleWeb.Tests.Server 11 | { 12 | [Category("SimpleWebTransport")] 13 | public class SendMessageTest : SimpleWebTestBase 14 | { 15 | protected override bool StartServer => true; 16 | 17 | [UnityTest] 18 | public IEnumerator SendOne() 19 | { 20 | Task task = RunNode.RunAsync("ReceiveMessages.js"); 21 | 22 | yield return server.WaitForConnection; 23 | 24 | byte[] bytes = new byte[] { 1, 2, 3, 4, 5 }; 25 | ArraySegment segment = new ArraySegment(bytes); 26 | 27 | server.ServerSend(new List { 1 }, Channels.DefaultReliable, segment); 28 | 29 | yield return new WaitForSeconds(0.5f); 30 | server.ServerDisconnect(1); 31 | 32 | yield return new WaitUntil(() => task.IsCompleted); 33 | 34 | RunNode.Result result = task.Result; 35 | 36 | result.AssetTimeout(false); 37 | result.AssetOutput( 38 | "length: 5 msg: 01 02 03 04 05" 39 | ); 40 | result.AssetErrors(); 41 | } 42 | 43 | [UnityTest] 44 | [TestCase(1, ExpectedResult = default)] 45 | [TestCase(50, ExpectedResult = default)] 46 | [TestCase(100, ExpectedResult = default)] 47 | [TestCase(124, ExpectedResult = default)] 48 | [TestCase(125, ExpectedResult = default)] 49 | [TestCase(126, ExpectedResult = default)] 50 | [TestCase(127, ExpectedResult = default)] 51 | [TestCase(128, ExpectedResult = default)] 52 | [TestCase(129, ExpectedResult = default)] 53 | [TestCase(250, ExpectedResult = default)] 54 | [TestCase(1000, ExpectedResult = default)] 55 | [TestCase(5000, ExpectedResult = default)] 56 | public IEnumerator SendDifferentSizes(int msgSize) 57 | { 58 | Task task = RunNode.RunAsync("ReceiveMessages.js"); 59 | 60 | yield return server.WaitForConnection; 61 | 62 | byte[] bytes = Enumerable.Range(1, msgSize).Select(x => (byte)x).ToArray(); 63 | ArraySegment segment = new ArraySegment(bytes); 64 | 65 | server.ServerSend(new List { 1 }, Channels.DefaultReliable, segment); 66 | 67 | yield return new WaitForSeconds(0.5f); 68 | server.ServerDisconnect(1); 69 | 70 | yield return new WaitUntil(() => task.IsCompleted); 71 | 72 | RunNode.Result result = task.Result; 73 | 74 | result.AssetTimeout(false); 75 | result.AssetOutput( 76 | $"length: {msgSize} msg: {BitConverter.ToString(bytes).Replace('-', ' ')}" 77 | ); 78 | result.AssetErrors(); 79 | } 80 | 81 | [UnityTest] 82 | public IEnumerator SendMany() 83 | { 84 | Task task = RunNode.RunAsync("ReceiveManyMessages.js", 5_000); 85 | 86 | yield return server.WaitForConnection; 87 | const int messageCount = 100; 88 | 89 | for (int i = 0; i < messageCount; i++) 90 | { 91 | using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter()) 92 | { 93 | writer.WriteByte((byte)i); 94 | writer.WriteInt32(100); 95 | 96 | ArraySegment segment = writer.ToArraySegment(); 97 | 98 | server.ServerSend(new List { 1 }, Channels.DefaultReliable, segment); 99 | } 100 | } 101 | 102 | yield return new WaitForSeconds(1); 103 | server.ServerDisconnect(1); 104 | 105 | yield return new WaitUntil(() => task.IsCompleted); 106 | 107 | RunNode.Result result = task.Result; 108 | 109 | string expectedFormat = "length: 5 msg: {0:X2} 64 00 00 00"; 110 | IEnumerable expected = Enumerable.Range(0, messageCount).Select(i => string.Format(expectedFormat, i)); 111 | 112 | result.AssetTimeout(false); 113 | result.AssetOutput(expected.ToArray()); 114 | result.AssetErrors(); 115 | } 116 | 117 | [UnityTest] 118 | public IEnumerator ErrorWhenMessageTooBig() 119 | { 120 | yield return null; 121 | 122 | ArraySegment segment = new ArraySegment(new byte[70_000]); 123 | 124 | LogAssert.Expect(LogType.Error, "ERROR: Message greater than max size"); 125 | server.ServerSend(new List { 1 }, Channels.DefaultReliable, segment); 126 | } 127 | 128 | [UnityTest] 129 | public IEnumerator ErrorWhenMessageTooSmall() 130 | { 131 | yield return null; 132 | 133 | ArraySegment segment = new ArraySegment(); 134 | 135 | LogAssert.Expect(LogType.Error, "ERROR: Message count was zero"); 136 | server.ServerSend(new List { 1 }, Channels.DefaultReliable, segment); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/Runtime/Server/SendMessageTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9cc73e8ef33a12c4291c8377187507dc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server/ServerDisconnectTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | using UnityEngine; 5 | using UnityEngine.TestTools; 6 | 7 | namespace Mirror.SimpleWeb.Tests.Server 8 | { 9 | [Category("SimpleWebTransport")] 10 | public class DisconnectTest : SimpleWebTestBase 11 | { 12 | protected override bool StartServer => true; 13 | 14 | [UnityTest] 15 | public IEnumerator CanKickConnection() 16 | { 17 | Task task = RunNode.RunAsync("Disconnect.js"); 18 | 19 | yield return server.WaitForConnection; 20 | 21 | server.ServerDisconnect(1); 22 | 23 | yield return new WaitUntil(() => task.IsCompleted); 24 | 25 | RunNode.Result result = task.Result; 26 | 27 | result.AssetTimeout(false); 28 | result.AssetOutput( 29 | "Connection closed" 30 | ); 31 | result.AssetErrors(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Runtime/Server/ServerDisconnectTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 66bc3951edfaeca40bee550bd3d4746f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server/SslServerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using UnityEngine; 6 | using UnityEngine.TestTools; 7 | 8 | namespace Mirror.SimpleWeb.Tests.Server 9 | { 10 | [Category("SimpleWebTransport")] 11 | [Ignore("Needs a CA key to work, see bottom of setup")] 12 | public class SslServerTest : SimpleWebTestBase 13 | { 14 | protected override bool StartServer => false; 15 | 16 | [SetUp] 17 | public override void SetUp() 18 | { 19 | base.SetUp(); 20 | 21 | server.sslEnabled = true; 22 | server.sslCertJson = "./Assets/SimpleWebTransport/source/.cert.example.Json"; 23 | server.ServerStart(); 24 | 25 | // to use these test you need to create a CA cert and use it to sign MirrorLocal 26 | // then add the cert to node so that it will accept it 27 | Environment.SetEnvironmentVariable("NODE_EXTRA_CA_CERTS", "path/to/CACert.pem"); 28 | } 29 | 30 | [UnityTest] 31 | public IEnumerator ClientCanConnectOverWss() 32 | { 33 | Task task = RunNode.RunAsync("WssConnectAndClose.js"); 34 | while (!task.IsCompleted) 35 | { 36 | yield return null; 37 | } 38 | RunNode.Result result = task.Result; 39 | 40 | result.AssetTimeout(false); 41 | result.AssetOutput( 42 | "Connection opened", 43 | "Closed after 2000ms" 44 | ); 45 | result.AssetErrors(); 46 | 47 | // wait for message to be processed 48 | yield return new WaitForSeconds(0.2f); 49 | 50 | Assert.That(server.onConnect, Has.Count.EqualTo(1), "Connect should be called once"); 51 | } 52 | } 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/Runtime/Server/SslServerTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d14ddc4551246b54295380c45600c9b9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/Server/StartAndStopTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using NUnit.Framework; 3 | using UnityEngine; 4 | using UnityEngine.TestTools; 5 | 6 | namespace Mirror.SimpleWeb.Tests.Server 7 | { 8 | [Category("SimpleWebTransport")] 9 | public class StartAndStopTest : SimpleWebTestBase 10 | { 11 | protected override bool StartServer => false; 12 | 13 | [UnityTest] 14 | public IEnumerator ServerCanStartAndStopWithoutErrors() 15 | { 16 | SimpleWebTransport transport = CreateTransport(); 17 | 18 | transport.ServerStart(); 19 | Assert.That(transport.ServerActive(), Is.True); 20 | yield return new WaitForSeconds(0.2f); 21 | Assert.That(transport.ServerActive(), Is.True); 22 | 23 | transport.ServerStop(); 24 | Assert.That(transport.ServerActive(), Is.False); 25 | yield return new WaitForSeconds(0.2f); 26 | Assert.That(transport.ServerActive(), Is.False); 27 | } 28 | 29 | 30 | [UnityTest] 31 | public IEnumerator CanStart2ndServerAfterFirstSTops() 32 | { 33 | // use {} block for local variable scope 34 | { 35 | SimpleWebTransport transport = CreateTransport(); 36 | 37 | transport.ServerStart(); 38 | Assert.That(transport.ServerActive(), Is.True); 39 | yield return new WaitForSeconds(0.2f); 40 | Assert.That(transport.ServerActive(), Is.True); 41 | 42 | transport.ServerStop(); 43 | Assert.That(transport.ServerActive(), Is.False); 44 | } 45 | 46 | { 47 | SimpleWebTransport transport = CreateTransport(); 48 | 49 | transport.ServerStart(); 50 | Assert.That(transport.ServerActive(), Is.True); 51 | yield return new WaitForSeconds(0.2f); 52 | Assert.That(transport.ServerActive(), Is.True); 53 | 54 | transport.ServerStop(); 55 | Assert.That(transport.ServerActive(), Is.False); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Runtime/Server/StartAndStopTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c4be3e42d6d59cd489ae8d17119cffce 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/SimpleWebTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Sockets; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | using NUnit.Framework; 9 | using UnityEngine; 10 | using UnityEngine.TestTools; 11 | 12 | namespace Mirror.SimpleWeb.Tests 13 | { 14 | [Category("SimpleWebTransport")] 15 | public abstract class SimpleWebTestBase 16 | { 17 | protected const int timeout = 4000; 18 | const Log.Levels LogLevel = Log.Levels.verbose; 19 | 20 | protected abstract bool StartServer { get; } 21 | 22 | protected ServerTestInstance server; 23 | protected ClientTestInstance client; 24 | 25 | List toCleanup = new List(); 26 | 27 | [SetUp] 28 | public virtual void SetUp() 29 | { 30 | Debug.Log($"SetUp {TestContext.CurrentContext.Test.Name}"); 31 | 32 | server = CreateTransport(); 33 | client = CreateTransport(); 34 | 35 | if (StartServer) 36 | { 37 | server.ServerStart(); 38 | } 39 | } 40 | 41 | 42 | [TearDown] 43 | public virtual void TearDown() 44 | { 45 | Debug.Log($"TearDown {TestContext.CurrentContext.Test.Name}"); 46 | 47 | foreach (GameObject obj in toCleanup) 48 | { 49 | if (obj != null) 50 | { 51 | Transport transport; 52 | if ((transport = obj.GetComponent()) != null) 53 | { 54 | transport.Shutdown(); 55 | } 56 | GameObject.DestroyImmediate(obj); 57 | } 58 | } 59 | } 60 | 61 | protected T CreateTransport() where T : SimpleWebTransport 62 | { 63 | GameObject go = new GameObject(); 64 | toCleanup.Add(go); 65 | 66 | T transport = go.AddComponent(); 67 | transport.port = 7776; 68 | transport.LogLevels = LogLevel; 69 | transport.receiveTimeout = timeout; 70 | transport.sendTimeout = timeout; 71 | 72 | if (transport is NeedInitTestInstance needInit) 73 | { 74 | needInit.Init(); 75 | } 76 | return transport; 77 | } 78 | 79 | protected static Task CreateBadClient() 80 | { 81 | return Task.Run(() => 82 | { 83 | try 84 | { 85 | TcpClient client = new TcpClient 86 | { 87 | SendTimeout = 1000, 88 | ReceiveTimeout = 1000 89 | }; 90 | 91 | client.Connect("localhost", 7776); 92 | 93 | return client; 94 | } 95 | catch (Exception e) 96 | { 97 | Debug.LogException(e); 98 | } 99 | return null; 100 | }); 101 | } 102 | 103 | protected static bool HasDisconnected(TcpClient client) 104 | { 105 | bool resetOrHasData = client.Client.Poll(-1, SelectMode.SelectRead); 106 | bool noData = client.Available == 0; 107 | bool reset = resetOrHasData && noData; 108 | 109 | return reset; 110 | } 111 | 112 | protected static void WriteBadData(TcpClient client, byte[] badData = null) 113 | { 114 | if (badData == null) 115 | badData = Enumerable.Range(1, 10).Select(x => (byte)x).ToArray(); 116 | try 117 | { 118 | client.GetStream().Write(badData, 0, badData.Length); 119 | } 120 | catch (IOException) { } 121 | } 122 | 123 | protected void ExpectHandshakeFailedError() 124 | { 125 | LogAssert.Expect(LogType.Error, new Regex("ERROR: Handshake Failed.*")); 126 | } 127 | 128 | protected void ExpectInvalidDataError() 129 | { 130 | LogAssert.Expect(LogType.Error, new Regex(@"ERROR: Invalid data from \[Conn:1")); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/Runtime/SimpleWebTestBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a88a985a8a4bab740b938695cd461d61 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/Runtime/SimpleWebTransport.Tests.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleWebTransport.Tests.Runtime", 3 | "references": [ 4 | "Mirror", 5 | "SimpleWebTransport", 6 | "SimpleWebTransport.Tests.Common" 7 | ], 8 | "optionalUnityReferences": [ 9 | "TestAssemblies" 10 | ], 11 | "includePlatforms": [], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": false, 14 | "overrideReferences": false, 15 | "precompiledReferences": [], 16 | "autoReferenced": true, 17 | "defineConstraints": [] 18 | } -------------------------------------------------------------------------------- /tests/Runtime/SimpleWebTransport.Tests.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 467fceee903232c4b923647585823e29 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /tests/Runtime/TestInstances.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace Mirror.SimpleWeb.Tests 6 | { 7 | public static class SimpleWebTransportExtension 8 | { 9 | /// 10 | /// Create copy of array, need to do this because buffers are re-used 11 | /// 12 | /// 13 | /// 14 | /// 15 | public static byte[] CreateCopy(this SimpleWebTransport _, ArraySegment segment) 16 | { 17 | byte[] copy = new byte[segment.Count]; 18 | Array.Copy(segment.Array, segment.Offset, copy, 0, segment.Count); 19 | return copy; 20 | } 21 | } 22 | interface NeedInitTestInstance 23 | { 24 | void Init(); 25 | } 26 | public class ServerTestInstance : SimpleWebTransport, NeedInitTestInstance 27 | { 28 | public readonly List onConnect = new List(); 29 | public readonly List onDisconnect = new List(); 30 | public readonly List<(int connId, byte[] data)> onData = new List<(int connId, byte[] data)>(); 31 | public readonly List<(int connId, Exception exception)> onError = new List<(int connId, Exception exception)>(); 32 | 33 | public void Init() 34 | { 35 | #if MIRROR_29_0_OR_NEWER 36 | base.OnServerConnected = (connId) => onConnect.Add(connId); 37 | base.OnServerDisconnected = (connId) => onDisconnect.Add(connId); 38 | base.OnServerDataReceived = (connId, data, _) => onData.Add((connId, this.CreateCopy(data))); 39 | base.OnServerError = (connId, exception) => onError.Add((connId, exception)); 40 | #else 41 | base.OnServerConnected.AddListener((connId) => onConnect.Add(connId)); 42 | base.OnServerDisconnected.AddListener((connId) => onDisconnect.Add(connId)); 43 | base.OnServerDataReceived.AddListener((connId, data, _) => onData.Add((connId, this.CreateCopy(data)))); 44 | base.OnServerError.AddListener((connId, exception) => onError.Add((connId, exception))); 45 | #endif 46 | } 47 | 48 | public WaitUntil WaitForConnection => new WaitUntil(() => onConnect.Count >= 1); 49 | 50 | #if MIRROR_26_0_OR_NEWER 51 | public void ServerSend(System.Collections.Generic.List connectionIds, int channelId, ArraySegment segment) 52 | { 53 | foreach (int id in connectionIds) 54 | { 55 | ServerSend(id, channelId, segment); 56 | } 57 | } 58 | #endif 59 | } 60 | public class ClientTestInstance : SimpleWebTransport, NeedInitTestInstance 61 | { 62 | public int onConnect = 0; 63 | public int onDisconnect = 0; 64 | public readonly List onData = new List(); 65 | public readonly List onError = new List(); 66 | 67 | public void Init() 68 | { 69 | #if MIRROR_29_0_OR_NEWER 70 | base.OnClientConnected = () => onConnect++; 71 | base.OnClientDisconnected = () => onDisconnect++; 72 | base.OnClientDataReceived = (data, _) => onData.Add(this.CreateCopy(data)); 73 | base.OnClientError = (exception) => onError.Add(exception); 74 | #else 75 | base.OnClientConnected.AddListener(() => onConnect++); 76 | base.OnClientDisconnected.AddListener(() => onDisconnect++); 77 | base.OnClientDataReceived.AddListener((data, _) => onData.Add(this.CreateCopy(data))); 78 | base.OnClientError.AddListener((exception) => onError.Add(exception)); 79 | #endif 80 | } 81 | 82 | public WaitUntil WaitForConnect => new WaitUntil(() => onConnect >= 1); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/Runtime/TestInstances.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1b4c7349297056942be9af5b8a4ff81a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /tests/node~/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /tests/node~/Connect.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('open', function (event) { 13 | console.log('Connection opened'); 14 | }); 15 | 16 | webSocket.addEventListener('close', function (event) { 17 | console.log('Connection closed'); 18 | process.exit(0); 19 | }); -------------------------------------------------------------------------------- /tests/node~/ConnectAndClose.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | const closeTimeout = 2000; 12 | let closed = false; 13 | 14 | // Connection opened 15 | webSocket.addEventListener('open', function (event) { 16 | console.log('Connection opened'); 17 | setTimeout(() => { 18 | if (!closed) { 19 | closed = true; 20 | console.log(`Closed after ${closeTimeout}ms`); 21 | webSocket.close(1000, `Closed after ${closeTimeout}ms`); 22 | setTimeout(() => { 23 | process.exit(0); 24 | }, 50); 25 | } 26 | }, closeTimeout); 27 | }); 28 | 29 | webSocket.addEventListener('close', function (event) { 30 | // stop process in case close is called 31 | process.exit(0); 32 | }); -------------------------------------------------------------------------------- /tests/node~/ConnectAndCloseMany.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | const RunMany = require("./RunMany").RunMany; 3 | 4 | RunMany((onExit, log, error) => { 5 | // Create webSocket connection. 6 | const webSocket = new WebSocket("ws://localhost:7776/"); 7 | webSocket.binaryType = 'arraybuffer'; 8 | 9 | webSocket.addEventListener('error', function (event) { 10 | error(`Socket Error ${event}`); 11 | }); 12 | 13 | const closeTimeout = 2000; 14 | let closed = false; 15 | 16 | // Connection opened 17 | webSocket.addEventListener('open', function (event) { 18 | log(`Connection opened`); 19 | setTimeout(() => { 20 | if (!closed) { 21 | closed = true; 22 | log(`Closed after ${closeTimeout}ms`); 23 | webSocket.close(1000, `Closed after ${closeTimeout}ms`); 24 | setTimeout(() => { 25 | onExit(); 26 | }, 50); 27 | } 28 | }, closeTimeout); 29 | }); 30 | 31 | webSocket.addEventListener('close', function (event) { 32 | // stop process in case close is called 33 | onExit(); 34 | }); 35 | }); -------------------------------------------------------------------------------- /tests/node~/Disconnect.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('close', function (event) { 13 | console.log('Connection closed'); 14 | process.exit(0); 15 | }); -------------------------------------------------------------------------------- /tests/node~/HelloError.js: -------------------------------------------------------------------------------- 1 | console.error("Hello Error!"); -------------------------------------------------------------------------------- /tests/node~/HelloError2.js: -------------------------------------------------------------------------------- 1 | console.error("Hello Error!"); 2 | console.error("Hello again Error!"); -------------------------------------------------------------------------------- /tests/node~/HelloWorld.js: -------------------------------------------------------------------------------- 1 | console.log("Hello World!"); -------------------------------------------------------------------------------- /tests/node~/HelloWorld2.js: -------------------------------------------------------------------------------- 1 | console.log("Hello World!"); 2 | console.log("Hello again World!"); -------------------------------------------------------------------------------- /tests/node~/Ping.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | const RunMany = require("./RunMany").RunMany; 4 | 5 | RunMany((onExit, log, error) => { 6 | // Create webSocket connection. 7 | const webSocket = new WebSocket("ws://localhost:7776/"); 8 | webSocket.binaryType = 'arraybuffer'; 9 | 10 | webSocket.addEventListener('error', function (event) { 11 | error(`Socket Error ${event}`); 12 | }); 13 | 14 | const pingInterval = 1000; 15 | let intervalHandle; 16 | // Connection opened 17 | webSocket.addEventListener('open', function (event) { 18 | intervalHandle = setInterval(() => { 19 | var buffer = new ArrayBuffer(4); 20 | var view = new Uint8Array(buffer); 21 | for (let i = 0; i < view.length; i++) { 22 | view[i] = i + 10; 23 | } 24 | webSocket.send(buffer); 25 | }, pingInterval); 26 | }); 27 | 28 | webSocket.addEventListener('close', function (event) { 29 | onExit(); 30 | clearInterval(intervalHandle); 31 | }); 32 | }); -------------------------------------------------------------------------------- /tests/node~/README.md: -------------------------------------------------------------------------------- 1 | use nodejs websocket client to do unit tests -------------------------------------------------------------------------------- /tests/node~/README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1c47e355172b40d46963931bdae91eaf 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /tests/node~/ReceiveManyMessages.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // use ping to keep connection alive 12 | const pingInterval = 1000; 13 | // Connection opened 14 | webSocket.addEventListener('open', function (event) { 15 | setInterval(() => { 16 | var buffer = new ArrayBuffer(4); 17 | var view = new Uint8Array(buffer); 18 | for (let i = 0; i < view.length; i++) { 19 | view[i] = i + 10; 20 | } 21 | webSocket.send(buffer); 22 | }, pingInterval); 23 | }); 24 | 25 | // let msgCount = 0; 26 | // Connection opened 27 | webSocket.addEventListener('message', function (event) { 28 | var buffer = event.data; 29 | if (buffer instanceof ArrayBuffer) { 30 | // msgCount++; 31 | // console.log(msgCount); 32 | console.log(`length: ${buffer.byteLength} msg: ${buf2hex(buffer)}`); 33 | } 34 | else { 35 | console.error("Message not array buffer"); 36 | } 37 | }); 38 | 39 | function buf2hex(buffer) { // buffer is an ArrayBuffer 40 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(' '); 41 | } 42 | 43 | webSocket.addEventListener('close', function (event) { 44 | process.exit(0); 45 | }); -------------------------------------------------------------------------------- /tests/node~/ReceiveMessages.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('message', function (event) { 13 | var buffer = event.data; 14 | if (buffer instanceof ArrayBuffer) { 15 | console.log(`length: ${buffer.byteLength} msg: ${buf2hex(buffer)}`); 16 | } 17 | else { 18 | console.error("Message not array buffer"); 19 | } 20 | }); 21 | 22 | function buf2hex(buffer) { // buffer is an ArrayBuffer 23 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(' '); 24 | } 25 | 26 | webSocket.addEventListener('close', function (event) { 27 | process.exit(0); 28 | }); -------------------------------------------------------------------------------- /tests/node~/RunMany.js: -------------------------------------------------------------------------------- 1 | const args = process.argv.slice(2); 2 | const runCount = parseInt(args[0]); 3 | 4 | 5 | const State = { 6 | NOT_STARTED: 'NOT_STARTED', 7 | RUNNING: 'RUNNING', 8 | STOPPED: 'STOPPED', 9 | } 10 | 11 | function RunMany(inner, timeoutBetweenRun = 20) { 12 | const currentlyRunning = []; 13 | for (let i = 0; i < runCount; i++) { 14 | currentlyRunning.push(State.NOT_STARTED); 15 | setTimeout(() => { 16 | currentlyRunning[i] = State.STARTED; 17 | 18 | function log(str) { 19 | console.log(`${i}: ${str}`); 20 | } 21 | function error(str) { 22 | console.error(`${i}: ${str}`); 23 | } 24 | function onExit() { 25 | currentlyRunning[i] = State.STOPPED; 26 | // check if all not running 27 | if (currentlyRunning.every(x => x == State.STOPPED)) { 28 | process.exit(0); 29 | } 30 | } 31 | 32 | inner(onExit, log, error); 33 | }, i * timeoutBetweenRun); 34 | } 35 | 36 | } 37 | exports.RunMany = RunMany; -------------------------------------------------------------------------------- /tests/node~/SendAlmostLargeMessages.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('open', function (event) { 13 | var buffer = new ArrayBuffer(10000); 14 | var view = new Uint8Array(buffer); 15 | for (let i = 0; i < view.length; i++) { 16 | view[i] = i % 255; 17 | } 18 | 19 | webSocket.send(buffer); 20 | }); 21 | 22 | webSocket.addEventListener('close', function (event) { 23 | process.exit(0); 24 | }); -------------------------------------------------------------------------------- /tests/node~/SendLargeMessages.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('open', function (event) { 13 | var buffer = new ArrayBuffer(16384); 14 | var view = new Uint8Array(buffer); 15 | for (let i = 0; i < view.length; i++) { 16 | view[i] = i % 255; 17 | } 18 | 19 | webSocket.send(buffer); 20 | }); 21 | 22 | webSocket.addEventListener('close', function (event) { 23 | process.exit(0); 24 | }); -------------------------------------------------------------------------------- /tests/node~/SendManyLargeMessages.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('open', function (event) { 13 | var buffer = new ArrayBuffer(16384); 14 | var view = new Uint8Array(buffer); 15 | for (let i = 0; i < view.length; i++) { 16 | view[i] = i % 255; 17 | } 18 | // send 100 message 20ms apart 19 | for (let i = 0; i < 100; i++) { 20 | 21 | setTimeout(() => { 22 | view[0] = i; 23 | webSocket.send(buffer); 24 | }, i * 20); 25 | } 26 | setTimeout(() => { 27 | webSocket.close(); 28 | }, (100 * 20) + 1000); 29 | }); 30 | 31 | webSocket.addEventListener('close', function (event) { 32 | process.exit(0); 33 | }); -------------------------------------------------------------------------------- /tests/node~/SendManyMessages.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('open', function (event) { 13 | var buffer = new ArrayBuffer(10); 14 | var view = new Uint8Array(buffer); 15 | for (let i = 0; i < view.length; i++) { 16 | view[i] = i + 10; 17 | } 18 | // send 100 message as fast as possible 19 | for (let i = 0; i < 100; i++) { 20 | view[0] = i; 21 | webSocket.send(buffer); 22 | } 23 | }); 24 | 25 | webSocket.addEventListener('close', function (event) { 26 | process.exit(0); 27 | }); -------------------------------------------------------------------------------- /tests/node~/SendMessages.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('open', function (event) { 13 | var buffer = new ArrayBuffer(10); 14 | var view = new Uint8Array(buffer); 15 | for (let i = 0; i < view.length; i++) { 16 | view[i] = i + 10; 17 | } 18 | webSocket.send(buffer); 19 | }); 20 | 21 | webSocket.addEventListener('close', function (event) { 22 | process.exit(0); 23 | }); -------------------------------------------------------------------------------- /tests/node~/SendTooLargeMessages.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("ws://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | // Connection opened 12 | webSocket.addEventListener('open', function (event) { 13 | var buffer = new ArrayBuffer(16385); 14 | var view = new Uint8Array(buffer); 15 | for (let i = 0; i < view.length; i++) { 16 | view[i] = i % 255; 17 | } 18 | 19 | webSocket.send(buffer); 20 | }); 21 | 22 | webSocket.addEventListener('close', function (event) { 23 | process.exit(0); 24 | }); -------------------------------------------------------------------------------- /tests/node~/Timeout.js: -------------------------------------------------------------------------------- 1 | setTimeout(() => { 2 | console.log("Should be running"); 3 | }, 1000); 4 | setTimeout(() => { 5 | console.error("Should be stopped"); 6 | }, 10000); -------------------------------------------------------------------------------- /tests/node~/WssConnectAndClose.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("websocket").w3cwebsocket; 2 | 3 | // Create webSocket connection. 4 | const webSocket = new WebSocket("wss://localhost:7776/"); 5 | webSocket.binaryType = 'arraybuffer'; 6 | 7 | webSocket.addEventListener('error', function (event) { 8 | console.error('Socket Error', event); 9 | }); 10 | 11 | const closeTimeout = 2000; 12 | let closed = false; 13 | 14 | // Connection opened 15 | webSocket.addEventListener('open', function (event) { 16 | console.log('Connection opened'); 17 | setTimeout(() => { 18 | if (!closed) { 19 | closed = true; 20 | console.log(`Closed after ${closeTimeout}ms`); 21 | webSocket.close(1000, `Closed after ${closeTimeout}ms`); 22 | setTimeout(() => { 23 | process.exit(0); 24 | }, 50); 25 | } 26 | }, closeTimeout); 27 | }); 28 | 29 | webSocket.addEventListener('close', function (event) { 30 | // stop process in case close is called 31 | process.exit(0); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/node~/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-web-transport-tests", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bufferutil": { 8 | "version": "4.0.1", 9 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.1.tgz", 10 | "integrity": "sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==", 11 | "requires": { 12 | "node-gyp-build": "~3.7.0" 13 | } 14 | }, 15 | "d": { 16 | "version": "1.0.1", 17 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", 18 | "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", 19 | "requires": { 20 | "es5-ext": "^0.10.50", 21 | "type": "^1.0.1" 22 | } 23 | }, 24 | "debug": { 25 | "version": "2.6.9", 26 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 27 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 28 | "requires": { 29 | "ms": "2.0.0" 30 | } 31 | }, 32 | "es5-ext": { 33 | "version": "0.10.53", 34 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", 35 | "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", 36 | "requires": { 37 | "es6-iterator": "~2.0.3", 38 | "es6-symbol": "~3.1.3", 39 | "next-tick": "~1.0.0" 40 | } 41 | }, 42 | "es6-iterator": { 43 | "version": "2.0.3", 44 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", 45 | "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", 46 | "requires": { 47 | "d": "1", 48 | "es5-ext": "^0.10.35", 49 | "es6-symbol": "^3.1.1" 50 | } 51 | }, 52 | "es6-symbol": { 53 | "version": "3.1.3", 54 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", 55 | "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", 56 | "requires": { 57 | "d": "^1.0.1", 58 | "ext": "^1.1.2" 59 | } 60 | }, 61 | "ext": { 62 | "version": "1.4.0", 63 | "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", 64 | "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", 65 | "requires": { 66 | "type": "^2.0.0" 67 | }, 68 | "dependencies": { 69 | "type": { 70 | "version": "2.1.0", 71 | "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", 72 | "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" 73 | } 74 | } 75 | }, 76 | "is-typedarray": { 77 | "version": "1.0.0", 78 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 79 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 80 | }, 81 | "ms": { 82 | "version": "2.0.0", 83 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 84 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 85 | }, 86 | "next-tick": { 87 | "version": "1.0.0", 88 | "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", 89 | "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" 90 | }, 91 | "node-gyp-build": { 92 | "version": "3.7.0", 93 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz", 94 | "integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==" 95 | }, 96 | "type": { 97 | "version": "1.2.0", 98 | "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", 99 | "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" 100 | }, 101 | "typedarray-to-buffer": { 102 | "version": "3.1.5", 103 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 104 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 105 | "requires": { 106 | "is-typedarray": "^1.0.0" 107 | } 108 | }, 109 | "utf-8-validate": { 110 | "version": "5.0.2", 111 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.2.tgz", 112 | "integrity": "sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==", 113 | "requires": { 114 | "node-gyp-build": "~3.7.0" 115 | } 116 | }, 117 | "websocket": { 118 | "version": "1.0.32", 119 | "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.32.tgz", 120 | "integrity": "sha512-i4yhcllSP4wrpoPMU2N0TQ/q0O94LRG/eUQjEAamRltjQ1oT1PFFKOG4i877OlJgCG8rw6LrrowJp+TYCEWF7Q==", 121 | "requires": { 122 | "bufferutil": "^4.0.1", 123 | "debug": "^2.2.0", 124 | "es5-ext": "^0.10.50", 125 | "typedarray-to-buffer": "^3.1.5", 126 | "utf-8-validate": "^5.0.2", 127 | "yaeti": "^0.0.6" 128 | } 129 | }, 130 | "yaeti": { 131 | "version": "0.0.6", 132 | "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", 133 | "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/node~/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-web-transport-tests", 3 | "version": "1.0.0", 4 | "description": "use nodejs websocket client to do unit tests", 5 | "author": "", 6 | "license": "ISC", 7 | "dependencies": { 8 | "websocket": "^1.0.32" 9 | } 10 | } 11 | --------------------------------------------------------------------------------