IPProtectionServerlist.sys.mjs (8547B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * This file contains functions that work on top of the RemoteSettings 7 * Bucket for the IP Protection server list. 8 */ 9 10 const lazy = {}; 11 12 ChromeUtils.defineESModuleGetters(lazy, { 13 IPPStartupCache: 14 "moz-src:///browser/components/ipprotection/IPPStartupCache.sys.mjs", 15 IPProtectionService: 16 "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs", 17 IPProtectionStates: 18 "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs", 19 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", 20 }); 21 22 /** 23 * 24 */ 25 export class IProtocol { 26 name = ""; 27 static construct(data) { 28 switch (data.name) { 29 case "masque": 30 return new MasqueProtocol(data); 31 case "connect": 32 return new ConnectProtocol(data); 33 default: 34 throw new Error("Unknown protocol: " + data.name); 35 } 36 } 37 } 38 39 /** 40 * 41 */ 42 export class MasqueProtocol extends IProtocol { 43 name = "masque"; 44 host = ""; 45 port = 0; 46 templateString = ""; 47 constructor(data) { 48 super(); 49 this.host = data.host || ""; 50 this.port = data.port || 0; 51 this.templateString = data.templateString || ""; 52 } 53 } 54 55 /** 56 * 57 */ 58 export class ConnectProtocol extends IProtocol { 59 name = "connect"; 60 host = ""; 61 port = 0; 62 scheme = "https"; 63 constructor(data) { 64 super(); 65 this.host = data.host || ""; 66 this.port = data.port || 0; 67 this.scheme = data.scheme || "https"; 68 } 69 } 70 71 /** 72 * Class representing a server. 73 */ 74 export class Server { 75 /** 76 * Port of the server 77 * 78 * @type {number} 79 */ 80 port = 443; 81 /** 82 * Hostname of the server 83 * 84 * @type {string} 85 */ 86 hostname = ""; 87 /** 88 * If true the server is quarantined 89 * and should not be used 90 * 91 * @type {boolean} 92 */ 93 quarantined = false; 94 95 /** 96 * List of supported protocols 97 * 98 * @type {Array<MasqueProtocol|ConnectProtocol>} 99 */ 100 protocols = []; 101 102 constructor(data) { 103 this.port = data.port || 443; 104 this.hostname = data.hostname || ""; 105 this.quarantined = !!data.quarantined; 106 this.protocols = (data.protocols || []).map(p => IProtocol.construct(p)); 107 108 // Default to connect if no protocols are specified 109 if (this.protocols.length === 0) { 110 this.protocols = [ 111 new ConnectProtocol({ 112 name: "connect", 113 host: this.hostname, 114 port: this.port, 115 }), 116 ]; 117 } 118 } 119 } 120 121 /** 122 * Class representing a city. 123 */ 124 class City { 125 /** 126 * Fallback name for the city if not available 127 * 128 * @type {string} 129 */ 130 name = ""; 131 /** 132 * A stable identifier for the city 133 * (Usually a Wikidata ID) 134 * 135 * @type {string} 136 */ 137 code = ""; 138 /** 139 * List of servers in this city 140 * 141 * @type {Server[]} 142 */ 143 servers = []; 144 145 constructor(data) { 146 this.name = data.name || ""; 147 this.code = data.code || ""; 148 this.servers = (data.servers || []).map(s => new Server(s)); 149 } 150 } 151 152 /** 153 * Class representing a country. 154 */ 155 class Country { 156 /** 157 * Fallback name for the country if not available 158 * 159 * @type {string} 160 */ 161 name; 162 /** 163 * A stable identifier for the country 164 * Usually a ISO 3166-1 alpha-2 code 165 * 166 * @type {string} 167 */ 168 code; 169 170 /** 171 * List of cities in this country 172 * 173 * @type {City[]} 174 */ 175 cities; 176 177 constructor(data) { 178 this.name = data.name || ""; 179 this.code = data.code || ""; 180 this.cities = (data.cities || []).map(c => new City(c)); 181 } 182 } 183 184 /** 185 * Base Class for the Serverlist 186 */ 187 export class IPProtectionServerlistBase { 188 __list = null; 189 190 init() {} 191 192 async initOnStartupCompleted() {} 193 194 uninit() {} 195 196 /** 197 * Tries to refresh the list from the underlining source. 198 * 199 * @param {*} _forceUpdate - if true, forces a refresh even if the list is already populated. 200 */ 201 maybeFetchList(_forceUpdate = false) { 202 throw new Error("Not implemented"); 203 } 204 205 /** 206 * Selects a default location - for alpha this is only the US. 207 * 208 * @returns {{Country, City}} - The best country/city to use. 209 */ 210 getDefaultLocation() { 211 /** @type {Country} */ 212 const usa = this.__list.find(country => country.code === "US"); 213 if (!usa) { 214 return null; 215 } 216 217 const city = usa.cities.find(c => c.servers.length); 218 return { 219 city, 220 country: usa, 221 }; 222 } 223 224 /** 225 * Given a city, it selects an available server. 226 * 227 * @param {City?} city 228 * @returns {Server|null} 229 */ 230 selectServer(city) { 231 if (!city) { 232 return null; 233 } 234 235 const servers = city.servers.filter(server => !server.quarantined); 236 if (servers.length === 1) { 237 return servers[0]; 238 } 239 240 if (servers.length > 1) { 241 return servers[Math.floor(Math.random() * servers.length)]; 242 } 243 244 return null; 245 } 246 247 get hasList() { 248 return this.__list.length !== 0; 249 } 250 251 static dataToList(list) { 252 if (!Array.isArray(list)) { 253 return []; 254 } 255 return list.map(c => new Country(c)); 256 } 257 } 258 259 /** 260 * Class representing the IP Protection Serverlist 261 * fetched from Remote Settings. 262 */ 263 export class RemoteSettingsServerlist extends IPProtectionServerlistBase { 264 #bucket = null; 265 #runningPromise = null; 266 267 constructor() { 268 super(); 269 this.handleEvent = this.#handleEvent.bind(this); 270 this.__list = IPProtectionServerlistBase.dataToList( 271 lazy.IPPStartupCache.locationList 272 ); 273 } 274 init() { 275 lazy.IPProtectionService.addEventListener( 276 "IPProtectionService:StateChanged", 277 this.handleEvent 278 ); 279 } 280 281 async initOnStartupCompleted() { 282 this.bucket.on("sync", async () => { 283 await this.maybeFetchList(true); 284 }); 285 } 286 287 uninit() { 288 lazy.IPProtectionService.removeEventListener( 289 "IPProtectionService:StateChanged", 290 this.handleEvent 291 ); 292 } 293 294 #handleEvent(_event) { 295 if (lazy.IPProtectionService.state === lazy.IPProtectionStates.READY) { 296 this.maybeFetchList(); 297 } 298 } 299 300 maybeFetchList(forceUpdate = false) { 301 if (this.__list.length !== 0 && !forceUpdate) { 302 return Promise.resolve(); 303 } 304 305 if (this.#runningPromise) { 306 return this.#runningPromise; 307 } 308 309 const fetchList = async () => { 310 this.__list = IPProtectionServerlistBase.dataToList( 311 await this.bucket.get() 312 ); 313 314 lazy.IPPStartupCache.storeLocationList(this.__list); 315 }; 316 317 this.#runningPromise = fetchList().finally( 318 () => (this.#runningPromise = null) 319 ); 320 321 return this.#runningPromise; 322 } 323 324 get bucket() { 325 if (!this.#bucket) { 326 this.#bucket = lazy.RemoteSettings("vpn-serverlist"); 327 } 328 return this.#bucket; 329 } 330 } 331 /** 332 * Class representing the IP Protection Serverlist 333 * from about:config preferences. 334 */ 335 export class PrefServerList extends IPProtectionServerlistBase { 336 #observer = null; 337 338 constructor() { 339 super(); 340 this.#observer = this.onPrefChange.bind(this); 341 this.maybeFetchList(); 342 } 343 344 onPrefChange() { 345 this.maybeFetchList(); 346 } 347 348 async initOnStartupCompleted() { 349 Services.prefs.addObserver( 350 IPProtectionServerlist.PREF_NAME, 351 this.#observer 352 ); 353 } 354 355 uninit() { 356 Services.prefs.removeObserver( 357 IPProtectionServerlist.PREF_NAME, 358 this.#observer 359 ); 360 } 361 maybeFetchList(_forceUpdate = false) { 362 this.__list = IPProtectionServerlistBase.dataToList( 363 PrefServerList.prefValue 364 ); 365 return Promise.resolve(); 366 } 367 368 static get PREF_NAME() { 369 return "browser.ipProtection.override.serverlist"; 370 } 371 /** 372 * Returns true if the preference has a valid value. 373 */ 374 static get hasPrefValue() { 375 return ( 376 Services.prefs.getPrefType(this.PREF_NAME) === 377 Services.prefs.PREF_STRING && 378 !!Services.prefs.getStringPref(this.PREF_NAME).length 379 ); 380 } 381 static get prefValue() { 382 try { 383 const value = Services.prefs.getStringPref(this.PREF_NAME); 384 return JSON.parse(value); 385 } catch (e) { 386 console.error(`IPProtection: Error parsing serverlist pref value: ${e}`); 387 return null; 388 } 389 } 390 } 391 /** 392 * 393 * @returns {IPProtectionServerlistBase} - The appropriate serverlist implementation. 394 */ 395 export function IPProtectionServerlistFactory() { 396 return PrefServerList.hasPrefValue 397 ? new PrefServerList() 398 : new RemoteSettingsServerlist(); 399 } 400 401 // Only check once which implementation to use. 402 const IPProtectionServerlist = IPProtectionServerlistFactory(); 403 404 export { IPProtectionServerlist };