diff options
Diffstat (limited to 'netwerk/test/unit/test_socks.js')
-rw-r--r-- | netwerk/test/unit/test_socks.js | 525 |
1 files changed, 525 insertions, 0 deletions
diff --git a/netwerk/test/unit/test_socks.js b/netwerk/test/unit/test_socks.js new file mode 100644 index 000000000..57c947979 --- /dev/null +++ b/netwerk/test/unit/test_socks.js @@ -0,0 +1,525 @@ +var CC = Components.Constructor; + +const ServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init"); +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); +const DirectoryService = CC("@mozilla.org/file/directory_service;1", + "nsIProperties"); +const Process = CC("@mozilla.org/process/util;1", "nsIProcess", "init"); + +const currentThread = Cc["@mozilla.org/thread-manager;1"] + .getService().currentThread; + +var socks_test_server = null; +var socks_listen_port = -1; + +function getAvailableBytes(input) +{ + var len = 0; + + try { + len = input.available(); + } catch (e) { + } + + return len; +} + +function runScriptSubprocess(script, args) +{ + var ds = new DirectoryService(); + var bin = ds.get("XREExeF", Ci.nsILocalFile); + if (!bin.exists()) { + do_throw("Can't find xpcshell binary"); + } + + var script = do_get_file(script); + var proc = new Process(bin); + var args = [script.path].concat(args); + + proc.run(false, args, args.length); + + return proc; +} + +function buf2ip(buf) +{ + if (buf.length == 16) { + var ip = (buf[0] << 4 | buf[1]).toString(16) + ':' + + (buf[2] << 4 | buf[3]).toString(16) + ':' + + (buf[4] << 4 | buf[5]).toString(16) + ':' + + (buf[6] << 4 | buf[7]).toString(16) + ':' + + (buf[8] << 4 | buf[9]).toString(16) + ':' + + (buf[10] << 4 | buf[11]).toString(16) + ':' + + (buf[12] << 4 | buf[13]).toString(16) + ':' + + (buf[14] << 4 | buf[15]).toString(16); + for (var i = 8; i >= 2; i--) { + var re = new RegExp("(^|:)(0(:|$)){" + i + "}"); + var shortip = ip.replace(re, '::'); + if (shortip != ip) { + return shortip; + } + } + return ip; + } else { + return buf.join('.'); + } +} + +function buf2int(buf) +{ + var n = 0; + + for (var i in buf) { + n |= buf[i] << ((buf.length - i - 1) * 8); + } + + return n; +} + +function buf2str(buf) +{ + return String.fromCharCode.apply(null, buf); +} + +const STATE_WAIT_GREETING = 1; +const STATE_WAIT_SOCKS4_REQUEST = 2; +const STATE_WAIT_SOCKS4_USERNAME = 3; +const STATE_WAIT_SOCKS4_HOSTNAME = 4; +const STATE_WAIT_SOCKS5_GREETING = 5; +const STATE_WAIT_SOCKS5_REQUEST = 6; +const STATE_WAIT_PONG = 7; +const STATE_GOT_PONG = 8; + +function SocksClient(server, client_in, client_out) +{ + this.server = server; + this.type = ''; + this.username = ''; + this.dest_name = ''; + this.dest_addr = []; + this.dest_port = []; + + this.client_in = client_in; + this.client_out = client_out; + this.inbuf = []; + this.outbuf = String(); + this.state = STATE_WAIT_GREETING; + this.waitRead(this.client_in); +} +SocksClient.prototype = { + onInputStreamReady: function(input) + { + var len = getAvailableBytes(input); + + if (len == 0) { + print('server: client closed!'); + do_check_eq(this.state, STATE_GOT_PONG); + this.server.testCompleted(this); + return; + } + + var bin = new BinaryInputStream(input); + var data = bin.readByteArray(len); + this.inbuf = this.inbuf.concat(data); + + switch (this.state) { + case STATE_WAIT_GREETING: + this.checkSocksGreeting(); + break; + case STATE_WAIT_SOCKS4_REQUEST: + this.checkSocks4Request(); + break; + case STATE_WAIT_SOCKS4_USERNAME: + this.checkSocks4Username(); + break; + case STATE_WAIT_SOCKS4_HOSTNAME: + this.checkSocks4Hostname(); + break; + case STATE_WAIT_SOCKS5_GREETING: + this.checkSocks5Greeting(); + break; + case STATE_WAIT_SOCKS5_REQUEST: + this.checkSocks5Request(); + break; + case STATE_WAIT_PONG: + this.checkPong(); + break; + default: + do_throw("server: read in invalid state!"); + } + + this.waitRead(input); + }, + + onOutputStreamReady: function(output) + { + var len = output.write(this.outbuf, this.outbuf.length); + if (len != this.outbuf.length) { + this.outbuf = this.outbuf.substring(len); + this.waitWrite(output); + } else + this.outbuf = String(); + }, + + waitRead: function(input) + { + input.asyncWait(this, 0, 0, currentThread); + }, + + waitWrite: function(output) + { + output.asyncWait(this, 0, 0, currentThread); + }, + + write: function(buf) + { + this.outbuf += buf; + this.waitWrite(this.client_out); + }, + + checkSocksGreeting: function() + { + if (this.inbuf.length == 0) + return; + + if (this.inbuf[0] == 4) { + print('server: got socks 4'); + this.type = 'socks4'; + this.state = STATE_WAIT_SOCKS4_REQUEST; + this.checkSocks4Request(); + } else if (this.inbuf[0] == 5) { + print('server: got socks 5'); + this.type = 'socks'; + this.state = STATE_WAIT_SOCKS5_GREETING; + this.checkSocks5Greeting(); + } else { + do_throw("Unknown socks protocol!"); + } + }, + + checkSocks4Request: function() + { + if (this.inbuf.length < 8) + return; + + do_check_eq(this.inbuf[1], 0x01); + + this.dest_port = this.inbuf.slice(2, 4); + this.dest_addr = this.inbuf.slice(4, 8); + + this.inbuf = this.inbuf.slice(8); + this.state = STATE_WAIT_SOCKS4_USERNAME; + this.checkSocks4Username(); + }, + + readString: function() + { + var i = this.inbuf.indexOf(0); + var str = null; + + if (i >= 0) { + var buf = this.inbuf.slice(0,i); + str = buf2str(buf); + this.inbuf = this.inbuf.slice(i+1); + } + + return str; + }, + + checkSocks4Username: function() + { + var str = this.readString(); + + if (str == null) + return; + + this.username = str; + if (this.dest_addr[0] == 0 && + this.dest_addr[1] == 0 && + this.dest_addr[2] == 0 && + this.dest_addr[3] != 0) { + this.state = STATE_WAIT_SOCKS4_HOSTNAME; + this.checkSocks4Hostname(); + } else { + this.sendSocks4Response(); + } + }, + + checkSocks4Hostname: function() + { + var str = this.readString(); + + if (str == null) + return; + + this.dest_name = str; + this.sendSocks4Response(); + }, + + sendSocks4Response: function() + { + this.outbuf = '\x00\x5a\x00\x00\x00\x00\x00\x00'; + this.sendPing(); + }, + + checkSocks5Greeting: function() + { + if (this.inbuf.length < 2) + return; + var nmethods = this.inbuf[1]; + if (this.inbuf.length < 2 + nmethods) + return; + + do_check_true(nmethods >= 1); + var methods = this.inbuf.slice(2, 2 + nmethods); + do_check_true(0 in methods); + + this.inbuf = []; + this.state = STATE_WAIT_SOCKS5_REQUEST; + this.write('\x05\x00'); + }, + + checkSocks5Request: function() + { + if (this.inbuf.length < 4) + return; + + do_check_eq(this.inbuf[0], 0x05); + do_check_eq(this.inbuf[1], 0x01); + do_check_eq(this.inbuf[2], 0x00); + + var atype = this.inbuf[3]; + var len; + var name = false; + + switch (atype) { + case 0x01: + len = 4; + break; + case 0x03: + len = this.inbuf[4]; + name = true; + break; + case 0x04: + len = 16; + break; + default: + do_throw("Unknown address type " + atype); + } + + if (name) { + if (this.inbuf.length < 4 + len + 1 + 2) + return; + + buf = this.inbuf.slice(5, 5 + len); + this.dest_name = buf2str(buf); + len += 1; + } else { + if (this.inbuf.length < 4 + len + 2) + return; + + this.dest_addr = this.inbuf.slice(4, 4 + len); + } + + len += 4; + this.dest_port = this.inbuf.slice(len, len + 2); + this.inbuf = this.inbuf.slice(len + 2); + this.sendSocks5Response(); + }, + + sendSocks5Response: function() + { + if (this.dest_addr.length == 16) { + // send a successful response with the address, [::1]:80 + this.outbuf += '\x05\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x80'; + } else { + // send a successful response with the address, 127.0.0.1:80 + this.outbuf += '\x05\x00\x00\x01\x7f\x00\x00\x01\x00\x80'; + } + this.sendPing(); + }, + + sendPing: function() + { + print('server: sending ping'); + this.state = STATE_WAIT_PONG; + this.outbuf += "PING!"; + this.inbuf = []; + this.waitWrite(this.client_out); + this.waitRead(this.client_in); + }, + + checkPong: function() + { + var pong = buf2str(this.inbuf); + do_check_eq(pong, "PONG!"); + this.state = STATE_GOT_PONG; + this.waitRead(this.client_in); + }, + + close: function() + { + this.client_in.close(); + this.client_out.close(); + } +}; + +function SocksTestServer() +{ + this.listener = ServerSocket(-1, true, -1); + socks_listen_port = this.listener.port; + print('server: listening on', socks_listen_port); + this.listener.asyncListen(this); + this.test_cases = []; + this.client_connections = []; + this.client_subprocess = null; + // port is used as the ID for test cases + this.test_port_id = 8000; + this.tests_completed = 0; +} +SocksTestServer.prototype = { + addTestCase: function(test) + { + test.finished = false; + test.port = this.test_port_id++; + this.test_cases.push(test); + }, + + pickTest: function(id) + { + for (var i in this.test_cases) { + var test = this.test_cases[i]; + if (test.port == id) { + this.tests_completed++; + return test; + } + } + do_throw("No test case with id " + id); + }, + + testCompleted: function(client) + { + var port_id = buf2int(client.dest_port); + var test = this.pickTest(port_id); + + print('server: test finished', test.port); + do_check_true(test != null); + do_check_eq(test.expectedType || test.type, client.type); + do_check_eq(test.port, port_id); + + if (test.remote_dns) + do_check_eq(test.host, client.dest_name); + else + do_check_eq(test.host, buf2ip(client.dest_addr)); + + if (this.test_cases.length == this.tests_completed) { + print('server: all tests completed'); + this.close(); + do_test_finished(); + } + }, + + runClientSubprocess: function() + { + var argv = []; + + // marshaled: socks_ver|server_port|dest_host|dest_port|<remote|local> + for (var test of this.test_cases) { + var arg = test.type + '|' + + String(socks_listen_port) + '|' + + test.host + '|' + test.port + '|'; + if (test.remote_dns) + arg += 'remote'; + else + arg += 'local'; + print('server: using test case', arg); + argv.push(arg); + } + + this.client_subprocess = runScriptSubprocess( + 'socks_client_subprocess.js', argv); + }, + + onSocketAccepted: function(socket, trans) + { + print('server: got client connection'); + var input = trans.openInputStream(0, 0, 0); + var output = trans.openOutputStream(0, 0, 0); + var client = new SocksClient(this, input, output); + this.client_connections.push(client); + }, + + onStopListening: function(socket) + { + }, + + close: function() + { + if (this.client_subprocess) { + try { + this.client_subprocess.kill(); + } catch (x) { + do_note_exception(x, 'Killing subprocess failed'); + } + this.client_subprocess = null; + } + for (var client of this.client_connections) + client.close(); + this.client_connections = []; + if (this.listener) { + this.listener.close(); + this.listener = null; + } + } +}; + +function test_timeout() +{ + socks_test_server.close(); + do_throw("SOCKS test took too long!"); +} + +function run_test() +{ + socks_test_server = new SocksTestServer(); + + socks_test_server.addTestCase({ + type: "socks4", + host: '127.0.0.1', + remote_dns: false, + }); + socks_test_server.addTestCase({ + type: "socks4", + host: '12345.xxx', + remote_dns: true, + }); + socks_test_server.addTestCase({ + type: "socks4", + expectedType: "socks", + host: '::1', + remote_dns: false, + }); + socks_test_server.addTestCase({ + type: "socks", + host: '127.0.0.1', + remote_dns: false, + }); + socks_test_server.addTestCase({ + type: "socks", + host: 'abcdefg.xxx', + remote_dns: true, + }); + socks_test_server.addTestCase({ + type: "socks", + host: '::1', + remote_dns: false, + }); + socks_test_server.runClientSubprocess(); + + do_timeout(120 * 1000, test_timeout); + do_test_pending(); +} |