Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
/**
* This file contains functions that work on top of the RemoteSettings
* Bucket for the IP Protection server list.
*/
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "logConsole", () =>
console.createInstance({
prefix: "IPProtectionServerlist",
maxLogLevel: Services.prefs.getBoolPref("browser.ipProtection.log", false)
? "Debug"
: "Warn",
})
);
ChromeUtils.defineESModuleGetters(lazy, {
IPPStartupCache:
"moz-src:///toolkit/components/ipprotection/IPPStartupCache.sys.mjs",
IPProtectionService:
"moz-src:///toolkit/components/ipprotection/IPProtectionService.sys.mjs",
IPProtectionStates:
"moz-src:///toolkit/components/ipprotection/IPProtectionService.sys.mjs",
});
/**
*
*/
export class IProtocol {
name = "";
static construct(data) {
switch (data.name) {
case "masque":
return new MasqueProtocol(data);
case "connect":
return new ConnectProtocol(data);
default:
throw new Error("Unknown protocol: " + data.name);
}
}
}
/**
*
*/
export class MasqueProtocol extends IProtocol {
name = "masque";
host = "";
port = 0;
templateString = "";
constructor(data) {
super();
this.host = data.host || "";
this.port = data.port || 0;
this.templateString = data.templateString || "";
}
}
/**
*
*/
export class ConnectProtocol extends IProtocol {
name = "connect";
host = "";
port = 0;
scheme = "https";
constructor(data) {
super();
this.host = data.host || "";
this.port = data.port || 0;
this.scheme = data.scheme || "https";
}
}
/**
* Class representing a server.
*/
export class Server {
/**
* Port of the server
*
* @type {number}
*/
port = 443;
/**
* Hostname of the server
*
* @type {string}
*/
hostname = "";
/**
* If true the server is quarantined
* and should not be used
*
* @type {boolean}
*/
quarantined = false;
/**
* List of supported protocols
*
* @type {Array<MasqueProtocol|ConnectProtocol>}
*/
protocols = [];
constructor(data) {
this.port = data.port || 443;
this.hostname = data.hostname || "";
this.quarantined = !!data.quarantined;
this.protocols = (data.protocols || []).map(p => IProtocol.construct(p));
// Default to connect if no protocols are specified
if (this.protocols.length === 0) {
this.protocols = [
new ConnectProtocol({
name: "connect",
host: this.hostname,
port: this.port,
}),
];
}
}
}
/**
* Class representing a city.
*/
class City {
/**
* Fallback name for the city if not available
*
* @type {string}
*/
name = "";
/**
* A stable identifier for the city
* (Usually a Wikidata ID)
*
* @type {string}
*/
code = "";
/**
* List of servers in this city
*
* @type {Server[]}
*/
servers = [];
constructor(data) {
this.name = data.name || "";
this.code = data.code || "";
this.servers = (data.servers || []).map(s => new Server(s));
}
}
/**
* Class representing a country.
*/
class Country {
/**
* Fallback name for the country if not available
*
* @type {string}
*/
name;
/**
* A stable identifier for the country
* Usually a ISO 3166-1 alpha-2 code
*
* @type {string}
*/
code;
/**
* List of cities in this country
*
* @type {City[]}
*/
cities;
constructor(data) {
this.name = data.name || "";
this.code = data.code || "";
this.cities = (data.cities || []).map(c => new City(c));
}
}
/**
* Base Class for the Serverlist
*/
export class IPProtectionServerlistBase {
__list = null;
init() {}
async initOnStartupCompleted() {}
uninit() {}
/**
* Tries to refresh the list from the underlining source.
*
* @param {*} _forceUpdate - if true, forces a refresh even if the list is already populated.
*/
maybeFetchList(_forceUpdate = false) {
throw new Error("Not implemented");
}
/**
* Selects a default location - for alpha this is only the US.
*
* @returns {{Country, City}} - The best country/city to use.
*/
getDefaultLocation() {
/** @type {Country} */
const usa = this.__list.find(country => country.code === "US");
if (!usa) {
return null;
}
const city = usa.cities.find(c => c.servers.length);
if (!city) {
return null;
}
return {
city,
country: usa,
};
}
/**
* Given a city, it selects an available server.
*
* @param {City?} city
* @returns {Server|null}
*/
selectServer(city) {
if (!city) {
return null;
}
const servers = city.servers.filter(server => !server.quarantined);
if (servers.length === 1) {
return servers[0];
}
if (servers.length > 1) {
return servers[Math.floor(Math.random() * servers.length)];
}
return null;
}
get hasList() {
return this.__list.length !== 0;
}
static dataToList(list) {
if (!Array.isArray(list)) {
return [];
}
return list.map(c => new Country(c));
}
}
/**
* Class representing the IP Protection Serverlist
* fetched from Remote Settings.
*/
export class RemoteSettingsServerlist extends IPProtectionServerlistBase {
#bucket = null;
#runningPromise = null;
constructor() {
super();
this.handleEvent = this.#handleEvent.bind(this);
this.__list = IPProtectionServerlistBase.dataToList(
lazy.IPPStartupCache.locationList
);
}
init() {
lazy.IPProtectionService.addEventListener(
"IPProtectionService:StateChanged",
this.handleEvent
);
}
async initOnStartupCompleted() {
this.bucket.on("sync", async () => {
await this.maybeFetchList(true);
});
}
uninit() {
lazy.IPProtectionService.removeEventListener(
"IPProtectionService:StateChanged",
this.handleEvent
);
}
#handleEvent(_event) {
if (lazy.IPProtectionService.state === lazy.IPProtectionStates.READY) {
this.maybeFetchList();
}
}
maybeFetchList(forceUpdate = false) {
if (this.__list.length !== 0 && !forceUpdate) {
return Promise.resolve();
}
if (this.#runningPromise) {
return this.#runningPromise;
}
const fetchList = async () => {
this.__list = IPProtectionServerlistBase.dataToList(
await this.bucket.get()
);
lazy.IPPStartupCache.storeLocationList(this.__list);
};
this.#runningPromise = fetchList().finally(
() => (this.#runningPromise = null)
);
return this.#runningPromise;
}
get bucket() {
if (!this.#bucket) {
this.#bucket = lazy.RemoteSettings("vpn-serverlist");
}
return this.#bucket;
}
}
/**
* Class representing the IP Protection Serverlist
* from about:config preferences.
*/
export class PrefServerList extends IPProtectionServerlistBase {
#observer = null;
constructor() {
super();
this.#observer = this.onPrefChange.bind(this);
this.maybeFetchList();
}
onPrefChange() {
this.maybeFetchList();
}
async initOnStartupCompleted() {
Services.prefs.addObserver(
IPProtectionServerlist.PREF_NAME,
this.#observer
);
}
uninit() {
Services.prefs.removeObserver(
IPProtectionServerlist.PREF_NAME,
this.#observer
);
}
maybeFetchList(_forceUpdate = false) {
this.__list = IPProtectionServerlistBase.dataToList(
PrefServerList.prefValue
);
return Promise.resolve();
}
static get PREF_NAME() {
return "browser.ipProtection.override.serverlist";
}
/**
* Returns true if the preference has a valid value.
*/
static get hasPrefValue() {
return (
Services.prefs.getPrefType(this.PREF_NAME) ===
Services.prefs.PREF_STRING &&
!!Services.prefs.getStringPref(this.PREF_NAME).length
);
}
static get prefValue() {
try {
const value = Services.prefs.getStringPref(this.PREF_NAME);
return JSON.parse(value);
} catch (e) {
lazy.logConsole.error(
`IPProtection: Error parsing serverlist pref value: ${e}`
);
return null;
}
}
}
/**
*
* @returns {IPProtectionServerlistBase} - The appropriate serverlist implementation.
*/
export function IPProtectionServerlistFactory() {
return PrefServerList.hasPrefValue
? new PrefServerList()
: new RemoteSettingsServerlist();
}
// Only check once which implementation to use.
const IPProtectionServerlist = IPProtectionServerlistFactory();
export { IPProtectionServerlist };