// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/

"use strict";

// a fork of test_be_conservative

// Tests that nsIHttpChannelInternal.tlsFlags can be used to set the
// client max version level. Flags can also be used to set the
// level of intolerance rollback and to test out an experimental 1.3
// hello, though they are not tested here.

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});

// Get a profile directory and ensure PSM initializes NSS.
do_get_profile();
Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);

function getCert() {
  return new Promise((resolve, reject) => {
    let certService = Cc["@mozilla.org/security/local-cert-service;1"]
                        .getService(Ci.nsILocalCertService);
    certService.getOrCreateCert("tlsflags-test", {
      handleCert: function(c, rv) {
        if (rv) {
          reject(rv);
          return;
        }
        resolve(c);
      }
    });
  });
}

class InputStreamCallback {
  constructor(output) {
    this.output = output;
    this.stopped = false;
  }

  onInputStreamReady(stream) {
    info("input stream ready");
    if (this.stopped) {
      info("input stream callback stopped - bailing");
      return;
    }
    let available = 0;
    try {
      available = stream.available();
    } catch (e) {
      // onInputStreamReady may fire when the stream has been closed.
      equal(e.result, Cr.NS_BASE_STREAM_CLOSED,
            "error should be NS_BASE_STREAM_CLOSED");
    }
    if (available > 0) {
      let request = NetUtil.readInputStreamToString(stream, available,
                                                    { charset: "utf8"});
      ok(request.startsWith("GET / HTTP/1.1\r\n"),
         "Should get a simple GET / HTTP/1.1 request");
      let response = "HTTP/1.1 200 OK\r\n" +
                     "Content-Length: 2\r\n" +
                     "Content-Type: text/plain\r\n" +
                     "\r\nOK";
      let written = this.output.write(response, response.length);
      equal(written, response.length,
            "should have been able to write entire response");
    }
    this.output.close();
    info("done with input stream ready");
  }

  stop() {
    this.stopped = true;
    this.output.close();
  }
}

class TLSServerSecurityObserver {
  constructor(input, output, expectedVersion) {
    this.input = input;
    this.output = output;
    this.expectedVersion = expectedVersion;
    this.callbacks = [];
    this.stopped = false;
  }

  onHandshakeDone(socket, status) {
    info("TLS handshake done");
    info(`TLS version used: ${status.tlsVersionUsed}`);
    info(this.expectedVersion);
    equal(status.tlsVersionUsed, this.expectedVersion, "expected version check");
    if (this.stopped) {
      info("handshake done callback stopped - bailing");
      return;
    }

    let callback = new InputStreamCallback(this.output);
    this.callbacks.push(callback);
    this.input.asyncWait(callback, 0, 0, Services.tm.currentThread);
  }

  stop() {
    this.stopped = true;
    this.input.close();
    this.output.close();
    this.callbacks.forEach((callback) => {
      callback.stop();
    });
  }
}


function startServer(cert, minServerVersion, maxServerVersion, expectedVersion) {
  let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"]
                    .createInstance(Ci.nsITLSServerSocket);
  tlsServer.init(-1, true, -1);
  tlsServer.serverCert = cert;
  tlsServer.setVersionRange(minServerVersion, maxServerVersion);
  tlsServer.setSessionTickets(false);

  let listener = {
    securityObservers : [],

    onSocketAccepted: function(socket, transport) {
      info("accepted TLS client connection");
      let connectionInfo = transport.securityInfo
          .QueryInterface(Ci.nsITLSServerConnectionInfo);
      let input = transport.openInputStream(0, 0, 0);
      let output = transport.openOutputStream(0, 0, 0);
      let securityObserver = new TLSServerSecurityObserver(input, output, expectedVersion);
      this.securityObservers.push(securityObserver);
      connectionInfo.setSecurityObserver(securityObserver);
    },

    // For some reason we get input stream callback events after we've stopped
    // listening, so this ensures we just drop those events.
    onStopListening: function() {
      info("onStopListening");
      this.securityObservers.forEach((observer) => {
        observer.stop();
      });
    }
  }
  tlsServer.asyncListen(listener);
  return tlsServer;
}

const hostname = "example.com"

function storeCertOverride(port, cert) {
  let certOverrideService = Cc["@mozilla.org/security/certoverride;1"]
                              .getService(Ci.nsICertOverrideService);
  let overrideBits = Ci.nsICertOverrideService.ERROR_UNTRUSTED |
                     Ci.nsICertOverrideService.ERROR_MISMATCH;
  certOverrideService.rememberValidityOverride(hostname, port, cert,
                                               overrideBits, true);
}

function startClient(port, tlsFlags, expectSuccess) {
  let req = new XMLHttpRequest();
  req.open("GET", `https://${hostname}:${port}`);
  let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal);
  internalChannel.tlsFlags = tlsFlags;
  return new Promise((resolve, reject) => {
    req.onload = () => {
      ok(expectSuccess,
         `should ${expectSuccess ? "" : "not "}have gotten load event`);
      equal(req.responseText, "OK", "response text should be 'OK'");
      resolve();
    };
    req.onerror = () => {
      ok(!expectSuccess,
         `should ${!expectSuccess ? "" : "not "}have gotten an error`);
      resolve();
    };

    req.send();
  });
}

add_task(async function() {
  Services.prefs.setIntPref("security.tls.version.max", 4);
  Services.prefs.setCharPref("network.dns.localDomains", hostname);
  let cert = await getCert();

  // server that accepts 1.1->1.3 and a client max 1.3. expect 1.3
  info("TEST 1");
  let server = startServer(cert,
                           Ci.nsITLSClientStatus.TLS_VERSION_1_1,
                           Ci.nsITLSClientStatus.TLS_VERSION_1_3,
                           Ci.nsITLSClientStatus.TLS_VERSION_1_3);
  storeCertOverride(server.port, cert);
  await startClient(server.port, 4, true /*should succeed*/);
  server.close();

  // server that accepts 1.1->1.3 and a client max 1.1. expect 1.1
  info("TEST 2");
  server = startServer(cert,
                       Ci.nsITLSClientStatus.TLS_VERSION_1_1,
                       Ci.nsITLSClientStatus.TLS_VERSION_1_3,
                       Ci.nsITLSClientStatus.TLS_VERSION_1_1);
  storeCertOverride(server.port, cert);
  await startClient(server.port, 2, true);
  server.close();

  // server that accepts 1.2->1.2 and a client max 1.3. expect 1.2
  info("TEST 3");
  server = startServer(cert,
                       Ci.nsITLSClientStatus.TLS_VERSION_1_2,
                       Ci.nsITLSClientStatus.TLS_VERSION_1_2,
                       Ci.nsITLSClientStatus.TLS_VERSION_1_2);
  storeCertOverride(server.port, cert);
  await startClient(server.port, 4, true);
  server.close();

  // server that accepts 1.2->1.2 and a client max 1.1. expect fail
  info("TEST 4");
  server = startServer(cert,
                       Ci.nsITLSClientStatus.TLS_VERSION_1_2,
                       Ci.nsITLSClientStatus.TLS_VERSION_1_2, 0);
  storeCertOverride(server.port, cert);
  await startClient(server.port, 2, false);

  server.close();
});

registerCleanupFunction(function() {
  Services.prefs.clearUserPref("security.tls.version.max");
  Services.prefs.clearUserPref("network.dns.localDomains");
});
