зеркало из https://github.com/mozilla/gecko-dev.git
Backed out changeset e69a7143b38d (bug 1840135
) for causing failures on test_sss_sanitizeOnShutdown.js.CLOSED TREE
This commit is contained in:
Родитель
ce72fea4a7
Коммит
02731904bb
|
@ -1211,23 +1211,6 @@ dependencies = [
|
|||
"nsstring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data_storage"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cstr",
|
||||
"firefox-on-glean",
|
||||
"log",
|
||||
"malloc_size_of_derive",
|
||||
"moz_task",
|
||||
"nserror",
|
||||
"nsstring",
|
||||
"thin-vec",
|
||||
"wr_malloc_size_of",
|
||||
"xpcom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.6.5"
|
||||
|
@ -2153,7 +2136,6 @@ dependencies = [
|
|||
"cubeb-sys",
|
||||
"dap_ffi",
|
||||
"data-encoding-ffi",
|
||||
"data_storage",
|
||||
"detect_win32k_conflicts",
|
||||
"dom",
|
||||
"encoding_glue",
|
||||
|
|
|
@ -0,0 +1,889 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "DataStorage.h"
|
||||
|
||||
#include "mozilla/AppShutdown.h"
|
||||
#include "mozilla/Assertions.h"
|
||||
#include "mozilla/ClearOnShutdown.h"
|
||||
#include "mozilla/FileUtils.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/StaticMutex.h"
|
||||
#include "mozilla/StaticPtr.h"
|
||||
#include "mozilla/TaskQueue.h"
|
||||
#include "mozilla/Telemetry.h"
|
||||
#include "mozilla/Unused.h"
|
||||
#include "nsAppDirectoryServiceDefs.h"
|
||||
#include "nsDirectoryServiceUtils.h"
|
||||
#include "nsIFileStreams.h"
|
||||
#include "nsIObserverService.h"
|
||||
#include "nsISafeOutputStream.h"
|
||||
#include "nsISerialEventTarget.h"
|
||||
#include "nsIThread.h"
|
||||
#include "nsITimer.h"
|
||||
#include "nsNetUtil.h"
|
||||
#include "nsPrintfCString.h"
|
||||
#include "nsStreamUtils.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "private/pprio.h"
|
||||
|
||||
#if defined(XP_WIN)
|
||||
# include "nsILocalFileWin.h"
|
||||
#endif
|
||||
|
||||
// NB: Read DataStorage.h first.
|
||||
|
||||
// The default time between data changing and a write, in milliseconds.
|
||||
static const uint32_t sDataStorageDefaultTimerDelay = 5u * 60u * 1000u;
|
||||
// The maximum score an entry can have (prevents overflow)
|
||||
static const uint32_t sMaxScore = UINT32_MAX;
|
||||
// The maximum number of entries per type of data (limits resource use)
|
||||
static const uint32_t sMaxDataEntries = 1024;
|
||||
static const int64_t sOneDayInMicroseconds =
|
||||
int64_t(24 * 60 * 60) * PR_USEC_PER_SEC;
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
NS_IMPL_ISUPPORTS(DataStorageManager, nsIDataStorageManager)
|
||||
NS_IMPL_ISUPPORTS(DataStorageItem, nsIDataStorageItem)
|
||||
NS_IMPL_ISUPPORTS(DataStorage, nsIDataStorage, nsIMemoryReporter, nsIObserver)
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorageManager::Get(nsIDataStorageManager::DataStorage aDataStorage,
|
||||
nsIDataStorage** aResult) {
|
||||
if (!NS_IsMainThread()) {
|
||||
return NS_ERROR_NOT_SAME_THREAD;
|
||||
}
|
||||
nsAutoString filename;
|
||||
switch (aDataStorage) {
|
||||
case nsIDataStorageManager::AlternateServices:
|
||||
if (mAlternateServicesCreated) {
|
||||
return NS_ERROR_ALREADY_INITIALIZED;
|
||||
}
|
||||
mAlternateServicesCreated = true;
|
||||
filename.Assign(u"AlternateServices.txt"_ns);
|
||||
break;
|
||||
case nsIDataStorageManager::ClientAuthRememberList:
|
||||
if (mClientAuthRememberListCreated) {
|
||||
return NS_ERROR_ALREADY_INITIALIZED;
|
||||
}
|
||||
mClientAuthRememberListCreated = true;
|
||||
filename.Assign(u"ClientAuthRememberList.txt"_ns);
|
||||
break;
|
||||
case nsIDataStorageManager::SiteSecurityServiceState:
|
||||
if (mSiteSecurityServiceStateCreated) {
|
||||
return NS_ERROR_ALREADY_INITIALIZED;
|
||||
}
|
||||
mSiteSecurityServiceStateCreated = true;
|
||||
filename.Assign(u"SiteSecurityServiceState.txt"_ns);
|
||||
break;
|
||||
default:
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
RefPtr<mozilla::DataStorage> dataStorage(new mozilla::DataStorage(filename));
|
||||
nsresult rv = dataStorage->Init();
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
nsCOMPtr<nsIMemoryReporter> memoryReporter(dataStorage.get());
|
||||
RegisterStrongMemoryReporter(memoryReporter);
|
||||
*aResult = dataStorage.forget().take();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorageItem::GetKey(nsACString& aKey) {
|
||||
aKey.Assign(key);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorageItem::GetValue(nsACString& aValue) {
|
||||
aValue.Assign(value);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorageItem::GetType(nsIDataStorage::DataType* aType) {
|
||||
if (!aType) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
*aType = type;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
DataStorage::DataStorage(const nsString& aFilename)
|
||||
: mMutex("DataStorage::mMutex"),
|
||||
mPendingWrite(false),
|
||||
mTimerArmed(false),
|
||||
mShuttingDown(false),
|
||||
mInitCalled(false),
|
||||
mReadyMonitor("DataStorage::mReadyMonitor"),
|
||||
mReady(false),
|
||||
mFilename(aFilename) {}
|
||||
|
||||
nsresult DataStorage::Init() {
|
||||
// Don't access the observer service or preferences off the main thread.
|
||||
if (!NS_IsMainThread()) {
|
||||
MOZ_ASSERT_UNREACHABLE("DataStorage::Init called off main thread");
|
||||
return NS_ERROR_NOT_SAME_THREAD;
|
||||
}
|
||||
|
||||
if (!XRE_IsParentProcess()) {
|
||||
MOZ_ASSERT_UNREACHABLE("DataStorage used in non-parent process");
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
|
||||
// Reject new DataStorage instances if the browser is shutting down. There
|
||||
// is no guarantee that DataStorage writes will be able to be persisted if
|
||||
// we init during shutdown, so we return an error here to hopefully make
|
||||
// this more explicit and consistent.
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
||||
// Ignore attempts to initialize several times.
|
||||
if (mInitCalled) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
mInitCalled = true;
|
||||
|
||||
nsCOMPtr<nsISerialEventTarget> target;
|
||||
nsresult rv = NS_CreateBackgroundTaskQueue(
|
||||
"DataStorage::mBackgroundTaskQueue", getter_AddRefs(target));
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
mBackgroundTaskQueue = TaskQueue::Create(target.forget(), "PSM DataStorage");
|
||||
|
||||
// For test purposes, we can set the write timer to be very fast.
|
||||
mTimerDelayMS = Preferences::GetInt("test.datastorage.write_timer_ms",
|
||||
sDataStorageDefaultTimerDelay);
|
||||
|
||||
rv = AsyncReadData(lock);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
|
||||
if (NS_WARN_IF(!os)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
// Clear private data as appropriate.
|
||||
os->AddObserver(this, "last-pb-context-exited", false);
|
||||
// Observe shutdown; save data and prevent any further writes.
|
||||
// We need to write to the profile directory, so we should listen for
|
||||
// profile-before-change so that we can safely write to the profile.
|
||||
os->AddObserver(this, "profile-before-change", false);
|
||||
// This is a backstop for xpcshell and other cases where
|
||||
// profile-before-change might not get sent.
|
||||
os->AddObserver(this, "xpcom-shutdown-threads", false);
|
||||
// On mobile, if the app is backgrounded, it may be killed. Observe this
|
||||
// notification to kick off an asynchronous write to avoid losing data.
|
||||
os->AddObserver(this, "application-background", false);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
class DataStorage::Reader : public Runnable {
|
||||
public:
|
||||
explicit Reader(DataStorage* aDataStorage)
|
||||
: Runnable("DataStorage::Reader"), mDataStorage(aDataStorage) {}
|
||||
~Reader();
|
||||
|
||||
private:
|
||||
NS_DECL_NSIRUNNABLE
|
||||
|
||||
static nsresult ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
|
||||
Entry& aEntryOut);
|
||||
|
||||
RefPtr<DataStorage> mDataStorage;
|
||||
};
|
||||
|
||||
DataStorage::Reader::~Reader() {
|
||||
// Notify that calls to Get can proceed.
|
||||
{
|
||||
MonitorAutoLock readyLock(mDataStorage->mReadyMonitor);
|
||||
mDataStorage->mReady = true;
|
||||
mDataStorage->mReadyMonitor.NotifyAll();
|
||||
}
|
||||
|
||||
// This is for tests.
|
||||
nsCOMPtr<nsIRunnable> job = NewRunnableMethod<const char*>(
|
||||
"DataStorage::NotifyObservers", mDataStorage,
|
||||
&DataStorage::NotifyObservers, "data-storage-ready");
|
||||
nsresult rv = NS_DispatchToMainThread(job, NS_DISPATCH_NORMAL);
|
||||
Unused << NS_WARN_IF(NS_FAILED(rv));
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::Reader::Run() {
|
||||
nsresult rv;
|
||||
// Concurrent operations on nsIFile objects are not guaranteed to be safe,
|
||||
// so we clone the file while holding the lock and then release the lock.
|
||||
// At that point, we can safely operate on the clone.
|
||||
nsCOMPtr<nsIFile> file;
|
||||
{
|
||||
MutexAutoLock lock(mDataStorage->mMutex);
|
||||
// If we don't have a profile, bail.
|
||||
if (!mDataStorage->mBackingFile) {
|
||||
return NS_OK;
|
||||
}
|
||||
rv = mDataStorage->mBackingFile->Clone(getter_AddRefs(file));
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
nsCOMPtr<nsIInputStream> fileInputStream;
|
||||
rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), file);
|
||||
// If we failed for some reason other than the file doesn't exist, bail.
|
||||
if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// If there is a file with data in it, read it. If there isn't,
|
||||
// we'll essentially fall through to notifying that we're good to go.
|
||||
nsCString data;
|
||||
if (fileInputStream) {
|
||||
// Limit to 2MB of data, but only store sMaxDataEntries entries.
|
||||
rv = NS_ConsumeStream(fileInputStream, 1u << 21, data);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
// Atomically parse the data and insert the entries read.
|
||||
// Don't clear existing entries - they may have been inserted between when
|
||||
// this read was kicked-off and when it was run.
|
||||
{
|
||||
MutexAutoLock lock(mDataStorage->mMutex);
|
||||
// The backing file consists of a list of
|
||||
// <key>\t<score>\t<last accessed time>\t<value>\n
|
||||
// The final \n is not optional; if it is not present the line is assumed
|
||||
// to be corrupt.
|
||||
int32_t currentIndex = 0;
|
||||
int32_t newlineIndex = 0;
|
||||
do {
|
||||
newlineIndex = data.FindChar('\n', currentIndex);
|
||||
// If there are no more newlines or the data table has too many
|
||||
// entries, we are done.
|
||||
if (newlineIndex < 0 ||
|
||||
mDataStorage->mPersistentDataTable.Count() >= sMaxDataEntries) {
|
||||
break;
|
||||
}
|
||||
|
||||
nsDependentCSubstring line(data, currentIndex,
|
||||
newlineIndex - currentIndex);
|
||||
currentIndex = newlineIndex + 1;
|
||||
nsCString key;
|
||||
Entry entry;
|
||||
nsresult parseRV = ParseLine(line, key, entry);
|
||||
if (NS_SUCCEEDED(parseRV)) {
|
||||
// It could be the case that a newer entry was added before
|
||||
// we got around to reading the file. Don't overwrite new entries.
|
||||
mDataStorage->mPersistentDataTable.LookupOrInsert(key,
|
||||
std::move(entry));
|
||||
}
|
||||
} while (true);
|
||||
|
||||
Telemetry::Accumulate(Telemetry::DATA_STORAGE_ENTRIES,
|
||||
mDataStorage->mPersistentDataTable.Count());
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// The key must be a non-empty string containing no instances of '\t' or '\n',
|
||||
// and must have a length no more than 256.
|
||||
// The value must not contain '\n' and must have a length no more than 1024.
|
||||
// The length limits are to prevent unbounded memory and disk usage.
|
||||
/* static */
|
||||
nsresult DataStorage::ValidateKeyAndValue(const nsACString& aKey,
|
||||
const nsACString& aValue) {
|
||||
if (aKey.IsEmpty()) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
if (aKey.Length() > 256) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
int32_t delimiterIndex = aKey.FindChar('\t', 0);
|
||||
if (delimiterIndex >= 0) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
delimiterIndex = aKey.FindChar('\n', 0);
|
||||
if (delimiterIndex >= 0) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
delimiterIndex = aValue.FindChar('\n', 0);
|
||||
if (delimiterIndex >= 0) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
if (aValue.Length() > 1024) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Each line is: <key>\t<score>\t<last accessed time>\t<value>
|
||||
// Where <score> is a uint32_t as a string, <last accessed time> is a
|
||||
// int32_t as a string, and the rest are strings.
|
||||
// <value> can contain anything but a newline.
|
||||
// Returns a successful status if the line can be decoded into a key and entry.
|
||||
// Otherwise, an error status is returned and the values assigned to the
|
||||
// output parameters are in an undefined state.
|
||||
/* static */
|
||||
nsresult DataStorage::Reader::ParseLine(nsDependentCSubstring& aLine,
|
||||
nsCString& aKeyOut, Entry& aEntryOut) {
|
||||
// First find the indices to each part of the line.
|
||||
int32_t scoreIndex;
|
||||
scoreIndex = aLine.FindChar('\t', 0) + 1;
|
||||
if (scoreIndex <= 0) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
int32_t accessedIndex = aLine.FindChar('\t', scoreIndex) + 1;
|
||||
if (accessedIndex <= 0) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
int32_t valueIndex = aLine.FindChar('\t', accessedIndex) + 1;
|
||||
if (valueIndex <= 0) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
// Now make substrings based on where each part is.
|
||||
nsDependentCSubstring keyPart(aLine, 0, scoreIndex - 1);
|
||||
nsDependentCSubstring scorePart(aLine, scoreIndex,
|
||||
accessedIndex - scoreIndex - 1);
|
||||
nsDependentCSubstring accessedPart(aLine, accessedIndex,
|
||||
valueIndex - accessedIndex - 1);
|
||||
nsDependentCSubstring valuePart(aLine, valueIndex);
|
||||
|
||||
nsresult rv;
|
||||
rv = DataStorage::ValidateKeyAndValue(nsCString(keyPart),
|
||||
nsCString(valuePart));
|
||||
if (NS_FAILED(rv)) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
// Now attempt to decode the score part as a uint32_t.
|
||||
// XXX nsDependentCSubstring doesn't support ToInteger
|
||||
int32_t integer = nsCString(scorePart).ToInteger(&rv);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
if (integer < 0) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
aEntryOut.mScore = (uint32_t)integer;
|
||||
|
||||
integer = nsCString(accessedPart).ToInteger(&rv);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
if (integer < 0) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
aEntryOut.mLastAccessed = integer;
|
||||
|
||||
// Now set the key and value.
|
||||
aKeyOut.Assign(keyPart);
|
||||
aEntryOut.mValue.Assign(valuePart);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult DataStorage::AsyncReadData(const MutexAutoLock& /*aProofOfLock*/) {
|
||||
mMutex.AssertCurrentThreadOwns();
|
||||
// Allocate a Reader so that even if it isn't dispatched,
|
||||
// the data-storage-ready notification will be fired and Get
|
||||
// will be able to proceed (this happens in its destructor).
|
||||
nsCOMPtr<nsIRunnable> job(new Reader(this));
|
||||
nsresult rv;
|
||||
// If we don't have a profile directory, this will fail.
|
||||
// That's okay - it just means there is no persistent state.
|
||||
rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
|
||||
getter_AddRefs(mBackingFile));
|
||||
if (NS_FAILED(rv)) {
|
||||
mBackingFile = nullptr;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
rv = mBackingFile->Append(mFilename);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = mBackgroundTaskQueue->Dispatch(job.forget());
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::IsReady(bool* aReady) {
|
||||
if (!aReady) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
MonitorAutoLock readyLock(mReadyMonitor);
|
||||
*aReady = mReady;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void DataStorage::WaitForReady() {
|
||||
MOZ_DIAGNOSTIC_ASSERT(mInitCalled, "Waiting before Init() has been called?");
|
||||
|
||||
MonitorAutoLock readyLock(mReadyMonitor);
|
||||
while (!mReady) {
|
||||
readyLock.Wait();
|
||||
}
|
||||
MOZ_ASSERT(mReady);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::Get(const nsACString& aKey, nsIDataStorage::DataType aType,
|
||||
nsACString& aValue) {
|
||||
WaitForReady();
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
||||
Entry entry;
|
||||
bool foundValue = GetInternal(aKey, &entry, aType, lock);
|
||||
if (!foundValue) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
// If we're here, we found a value. Maybe update its score.
|
||||
if (entry.UpdateScore()) {
|
||||
PutInternal(aKey, entry, aType, lock);
|
||||
}
|
||||
|
||||
aValue.Assign(entry.mValue);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool DataStorage::GetInternal(const nsACString& aKey, Entry* aEntry,
|
||||
nsIDataStorage::DataType aType,
|
||||
const MutexAutoLock& aProofOfLock) {
|
||||
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
|
||||
bool foundValue = table.Get(aKey, aEntry);
|
||||
return foundValue;
|
||||
}
|
||||
|
||||
DataStorage::DataStorageTable& DataStorage::GetTableForType(
|
||||
nsIDataStorage::DataType aType, const MutexAutoLock& /*aProofOfLock*/) {
|
||||
switch (aType) {
|
||||
case nsIDataStorage::DataType::Persistent:
|
||||
return mPersistentDataTable;
|
||||
case nsIDataStorage::DataType::Temporary:
|
||||
return mTemporaryDataTable;
|
||||
case nsIDataStorage::DataType::Private:
|
||||
return mPrivateDataTable;
|
||||
}
|
||||
|
||||
MOZ_CRASH("given bad DataStorage storage type");
|
||||
}
|
||||
|
||||
void DataStorage::ReadAllFromTable(nsIDataStorage::DataType aType,
|
||||
nsTArray<RefPtr<nsIDataStorageItem>>& aItems,
|
||||
const MutexAutoLock& aProofOfLock) {
|
||||
for (auto iter = GetTableForType(aType, aProofOfLock).Iter(); !iter.Done();
|
||||
iter.Next()) {
|
||||
nsCOMPtr<nsIDataStorageItem> item(
|
||||
new DataStorageItem(iter.Key(), iter.Data().mValue, aType));
|
||||
aItems.AppendElement(item);
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::GetAll(nsTArray<RefPtr<nsIDataStorageItem>>& aItems) {
|
||||
WaitForReady();
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
||||
aItems.SetCapacity(mPersistentDataTable.Count() +
|
||||
mTemporaryDataTable.Count() + mPrivateDataTable.Count());
|
||||
ReadAllFromTable(nsIDataStorage::DataType::Persistent, aItems, lock);
|
||||
ReadAllFromTable(nsIDataStorage::DataType::Temporary, aItems, lock);
|
||||
ReadAllFromTable(nsIDataStorage::DataType::Private, aItems, lock);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Limit the number of entries per table. This is to prevent unbounded
|
||||
// resource use. The eviction strategy is as follows:
|
||||
// - An entry's score is incremented once for every day it is accessed.
|
||||
// - Evict an entry with score no more than any other entry in the table
|
||||
// (this is the same as saying evict the entry with the lowest score,
|
||||
// except for when there are multiple entries with the lowest score,
|
||||
// in which case one of them is evicted - which one is not specified).
|
||||
void DataStorage::MaybeEvictOneEntry(nsIDataStorage::DataType aType,
|
||||
const MutexAutoLock& aProofOfLock) {
|
||||
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
|
||||
if (table.Count() >= sMaxDataEntries) {
|
||||
KeyAndEntry toEvict;
|
||||
// If all entries have score sMaxScore, this won't actually remove
|
||||
// anything. This will never happen, however, because having that high
|
||||
// a score either means someone tampered with the backing file or every
|
||||
// entry has been accessed once a day for ~4 billion days.
|
||||
// The worst that will happen is there will be 1025 entries in the
|
||||
// persistent data table, with the 1025th entry being replaced every time
|
||||
// data with a new key is inserted into the table. This is bad but
|
||||
// ultimately not that concerning, considering that if an attacker can
|
||||
// modify data in the profile, they can cause much worse harm.
|
||||
toEvict.mEntry.mScore = sMaxScore;
|
||||
|
||||
for (auto iter = table.Iter(); !iter.Done(); iter.Next()) {
|
||||
Entry entry = iter.UserData();
|
||||
if (entry.mScore < toEvict.mEntry.mScore) {
|
||||
toEvict.mKey = iter.Key();
|
||||
toEvict.mEntry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
table.Remove(toEvict.mKey);
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::Put(const nsACString& aKey, const nsACString& aValue,
|
||||
nsIDataStorage::DataType aType) {
|
||||
WaitForReady();
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
||||
nsresult rv;
|
||||
rv = ValidateKeyAndValue(aKey, aValue);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
Entry entry;
|
||||
bool exists = GetInternal(aKey, &entry, aType, lock);
|
||||
if (exists) {
|
||||
entry.UpdateScore();
|
||||
} else {
|
||||
MaybeEvictOneEntry(aType, lock);
|
||||
}
|
||||
entry.mValue = aValue;
|
||||
return PutInternal(aKey, entry, aType, lock);
|
||||
}
|
||||
|
||||
nsresult DataStorage::PutInternal(const nsACString& aKey, Entry& aEntry,
|
||||
nsIDataStorage::DataType aType,
|
||||
const MutexAutoLock& aProofOfLock) {
|
||||
mMutex.AssertCurrentThreadOwns();
|
||||
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
|
||||
table.InsertOrUpdate(aKey, aEntry);
|
||||
|
||||
if (aType == nsIDataStorage::DataType::Persistent) {
|
||||
mPendingWrite = true;
|
||||
ArmTimer(aProofOfLock);
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::Remove(const nsACString& aKey, nsIDataStorage::DataType aType) {
|
||||
WaitForReady();
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
||||
DataStorageTable& table = GetTableForType(aType, lock);
|
||||
table.Remove(aKey);
|
||||
|
||||
if (aType == nsIDataStorage::DataType::Persistent) {
|
||||
mPendingWrite = true;
|
||||
ArmTimer(lock);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
class DataStorage::Writer final : public Runnable {
|
||||
public:
|
||||
Writer(nsCString& aData, DataStorage* aDataStorage)
|
||||
: Runnable("DataStorage::Writer"),
|
||||
mData(aData),
|
||||
mDataStorage(aDataStorage) {}
|
||||
|
||||
protected:
|
||||
NS_DECL_NSIRUNNABLE
|
||||
|
||||
nsCString mData;
|
||||
RefPtr<DataStorage> mDataStorage;
|
||||
};
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::Writer::Run() {
|
||||
nsresult rv;
|
||||
// Concurrent operations on nsIFile objects are not guaranteed to be safe,
|
||||
// so we clone the file while holding the lock and then release the lock.
|
||||
// At that point, we can safely operate on the clone.
|
||||
nsCOMPtr<nsIFile> file;
|
||||
{
|
||||
MutexAutoLock lock(mDataStorage->mMutex);
|
||||
// If we don't have a profile, bail.
|
||||
if (!mDataStorage->mBackingFile) {
|
||||
return NS_OK;
|
||||
}
|
||||
rv = mDataStorage->mBackingFile->Clone(getter_AddRefs(file));
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIOutputStream> outputStream;
|
||||
rv =
|
||||
NS_NewSafeLocalFileOutputStream(getter_AddRefs(outputStream), file,
|
||||
PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
// When the output stream is null, it means we don't have a profile.
|
||||
if (!outputStream) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
const char* ptr = mData.get();
|
||||
uint32_t remaining = mData.Length();
|
||||
uint32_t written = 0;
|
||||
while (remaining > 0) {
|
||||
rv = outputStream->Write(ptr, remaining, &written);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
remaining -= written;
|
||||
ptr += written;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsISafeOutputStream> safeOutputStream =
|
||||
do_QueryInterface(outputStream);
|
||||
if (!safeOutputStream) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
rv = safeOutputStream->Finish();
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Observed by tests.
|
||||
nsCOMPtr<nsIRunnable> job = NewRunnableMethod<const char*>(
|
||||
"DataStorage::NotifyObservers", mDataStorage,
|
||||
&DataStorage::NotifyObservers, "data-storage-written");
|
||||
rv = NS_DispatchToMainThread(job, NS_DISPATCH_NORMAL);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult DataStorage::AsyncWriteData(const MutexAutoLock& /*aProofOfLock*/) {
|
||||
mMutex.AssertCurrentThreadOwns();
|
||||
if (!mPendingWrite || mShuttingDown || !mBackingFile) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsCString output;
|
||||
for (auto iter = mPersistentDataTable.Iter(); !iter.Done(); iter.Next()) {
|
||||
Entry entry = iter.UserData();
|
||||
output.Append(iter.Key());
|
||||
output.Append('\t');
|
||||
output.AppendInt(entry.mScore);
|
||||
output.Append('\t');
|
||||
output.AppendInt(entry.mLastAccessed);
|
||||
output.Append('\t');
|
||||
output.Append(entry.mValue);
|
||||
output.Append('\n');
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIRunnable> job(new Writer(output, this));
|
||||
nsresult rv = mBackgroundTaskQueue->Dispatch(job.forget());
|
||||
mPendingWrite = false;
|
||||
if (mTimerArmed) {
|
||||
rv = mTimer->Cancel();
|
||||
Unused << NS_WARN_IF(NS_FAILED(rv));
|
||||
mTimerArmed = false;
|
||||
}
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::Clear() {
|
||||
WaitForReady();
|
||||
MutexAutoLock lock(mMutex);
|
||||
mPersistentDataTable.Clear();
|
||||
mTemporaryDataTable.Clear();
|
||||
mPrivateDataTable.Clear();
|
||||
mPendingWrite = true;
|
||||
|
||||
// Asynchronously clear the file. This is similar to the permission manager
|
||||
// in that it doesn't wait to synchronously remove the data from its backing
|
||||
// storage either.
|
||||
return AsyncWriteData(lock);
|
||||
}
|
||||
|
||||
/* static */
|
||||
void DataStorage::TimerCallback(nsITimer* aTimer, void* aClosure) {
|
||||
RefPtr<DataStorage> aDataStorage = (DataStorage*)aClosure;
|
||||
MutexAutoLock lock(aDataStorage->mMutex);
|
||||
aDataStorage->mTimerArmed = false;
|
||||
Unused << aDataStorage->AsyncWriteData(lock);
|
||||
}
|
||||
|
||||
void DataStorage::NotifyObservers(const char* aTopic) {
|
||||
// Don't access the observer service off the main thread.
|
||||
if (!NS_IsMainThread()) {
|
||||
MOZ_ASSERT_UNREACHABLE(
|
||||
"DataStorage::NotifyObservers called off main thread");
|
||||
return;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
|
||||
if (os) {
|
||||
os->NotifyObservers(nullptr, aTopic, mFilename.get());
|
||||
}
|
||||
}
|
||||
|
||||
void DataStorage::ArmTimer(const MutexAutoLock& /*aProofOfLock*/) {
|
||||
mMutex.AssertCurrentThreadOwns();
|
||||
if (mTimerArmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mTimer) {
|
||||
mTimer = NS_NewTimer(mBackgroundTaskQueue);
|
||||
if (NS_WARN_IF(!mTimer)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nsresult rv = mTimer->InitWithNamedFuncCallback(
|
||||
DataStorage::TimerCallback, this, mTimerDelayMS, nsITimer::TYPE_ONE_SHOT,
|
||||
"DataStorageTimer");
|
||||
Unused << NS_WARN_IF(NS_FAILED(rv));
|
||||
|
||||
mTimerArmed = true;
|
||||
}
|
||||
|
||||
void DataStorage::ShutdownTimer() {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
if (mTimer) {
|
||||
nsresult rv = mTimer->Cancel();
|
||||
Unused << NS_WARN_IF(NS_FAILED(rv));
|
||||
mTimer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------
|
||||
// DataStorage::nsIMemoryReporter
|
||||
//------------------------------------------------------------
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::CollectReports(nsIHandleReportCallback* aHandleReport,
|
||||
nsISupports* aData, bool aAnonymize) {
|
||||
MutexAutoLock lock(mMutex);
|
||||
size_t sizeOfExcludingThis =
|
||||
mPersistentDataTable.ShallowSizeOfExcludingThis(MallocSizeOf) +
|
||||
mTemporaryDataTable.ShallowSizeOfExcludingThis(MallocSizeOf) +
|
||||
mPrivateDataTable.ShallowSizeOfExcludingThis(MallocSizeOf) +
|
||||
mFilename.SizeOfExcludingThisIfUnshared(MallocSizeOf);
|
||||
size_t amount = MallocSizeOf(this) + sizeOfExcludingThis;
|
||||
nsPrintfCString path("explicit/data-storage/%s",
|
||||
NS_ConvertUTF16toUTF8(mFilename).get());
|
||||
return aHandleReport->Callback(""_ns, path, KIND_HEAP, UNITS_BYTES, amount,
|
||||
"Memory used by PSM data storage cache."_ns,
|
||||
aData);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------
|
||||
// DataStorage::nsIObserver
|
||||
//------------------------------------------------------------
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorage::Observe(nsISupports* /*aSubject*/, const char* aTopic,
|
||||
const char16_t* /*aData*/) {
|
||||
if (!NS_IsMainThread()) {
|
||||
MOZ_ASSERT_UNREACHABLE("DataStorage::Observe called off main thread");
|
||||
return NS_ERROR_NOT_SAME_THREAD;
|
||||
}
|
||||
|
||||
if (strcmp(aTopic, "last-pb-context-exited") == 0) {
|
||||
MutexAutoLock lock(mMutex);
|
||||
mPrivateDataTable.Clear();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (strcmp(aTopic, "profile-before-change") == 0 ||
|
||||
strcmp(aTopic, "xpcom-shutdown-threads") == 0) {
|
||||
RefPtr<TaskQueue> taskQueueToAwait;
|
||||
{
|
||||
MutexAutoLock lock(mMutex);
|
||||
if (!mShuttingDown) {
|
||||
nsresult rv = AsyncWriteData(lock);
|
||||
Unused << NS_WARN_IF(NS_FAILED(rv));
|
||||
mShuttingDown = true;
|
||||
mBackgroundTaskQueue->BeginShutdown();
|
||||
taskQueueToAwait = mBackgroundTaskQueue;
|
||||
}
|
||||
}
|
||||
// Tasks on the background queue may take the lock, so it can't be held
|
||||
// while waiting for them to finish.
|
||||
if (taskQueueToAwait) {
|
||||
taskQueueToAwait->AwaitShutdownAndIdle();
|
||||
}
|
||||
ShutdownTimer();
|
||||
}
|
||||
|
||||
// On mobile, if the app is backgrounded, it may be killed. Kick off an
|
||||
// asynchronous write to avoid losing data.
|
||||
if (strcmp(aTopic, "application-background") == 0) {
|
||||
MutexAutoLock lock(mMutex);
|
||||
if (!mShuttingDown) {
|
||||
nsresult rv = AsyncWriteData(lock);
|
||||
Unused << NS_WARN_IF(NS_FAILED(rv));
|
||||
}
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
DataStorage::Entry::Entry()
|
||||
: mScore(0), mLastAccessed((int32_t)(PR_Now() / sOneDayInMicroseconds)) {}
|
||||
|
||||
// Updates this entry's score. Returns true if the score has actually changed.
|
||||
// If it's been less than a day since this entry has been accessed, the score
|
||||
// does not change. Otherwise, the score increases by 1.
|
||||
// The default score is 0. The maximum score is the maximum value that can
|
||||
// be represented by an unsigned 32 bit integer.
|
||||
// This is to handle evictions from our tables, which in turn is to prevent
|
||||
// unbounded resource use.
|
||||
bool DataStorage::Entry::UpdateScore() {
|
||||
int32_t nowInDays = (int32_t)(PR_Now() / sOneDayInMicroseconds);
|
||||
int32_t daysSinceAccessed = (nowInDays - mLastAccessed);
|
||||
|
||||
// Update the last accessed time.
|
||||
mLastAccessed = nowInDays;
|
||||
|
||||
// If it's been less than a day since we've been accessed,
|
||||
// the score isn't updated.
|
||||
if (daysSinceAccessed < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, increment the score (but don't overflow).
|
||||
if (mScore < sMaxScore) {
|
||||
mScore++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
|
@ -0,0 +1,205 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_DataStorage_h
|
||||
#define mozilla_DataStorage_h
|
||||
|
||||
#include "mozilla/Atomics.h"
|
||||
#include "mozilla/MemoryReporting.h"
|
||||
#include "mozilla/Monitor.h"
|
||||
#include "mozilla/Mutex.h"
|
||||
#include "mozilla/StaticPtr.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsTHashMap.h"
|
||||
#include "nsIDataStorage.h"
|
||||
#include "nsIMemoryReporter.h"
|
||||
#include "nsIObserver.h"
|
||||
#include "nsITimer.h"
|
||||
#include "nsRefPtrHashtable.h"
|
||||
#include "nsString.h"
|
||||
|
||||
namespace mozilla {
|
||||
class TaskQueue;
|
||||
|
||||
/**
|
||||
* DataStorage is a threadsafe, generic, narrow string-based hash map that
|
||||
* persists data on disk and additionally handles temporary and private data.
|
||||
* However, if used in a context where there is no profile directory, data
|
||||
* will not be persisted.
|
||||
*
|
||||
* Its lifecycle is as follows:
|
||||
* - Allocate with a filename (this is or will eventually be a file in the
|
||||
* profile directory, if the profile exists).
|
||||
* - Call Init() from the main thread. This spins off an asynchronous read
|
||||
* of the backing file.
|
||||
* - Eventually observers of the topic "data-storage-ready" will be notified
|
||||
* with the backing filename as the data in the notification when this
|
||||
* has completed.
|
||||
* - Should the profile directory not be available, (e.g. in xpcshell),
|
||||
* DataStorage will not initially read any persistent data. The
|
||||
* "data-storage-ready" event will still be emitted. This follows semantics
|
||||
* similar to the permission manager and allows tests that test unrelated
|
||||
* components to proceed without a profile.
|
||||
* - A timer periodically fires on a background thread that checks if any
|
||||
* persistent data has changed, and if so writes all persistent data to the
|
||||
* backing file. When this happens, observers will be notified with the
|
||||
* topic "data-storage-written" and the backing filename as the data.
|
||||
* It is possible to receive a "data-storage-written" event while there exist
|
||||
* pending persistent data changes. However, those changes will eventually be
|
||||
* written when the timer fires again, and eventually another
|
||||
* "data-storage-written" event will be sent.
|
||||
* - When a DataStorage instance observes the topic "profile-before-change" in
|
||||
* anticipation of shutdown, all persistent data for that DataStorage is
|
||||
* written to the backing file (this blocks the main thread). In the process
|
||||
* of doing this, the background serial event target responsible for these
|
||||
* writes is then shut down to prevent further writes to that file (the
|
||||
* background timer is also cancelled when this happens).
|
||||
* If "profile-before-change" is not observed, this happens upon observing
|
||||
* "xpcom-shutdown-threads".
|
||||
* - For testing purposes, the preference "test.datastorage.write_timer_ms" can
|
||||
* be set to cause the asynchronous writing of data to happen more quickly.
|
||||
* - To prevent unbounded memory and disk use, the number of entries in each
|
||||
* table is limited to 1024. Evictions are handled in by a modified LRU scheme
|
||||
* (see implementation comments).
|
||||
* - NB: Instances of DataStorage have long lifetimes because they are strong
|
||||
* observers of events and won't go away until the observer service does.
|
||||
*
|
||||
* For each key/value:
|
||||
* - The key must be a non-empty string containing no instances of '\t' or '\n'
|
||||
* (this is a limitation of how the data is stored and will be addressed in
|
||||
* the future).
|
||||
* - The key must have a length no more than 256.
|
||||
* - The value must not contain '\n' and must have a length no more than 1024.
|
||||
* (the length limits are to prevent unbounded disk and memory usage)
|
||||
*/
|
||||
|
||||
class DataStorageManager final : public nsIDataStorageManager {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIDATASTORAGEMANAGER
|
||||
|
||||
private:
|
||||
~DataStorageManager() = default;
|
||||
|
||||
bool mAlternateServicesCreated = false;
|
||||
bool mClientAuthRememberListCreated = false;
|
||||
bool mSiteSecurityServiceStateCreated = false;
|
||||
};
|
||||
|
||||
class DataStorageItem final : public nsIDataStorageItem {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIDATASTORAGEITEM
|
||||
|
||||
DataStorageItem(const nsACString& aKey, const nsACString& aValue,
|
||||
nsIDataStorage::DataType aType)
|
||||
: key(aKey), value(aValue), type(aType) {}
|
||||
|
||||
private:
|
||||
~DataStorageItem() = default;
|
||||
|
||||
nsAutoCString key;
|
||||
nsAutoCString value;
|
||||
nsIDataStorage::DataType type;
|
||||
};
|
||||
|
||||
class DataStorage final : public nsIDataStorage,
|
||||
public nsIMemoryReporter,
|
||||
public nsIObserver {
|
||||
MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
|
||||
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIDATASTORAGE
|
||||
NS_DECL_NSIMEMORYREPORTER
|
||||
NS_DECL_NSIOBSERVER
|
||||
|
||||
explicit DataStorage(const nsString& aFilename);
|
||||
// Initializes the DataStorage. Must be called before using.
|
||||
nsresult Init();
|
||||
|
||||
private:
|
||||
~DataStorage() = default;
|
||||
|
||||
void ArmTimer(const MutexAutoLock& aProofOfLock);
|
||||
void ShutdownTimer();
|
||||
|
||||
class Writer;
|
||||
class Reader;
|
||||
|
||||
class Entry {
|
||||
public:
|
||||
Entry();
|
||||
bool UpdateScore();
|
||||
|
||||
uint32_t mScore;
|
||||
int32_t mLastAccessed; // the last accessed time in days since the epoch
|
||||
nsCString mValue;
|
||||
};
|
||||
|
||||
// Utility class for scanning tables for an entry to evict.
|
||||
class KeyAndEntry {
|
||||
public:
|
||||
nsCString mKey;
|
||||
Entry mEntry;
|
||||
};
|
||||
|
||||
typedef nsTHashMap<nsCStringHashKey, Entry> DataStorageTable;
|
||||
typedef nsRefPtrHashtable<nsStringHashKey, DataStorage> DataStorages;
|
||||
|
||||
void WaitForReady();
|
||||
nsresult AsyncWriteData(const MutexAutoLock& aProofOfLock);
|
||||
nsresult AsyncReadData(const MutexAutoLock& aProofOfLock);
|
||||
|
||||
static nsresult ValidateKeyAndValue(const nsACString& aKey,
|
||||
const nsACString& aValue);
|
||||
static void TimerCallback(nsITimer* aTimer, void* aClosure);
|
||||
void NotifyObservers(const char* aTopic);
|
||||
|
||||
bool GetInternal(const nsACString& aKey, Entry* aEntry,
|
||||
nsIDataStorage::DataType aType,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
nsresult PutInternal(const nsACString& aKey, Entry& aEntry,
|
||||
nsIDataStorage::DataType aType,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
void MaybeEvictOneEntry(nsIDataStorage::DataType aType,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
DataStorageTable& GetTableForType(nsIDataStorage::DataType aType,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
|
||||
void ReadAllFromTable(nsIDataStorage::DataType aType,
|
||||
nsTArray<RefPtr<nsIDataStorageItem>>& aItems,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
|
||||
Mutex mMutex; // This mutex protects access to the following members:
|
||||
DataStorageTable mPersistentDataTable MOZ_GUARDED_BY(mMutex);
|
||||
DataStorageTable mTemporaryDataTable MOZ_GUARDED_BY(mMutex);
|
||||
DataStorageTable mPrivateDataTable MOZ_GUARDED_BY(mMutex);
|
||||
nsCOMPtr<nsIFile> mBackingFile MOZ_GUARDED_BY(mMutex);
|
||||
bool mPendingWrite MOZ_GUARDED_BY(
|
||||
mMutex); // true if a write is needed but hasn't been dispatched
|
||||
bool mTimerArmed MOZ_GUARDED_BY(mMutex);
|
||||
bool mShuttingDown MOZ_GUARDED_BY(mMutex);
|
||||
RefPtr<TaskQueue> mBackgroundTaskQueue MOZ_GUARDED_BY(mMutex);
|
||||
// (End list of members protected by mMutex)
|
||||
|
||||
nsCOMPtr<nsITimer> mTimer;
|
||||
|
||||
mozilla::Atomic<bool> mInitCalled; // Indicates that Init() has been called.
|
||||
uint32_t mTimerDelayMS;
|
||||
|
||||
Monitor mReadyMonitor; // Do not acquire this at the same time as mMutex.
|
||||
bool mReady MOZ_GUARDED_BY(mReadyMonitor); // Indicates that saved data has
|
||||
// been read and Get can proceed.
|
||||
|
||||
const nsString mFilename;
|
||||
|
||||
static StaticAutoPtr<DataStorages> sDataStorages;
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_DataStorage_h
|
|
@ -1,68 +0,0 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "DataStorageManager.h"
|
||||
#include "MainThreadUtils.h"
|
||||
#include "nsIMemoryReporter.h"
|
||||
#include "nsString.h"
|
||||
|
||||
using VoidPtrToSizeFn = uintptr_t (*)(const void* ptr);
|
||||
|
||||
extern "C" nsresult make_data_storage(const nsAString* basename,
|
||||
size_t valueLength,
|
||||
VoidPtrToSizeFn sizeOfOp,
|
||||
VoidPtrToSizeFn enclosingSizeOfOp,
|
||||
nsIDataStorage** result);
|
||||
|
||||
MOZ_DEFINE_MALLOC_SIZE_OF(DataStorageMallocSizeOf)
|
||||
MOZ_DEFINE_MALLOC_ENCLOSING_SIZE_OF(DataStorageMallocEnclosingSizeOf)
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
NS_IMPL_ISUPPORTS(DataStorageManager, nsIDataStorageManager)
|
||||
|
||||
NS_IMETHODIMP
|
||||
DataStorageManager::Get(nsIDataStorageManager::DataStorage aDataStorage,
|
||||
nsIDataStorage** aResult) {
|
||||
if (!NS_IsMainThread()) {
|
||||
return NS_ERROR_NOT_SAME_THREAD;
|
||||
}
|
||||
nsAutoString filename;
|
||||
size_t valueLength = 1024;
|
||||
switch (aDataStorage) {
|
||||
case nsIDataStorageManager::AlternateServices:
|
||||
if (mAlternateServicesCreated) {
|
||||
return NS_ERROR_ALREADY_INITIALIZED;
|
||||
}
|
||||
mAlternateServicesCreated = true;
|
||||
filename.Assign(u"AlternateServices"_ns);
|
||||
break;
|
||||
case nsIDataStorageManager::ClientAuthRememberList:
|
||||
if (mClientAuthRememberListCreated) {
|
||||
return NS_ERROR_ALREADY_INITIALIZED;
|
||||
}
|
||||
mClientAuthRememberListCreated = true;
|
||||
filename.Assign(u"ClientAuthRememberList"_ns);
|
||||
break;
|
||||
case nsIDataStorageManager::SiteSecurityServiceState:
|
||||
if (mSiteSecurityServiceStateCreated) {
|
||||
return NS_ERROR_ALREADY_INITIALIZED;
|
||||
}
|
||||
mSiteSecurityServiceStateCreated = true;
|
||||
filename.Assign(u"SiteSecurityServiceState"_ns);
|
||||
// For most nsIDataStorage use cases, values can be quite long (1024
|
||||
// bytes by default). For HSTS, much less information is stored, so save
|
||||
// space by limiting values to 24 bytes.
|
||||
valueLength = 24;
|
||||
break;
|
||||
default:
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
return make_data_storage(&filename, valueLength, &DataStorageMallocSizeOf,
|
||||
&DataStorageMallocEnclosingSizeOf, aResult);
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
|
@ -1,29 +0,0 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_DataStorageManager_h
|
||||
#define mozilla_DataStorageManager_h
|
||||
|
||||
#include "nsIDataStorage.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
class DataStorageManager final : public nsIDataStorageManager {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIDATASTORAGEMANAGER
|
||||
|
||||
private:
|
||||
~DataStorageManager() = default;
|
||||
|
||||
bool mAlternateServicesCreated = false;
|
||||
bool mClientAuthRememberListCreated = false;
|
||||
bool mSiteSecurityServiceStateCreated = false;
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_DataStorageManager_h
|
|
@ -144,7 +144,7 @@ Classes = [
|
|||
'cid': '{71b49926-fd4e-43e2-ab8d-d9b049413c0b}',
|
||||
'contract_ids': ['@mozilla.org/security/datastoragemanager;1'],
|
||||
'type': 'mozilla::DataStorageManager',
|
||||
'headers': ['/security/manager/ssl/DataStorageManager.h'],
|
||||
'headers': ['/security/manager/ssl//DataStorage.h'],
|
||||
},
|
||||
{
|
||||
'cid': '{d7d2490d-2640-411b-9f09-a538803c11ee}',
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
[package]
|
||||
name = "data_storage"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1"
|
||||
cstr = "0.2"
|
||||
firefox-on-glean = { path = "../../../../toolkit/components/glean/api" }
|
||||
log = "0.4"
|
||||
malloc_size_of_derive = { path = "../../../../xpcom/rust/malloc_size_of_derive" }
|
||||
moz_task = { path = "../../../../xpcom/rust/moz_task" }
|
||||
nserror = { path = "../../../../xpcom/rust/nserror" }
|
||||
nsstring = { path = "../../../../xpcom/rust/nsstring" }
|
||||
thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
|
||||
wr_malloc_size_of = { path = "../../../../gfx/wr/wr_malloc_size_of" }
|
||||
xpcom = { path = "../../../../xpcom/rust/xpcom" }
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,47 +0,0 @@
|
|||
# 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/.
|
||||
|
||||
# Adding a new metric? We have docs for that!
|
||||
# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
|
||||
|
||||
---
|
||||
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
|
||||
$tags:
|
||||
- 'Core :: Security: PSM'
|
||||
|
||||
data_storage:
|
||||
entries:
|
||||
type: labeled_counter
|
||||
description:
|
||||
Counts the number of entries stored in each nsIDataStorage.
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1840135
|
||||
data_reviews:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1840135
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- dkeeler@mozilla.com
|
||||
expires: never
|
||||
labels:
|
||||
- AlternateServices
|
||||
- ClientAuthRememberList
|
||||
- SiteSecurityServiceState
|
||||
migration:
|
||||
type: labeled_boolean
|
||||
description:
|
||||
Indicates whether or not migration was successful for each nsIDataStorage.
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1840135
|
||||
data_reviews:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1840135
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- dkeeler@mozilla.com
|
||||
expires: 124
|
||||
labels:
|
||||
- AlternateServices
|
||||
- ClientAuthRememberList
|
||||
- SiteSecurityServiceState
|
|
@ -105,7 +105,7 @@ UNIFIED_SOURCES += [
|
|||
"CommonSocketControl.cpp",
|
||||
"ContentSignatureVerifier.cpp",
|
||||
"CryptoTask.cpp",
|
||||
"DataStorageManager.cpp",
|
||||
"DataStorage.cpp",
|
||||
"EnterpriseRoots.cpp",
|
||||
"IPCClientCertsChild.cpp",
|
||||
"IPCClientCertsParent.cpp",
|
||||
|
|
|
@ -11,10 +11,6 @@ interface nsIDataStorageItem;
|
|||
|
||||
[scriptable, uuid(71b49926-fd4e-43e2-ab8d-d9b049413c0b)]
|
||||
interface nsIDataStorageManager : nsISupports {
|
||||
// Because of its specialized nature, nsIDataStorage instances are limited to
|
||||
// the following pre-defined set. To add a new type of data storage, add an
|
||||
// entry to the enum and get review from someone on the security and privacy
|
||||
// engineering team.
|
||||
cenum DataStorage : 8 {
|
||||
AlternateServices,
|
||||
ClientAuthRememberList,
|
||||
|
@ -24,90 +20,39 @@ interface nsIDataStorageManager : nsISupports {
|
|||
nsIDataStorage get(in nsIDataStorageManager_DataStorage dataStorage);
|
||||
};
|
||||
|
||||
/**
|
||||
* nsIDataStorage is a threadsafe, generic, narrow string-based hash map that
|
||||
* persists data on disk and additionally handles private (temporary) data.
|
||||
* The file format is portable across architectures. If used in a context where
|
||||
* there is no profile directory, data will not be persisted.
|
||||
*
|
||||
* Its lifecycle is as follows:
|
||||
* - Use nsIDataStorageManager to obtain the nsIDataStorage of a particular
|
||||
* purpose. Its backing file will be read on a background thread.
|
||||
* - Should the profile directory not be available, (e.g. in xpcshell),
|
||||
* nsIDataStorage will not read any persistent data.
|
||||
* - When data in the nsIDataStorage changes, those changes will be written
|
||||
* to the backing file on a background thread. If the program crashes or is
|
||||
* closed unexpectedly before the write completes, the changes may be lost.
|
||||
* If the changes were an update to previously stored data, the original data
|
||||
* may be lost as well. A checksum associated with each entry helps identify
|
||||
* incompletely written entries.
|
||||
* - nsIDataStorage does not support transactions. Each entry is independent of
|
||||
* the others.
|
||||
* - When an nsIDataStorage instance observes the topic "profile-before-change"
|
||||
* in anticipation of shutdown, no more changes will be written to the
|
||||
* backing file. To ensure no data is lost, users of nsIDataStorage should
|
||||
* not attempt to change any data after this point.
|
||||
* If "profile-before-change" is not observed, this happens upon observing
|
||||
* "xpcom-shutdown-threads".
|
||||
* - To prevent unbounded memory and disk use, the number of entries in each
|
||||
* table is limited to 2048. Evictions are handled in by a modified LRU scheme
|
||||
* (see implementation comments).
|
||||
* - Note that instances of nsIDataStorage have long lifetimes because they are
|
||||
* strong observers of events and won't go away until the observer service
|
||||
* does.
|
||||
*
|
||||
* For each key/value:
|
||||
* - The key must have a length no more than 256.
|
||||
* - The value have a length no more than 1024 (24 for the site security
|
||||
* service state).
|
||||
* The length limits are to prevent unbounded disk and memory usage, and
|
||||
* nsIDataStorage will throw/return an error if given keys or values of
|
||||
* excess length.
|
||||
* Take care when storing data containing bytes that may be 0. When read
|
||||
* from disk, all trailing 0 bytes from keys and values are stripped.
|
||||
*/
|
||||
[scriptable, uuid(fcbb5ec4-7134-4069-91c6-9378eff51e03)]
|
||||
interface nsIDataStorage : nsISupports {
|
||||
/**
|
||||
* Data that is Persistent is saved on disk. Data that is Private is not
|
||||
* Data that is Persistent is saved on disk. Temporary and Private are not
|
||||
* saved. Private is meant to only be set and accessed from private contexts.
|
||||
* It will be cleared upon observing the event "last-pb-context-exited".
|
||||
*/
|
||||
cenum DataType : 8 {
|
||||
Persistent,
|
||||
Temporary,
|
||||
Private,
|
||||
};
|
||||
|
||||
// Given a key and a type of data, returns a value. Returns
|
||||
// NS_ERROR_NOT_AVAILABLE if the key is not present for that type of data.
|
||||
// This operation may block the current thread until the background task
|
||||
// reading the backing file from disk has completed.
|
||||
// NS_ERROR_NOT_AVAILABLE if the key is not present for that type of data. If
|
||||
// Get is called before the "data-storage-ready" event is observed, it will
|
||||
// block.
|
||||
ACString get(in ACString key, in nsIDataStorage_DataType type);
|
||||
|
||||
// Give a key, value, and type of data, adds an entry as appropriate.
|
||||
// Updates existing entries.
|
||||
// This operation may block the current thread until the background task
|
||||
// reading the backing file from disk has completed.
|
||||
void put(in ACString key, in ACString value, in nsIDataStorage_DataType type);
|
||||
|
||||
// Given a key and type of data, removes an entry if present.
|
||||
// This operation may block the current thread until the background task
|
||||
// reading the backing file from disk has completed.
|
||||
void remove(in ACString key, in nsIDataStorage_DataType type);
|
||||
|
||||
// Removes all entries of all types of data.
|
||||
// This operation may block the current thread until the background task
|
||||
// reading the backing file from disk has completed.
|
||||
void clear();
|
||||
|
||||
// Returns true if this data storage is ready to be used. To avoid blocking
|
||||
// when calling other nsIDataStorage functions, callers may wish to first
|
||||
// ensure this function returns true.
|
||||
// Returns true if this data storage is ready to be used.
|
||||
bool isReady();
|
||||
|
||||
// Read all of the data items.
|
||||
// This operation may block the current thread until the background task
|
||||
// reading the backing file from disk has completed.
|
||||
Array<nsIDataStorageItem> getAll();
|
||||
};
|
||||
|
||||
|
|
|
@ -40,9 +40,8 @@ const isDebugBuild = Cc["@mozilla.org/xpcom/debug;1"].getService(
|
|||
// The test EV roots are only enabled in debug builds as a security measure.
|
||||
const gEVExpected = isDebugBuild;
|
||||
|
||||
const CLIENT_AUTH_FILE_NAME = "ClientAuthRememberList.bin";
|
||||
const SSS_STATE_FILE_NAME = "SiteSecurityServiceState.bin";
|
||||
const SSS_STATE_OLD_FILE_NAME = "SiteSecurityServiceState.txt";
|
||||
const CLIENT_AUTH_FILE_NAME = "ClientAuthRememberList.txt";
|
||||
const SSS_STATE_FILE_NAME = "SiteSecurityServiceState.txt";
|
||||
const CERT_OVERRIDE_FILE_NAME = "cert_override.txt";
|
||||
|
||||
const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
|
||||
|
@ -1195,54 +1194,3 @@ function run_certutil_on_directory(directory, args, expectSuccess = true) {
|
|||
Assert.equal(process.exitValue, 0, "certutil should succeed");
|
||||
}
|
||||
}
|
||||
|
||||
function get_data_storage_contents(dataStorageFileName) {
|
||||
let stateFile = do_get_profile();
|
||||
stateFile.append(dataStorageFileName);
|
||||
ok(stateFile.exists());
|
||||
return readFile(stateFile);
|
||||
}
|
||||
|
||||
function u16_to_big_endian_bytes(u16) {
|
||||
Assert.less(u16, 65536);
|
||||
return [u16 / 256, u16 % 256];
|
||||
}
|
||||
|
||||
// Appends a line to the given data storage file (as an nsIOutputStream).
|
||||
// score is an integer representing the number of unique days the item has been accessed.
|
||||
// lastAccessed is the day since the epoch the item was last accessed.
|
||||
// key and value are strings representing the key and value of the item.
|
||||
function append_line_to_data_storage_file(
|
||||
outputStream,
|
||||
score,
|
||||
lastAccessed,
|
||||
key,
|
||||
value,
|
||||
valueLength = 24,
|
||||
useBadChecksum = false
|
||||
) {
|
||||
let line = arrayToString(u16_to_big_endian_bytes(score));
|
||||
line = line + arrayToString(u16_to_big_endian_bytes(lastAccessed));
|
||||
line = line + key;
|
||||
let keyPadding = [];
|
||||
for (let i = 0; i < 256 - key.length; i++) {
|
||||
keyPadding.push(0);
|
||||
}
|
||||
line = line + arrayToString(keyPadding);
|
||||
line = line + value;
|
||||
let valuePadding = [];
|
||||
for (let i = 0; i < valueLength - value.length; i++) {
|
||||
valuePadding.push(0);
|
||||
}
|
||||
line = line + arrayToString(valuePadding);
|
||||
let checksum = 0;
|
||||
Assert.equal(line.length % 2, 0);
|
||||
for (let i = 0; i < line.length; i += 2) {
|
||||
checksum ^= (line.charCodeAt(i) << 8) + line.charCodeAt(i + 1);
|
||||
}
|
||||
line =
|
||||
arrayToString(
|
||||
u16_to_big_endian_bytes(useBadChecksum ? ~checksum & 0xffff : checksum)
|
||||
) + line;
|
||||
outputStream.write(line, line.length);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
example.com,C9:65:33:89:EE:DC:4D:05:DA:16:3D:D0:12:61:BC:61:21:51:AF:2B:CC:C6:E1:72:B3:78:23:0F:13:B1:C7:4D, 0 19486 AAAA
|
||||
example.com,C9:65:33:89:EE:DC:4D:05:DA:16:3D:D0:12:61:BC:61:21:51:AF:2B:CC:C6:E1:72:B3:78:23:0F:13:B1:C7:4D,^partitionKey=%28https%2Cexample.com%29 0 19486 BBBB
|
||||
example.test,, 0 19486 CCCC
|
|
@ -7,30 +7,11 @@
|
|||
// state file.
|
||||
|
||||
function run_test() {
|
||||
let stateFile = do_get_profile();
|
||||
stateFile.append(CLIENT_AUTH_FILE_NAME);
|
||||
let outputStream = FileUtils.openFileOutputStream(stateFile);
|
||||
let keyValuePairs = [
|
||||
{
|
||||
key: "example.com,C9:65:33:89:EE:DC:4D:05:DA:16:3D:D0:12:61:BC:61:21:51:AF:2B:CC:C6:E1:72:B3:78:23:0F:13:B1:C7:4D,",
|
||||
value: "AAAA",
|
||||
},
|
||||
{
|
||||
key: "example.com,C9:65:33:89:EE:DC:4D:05:DA:16:3D:D0:12:61:BC:61:21:51:AF:2B:CC:C6:E1:72:B3:78:23:0F:13:B1:C7:4D,^partitionKey=%28https%2Cexample.com%29",
|
||||
value: "BBBB",
|
||||
},
|
||||
{ key: "example.test,,", value: "CCCC" },
|
||||
];
|
||||
for (let keyValuePair of keyValuePairs) {
|
||||
append_line_to_data_storage_file(
|
||||
outputStream,
|
||||
1,
|
||||
1,
|
||||
keyValuePair.key,
|
||||
keyValuePair.value,
|
||||
1024
|
||||
);
|
||||
}
|
||||
let profile = do_get_profile();
|
||||
let clientAuthRememberFile = do_get_file(
|
||||
`test_client_auth_remember_service/${CLIENT_AUTH_FILE_NAME}`
|
||||
);
|
||||
clientAuthRememberFile.copyTo(profile, CLIENT_AUTH_FILE_NAME);
|
||||
|
||||
let clientAuthRememberService = Cc[
|
||||
"@mozilla.org/security/clientAuthRememberService;1"
|
||||
|
|
|
@ -19,13 +19,23 @@ add_task(function test_data_storage() {
|
|||
Assert.equal(dataStorage.get("test", Ci.nsIDataStorage.Persistent), "value");
|
||||
|
||||
// Test that getting a value with the same key but of a different type throws.
|
||||
Assert.throws(
|
||||
() => dataStorage.get("test", Ci.nsIDataStorage.Temporary),
|
||||
/NS_ERROR_NOT_AVAILABLE/,
|
||||
"getting a value of a type that hasn't been set yet should throw"
|
||||
);
|
||||
Assert.throws(
|
||||
() => dataStorage.get("test", Ci.nsIDataStorage.Private),
|
||||
/NS_ERROR_NOT_AVAILABLE/,
|
||||
"getting a value of a type that hasn't been set yet should throw"
|
||||
);
|
||||
|
||||
// Put with Private data shouldn't affect Persistent data
|
||||
// Put with Temporary/Private data shouldn't affect Persistent data
|
||||
dataStorage.put("test", "temporary", Ci.nsIDataStorage.Temporary);
|
||||
Assert.equal(
|
||||
dataStorage.get("test", Ci.nsIDataStorage.Temporary),
|
||||
"temporary"
|
||||
);
|
||||
dataStorage.put("test", "private", Ci.nsIDataStorage.Private);
|
||||
Assert.equal(dataStorage.get("test", Ci.nsIDataStorage.Private), "private");
|
||||
Assert.equal(dataStorage.get("test", Ci.nsIDataStorage.Persistent), "value");
|
||||
|
@ -41,79 +51,23 @@ add_task(function test_data_storage() {
|
|||
/NS_ERROR_NOT_AVAILABLE/,
|
||||
"getting a removed value should throw"
|
||||
);
|
||||
// But removing one type shouldn't affect the other
|
||||
// But removing one type shouldn't affect the others
|
||||
Assert.equal(
|
||||
dataStorage.get("test", Ci.nsIDataStorage.Temporary),
|
||||
"temporary"
|
||||
);
|
||||
Assert.equal(dataStorage.get("test", Ci.nsIDataStorage.Private), "private");
|
||||
// Test removing the other type as well
|
||||
// Test removing the other types as well
|
||||
dataStorage.remove("test", Ci.nsIDataStorage.Temporary);
|
||||
dataStorage.remove("test", Ci.nsIDataStorage.Private);
|
||||
Assert.throws(
|
||||
() => dataStorage.get("test", Ci.nsIDataStorage.Temporary),
|
||||
/NS_ERROR_NOT_AVAILABLE/,
|
||||
"getting a removed value should throw"
|
||||
);
|
||||
Assert.throws(
|
||||
() => dataStorage.get("test", Ci.nsIDataStorage.Private),
|
||||
/NS_ERROR_NOT_AVAILABLE/,
|
||||
"getting a removed value should throw"
|
||||
);
|
||||
|
||||
// Saturate the storage tables (there is a maximum of 2048 entries for each
|
||||
// type of data).
|
||||
for (let i = 0; i < 2048; i++) {
|
||||
let padded = i.toString().padStart(4, "0");
|
||||
dataStorage.put(
|
||||
`key${padded}`,
|
||||
`value${padded}`,
|
||||
Ci.nsIDataStorage.Persistent
|
||||
);
|
||||
dataStorage.put(
|
||||
`key${padded}`,
|
||||
`value${padded}`,
|
||||
Ci.nsIDataStorage.Private
|
||||
);
|
||||
}
|
||||
// Ensure the data can be read back.
|
||||
for (let i = 0; i < 2048; i++) {
|
||||
let padded = i.toString().padStart(4, "0");
|
||||
let val = dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Persistent);
|
||||
Assert.equal(val, `value${padded}`);
|
||||
val = dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Private);
|
||||
Assert.equal(val, `value${padded}`);
|
||||
}
|
||||
// Remove each entry.
|
||||
for (let i = 0; i < 2048; i++) {
|
||||
let padded = i.toString().padStart(4, "0");
|
||||
dataStorage.remove(`key${padded}`, Ci.nsIDataStorage.Persistent);
|
||||
dataStorage.remove(`key${padded}`, Ci.nsIDataStorage.Private);
|
||||
}
|
||||
// Ensure the entries are not present.
|
||||
for (let i = 0; i < 2048; i++) {
|
||||
let padded = i.toString().padStart(4, "0");
|
||||
Assert.throws(
|
||||
() => dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Persistent),
|
||||
/NS_ERROR_NOT_AVAILABLE/,
|
||||
"getting a removed value should throw"
|
||||
);
|
||||
Assert.throws(
|
||||
() => dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Private),
|
||||
/NS_ERROR_NOT_AVAILABLE/,
|
||||
"getting a removed value should throw"
|
||||
);
|
||||
}
|
||||
// Add new entries.
|
||||
for (let i = 0; i < 2048; i++) {
|
||||
let padded = i.toString().padStart(5, "1");
|
||||
dataStorage.put(
|
||||
`key${padded}`,
|
||||
`value${padded}`,
|
||||
Ci.nsIDataStorage.Persistent
|
||||
);
|
||||
dataStorage.put(
|
||||
`key${padded}`,
|
||||
`value${padded}`,
|
||||
Ci.nsIDataStorage.Private
|
||||
);
|
||||
}
|
||||
// Ensure each new entry was added.
|
||||
for (let i = 0; i < 2048; i++) {
|
||||
let padded = i.toString().padStart(5, "1");
|
||||
let val = dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Persistent);
|
||||
Assert.equal(val, `value${padded}`);
|
||||
val = dataStorage.get(`key${padded}`, Ci.nsIDataStorage.Private);
|
||||
Assert.equal(val, `value${padded}`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,37 +5,83 @@
|
|||
|
||||
// The purpose of this test is to check that a frequently visited site
|
||||
// will not be evicted over an infrequently visited site.
|
||||
|
||||
var gSSService = null;
|
||||
var gProfileDir = null;
|
||||
|
||||
function do_state_written(aSubject, aTopic, aData) {
|
||||
if (aData == CLIENT_AUTH_FILE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
equal(aData, SSS_STATE_FILE_NAME);
|
||||
|
||||
let stateFile = gProfileDir.clone();
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
ok(stateFile.exists());
|
||||
let stateFileContents = readFile(stateFile);
|
||||
// the last part is removed because it's the empty string after the final \n
|
||||
let lines = stateFileContents.split("\n").slice(0, -1);
|
||||
// We can receive multiple data-storage-written events. In particular, we
|
||||
// may receive one where DataStorage wrote out data before we were done
|
||||
// processing all of our headers. In this case, the data may not be
|
||||
// as we expect. We only care about the final one being correct, however,
|
||||
// so we return and wait for the next event if things aren't as we expect.
|
||||
// There should be 1024 entries.
|
||||
if (lines.length != 1024) {
|
||||
return;
|
||||
}
|
||||
|
||||
let foundLegitSite = false;
|
||||
for (let line of lines) {
|
||||
if (line.startsWith("frequentlyused.example.com")) {
|
||||
foundLegitSite = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ok(foundLegitSite);
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
function do_state_read(aSubject, aTopic, aData) {
|
||||
if (aData == CLIENT_AUTH_FILE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
equal(aData, SSS_STATE_FILE_NAME);
|
||||
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://frequentlyused.example.com")
|
||||
)
|
||||
);
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
let uri = Services.io.newURI("http://bad" + i + ".example.com");
|
||||
gSSService.processHeader(uri, "max-age=1000");
|
||||
}
|
||||
do_test_pending();
|
||||
Services.obs.addObserver(do_state_written, "data-storage-written");
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
let stateFile = do_get_profile();
|
||||
Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
|
||||
gProfileDir = do_get_profile();
|
||||
let stateFile = gProfileDir.clone();
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
// Assuming we're working with a clean slate, the file shouldn't exist
|
||||
// until we create it.
|
||||
ok(!stateFile.exists());
|
||||
let outputStream = FileUtils.openFileOutputStream(stateFile);
|
||||
let now = new Date().getTime();
|
||||
let key = "frequentlyused.example.com";
|
||||
let value = `${now + 100000},1,0`;
|
||||
append_line_to_data_storage_file(outputStream, 4, 1000, key, value);
|
||||
let line = "frequentlyused.example.com\t4\t0\t" + (now + 100000) + ",1,0\n";
|
||||
outputStream.write(line, line.length);
|
||||
outputStream.close();
|
||||
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Services.obs.addObserver(do_state_read, "data-storage-ready");
|
||||
do_test_pending();
|
||||
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Ci.nsISiteSecurityService
|
||||
);
|
||||
notEqual(siteSecurityService, null);
|
||||
// isSecureURI blocks until the backing data is read.
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://frequentlyused.example.com")
|
||||
)
|
||||
);
|
||||
// The storage limit is currently 2048, so this should cause evictions.
|
||||
for (let i = 0; i < 3000; i++) {
|
||||
let uri = Services.io.newURI("http://bad" + i + ".example.com");
|
||||
siteSecurityService.processHeader(uri, "max-age=1000");
|
||||
}
|
||||
// The frequently used entry should not be evicted.
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://frequentlyused.example.com")
|
||||
)
|
||||
);
|
||||
notEqual(gSSService, null);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
function run_test() {
|
||||
let profileDir = do_get_profile();
|
||||
let stateFile = profileDir.clone();
|
||||
stateFile.append(SSS_STATE_OLD_FILE_NAME);
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
// Assuming we're working with a clean slate, the file shouldn't exist
|
||||
// until we create it.
|
||||
ok(!stateFile.exists());
|
||||
|
|
|
@ -6,136 +6,118 @@
|
|||
// The purpose of this test is to create a site security service state file
|
||||
// and see that the site security service reads it properly.
|
||||
|
||||
var gSSService = null;
|
||||
|
||||
function checkStateRead(aSubject, aTopic, aData) {
|
||||
if (aData == CLIENT_AUTH_FILE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
equal(aData, SSS_STATE_FILE_NAME);
|
||||
|
||||
ok(
|
||||
!gSSService.isSecureURI(Services.io.newURI("https://expired.example.com"))
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(Services.io.newURI("https://notexpired.example.com"))
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://sub.includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://incsubdomain.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://sub.incsubdomain.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains2.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://sub.includesubdomains2.preloaded.test")
|
||||
)
|
||||
);
|
||||
|
||||
// Clearing the data should make everything go back to default.
|
||||
gSSService.clearAll();
|
||||
ok(
|
||||
!gSSService.isSecureURI(Services.io.newURI("https://expired.example.com"))
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://notexpired.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://sub.includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://incsubdomain.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://sub.incsubdomain.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains2.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://sub.includesubdomains2.preloaded.test")
|
||||
)
|
||||
);
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
let stateFile = do_get_profile();
|
||||
let profileDir = do_get_profile();
|
||||
let stateFile = profileDir.clone();
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
// Assuming we're working with a clean slate, the file shouldn't exist
|
||||
// until we create it.
|
||||
ok(!stateFile.exists());
|
||||
let outputStream = FileUtils.openFileOutputStream(stateFile);
|
||||
let now = Date.now();
|
||||
let keyValuePairs = [
|
||||
{ key: "expired.example.com", value: `${now - 100000},1,0` },
|
||||
{ key: "notexpired.example.com", value: `${now + 100000},1,0` },
|
||||
let lines = [
|
||||
`expired.example.com\t0\t0\t${now - 100000},1,0`,
|
||||
`notexpired.example.com\t0\t0\t${now + 100000},1,0`,
|
||||
// This overrides an entry on the preload list.
|
||||
{ key: "includesubdomains.preloaded.test", value: `${now + 100000},1,0` },
|
||||
{ key: "incsubdomain.example.com", value: `${now + 100000},1,1` },
|
||||
`includesubdomains.preloaded.test\t0\t0\t${now + 100000},1,0`,
|
||||
`incsubdomain.example.com\t0\t0\t${now + 100000},1,1`,
|
||||
// This overrides an entry on the preload list.
|
||||
{ key: "includesubdomains2.preloaded.test", value: "0,2,0" },
|
||||
"includesubdomains2.preloaded.test\t0\t0\t0,2,0",
|
||||
];
|
||||
for (let keyValuePair of keyValuePairs) {
|
||||
append_line_to_data_storage_file(
|
||||
outputStream,
|
||||
1,
|
||||
1,
|
||||
keyValuePair.key,
|
||||
keyValuePair.value
|
||||
);
|
||||
}
|
||||
// Append a line with a bad checksum.
|
||||
append_line_to_data_storage_file(
|
||||
outputStream,
|
||||
1,
|
||||
1,
|
||||
"badchecksum.example.com",
|
||||
`${now + 100000},1,0`,
|
||||
24,
|
||||
true
|
||||
);
|
||||
outputStream.close();
|
||||
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
writeLinesAndClose(lines, outputStream);
|
||||
Services.obs.addObserver(checkStateRead, "data-storage-ready");
|
||||
do_test_pending();
|
||||
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Ci.nsISiteSecurityService
|
||||
);
|
||||
notEqual(siteSecurityService, null);
|
||||
|
||||
// The backing data storage will block until the background task that reads
|
||||
// the backing file has finished.
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://expired.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://notexpired.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://sub.includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://incsubdomain.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://sub.incsubdomain.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains2.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://sub.includesubdomains2.preloaded.test")
|
||||
)
|
||||
);
|
||||
|
||||
// Clearing the data should make everything go back to default.
|
||||
siteSecurityService.clearAll();
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://expired.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://notexpired.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://sub.includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://incsubdomain.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://sub.incsubdomain.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains2.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://sub.includesubdomains2.preloaded.test")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://badchecksum.example.com")
|
||||
)
|
||||
);
|
||||
notEqual(gSSService, null);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,30 @@
|
|||
// The purpose of this test is to create an empty site security service state
|
||||
// file and see that the site security service doesn't fail when reading it.
|
||||
|
||||
var gSSService = null;
|
||||
|
||||
function checkStateRead(aSubject, aTopic, aData) {
|
||||
// nonexistent.example.com should never be an HSTS host
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://nonexistent.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
// notexpired.example.com is an HSTS host in a different test - we
|
||||
// want to make sure that test hasn't interfered with this one.
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://notexpired.example.com")
|
||||
)
|
||||
);
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
let profileDir = do_get_profile();
|
||||
let stateFile = profileDir.clone();
|
||||
|
@ -17,27 +41,10 @@ function run_test() {
|
|||
ok(stateFile.exists());
|
||||
// Initialize nsISiteSecurityService after do_get_profile() so it
|
||||
// can read the state file.
|
||||
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Services.obs.addObserver(checkStateRead, "data-storage-ready");
|
||||
do_test_pending();
|
||||
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Ci.nsISiteSecurityService
|
||||
);
|
||||
notEqual(siteSecurityService, null);
|
||||
// nsISiteSecurityService.isSecureURI blocks until the backing file has been read.
|
||||
// nonexistent.example.com should never be an HSTS host
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://nonexistent.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://includesubdomains.preloaded.test")
|
||||
)
|
||||
);
|
||||
// notexpired.example.com is an HSTS host in a different test - we
|
||||
// want to make sure that test hasn't interfered with this one.
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://notexpired.example.com")
|
||||
)
|
||||
);
|
||||
notEqual(gSSService, null);
|
||||
}
|
||||
|
|
|
@ -3,14 +3,57 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
// The purpose of this test is to create a mostly bogus old site security
|
||||
// service state file and see that the site security service migrates it
|
||||
// to the new format properly, discarding invalid data.
|
||||
// The purpose of this test is to create a mostly bogus site security service
|
||||
// state file and see that the site security service handles it properly.
|
||||
|
||||
var gSSService = null;
|
||||
|
||||
function checkStateRead(aSubject, aTopic, aData) {
|
||||
if (aData == CLIENT_AUTH_FILE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
equal(aData, SSS_STATE_FILE_NAME);
|
||||
|
||||
const HSTS_HOSTS = [
|
||||
"https://example1.example.com",
|
||||
"https://example2.example.com",
|
||||
];
|
||||
for (let host of HSTS_HOSTS) {
|
||||
ok(
|
||||
gSSService.isSecureURI(Services.io.newURI(host)),
|
||||
`${host} should be HSTS enabled`
|
||||
);
|
||||
}
|
||||
|
||||
const NOT_HSTS_HOSTS = [
|
||||
"https://example.com",
|
||||
"https://example3.example.com",
|
||||
"https://extra.comma.example.com",
|
||||
"https://empty.statestring.example.com",
|
||||
"https://rubbish.statestring.example.com",
|
||||
"https://spaces.statestring.example.com",
|
||||
"https://invalid.expirytime.example.com",
|
||||
"https://text.securitypropertystate.example.com",
|
||||
"https://invalid.securitypropertystate.example.com",
|
||||
"https://text.includesubdomains.example.com",
|
||||
"https://invalid.includesubdomains.example.com",
|
||||
];
|
||||
for (let host of NOT_HSTS_HOSTS) {
|
||||
ok(
|
||||
!gSSService.isSecureURI(Services.io.newURI(host)),
|
||||
`${host} should not be HSTS enabled`
|
||||
);
|
||||
}
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
Services.prefs.setBoolPref("security.cert_pinning.hpkp.enabled", true);
|
||||
let profileDir = do_get_profile();
|
||||
let stateFile = profileDir.clone();
|
||||
stateFile.append(SSS_STATE_OLD_FILE_NAME);
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
// Assuming we're working with a clean slate, the file shouldn't exist
|
||||
// until we create it.
|
||||
ok(!stateFile.exists());
|
||||
|
@ -38,40 +81,15 @@ function run_test() {
|
|||
`invalid.includesubdomains.example.com\t0\t0\t${expiryTime},1,0foo`,
|
||||
];
|
||||
writeLinesAndClose(lines, outputStream);
|
||||
|
||||
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Services.obs.addObserver(checkStateRead, "data-storage-ready");
|
||||
do_test_pending();
|
||||
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Ci.nsISiteSecurityService
|
||||
);
|
||||
notEqual(siteSecurityService, null);
|
||||
notEqual(gSSService, null);
|
||||
|
||||
const HSTS_HOSTS = [
|
||||
"https://example1.example.com",
|
||||
"https://example2.example.com",
|
||||
];
|
||||
for (let host of HSTS_HOSTS) {
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(Services.io.newURI(host)),
|
||||
`${host} should be HSTS enabled`
|
||||
);
|
||||
}
|
||||
|
||||
const NOT_HSTS_HOSTS = [
|
||||
"https://example.com",
|
||||
"https://example3.example.com",
|
||||
"https://extra.comma.example.com",
|
||||
"https://empty.statestring.example.com",
|
||||
"https://rubbish.statestring.example.com",
|
||||
"https://spaces.statestring.example.com",
|
||||
"https://invalid.expirytime.example.com",
|
||||
"https://text.securitypropertystate.example.com",
|
||||
"https://invalid.securitypropertystate.example.com",
|
||||
"https://text.includesubdomains.example.com",
|
||||
"https://invalid.includesubdomains.example.com",
|
||||
];
|
||||
for (let host of NOT_HSTS_HOSTS) {
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(Services.io.newURI(host)),
|
||||
`${host} should not be HSTS enabled`
|
||||
);
|
||||
}
|
||||
Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 2);
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("security.cert_pinning.enforcement_level");
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,14 +3,57 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
// The purpose of this test is to create an old site security service state
|
||||
// file that is too large and see that the site security service migrates it to
|
||||
// the new format properly.
|
||||
// The purpose of this test is to create a site security service state file
|
||||
// that is too large and see that the site security service reads it properly
|
||||
// (this means discarding all entries after the 1024th).
|
||||
|
||||
var gSSService = null;
|
||||
|
||||
function checkStateRead(aSubject, aTopic, aData) {
|
||||
if (aData == CLIENT_AUTH_FILE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
equal(aData, SSS_STATE_FILE_NAME);
|
||||
|
||||
ok(
|
||||
gSSService.isSecureURI(Services.io.newURI("https://example0.example.com"))
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(Services.io.newURI("https://example423.example.com"))
|
||||
);
|
||||
ok(
|
||||
gSSService.isSecureURI(
|
||||
Services.io.newURI("https://example1023.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://example1024.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://example1025.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://example9000.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!gSSService.isSecureURI(
|
||||
Services.io.newURI("https://example99999.example.com")
|
||||
)
|
||||
);
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
let profileDir = do_get_profile();
|
||||
let stateFile = profileDir.clone();
|
||||
stateFile.append(SSS_STATE_OLD_FILE_NAME);
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
// Assuming we're working with a clean slate, the file shouldn't exist
|
||||
// until we create it.
|
||||
ok(!stateFile.exists());
|
||||
|
@ -24,49 +67,14 @@ function run_test() {
|
|||
`example${i}.example.com\t` +
|
||||
"0000000000000000000000000000000000000000000000000\t" +
|
||||
"00000000000000000000000000000000000000\t" +
|
||||
`${expiryTime},1,0`
|
||||
`${expiryTime},1,0000000000000000000000000000000000000000000000000000000000000000000000000`
|
||||
);
|
||||
}
|
||||
writeLinesAndClose(lines, outputStream);
|
||||
|
||||
let siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Services.obs.addObserver(checkStateRead, "data-storage-ready");
|
||||
do_test_pending();
|
||||
gSSService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Ci.nsISiteSecurityService
|
||||
);
|
||||
notEqual(siteSecurityService, null);
|
||||
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://example0.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://example423.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://example1023.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://example1024.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://example1025.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://example9000.example.com")
|
||||
)
|
||||
);
|
||||
ok(
|
||||
!siteSecurityService.isSecureURI(
|
||||
Services.io.newURI("https://example99999.example.com")
|
||||
)
|
||||
);
|
||||
notEqual(gSSService, null);
|
||||
}
|
||||
|
|
|
@ -22,17 +22,30 @@ Cc["@mozilla.org/serviceworkers/manager;1"].getService(
|
|||
Ci.nsIServiceWorkerManager
|
||||
);
|
||||
|
||||
function getStateFileContents() {
|
||||
let stateFile = do_get_profile();
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
ok(stateFile.exists());
|
||||
return readFile(stateFile);
|
||||
}
|
||||
|
||||
add_task(async function run_test() {
|
||||
Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
|
||||
do_get_profile();
|
||||
let SSService = Cc["@mozilla.org/ssservice;1"].getService(
|
||||
Ci.nsISiteSecurityService
|
||||
);
|
||||
let header = "max-age=50000";
|
||||
SSService.processHeader(Services.io.newURI("http://example.com"), header);
|
||||
await TestUtils.waitForCondition(() => {
|
||||
let stateFileContents = get_data_storage_contents(SSS_STATE_FILE_NAME);
|
||||
return stateFileContents.includes("example.com");
|
||||
});
|
||||
await TestUtils.topicObserved(
|
||||
"data-storage-written",
|
||||
(_, data) => data == SSS_STATE_FILE_NAME
|
||||
);
|
||||
let stateFileContents = getStateFileContents();
|
||||
ok(
|
||||
stateFileContents.includes("example.com"),
|
||||
"should have written out state file"
|
||||
);
|
||||
|
||||
// Configure Firefox to clear this data on shutdown.
|
||||
Services.prefs.setBoolPref(
|
||||
|
@ -49,9 +62,5 @@ add_task(async function run_test() {
|
|||
Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN
|
||||
);
|
||||
|
||||
await TestUtils.waitForCondition(() => {
|
||||
let stateFile = do_get_profile();
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
return !stateFile.exists();
|
||||
});
|
||||
equal(getStateFileContents(), "", "state file should be empty");
|
||||
});
|
||||
|
|
|
@ -6,56 +6,92 @@
|
|||
// The purpose of this test is to see that the site security service properly
|
||||
// writes its state file.
|
||||
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
|
||||
});
|
||||
|
||||
const EXPECTED_ENTRIES = 5;
|
||||
const EXPECTED_HSTS_COLUMNS = 3;
|
||||
|
||||
function contents_is_as_expected() {
|
||||
// The file consists of a series of [score][last accessed][key][value], where
|
||||
// score and last accessed are 2 bytes big-endian, key is 0-padded to 256
|
||||
// bytes, and value is 0-padded to 24 bytes.
|
||||
// Each score will be 1, and last accessed is some number of days (>255)
|
||||
// since the epoch, so there will be 3 non-0 bytes just in front of the key.
|
||||
// Splitting by 0 and filtering out zero-length strings will result in a series of
|
||||
// [BBBkey1, value1, BBBkey2, value2, ...], where "BBB" are the score and
|
||||
// last accessed bytes, which are ignored here.
|
||||
let contents = get_data_storage_contents(SSS_STATE_FILE_NAME);
|
||||
let keysAndValues = contents.split("\0").filter(s => !!s.length);
|
||||
let keys = keysAndValues
|
||||
.filter((_, i) => i % 2 == 0)
|
||||
.map(key => key.substring(3));
|
||||
let values = keysAndValues.filter((_, i) => i % 2 == 1);
|
||||
var gProfileDir = null;
|
||||
var gExpectingWrites = true;
|
||||
|
||||
if (keys.length != EXPECTED_ENTRIES || values.length != EXPECTED_ENTRIES) {
|
||||
return false;
|
||||
// For reference, the format of the state file is a list of:
|
||||
// <domain name> <expiration time in milliseconds>,<sts status>,<includeSubdomains>
|
||||
// separated by newlines ('\n')
|
||||
|
||||
function checkStateWritten(aSubject, aTopic, aData) {
|
||||
if (aData == CLIENT_AUTH_FILE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
equal(aData, SSS_STATE_FILE_NAME);
|
||||
ok(gExpectingWrites);
|
||||
|
||||
let stateFile = gProfileDir.clone();
|
||||
stateFile.append(SSS_STATE_FILE_NAME);
|
||||
ok(stateFile.exists());
|
||||
let stateFileContents = readFile(stateFile);
|
||||
// the last line is removed because it's just a trailing newline
|
||||
let lines = stateFileContents.split("\n").slice(0, -1);
|
||||
equal(lines.length, EXPECTED_ENTRIES);
|
||||
let sites = {}; // a map of domain name -> [the entry in the state file]
|
||||
for (let i in keys) {
|
||||
let host = keys[i];
|
||||
let entry = values[i].split(",");
|
||||
equal(entry.length, EXPECTED_HSTS_COLUMNS);
|
||||
for (let line of lines) {
|
||||
let parts = line.split("\t");
|
||||
let host = parts[0];
|
||||
let entry = parts[3].split(",");
|
||||
let expectedColumns = EXPECTED_HSTS_COLUMNS;
|
||||
equal(entry.length, expectedColumns);
|
||||
sites[host] = entry;
|
||||
}
|
||||
|
||||
// While we're still processing headers, multiple writes of the backing data
|
||||
// can be scheduled, and thus we can receive multiple data-storage-written
|
||||
// notifications. In these cases, the data may not be as we expect. We only
|
||||
// care about the final one being correct, however, so we return and wait for
|
||||
// the next event if things aren't as we expect.
|
||||
// each sites[url][1] should be SecurityPropertySet (i.e. 1).
|
||||
// sites[url][2] corresponds to includeSubdomains, so every other one should
|
||||
// be set (i.e. 1);
|
||||
return (
|
||||
sites["includesubdomains.preloaded.test"][1] == 1 &&
|
||||
sites["includesubdomains.preloaded.test"][2] == 0 &&
|
||||
sites["a.example.com"][1] == 1 &&
|
||||
sites["a.example.com"][2] == 1 &&
|
||||
sites["b.example.com"][1] == 1 &&
|
||||
sites["b.example.com"][2] == 0 &&
|
||||
sites["c.c.example.com"][1] == 1 &&
|
||||
sites["c.c.example.com"][2] == 1 &&
|
||||
sites["d.example.com"][1] == 1 &&
|
||||
sites["d.example.com"][2] == 0
|
||||
);
|
||||
if (sites["includesubdomains.preloaded.test"][1] != 1) {
|
||||
return;
|
||||
}
|
||||
if (sites["includesubdomains.preloaded.test"][2] != 0) {
|
||||
return;
|
||||
}
|
||||
if (sites["a.example.com"][1] != 1) {
|
||||
return;
|
||||
}
|
||||
if (sites["a.example.com"][2] != 1) {
|
||||
return;
|
||||
}
|
||||
if (sites["b.example.com"][1] != 1) {
|
||||
return;
|
||||
}
|
||||
if (sites["b.example.com"][2] != 0) {
|
||||
return;
|
||||
}
|
||||
if (sites["c.c.example.com"][1] != 1) {
|
||||
return;
|
||||
}
|
||||
if (sites["c.c.example.com"][2] != 1) {
|
||||
return;
|
||||
}
|
||||
if (sites["d.example.com"][1] != 1) {
|
||||
return;
|
||||
}
|
||||
if (sites["d.example.com"][2] != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here, the file was as expected and we no longer expect any
|
||||
// data-storage-written notifications.
|
||||
gExpectingWrites = false;
|
||||
|
||||
// Process the headers again to test that seeing them again after such a
|
||||
// short delay doesn't cause another write.
|
||||
process_headers();
|
||||
|
||||
// Wait a bit before finishing the test, to see if another write happens.
|
||||
do_timeout(2000, function () {
|
||||
do_test_finished();
|
||||
});
|
||||
}
|
||||
|
||||
function process_headers() {
|
||||
|
@ -82,7 +118,9 @@ function process_headers() {
|
|||
}
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
|
||||
gProfileDir = do_get_profile();
|
||||
process_headers();
|
||||
TestUtils.waitForCondition(contents_is_as_expected);
|
||||
do_test_pending();
|
||||
Services.obs.addObserver(checkStateWritten, "data-storage-written");
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ support-files =
|
|||
test_cert_utf8/**
|
||||
test_cert_version/**
|
||||
test_certDB_import/**
|
||||
test_client_auth_remember_service/**
|
||||
test_content_signing/**
|
||||
test_crlite_filters/**
|
||||
test_crlite_preexisting/**
|
||||
|
|
|
@ -23,7 +23,6 @@ gecko_metrics = [
|
|||
"mobile/android/actors/metrics.yaml",
|
||||
"netwerk/metrics.yaml",
|
||||
"netwerk/protocol/http/metrics.yaml",
|
||||
"security/manager/ssl/metrics.yaml",
|
||||
"toolkit/components/cookiebanners/metrics.yaml",
|
||||
"toolkit/components/extensions/metrics.yaml",
|
||||
"toolkit/components/formautofill/metrics.yaml",
|
||||
|
|
|
@ -14227,6 +14227,15 @@
|
|||
"releaseChannelCollection": "opt-out",
|
||||
"description": "The submission status when main/plugin/content crashes are submitted. 1 is success, 0 is failure. Keyed on the CrashManager Crash.type."
|
||||
},
|
||||
"DATA_STORAGE_ENTRIES": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"products": ["firefox", "fennec"],
|
||||
"expires_in_version": "default",
|
||||
"kind": "linear",
|
||||
"high": 1024,
|
||||
"n_buckets": 16,
|
||||
"description": "The number of entries in persistent DataStorage (HSTS and HPKP data, basically)"
|
||||
},
|
||||
"VIDEO_PLAY_TIME_MS": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"products": ["firefox"],
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"CHECK_ADDONS_MODIFIED_MS",
|
||||
"COMPONENTS_SHIM_ACCESSED_BY_CONTENT",
|
||||
"CRASH_STORE_COMPRESSED_BYTES",
|
||||
"DATA_STORAGE_ENTRIES",
|
||||
"DEFECTIVE_PERMISSIONS_SQL_REMOVED",
|
||||
"DEFERRED_FINALIZE_ASYNC",
|
||||
"DENIED_TRANSLATION_OFFERS",
|
||||
|
@ -401,6 +402,7 @@
|
|||
"CYCLE_COLLECTOR_WORKER_VISITED_GCED",
|
||||
"CYCLE_COLLECTOR_WORKER_VISITED_REF_COUNTED",
|
||||
"D3D11_SYNC_HANDLE_FAILURE",
|
||||
"DATA_STORAGE_ENTRIES",
|
||||
"DEFECTIVE_PERMISSIONS_SQL_REMOVED",
|
||||
"DEFERRED_FINALIZE_ASYNC",
|
||||
"DENIED_TRANSLATION_OFFERS",
|
||||
|
@ -940,6 +942,7 @@
|
|||
"FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
|
||||
"FX_SESSION_RESTORE_COLLECT_DATA_MS",
|
||||
"FX_SESSION_RESTORE_FILE_SIZE_BYTES",
|
||||
"DATA_STORAGE_ENTRIES",
|
||||
"TRANSLATED_PAGES_BY_LANGUAGE",
|
||||
"LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
|
||||
"FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED",
|
||||
|
|
|
@ -39,7 +39,6 @@ jsrust_shared = { path = "../../../../js/src/rust/shared" }
|
|||
cascade_bloom_filter = { path = "../../../components/cascade_bloom_filter" }
|
||||
cert_storage = { path = "../../../../security/manager/ssl/cert_storage" }
|
||||
crypto_hash = { path = "../../../../security/manager/ssl/crypto_hash" }
|
||||
data_storage = { path = "../../../../security/manager/ssl/data_storage" }
|
||||
bitsdownload = { path = "../../../components/bitsdownload", optional = true }
|
||||
storage = { path = "../../../../storage/rust" }
|
||||
bookmark_sync = { path = "../../../components/places/bookmark_sync", optional = true }
|
||||
|
|
|
@ -23,7 +23,6 @@ extern crate cosec;
|
|||
extern crate cubeb_coreaudio;
|
||||
#[cfg(feature = "cubeb_pulse_rust")]
|
||||
extern crate cubeb_pulse;
|
||||
extern crate data_storage;
|
||||
extern crate encoding_glue;
|
||||
extern crate fog_control;
|
||||
extern crate gecko_profiler;
|
||||
|
|
|
@ -16,7 +16,6 @@ clippy:
|
|||
- modules/libpref/init/static_prefs/
|
||||
- mozglue/static/rust/
|
||||
- netwerk/base/mozurl/
|
||||
- security/manager/ssl/data_storage/
|
||||
- servo/components/derive_common/
|
||||
- servo/components/selectors/
|
||||
- servo/components/servo_arc/
|
||||
|
|
Загрузка…
Ссылка в новой задаче