// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
// 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";

// Checks various aspects of the OCSP cache, mainly to to ensure we do not fetch
// responses more than necessary.

var gFetchCount = 0;
var gGoodOCSPResponse = null;
var gResponsePattern = [];

function respondWithGoodOCSP(request, response) {
  info("returning 200 OK");
  response.setStatusLine(request.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "application/ocsp-response");
  response.write(gGoodOCSPResponse);
}

function respondWithSHA1OCSP(request, response) {
  info("returning 200 OK with sha-1 delegated response");
  response.setStatusLine(request.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "application/ocsp-response");

  let args = [["good-delegated", "default-ee", "delegatedSHA1Signer", 0]];
  let responses = generateOCSPResponses(args, "ocsp_certs");
  response.write(responses[0]);
}

function respondWithError(request, response) {
  info("returning 500 Internal Server Error");
  response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
  let body = "Refusing to return a response";
  response.bodyOutputStream.write(body, body.length);
}

function generateGoodOCSPResponse(thisUpdateSkew) {
  let args = [["good", "default-ee", "unused", thisUpdateSkew]];
  let responses = generateOCSPResponses(args, "ocsp_certs");
  return responses[0];
}

function add_ocsp_test(
  aHost,
  aExpectedResult,
  aResponses,
  aMessage,
  aOriginAttributes
) {
  add_connection_test(
    aHost,
    aExpectedResult,
    function () {
      clearSessionCache();
      gFetchCount = 0;
      gResponsePattern = aResponses;
    },
    function () {
      // check the number of requests matches the size of aResponses
      equal(gFetchCount, aResponses.length, aMessage);
    },
    null,
    aOriginAttributes
  );
}

function run_test() {
  do_get_profile();
  Services.prefs.setBoolPref("security.ssl.enable_ocsp_stapling", true);
  Services.prefs.setIntPref("security.OCSP.enabled", 1);
  add_tls_server_setup("OCSPStaplingServer", "ocsp_certs");

  let ocspResponder = new HttpServer();
  ocspResponder.registerPrefixHandler("/", function (request, response) {
    info("gFetchCount: " + gFetchCount);
    let responseFunction = gResponsePattern[gFetchCount];
    Assert.notEqual(undefined, responseFunction);

    ++gFetchCount;
    responseFunction(request, response);
  });
  ocspResponder.start(8888);

  add_tests();

  add_test(function () {
    ocspResponder.stop(run_next_test);
  });
  run_next_test();
}

function add_tests() {
  // Test that verifying a certificate with a "short lifetime" doesn't result
  // in OCSP fetching. Due to longevity requirements in our testing
  // infrastructure, the certificate we encounter is valid for a very long
  // time, so we have to define a "short lifetime" as something very long.
  add_test(function () {
    Services.prefs.setIntPref(
      "security.pki.cert_short_lifetime_in_days",
      12000
    );
    run_next_test();
  });

  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [],
    "expected zero OCSP requests for a short-lived certificate"
  );

  add_test(function () {
    Services.prefs.setIntPref("security.pki.cert_short_lifetime_in_days", 100);
    run_next_test();
  });

  // If a "short lifetime" is something more reasonable, ensure that we do OCSP
  // fetching for this long-lived certificate.

  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithError],
    "expected one OCSP request for a long-lived certificate"
  );
  add_test(function () {
    Services.prefs.clearUserPref("security.pki.cert_short_lifetime_in_days");
    run_next_test();
  });
  // ---------------------------------------------------------------------------

  // Reset state
  add_test(function () {
    clearOCSPCache();
    run_next_test();
  });

  // This test assumes that OCSPStaplingServer uses the same cert for
  // ocsp-stapling-unknown.example.com and ocsp-stapling-none.example.com.

  // Get an Unknown response for the *.example.com cert and put it in the
  // OCSP cache.
  add_ocsp_test(
    "ocsp-stapling-unknown.example.com",
    SEC_ERROR_OCSP_UNKNOWN_CERT,
    [],
    "Stapled Unknown response -> a fetch should not have been attempted"
  );

  // A failure to retrieve an OCSP response must result in the cached Unknown
  // response being recognized and honored.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    SEC_ERROR_OCSP_UNKNOWN_CERT,
    [respondWithError, respondWithError],
    "No stapled response -> a fetch should have been attempted"
  );

  // A valid Good response from the OCSP responder must override the cached
  // Unknown response.
  //
  // Note that We need to make sure that the Unknown response and the Good
  // response have different thisUpdate timestamps; otherwise, the Good
  // response will be seen as "not newer" and it won't replace the existing
  // entry.
  add_test(function () {
    gGoodOCSPResponse = generateGoodOCSPResponse(1200);
    run_next_test();
  });
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithGoodOCSP],
    "Cached Unknown response, no stapled response -> a fetch" +
      " should have been attempted"
  );

  // The Good response retrieved from the previous fetch must have replaced
  // the Unknown response in the cache, resulting in the catched Good response
  // being returned and no fetch.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [],
    "Cached Good response -> a fetch should not have been attempted"
  );

  // ---------------------------------------------------------------------------

  // Reset state
  add_test(function () {
    clearOCSPCache();
    run_next_test();
  });

  // A failure to retrieve an OCSP response will result in an error entry being
  // added to the cache.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithError],
    "No stapled response -> a fetch should have been attempted"
  );

  // The error entry will prevent a fetch from happening for a while.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [],
    "Noted OCSP server failure -> a fetch should not have been attempted"
  );

  // The error entry must not prevent a stapled OCSP response from being
  // honored.
  add_ocsp_test(
    "ocsp-stapling-revoked.example.com",
    SEC_ERROR_REVOKED_CERTIFICATE,
    [],
    "Stapled Revoked response -> a fetch should not have been attempted"
  );

  // ---------------------------------------------------------------------------

  // Ensure OCSP responses from signers with SHA1 certificates are OK. This
  // is included in the OCSP caching tests since there were OCSP cache-related
  // regressions when sha-1 telemetry probes were added.
  add_test(function () {
    clearOCSPCache();
    // set security.OCSP.require so that checking the OCSP signature fails
    Services.prefs.setBoolPref("security.OCSP.require", true);
    run_next_test();
  });

  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    SEC_ERROR_OCSP_INVALID_SIGNING_CERT,
    [respondWithSHA1OCSP],
    "OCSP signing cert was issued with sha1 - should fail"
  );

  add_test(function () {
    Services.prefs.setBoolPref("security.OCSP.require", false);
    run_next_test();
  });

  // ---------------------------------------------------------------------------

  // Reset state
  add_test(function () {
    clearOCSPCache();
    run_next_test();
  });

  // This test makes sure that OCSP cache are isolated by firstPartyDomain.

  let gObservedCnt = 0;
  let protocolProxyService = Cc[
    "@mozilla.org/network/protocol-proxy-service;1"
  ].getService(Ci.nsIProtocolProxyService);

  // Observe all channels and make sure the firstPartyDomain in their loadInfo's
  // origin attributes are aFirstPartyDomain.
  function startObservingChannels(aFirstPartyDomain) {
    // We use a dummy proxy filter to catch all channels, even those that do not
    // generate an "http-on-modify-request" notification.
    let proxyFilter = {
      applyFilter(aChannel, aProxy, aCallback) {
        // We have the channel; provide it to the callback.
        if (aChannel.originalURI.spec == "http://localhost:8888/") {
          gObservedCnt++;
          equal(
            aChannel.loadInfo.originAttributes.firstPartyDomain,
            aFirstPartyDomain,
            "firstPartyDomain should match"
          );
        }
        // Pass on aProxy unmodified.
        aCallback.onProxyFilterResult(aProxy);
      },
    };
    protocolProxyService.registerChannelFilter(proxyFilter, 0);
    // Return the stop() function:
    return () => protocolProxyService.unregisterChannelFilter(proxyFilter);
  }

  let stopObservingChannels;
  add_test(function () {
    stopObservingChannels = startObservingChannels("foo.com");
    run_next_test();
  });

  // A good OCSP response will be cached.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithGoodOCSP],
    "No stapled response (firstPartyDomain = foo.com) -> a fetch " +
      "should have been attempted",
    { firstPartyDomain: "foo.com" }
  );

  // The cache will prevent a fetch from happening.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [],
    "Noted OCSP server failure (firstPartyDomain = foo.com) -> a " +
      "fetch should not have been attempted",
    { firstPartyDomain: "foo.com" }
  );

  add_test(function () {
    stopObservingChannels();
    equal(gObservedCnt, 1, "should have observed only 1 OCSP requests");
    gObservedCnt = 0;
    run_next_test();
  });

  add_test(function () {
    stopObservingChannels = startObservingChannels("bar.com");
    run_next_test();
  });

  // But using a different firstPartyDomain should result in a fetch.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithGoodOCSP],
    "No stapled response (firstPartyDomain = bar.com) -> a fetch " +
      "should have been attempted",
    { firstPartyDomain: "bar.com" }
  );

  add_test(function () {
    stopObservingChannels();
    equal(gObservedCnt, 1, "should have observed only 1 OCSP requests");
    gObservedCnt = 0;
    run_next_test();
  });

  // ---------------------------------------------------------------------------

  // Reset state
  add_test(function () {
    clearOCSPCache();
    run_next_test();
  });

  // Test that the OCSP cache is not isolated by userContextId.

  // A good OCSP response will be cached.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithGoodOCSP],
    "No stapled response (userContextId = 1) -> a fetch " +
      "should have been attempted",
    { userContextId: 1 }
  );

  // The cache will prevent a fetch from happening.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [],
    "Noted OCSP server failure (userContextId = 1) -> a " +
      "fetch should not have been attempted",
    { userContextId: 1 }
  );

  // Fetching is prevented even if in a different userContextId.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [],
    "Noted OCSP server failure (userContextId = 2) -> a " +
      "fetch should not have been attempted",
    { userContextId: 2 }
  );

  // ---------------------------------------------------------------------------

  // Reset state
  add_test(function () {
    clearOCSPCache();
    run_next_test();
  });

  // This test makes sure that OCSP cache are isolated by partitionKey.

  add_test(function () {
    Services.prefs.setBoolPref(
      "privacy.partition.network_state.ocsp_cache",
      true
    );
    run_next_test();
  });

  // A good OCSP response will be cached.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithGoodOCSP],
    "No stapled response (partitionKey = (https,foo.com)) -> a fetch " +
      "should have been attempted",
    { partitionKey: "(https,foo.com)" }
  );

  // The cache will prevent a fetch from happening.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [],
    "Noted OCSP server failure (partitionKey = (https,foo.com)) -> a " +
      "fetch should not have been attempted",
    { partitionKey: "(https,foo.com)" }
  );

  // Using a different partitionKey should result in a fetch.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithGoodOCSP],
    "Noted OCSP server failure (partitionKey = (https,bar.com)) -> a " +
      "fetch should have been attempted",
    { partitionKey: "(https,bar.com)" }
  );

  // ---------------------------------------------------------------------------

  // Reset state
  add_test(function () {
    Services.prefs.clearUserPref("privacy.partition.network_state.ocsp_cache");
    clearOCSPCache();
    run_next_test();
  });

  // This test makes sure that OCSP cache are isolated by partitionKey in
  // private mode.

  // A good OCSP response will be cached.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithGoodOCSP],
    "No stapled response (partitionKey = (https,foo.com)) -> a fetch " +
      "should have been attempted",
    { partitionKey: "(https,foo.com)", privateBrowsingId: 1 }
  );

  // The cache will prevent a fetch from happening.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [],
    "Noted OCSP server failure (partitionKey = (https,foo.com)) -> a " +
      "fetch should not have been attempted",
    { partitionKey: "(https,foo.com)", privateBrowsingId: 1 }
  );

  // Using a different partitionKey should result in a fetch.
  add_ocsp_test(
    "ocsp-stapling-none.example.com",
    PRErrorCodeSuccess,
    [respondWithGoodOCSP],
    "Noted OCSP server failure (partitionKey = (https,bar.com)) -> a " +
      "fetch should have been attempted",
    { partitionKey: "(https,bar.com)", privateBrowsingId: 1 }
  );

  // ---------------------------------------------------------------------------

  // Reset state
  add_test(function () {
    clearOCSPCache();
    run_next_test();
  });
}
