/* -*- 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 "nsISSLStatusProvider.h"
#include "nsISSLStatus.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.Equals(NS_LITERAL_CSTRING("https"));

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

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 NeckoOriginAttributes &originAttributes)
{
  MOZ_ASSERT(NS_IsMainThread());
  LOG(("AltSvcMapping::ProcessHeader: %s\n", buf.get()));
  if (!callbacks) {
    return;
  }

  if (proxyInfo && !proxyInfo->IsDirect()) {
    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.Equals(NS_LITERAL_CSTRING("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.Equals(NS_LITERAL_CSTRING("ma"))) {
        maxage = atoi(PromiseFlatCString(currentValue).get());
        break;
      } else {
        LOG(("Alt Svc ignoring parameter %s", currentName.BeginReading()));
      }
    }

    if (clearEntry) {
      LOG(("Alt Svc clearing mapping for %s:%d", originHost.get(), originPort));
      gHttpHandler->ConnMgr()->ClearHostMapping(originHost, originPort);
      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);
    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);
    } 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)
  : mStorage(storage)
  , mStorageEpoch(epoch)
  , mAlternateHost(alternateHost)
  , mAlternatePort(alternatePort)
  , mOriginHost(originHost)
  , mOriginPort(originPort)
  , mUsername(username)
  , mPrivate(privateBrowsing)
  , mExpiresAt(expiresAt)
  , mValidated(false)
  , mMixedScheme(false)
  , mNPNToken(npnToken)
{
  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);
  }
}

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

  if (originPort == -1) {
    bool isHttps = originScheme.Equals("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' : '.');
}

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

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

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

  if (!NS_IsMainThread()) {
    nsCOMPtr<nsIRunnable> r;
    r = NewRunnableMethod<nsCString>(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 NeckoOriginAttributes &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(':');
}

AltSvcMapping::AltSvcMapping(DataStorage *storage, int32_t epoch, const nsCString &str)
  : mStorage(storage)
  , mStorageEpoch(epoch)
{
  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).Equals(NS_LITERAL_CSTRING("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).Equals(NS_LITERAL_CSTRING("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).Equals(NS_LITERAL_CSTRING("y"));
    _NS_NEXT_TOKEN;
    mStorageEpoch = nsCString(Substring(str, start, idx - start)).ToInteger(&code);
    _NS_NEXT_TOKEN;
    mMixedScheme = Substring(str, start, idx - start).Equals(NS_LITERAL_CSTRING("y"));
    #undef _NS_NEXT_TOKEN

    MakeHashKey(mHashKey, mHttps ? NS_LITERAL_CSTRING("https") : NS_LITERAL_CSTRING("http"),
                mOriginHost, mOriginPort, mPrivate);
  } 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=%x running=%d conn=%p write=%d",
         this, 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
    uint32_t version = mConnection->Version();
    LOG(("AltSvcTransaction::MaybeValidate() %p version %d\n", this, version));
    if (version != HTTP_VERSION_2) {
      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=%x running %d",
         this, 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"));
    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.Equals(NS_LITERAL_CSTRING("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;
          uu->GetValid(&validWK);
          uu->GetLifetime(&lifetime);
          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)
  {
    nsID 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 %x conn %p\n",
       this, aTrans, reason, conn.get()));
  if (!conn) {
    return;
  }
  uint32_t version = conn->Version();
  mVersionOK = (((reason == NS_BASE_STREAM_CLOSED) || (reason == NS_OK)) &&
                conn->Version() == HTTP_VERSION_2);

  nsCOMPtr<nsISupports> secInfo;
  conn->GetSecurityInfo(getter_AddRefs(secInfo));
  nsCOMPtr<nsISSLSocketControl> socketControl = do_QueryInterface(secInfo);
  LOG(("TransactionObserver::Complete version %u socketControl %p\n",
       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());
  uint64_t newLen = aCount + mWKResponse.Length();
  if (newLen < MAX_WK) {
    char *startByte =  reinterpret_cast<char *>(mWKResponse.BeginWriting()) + mWKResponse.Length();
    uint32_t amtRead;
    if (NS_SUCCEEDED(aStream->Read(startByte, aCount, &amtRead))) {
      MOZ_ASSERT(mWKResponse.Length() + amtRead < MAX_WK);
      mWKResponse.SetLength(mWKResponse.Length() + amtRead);
      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 %x\n", this, 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 NeckoOriginAttributes &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;
    }

    // new alternate. remove old entry and start new validation
    LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p overwrites %p\n",
         this, map, existing.get()));
    existing = nullptr;
    mStorage->Remove(map->HashKey(),
                     map->Private() ? DataStorage_Private : DataStorage_Persistent);
  }

  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
  MOZ_ASSERT(!map->Validated());
  map->Sync();

  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);
    gHttpHandler->ConnMgr()->SpeculativeConnect(ci, callbacks, caps, nullTransaction);
  } 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.Append(NS_LITERAL_CSTRING("/.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)
{
  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(NS_LITERAL_STRING("AlternateServices.txt"));
    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);
  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)
    : mHost(host)
    , mPort(port)
    {}

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

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

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

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

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

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

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