summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs')
-rw-r--r--mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs648
1 files changed, 648 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs b/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs
new file mode 100644
index 0000000000..019592bfec
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs
@@ -0,0 +1,648 @@
+/* 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/. */
+
+import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.sys.mjs",
+ E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
+ LoadURIDelegate: "resource://gre/modules/LoadURIDelegate.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "ReferrerInfo", () =>
+ Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ )
+);
+
+// Filter out request headers as per discussion in Bug #1567549
+// CONNECTION: Used by Gecko to manage connections
+// HOST: Relates to how gecko will ultimately interpret the resulting resource as that
+// determines the effective request URI
+const BAD_HEADERS = ["connection", "host"];
+
+// Headers use |\r\n| as separator so these characters cannot appear
+// in the header name or value
+const FORBIDDEN_HEADER_CHARACTERS = ["\n", "\r"];
+
+// Keep in sync with GeckoSession.java
+const HEADER_FILTER_CORS_SAFELISTED = 1;
+// eslint-disable-next-line no-unused-vars
+const HEADER_FILTER_UNRESTRICTED_UNSAFE = 2;
+
+// Create default ReferrerInfo instance for the given referrer URI string.
+const createReferrerInfo = aReferrer => {
+ let referrerUri;
+ try {
+ referrerUri = Services.io.newURI(aReferrer);
+ } catch (ignored) {}
+
+ return new lazy.ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, referrerUri);
+};
+
+function convertFlags(aFlags) {
+ let navFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (!aFlags) {
+ return navFlags;
+ }
+ // These need to match the values in GeckoSession.LOAD_FLAGS_*
+ if (aFlags & (1 << 0)) {
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+ }
+ if (aFlags & (1 << 1)) {
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY;
+ }
+ if (aFlags & (1 << 2)) {
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
+ }
+ if (aFlags & (1 << 3)) {
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS;
+ }
+ if (aFlags & (1 << 4)) {
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER;
+ }
+ if (aFlags & (1 << 5)) {
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
+ }
+ if (aFlags & (1 << 6)) {
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ }
+ if (aFlags & (1 << 7)) {
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE;
+ }
+ return navFlags;
+}
+
+// Handles navigation requests between Gecko and a GeckoView.
+// Handles GeckoView:GoBack and :GoForward requests dispatched by
+// GeckoView.goBack and .goForward.
+// Dispatches GeckoView:LocationChange to the GeckoView on location change when
+// active.
+// Implements nsIBrowserDOMWindow.
+export class GeckoViewNavigation extends GeckoViewModule {
+ onInitBrowser() {
+ this.window.browserDOMWindow = this;
+
+ debug`sessionContextId=${this.settings.sessionContextId}`;
+
+ if (this.settings.sessionContextId !== null) {
+ // Gecko may have issues with strings containing special characters,
+ // so we restrict the string format to a specific pattern.
+ if (!/^gvctx(-)?([a-f0-9]+)$/.test(this.settings.sessionContextId)) {
+ throw new Error("sessionContextId has illegal format");
+ }
+
+ this.browser.setAttribute(
+ "geckoViewSessionContextId",
+ this.settings.sessionContextId
+ );
+ }
+
+ // There may be a GeckoViewNavigation module in another window waiting for
+ // us to create a browser so it can set openWindowInfo, so allow them to do
+ // that now.
+ Services.obs.notifyObservers(this.window, "geckoview-window-created");
+ }
+
+ onInit() {
+ debug`onInit`;
+
+ this.registerListener([
+ "GeckoView:GoBack",
+ "GeckoView:GoForward",
+ "GeckoView:GotoHistoryIndex",
+ "GeckoView:LoadUri",
+ "GeckoView:Reload",
+ "GeckoView:Stop",
+ "GeckoView:PurgeHistory",
+ "GeckoView:DotPrintFinish",
+ ]);
+
+ this._initialAboutBlank = true;
+ }
+
+ validateHeader(key, value, filter) {
+ if (!key) {
+ // Key cannot be empty
+ return false;
+ }
+
+ for (const c of FORBIDDEN_HEADER_CHARACTERS) {
+ if (key.includes(c) || value?.includes(c)) {
+ return false;
+ }
+ }
+
+ if (BAD_HEADERS.includes(key.toLowerCase().trim())) {
+ return false;
+ }
+
+ if (
+ filter == HEADER_FILTER_CORS_SAFELISTED &&
+ !this.window.windowUtils.isCORSSafelistedRequestHeader(key, value)
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // Bundle event handler.
+ async onEvent(aEvent, aData, aCallback) {
+ debug`onEvent: event=${aEvent}, data=${aData}`;
+
+ switch (aEvent) {
+ case "GeckoView:GoBack":
+ this.browser.goBack(aData.userInteraction);
+ break;
+ case "GeckoView:GoForward":
+ this.browser.goForward(aData.userInteraction);
+ break;
+ case "GeckoView:GotoHistoryIndex":
+ this.browser.gotoIndex(aData.index);
+ break;
+ case "GeckoView:LoadUri":
+ const {
+ uri,
+ referrerUri,
+ referrerSessionId,
+ flags,
+ headers,
+ headerFilter,
+ } = aData;
+
+ let navFlags = convertFlags(flags);
+ // For performance reasons we don't call the LoadUriDelegate.loadUri
+ // from Gecko, and instead we call it directly in the loadUri Java API.
+ navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE;
+
+ let triggeringPrincipal, referrerInfo, csp;
+ if (referrerSessionId) {
+ const referrerWindow = Services.ww.getWindowByName(referrerSessionId);
+ triggeringPrincipal = referrerWindow.browser.contentPrincipal;
+ csp = referrerWindow.browser.csp;
+
+ const referrerPolicy = referrerWindow.browser.referrerInfo
+ ? referrerWindow.browser.referrerInfo.referrerPolicy
+ : Ci.nsIReferrerInfo.EMPTY;
+
+ referrerInfo = new lazy.ReferrerInfo(
+ referrerPolicy,
+ true,
+ referrerWindow.browser.documentURI
+ );
+ } else if (referrerUri) {
+ referrerInfo = createReferrerInfo(referrerUri);
+ } else {
+ // External apps are treated like web pages, so they should not get
+ // a privileged principal.
+ const isExternal =
+ navFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
+ if (!isExternal) {
+ // Always use the system principal as the triggering principal
+ // for user-initiated (ie. no referrer session and not external)
+ // loads. See discussion in bug 1573860.
+ triggeringPrincipal =
+ Services.scriptSecurityManager.getSystemPrincipal();
+ }
+ }
+
+ if (!triggeringPrincipal) {
+ triggeringPrincipal =
+ Services.scriptSecurityManager.createNullPrincipal({});
+ }
+
+ let additionalHeaders = null;
+ if (headers) {
+ additionalHeaders = "";
+ for (const [key, value] of Object.entries(headers)) {
+ if (!this.validateHeader(key, value, headerFilter)) {
+ console.error(`Ignoring invalid header '${key}'='${value}'.`);
+ continue;
+ }
+
+ additionalHeaders += `${key}:${value ?? ""}\r\n`;
+ }
+
+ if (additionalHeaders != "") {
+ additionalHeaders =
+ lazy.E10SUtils.makeInputStream(additionalHeaders);
+ } else {
+ additionalHeaders = null;
+ }
+ }
+
+ // For any navigation here, we should have an appropriate triggeringPrincipal:
+ //
+ // 1) If we have a referring session, triggeringPrincipal is the contentPrincipal from the
+ // referring document.
+ // 2) For certain URI schemes listed above, we will have a codebase principal.
+ // 3) In all other cases, we create a NullPrincipal.
+ //
+ // The navigation flags are driven by the app. We purposely do not propagate these from
+ // the referring document, but expect that the app will in most cases.
+ //
+ // The referrerInfo is derived from the referring document, if present, by propagating any
+ // referrer policy. If we only have the referrerUri from the app, we create a referrerInfo
+ // with the specified URI and no policy set. If no referrerUri is present and we have no
+ // referring session, the referrerInfo is null.
+ //
+ // csp is only present if we have a referring document, null otherwise.
+ this.browser.fixupAndLoadURIString(uri, {
+ flags: navFlags,
+ referrerInfo,
+ triggeringPrincipal,
+ headers: additionalHeaders,
+ csp,
+ });
+ break;
+ case "GeckoView:Reload":
+ // At the moment, GeckoView only supports one reload, which uses
+ // nsIWebNavigation.LOAD_FLAGS_NONE flag, and the telemetry doesn't
+ // do anything to differentiate reloads (i.e normal vs skip caches)
+ // So whenever we add more reload methods, please make sure the
+ // telemetry probe is adjusted
+ this.browser.reloadWithFlags(convertFlags(aData.flags));
+ break;
+ case "GeckoView:Stop":
+ this.browser.stop();
+ break;
+ case "GeckoView:PurgeHistory":
+ this.browser.purgeSessionHistory();
+ break;
+ case "GeckoView:DotPrintFinish":
+ var printActor = this.moduleManager.getActor("GeckoViewPrintDelegate");
+ printActor.clearStaticClone();
+ printActor.telemetryDotPrintPdfCompleted(aData);
+ break;
+ }
+ }
+
+ waitAndSetupWindow(aSessionId, aOpenWindowInfo, aName) {
+ if (!aSessionId) {
+ return Promise.resolve(null);
+ }
+
+ return new Promise(resolve => {
+ const handler = {
+ observe(aSubject, aTopic, aData) {
+ if (
+ aTopic === "geckoview-window-created" &&
+ aSubject.name === aSessionId
+ ) {
+ // This value will be read by nsFrameLoader while it is being initialized.
+ aSubject.browser.openWindowInfo = aOpenWindowInfo;
+
+ // Gecko will use this attribute to set the name of the opened window.
+ if (aName) {
+ aSubject.browser.setAttribute("name", aName);
+ }
+
+ if (
+ !aOpenWindowInfo.isRemote &&
+ aSubject.browser.hasAttribute("remote")
+ ) {
+ // We cannot start in remote mode when we have an opener.
+ aSubject.browser.setAttribute("remote", "false");
+ aSubject.browser.removeAttribute("remoteType");
+ }
+ Services.obs.removeObserver(handler, "geckoview-window-created");
+ resolve(aSubject);
+ }
+ },
+ };
+
+ // This event is emitted from createBrowser() in geckoview.js
+ Services.obs.addObserver(handler, "geckoview-window-created");
+ });
+ }
+
+ handleNewSession(aUri, aOpenWindowInfo, aWhere, aFlags, aName) {
+ debug`handleNewSession: uri=${aUri && aUri.spec}
+ where=${aWhere} flags=${aFlags}`;
+
+ if (!this.enabled) {
+ return null;
+ }
+
+ const newSessionId = Services.uuid
+ .generateUUID()
+ .toString()
+ .slice(1, -1)
+ .replace(/-/g, "");
+
+ const message = {
+ type: "GeckoView:OnNewSession",
+ uri: aUri ? aUri.displaySpec : "",
+ newSessionId,
+ };
+
+ // The window might be already open by the time we get the response from
+ // the Java layer, so we need to start waiting before sending the message.
+ const setupPromise = this.waitAndSetupWindow(
+ newSessionId,
+ aOpenWindowInfo,
+ aName
+ );
+
+ let browser = undefined;
+ this.eventDispatcher
+ .sendRequestForResult(message)
+ .then(didOpenSession => {
+ if (!didOpenSession) {
+ return Promise.reject();
+ }
+ return setupPromise;
+ })
+ .then(
+ window => {
+ browser = window.browser;
+ },
+ () => {
+ browser = null;
+ }
+ );
+
+ // Wait indefinitely for app to respond with a browser or null
+ Services.tm.spinEventLoopUntil(
+ "GeckoViewNavigation.jsm:handleNewSession",
+ () => this.window.closed || browser !== undefined
+ );
+ return browser || null;
+ }
+
+ // nsIBrowserDOMWindow.
+ createContentWindow(
+ aUri,
+ aOpenWindowInfo,
+ aWhere,
+ aFlags,
+ aTriggeringPrincipal,
+ aCsp
+ ) {
+ debug`createContentWindow: uri=${aUri && aUri.spec}
+ where=${aWhere} flags=${aFlags}`;
+
+ if (
+ lazy.LoadURIDelegate.load(
+ this.window,
+ this.eventDispatcher,
+ aUri,
+ aWhere,
+ aFlags,
+ aTriggeringPrincipal
+ )
+ ) {
+ // The app has handled the load, abort open-window handling.
+ Components.returnCode = Cr.NS_ERROR_ABORT;
+ return null;
+ }
+
+ const browser = this.handleNewSession(
+ aUri,
+ aOpenWindowInfo,
+ aWhere,
+ aFlags,
+ null
+ );
+ if (!browser) {
+ Components.returnCode = Cr.NS_ERROR_ABORT;
+ return null;
+ }
+
+ return browser.browsingContext;
+ }
+
+ // nsIBrowserDOMWindow.
+ createContentWindowInFrame(aUri, aParams, aWhere, aFlags, aName) {
+ debug`createContentWindowInFrame: uri=${aUri && aUri.spec}
+ where=${aWhere} flags=${aFlags}
+ name=${aName}`;
+
+ if (aWhere === Ci.nsIBrowserDOMWindow.OPEN_PRINT_BROWSER) {
+ return this.window.moduleManager.onPrintWindow(aParams);
+ }
+
+ if (
+ lazy.LoadURIDelegate.load(
+ this.window,
+ this.eventDispatcher,
+ aUri,
+ aWhere,
+ aFlags,
+ aParams.triggeringPrincipal
+ )
+ ) {
+ // The app has handled the load, abort open-window handling.
+ Components.returnCode = Cr.NS_ERROR_ABORT;
+ return null;
+ }
+
+ const browser = this.handleNewSession(
+ aUri,
+ aParams.openWindowInfo,
+ aWhere,
+ aFlags,
+ aName
+ );
+ if (!browser) {
+ Components.returnCode = Cr.NS_ERROR_ABORT;
+ return null;
+ }
+
+ return browser;
+ }
+
+ handleOpenUri({
+ uri,
+ openWindowInfo,
+ where,
+ flags,
+ triggeringPrincipal,
+ csp,
+ referrerInfo = null,
+ name = null,
+ }) {
+ debug`handleOpenUri: uri=${uri && uri.spec}
+ where=${where} flags=${flags}`;
+
+ if (
+ lazy.LoadURIDelegate.load(
+ this.window,
+ this.eventDispatcher,
+ uri,
+ where,
+ flags,
+ triggeringPrincipal
+ )
+ ) {
+ return null;
+ }
+
+ let browser = this.browser;
+
+ if (
+ where === Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
+ where === Ci.nsIBrowserDOMWindow.OPEN_NEWTAB
+ ) {
+ browser = this.handleNewSession(uri, openWindowInfo, where, flags, name);
+ }
+
+ if (!browser) {
+ // Should we throw?
+ return null;
+ }
+
+ // 3) We have a new session and a browser element, load the requested URI.
+ browser.loadURI(uri, {
+ triggeringPrincipal,
+ csp,
+ referrerInfo,
+ });
+ return browser;
+ }
+
+ // nsIBrowserDOMWindow.
+ openURI(aUri, aOpenWindowInfo, aWhere, aFlags, aTriggeringPrincipal, aCsp) {
+ const browser = this.handleOpenUri({
+ uri: aUri,
+ openWindowInfo: aOpenWindowInfo,
+ where: aWhere,
+ flags: aFlags,
+ triggeringPrincipal: aTriggeringPrincipal,
+ csp: aCsp,
+ });
+ return browser && browser.browsingContext;
+ }
+
+ // nsIBrowserDOMWindow.
+ openURIInFrame(aUri, aParams, aWhere, aFlags, aName) {
+ const browser = this.handleOpenUri({
+ uri: aUri,
+ openWindowInfo: aParams.openWindowInfo,
+ where: aWhere,
+ flags: aFlags,
+ triggeringPrincipal: aParams.triggeringPrincipal,
+ csp: aParams.csp,
+ referrerInfo: aParams.referrerInfo,
+ name: aName,
+ });
+ return browser;
+ }
+
+ // nsIBrowserDOMWindow.
+ canClose() {
+ debug`canClose`;
+ return true;
+ }
+
+ onEnable() {
+ debug`onEnable`;
+
+ const flags = Ci.nsIWebProgress.NOTIFY_LOCATION;
+ this.progressFilter = Cc[
+ "@mozilla.org/appshell/component/browser-status-filter;1"
+ ].createInstance(Ci.nsIWebProgress);
+ this.progressFilter.addProgressListener(this, flags);
+ this.browser.addProgressListener(this.progressFilter, flags);
+ }
+
+ onDisable() {
+ debug`onDisable`;
+
+ if (!this.progressFilter) {
+ return;
+ }
+ this.progressFilter.removeProgressListener(this);
+ this.browser.removeProgressListener(this.progressFilter);
+ }
+
+ serializePermission({ type, capability, principal }) {
+ const { URI, originAttributes, privateBrowsingId } = principal;
+ return {
+ uri: Services.io.createExposableURI(URI).displaySpec,
+ principal: lazy.E10SUtils.serializePrincipal(principal),
+ perm: type,
+ value: capability,
+ contextId: originAttributes.geckoViewSessionContextId,
+ privateMode: privateBrowsingId != 0,
+ };
+ }
+
+ // WebProgress event handler.
+ onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+ debug`onLocationChange`;
+
+ let fixedURI = aLocationURI;
+
+ try {
+ fixedURI = Services.io.createExposableURI(aLocationURI);
+ } catch (ex) {}
+
+ // We manually fire the initial about:blank messages to make sure that we
+ // consistently send them so there's nothing to do here.
+ const ignore = this._initialAboutBlank && fixedURI.spec === "about:blank";
+ this._initialAboutBlank = false;
+
+ if (ignore) {
+ return;
+ }
+
+ const { contentPrincipal } = this.browser;
+ let permissions;
+ if (
+ contentPrincipal &&
+ lazy.GeckoViewUtils.isSupportedPermissionsPrincipal(contentPrincipal)
+ ) {
+ let rawPerms = [];
+ try {
+ rawPerms = Services.perms.getAllForPrincipal(contentPrincipal);
+ } catch (ex) {
+ warn`Could not get permissions for principal. ${ex}`;
+ }
+ permissions = rawPerms.map(this.serializePermission);
+
+ // The only way for apps to set permissions is to get hold of an existing
+ // permission and change its value.
+ // Tracking protection exception permissions are only present when
+ // explicitly added by the app, so if one is not present, we need to send
+ // a DENY_ACTION tracking protection permission so that apps can use it
+ // to add tracking protection exceptions.
+ const trackingProtectionPermission =
+ contentPrincipal.privateBrowsingId == 0
+ ? "trackingprotection"
+ : "trackingprotection-pb";
+ if (
+ contentPrincipal.isContentPrincipal &&
+ rawPerms.findIndex(p => p.type == trackingProtectionPermission) == -1
+ ) {
+ permissions.push(
+ this.serializePermission({
+ type: trackingProtectionPermission,
+ capability: Ci.nsIPermissionManager.DENY_ACTION,
+ principal: contentPrincipal,
+ })
+ );
+ }
+ }
+
+ const message = {
+ type: "GeckoView:LocationChange",
+ uri: fixedURI.displaySpec,
+ canGoBack: this.browser.canGoBack,
+ canGoForward: this.browser.canGoForward,
+ isTopLevel: aWebProgress.isTopLevel,
+ permissions,
+ };
+
+ this.eventDispatcher.sendRequest(message);
+ }
+}
+
+const { debug, warn } = GeckoViewNavigation.initLogging("GeckoViewNavigation");