netwerk/protocol/http/AlternateServices.cpp
author Nicholas Hurley <hurley@mozilla.com>
Mon, 15 Oct 2018 21:52:04 +0000
changeset 497100 32e5d5e6c9804247b0f82f931dc875fdc838f750
parent 488884 79ff858fea6bdff2ae0d7d1fa8c7fd98c0895f0f
child 497478 b6780702981d863bfd913ca404d5b5209b952066
permissions -rw-r--r--
Bug 1493724 - Don't overwrite still-valid altsvc mappings until a new one is validated. r=valentin Right now, as soon as we receive an alt-svc header or frame for an origin, we will overwrite any mapping we already have for that origin, even if it's still valid. This means that, should validation fail for the new mapping, we will have blown away a perfectly usable alt-svc mapping for no good reason. This patch prevents us from overwriting until we know the new mapping is good. Differential Revision: https://phabricator.services.mozilla.com/D8747

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=8 et 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/. */

#include "HttpLog.h"

#include "AlternateServices.h"
#include "LoadInfo.h"
#include "nsEscape.h"
#include "nsHttpConnectionInfo.h"
#include "nsHttpChannel.h"
#include "nsHttpHandler.h"
#include "nsThreadUtils.h"
#include "nsHttpTransaction.h"
#include "NullHttpTransaction.h"
#include "nsISSLSocketControl.h"
#include "nsIWellKnownOpportunisticUtils.h"

/* RFC 7838 Alternative Services
   http://httpwg.org/http-extensions/opsec.html
    note that connections currently do not do mixed-scheme (the I attribute
    in the ConnectionInfo prevents it) but could, do not honor tls-commit and should
    not, and always require authentication
*/

namespace mozilla {
namespace net {

// function places true in outIsHTTPS if scheme is https, false if
// http, and returns an error if neither. originScheme passed into
// alternate service should already be normalized to those lower case
// strings by the URI parser (and so there is an assert)- this is an extra check.
static nsresult
SchemeIsHTTPS(const nsACString &originScheme, bool &outIsHTTPS)
{
  outIsHTTPS = originScheme.EqualsLiteral("https");

  if (!outIsHTTPS && !originScheme.EqualsLiteral("http")) {
      MOZ_ASSERT(false, "unexpected scheme");
      return NS_ERROR_UNEXPECTED;
  }
  return NS_OK;
}

bool
AltSvcMapping::AcceptableProxy(nsProxyInfo *proxyInfo)
{
  return !proxyInfo || proxyInfo->IsDirect() || proxyInfo->IsSOCKS();
}

void
AltSvcMapping::ProcessHeader(const nsCString &buf, const nsCString &originScheme,
                             const nsCString &originHost, int32_t originPort,
                             const nsACString &username, bool privateBrowsing,
                             nsIInterfaceRequestor *callbacks, nsProxyInfo *proxyInfo,
                             uint32_t caps, const OriginAttributes &originAttributes)
{
  MOZ_ASSERT(NS_IsMainThread());
  LOG(("AltSvcMapping::ProcessHeader: %s\n", buf.get()));
  if (!callbacks) {
    return;
  }

  if (!AcceptableProxy(proxyInfo)) {
    LOG(("AltSvcMapping::ProcessHeader ignoring due to proxy\n"));
    return;
  }

  bool isHTTPS;
  if (NS_FAILED(SchemeIsHTTPS(originScheme, isHTTPS))) {
    return;
  }
  if (!isHTTPS && !gHttpHandler->AllowAltSvcOE()) {
    LOG(("Alt-Svc Response Header for http:// origin but OE disabled\n"));
    return;
  }

  LOG(("Alt-Svc Response Header %s\n", buf.get()));
  ParsedHeaderValueListList parsedAltSvc(buf);

  for (uint32_t index = 0; index < parsedAltSvc.mValues.Length(); ++index) {
    uint32_t maxage = 86400; // default
    nsAutoCString hostname;
    nsAutoCString npnToken;
    int32_t portno = originPort;
    bool clearEntry = false;

    for (uint32_t pairIndex = 0;
         pairIndex < parsedAltSvc.mValues[index].mValues.Length();
         ++pairIndex) {
      nsDependentCSubstring &currentName =
        parsedAltSvc.mValues[index].mValues[pairIndex].mName;
      nsDependentCSubstring &currentValue =
        parsedAltSvc.mValues[index].mValues[pairIndex].mValue;

      if (!pairIndex) {
        if (currentName.EqualsLiteral("clear")) {
          clearEntry = true;
          break;
        }

        // h2=[hostname]:443
        npnToken = currentName;
        int32_t colonIndex = currentValue.FindChar(':');
        if (colonIndex >= 0) {
          portno =
            atoi(PromiseFlatCString(currentValue).get() + colonIndex + 1);
        } else {
          colonIndex = 0;
        }
        hostname.Assign(currentValue.BeginReading(), colonIndex);
      } else if (currentName.EqualsLiteral("ma")) {
        maxage = atoi(PromiseFlatCString(currentValue).get());
        break;
      } else {
        LOG(("Alt Svc ignoring parameter %s", currentName.BeginReading()));
      }
    }

    if (clearEntry) {
      nsCString suffix;
      originAttributes.CreateSuffix(suffix);
      LOG(("Alt Svc clearing mapping for %s:%d:%s", originHost.get(),
           originPort, suffix.get()));
      gHttpHandler->ConnMgr()->ClearHostMapping(originHost, originPort, originAttributes);
      continue;
    }

    // unescape modifies a c string in place, so afterwards
    // update nsCString length
    nsUnescape(npnToken.BeginWriting());
    npnToken.SetLength(strlen(npnToken.BeginReading()));

    uint32_t spdyIndex;
    SpdyInformation *spdyInfo = gHttpHandler->SpdyInfo();
    if (!(NS_SUCCEEDED(spdyInfo->GetNPNIndex(npnToken, &spdyIndex)) &&
          spdyInfo->ProtocolEnabled(spdyIndex))) {
      LOG(("Alt Svc unknown protocol %s, ignoring", npnToken.get()));
      continue;
    }

    RefPtr<AltSvcMapping> mapping = new AltSvcMapping(gHttpHandler->ConnMgr()->GetStoragePtr(),
                                                      gHttpHandler->ConnMgr()->StorageEpoch(),
                                                      originScheme,
                                                      originHost, originPort,
                                                      username, privateBrowsing,
                                                      NowInSeconds() + maxage,
                                                      hostname, portno, npnToken,
                                                      originAttributes);
    if (mapping->TTL() <= 0) {
      LOG(("Alt Svc invalid map"));
      mapping = nullptr;
      // since this isn't a parse error, let's clear any existing mapping
      // as that would have happened if we had accepted the parameters.
      gHttpHandler->ConnMgr()->ClearHostMapping(originHost, originPort, originAttributes);
    } else {
      gHttpHandler->UpdateAltServiceMapping(mapping, proxyInfo, callbacks, caps,
                                            originAttributes);
    }
  }
}

AltSvcMapping::AltSvcMapping(DataStorage *storage, int32_t epoch,
                             const nsACString &originScheme,
                             const nsACString &originHost,
                             int32_t originPort,
                             const nsACString &username,
                             bool privateBrowsing,
                             uint32_t expiresAt,
                             const nsACString &alternateHost,
                             int32_t alternatePort,
                             const nsACString &npnToken,
                             const OriginAttributes &originAttributes)
  : mStorage(storage)
  , mStorageEpoch(epoch)
  , mAlternateHost(alternateHost)
  , mAlternatePort(alternatePort)
  , mOriginHost(originHost)
  , mOriginPort(originPort)
  , mUsername(username)
  , mPrivate(privateBrowsing)
  , mExpiresAt(expiresAt)
  , mValidated(false)
  , mMixedScheme(false)
  , mNPNToken(npnToken)
  , mOriginAttributes(originAttributes)
  , mSyncOnlyOnSuccess(false)
{
  MOZ_ASSERT(NS_IsMainThread());

  if (NS_FAILED(SchemeIsHTTPS(originScheme, mHttps))) {
    LOG(("AltSvcMapping ctor %p invalid scheme\n", this));
    mExpiresAt = 0; // invalid
  }

  if (mAlternatePort == -1) {
    mAlternatePort = mHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT;
  }
  if (mOriginPort == -1) {
    mOriginPort = mHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT;
  }

  LOG(("AltSvcMapping ctor %p %s://%s:%d to %s:%d\n", this,
       nsCString(originScheme).get(), mOriginHost.get(), mOriginPort,
       mAlternateHost.get(), mAlternatePort));

  if (mAlternateHost.IsEmpty()) {
    mAlternateHost = mOriginHost;
  }

  if ((mAlternatePort == mOriginPort) &&
      mAlternateHost.EqualsIgnoreCase(mOriginHost.get())) {
    LOG(("Alt Svc is also origin Svc - ignoring\n"));
    mExpiresAt = 0; // invalid
  }

  if (mExpiresAt) {
    MakeHashKey(mHashKey, originScheme, mOriginHost, mOriginPort, mPrivate,
                mOriginAttributes);
  }
}

void
AltSvcMapping::MakeHashKey(nsCString &outKey,
                           const nsACString &originScheme,
                           const nsACString &originHost,
                           int32_t originPort,
                           bool privateBrowsing,
                           const OriginAttributes &originAttributes)
{
  outKey.Truncate();

  if (originPort == -1) {
    bool isHttps = originScheme.EqualsLiteral("https");
    originPort = isHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT;
  }

  outKey.Append(originScheme);
  outKey.Append(':');
  outKey.Append(originHost);
  outKey.Append(':');
  outKey.AppendInt(originPort);
  outKey.Append(':');
  outKey.Append(privateBrowsing ? 'P' : '.');
  outKey.Append(':');
  nsAutoCString suffix;
  originAttributes.CreateSuffix(suffix);
  outKey.Append(suffix);
}

int32_t
AltSvcMapping::TTL()
{
  return mExpiresAt - NowInSeconds();
}

void
AltSvcMapping::SyncString(const nsCString& str)
{
  MOZ_ASSERT(NS_IsMainThread());
  mStorage->Put(HashKey(), str,
                mPrivate ? DataStorage_Private : DataStorage_Persistent);
}

void
AltSvcMapping::Sync()
{
  if (!mStorage) {
    return;
  }
  if (mSyncOnlyOnSuccess && !mValidated) {
    return;
  }
  nsCString value;
  Serialize(value);

  if (!NS_IsMainThread()) {
    nsCOMPtr<nsIRunnable> r;
    r = NewRunnableMethod<nsCString>("net::AltSvcMapping::SyncString",
                                     this,
                                     &AltSvcMapping::SyncString,
                                     value);
    NS_DispatchToMainThread(r, NS_DISPATCH_NORMAL);
    return;
  }

  mStorage->Put(HashKey(), value,
                mPrivate ? DataStorage_Private : DataStorage_Persistent);
}

void
AltSvcMapping::SetValidated(bool val)
{
  mValidated = val;
  Sync();
}

void
AltSvcMapping::SetMixedScheme(bool val)
{
  mMixedScheme = val;
  Sync();
}

void
AltSvcMapping::SetExpiresAt(int32_t val)
{
  mExpiresAt = val;
  Sync();
}

void
AltSvcMapping::SetExpired()
{
  LOG(("AltSvcMapping SetExpired %p origin %s alternate %s\n", this,
       mOriginHost.get(), mAlternateHost.get()));
  mExpiresAt = NowInSeconds() - 1;
  Sync();
}

bool
AltSvcMapping::RouteEquals(AltSvcMapping *map)
{
  MOZ_ASSERT(map->mHashKey.Equals(mHashKey));
  return mAlternateHost.Equals(map->mAlternateHost) &&
    (mAlternatePort == map->mAlternatePort) &&
    mNPNToken.Equals(map->mNPNToken);
}

void
AltSvcMapping::GetConnectionInfo(nsHttpConnectionInfo **outCI,
                                 nsProxyInfo *pi,
                                 const OriginAttributes &originAttributes)
{
  RefPtr<nsHttpConnectionInfo> ci =
    new nsHttpConnectionInfo(mOriginHost, mOriginPort, mNPNToken,
                             mUsername, pi, originAttributes,
                             mAlternateHost, mAlternatePort);

  // http:// without the mixed-scheme attribute needs to be segmented in the
  // connection manager connection information hash with this attribute
  if (!mHttps && !mMixedScheme) {
    ci->SetInsecureScheme(true);
  }
  ci->SetPrivate(mPrivate);
  ci.forget(outCI);
}

void
AltSvcMapping::Serialize(nsCString &out)
{
  out = mHttps ? NS_LITERAL_CSTRING("https:") : NS_LITERAL_CSTRING("http:");
  out.Append(mOriginHost);
  out.Append(':');
  out.AppendInt(mOriginPort);
  out.Append(':');
  out.Append(mAlternateHost);
  out.Append(':');
  out.AppendInt(mAlternatePort);
  out.Append(':');
  out.Append(mUsername);
  out.Append(':');
  out.Append(mPrivate ? 'y' : 'n');
  out.Append(':');
  out.AppendInt(mExpiresAt);
  out.Append(':');
  out.Append(mNPNToken);
  out.Append(':');
  out.Append(mValidated ? 'y' : 'n');
  out.Append(':');
  out.AppendInt(mStorageEpoch);
  out.Append(':');
  out.Append(mMixedScheme ? 'y' : 'n');
  out.Append(':');
  nsAutoCString suffix;
  mOriginAttributes.CreateSuffix(suffix);
  out.Append(suffix);
  out.Append(':');
}

AltSvcMapping::AltSvcMapping(DataStorage *storage, int32_t epoch, const nsCString &str)
  : mStorage(storage)
  , mStorageEpoch(epoch)
  , mSyncOnlyOnSuccess(false)
{
  mValidated = false;
  nsresult code;

  // The the do {} while(0) loop acts like try/catch(e){} with the break in _NS_NEXT_TOKEN
  do {
#ifdef _NS_NEXT_TOKEN
COMPILER ERROR
#endif
    #define _NS_NEXT_TOKEN start = idx + 1; idx = str.FindChar(':', start); if (idx < 0) break;
    int32_t start = 0;
    int32_t idx;
    idx = str.FindChar(':', start); if (idx < 0) break;
    mHttps = Substring(str, start, idx - start).EqualsLiteral("https");
    _NS_NEXT_TOKEN;
    mOriginHost = Substring(str, start, idx - start);
    _NS_NEXT_TOKEN;
    mOriginPort = nsCString(Substring(str, start, idx - start)).ToInteger(&code);
    _NS_NEXT_TOKEN;
    mAlternateHost = Substring(str, start, idx - start);
    _NS_NEXT_TOKEN;
    mAlternatePort = nsCString(Substring(str, start, idx - start)).ToInteger(&code);
    _NS_NEXT_TOKEN;
    mUsername = Substring(str, start, idx - start);
    _NS_NEXT_TOKEN;
    mPrivate = Substring(str, start, idx - start).EqualsLiteral("y");
    _NS_NEXT_TOKEN;
    mExpiresAt = nsCString(Substring(str, start, idx - start)).ToInteger(&code);
    _NS_NEXT_TOKEN;
    mNPNToken = Substring(str, start, idx - start);
    _NS_NEXT_TOKEN;
    mValidated = Substring(str, start, idx - start).EqualsLiteral("y");
    _NS_NEXT_TOKEN;
    mStorageEpoch = nsCString(Substring(str, start, idx - start)).ToInteger(&code);
    _NS_NEXT_TOKEN;
    mMixedScheme = Substring(str, start, idx - start).EqualsLiteral("y");
    _NS_NEXT_TOKEN;
    Unused << mOriginAttributes.PopulateFromSuffix(Substring(str, start, idx - start));
    #undef _NS_NEXT_TOKEN

    MakeHashKey(mHashKey, mHttps ? NS_LITERAL_CSTRING("https") : NS_LITERAL_CSTRING("http"),
                mOriginHost, mOriginPort, mPrivate, mOriginAttributes);
  } while (false);
}

// This is the asynchronous null transaction used to validate
// an alt-svc advertisement only for https://
class AltSvcTransaction final : public NullHttpTransaction
{
public:
    AltSvcTransaction(AltSvcMapping *map,
                      nsHttpConnectionInfo *ci,
                      nsIInterfaceRequestor *callbacks,
                      uint32_t caps)
    : NullHttpTransaction(ci, callbacks, caps & ~NS_HTTP_ALLOW_KEEPALIVE)
    , mMapping(map)
    , mRunning(true)
    , mTriedToValidate(false)
    , mTriedToWrite(false)
  {
    LOG(("AltSvcTransaction ctor %p map %p [%s -> %s]",
         this, map, map->OriginHost().get(), map->AlternateHost().get()));
    MOZ_ASSERT(mMapping);
    MOZ_ASSERT(mMapping->HTTPS());
  }

  ~AltSvcTransaction() override
  {
    LOG(("AltSvcTransaction dtor %p map %p running %d",
         this, mMapping.get(), mRunning));

    if (mRunning) {
      MaybeValidate(NS_OK);
    }
    if (!mMapping->Validated()) {
      // try again later
      mMapping->SetExpiresAt(NowInSeconds() + 2);
    }
    LOG(("AltSvcTransaction dtor %p map %p validated %d [%s]",
         this, mMapping.get(), mMapping->Validated(),
         mMapping->HashKey().get()));
  }

private:
  // check on alternate route.
  // also evaluate 'reasonable assurances' for opportunistic security
  void MaybeValidate(nsresult reason)
  {
    MOZ_ASSERT(mMapping->HTTPS()); // http:// uses the .wk path

    if (mTriedToValidate) {
      return;
    }
    mTriedToValidate = true;

    LOG(("AltSvcTransaction::MaybeValidate() %p reason=%" PRIx32 " running=%d conn=%p write=%d",
         this, static_cast<uint32_t>(reason), mRunning, mConnection.get(), mTriedToWrite));

    if (mTriedToWrite && reason == NS_BASE_STREAM_CLOSED) {
      // The normal course of events is to cause the transaction to fail with CLOSED
      // on a write - so that's a success that means the HTTP/2 session is setup.
      reason = NS_OK;
    }

    if (NS_FAILED(reason) || !mRunning || !mConnection) {
      LOG(("AltSvcTransaction::MaybeValidate %p Failed due to precondition", this));
      return;
    }

    // insist on >= http/2
    HttpVersion version = mConnection->Version();
    LOG(("AltSvcTransaction::MaybeValidate() %p version %d\n", this, static_cast<int32_t>(version)));
    if (version != HttpVersion::v2_0) {
      LOG(("AltSvcTransaction::MaybeValidate %p Failed due to protocol version", this));
      return;
    }

    nsCOMPtr<nsISupports> secInfo;
    mConnection->GetSecurityInfo(getter_AddRefs(secInfo));
    nsCOMPtr<nsISSLSocketControl> socketControl = do_QueryInterface(secInfo);

    LOG(("AltSvcTransaction::MaybeValidate() %p socketControl=%p\n",
         this, socketControl.get()));

    if (socketControl->GetFailedVerification()) {
      LOG(("AltSvcTransaction::MaybeValidate() %p "
           "not validated due to auth error", this));
      return;
    }

    LOG(("AltSvcTransaction::MaybeValidate() %p "
         "validating alternate service with successful auth check", this));
    mMapping->SetValidated(true);
  }

public:
  void Close(nsresult reason) override
  {
    LOG(("AltSvcTransaction::Close() %p reason=%" PRIx32 " running %d",
         this, static_cast<uint32_t>(reason), mRunning));

    MaybeValidate(reason);
    if (!mMapping->Validated() && mConnection) {
      mConnection->DontReuse();
    }
    NullHttpTransaction::Close(reason);
  }

  nsresult ReadSegments(nsAHttpSegmentReader *reader,
                        uint32_t count, uint32_t *countRead) override
  {
    LOG(("AltSvcTransaction::ReadSegements() %p\n", this));
    mTriedToWrite = true;
    return NullHttpTransaction::ReadSegments(reader, count, countRead);
  }

private:
  RefPtr<AltSvcMapping>   mMapping;
  uint32_t                mRunning : 1;
  uint32_t                mTriedToValidate : 1;
  uint32_t                mTriedToWrite : 1;
};

class WellKnownChecker
{
public:
  WellKnownChecker(nsIURI *uri, const nsCString &origin, uint32_t caps, nsHttpConnectionInfo *ci, AltSvcMapping *mapping)
    : mWaiting(2) // waiting for 2 channels (default and alternate) to complete
    , mOrigin(origin)
    , mAlternatePort(ci->RoutedPort())
    , mMapping(mapping)
    , mCI(ci)
    , mURI(uri)
    , mCaps(caps)
  {
    LOG(("WellKnownChecker ctor %p\n", this));
    MOZ_ASSERT(!mMapping->HTTPS());
  }

  nsresult Start()
  {
    LOG(("WellKnownChecker::Start %p\n", this));
    nsCOMPtr<nsILoadInfo> loadInfo = new LoadInfo(nsContentUtils::GetSystemPrincipal(),
                                                  nullptr, nullptr,
                                                  nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
                                                  nsIContentPolicy::TYPE_OTHER);
    loadInfo->SetOriginAttributes(mCI->GetOriginAttributes());

    RefPtr<nsHttpChannel> chan = new nsHttpChannel();
    nsresult rv;

    mTransactionAlternate = new TransactionObserver(chan, this);
    RefPtr<nsHttpConnectionInfo> newCI = mCI->Clone();
    rv = MakeChannel(chan, mTransactionAlternate, newCI, mURI, mCaps, loadInfo);
    if (NS_FAILED(rv)) {
      return rv;
    }
    chan = new nsHttpChannel();
    mTransactionOrigin = new TransactionObserver(chan, this);
    newCI = nullptr;
    return MakeChannel(chan, mTransactionOrigin, newCI, mURI, mCaps, loadInfo);
  }

  void Done(TransactionObserver *finished)
  {
    MOZ_ASSERT(NS_IsMainThread());
    LOG(("WellKnownChecker::Done %p waiting for %d\n", this, mWaiting));

    mWaiting--; // another channel is complete
    if (!mWaiting) { // there are all complete!
      nsAutoCString mAlternateCT, mOriginCT;
      mTransactionOrigin->mChannel->GetContentType(mOriginCT);
      mTransactionAlternate->mChannel->GetContentType(mAlternateCT);
      nsCOMPtr<nsIWellKnownOpportunisticUtils> uu = do_CreateInstance(NS_WELLKNOWNOPPORTUNISTICUTILS_CONTRACTID);
      bool accepted = false;

      if (!mTransactionOrigin->mStatusOK) {
        LOG(("WellKnownChecker::Done %p origin was not 200 response code\n", this));
      } else if (!mTransactionAlternate->mAuthOK) {
        LOG(("WellKnownChecker::Done %p alternate was not TLS authenticated\n", this));
      } else if (!mTransactionAlternate->mStatusOK) {
        LOG(("WellKnownChecker::Done %p alternate was not 200 response code\n", this));
      } else if (!mTransactionAlternate->mVersionOK) {
        LOG(("WellKnownChecker::Done %p alternate was not at least h2\n", this));
      } else if (!mTransactionAlternate->mWKResponse.Equals(mTransactionOrigin->mWKResponse)) {
        LOG(("WellKnownChecker::Done %p alternate and origin "
             ".wk representations don't match\norigin: %s\alternate:%s\n", this,
             mTransactionOrigin->mWKResponse.get(),
             mTransactionAlternate->mWKResponse.get()));
      } else if (!mAlternateCT.Equals(mOriginCT)) {
        LOG(("WellKnownChecker::Done %p alternate and origin content types dont match\n", this));
      } else if (!mAlternateCT.EqualsLiteral("application/json")) {
        LOG(("WellKnownChecker::Done %p .wk content type is %s\n", this, mAlternateCT.get()));
      } else if (!uu) {
        LOG(("WellKnownChecker::Done %p json parser service unavailable\n", this));
      } else {
        accepted = true;
      }

      if (accepted) {
        MOZ_ASSERT(!mMapping->HTTPS()); // https:// does not use .wk

        nsresult rv = uu->Verify(mTransactionAlternate->mWKResponse, mOrigin, mAlternatePort);
        if (NS_SUCCEEDED(rv)) {
          bool validWK = false;
          bool mixedScheme = false;
          int32_t lifetime = 0;
          Unused << uu->GetValid(&validWK);
          Unused << uu->GetLifetime(&lifetime);
          Unused << uu->GetMixed(&mixedScheme);
          if (!validWK) {
            LOG(("WellKnownChecker::Done %p json parser declares invalid\n%s\n", this, mTransactionAlternate->mWKResponse.get()));
            accepted = false;
          }
          if (accepted && (lifetime > 0)) {
            if (mMapping->TTL() > lifetime) {
              LOG(("WellKnownChecker::Done %p atl-svc lifetime reduced by .wk\n", this));
              mMapping->SetExpiresAt(NowInSeconds() + lifetime);
            } else {
              LOG(("WellKnownChecker::Done %p .wk lifetime exceeded alt-svc ma so ignored\n", this));
            }
          }
          if (accepted && mixedScheme) {
            mMapping->SetMixedScheme(true);
            LOG(("WellKnownChecker::Done %p atl-svc .wk allows mixed scheme\n", this));
          }
        } else {
          LOG(("WellKnownChecker::Done %p .wk jason eval failed to run\n", this));
          accepted = false;
        }
      }

      MOZ_ASSERT(!mMapping->Validated());
      if (accepted) {
        LOG(("WellKnownChecker::Done %p Alternate for %s ACCEPTED\n", this, mOrigin.get()));
        mMapping->SetValidated(true);
      } else {
        LOG(("WellKnownChecker::Done %p Alternate for %s FAILED\n", this, mOrigin.get()));
        // try again soon
        mMapping->SetExpiresAt(NowInSeconds() + 2);
      }

      delete this;
    }
  }

  ~WellKnownChecker()
  {
    LOG(("WellKnownChecker dtor %p\n", this));
  }

private:
  nsresult
  MakeChannel(nsHttpChannel *chan, TransactionObserver *obs, nsHttpConnectionInfo *ci,
              nsIURI *uri, uint32_t caps, nsILoadInfo *loadInfo)
  {
    uint64_t channelId;
    nsLoadFlags flags;
    if (NS_FAILED(gHttpHandler->NewChannelId(channelId)) ||
        NS_FAILED(chan->Init(uri, caps, nullptr, 0, nullptr, channelId)) ||
        NS_FAILED(chan->SetAllowAltSvc(false)) ||
        NS_FAILED(chan->SetRedirectMode(nsIHttpChannelInternal::REDIRECT_MODE_ERROR)) ||
        NS_FAILED(chan->SetLoadInfo(loadInfo)) ||
        NS_FAILED(chan->GetLoadFlags(&flags))) {
      return NS_ERROR_FAILURE;
    }
    flags |= HttpBaseChannel::LOAD_BYPASS_CACHE;
    if (NS_FAILED(chan->SetLoadFlags(flags))) {
      return NS_ERROR_FAILURE;
    }
    chan->SetTransactionObserver(obs);
    chan->SetConnectionInfo(ci);
    return chan->AsyncOpen2(obs);
  }

  RefPtr<TransactionObserver>  mTransactionAlternate;
  RefPtr<TransactionObserver>  mTransactionOrigin;
  uint32_t                     mWaiting; // semaphore
  nsCString                    mOrigin;
  int32_t                      mAlternatePort;
  RefPtr<AltSvcMapping>        mMapping;
  RefPtr<nsHttpConnectionInfo> mCI;
  nsCOMPtr<nsIURI>             mURI;
  uint32_t                     mCaps;
};

NS_IMPL_ISUPPORTS(TransactionObserver, nsIStreamListener)

TransactionObserver::TransactionObserver(nsHttpChannel *channel, WellKnownChecker *checker)
  : mChannel(channel)
  , mChecker(checker)
  , mRanOnce(false)
  , mAuthOK(false)
  , mVersionOK(false)
  , mStatusOK(false)
{
  LOG(("TransactionObserver ctor %p channel %p checker %p\n", this, channel, checker));
  mChannelRef = do_QueryInterface((nsIHttpChannel *)channel);
}

void
TransactionObserver::Complete(nsHttpTransaction *aTrans, nsresult reason)
{
  // socket thread
  MOZ_ASSERT(!NS_IsMainThread());
  if (mRanOnce) {
    return;
  }
  mRanOnce = true;

  RefPtr<nsAHttpConnection> conn = aTrans->GetConnectionReference();
  LOG(("TransactionObserver::Complete %p aTrans %p reason %" PRIx32 " conn %p\n",
       this, aTrans, static_cast<uint32_t>(reason), conn.get()));
  if (!conn) {
    return;
  }
  HttpVersion version = conn->Version();
  mVersionOK = (((reason == NS_BASE_STREAM_CLOSED) || (reason == NS_OK)) &&
                conn->Version() == HttpVersion::v2_0);

  nsCOMPtr<nsISupports> secInfo;
  conn->GetSecurityInfo(getter_AddRefs(secInfo));
  nsCOMPtr<nsISSLSocketControl> socketControl = do_QueryInterface(secInfo);
  LOG(("TransactionObserver::Complete version %u socketControl %p\n",
       static_cast<int32_t>(version), socketControl.get()));
  if (!socketControl) {
    return;
  }

  mAuthOK = !socketControl->GetFailedVerification();
  LOG(("TransactionObserve::Complete %p trans %p authOK %d versionOK %d\n",
       this, aTrans, mAuthOK, mVersionOK));
}

#define MAX_WK 32768

NS_IMETHODIMP
TransactionObserver::OnStartRequest(nsIRequest *aRequest, nsISupports *aContext)
{
  MOZ_ASSERT(NS_IsMainThread());
  // only consider the first 32KB.. because really.
  mWKResponse.SetCapacity(MAX_WK);
  return NS_OK;
}

NS_IMETHODIMP
TransactionObserver::OnDataAvailable(nsIRequest *aRequest, nsISupports *aContext,
                                     nsIInputStream *aStream, uint64_t aOffset, uint32_t aCount)
{
  MOZ_ASSERT(NS_IsMainThread());
  uint32_t oldLen = mWKResponse.Length();
  uint64_t newLen = aCount + oldLen;
  if (newLen < MAX_WK) {
    nsresult rv;
    auto handle = mWKResponse.BulkWrite(newLen, oldLen, false, rv);
    if (NS_FAILED(rv)) {
      return rv;
    }
    uint32_t amtRead;
    if (NS_SUCCEEDED(aStream->Read(handle.Elements() + oldLen, aCount, &amtRead))) {
      MOZ_ASSERT(oldLen + amtRead <= newLen);
      handle.Finish(oldLen + amtRead, false);
      LOG(("TransactionObserver onDataAvailable %p read %d of .wk [%d]\n",
           this, amtRead, mWKResponse.Length()));
    } else {
      LOG(("TransactionObserver onDataAvailable %p read error\n", this));
    }
  }
  return NS_OK;
}

NS_IMETHODIMP
TransactionObserver::OnStopRequest(nsIRequest *aRequest, nsISupports *aContext, nsresult code)
{
  MOZ_ASSERT(NS_IsMainThread());
  LOG(("TransactionObserver onStopRequest %p code %" PRIx32 "\n",
       this, static_cast<uint32_t>(code)));
  if (NS_SUCCEEDED(code)) {
    nsHttpResponseHead *hdrs = mChannel->GetResponseHead();
    LOG(("TransactionObserver onStopRequest %p http resp %d\n",
         this, hdrs ? hdrs->Status() : -1));
    mStatusOK = hdrs && (hdrs->Status() == 200);
  }
  if (mChecker) {
    mChecker->Done(this);
  }
  return NS_OK;
}

already_AddRefed<AltSvcMapping>
AltSvcCache::LookupMapping(const nsCString &key, bool privateBrowsing)
{
  LOG(("AltSvcCache::LookupMapping %p %s\n", this, key.get()));
  if (!mStorage) {
    LOG(("AltSvcCache::LookupMapping %p no backing store\n", this));
    return nullptr;
  }
  nsCString val(mStorage->Get(key,
                              privateBrowsing ? DataStorage_Private : DataStorage_Persistent));
  if (val.IsEmpty()) {
    LOG(("AltSvcCache::LookupMapping %p MISS\n", this));
    return nullptr;
  }
  RefPtr<AltSvcMapping> rv = new AltSvcMapping(mStorage, mStorageEpoch, val);
  if (!rv->Validated() && (rv->StorageEpoch() != mStorageEpoch)) {
    // this was an in progress validation abandoned in a different session
    // rare edge case will not detect session change - that's ok as only impact
    // will be loss of alt-svc to this origin for this session.
    LOG(("AltSvcCache::LookupMapping %p invalid hit - MISS\n", this));
    mStorage->Remove(key,
                     rv->Private() ? DataStorage_Private : DataStorage_Persistent);
    return nullptr;
  }

  if (rv->TTL() <= 0) {
    LOG(("AltSvcCache::LookupMapping %p expired hit - MISS\n", this));
    mStorage->Remove(key,
                     rv->Private() ? DataStorage_Private : DataStorage_Persistent);
    return nullptr;
  }

  MOZ_ASSERT(rv->Private() == privateBrowsing);
  LOG(("AltSvcCache::LookupMapping %p HIT %p\n", this, rv.get()));
  return rv.forget();
}

void
AltSvcCache::UpdateAltServiceMapping(AltSvcMapping *map, nsProxyInfo *pi,
                                     nsIInterfaceRequestor *aCallbacks,
                                     uint32_t caps,
                                     const OriginAttributes &originAttributes)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (!mStorage) {
    return;
  }
  RefPtr<AltSvcMapping> existing = LookupMapping(map->HashKey(), map->Private());
  LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p existing %p %s validated=%d",
       this, map, existing.get(), map->AlternateHost().get(),
       existing ? existing->Validated() : 0));

  if (existing && existing->Validated()) {
    if (existing->RouteEquals(map)){
      // update expires in storage
      // if this is http:// then a ttl can only be extended via .wk, so ignore this
      // header path unless it is making things shorter
      if (existing->HTTPS()) {
        LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p updates ttl of %p\n",
             this, map, existing.get()));
        existing->SetExpiresAt(map->GetExpiresAt());
      } else {
        if (map->GetExpiresAt() < existing->GetExpiresAt()) {
          LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p reduces ttl of %p\n",
               this, map, existing.get()));
          existing->SetExpiresAt(map->GetExpiresAt());
        } else {
          LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p tries to extend %p but"
               " cannot as without .wk\n",
               this, map, existing.get()));
        }
      }
      return;
    }

    if (map->GetExpiresAt() < existing->GetExpiresAt()) {
      LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p ttl shorter than %p, ignoring",
           this, map, existing.get()));
      return;
    }

    // new alternate. start new validation
    LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p may overwrite %p\n",
         this, map, existing.get()));
  }

  if (existing && !existing->Validated()) {
    LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p ignored because %p "
         "still in progress\n", this, map, existing.get()));
    return;
  }

  // start new validation, but don't overwrite a valid existing mapping unless
  // this completes successfully
  MOZ_ASSERT(!map->Validated());
  if (!existing) {
    map->Sync();
  } else {
    map->SetSyncOnlyOnSuccess(true);
  }

  RefPtr<nsHttpConnectionInfo> ci;
  map->GetConnectionInfo(getter_AddRefs(ci), pi, originAttributes);
  caps |= ci->GetAnonymous() ? NS_HTTP_LOAD_ANONYMOUS : 0;
  caps |= NS_HTTP_ERROR_SOFTLY;

  if (map->HTTPS()) {
    LOG(("AltSvcCache::UpdateAltServiceMapping %p validation via "
         "speculative connect started\n", this));
    // for https resources we only establish a connection
    nsCOMPtr<nsIInterfaceRequestor> callbacks = new AltSvcOverride(aCallbacks);
    RefPtr<AltSvcTransaction> nullTransaction =
      new AltSvcTransaction(map, ci, aCallbacks, caps);
    nsresult rv = gHttpHandler->ConnMgr()->SpeculativeConnect(ci, callbacks, caps, nullTransaction);
    if (NS_FAILED(rv)) {
      LOG(("AltSvcCache::UpdateAltServiceMapping %p "
           "speculative connect failed with code %08x\n", this,
           static_cast<uint32_t>(rv)));
    }
  } else {
    // for http:// resources we fetch .well-known too
    nsAutoCString origin (NS_LITERAL_CSTRING("http://") + map->OriginHost());
    if (map->OriginPort() != NS_HTTP_DEFAULT_PORT) {
      origin.Append(':');
      origin.AppendInt(map->OriginPort());
    }

    nsCOMPtr<nsIURI> wellKnown;
    nsAutoCString uri(origin);
    uri.AppendLiteral("/.well-known/http-opportunistic");
    NS_NewURI(getter_AddRefs(wellKnown), uri);

    auto *checker = new WellKnownChecker(wellKnown, origin, caps, ci, map);
    if (NS_FAILED(checker->Start())) {
      LOG(("AltSvcCache::UpdateAltServiceMapping %p .wk checker failed to start\n", this));
      map->SetExpired();
      delete checker;
      checker = nullptr;
    } else {
      // object deletes itself when done if started
      LOG(("AltSvcCache::UpdateAltServiceMapping %p .wk checker started %p\n", this, checker));
    }
  }
}

already_AddRefed<AltSvcMapping>
AltSvcCache::GetAltServiceMapping(const nsACString &scheme, const nsACString &host,
                                  int32_t port, bool privateBrowsing,
                                  const OriginAttributes &originAttributes)
{
  bool isHTTPS;
  MOZ_ASSERT(NS_IsMainThread());
  if (!mStorage) {
    // DataStorage gives synchronous access to a memory based hash table
    // that is backed by disk where those writes are done asynchronously
    // on another thread
    mStorage = DataStorage::Get(DataStorageClass::AlternateServices);
    if (mStorage) {
      bool storageWillPersist = false;
      if (NS_FAILED(mStorage->Init(storageWillPersist))) {
        mStorage = nullptr;
      }
    }
    if (!mStorage) {
      LOG(("AltSvcCache::GetAltServiceMapping WARN NO STORAGE\n"));
    }
    mStorageEpoch = NowInSeconds();
  }

  if (NS_FAILED(SchemeIsHTTPS(scheme, isHTTPS))) {
    return nullptr;
  }
  if (!gHttpHandler->AllowAltSvc()) {
    return nullptr;
  }
  if (!gHttpHandler->AllowAltSvcOE() && !isHTTPS) {
    return nullptr;
  }

  nsAutoCString key;
  AltSvcMapping::MakeHashKey(key, scheme, host, port, privateBrowsing, originAttributes);
  RefPtr<AltSvcMapping> existing = LookupMapping(key, privateBrowsing);
  LOG(("AltSvcCache::GetAltServiceMapping %p key=%s "
       "existing=%p validated=%d ttl=%d",
       this, key.get(), existing.get(), existing ? existing->Validated() : 0,
       existing ? existing->TTL() : 0));
  if (existing && !existing->Validated()) {
    existing = nullptr;
  }
  return existing.forget();
}

class ProxyClearHostMapping : public Runnable {
public:
  explicit ProxyClearHostMapping(const nsACString& host,
                                 int32_t port,
                                 const OriginAttributes& originAttributes)
    : Runnable("net::ProxyClearHostMapping")
    , mHost(host)
    , mPort(port)
    , mOriginAttributes(originAttributes)
  {
  }

  NS_IMETHOD Run() override
  {
    MOZ_ASSERT(NS_IsMainThread());
    gHttpHandler->ConnMgr()->ClearHostMapping(mHost, mPort, mOriginAttributes);
    return NS_OK;
    }
private:
    nsCString mHost;
    int32_t mPort;
    OriginAttributes mOriginAttributes;
};

void
AltSvcCache::ClearHostMapping(const nsACString &host, int32_t port,
                              const OriginAttributes &originAttributes)
{
  if (!NS_IsMainThread()) {
    nsCOMPtr<nsIRunnable> event = new ProxyClearHostMapping(host, port, originAttributes);
    if (event) {
      NS_DispatchToMainThread(event);
    }
    return;
  }
  nsAutoCString key;
  AltSvcMapping::MakeHashKey(key, NS_LITERAL_CSTRING("http"), host, port, true,
                             originAttributes);
  RefPtr<AltSvcMapping> existing = LookupMapping(key, true);
  if (existing) {
    existing->SetExpired();
  }

  AltSvcMapping::MakeHashKey(key, NS_LITERAL_CSTRING("https"), host, port, true,
                             originAttributes);
  existing = LookupMapping(key, true);
  if (existing) {
    existing->SetExpired();
  }

  AltSvcMapping::MakeHashKey(key, NS_LITERAL_CSTRING("http"), host, port, false,
                             originAttributes);
  existing = LookupMapping(key, false);
  if (existing) {
    existing->SetExpired();
  }

  AltSvcMapping::MakeHashKey(key, NS_LITERAL_CSTRING("https"), host, port, false,
                             originAttributes);
  existing = LookupMapping(key, false);
  if (existing) {
    existing->SetExpired();
  }
}

void
AltSvcCache::ClearHostMapping(nsHttpConnectionInfo *ci)
{
  if (!ci->GetOrigin().IsEmpty()) {
    ClearHostMapping(ci->GetOrigin(), ci->OriginPort(), ci->GetOriginAttributes());
  }
}

void
AltSvcCache::ClearAltServiceMappings()
{
    MOZ_ASSERT(NS_IsMainThread());
    if (mStorage) {
      mStorage->Clear();
    }
}

NS_IMETHODIMP
AltSvcOverride::GetInterface(const nsIID &iid, void **result)
{
  if (NS_SUCCEEDED(QueryInterface(iid, result)) && *result) {
    return NS_OK;
  }
  return mCallbacks->GetInterface(iid, result);
}

NS_IMETHODIMP
AltSvcOverride::GetIgnoreIdle(bool *ignoreIdle)
{
  *ignoreIdle = true;
  return NS_OK;
}

NS_IMETHODIMP
AltSvcOverride::GetParallelSpeculativeConnectLimit(
  uint32_t *parallelSpeculativeConnectLimit)
{
  *parallelSpeculativeConnectLimit = 32;
  return NS_OK;
}

NS_IMETHODIMP
AltSvcOverride::GetIsFromPredictor(bool *isFromPredictor)
{
  *isFromPredictor = false;
  return NS_OK;
}

NS_IMETHODIMP
AltSvcOverride::GetAllow1918(bool *allow)
{
  // normally we don't do speculative connects to 1918.. and we use
  // speculative connects for the mapping validation, so override
  // that default here for alt-svc
  *allow = true;
  return NS_OK;
}

NS_IMPL_ISUPPORTS(AltSvcOverride, nsIInterfaceRequestor, nsISpeculativeConnectionOverrider)

} // namespace net
} // namespace mozilla