diff options
Diffstat (limited to 'netwerk/protocol/http/AlternateServices.cpp')
-rw-r--r-- | netwerk/protocol/http/AlternateServices.cpp | 1322 |
1 files changed, 1322 insertions, 0 deletions
diff --git a/netwerk/protocol/http/AlternateServices.cpp b/netwerk/protocol/http/AlternateServices.cpp new file mode 100644 index 0000000000..83996ce87c --- /dev/null +++ b/netwerk/protocol/http/AlternateServices.cpp @@ -0,0 +1,1322 @@ +/* -*- 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 "mozilla/Atomics.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/net/AltSvcTransactionChild.h" +#include "mozilla/net/AltSvcTransactionParent.h" +#include "nsComponentManagerUtils.h" +#include "nsEscape.h" +#include "nsHttpChannel.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpHandler.h" +#include "nsHttpTransaction.h" +#include "nsIOService.h" +#include "nsITLSSocketControl.h" +#include "nsIWellKnownOpportunisticUtils.h" +#include "nsThreadUtils.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(!originScheme.LowerCaseEqualsLiteral("https") && + !originScheme.LowerCaseEqualsLiteral("http"), + "The scheme should already be lowercase"); + 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, + bool aDontValidate /* = false */) { // aDontValidate is only used for + // testing + MOZ_ASSERT(NS_IsMainThread()); + LOG(("AltSvcMapping::ProcessHeader: %s\n", buf.get())); + + if (StaticPrefs::network_http_altsvc_proxy_checks() && + !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); + int32_t numEntriesInHeader = parsedAltSvc.mValues.Length(); + + // Only use one http3 version. + bool http3Found = false; + + 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; + bool isHttp3 = 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; + --numEntriesInHeader; // Only want to keep track of actual alt-svc + // maps, not clearing + break; + } + + // h2=[hostname]:443 or h3-xx=[hostname]:port + // XX is current version we support and it is define in nsHttp.h. + isHttp3 = gHttpHandler->IsHttp3VersionSupported(currentName); + 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()); + } 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->AltServiceCache()->ClearHostMapping(originHost, originPort, + originAttributes); + continue; + } + + if (NS_FAILED(NS_CheckPortSafety(portno, originScheme.get()))) { + LOG(("Alt Svc doesn't allow port %d, ignoring", portno)); + continue; + } + + // unescape modifies a c string in place, so afterwards + // update nsCString length + nsUnescape(npnToken.BeginWriting()); + npnToken.SetLength(strlen(npnToken.BeginReading())); + + if (http3Found && isHttp3) { + LOG(("Alt Svc ignore multiple Http3 options (%s)", npnToken.get())); + continue; + } + + SpdyInformation* spdyInfo = gHttpHandler->SpdyInfo(); + if (!(npnToken.Equals(spdyInfo->VersionString) && + StaticPrefs::network_http_http2_enabled()) && + !(isHttp3 && nsHttpHandler::IsHttp3Enabled() && + !gHttpHandler->IsHttp3Excluded(hostname.IsEmpty() ? originHost + : hostname))) { + LOG(("Alt Svc unknown protocol %s, ignoring", npnToken.get())); + continue; + } + + if (isHttp3) { + http3Found = true; + } + + RefPtr<AltSvcMapping> mapping = + new AltSvcMapping(gHttpHandler->AltServiceCache()->GetStoragePtr(), + gHttpHandler->AltServiceCache()->StorageEpoch(), + originScheme, originHost, originPort, username, + privateBrowsing, NowInSeconds() + maxage, hostname, + portno, npnToken, originAttributes, isHttp3); + 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->AltServiceCache()->ClearHostMapping(originHost, originPort, + originAttributes); + } else if (!aDontValidate) { + gHttpHandler->UpdateAltServiceMapping(mapping, proxyInfo, callbacks, caps, + originAttributes); + } else { + gHttpHandler->UpdateAltServiceMappingWithoutValidation( + mapping, proxyInfo, callbacks, caps, originAttributes); + } + } + + if (numEntriesInHeader) { // Ignore headers that were just "alt-svc: clear" + Telemetry::Accumulate(Telemetry::HTTP_ALTSVC_ENTRIES_PER_HEADER, + numEntriesInHeader); + } +} + +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, + bool aIsHttp3) + : mStorage(storage), + mStorageEpoch(epoch), + mAlternateHost(alternateHost), + mAlternatePort(alternatePort), + mOriginHost(originHost), + mOriginPort(originPort), + mUsername(username), + mPrivate(privateBrowsing), + mExpiresAt(expiresAt), + mNPNToken(npnToken), + mOriginAttributes(originAttributes), + mIsHttp3(aIsHttp3) { + 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()) && !mIsHttp3) { + // Http2 on the same host:port does not make sense because we are + // connecting to the same end point over the same protocol (TCP) as with + // original host. On the other hand, for Http3 alt-svc can be hosted on + // the same host:port because protocol(UDP vs. TCP) is always different and + // we are not connecting to the same end point. + LOG(("Alt Svc is also origin Svc - ignoring\n")); + mExpiresAt = 0; // invalid + } + + if (mExpiresAt) { + MakeHashKey(mHashKey, originScheme, mOriginHost, mOriginPort, mPrivate, + mOriginAttributes, mIsHttp3); + } +} + +void AltSvcMapping::MakeHashKey(nsCString& outKey, + const nsACString& originScheme, + const nsACString& originHost, + int32_t originPort, bool privateBrowsing, + const OriginAttributes& originAttributes, + bool aHttp3) { + 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); + outKey.Append(':'); + + outKey.Append(aHttp3 ? '3' : '.'); +} + +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, mIsHttp3, false); + + // 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) { + // Be careful, when serializing new members, add them to the end of this list. + out = mHttps ? "https:"_ns : "http:"_ns; + 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(':'); + out.Append(""_ns); // Formerly topWindowOrigin. Now unused empty string. + out.Append('|'); // Be careful, the top window origin may contain colons! + out.Append('n'); // Formerly mIsolated. Now always 'n'. Should remove someday + out.Append(':'); + out.Append(mIsHttp3 ? 'y' : 'n'); + out.Append(':'); + // Add code to serialize new members here! +} + +AltSvcMapping::AltSvcMapping(DataStorage* storage, int32_t epoch, + const nsCString& str) + : mStorage(storage), mStorageEpoch(epoch) { + mValidated = false; + nsresult code; + char separator = ':'; + + // 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(separator, start); \ + if (idx < 0) break; + int32_t start = 0; + int32_t idx; + idx = str.FindChar(separator, start); + if (idx < 0) break; + // Be careful, when deserializing new members, add them to the end of this + // list. + 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)); + // The separator after the top window origin is a pipe character since the + // origin string can contain colons. + separator = '|'; + _NS_NEXT_TOKEN; + // TopWindowOrigin used to be encoded here. Now it's unused. + separator = ':'; + _NS_NEXT_TOKEN; + // mIsolated used to be encoded here. Now it's unused. + _NS_NEXT_TOKEN; + mIsHttp3 = Substring(str, start, idx - start).EqualsLiteral("y"); + // Add code to deserialize new members here! +#undef _NS_NEXT_TOKEN + + MakeHashKey(mHashKey, mHttps ? "https"_ns : "http"_ns, mOriginHost, + mOriginPort, mPrivate, mOriginAttributes, mIsHttp3); + } while (false); +} + +AltSvcMappingValidator::AltSvcMappingValidator(AltSvcMapping* aMap) + : mMapping(aMap) { + LOG(("AltSvcMappingValidator ctor %p map %p [%s -> %s]", this, aMap, + aMap->OriginHost().get(), aMap->AlternateHost().get())); + MOZ_ASSERT(mMapping); + MOZ_ASSERT(mMapping->HTTPS()); // http:// uses the .wk path +} + +void AltSvcMappingValidator::OnTransactionDestroy(bool aValidateResult) { + mMapping->SetValidated(aValidateResult); + if (!mMapping->Validated()) { + // try again later + mMapping->SetExpiresAt(NowInSeconds() + 2); + } + LOG( + ("AltSvcMappingValidator::OnTransactionDestroy %p map %p validated %d " + "[%s]", + this, mMapping.get(), mMapping->Validated(), mMapping->HashKey().get())); +} + +void AltSvcMappingValidator::OnTransactionClose(bool aValidateResult) { + mMapping->SetValidated(aValidateResult); + LOG( + ("AltSvcMappingValidator::OnTransactionClose %p map %p validated %d " + "[%s]", + this, mMapping.get(), mMapping->Validated(), mMapping->HashKey().get())); +} + +template <class Validator> +AltSvcTransaction<Validator>::AltSvcTransaction( + nsHttpConnectionInfo* ci, nsIInterfaceRequestor* callbacks, uint32_t caps, + Validator* aValidator, bool aIsHttp3) + : SpeculativeTransaction(ci, callbacks, caps), + mValidator(aValidator), + mIsHttp3(aIsHttp3), + mRunning(true), + mTriedToValidate(false), + mTriedToWrite(false), + mValidatedResult(false) { + MOZ_ASSERT_IF(nsIOService::UseSocketProcess(), XRE_IsSocketProcess()); + MOZ_ASSERT_IF(!nsIOService::UseSocketProcess(), XRE_IsParentProcess()); + // We don't want to let this transaction use consistent connection. + mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE; +} + +template <class Validator> +AltSvcTransaction<Validator>::~AltSvcTransaction() { + LOG(("AltSvcTransaction dtor %p running %d", this, mRunning)); + + if (mRunning) { + mValidatedResult = MaybeValidate(NS_OK); + mValidator->OnTransactionDestroy(mValidatedResult); + } +} + +template <class Validator> +bool AltSvcTransaction<Validator>::MaybeValidate(nsresult reason) { + if (mTriedToValidate) { + return mValidatedResult; + } + 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 false; + } + + // insist on >= http/2 + HttpVersion version = mConnection->Version(); + LOG(("AltSvcTransaction::MaybeValidate() %p version %d\n", this, + static_cast<int32_t>(version))); + if ((!mIsHttp3 && (version != HttpVersion::v2_0)) || + (mIsHttp3 && (version != HttpVersion::v3_0))) { + LOG( + ("AltSvcTransaction::MaybeValidate %p Failed due to protocol version" + " expacted %s.", + this, mIsHttp3 ? "Http3" : "Http2")); + return false; + } + + nsCOMPtr<nsITLSSocketControl> socketControl; + mConnection->GetTLSSocketControl(getter_AddRefs(socketControl)); + + 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 false; + } + + LOG( + ("AltSvcTransaction::MaybeValidate() %p " + "validating alternate service with successful auth check", + this)); + + return true; +} + +template <class Validator> +void AltSvcTransaction<Validator>::Close(nsresult reason) { + LOG(("AltSvcTransaction::Close() %p reason=%" PRIx32 " running %d", this, + static_cast<uint32_t>(reason), mRunning)); + + mValidatedResult = MaybeValidate(reason); + mValidator->OnTransactionClose(mValidatedResult); + if (!mValidatedResult && mConnection) { + mConnection->DontReuse(); + } + NullHttpTransaction::Close(reason); +} + +template <class Validator> +nsresult AltSvcTransaction<Validator>::ReadSegments( + nsAHttpSegmentReader* reader, uint32_t count, uint32_t* countRead) { + LOG(("AltSvcTransaction::ReadSegements() %p\n", this)); + mTriedToWrite = true; + return NullHttpTransaction::ReadSegments(reader, count, countRead); +} + +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_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + loadInfo->SetOriginAttributes(mCI->GetOriginAttributes()); + // allow deprecated HTTP request from SystemPrincipal + loadInfo->SetAllowDeprecatedSystemRequests(true); + + 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 or h3\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); + if (NS_SUCCEEDED(rv)) { + bool validWK = false; + Unused << uu->GetValid(&validWK); + if (!validWK) { + LOG(("WellKnownChecker::Done %p json parser declares invalid\n%s\n", + this, mTransactionAlternate->mWKResponse.get())); + accepted = false; + } + } 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; + + ExtContentPolicyType contentPolicyType = + loadInfo->GetExternalContentPolicyType(); + + if (NS_FAILED(gHttpHandler->NewChannelId(channelId)) || + NS_FAILED(chan->Init(uri, caps, nullptr, 0, nullptr, channelId, + contentPolicyType, loadInfo)) || + NS_FAILED(chan->SetAllowAltSvc(false)) || + NS_FAILED(chan->SetRedirectMode( + nsIHttpChannelInternal::REDIRECT_MODE_ERROR)) || + 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->AsyncOpen(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), + mStatusOK(false), + mAuthOK(false), + mVersionOK(false) { + LOG(("TransactionObserver ctor %p channel %p checker %p\n", this, channel, + checker)); + mChannelRef = do_QueryInterface((nsIHttpChannel*)channel); +} + +void TransactionObserver::Complete(bool versionOK, bool authOK, + nsresult reason) { + if (mRanOnce) { + return; + } + mRanOnce = true; + + mVersionOK = versionOK; + mAuthOK = authOK; + + LOG( + ("TransactionObserve::Complete %p authOK %d versionOK %d" + " reason %" PRIx32, + this, authOK, versionOK, static_cast<uint32_t>(reason))); +} + +#define MAX_WK 32768 + +NS_IMETHODIMP +TransactionObserver::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + // only consider the first 32KB.. because really. + mWKResponse.SetCapacity(MAX_WK); + return NS_OK; +} + +NS_IMETHODIMP +TransactionObserver::OnDataAvailable(nsIRequest* aRequest, + 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) { + auto handleOrErr = mWKResponse.BulkWrite(newLen, oldLen, false); + if (handleOrErr.isErr()) { + return handleOrErr.unwrapErr(); + } + auto handle = handleOrErr.unwrap(); + 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 [%zd]\n", + this, amtRead, mWKResponse.Length())); + } else { + LOG(("TransactionObserver onDataAvailable %p read error\n", this)); + } + } + return NS_OK; +} + +NS_IMETHODIMP +TransactionObserver::OnStopRequest(nsIRequest* aRequest, 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; +} + +void AltSvcCache::EnsureStorageInited() { + static Atomic<bool> initialized(false); + + if (initialized) { + return; + } + + auto initTask = [&]() { + MOZ_ASSERT(NS_IsMainThread()); + + // 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) { + LOG(("AltSvcCache::EnsureStorageInited WARN NO STORAGE\n")); + return; + } + + if (NS_FAILED(mStorage->Init())) { + mStorage = nullptr; + } else { + initialized = true; + } + + mStorageEpoch = NowInSeconds(); + }; + + if (NS_IsMainThread()) { + initTask(); + return; + } + + nsCOMPtr<nsIEventTarget> main = GetMainThreadSerialEventTarget(); + if (!main) { + return; + } + + SyncRunnable::DispatchToThread( + main, + NS_NewRunnableFunction("AltSvcCache::EnsureStorageInited", initTask)); +} + +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; + } + + if (NS_IsMainThread() && !mStorage->IsReady()) { + LOG(("AltSvcCache::LookupMapping %p skip when storage is not ready\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->IsHttp3() && + (!nsHttpHandler::IsHttp3Enabled() || + !gHttpHandler->IsHttp3VersionSupported(rv->NPNToken()) || + gHttpHandler->IsHttp3Excluded(rv->AlternateHost()))) { + // If Http3 is disabled or the version not supported anymore, remove the + // mapping. + 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(); +} + +// This is only used for testing! +void AltSvcCache::UpdateAltServiceMappingWithoutValidation( + 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::UpdateAltServiceMappingWithoutValidation %p map %p " + "existing %p %s", + this, map, existing.get(), map->AlternateHost().get())); + if (!existing) { + map->SetValidated(true); + } +} + +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())); + } + } + Telemetry::Accumulate(Telemetry::HTTP_ALTSVC_MAPPING_CHANGED_TARGET, + false); + 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())); + Telemetry::Accumulate(Telemetry::HTTP_ALTSVC_MAPPING_CHANGED_TARGET, true); + } + + if (existing && !existing->Validated()) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p ignored because %p " + "still in progress\n", + this, map, existing.get())); + return; + } + + if (map->IsHttp3()) { + bool isDirectOrNoProxy = pi ? pi->IsDirect() : true; + if (!isDirectOrNoProxy) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p ignored h3 because " + "proxy is in use %p\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<AltSvcMappingValidator> validator = new AltSvcMappingValidator(map); + RefPtr<SpeculativeTransaction> transaction; + if (nsIOService::UseSocketProcess()) { + RefPtr<AltSvcTransactionParent> parent = + new AltSvcTransactionParent(ci, aCallbacks, caps, validator); + if (!parent->Init()) { + return; + } + transaction = parent; + } else { + transaction = new AltSvcTransaction<AltSvcMappingValidator>( + ci, aCallbacks, caps, validator, map->IsHttp3()); + } + + nsresult rv = + gHttpHandler->SpeculativeConnect(ci, callbacks, caps, transaction); + 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("http://"_ns); + + // Check whether origin is an ipv6 address. In that case we need to add + // '[]'. + if (map->OriginHost().FindChar(':') != kNotFound) { + origin.Append('['); + origin.Append(map->OriginHost()); + origin.Append(']'); + } else { + origin.Append(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 aHttp2Allowed, bool aHttp3Allowed) { + EnsureStorageInited(); + + bool isHTTPS; + if (NS_FAILED(SchemeIsHTTPS(scheme, isHTTPS))) { + return nullptr; + } + if (!gHttpHandler->AllowAltSvc()) { + return nullptr; + } + if (!gHttpHandler->AllowAltSvcOE() && !isHTTPS) { + return nullptr; + } + + // First look for HTTP3 + if (aHttp3Allowed) { + nsAutoCString key; + AltSvcMapping::MakeHashKey(key, scheme, host, port, privateBrowsing, + originAttributes, true); + 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()) { + return existing.forget(); + } + } + + // Now look for HTTP2. + if (aHttp2Allowed) { + nsAutoCString key; + AltSvcMapping::MakeHashKey(key, scheme, host, port, privateBrowsing, + originAttributes, false); + 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()) { + return existing.forget(); + } + } + + return nullptr; +} + +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->AltServiceCache()->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) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!NS_IsMainThread()) { + nsCOMPtr<nsIRunnable> event = + new ProxyClearHostMapping(host, port, originAttributes); + if (event) { + NS_DispatchToMainThread(event); + } + return; + } + nsAutoCString key; + for (int secure = 0; secure < 2; ++secure) { + constexpr auto http = "http"_ns; + constexpr auto https = "https"_ns; + const nsLiteralCString& scheme = secure ? https : http; + for (int pb = 1; pb >= 0; --pb) { + AltSvcMapping::MakeHashKey(key, scheme, host, port, bool(pb), + originAttributes, false); + RefPtr<AltSvcMapping> existing = LookupMapping(key, bool(pb)); + if (existing) { + existing->SetExpired(); + } + AltSvcMapping::MakeHashKey(key, scheme, host, port, bool(pb), + originAttributes, true); + existing = LookupMapping(key, bool(pb)); + 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(); + } +} + +nsresult AltSvcCache::GetAltSvcCacheKeys(nsTArray<nsCString>& value) { + MOZ_ASSERT(NS_IsMainThread()); + if (gHttpHandler->AllowAltSvc() && mStorage) { + nsTArray<DataStorageItem> items; + mStorage->GetAll(&items); + + for (const auto& item : items) { + value.AppendElement(item.key); + } + } + return NS_OK; +} + +NS_IMETHODIMP +AltSvcOverride::GetInterface(const nsIID& iid, void** result) { + if (NS_SUCCEEDED(QueryInterface(iid, result)) && *result) { + return NS_OK; + } + + if (mCallbacks) { + return mCallbacks->GetInterface(iid, result); + } + + return NS_ERROR_NO_INTERFACE; +} + +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; +} + +template class AltSvcTransaction<AltSvcTransactionChild>; + +NS_IMPL_ISUPPORTS(AltSvcOverride, nsIInterfaceRequestor, + nsISpeculativeConnectionOverrider) + +} // namespace net +} // namespace mozilla |