├── .gitignore ├── LICENSE.md ├── Properties └── AssemblyInfo.cs ├── README.md ├── WebSocketEventListener.cs ├── WebSocketServer.cs ├── WebSocketServer.csproj ├── WebSocketServer.sln └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | bin 3 | obj 4 | packages 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kevin Poirot 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 | 23 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | // Information about this assembly is defined by the following attributes. 5 | // Change them to the values specific to your project. 6 | [assembly: AssemblyTitle("WebSocket Server")] 7 | [assembly: AssemblyDescription ("")] 8 | [assembly: AssemblyConfiguration ("")] 9 | [assembly: AssemblyCompany("Sourdough")] 10 | [assembly: AssemblyProduct ("")] 11 | [assembly: AssemblyCopyright("")] 12 | [assembly: AssemblyTrademark ("")] 13 | [assembly: AssemblyCulture ("")] 14 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 15 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 16 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 17 | [assembly: AssemblyVersion("0.2.1")] 18 | // The following attributes are used to specify the signing key for the assembly, 19 | // if desired. See the Mono documentation for more information about signing. 20 | //[assembly: AssemblyDelaySign(false)] 21 | //[assembly: AssemblyKeyFile("")] 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket Server 2 | 3 | This library exposes a simple WebSocket server into FXServer. 4 | 5 | ## Features 6 | 7 | * Async 8 | * Text messages 9 | * Authorization header support 10 | 11 | ## Configuration 12 | 13 | Convars available: 14 | 15 | | Name | Type | Default value | Description | 16 | |------------------------- |-------- |--------------- |------------------------------------------------------------- | 17 | | websocket_debug | bool | false | Defines the verbosity of logs | 18 | | websocket_host | string | "127.0.0.1" | Defines listening host | 19 | | websocket_port | int | 80 | Defines listening port | 20 | | websocket_authorization | string | "" | Defines accepted Authorization header value (auth disabled if empty) | 21 | 22 | ## Usage 23 | 24 | ### Add a listener to receive messages 25 | 26 | ```lua 27 | AddEventHandler("WebSocketServer:onMessage", function(message, endpoint) 28 | print("Received message from " .. endpoint .. ": " .. message) 29 | end) 30 | ``` 31 | 32 | ### Add a listener to get new connected remote endpoints 33 | 34 | ```lua 35 | AddEventHandler("WebSocketServer:onConnect", function(endpoint) 36 | print("New WS remote endpoint: " .. endpoint) 37 | end) 38 | ``` 39 | 40 | ### Add a listener to get disconnected remote endpoints 41 | 42 | ```lua 43 | AddEventHandler("WebSocketServer:onDisconnect", function(endpoint) 44 | print("WS remote endpoint " .. endpoint .. " has been disconnected") 45 | end) 46 | ``` 47 | 48 | ### Send a message to connected WebSocket clients 49 | 50 | ```lua 51 | TriggerEvent("WebSocketServer:broadcast", "This message will be broadcasted to all connected webSocket clients."); 52 | ``` 53 | 54 | ### Send a message to a specific WebSocket client 55 | 56 | ```lua 57 | TriggerEvent("WebSocketServer:send", "This message will be sent to a specific webSocket client.", someValidAndConnectedRemoteEndpoint); 58 | ``` 59 | 60 | ## Built With 61 | 62 | * [deniszykov/WebSocketListener](https://github.com/deniszykov/WebSocketListener) 63 | 64 | ## License 65 | 66 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 67 | -------------------------------------------------------------------------------- /WebSocketEventListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using vtortola.WebSockets; 6 | using vtortola.WebSockets.Rfc6455; 7 | 8 | namespace WebSocketServer 9 | { 10 | public delegate void WebSocketEventListenerOnConnect(WebSocket webSocket); 11 | public delegate void WebSocketEventListenerOnDisconnect(WebSocket webSocket); 12 | public delegate void WebSocketEventListenerOnMessage(WebSocket webSocket, String message); 13 | public delegate void WebSocketEventListenerOnError(WebSocket webSocket, Exception error); 14 | 15 | public class WebSocketEventListener : IDisposable 16 | { 17 | public event WebSocketEventListenerOnConnect OnConnect; 18 | public event WebSocketEventListenerOnDisconnect OnDisconnect; 19 | public event WebSocketEventListenerOnMessage OnMessage; 20 | public event WebSocketEventListenerOnError OnError; 21 | 22 | readonly WebSocketListener _listener; 23 | 24 | public WebSocketEventListener(IPEndPoint endpoint) : this(endpoint, new WebSocketListenerOptions()) 25 | { 26 | } 27 | 28 | public WebSocketEventListener(IPEndPoint endpoint, WebSocketListenerOptions options) 29 | { 30 | options.Standards.RegisterRfc6455(); 31 | _listener = new WebSocketListener(endpoint, options); 32 | } 33 | 34 | public void Start() 35 | { 36 | _listener.StartAsync().Wait(); 37 | } 38 | 39 | public void Stop() 40 | { 41 | _listener.StopAsync().Wait(); 42 | } 43 | 44 | public async Task ListenAsync() 45 | { 46 | while (_listener.IsStarted) 47 | { 48 | try 49 | { 50 | var websocket = await _listener.AcceptWebSocketAsync(CancellationToken.None).ConfigureAwait(false); 51 | if (websocket != null) 52 | Task.Run(() => HandleWebSocketAsync(websocket)); 53 | } 54 | catch(Exception ex) 55 | { 56 | if (OnError != null) 57 | OnError.Invoke(null, ex); 58 | } 59 | } 60 | } 61 | 62 | private async Task HandleWebSocketAsync(WebSocket websocket) 63 | { 64 | try 65 | { 66 | if (OnConnect != null) 67 | OnConnect.Invoke(websocket); 68 | 69 | while (websocket.IsConnected) 70 | { 71 | var message = await websocket.ReadStringAsync(CancellationToken.None).ConfigureAwait(false); 72 | if (message != null && OnMessage != null) 73 | OnMessage.Invoke(websocket, message); 74 | } 75 | 76 | if (OnDisconnect != null) 77 | OnDisconnect.Invoke(websocket); 78 | } 79 | catch (Exception ex) 80 | { 81 | if (OnError != null) 82 | OnError.Invoke(websocket, ex); 83 | } 84 | finally 85 | { 86 | websocket.Dispose(); 87 | } 88 | } 89 | 90 | public void Dispose() 91 | { 92 | _listener.Dispose(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /WebSocketServer.cs: -------------------------------------------------------------------------------- 1 | using CitizenFX.Core; 2 | using CitizenFX.Core.Native; 3 | using System; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Collections.Generic; 8 | using vtortola.WebSockets; 9 | 10 | namespace WebSocketServer 11 | { 12 | class WebSocketServer : BaseScript 13 | { 14 | private enum LogLevels { Debug, Info, Warn, Error }; 15 | private LogLevels logLevel; 16 | 17 | private string authorization; 18 | private List authorizationHandlers; 19 | private List webSockets; 20 | 21 | private async Task CheckHttpHeaders(WebSocketHttpRequest request, WebSocketHttpResponse response) 22 | { 23 | await Task.Run(() => 24 | { 25 | if (authorization.Length > 0) 26 | { 27 | var authHeader = request.Headers["Authorization"]; 28 | if (authHeader == null || authHeader != authorization) 29 | { 30 | response.Status = HttpStatusCode.Unauthorized; 31 | Log("Rejected Authorization header: " + authHeader, LogLevels.Debug); 32 | } 33 | } 34 | 35 | for (int i = authorizationHandlers.Count - 1; i >= 0; i--) 36 | { 37 | try 38 | { 39 | if (!(bool) authorizationHandlers[i](request.RemoteEndPoint.ToString(), request.Headers.ToString())) 40 | { 41 | response.Status = HttpStatusCode.Unauthorized; 42 | } 43 | } 44 | catch (Exception e) 45 | { 46 | Log("An error occurred while calling an authentication handler: " + e.Message, LogLevels.Debug); 47 | authorizationHandlers.RemoveAt(i); 48 | } 49 | } 50 | }); 51 | 52 | return true; 53 | } 54 | 55 | public WebSocketServer() 56 | { 57 | logLevel = Function.Call(Hash.GET_CONVAR, "websocket_debug", "false") == "true" ? LogLevels.Debug : LogLevels.Info; 58 | authorization = Function.Call(Hash.GET_CONVAR, "websocket_authorization", ""); 59 | authorizationHandlers = new List(); 60 | 61 | IPAddress listeningHost = IPAddress.Loopback; 62 | IPAddress.TryParse(Function.Call(Hash.GET_CONVAR, "websocket_host", "127.0.0.1"), out listeningHost); 63 | int listeningPort = Function.Call(Hash.GET_CONVAR_INT, "websocket_port", 80); 64 | var endpoint = new IPEndPoint(listeningHost, listeningPort); 65 | 66 | var server = new WebSocketEventListener (endpoint, new WebSocketListenerOptions () { 67 | SubProtocols = new String[]{ "text" }, 68 | HttpAuthenticationHandler = CheckHttpHeaders 69 | }); 70 | server.OnConnect += (ws) => 71 | { 72 | Log ("Connection from " + ws.RemoteEndpoint.ToString (), LogLevels.Debug); 73 | 74 | TriggerEvent("WebSocketServer:onConnect", ws.RemoteEndpoint.ToString ()); 75 | 76 | lock (webSockets) 77 | { 78 | webSockets.Add(ws); 79 | } 80 | }; 81 | server.OnDisconnect += (ws) => 82 | { 83 | Log ("Disconnection from " + ws.RemoteEndpoint.ToString (), LogLevels.Debug); 84 | 85 | TriggerEvent("WebSocketServer:onDisconnect", ws.RemoteEndpoint.ToString ()); 86 | 87 | lock (webSockets) 88 | { 89 | webSockets.Remove(ws); 90 | } 91 | }; 92 | server.OnError += (ws, ex) => Log("Error: " + ex.Message, LogLevels.Debug); 93 | server.OnMessage += (ws, msg) => 94 | { 95 | Log("Received message: " + msg, LogLevels.Debug); 96 | 97 | TriggerEvent("WebSocketServer:onMessage", msg, ws.RemoteEndpoint.ToString ()); 98 | }; 99 | 100 | EventHandlers["onResourceStart"] += new Action((dynamic resourceName) => 101 | { 102 | if ((string) resourceName == "WebSocketServer") 103 | { 104 | try 105 | { 106 | server.Start(); 107 | Tick += server.ListenAsync; 108 | 109 | Log("Started at " + endpoint.ToString()); 110 | } 111 | catch (Exception e) 112 | { 113 | Log("Can't start server at " + endpoint.ToString() + ": " + e.Message); 114 | } 115 | } 116 | }); 117 | 118 | EventHandlers["onResourceStop"] += new Action((dynamic resourceName) => 119 | { 120 | if ((string) resourceName == "WebSocketServer") 121 | { 122 | server.Stop(); 123 | server.Dispose(); 124 | } 125 | }); 126 | 127 | EventHandlers["WebSocketServer:addAuthHandler"] += new Action((dynamic handler) => 128 | { 129 | authorizationHandlers.Add(handler); 130 | }); 131 | 132 | webSockets = new List(); 133 | EventHandlers["WebSocketServer:broadcast"] += new Action((dynamic message) => 134 | { 135 | lock (webSockets) 136 | { 137 | foreach (var webSocket in webSockets) 138 | { 139 | if (webSocket.IsConnected) 140 | { 141 | Task.Run(async () => 142 | { 143 | try 144 | { 145 | await webSocket.WriteStringAsync((string) message, CancellationToken.None); 146 | } 147 | catch (Exception e) 148 | { 149 | Log("An error occurred while sending a message to " + webSocket.RemoteEndpoint.ToString() + ": " + e.Message, LogLevels.Debug); 150 | } 151 | }); 152 | } 153 | else 154 | { 155 | webSockets.Remove(webSocket); 156 | } 157 | } 158 | } 159 | }); 160 | 161 | EventHandlers["WebSocketServer:send"] += new Action((dynamic message, dynamic client) => 162 | { 163 | lock (webSockets) 164 | { 165 | foreach (var webSocket in webSockets) 166 | { 167 | if (webSocket.IsConnected) 168 | { 169 | if (webSocket.RemoteEndpoint.ToString () == client) 170 | { 171 | Task.Run(async () => 172 | { 173 | try 174 | { 175 | await webSocket.WriteStringAsync((string) message, CancellationToken.None); 176 | } 177 | catch (Exception e) 178 | { 179 | Log("An error occurred while sending a message to " + webSocket.RemoteEndpoint.ToString() + ": " + e.Message, LogLevels.Debug); 180 | } 181 | }); 182 | } 183 | } 184 | else 185 | { 186 | webSockets.Remove(webSocket); 187 | } 188 | } 189 | } 190 | }); 191 | 192 | EventHandlers["WebSocketServer:removeEndpoint"] += new Action((dynamic client) => 193 | { 194 | lock (webSockets) 195 | { 196 | foreach (var webSocket in webSockets) 197 | { 198 | if (webSocket.IsConnected && webSocket.RemoteEndpoint.ToString() == client) 199 | { 200 | webSockets.Remove(webSocket); 201 | } 202 | } 203 | } 204 | }); 205 | } 206 | 207 | private void Log(string message, LogLevels level = LogLevels.Info) 208 | { 209 | if (logLevel > level) 210 | { 211 | return; 212 | } 213 | 214 | Debug.WriteLine("[WebSocket Server] " + message); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /WebSocketServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {A1D77B7F-3CFB-406F-9F54-DE77E6995C5D} 9 | Library 10 | WebSocketServer 11 | WebSocketServer.net 12 | v4.5.2 13 | 0.1.0 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug 21 | DEBUG; 22 | prompt 23 | 4 24 | true 25 | AnyCPU 26 | 27 | 28 | full 29 | true 30 | bin\Release 31 | prompt 32 | 4 33 | true 34 | x86 35 | 36 | 37 | 38 | ..\..\..\..\FX\alpine\opt\cfx-server\citizen\clr2\lib\mono\4.5\CitizenFX.Core.dll 39 | 40 | 41 | packages\deniszykov.WebSocketListener.4.2.2\lib\net45\deniszykov.WebSocketListener.dll 42 | 43 | 44 | 45 | bin\Debug\Microsoft.CSharp.dll 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /WebSocketServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketServer", "WebSocketServer.csproj", "{A1D77B7F-3CFB-406F-9F54-DE77E6995C5D}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A1D77B7F-3CFB-406F-9F54-DE77E6995C5D}.Debug|x86.ActiveCfg = Debug|x86 15 | {A1D77B7F-3CFB-406F-9F54-DE77E6995C5D}.Debug|x86.Build.0 = Debug|x86 16 | {A1D77B7F-3CFB-406F-9F54-DE77E6995C5D}.Release|x86.ActiveCfg = Release|x86 17 | {A1D77B7F-3CFB-406F-9F54-DE77E6995C5D}.Release|x86.Build.0 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------