/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
/* vim: set ts=4 sts=4 et sw=4 tw=80: */
/* 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,
];

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",
];

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();
}

/** 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();
}
