2

I've been working on a hello-world sort of WebSocket app to see if I can get it working in VS 2010. The client side code is very straight forward, and it works great in Safari because Safari still runs on the WebSocket 76 protocol.

Google Chrome on the other hand runs the newer Sec-WebSocket-Version code which is more complicated. I "borrowed" some code as a base from a code-plex project to get it working for the older 76 and 75 protocols: http://wsaspnet.codeplex.com/SourceControl/changeset/view/58121#1561340

I then proceeded to upgrade the code I found there until it could perform the new handshake (in the code below). The part I have finally hit a wall at and cannot seem to find any help on is communication to a Sec-WebSocket-Version 8+ socket. It used to be you could just append a 0x00 to the beginning, and a 0xFF to the end and tada! it worked! but now apparently there is more to be done to fix a security problem. I can tell that the socket is closing when I send a message by debugging the code, and I read in several places that chrome likes to close a socket when it receives a message it doesn't understand.

So then I found this post: How can I send and receive WebSocket messages on the server side? where the guy had the same issue as me, but I can't seem to translate the pseudo-code that his answerer posted into C#. Can anyone help me out?

The client code:

<div id="ComLog" style="height:600px;">


</div>
<input type="button" onclick="javascript:connectToServer();" value='connect' />

<script type="text/javascript">
    var sock;
    function connectToServer() {
        try {
            sock = new WebSocket("ws://localhost:8181/websock");
            //sock = new WebSocket("ws://192.168.0.100:8181/websock");

            //sock = new WebSocket("ws://websockets.org:8787");

            sock.onopen = sockOpen;
            sock.onerror = sockError;
            sock.onclose = sockClosed;
            sock.onmessage = sockMessage;
        } catch (e) {
            log("error:" + e);
        }
    }

    function sockOpen() {
        log("connected");
    }

    function sockError(error, p2) {
        log("socket error!");
    }

    function sockClosed() {
        log("socket closed");
    }

    function sockMessage(event) {
        log("<b>Server:</b> " + event.data);
    }

    function log(msg) {
        var txtLog = document.getElementById("ComLog");
        txtLog.innerHTML += msg + "</br>";
    }
</script>

The Server code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.IO;
using System.Web;
using System.Collections.Specialized;
using System.Text.RegularExpressions;
using System.Threading;
using System.Security.Cryptography;

namespace WebSocks
{
    public class WebSockServer
    {

        /// <summary>
        /// Port number to listen on
        /// </summary>
        private const int PortNumber = 8181;

        /// <summary>
        /// Socket which awaits connections
        /// </summary>
        private static Socket ListenerSocket;

        /// <summary>
        /// Thread in which we await for incomming connections.
        /// </summary>
        static System.Threading.Thread _serverThread;


        static WebSockServer() { }

        /// <summary>
        /// Starts thread with listening socket.
        /// </summary>
        public static void Start()
        {
            System.Threading.ThreadStart ts = new System.Threading.ThreadStart(Listen);
            _serverThread = new System.Threading.Thread(ts);
            _serverThread.Start();
        }

        /// <summary>
        /// Stops listening for connections.
        /// </summary>
        public static void End()
        {
            _serverThread.Abort();
            ListenerSocket.Dispose();
        }

        public static void Listen()
        {
            //Start listening
            ListenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            EndPoint ep = new IPEndPoint(IPAddress.Parse("0.0.0.0"), PortNumber);
            ListenerSocket.Bind(ep);
            ListenerSocket.Listen(5);

            while (true)
            {
                //New client
                using (Socket client = ListenerSocket.Accept())
                {
                    //Receiving clientHandshake
                    string clientHandshake = String.Empty;
                    byte[] buffer = null;
                    int readBytes = 0;
                    do
                    {
                        buffer = new byte[client.Available];
                        readBytes = client.Receive(buffer);
                        clientHandshake += Encoding.UTF8.GetString(buffer);
                    }
                    while (client.Available > 0);

                    //Last eight bytes are body of requets (we should include it in response)
                    byte[] secKey3 = buffer.Skip(readBytes - 8).Take(8).ToArray();

                    //Variables we can extract from clientHandshake
                    string clientOrigin = String.Empty;
                    string secKey1 = String.Empty;
                    string secKey2 = String.Empty;
                    string WebSocketVersion = String.Empty;
                    int WSV = 0;
                    string WebSocketKey = String.Empty;

                    //Extracting values from headers (key:value)
                    string[] clientHandshakeLines = Regex.Split(clientHandshake, Environment.NewLine);
                    foreach (string hline in clientHandshakeLines)
                    {
                        int valueStartIndex = hline.IndexOf(':') + 2;
                        if (valueStartIndex > 0)
                        {
                            if (hline.StartsWith("Origin"))
                            {
                                clientOrigin = hline.Substring(valueStartIndex, hline.Length - valueStartIndex);
                            }
                            else if (hline.StartsWith("Sec-WebSocket-Key2"))
                            {
                                secKey2 = hline.Substring(valueStartIndex, hline.Length - valueStartIndex);
                            }
                            else if (hline.StartsWith("Sec-WebSocket-Key1"))
                            {
                                secKey1 = hline.Substring(valueStartIndex, hline.Length - valueStartIndex);
                            }

                            if (hline.StartsWith("Sec-WebSocket-Version"))
                            {
                                WebSocketVersion = hline.Replace("Sec-WebSocket-Version: ", "");
                                WSV = Convert.ToInt32(WebSocketVersion);
                            }

                            if (hline.StartsWith("Sec-WebSocket-Key"))
                            {
                                WebSocketKey = hline.Replace("Sec-WebSocket-Key: ", "");
                            }
                        }
                    }




                    if (!String.IsNullOrEmpty(WebSocketVersion)) //WebSocketVersion 8 and up handshake check
                    {
                        //New WebSocketVersion number, included after Version 8
                        StringBuilder mResponse = new StringBuilder();
                        mResponse.AppendLine("HTTP/1.1 101 Switching Protocols");
                        mResponse.AppendLine("Upgrade: WebSocket");
                        mResponse.AppendLine("Connection: Upgrade");
                        mResponse.AppendLine(String.Format("Sec-WebSocket-Accept: {0}", ComputeWebSocketHandshakeSecurityHash09(WebSocketKey)) + Environment.NewLine);

                        byte[] HSText = Encoding.UTF8.GetBytes(mResponse.ToString());

                        client.Send(HSText, 0, HSText.Length, 0);
                    }
                    else
                    {
                        //This part is common for all websockets editions (v. 75 & v.76)
                        client.Send(Encoding.UTF8.GetBytes("HTTP/1.1 101 Web Socket Protocol Handshake" + Environment.NewLine));
                        client.Send(Encoding.UTF8.GetBytes("Upgrade: WebSocket" + Environment.NewLine));
                        client.Send(Encoding.UTF8.GetBytes("Connection: Upgrade" + Environment.NewLine));


                        if (String.IsNullOrEmpty(secKey1) && String.IsNullOrEmpty(secKey2))  //75 or less handshake check
                        {                 
                            client.Send(Encoding.UTF8.GetBytes(String.Format("WebSocket-Origin: {0}", clientOrigin) + Environment.NewLine));
                            client.Send(Encoding.UTF8.GetBytes("WebSocket-Location: ws://localhost:8181/websock" + Environment.NewLine));
                            client.Send(Encoding.UTF8.GetBytes(Environment.NewLine));
                        }
                        else //76 handshake check
                        {
                            //Keys present, this means 76 version is used. Writing Sec-* headers 
                            client.Send(Encoding.UTF8.GetBytes(String.Format("Sec-WebSocket-Origin: {0}", clientOrigin) + Environment.NewLine));
                            client.Send(Encoding.UTF8.GetBytes("Sec-WebSocket-Location: ws://localhost:8181/websock" + Environment.NewLine));
                            client.Send(Encoding.UTF8.GetBytes(Environment.NewLine));

                            //Calculating response body
                            byte[] secret = CalculateSecurityBody(secKey1, secKey2, secKey3);
                            client.Send(secret);
                        }
                    }

                    Thread.Sleep(1000);

                    SendMessage("This message will terminate in 5 seconds...", client, WSV);

                    Thread.Sleep(1000);

                    SendMessage("4", client, WSV);

                    Thread.Sleep(1000);

                    SendMessage("3", client, WSV);

                    Thread.Sleep(1000);

                    SendMessage("2", client, WSV);

                    Thread.Sleep(1000);

                    SendMessage("1", client, WSV);

                    Thread.Sleep(1000);

                }
            }
        }

        private static void SendMessage(string Msg, Socket client, int WebSockVersion)
        {
            if (WebSockVersion >= 8)
            {
                //This is the section that doesn't work
                client.Send(Encoding.UTF8.GetBytes(Msg));

            }
            else 
            {
                client.Send(new byte[] { 0x00 });
                client.Send(Encoding.UTF8.GetBytes(Msg));
                client.Send(new byte[] { 0xFF });
            }
        }


        public static byte[] CalculateSecurityBody(string secKey1, string secKey2, byte[] secKey3)
        {
            //Remove all symbols that are not numbers
            string k1 = Regex.Replace(secKey1, "[^0-9]", String.Empty);
            string k2 = Regex.Replace(secKey2, "[^0-9]", String.Empty);

            //Convert received string to 64 bit integer.
            Int64 intK1 = Int64.Parse(k1);
            Int64 intK2 = Int64.Parse(k2);

            //Dividing on number of spaces
            int k1Spaces = secKey1.Count(c => c == ' ');
            int k2Spaces = secKey2.Count(c => c == ' ');
            int k1FinalNum = (int)(intK1 / k1Spaces);
            int k2FinalNum = (int)(intK2 / k2Spaces);

            //Getting byte parts
            byte[] b1 = BitConverter.GetBytes(k1FinalNum).Reverse().ToArray();
            byte[] b2 = BitConverter.GetBytes(k2FinalNum).Reverse().ToArray();
            //byte[] b3 = Encoding.UTF8.GetBytes(secKey3);
            byte[] b3 = secKey3;

            //Concatenating everything into 1 byte array for hashing.
            List<byte> bChallenge = new List<byte>();
            bChallenge.AddRange(b1);
            bChallenge.AddRange(b2);
            bChallenge.AddRange(b3);

            //Hash and return
            byte[] hash = MD5.Create().ComputeHash(bChallenge.ToArray());
            return hash;
        }

        public static String ComputeWebSocketHandshakeSecurityHash09(String secWebSocketKey)         
        {             
            const String MagicKEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";             
            String secWebSocketAccept = String.Empty;              
            // 1. Combine the request Sec-WebSocket-Key with magic key.             
            String ret = secWebSocketKey + MagicKEY;              
            // 2. Compute the SHA1 hash             
            SHA1 sha = new SHA1CryptoServiceProvider();             
            byte[] sha1Hash = sha.ComputeHash(Encoding.UTF8.GetBytes(ret));              
            // 3. Base64 encode the hash             
            secWebSocketAccept = Convert.ToBase64String(sha1Hash);              
            return secWebSocketAccept;         
        } 
    }
}

The apparent untranslated "answer" to all my problems:

bytesFormatted[0] = 129

indexStartRawData = -1 // don't matter what value is here; it will be set now:

if bytesRaw.length <= 125
    bytesFormatted[1] = bytesRaw.length

    indexStartRawData = 2

else if bytesRaw.length >= 126 and bytesRaw.length <= 65535
    bytesFormatted[1] = 126
    bytesFormatted[2] = ( bytesRaw.length >> 8 ) AND 255
    bytesFormatted[3] = ( bytesRaw.length      ) AND 255

    indexStartRawData = 4

else
    bytesFormatted[1] = 127
    bytesFormatted[2] = ( bytesRaw.length >> 56 ) AND 255
    bytesFormatted[3] = ( bytesRaw.length >> 48 ) AND 255
    bytesFormatted[4] = ( bytesRaw.length >> 40 ) AND 255
    bytesFormatted[5] = ( bytesRaw.length >> 32 ) AND 255
    bytesFormatted[6] = ( bytesRaw.length >> 24 ) AND 255
    bytesFormatted[7] = ( bytesRaw.length >> 16 ) AND 255
    bytesFormatted[8] = ( bytesRaw.length >>  8 ) AND 255
    bytesFormatted[9] = ( bytesRaw.length       ) AND 255

    indexStartRawData = 10

// put raw data at the correct index
bytesFormatted.put(bytesRaw, indexStartRawData)


// now send bytesFormatted (e.g. write it to the socket stream)

~~~~~~~~~~~Edit~~~~~~~~~~~~

Current version of send function:

private static void SendMessage(string Msg, Socket client, int WebSockVersion)
        {
            if (WebSockVersion >= 8)
            {
                bool IsFinal = true;
                int OpCode = 1;
                int? Mask = null;
                int PayloadLength = Encoding.UTF8.GetBytes(Msg).Length;
                byte[] buffer = Encoding.UTF8.GetBytes(Msg);

                int offset = 0;
                buffer[offset++] = (byte)((IsFinal ? 128 : 0) | ((int)OpCode & 15));
                if (PayloadLength > ushort.MaxValue)
                { // write as a 64-bit length
                    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 127);
                    buffer[offset++] = 0;
                    buffer[offset++] = 0;
                    buffer[offset++] = 0;
                    buffer[offset++] = 0;
                    buffer[offset++] = (byte)(PayloadLength >> 24);
                    buffer[offset++] = (byte)(PayloadLength >> 16);
                    buffer[offset++] = (byte)(PayloadLength >> 8);
                    buffer[offset++] = (byte)(PayloadLength);
                }
                else if (PayloadLength > 125)
                { // write as a 16-bit length
                    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 126);
                    buffer[offset++] = (byte)(PayloadLength >> 8);
                    buffer[offset++] = (byte)(PayloadLength);
                }
                else
                { // write in the header
                    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | PayloadLength);
                }
                if (Mask.HasValue)
                {
                    int mask = Mask.Value;
                    buffer[offset++] = (byte)(mask >> 24);
                    buffer[offset++] = (byte)(mask >> 16);
                    buffer[offset++] = (byte)(mask >> 8);
                    buffer[offset++] = (byte)(mask);
                }
                //stream.Write(buffer, 0, offset);

                client.Send(buffer, 0, PayloadLength, SocketFlags.None);

            }
            else 
            {
                client.Send(new byte[] { 0x00 });
                client.Send(Encoding.UTF8.GetBytes(Msg));
                client.Send(new byte[] { 0xFF });
            }
        }
Community
  • 1
  • 1
Jrud
  • 1,004
  • 9
  • 25

1 Answers1

2

That looks about right to be honest; it is pretty similar to my working code (which only handles 32-bit lengths, hence the lack of >> 32 and above):

int offset = 0;
buffer[offset++] = (byte)((IsFinal ? 128 : 0) | ((int)OpCode & 15));
if (PayloadLength > ushort.MaxValue)
{ // write as a 64-bit length
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 127);
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = (byte)(PayloadLength >> 24);
    buffer[offset++] = (byte)(PayloadLength >> 16);
    buffer[offset++] = (byte)(PayloadLength >> 8);
    buffer[offset++] = (byte)(PayloadLength);
}
else if (PayloadLength > 125)
{ // write as a 16-bit length
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 126);
    buffer[offset++] = (byte)(PayloadLength >> 8);
    buffer[offset++] = (byte)(PayloadLength);
}
else
{ // write in the header
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | PayloadLength);
}
if (Mask.HasValue)
{
    int mask = Mask.Value;
    buffer[offset++] = (byte)(mask >> 24);
    buffer[offset++] = (byte)(mask >> 16);
    buffer[offset++] = (byte)(mask >> 8);
    buffer[offset++] = (byte)(mask);
}
stream.Write(buffer, 0, offset);

after that, it just writes the payload (if any). Since you are writing from the server to the client, you don't actually need to worry about the outbound mask, so that would always be just 127. However, if you are accepting incoming messages, then you very much do need to worry about masks.

Notes: .NET 4.5 on Windows 8 has WebSocket support built into HttpListener and ASP.NET; until then, another approach might be to look at SuperWebSocket.


Re your edit: you have chomped your own data! Try instead:

bool IsFinal = true;
int OpCode = 1;
int? Mask = null;
byte[] payload = Encoding.UTF8.GetBytes(Msg);
int PayloadLength = payload.Length;
byte[] buffer = new byte[64]; // for working out the header

int offset = 0;
buffer[offset++] = (byte)((IsFinal ? 128 : 0) | ((int)OpCode & 15));
if (PayloadLength > ushort.MaxValue)
{ // write as a 64-bit length
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 127);
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = (byte)(PayloadLength >> 24);
    buffer[offset++] = (byte)(PayloadLength >> 16);
    buffer[offset++] = (byte)(PayloadLength >> 8);
    buffer[offset++] = (byte)(PayloadLength);
}
else if (PayloadLength > 125)
{ // write as a 16-bit length
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 126);
    buffer[offset++] = (byte)(PayloadLength >> 8);
    buffer[offset++] = (byte)(PayloadLength);
}
else
{ // write in the header
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | PayloadLength);
}
if (Mask.HasValue)
{
    int mask = Mask.Value;
    buffer[offset++] = (byte)(mask >> 24);
    buffer[offset++] = (byte)(mask >> 16);
    buffer[offset++] = (byte)(mask >> 8);
    buffer[offset++] = (byte)(mask);
}
// you might want to manually combine these into 1 packet
client.Send(buffer, 0, offset, SocketFlags.None);
client.Send(payload, 0, payload.Length, SocketFlags.None);
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • I should probably note that the `(byte)(expression)` conversion explicitly only takes the least significant byte, so is identical to `(byte)(expression & 255)` – Marc Gravell Mar 08 '12 at 20:50
  • 1
    I really don't understand this stuff. I know moving data from the server to client needs to be serialized, encrypted, sent, received, unencrypted, and unserialized but why the hell does it have to be so freaking complicated with hex codes and crap? My problem is that I don't understand the REALLY low level of protocol, and I don't want to, I just want it to work, and move on with my life without having to worry about it breaking or being crazy inefficient later because I picked the wrong 3rd party DLL to use. – Jrud Mar 08 '12 at 21:01
  • What do the variables: PayloadLength, buffer, IsFinal, OpCode, and Mask do, and what do they need to be set to before your code runs? – Jrud Mar 08 '12 at 21:02
  • `PayloadLength` is the size of the message, in bytes, that I'm about to send; so if I'm sending a string message, it is the length of the UTF-8 encoded hunk. `buffer` is a `byte[]` that I'm using a scratch-buffer; `IsFinal` should probably be `true` if you don't know what it is... it is possible to split a single message over multiple "frames" with WebSockets; this controls how that works; `OpCode` is `1` for a text message, `2` for a binary message, `8` for a close, `0` for a continuation, `9` for a ping and `10` for a pong; `Mask` is the security thing; since you are talking server-to-client – Marc Gravell Mar 08 '12 at 21:07
  • it should basically be `int? Mask = null;` for that code to work "as is", since the server never uses a mask (the client always uses a mask); that do, @Jrud ? – Marc Gravell Mar 08 '12 at 21:07
  • @Jrud the protocol is **relatively speaking** pretty simple, actually; I don't say that as a negative thing - I just mean: compared to many others, it isn't bad, and to be honest it is designed for library authors who are familiar with binary (like me!), not app authors. Hence why tools like SuperWebSocket exist, and hence why it will be included in HttpListener and ASP.NET in vNext – Marc Gravell Mar 08 '12 at 21:11
  • Nope, its still closing immediately, I updated my original post with the current version of my send function at the bottom – Jrud Mar 08 '12 at 21:43
  • Yeah, that explains why its so foreign to me, I am certainly not a library author. I do not do well with hex or binary. – Jrud Mar 08 '12 at 21:47
  • @JRud yes, you've destroyed your own data - 2 secs; I'll edit – Marc Gravell Mar 09 '12 at 06:20
  • lol chomping data sounds nom noms... Your code worked great, thank you so much. To anyone who finds this topic in the future, copying his Edit code and pasting it over the part of my SendMessage function that says "This part doesn't work" and has the broken send line will make the project work properly for WebSockets version 8+. Happy coding, this question is closed. – Jrud Mar 09 '12 at 16:47