Bug 1381290 add a socks authentication test, r?kmag draft
authorShane Caraveo <scaraveo@mozilla.com>
Sun, 10 Sep 2017 22:52:28 -0700
changeset 662199 e3a080974b59cce98ec7a8bafa982b3312da1dc9
parent 662198 c9091c5d3b3f42c462cda529483877794fedbbad
child 730779 52b9acd2fcfa2c48f7b47aa6312a0fe748a1cc12
push id78989
push usermixedpuppy@gmail.com
push dateMon, 11 Sep 2017 06:59:57 +0000
reviewerskmag
bugs1381290
milestone57.0a1
Bug 1381290 add a socks authentication test, r?kmag MozReview-Commit-ID: 6zMcdYLwqln
toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_proxy_socks.js
@@ -0,0 +1,536 @@
+"use strict";
+
+const CC = Components.Constructor;
+
+const ServerSocket = CC("@mozilla.org/network/server-socket;1",
+                        "nsIServerSocket",
+                        "init");
+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.
+
+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_SOCKS5_AUTH = 7;
+const STATE_WAIT_INPUT = 8;
+const STATE_FINISHED = 9;
+
+class SocksClient {
+  constructor(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);
+  }
+
+  onInputStreamReady(input) {
+    let len = 0;
+    try {
+      len = input.available();
+    } catch (e) {}
+
+    if (len == 0 && this.state == STATE_FINISHED) {
+      this.close();
+      this.server.requestCompleted(this);
+      return;
+    }
+
+    let bin = new BinaryInputStream(input);
+    let 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_SOCKS5_AUTH:
+        this.checkSocks5Auth();
+        break;
+      case STATE_WAIT_INPUT:
+        this.checkRequest();
+        break;
+      default:
+        do_throw("server: read in invalid state!");
+    }
+
+    this.waitRead(input);
+  }
+
+  onOutputStreamReady(output) {
+    let 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(input) {
+    input.asyncWait(this, 0, 0, currentThread);
+  }
+
+  waitWrite(output) {
+    output.asyncWait(this, 0, 0, currentThread);
+  }
+
+  write(buf) {
+    this.outbuf += buf;
+    this.waitWrite(this.client_out);
+  }
+
+  checkSocksGreeting() {
+    if (this.inbuf.length == 0) {
+      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 buf = this.inbuf.slice(0, i);
+      str = buf2str(buf);
+      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.outbuf = "\x00\x5a\x00\x00\x00\x00\x00\x00";
+    this.state = STATE_WAIT_INPUT;
+    this.inbuf = [];
+    this.waitWrite(this.client_out);
+    this.waitRead(this.client_in);
+  }
+
+  /**
+   * 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("\x05\x02");
+    } else {
+      this.state = STATE_WAIT_SOCKS5_REQUEST;
+      this.write("\x05\x00");
+    }
+  }
+
+  checkSocks5Auth() {
+    do_check_eq(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 username = buf2str(unnamebuf);
+    let password = buf2str(pwordbuf);
+    this.inbuf = [];
+    if (username == this.server.username && password == this.server.password) {
+      this.state = STATE_WAIT_SOCKS5_REQUEST;
+      // x00 is success, any other value close connection
+      this.write("\x01\x00");
+      return;
+    }
+    this.state = STATE_FINISHED;
+    this.write("\x01\x01");
+  }
+
+  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);
+      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() {
+    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.state = STATE_WAIT_INPUT;
+    this.inbuf = [];
+    this.waitWrite(this.client_out);
+    this.waitRead(this.client_in);
+  }
+
+  checkRequest() {
+    let request = buf2str(this.inbuf);
+    if (request == "PING!") {
+      this.outbuf += "PONG!";
+      this.state = STATE_FINISHED;
+      this.waitWrite(this.client_out);
+    } else if (request.startsWith("GET / HTTP/1.1")) {
+      this.outbuf = "HTTP/1.1 200 OK\r\n" +
+                     "Content-Length: 2\r\n" +
+                     "Content-Type: text/html\r\n" +
+                     "\r\nOK";
+      this.state = STATE_FINISHED;
+      this.waitWrite(this.client_out);
+    }
+  }
+
+  close() {
+    this.client_in.close();
+    this.client_out.close();
+  }
+}
+
+class SocksTestServer {
+  constructor() {
+    this.client_connections = new Set();
+    this.listener = ServerSocket(-1, true, -1);
+    this.listener.asyncListen(this);
+  }
+
+  onSocketAccepted(socket, transport) {
+    let input = transport.openInputStream(0, 0, 0);
+    let output = transport.openOutputStream(0, 0, 0);
+    let client = new SocksClient(this, input, output);
+    this.client_connections.add(client);
+  }
+
+  onStopListening() {
+  }
+
+  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;
+  }
+}
+
+class SocksTestClient {
+  constructor(socks, dest, resolve, reject) {
+    const ProtocolProxyService = CC("@mozilla.org/network/protocol-proxy-service;1",
+                                    "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 pps = new ProtocolProxyService();
+    let pi = pps.newProxyInfoWithAuth(socks.version, socks.host, socks.port,
+                                      socks.username, socks.password,
+                                      pi_flags, -1, null);
+
+    this.trans = sts.createTransport(null, 0, dest.host, dest.port, pi);
+    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 result = buf2str(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");
+do_register_cleanup(() => {
+  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.port,
+    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}`);
+  });
+});
+
+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>"]});
+
+    await browser.proxy.register("proxy.js");
+    browser.test.sendMessage("pac-ready");
+  }
+
+  let handlingExt = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: [
+        "proxy",
+        "webRequest",
+        "webRequestBlocking",
+        "<all_urls>",
+      ],
+    },
+    background: `(${background})(${socksServer.listener.port})`,
+    files: {
+      "proxy.js": `
+        function FindProxyForURL(url, host) {
+          return [{
+            type: "socks",
+            host: "127.0.0.1",
+            port: ${socksServer.listener.port},
+            username: "foo",
+            password: "bar",
+          }];
+        }`,
+    },
+  });
+
+  await handlingExt.startup();
+  await handlingExt.awaitMessage("pac-ready");
+
+  let contentPage = await ExtensionTestUtils.loadContentPage(`http://localhost/`);
+
+  await handlingExt.awaitMessage("done");
+  await contentPage.close();
+  await handlingExt.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -40,16 +40,17 @@ skip-if = os == "android" # checking for
 [test_ext_management.js]
 [test_ext_management_uninstall_self.js]
 [test_ext_onmessage_removelistener.js]
 skip-if = true # This test no longer tests what it is meant to test.
 [test_ext_privacy.js]
 [test_ext_privacy_disable.js]
 [test_ext_privacy_update.js]
 [test_ext_proxy_auth.js]
+[test_ext_proxy_socks.js]
 [test_ext_redirects.js]
 [test_ext_runtime_connect_no_receiver.js]
 [test_ext_runtime_getBrowserInfo.js]
 [test_ext_runtime_getPlatformInfo.js]
 [test_ext_runtime_onInstalled_and_onStartup.js]
 [test_ext_runtime_sendMessage.js]
 [test_ext_runtime_sendMessage_errors.js]
 [test_ext_runtime_sendMessage_no_receiver.js]