Merge inbound to m-c a=merge

This commit is contained in:
Wes Kocher 2014-09-05 19:04:52 -07:00
commit ca62a34614
62 changed files with 2466 additions and 554 deletions

View File

@ -390,6 +390,11 @@ Sanitizer.prototype = {
for each (var host in hosts) {
pwmgr.setLoginSavingEnabled(host, true);
}
// Clear site security settings
var sss = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
sss.clearAll();
},
get canClear()

View File

@ -134,6 +134,11 @@ Sanitizer.prototype = {
for each (var host in hosts) {
pwmgr.setLoginSavingEnabled(host, true);
}
// Clear site security settings
var sss = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
sss.clearAll();
},
get canClear()

View File

@ -377,9 +377,11 @@ Navigator::GetAppName(nsAString& aAppName)
*
* An empty array will be returned if there is no valid languages.
*/
void
/* static */ void
Navigator::GetAcceptLanguages(nsTArray<nsString>& aLanguages)
{
MOZ_ASSERT(NS_IsMainThread());
// E.g. "de-de, en-us,en".
const nsAdoptingString& acceptLang =
Preferences::GetLocalizedString("intl.accept_languages");
@ -438,7 +440,7 @@ Navigator::GetLanguage(nsAString& aLanguage)
aLanguage.Truncate();
}
return NS_OK;
return NS_OK;
}
void

View File

@ -267,7 +267,8 @@ public:
void GetOwnPropertyNames(JSContext* aCx, nsTArray<nsString>& aNames,
ErrorResult& aRv);
void GetLanguages(nsTArray<nsString>& aLanguages);
void GetAcceptLanguages(nsTArray<nsString>& aLanguages);
static void GetAcceptLanguages(nsTArray<nsString>& aLanguages);
// WebIDL helper methods
static bool HasWakeLockSupport(JSContext* /* unused*/, JSObject* /*unused */);

View File

@ -18,6 +18,7 @@
#include "nsCOMPtr.h"
#include "nsISupportsPrimitives.h"
#include "nsReadableUtils.h"
#include "nsDOMJSUtils.h"
#include "nsJSUtils.h"
#include "nsIDocShell.h"
#include "nsIDocShellTreeItem.h"
@ -564,7 +565,13 @@ NS_ScriptErrorReporter(JSContext *cx,
::JS_ClearPendingException(cx);
MOZ_ASSERT(cx == nsContentUtils::GetCurrentJSContext());
nsCOMPtr<nsIGlobalObject> globalObject = GetEntryGlobal();
nsCOMPtr<nsIGlobalObject> globalObject;
if (nsIScriptContext* scx = GetScriptContextFromJSContext(cx)) {
nsCOMPtr<nsPIDOMWindow> outer = do_QueryInterface(scx->GetGlobalObject());
if (outer) {
globalObject = static_cast<nsGlobalWindow*>(outer->GetCurrentInnerWindow());
}
}
if (globalObject) {
nsCOMPtr<nsPIDOMWindow> win = do_QueryInterface(globalObject);

View File

@ -50,7 +50,7 @@ interface NavigatorID {
boolean taintEnabled(); // constant false
};
[NoInterfaceObject]
[NoInterfaceObject, Exposed=(Window,Worker)]
interface NavigatorLanguage {
readonly attribute DOMString? language;
[Pure, Cached, Frozen]

View File

@ -8,5 +8,6 @@ interface WorkerNavigator {
};
WorkerNavigator implements NavigatorID;
WorkerNavigator implements NavigatorLanguage;
WorkerNavigator implements NavigatorOnLine;
WorkerNavigator implements NavigatorDataStore;

View File

@ -38,7 +38,7 @@ WorkerNavigator::Create(bool aOnLine)
nsRefPtr<WorkerNavigator> navigator =
new WorkerNavigator(properties.mAppName, properties.mAppVersion,
properties.mPlatform, properties.mUserAgent,
aOnLine);
properties.mLanguages, aOnLine);
return navigator.forget();
}
@ -278,4 +278,11 @@ WorkerNavigator::GetDataStores(JSContext* aCx,
return promise.forget();
}
void
WorkerNavigator::SetLanguages(const nsTArray<nsString>& aLanguages)
{
WorkerNavigatorBinding_workers::ClearCachedLanguagesValue(this);
mLanguages = aLanguages;
}
END_WORKERS_NAMESPACE

View File

@ -28,17 +28,20 @@ class WorkerNavigator MOZ_FINAL : public nsWrapperCache
nsString mAppVersion;
nsString mPlatform;
nsString mUserAgent;
nsTArray<nsString> mLanguages;
bool mOnline;
WorkerNavigator(const nsAString& aAppName,
const nsAString& aAppVersion,
const nsAString& aPlatform,
const nsAString& aUserAgent,
const nsTArray<nsString>& aLanguages,
bool aOnline)
: mAppName(aAppName)
, mAppVersion(aAppVersion)
, mPlatform(aPlatform)
, mUserAgent(aUserAgent)
, mLanguages(aLanguages)
, mOnline(aOnline)
{
MOZ_COUNT_CTOR(WorkerNavigator);
@ -92,6 +95,20 @@ public:
return false;
}
void GetLanguage(nsString& aLanguage) const
{
if (mLanguages.Length() >= 1) {
aLanguage.Assign(mLanguages[0]);
} else {
aLanguage.Truncate();
}
}
void GetLanguages(nsTArray<nsString>& aLanguages) const
{
aLanguages = mLanguages;
}
void GetUserAgent(nsString& aUserAgent) const
{
aUserAgent = mUserAgent;
@ -108,6 +125,8 @@ public:
mOnline = aOnline;
}
void SetLanguages(const nsTArray<nsString>& aLanguages);
already_AddRefed<Promise> GetDataStores(JSContext* aCx,
const nsAString& aName,
const nsAString& aOwner,

View File

@ -156,6 +156,7 @@ static_assert(MAX_WORKERS_PER_DOMAIN >= 1,
#define PREF_DOM_FETCH_ENABLED "dom.fetch.enabled"
#define PREF_WORKERS_LATEST_JS_VERSION "dom.workers.latestJSVersion"
#define PREF_INTL_ACCEPT_LANGUAGES "intl.accept_languages"
namespace {
@ -1025,6 +1026,20 @@ private:
}
};
void
PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */)
{
AssertIsOnMainThread();
nsTArray<nsString> languages;
Navigator::GetAcceptLanguages(languages);
RuntimeService* runtime = RuntimeService::GetService();
if (runtime) {
runtime->UpdateAllWorkerLanguages(languages);
}
}
} /* anonymous namespace */
class RuntimeService::WorkerThread MOZ_FINAL : public nsThread
@ -1456,6 +1471,7 @@ RuntimeService::RegisterWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
return false;
}
Navigator::GetAcceptLanguages(mNavigatorProperties.mLanguages);
mNavigatorPropertiesLoaded = true;
}
@ -1795,6 +1811,9 @@ RuntimeService::Init()
LoadRuntimeOptions,
PREF_WORKERS_OPTIONS_PREFIX,
nullptr)) ||
NS_FAILED(Preferences::RegisterCallbackAndCall(PrefLanguagesChanged,
PREF_INTL_ACCEPT_LANGUAGES,
nullptr)) ||
NS_FAILED(Preferences::RegisterCallbackAndCall(
JSVersionChanged,
PREF_WORKERS_LATEST_JS_VERSION,
@ -1941,6 +1960,9 @@ RuntimeService::Cleanup()
if (NS_FAILED(Preferences::UnregisterCallback(JSVersionChanged,
PREF_WORKERS_LATEST_JS_VERSION,
nullptr)) ||
NS_FAILED(Preferences::UnregisterCallback(PrefLanguagesChanged,
PREF_INTL_ACCEPT_LANGUAGES,
nullptr)) ||
NS_FAILED(Preferences::UnregisterCallback(LoadRuntimeOptions,
PREF_JS_OPTIONS_PREFIX,
nullptr)) ||
@ -2407,6 +2429,15 @@ RuntimeService::UpdateAllWorkerPreference(WorkerPreference aPref, bool aValue)
BROADCAST_ALL_WORKERS(UpdatePreference, aPref, aValue);
}
void
RuntimeService::UpdateAllWorkerLanguages(const nsTArray<nsString>& aLanguages)
{
MOZ_ASSERT(NS_IsMainThread());
mNavigatorProperties.mLanguages = aLanguages;
BROADCAST_ALL_WORKERS(UpdateLanguages, aLanguages);
}
void
RuntimeService::UpdateAllWorkerMemoryParameter(JSGCParamKey aKey,
uint32_t aValue)

View File

@ -108,6 +108,7 @@ public:
nsString mAppVersion;
nsString mPlatform;
nsString mUserAgent;
nsTArray<nsString> mLanguages;
};
private:
@ -202,6 +203,9 @@ public:
void
UpdateAllWorkerRuntimeOptions();
void
UpdateAllWorkerLanguages(const nsTArray<nsString>& aLanguages);
void
UpdateAllWorkerPreference(WorkerPreference aPref, bool aValue);

View File

@ -1564,6 +1564,25 @@ public:
}
};
class UpdateLanguagesRunnable MOZ_FINAL : public WorkerRunnable
{
nsTArray<nsString> mLanguages;
public:
UpdateLanguagesRunnable(WorkerPrivate* aWorkerPrivate,
const nsTArray<nsString>& aLanguages)
: WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount),
mLanguages(aLanguages)
{ }
virtual bool
WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) MOZ_OVERRIDE
{
aWorkerPrivate->UpdateLanguagesInternal(aCx, mLanguages);
return true;
}
};
class UpdateJSWorkerMemoryParameterRunnable MOZ_FINAL :
public WorkerControlRunnable
{
@ -2891,6 +2910,21 @@ WorkerPrivateParent<Derived>::UpdatePreference(JSContext* aCx, WorkerPreference
}
}
template <class Derived>
void
WorkerPrivateParent<Derived>::UpdateLanguages(JSContext* aCx,
const nsTArray<nsString>& aLanguages)
{
AssertIsOnParentThread();
nsRefPtr<UpdateLanguagesRunnable> runnable =
new UpdateLanguagesRunnable(ParentAsWorkerPrivate(), aLanguages);
if (!runnable->Dispatch(aCx)) {
NS_WARNING("Failed to update worker languages!");
JS_ClearPendingException(aCx);
}
}
template <class Derived>
void
WorkerPrivateParent<Derived>::UpdateJSWorkerMemoryParameter(JSContext* aCx,
@ -5527,6 +5561,23 @@ WorkerPrivate::UpdateRuntimeOptionsInternal(
}
}
void
WorkerPrivate::UpdateLanguagesInternal(JSContext* aCx,
const nsTArray<nsString>& aLanguages)
{
WorkerGlobalScope* globalScope = GlobalScope();
if (globalScope) {
nsRefPtr<WorkerNavigator> nav = globalScope->GetExistingNavigator();
if (nav) {
nav->SetLanguages(aLanguages);
}
}
for (uint32_t index = 0; index < mChildWorkers.Length(); index++) {
mChildWorkers[index]->UpdateLanguages(aCx, aLanguages);
}
}
void
WorkerPrivate::UpdatePreferenceInternal(JSContext* aCx, WorkerPreference aPref, bool aValue)
{

View File

@ -397,6 +397,9 @@ public:
UpdateRuntimeOptions(JSContext* aCx,
const JS::RuntimeOptions& aRuntimeOptions);
void
UpdateLanguages(JSContext* aCx, const nsTArray<nsString>& aLanguages);
void
UpdatePreference(JSContext* aCx, WorkerPreference aPref, bool aValue);
@ -935,6 +938,9 @@ public:
void
UpdateRuntimeOptionsInternal(JSContext* aCx, const JS::RuntimeOptions& aRuntimeOptions);
void
UpdateLanguagesInternal(JSContext* aCx, const nsTArray<nsString>& aLanguages);
void
UpdatePreferenceInternal(JSContext* aCx, WorkerPreference aPref, bool aValue);

View File

@ -36,6 +36,7 @@ support-files =
longThread_worker.js
multi_sharedWorker_frame.html
multi_sharedWorker_sharedWorker.js
navigator_languages_worker.js
navigator_worker.js
newError_worker.js
onLine_worker.js
@ -126,6 +127,7 @@ skip-if = (toolkit == 'gonk' && debug) #debug-only failure
[test_multi_sharedWorker.html]
[test_multi_sharedWorker_lifetimes.html]
[test_navigator.html]
[test_navigator_languages.html]
skip-if = buildapp == 'mulet'
[test_newError.html]
[test_onLine.html]

View File

@ -0,0 +1,11 @@
var active = true;
onmessage = function(e) {
if (e.data == 'finish') {
active = false;
return;
}
if (active) {
postMessage(navigator.languages);
}
}

View File

@ -13,7 +13,9 @@ var supportedProps = [
"product",
"taintEnabled",
"userAgent",
"onLine"
"onLine",
"language",
"languages",
];
var isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);

View File

@ -46,6 +46,11 @@ Tests of DOM Worker Navigator
return;
}
if (args.name === "languages") {
is(navigator.languages.toString(), args.value.toString(), "languages matches");
return;
}
is(navigator[args.name], args.value,
"Mismatched navigator string for " + args.name + "!");
};

View File

@ -0,0 +1,53 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<!DOCTYPE HTML>
<html>
<!--
Tests of DOM Worker Navigator
-->
<head>
<title>Test for DOM Worker Navigator.languages</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
<script class="testbody" language="javascript">
var tests = [ 'en,it', 'it,en,fr', '', 'en' ];
var expectedLanguages;
function runTests() {
if (!tests.length) {
worker.postMessage('finish');
SimpleTest.finish();
return;
}
expectedLanguages = tests.shift();
SpecialPowers.pushPrefEnv({"set": [["intl.accept_languages", expectedLanguages]]}, function() {
worker.postMessage(true);
});
}
SimpleTest.waitForExplicitFinish();
var worker = new Worker("navigator_languages_worker.js");
worker.onmessage = function(event) {
is(event.data.toString(), navigator.languages.toString(), "The languages mach.");
is(event.data.toString(), expectedLanguages, "This is the correct result.");
runTests();
}
runTests();
</script>
</pre>
</body>
</html>

View File

@ -56,12 +56,15 @@ js::CreateRegExpMatchResult(JSContext *cx, HandleString input, const MatchPairs
if (pair.isUndefined()) {
JS_ASSERT(i != 0); /* Since we had a match, first pair must be present. */
arr->setDenseInitializedLength(i + 1);
arr->initDenseElement(i, UndefinedValue());
arr->initDenseElementWithType(cx, i, UndefinedValue());
} else {
JSLinearString *str = NewDependentString(cx, input, pair.start, pair.length());
if (!str)
return false;
arr->setDenseInitializedLength(i + 1);
// We don't have to update type information here, since the match
// result template is already known to have string elements.
arr->initDenseElement(i, StringValue(str));
}
}

View File

@ -0,0 +1,9 @@
function f(str) {
for (var i=0; i<10; i++) {
arr = /foo(ba(r))?/.exec(str);
var x = arr[0] + " " + arr[1] + " " + arr[2];
}
}
f("foobar");
f("foo");

View File

@ -891,6 +891,10 @@ jit::FinishDiscardBaselineScript(FreeOp *fop, JSScript *script)
// Reset |active| flag so that we don't need a separate script
// iteration to unmark them.
script->baselineScript()->resetActive();
// The baseline caches have been wiped out, so the script will need to
// warm back up before it can be inlined during Ion compilation.
script->baselineScript()->clearIonCompiledOrInlined();
return;
}

View File

@ -151,7 +151,12 @@ struct BaselineScript
// Flag set when compiled for use for debug mode. Handles various
// Debugger hooks and compiles toggled calls for traps.
DEBUG_MODE = 1 << 3
DEBUG_MODE = 1 << 3,
// Flag set if this script has ever been Ion compiled, either directly
// or inlined into another script. This is cleared when the script's
// type information or caches are cleared.
ION_COMPILED_OR_INLINED = 1 << 4
};
private:
@ -229,6 +234,16 @@ struct BaselineScript
return flags_ & DEBUG_MODE;
}
void setIonCompiledOrInlined() {
flags_ |= ION_COMPILED_OR_INLINED;
}
void clearIonCompiledOrInlined() {
flags_ &= ~ION_COMPILED_OR_INLINED;
}
bool ionCompiledOrInlined() const {
return flags_ & ION_COMPILED_OR_INLINED;
}
uint32_t prologueOffset() const {
return prologueOffset_;
}

View File

@ -148,6 +148,9 @@ IonBuilder::IonBuilder(JSContext *analysisContext, CompileCompartment *comp,
JS_ASSERT(script()->hasBaselineScript() == (info->executionMode() != ArgumentsUsageAnalysis));
JS_ASSERT(!!analysisContext == (info->executionMode() == DefinitePropertiesAnalysis));
if (!info->executionModeIsAnalysis())
script()->baselineScript()->setIonCompiledOrInlined();
}
void
@ -4202,6 +4205,7 @@ IonBuilder::makeInliningDecision(JSFunction *target, CallInfo &callInfo)
// type information, except for definite properties analysis,
// as the caller has not run yet.
if (targetScript->getUseCount() < optimizationInfo().usesBeforeInlining() &&
!targetScript->baselineScript()->ionCompiledOrInlined() &&
info().executionMode() != DefinitePropertiesAnalysis)
{
return DontInline(targetScript, "Vetoed: callee is insufficiently hot.");

View File

@ -816,7 +816,7 @@ HandleExecutionInterrupt(JSContext *cx);
* break out of its loop. This happens if, for example, the user clicks "Stop
* script" on the slow script dialog; treat it as an uncatchable error.
*/
inline bool
MOZ_ALWAYS_INLINE bool
CheckForInterrupt(JSContext *cx)
{
MOZ_ASSERT(cx->runtime()->requestDepth >= 1);

View File

@ -708,6 +708,12 @@ RegExpCompartment::createMatchResultTemplateObject(JSContext *cx)
if (!templateObject)
return matchResultTemplateObject_; // = nullptr
// Create a new type for the template.
Rooted<TaggedProto> proto(cx, templateObject->getTaggedProto());
types::TypeObject *type =
cx->compartment()->types.newTypeObject(cx, templateObject->getClass(), proto);
templateObject->setType(type);
/* Set dummy index property */
RootedValue index(cx, Int32Value(0));
if (!baseops::DefineProperty(cx, templateObject, cx->names().index, index,
@ -727,6 +733,10 @@ RegExpCompartment::createMatchResultTemplateObject(JSContext *cx)
JS_ASSERT(shape->slot() == 1 &&
shape->propidRef() == NameToId(cx->names().input));
// Make sure type information reflects the indexed properties which might
// be added.
types::AddTypePropertyId(cx, templateObject, JSID_VOID, types::Type::StringType());
matchResultTemplateObject_.set(templateObject);
return matchResultTemplateObject_;

View File

@ -120,6 +120,11 @@ Sanitizer.prototype = {
Services.logins.setLoginSavingEnabled(host, true);
}
// Clear site security settings
var sss = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
sss.clearAll();
resolve();
});
},

View File

@ -8,7 +8,7 @@ interface nsIURI;
interface nsIObserver;
interface nsIHttpChannel;
[scriptable, uuid(b20a9242-5732-45bc-9fa0-a178154f2721)]
[scriptable, uuid(e7da4bd1-7c38-4d73-843d-c1d6af9b3c85)]
interface nsISiteSecurityService : nsISupports
{
const uint32_t HEADER_HSTS = 0;
@ -90,11 +90,12 @@ interface nsISiteSecurityService : nsISupports
*/
boolean isSecureURI(in uint32_t aType, in nsIURI aURI, in uint32_t aFlags);
/**
* Removes all security state by resetting to factory-original settings.
*/
void clearAll();
};
%{C++
#define NS_SSSERVICE_CONTRACTID "@mozilla.org/ssservice;1"
#define STS_PERMISSION "sts/use"
#define STS_SUBDOMAIN_PERMISSION "sts/subd"
%}

View File

@ -0,0 +1,793 @@
/* -*- 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/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/unused.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIObserverService.h"
#include "nsITimer.h"
#include "nsNetUtil.h"
#include "nsStreamUtils.h"
#include "nsThreadUtils.h"
#include "prprf.h"
// 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(DataStorage,
nsIObserver)
DataStorage::DataStorage(const nsString& aFilename)
: mMutex("DataStorage::mMutex")
, mPendingWrite(false)
, mShuttingDown(false)
, mReadyMonitor("DataStorage::mReadyMonitor")
, mReady(false)
, mFilename(aFilename)
{
}
DataStorage::~DataStorage()
{
}
nsresult
DataStorage::Init(bool& aDataWillPersist)
{
// Don't access the observer service or preferences off the main thread.
if (!NS_IsMainThread()) {
NS_NOTREACHED("DataStorage::Init called off main thread");
return NS_ERROR_NOT_SAME_THREAD;
}
MutexAutoLock lock(mMutex);
nsresult rv;
rv = NS_NewThread(getter_AddRefs(mWorkerThread));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = AsyncReadData(aDataWillPersist, 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.
os->AddObserver(this, "profile-before-change", false);
// For test purposes, we can set the write timer to be very fast.
mTimerDelay = Preferences::GetInt("test.datastorage.write_timer_ms",
sDataStorageDefaultTimerDelay);
rv = Preferences::AddStrongObserver(this, "test.datastorage.write_timer_ms");
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
class DataStorage::Reader : public nsRunnable
{
public:
explicit Reader(DataStorage* aDataStorage)
: mDataStorage(aDataStorage)
{
}
~Reader();
private:
NS_DECL_NSIRUNNABLE
static nsresult ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
Entry& aEntryOut);
nsRefPtr<DataStorage> mDataStorage;
};
DataStorage::Reader::~Reader()
{
// Notify that calls to Get can proceed.
{
MonitorAutoLock readyLock(mDataStorage->mReadyMonitor);
mDataStorage->mReady = true;
nsresult rv = mDataStorage->mReadyMonitor.NotifyAll();
unused << NS_WARN_IF(NS_FAILED(rv));
}
// This is for tests.
nsCOMPtr<nsIRunnable> job =
NS_NewRunnableMethodWithArg<const char*>(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_TARGET_DOES_NOT_EXIST && // on Unix
rv != NS_ERROR_FILE_NOT_FOUND)) { // on Windows
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.
Entry newerEntry;
bool present = mDataStorage->mPersistentDataTable.Get(key, &newerEntry);
if (!present) {
mDataStorage->mPersistentDataTable.Put(key, entry);
}
}
} while (true);
}
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 nsCString& aKey, const nsCString& 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(bool& aHaveProfileDir,
const MutexAutoLock& /*aProofOfLock*/)
{
aHaveProfileDir = false;
// 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).
nsRefPtr<Reader> 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 = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
aHaveProfileDir = true;
return NS_OK;
}
void
DataStorage::WaitForReady()
{
MonitorAutoLock readyLock(mReadyMonitor);
while (!mReady) {
nsresult rv = readyLock.Wait();
if (NS_WARN_IF(NS_FAILED(rv))) {
break;
}
}
MOZ_ASSERT(mReady);
}
nsCString
DataStorage::Get(const nsCString& aKey, DataStorageType aType)
{
WaitForReady();
MutexAutoLock lock(mMutex);
Entry entry;
bool foundValue = GetInternal(aKey, &entry, aType, lock);
if (!foundValue) {
return EmptyCString();
}
// If we're here, we found a value. Maybe update its score.
if (entry.UpdateScore()) {
PutInternal(aKey, entry, aType, lock);
}
return entry.mValue;
}
bool
DataStorage::GetInternal(const nsCString& aKey, Entry* aEntry,
DataStorageType aType,
const MutexAutoLock& aProofOfLock)
{
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
bool foundValue = table.Get(aKey, aEntry);
return foundValue;
}
DataStorage::DataStorageTable&
DataStorage::GetTableForType(DataStorageType aType,
const MutexAutoLock& /*aProofOfLock*/)
{
switch (aType) {
case DataStorage_Persistent:
return mPersistentDataTable;
case DataStorage_Temporary:
return mTemporaryDataTable;
case DataStorage_Private:
return mPrivateDataTable;
}
MOZ_CRASH("given bad DataStorage storage type");
}
// NB: The lock must be held when calling this function.
/* static */
PLDHashOperator
DataStorage::EvictCallback(const nsACString& aKey, Entry aEntry, void* aArg)
{
KeyAndEntry* toEvict = (KeyAndEntry*)aArg;
if (aEntry.mScore < toEvict->mEntry.mScore) {
toEvict->mKey = aKey;
toEvict->mEntry = aEntry;
}
return PLDHashOperator::PL_DHASH_NEXT;
}
// 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(DataStorageType 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;
table.EnumerateRead(EvictCallback, (void*)&toEvict);
table.Remove(toEvict.mKey);
}
}
nsresult
DataStorage::Put(const nsCString& aKey, const nsCString& aValue,
DataStorageType 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;
rv = PutInternal(aKey, entry, aType, lock);
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
nsresult
DataStorage::PutInternal(const nsCString& aKey, Entry& aEntry,
DataStorageType aType,
const MutexAutoLock& aProofOfLock)
{
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
table.Put(aKey, aEntry);
if (aType == DataStorage_Persistent && !mPendingWrite) {
return AsyncSetTimer(aProofOfLock);
}
return NS_OK;
}
void
DataStorage::Remove(const nsCString& aKey, DataStorageType aType)
{
WaitForReady();
MutexAutoLock lock(mMutex);
DataStorageTable& table = GetTableForType(aType, lock);
table.Remove(aKey);
if (aType == DataStorage_Persistent && !mPendingWrite) {
unused << AsyncSetTimer(lock);
}
}
class DataStorage::Writer : public nsRunnable
{
public:
Writer(nsCString& aData, DataStorage* aDataStorage)
: mData(aData)
, mDataStorage(aDataStorage)
{
}
private:
NS_DECL_NSIRUNNABLE
nsCString mData;
nsRefPtr<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_NewLocalFileOutputStream(getter_AddRefs(outputStream), file,
PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
const char* ptr = mData.get();
int32_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;
}
// Observed by tests.
nsCOMPtr<nsIRunnable> job =
NS_NewRunnableMethodWithArg<const char*>(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;
}
// NB: The lock must be held when calling this function.
/* static */
PLDHashOperator
DataStorage::WriteDataCallback(const nsACString& aKey, Entry aEntry, void* aArg)
{
nsCString* output = (nsCString*)aArg;
output->Append(aKey);
output->Append('\t');
output->AppendInt(aEntry.mScore);
output->Append('\t');
output->AppendInt(aEntry.mLastAccessed);
output->Append('\t');
output->Append(aEntry.mValue);
output->Append('\n');
return PLDHashOperator::PL_DHASH_NEXT;
}
nsresult
DataStorage::AsyncWriteData(const MutexAutoLock& /*aProofOfLock*/)
{
if (mShuttingDown || !mBackingFile) {
return NS_OK;
}
nsCString output;
mPersistentDataTable.EnumerateRead(WriteDataCallback, (void*)&output);
nsRefPtr<Writer> job(new Writer(output, this));
nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
mPendingWrite = false;
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
DataStorage::Clear()
{
WaitForReady();
MutexAutoLock lock(mMutex);
mPersistentDataTable.Clear();
mTemporaryDataTable.Clear();
mPrivateDataTable.Clear();
// 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.
nsresult rv = AsyncWriteData(lock);
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
/* static */
void
DataStorage::TimerCallback(nsITimer* aTimer, void* aClosure)
{
nsRefPtr<DataStorage> aDataStorage = (DataStorage*)aClosure;
MutexAutoLock lock(aDataStorage->mMutex);
unused << aDataStorage->AsyncWriteData(lock);
}
// We only initialize the timer on the worker thread because it's not safe
// to mix what threads are operating on the timer.
nsresult
DataStorage::AsyncSetTimer(const MutexAutoLock& /*aProofOfLock*/)
{
if (mShuttingDown) {
return NS_OK;
}
mPendingWrite = true;
nsCOMPtr<nsIRunnable> job =
NS_NewRunnableMethod(this, &DataStorage::SetTimer);
nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
void
DataStorage::SetTimer()
{
MOZ_ASSERT(!NS_IsMainThread());
MutexAutoLock lock(mMutex);
nsresult rv;
if (!mTimer) {
mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
}
rv = mTimer->InitWithFuncCallback(TimerCallback, this,
mTimerDelay, nsITimer::TYPE_ONE_SHOT);
unused << NS_WARN_IF(NS_FAILED(rv));
}
void
DataStorage::NotifyObservers(const char* aTopic)
{
// Don't access the observer service off the main thread.
if (!NS_IsMainThread()) {
NS_NOTREACHED("DataStorage::NotifyObservers called off main thread");
return;
}
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
if (os) {
os->NotifyObservers(nullptr, aTopic, mFilename.get());
}
}
nsresult
DataStorage::DispatchShutdownTimer(const MutexAutoLock& /*aProofOfLock*/)
{
nsCOMPtr<nsIRunnable> job =
NS_NewRunnableMethod(this, &DataStorage::ShutdownTimer);
nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
void
DataStorage::ShutdownTimer()
{
MOZ_ASSERT(!NS_IsMainThread());
MutexAutoLock lock(mMutex);
nsresult rv = mTimer->Cancel();
unused << NS_WARN_IF(NS_FAILED(rv));
mTimer = nullptr;
}
//------------------------------------------------------------
// DataStorage::nsIObserver
//------------------------------------------------------------
NS_IMETHODIMP
DataStorage::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData)
{
// Don't access preferences off the main thread.
if (!NS_IsMainThread()) {
NS_NOTREACHED("DataStorage::Observe called off main thread");
return NS_ERROR_NOT_SAME_THREAD;
}
nsresult rv;
if (strcmp(aTopic, "last-pb-context-exited") == 0) {
MutexAutoLock lock(mMutex);
mPrivateDataTable.Clear();
} else if (strcmp(aTopic, "profile-before-change") == 0) {
{
MutexAutoLock lock(mMutex);
rv = AsyncWriteData(lock);
mShuttingDown = true;
unused << NS_WARN_IF(NS_FAILED(rv));
if (mTimer) {
rv = DispatchShutdownTimer(lock);
unused << NS_WARN_IF(NS_FAILED(rv));
}
}
// Run the thread to completion and prevent any further events
// being scheduled to it. The thread may need the lock, so we can't
// hold it here.
rv = mWorkerThread->Shutdown();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else if (strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) {
MutexAutoLock lock(mMutex);
mTimerDelay = Preferences::GetInt("test.datastorage.write_timer_ms",
sDataStorageDefaultTimerDelay);
}
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

View File

@ -0,0 +1,185 @@
/* -*- 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/Monitor.h"
#include "mozilla/Mutex.h"
#include "nsCOMPtr.h"
#include "nsDataHashtable.h"
#include "nsIObserver.h"
#include "nsIThread.h"
#include "nsITimer.h"
#include "nsString.h"
namespace mozilla {
/**
* 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.
* - When any persistent data changes, a timer is initialized that will
* eventually asynchronously write 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 cause the
* timer to be reinitialized and another "data-storage-written" event will
* be sent.
* - When DataStorage observes the topic "profile-before-change" in
* anticipation of shutdown, all persistent data is synchronously written to
* the backing file. The worker thread responsible for these writes is then
* disabled to prevent further writes to that file (the delayed-write timer
* is cancelled when this happens).
* - 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)
*/
/**
* Data that is DataStorage_Persistent is saved on disk. DataStorage_Temporary
* and DataStorage_Private are not saved. DataStorage_Private is meant to
* only be set and accessed from private contexts. It will be cleared upon
* observing the event "last-pb-context-exited".
*/
enum DataStorageType {
DataStorage_Persistent,
DataStorage_Temporary,
DataStorage_Private
};
class DataStorage : public nsIObserver
{
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIOBSERVER
// If there is a profile directory, there is or will eventually be a file
// by the name specified by aFilename there.
explicit DataStorage(const nsString& aFilename);
// Initializes the DataStorage. Must be called before using.
// aDataWillPersist returns whether or not data can be persistently saved.
nsresult Init(/*out*/bool& aDataWillPersist);
// Given a key and a type of data, returns a value. Returns an empty string 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. NB: It is not
// currently possible to differentiate between missing data and data that is
// the empty string.
nsCString Get(const nsCString& aKey, DataStorageType aType);
// Give a key, value, and type of data, adds an entry as appropriate.
// Updates existing entries.
nsresult Put(const nsCString& aKey, const nsCString& aValue,
DataStorageType aType);
// Given a key and type of data, removes an entry if present.
void Remove(const nsCString& aKey, DataStorageType aType);
// Removes all entries of all types of data.
nsresult Clear();
private:
virtual ~DataStorage();
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 nsDataHashtable<nsCStringHashKey, Entry> DataStorageTable;
void WaitForReady();
nsresult AsyncWriteData(const MutexAutoLock& aProofOfLock);
nsresult AsyncReadData(bool& aHaveProfileDir,
const MutexAutoLock& aProofOfLock);
nsresult AsyncSetTimer(const MutexAutoLock& aProofOfLock);
nsresult DispatchShutdownTimer(const MutexAutoLock& aProofOfLock);
static nsresult ValidateKeyAndValue(const nsCString& aKey,
const nsCString& aValue);
static void TimerCallback(nsITimer* aTimer, void* aClosure);
static PLDHashOperator WriteDataCallback(const nsACString& aKey, Entry aEntry,
void* aArg);
static PLDHashOperator EvictCallback(const nsACString& aKey, Entry aEntry,
void* aArg);
void SetTimer();
void ShutdownTimer();
void NotifyObservers(const char* aTopic);
bool GetInternal(const nsCString& aKey, Entry* aEntry, DataStorageType aType,
const MutexAutoLock& aProofOfLock);
nsresult PutInternal(const nsCString& aKey, Entry& aEntry,
DataStorageType aType,
const MutexAutoLock& aProofOfLock);
void MaybeEvictOneEntry(DataStorageType aType,
const MutexAutoLock& aProofOfLock);
DataStorageTable& GetTableForType(DataStorageType aType,
const MutexAutoLock& aProofOfLock);
Mutex mMutex; // This mutex protects access to the following members:
DataStorageTable mPersistentDataTable;
DataStorageTable mTemporaryDataTable;
DataStorageTable mPrivateDataTable;
nsCOMPtr<nsIThread> mWorkerThread;
nsCOMPtr<nsIFile> mBackingFile;
nsCOMPtr<nsITimer> mTimer; // All uses after init must be on the worker thread
uint32_t mTimerDelay; // in milliseconds
bool mPendingWrite; // true if a write is needed but hasn't been dispatched
bool mShuttingDown;
// (End list of members protected by mMutex)
Monitor mReadyMonitor; // Do not acquire this at the same time as mMutex.
bool mReady; // Indicates that saved data has been read and Get can proceed.
const nsString mFilename;
};
}; // namespace mozilla
#endif // mozilla_DataStorage_h

View File

@ -4,7 +4,12 @@
# 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/.
EXPORTS.mozilla += [
'DataStorage.h',
]
UNIFIED_SOURCES += [
'DataStorage.cpp',
'nsBOOTModule.cpp',
'nsEntropyCollector.cpp',
'nsSecurityHeaderParser.cpp',

View File

@ -2,40 +2,34 @@
* 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 "plstr.h"
#include "prlog.h"
#include "prprf.h"
#include "prnetdb.h"
#include "nsSiteSecurityService.h"
#include "mozilla/LinkedList.h"
#include "mozilla/Preferences.h"
#include "nsCRTGlue.h"
#include "nsIPermissionManager.h"
#include "nsISSLStatus.h"
#include "nsISSLStatusProvider.h"
#include "nsSiteSecurityService.h"
#include "nsISocketProvider.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsThreadUtils.h"
#include "nsString.h"
#include "nsIScriptSecurityManager.h"
#include "nsISocketProvider.h"
#include "mozilla/Preferences.h"
#include "mozilla/LinkedList.h"
#include "nsSecurityHeaderParser.h"
#include "nsString.h"
#include "nsThreadUtils.h"
#include "plstr.h"
#include "prlog.h"
#include "prnetdb.h"
#include "prprf.h"
#include "nsXULAppAPI.h"
// A note about the preload list:
// When a site specifically disables sts by sending a header with
// When a site specifically disables HSTS by sending a header with
// 'max-age: 0', we keep a "knockout" value that means "we have no information
// regarding the sts state of this host" (any ancestor of "this host" can still
// influence its sts status via include subdomains, however).
// regarding the HSTS state of this host" (any ancestor of "this host" can still
// influence its HSTS status via include subdomains, however).
// This prevents the preload list from overriding the site's current
// desired sts status. Knockout values are indicated by permission values of
// STS_KNOCKOUT.
// desired HSTS status.
#include "nsSTSPreloadList.inc"
#define STS_SET (nsIPermissionManager::ALLOW_ACTION)
#define STS_UNSET (nsIPermissionManager::UNKNOWN_ACTION)
#define STS_KNOCKOUT (nsIPermissionManager::DENY_ACTION)
#if defined(PR_LOGGING)
static PRLogModuleInfo *
GetSSSLog()
@ -51,29 +45,58 @@ GetSSSLog()
////////////////////////////////////////////////////////////////////////////////
nsSSSHostEntry::nsSSSHostEntry(const char* aHost)
: mHost(aHost)
, mExpireTime(0)
, mStsPermission(STS_UNSET)
, mExpired(false)
, mIncludeSubdomains(false)
SiteSecurityState::SiteSecurityState(nsCString& aStateString)
: mHSTSExpireTime(0)
, mHSTSState(SecurityPropertyUnset)
, mHSTSIncludeSubdomains(false)
{
uint32_t hstsState = 0;
uint32_t hstsIncludeSubdomains = 0; // PR_sscanf doesn't handle bools.
int32_t matches = PR_sscanf(aStateString.get(), "%lld,%lu,%lu",
&mHSTSExpireTime, &hstsState,
&hstsIncludeSubdomains);
bool valid = (matches == 3 &&
(hstsIncludeSubdomains == 0 || hstsIncludeSubdomains == 1) &&
((SecurityPropertyState)hstsState == SecurityPropertyUnset ||
(SecurityPropertyState)hstsState == SecurityPropertySet ||
(SecurityPropertyState)hstsState == SecurityPropertyKnockout));
if (valid) {
mHSTSState = (SecurityPropertyState)hstsState;
mHSTSIncludeSubdomains = (hstsIncludeSubdomains == 1);
} else {
SSSLOG(("%s is not a valid SiteSecurityState", aStateString.get()));
mHSTSExpireTime = 0;
mHSTSState = SecurityPropertyUnset;
mHSTSIncludeSubdomains = false;
}
}
SiteSecurityState::SiteSecurityState(PRTime aHSTSExpireTime,
SecurityPropertyState aHSTSState,
bool aHSTSIncludeSubdomains)
: mHSTSExpireTime(aHSTSExpireTime)
, mHSTSState(aHSTSState)
, mHSTSIncludeSubdomains(aHSTSIncludeSubdomains)
{
}
nsSSSHostEntry::nsSSSHostEntry(const nsSSSHostEntry& toCopy)
: mHost(toCopy.mHost)
, mExpireTime(toCopy.mExpireTime)
, mStsPermission(toCopy.mStsPermission)
, mExpired(toCopy.mExpired)
, mIncludeSubdomains(toCopy.mIncludeSubdomains)
void
SiteSecurityState::ToString(nsCString& aString)
{
aString.Truncate();
aString.AppendInt(mHSTSExpireTime);
aString.Append(',');
aString.AppendInt(mHSTSState);
aString.Append(',');
aString.AppendInt(static_cast<uint32_t>(mHSTSIncludeSubdomains));
}
////////////////////////////////////////////////////////////////////////////////
nsSiteSecurityService::nsSiteSecurityService()
: mUsePreloadList(true)
, mPreloadListTimeOffset(0)
{
}
@ -93,18 +116,35 @@ nsSiteSecurityService::Init()
MOZ_CRASH("Child process: no direct access to nsSiteSecurityService");
}
nsresult rv;
// Don't access Preferences off the main thread.
if (!NS_IsMainThread()) {
NS_NOTREACHED("nsSiteSecurityService initialized off main thread");
return NS_ERROR_NOT_SAME_THREAD;
}
mPermMgr = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
mUsePreloadList = mozilla::Preferences::GetBool(
"network.stricttransportsecurity.preloadlist", true);
mozilla::Preferences::AddStrongObserver(this,
"network.stricttransportsecurity.preloadlist");
mPreloadListTimeOffset = mozilla::Preferences::GetInt(
"test.currentTimeOffsetSeconds", 0);
mozilla::Preferences::AddStrongObserver(this,
"test.currentTimeOffsetSeconds");
mSiteStateStorage =
new mozilla::DataStorage(NS_LITERAL_STRING("SiteSecurityServiceState.txt"));
bool storageWillPersist = false;
nsresult rv = mSiteStateStorage->Init(storageWillPersist);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// This is not fatal. There are some cases where there won't be a
// profile directory (e.g. running xpcshell). There isn't the
// expectation that site information will be presisted in those cases.
if (!storageWillPersist) {
NS_WARNING("site security information will not be persisted");
}
mUsePreloadList = mozilla::Preferences::GetBool("network.stricttransportsecurity.preloadlist", true);
mozilla::Preferences::AddStrongObserver(this, "network.stricttransportsecurity.preloadlist");
mObserverService = mozilla::services::GetObserverService();
if (mObserverService)
mObserverService->AddObserver(this, "last-pb-context-exited", false);
return NS_OK;
return NS_OK;
}
nsresult
@ -121,29 +161,6 @@ nsSiteSecurityService::GetHost(nsIURI *aURI, nsACString &aResult)
return NS_OK;
}
nsresult
nsSiteSecurityService::GetPrincipalForURI(nsIURI* aURI,
nsIPrincipal** aPrincipal)
{
nsresult rv;
nsCOMPtr<nsIScriptSecurityManager> securityManager =
do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
// We have to normalize the scheme of the URIs we're using, so just use https.
// HSTS information is shared across all ports for a given host.
nsAutoCString host;
rv = GetHost(aURI, host);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> uri;
rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://") + host);
NS_ENSURE_SUCCESS(rv, rv);
// We want all apps to share HSTS state, so this is one of the few places
// where we do not silo persistent state by extended origin.
return securityManager->GetNoAppCodebasePrincipal(uri, aPrincipal);
}
nsresult
nsSiteSecurityService::SetState(uint32_t aType,
nsIURI* aSourceURI,
@ -152,7 +169,7 @@ nsSiteSecurityService::SetState(uint32_t aType,
uint32_t flags)
{
// If max-age is zero, that's an indication to immediately remove the
// permissions, so here's a shortcut.
// security state, so here's a shortcut.
if (!maxage) {
return RemoveState(aType, aSourceURI, flags);
}
@ -162,46 +179,28 @@ nsSiteSecurityService::SetState(uint32_t aType,
int64_t expiretime = (PR_Now() / PR_USEC_PER_MSEC) +
(maxage * PR_MSEC_PER_SEC);
SiteSecurityState siteState(expiretime, SecurityPropertySet,
includeSubdomains);
nsAutoCString stateString;
siteState.ToString(stateString);
nsAutoCString hostname;
nsresult rv = GetHost(aSourceURI, hostname);
NS_ENSURE_SUCCESS(rv, rv);
SSSLOG(("SSS: setting state for %s", hostname.get()));
bool isPrivate = flags & nsISocketProvider::NO_PERMANENT_STORAGE;
// record entry for this host with max-age in the permissions manager
SSSLOG(("SSS: maxage permission SET, adding permission\n"));
nsresult rv = AddPermission(aSourceURI,
STS_PERMISSION,
(uint32_t) STS_SET,
(uint32_t) nsIPermissionManager::EXPIRE_TIME,
expiretime,
isPrivate);
mozilla::DataStorageType storageType = isPrivate
? mozilla::DataStorage_Private
: mozilla::DataStorage_Persistent;
rv = mSiteStateStorage->Put(hostname, stateString, storageType);
NS_ENSURE_SUCCESS(rv, rv);
if (includeSubdomains) {
// record entry for this host with include subdomains in the permissions manager
SSSLOG(("SSS: subdomains permission SET, adding permission\n"));
rv = AddPermission(aSourceURI,
STS_SUBDOMAIN_PERMISSION,
(uint32_t) STS_SET,
(uint32_t) nsIPermissionManager::EXPIRE_TIME,
expiretime,
isPrivate);
NS_ENSURE_SUCCESS(rv, rv);
} else { // !includeSubdomains
nsAutoCString hostname;
rv = GetHost(aSourceURI, hostname);
NS_ENSURE_SUCCESS(rv, rv);
SSSLOG(("SSS: subdomains permission UNSET, removing any existing ones\n"));
rv = RemovePermission(hostname, STS_SUBDOMAIN_PERMISSION, isPrivate);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
NS_IMETHODIMP
nsSiteSecurityService::RemoveState(uint32_t aType, nsIURI* aURI, uint32_t aFlags)
nsSiteSecurityService::RemoveState(uint32_t aType, nsIURI* aURI,
uint32_t aFlags)
{
// Should be called on the main thread (or via proxy) since the permission
// manager is used and it's not threadsafe.
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
// Only HSTS is supported at the moment.
NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS,
NS_ERROR_NOT_IMPLEMENTED);
@ -211,14 +210,21 @@ nsSiteSecurityService::RemoveState(uint32_t aType, nsIURI* aURI, uint32_t aFlags
NS_ENSURE_SUCCESS(rv, rv);
bool isPrivate = aFlags & nsISocketProvider::NO_PERMANENT_STORAGE;
rv = RemovePermission(hostname, STS_PERMISSION, isPrivate);
NS_ENSURE_SUCCESS(rv, rv);
SSSLOG(("SSS: deleted maxage permission\n"));
rv = RemovePermission(hostname, STS_SUBDOMAIN_PERMISSION, isPrivate);
NS_ENSURE_SUCCESS(rv, rv);
SSSLOG(("SSS: deleted subdomains permission\n"));
mozilla::DataStorageType storageType = isPrivate
? mozilla::DataStorage_Private
: mozilla::DataStorage_Persistent;
// If this host is in the preload list, we have to store a knockout entry.
if (GetPreloadListEntry(hostname.get())) {
SSSLOG(("SSS: storing knockout entry for %s", hostname.get()));
SiteSecurityState siteState(0, SecurityPropertyKnockout, false);
nsAutoCString stateString;
siteState.ToString(stateString);
rv = mSiteStateStorage->Put(hostname, stateString, storageType);
NS_ENSURE_SUCCESS(rv, rv);
} else {
SSSLOG(("SSS: removing entry for %s", hostname.get()));
mSiteStateStorage->Remove(hostname, storageType);
}
return NS_OK;
}
@ -238,9 +244,6 @@ nsSiteSecurityService::ProcessHeader(uint32_t aType,
uint64_t *aMaxAge,
bool *aIncludeSubdomains)
{
// Should be called on the main thread (or via proxy) since the permission
// manager is used and it's not threadsafe.
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
// Only HSTS is supported at the moment.
NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS,
NS_ERROR_NOT_IMPLEMENTED);
@ -354,11 +357,13 @@ nsSiteSecurityService::ProcessHeaderMutating(uint32_t aType,
foundIncludeSubdomains = true;
if (directive->mValue.Length() != 0) {
SSSLOG(("SSS: includeSubdomains directive unexpectedly had value '%s'", directive->mValue.get()));
SSSLOG(("SSS: includeSubdomains directive unexpectedly had value '%s'",
directive->mValue.get()));
return NS_ERROR_FAILURE;
}
} else {
SSSLOG(("SSS: ignoring unrecognized directive '%s'", directive->mName.get()));
SSSLOG(("SSS: ignoring unrecognized directive '%s'",
directive->mName.get()));
foundUnrecognizedDirective = true;
}
}
@ -381,34 +386,28 @@ nsSiteSecurityService::ProcessHeaderMutating(uint32_t aType,
*aIncludeSubdomains = foundIncludeSubdomains;
}
return foundUnrecognizedDirective ?
NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA :
NS_OK;
return foundUnrecognizedDirective ? NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA
: NS_OK;
}
NS_IMETHODIMP
nsSiteSecurityService::IsSecureHost(uint32_t aType, const char* aHost,
uint32_t aFlags, bool* aResult)
nsSiteSecurityService::IsSecureURI(uint32_t aType, nsIURI* aURI,
uint32_t aFlags, bool* aResult)
{
// Should be called on the main thread (or via proxy) since the permission
// manager is used and it's not threadsafe.
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
// Only HSTS is supported at the moment.
NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS,
NS_ERROR_NOT_IMPLEMENTED);
nsAutoCString hostname;
nsresult rv = GetHost(aURI, hostname);
NS_ENSURE_SUCCESS(rv, rv);
/* An IP address never qualifies as a secure URI. */
if (HostIsIPAddress(aHost)) {
if (HostIsIPAddress(hostname.get())) {
*aResult = false;
return NS_OK;
}
nsCOMPtr<nsIURI> uri;
nsDependentCString hostString(aHost);
nsresult rv = NS_NewURI(getter_AddRefs(uri),
NS_LITERAL_CSTRING("https://") + hostString);
NS_ENSURE_SUCCESS(rv, rv);
return IsSecureURI(aType, uri, aFlags, aResult);
return IsSecureHost(aType, hostname.get(), aFlags, aResult);
}
int STSPreloadCompare(const void *key, const void *entry)
@ -424,14 +423,7 @@ int STSPreloadCompare(const void *key, const void *entry)
const nsSTSPreload *
nsSiteSecurityService::GetPreloadListEntry(const char *aHost)
{
PRTime currentTime = PR_Now();
int32_t timeOffset = 0;
nsresult rv = mozilla::Preferences::GetInt("test.currentTimeOffsetSeconds",
&timeOffset);
if (NS_SUCCEEDED(rv)) {
currentTime += (PRTime(timeOffset) * PR_USEC_PER_SEC);
}
PRTime currentTime = PR_Now() + (mPreloadListTimeOffset * PR_USEC_PER_SEC);
if (mUsePreloadList && currentTime < gPreloadListExpirationTime) {
return (const nsSTSPreload *) bsearch(aHost,
kSTSPreloadList,
@ -444,12 +436,9 @@ nsSiteSecurityService::GetPreloadListEntry(const char *aHost)
}
NS_IMETHODIMP
nsSiteSecurityService::IsSecureURI(uint32_t aType, nsIURI* aURI,
uint32_t aFlags, bool* aResult)
nsSiteSecurityService::IsSecureHost(uint32_t aType, const char* aHost,
uint32_t aFlags, bool* aResult)
{
// Should be called on the main thread (or via proxy) since the permission
// manager is used and it's not threadsafe.
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
// Only HSTS is supported at the moment.
NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS,
NS_ERROR_NOT_IMPLEMENTED);
@ -457,75 +446,58 @@ nsSiteSecurityService::IsSecureURI(uint32_t aType, nsIURI* aURI,
// set default in case if we can't find any STS information
*aResult = false;
nsAutoCString host;
nsresult rv = GetHost(aURI, host);
NS_ENSURE_SUCCESS(rv, rv);
/* An IP address never qualifies as a secure URI. */
if (HostIsIPAddress(host.BeginReading())) {
if (HostIsIPAddress(aHost)) {
return NS_OK;
}
// Holepunch chart.apis.google.com and subdomains.
nsAutoCString host(aHost);
ToLowerCase(host);
if (host.EqualsLiteral("chart.apis.google.com") ||
StringEndsWith(host, NS_LITERAL_CSTRING(".chart.apis.google.com"))) {
return NS_OK;
}
const nsSTSPreload *preload = nullptr;
nsSSSHostEntry *pbEntry = nullptr;
bool isPrivate = aFlags & nsISocketProvider::NO_PERMANENT_STORAGE;
if (isPrivate) {
pbEntry = mPrivateModeHostTable.GetEntry(host.get());
}
nsCOMPtr<nsIPrincipal> principal;
rv = GetPrincipalForURI(aURI, getter_AddRefs(principal));
NS_ENSURE_SUCCESS(rv, rv);
uint32_t permMgrPermission;
rv = mPermMgr->TestExactPermissionFromPrincipal(principal, STS_PERMISSION,
&permMgrPermission);
NS_ENSURE_SUCCESS(rv, rv);
// First check the exact host. This involves first checking for an entry in
// the private browsing table. If that entry exists, we don't want to check
// in either the permission manager or the preload list. We only want to use
// the stored permission if it is not a knockout entry, however.
// site security storage. If that entry exists, we don't want to check
// in the preload list. We only want to use the stored value if it is not a
// knockout entry, however.
// Additionally, if it is a knockout entry, we want to stop looking for data
// on the host, because the knockout entry indicates "we have no information
// regarding the sts status of this host".
if (pbEntry && pbEntry->mStsPermission != STS_UNSET) {
SSSLOG(("Found private browsing table entry for %s", host.get()));
if (!pbEntry->IsExpired() && pbEntry->mStsPermission == STS_SET) {
// regarding the security status of this host".
bool isPrivate = aFlags & nsISocketProvider::NO_PERMANENT_STORAGE;
mozilla::DataStorageType storageType = isPrivate
? mozilla::DataStorage_Private
: mozilla::DataStorage_Persistent;
nsCString value = mSiteStateStorage->Get(host, storageType);
SiteSecurityState siteState(value);
if (siteState.mHSTSState != SecurityPropertyUnset) {
SSSLOG(("Found entry for %s", host.get()));
bool expired = siteState.IsExpired(aType);
if (!expired && siteState.mHSTSState == SecurityPropertySet) {
*aResult = true;
return NS_OK;
}
}
// Next we look in the permission manager. Same story here regarding
// knockout entries.
else if (permMgrPermission != STS_UNSET) {
SSSLOG(("Found permission manager entry for %s", host.get()));
if (permMgrPermission == STS_SET) {
*aResult = true;
return NS_OK;
// If the entry is expired and not in the preload list, we can remove it.
if (expired && !GetPreloadListEntry(host.get())) {
mSiteStateStorage->Remove(host, storageType);
}
}
// Finally look in the preloaded list. This is the exact host,
// so if an entry exists at all, this host is sts.
// so if an entry exists at all, this host is HSTS.
else if (GetPreloadListEntry(host.get())) {
SSSLOG(("%s is a preloaded STS host", host.get()));
*aResult = true;
return NS_OK;
}
// Used for testing permissions as we walk up the domain tree.
nsCOMPtr<nsIURI> domainWalkURI;
nsCOMPtr<nsIPrincipal> domainWalkPrincipal;
SSSLOG(("no HSTS data for %s found, walking up domain", host.get()));
const char *subdomain;
SSSLOG(("no HSTS data for %s found, walking up domain", host.get()));
uint32_t offset = 0;
for (offset = host.FindChar('.', offset) + 1;
offset > 0;
@ -538,46 +510,26 @@ nsSiteSecurityService::IsSecureURI(uint32_t aType, nsIURI* aURI,
break;
}
if (isPrivate) {
pbEntry = mPrivateModeHostTable.GetEntry(subdomain);
}
// normalize all URIs with https://
rv = NS_NewURI(getter_AddRefs(domainWalkURI),
NS_LITERAL_CSTRING("https://") + Substring(host, offset));
NS_ENSURE_SUCCESS(rv, rv);
rv = GetPrincipalForURI(domainWalkURI, getter_AddRefs(domainWalkPrincipal));
NS_ENSURE_SUCCESS(rv, rv);
rv = mPermMgr->TestExactPermissionFromPrincipal(domainWalkPrincipal,
STS_PERMISSION,
&permMgrPermission);
NS_ENSURE_SUCCESS(rv, rv);
// Do the same thing as with the exact host, except now we're looking at
// ancestor domains of the original host. So, we have to look at the
// include subdomains permissions (although we still have to check for the
// STS_PERMISSION first to check that this is an sts host and not a
// knockout entry - and again, if it is a knockout entry, we stop looking
// for data on it and skip to the next higher up ancestor domain).
if (pbEntry && pbEntry->mStsPermission != STS_UNSET) {
SSSLOG(("Found private browsing table entry for %s", subdomain));
if (!pbEntry->IsExpired() && pbEntry->mStsPermission == STS_SET) {
*aResult = pbEntry->mIncludeSubdomains;
// include subdomains flag (although we still have to check for a
// SecurityPropertySet flag first to check that this is a secure host and
// not a knockout entry - and again, if it is a knockout entry, we stop
// looking for data on it and skip to the next higher up ancestor domain).
nsCString subdomainString(subdomain);
value = mSiteStateStorage->Get(subdomainString, storageType);
SiteSecurityState siteState(value);
if (siteState.mHSTSState != SecurityPropertyUnset) {
SSSLOG(("Found entry for %s", subdomain));
bool expired = siteState.IsExpired(aType);
if (!expired && siteState.mHSTSState == SecurityPropertySet) {
*aResult = siteState.mHSTSIncludeSubdomains;
break;
}
}
else if (permMgrPermission != STS_UNSET) {
SSSLOG(("Found permission manager entry for %s", subdomain));
if (permMgrPermission == STS_SET) {
uint32_t subdomainPermission;
rv = mPermMgr->TestExactPermissionFromPrincipal(domainWalkPrincipal,
STS_SUBDOMAIN_PERMISSION,
&subdomainPermission);
NS_ENSURE_SUCCESS(rv, rv);
*aResult = (subdomainPermission == STS_SET);
break;
// If the entry is expired and not in the preload list, we can remove it.
if (expired && !GetPreloadListEntry(subdomain)) {
mSiteStateStorage->Remove(subdomainString, storageType);
}
}
// This is an ancestor, so if we get a match, we have to check if the
@ -630,6 +582,12 @@ nsSiteSecurityService::ShouldIgnoreHeaders(nsISupports* aSecurityInfo,
return NS_OK;
}
NS_IMETHODIMP
nsSiteSecurityService::ClearAll()
{
return mSiteStateStorage->Clear();
}
//------------------------------------------------------------
// nsSiteSecurityService::nsIObserver
//------------------------------------------------------------
@ -639,123 +597,18 @@ nsSiteSecurityService::Observe(nsISupports *subject,
const char *topic,
const char16_t *data)
{
if (strcmp(topic, "last-pb-context-exited") == 0) {
mPrivateModeHostTable.Clear();
// Don't access Preferences off the main thread.
if (!NS_IsMainThread()) {
NS_NOTREACHED("Preferences accessed off main thread");
return NS_ERROR_NOT_SAME_THREAD;
}
else if (strcmp(topic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) {
mUsePreloadList = mozilla::Preferences::GetBool("network.stricttransportsecurity.preloadlist", true);
if (strcmp(topic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) {
mUsePreloadList = mozilla::Preferences::GetBool(
"network.stricttransportsecurity.preloadlist", true);
mPreloadListTimeOffset =
mozilla::Preferences::GetInt("test.currentTimeOffsetSeconds", 0);
}
return NS_OK;
}
//------------------------------------------------------------
// Functions to overlay the permission manager calls in case
// we're in private browsing mode.
//------------------------------------------------------------
nsresult
nsSiteSecurityService::AddPermission(nsIURI *aURI,
const char *aType,
uint32_t aPermission,
uint32_t aExpireType,
int64_t aExpireTime,
bool aIsPrivate)
{
// Private mode doesn't address user-set (EXPIRE_NEVER) permissions: let
// those be stored persistently.
if (!aIsPrivate || aExpireType == nsIPermissionManager::EXPIRE_NEVER) {
// Not in private mode, or manually-set permission
nsCOMPtr<nsIPrincipal> principal;
nsresult rv = GetPrincipalForURI(aURI, getter_AddRefs(principal));
NS_ENSURE_SUCCESS(rv, rv);
return mPermMgr->AddFromPrincipal(principal, aType, aPermission,
aExpireType, aExpireTime);
}
nsAutoCString host;
nsresult rv = GetHost(aURI, host);
NS_ENSURE_SUCCESS(rv, rv);
SSSLOG(("AddPermission for entry for %s", host.get()));
// Update in mPrivateModeHostTable only, so any changes will be rolled
// back when exiting private mode.
// Note: EXPIRE_NEVER permissions should trump anything that shows up in
// the HTTP header, so if there's an EXPIRE_NEVER permission already
// don't store anything new.
// Currently there's no way to get the type of expiry out of the
// permission manager, but that's okay since there's nothing that stores
// EXPIRE_NEVER permissions.
// PutEntry returns an existing entry if there already is one, or it
// creates a new one if there isn't.
nsSSSHostEntry* entry = mPrivateModeHostTable.PutEntry(host.get());
if (!entry) {
return NS_ERROR_OUT_OF_MEMORY;
}
SSSLOG(("Created private mode entry for %s", host.get()));
// AddPermission() will be called twice if the STS header encountered has
// includeSubdomains (first for the main permission and second for the
// subdomains permission). If AddPermission() gets called a second time
// with the STS_SUBDOMAIN_PERMISSION, we just have to flip that bit in
// the nsSSSHostEntry.
if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) {
entry->mIncludeSubdomains = true;
}
else if (strcmp(aType, STS_PERMISSION) == 0) {
entry->mStsPermission = aPermission;
}
// Also refresh the expiration time.
entry->SetExpireTime(aExpireTime);
return NS_OK;
}
nsresult
nsSiteSecurityService::RemovePermission(const nsCString &aHost,
const char *aType,
bool aIsPrivate)
{
// Build up a principal for use with the permission manager.
// normalize all URIs with https://
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri),
NS_LITERAL_CSTRING("https://") + aHost);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIPrincipal> principal;
rv = GetPrincipalForURI(uri, getter_AddRefs(principal));
NS_ENSURE_SUCCESS(rv, rv);
if (!aIsPrivate) {
// Not in private mode: remove permissions persistently.
// This means setting the permission to STS_KNOCKOUT in case
// this host is on the preload list (so we can override it).
return mPermMgr->AddFromPrincipal(principal, aType,
STS_KNOCKOUT,
nsIPermissionManager::EXPIRE_NEVER, 0);
}
// Make changes in mPrivateModeHostTable only, so any changes will be
// rolled back when exiting private mode.
nsSSSHostEntry* entry = mPrivateModeHostTable.GetEntry(aHost.get());
if (!entry) {
entry = mPrivateModeHostTable.PutEntry(aHost.get());
if (!entry) {
return NS_ERROR_OUT_OF_MEMORY;
}
SSSLOG(("Created private mode deleted mask for %s", aHost.get()));
}
if (strcmp(aType, STS_PERMISSION) == 0) {
entry->mStsPermission = STS_KNOCKOUT;
}
else if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) {
entry->mIncludeSubdomains = false;
}
return NS_OK;
}

View File

@ -2,122 +2,75 @@
* 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/. */
/**
* This wraps nsSimpleURI so that all calls to it are done on the main thread.
*/
#ifndef __nsSiteSecurityService_h__
#define __nsSiteSecurityService_h__
#include "nsISiteSecurityService.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIPermissionManager.h"
#include "mozilla/DataStorage.h"
#include "nsCOMPtr.h"
#include "nsIURI.h"
#include "nsIObserver.h"
#include "nsISiteSecurityService.h"
#include "nsString.h"
#include "nsTHashtable.h"
#include "prtime.h"
class nsIURI;
// {16955eee-6c48-4152-9309-c42a465138a1}
#define NS_SITE_SECURITY_SERVICE_CID \
{0x16955eee, 0x6c48, 0x4152, \
{0x93, 0x09, 0xc4, 0x2a, 0x46, 0x51, 0x38, 0xa1} }
////////////////////////////////////////////////////////////////////////////////
// nsSSSHostEntry - similar to the nsHostEntry class in
// nsPermissionManager.cpp, but specific to private-mode caching of STS
// permissions.
//
// Each nsSSSHostEntry contains:
// - Expiry time (PRTime, milliseconds)
// - Expired flag (bool, default false)
// - STS permission (uint32_t, default STS_UNSET)
// - Include subdomains flag (bool, default false)
//
// Note: the subdomains flag has no meaning if the STS permission is STS_UNSET.
//
// The existence of the nsSSSHostEntry implies STS state is set for the given
// host -- unless the expired flag is set, in which case not only is the STS
// state not set for the host, but any permission actually present in the
// permission manager should be ignored.
//
// Note: Only one expiry time is stored since the subdomains and STS
// permissions are both encountered at the same time in the HTTP header; if the
// includeSubdomains directive isn't present in the header, it means to delete
// the permission, so the subdomains flag in the nsSSSHostEntry means both that
// the permission doesn't exist and any permission in the real permission
// manager should be ignored since newer information about it has been
// encountered in private browsing mode.
//
// Note: If there's a permission set by the user (EXPIRE_NEVER), STS is not set
// for the host (including the subdomains permission) when the header is
// encountered. Furthermore, any user-set permissions are stored persistently
// and can't be shadowed.
class nsSSSHostEntry : public PLDHashEntryHdr
{
public:
explicit nsSSSHostEntry(const char* aHost);
explicit nsSSSHostEntry(const nsSSSHostEntry& toCopy);
nsCString mHost;
PRTime mExpireTime;
uint32_t mStsPermission;
bool mExpired;
bool mIncludeSubdomains;
// Hash methods
typedef const char* KeyType;
typedef const char* KeyTypePointer;
KeyType GetKey() const
{
return mHost.get();
}
bool KeyEquals(KeyTypePointer aKey) const
{
return !strcmp(mHost.get(), aKey);
}
static KeyTypePointer KeyToPointer(KeyType aKey)
{
return aKey;
}
static PLDHashNumber HashKey(KeyTypePointer aKey)
{
return PL_DHashStringKey(nullptr, aKey);
}
void SetExpireTime(PRTime aExpireTime)
{
mExpireTime = aExpireTime;
mExpired = false;
}
bool IsExpired()
{
// If mExpireTime is 0, this entry never expires (this is the case for
// knockout entries).
// If we've already expired or we never expire, return early.
if (mExpired || mExpireTime == 0) {
return mExpired;
}
PRTime now = PR_Now() / PR_USEC_PER_MSEC;
if (now > mExpireTime) {
mExpired = true;
}
return mExpired;
}
// force the hashtable to use the copy constructor.
enum { ALLOW_MEMMOVE = false };
/**
* SecurityPropertyState: A utility enum for representing the different states
* a security property can be in.
* SecurityPropertySet and SecurityPropertyUnset correspond to indicating
* a site has or does not have the security property in question, respectively.
* SecurityPropertyKnockout indicates a value on a preloaded list is being
* overridden, and the associated site does not have the security property
* in question.
*/
enum SecurityPropertyState {
SecurityPropertyUnset = 0,
SecurityPropertySet = 1,
SecurityPropertyKnockout = 2
};
/**
* SiteSecurityState: A utility class that encodes/decodes a string describing
* the security state of a site. Currently only handles HSTS.
* HSTS state consists of:
* - Expiry time (PRTime (aka int64_t) in milliseconds)
* - A state flag (SecurityPropertyState, default SecurityPropertyUnset)
* - An include subdomains flag (bool, default false)
*/
class SiteSecurityState
{
public:
SiteSecurityState(nsCString& aStateString);
SiteSecurityState(PRTime aHSTSExpireTime, SecurityPropertyState aHSTSState,
bool aHSTSIncludeSubdomains);
PRTime mHSTSExpireTime;
SecurityPropertyState mHSTSState;
bool mHSTSIncludeSubdomains;
bool IsExpired(uint32_t aType)
{
// If mHSTSExpireTime is 0, this entry never expires (this is the case for
// knockout entries).
if (mHSTSExpireTime == 0) {
return false;
}
PRTime now = PR_Now() / PR_USEC_PER_MSEC;
if (now > mHSTSExpireTime) {
return true;
}
return false;
}
void ToString(nsCString &aString);
};
////////////////////////////////////////////////////////////////////////////////
class nsSTSPreload;
@ -137,7 +90,6 @@ protected:
private:
nsresult GetHost(nsIURI *aURI, nsACString &aResult);
nsresult GetPrincipalForURI(nsIURI *aURI, nsIPrincipal **aPrincipal);
nsresult SetState(uint32_t aType, nsIURI* aSourceURI, int64_t maxage,
bool includeSubdomains, uint32_t flags);
nsresult ProcessHeaderMutating(uint32_t aType, nsIURI* aSourceURI,
@ -145,23 +97,9 @@ private:
uint64_t *aMaxAge, bool *aIncludeSubdomains);
const nsSTSPreload *GetPreloadListEntry(const char *aHost);
// private-mode-preserving permission manager overlay functions
nsresult AddPermission(nsIURI *aURI,
const char *aType,
uint32_t aPermission,
uint32_t aExpireType,
int64_t aExpireTime,
bool aIsPrivate);
nsresult RemovePermission(const nsCString &aHost,
const char *aType,
bool aIsPrivate);
// cached services
nsCOMPtr<nsIPermissionManager> mPermMgr;
nsCOMPtr<nsIObserverService> mObserverService;
nsTHashtable<nsSSSHostEntry> mPrivateModeHostTable;
bool mUsePreloadList;
int64_t mPreloadListTimeOffset;
nsRefPtr<mozilla::DataStorage> mSiteStateStorage;
};
#endif // __nsSiteSecurityService_h__

View File

@ -0,0 +1,224 @@
/* -*- 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 "gtest/gtest.h"
#include "mozilla/DataStorage.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"
#include "nsStreamUtils.h"
using namespace mozilla;
class DataStorageTest : public ::testing::Test
{
protected:
virtual void SetUp()
{
const ::testing::TestInfo* const testInfo =
::testing::UnitTest::GetInstance()->current_test_info();
NS_ConvertUTF8toUTF16 testName(testInfo->name());
storage = new DataStorage(testName);
storage->Init(dataWillPersist);
}
nsRefPtr<DataStorage> storage;
bool dataWillPersist;
};
NS_NAMED_LITERAL_CSTRING(testKey, "test");
NS_NAMED_LITERAL_CSTRING(testValue, "value");
NS_NAMED_LITERAL_CSTRING(privateTestValue, "private");
TEST_F(DataStorageTest, GetPutRemove)
{
EXPECT_TRUE(dataWillPersist);
// Test Put/Get on Persistent data
EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
// Don't re-use testKey / testValue here, to make sure that this works as
// expected with objects that have the same semantic value but are not
// literally the same object.
nsCString result = storage->Get(NS_LITERAL_CSTRING("test"),
DataStorage_Persistent);
EXPECT_STREQ("value", result.get());
// Get on Temporary/Private data with the same key should give nothing
result = storage->Get(testKey, DataStorage_Temporary);
EXPECT_TRUE(result.IsEmpty());
result = storage->Get(testKey, DataStorage_Private);
EXPECT_TRUE(result.IsEmpty());
// Put with Temporary/Private data shouldn't affect Persistent data
NS_NAMED_LITERAL_CSTRING(temporaryTestValue, "temporary");
EXPECT_EQ(NS_OK, storage->Put(testKey, temporaryTestValue,
DataStorage_Temporary));
EXPECT_EQ(NS_OK, storage->Put(testKey, privateTestValue,
DataStorage_Private));
result = storage->Get(testKey, DataStorage_Temporary);
EXPECT_STREQ("temporary", result.get());
result = storage->Get(testKey, DataStorage_Private);
EXPECT_STREQ("private", result.get());
result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_STREQ("value", result.get());
// Put of a previously-present key overwrites it (if of the same type)
NS_NAMED_LITERAL_CSTRING(newValue, "new");
EXPECT_EQ(NS_OK, storage->Put(testKey, newValue, DataStorage_Persistent));
result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_STREQ("new", result.get());
// Removal should work
storage->Remove(testKey, DataStorage_Temporary);
result = storage->Get(testKey, DataStorage_Temporary);
EXPECT_TRUE(result.IsEmpty());
// But removing one type shouldn't affect the others
result = storage->Get(testKey, DataStorage_Private);
EXPECT_STREQ("private", result.get());
result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_STREQ("new", result.get());
// Test removing the other types as well
storage->Remove(testKey, DataStorage_Private);
result = storage->Get(testKey, DataStorage_Private);
EXPECT_TRUE(result.IsEmpty());
storage->Remove(testKey, DataStorage_Persistent);
result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_TRUE(result.IsEmpty());
}
TEST_F(DataStorageTest, InputValidation)
{
EXPECT_TRUE(dataWillPersist);
// Keys may not have tabs or newlines
EXPECT_EQ(NS_ERROR_INVALID_ARG,
storage->Put(NS_LITERAL_CSTRING("key\thas tab"), testValue,
DataStorage_Persistent));
nsCString result = storage->Get(NS_LITERAL_CSTRING("key\thas tab"),
DataStorage_Persistent);
EXPECT_TRUE(result.IsEmpty());
EXPECT_EQ(NS_ERROR_INVALID_ARG,
storage->Put(NS_LITERAL_CSTRING("key has\nnewline"), testValue,
DataStorage_Persistent));
result = storage->Get(NS_LITERAL_CSTRING("keyhas\nnewline"),
DataStorage_Persistent);
EXPECT_TRUE(result.IsEmpty());
// Values may not have newlines
EXPECT_EQ(NS_ERROR_INVALID_ARG,
storage->Put(testKey, NS_LITERAL_CSTRING("value\nhas newline"),
DataStorage_Persistent));
result = storage->Get(testKey, DataStorage_Persistent);
// Values may have tabs
EXPECT_TRUE(result.IsEmpty());
EXPECT_EQ(NS_OK, storage->Put(testKey,
NS_LITERAL_CSTRING("val\thas tab; this is ok"),
DataStorage_Persistent));
result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_STREQ("val\thas tab; this is ok", result.get());
nsCString longKey("a");
for (int i = 0; i < 8; i++) {
longKey.Append(longKey);
}
// A key of length 256 will work
EXPECT_EQ(NS_OK, storage->Put(longKey, testValue, DataStorage_Persistent));
result = storage->Get(longKey, DataStorage_Persistent);
EXPECT_STREQ("value", result.get());
longKey.Append("a");
// A key longer than that will not work
EXPECT_EQ(NS_ERROR_INVALID_ARG,
storage->Put(longKey, testValue, DataStorage_Persistent));
result = storage->Get(longKey, DataStorage_Persistent);
EXPECT_TRUE(result.IsEmpty());
nsCString longValue("a");
for (int i = 0; i < 10; i++) {
longValue.Append(longValue);
}
// A value of length 1024 will work
EXPECT_EQ(NS_OK, storage->Put(testKey, longValue, DataStorage_Persistent));
result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_STREQ(longValue.get(), result.get());
longValue.Append("a");
// A value longer than that will not work
storage->Remove(testKey, DataStorage_Persistent);
EXPECT_EQ(NS_ERROR_INVALID_ARG,
storage->Put(testKey, longValue, DataStorage_Persistent));
result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_TRUE(result.IsEmpty());
}
TEST_F(DataStorageTest, Eviction)
{
EXPECT_TRUE(dataWillPersist);
// Eviction is on a per-table basis. Tables shouldn't affect each other.
EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
for (int i = 0; i < 1025; i++) {
EXPECT_EQ(NS_OK, storage->Put(nsPrintfCString("%d", i),
nsPrintfCString("%d", i),
DataStorage_Temporary));
nsCString result = storage->Get(nsPrintfCString("%d", i),
DataStorage_Temporary);
EXPECT_STREQ(nsPrintfCString("%d", i).get(), result.get());
}
// We don't know which entry got evicted, but we can count them.
int entries = 0;
for (int i = 0; i < 1025; i++) {
nsCString result = storage->Get(nsPrintfCString("%d", i),
DataStorage_Temporary);
if (!result.IsEmpty()) {
entries++;
}
}
EXPECT_EQ(entries, 1024);
nsCString result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_STREQ("value", result.get());
}
TEST_F(DataStorageTest, ClearPrivateData)
{
EXPECT_TRUE(dataWillPersist);
EXPECT_EQ(NS_OK, storage->Put(testKey, privateTestValue,
DataStorage_Private));
nsCString result = storage->Get(testKey, DataStorage_Private);
EXPECT_STREQ("private", result.get());
storage->Observe(nullptr, "last-pb-context-exited", nullptr);
result = storage->Get(testKey, DataStorage_Private);
EXPECT_TRUE(result.IsEmpty());
}
TEST_F(DataStorageTest, Shutdown)
{
EXPECT_TRUE(dataWillPersist);
EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
nsCString result = storage->Get(testKey, DataStorage_Persistent);
EXPECT_STREQ("value", result.get());
// Get "now" (in days) close to when the data was last touched, so we won't
// get intermittent failures with the day not matching.
int64_t microsecondsPerDay = 24 * 60 * 60 * int64_t(PR_USEC_PER_SEC);
int32_t nowInDays = int32_t(PR_Now() / microsecondsPerDay);
storage->Observe(nullptr, "profile-before-change", nullptr);
nsCOMPtr<nsIFile> backingFile;
EXPECT_EQ(NS_OK, NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(backingFile)));
const ::testing::TestInfo* const testInfo =
::testing::UnitTest::GetInstance()->current_test_info();
NS_ConvertUTF8toUTF16 testName(testInfo->name());
EXPECT_EQ(NS_OK, backingFile->Append(testName));
nsCOMPtr<nsIInputStream> fileInputStream;
EXPECT_EQ(NS_OK, NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream),
backingFile));
nsCString data;
EXPECT_EQ(NS_OK, NS_ConsumeStream(fileInputStream, UINT32_MAX, data));
// The data will be of the form 'test\t0\t<days since the epoch>\tvalue'
EXPECT_STREQ(nsPrintfCString("test\t0\t%d\tvalue\n", nowInDays).get(),
data.get());
}

View File

@ -5,6 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
SOURCES += [
'DataStorageTest.cpp',
'OCSPCacheTest.cpp',
'TLSIntoleranceTest.cpp',
]

View File

@ -18,6 +18,8 @@ let gIsWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
const isDebugBuild = Cc["@mozilla.org/xpcom/debug;1"]
.getService(Ci.nsIDebug2).isDebugBuild;
const SSS_STATE_FILE_NAME = "SiteSecurityServiceState.txt";
const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
const SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE;
const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;

View File

@ -0,0 +1,75 @@
/* 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/. */
// The purpose of this test is to check that a frequently visited site
// will not be evicted over an infrequently visited site.
let gSSService = null;
let gProfileDir = null;
function do_state_written(aSubject, aTopic, aData) {
do_check_eq(aData, SSS_STATE_FILE_NAME);
let stateFile = gProfileDir.clone();
stateFile.append(SSS_STATE_FILE_NAME);
do_check_true(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;
}
}
do_check_true(foundLegitSite);
do_test_finished();
}
function do_state_read(aSubject, aTopic, aData) {
do_check_eq(aData, SSS_STATE_FILE_NAME);
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"frequentlyused.example.com", 0));
for (let i = 0; i < 2000; i++) {
let uri = Services.io.newURI("http://bad" + i + ".example.com", null, null);
gSSService.processHeader(Ci.nsISiteSecurityService.HEADER_HSTS, uri,
"max-age=1000", 0);
}
do_test_pending();
Services.obs.addObserver(do_state_written, "data-storage-written", false);
do_test_finished();
}
function run_test() {
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.
do_check_false(stateFile.exists());
let outputStream = FileUtils.openFileOutputStream(stateFile);
let now = (new Date()).getTime();
let line = "frequentlyused.example.com\t4\t0\t" + (now + 100000) + ",1,0\n";
outputStream.write(line, line.length);
outputStream.close();
Services.obs.addObserver(do_state_read, "data-storage-ready", false);
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
do_check_true(gSSService != null);
}

View File

@ -0,0 +1,77 @@
/* 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/. */
// The purpose of this test is to create a site security service state file
// and see that the site security service reads it properly.
function writeLine(aLine, aOutputStream) {
aOutputStream.write(aLine, aLine.length);
}
let gSSService = null;
function checkStateRead(aSubject, aTopic, aData) {
do_check_eq(aData, SSS_STATE_FILE_NAME);
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"expired.example.com", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"notexpired.example.com", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"bugzilla.mozilla.org", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"sub.bugzilla.mozilla.org", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"incsubdomain.example.com", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"sub.incsubdomain.example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"login.persona.org", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"sub.login.persona.org", 0));
// Clearing the data should make everything go back to default.
gSSService.clearAll();
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"expired.example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"notexpired.example.com", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"bugzilla.mozilla.org", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"sub.bugzilla.mozilla.org", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"incsubdomain.example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"sub.incsubdomain.example.com", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"login.persona.org", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"sub.login.persona.org", 0));
do_test_finished();
}
function run_test() {
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.
do_check_false(stateFile.exists());
let outputStream = FileUtils.openFileOutputStream(stateFile);
let now = (new Date()).getTime();
writeLine("expired.example.com\t0\t0\t" + (now - 100000) + ",1,0\n", outputStream);
writeLine("notexpired.example.com\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream);
// This overrides an entry on the preload list.
writeLine("bugzilla.mozilla.org\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream);
writeLine("incsubdomain.example.com\t0\t0\t" + (now + 100000) + ",1,1\n", outputStream);
// This overrides an entry on the preload list.
writeLine("login.persona.org\t0\t0\t0,2,0\n", outputStream);
outputStream.close();
Services.obs.addObserver(checkStateRead, "data-storage-ready", false);
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
do_check_true(gSSService != null);
}

View File

@ -0,0 +1,40 @@
/* 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/. */
// 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.
let gSSService = null;
function checkStateRead(aSubject, aTopic, aData) {
// nonexistent.example.com should never be an HSTS host
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"nonexistent.example.com", 0));
// bugzilla.mozilla.org is preloaded
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"bugzilla.mozilla.org", 0));
// notexpired.example.com is an HSTS host in a different test - we
// want to make sure that test hasn't interfered with this one.
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"notexpired.example.com", 0));
do_test_finished();
}
function run_test() {
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.
do_check_false(stateFile.exists());
stateFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0x1a4); // 0x1a4 == 0644
do_check_true(stateFile.exists());
// Initialize nsISiteSecurityService after do_get_profile() so it
// can read the state file.
Services.obs.addObserver(checkStateRead, "data-storage-ready", false);
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
do_check_true(gSSService != null);
}

View File

@ -0,0 +1,51 @@
/* 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/. */
// 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.
function writeLine(aLine, aOutputStream) {
aOutputStream.write(aLine, aLine.length);
}
let gSSService = null;
function checkStateRead(aSubject, aTopic, aData) {
do_check_eq(aData, SSS_STATE_FILE_NAME);
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example1.example.com", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example2.example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example3.example.com", 0));
do_test_finished();
}
function run_test() {
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.
do_check_false(stateFile.exists());
let outputStream = FileUtils.openFileOutputStream(stateFile);
let now = (new Date()).getTime();
writeLine("example1.example.com\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream);
writeLine("I'm a lumberjack and I'm okay; I work all night and I sleep all day!\n", outputStream);
writeLine("This is a totally bogus entry\t\n", outputStream);
writeLine("0\t0\t0\t0\t\n", outputStream);
writeLine("\t\t\t\t\t\t\t\n", outputStream);
writeLine("example.com\t\t\t\t\t\t\t\n", outputStream);
writeLine("example3.example.com\t0\t\t\t\t\t\t\n", outputStream);
writeLine("example2.example.com\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream);
outputStream.close();
Services.obs.addObserver(checkStateRead, "data-storage-ready", false);
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
do_check_true(gSSService != null);
}

View File

@ -0,0 +1,55 @@
/* 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/. */
// 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).
function writeLine(aLine, aOutputStream) {
aOutputStream.write(aLine, aLine.length);
}
let gSSService = null;
function checkStateRead(aSubject, aTopic, aData) {
do_check_eq(aData, SSS_STATE_FILE_NAME);
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example0.example.com", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example423.example.com", 0));
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example1023.example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example1024.example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example1025.example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example9000.example.com", 0));
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"example99999.example.com", 0));
do_test_finished();
}
function run_test() {
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.
do_check_false(stateFile.exists());
let outputStream = FileUtils.openFileOutputStream(stateFile);
let now = (new Date()).getTime();
for (let i = 0; i < 10000; i++) {
// The 0s will all get squashed down into one 0 when they are read.
// This is just to make the file size large (>2MB).
writeLine("example" + i + ".example.com\t0000000000000000000000000000000000000000000000000\t00000000000000000000000000000000000000\t" + (now + 100000) + ",1,0000000000000000000000000000000000000000000000000000000000000000000000000\n", outputStream);
}
outputStream.close();
Services.obs.addObserver(checkStateRead, "data-storage-ready", false);
do_test_pending();
gSSService = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
do_check_true(gSSService != null);
}

View File

@ -0,0 +1,104 @@
/* 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/. */
// The purpose of this test is to see that the site security service properly
// writes its state file.
const EXPECTED_ENTRIES = 5;
const EXPECTED_COLUMNS = 3;
let gProfileDir = null;
// 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) {
do_check_eq(aData, SSS_STATE_FILE_NAME);
let stateFile = gProfileDir.clone();
stateFile.append(SSS_STATE_FILE_NAME);
do_check_true(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);
do_check_eq(lines.length, EXPECTED_ENTRIES);
let sites = {}; // a map of domain name -> [the entry in the state file]
for (let line of lines) {
let parts = line.split('\t');
let host = parts[0];
let score = parts[1];
let lastAccessed = parts[2];
let entry = parts[3].split(',');
do_check_eq(entry.length, EXPECTED_COLUMNS);
sites[host] = entry;
}
// 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.
// sites[url][1] corresponds to SecurityPropertySet (if 1) and
// SecurityPropertyUnset (if 0)
// sites[url][2] corresponds to includeSubdomains
if (sites["bugzilla.mozilla.org"][1] != 1) {
return;
}
if (sites["bugzilla.mozilla.org"][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;
}
do_test_finished();
}
function run_test() {
Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
gProfileDir = do_get_profile();
let SSService = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
let uris = [ Services.io.newURI("http://bugzilla.mozilla.org", null, null),
Services.io.newURI("http://a.example.com", null, null),
Services.io.newURI("http://b.example.com", null, null),
Services.io.newURI("http://c.c.example.com", null, null),
Services.io.newURI("http://d.example.com", null, null) ];
for (let i = 0; i < 1000; i++) {
let uriIndex = i % uris.length;
// vary max-age
let maxAge = "max-age=" + (i * 1000);
// alternate setting includeSubdomains
let includeSubdomains = (i % 2 == 0 ? "; includeSubdomains" : "");
SSService.processHeader(Ci.nsISiteSecurityService.HEADER_HSTS,
uris[uriIndex], maxAge + includeSubdomains, 0);
}
do_test_pending();
Services.obs.addObserver(checkStateWritten, "data-storage-written", false);
}

View File

@ -17,28 +17,9 @@ Observer.prototype = {
var gObserver = new Observer();
// nsISiteSecurityService.removeStsState removes a given domain's
// HSTS status. This means that a domain on the preload list will be
// considered not HSTS if this is called. So, to reset everything to its
// original state, we have to reach into the permission manager and clear
// any HSTS-related state manually.
function clearStsState() {
var permissionManager = Cc["@mozilla.org/permissionmanager;1"]
.getService(Ci.nsIPermissionManager);
// This is a list of every host we call processHeader with
// (so we can remove any state added to the sts service)
var hosts = ["bugzilla.mozilla.org", "login.persona.org",
"subdomain.www.torproject.org",
"subdomain.bugzilla.mozilla.org" ];
for (var host of hosts) {
permissionManager.remove(host, "sts/use");
permissionManager.remove(host, "sts/subd");
}
}
function cleanup() {
Services.obs.removeObserver(gObserver, "last-pb-context-exited");
clearStsState();
gSSService.clearAll();
}
function run_test() {
@ -103,7 +84,7 @@ function test_part1() {
// but this time include subdomains was not set, so test for that
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"subdomain.bugzilla.mozilla.org", 0));
clearStsState();
gSSService.clearAll();
// check that processing a header with max-age: 0 from a subdomain of a site
// will not remove that (ancestor) site from the list
@ -150,14 +131,28 @@ function test_part1() {
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"another.subdomain.bugzilla.mozilla.org", 0));
// Simulate leaving private browsing mode
Services.obs.notifyObservers(null, "last-pb-context-exited", null);
// Test that an expired non-private browsing entry results in correctly
// identifying a host that is on the preload list as no longer sts.
// (This happens when we're in regular browsing mode, we get a header from
// a site on the preload list, and that header later expires. We need to
// then treat that host as no longer an sts host.)
// (sanity check first - this should be in the preload list)
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"login.persona.org", 0));
var uri = Services.io.newURI("http://login.persona.org", null, null);
gSSService.processHeader(Ci.nsISiteSecurityService.HEADER_HSTS, uri,
"max-age=1", 0);
do_timeout(1250, function() {
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"login.persona.org", 0));
run_next_test();
});
}
const IS_PRIVATE = Ci.nsISocketProvider.NO_PERMANENT_STORAGE;
function test_private_browsing1() {
clearStsState();
gSSService.clearAll();
// sanity - bugzilla.mozilla.org is preloaded, includeSubdomains set
do_check_true(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"bugzilla.mozilla.org", IS_PRIVATE));
@ -189,9 +184,6 @@ function test_private_browsing1() {
do_check_false(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
"subdomain.bugzilla.mozilla.org", IS_PRIVATE));
// TODO unfortunately we don't have a good way to know when an entry
// has expired in the permission manager, so we can't yet extend this test
// to that case.
// Test that an expired private browsing entry results in correctly
// identifying a host that is on the preload list as no longer sts.
// (This happens when we're in private browsing mode, we get a header from

View File

@ -30,6 +30,13 @@ skip-if = buildapp == "b2g" && processor = "arm"
[test_sts_holepunch.js]
[test_sts_ipv4_ipv6.js]
[test_sss_eviction.js]
[test_sss_readstate.js]
[test_sss_readstate_empty.js]
[test_sss_readstate_garbage.js]
[test_sss_readstate_huge.js]
[test_sss_savestate.js]
[test_certificate_usages.js]
[test_ocsp_stapling.js]
run-sequentially = hardcoded ports

View File

@ -1 +1 @@
NSS_3_17_1_BETA1
NSS_3_17_1_BETA2

View File

@ -42,7 +42,7 @@ ifndef INTERNAL_TOOLS
CROSS_COMPILE = 1
endif
endif
ifeq ($(OS_TEST),ppc64)
ifeq (,$(filter-out ppc64 ppc64le,$(OS_TEST)))
CPU_ARCH = ppc
ifeq ($(USE_64),1)
ARCHFLAG = -m64

View File

@ -30,9 +30,16 @@ else
BSDECHO = echo
RC = rc.exe
MT = mt.exe
# Check for clang-cl
CLANG_CL := $(shell expr `$(CC) -? 2>&1 | grep -w clang | wc -l` \> 0)
# Determine compiler version
CC_VERSION := $(shell $(CC) 2>&1 | sed -ne \
ifeq ($(CLANG_CL),1)
# clang-cl pretends to be MSVC 2012.
CC_VERSION := 17.00.00.00
else
CC_VERSION := $(shell $(CC) 2>&1 | sed -ne \
's|.* \([0-9]\+\.[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*|\1|p')
endif
# Change the dots to spaces.
_CC_VERSION_WORDS := $(subst ., ,$(CC_VERSION))
_CC_VMAJOR := $(word 1,$(_CC_VERSION_WORDS))
@ -203,12 +210,18 @@ endif
ifeq (,$(filter-out x386 x86_64,$(CPU_ARCH)))
ifdef USE_64
DEFINES += -D_AMD64_
# Use subsystem 5.02 to allow running on Windows XP.
ifeq ($(_MSC_VER_GE_11),1)
LDFLAGS += -SUBSYSTEM:CONSOLE,5.02
endif
else
DEFINES += -D_X86_
# VS2012 defaults to -arch:SSE2. Use -arch:IA32 to avoid requiring
# SSE2.
# Use subsystem 5.01 to allow running on Windows XP.
ifeq ($(_MSC_VER_GE_11),1)
OS_CFLAGS += -arch:IA32
LDFLAGS += -SUBSYSTEM:CONSOLE,5.01
endif
endif
endif

View File

@ -10,3 +10,4 @@
*/
#error "Do not include this header file."

View File

@ -445,11 +445,11 @@ SEC_GetSignatureAlgorithmOidTag(KeyType keyType, SECOidTag hashAlgTag)
sigTag = SEC_OID_PKCS1_MD2_WITH_RSA_ENCRYPTION; break;
case SEC_OID_MD5:
sigTag = SEC_OID_PKCS1_MD5_WITH_RSA_ENCRYPTION; break;
case SEC_OID_UNKNOWN: /* default for RSA if not specified */
case SEC_OID_SHA1:
sigTag = SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION; break;
case SEC_OID_SHA224:
sigTag = SEC_OID_PKCS1_SHA224_WITH_RSA_ENCRYPTION; break;
case SEC_OID_UNKNOWN: /* default for RSA if not specified */
case SEC_OID_SHA256:
sigTag = SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION; break;
case SEC_OID_SHA384:

View File

@ -141,6 +141,9 @@ else
DEFINES += -DUSE_HW_AES -DINTEL_GCM
ASFILES += intel-aes-x86-masm.asm intel-gcm-x86-masm.asm
EXTRA_SRCS += intel-gcm-wrap.c
ifeq ($(CLANG_CL),1)
INTEL_GCM_CLANG_CL = 1
endif
endif
endif
else
@ -668,3 +671,10 @@ ifneq (,$(findstring clang,$(shell $(AS) --version)))
$(OBJDIR)/$(PROG_PREFIX)intel-gcm$(OBJ_SUFFIX): ASFLAGS += -no-integrated-as
endif
endif
ifdef INTEL_GCM_CLANG_CL
#
# clang-cl needs -mssse3
#
$(OBJDIR)/$(PROG_PREFIX)intel-gcm-wrap$(OBJ_SUFFIX): CFLAGS += -mssse3
endif

View File

@ -1,5 +0,0 @@
[001.html]
type: testharness
[navigator]
expected: FAIL

View File

@ -1,5 +0,0 @@
[language.html]
type: testharness
[navigator.language]
expected: FAIL

View File

@ -30,8 +30,6 @@
static const int TIME_TO_WAIT_ON_UPDATER = 15 * 60 * 1000;
char16_t* MakeCommandLine(int argc, char16_t **argv);
BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode);
BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath,
LPCWSTR newFileName);
/*
* Read the update.status file and sets isApplying to true if

View File

@ -3321,11 +3321,17 @@
"description": "Updater: The interval in days between the previous and the current background update check when the check was timer initiated"
},
"UPDATER_STATUS_CODES": {
"expires_in_version": "never",
"expires_in_version": "35",
"kind": "enumerated",
"n_values": 50,
"description": "Updater: the status of the latest update performed"
},
"UPDATER_ALL_STATUS_CODES": {
"expires_in_version": "never",
"kind": "enumerated",
"n_values": 200,
"description": "Updater: the status of the latest update performed"
},
"UPDATER_UPDATES_ENABLED": {
"expires_in_version": "never",
"kind": "boolean",

View File

@ -69,6 +69,11 @@
#define WRITE_ERROR_SHARING_VIOLATION_NOPROCESSFORPID 47
#define WRITE_ERROR_SHARING_VIOLATION_NOPID 48
#define FOTA_FILE_OPERATION_ERROR 49
#define FOTA_RECOVERY_ERROR 50
#define SECURE_LOCATION_UPDATE_ERROR 51
// The following error codes are only used by updater.exe
// when a fallback key exists and XPCShell tests are being run.
#define FALLBACKKEY_UNKNOWN_ERROR 100

View File

@ -4,6 +4,7 @@
#include <windows.h>
#include <wtsapi32.h>
#include <aclapi.h>
#include "uachelper.h"
#include "updatelogging.h"
@ -161,11 +162,15 @@ UACHelper::DisableUnneededPrivileges(HANDLE token,
BOOL result = TRUE;
for (size_t i = 0; i < count; i++) {
if (SetPrivilege(token, unneededPrivs[i], FALSE)) {
#ifdef UPDATER_LOG_PRIVS
LOG(("Disabled unneeded token privilege: %s.",
unneededPrivs[i]));
#endif
} else {
#ifdef UPDATER_LOG_PRIVS
LOG(("Could not disable token privilege value: %s. (%d)",
unneededPrivs[i], GetLastError()));
#endif
result = FALSE;
}
}
@ -220,3 +225,93 @@ UACHelper::CanUserElevate()
return canElevate;
}
/**
* Denies write access for everyone on the specified path.
*
* @param path The file path to modify the DACL on
* @param originalACL out parameter, set only if successful.
* caller must free.
* @return true on success
*/
bool
UACHelper::DenyWriteACLOnPath(LPCWSTR path, PACL *originalACL,
PSECURITY_DESCRIPTOR *sd)
{
// Get the old security information on the path.
// Note that the actual buffer to be freed is contained in *sd.
// originalACL points within *sd's buffer.
*originalACL = nullptr;
*sd = nullptr;
DWORD result =
GetNamedSecurityInfoW(path, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
nullptr, nullptr, originalACL, nullptr, sd);
if (result != ERROR_SUCCESS) {
*sd = nullptr;
*originalACL = nullptr;
return false;
}
// Adjust the security for everyone to deny write
EXPLICIT_ACCESSW ea;
ZeroMemory(&ea, sizeof(EXPLICIT_ACCESSW));
ea.grfAccessPermissions = FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES |
FILE_WRITE_DATA | FILE_WRITE_EA;
ea.grfAccessMode = DENY_ACCESS;
ea.grfInheritance = NO_INHERITANCE;
ea.Trustee.TrusteeForm = TRUSTEE_IS_NAME;
ea.Trustee.TrusteeType = TRUSTEE_IS_GROUP;
ea.Trustee.ptstrName = L"EVERYONE";
PACL dacl = nullptr;
result = SetEntriesInAclW(1, &ea, *originalACL, &dacl);
if (result != ERROR_SUCCESS) {
LocalFree(*sd);
*originalACL = nullptr;
*sd = nullptr;
return false;
}
// Update the path to have a the new DACL
result = SetNamedSecurityInfoW(const_cast<LPWSTR>(path), SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION, nullptr, nullptr,
dacl, nullptr);
LocalFree(dacl);
return result == ERROR_SUCCESS;
}
/**
* Determines if the specified directory only has updater.exe inside of it,
* and nothing else.
*
* @param inputPath the directory path to search
* @return true if updater.exe is the only file in the directory
*/
bool
UACHelper::IsDirectorySafe(LPCWSTR inputPath)
{
WIN32_FIND_DATAW findData;
HANDLE findHandle = nullptr;
WCHAR searchPath[MAX_PATH + 1] = { L'\0' };
wsprintfW(searchPath, L"%s\\*.*", inputPath);
findHandle = FindFirstFileW(searchPath, &findData);
if(findHandle == INVALID_HANDLE_VALUE) {
return false;
}
// Enumerate the files and if we find anything other than the current
// directory, the parent directory, or updater.exe. Then fail.
do {
if(wcscmp(findData.cFileName, L".") != 0 &&
wcscmp(findData.cFileName, L"..") != 0 &&
wcscmp(findData.cFileName, L"updater.exe") != 0) {
FindClose(findHandle);
return false;
}
} while(FindNextFileW(findHandle, &findData));
FindClose(findHandle);
return true;
}

View File

@ -12,12 +12,15 @@ public:
static HANDLE OpenLinkedToken(HANDLE token);
static BOOL DisablePrivileges(HANDLE token);
static bool CanUserElevate();
static bool IsDirectorySafe(LPCWSTR inputPath);
static bool DenyWriteACLOnPath(LPCWSTR path, PACL *originalACL,
PSECURITY_DESCRIPTOR *sd);
private:
static BOOL SetPrivilege(HANDLE token, LPCTSTR privs, BOOL enable);
static BOOL DisableUnneededPrivileges(HANDLE token,
static BOOL DisableUnneededPrivileges(HANDLE token,
LPCTSTR *unneededPrivs, size_t count);
static LPCTSTR PrivsToDisable[];
static LPCTSTR PrivsToDisable[];
};
#endif

View File

@ -33,7 +33,6 @@ using mozilla::MakeUnique;
using mozilla::UniquePtr;
WCHAR* MakeCommandLine(int argc, WCHAR **argv);
BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
/**
* Obtains the path of a file in the same directory as the specified file.

View File

@ -17,6 +17,10 @@ BOOL DoesFallbackKeyExist();
BOOL IsLocalFile(LPCWSTR file, BOOL &isLocal);
DWORD StartServiceCommand(int argc, LPCWSTR* argv);
BOOL IsUnpromptedElevation(BOOL &isUnpromptedElevation);
BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer,
LPCWSTR siblingFilePath,
LPCWSTR newFileName);
BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
#define SVC_NAME L"MozillaMaintenance"

View File

@ -162,6 +162,7 @@ const WRITE_ERROR_SHARING_VIOLATION_NOPROCESSFORPID = 47;
const WRITE_ERROR_SHARING_VIOLATION_NOPID = 48;
const FOTA_FILE_OPERATION_ERROR = 49;
const FOTA_RECOVERY_ERROR = 50;
const SECURE_LOCATION_UPDATE_ERROR = 51;
const CERT_ATTR_CHECK_FAILED_NO_UPDATE = 100;
const CERT_ATTR_CHECK_FAILED_HAS_UPDATE = 101;
@ -1477,7 +1478,8 @@ function handleUpdateFailure(update, errorCode) {
return true;
}
if (update.errorCode == ELEVATION_CANCELED) {
if (update.errorCode == ELEVATION_CANCELED ||
update.errorCode == SECURE_LOCATION_UPDATE_ERROR) {
writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
return true;
}
@ -2383,7 +2385,7 @@ UpdateService.prototype = {
if (parts.length > 1) {
result = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
}
Services.telemetry.getHistogramById("UPDATER_STATUS_CODES").add(result);
Services.telemetry.getHistogramById("UPDATER_ALL_STATUS_CODES").add(result);
} catch(e) {
// Don't allow any exception to be propagated.
Cu.reportError(e);

View File

@ -1508,15 +1508,18 @@ function runUpdate(aExpectedExitValue, aExpectedStatus, aCallback) {
if (gDisableReplaceFallback) {
env.set("MOZ_NO_REPLACE_FALLBACK", "1");
}
env.set("MOZ_EMULATE_ELEVATION_PATH", "1");
let process = AUS_Cc["@mozilla.org/process/util;1"].
createInstance(AUS_Ci.nsIProcess);
process.init(updateBin);
process.run(true, args, args.length);
if (gDisableReplaceFallback) {
env.set("MOZ_NO_REPLACE_FALLBACK", "");
}
env.set("MOZ_EMULATE_ELEVATION_PATH", "");
let status = readStatusFile();
if (process.exitValue != aExpectedExitValue || status != aExpectedStatus) {

View File

@ -41,6 +41,8 @@ if CONFIG['OS_ARCH'] == 'WINNT':
'shlwapi',
'crypt32',
'advapi32',
'ole32',
'rpcrt4',
]
else:
USE_LIBS += [

View File

@ -111,6 +111,7 @@ static bool sUseHardLinks = true;
#ifdef XP_WIN
#include "updatehelper.h"
#include <aclapi.h>
// Closes the handle if valid and if the updater is elevated returns with the
// return code specified. This prevents multiple launches of the callback
@ -2393,8 +2394,9 @@ int NS_main(int argc, NS_tchar **argv)
bool useService = false;
bool testOnlyFallbackKeyExists = false;
bool noServiceFallback = getenv("MOZ_NO_SERVICE_FALLBACK") != nullptr;
bool emulateElevation = getenv("MOZ_EMULATE_ELEVATION_PATH") != nullptr;
putenv(const_cast<char*>("MOZ_NO_SERVICE_FALLBACK="));
putenv(const_cast<char*>("MOZ_EMULATE_ELEVATION_PATH="));
// We never want the service to be used unless we build with
// the maintenance service.
#ifdef MOZ_MAINTENANCE_SERVICE
@ -2484,12 +2486,6 @@ int NS_main(int argc, NS_tchar **argv)
return 1;
}
if (sStagedUpdate) {
LOG(("Performing a staged update"));
} else if (sReplaceRequest) {
LOG(("Performing a replace request"));
}
#ifdef MOZ_WIDGET_GONK
const char *prioEnv = getenv("MOZ_UPDATER_PRIO");
if (prioEnv) {
@ -2618,13 +2614,15 @@ int NS_main(int argc, NS_tchar **argv)
return 1;
}
updateLockFileHandle = CreateFileW(updateLockFilePath,
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_ALWAYS,
FILE_FLAG_DELETE_ON_CLOSE,
nullptr);
if (!emulateElevation) {
updateLockFileHandle = CreateFileW(updateLockFilePath,
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_ALWAYS,
FILE_FLAG_DELETE_ON_CLOSE,
nullptr);
}
NS_tsnprintf(elevatedLockFilePath,
sizeof(elevatedLockFilePath)/sizeof(elevatedLockFilePath[0]),
@ -2775,7 +2773,8 @@ int NS_main(int argc, NS_tchar **argv)
// If the service can't be used when staging and update, make sure that
// the UAC prompt is not shown! In this case, just set the status to
// pending and the update will be applied during the next startup.
if (!useService && sStagedUpdate) {
// When emulateElevation is true fall through to the elevation code path.
if (!useService && sStagedUpdate && !emulateElevation) {
if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
CloseHandle(updateLockFileHandle);
}
@ -2801,35 +2800,145 @@ int NS_main(int argc, NS_tchar **argv)
}
}
// If we didn't want to use the service at all, or if an update was
// already happening, or launching the service command failed, then
DWORD returnCode = 0;
// If we didn't want to use the service at all, or if an update was
// already happening, or launching the service command failed, then
// launch the elevated updater.exe as we do without the service.
// We don't launch the elevated updater in the case that we did have
// write access all along because in that case the only reason we're
// using the service is because we are testing.
if (!useService && !noServiceFallback &&
// using the service is because we are testing.
if (!useService && !noServiceFallback &&
updateLockFileHandle == INVALID_HANDLE_VALUE) {
SHELLEXECUTEINFO sinfo;
memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO));
sinfo.cbSize = sizeof(SHELLEXECUTEINFO);
sinfo.fMask = SEE_MASK_FLAG_NO_UI |
SEE_MASK_FLAG_DDEWAIT |
SEE_MASK_NOCLOSEPROCESS;
sinfo.hwnd = nullptr;
sinfo.lpFile = argv[0];
sinfo.lpParameters = cmdLine;
sinfo.lpVerb = L"runas";
sinfo.nShow = SW_SHOWNORMAL;
bool result = ShellExecuteEx(&sinfo);
free(cmdLine);
if (result) {
WaitForSingleObject(sinfo.hProcess, INFINITE);
CloseHandle(sinfo.hProcess);
// Get a unique directory name to secure
RPC_WSTR guidString = RPC_WSTR(L"");
GUID guid;
HRESULT hr = CoCreateGuid(&guid);
BOOL result = TRUE;
bool safeToUpdate = true;
WCHAR secureUpdaterPath[MAX_PATH + 1] = { L'\0' };
WCHAR secureDirPath[MAX_PATH + 1] = { L'\0' };
if (SUCCEEDED(hr)) {
UuidToString(&guid, &guidString);
result = PathGetSiblingFilePath(secureDirPath, argv[0],
reinterpret_cast<LPCWSTR>(guidString));
RpcStringFree(&guidString);
} else {
WriteStatusFile(ELEVATION_CANCELED);
// This should never happen, but just in case
result = PathGetSiblingFilePath(secureDirPath, argv[0], L"tmp_update");
}
if (!result) {
fprintf(stderr, "Could not obtain secure update directory path");
safeToUpdate = false;
}
// If it's still safe to update, create the directory
if (safeToUpdate) {
result = CreateDirectoryW(secureDirPath, nullptr);
if (!result) {
fprintf(stderr, "Could not create secure update directory");
safeToUpdate = false;
}
}
// If it's still safe to update, get the new updater path
if (safeToUpdate) {
wcsncpy(secureUpdaterPath, secureDirPath, MAX_PATH);
result = PathAppendSafe(secureUpdaterPath, L"updater.exe");
if (!result) {
fprintf(stderr, "Could not obtain secure updater file name");
safeToUpdate = false;
}
}
// If it's still safe to update, copy the file in
if (safeToUpdate) {
result = CopyFileW(argv[0], secureUpdaterPath, TRUE);
if (!result) {
fprintf(stderr, "Could not copy updater to secure location");
safeToUpdate = false;
}
}
// If it's still safe to update, restrict access to the directory item
// itself so that the directory cannot be deleted and re-created,
// nor have its properties modified. Note that this does not disallow
// adding items inside the directory.
HANDLE handle = INVALID_HANDLE_VALUE;
if (safeToUpdate) {
handle = CreateFileW(secureDirPath, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, nullptr);
safeToUpdate = handle != INVALID_HANDLE_VALUE;
}
// If it's still safe to update, deny write access completely to the
// directory.
PACL originalACL = nullptr;
PSECURITY_DESCRIPTOR sd = nullptr;
if (safeToUpdate) {
safeToUpdate = UACHelper::DenyWriteACLOnPath(secureDirPath,
&originalACL, &sd);
}
// If it's still safe to update, verify that there is only updater.exe
// in the directory and nothing else.
if (safeToUpdate) {
if (!UACHelper::IsDirectorySafe(secureDirPath)) {
safeToUpdate = false;
}
}
if (!safeToUpdate) {
fprintf(stderr, "Will not proceed to copy secure updater because it "
"is not safe to do so.");
WriteStatusFile(SECURE_LOCATION_UPDATE_ERROR);
} else {
SHELLEXECUTEINFO sinfo;
memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO));
sinfo.cbSize = sizeof(SHELLEXECUTEINFO);
sinfo.fMask = SEE_MASK_FLAG_NO_UI |
SEE_MASK_FLAG_DDEWAIT |
SEE_MASK_NOCLOSEPROCESS;
sinfo.hwnd = nullptr;
sinfo.lpFile = secureUpdaterPath;
sinfo.lpParameters = cmdLine;
sinfo.lpVerb = emulateElevation ? L"open" : L"runas";
sinfo.nShow = SW_SHOWNORMAL;
bool result = ShellExecuteEx(&sinfo);
free(cmdLine);
if (result) {
WaitForSingleObject(sinfo.hProcess, INFINITE);
// Bubble the elevated updater return code to this updater
GetExitCodeProcess(sinfo.hProcess, &returnCode);
CloseHandle(sinfo.hProcess);
} else {
WriteStatusFile(ELEVATION_CANCELED);
}
}
// All done, revert back the permissions.
if (originalACL) {
SetNamedSecurityInfoW(const_cast<LPWSTR>(secureDirPath), SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION, nullptr, nullptr,
originalACL, nullptr);
}
if (sd) {
LocalFree(sd);
}
// Done with the directory, no need to lock it.
if (INVALID_HANDLE_VALUE != handle) {
CloseHandle(handle);
}
// We no longer need the secure updater and directory
DeleteFileW(secureUpdaterPath);
RemoveDirectoryW(secureDirPath);
}
if (argc > callbackIndex) {
@ -2844,7 +2953,7 @@ int NS_main(int argc, NS_tchar **argv)
// We didn't use the service and we did run the elevated updater.exe.
// The elevated updater.exe is responsible for writing out the
// update.status file.
return 0;
return returnCode;
} else if(useService) {
// The service command was launched. The service is responsible for
// writing out the update.status file.
@ -2866,6 +2975,13 @@ int NS_main(int argc, NS_tchar **argv)
}
#endif
if (sStagedUpdate) {
LOG(("Performing a staged update"));
}
else if (sReplaceRequest) {
LOG(("Performing a replace request"));
}
#if defined(MOZ_WIDGET_GONK)
// In gonk, the master b2g process sets its umask to 0027 because
// there's no reason for it to ever create world-readable files.
@ -3170,7 +3286,7 @@ int NS_main(int argc, NS_tchar **argv)
}
}
}
EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 0);
EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, gSucceeded ? 0 : 1);
#endif /* XP_WIN */
#ifdef XP_MACOSX
if (gSucceeded) {