Skip to content
Snippets Groups Projects
Verified Commit 0d7b5f22 authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame :jack_o_lantern:
Browse files

Bug 40458: Implement .tor.onion aliases

We have enabled HTTPS-Only mode, therefore we do not need
HTTPS-Everywhere anymore.
However, we want to keep supporting .tor.onion aliases (especially for
securedrop).
Therefore, in this patch we implemented the parsing of HTTPS-Everywhere
rulesets, and the redirect of .tor.onion domains.
Actually, Tor Browser believes they are actual domains. We change them
on the fly on the SOCKS proxy requests to resolve the domain, and on
the code that verifies HTTPS certificates.
parent 5aef813d
Branches
Tags
1 merge request!636Bug 41757: Rebased Tor Browser alpha to 102.11.0esr
Showing
with 2029 additions and 0 deletions
......@@ -56,6 +56,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
Normandy: "resource://normandy/Normandy.jsm",
OnboardingMessageProvider:
"resource://activity-stream/lib/OnboardingMessageProvider.jsm",
OnionAliasStore: "resource:///modules/OnionAliasStore.jsm",
OsEnvironment: "resource://gre/modules/OsEnvironment.jsm",
PageActions: "resource:///modules/PageActions.jsm",
PageThumbs: "resource://gre/modules/PageThumbs.jsm",
......@@ -658,6 +659,19 @@ let JSWINDOWACTORS = {
enablePreference: "accessibility.blockautorefresh",
},
Rulesets: {
parent: {
moduleURI: "resource:///modules/RulesetsParent.jsm",
},
child: {
moduleURI: "resource:///modules/RulesetsChild.jsm",
events: {
DOMWindowCreated: {},
},
},
matches: ["about:rulesets*"],
},
ScreenshotsComponent: {
parent: {
moduleURI: "resource:///modules/ScreenshotsUtils.jsm",
......@@ -2002,6 +2016,7 @@ BrowserGlue.prototype = {
() => RFPHelper.uninit(),
() => UpdateListener.reset(),
() => OnionAliasStore.uninit(),
];
for (let task of tasks) {
......@@ -2627,6 +2642,33 @@ BrowserGlue.prototype = {
},
},
{
task: () => {
const { TorConnect, TorConnectTopics } = ChromeUtils.import(
"resource:///modules/TorConnect.jsm"
);
if (!TorConnect.shouldShowTorConnect) {
// we will take this path when the user is using the legacy tor launcher or
// when Tor Browser didn't launch its own tor.
OnionAliasStore.init();
} else {
// this path is taken when using about:torconnect, we wait to init
// after we are bootstrapped and connected to tor
const topic = TorConnectTopics.BootstrapComplete;
let bootstrapObserver = {
observe(aSubject, aTopic, aData) {
if (aTopic === topic) {
OnionAliasStore.init();
// we only need to init once, so remove ourselves as an obvserver
Services.obs.removeObserver(this, topic);
}
},
};
Services.obs.addObserver(bootstrapObserver, topic);
}
},
},
{
task: () => {
TabUnloader.init();
......
......
......@@ -90,6 +90,11 @@ static const RedirEntry kRedirMap[] = {
{"robots", "chrome://browser/content/aboutRobots.xhtml",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT},
{"rulesets", "chrome://browser/content/rulesets/aboutRulesets.html",
nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS |
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::IS_SECURE_CHROME_UI},
{"sessionrestore", "chrome://browser/content/aboutSessionRestore.xhtml",
nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT |
nsIAboutModule::IS_SECURE_CHROME_UI},
......
......
......@@ -21,6 +21,7 @@ pages = [
'restartrequired',
'rights',
'robots',
'rulesets',
'sessionrestore',
'tabcrashed',
'torconnect',
......
......
......@@ -48,6 +48,7 @@ DIRS += [
"prompts",
"protocolhandler",
"resistfingerprinting",
"rulesets",
"screenshots",
"search",
"securitylevel",
......
......
// Copyright (c) 2022, The Tor Project, Inc.
"use strict";
const EXPORTED_SYMBOLS = ["OnionAliasStore", "OnionAliasStoreTopics"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { setTimeout, clearTimeout } = ChromeUtils.import(
"resource://gre/modules/Timer.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"JSONFile",
"resource://gre/modules/JSONFile.jsm"
);
Cu.importGlobalProperties(["crypto", "fetch"]);
/* OnionAliasStore observer topics */
const OnionAliasStoreTopics = Object.freeze({
ChannelsChanged: "onionaliasstore:channels-changed",
});
const SECURE_DROP = {
name: "SecureDropTorOnion2021",
pathPrefix: "https://securedrop.org/https-everywhere-2021/",
jwk: {
kty: "RSA",
e: "AQAB",
n:
"vsC7BNafkRe8Uh1DUgCkv6RbPQMdJgAKKnWdSqQd7tQzU1mXfmo_k1Py_2MYMZXOWmqSZ9iwIYkykZYywJ2VyMGve4byj1sLn6YQoOkG8g5Z3V4y0S2RpEfmYumNjTzfq8nxtLnwjaYd4sCUd5wa0SzeLrpRQuXo2bF3QuUF2xcbLJloxX1MmlsMMCdBc-qGNonLJ7bpn_JuyXlDWy1Fkeyw1qgjiOdiRIbMC1x302zgzX6dSrBrNB8Cpsh-vCE0ZjUo8M9caEv06F6QbYmdGJHM0ZZY34OHMSNdf-_qUKIV_SuxuSuFE99tkAeWnbWpyI1V-xhVo1sc7NzChP8ci2TdPvI3_0JyAuCvL6zIFqJUJkZibEUghhg6F09-oNJKpy7rhUJq7zZyLXJsvuXnn0gnIxfjRvMcDfZAKUVMZKRdw7fwWzwQril4Ib0MQOVda9vb_4JMk7Gup-TUI4sfuS4NKwsnKoODIO-2U5QpJWdtp1F4AQ1pBv8ajFl1WTrVGvkRGK0woPWaO6pWyJ4kRnhnxrV2FyNNt3JSR-0JEjhFWws47kjBvpr0VRiVRFppKA-plKs4LPlaaCff39TleYmY3mETe3w1GIGc2Lliad32Jpbx496IgDe1K3FMBEoKFZfhmtlRSXft8NKgSzPt2zkatM9bFKfaCYRaSy7akbk",
},
scope: /^https?:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.securedrop\.tor\.onion\//,
enabled: true,
mappings: [],
currentTimestamp: 0,
};
const kPrefOnionAliasEnabled = "browser.urlbar.onionRewrites.enabled";
// Logger adapted from CustomizableUI.jsm
const kPrefOnionAliasDebug = "browser.onionalias.debug";
XPCOMUtils.defineLazyPreferenceGetter(
this,
"gDebuggingEnabled",
kPrefOnionAliasDebug,
false,
(pref, oldVal, newVal) => {
if (typeof log != "undefined") {
log.maxLogLevel = newVal ? "all" : "log";
}
}
);
XPCOMUtils.defineLazyGetter(this, "log", () => {
const { ConsoleAPI } = ChromeUtils.import(
"resource://gre/modules/Console.jsm"
);
let consoleOptions = {
maxLogLevel: gDebuggingEnabled ? "all" : "log",
prefix: "OnionAlias",
};
return new ConsoleAPI(consoleOptions);
});
// Inspired by aboutMemory.js and PingCentre.jsm
function gunzip(buffer) {
return new Promise((resolve, reject) => {
const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
Ci.nsIStreamLoader
);
listener.init({
onStreamComplete(loader, context, status, length, result) {
resolve(String.fromCharCode(...result));
},
});
const scs = Cc["@mozilla.org/streamConverters;1"].getService(
Ci.nsIStreamConverterService
);
const converter = scs.asyncConvertData(
"gzip",
"uncompressed",
listener,
null
);
const stream = Cc[
"@mozilla.org/io/arraybuffer-input-stream;1"
].createInstance(Ci.nsIArrayBufferInputStream);
stream.setData(buffer, 0, buffer.byteLength);
converter.onStartRequest(null, null);
converter.onDataAvailable(null, stream, 0, buffer.byteLength);
converter.onStopRequest(null, null, null);
});
}
class Channel {
static get SIGN_ALGORITHM() {
return {
name: "RSA-PSS",
saltLength: 32,
hash: { name: "SHA-256" },
};
}
constructor(name, pathPrefix, jwk, scope, enabled) {
this.name = name;
this.pathPrefix = pathPrefix;
this.jwk = jwk;
this.scope = scope;
this._enabled = enabled;
this.mappings = [];
this.currentTimestamp = 0;
this.latestTimestamp = 0;
}
async updateLatestTimestamp() {
const timestampUrl = this.pathPrefix + "/latest-rulesets-timestamp";
log.debug(`Updating ${this.name} timestamp from ${timestampUrl}`);
const response = await fetch(timestampUrl);
if (!response.ok) {
throw Error(`Could not fetch timestamp for ${this.name}`, {
cause: response.status,
});
}
const timestampStr = await response.text();
const timestamp = parseInt(timestampStr);
// Avoid hijacking, sanitize the timestamp
if (isNaN(timestamp)) {
throw Error("Latest timestamp is not a number");
}
log.debug(`Updated ${this.name} timestamp: ${timestamp}`);
this.latestTimestamp = timestamp;
}
async makeKey() {
return crypto.subtle.importKey(
"jwk",
this.jwk,
Channel.SIGN_ALGORITHM,
false,
["verify"]
);
}
async downloadVerifiedRules() {
log.debug(`Downloading and verifying ruleset for ${this.name}`);
const key = await this.makeKey();
const signatureUrl =
this.pathPrefix + `/rulesets-signature.${this.latestTimestamp}.sha256`;
const signatureResponse = await fetch(signatureUrl);
if (!signatureResponse.ok) {
throw Error("Could not fetch the rules signature");
}
const signature = await signatureResponse.arrayBuffer();
const rulesUrl =
this.pathPrefix + `/default.rulesets.${this.latestTimestamp}.gz`;
const rulesResponse = await fetch(rulesUrl);
if (!rulesResponse.ok) {
throw Error("Could not fetch rules");
}
const rulesGz = await rulesResponse.arrayBuffer();
if (
!(await crypto.subtle.verify(
Channel.SIGN_ALGORITHM,
key,
signature,
rulesGz
))
) {
throw Error("Could not verify rules signature");
}
log.debug(
`Downloaded and verified rules for ${this.name}, now uncompressing`
);
this._makeMappings(JSON.parse(await gunzip(rulesGz)));
}
_makeMappings(rules) {
const toTest = /^https?:\/\/[a-zA-Z0-9\.]{56}\.onion$/;
const mappings = [];
rules.rulesets.forEach(rule => {
if (rule.rule.length != 1) {
log.warn(`Unsupported rule lenght: ${rule.rule.length}`);
return;
}
if (!toTest.test(rule.rule[0].to)) {
log.warn(
`Ignoring rule, because of a malformed to: ${rule.rule[0].to}`
);
return;
}
let toHostname;
try {
const toUrl = new URL(rule.rule[0].to);
toHostname = toUrl.hostname;
} catch (err) {
log.error(
"Cannot detect the hostname from the to rule",
rule.rule[0].to,
err
);
}
let fromRe;
try {
fromRe = new RegExp(rule.rule[0].from);
} catch (err) {
log.error("Malformed from field", rule.rule[0].from, err);
return;
}
for (const target of rule.target) {
if (
target.endsWith(".tor.onion") &&
this.scope.test(`http://${target}/`) &&
fromRe.test(`http://${target}/`)
) {
mappings.push([target, toHostname]);
} else {
log.warn("Ignoring malformed rule", rule);
}
}
});
this.mappings = mappings;
this.currentTimestamp = rules.timestamp;
log.debug(`Updated mappings for ${this.name}`, mappings);
}
async updateMappings(force) {
force = force === undefined ? false : !!force;
if (!this._enabled && !force) {
return;
}
await this.updateLatestTimestamp();
if (this.latestTimestamp <= this.currentTimestamp && !force) {
log.debug(
`Rules for ${this.name} are already up to date, skipping update`
);
return;
}
await this.downloadVerifiedRules();
}
get enabled() {
return this._enabled;
}
set enabled(enabled) {
this._enabled = enabled;
if (!enabled) {
this.mappings = [];
this.currentTimestamp = 0;
this.latestTimestamp = 0;
}
}
toJSON() {
let scope = this.scope.toString();
scope = scope.substr(1, scope.length - 2);
return {
name: this.name,
pathPrefix: this.pathPrefix,
jwk: this.jwk,
scope,
enabled: this._enabled,
mappings: this.mappings,
currentTimestamp: this.currentTimestamp,
};
}
static fromJSON(obj) {
let channel = new Channel(
obj.name,
obj.pathPrefix,
obj.jwk,
new RegExp(obj.scope),
obj.enabled
);
if (obj.enabled) {
channel.mappings = obj.mappings;
channel.currentTimestamp = obj.currentTimestamp;
}
return channel;
}
}
class _OnionAliasStore {
static get RULESET_CHECK_INTERVAL() {
return 86400 * 1000; // 1 day, like HTTPS-Everywhere
}
constructor() {
this._channels = new Map();
this._rulesetTimeout = null;
this._lastCheck = 0;
this._storage = null;
}
async init() {
await this._loadSettings();
if (this.enabled) {
await this._startUpdates();
}
Services.prefs.addObserver(kPrefOnionAliasEnabled, this);
}
uninit() {
this._clear();
if (this._rulesetTimeout) {
clearTimeout(this._rulesetTimeout);
}
this._rulesetTimeout = null;
Services.prefs.removeObserver(kPrefOnionAliasEnabled, this);
}
async getChannels() {
if (this._storage === null) {
await this._loadSettings();
}
return Array.from(this._channels.values(), ch => ch.toJSON());
}
async setChannel(chanData) {
const name = chanData.name?.trim();
if (!name) {
throw Error("Name cannot be empty");
}
new URL(chanData.pathPrefix);
const scope = new RegExp(chanData.scope);
const ch = new Channel(
name,
chanData.pathPrefix,
chanData.jwk,
scope,
!!chanData.enabled
);
// Call makeKey to make it throw if the key is invalid
await ch.makeKey();
this._channels.set(name, ch);
this._applyMappings();
this._saveSettings();
setTimeout(this._notifyChanges.bind(this), 1);
return ch;
}
enableChannel(name, enabled) {
const channel = this._channels.get(name);
if (channel !== null) {
channel.enabled = enabled;
this._applyMappings();
this._saveSettings();
this._notifyChanges();
if (this.enabled && enabled && !channel.currentTimestamp) {
this.updateChannel(name);
}
}
}
async updateChannel(name) {
if (!this.enabled) {
throw Error("Onion Aliases are disabled");
}
const channel = this._channels.get(name);
if (channel === null) {
throw Error("Channel not found");
}
await channel.updateMappings(true);
this._saveSettings();
this._applyMappings();
setTimeout(this._notifyChanges.bind(this), 1);
return channel;
}
deleteChannel(name) {
if (this._channels.delete(name)) {
this._saveSettings();
this._applyMappings();
this._notifyChanges();
}
}
async _loadSettings() {
if (this._storage !== null) {
return;
}
this._channels = new Map();
this._storage = new JSONFile({
path: PathUtils.join(
Services.dirsvc.get("ProfD", Ci.nsIFile).path,
"onion-aliases.json"
),
dataPostProcessor: this._settingsProcessor.bind(this),
});
await this._storage.load();
log.debug("Loaded settings", this._storage.data, this._storage.path);
this._applyMappings();
this._notifyChanges();
}
_settingsProcessor(data) {
if ("lastCheck" in data) {
this._lastCheck = data.lastCheck;
} else {
data.lastCheck = 0;
}
if (!("channels" in data) || !Array.isArray(data.channels)) {
data.channels = [SECURE_DROP];
// Force updating
data.lastCheck = 0;
}
const channels = new Map();
data.channels = data.channels.filter(ch => {
try {
channels.set(ch.name, Channel.fromJSON(ch));
} catch (err) {
log.error("Could not load a channel", err, ch);
return false;
}
return true;
});
this._channels = channels;
return data;
}
_saveSettings() {
if (this._storage === null) {
throw Error("Settings have not been loaded");
}
this._storage.data.lastCheck = this._lastCheck;
this._storage.data.channels = Array.from(this._channels.values(), ch =>
ch.toJSON()
);
this._storage.saveSoon();
}
_addMapping(shortOnionHost, longOnionHost) {
const service = Cc["@torproject.org/onion-alias-service;1"].getService(
Ci.IOnionAliasService
);
service.addOnionAlias(shortOnionHost, longOnionHost);
}
_clear() {
const service = Cc["@torproject.org/onion-alias-service;1"].getService(
Ci.IOnionAliasService
);
service.clearOnionAliases();
}
_applyMappings() {
this._clear();
for (const ch of this._channels.values()) {
if (!ch.enabled) {
continue;
}
for (const [short, long] of ch.mappings) {
this._addMapping(short, long);
}
}
}
async _periodicRulesetCheck() {
if (!this.enabled) {
log.debug("Onion Aliases are disabled, not updating rulesets.");
return;
}
log.debug("Begin scheduled ruleset update");
this._lastCheck = Date.now();
let anyUpdated = false;
for (const ch of this._channels.values()) {
if (!ch.enabled) {
log.debug(`Not updating ${ch.name} because not enabled`);
continue;
}
log.debug(`Updating ${ch.name}`);
try {
await ch.updateMappings();
anyUpdated = true;
} catch (err) {
log.error(`Could not update mappings for channel ${ch.name}`, err);
}
}
if (anyUpdated) {
this._saveSettings();
this._applyMappings();
this._notifyChanges();
} else {
log.debug("No channel has been updated, avoid saving");
}
this._scheduleCheck(_OnionAliasStore.RULESET_CHECK_INTERVAL);
}
async _startUpdates() {
// This is a "private" function, so we expect the callers to verify wheter
// onion aliases are enabled.
// Callees will also do, so we avoid an additional check here.
const dt = Date.now() - this._lastCheck;
let force = false;
for (const ch of this._channels.values()) {
if (ch.enabled && !ch.currentTimestamp) {
// Edited while being offline or some other error happened
force = true;
break;
}
}
if (dt > _OnionAliasStore.RULESET_CHECK_INTERVAL || force) {
log.debug(
`Mappings are stale (${dt}), or force check requested (${force}), checking them immediately`
);
await this._periodicRulesetCheck();
} else {
this._scheduleCheck(_OnionAliasStore.RULESET_CHECK_INTERVAL - dt);
}
}
_scheduleCheck(dt) {
if (this._rulesetTimeout) {
log.warn("The previous update timeout was not null");
clearTimeout(this._rulesetTimeout);
}
if (!this.enabled) {
log.warn(
"Ignoring the scheduling of a new check because the Onion Alias feature is currently disabled."
);
this._rulesetTimeout = null;
return;
}
log.debug(`Scheduling ruleset update in ${dt}`);
this._rulesetTimeout = setTimeout(() => {
this._rulesetTimeout = null;
this._periodicRulesetCheck();
}, dt);
}
_notifyChanges() {
Services.obs.notifyObservers(
Array.from(this._channels.values(), ch => ch.toJSON()),
OnionAliasStoreTopics.ChannelsChanged
);
}
get enabled() {
return Services.prefs.getBoolPref(kPrefOnionAliasEnabled, true);
}
observe(aSubject, aTopic, aData) {
if (aTopic === "nsPref:changed") {
if (this.enabled) {
this._startUpdates();
} else if (this._rulesetTimeout) {
clearTimeout(this._rulesetTimeout);
this._rulesetTimeout = null;
}
}
}
}
const OnionAliasStore = new _OnionAliasStore();
JAR_MANIFESTS += ["jar.mn"]
EXTRA_JS_MODULES += [
"OnionAliasStore.jsm",
"OnionLocationChild.jsm",
"OnionLocationParent.jsm",
]
// Copyright (c) 2022, The Tor Project, Inc.
"use strict";
var EXPORTED_SYMBOLS = ["RulesetsChild"];
const { RemotePageChild } = ChromeUtils.import(
"resource://gre/actors/RemotePageChild.jsm"
);
class RulesetsChild extends RemotePageChild {}
// Copyright (c) 2022, The Tor Project, Inc.
"use strict";
var EXPORTED_SYMBOLS = ["RulesetsParent"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
const { OnionAliasStore, OnionAliasStoreTopics } = ChromeUtils.import(
"resource:///modules/OnionAliasStore.jsm"
);
const kShowWarningPref = "torbrowser.rulesets.show_warning";
// This class allows about:rulesets to get TorStrings and to load/save the
// preference for skipping the warning
class RulesetsParent extends JSWindowActorParent {
constructor(...args) {
super(...args);
const self = this;
this.observer = {
observe(aSubject, aTopic, aData) {
const obj = aSubject?.wrappedJSObject;
if (aTopic === OnionAliasStoreTopics.ChannelsChanged && obj) {
self.sendAsyncMessage("rulesets:channels-change", obj);
}
},
};
Services.obs.addObserver(
this.observer,
OnionAliasStoreTopics.ChannelsChanged
);
}
willDestroy() {
Services.obs.removeObserver(
this.observer,
OnionAliasStoreTopics.ChannelsChanged
);
}
async receiveMessage(message) {
switch (message.name) {
// RPMSendAsyncMessage
case "rulesets:delete-channel":
OnionAliasStore.deleteChannel(message.data);
break;
case "rulesets:enable-channel":
OnionAliasStore.enableChannel(message.data.name, message.data.enabled);
break;
case "rulesets:set-show-warning":
Services.prefs.setBoolPref(kShowWarningPref, message.data);
break;
// RPMSendQuery
case "rulesets:get-channels":
return OnionAliasStore.getChannels();
case "rulesets:get-init-args":
return {
TorStrings,
showWarning: Services.prefs.getBoolPref(kShowWarningPref, true),
};
case "rulesets:set-channel":
const ch = await OnionAliasStore.setChannel(message.data);
return ch;
case "rulesets:update-channel":
// We need to catch any error in this way, because in case of an
// exception, RPMSendQuery does not return on the other side
try {
const channel = await OnionAliasStore.updateChannel(message.data);
return channel;
} catch (err) {
console.error("Cannot update the channel", err);
return { error: err.toString() };
}
}
return undefined;
}
}
/* Copyright (c) 2022, The Tor Project, Inc. */
/* General rules */
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
font: message-box;
background-color: var(--in-content-page-background);
color: var(--in-content-page-color);
font-size: 15px;
cursor: default;
}
label {
display: flex;
align-items: center;
padding: 6px 0;
}
input[type=text] {
margin: 0;
width: 360px;
max-width: 100%;
}
textarea {
margin: 0;
width: var(--content-width);
max-width: 100%;
box-sizing: border-box;
}
select, option {
font-weight: 700;
}
dt {
margin: var(--ruleset-vmargin) 0 0 0;
padding: 0;
color: var(--in-content-deemphasized-text);
font-size: 85%;
}
dd {
margin: 8px 0 0 0;
padding: 0;
max-width: 600px;
box-sizing: border-box;
line-height: 1.4;
}
hr {
width: 40px;
margin: 0;
border: none;
border-top: 1px solid var(--in-content-border-color);
}
.hidden {
display: none !important;
}
/* Initial warning */
#warning-wrapper {
display: none;
}
.state-warning #warning-wrapper {
display: flex;
align-items: center;
height: 100%;
}
#warning {
margin-top: -20vh;
padding: 0 160px;
background-image: url("chrome://global/skin/icons/warning.svg");
background-position: 84px 0;
background-repeat: no-repeat;
background-size: 48px;
fill: #ffbd4f;
-moz-context-properties: fill;
}
#warning:dir(rtl) {
background-position: right 84px top 0;
}
#warning-description {
margin: 30px 0 16px 0;
}
#warning-buttonbar {
margin-top: 30px;
text-align: right;
}
/* Actual content */
:root {
--sidebar-width: 320px;
--content-width: 600px;
--ruleset-vmargin: 40px;
}
#main-content {
display: flex;
height: 100%;
}
.state-warning #main-content {
display: none;
}
section {
display: none;
flex: 1 0 auto;
padding: 40px;
}
.title {
display: flex;
align-items: center;
width: var(--content-width);
max-width: 100%;
padding-bottom: 16px;
border-bottom: 1px solid var(--in-content-border-color);
}
.title h1 {
margin: 0;
padding: 0;
padding-inline-start: 35px;
font-size: 20px;
font-weight: 700;
line-height: 30px;
background-image: url("chrome://browser/content/rulesets/securedrop.svg");
background-position: 0 4px;
background-size: 22px;
background-repeat: no-repeat;
}
#main-content h1:dir(rtl) {
background-position: right 0 top 4px;
}
/* Ruleset list */
aside {
display: flex;
flex-direction: column;
flex: 0 0 var(--sidebar-width);
box-sizing: border-box;
border-inline-end: 1px solid var(--in-content-border-color);
background-color: var(--in-content-box-background);
}
#ruleset-heading {
padding: 16px;
text-align: center;
font-weight: 700;
border-bottom: 1px solid var(--in-content-border-color);
}
#ruleset-list-container {
flex: 1;
}
#ruleset-list-empty {
padding: 16px;
text-align: center;
}
#ruleset-list-empty-description {
font-size: 80%;
}
#ruleset-list {
margin: 0;
padding: 0;
}
#ruleset-list li {
display: flex;
align-items: center;
margin: 0;
padding: 10px 18px;
list-style: none;
border-inline-start: 4px solid transparent;
border-bottom: 1px solid var(--in-content-border-color);
}
#ruleset-list li:last-child {
border-bottom: none;
}
#ruleset-list .icon {
width: 16px;
height: 16px;
margin-inline-end: 12px;
background-image: url("chrome://browser/content/rulesets/securedrop.svg");
background-size: 16px;
}
#ruleset-list .icon.has-favicon {
background: transparent;
}
#ruleset-list .name {
font-weight: 700;
}
#ruleset-list .description {
font-size: 85%;
color: var(--in-content-deemphasized-text);
}
#ruleset-list .selected {
border-inline-start-color: var(--in-content-accent-color);
}
#ruleset-list .selected.disabled {
border-inline-start-color: var(--in-content-border-color);
}
#ruleset-list li:not(.selected):hover {
background-color: var(--in-content-button-background-hover);
color: var(--in-content-button-text-color-hover);
}
#ruleset-list li:not(.selected):hover:active {
background-color: var(--in-content-button-background-active);
}
#ruleset-list #ruleset-template {
display: none;
}
/* Ruleset details */
.state-details #ruleset-details {
display: block;
}
#ruleset-jwk-value {
padding: 8px;
border-radius: 2px;
background-color: var(--in-content-box-background);
font-size: 85%;
line-break: anywhere;
}
#ruleset-edit {
margin-inline-start: auto;
padding-inline-start: 32px;
background-image: url("chrome://global/skin/icons/edit.svg");
background-repeat: no-repeat;
background-position: 8px;
-moz-context-properties: fill;
fill: currentColor;
min-width: auto;
flex: 0 0 auto;
}
#ruleset-enable {
margin-top: var(--ruleset-vmargin);
}
#ruleset-buttonbar {
margin: var(--ruleset-vmargin) 0;
}
#ruleset-updated {
margin-top: 24px;
color: var(--in-content-deemphasized-text);
font-size: 85%;
}
/* Edit ruleset */
.state-edit #edit-ruleset {
display: block;
}
#edit-ruleset label {
color: var(--in-content-deemphasized-text);
display: block;
}
#edit-ruleset label, #edit-buttonbar {
margin-top: var(--ruleset-vmargin);
}
label#edit-enable {
display: flex;
align-items: center;
}
<!-- Copyright (c) 2022, The Tor Project, Inc. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" />
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/rulesets/aboutRulesets.css">
</head>
<body>
<!-- Warning -->
<div id="warning-wrapper">
<div id="warning">
<h1 id="warning-title"></h1>
<p id="warning-description"></p>
<p>
<label>
<input id="warning-enable-checkbox" type="checkbox" checked="checked">
<span id="warning-enable-label"></span>
</label>
</p>
<div id="warning-buttonbar">
<button id="warning-button" autofocus="autofocus"></button>
</div>
</div>
</div>
<div id="main-content">
<!-- Ruleset list -->
<aside>
<div id="ruleset-heading"></div>
<div id="ruleset-list-container">
<div id="ruleset-list-empty">
<p id="ruleset-list-empty-title"></p>
<p id="ruleset-list-empty-description"></p>
</div>
<ul id="ruleset-list">
<li id="ruleset-template">
<div class="icon">
</div>
<div>
<div class="name"></div>
<div class="description"></div>
</div>
</li>
</ul>
</div>
</aside>
<!-- Ruleset details -->
<section id="ruleset-details">
<div class="title">
<h1 id="ruleset-title"></h1>
<button id="ruleset-edit" class="ghost-button"></button>
</div>
<dl>
<dt id="ruleset-jwk-label"></dt>
<dd id="ruleset-jwk-value"></dd>
<dt id="ruleset-path-prefix-label"></dt>
<dd>
<a id="ruleset-path-prefix-value" target="_blank"></a>
</dd>
<dt id="ruleset-scope-label"></dt>
<dd id="ruleset-scope-value"></dd>
</dl>
<label id="ruleset-enable">
<input type="checkbox" id="ruleset-enable-checkbox">
<span id="ruleset-enable-label"></span>
</label>
<div id="ruleset-buttonbar">
<button id="ruleset-update-button"></button>
</div>
<hr>
<p id="ruleset-updated"></p>
</section>
<!-- Edit ruleset -->
<section id="edit-ruleset">
<div class="title">
<h1 id="edit-title"></h1>
</div>
<form id="edit-ruleset-form">
<label>
<div id="edit-jwk-label"></div>
<textarea id="edit-jwk-textarea" rows="10"></textarea>
</label>
<label>
<div id="edit-path-prefix-label"></div>
<input id="edit-path-prefix-input" type="text">
</label>
<label>
<div id="edit-scope-label"></div>
<input id="edit-scope-input" type="text">
</label>
<label id="edit-enable">
<input type="checkbox" id="edit-enable-checkbox">
<span id="edit-enable-label"></span>
</label>
<div id="edit-buttonbar">
<button id="edit-save" class="primary"></button>
<button id="edit-cancel"></button>
</div>
</form>
</section>
</div>
<script src="chrome://browser/content/rulesets/aboutRulesets.js"></script>
</body>
</html>
"use strict";
/* globals RPMAddMessageListener, RPMSendQuery, RPMSendAsyncMessage */
let TorStrings;
const Orders = Object.freeze({
Name: "name",
NameDesc: "name-desc",
LastUpdate: "last-update",
});
const States = Object.freeze({
Warning: "warning",
Details: "details",
Edit: "edit",
NoRulesets: "noRulesets",
});
function setUpdateDate(ruleset, element) {
if (!ruleset.enabled) {
element.textContent = TorStrings.rulesets.disabled;
return;
}
if (!ruleset.currentTimestamp) {
element.textContent = TorStrings.rulesets.neverUpdated;
return;
}
const formatter = new Intl.DateTimeFormat(navigator.languages, {
year: "numeric",
month: "long",
day: "numeric",
});
element.textContent = TorStrings.rulesets.lastUpdated.replace(
"%S",
formatter.format(new Date(ruleset.currentTimestamp * 1000))
);
}
class WarningState {
selectors = Object.freeze({
wrapper: "#warning-wrapper",
title: "#warning-title",
description: "#warning-description",
enableCheckbox: "#warning-enable-checkbox",
enableLabel: "#warning-enable-label",
button: "#warning-button",
});
elements = Object.freeze({
wrapper: document.querySelector(this.selectors.wrapper),
title: document.querySelector(this.selectors.title),
description: document.querySelector(this.selectors.description),
enableCheckbox: document.querySelector(this.selectors.enableCheckbox),
enableLabel: document.querySelector(this.selectors.enableLabel),
button: document.querySelector(this.selectors.button),
});
constructor() {
const elements = this.elements;
elements.title.textContent = TorStrings.rulesets.warningTitle;
elements.description.textContent = TorStrings.rulesets.warningDescription;
elements.enableLabel.textContent = TorStrings.rulesets.warningEnable;
elements.button.textContent = TorStrings.rulesets.warningButton;
elements.enableCheckbox.addEventListener(
"change",
this.onEnableChange.bind(this)
);
elements.button.addEventListener("click", this.onButtonClick.bind(this));
}
show() {
this.elements.button.focus();
}
hide() {}
onEnableChange() {
RPMSendAsyncMessage(
"rulesets:set-show-warning",
this.elements.enableCheckbox.checked
);
}
onButtonClick() {
gAboutRulesets.selectFirst();
}
}
class DetailsState {
selectors = Object.freeze({
title: "#ruleset-title",
edit: "#ruleset-edit",
jwkLabel: "#ruleset-jwk-label",
jwkValue: "#ruleset-jwk-value",
pathPrefixLabel: "#ruleset-path-prefix-label",
pathPrefixValue: "#ruleset-path-prefix-value",
scopeLabel: "#ruleset-scope-label",
scopeValue: "#ruleset-scope-value",
enableCheckbox: "#ruleset-enable-checkbox",
enableLabel: "#ruleset-enable-label",
updateButton: "#ruleset-update-button",
updated: "#ruleset-updated",
});
elements = Object.freeze({
title: document.querySelector(this.selectors.title),
edit: document.querySelector(this.selectors.edit),
jwkLabel: document.querySelector(this.selectors.jwkLabel),
jwkValue: document.querySelector(this.selectors.jwkValue),
pathPrefixLabel: document.querySelector(this.selectors.pathPrefixLabel),
pathPrefixValue: document.querySelector(this.selectors.pathPrefixValue),
scopeLabel: document.querySelector(this.selectors.scopeLabel),
scopeValue: document.querySelector(this.selectors.scopeValue),
enableCheckbox: document.querySelector(this.selectors.enableCheckbox),
enableLabel: document.querySelector(this.selectors.enableLabel),
updateButton: document.querySelector(this.selectors.updateButton),
updated: document.querySelector(this.selectors.updated),
});
constructor() {
const elements = this.elements;
elements.edit.textContent = TorStrings.rulesets.edit;
elements.edit.addEventListener("click", this.onEdit.bind(this));
elements.jwkLabel.textContent = TorStrings.rulesets.jwk;
elements.pathPrefixLabel.textContent = TorStrings.rulesets.pathPrefix;
elements.scopeLabel.textContent = TorStrings.rulesets.scope;
elements.enableCheckbox.addEventListener(
"change",
this.onEnable.bind(this)
);
elements.enableLabel.textContent = TorStrings.rulesets.enable;
elements.updateButton.textContent = TorStrings.rulesets.checkUpdates;
elements.updateButton.addEventListener("click", this.onUpdate.bind(this));
}
show(ruleset) {
const elements = this.elements;
elements.title.textContent = ruleset.name;
elements.jwkValue.textContent = JSON.stringify(ruleset.jwk);
elements.pathPrefixValue.setAttribute("href", ruleset.pathPrefix);
elements.pathPrefixValue.textContent = ruleset.pathPrefix;
elements.scopeValue.textContent = ruleset.scope;
elements.enableCheckbox.checked = ruleset.enabled;
if (ruleset.enabled) {
elements.updateButton.removeAttribute("disabled");
} else {
elements.updateButton.setAttribute("disabled", "disabled");
}
setUpdateDate(ruleset, elements.updated);
this._showing = ruleset;
gAboutRulesets.list.setItemSelected(ruleset.name);
}
hide() {
this._showing = null;
}
onEdit() {
gAboutRulesets.setState(States.Edit, this._showing);
}
async onEnable() {
await RPMSendAsyncMessage("rulesets:enable-channel", {
name: this._showing.name,
enabled: this.elements.enableCheckbox.checked,
});
}
async onUpdate() {
try {
await RPMSendQuery("rulesets:update-channel", this._showing.name);
} catch (err) {
console.error("Could not update the rulesets", err);
}
}
}
class EditState {
selectors = Object.freeze({
form: "#edit-ruleset-form",
title: "#edit-title",
nameGroup: "#edit-name-group",
nameLabel: "#edit-name-label",
nameInput: "#edit-name-input",
jwkLabel: "#edit-jwk-label",
jwkTextarea: "#edit-jwk-textarea",
pathPrefixLabel: "#edit-path-prefix-label",
pathPrefixInput: "#edit-path-prefix-input",
scopeLabel: "#edit-scope-label",
scopeInput: "#edit-scope-input",
enableCheckbox: "#edit-enable-checkbox",
enableLabel: "#edit-enable-label",
save: "#edit-save",
cancel: "#edit-cancel",
});
elements = Object.freeze({
form: document.querySelector(this.selectors.form),
title: document.querySelector(this.selectors.title),
jwkLabel: document.querySelector(this.selectors.jwkLabel),
jwkTextarea: document.querySelector(this.selectors.jwkTextarea),
pathPrefixLabel: document.querySelector(this.selectors.pathPrefixLabel),
pathPrefixInput: document.querySelector(this.selectors.pathPrefixInput),
scopeLabel: document.querySelector(this.selectors.scopeLabel),
scopeInput: document.querySelector(this.selectors.scopeInput),
enableCheckbox: document.querySelector(this.selectors.enableCheckbox),
enableLabel: document.querySelector(this.selectors.enableLabel),
save: document.querySelector(this.selectors.save),
cancel: document.querySelector(this.selectors.cancel),
});
constructor() {
const elements = this.elements;
elements.jwkLabel.textContent = TorStrings.rulesets.jwk;
elements.jwkTextarea.setAttribute(
"placeholder",
TorStrings.rulesets.jwkPlaceholder
);
elements.pathPrefixLabel.textContent = TorStrings.rulesets.pathPrefix;
elements.pathPrefixInput.setAttribute(
"placeholder",
TorStrings.rulesets.pathPrefixPlaceholder
);
elements.scopeLabel.textContent = TorStrings.rulesets.scope;
elements.scopeInput.setAttribute(
"placeholder",
TorStrings.rulesets.scopePlaceholder
);
elements.enableLabel.textContent = TorStrings.rulesets.enable;
elements.save.textContent = TorStrings.rulesets.save;
elements.save.addEventListener("click", this.onSave.bind(this));
elements.cancel.textContent = TorStrings.rulesets.cancel;
elements.cancel.addEventListener("click", this.onCancel.bind(this));
}
show(ruleset) {
const elements = this.elements;
elements.form.reset();
elements.title.textContent = ruleset.name;
elements.jwkTextarea.value = JSON.stringify(ruleset.jwk);
elements.pathPrefixInput.value = ruleset.pathPrefix;
elements.scopeInput.value = ruleset.scope;
elements.enableCheckbox.checked = ruleset.enabled;
this._editing = ruleset;
}
hide() {
this.elements.form.reset();
this._editing = null;
}
async onSave(e) {
e.preventDefault();
const elements = this.elements;
let valid = true;
const name = this._editing.name;
let jwk;
try {
jwk = JSON.parse(elements.jwkTextarea.value);
await crypto.subtle.importKey(
"jwk",
jwk,
{
name: "RSA-PSS",
saltLength: 32,
hash: { name: "SHA-256" },
},
true,
["verify"]
);
elements.jwkTextarea.setCustomValidity("");
} catch (err) {
console.error("Invalid JSON or invalid JWK", err);
elements.jwkTextarea.setCustomValidity(TorStrings.rulesets.jwkInvalid);
valid = false;
}
const pathPrefix = elements.pathPrefixInput.value.trim();
try {
const url = new URL(pathPrefix);
if (url.protocol !== "http:" && url.protocol !== "https:") {
elements.pathPrefixInput.setCustomValidity(
TorStrings.rulesets.pathPrefixInvalid
);
valid = false;
} else {
elements.pathPrefixInput.setCustomValidity("");
}
} catch (err) {
console.error("The path prefix is not a valid URL", err);
elements.pathPrefixInput.setCustomValidity(
TorStrings.rulesets.pathPrefixInvalid
);
valid = false;
}
let scope;
try {
scope = new RegExp(elements.scopeInput.value.trim());
elements.scopeInput.setCustomValidity("");
} catch (err) {
elements.scopeInput.setCustomValidity(TorStrings.rulesets.scopeInvalid);
valid = false;
}
if (!valid) {
return;
}
const enabled = elements.enableCheckbox.checked;
const rulesetData = { name, jwk, pathPrefix, scope, enabled };
const ruleset = await RPMSendQuery("rulesets:set-channel", rulesetData);
gAboutRulesets.setState(States.Details, ruleset);
if (enabled) {
try {
await RPMSendQuery("rulesets:update-channel", name);
} catch (err) {
console.warn("Could not update the ruleset after adding it", err);
}
}
}
onCancel(e) {
e.preventDefault();
if (this._editing === null) {
gAboutRulesets.selectFirst();
} else {
gAboutRulesets.setState(States.Details, this._editing);
}
}
}
class NoRulesetsState {
show() {}
hide() {}
}
class RulesetList {
selectors = Object.freeze({
heading: "#ruleset-heading",
list: "#ruleset-list",
emptyContainer: "#ruleset-list-empty",
emptyTitle: "#ruleset-list-empty-title",
emptyDescription: "#ruleset-list-empty-description",
itemTemplate: "#ruleset-template",
itemName: ".name",
itemDescr: ".description",
});
elements = Object.freeze({
heading: document.querySelector(this.selectors.heading),
list: document.querySelector(this.selectors.list),
emptyContainer: document.querySelector(this.selectors.emptyContainer),
emptyTitle: document.querySelector(this.selectors.emptyTitle),
emptyDescription: document.querySelector(this.selectors.emptyDescription),
itemTemplate: document.querySelector(this.selectors.itemTemplate),
});
nameAttribute = "data-name";
rulesets = [];
constructor() {
const elements = this.elements;
// Header
elements.heading.textContent = TorStrings.rulesets.rulesets;
// Empty
elements.emptyTitle.textContent = TorStrings.rulesets.noRulesets;
elements.emptyDescription.textContent = TorStrings.rulesets.noRulesetsDescr;
RPMAddMessageListener(
"rulesets:channels-change",
this.onRulesetsChanged.bind(this)
);
}
getSelectedRuleset() {
const name = this.elements.list
.querySelector(".selected")
?.getAttribute(this.nameAttribute);
for (const ruleset of this.rulesets) {
if (ruleset.name == name) {
return ruleset;
}
}
return null;
}
isEmpty() {
return !this.rulesets.length;
}
async update() {
this.rulesets = await RPMSendQuery("rulesets:get-channels");
await this._populateRulesets();
}
setItemSelected(name) {
name = name.replace(/["\\]/g, "\\$&");
const item = this.elements.list.querySelector(
`.item[${this.nameAttribute}="${name}"]`
);
this._selectItem(item);
}
async _populateRulesets() {
if (this.isEmpty()) {
this.elements.emptyContainer.classList.remove("hidden");
} else {
this.elements.emptyContainer.classList.add("hidden");
}
const list = this.elements.list;
const selName = list
.querySelector(".item.selected")
?.getAttribute(this.nameAttribute);
const items = list.querySelectorAll(".item");
for (const item of items) {
item.remove();
}
for (const ruleset of this.rulesets) {
const item = this._addItem(ruleset);
if (ruleset.name === selName) {
this._selectItem(item);
}
}
}
_addItem(ruleset) {
const item = this.elements.itemTemplate.cloneNode(true);
item.removeAttribute("id");
item.classList.add("item");
item.querySelector(this.selectors.itemName).textContent = ruleset.name;
const descr = item.querySelector(this.selectors.itemDescr);
if (ruleset.enabled) {
setUpdateDate(ruleset, descr);
} else {
descr.textContent = TorStrings.rulesets.disabled;
item.classList.add("disabled");
}
item.setAttribute(this.nameAttribute, ruleset.name);
item.addEventListener("click", () => {
this.onRulesetClick(ruleset);
});
this.elements.list.append(item);
return item;
}
_selectItem(item) {
this.elements.list.querySelector(".selected")?.classList.remove("selected");
item?.classList.add("selected");
}
onRulesetClick(ruleset) {
gAboutRulesets.setState(States.Details, ruleset);
}
onRulesetsChanged(data) {
this.rulesets = data.data;
this._populateRulesets();
const selected = this.getSelectedRuleset();
if (selected !== null) {
gAboutRulesets.setState(States.Details, selected);
}
}
}
class AboutRulesets {
_state = null;
async init() {
const args = await RPMSendQuery("rulesets:get-init-args");
TorStrings = args.TorStrings;
const showWarning = args.showWarning;
this.list = new RulesetList();
this._states = {};
this._states[States.Warning] = new WarningState();
this._states[States.Details] = new DetailsState();
this._states[States.Edit] = new EditState();
this._states[States.NoRulesets] = new NoRulesetsState();
await this.refreshRulesets();
if (showWarning) {
this.setState(States.Warning);
} else {
this.selectFirst();
}
}
setState(state, ...args) {
document.querySelector("body").className = `state-${state}`;
this._state?.hide();
this._state = this._states[state];
this._state.show(...args);
}
async refreshRulesets() {
await this.list.update();
if (this._state === this._states[States.Details]) {
const ruleset = this.list.getSelectedRuleset();
if (ruleset !== null) {
this.setState(States.Details, ruleset);
} else {
this.selectFirst();
}
} else if (this.list.isEmpty()) {
this.setState(States.NoRulesets);
}
}
selectFirst() {
if (this.list.isEmpty()) {
this.setState(States.NoRulesets);
} else {
this.setState("details", this.list.rulesets[0]);
}
}
}
const gAboutRulesets = new AboutRulesets();
gAboutRulesets.init();
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 23.0.5, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 423.3 423.3"
xml:space="preserve"
width="423.29999"
height="423.29999"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
id="defs49">
<defs
id="defs24">
<filter
id="Adobe_OpacityMaskFilter_1_"
filterUnits="userSpaceOnUse"
x="-66"
y="-0.89999998"
width="183.3"
height="318.20001">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix21" />
</filter>
</defs>
<mask
maskUnits="userSpaceOnUse"
x="-66"
y="-0.9"
width="183.3"
height="318.2"
id="mask-4_1_">
<g
class="st4"
id="g27">
<polygon
id="path-3_1_"
class="st2"
points="117.3,-0.9 117.3,317.3 -66,317.3 -66,-0.9 " />
</g>
</mask>
<defs
id="defs36">
<filter
id="Adobe_OpacityMaskFilter_2_"
filterUnits="userSpaceOnUse"
x="-66"
y="-1"
width="366.29999"
height="211.3">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix33" />
</filter>
</defs>
<mask
maskUnits="userSpaceOnUse"
x="-66"
y="-1"
width="366.3"
height="211.3"
id="mask-6_1_">
<g
class="st6"
id="g39">
<polygon
id="path-5_1_"
class="st2"
points="300.3,-1 300.3,210.3 -66,210.3 -66,-1 " />
</g>
</mask>
<defs
id="defs11">
<filter
id="Adobe_OpacityMaskFilter"
filterUnits="userSpaceOnUse"
x="-65.199997"
y="-0.89999998"
width="183.5"
height="318.20001">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix8" />
</filter>
</defs>
<mask
maskUnits="userSpaceOnUse"
x="-65.2"
y="-0.9"
width="183.5"
height="318.2"
id="mask-2_1_">
<g
class="st1"
id="g14">
<polygon
id="path-1_1_"
class="st2"
points="-65.2,317.3 -65.2,-0.9 118.3,-0.9 118.3,317.3 " />
</g>
</mask>
</defs>
<style
type="text/css"
id="style2">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#055FB4;}
.st1{filter:url(#Adobe_OpacityMaskFilter);}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st3{mask:url(#mask-2_1_);fill-rule:evenodd;clip-rule:evenodd;fill:#055FB4;}
.st4{filter:url(#Adobe_OpacityMaskFilter_1_);}
.st5{mask:url(#mask-4_1_);fill-rule:evenodd;clip-rule:evenodd;fill:#093D70;}
.st6{filter:url(#Adobe_OpacityMaskFilter_2_);}
.st7{mask:url(#mask-6_1_);fill-rule:evenodd;clip-rule:evenodd;fill:#2E8AE8;}
</style>
<title
id="title4">Big Logo HP</title>
<circle
style="fill:#ffffff;stroke:none;stroke-width:2.66667"
id="path1626"
r="176.46054"
cy="211.64999"
cx="211.64999" /><path
id="Fill-1"
class="st0"
d="m 327.99999,225.5 -41.8,23.9 0.2,58.5 42.5,-23.6 c 5.1,-2.8 8.3,-8.3 8.3,-14 v -39.7 c -0.2,-0.9 -0.2,-2.1 -0.9,-2.8 -1.9,-2.8 -5.6,-3.9 -8.3,-2.3" /><path
id="Fill-3"
class="st3"
d="m 85.9,173.2 c 0,9.9 -5.3,19 -14,24.1 l -90.7,52.3 V 127.3 l 84,-48.6 c 2.1,-1.1 4.4,-1.8 6.9,-1.8 7.6,0 13.8,6.2 13.8,13.8 z M -65.2,104.9 V 317.3 L 118.3,211.5 V -0.9 Z"
mask="url(#mask-2_1_)"
transform="translate(276.49999,106)" /><path
id="Fill-7"
class="st5"
d="M 71.7,158.3 3.3,118.8 v 14 l 68.4,39.5 v 73.9 L -22.2,192 v -30.1 l 64,37.2 v -13.8 l -64,-37.2 V 75 l 93.8,54.2 v 29.1 z M -66,-0.9 V 211.5 L 117.3,317.3 V 104.9 Z"
mask="url(#mask-4_1_)"
transform="translate(94.499994,106)" /><path
id="Fill-10"
class="st7"
d="m 135,143.2 55.3,-31.1 -62.2,-17.2 c 1.1,-2.1 1.8,-4.4 1.8,-6.6 0,-11.5 -16.7,-21.1 -37.4,-21.1 -20.6,0 -37.4,9.4 -37.4,21.1 0,11.7 16.7,21.1 37.4,21.1 2.8,0 5.3,-0.2 8,-0.5 z M 117,210.3 -66,104.7 117,-1 300.3,104.7 Z"
mask="url(#mask-6_1_)"
transform="translate(94.499994,1)" />
<metadata
id="metadata866"><rdf:RDF><cc:Work
rdf:about=""><dc:title>Big Logo HP</dc:title></cc:Work></rdf:RDF></metadata></svg>
browser.jar:
content/browser/rulesets/aboutRulesets.css (content/aboutRulesets.css)
content/browser/rulesets/aboutRulesets.html (content/aboutRulesets.html)
content/browser/rulesets/aboutRulesets.js (content/aboutRulesets.js)
content/browser/rulesets/securedrop.svg (content/securedrop.svg)
JAR_MANIFESTS += ['jar.mn']
EXTRA_JS_MODULES += [
'RulesetsChild.jsm',
'RulesetsParent.jsm',
]
......@@ -1546,6 +1546,11 @@
value: true
mirror: always
- name: browser.urlbar.onionRewrites.enabled
type: RelaxedAtomicBool
value: true
mirror: always
- name: browser.viewport.desktopWidth
type: RelaxedAtomicInt32
value: 980
......
......
......@@ -658,3 +658,14 @@ if link_service:
'singleton': True,
}, **link_service)
]
Classes += [
{
'cid': '{0df7784b-7316-486d-bc99-bf47b7a05974}',
'contract_ids': ['@torproject.org/onion-alias-service;1'],
'singleton': True,
'type': 'IOnionAliasService',
'constructor': 'torproject::OnionAliasService::GetSingleton',
'headers': ['torproject/OnionAliasService.h'],
},
]
......@@ -845,4 +845,14 @@
} \
}
// Onion alias service implementing IOnionAliasService
#define ONIONALIAS_CONTRACTID \
"@torproject.org/onion-alias-service;1"
#define ONIONALIAS_CID \
{ /* 0df7784b-7316-486d-bc99-bf47b7a05974 */ \
0x0df7784b, 0x7316, 0x486d, { \
0xbc, 0x99, 0xbf, 0x47, 0xb7, 0xa0, 0x59, 0x74 \
} \
}
#endif // nsNetCID_h__
#include "nsISupports.idl"
/**
* Service used for .tor.onion aliases.
* It stores the real .onion address that correspond to .tor.onion addresses,
* so that both C++ code and JS can access them.
*/
[scriptable, uuid(0df7784b-7316-486d-bc99-bf47b7a05974)]
interface IOnionAliasService : nsISupports
{
/**
* Add a new Onion alias
* @param aShortHostname
* The short hostname that is being rewritten
* @param aLongHostname
* The complete onion v3 hostname
*/
void addOnionAlias(in ACString aShortHostname,
in ACString aLongHostname);
/**
* Return an onion alias.
*
* @param aShortHostname
* The .tor.onion hostname to resolve
* @return a v3 address, or the input, if the short hostname is not known
*/
ACString getOnionAlias(in ACString aShortHostname);
/**
* Clears Onion aliases.
*/
void clearOnionAliases();
};
#include "torproject/OnionAliasService.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPrefs_browser.h"
#include "nsUnicharUtils.h"
/**
* Check if a hostname is a valid Onion v3 hostname.
*
* @param aHostname
* The hostname to verify. It is not a const reference because any
* uppercase character will be transformed to lowercase during the
* verification.
* @return Tells whether the input string is an Onion v3 address
*/
static bool ValidateOnionV3(nsACString& aHostname) {
constexpr nsACString::size_type v3Length = 56 + 6;
if (aHostname.Length() != v3Length) {
return false;
}
ToLowerCase(aHostname);
if (!StringEndsWith(aHostname, ".onion"_ns)) {
return false;
}
const char* cur = aHostname.BeginWriting();
// We have already checked that it ends by ".onion"
const char* end = aHostname.EndWriting() - 6;
for (; cur < end; ++cur) {
if (!(islower(*cur) || ('2' <= *cur && *cur <= '7'))) {
return false;
}
}
return true;
}
namespace torproject {
NS_IMPL_ISUPPORTS(OnionAliasService, IOnionAliasService)
static mozilla::StaticRefPtr<OnionAliasService> gOAService;
// static
already_AddRefed<IOnionAliasService> OnionAliasService::GetSingleton() {
if (gOAService) {
return do_AddRef(gOAService);
}
gOAService = new OnionAliasService();
ClearOnShutdown(&gOAService);
return do_AddRef(gOAService);
}
NS_IMETHODIMP
OnionAliasService::AddOnionAlias(const nsACString& aShortHostname,
const nsACString& aLongHostname) {
nsAutoCString shortHostname;
ToLowerCase(aShortHostname, shortHostname);
mozilla::UniquePtr<nsAutoCString> longHostname =
mozilla::MakeUnique<nsAutoCString>(aLongHostname);
if (!longHostname) {
return NS_ERROR_OUT_OF_MEMORY;
}
if (!StringEndsWith(shortHostname, ".tor.onion"_ns) ||
!ValidateOnionV3(*longHostname)) {
return NS_ERROR_INVALID_ARG;
}
mozilla::AutoWriteLock lock(mLock);
mOnionAliases.InsertOrUpdate(shortHostname, std::move(longHostname));
return NS_OK;
}
NS_IMETHODIMP
OnionAliasService::GetOnionAlias(const nsACString& aShortHostname,
nsACString& aLongHostname) {
aLongHostname = aShortHostname;
if (mozilla::StaticPrefs::browser_urlbar_onionRewrites_enabled() &&
StringEndsWith(aShortHostname, ".tor.onion"_ns)) {
nsAutoCString* alias = nullptr;
// We want to keep the string stored in the map alive at least until we
// finish to copy it to the output parameter.
mozilla::AutoReadLock lock(mLock);
if (mOnionAliases.Get(aShortHostname, &alias)) {
// We take for granted aliases have already been validated
aLongHostname.Assign(*alias);
}
}
return NS_OK;
}
NS_IMETHODIMP
OnionAliasService::ClearOnionAliases() {
mozilla::AutoWriteLock lock(mLock);
mOnionAliases.Clear();
return NS_OK;
}
} // namespace torproject
#ifndef OnionAliasService_h_
#define OnionAliasService_h_
#include "IOnionAliasService.h"
#include "mozilla/RWLock.h"
#include "nsClassHashtable.h"
#include "nsHashKeys.h"
#include "ScopedNSSTypes.h"
namespace torproject {
class OnionAliasService final : public IOnionAliasService {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_IONIONALIASSERVICE
static already_AddRefed<IOnionAliasService> GetSingleton();
private:
OnionAliasService() = default;
OnionAliasService(const OnionAliasService&) = delete;
OnionAliasService(OnionAliasService&&) = delete;
OnionAliasService &operator=(const OnionAliasService&) = delete;
OnionAliasService &operator=(OnionAliasService&&) = delete;
virtual ~OnionAliasService() = default;
// mLock protects access to mOnionAliases
mozilla::RWLock mLock{"OnionAliasService.mLock"};
// AutoCStrings have a 64 byte buffer, so it is advised not to use them for
// long storage. However, it is enough to contain onion addresses, so we use
// them instead, and avoid allocating on heap for each alias
nsClassHashtable<nsCStringHashKey, nsAutoCString> mOnionAliases;
};
}
#endif // OnionAliasService_h_
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment