From fd758dd3357355399a573a3e3d9520dc838705b6 Mon Sep 17 00:00:00 2001 From: Joel Martin Date: Sun, 7 Nov 2010 19:06:20 -0600 Subject: [PATCH] First pass at wsproxy using node (node.js). Node: http://nodejs.org/ https://github.com/ry/node It mostly works, but it eventually gets an error from the target which is probably due to missing support for re-assembly of client WebSockets frames. --- utils/README.md | 16 +++- utils/wsproxy.js | 235 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 utils/wsproxy.js diff --git a/utils/README.md b/utils/README.md index 853cd817..b0ed6018 100644 --- a/utils/README.md +++ b/utils/README.md @@ -40,8 +40,8 @@ These are not necessary for the basic operation. ### Implementations -There are two implementations of wsproxy included: a python -implementation and a C implementation. +There are three implementations of wsproxy included: python, C, and +Node (node.js). Here is the feature support matrix for the wsproxy implementations: @@ -50,6 +50,7 @@ Here is the feature support matrix for the wsproxy implementations: Implementation Basic Proxying + Multi-process Daemonizing SSL/wss Flash Policy Server @@ -58,6 +59,7 @@ Here is the feature support matrix for the wsproxy implementations: python yes yes + yes yes 1 yes yes @@ -67,6 +69,16 @@ Here is the feature support matrix for the wsproxy implementations: yes yes yes + yes + no + + + Node (node.js) + yes + yes + no + no + no no diff --git a/utils/wsproxy.js b/utils/wsproxy.js new file mode 100644 index 00000000..7d3a9220 --- /dev/null +++ b/utils/wsproxy.js @@ -0,0 +1,235 @@ +// A WebSocket to TCP socket proxy +// Copyright 2010 Joel Martin +// Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + +var net = require('net'), + sys = require('sys'), + crypto = require('crypto'), + source_arg, source_host, source_port, + target_arg, target_host, target_port; + +// md5 calculation borrowed from Socket.IO (MIT license) +function gen_md5(headers, k3) { + var k1 = headers['sec-websocket-key1'], + k2 = headers['sec-websocket-key2'], + md5 = crypto.createHash('md5'); + + [k1, k2].forEach(function(k){ + var n = parseInt(k.replace(/[^\d]/g, '')), + spaces = k.replace(/[^ ]/g, '').length; + + if (spaces === 0 || n % spaces !== 0){ + return false; + } + + n /= spaces; + + md5.update(String.fromCharCode( + n >> 24 & 0xFF, + n >> 16 & 0xFF, + n >> 8 & 0xFF, + n & 0xFF)); + }); + + md5.update(k3.toString('binary')); + + return md5.digest('binary'); +} + +function encode(buf) { + return String.fromCharCode(0) + + buf.toString('base64', 0) + + String.fromCharCode(255); +} + + +function decode(str) { + var buf = new Buffer(str.length); + len = buf.write(str.substring(1, str.length-1), 0, 'base64'); + return buf.toString('binary', 0, len); +} + + +var server = net.createServer(function (client) { + var handshake = "", headers = {}, header, + version, path, k1, k2, k3, target = null; + + function do_handshake(data) { + var i, idx, dlen = data.length, lines, location, rheaders, + sec_hdr; + //sys.log("received handshake data: " + data); + handshake += data.toString('utf8'); + if ((data[dlen-12] != 13) || + (data[dlen-11] != 10) || + (data[dlen-10] != 13) || + (data[dlen-9] != 10)) { + //sys.log("Got partial handshake"); + return; + } + //sys.log("Got whole handshake"); + + if (handshake.indexOf('GET ') != 0) { + sys.error("Got invalid handshake"); + client.end(); + return; + } + + lines = handshake.split('\r\n'); + path = lines[0].split(' ')[1]; + //sys.log("path: " + path); + + k3 = data.slice(dlen-8, dlen); + for (i = 1; i < lines.length; i++) { + //sys.log("lines[i]: " + lines[i]); + if (lines[i].length == 0) { break; } + idx = lines[i].indexOf(': '); + if (idx < 0) { + sys.error("Got invalid handshake header"); + client.end(); + return; + } + header = lines[i].substring(0, idx).toLowerCase(); + headers[header] = lines[i].substring(idx+2); + } + //console.dir(headers); + //sys.log("k3: " + k3 + ", k3.length: " + k3.length); + + if (headers.upgrade !== 'WebSocket') { + sys.error("Upgrade header is not 'WebSocket'"); + client.end(); + return; + } + + location = (headers.origin.substr(0, 5) == 'https' ? 'wss' : 'ws') + + '://' + headers.host + path; + //sys.log("location: " + location); + + if ('sec-websocket-key1' in headers) { + version = 76; + sec_hdr = "Sec-"; + } else { + version = 75; + sec_hdr = ""; + } + sys.log("using protocol version " + version); + + rheaders = [ + 'HTTP/1.1 101 WebSocket Protocol Handshake', + 'Upgrade: WebSocket', + 'Connection: Upgrade', + sec_hdr + 'WebSocket-Origin: ' + headers.origin, + sec_hdr + 'WebSocket-Location: ' + location + ]; + if ('sec-websocket-protocol' in headers) { + rheaders.push('Sec-WebSocket-Protocol: ' + headers['sec-websocket-protocol']); + } + rheaders.push(''); + if (version === 76) { + rheaders.push(gen_md5(headers, k3)); + } + + // Switch listener to normal data path + client.on('data', client_data); + client.setEncoding('utf8'); + client.removeListener('data', do_handshake); + // Do not delay writes + client.setNoDelay(true); + + // Send the handshake response + try { + //sys.log("response: " + rheaders.join('\r\n')); + client.write(rheaders.join('\r\n'), 'binary'); + } catch(e) { + sys.error("Failed to send handshake response"); + client.end(); + return; + } + + // Create a connection to the target + target = net.createConnection(target_port, target_host); + target.on('data', target_data); + target.on('end', function () { + sys.log("received target end"); + client.end(); + if (target) { + target.end(); + target = null; + } + }); + } + + function client_data(data) { + //sys.log("received client data: " + data); + //sys.log(" decoded: " + decode(data)); + try { + target.write(decode(data), 'binary'); + } catch(e) { + sys.log("fatal error writing to target"); + client.end(); + if (target) { + target.end(); + target = null; + } + } + } + + function target_data(data) { + //sys.log("received target data: " + data); + //sys.log(" encoded: " + encode(data)); + try { + client.write(encode(data), 'binary'); + } catch(e) { + sys.log("fatal error writing to client"); + client.end(); + target.end(); + target = null; + } + } + + client.on('connect', function () { + sys.log("Got client connection"); + }); + client.on('data', do_handshake); + client.on('end', function () { + sys.log("recieved client end"); + client.end(); + if (target) { + target.end(); + target = null; + } + }); +}); + + +// parse source and target into parts +source_arg = process.argv[2]; +target_arg = process.argv[3]; +try { + var idx; + idx = source_arg.indexOf(":"); + if (idx >= 0) { + source_host = source_arg.substring(0, idx); + source_port = parseInt(source_arg.substring(idx+1), 10); + } else { + source_host = ""; + source_port = parseInt(source_arg, 10); + } + + idx = target_arg.indexOf(":"); + if (idx < 0) { + throw("target must be host:port"); + } + target_host = target_arg.substring(0, idx); + target_port = parseInt(target_arg.substring(idx+1), 10); + + if (isNaN(source_port) || isNaN(target_port)) { + throw("illegal port"); + } +} catch(e) { + console.error("wsproxy.py [source_addr:]source_port target_addr:target_port"); + process.exit(2); +} + +sys.log("source: " + source_host + ":" + source_port); +sys.log("target: " + target_host + ":" + target_port); +server.listen(source_port, source_host);