diff --git a/exec/websocketservice-debug.js b/exec/websocketservice-debug.js
index 2822a6de860d2e9631ada15eb44d769a9816b7a9..7bbbadbde22f528ca96953db4bfee117ffa0bd7d 100644
--- a/exec/websocketservice-debug.js
+++ b/exec/websocketservice-debug.js
@@ -1,748 +1,750 @@
-// Synchronet Service for redirecting incoming WebSocket connections to the Telnet and/or RLogin server
-// Mainly to be used in conjunction with fTelnet
-
-// Command-line syntax:
-// websocketservice.js [hostname] [port]
-
-// Example configuration (in ctrl/services.ini)
-//
-// [WS]
-// Port=1123
-// Options=NO_HOST_LOOKUP
-// Command=websocketservice.js
-//
-// [WSS]
-// Port=11235
-// Options=NO_HOST_LOOKUP|TLS
-// Command=websocketservice.js
-
-//include definitions for synchronet
-load("nodedefs.js");
-load("sockdefs.js");
-load("sbbsdefs.js");
-load("ftelnethelper.js");
-load("sha1.js");
-
-// State flags
-const WEBSOCKET_NEED_PACKET_START   = 0;
-const WEBSOCKET_NEED_PAYLOAD_LENGTH = 1;
-const WEBSOCKET_NEED_MASKING_KEY	= 2;
-const WEBSOCKET_DATA                = 3;
-
-// Frame types
-const WEBSOCKET_FRAME_UNKNOWN = 0;
-const WEBSOCKET_FRAME_TEXT    = 1;
-const WEBSOCKET_FRAME_BINARY  = 2;
-
-// Global variables
-var FFrameMask = [];
-var FFrameMasked = false;
-var FFrameOpCode = 0;
-var FFramePayloadLength = 0;
-var FFramePayloadReceived = 0;
-var FFrameType = WEBSOCKET_FRAME_UNKNOWN;
-var FServerSocket = null;
-var FWebSocketDataQueue = '';
-var FWebSocketHeader = {};
-var FWebSocketState = WEBSOCKET_NEED_PACKET_START;
-var FWebSocketSubProtocol = '';
-var ipFile;
-
-// Main line
-try {
-    // Parse and respond to the WebSocket handshake request
-    if (ShakeHands()) {
-
-		if (UsingHAProxy() && FWebSocketHeader['X-Forwarded-For'] === undefined) {
-			throw new Error('BBS is using HAProxy, but no X-Forwarded-For header present.');
-		}
-
-        SendToWebSocketClient(StringToBytes("Redirecting to server...\r\n"));
-
-        // Default to localhost on the telnet port
-		var TargetHostname = GetTelnetInterface();
-		var TargetPort = GetTelnetPort();
-        
-        // If fTelnet client sent a port on the querystring, try to use that
-        var Path = ParsePathHeader();
-        if (Path.query.Port) {
-            RequestedPort = parseInt(Path.query.Port);
-
-            // Confirm the user requested either the telnet or rlogin ports (we don't want to allow them to request any arbitrary port as that would be a gaping security hole)
-            if ((RequestedPort > 0) && (RequestedPort <= 65535) && ((RequestedPort == GetTelnetPort()) || (RequestedPort == GetRLoginPort()))) {
-                TargetPort = RequestedPort;
-                log(LOG_DEBUG, "Using user-requested port " + Path.query.Port);
-                if (TargetPort == GetRLoginPort()) TargetHostname = GetRLoginInterface();
-            } else {
-                log(LOG_NOTICE, "Client requested to connect to port " + Path.query.Port + ", which was denied");
-            }
-        } else {
-            // If SysOp gave an alternate hostname/port when installing the service, use that instead
-			for(var i in argv) {
-                var port = parseInt(argv[i], 10);
-                if (argv[i].search(/\D/) > -1 || port < 0 || port > 65535) {
-                    TargetHostname = argv[i];
-                } else if (!isNaN(port)) {
-                    TargetPort = port;
-                }
-            }
-        }
-        
-		// Connect to the server
-        FServerSocket = new Socket();
-		log(LOG_DEBUG, "Connecting to " + TargetHostname + ":" + TargetPort);
-        if (FServerSocket.connect(TargetHostname, TargetPort)) {
-
-            ipFile = new File(system.temp_dir + 'sbbs-ws-' + FServerSocket.local_port + '.ip');
-            if (ipFile.open('w')) {
-                ipFile.write(client.ip_address);
-                ipFile.close();
-            }
-
-            // Variables we'll use in the loop
-            var DoYield = true;
-            var ClientData = [];
-            var ServerData = [];
-
-			if (UsingHAProxy()) {
-				var hapstr = '\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21';
-				if (client.socket.family === PF_INET) {
-					hapstr += '\x11\x00\x0C';
-				} else if (client.socket.family === PF_INET6) {
-					hapstr += '\x21\x00\x24';
-				}
-				hapstr += inet_pton(FWebSocketHeader['X-Forwarded-For']);
-				hapstr += inet_pton(FServerSocket.remote_ip_address);
-				hapstr += client.port.toString(16);
-				hapstr += TargetPort.toString(16);
-				FServerSocket.send(hapstr);
-			}
-            
-            // Loop while we're still connected on both ends
-            while ((client.socket.is_connected) && (FServerSocket.is_connected)) {
-                // Should we yield or not (default true, but disable if we had line activity)
-                DoYield = true;
-            
-                // Check if the client sent anything
-                ClientData = GetFromWebSocketClient();
-                if (ClientData === null) {
-                    // null ClientData means the client socket is no longer connected, or a close frame was received
-                    break;
-                } else if (ClientData.length > 0) {
-                    SendToServer(ClientData);
-                    DoYield = false;
-                }
-                
-                // Check if the server sent anything
-                ServerData = GetFromServer();
-                if (ServerData === null) {
-                    // null ServerData means the server socket is no longer connected
-                    break;
-                } else if (ServerData.length > 0) {
-                    SendToWebSocketClient(ServerData);
-                    DoYield = false;
-                }    
-            
-                // Yield if we didn't transfer any data
-                if (DoYield) {
-                    mswait(10);
-                    yield();
-                }
-            }
-            if (!client.socket.is_connected) log(LOG_DEBUG, 'Client socket no longer connected');
-			if (!FServerSocket.is_connected) log(LOG_DEBUG, 'Server socket no longer connected');
-        } else {
-            // FServerSocket.connect() failed
-            log(LOG_ERR, "Error " + FServerSocket.error + " connecting to server at " + TargetHostname + ":" + TargetPort);
-            SendToWebSocketClient(StringToBytes("ERROR: Unable to connect to server\r\n"));
-            mswait(2500);
-        }
-    } else {
-        log(LOG_DEBUG, "WebSocket handshake failed (likely a port scanner)");
-    }
-} catch (err) {
-    log(LOG_ERR, err.toString());
-} finally {
-    if (FServerSocket != null) {
-        FServerSocket.close();
-    }
-    if (ipFile instanceof File) ipFile.remove();
-}
-
-function CalculateWebSocketKey(InLine) {
-    var Digits = "";
-    var Spaces = 0;
-
-    // Loop through the line, looking for digits and spaces
-    for (var i = 0; i < InLine.length; i++) {
-        if (InLine.charAt(i) == " ") {
-            Spaces++;
-        } else if (!isNaN(InLine.charAt(i))) {
-            Digits += InLine.charAt(i);
-        }
-    }
-    
-    return Digits / Spaces;
-}
-
-function GetFromServer() {
-    if (!FServerSocket.is_connected) return null;
-
-    var Result = [];
-    var InStr = '';
-    
-    if (FServerSocket.data_waiting) {
-        InStr = FServerSocket.recv(4096);
-        for (var i = 0; i < InStr.length; i++) {
-            Result.push(InStr.charCodeAt(i));
-        }
-    }
-   
-    return Result;
-}
-
-function GetFromWebSocketClient() {
-    if (!client.socket.is_connected) return null;
-
-    switch (FWebSocketHeader['Version']) {
-        case 0: return GetFromWebSocketClientDraft0();
-		case 7: 
-		case 8: 
-        case 13:
-			return GetFromWebSocketClientVersion7();
-    }
-}
-
-function GetFromWebSocketClientDraft0() {
-    var Result = '';
-    var InByte = 0;
-    var InByte2 = 0;
-    var InByte3 = 0;
-    
-    while (client.socket.data_waiting) {
-        InByte = client.socket.recvBin(1);
-        
-        // Check what the client packet state is
-        switch (FWebSocketState) {
-            case WEBSOCKET_NEED_PACKET_START:
-                // Check for 0x00 to indicate the start of a data packet
-                if (InByte === 0x00) {
-                    FWebSocketState = WEBSOCKET_DATA;
-                }
-                break;
-            case WEBSOCKET_DATA:
-                // We're in a data packet, so check for 0xFF, which indicates the data packet is done
-                if (InByte == 0xFF) {
-                    FWebSocketState = WEBSOCKET_NEED_PACKET_START;
-                } else {
-                    // Check if the byte needs to be UTF-8 decoded
-                    if (InByte < 128) {
-                        Result += String.fromCharCode(InByte);
-                    } else if ((InByte > 191) && (InByte < 224)) {
-                        // Handle UTF-8 decode
-                        InByte2 = client.socket.recvBin(1);
-                        Result += String.fromCharCode(((InByte & 31) << 6) | (InByte2 & 63));
-                    } else {
-                        // Handle UTF-8 decode (should never need this, but included anyway)
-                        InByte2 = client.socket.recvBin(1);
-                        InByte3 = client.socket.recvBin(1);
-                        Result += String.fromCharCode(((InByte & 15) << 12) | ((InByte2 & 63) << 6) | (InByte3 & 63));
-                    }
-                }
-                break;
-        }
-    }
-    
-    return Result;
-}
-
-function GetFromWebSocketClientVersion7() {
-	var Result = '';
-	var InByte = 0;
-	var InByte2 = 0;
-	var InByte3 = 0;
-
-    while (client.socket.data_waiting && (Result.length <= 4096)) {
-        // Check what the client packet state is
-        switch (FWebSocketState) {
-            case WEBSOCKET_NEED_PACKET_START:
-				// Next byte will give us the opcode, and also tell is if the message is fragmented
-				FFrameOpCode = client.socket.recvBin(1) & 0x0F;
-                SendToWebSocketClient_RickDebug('\r\nFWebSocketState=WEBSOCKET_NEED_PACKET_START, FFrameOpCode=' + FFrameOpCode + '\r\n');
-                switch (FFrameOpCode) {
-                    case 0: // Continuation frame, keep same opcode as previous frame
-                        break;
-
-                    case 1: // Text frame
-                    case 2: // Binary frame
-                        FFrameType = FFrameOpCode;
-                        break;
-
-                    case 8: // Close frame
-                        log(LOG_DEBUG, "Client sent a close frame");
-                        // TODO Protocol says to respond with a close frame
-                        client.socket.close();
-                        return null;
-                        
-                    case 9: // Ping frame
-                        log(LOG_DEBUG, "Client setnt a ping frame");
-                        // TODO Protocol says to respond with a pong frame
-                        break;
-                        
-                    case 10: // Pong frame
-                        log(LOG_DEBUG, "Client sent a pong frame");
-                        break;
-                        
-                    default: // Reserved or unknown frame
-                        throw new Error('Client sent a reserved/unknown frame: ' + FFrameOpCode);
-                }
-
-                // If we get here, we want to move on to the next state
-				FFrameMask = [];
-                FFrameMasked = false;
-                FFramePayloadLength = 0;
-                FFramePayloadReceived = 0;
-                FWebSocketState = WEBSOCKET_NEED_PAYLOAD_LENGTH;
-                break;
-            case WEBSOCKET_NEED_PAYLOAD_LENGTH:
-                InByte = client.socket.recvBin(1);
-                
-                FFrameMasked = ((InByte & 0x80) == 128);
-
-                FFramePayloadLength = (InByte & 0x7F);
-				if (FFramePayloadLength === 126) {
-					FFramePayloadLength = client.socket.recvBin(2);
-				} else if (FFramePayloadLength === 127) {
-					FFramePayloadLength = client.socket.recvBin(8);
-				}
-                
-				SendToWebSocketClient_RickDebug('\r\nFWebSocketState=WEBSOCKET_NEED_PAYLOAD_LENGTH, InByte=' + InByte + ', FFrameMasked = ' + FFrameMasked + ', FFramePayloadLength = ' + FFramePayloadLength + '\r\n');
-                if (FFrameMasked) {
-                    FWebSocketState = WEBSOCKET_NEED_MASKING_KEY;
-                } else {
-                    FWebSocketState = (FFramePayloadLength > 0 ? WEBSOCKET_DATA : WEBSOCKET_NEED_PACKET_START); // NB: Might not be any data to read, so check for payload length before setting state
-                }
-				break;
-            case WEBSOCKET_NEED_MASKING_KEY:
-				InByte = client.socket.recvBin(4);
-				FFrameMask[0] = (InByte >> 24) & 0xFF; // (InByte & 0xFF000000) >> 24;
-				FFrameMask[1] = (InByte >> 16) & 0xFF; // (InByte & 0x00FF0000) >> 16;
-				FFrameMask[2] = (InByte >> 8) & 0xFF; // (InByte & 0x0000FF00) >> 8;
-				FFrameMask[3] = (InByte >> 0) & 0xFF; // InByte & 0x000000FF;
-                SendToWebSocketClient_RickDebug('\r\nFWebSocketState=WEBSOCKET_NEED_MASKING_KEY, InByte=' + InByte + ', FFrameMask = ' + FFrameMask[0] + ' ' + FFrameMask[1] + ' ' + FFrameMask[2] + ' ' + FFrameMask[3] + '\r\n');
-                FWebSocketState = (FFramePayloadLength > 0 ? WEBSOCKET_DATA : WEBSOCKET_NEED_PACKET_START); // NB: Might not be any data to read, so check for payload length before setting state
-				break;
-            case WEBSOCKET_DATA:
-                var BytesToRead = FFramePayloadLength - FWebSocketDataQueue.length;
-                var InStr = client.socket.recv(BytesToRead);
-                SendToWebSocketClient_RickDebug('\r\nFWebSocketState=WEBSOCKET_DATA, InStr=' + StringToBytes(InStr).join(' ') + '\r\n');
-                if (InStr) {
-                    if (InStr.length != BytesToRead) {
-                        // Didn't get the whole websocket data packet this read, so queue up the latest read, then return the data we have so far
-                        log(LOG_DEBUG, 'Asked for ' + BytesToRead + ' but got ' + InStr.length + ', queuing for a subsequent read');
-                        FWebSocketDataQueue += InStr;
-                        return Result;
-                    }
-
-                    // Prepend the queued text, if we have any
-                    if (FWebSocketDataQueue.length > 0) {
-                        InStr = FWebSocketDataQueue + InStr;
-                    }
-
-                    var tempBytes = [];
-                    if (FFrameType == WEBSOCKET_FRAME_TEXT) {
-                        for (var i = 0; i < InStr.length; i++) {
-                            InByte = InStr.charCodeAt(i);
-                            if (FFrameMasked) InByte ^= FFrameMask[FFramePayloadReceived++ % 4];
-                            tempBytes.push(InByte);
-
-                            // Check if the byte needs to be UTF-8 decoded
-                            if ((InByte & 0x80) === 0) {
-                                Result += String.fromCharCode(InByte);
-                            } else if ((InByte & 0xE0) === 0xC0) {
-                                // Handle UTF-8 decode
-                                InByte2 = InStr.charCodeAt(++i);
-                                if (FFrameMasked) InByte2 ^= FFrameMask[FFramePayloadReceived++ % 4];
-                                tempBytes.push(InByte2);
-                                Result += String.fromCharCode(((InByte & 31) << 6) | (InByte2 & 63));
-                            } else {
-                                throw new Error('GetFromWebSocketClientVersion7 Byte out of range: ' + InByte);
-                            }
-                        }
-                    } else if (FFrameType === WEBSOCKET_FRAME_BINARY) {
-                        for (var i = 0; i < InStr.length; i++) {
-                            InByte = InStr.charCodeAt(i);
-                            if (FFrameMasked) InByte ^= FFrameMask[FFramePayloadReceived++ % 4];
-                            tempBytes.push(InByte);
-                            Result += String.fromCharCode(InByte);
-                        }
-                    } else {
-                        throw new Error('GetFromWebSocketClientVersion7 Invalid frame type: ' + FFrameType);
-                    }
-
-                    SendToWebSocketClient_RickDebug('\r\n    tempBytes = ' + tempBytes.join(' ') + '\r\n');
-                    // Finished the data packet, so reset variables
-                    FWebSocketDataQueue = '';
-                    FWebSocketState = WEBSOCKET_NEED_PACKET_START;
-                }
-                break;
-        }
-    }
-
-    return Result;
-}
-
-function ParsePathHeader() {
-    var Result = {};
-    Result.query = {};
-    
-    var Path = FWebSocketHeader['Path'];
-    if (Path) {
-        if (Path.indexOf('?') === -1) {
-            // No querystring
-            Result.path_info = Path;
-        } else {
-            // Have querystring, so parse it out
-            Result.path_info = Path.split('?')[0];
-            var KeyValues = Path.split('?')[1].split('&');
-            for (var i = 0; i < KeyValues.length; i++) {
-                var KeyValue = KeyValues[i].split('=');
-                Result.query[KeyValue[0]] = KeyValue[1];
-            }
-        }
-    }
-    
-    return Result;
-}
-
-function SendToServer(AStr) {
-    if (!FServerSocket.is_connected) return;
-    if (AStr.length == 0) return;
-
-    var bytesSent = FServerSocket.send(AStr);
-    if (bytesSent != AStr.length) {
-        throw new Error('SendToServer only sent ' + bytesSent + ' of ' + AStr.length + ' bytes');
-    }
-}
-
-function SendToWebSocketClient(AData) {
-    if (!client.socket.is_connected) return;
-    if (AData.length == 0) return;
-
-    switch (FWebSocketHeader['Version']) {
-        case 0: 
-			SendToWebSocketClientDraft0(AData); 
-			break;
-		case 7: 
-		case 8: 
-        case 13:
-			SendToWebSocketClientVersion7(AData); 
-			break;
-    }
-}
-
-function SendToWebSocketClient_RickDebug(AStr) {
-    if (!client.socket.is_connected) return;
-    if (AStr.length == 0) return;
-
-    SendToWebSocketClient(StringToBytes(AStr));
-}
-
-function SendToWebSocketClientDraft0(AData) {
-    // Send 0x00 to indicate the start of a data packet
-    client.socket.sendBin(0x00, 1);
-
-    for (var i = 0; i < AData.length; i++) {
-        // Check if the byte needs to be UTF-8 encoded
-        if ((AData[i] & 0xFF) <= 127) {
-            client.socket.sendBin(AData[i], 1);
-        } else if ((AData[i] & 0xFF) <= 2047) {
-            // Handle UTF-8 encode
-            client.socket.sendBin((AData[i] >> 6) | 192, 1);
-            client.socket.sendBin((AData[i] & 63) | 128, 1);
-        } else {
-            throw new Error('SendToWebSocketClientDraft0 Byte out of range: ' + AData[i]);
-        }
-    }
-
-    // Send 0xFF to indicate the end of a data packet
-    client.socket.sendBin(0xFF, 1);
-}
-
-function SendToWebSocketClientVersion7(AData) {
-    var ToSend = '';
-
-    if (FWebSocketSubProtocol === 'binary') {
-        for (var i = 0; i < AData.length; i++) {
-            ToSend += String.fromCharCode(AData[i]);
-        }
-        client.socket.sendBin(0x82, 1);
-    } else {
-        for (var i = 0; i < AData.length; i++) {
-            // Check if the byte needs to be UTF-8 encoded
-            if ((AData[i] & 0xFF) <= 127) {
-                ToSend += String.fromCharCode(AData[i]);
-            } else if (((AData[i] & 0xFF) >= 128) && ((AData[i] & 0xFF) <= 2047)) {
-                // Handle UTF-8 encode
-                ToSend += String.fromCharCode((AData[i] >> 6) | 192);
-                ToSend += String.fromCharCode((AData[i] & 63) | 128);
-            } else {
-                throw new Error('SendToWebSocketClientVersion7 Byte out of range: ' + AData[i]);
-            }
-        }
-        client.socket.sendBin(0x81, 1);
-    }
-
-    if (ToSend.length <= 125) {
-        client.socket.sendBin(ToSend.length, 1);
-    } else if (ToSend.length <= 65535) {
-        client.socket.sendBin(126, 1);
-        client.socket.sendBin(ToSend.length, 2);
-    } else {
-        // NOTE: client.socket.sendBin(ToSend.length, 8); didn't work, so this
-        //       modification limits the send to 2^32 bytes (probably not an issue)
-        //       Probably should look into a proper fix at some point though
-        client.socket.sendBin(127, 1);
-        client.socket.sendBin(0, 4);
-        client.socket.sendBin(ToSend.length, 4);
-    }
-
-    var bytesSent = client.socket.send(ToSend);
-    if (bytesSent != ToSend.length) {
-        throw new Error('SendToWebSocketClientVersion7 only sent ' + bytesSent + ' of ' + ToSend.length + ' bytes');
-    }
-}
-
-function ShakeHands() {
-	FWebSocketHeader['Version'] = 0;
-
-    try {
-        // Keep reading header data until we get all the data we want
-        while (true) {
-            // Read another line, and abort if we don't get one within 5 seconds
-            var InLine = client.socket.recvline(512, 5);
-            if (InLine === null) {
-                log(LOG_DEBUG, "Timeout exceeded while waiting for complete handshake");
-                return false;
-            }
-
-            log(LOG_DEBUG, "Handshake Line: " + InLine);
-            
-            // Check for blank line (indicates we have most of the header, and only the last 8 bytes remain
-            if (InLine === "") {
-    			switch (FWebSocketHeader['Version']) {
-			        case 0: 
-						return ShakeHandsDraft0();
-					case 7: 
-					case 8: 
-                    case 13:
-						return ShakeHandsVersion7();
-					default: 
-						//		    TODO If this version does not
-						//          match a version understood by the server, the server MUST
-						//          abort the websocket handshake described in this section and
-						//          instead send an appropriate HTTP error code (such as 426
-						//          Upgrade Required), and a |Sec-WebSocket-Version| header
-						//          indicating the version(s) the server is capable of
-						//          understanding.
-						break;
-			    }
-				break;
-            } else if (InLine.search(/^Connection:/i) === 0) {
-                // Example: "Connection: Upgrade"
-                FWebSocketHeader['Connection'] = InLine.replace(/Connection:\s?/i, "");
-            } else if (InLine.search(/^GET/i) === 0) {
-                // Example: "GET /demo HTTP/1.1"
-                var GET = InLine.split(" ");
-                FWebSocketHeader['Path'] = GET[1];
-            } else if (InLine.search(/^Host:/i) === 0) {
-                // Example: "Host: example.com"
-                FWebSocketHeader['Host'] = InLine.replace(/Host:\s?/i, "");
-            } else if (InLine.search(/^Origin:/i) === 0) {
-                // Example: "Origin: http://example.com"
-                FWebSocketHeader['Origin'] = InLine.replace(/Origin:\s?/i, "");
-            } else if (InLine.search(/^Sec-WebSocket-Key:/i) === 0) {
-                // Example: "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=="
-                FWebSocketHeader['Key'] = InLine.replace(/Sec-WebSocket-Key:\s?/i, "");
-            } else if (InLine.search(/^Sec-WebSocket-Key1:/i) === 0) {
-                // Example: "Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5"
-                FWebSocketHeader['Key1'] = CalculateWebSocketKey(InLine.replace(/Sec-WebSocket-Key1:\s?/i, ""));
-            } else if (InLine.search(/^Sec-WebSocket-Key2:/i) === 0) {
-                // Example: "Sec-WebSocket-Key2: 12998 5 Y3 1  .P00"
-                FWebSocketHeader['Key2'] = CalculateWebSocketKey(InLine.replace(/Sec-WebSocket-Key2:\s?/i, ""));
-            } else if (InLine.search(/^Sec-WebSocket-Origin:/i) === 0) {
-                // Example: "Sec-WebSocket-Origin: http://example.com"
-                FWebSocketHeader['Origin'] = InLine.replace(/Sec-WebSocket-Origin:\s?/i, "");
-            } else if (InLine.search(/^Sec-WebSocket-Protocol:/i) === 0) {
-                // Example: "Sec-WebSocket-Protocol: sample"
-                FWebSocketHeader['SubProtocol'] = InLine.replace(/Sec-WebSocket-Protocol:\s?/i, "");
-            } else if (InLine.search(/^Sec-WebSocket-Draft/i) === 0) {
-				// Example: "Sec-WebSocket-Draft: 2"
-                try {
-                    FWebSocketHeader['Version'] = parseInt(InLine.replace(/Sec-WebSocket-Draft:\s?/i, ""));
-                } catch (err) {
-                    FWebSocketHeader['Version'] = 0;
-                }
-            } else if (InLine.search(/^Sec-WebSocket-Version/i) === 0) {
-				// Example: "Sec-WebSocket-Version: 8"
-                try {
-                    FWebSocketHeader['Version'] = parseInt(InLine.replace(/Sec-WebSocket-Version:\s?/i, ""));
-                } catch (err) {
-                    FWebSocketHeader['Version'] = 0;
-                }
-            } else if (InLine.search(/^Upgrade:/i) === 0) {
-				// Example: "Upgrade: websocket"
-				FWebSocketHeader['Upgrade'] = InLine.replace(/Upgrade:\s?/i, "");
-			} else if (InLine.search(/^X-Forwarded-For/i) === 0) {
-				FWebSocketHeader['X-Forwarded-For'] = InLine.replace(/X-Forwarded-For:\s?/i, "");
-			}
-        }
-    } catch (err) {
-        log(LOG_DEBUG, "ShakeHands() error: " + err.toString());
-    }
-    
-    return false;
-}
-
-function ShakeHandsDraft0() {
-	// Ensure we have all the data we need
-	if (('Key1' in FWebSocketHeader) && ('Key2' in FWebSocketHeader) && ('Host' in FWebSocketHeader) && ('Origin' in FWebSocketHeader !== "") && ('Path' in FWebSocketHeader)) {
-		// Combine Key1, Key2, and the last 8 bytes into a string that we will later hash
-		var ToHash = ""
-		ToHash += String.fromCharCode((FWebSocketHeader['Key1'] & 0xFF000000) >> 24);
-		ToHash += String.fromCharCode((FWebSocketHeader['Key1'] & 0x00FF0000) >> 16);
-		ToHash += String.fromCharCode((FWebSocketHeader['Key1'] & 0x0000FF00) >> 8);
-		ToHash += String.fromCharCode((FWebSocketHeader['Key1'] & 0x000000FF) >> 0);
-		ToHash += String.fromCharCode((FWebSocketHeader['Key2'] & 0xFF000000) >> 24);
-		ToHash += String.fromCharCode((FWebSocketHeader['Key2'] & 0x00FF0000) >> 16);
-		ToHash += String.fromCharCode((FWebSocketHeader['Key2'] & 0x0000FF00) >> 8);
-		ToHash += String.fromCharCode((FWebSocketHeader['Key2'] & 0x000000FF) >> 0);				
-		for (var i = 0; i < 8; i++) {
-			ToHash += String.fromCharCode(client.socket.recvBin(1));
-		}
-		
-		// Hash the string
-		var Hashed = md5_calc(ToHash, true);
-
-		// Setup the handshake response
-		var Response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
-					   "Upgrade: WebSocket\r\n" +
-					   "Connection: Upgrade\r\n" +
-					   "Sec-WebSocket-Origin: " + FWebSocketHeader['Origin'] + "\r\n" +
-					   "Sec-WebSocket-Location: ws://" + FWebSocketHeader['Host'] + FWebSocketHeader['Path'] + "\r\n";
-		if ('SubProtocol' in FWebSocketHeader) Response += "Sec-WebSocket-Protocol: " + FWebSocketHeader['SubProtocol'] + "\r\n";
-		Response += "\r\n";
-		
-		// Loop through the hash string (which is hex encoded) and append the individual bytes to the response
-		for (var i = 0; i < Hashed.length; i += 2) {
-			Response += String.fromCharCode(parseInt(Hashed.charAt(i) + Hashed.charAt(i + 1), 16));
-		}
-		
-		// Send the response and return
-		client.socket.send(Response);
-		return true;
-	} else {
-		// We're missing some pice of data, log what we do have
-		log(LOG_DEBUG, "Missing some piece of handshake data.  Here's what we have:");
-		for(var x in FWebSocketHeader) { 
-			log(LOG_DEBUG, x + " => " + FWebSocketHeader[x]); 
-		}
-		return false;
-	}
-}
-
-function ShakeHandsVersion7() {
-	// Ensure we have all the data we need
-	if (('Key' in FWebSocketHeader) && ('Host' in FWebSocketHeader) && ('Origin' in FWebSocketHeader !== "") && ('Path' in FWebSocketHeader)) {
-		var AcceptGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
-
-		// Combine Key and GUID
-		var ToHash = FWebSocketHeader['Key'] + AcceptGUID;
-		
-		// Hash the string
-		var Hashed = Sha1.hash(ToHash, false);
-
-        // Encode the hash
-        var ToEncode = '';
-        for (var i = 0; i <= 38; i += 2) {
-        	ToEncode += String.fromCharCode(parseInt(Hashed.substr(i, 2), 16));
-		}
-		var Encoded = base64_encode(ToEncode);
-
-		// Setup the handshake response
-		var Response = "HTTP/1.1 101 Switching Protocols\r\n" +
-					   "Upgrade: websocket\r\n" +
-					   "Connection: Upgrade\r\n" +
-					   "Sec-WebSocket-Accept: " + Encoded + "\r\n";
-		if ('SubProtocol' in FWebSocketHeader) {
-            if (FWebSocketHeader['SubProtocol'].indexOf('binary') >= 0) {
-                FWebSocketSubProtocol = 'binary';
-            } else if (FWebSocketHeader['SubProtocol'].indexOf('plain') >= 0) {
-                FWebSocketSubProtocol = 'plain';
-            } else {
-                log(LOG_ERR, 'No supported SubProtocols found ("binary" and "plain" are only supported options');
-                return false;
-            }
-
-            log(LOG_DEBUG, 'Selecting "' + FWebSocketSubProtocol + '" SubProtocol');
-            Response += "Sec-WebSocket-Protocol: " + FWebSocketSubProtocol + "\r\n";
-        }
-		Response += "\r\n";
-		
-		// Send the response and return
-		client.socket.send(Response);
-		return true;
-	} else {
-		// We're missing some pice of data, log what we do have
-		log(LOG_DEBUG, "Missing some piece of handshake data.  Here's what we have:");
-		for(var x in FWebSocketHeader) { 
-			log(LOG_DEBUG, x + " => " + FWebSocketHeader[x]); 
-		}
-		return false;
-	}
-}
-
-function inet_pton (a) {
-    // http://kevin.vanzonneveld.net
-    // +   original by: Theriault
-    // *     example 1: inet_pton('::');
-    // *     returns 1: '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0' (binary)
-    // *     example 2: inet_pton('127.0.0.1');
-    // *     returns 2: '\x7F\x00\x00\x01' (binary)
-    var r, m, x, i, j, f = String.fromCharCode;
-    m = a.match(/^(?:\d{1,3}(?:\.|$)){4}/); // IPv4
-    if (m) {
-        m = m[0].split('.');
-        m = f(m[0]) + f(m[1]) + f(m[2]) + f(m[3]);
-        // Return if 4 bytes, otherwise false.
-        return m.length === 4 ? m : false;
-    }
-    r = /^((?:[\da-f]{1,4}(?::|)){0,8})(::)?((?:[\da-f]{1,4}(?::|)){0,8})$/;
-    m = a.match(r); // IPv6
-    if (m) {
-        // Translate each hexadecimal value.
-        for (j = 1; j < 4; j++) {
-            // Indice 2 is :: and if no length, continue.
-            if (j === 2 || m[j].length === 0) {
-                continue;
-            }
-            m[j] = m[j].split(':');
-            for (i = 0; i < m[j].length; i++) {
-                m[j][i] = parseInt(m[j][i], 16);
-                // Would be NaN if it was blank, return false.
-                if (isNaN(m[j][i])) {
-                    return false; // Invalid IP.
-                }
-                m[j][i] = f(m[j][i] >> 8) + f(m[j][i] & 0xFF);
-            }
-            m[j] = m[j].join('');
-        }
-        x = m[1].length + m[3].length;
-        if (x === 16) {
-            return m[1] + m[3];
-        } else if (x < 16 && m[2].length > 0) {
-            return m[1] + (new Array(16 - x + 1)).join('\x00') + m[3];
-        }
-    }
-    return false; // Invalid IP.
-}
+// Synchronet Service for redirecting incoming WebSocket connections to the Telnet and/or RLogin server
+// Mainly to be used in conjunction with fTelnet
+
+// Command-line syntax:
+// websocketservice.js [hostname] [port]
+
+// Example configuration (in ctrl/services.ini)
+//
+// [WS]
+// Port=1123
+// Options=NO_HOST_LOOKUP
+// Command=websocketservice.js
+//
+// [WSS]
+// Port=11235
+// Options=NO_HOST_LOOKUP|TLS
+// Command=websocketservice.js
+
+//include definitions for synchronet
+load("nodedefs.js");
+load("sockdefs.js");
+load("sbbsdefs.js");
+load("ftelnethelper.js");
+load("sha1.js");
+
+// State flags
+const WEBSOCKET_NEED_PACKET_START   = 0;
+const WEBSOCKET_NEED_PAYLOAD_LENGTH = 1;
+const WEBSOCKET_NEED_MASKING_KEY	= 2;
+const WEBSOCKET_DATA                = 3;
+
+// Frame types
+const WEBSOCKET_FRAME_UNKNOWN = 0;
+const WEBSOCKET_FRAME_TEXT    = 1;
+const WEBSOCKET_FRAME_BINARY  = 2;
+
+// Global variables
+var FFrameMask = [];
+var FFrameMasked = false;
+var FFrameOpCode = 0;
+var FFramePayloadLength = 0;
+var FFramePayloadReceived = 0;
+var FFrameType = WEBSOCKET_FRAME_UNKNOWN;
+var FServerSocket = null;
+var FWebSocketDataQueue = '';
+var FWebSocketHeader = {};
+var FWebSocketState = WEBSOCKET_NEED_PACKET_START;
+var FWebSocketSubProtocol = '';
+var ipFile;
+
+// Main line
+try {
+    // Parse and respond to the WebSocket handshake request
+    if (ShakeHands()) {
+
+		if (UsingHAProxy() && FWebSocketHeader['X-Forwarded-For'] === undefined) {
+			throw new Error('BBS is using HAProxy, but no X-Forwarded-For header present.');
+		}
+
+        SendToWebSocketClient(StringToBytes("Redirecting to server...\r\n"));
+
+        // Default to localhost on the telnet port
+		var TargetHostname = GetTelnetInterface();
+		var TargetPort = GetTelnetPort();
+        
+        // If fTelnet client sent a port on the querystring, try to use that
+        var Path = ParsePathHeader();
+        if (Path.query.Port) {
+            RequestedPort = parseInt(Path.query.Port);
+
+            // Confirm the user requested either the telnet or rlogin ports (we don't want to allow them to request any arbitrary port as that would be a gaping security hole)
+            if ((RequestedPort > 0) && (RequestedPort <= 65535) && ((RequestedPort == GetTelnetPort()) || (RequestedPort == GetRLoginPort()))) {
+                TargetPort = RequestedPort;
+                log(LOG_DEBUG, "Using user-requested port " + Path.query.Port);
+                if (TargetPort == GetRLoginPort()) TargetHostname = GetRLoginInterface();
+            } else {
+                log(LOG_NOTICE, "Client requested to connect to port " + Path.query.Port + ", which was denied");
+            }
+        } else {
+            // If SysOp gave an alternate hostname/port when installing the service, use that instead
+			for(var i in argv) {
+                var port = parseInt(argv[i], 10);
+                if (argv[i].search(/\D/) > -1 || port < 0 || port > 65535) {
+                    TargetHostname = argv[i];
+                } else if (!isNaN(port)) {
+                    TargetPort = port;
+                }
+            }
+        }
+        
+		// Connect to the server
+        FServerSocket = new Socket();
+		log(LOG_DEBUG, "Connecting to " + TargetHostname + ":" + TargetPort);
+        if (FServerSocket.connect(TargetHostname, TargetPort)) {
+
+            ipFile = new File(system.temp_dir + 'sbbs-ws-' + FServerSocket.local_port + '.ip');
+            if (ipFile.open('w')) {
+                ipFile.write(client.ip_address);
+                ipFile.close();
+            }
+
+            // Variables we'll use in the loop
+            var DoYield = true;
+            var ClientData = [];
+            var ServerData = [];
+
+			if (UsingHAProxy()) {
+				var hapstr = '\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21';
+				if (client.socket.family === PF_INET) {
+					hapstr += '\x11\x00\x0C';
+				} else if (client.socket.family === PF_INET6) {
+					hapstr += '\x21\x00\x24';
+				}
+				hapstr += inet_pton(FWebSocketHeader['X-Forwarded-For']);
+				hapstr += inet_pton(FServerSocket.remote_ip_address);
+				hapstr += client.port.toString(16);
+				hapstr += TargetPort.toString(16);
+				FServerSocket.send(hapstr);
+			}
+            
+            // Loop while we're still connected on both ends
+            while ((client.socket.is_connected) && (FServerSocket.is_connected)) {
+                // Should we yield or not (default true, but disable if we had line activity)
+                DoYield = true;
+            
+                // Check if the client sent anything
+                ClientData = GetFromWebSocketClient();
+                if (ClientData === null) {
+                    // null ClientData means the client socket is no longer connected, or a close frame was received
+                    break;
+                } else if (ClientData.length > 0) {
+                    SendToServer(ClientData);
+                    DoYield = false;
+                }
+                
+                // Check if the server sent anything
+                ServerData = GetFromServer();
+                if (ServerData === null) {
+                    // null ServerData means the server socket is no longer connected
+                    break;
+                } else if (ServerData.length > 0) {
+                    SendToWebSocketClient(ServerData);
+                    DoYield = false;
+                }    
+            
+                // Yield if we didn't transfer any data
+                if (DoYield) {
+                    mswait(10);
+                    yield();
+                }
+            }
+            if (!client.socket.is_connected) log(LOG_DEBUG, 'Client socket no longer connected');
+			if (!FServerSocket.is_connected) log(LOG_DEBUG, 'Server socket no longer connected');
+        } else {
+            // FServerSocket.connect() failed
+            log(LOG_ERR, "Error " + FServerSocket.error + " connecting to server at " + TargetHostname + ":" + TargetPort);
+            SendToWebSocketClient(StringToBytes("ERROR: Unable to connect to server\r\n"));
+            mswait(2500);
+        }
+    } else {
+        log(LOG_DEBUG, "WebSocket handshake failed (likely a port scanner)");
+    }
+} catch (err) {
+    log(LOG_ERR, err.toString());
+} finally {
+    if (FServerSocket != null) {
+        FServerSocket.close();
+    }
+    if (ipFile instanceof File) ipFile.remove();
+}
+
+function CalculateWebSocketKey(InLine) {
+    var Digits = "";
+    var Spaces = 0;
+
+    // Loop through the line, looking for digits and spaces
+    for (var i = 0; i < InLine.length; i++) {
+        if (InLine.charAt(i) == " ") {
+            Spaces++;
+        } else if (!isNaN(InLine.charAt(i))) {
+            Digits += InLine.charAt(i);
+        }
+    }
+    
+    return Digits / Spaces;
+}
+
+function GetFromServer() {
+    if (!FServerSocket.is_connected) return null;
+
+    var Result = [];
+    var InStr = '';
+    
+    if (FServerSocket.data_waiting) {
+        InStr = FServerSocket.recv(4096);
+        for (var i = 0; i < InStr.length; i++) {
+            Result.push(InStr.charCodeAt(i));
+        }
+    }
+   
+    return Result;
+}
+
+function GetFromWebSocketClient() {
+    if (!client.socket.is_connected) return null;
+
+    switch (FWebSocketHeader['Version']) {
+        case 0: return GetFromWebSocketClientDraft0();
+		case 7: 
+		case 8: 
+        case 13:
+			return GetFromWebSocketClientVersion7();
+    }
+}
+
+function GetFromWebSocketClientDraft0() {
+    var Result = '';
+    var InByte = 0;
+    var InByte2 = 0;
+    var InByte3 = 0;
+    
+    while (client.socket.data_waiting) {
+        InByte = client.socket.recvBin(1);
+        
+        // Check what the client packet state is
+        switch (FWebSocketState) {
+            case WEBSOCKET_NEED_PACKET_START:
+                // Check for 0x00 to indicate the start of a data packet
+                if (InByte === 0x00) {
+                    FWebSocketState = WEBSOCKET_DATA;
+                }
+                break;
+            case WEBSOCKET_DATA:
+                // We're in a data packet, so check for 0xFF, which indicates the data packet is done
+                if (InByte == 0xFF) {
+                    FWebSocketState = WEBSOCKET_NEED_PACKET_START;
+                } else {
+                    // Check if the byte needs to be UTF-8 decoded
+                    if (InByte < 128) {
+                        Result += String.fromCharCode(InByte);
+                    } else if ((InByte > 191) && (InByte < 224)) {
+                        // Handle UTF-8 decode
+                        InByte2 = client.socket.recvBin(1);
+                        Result += String.fromCharCode(((InByte & 31) << 6) | (InByte2 & 63));
+                    } else {
+                        // Handle UTF-8 decode (should never need this, but included anyway)
+                        InByte2 = client.socket.recvBin(1);
+                        InByte3 = client.socket.recvBin(1);
+                        Result += String.fromCharCode(((InByte & 15) << 12) | ((InByte2 & 63) << 6) | (InByte3 & 63));
+                    }
+                }
+                break;
+        }
+    }
+    
+    return Result;
+}
+
+function GetFromWebSocketClientVersion7() {
+	var Result = '';
+	var InByte = 0;
+	var InByte2 = 0;
+	var InByte3 = 0;
+
+    while (client.socket.data_waiting && (Result.length <= 4096)) {
+        // Check what the client packet state is
+        switch (FWebSocketState) {
+            case WEBSOCKET_NEED_PACKET_START:
+				// Next byte will give us the opcode, and also tell is if the message is fragmented
+				FFrameOpCode = client.socket.recvBin(1) & 0x0F;
+                SendToWebSocketClient_RickDebug('\r\nFWebSocketState=WEBSOCKET_NEED_PACKET_START, FFrameOpCode=' + FFrameOpCode + '\r\n');
+                switch (FFrameOpCode) {
+                    case 0: // Continuation frame, keep same opcode as previous frame
+                        break;
+
+                    case 1: // Text frame
+                    case 2: // Binary frame
+                        FFrameType = FFrameOpCode;
+                        break;
+
+                    case 8: // Close frame
+                        log(LOG_DEBUG, "Client sent a close frame");
+                        // TODO Protocol says to respond with a close frame
+                        client.socket.close();
+                        return null;
+                        
+                    case 9: // Ping frame
+                        log(LOG_DEBUG, "Client setnt a ping frame");
+                        // TODO Protocol says to respond with a pong frame
+                        break;
+                        
+                    case 10: // Pong frame
+                        log(LOG_DEBUG, "Client sent a pong frame");
+                        break;
+                        
+                    default: // Reserved or unknown frame
+                        throw new Error('Client sent a reserved/unknown frame: ' + FFrameOpCode);
+                }
+
+                // If we get here, we want to move on to the next state
+				FFrameMask = [];
+                FFrameMasked = false;
+                FFramePayloadLength = 0;
+                FFramePayloadReceived = 0;
+                FWebSocketState = WEBSOCKET_NEED_PAYLOAD_LENGTH;
+                break;
+            case WEBSOCKET_NEED_PAYLOAD_LENGTH:
+                InByte = client.socket.recvBin(1);
+                
+                FFrameMasked = ((InByte & 0x80) == 128);
+
+                FFramePayloadLength = (InByte & 0x7F);
+				if (FFramePayloadLength === 126) {
+					FFramePayloadLength = client.socket.recvBin(2);
+				} else if (FFramePayloadLength === 127) {
+					FFramePayloadLength = client.socket.recvBin(8);
+				}
+                
+				SendToWebSocketClient_RickDebug('\r\nFWebSocketState=WEBSOCKET_NEED_PAYLOAD_LENGTH, InByte=' + InByte + ', FFrameMasked = ' + FFrameMasked + ', FFramePayloadLength = ' + FFramePayloadLength + '\r\n');
+                if (FFrameMasked) {
+                    FWebSocketState = WEBSOCKET_NEED_MASKING_KEY;
+                } else {
+                    FWebSocketState = (FFramePayloadLength > 0 ? WEBSOCKET_DATA : WEBSOCKET_NEED_PACKET_START); // NB: Might not be any data to read, so check for payload length before setting state
+                }
+				break;
+            case WEBSOCKET_NEED_MASKING_KEY:
+				InByte = client.socket.recvBin(4);
+				FFrameMask[0] = (InByte >> 24) & 0xFF; // (InByte & 0xFF000000) >> 24;
+				FFrameMask[1] = (InByte >> 16) & 0xFF; // (InByte & 0x00FF0000) >> 16;
+				FFrameMask[2] = (InByte >> 8) & 0xFF; // (InByte & 0x0000FF00) >> 8;
+				FFrameMask[3] = (InByte >> 0) & 0xFF; // InByte & 0x000000FF;
+                SendToWebSocketClient_RickDebug('\r\nFWebSocketState=WEBSOCKET_NEED_MASKING_KEY, InByte=' + InByte + ', FFrameMask = ' + FFrameMask[0] + ' ' + FFrameMask[1] + ' ' + FFrameMask[2] + ' ' + FFrameMask[3] + '\r\n');
+                FWebSocketState = (FFramePayloadLength > 0 ? WEBSOCKET_DATA : WEBSOCKET_NEED_PACKET_START); // NB: Might not be any data to read, so check for payload length before setting state
+				break;
+            case WEBSOCKET_DATA:
+                var BytesToRead = FFramePayloadLength - FWebSocketDataQueue.length;
+                var InStr = client.socket.recv(BytesToRead);
+                SendToWebSocketClient_RickDebug('\r\nFWebSocketState=WEBSOCKET_DATA, InStr=' + StringToBytes(InStr).join(' ') + '\r\n');
+                if (InStr) {
+                    if (InStr.length != BytesToRead) {
+                        // Didn't get the whole websocket data packet this read, so queue up the latest read, then return the data we have so far
+                        log(LOG_DEBUG, 'Asked for ' + BytesToRead + ' but got ' + InStr.length + ', queuing for a subsequent read');
+                        FWebSocketDataQueue += InStr;
+                        return Result;
+                    }
+
+                    // Prepend the queued text, if we have any
+                    if (FWebSocketDataQueue.length > 0) {
+                        InStr = FWebSocketDataQueue + InStr;
+                    }
+
+                    var tempBytes = [];
+                    if (FFrameType == WEBSOCKET_FRAME_TEXT) {
+                        for (var i = 0; i < InStr.length; i++) {
+                            InByte = InStr.charCodeAt(i);
+                            if (FFrameMasked) InByte ^= FFrameMask[FFramePayloadReceived++ % 4];
+                            tempBytes.push(InByte);
+
+                            // Check if the byte needs to be UTF-8 decoded
+                            if ((InByte & 0x80) === 0) {
+                                Result += String.fromCharCode(InByte);
+                            } else if ((InByte & 0xE0) === 0xC0) {
+                                // Handle UTF-8 decode
+                                InByte2 = InStr.charCodeAt(++i);
+                                if (FFrameMasked) InByte2 ^= FFrameMask[FFramePayloadReceived++ % 4];
+                                tempBytes.push(InByte2);
+                                Result += String.fromCharCode(((InByte & 31) << 6) | (InByte2 & 63));
+                            } else {
+                                throw new Error('GetFromWebSocketClientVersion7 Byte out of range: ' + InByte);
+                            }
+                        }
+                    } else if (FFrameType === WEBSOCKET_FRAME_BINARY) {
+                        SendToWebSocketClient_RickDebug('\r\n');
+                        for (var i = 0; i < InStr.length; i++) {
+                            InByte = InStr.charCodeAt(i);
+                            SendToWebSocketClient_RickDebug('    InByte = ' + InByte + ', FFramePayloadReceived = ' + FFramePayloadReceived + ', Mask = ' + FFrameMask[FFramePayloadReceived % 4] + ', OutByte = ' + (InByte ^ FFrameMask[FFramePayloadReceived % 4]) + '\r\n');
+                            if (FFrameMasked) InByte ^= FFrameMask[FFramePayloadReceived++ % 4];
+                            tempBytes.push(InByte);
+                            Result += String.fromCharCode(InByte);
+                        }
+                    } else {
+                        throw new Error('GetFromWebSocketClientVersion7 Invalid frame type: ' + FFrameType);
+                    }
+
+                    SendToWebSocketClient_RickDebug('\r\n    tempBytes = ' + tempBytes.join(' ') + '\r\n');
+                    // Finished the data packet, so reset variables
+                    FWebSocketDataQueue = '';
+                    FWebSocketState = WEBSOCKET_NEED_PACKET_START;
+                }
+                break;
+        }
+    }
+
+    return Result;
+}
+
+function ParsePathHeader() {
+    var Result = {};
+    Result.query = {};
+    
+    var Path = FWebSocketHeader['Path'];
+    if (Path) {
+        if (Path.indexOf('?') === -1) {
+            // No querystring
+            Result.path_info = Path;
+        } else {
+            // Have querystring, so parse it out
+            Result.path_info = Path.split('?')[0];
+            var KeyValues = Path.split('?')[1].split('&');
+            for (var i = 0; i < KeyValues.length; i++) {
+                var KeyValue = KeyValues[i].split('=');
+                Result.query[KeyValue[0]] = KeyValue[1];
+            }
+        }
+    }
+    
+    return Result;
+}
+
+function SendToServer(AStr) {
+    if (!FServerSocket.is_connected) return;
+    if (AStr.length == 0) return;
+
+    var bytesSent = FServerSocket.send(AStr);
+    if (bytesSent != AStr.length) {
+        throw new Error('SendToServer only sent ' + bytesSent + ' of ' + AStr.length + ' bytes');
+    }
+}
+
+function SendToWebSocketClient(AData) {
+    if (!client.socket.is_connected) return;
+    if (AData.length == 0) return;
+
+    switch (FWebSocketHeader['Version']) {
+        case 0: 
+			SendToWebSocketClientDraft0(AData); 
+			break;
+		case 7: 
+		case 8: 
+        case 13:
+			SendToWebSocketClientVersion7(AData); 
+			break;
+    }
+}
+
+function SendToWebSocketClient_RickDebug(AStr) {
+    if (!client.socket.is_connected) return;
+    if (AStr.length == 0) return;
+
+    SendToWebSocketClient(StringToBytes(AStr));
+}
+
+function SendToWebSocketClientDraft0(AData) {
+    // Send 0x00 to indicate the start of a data packet
+    client.socket.sendBin(0x00, 1);
+
+    for (var i = 0; i < AData.length; i++) {
+        // Check if the byte needs to be UTF-8 encoded
+        if ((AData[i] & 0xFF) <= 127) {
+            client.socket.sendBin(AData[i], 1);
+        } else if ((AData[i] & 0xFF) <= 2047) {
+            // Handle UTF-8 encode
+            client.socket.sendBin((AData[i] >> 6) | 192, 1);
+            client.socket.sendBin((AData[i] & 63) | 128, 1);
+        } else {
+            throw new Error('SendToWebSocketClientDraft0 Byte out of range: ' + AData[i]);
+        }
+    }
+
+    // Send 0xFF to indicate the end of a data packet
+    client.socket.sendBin(0xFF, 1);
+}
+
+function SendToWebSocketClientVersion7(AData) {
+    var ToSend = '';
+
+    if (FWebSocketSubProtocol === 'binary') {
+        for (var i = 0; i < AData.length; i++) {
+            ToSend += String.fromCharCode(AData[i]);
+        }
+        client.socket.sendBin(0x82, 1);
+    } else {
+        for (var i = 0; i < AData.length; i++) {
+            // Check if the byte needs to be UTF-8 encoded
+            if ((AData[i] & 0xFF) <= 127) {
+                ToSend += String.fromCharCode(AData[i]);
+            } else if (((AData[i] & 0xFF) >= 128) && ((AData[i] & 0xFF) <= 2047)) {
+                // Handle UTF-8 encode
+                ToSend += String.fromCharCode((AData[i] >> 6) | 192);
+                ToSend += String.fromCharCode((AData[i] & 63) | 128);
+            } else {
+                throw new Error('SendToWebSocketClientVersion7 Byte out of range: ' + AData[i]);
+            }
+        }
+        client.socket.sendBin(0x81, 1);
+    }
+
+    if (ToSend.length <= 125) {
+        client.socket.sendBin(ToSend.length, 1);
+    } else if (ToSend.length <= 65535) {
+        client.socket.sendBin(126, 1);
+        client.socket.sendBin(ToSend.length, 2);
+    } else {
+        // NOTE: client.socket.sendBin(ToSend.length, 8); didn't work, so this
+        //       modification limits the send to 2^32 bytes (probably not an issue)
+        //       Probably should look into a proper fix at some point though
+        client.socket.sendBin(127, 1);
+        client.socket.sendBin(0, 4);
+        client.socket.sendBin(ToSend.length, 4);
+    }
+
+    var bytesSent = client.socket.send(ToSend);
+    if (bytesSent != ToSend.length) {
+        throw new Error('SendToWebSocketClientVersion7 only sent ' + bytesSent + ' of ' + ToSend.length + ' bytes');
+    }
+}
+
+function ShakeHands() {
+	FWebSocketHeader['Version'] = 0;
+
+    try {
+        // Keep reading header data until we get all the data we want
+        while (true) {
+            // Read another line, and abort if we don't get one within 5 seconds
+            var InLine = client.socket.recvline(512, 5);
+            if (InLine === null) {
+                log(LOG_DEBUG, "Timeout exceeded while waiting for complete handshake");
+                return false;
+            }
+
+            log(LOG_DEBUG, "Handshake Line: " + InLine);
+            
+            // Check for blank line (indicates we have most of the header, and only the last 8 bytes remain
+            if (InLine === "") {
+    			switch (FWebSocketHeader['Version']) {
+			        case 0: 
+						return ShakeHandsDraft0();
+					case 7: 
+					case 8: 
+                    case 13:
+						return ShakeHandsVersion7();
+					default: 
+						//		    TODO If this version does not
+						//          match a version understood by the server, the server MUST
+						//          abort the websocket handshake described in this section and
+						//          instead send an appropriate HTTP error code (such as 426
+						//          Upgrade Required), and a |Sec-WebSocket-Version| header
+						//          indicating the version(s) the server is capable of
+						//          understanding.
+						break;
+			    }
+				break;
+            } else if (InLine.search(/^Connection:/i) === 0) {
+                // Example: "Connection: Upgrade"
+                FWebSocketHeader['Connection'] = InLine.replace(/Connection:\s?/i, "");
+            } else if (InLine.search(/^GET/i) === 0) {
+                // Example: "GET /demo HTTP/1.1"
+                var GET = InLine.split(" ");
+                FWebSocketHeader['Path'] = GET[1];
+            } else if (InLine.search(/^Host:/i) === 0) {
+                // Example: "Host: example.com"
+                FWebSocketHeader['Host'] = InLine.replace(/Host:\s?/i, "");
+            } else if (InLine.search(/^Origin:/i) === 0) {
+                // Example: "Origin: http://example.com"
+                FWebSocketHeader['Origin'] = InLine.replace(/Origin:\s?/i, "");
+            } else if (InLine.search(/^Sec-WebSocket-Key:/i) === 0) {
+                // Example: "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=="
+                FWebSocketHeader['Key'] = InLine.replace(/Sec-WebSocket-Key:\s?/i, "");
+            } else if (InLine.search(/^Sec-WebSocket-Key1:/i) === 0) {
+                // Example: "Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5"
+                FWebSocketHeader['Key1'] = CalculateWebSocketKey(InLine.replace(/Sec-WebSocket-Key1:\s?/i, ""));
+            } else if (InLine.search(/^Sec-WebSocket-Key2:/i) === 0) {
+                // Example: "Sec-WebSocket-Key2: 12998 5 Y3 1  .P00"
+                FWebSocketHeader['Key2'] = CalculateWebSocketKey(InLine.replace(/Sec-WebSocket-Key2:\s?/i, ""));
+            } else if (InLine.search(/^Sec-WebSocket-Origin:/i) === 0) {
+                // Example: "Sec-WebSocket-Origin: http://example.com"
+                FWebSocketHeader['Origin'] = InLine.replace(/Sec-WebSocket-Origin:\s?/i, "");
+            } else if (InLine.search(/^Sec-WebSocket-Protocol:/i) === 0) {
+                // Example: "Sec-WebSocket-Protocol: sample"
+                FWebSocketHeader['SubProtocol'] = InLine.replace(/Sec-WebSocket-Protocol:\s?/i, "");
+            } else if (InLine.search(/^Sec-WebSocket-Draft/i) === 0) {
+				// Example: "Sec-WebSocket-Draft: 2"
+                try {
+                    FWebSocketHeader['Version'] = parseInt(InLine.replace(/Sec-WebSocket-Draft:\s?/i, ""));
+                } catch (err) {
+                    FWebSocketHeader['Version'] = 0;
+                }
+            } else if (InLine.search(/^Sec-WebSocket-Version/i) === 0) {
+				// Example: "Sec-WebSocket-Version: 8"
+                try {
+                    FWebSocketHeader['Version'] = parseInt(InLine.replace(/Sec-WebSocket-Version:\s?/i, ""));
+                } catch (err) {
+                    FWebSocketHeader['Version'] = 0;
+                }
+            } else if (InLine.search(/^Upgrade:/i) === 0) {
+				// Example: "Upgrade: websocket"
+				FWebSocketHeader['Upgrade'] = InLine.replace(/Upgrade:\s?/i, "");
+			} else if (InLine.search(/^X-Forwarded-For/i) === 0) {
+				FWebSocketHeader['X-Forwarded-For'] = InLine.replace(/X-Forwarded-For:\s?/i, "");
+			}
+        }
+    } catch (err) {
+        log(LOG_DEBUG, "ShakeHands() error: " + err.toString());
+    }
+    
+    return false;
+}
+
+function ShakeHandsDraft0() {
+	// Ensure we have all the data we need
+	if (('Key1' in FWebSocketHeader) && ('Key2' in FWebSocketHeader) && ('Host' in FWebSocketHeader) && ('Origin' in FWebSocketHeader !== "") && ('Path' in FWebSocketHeader)) {
+		// Combine Key1, Key2, and the last 8 bytes into a string that we will later hash
+		var ToHash = ""
+		ToHash += String.fromCharCode((FWebSocketHeader['Key1'] & 0xFF000000) >> 24);
+		ToHash += String.fromCharCode((FWebSocketHeader['Key1'] & 0x00FF0000) >> 16);
+		ToHash += String.fromCharCode((FWebSocketHeader['Key1'] & 0x0000FF00) >> 8);
+		ToHash += String.fromCharCode((FWebSocketHeader['Key1'] & 0x000000FF) >> 0);
+		ToHash += String.fromCharCode((FWebSocketHeader['Key2'] & 0xFF000000) >> 24);
+		ToHash += String.fromCharCode((FWebSocketHeader['Key2'] & 0x00FF0000) >> 16);
+		ToHash += String.fromCharCode((FWebSocketHeader['Key2'] & 0x0000FF00) >> 8);
+		ToHash += String.fromCharCode((FWebSocketHeader['Key2'] & 0x000000FF) >> 0);				
+		for (var i = 0; i < 8; i++) {
+			ToHash += String.fromCharCode(client.socket.recvBin(1));
+		}
+		
+		// Hash the string
+		var Hashed = md5_calc(ToHash, true);
+
+		// Setup the handshake response
+		var Response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
+					   "Upgrade: WebSocket\r\n" +
+					   "Connection: Upgrade\r\n" +
+					   "Sec-WebSocket-Origin: " + FWebSocketHeader['Origin'] + "\r\n" +
+					   "Sec-WebSocket-Location: ws://" + FWebSocketHeader['Host'] + FWebSocketHeader['Path'] + "\r\n";
+		if ('SubProtocol' in FWebSocketHeader) Response += "Sec-WebSocket-Protocol: " + FWebSocketHeader['SubProtocol'] + "\r\n";
+		Response += "\r\n";
+		
+		// Loop through the hash string (which is hex encoded) and append the individual bytes to the response
+		for (var i = 0; i < Hashed.length; i += 2) {
+			Response += String.fromCharCode(parseInt(Hashed.charAt(i) + Hashed.charAt(i + 1), 16));
+		}
+		
+		// Send the response and return
+		client.socket.send(Response);
+		return true;
+	} else {
+		// We're missing some pice of data, log what we do have
+		log(LOG_DEBUG, "Missing some piece of handshake data.  Here's what we have:");
+		for(var x in FWebSocketHeader) { 
+			log(LOG_DEBUG, x + " => " + FWebSocketHeader[x]); 
+		}
+		return false;
+	}
+}
+
+function ShakeHandsVersion7() {
+	// Ensure we have all the data we need
+	if (('Key' in FWebSocketHeader) && ('Host' in FWebSocketHeader) && ('Origin' in FWebSocketHeader !== "") && ('Path' in FWebSocketHeader)) {
+		var AcceptGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+		// Combine Key and GUID
+		var ToHash = FWebSocketHeader['Key'] + AcceptGUID;
+		
+		// Hash the string
+		var Hashed = Sha1.hash(ToHash, false);
+
+        // Encode the hash
+        var ToEncode = '';
+        for (var i = 0; i <= 38; i += 2) {
+        	ToEncode += String.fromCharCode(parseInt(Hashed.substr(i, 2), 16));
+		}
+		var Encoded = base64_encode(ToEncode);
+
+		// Setup the handshake response
+		var Response = "HTTP/1.1 101 Switching Protocols\r\n" +
+					   "Upgrade: websocket\r\n" +
+					   "Connection: Upgrade\r\n" +
+					   "Sec-WebSocket-Accept: " + Encoded + "\r\n";
+		if ('SubProtocol' in FWebSocketHeader) {
+            if (FWebSocketHeader['SubProtocol'].indexOf('binary') >= 0) {
+                FWebSocketSubProtocol = 'binary';
+            } else if (FWebSocketHeader['SubProtocol'].indexOf('plain') >= 0) {
+                FWebSocketSubProtocol = 'plain';
+            } else {
+                log(LOG_ERR, 'No supported SubProtocols found ("binary" and "plain" are only supported options');
+                return false;
+            }
+
+            log(LOG_DEBUG, 'Selecting "' + FWebSocketSubProtocol + '" SubProtocol');
+            Response += "Sec-WebSocket-Protocol: " + FWebSocketSubProtocol + "\r\n";
+        }
+		Response += "\r\n";
+		
+		// Send the response and return
+		client.socket.send(Response);
+		return true;
+	} else {
+		// We're missing some pice of data, log what we do have
+		log(LOG_DEBUG, "Missing some piece of handshake data.  Here's what we have:");
+		for(var x in FWebSocketHeader) { 
+			log(LOG_DEBUG, x + " => " + FWebSocketHeader[x]); 
+		}
+		return false;
+	}
+}
+
+function inet_pton (a) {
+    // http://kevin.vanzonneveld.net
+    // +   original by: Theriault
+    // *     example 1: inet_pton('::');
+    // *     returns 1: '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0' (binary)
+    // *     example 2: inet_pton('127.0.0.1');
+    // *     returns 2: '\x7F\x00\x00\x01' (binary)
+    var r, m, x, i, j, f = String.fromCharCode;
+    m = a.match(/^(?:\d{1,3}(?:\.|$)){4}/); // IPv4
+    if (m) {
+        m = m[0].split('.');
+        m = f(m[0]) + f(m[1]) + f(m[2]) + f(m[3]);
+        // Return if 4 bytes, otherwise false.
+        return m.length === 4 ? m : false;
+    }
+    r = /^((?:[\da-f]{1,4}(?::|)){0,8})(::)?((?:[\da-f]{1,4}(?::|)){0,8})$/;
+    m = a.match(r); // IPv6
+    if (m) {
+        // Translate each hexadecimal value.
+        for (j = 1; j < 4; j++) {
+            // Indice 2 is :: and if no length, continue.
+            if (j === 2 || m[j].length === 0) {
+                continue;
+            }
+            m[j] = m[j].split(':');
+            for (i = 0; i < m[j].length; i++) {
+                m[j][i] = parseInt(m[j][i], 16);
+                // Would be NaN if it was blank, return false.
+                if (isNaN(m[j][i])) {
+                    return false; // Invalid IP.
+                }
+                m[j][i] = f(m[j][i] >> 8) + f(m[j][i] & 0xFF);
+            }
+            m[j] = m[j].join('');
+        }
+        x = m[1].length + m[3].length;
+        if (x === 16) {
+            return m[1] + m[3];
+        } else if (x < 16 && m[2].length > 0) {
+            return m[1] + (new Array(16 - x + 1)).join('\x00') + m[3];
+        }
+    }
+    return false; // Invalid IP.
+}