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
1503 lines
47 KiB
C++
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
|