diff --git a/MTSC-TestClient/Program.cs b/MTSC-TestClient/Program.cs index c7f7710..133745c 100644 --- a/MTSC-TestClient/Program.cs +++ b/MTSC-TestClient/Program.cs @@ -1,5 +1,6 @@ using MTSC.Client; using MTSC.Client.Handlers; +using MTSC.Common.WebSockets.ClientModules; using MTSC.Logging; using System; @@ -9,15 +10,23 @@ namespace MTSC_TestClient { static void Main(string[] args) { - Client client = new Client(true); + Client client = new Client(); + WebsocketHandler websocketHandler = new WebsocketHandler(); + ChatModule chatModule = new ChatModule(); client .SetServerAddress("127.0.0.1") - .SetPort(555) - .AddHandler(new EncryptionHandler()) + .SetPort(80) + .AddHandler(websocketHandler.AddModule(chatModule)) + //.AddHandler(new EncryptionHandler()) //.AddHandler(new BroadcastHandler()) .AddLogger(new ConsoleLogger()) .AddLogger(new DebugConsoleLogger()) .Connect(); + while (true) + { + string message = Console.ReadLine(); + chatModule.SendMessage(websocketHandler, message); + } } } } diff --git a/MTSC/Client/Handlers/WebsocketHandler.cs b/MTSC/Client/Handlers/WebsocketHandler.cs new file mode 100644 index 0000000..23702bf --- /dev/null +++ b/MTSC/Client/Handlers/WebsocketHandler.cs @@ -0,0 +1,138 @@ +using MTSC.Common.Http; +using MTSC.Common.WebSockets.ClientModules; +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace MTSC.Client.Handlers +{ + /// + /// Handler implementing websocket protocol. + /// + public class WebsocketHandler : IHandler + { + private static string WebsocketHeaderAcceptKey = "Sec-WebSocket-Accept"; + private static string WebsocketHeaderKey = "Sec-WebSocket-Key"; + private static string WebsocketProtocolKey = "Sec-WebSocket-Protocol"; + private static string WebsocketProtocolVersionKey = "Sec-WebSocket-Version"; + private static string GlobalUniqueIdentifier = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private static SHA1 sha1Provider = SHA1.Create(); + private enum SocketState + { + Initial, + Handshaking, + Established, + Closed + } + + #region Fields + SocketState state = SocketState.Initial; + Queue messageQueue = new Queue(); + List websocketModules = new List(); + string expectedguid = string.Empty; + #endregion + #region Properties + public string WebsocketURI { get; set; } + #endregion + #region Constructors + public WebsocketHandler() + { + WebsocketURI = "/"; + } + #endregion + #region Public Methods + /// + /// Add a module to the websocket handler. + /// + /// Module to be added. + /// This handler object. + public WebsocketHandler AddModule(IWebsocketModule websocketModule) + { + websocketModules.Add(websocketModule); + return this; + } + /// + /// Add a message to the queue to be sent. + /// + /// Message to be sent. + public void QueueMessage(byte[] message) + { + messageQueue.Enqueue(message); + } + #endregion + #region Handler Implementation + void IHandler.Disconnected(Client client) + { + + } + + bool IHandler.HandleReceivedMessage(Client client, Message message) + { + if(state == SocketState.Handshaking) + { + HttpMessage response = new HttpMessage(); + response.ParseResponse(message.MessageBytes); + if(response.StatusCode == HttpMessage.StatusCodes.SwitchingProtocols && + response["Upgrade"] == "websocket" && + response[HttpMessage.GeneralHeadersEnum.Connection].ToLower() == "upgrade" && + response[WebsocketHeaderAcceptKey].Trim() == expectedguid) + { + state = SocketState.Established; + } + return true; + } + else if(state == SocketState.Established) + { + foreach(IWebsocketModule websocketModule in websocketModules) + { + if(websocketModule.HandleReceivedMessage(client, this, message.MessageBytes)) + { + break; + } + } + return true; + } + return false; + } + + bool IHandler.HandleSendMessage(Client client, ref Message message) + { + return false; + } + + bool IHandler.InitializeConnection(Client client) + { + state = SocketState.Handshaking; + string handshakeGuid = Guid.NewGuid().ToString(); + string handshakeKey = handshakeGuid+ GlobalUniqueIdentifier; + expectedguid = Convert.ToBase64String(sha1Provider.ComputeHash(Encoding.UTF8.GetBytes(handshakeKey))); + HttpMessage beginRequest = new HttpMessage(); + beginRequest.Method = HttpMessage.MethodEnum.Get; + beginRequest.RequestURI = WebsocketURI; + beginRequest[HttpMessage.RequestHeadersEnum.Host] = client.Address; + beginRequest[HttpMessage.GeneralHeadersEnum.Connection] = "Upgrade"; + beginRequest[WebsocketHeaderKey] = handshakeGuid; + beginRequest["Origin"] = client.Address; + beginRequest[WebsocketProtocolKey] = "chat"; + beginRequest[WebsocketProtocolVersionKey] = "13"; + client.QueueMessage(beginRequest.GetRequest()); + return true; + } + + bool IHandler.PreHandleReceivedMessage(Client client, ref Message message) + { + return false; + } + + void IHandler.Tick(Client client) + { + while(messageQueue.Count > 0) + { + byte[] message = messageQueue.Dequeue(); + client.QueueMessage(message); + } + } + #endregion + } +} diff --git a/MTSC/Common/Http/HttpMessage.cs b/MTSC/Common/Http/HttpMessage.cs index 424e823..2cd6a28 100644 --- a/MTSC/Common/Http/HttpMessage.cs +++ b/MTSC/Common/Http/HttpMessage.cs @@ -222,7 +222,7 @@ namespace MTSC.Common.Http public byte[] GetRequest() { StringBuilder requestString = new StringBuilder(); - requestString.Append(Method.ToString()).Append(SP).Append(RequestURI.ToString()).Append(SP).Append(HTTPVER).Append(CRLF); + requestString.Append(methods[(int)Method]).Append(SP).Append(RequestURI.ToString()).Append(SP).Append(HTTPVER).Append(CRLF); foreach(KeyValuePair header in headers) { requestString.Append(header.Key).Append(':').Append(SP).Append(header.Value).Append(CRLF); @@ -525,7 +525,7 @@ namespace MTSC.Common.Http #region Private Methods private MethodEnum GetMethod(string methodString) { - int index = Array.IndexOf(methods, methodString); + int index = Array.IndexOf(methods, methodString.ToUpper()); return (MethodEnum)index; } diff --git a/MTSC/Common/WebSockets/ClientModules/ChatModule.cs b/MTSC/Common/WebSockets/ClientModules/ChatModule.cs new file mode 100644 index 0000000..cec2a5b --- /dev/null +++ b/MTSC/Common/WebSockets/ClientModules/ChatModule.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MTSC.Client.Handlers; + +namespace MTSC.Common.WebSockets.ClientModules +{ + public class ChatModule : IWebsocketModule + { + #region Public Methods + public void SendMessage(WebsocketHandler websocketHandler, string message) + { + byte[] encodedMessage = WebsocketHelper.EncodeMessage(message); + websocketHandler.QueueMessage(encodedMessage); + } + #endregion + #region Interface Implementation + bool IWebsocketModule.HandleReceivedMessage(Client.Client client, IHandler handler, byte[] messageBytes) + { + string receivedMessage = WebsocketHelper.DecodeMessage(messageBytes); + client.Log(">" + receivedMessage); + return false; + } + #endregion + } +} diff --git a/MTSC/Common/WebSockets/ClientModules/IWebsocketModule.cs b/MTSC/Common/WebSockets/ClientModules/IWebsocketModule.cs new file mode 100644 index 0000000..a145173 --- /dev/null +++ b/MTSC/Common/WebSockets/ClientModules/IWebsocketModule.cs @@ -0,0 +1,22 @@ +using MTSC.Client.Handlers; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MTSC.Common.WebSockets.ClientModules +{ + /// + /// Interface for websocket modules. + /// + public interface IWebsocketModule + { + /// + /// Handle received message. + /// + /// Client object. + /// Handler currently processing the message. + /// Array containing the message. + /// True if no other module should handle this message. + bool HandleReceivedMessage(Client.Client client, IHandler handler, byte[] messageBytes); + } +} diff --git a/MTSC/Common/WebSockets/ServerModules/EchoModule.cs b/MTSC/Common/WebSockets/ServerModules/EchoModule.cs index 7c5b792..61bab89 100644 --- a/MTSC/Common/WebSockets/ServerModules/EchoModule.cs +++ b/MTSC/Common/WebSockets/ServerModules/EchoModule.cs @@ -12,93 +12,14 @@ namespace MTSC.Common.WebSockets.ServerModules #region Public Methods public void SendMessage(WebsocketHandler handler, ClientData client, string message) { - byte[] encodedMessage = EncodeMessage(message); + byte[] encodedMessage = WebsocketHelper.EncodeMessage(message); handler.QueueMessage(client, encodedMessage); } #endregion - #region Private Methods - private string DecodeMessage(byte[] bytes) - { - byte secondByte = bytes[1]; - int dataLength = secondByte & 127; - int indexFirstMask = 2; - if (dataLength == 126) - indexFirstMask = 4; - else if (dataLength == 127) - indexFirstMask = 10; - - IEnumerable keys = bytes.Skip(indexFirstMask).Take(4); - int indexFirstDataByte = indexFirstMask + 4; - - byte[] decoded = new byte[bytes.Length - indexFirstDataByte]; - for (int i = indexFirstDataByte, j = 0; i < bytes.Length; i++, j++) - { - decoded[j] = (byte)(bytes[i] ^ keys.ElementAt(j % 4)); - } - - return Encoding.UTF8.GetString(decoded, 0, decoded.Length); - } - private static byte[] EncodeMessage(string message) - { - byte[] response; - byte[] bytesRaw = Encoding.UTF8.GetBytes(message); - byte[] frame = new byte[10]; - - int indexStartRawData = -1; - int length = bytesRaw.Length; - - frame[0] = (byte)129; - if (length <= 125) - { - frame[1] = (byte)length; - indexStartRawData = 2; - } - else if (length >= 126 && length <= 65535) - { - frame[1] = (byte)126; - frame[2] = (byte)((length >> 8) & 255); - frame[3] = (byte)(length & 255); - indexStartRawData = 4; - } - else - { - frame[1] = (byte)127; - frame[2] = (byte)((length >> 56) & 255); - frame[3] = (byte)((length >> 48) & 255); - frame[4] = (byte)((length >> 40) & 255); - frame[5] = (byte)((length >> 32) & 255); - frame[6] = (byte)((length >> 24) & 255); - frame[7] = (byte)((length >> 16) & 255); - frame[8] = (byte)((length >> 8) & 255); - frame[9] = (byte)(length & 255); - - indexStartRawData = 10; - } - - response = new byte[indexStartRawData + length]; - - int i, reponseIdx = 0; - - //Add the frame bytes to the reponse - for (i = 0; i < indexStartRawData; i++) - { - response[reponseIdx] = frame[i]; - reponseIdx++; - } - - //Add the data bytes to the response - for (i = 0; i < length; i++) - { - response[reponseIdx] = bytesRaw[i]; - reponseIdx++; - } - return response; - } - #endregion #region Interface Implementation bool IWebsocketModule.HandleReceivedMessage(IHandler handler, ClientData client, byte[] messageBytes) { - string receivedMessage = DecodeMessage(messageBytes); + string receivedMessage = WebsocketHelper.DecodeMessage(messageBytes); SendMessage((WebsocketHandler)handler, client, receivedMessage); return false; } diff --git a/MTSC/Common/WebSockets/WebsocketHelper.cs b/MTSC/Common/WebSockets/WebsocketHelper.cs new file mode 100644 index 0000000..7b192c6 --- /dev/null +++ b/MTSC/Common/WebSockets/WebsocketHelper.cs @@ -0,0 +1,116 @@ +using MTSC.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MTSC.Common.WebSockets +{ + /// + /// Class with websocket helper functions. + /// + public class WebsocketHelper + { + /// + /// Decode text websocket message. + /// + /// Byte array containing the received message. + /// String containing the received message. + public static string DecodeMessage(byte[] bytes) + { + if(bytes[0] != 129) + { + throw new InvalidFirstByteException(); + } + int startIndex = 1; + int length = 0; + if(bytes[1] <= 125) + { + startIndex = 2; + length = bytes[1]; + } + else if(bytes[1] == 126) + { + startIndex = 4; + length = bytes[2] << 8; + length += bytes[3]; + } + else if(bytes[1] == 127) + { + startIndex = 10; + length = bytes[2] << 56; + length += bytes[3] << 48; + length += bytes[4] << 40; + length += bytes[5] << 32; + length += bytes[6] << 24; + length += bytes[7] << 16; + length += bytes[8] << 8; + length += bytes[9]; + } + byte[] message = new byte[length]; + Array.Copy(bytes, startIndex, message, 0, length); + return Encoding.UTF8.GetString(message); + } + /// + /// Encode text websocket message. + /// + /// Message to encode. + /// Byte array containing the encoded message. + public static byte[] EncodeMessage(string message) + { + byte[] response; + byte[] bytesRaw = Encoding.UTF8.GetBytes(message); + byte[] frame = new byte[10]; + + int indexStartRawData = -1; + int length = bytesRaw.Length; + + frame[0] = (byte)129; + if (length <= 125) + { + frame[1] = (byte)length; + indexStartRawData = 2; + } + else if (length >= 126 && length <= 65535) + { + frame[1] = (byte)126; + frame[2] = (byte)((length >> 8) & 255); + frame[3] = (byte)(length & 255); + indexStartRawData = 4; + } + else + { + frame[1] = (byte)127; + frame[2] = (byte)((length >> 56) & 255); + frame[3] = (byte)((length >> 48) & 255); + frame[4] = (byte)((length >> 40) & 255); + frame[5] = (byte)((length >> 32) & 255); + frame[6] = (byte)((length >> 24) & 255); + frame[7] = (byte)((length >> 16) & 255); + frame[8] = (byte)((length >> 8) & 255); + frame[9] = (byte)(length & 255); + + indexStartRawData = 10; + } + + response = new byte[indexStartRawData + length]; + + int i, reponseIdx = 0; + + //Add the frame bytes to the reponse + for (i = 0; i < indexStartRawData; i++) + { + response[reponseIdx] = frame[i]; + reponseIdx++; + } + + //Add the data bytes to the response + for (i = 0; i < length; i++) + { + response[reponseIdx] = bytesRaw[i]; + reponseIdx++; + } + return response; + } + } +} diff --git a/MTSC/Exceptions/InvalidFirstByteException.cs b/MTSC/Exceptions/InvalidFirstByteException.cs new file mode 100644 index 0000000..d61cf8f --- /dev/null +++ b/MTSC/Exceptions/InvalidFirstByteException.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; + +namespace MTSC.Exceptions +{ + /// + /// Exception in case of first invalid byte for websocket communication. + /// + public class InvalidFirstByteException : Exception + { + public InvalidFirstByteException() + { + } + + public InvalidFirstByteException(string message) : base(message) + { + } + + public InvalidFirstByteException(string message, Exception innerException) : base(message, innerException) + { + } + + protected InvalidFirstByteException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/MTSC/MTSC.csproj b/MTSC/MTSC.csproj index 161319f..de0a40e 100644 --- a/MTSC/MTSC.csproj +++ b/MTSC/MTSC.csproj @@ -15,8 +15,4 @@ AnyCPU;x64 - - - - diff --git a/MTSC/Server/Handlers/WebsocketHandler.cs b/MTSC/Server/Handlers/WebsocketHandler.cs index 0b7940e..fe03096 100644 --- a/MTSC/Server/Handlers/WebsocketHandler.cs +++ b/MTSC/Server/Handlers/WebsocketHandler.cs @@ -23,10 +23,11 @@ namespace MTSC.Server.Handlers Established, Closed } + #region Fields ConcurrentDictionary webSockets = new ConcurrentDictionary(); ConcurrentQueue> messageQueue = new ConcurrentQueue>(); List websocketModules = new List(); - + #endregion #region Public Methods /// /// Add a webSocket module onto the server. @@ -100,6 +101,7 @@ namespace MTSC.Server.Handlers break; } } + return true; } return false; }