diff --git a/Nibriboard/Client/HttpClientSettingsHandler.cs b/Nibriboard/Client/HttpClientSettingsHandler.cs deleted file mode 100644 index a191cd9..0000000 --- a/Nibriboard/Client/HttpClientSettingsHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.IO; - -using SBRL.GlidingSquirrel.Http; -using SBRL.GlidingSquirrel.Websocket; - -using Newtonsoft.Json; - -namespace Nibriboard.Client -{ - public class HttpClientSettingsHandler : WebsocketServer - { - private ClientSettings settings; - - public HttpClientSettingsHandler(ClientSettings inSettings) - { - settings = inSettings; - } - - public void HandleRequest(string uri, HttpRequest request, HttpResponse response, HttpContext context) { - StreamWriter responseData = new StreamWriter(response.Content) { AutoFlush = true }; - - string settingsJson = JsonConvert.SerializeObject(settings); - response.ContentLength = settingsJson.Length; - response.Headers.Add("content-type", "application/json"); - - responseData.Write(settingsJson); - - Log.WriteLine("[Http/ClientSettings] Sent settings"); - } - } -} - diff --git a/Nibriboard/Client/HttpEmbeddedFileHandler.cs b/Nibriboard/Client/HttpEmbeddedFileHandler.cs deleted file mode 100644 index c2619e3..0000000 --- a/Nibriboard/Client/HttpEmbeddedFileHandler.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.IO; -using MimeSharp; - -using SBRL.GlidingSquirrel.Websocket; - -using SBRL.Utilities; -using System.Collections.Generic; -using System.Reflection; - -namespace Nibriboard.Client -{ - public class HttpEmbeddedFileHandler : WebsocketsServer - { - private string filePrefix; - - private Mime mimeTypeFinder = new Mime(); - private Dictionary mimeTypeOverrides = new Dictionary() { - ["application/xhtml+xml"] = "text/html", - ["application/tei+xml"] = "image/x-icon" - }; - - private List embeddedFiles = new List(EmbeddedFiles.ResourceList); - - public HttpEmbeddedFileHandler(string inFilePrefix) - { - filePrefix = inFilePrefix; - } - - public void HandleRequest(string uri, HttpRequest request, HttpResponse response, HttpContext context) { - StreamWriter responseData = new StreamWriter(response.Content) { AutoFlush = true }; - if (request.Method != HttpMethod.Get) { - response.ResponseCode = HttpResponseCode.MethodNotAllowed; - response.ContentType = "text/plain"; - responseData.WriteLine("Error: That method isn't supported yet."); - logRequest(request, response); - return; - } - - - string expandedFilePath = getEmbeddedFileReference(request.URI); - if (!embeddedFiles.Contains(expandedFilePath)) { - expandedFilePath += "index.html"; - } - if (!embeddedFiles.Contains(expandedFilePath)) { - response.ResponseCode = HttpResponseCode.NotFound; - response.ContentType = "text/plain"; - responseData.WriteLine("Can't find {0}.", expandedFilePath); - logRequest(request, response); - return; - } - - response.ContentType = getMimeType(expandedFilePath); - response.Headers.Add("content-type", response.ContentType); - - byte[] embeddedFile = EmbeddedFiles.ReadAllBytes(expandedFilePath); - response.ContentLength = embeddedFile.Length; - - try - { - response.Content.Write(embeddedFile, 0, embeddedFile.Length); - } - catch(Exception error) - { - Log.WriteLine($"[Nibriboard/EmbeddedFileHandler] Error: {error.Message} Details:"); - Log.WriteLine(error.ToString()); - } - logRequest(request, response); - } - - protected string getEmbeddedFileReference(string uri) { - return filePrefix + "." + uri.TrimStart("/".ToCharArray()).Replace('/', '.'); - } - - protected string getMimeType(string uri) { - string mimeType = mimeTypeFinder.Lookup(uri); - foreach (KeyValuePair mimeMapping in mimeTypeOverrides) { - if (mimeType == mimeMapping.Key) - mimeType = mimeMapping.Value; - } - return mimeType; - } - - private void logRequest(HttpRequest request, HttpResponse response) { - Log.WriteLine("[Http/FileHandler] {0} {1} {2} {3}", response.ResponseCode.ResponseCode(), response.ContentType, request.Method.ToString().ToUpper(), request.URI); - } - } -} - diff --git a/Nibriboard/Client/NibriClient.cs b/Nibriboard/Client/NibriClient.cs index 315726e..790a141 100644 --- a/Nibriboard/Client/NibriClient.cs +++ b/Nibriboard/Client/NibriClient.cs @@ -5,12 +5,13 @@ using System.Text; using System.Linq; using System.Reflection; -using IotWeb.Common.Http; using Newtonsoft.Json; using SBRL.Utilities; using Nibriboard.Client.Messages; using Nibriboard.RippleSpace; +using SBRL.GlidingSquirrel.Websocket; + namespace Nibriboard.Client { /// @@ -38,7 +39,7 @@ namespace Nibriboard.Client /// /// The nibri client manager /// - private readonly NibriClientManager manager; + private readonly NibriboardApp manager; /// /// The plane that this client is currently on. /// @@ -48,7 +49,7 @@ namespace Nibriboard.Client /// The underlying websocket connection to the client. /// Please try not to call the send method on here - use the NibriClient Send() method instead. /// - private readonly WebSocket client; + private readonly WebsocketClient connection; private static readonly Dictionary messageEventTypes = new Dictionary() { ["HandshakeRequest"] = typeof(HandshakeRequestMessage), @@ -64,7 +65,11 @@ namespace Nibriboard.Client /// /// Whether this nibri client is still connected. /// - public bool Connected = true; + public bool Connected { + get { + return connection.IsClosing; + } + } /// /// Fires when this nibri client disconnects. /// @@ -117,41 +122,26 @@ namespace Nibriboard.Client #region Core Setup & Message Routing Logic - public NibriClient(NibriClientManager inManager, WebSocket inClient) + public NibriClient(NibriboardApp inManager, WebsocketClient inClient) { Log.WriteLine("[Nibriboard/WebSocket] New NibriClient connected with id #{0}.", Id); manager = inManager; - client = inClient; + connection = inClient; - client.DataReceived += async (WebSocket clientSocket, string frame) => { - try - { - await handleMessage(frame); - } - catch (Exception error) - { - await Console.Error.WriteLineAsync(error.ToString()); - throw; - } + // Attach a few events + connection.OnDisconnection += handleDisconnection; + connection.OnTextMessage += handleMessage; - //Task.Run(async () => await onMessage(frame)).Wait(); - }; - // Store whether this NibriClient is still connected or not - client.ConnectionClosed += (WebSocket socket) => { - Connected = false; - Disconnected(this); - Log.WriteLine("[NibriClient] Client #{0} disconnected.", Id); - }; } - private async Task handleMessage(string frame) + private async Task handleMessage(object sender, TextMessageEventArgs eventArgs) { // Update the last time we received a message from the client LastMessageTime = DateTime.Now; // Extract the event name from the message that the client sent. - string eventName = JsonUtilities.DeserializeProperty(frame, "Event"); + string eventName = JsonUtilities.DeserializeProperty(eventArgs.Payload, "Event"); if(eventName == null) { Log.WriteLine("[NibriClient#{0}] Received message that didn't have an event.", Id); @@ -170,7 +160,7 @@ namespace Nibriboard.Client Type jsonNet = typeof(JsonConvert); MethodInfo deserialiserInfo = jsonNet.GetMethods().First(method => method.Name == "DeserializeObject" && method.IsGenericMethod); MethodInfo genericInfo = deserialiserInfo.MakeGenericMethod(messageType); - var decodedMessage = genericInfo.Invoke(null, new object[] { frame }); + var decodedMessage = genericInfo.Invoke(null, new object[] { eventArgs.Payload }); string handlerMethodName = "handle" + decodedMessage.GetType().Name; Type clientType = this.GetType(); @@ -180,11 +170,19 @@ namespace Nibriboard.Client catch(Exception error) { Log.WriteLine("[NibriClient#{0}] Error decoding and / or handling message.", Id); - Log.WriteLine("[NibriClient#{0}] Raw frame content: {1}", Id, frame); + Log.WriteLine("[NibriClient#{0}] Raw frame content: {1}", Id, eventArgs.Payload); Log.WriteLine("[NibriClient#{0}] Exception details: {1}", Id, error); } } + private Task handleDisconnection(object sender, ClientDisconnectedEventArgs eventArgs) + { + Disconnected?.Invoke(this); + Log.WriteLine("[NibriClient] Client #{0} disconnected.", Id); + + return Task.CompletedTask; + } + #endregion @@ -222,7 +220,7 @@ namespace Nibriboard.Client Log.WriteLine("[NibriClient/#{0}] Sending message with length {1}.", Id, message.Length); - client.Send(message); + connection.Send(message); return true; } @@ -239,7 +237,7 @@ namespace Nibriboard.Client /// /// Closes the connection to the client gracefully. /// - public void CloseConnection(Message lastMessage) + public async Task CloseConnection(Message lastMessage) { if (!Connected) return; @@ -247,7 +245,7 @@ namespace Nibriboard.Client // Tell the client that we're shutting down Send(lastMessage); - client.Close(); + await connection.Close(WebsocketCloseReason.Normal); } /// @@ -514,7 +512,7 @@ namespace Nibriboard.Client protected ClientStatesMessage GenerateClientStateUpdate() { ClientStatesMessage result = new ClientStatesMessage(); - foreach (NibriClient otherClient in manager.Clients) + foreach (NibriClient otherClient in manager.NibriClients) { // Don't include ourselves in the update message! if (otherClient == this) diff --git a/Nibriboard/Client/NibriClientManager.cs b/Nibriboard/Client/NibriClientManager.cs deleted file mode 100644 index 7158760..0000000 --- a/Nibriboard/Client/NibriClientManager.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using IotWeb.Common.Http; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Diagnostics; -using Nibriboard.Client.Messages; -using System.Threading; -using Nibriboard.RippleSpace; - -namespace Nibriboard.Client -{ - /// - /// Manages a group of s. - /// - public class NibriClientManager : IWebSocketRequestHandler - { - /// - /// The ripple space manager that this client manager is connected to. - /// - public RippleSpaceManager SpaceManager { get; private set; } - - private ClientSettings clientSettings; - public List Clients = new List(); - - public LineIncubator LineIncubator = new LineIncubator(); - - /// - /// The cancellation token that's used by the main server to tell us when we should shut down. - /// - protected CancellationToken canceller; - - /// - /// The interval at which heatbeats should be sent to the client. - /// - public readonly int HeatbeatInterval = 5000; - - /// - /// The number of clients currently connected to this Nibriboard. - /// - public int ClientCount { - get { - return Clients.Count; - } - } - - public NibriClientManager(ClientSettings inClientSettings, RippleSpaceManager inSpaceManager, CancellationToken inCancellationToken) - { - clientSettings = inClientSettings; - canceller = inCancellationToken; - - SpaceManager = inSpaceManager; - } - - /// - /// Whether we will accept a given new WebSocket connection or not. - /// - /// The uri the user connected to. - /// The protocol the user is connecting with. - /// Whether we want to accept the WebSocket connection attempt or not. - public bool WillAcceptRequest(string uri, string protocol) - { - //Log.WriteLine("[Nibriboard/Websocket] Accepting new {0} connection.", protocol); - return clientSettings.WebsocketProtocol == protocol; - } - /// - /// Handles WebSocket clients when they first connect, wrapping them in - /// a instance and adding them to - /// the client list. - /// - /// New socket. - public void Connected(WebSocket newSocket) - { - NibriClient client = new NibriClient(this, newSocket); - client.Disconnected += handleDisconnection; // Clean up when the client disconnects - - Clients.Add(client); - } - - /// - /// Sends a message to all the connected clients, except the one who's sending it. - /// - /// The client sending the message. - /// The message that is to bee sent. - public void Broadcast(NibriClient sendingClient, Message message) - { - foreach(NibriClient client in Clients) - { - // Don't send the message to the sender - if (client == sendingClient) - continue; - - client.Send(message); - } - } - /// - /// Sends a message to everyone on the same plane as the sender, except the sender themselves. - /// - /// The sending client. - /// The message to send. - public void BroadcastPlane(NibriClient sendingClient, Message message) - { - foreach(NibriClient client in Clients) - { - // Don't send the message to the sender - if(client == sendingClient) - continue; - // Only send the message to others on the same plane - if(client.CurrentPlane != sendingClient.CurrentPlane) - continue; - - client.Send(message); - } - } - - /// - /// Sends a message to everyone on a specified plane. - /// - /// The plane to send the message to. - /// The message to send. - public void ReflectPlane(Plane plane, Message message) - { - foreach(NibriClient client in Clients) - { - if(client.CurrentPlane != plane) - continue; - client.Send(message); - } - } - - /// - /// Periodically tidies up the client list, disconnecting old clients. - /// - private async Task ClientMaintenanceMonkey() - { - while (true) { - // Exit if we've been asked to shut down - if (canceller.IsCancellationRequested) { - close(); - return; - } - - // Disconnect unresponsive clients. - foreach (NibriClient client in Clients) { - // If we haven't heard from this client in a little while, send a heartbeat message - if(client.MillisecondsSinceLastMessage > HeatbeatInterval) - client.SendHeartbeat(); - - // If the client hasn't sent us a message in a while (even though we sent - // them a heartbeat to check on them on the last loop), disconnect them - if (client.MillisecondsSinceLastMessage > HeatbeatInterval * 2) - client.CloseConnection(new IdleDisconnectMessage()); - } - - await Task.Delay(HeatbeatInterval); - } - } - - /// - /// Cleans up this NibriClient manager ready for shutdown. - /// - private void close() - { - // Close the connection to all the remaining nibri clients, telling them that the server is about to shut down - foreach (NibriClient client in Clients) - client.CloseConnection(new ShutdownMessage()); - } - - /// - /// Clean up after a client disconnects from the server. - /// - /// The client that has disconnected. - private void handleDisconnection(NibriClient disconnectedClient) - { - Clients.Remove(disconnectedClient); - } - } -} diff --git a/Nibriboard/Nibriboard.csproj b/Nibriboard/Nibriboard.csproj index 17b96ba..3adc467 100644 --- a/Nibriboard/Nibriboard.csproj +++ b/Nibriboard/Nibriboard.csproj @@ -69,11 +69,8 @@ - - - @@ -109,6 +106,7 @@ + diff --git a/Nibriboard/NibriboardApp.cs b/Nibriboard/NibriboardApp.cs new file mode 100644 index 0000000..fe89c8d --- /dev/null +++ b/Nibriboard/NibriboardApp.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Nibriboard.Client; +using Nibriboard.Client.Messages; +using Nibriboard.RippleSpace; +using SBRL.GlidingSquirrel.Http; +using SBRL.GlidingSquirrel.Websocket; +using SBRL.Utilities; + +namespace Nibriboard +{ + public class NibriboardAppStartInfo + { + public string FilePrefix { get; set; } + + public ClientSettings ClientSettings { get; set; } + public RippleSpaceManager SpaceManager { get; set; } + } + + public class NibriboardApp : WebsocketServer + { + private string filePrefix; + private List embeddedFiles = new List(EmbeddedFiles.ResourceList); + + /// + /// The ripple space manager that this client manager is connected to. + /// + public RippleSpaceManager SpaceManager { get; private set; } + + public LineIncubator LineIncubator = new LineIncubator(); + + private ClientSettings clientSettings; + + public List NibriClients = new List(); + + /// + /// The number of clients currently connected to this Nibriboard. + /// + public int ClientCount { + get { + return Clients.Count; + } + } + + public NibriboardApp(NibriboardAppStartInfo startInfo, IPAddress inBindAddress, int inPort) : base(inBindAddress, inPort) + { + clientSettings = startInfo.ClientSettings; + SpaceManager = startInfo.SpaceManager; + + filePrefix = startInfo.FilePrefix; + MimeTypeOverrides.Add(".ico", "image/x-icon"); + } + + + public override Task HandleClientConnected(object sender, ClientConnectedEventArgs eventArgs) + { + NibriClient client = new NibriClient(this, eventArgs.ConnectingClient); + + client.Disconnected += (NibriClient disconnectedClient) => NibriClients.Remove(disconnectedClient); + NibriClients.Add(client); + + return Task.CompletedTask; + } + + public override Task HandleClientDisconnected(object sender, ClientDisconnectedEventArgs eventArgs) + { + return Task.CompletedTask; + } + + public override async Task HandleHttpRequest(HttpRequest request, HttpResponse response) + { + if(request.Method != HttpMethod.GET) + { + response.ResponseCode = HttpResponseCode.MethodNotAllowed; + response.ContentType = "text/plain"; + await response.SetBody("Error: That method isn't supported yet."); + logRequest(request, response); + return; + } + + if(request.Url == "/Settings.json") + { + + string settingsJson = JsonConvert.SerializeObject(clientSettings); + response.ContentLength = settingsJson.Length; + response.ContentType = "application/json"; + await response.SetBody(settingsJson); + + Log.WriteLine("[Http/ClientSettings] Sent settings to {0}", request.ClientAddress); + return; + } + + + string expandedFilePath = getEmbeddedFileReference(request.Url); + if(!embeddedFiles.Contains(expandedFilePath)) + { + expandedFilePath += "index.html"; + } + if(!embeddedFiles.Contains(expandedFilePath)) + { + response.ResponseCode = HttpResponseCode.NotFound; + response.ContentType = "text/plain"; + await response.SetBody($"Can't find expandedFilePath."); + logRequest(request, response); + return; + } + + response.ContentType = LookupMimeType(expandedFilePath); + + string embeddedFile = EmbeddedFiles.ReadAllText(expandedFilePath); + + try + { + await response.SetBody(embeddedFile); + } + catch(Exception error) + { + Log.WriteLine($"[Nibriboard/EmbeddedFileHandler] Error: {error.Message} Details:"); + Log.WriteLine(error.ToString()); + } + logRequest(request, response); + } + + #region Interface Methods + + /// + /// Sends a message to all the connected clients, except the one who's sending it. + /// + /// The client sending the message. + /// The message that is to bee sent. + public void Broadcast(NibriClient sendingClient, Message message) + { + foreach(NibriClient client in NibriClients) + { + // Don't send the message to the sender + if(client == sendingClient) + continue; + + client.Send(message); + } + } + /// + /// Sends a message to everyone on the same plane as the sender, except the sender themselves. + /// + /// The sending client. + /// The message to send. + public void BroadcastPlane(NibriClient sendingClient, Message message) + { + foreach(NibriClient client in NibriClients) + { + // Don't send the message to the sender + if(client == sendingClient) + continue; + // Only send the message to others on the same plane + if(client.CurrentPlane != sendingClient.CurrentPlane) + continue; + + client.Send(message); + } + } + + /// + /// Sends a message to everyone on a specified plane. + /// + /// The plane to send the message to. + /// The message to send. + public void ReflectPlane(Plane plane, Message message) + { + foreach(NibriClient client in NibriClients) + { + if(client.CurrentPlane != plane) + continue; + client.Send(message); + } + } + + #endregion + + #region Utility Methods + + protected string getEmbeddedFileReference(string uri) + { + return filePrefix + "." + uri.TrimStart("/".ToCharArray()).Replace('/', '.'); + } + + private void logRequest(HttpRequest request, HttpResponse response) + { + Log.WriteLine( + "[Http/FileHandler] {0} {1} {2} {3}", + response.ResponseCode, + response.ContentType, + request.Method, + request.Url + ); + } + + #endregion + } +} diff --git a/Nibriboard/NibriboardServer.cs b/Nibriboard/NibriboardServer.cs index f44901c..8ebed31 100644 --- a/Nibriboard/NibriboardServer.cs +++ b/Nibriboard/NibriboardServer.cs @@ -1,14 +1,15 @@ using System; using System.Threading.Tasks; using System.Threading; - - -using Nibriboard.RippleSpace; -using Nibriboard.Client; using System.Net.Sockets; using System.Net; using System.IO; +using SBRL.GlidingSquirrel.Websocket; + +using Nibriboard.RippleSpace; +using Nibriboard.Client; + namespace Nibriboard { /// @@ -18,14 +19,11 @@ namespace Nibriboard public class NibriboardServer { private TcpListener commandServer; - private WebsocketsServer httpServer; + private NibriboardApp appServer; private ClientSettings clientSettings; private RippleSpaceManager planeManager; - private readonly CancellationTokenSource clientManagerCanceller = new CancellationTokenSource(); - private NibriClientManager clientManager; - public readonly int CommandPort = 31587; public readonly int Port = 31586; @@ -49,37 +47,16 @@ namespace Nibriboard }; // HTTP Server setup - httpServer = new HttpServer(Port); - httpServer.AddHttpRequestHandler( - "/", - new HttpEmbeddedFileHandler("Nibriboard.ClientFiles") - /*new HttpResourceHandler( - Assembly.GetExecutingAssembly(), - "ClientFiles", - "index.html" - )*/ - ); - httpServer.AddHttpRequestHandler( - "/Settings.json", - new HttpClientSettingsHandler(clientSettings) - ); - - // Websocket setup - clientManager = new NibriClientManager( - clientSettings, - planeManager, - clientManagerCanceller.Token - ); - httpServer.AddWebSocketRequestHandler( - clientSettings.WebSocketPath, - - clientManager - ); + appServer = new NibriboardApp(new NibriboardAppStartInfo() { + FilePrefix = "Nibriboard.ClientFiles", + ClientSettings = clientSettings, + SpaceManager = planeManager + }, IPAddress.IPv6Any, Port); } public async Task Start() { - httpServer.Start(); + await appServer.Start(); Log.WriteLine("[NibriboardServer] Started on port {0}", Port); await planeManager.StartMaintenanceMonkey(); diff --git a/Nibriboard/lib/GlidingSquirrel b/Nibriboard/lib/GlidingSquirrel index e219ad1..e59dd72 160000 --- a/Nibriboard/lib/GlidingSquirrel +++ b/Nibriboard/lib/GlidingSquirrel @@ -1 +1 @@ -Subproject commit e219ad112d256e00563eca76f13691ebe486e3bd +Subproject commit e59dd72dd475b685ede1a05258ea0369bdcbc8c1