/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var CC = Components.Constructor;
const ServerSocket = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "init"
);
var serv;
var ios;

/**
 * Example local IP addresses (literal IP address hostname).
 *
 * Note: for IPv6 Unique Local and Link Local, a wider range of addresses is
 * set aside than those most commonly used. Technically, link local addresses
 * include those beginning with fe80:: through febf::, although in practise
 * only fe80:: is used. Necko code blocks speculative connections for the wider
 * range; hence, this test considers that range too.
 */
var localIPv4Literals = [
  // IPv4 RFC1918 \
  "10.0.0.1",
  "10.10.10.10",
  "10.255.255.255", // 10/8
  "172.16.0.1",
  "172.23.172.12",
  "172.31.255.255", // 172.16/20
  "192.168.0.1",
  "192.168.192.168",
  "192.168.255.255", // 192.168/16
  // IPv4 Link Local
  "169.254.0.1",
  "169.254.192.154",
  "169.254.255.255", // 169.254/16
];
var localIPv6Literals = [
  // IPv6 Unique Local fc00::/7
  "fc00::1",
  "fdfe:dcba:9876:abcd:ef01:2345:6789:abcd",
  "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
  // IPv6 Link Local fe80::/10
  "fe80::1",
  "fe80::abcd:ef01:2345:6789",
  "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
];
var localIPLiterals = localIPv4Literals.concat(localIPv6Literals);

/**
 * Test function list and descriptions.
 */
var testList = [
  test_localhost_http_speculative_connect,
  test_localhost_https_speculative_connect,
  test_hostnames_resolving_to_local_addresses,
  test_proxies_with_local_addresses,
  test_speculative_connect_with_proxy_filter,
  test_speculative_connect_with_proxy_filter_and_callback,
];

var testDescription = [
  "Expect pass with localhost, http",
  "Expect pass with localhost, https",
  "Expect failure with resolved local IPs",
  "Expect failure for proxies with local IPs",
  "Expect failure without notification callbacks",
  "Expect pass with notification callbacks and proxy filter",
];

var testIdx = 0;
var hostIdx = 0;

/**
 * TestServer
 *
 * Implements nsIServerSocket for test_speculative_connect.
 */
function TestServer() {
  this.listener = ServerSocket(-1, true, -1);
  this.listener.asyncListen(this);
}

TestServer.prototype = {
  QueryInterface: ChromeUtils.generateQI(["nsIServerSocket"]),
  onSocketAccepted() {
    try {
      this.listener.close();
    } catch (e) {}
    Assert.ok(true);
    next_test();
  },

  onStopListening() {},
};

/**
 * TestFailedStreamCallback
 *
 * Implements nsI[Input|Output]StreamCallback for socket layer tests.
 * Expect failure in all cases
 */
function TestFailedStreamCallback(transport, hostname, next) {
  this.transport = transport;
  this.hostname = hostname;
  this.next = next;
  this.dummyContent = "G";
  this.closed = false;
}

TestFailedStreamCallback.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIInputStreamCallback",
    "nsIOutputStreamCallback",
  ]),
  processException(e) {
    if (this.closed) {
      return;
    }
    do_check_instanceof(e, Ci.nsIException);
    // A refusal to connect speculatively should throw an error.
    Assert.equal(e.result, Cr.NS_ERROR_CONNECTION_REFUSED);
    this.closed = true;
    this.transport.close(Cr.NS_BINDING_ABORTED);
    this.next();
  },
  onOutputStreamReady(outstream) {
    info("outputstream handler.");
    Assert.notEqual(typeof outstream, undefined);
    try {
      outstream.write(this.dummyContent, this.dummyContent.length);
    } catch (e) {
      this.processException(e);
      return;
    }
    info("no exception on write. Wait for read.");
  },
  onInputStreamReady(instream) {
    info("inputstream handler.");
    Assert.notEqual(typeof instream, undefined);
    try {
      instream.available();
    } catch (e) {
      this.processException(e);
      return;
    }
    do_throw("Speculative Connect should have failed for " + this.hostname);
    this.transport.close(Cr.NS_BINDING_ABORTED);
    this.next();
  },
};

/**
 * test_localhost_http_speculative_connect
 *
 * Tests a basic positive case using nsIOService.SpeculativeConnect:
 * connecting to localhost via http.
 */
function test_localhost_http_speculative_connect() {
  serv = new TestServer();
  var ssm = Services.scriptSecurityManager;
  var URI = ios.newURI(
    "http://localhost:" + serv.listener.port + "/just/a/test"
  );
  var principal = ssm.createContentPrincipal(URI, {});

  ios
    .QueryInterface(Ci.nsISpeculativeConnect)
    .speculativeConnect(URI, principal, null, false);
}

/**
 * test_localhost_https_speculative_connect
 *
 * Tests a basic positive case using nsIOService.SpeculativeConnect:
 * connecting to localhost via https.
 */
function test_localhost_https_speculative_connect() {
  serv = new TestServer();
  var ssm = Services.scriptSecurityManager;
  var URI = ios.newURI(
    "https://localhost:" + serv.listener.port + "/just/a/test"
  );
  var principal = ssm.createContentPrincipal(URI, {});

  ios
    .QueryInterface(Ci.nsISpeculativeConnect)
    .speculativeConnect(URI, principal, null, false);
}

/* Speculative connections should not be allowed for hosts with local IP
 * addresses (Bug 853423). That list includes:
 *  -- IPv4 RFC1918 and Link Local Addresses.
 *  -- IPv6 Unique and Link Local Addresses.
 *
 * Two tests are required:
 *  1. Verify IP Literals passed to the SpeculativeConnect API.
 *  2. Verify hostnames that need to be resolved at the socket layer.
 */

/**
 * test_hostnames_resolving_to_addresses
 *
 * Common test function for resolved hostnames. Takes a list of hosts, a
 * boolean to determine if the test is expected to succeed or fail, and a
 * function to call the next test case.
 */
function test_hostnames_resolving_to_addresses(host, next) {
  info(host);
  var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
    Ci.nsISocketTransportService
  );
  Assert.notEqual(typeof sts, undefined);
  var transport = sts.createTransport([], host, 80, null, null);
  Assert.notEqual(typeof transport, undefined);

  transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918;
  transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1);
  transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1);
  Assert.equal(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT));

  var outStream = transport.openOutputStream(
    Ci.nsITransport.OPEN_UNBUFFERED,
    0,
    0
  );
  var inStream = transport.openInputStream(0, 0, 0);
  Assert.notEqual(typeof outStream, undefined);
  Assert.notEqual(typeof inStream, undefined);

  var callback = new TestFailedStreamCallback(transport, host, next);
  Assert.notEqual(typeof callback, undefined);

  // Need to get main thread pointer to ensure nsSocketTransport::AsyncWait
  // adds callback to ns*StreamReadyEvent on main thread, and doesn't
  // addref off the main thread.
  var gThreadManager = Services.tm;
  var mainThread = gThreadManager.currentThread;

  try {
    outStream
      .QueryInterface(Ci.nsIAsyncOutputStream)
      .asyncWait(callback, 0, 0, mainThread);
    inStream
      .QueryInterface(Ci.nsIAsyncInputStream)
      .asyncWait(callback, 0, 0, mainThread);
  } catch (e) {
    do_throw("asyncWait should not fail!");
  }
}

/**
 * test_hostnames_resolving_to_local_addresses
 *
 * Creates an nsISocketTransport and simulates a speculative connect request
 * for a hostname that resolves to a local IP address.
 * Runs asynchronously; on test success (i.e. failure to connect), the callback
 * will call this function again until all hostnames in the test list are done.
 *
 * Note: This test also uses an IP literal for the hostname. This should be ok,
 * as the socket layer will ask for the hostname to be resolved anyway, and DNS
 * code should return a numerical version of the address internally.
 */
function test_hostnames_resolving_to_local_addresses() {
  if (hostIdx >= localIPLiterals.length) {
    // No more local IP addresses; move on.
    next_test();
    return;
  }
  var host = localIPLiterals[hostIdx++];
  // Test another local IP address when the current one is done.
  var next = test_hostnames_resolving_to_local_addresses;
  test_hostnames_resolving_to_addresses(host, next);
}

/**
 * test_speculative_connect_with_host_list
 *
 * Common test function for resolved proxy hosts. Takes a list of hosts, a
 * boolean to determine if the test is expected to succeed or fail, and a
 * function to call the next test case.
 */
function test_proxies(proxyHost, next) {
  info("Proxy: " + proxyHost);
  var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
    Ci.nsISocketTransportService
  );
  Assert.notEqual(typeof sts, undefined);
  var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
  Assert.notEqual(typeof pps, undefined);

  var proxyInfo = pps.newProxyInfo("http", proxyHost, 8080, "", "", 0, 1, null);
  Assert.notEqual(typeof proxyInfo, undefined);

  var transport = sts.createTransport([], "dummyHost", 80, proxyInfo, null);
  Assert.notEqual(typeof transport, undefined);

  transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918;

  transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1);
  Assert.equal(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT));
  transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1);

  var outStream = transport.openOutputStream(
    Ci.nsITransport.OPEN_UNBUFFERED,
    0,
    0
  );
  var inStream = transport.openInputStream(0, 0, 0);
  Assert.notEqual(typeof outStream, undefined);
  Assert.notEqual(typeof inStream, undefined);

  var callback = new TestFailedStreamCallback(transport, proxyHost, next);
  Assert.notEqual(typeof callback, undefined);

  // Need to get main thread pointer to ensure nsSocketTransport::AsyncWait
  // adds callback to ns*StreamReadyEvent on main thread, and doesn't
  // addref off the main thread.
  var gThreadManager = Services.tm;
  var mainThread = gThreadManager.currentThread;

  try {
    outStream
      .QueryInterface(Ci.nsIAsyncOutputStream)
      .asyncWait(callback, 0, 0, mainThread);
    inStream
      .QueryInterface(Ci.nsIAsyncInputStream)
      .asyncWait(callback, 0, 0, mainThread);
  } catch (e) {
    do_throw("asyncWait should not fail!");
  }
}

/**
 * test_proxies_with_local_addresses
 *
 * Creates an nsISocketTransport and simulates a speculative connect request
 * for a proxy that resolves to a local IP address.
 * Runs asynchronously; on test success (i.e. failure to connect), the callback
 * will call this function again until all proxies in the test list are done.
 *
 * Note: This test also uses an IP literal for the proxy. This should be ok,
 * as the socket layer will ask for the proxy to be resolved anyway, and DNS
 * code should return a numerical version of the address internally.
 */
function test_proxies_with_local_addresses() {
  if (hostIdx >= localIPLiterals.length) {
    // No more local IP addresses; move on.
    next_test();
    return;
  }
  var host = localIPLiterals[hostIdx++];
  // Test another local IP address when the current one is done.
  var next = test_proxies_with_local_addresses;
  test_proxies(host, next);
}

class ProxyFilter {
  constructor(type, host, port, flags) {
    this._type = type;
    this._host = host;
    this._port = port;
    this._flags = flags;
    this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]);
  }
  applyFilter(uri, pi, cb) {
    const pps =
      Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
    cb.onProxyFilterResult(
      pps.newProxyInfo(
        this._type,
        this._host,
        this._port,
        "",
        "",
        this._flags,
        1000,
        null
      )
    );
  }
}

function test_speculative_connect_with_proxy_filter() {
  let filter = new ProxyFilter("https", "localhost", 80, 0);
  let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
  pps.registerFilter(filter, 10);
  let URI = ios.newURI("https://not-exist-dommain.com");
  let principal = Services.scriptSecurityManager.createContentPrincipal(
    URI,
    {}
  );

  Assert.throws(
    () =>
      ios
        .QueryInterface(Ci.nsISpeculativeConnect)
        .speculativeConnect(URI, principal, null, false),
    /NS_ERROR_FAILURE/,
    "speculativeConnect should throw when no callback is provided and a proxy filter is registered"
  );
  pps.unregisterFilter(filter);
  next_test();
}

function test_speculative_connect_with_proxy_filter_and_callback() {
  let filter = new ProxyFilter("https", "localhost", 80, 0);
  let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
  pps.registerFilter(filter, 10);
  let URI = ios.newURI("https://not-exist-dommain.com");
  let principal = Services.scriptSecurityManager.createContentPrincipal(
    URI,
    {}
  );

  let callback = {
    QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
    getInterface() {
      return null;
    },
  };

  ios
    .QueryInterface(Ci.nsISpeculativeConnect)
    .speculativeConnect(URI, principal, callback, false);
  Assert.ok(
    true,
    "speculativeConnect should succeed when callback is provided with proxy filter registered"
  );

  pps.unregisterFilter(filter);
  next_test();
}

/**
 * next_test
 *
 * Calls the next test in testList. Each test is responsible for calling this
 * function when its test cases are complete.
 */
function next_test() {
  if (testIdx >= testList.length) {
    // No more tests; we're done.
    do_test_finished();
    return;
  }
  info("SpeculativeConnect: " + testDescription[testIdx]);
  hostIdx = 0;
  // Start next test in list.
  testList[testIdx++]();
}

/**
 * run_test
 *
 * Main entry function for test execution.
 */
function run_test() {
  ios = Services.io;

  Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
  });

  do_test_pending();
  next_test();
}
