summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js660
1 files changed, 660 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js b/toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js
new file mode 100644
index 0000000000..6ebd9fbfcc
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js
@@ -0,0 +1,660 @@
+"use strict";
+
+/* globals TCPServerSocket */
+
+const CC = Components.Constructor;
+
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const currentThread =
+ Cc["@mozilla.org/thread-manager;1"].getService().currentThread;
+
+// Most of the socks logic here is copied and upgraded to support authentication
+// for socks5. The original test is from netwerk/test/unit/test_socks.js
+
+// Socks 4 support was left in place for future tests.
+
+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_SOCKS5_AUTH = 7;
+const STATE_WAIT_INPUT = 8;
+const STATE_FINISHED = 9;
+
+/**
+ * A basic socks proxy setup that handles a single http response page. This
+ * is used for testing socks auth with webrequest. We don't bother making
+ * sure we buffer ondata, etc., we'll never get anything but tiny chunks here.
+ */
+class SocksClient {
+ constructor(server, socket) {
+ this.server = server;
+ this.type = "";
+ this.username = "";
+ this.dest_name = "";
+ this.dest_addr = [];
+ this.dest_port = [];
+
+ this.inbuf = [];
+ this.state = STATE_WAIT_GREETING;
+ this.socket = socket;
+
+ socket.onclose = event => {
+ this.server.requestCompleted(this);
+ };
+ socket.ondata = event => {
+ let len = event.data.byteLength;
+
+ if (len == 0 && this.state == STATE_FINISHED) {
+ this.close();
+ this.server.requestCompleted(this);
+ return;
+ }
+
+ this.inbuf = new Uint8Array(event.data);
+ Promise.resolve().then(() => {
+ this.callState();
+ });
+ };
+ }
+
+ callState() {
+ 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_SOCKS5_AUTH:
+ this.checkSocks5Auth();
+ break;
+ case STATE_WAIT_INPUT:
+ this.checkRequest();
+ break;
+ default:
+ do_throw("server: read in invalid state!");
+ }
+ }
+
+ write(buf) {
+ this.socket.send(new Uint8Array(buf).buffer);
+ }
+
+ checkSocksGreeting() {
+ if (!this.inbuf.length) {
+ return;
+ }
+
+ if (this.inbuf[0] == 4) {
+ this.type = "socks4";
+ this.state = STATE_WAIT_SOCKS4_REQUEST;
+ this.checkSocks4Request();
+ } else if (this.inbuf[0] == 5) {
+ this.type = "socks";
+ this.state = STATE_WAIT_SOCKS5_GREETING;
+ this.checkSocks5Greeting();
+ } else {
+ do_throw("Unknown socks protocol!");
+ }
+ }
+
+ checkSocks4Request() {
+ if (this.inbuf.length < 8) {
+ return;
+ }
+
+ 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() {
+ let i = this.inbuf.indexOf(0);
+ let str = null;
+
+ if (i >= 0) {
+ let decoder = new TextDecoder();
+ str = decoder.decode(this.inbuf.slice(0, i));
+ this.inbuf = this.inbuf.slice(i + 1);
+ }
+
+ return str;
+ }
+
+ checkSocks4Username() {
+ let 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() {
+ let str = this.readString();
+
+ if (str == null) {
+ return;
+ }
+
+ this.dest_name = str;
+ this.sendSocks4Response();
+ }
+
+ sendSocks4Response() {
+ this.state = STATE_WAIT_INPUT;
+ this.inbuf = [];
+ this.write([0, 0x5a, 0, 0, 0, 0, 0, 0]);
+ }
+
+ /**
+ * checks authentication information.
+ *
+ * buf[0] socks version
+ * buf[1] number of auth methods supported
+ * buf[2+nmethods] value for each auth method
+ *
+ * Response is
+ * byte[0] socks version
+ * byte[1] desired auth method
+ *
+ * For whatever reason, Firefox does not present auth method 0x02 however
+ * responding with that does cause Firefox to send authentication if
+ * the nsIProxyInfo instance has the data. IUUC Firefox should send
+ * supported methods, but I'm no socks expert.
+ */
+ checkSocks5Greeting() {
+ if (this.inbuf.length < 2) {
+ return;
+ }
+ let nmethods = this.inbuf[1];
+ if (this.inbuf.length < 2 + nmethods) {
+ return;
+ }
+
+ // See comment above, keeping for future update.
+ // let methods = this.inbuf.slice(2, 2 + nmethods);
+
+ this.inbuf = [];
+ if (this.server.password || this.server.username) {
+ this.state = STATE_WAIT_SOCKS5_AUTH;
+ this.write([5, 2]);
+ } else {
+ this.state = STATE_WAIT_SOCKS5_REQUEST;
+ this.write([5, 0]);
+ }
+ }
+
+ checkSocks5Auth() {
+ equal(this.inbuf[0], 0x01, "subnegotiation version");
+ let uname_len = this.inbuf[1];
+ let pass_len = this.inbuf[2 + uname_len];
+ let unnamebuf = this.inbuf.slice(2, 2 + uname_len);
+ let pass_start = 2 + uname_len + 1;
+ let pwordbuf = this.inbuf.slice(pass_start, pass_start + pass_len);
+ let decoder = new TextDecoder();
+ let username = decoder.decode(unnamebuf);
+ let password = decoder.decode(pwordbuf);
+ this.inbuf = [];
+ equal(username, this.server.username, "socks auth username");
+ equal(password, this.server.password, "socks auth password");
+ if (username == this.server.username && password == this.server.password) {
+ this.state = STATE_WAIT_SOCKS5_REQUEST;
+ // x00 is success, any other value closes the connection
+ this.write([1, 0]);
+ return;
+ }
+ this.state = STATE_FINISHED;
+ this.write([1, 1]);
+ }
+
+ checkSocks5Request() {
+ if (this.inbuf.length < 4) {
+ return;
+ }
+
+ let atype = this.inbuf[3];
+ let len;
+ let 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;
+ }
+
+ let buf = this.inbuf.slice(5, 5 + len);
+ let decoder = new TextDecoder();
+ this.dest_name = decoder.decode(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() {
+ let buf;
+ if (this.dest_addr.length == 16) {
+ // send a successful response with the address, [::1]:80
+ buf = [5, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 80];
+ } else {
+ // send a successful response with the address, 127.0.0.1:80
+ buf = [5, 0, 0, 1, 127, 0, 0, 1, 0, 80];
+ }
+ this.state = STATE_WAIT_INPUT;
+ this.inbuf = [];
+ this.write(buf);
+ }
+
+ checkRequest() {
+ let decoder = new TextDecoder();
+ let request = decoder.decode(this.inbuf);
+
+ if (request == "PING!") {
+ this.state = STATE_FINISHED;
+ this.socket.send("PONG!");
+ } else if (request.startsWith("GET / HTTP/1.1")) {
+ this.socket.send(
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 2\r\n" +
+ "Content-Type: text/html\r\n" +
+ "\r\nOK"
+ );
+ this.state = STATE_FINISHED;
+ }
+ }
+
+ close() {
+ this.socket.close();
+ }
+}
+
+class SocksTestServer {
+ constructor() {
+ this.client_connections = new Set();
+ this.listener = new TCPServerSocket(-1, { binaryType: "arraybuffer" }, -1);
+ this.listener.onconnect = event => {
+ let client = new SocksClient(this, event.socket);
+ this.client_connections.add(client);
+ };
+ }
+
+ requestCompleted(client) {
+ this.client_connections.delete(client);
+ }
+
+ close() {
+ for (let client of this.client_connections) {
+ client.close();
+ }
+ this.client_connections = new Set();
+ if (this.listener) {
+ this.listener.close();
+ this.listener = null;
+ }
+ }
+
+ setUserPass(username, password) {
+ this.username = username;
+ this.password = password;
+ }
+}
+
+/**
+ * Tests the basic socks logic using a simple socket connection and the
+ * protocol proxy service. Before 902346, TCPSocket has no way to tie proxy
+ * data to it, so we go old school here.
+ */
+class SocksTestClient {
+ constructor(socks, dest, resolve, reject) {
+ let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
+ Ci.nsIProtocolProxyService
+ );
+ let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+
+ let pi_flags = 0;
+ if (socks.dns == "remote") {
+ pi_flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+ }
+
+ let pi = pps.newProxyInfoWithAuth(
+ socks.version,
+ socks.host,
+ socks.port,
+ socks.username,
+ socks.password,
+ "",
+ "",
+ pi_flags,
+ -1,
+ null
+ );
+
+ this.trans = sts.createTransport([], dest.host, dest.port, pi, null);
+ this.input = this.trans.openInputStream(
+ Ci.nsITransport.OPEN_BLOCKING,
+ 0,
+ 0
+ );
+ this.output = this.trans.openOutputStream(
+ Ci.nsITransport.OPEN_BLOCKING,
+ 0,
+ 0
+ );
+ this.outbuf = String();
+ this.resolve = resolve;
+ this.reject = reject;
+
+ this.write("PING!");
+ this.input.asyncWait(this, 0, 0, currentThread);
+ }
+
+ onInputStreamReady(stream) {
+ let len = 0;
+ try {
+ len = stream.available();
+ } catch (e) {
+ // This will happen on auth failure.
+ this.reject(e);
+ return;
+ }
+ let bin = new BinaryInputStream(stream);
+ let data = bin.readByteArray(len);
+ let decoder = new TextDecoder();
+ let result = decoder.decode(data);
+ if (result == "PONG!") {
+ this.resolve(result);
+ } else {
+ this.reject();
+ }
+ }
+
+ write(buf) {
+ this.outbuf += buf;
+ this.output.asyncWait(this, 0, 0, currentThread);
+ }
+
+ onOutputStreamReady(stream) {
+ let len = stream.write(this.outbuf, this.outbuf.length);
+ if (len != this.outbuf.length) {
+ this.outbuf = this.outbuf.substring(len);
+ stream.asyncWait(this, 0, 0, currentThread);
+ } else {
+ this.outbuf = String();
+ }
+ }
+
+ close() {
+ this.output.close();
+ }
+}
+
+const socksServer = new SocksTestServer();
+socksServer.setUserPass("foo", "bar");
+registerCleanupFunction(() => {
+ socksServer.close();
+});
+
+// A simple ping/pong to test the socks server.
+add_task(async function test_socks_server() {
+ let socks = {
+ version: "socks",
+ host: "127.0.0.1",
+ port: socksServer.listener.localPort,
+ username: "foo",
+ password: "bar",
+ dns: false,
+ };
+ let dest = {
+ host: "localhost",
+ port: 8888,
+ };
+
+ new Promise((resolve, reject) => {
+ new SocksTestClient(socks, dest, resolve, reject);
+ })
+ .then(result => {
+ equal("PONG!", result, "socks test ok");
+ })
+ .catch(result => {
+ ok(false, `socks test failed ${result}`);
+ });
+});
+
+// Register a proxy to be used by TCPSocket connections later.
+function registerProxy(socks) {
+ let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
+ Ci.nsIProtocolProxyService
+ );
+ let filter = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]),
+ applyFilter(uri, proxyInfo, callback) {
+ callback.onProxyFilterResult(
+ pps.newProxyInfoWithAuth(
+ socks.version,
+ socks.host,
+ socks.port,
+ socks.username,
+ socks.password,
+ "",
+ "",
+ socks.dns == "remote"
+ ? Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST
+ : 0,
+ -1,
+ null
+ )
+ );
+ },
+ };
+ pps.registerFilter(filter, 0);
+ registerCleanupFunction(() => {
+ pps.unregisterFilter(filter);
+ });
+}
+
+// A simple ping/pong to test the socks server with TCPSocket.
+add_task(async function test_tcpsocket_proxy() {
+ let socks = {
+ version: "socks",
+ host: "127.0.0.1",
+ port: socksServer.listener.localPort,
+ username: "foo",
+ password: "bar",
+ dns: false,
+ };
+ let dest = {
+ host: "localhost",
+ port: 8888,
+ };
+
+ registerProxy(socks);
+ await new Promise((resolve, reject) => {
+ let client = new TCPSocket(dest.host, dest.port);
+ client.onopen = () => {
+ client.send("PING!");
+ };
+ client.ondata = e => {
+ equal("PONG!", e.data, "socks test ok");
+ resolve();
+ };
+ client.onerror = () => reject();
+ });
+});
+
+add_task(async function test_webRequest_socks_proxy() {
+ async function background(port) {
+ function checkProxyData(details) {
+ browser.test.assertEq("127.0.0.1", details.proxyInfo.host, "proxy host");
+ browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
+ browser.test.assertEq("socks", details.proxyInfo.type, "proxy type");
+ browser.test.assertEq(
+ "foo",
+ details.proxyInfo.username,
+ "proxy username not set"
+ );
+ browser.test.assertEq(
+ undefined,
+ details.proxyInfo.password,
+ "no proxy password passed to webrequest"
+ );
+ }
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ checkProxyData(details);
+ },
+ { urls: ["<all_urls>"] }
+ );
+ browser.webRequest.onAuthRequired.addListener(
+ details => {
+ // We should never get onAuthRequired for socks proxy
+ browser.test.fail("onAuthRequired");
+ },
+ { urls: ["<all_urls>"] },
+ ["blocking"]
+ );
+ browser.webRequest.onCompleted.addListener(
+ details => {
+ checkProxyData(details);
+ browser.test.sendMessage("done");
+ },
+ { urls: ["<all_urls>"] }
+ );
+ browser.proxy.onRequest.addListener(
+ () => {
+ return [
+ {
+ type: "socks",
+ host: "127.0.0.1",
+ port,
+ username: "foo",
+ password: "bar",
+ },
+ ];
+ },
+ { urls: ["<all_urls>"] }
+ );
+ }
+
+ let handlingExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["proxy", "webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+ background: `(${background})(${socksServer.listener.localPort})`,
+ });
+
+ // proxy.register is deprecated - bug 1443259.
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await handlingExt.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ `http://localhost/`
+ );
+
+ await handlingExt.awaitMessage("done");
+ await contentPage.close();
+ await handlingExt.unload();
+});
+
+add_task(async function test_onRequest_tcpsocket_proxy() {
+ async function background(port) {
+ browser.proxy.onRequest.addListener(
+ () => {
+ return [
+ {
+ type: "socks",
+ host: "127.0.0.1",
+ port,
+ username: "foo",
+ password: "bar",
+ },
+ ];
+ },
+ { urls: ["<all_urls>"] }
+ );
+ }
+
+ let handlingExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["proxy", "webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+ background: `(${background})(${socksServer.listener.localPort})`,
+ });
+
+ await handlingExt.startup();
+
+ await new Promise((resolve, reject) => {
+ let client = new TCPSocket("localhost", 8888);
+ client.onopen = () => {
+ client.send("PING!");
+ };
+ client.ondata = e => {
+ equal("PONG!", e.data, "socks test ok");
+ resolve();
+ };
+ client.onerror = () => reject();
+ });
+
+ await handlingExt.unload();
+});