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/. */
5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
6 import { ServiceRequest } from "resource://gre/modules/ServiceRequest.sys.mjs";
7 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
11 ChromeUtils.defineESModuleGetters(lazy, {
12 SharedUtils: "resource://services-settings/SharedUtils.sys.mjs",
15 XPCOMUtils.defineLazyServiceGetter(
17 "CaptivePortalService",
18 "@mozilla.org/network/captive-portal-service;1",
19 "nsICaptivePortalService"
21 XPCOMUtils.defineLazyServiceGetter(
23 "gNetworkLinkService",
24 "@mozilla.org/network/network-link-service;1",
25 "nsINetworkLinkService"
28 // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
29 // See LOG_LEVELS in Console.sys.mjs. Common examples: "all", "debug", "info",
32 const { ConsoleAPI } = ChromeUtils.importESModule(
33 "resource://gre/modules/Console.sys.mjs"
35 return new ConsoleAPI({
37 maxLogLevelPref: "services.settings.loglevel",
38 prefix: "services.settings",
42 ChromeUtils.defineLazyGetter(lazy, "isRunningTests", () => {
43 if (Services.env.get("MOZ_DISABLE_NONLOCAL_CONNECTIONS") === "1") {
44 // Allow to override the server URL if non-local connections are disabled,
45 // usually true when running tests.
51 // Overriding the server URL is normally disabled on Beta and Release channels,
52 // except under some conditions.
53 ChromeUtils.defineLazyGetter(lazy, "allowServerURLOverride", () => {
54 if (!AppConstants.RELEASE_OR_BETA) {
55 // Always allow to override the server URL on Nightly/DevEdition.
59 if (lazy.isRunningTests) {
63 if (Services.env.get("MOZ_REMOTE_SETTINGS_DEVTOOLS") === "1") {
64 // Allow to override the server URL when using remote settings devtools.
68 if (lazy.gServerURL != AppConstants.REMOTE_SETTINGS_SERVER_URL) {
69 log.warn("Ignoring preference override of remote settings server");
71 "Allow by setting MOZ_REMOTE_SETTINGS_DEVTOOLS=1 in the environment"
78 XPCOMUtils.defineLazyPreferenceGetter(
81 "services.settings.server",
82 AppConstants.REMOTE_SETTINGS_SERVER_URL
85 XPCOMUtils.defineLazyPreferenceGetter(
88 "services.settings.preview_enabled",
92 function _isUndefined(value) {
93 return typeof value === "undefined";
98 return lazy.allowServerURLOverride
100 : AppConstants.REMOTE_SETTINGS_SERVER_URL;
103 CHANGES_PATH: "/buckets/monitor/collections/changes/changeset",
110 get shouldSkipRemoteActivityDueToTests() {
112 (lazy.isRunningTests || Cu.isInAutomation) &&
113 this.SERVER_URL == "data:,#remote-settings-dummy/v1"
117 get CERT_CHAIN_ROOT_IDENTIFIER() {
118 if (this.SERVER_URL == AppConstants.REMOTE_SETTINGS_SERVER_URL) {
119 return Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot;
121 if (this.SERVER_URL.includes("allizom.")) {
122 return Ci.nsIContentSignatureVerifier.ContentSignatureStageRoot;
124 if (this.SERVER_URL.includes("dev.")) {
125 return Ci.nsIContentSignatureVerifier.ContentSignatureDevRoot;
127 if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
128 return Ci.nsIX509CertDB.AppXPCShellRoot;
130 return Ci.nsIContentSignatureVerifier.ContentSignatureLocalRoot;
134 // Load dumps only if pulling data from the production server, or in tests.
136 this.SERVER_URL == AppConstants.REMOTE_SETTINGS_SERVER_URL ||
142 // We want to offer the ability to set preview mode via a preference
143 // for consumers who want to pull from the preview bucket on startup.
144 if (_isUndefined(this._previewModeEnabled) && lazy.allowServerURLOverride) {
145 return lazy.gPreviewEnabled;
147 return !!this._previewModeEnabled;
151 * Internal method to enable pulling data from preview buckets.
154 enablePreviewMode(enabled) {
155 const bool2str = v =>
156 // eslint-disable-next-line no-nested-ternary
157 _isUndefined(v) ? "unset" : v ? "enabled" : "disabled";
159 `Preview mode: ${bool2str(this._previewModeEnabled)} -> ${bool2str(
163 this._previewModeEnabled = enabled;
167 * Returns the actual bucket name to be used. When preview mode is enabled,
168 * this adds the *preview* suffix.
170 * See also `SharedUtils.loadJSONDump()` which strips the preview suffix to identify
171 * the packaged JSON file.
173 * @param bucketName the client bucket
174 * @returns the final client bucket depending whether preview mode is enabled.
176 actualBucketName(bucketName) {
177 let actual = bucketName.replace("-preview", "");
178 if (this.PREVIEW_MODE) {
179 actual += "-preview";
185 * Check if network is down.
187 * Note that if this returns false, it does not guarantee
188 * that network is up.
190 * @return {bool} Whether network is down or not.
195 Services.io.offline ||
196 lazy.CaptivePortalService.state ==
197 lazy.CaptivePortalService.LOCKED_PORTAL ||
198 !lazy.gNetworkLinkService.isLinkUp
201 log.warn("Could not determine network status.", ex);
207 * A wrapper around `ServiceRequest` that behaves like `fetch()`.
209 * Use this in order to leverage the `beConservative` flag, for
210 * example to avoid using HTTP3 to fetch critical data.
212 * @param input a resource
213 * @param init request options
214 * @returns a Response object
216 async fetch(input, init = {}) {
217 return new Promise(function (resolve, reject) {
218 const request = new ServiceRequest();
219 function fallbackOrReject(err) {
221 // At most one recursive Utils.fetch call (bypassProxy=false to true).
223 Services.startup.shuttingDown ||
225 !request.isProxied ||
226 !request.bypassProxyEnabled
231 ServiceRequest.logProxySource(request.channel, "remote-settings");
232 resolve(Utils.fetch(input, { ...init, bypassProxy: true }));
235 request.onerror = () =>
236 fallbackOrReject(new TypeError("NetworkError: Network request failed"));
237 request.ontimeout = () =>
238 fallbackOrReject(new TypeError("Timeout: Network request failed"));
239 request.onabort = () =>
240 fallbackOrReject(new DOMException("Aborted", "AbortError"));
241 request.onload = () => {
242 // Parse raw response headers into `Headers` object.
243 const headers = new Headers();
244 const rawHeaders = request.getAllResponseHeaders();
249 const parts = line.split(": ");
250 const header = parts.shift();
251 const value = parts.join(": ");
252 headers.set(header, value);
255 const responseAttributes = {
256 status: request.status,
257 statusText: request.statusText,
258 url: request.responseURL,
261 resolve(new Response(request.response, responseAttributes));
264 const { method = "GET", headers = {}, bypassProxy = false } = init;
266 request.open(method, input, { bypassProxy });
267 // By default, XMLHttpRequest converts the response based on the
268 // Content-Type header, or UTF-8 otherwise. This may mangle binary
269 // responses. Avoid that by requesting the raw bytes.
270 request.responseType = "arraybuffer";
272 for (const [name, value] of Object.entries(headers)) {
273 request.setRequestHeader(name, value);
281 * Check if local data exist for the specified client.
283 * @param {RemoteSettingsClient} client
284 * @return {bool} Whether it exists or not.
286 async hasLocalData(client) {
287 const timestamp = await client.db.getLastModified();
288 return timestamp !== null;
292 * Check if we ship a JSON dump for the specified bucket and collection.
294 * @param {String} bucket
295 * @param {String} collection
296 * @return {bool} Whether it is present or not.
298 async hasLocalDump(bucket, collection) {
301 `resource://app/defaults/settings/${bucket}/${collection}.json`,
313 * Look up the last modification time of the JSON dump.
315 * @param {String} bucket
316 * @param {String} collection
317 * @return {int} The last modification time of the dump. -1 if non-existent.
319 async getLocalDumpLastModified(bucket, collection) {
320 if (!this._dumpStats) {
321 if (!this._dumpStatsInitPromise) {
322 this._dumpStatsInitPromise = (async () => {
324 let res = await fetch(
325 "resource://app/defaults/settings/last_modified.json"
327 this._dumpStats = await res.json();
329 log.warn(`Failed to load last_modified.json: ${e}`);
330 this._dumpStats = {};
332 delete this._dumpStatsInitPromise;
335 await this._dumpStatsInitPromise;
337 const identifier = `${bucket}/${collection}`;
338 let lastModified = this._dumpStats[identifier];
339 if (lastModified === undefined) {
340 const { timestamp: dumpTimestamp } = await lazy.SharedUtils.loadJSONDump(
344 // Client recognize -1 as missing dump.
345 lastModified = dumpTimestamp ?? -1;
346 this._dumpStats[identifier] = lastModified;
352 * Fetch the list of remote collections and their timestamp.
355 * "timestamp": 1486545678,
358 * "host":"kinto-ota.dev.mozaws.net",
359 * "last_modified":1450717104423,
360 * "bucket":"blocklists",
361 * "collection":"certificates"
368 * @param {String} serverUrl The server URL (eg. `https://server.org/v1`)
369 * @param {int} expectedTimestamp The timestamp that the server is supposed to return.
370 * We obtained it from the Megaphone notification payload,
371 * and we use it only for cache busting (Bug 1497159).
372 * @param {String} lastEtag (optional) The Etag of the latest poll to be matched
373 * by the server (eg. `"123456789"`).
374 * @param {Object} filters
376 async fetchLatestChanges(serverUrl, options = {}) {
377 const { expectedTimestamp, lastEtag = "", filters = {} } = options;
379 let url = serverUrl + Utils.CHANGES_PATH;
382 _expected: expectedTimestamp ?? 0,
384 if (lastEtag != "") {
385 params._since = lastEtag;
390 Object.entries(params)
391 .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
394 const response = await Utils.fetch(url);
396 if (response.status >= 500) {
397 throw new Error(`Server error ${response.status} ${response.statusText}`);
400 const is404FromCustomServer =
401 response.status == 404 &&
402 Services.prefs.prefHasUserValue("services.settings.server");
404 const ct = response.headers.get("Content-Type");
405 if (!is404FromCustomServer && (!ct || !ct.includes("application/json"))) {
406 throw new Error(`Unexpected content-type "${ct}"`);
411 payload = await response.json();
416 if (!payload.hasOwnProperty("changes")) {
417 // If the server is failing, the JSON response might not contain the
418 // expected data. For example, real server errors (Bug 1259145)
419 // or dummy local server for tests (Bug 1481348)
420 if (!is404FromCustomServer) {
422 `Server error ${url} ${response.status} ${
424 }: ${JSON.stringify(payload)}`
429 const { changes = [], timestamp } = payload;
431 let serverTimeMillis = Date.parse(response.headers.get("Date"));
432 // Since the response is served via a CDN, the Date header value could have been cached.
433 const cacheAgeSeconds = response.headers.has("Age")
434 ? parseInt(response.headers.get("Age"), 10)
436 serverTimeMillis += cacheAgeSeconds * 1000;
438 // Age of data (time between publication and now).
439 const ageSeconds = (serverTimeMillis - timestamp) / 1000;
441 // Check if the server asked the clients to back off.
443 if (response.headers.has("Backoff")) {
444 const value = parseInt(response.headers.get("Backoff"), 10);
446 backoffSeconds = value;
452 currentEtag: `"${timestamp}"`,
460 * Test if a single object matches all given filters.
462 * @param {Object} filters The filters object.
463 * @param {Object} entry The object to filter.
466 filterObject(filters, entry) {
467 return Object.entries(filters).every(([filter, value]) => {
468 if (Array.isArray(value)) {
469 return value.some(candidate => candidate === entry[filter]);
470 } else if (typeof value === "object") {
471 return Utils.filterObject(value, entry[filter]);
472 } else if (!Object.prototype.hasOwnProperty.call(entry, filter)) {
473 console.error(`The property ${filter} does not exist`);
476 return entry[filter] === value;
481 * Sorts records in a list according to a given ordering.
483 * @param {String} order The ordering, eg. `-last_modified`.
484 * @param {Array} list The collection to order.
487 sortObjects(order, list) {
488 const hasDash = order[0] === "-";
489 const field = hasDash ? order.slice(1) : order;
490 const direction = hasDash ? -1 : 1;
491 return list.slice().sort((a, b) => {
492 if (a[field] && _isUndefined(b[field])) {
495 if (b[field] && _isUndefined(a[field])) {
498 if (_isUndefined(a[field]) && _isUndefined(b[field])) {
501 return a[field] > b[field] ? direction : -direction;