/* 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 <algorithm>
#include "mozilla/Atomics.h"
#include "mozilla/glean/NetwerkProtocolHttpMetrics.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 "mozilla/SyncRunnable.h"
#include "nsEscape.h"
#include "nsHttpConnectionInfo.h"
#include "nsHttpHandler.h"
#include "nsHttpTransaction.h"
#include "nsIOService.h"
#include "nsITLSSocketControl.h"
#include "nsThreadUtils.h"
#include "xpcpublic.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) {
// TODO: We also need to make sure the inner connection will connect to the
// routed host.
return !proxyInfo || proxyInfo->IsDirect() || proxyInfo->IsSOCKS() ||
proxyInfo->IsHttp3Proxy();
}
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,
nsHttpConnectionInfo* aTransConnInfo,
bool aDontValidate
/* = false */) {
LOG((
"AltSvcMapping::ProcessHeader: %s\n", buf.get()));
// In tests, this might be called off the main thread. If so, dispatch it
// synchronously to the main thread.
if (!NS_IsMainThread() && xpc::AreNonLocalConnectionsDisabled()) {
nsCOMPtr<nsIThread> mainThread;
nsresult rv = NS_GetMainThread(getter_AddRefs(mainThread));
if (NS_FAILED(rv)) {
return;
}
nsCString userName(username);
nsCOMPtr<nsIInterfaceRequestor> cb = callbacks;
RefPtr<nsProxyInfo> info = proxyInfo;
RefPtr<nsHttpConnectionInfo> connInfo = aTransConnInfo;
// Forward to the main thread synchronously.
mozilla::SyncRunnable::DispatchToThread(
mainThread,
NS_NewRunnableFunction(
"AltSvcMapping::ProcessHeader",
[buf(buf), originScheme(originScheme), originHost(originHost),
originPort, userName, privateBrowsing, cb, info, caps,
originAttributes, connInfo, aDontValidate]() {
AltSvcMapping::ProcessHeader(
buf, originScheme, originHost, originPort, userName,
privateBrowsing, cb, info, caps, originAttributes, connInfo,
aDontValidate);
}));
return;
}
// AltSvcMapping::ProcessHeader is not thread-safe.
if (!NS_IsMainThread()) {
return;
}
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) {
LOG((
"Alt-Svc Response Header for http:// origin ignored\n"));
return;
}
LOG((
"Alt-Svc Response Header %s\n", buf.get()));
ParsedHeaderValueListList parsedAltSvc(buf);
nsTArray<RefPtr<AltSvcMapping>> h3Mappings;
nsTArray<RefPtr<AltSvcMapping>> otherMappings;
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;
SupportedAlpnRank alpnRank = SupportedAlpnRank::NOT_SUPPORTED;
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 or h3-xx=[hostname]:port
// XX is current version we support and it is define in nsHttp.h.
alpnRank = IsAlpnSupported(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",
PromiseFlatCString(currentName).get()));
}
}
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.get()));
bool isHttp3 = net::IsHttp3(alpnRank);
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;
}
LOG((
"AltSvcMapping created npnToken=%s", npnToken.get()));
RefPtr<AltSvcMapping> mapping =
new AltSvcMapping(
gHttpHandler->AltServiceCache()->GetStoragePtr(),
gHttpHandler->AltServiceCache()->StorageEpoch(), originScheme,
originHost, originPort, username, privateBrowsing,
NowInSeconds() + maxage, hostname, portno, npnToken, originAttributes,
isHttp3, alpnRank);
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 (isHttp3) {
h3Mappings.AppendElement(std::move(mapping));
}
else {
otherMappings.AppendElement(std::move(mapping));
}
}
}
auto doUpdateAltSvcMapping = [&](AltSvcMapping* aMapping) {
if (aTransConnInfo) {
if (!aTransConnInfo->GetEchConfig().IsEmpty()) {
LOG((
"Server has ECH, use HTTPS RR to connect instead"));
return;
}
if (StaticPrefs::network_http_skip_alt_svc_validation_on_https_rr()) {
RefPtr<nsHttpConnectionInfo> ci;
aMapping->GetConnectionInfo(getter_AddRefs(ci), proxyInfo,
originAttributes);
if (ci->HashKey().Equals(aTransConnInfo->HashKey())) {
LOG((
"The transaction's conninfo is the same, no need to validate"));
aDontValidate = true;
}
}
}
if (!aDontValidate) {
gHttpHandler->UpdateAltServiceMapping(aMapping, proxyInfo, callbacks,
caps, originAttributes);
}
else {
gHttpHandler->UpdateAltServiceMappingWithoutValidation(
aMapping, proxyInfo, callbacks, caps, originAttributes);
}
};
if (!h3Mappings.IsEmpty()) {
// Select the HTTP/3 (h3) AltSvcMapping with the highest ALPN rank from
// h3Mappings.
RefPtr<AltSvcMapping> latestH3Mapping = *std::max_element(
h3Mappings.begin(), h3Mappings.end(),
[](
const RefPtr<AltSvcMapping>& a,
const RefPtr<AltSvcMapping>& b) {
return a->AlpnRank() < b->AlpnRank();
});
doUpdateAltSvcMapping(latestH3Mapping);
}
std::for_each(otherMappings.begin(), otherMappings.end(),
doUpdateAltSvcMapping);
}
AltSvcMapping::AltSvcMapping(nsIDataStorage* 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, SupportedAlpnRank aRank)
: mStorage(storage),
mStorageEpoch(epoch),
mAlternateHost(alternateHost),
mAlternatePort(alternatePort),
mOriginHost(originHost),
mOriginPort(originPort),
mUsername(username),
mPrivate(privateBrowsing),
mExpiresAt(expiresAt),
mNPNToken(npnToken),
mOriginAttributes(originAttributes),
mIsHttp3(aIsHttp3),
mAlpnRank(aRank) {
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,
PromiseFlatCString(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);
}
}
AltSvcMapping::AltSvcMapping(
const nsACString& originScheme,
const nsACString& originHost, int32_t originPort,
const nsACString& username,
bool privateBrowsing,
const nsACString& alternateHost,
int32_t alternatePort,
const nsACString& npnToken,
const OriginAttributes& originAttributes,
bool aIsHttp3, SupportedAlpnRank aRank)
: mAlternateHost(alternateHost),
mAlternatePort(alternatePort),
mOriginHost(originHost),
mOriginPort(originPort),
mUsername(username),
mPrivate(privateBrowsing),
mNPNToken(npnToken),
mOriginAttributes(originAttributes),
mIsHttp3(aIsHttp3),
mAlpnRank(aRank) {}
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());
(
void)mStorage->Put(HashKey(), str,
mPrivate ? nsIDataStorage::DataType::
Private
: nsIDataStorage::DataType::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;
}
(
void)mStorage->Put(HashKey(), value,
mPrivate ? nsIDataStorage::DataType::
Private
: nsIDataStorage::DataType::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);
}
if (StaticPrefs::network_http_happy_eyeballs_enabled() && !pi) {
ci->SetHappyEyeballsEnabled(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(nsIDataStorage* 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 = Substring(str, start, idx - start).ToInteger(&code);
_NS_NEXT_TOKEN;
mAlternateHost = Substring(str, start, idx - start);
_NS_NEXT_TOKEN;
mAlternatePort = 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 = 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;
(
void)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);
}
void AltSvcCache::EnsureStorageInited() {
static Atomic<
bool> initialized(
false);
if (initialized) {
return;
}
auto initTask = [&]() {
MOZ_ASSERT(NS_IsMainThread());
// nsIDataStorage gives synchronous access to a memory based hash table
// that is backed by disk where those writes are done asynchronously
// on another thread
nsCOMPtr<nsIDataStorageManager> dataStorageManager(
do_GetService(
"@mozilla.org/security/datastoragemanager;1"));
if (!dataStorageManager) {
LOG((
"AltSvcCache::EnsureStorageInited WARN NO STORAGE MANAGER\n"));
return;
}
nsresult rv = dataStorageManager->Get(
nsIDataStorageManager::AlternateServices, getter_AddRefs(mStorage));
if (NS_FAILED(rv) || !mStorage) {
LOG((
"AltSvcCache::EnsureStorageInited WARN NO STORAGE\n"));
return;
}
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()) {
bool isReady;
nsresult rv = mStorage->IsReady(&isReady);
if (NS_FAILED(rv)) {
LOG((
"AltSvcCache::LookupMapping %p mStorage->IsReady failed\n", this));
return nullptr;
}
if (!isReady) {
LOG((
"AltSvcCache::LookupMapping %p skip when storage is not ready\n",
this));
return nullptr;
}
}
nsAutoCString val;
nsresult rv =
mStorage->Get(key,
privateBrowsing ? nsIDataStorage::DataType::
Private
: nsIDataStorage::DataType::Persistent,
val);
if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) {
LOG((
"AltSvcCache::LookupMapping %p mStorage->Get failed \n", this));
return nullptr;
}
if (rv == NS_ERROR_NOT_AVAILABLE || val.IsEmpty()) {
LOG((
"AltSvcCache::LookupMapping %p MISS\n", this));
return nullptr;
}
RefPtr<AltSvcMapping> mapping =
new AltSvcMapping(mStorage, mStorageEpoch, val);
if (!mapping->Validated() && (mapping->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));
(
void)mStorage->Remove(key, mapping->
Private()
? nsIDataStorage::DataType::
Private
: nsIDataStorage::DataType::Persistent);
return nullptr;
}
if (mapping->IsHttp3() &&
(!nsHttpHandler::IsHttp3Enabled() ||
!gHttpHandler->IsHttp3VersionSupported(mapping->NPNToken()) ||
gHttpHandler->IsHttp3Excluded(mapping->AlternateHost()))) {
// If Http3 is disabled or the version not supported anymore, remove the
// mapping.
(
void)mStorage->Remove(key, mapping->
Private()
? nsIDataStorage::DataType::
Private
: nsIDataStorage::DataType::Persistent);
return nullptr;
}
if (mapping->TTL() <=
0) {
LOG((
"AltSvcCache::LookupMapping %p expired hit - MISS\n", this));
(
void)mStorage->Remove(key, mapping->
Private()
? nsIDataStorage::DataType::
Private
: nsIDataStorage::DataType::Persistent);
return nullptr;
}
MOZ_ASSERT(mapping->
Private() == privateBrowsing);
LOG((
"AltSvcCache::LookupMapping %p HIT %p\n", this, mapping.get()));
return mapping.forget();
}
// For cases where the connection's hash key matches the hash key generated
// from the alt-svc header, validation is skipped since an equivalent connection
// already exists.
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)) {
MOZ_ASSERT(existing->HTTPS());
LOG(
(
"AltSvcCache::UpdateAltServiceMapping %p map %p updates ttl of "
"%p\n",
this, map, existing.get()));
existing->SetExpiresAt(map->GetExpiresAt());
glean::http::altsvc_mapping_changed_target
.EnumGet(glean::http::AltsvcMappingChangedTargetLabel::eFalse)
.Add();
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()));
glean::http::altsvc_mapping_changed_target
.EnumGet(glean::http::AltsvcMappingChangedTargetLabel::eTrue)
.Add();
}
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 isProxyAllowed = pi ? (pi->IsDirect() || pi->IsHttp3Proxy()) : true;
if (!isProxyAllowed) {
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 (StaticPrefs::network_http_happy_eyeballs_enabled()) {
ci->SetHappyEyeballsEnabled(true);
}
MOZ_ASSERT(map->HTTPS());
LOG(
(
"AltSvcCache::UpdateAltServiceMapping %p validation via "
"speculative connect started http3=%d\n",
this, map->IsHttp3()));
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)));
}
}
already_AddRefed<AltSvcMapping> AltSvcCache::GetAltServiceMapping(
const nsACString& scheme,
const nsACString& host, int32_t port,
bool privateBrowsing,
const OriginAttributes& originAttributes,
bool aHttp2Allowed,
bool aHttp3Allowed,
bool aForceHttp3First) {
EnsureStorageInited();
bool isHTTPS;
if (NS_FAILED(SchemeIsHTTPS(scheme, isHTTPS))) {
return nullptr;
}
if (!gHttpHandler->AllowAltSvc()) {
return nullptr;
}
if (!isHTTPS) {
return nullptr;
}
// First look for HTTP3
if (aHttp3Allowed) {
if (aForceHttp3First) {
RefPtr<Http3FirstAltSvcMapping> mapping =
new Http3FirstAltSvcMapping(
"https"_ns, host, port,
""_ns, privateBrowsing, host, port,
"h3"_ns,
originAttributes, true, SupportedAlpnRank::HTTP_3_VER_1);
return mapping.forget();
}
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) {
(
void)mStorage->Clear();
}
}
nsresult AltSvcCache::GetAltSvcCacheKeys(nsTArray<nsCString>& value) {
MOZ_ASSERT(NS_IsMainThread());
if (gHttpHandler->AllowAltSvc() && mStorage) {
nsTArray<RefPtr<nsIDataStorageItem>> items;
nsresult rv = mStorage->GetAll(items);
if (NS_FAILED(rv)) {
return rv;
}
for (
const auto& item : items) {
nsAutoCString key;
rv = item->GetKey(key);
if (NS_FAILED(rv)) {
return rv;
}
value.AppendElement(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::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)
Http3FirstAltSvcMapping::Http3FirstAltSvcMapping(
const nsACString& originScheme,
const nsACString& originHost,
int32_t originPort,
const nsACString& username,
bool privateBrowsing,
const nsACString& alternateHost, int32_t alternatePort,
const nsACString& npnToken,
const OriginAttributes& originAttributes,
bool aIsHttp3, SupportedAlpnRank aRank)
: AltSvcMapping(originScheme, originHost, originPort, username,
privateBrowsing, alternateHost, alternatePort, npnToken,
originAttributes, aIsHttp3, aRank) {}
Http3FirstAltSvcMapping::~Http3FirstAltSvcMapping() =
default;
void Http3FirstAltSvcMapping::GetConnectionInfo(
nsHttpConnectionInfo** outCI, nsProxyInfo* pi,
const OriginAttributes& originAttributes) {
RefPtr<nsHttpConnectionInfo> ci =
new nsHttpConnectionInfo(
mOriginHost, mOriginPort, mNPNToken, mUsername, pi, originAttributes,
mAlternateHost, mAlternatePort, mIsHttp3,
false);
if (StaticPrefs::network_http_happy_eyeballs_enabled() && !pi) {
ci->SetHappyEyeballsEnabled(true);
}
ci->SetPrivate(mPrivate);
ci.forget(outCI);
}
}
// namespace net
}
// namespace mozilla