Files
sousa-gecko/netwerk/base/SSLTokensCache.cpp
T
Emilio Cobos Álvarez abbeff4fd9 Bug 2054311 - Sort headers in netwerk/. r=webrtc-reviewers,cookie-reviewers,mjf,timhuang
Autogenerated with:

    cp dom/.clang-format netwerk/ && mach format netwerk/**.{cpp,h,mm}

Manual fixes:

 * mozurl/cbindgen.toml (including headers earlier)
 * NetlinkService.h (forward-declaring a necessary struct)
 * nsIWebSocketChannel.idl (forward-declaring OriginAttributes).
 * nsAsyncRedirectVerifyHelper.h (forward-declaring nsIEventTarget)
 * CapsuleEncoder.h gets a missing Capsule forward-decl.
 * WebSocketConnectionBase.h gets some missing includes.
 * HappyEyeballsConnectionAttempt gets some missing includes.
 * nsUDPSocket missed nsSocketTransportService2.h for OnSocketThread().
 * HttpInfo missed a forward declaration.
 * MockHttpAuth missed an nsCOMPtr include.
 * nsHttpAuthManager missed a forward declaration for nsIHttpAuthCache.
 * WebTransportSessionProxy was using mozilla::Mutex in a member without
   including it.
 * win32 fixes to keep include order because windows headers suck.
 * nsInputStreamPump needs an Atomics.h include.

Differential Revision: https://phabricator.services.mozilla.com/D311696
2026-07-13 09:55:57 +00:00

1503 lines
47 KiB
C++

/* 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 "SSLTokensCache.h"
#include "CertVerifier.h"
#include "CommonSocketControl.h"
#include "TransportSecurityInfo.h"
#include "brotli/decode.h"
#include "brotli/encode.h"
#include "mozilla/ArrayAlgorithm.h"
#include "mozilla/Components.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/Logging.h"
#include "mozilla/OriginAttributes.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/glean/NetwerkMetrics.h"
#include "mozilla/ipc/ByteBuf.h"
#include "mozilla/net/SocketProcessChild.h"
#include "mozilla/net/SocketProcessParent.h"
#include "mozilla/net/ssl_tokens_cache.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIEventTarget.h"
#include "nsIOService.h"
#include "nsIObserverService.h"
#include "nsThreadUtils.h"
#include "prtime.h"
#include "ssl.h"
#include "sslexp.h"
namespace mozilla {
namespace net {
static LazyLogModule gSSLTokensCacheLog("SSLTokensCache");
#undef LOG
#define LOG(args) MOZ_LOG(gSSLTokensCacheLog, mozilla::LogLevel::Debug, args)
#undef LOG5_ENABLED
#define LOG5_ENABLED() \
MOZ_LOG_TEST(mozilla::net::gSSLTokensCacheLog, mozilla::LogLevel::Verbose)
class ExpirationComparator {
public:
bool Equals(SSLTokensCache::TokenCacheRecord* a,
SSLTokensCache::TokenCacheRecord* b) const {
return a->mExpirationTime == b->mExpirationTime;
}
bool LessThan(SSLTokensCache::TokenCacheRecord* a,
SSLTokensCache::TokenCacheRecord* b) const {
return a->mExpirationTime < b->mExpirationTime;
}
};
static nsTArray<nsTArray<uint8_t>> CloneCertChain(
const nsTArray<nsTArray<uint8_t>>& aSrc) {
return TransformIntoNewArray(aSrc, [](const auto& c) { return c.Clone(); });
}
SessionCacheInfo SessionCacheInfo::Clone() const {
SessionCacheInfo result;
result.mEVStatus = mEVStatus;
result.mCertificateTransparencyStatus = mCertificateTransparencyStatus;
result.mServerCertBytes = mServerCertBytes.Clone();
result.mSucceededCertChainBytes =
mSucceededCertChainBytes.map(CloneCertChain);
result.mIsBuiltCertChainRootBuiltInRoot = mIsBuiltCertChainRootBuiltInRoot;
result.mOverridableErrorCategory = mOverridableErrorCategory;
result.mHandshakeCertificatesBytes =
mHandshakeCertificatesBytes.map(CloneCertChain);
return result;
}
// Serialized payload layout (all integers little-endian):
// [4: token_len][token bytes]
// [1: ev_status]
// [2: ct_status]
// [1: overridable_error]
// [1: builtin_root] (0=Nothing, 1=Some(false), 2=Some(true))
// [4: server_cert_len][server_cert bytes]
// [1: has_succeeded_chain] if 1: [1: count] then for each [4: len][bytes]
// [1: has_handshake_certs] if 1: [1: count] then for each [4: len][bytes]
//
// Stored compressed with a 4-byte LE original-size prefix.
template <typename T>
static void AppendLE(nsTArray<uint8_t>& aBuf, T aVal) {
T le = mozilla::NativeEndian::swapToLittleEndian(aVal);
aBuf.AppendElements(reinterpret_cast<const uint8_t*>(&le), sizeof(T));
}
static nsTArray<uint8_t> SerializeRecord(Span<const uint8_t> aToken,
const SessionCacheInfo& aInfo) {
nsTArray<uint8_t> buf;
AppendLE(buf, AssertedCast<uint32_t>(aToken.Length()));
buf.AppendElements(aToken.Elements(), aToken.Length());
buf.AppendElement(aInfo.mEVStatus == psm::EVStatus::EV ? 1 : 0);
AppendLE(buf, aInfo.mCertificateTransparencyStatus);
buf.AppendElement(static_cast<uint8_t>(aInfo.mOverridableErrorCategory));
if (aInfo.mIsBuiltCertChainRootBuiltInRoot.isNothing()) {
buf.AppendElement(0);
} else {
buf.AppendElement(*aInfo.mIsBuiltCertChainRootBuiltInRoot ? 2 : 1);
}
AppendLE(buf, AssertedCast<uint32_t>(aInfo.mServerCertBytes.Length()));
buf.AppendElements(aInfo.mServerCertBytes.Elements(),
aInfo.mServerCertBytes.Length());
auto appendChain = [&](const Maybe<nsTArray<nsTArray<uint8_t>>>& aChain) {
if (aChain.isNothing()) {
buf.AppendElement(0);
return;
}
buf.AppendElement(1);
MOZ_RELEASE_ASSERT(aChain->Length() <= 0xFF);
buf.AppendElement(static_cast<uint8_t>(aChain->Length()));
for (const auto& cert : *aChain) {
AppendLE(buf, AssertedCast<uint32_t>(cert.Length()));
buf.AppendElements(cert.Elements(), cert.Length());
}
};
appendChain(aInfo.mSucceededCertChainBytes);
appendChain(aInfo.mHandshakeCertificatesBytes);
return buf;
}
// Returns empty array on failure.
static nsTArray<uint8_t> CompressRecord(Span<const uint8_t> aPayload) {
size_t bound = BrotliEncoderMaxCompressedSize(aPayload.Length());
nsTArray<uint8_t> result;
if (!result.SetLength(4 + bound, fallible)) {
return {};
}
uint32_t originalLen = AssertedCast<uint32_t>(aPayload.Length());
LittleEndian::writeUint32(result.Elements(), originalLen);
size_t encodedSize = bound;
if (!BrotliEncoderCompress(5, BROTLI_DEFAULT_WINDOW, BROTLI_MODE_GENERIC,
aPayload.Length(), aPayload.Elements(),
&encodedSize, result.Elements() + 4)) {
return {};
}
result.TruncateLength(4 + encodedSize);
return result;
}
// Returns empty array on failure.
static nsTArray<uint8_t> DecompressRecord(Span<const uint8_t> aCompressed) {
if (aCompressed.Length() < 4) {
return {};
}
uint32_t originalLen = LittleEndian::readUint32(aCompressed.Elements());
if (originalLen > 256 * 1024) {
LOG(("SSLTokensCache: implausible payload originalLen %" PRIu32,
originalLen));
return {};
}
nsTArray<uint8_t> result;
if (!result.SetLength(originalLen, fallible)) {
return {};
}
size_t decodedSize = originalLen;
BrotliDecoderResult r = BrotliDecoderDecompress(
aCompressed.Length() - 4, aCompressed.Elements() + 4, &decodedSize,
result.Elements());
if (r != BROTLI_DECODER_RESULT_SUCCESS || decodedSize != originalLen) {
return {};
}
return result;
}
struct PayloadReader {
Span<const uint8_t> buf;
size_t pos = 0;
template <typename T>
bool Read(T& out) {
if (buf.Length() - pos < sizeof(T)) return false;
if constexpr (sizeof(T) == 1) {
out = static_cast<T>(buf[pos]);
} else {
T le;
memcpy(&le, buf.Elements() + pos, sizeof(T));
out = mozilla::NativeEndian::swapFromLittleEndian(le);
}
pos += sizeof(T);
return true;
}
bool Bytes(nsTArray<uint8_t>& out, uint32_t len) {
if (buf.Length() - pos < len) return false;
if (!out.SetLength(len, fallible)) return false;
memcpy(out.Elements(), buf.Elements() + pos, len);
pos += len;
return true;
}
bool AtEnd() const { return pos == buf.Length(); }
};
static bool DeserializeRecord(Span<const uint8_t> aBuf,
nsTArray<uint8_t>& aToken,
SessionCacheInfo& aInfo) {
PayloadReader r{aBuf};
uint32_t tokenLen;
if (!r.Read(tokenLen)) return false;
if (tokenLen > 256 * 1024) return false;
if (!r.Bytes(aToken, tokenLen)) return false;
uint8_t evStatus;
if (!r.Read(evStatus)) return false;
aInfo.mEVStatus = evStatus ? psm::EVStatus::EV : psm::EVStatus::NotEV;
uint16_t ctStatus;
if (!r.Read(ctStatus)) return false;
aInfo.mCertificateTransparencyStatus = ctStatus;
uint8_t overridableError;
if (!r.Read(overridableError)) return false;
aInfo.mOverridableErrorCategory =
static_cast<nsITransportSecurityInfo::OverridableErrorCategory>(
overridableError);
uint8_t builtinRoot;
if (!r.Read(builtinRoot)) return false;
if (builtinRoot == 0) {
aInfo.mIsBuiltCertChainRootBuiltInRoot = Nothing();
} else if (builtinRoot == 1) {
aInfo.mIsBuiltCertChainRootBuiltInRoot = Some(false);
} else if (builtinRoot == 2) {
aInfo.mIsBuiltCertChainRootBuiltInRoot = Some(true);
} else {
return false;
}
uint32_t serverCertLen;
if (!r.Read(serverCertLen)) return false;
if (serverCertLen > 64 * 1024) return false;
if (!r.Bytes(aInfo.mServerCertBytes, serverCertLen)) return false;
auto readChain = [&](Maybe<nsTArray<nsTArray<uint8_t>>>& aChain) -> bool {
uint8_t present;
if (!r.Read(present)) return false;
if (!present) {
aChain = Nothing();
return true;
}
uint8_t count;
if (!r.Read(count)) return false;
nsTArray<nsTArray<uint8_t>> chain;
for (uint8_t i = 0; i < count; i++) {
uint32_t certLen;
if (!r.Read(certLen)) return false;
if (certLen > 64 * 1024) return false;
nsTArray<uint8_t> cert;
if (!r.Bytes(cert, certLen)) return false;
chain.AppendElement(std::move(cert));
}
aChain = Some(std::move(chain));
return true;
};
if (!readChain(aInfo.mSucceededCertChainBytes)) return false;
if (!readChain(aInfo.mHandshakeCertificatesBytes)) return false;
return r.AtEnd();
}
StaticRefPtr<SSLTokensCache> SSLTokensCache::gInstance;
StaticMutex SSLTokensCache::sLock;
uint64_t SSLTokensCache::sRecordId = 0;
SSLTokensCache::TokenCacheRecord::~TokenCacheRecord() {
if (!gInstance) {
return;
}
gInstance->OnRecordDestroyed(this);
}
uint32_t SSLTokensCache::TokenCacheRecord::Size() const {
return mKey.Length() + mCompressedPayload.Length();
}
uint32_t SSLTokensCache::TokenCacheEntry::Size() const {
uint32_t size = 0;
for (const auto& rec : mRecords) {
size += rec->Size();
}
return size;
}
void SSLTokensCache::TokenCacheEntry::AddRecord(
UniquePtr<SSLTokensCache::TokenCacheRecord>&& aRecord,
nsTArray<TokenCacheRecord*>& aExpirationArray) {
if (mRecords.Length() ==
StaticPrefs::network_ssl_tokens_cache_records_per_entry()) {
aExpirationArray.RemoveElement(mRecords[0].get());
mRecords.RemoveElementAt(0);
}
aExpirationArray.AppendElement(aRecord.get());
for (int32_t i = mRecords.Length() - 1; i >= 0; --i) {
if (aRecord->mExpirationTime > mRecords[i]->mExpirationTime) {
mRecords.InsertElementAt(i + 1, std::move(aRecord));
return;
}
}
mRecords.InsertElementAt(0, std::move(aRecord));
}
UniquePtr<SSLTokensCache::TokenCacheRecord>
SSLTokensCache::TokenCacheEntry::RemoveWithId(uint64_t aId) {
for (int32_t i = mRecords.Length() - 1; i >= 0; --i) {
if (mRecords[i]->mId == aId) {
UniquePtr<TokenCacheRecord> record = std::move(mRecords[i]);
mRecords.RemoveElementAt(i);
return record;
}
}
return nullptr;
}
const UniquePtr<SSLTokensCache::TokenCacheRecord>&
SSLTokensCache::TokenCacheEntry::Get() {
return mRecords[0];
}
NS_IMPL_ISUPPORTS(SSLTokensCache, nsIMemoryReporter, nsIObserver,
nsIAsyncShutdownBlocker)
template <typename Pred>
void SSLTokensCache::RemoveMatchingLocked(Pred&& aPredicate) {
sLock.AssertCurrentThreadOwns();
AutoTArray<nsCString, 4> keysToRemove;
for (const auto& entry : mTokenCacheRecords) {
if (aPredicate(entry.GetKey())) {
keysToRemove.AppendElement(entry.GetKey());
}
}
for (const auto& key : keysToRemove) {
(void)RemoveAllLocked(key);
}
}
// static
void SSLTokensCache::PutFromPersistedCallback(
void* aCtx, const SslTokensPersistedRecord* aRec) {
(void)PutFromPersisted(aRec, *static_cast<uint32_t*>(aCtx));
}
// static
void SSLTokensCache::LoadCallback(void* aCtx,
const SslTokensPersistedRecord* aRec) {
auto* ctx = static_cast<LoadCtx*>(aCtx);
if (PutFromPersisted(aRec, ctx->loadGen)) {
ctx->count++;
}
}
void SSLTokensCache::ClearCacheLocked() {
sLock.AssertCurrentThreadOwns();
mLoadGeneration++;
mExpirationArray.Clear();
mTokenCacheRecords.Clear();
mCacheSize = 0;
}
// static
nsTArray<uint8_t> SSLTokensCache::SerializeForIPC() {
StaticMutexAutoLock lock(sLock);
return SerializeSnapshotLocked();
}
// static
void SSLTokensCache::DeserializeFromIPC(Span<const uint8_t> aData) {
if (aData.IsEmpty()) {
return;
}
uint32_t loadGen = 0;
{
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
return;
}
gInstance->ClearCacheLocked();
loadGen = gInstance->mLoadGeneration;
}
// callback is invoked synchronously within ssl_tokens_cache_deserialize_ipc,
// so &loadGen remains valid for the entire call.
ssl_tokens_cache_deserialize_ipc(aData.data(), aData.Length(), PR_Now(),
PutFromPersistedCallback, &loadGen);
}
// static
void SSLTokensCache::DeserializeFromIPCAsync(mozilla::ipc::ByteBuf&& aBuf) {
if (aBuf.mLen == 0) {
return;
}
NS_DispatchBackgroundTask(NS_NewRunnableFunction(
"SSLTokensCache::DeserializeFromIPCAsync", [buf = std::move(aBuf)]() {
DeserializeFromIPC(Span(buf.mData, buf.mLen));
}));
}
// static
nsDependentCSubstring SSLTokensCache::BasePartFromKey(const nsACString& aKey) {
int32_t caretPos = aKey.FindChar('^');
return nsDependentCSubstring(
aKey, 0, caretPos == kNotFound ? aKey.Length() : caretPos);
}
// static
// Extracts the host from a key's base part ("host:port" or "prefix:host:port").
// Returns an empty string if no colon is found.
nsDependentCSubstring SSLTokensCache::HostFromBasePart(
const nsDependentCSubstring& aBasePart) {
int32_t lastColon = aBasePart.RFindChar(':');
if (lastColon == kNotFound) {
return nsDependentCSubstring();
}
return nsDependentCSubstring(aBasePart, 0, lastColon);
}
// static
OriginAttributes SSLTokensCache::OAFromPeerId(const nsACString& aPeerId) {
OriginAttributes oa;
int32_t caretPos = aPeerId.FindChar('^');
if (caretPos != kNotFound) {
nsAutoCString suffix(Substring(aPeerId, caretPos + 1));
(void)oa.PopulateFromSuffix(suffix);
}
return oa;
}
// static
nsCString SSLTokensCache::SetupPersistenceLocked(uint32_t& aLoadGen) {
sLock.AssertCurrentThreadOwns();
MOZ_ASSERT(gInstance);
MOZ_ASSERT(!gInstance->mBackingFile);
nsCOMPtr<nsIFile> profileDir;
if (NS_FAILED(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(profileDir)))) {
return ""_ns;
}
profileDir->Clone(getter_AddRefs(gInstance->mBackingFile));
gInstance->mBackingFile->AppendNative("ssl_tokens_cache.bin"_ns);
nsCOMPtr<nsISerialEventTarget> writeQueue;
NS_CreateBackgroundTaskQueue("SslTokensCachePersist",
getter_AddRefs(writeQueue));
gInstance->mWriteTaskQueue = writeQueue;
gInstance->mLoadStartTime = TimeStamp::Now();
aLoadGen = gInstance->mLoadGeneration;
nsAutoString widePath;
gInstance->mBackingFile->GetPath(widePath);
return NS_ConvertUTF16toUTF8(widePath);
}
// static
nsresult SSLTokensCache::Init() {
MOZ_ASSERT(NS_IsMainThread());
nsCString backgroundLoadPath;
uint32_t loadGen = 0;
{
StaticMutexAutoLock lock(sLock);
// SSLTokensCache is used in both the parent process and the socket process.
// The socket process runs TLS handshakes and holds the live token cache;
// the parent process holds the persistence layer (disk I/O) and receives
// periodic token updates from the socket process via IPC.
// Some xpcshell tests also use sockets directly in the parent process.
if (!(XRE_IsSocketProcess() || XRE_IsParentProcess())) {
return NS_OK;
}
MOZ_ASSERT(!gInstance);
gInstance = new SSLTokensCache();
RegisterWeakMemoryReporter(gInstance);
// Register unconditionally: user prefs are applied during
// profile-after-change, after Init() runs.
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs && XRE_IsParentProcess()) {
obs->AddObserver(gInstance, "profile-after-change", false);
}
if (!StaticPrefs::network_ssl_tokens_cache_persistence()) {
return NS_OK;
}
if (obs) {
obs->AddObserver(gInstance, "application-background", false);
obs->AddObserver(gInstance, "idle-daily", false);
gInstance->mWriteObserversRegistered = true;
}
if (!XRE_IsParentProcess()) {
return NS_OK;
}
backgroundLoadPath = SetupPersistenceLocked(loadGen);
} // sLock released before dispatching
DispatchLoad(std::move(backgroundLoadPath), loadGen);
return NS_OK;
}
// static
nsresult SSLTokensCache::Shutdown() {
RefPtr<SSLTokensCache> instance;
nsCOMPtr<nsIObserverService> obs;
bool blockerRegistered = false;
{
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
return NS_ERROR_UNEXPECTED;
}
UnregisterWeakMemoryReporter(gInstance);
instance = gInstance;
obs = mozilla::services::GetObserverService();
blockerRegistered = gInstance->mShutdownBarrier != nullptr;
}
// With no blocker, BlockShutdown will never fire: write synchronously
// (test environments only) and clear gInstance now. Otherwise leave
// gInstance set so BlockShutdown -> DoWrite -> SerializeSnapshotLocked
// can build the snapshot; RemoveShutdownBlocker nulls it afterward.
if (!blockerRegistered) {
#ifdef ENABLE_TESTS
instance->DoWrite(true);
#endif
StaticMutexAutoLock lock(sLock);
gInstance = nullptr;
}
if (obs && instance) {
bool hadWriteObservers;
{
StaticMutexAutoLock lock(sLock);
hadWriteObservers = instance->mWriteObserversRegistered;
}
if (hadWriteObservers) {
obs->RemoveObserver(instance, "application-background");
obs->RemoveObserver(instance, "idle-daily");
}
if (XRE_IsParentProcess()) {
obs->RemoveObserver(instance, "profile-after-change");
}
}
return NS_OK;
}
SSLTokensCache::SSLTokensCache() { LOG(("SSLTokensCache::SSLTokensCache")); }
SSLTokensCache::~SSLTokensCache() { LOG(("SSLTokensCache::~SSLTokensCache")); }
nsTArray<SslTokensPersistedRecord> SSLTokensCache::CollectSnapshotLocked()
const {
sLock.AssertCurrentThreadOwns();
nsTArray<SslTokensPersistedRecord> snapshot;
for (const auto& entry : mTokenCacheRecords.Values()) {
for (const auto& rec : entry->Records()) {
if (!ShouldPersistKey(rec->mKey, rec->mOverridableError)) {
continue;
}
auto& ffi = *snapshot.AppendElement();
ffi.id = rec->mId;
ffi.key = rec->mKey;
ffi.expiration_time = static_cast<int64_t>(rec->mExpirationTime);
ffi.overridable_error = rec->mOverridableError;
ffi.compressed_payload = rec->mCompressedPayload.Elements();
ffi.compressed_payload_len = rec->mCompressedPayload.Length();
}
}
return snapshot;
}
// static
nsTArray<uint8_t> SSLTokensCache::SerializeSnapshotLocked() {
sLock.AssertCurrentThreadOwns();
if (!gInstance) {
return {};
}
auto snapshot = gInstance->CollectSnapshotLocked();
if (snapshot.IsEmpty()) {
return {};
}
nsTArray<uint8_t> out;
ssl_tokens_cache_serialize_snapshot(&snapshot, &out);
return out;
}
// static
nsresult SSLTokensCache::Put(const nsACString& aKey, const uint8_t* aToken,
uint32_t aTokenLen,
CommonSocketControl* aSocketControl) {
PRTime expirationTime;
SSLResumptionTokenInfo tokenInfo;
if (SSL_GetResumptionTokenInfo(aToken, aTokenLen, &tokenInfo,
sizeof(tokenInfo)) != SECSuccess) {
LOG((" cannot get expiration time from the token, NSS error %d",
PORT_GetError()));
return NS_ERROR_FAILURE;
}
expirationTime = tokenInfo.expirationTime;
SSL_DestroyResumptionTokenInfo(&tokenInfo);
return Put(aKey, aToken, aTokenLen, aSocketControl, expirationTime);
}
// static
nsresult SSLTokensCache::Put(const nsACString& aKey, const uint8_t* aToken,
uint32_t aTokenLen,
CommonSocketControl* aSocketControl,
PRTime aExpirationTime) {
LOG(("SSLTokensCache::Put [key=%s, tokenLen=%u]",
PromiseFlatCString(aKey).get(), aTokenLen));
if (!aSocketControl) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsITransportSecurityInfo> securityInfo;
nsresult rv = aSocketControl->GetSecurityInfo(getter_AddRefs(securityInfo));
if (NS_FAILED(rv)) {
return rv;
}
nsCOMPtr<nsIX509Cert> cert;
securityInfo->GetServerCert(getter_AddRefs(cert));
if (!cert) {
return NS_ERROR_FAILURE;
}
nsTArray<uint8_t> certBytes;
rv = cert->GetRawDER(certBytes);
if (NS_FAILED(rv)) {
return rv;
}
Maybe<nsTArray<nsTArray<uint8_t>>> succeededCertChainBytes;
nsTArray<RefPtr<nsIX509Cert>> succeededCertArray;
rv = securityInfo->GetSucceededCertChain(succeededCertArray);
if (NS_FAILED(rv)) {
return rv;
}
auto getRawDerAll = [](nsTArray<RefPtr<nsIX509Cert>>& aCerts)
-> Result<nsTArray<nsTArray<uint8_t>>, nsresult> {
return TransformIntoNewArrayAbortOnErr(
aCerts,
[](const RefPtr<nsIX509Cert>& aCert)
-> Result<nsTArray<uint8_t>, nsresult> {
nsTArray<uint8_t> raw;
MOZ_TRY(aCert->GetRawDER(raw));
return std::move(raw);
},
fallible);
};
Maybe<bool> isBuiltCertChainRootBuiltInRoot;
if (!succeededCertArray.IsEmpty()) {
auto result = getRawDerAll(succeededCertArray);
if (result.isErr()) return result.unwrapErr();
succeededCertChainBytes.emplace(result.unwrap());
bool builtInRoot = false;
rv = securityInfo->GetIsBuiltCertChainRootBuiltInRoot(&builtInRoot);
if (NS_FAILED(rv)) {
return rv;
}
isBuiltCertChainRootBuiltInRoot.emplace(builtInRoot);
}
bool isEV;
rv = securityInfo->GetIsExtendedValidation(&isEV);
if (NS_FAILED(rv)) {
return rv;
}
uint16_t certificateTransparencyStatus;
rv = securityInfo->GetCertificateTransparencyStatus(
&certificateTransparencyStatus);
if (NS_FAILED(rv)) {
return rv;
}
nsITransportSecurityInfo::OverridableErrorCategory overridableErrorCategory;
rv = securityInfo->GetOverridableErrorCategory(&overridableErrorCategory);
if (NS_FAILED(rv)) {
return rv;
}
Maybe<nsTArray<nsTArray<uint8_t>>> handshakeCertificatesBytes;
nsTArray<RefPtr<nsIX509Cert>> handshakeCertificates;
rv = securityInfo->GetHandshakeCertificates(handshakeCertificates);
if (NS_FAILED(rv)) {
return rv;
}
if (!handshakeCertificates.IsEmpty()) {
auto result = getRawDerAll(handshakeCertificates);
if (result.isErr()) return result.unwrapErr();
handshakeCertificatesBytes.emplace(result.unwrap());
}
SessionCacheInfo info;
info.mEVStatus = isEV ? psm::EVStatus::EV : psm::EVStatus::NotEV;
info.mCertificateTransparencyStatus = certificateTransparencyStatus;
info.mOverridableErrorCategory = overridableErrorCategory;
info.mIsBuiltCertChainRootBuiltInRoot = isBuiltCertChainRootBuiltInRoot;
info.mServerCertBytes = std::move(certBytes);
info.mSucceededCertChainBytes = std::move(succeededCertChainBytes);
info.mHandshakeCertificatesBytes = std::move(handshakeCertificatesBytes);
nsTArray<uint8_t> payload = SerializeRecord({aToken, aTokenLen}, info);
nsTArray<uint8_t> compressed = CompressRecord(payload);
if (compressed.IsEmpty()) {
LOG(("SSLTokensCache::Put: compression failed"));
return NS_ERROR_FAILURE;
}
{
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
LOG((" service not initialized"));
return NS_ERROR_NOT_INITIALIZED;
}
auto makeRecord = [&]() MOZ_REQUIRES(sLock) {
auto rec = MakeUnique<TokenCacheRecord>();
rec->mKey = aKey;
rec->mExpirationTime = aExpirationTime;
rec->mOverridableError = static_cast<uint8_t>(overridableErrorCategory);
rec->mCompressedPayload = std::move(compressed);
return rec;
};
gInstance->InsertRecordLocked(makeRecord());
gInstance->LogStats();
} // sLock released
return NS_OK;
}
// static
nsresult SSLTokensCache::Get(const nsACString& aKey, nsTArray<uint8_t>& aToken,
SessionCacheInfo& aResult, uint64_t* aTokenId) {
LOG(("SSLTokensCache::Get [key=%s]", PromiseFlatCString(aKey).get()));
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
LOG((" service not initialized"));
return NS_ERROR_NOT_INITIALIZED;
}
UniquePtr<TokenCacheRecord> owned =
gInstance->GetRecordLocked(aKey, aTokenId);
if (!owned) {
return NS_ERROR_NOT_AVAILABLE;
}
nsTArray<uint8_t> payload = DecompressRecord(owned->mCompressedPayload);
if (payload.IsEmpty() || !DeserializeRecord(payload, aToken, aResult)) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
UniquePtr<SSLTokensCache::TokenCacheRecord> SSLTokensCache::GetRecordLocked(
const nsACString& aKey, uint64_t* aTokenId) {
sLock.AssertCurrentThreadOwns();
if (!mLoadComplete && mBackingFile) {
LOG(("SSLTokensCache::GetRecordLocked: connection before load complete"));
mozilla::glean::network::ssl_token_cache_early_connections.Add(1);
}
TokenCacheEntry* cacheEntry = nullptr;
if (mTokenCacheRecords.Get(aKey, &cacheEntry)) {
if (cacheEntry->RecordCount() == 0) {
MOZ_ASSERT(false, "Found a cacheEntry with no records");
mTokenCacheRecords.Remove(aKey);
return nullptr;
}
PRTime now = PR_Now();
while (cacheEntry->RecordCount() > 0) {
const UniquePtr<TokenCacheRecord>& rec = cacheEntry->Get();
if (rec->mExpirationTime > now) {
uint64_t id = rec->mId;
uint32_t size = rec->Size();
UniquePtr<TokenCacheRecord> owned = cacheEntry->RemoveWithId(id);
if (aTokenId) {
*aTokenId = id;
}
mCacheSize -= size;
if (cacheEntry->RecordCount() == 0) {
mTokenCacheRecords.Remove(aKey);
}
mozilla::glean::network::ssl_token_cache_hits.Get("hit"_ns).Add(1);
LOG(("SSLTokensCache::GetRecordLocked: hit [key=%s, load_complete=%s]",
PromiseFlatCString(aKey).get(), mLoadComplete ? "yes" : "no"));
return owned;
}
LOG((" skipping expired token [expirationTime=%" PRId64 ", now=%" PRId64
"]",
rec->mExpirationTime, now));
mozilla::glean::network::ssl_token_cache_expired.Add(1);
uint64_t expiredId = rec->mId;
mCacheSize -= rec->Size();
cacheEntry->RemoveWithId(expiredId);
}
mTokenCacheRecords.Remove(aKey);
}
LOG((" token not found"));
mozilla::glean::network::ssl_token_cache_hits.Get("miss"_ns).Add(1);
return nullptr;
}
// static
nsresult SSLTokensCache::Remove(const nsACString& aKey, uint64_t aId) {
StaticMutexAutoLock lock(sLock);
LOG(("SSLTokensCache::Remove [key=%s]", PromiseFlatCString(aKey).get()));
if (!gInstance) {
LOG((" service not initialized"));
return NS_ERROR_NOT_INITIALIZED;
}
return gInstance->RemoveLocked(aKey, aId);
}
nsresult SSLTokensCache::RemoveLocked(const nsACString& aKey, uint64_t aId) {
sLock.AssertCurrentThreadOwns();
LOG(("SSLTokensCache::RemoveLocked [key=%s, id=%" PRIu64 "]",
PromiseFlatCString(aKey).get(), aId));
TokenCacheEntry* cacheEntry;
if (!mTokenCacheRecords.Get(aKey, &cacheEntry)) {
return NS_ERROR_NOT_AVAILABLE;
}
UniquePtr<TokenCacheRecord> rec = cacheEntry->RemoveWithId(aId);
if (!rec) {
return NS_ERROR_NOT_AVAILABLE;
}
mCacheSize -= rec->Size();
if (cacheEntry->RecordCount() == 0) {
mTokenCacheRecords.Remove(aKey);
}
// Release the record immediately, so mExpirationArray can be also updated.
rec = nullptr;
LogStats();
return NS_OK;
}
// static
nsresult SSLTokensCache::RemoveAll(const nsACString& aKey) {
StaticMutexAutoLock lock(sLock);
LOG(("SSLTokensCache::RemoveAll [key=%s]", PromiseFlatCString(aKey).get()));
if (!gInstance) {
LOG((" service not initialized"));
return NS_ERROR_NOT_INITIALIZED;
}
return gInstance->RemoveAllLocked(aKey);
}
nsresult SSLTokensCache::RemoveAllLocked(const nsACString& aKey) {
sLock.AssertCurrentThreadOwns();
LOG(("SSLTokensCache::RemoveAllLocked [key=%s]",
PromiseFlatCString(aKey).get()));
UniquePtr<TokenCacheEntry> cacheEntry;
if (!mTokenCacheRecords.Remove(aKey, &cacheEntry)) {
return NS_ERROR_NOT_AVAILABLE;
}
mCacheSize -= cacheEntry->Size();
cacheEntry = nullptr;
LogStats();
return NS_OK;
}
void SSLTokensCache::OnRecordDestroyed(TokenCacheRecord* aRec) {
// Always called from destructors of map entries while sLock is held.
sLock.AssertCurrentThreadOwns();
mExpirationArray.RemoveElement(aRec);
}
void SSLTokensCache::EvictIfNecessary() {
sLock.AssertCurrentThreadOwns();
// kilobytes to bytes
uint32_t capacity = StaticPrefs::network_ssl_tokens_cache_capacity() << 10;
if (mCacheSize <= capacity) {
return;
}
LOG(("SSLTokensCache::EvictIfNecessary: evicting"));
mExpirationArray.Sort(ExpirationComparator());
PRTime now = PR_Now();
while (mCacheSize > capacity && mExpirationArray.Length() > 0) {
auto* rec = mExpirationArray[0];
// Only count evictions of tokens that are still valid; already-expired
// tokens would be discarded on lookup anyway and are tracked separately
// by ssl_token_cache_expired.
if (rec->mExpirationTime > now) {
mozilla::glean::network::ssl_token_cache_evictions.Add(1);
}
DebugOnly<nsresult> rv = RemoveLocked(rec->mKey, rec->mId);
MOZ_ASSERT(NS_SUCCEEDED(rv),
"mExpirationArray and mTokenCacheRecords are out of sync!");
}
}
void SSLTokensCache::LogStats() {
sLock.AssertCurrentThreadOwns();
if (!LOG5_ENABLED()) {
return;
}
LOG(("SSLTokensCache::LogStats [count=%zu, cacheSize=%u]",
mExpirationArray.Length(), mCacheSize));
for (const auto& ent : mTokenCacheRecords.Values()) {
const UniquePtr<TokenCacheRecord>& rec = ent->Get();
LOG((" [key=%s, count=%d]", rec->mKey.get(), ent->RecordCount()));
}
}
size_t SSLTokensCache::SizeOfIncludingThis(
mozilla::MallocSizeOf mallocSizeOf) const {
size_t n = mallocSizeOf(this);
n += mTokenCacheRecords.ShallowSizeOfExcludingThis(mallocSizeOf);
n += mExpirationArray.ShallowSizeOfExcludingThis(mallocSizeOf);
for (const auto* rec : mExpirationArray) {
n += mallocSizeOf(rec);
n += rec->mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf);
n += rec->mCompressedPayload.ShallowSizeOfExcludingThis(mallocSizeOf);
}
return n;
}
MOZ_DEFINE_MALLOC_SIZE_OF(SSLTokensCacheMallocSizeOf)
NS_IMETHODIMP
SSLTokensCache::CollectReports(nsIHandleReportCallback* aHandleReport,
nsISupports* aData, bool aAnonymize) {
StaticMutexAutoLock lock(sLock);
MOZ_COLLECT_REPORT("explicit/network/ssl-tokens-cache", KIND_HEAP,
UNITS_BYTES,
SizeOfIncludingThis(SSLTokensCacheMallocSizeOf),
"Memory used for the SSL tokens cache.");
return NS_OK;
}
static void RemoveFilesSync(nsIFile* aBackingFile) {
aBackingFile->Remove(false);
nsCOMPtr<nsIFile> tmp;
aBackingFile->Clone(getter_AddRefs(tmp));
tmp->SetLeafName(u"ssl_tokens_cache.tmp"_ns);
tmp->Remove(false);
}
static void DispatchFileRemoval(nsCOMPtr<nsIFile> aBackingFile) {
NS_DispatchBackgroundTask(NS_NewRunnableFunction(
"SSLTokensCache::RemoveFiles", [backingFile = std::move(aBackingFile)]() {
RemoveFilesSync(backingFile);
}));
}
// static
void SSLTokensCache::Clear() {
LOG(("SSLTokensCache::Clear"));
nsCOMPtr<nsIFile> backingFile;
nsCOMPtr<nsISerialEventTarget> taskQueue;
{
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
LOG((" service not initialized"));
return;
}
gInstance->ClearCacheLocked();
backingFile = gInstance->mBackingFile;
taskQueue = gInstance->mWriteTaskQueue;
}
if (backingFile) {
if (taskQueue) {
// Route through the serial write queue so the deletion is ordered after
// any pending write task and cannot re-create the file after deletion.
InvokeAsync(taskQueue.get(), __func__,
[bf = std::move(backingFile)]() mutable {
RemoveFilesSync(bf);
return GenericPromise::CreateAndResolve(true, __func__);
});
} else {
DispatchFileRemoval(std::move(backingFile));
}
}
}
void SSLTokensCache::DoWrite(bool aSynchronous) {
nsCOMPtr<nsIFile> backingFile;
nsCOMPtr<nsISerialEventTarget> taskQueue;
nsTArray<uint8_t> serialized;
{
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
return;
}
backingFile = mBackingFile;
taskQueue = mWriteTaskQueue;
serialized = SerializeSnapshotLocked();
}
if (!backingFile) {
if (XRE_IsSocketProcess() && !serialized.IsEmpty()) {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"SSLTokensCache::SendToParent", [data = std::move(serialized)]() {
auto* child = SocketProcessChild::GetSingleton();
if (child && child->CanSend()) {
(void)child->SendSSLTokensCacheData(
mozilla::ipc::ByteBufFrom(data));
}
}));
}
return;
}
if (serialized.IsEmpty()) {
if (aSynchronous) {
RemoveFilesSync(backingFile);
} else if (!taskQueue) {
DispatchFileRemoval(std::move(backingFile));
} else {
InvokeAsync(taskQueue.get(), __func__,
[bf = std::move(backingFile)]() mutable {
RemoveFilesSync(bf);
return GenericPromise::CreateAndResolve(true, __func__);
});
}
return;
}
nsAutoString widePath;
if (NS_FAILED(backingFile->GetPath(widePath))) {
return;
}
nsCString pathStr = NS_ConvertUTF16toUTF8(widePath);
if (aSynchronous) {
ssl_tokens_cache_write_bytes(&pathStr, &serialized);
} else {
if (!taskQueue) {
return;
}
InvokeAsync(taskQueue.get(), __func__,
[path = std::move(pathStr), data = std::move(serialized)]() {
ssl_tokens_cache_write_bytes(&path, &data);
return GenericPromise::CreateAndResolve(true, __func__);
});
}
}
// static
void SSLTokensCache::DispatchLoad(nsCString aPath, uint32_t aLoadGen) {
if (aPath.IsEmpty()) {
return;
}
NS_DispatchBackgroundTask(
NS_NewRunnableFunction("SSLTokensCache::LoadPersisted",
[path = std::move(aPath), aLoadGen]() {
nsAutoLowPriorityIO lowPriorityIO;
LoadCtx ctx{aLoadGen};
ssl_tokens_cache_read(&path, PR_Now(),
LoadCallback, &ctx);
OnLoadCompleteNotify(ctx.count);
}),
NS_DISPATCH_EVENT_MAY_BLOCK);
}
// static
void SSLTokensCache::OnLoadCompleteNotify(uint32_t aCount) {
mozilla::glean::network::ssl_token_cache_persistence_records_loaded.Add(
AssertedCast<int32_t>(aCount));
TimeDuration elapsed;
{
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
return;
}
gInstance->mLoadComplete = true;
elapsed = TimeStamp::Now() - gInstance->mLoadStartTime;
}
mozilla::glean::network::ssl_token_cache_load_time.AccumulateRawDuration(
elapsed);
LOG(("SSLTokensCache::OnLoadCompleteNotify [records=%u, time=%.1fms]", aCount,
elapsed.ToMilliseconds()));
// Forward persisted tokens to the socket process. Uses
// CallOrWaitForSocketProcess so it fires immediately if the socket process
// is already up, or is deferred until it is ready.
if (StaticPrefs::network_ssl_tokens_cache_persistence()) {
NS_DispatchToMainThread(
NS_NewRunnableFunction("SSLTokensCache::ForwardToSocketProcess", []() {
if (!gIOService || !nsIOService::UseSocketProcess()) {
return;
}
// Serialize on a background thread, then send on the main thread.
// No captures: CallOrWaitForSocketProcess copies its callable when
// deferring, so the lambda must remain copyable.
gIOService->CallOrWaitForSocketProcess([]() {
NS_DispatchBackgroundTask(NS_NewRunnableFunction(
"SSLTokensCache::SerializeForSocket", []() {
nsTArray<uint8_t> data = SSLTokensCache::SerializeForIPC();
if (data.IsEmpty()) {
return;
}
NS_DispatchToMainThread(NS_NewRunnableFunction(
"SSLTokensCache::SendToSocket",
[data = std::move(data)]() {
RefPtr<SocketProcessParent> parent =
SocketProcessParent::GetSingleton();
if (parent && parent->CanSend()) {
(void)parent->SendLoadSSLTokensCache(
mozilla::ipc::ByteBufFrom(data));
}
}));
}));
});
}));
}
}
// static
bool SSLTokensCache::PutFromPersisted(const SslTokensPersistedRecord* aRec,
uint32_t aExpectedGen) {
StaticMutexAutoLock lock(sLock);
if (!gInstance || gInstance->mLoadGeneration != aExpectedGen) {
return false;
}
auto rec = MakeUnique<TokenCacheRecord>();
rec->mKey = aRec->key;
rec->mExpirationTime = static_cast<PRTime>(aRec->expiration_time);
rec->mOverridableError = aRec->overridable_error;
rec->mCompressedPayload.AppendElements(aRec->compressed_payload,
aRec->compressed_payload_len);
gInstance->InsertRecordLocked(std::move(rec));
return true;
}
uint64_t SSLTokensCache::InsertRecordLocked(UniquePtr<TokenCacheRecord> aRec) {
sLock.AssertCurrentThreadOwns();
const uint64_t id = ++sRecordId;
aRec->mId = id;
// aRec->mKey must be read before AddRecord() moves aRec.
TokenCacheEntry* cacheEntry = mTokenCacheRecords.GetOrInsertNew(aRec->mKey);
if (cacheEntry->RecordCount() > 0) {
mCacheSize -= cacheEntry->Size();
}
cacheEntry->AddRecord(std::move(aRec), mExpirationArray);
mCacheSize += cacheEntry->Size();
EvictIfNecessary();
return id;
}
// static
bool SSLTokensCache::ShouldPersistKey(const nsACString& aKey,
uint8_t aOverridableError) {
return aOverridableError == 0 && OAFromPeerId(aKey).mPrivateBrowsingId == 0;
}
// static
void SSLTokensCache::RemoveByMatchAndOAPattern(
const nsACString& aValue, const nsACString& aSeparatedValue,
const mozilla::OriginAttributesPattern& aPattern) {
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
return;
}
gInstance->RemoveMatchingLocked(
[&aValue, &aSeparatedValue, &aPattern](const nsACString& aKey) {
nsDependentCSubstring host = HostFromBasePart(BasePartFromKey(aKey));
return !host.IsEmpty() &&
(host.Equals(aValue) || StringEndsWith(host, aSeparatedValue)) &&
aPattern.Matches(OAFromPeerId(aKey));
});
}
// static
void SSLTokensCache::RemoveByHostAndOAPattern(
const nsACString& aHost, const mozilla::OriginAttributesPattern& aPattern) {
LOG(("SSLTokensCache::RemoveByHostAndOAPattern"));
RemoveByMatchAndOAPattern(aHost, ":"_ns + aHost, aPattern);
}
// static
void SSLTokensCache::RemoveBySiteAndOAPattern(
const nsACString& aSite, const mozilla::OriginAttributesPattern& aPattern) {
LOG(("SSLTokensCache::RemoveBySiteAndOAPattern"));
// Three cases for the host part of a key (after HostFromBasePart):
// "example.com" — exact, no prefix → Equals(aSite)
// "sub.example.com" — subdomain → ends with "."_ns + aSite
// "anon:example.com" — connection-type prefix → ends with ":"_ns + aSite
// (Prefixed subdomains like "anon:sub.example.com" are caught by dotSite.)
nsAutoCString dotSite("."_ns + aSite);
nsAutoCString colonSite(":"_ns + aSite);
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
return;
}
gInstance->RemoveMatchingLocked(
[&aSite, &dotSite, &colonSite, &aPattern](const nsACString& aKey) {
nsDependentCSubstring host = HostFromBasePart(BasePartFromKey(aKey));
return !host.IsEmpty() &&
(host.Equals(aSite) || StringEndsWith(host, dotSite) ||
StringEndsWith(host, colonSite)) &&
aPattern.Matches(OAFromPeerId(aKey));
});
}
#ifdef ENABLE_TESTS
// static
void SSLTokensCache::TriggerWriteForTest(const nsACString& aPath) {
nsTArray<uint8_t> serialized;
{
StaticMutexAutoLock lock(sLock);
serialized = SerializeSnapshotLocked();
}
nsCString flatPath(aPath);
if (serialized.IsEmpty()) {
nsCOMPtr<nsIFile> file;
if (NS_SUCCEEDED(NS_NewNativeLocalFile(flatPath, getter_AddRefs(file)))) {
(void)file->Remove(false);
}
return;
}
ssl_tokens_cache_write_bytes(&flatPath, &serialized);
}
// static
void SSLTokensCache::LoadForTest(const nsACString& aPath) {
uint32_t loadGen = 0;
{
StaticMutexAutoLock lock(sLock);
if (gInstance) {
loadGen = gInstance->mLoadGeneration;
}
}
nsCString flatPath(aPath);
ssl_tokens_cache_read(&flatPath, PR_Now(), PutFromPersistedCallback,
&loadGen);
}
// static
uint32_t SSLTokensCache::CountForTest() {
StaticMutexAutoLock lock(sLock);
if (!gInstance) {
return 0;
}
return gInstance->mTokenCacheRecords.Count();
}
// static
uint32_t SSLTokensCache::CacheSizeForTest() {
StaticMutexAutoLock lock(sLock);
return gInstance ? gInstance->mCacheSize : 0;
}
// static
void SSLTokensCache::PutForTest(const nsACString& aKey) {
uint32_t gen = 0;
{
StaticMutexAutoLock lock(sLock);
if (gInstance) {
gen = gInstance->mLoadGeneration;
}
}
uint8_t dummyToken[] = {0xDE, 0xAD, 0xBE, 0xEF};
SessionCacheInfo info;
nsTArray<uint8_t> payload =
SerializeRecord({dummyToken, sizeof(dummyToken)}, info);
nsTArray<uint8_t> compressed = CompressRecord(payload);
if (compressed.IsEmpty()) {
return;
}
SslTokensPersistedRecord rec{};
rec.key = aKey;
rec.expiration_time = PR_Now() + 3600LL * PR_USEC_PER_SEC;
rec.compressed_payload = compressed.Elements();
rec.compressed_payload_len = compressed.Length();
PutFromPersisted(&rec, gen);
}
#endif // ENABLE_TESTS
NS_IMETHODIMP
SSLTokensCache::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
if (!strcmp(aTopic, "application-background") ||
!strcmp(aTopic, "idle-daily")) {
LOG(("SSLTokensCache::Observe [topic=%s]", aTopic));
DoWrite(false);
} else if (!strcmp(aTopic, "profile-after-change")) {
MOZ_ASSERT(XRE_IsParentProcess());
LOG(("SSLTokensCache::Observe [topic=profile-after-change]"));
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (!obs) {
return NS_OK;
}
// Reconcile persistence state with the pref, which may differ from
// what Init() saw (user.js is applied between Init() and here).
bool wantPersistence = StaticPrefs::network_ssl_tokens_cache_persistence();
bool addObservers = false;
bool removeObservers = false;
nsCString loadPath;
uint32_t loadGen = 0;
{
StaticMutexAutoLock lock(sLock);
if (gInstance) {
bool wasRegistered = gInstance->mWriteObserversRegistered;
gInstance->mWriteObserversRegistered = wantPersistence;
addObservers = wantPersistence && !wasRegistered;
removeObservers = !wantPersistence && wasRegistered;
if (wantPersistence && !gInstance->mBackingFile) {
loadPath = SetupPersistenceLocked(loadGen);
}
if (!wantPersistence) {
gInstance->mBackingFile = nullptr;
gInstance->mWriteTaskQueue = nullptr;
gInstance->ClearCacheLocked();
}
}
}
if (addObservers) {
obs->AddObserver(this, "application-background", false);
obs->AddObserver(this, "idle-daily", false);
} else if (removeObservers) {
obs->RemoveObserver(this, "application-background");
obs->RemoveObserver(this, "idle-daily");
}
if (wantPersistence) {
DispatchLoad(std::move(loadPath), loadGen);
RegisterShutdownBlocker();
}
}
return NS_OK;
}
// nsIAsyncShutdownBlocker
NS_IMETHODIMP
SSLTokensCache::BlockShutdown(nsIAsyncShutdownClient* /* aClient */) {
LOG(("SSLTokensCache::BlockShutdown"));
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(XRE_IsParentProcess());
nsCOMPtr<nsISerialEventTarget> taskQueue;
{
StaticMutexAutoLock lock(sLock);
taskQueue = mWriteTaskQueue;
}
if (!taskQueue) {
RemoveShutdownBlocker();
return NS_OK;
}
// Deserialize socket tokens (if any) then write; both run on the same serial
// queue for atomicity. The blocker keeps the main thread alive until done.
RefPtr<SSLTokensCache> self = this;
auto writeAndRelease = [taskQueue, self](mozilla::ipc::ByteBuf aBuf) {
InvokeAsync(
taskQueue.get(), __func__,
[self, buf = std::move(aBuf)]() {
if (buf.mLen > 0) {
SSLTokensCache::DeserializeFromIPC(Span(buf.mData, buf.mLen));
}
self->DoWrite(true);
return GenericPromise::CreateAndResolve(true, __func__);
})
->Then(
GetMainThreadSerialEventTarget(), __func__,
[self](bool) { self->RemoveShutdownBlocker(); },
[self](nsresult) { self->RemoveShutdownBlocker(); });
};
// If the socket process is alive, flush its token cache first so the
// persisted file reflects the most recent handshake data.
RefPtr<SocketProcessParent> socketParent =
SocketProcessParent::GetSingleton();
if (!socketParent || !socketParent->CanSend()) {
writeAndRelease(mozilla::ipc::ByteBuf{});
return NS_OK;
}
socketParent->SendFlushSSLTokensCache()->Then(
GetMainThreadSerialEventTarget(), __func__,
[writeAndRelease](mozilla::ipc::ByteBuf&& aBuf) {
writeAndRelease(std::move(aBuf));
},
[writeAndRelease](mozilla::ipc::ResponseRejectReason) {
writeAndRelease(mozilla::ipc::ByteBuf{});
});
return NS_OK;
}
NS_IMETHODIMP
SSLTokensCache::GetName(nsAString& aName) {
aName.AssignLiteral("SSLTokensCache: writing cache to disk");
return NS_OK;
}
NS_IMETHODIMP
SSLTokensCache::GetState(nsIPropertyBag** aState) {
*aState = nullptr;
return NS_OK;
}
void SSLTokensCache::RegisterShutdownBlocker() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(XRE_IsParentProcess());
{
StaticMutexAutoLock lock(sLock);
if (!gInstance || !gInstance->mWriteTaskQueue) {
return;
}
if (gInstance->mShutdownBarrier) {
return;
}
}
// sLock is intentionally released before the AsyncShutdown calls below:
// Service() and AddBlocker may run JS, which mustn't happen under sLock.
nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service();
if (!svc) {
return;
}
nsCOMPtr<nsIAsyncShutdownClient> client;
svc->GetProfileBeforeChange(getter_AddRefs(client));
if (!client) {
return;
}
{
StaticMutexAutoLock lock(sLock);
mShutdownBarrier = client;
}
LOG(("SSLTokensCache::RegisterShutdownBlocker"));
client->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
u""_ns);
}
void SSLTokensCache::RemoveShutdownBlocker() {
nsCOMPtr<nsIAsyncShutdownClient> barrier;
{
StaticMutexAutoLock lock(sLock);
barrier = std::move(mShutdownBarrier);
// Shutdown() left gInstance set so BlockShutdown could write; tear it
// down now that the blocker is releasing.
gInstance = nullptr;
}
if (barrier) {
barrier->RemoveBlocker(this);
}
}
} // namespace net
} // namespace mozilla