diff options
Diffstat (limited to 'remote/domains/parent/Network.jsm')
-rw-r--r-- | remote/domains/parent/Network.jsm | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/remote/domains/parent/Network.jsm b/remote/domains/parent/Network.jsm new file mode 100644 index 0000000000..16850f397d --- /dev/null +++ b/remote/domains/parent/Network.jsm @@ -0,0 +1,512 @@ +/* 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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["Network"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]); + +const { Domain } = ChromeUtils.import( + "chrome://remote/content/domains/Domain.jsm" +); + +const MAX_COOKIE_EXPIRY = Number.MAX_SAFE_INTEGER; + +const LOAD_CAUSE_STRINGS = { + [Ci.nsIContentPolicy.TYPE_INVALID]: "Invalid", + [Ci.nsIContentPolicy.TYPE_OTHER]: "Other", + [Ci.nsIContentPolicy.TYPE_SCRIPT]: "Script", + [Ci.nsIContentPolicy.TYPE_IMAGE]: "Img", + [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "Stylesheet", + [Ci.nsIContentPolicy.TYPE_OBJECT]: "Object", + [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "Document", + [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "Subdocument", + [Ci.nsIContentPolicy.TYPE_PING]: "Ping", + [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "Xhr", + [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "ObjectSubdoc", + [Ci.nsIContentPolicy.TYPE_DTD]: "Dtd", + [Ci.nsIContentPolicy.TYPE_FONT]: "Font", + [Ci.nsIContentPolicy.TYPE_MEDIA]: "Media", + [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "Websocket", + [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "Csp", + [Ci.nsIContentPolicy.TYPE_XSLT]: "Xslt", + [Ci.nsIContentPolicy.TYPE_BEACON]: "Beacon", + [Ci.nsIContentPolicy.TYPE_FETCH]: "Fetch", + [Ci.nsIContentPolicy.TYPE_IMAGESET]: "Imageset", + [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "WebManifest", +}; + +class Network extends Domain { + constructor(session) { + super(session); + this.enabled = false; + + this._onRequest = this._onRequest.bind(this); + this._onResponse = this._onResponse.bind(this); + } + + destructor() { + this.disable(); + + super.destructor(); + } + + enable() { + if (this.enabled) { + return; + } + this.enabled = true; + this.session.networkObserver.startTrackingBrowserNetwork( + this.session.target.browser + ); + this.session.networkObserver.on("request", this._onRequest); + this.session.networkObserver.on("response", this._onResponse); + } + + disable() { + if (!this.enabled) { + return; + } + this.session.networkObserver.stopTrackingBrowserNetwork( + this.session.target.browser + ); + this.session.networkObserver.off("request", this._onRequest); + this.session.networkObserver.off("response", this._onResponse); + this.enabled = false; + } + + /** + * Deletes browser cookies with matching name and url or domain/path pair. + * + * @param {Object} options + * @param {string} name + * Name of the cookies to remove. + * @param {string=} url + * If specified, deletes all the cookies with the given name + * where domain and path match provided URL. + * @param {string=} domain + * If specified, deletes only cookies with the exact domain. + * @param {string=} path + * If specified, deletes only cookies with the exact path. + */ + async deleteCookies(options = {}) { + const { domain, name, path = "/", url } = options; + + if (typeof name != "string") { + throw new TypeError("name: string value expected"); + } + + if (!url && !domain) { + throw new TypeError( + "At least one of the url and domain needs to be specified" + ); + } + + // Retrieve host. Check domain first because it has precedence. + let hostname = domain || ""; + if (hostname.length == 0) { + const cookieURL = new URL(url); + if (!["http:", "https:"].includes(cookieURL.protocol)) { + throw new TypeError("An http or https url must be specified"); + } + hostname = cookieURL.hostname; + } + + const cookiesFound = Services.cookies.getCookiesWithOriginAttributes( + JSON.stringify({}), + hostname + ); + + for (const cookie of cookiesFound) { + if (cookie.name == name && cookie.path.startsWith(path)) { + Services.cookies.remove( + cookie.host, + cookie.name, + cookie.path, + cookie.originAttributes + ); + } + } + } + + /** + * Activates emulation of network conditions. + * + * @param {Object} options + * @param {boolean} offline + * True to emulate internet disconnection. + */ + emulateNetworkConditions(options = {}) { + const { offline } = options; + + if (typeof offline != "boolean") { + throw new TypeError("offline: boolean value expected"); + } + + Services.io.offline = offline; + } + + /** + * Returns all browser cookies. + * + * Depending on the backend support, will return detailed cookie information in the cookies field. + * + * @param {Object} options + * + * @return {Array<Cookie>} + * Array of cookie objects. + */ + async getAllCookies(options = {}) { + const cookies = []; + for (const cookie of Services.cookies.cookies) { + cookies.push(_buildCookie(cookie)); + } + + return { cookies }; + } + + /** + * Returns all browser cookies for the current URL. + * + * @param {Object} options + * @param {Array<string>=} urls + * The list of URLs for which applicable cookies will be fetched. + * Defaults to the currently open URL. + * + * @return {Array<Cookie>} + * Array of cookie objects. + */ + async getCookies(options = {}) { + // Bug 1605354 - Add support for options.urls + const urls = [this.session.target.url]; + + const cookies = []; + for (let url of urls) { + url = new URL(url); + + const secureProtocol = ["https:", "wss:"].includes(url.protocol); + + const cookiesFound = Services.cookies.getCookiesWithOriginAttributes( + JSON.stringify({}), + url.hostname + ); + + for (const cookie of cookiesFound) { + // Ignore secure cookies for non-secure protocols + if (cookie.isSecure && !secureProtocol) { + continue; + } + + // Ignore cookies which do not match the given path + if (!url.pathname.startsWith(cookie.path)) { + continue; + } + + cookies.push(_buildCookie(cookie)); + } + } + + return { cookies }; + } + + /** + * Sets a cookie with the given cookie data. + * + * Note that it may overwrite equivalent cookies if they exist. + * + * @param {Object} cookie + * @param {string} name + * Cookie name. + * @param {string} value + * Cookie value. + * @param {string=} domain + * Cookie domain. + * @param {number=} expires + * Cookie expiration date, session cookie if not set. + * @param {boolean=} httpOnly + * True if cookie is http-only. + * @param {string=} path + * Cookie path. + * @param {string=} sameSite + * Cookie SameSite type. + * @param {boolean=} secure + * True if cookie is secure. + * @param {string=} url + * The request-URI to associate with the setting of the cookie. + * This value can affect the default domain and path values of the + * created cookie. + * + * @return {boolean} + * True if successfully set cookie. + */ + setCookie(cookie) { + if (typeof cookie.name != "string") { + throw new TypeError("name: string value expected"); + } + + if (typeof cookie.value != "string") { + throw new TypeError("value: string value expected"); + } + + if ( + typeof cookie.url == "undefined" && + typeof cookie.domain == "undefined" + ) { + throw new TypeError( + "At least one of the url and domain needs to be specified" + ); + } + + // Retrieve host. Check domain first because it has precedence. + let hostname = cookie.domain || ""; + let cookieURL; + let schemeType = Ci.nsICookie.SCHEME_UNSET; + if (hostname.length == 0) { + try { + cookieURL = new URL(cookie.url); + } catch (e) { + return { success: false }; + } + + if (!["http:", "https:"].includes(cookieURL.protocol)) { + throw new TypeError(`Invalid protocol ${cookieURL.protocol}`); + } + + if (cookieURL.protocol == "https:") { + cookie.secure = true; + schemeType = Ci.nsICookie.SCHEME_HTTPS; + } else { + schemeType = Ci.nsICookie.SCHEME_HTTP; + } + + hostname = cookieURL.hostname; + } + + if (typeof cookie.path == "undefined") { + cookie.path = "/"; + } + + let isSession = false; + if (typeof cookie.expires == "undefined") { + isSession = true; + cookie.expires = MAX_COOKIE_EXPIRY; + } + + const sameSiteMap = new Map([ + ["None", Ci.nsICookie.SAMESITE_NONE], + ["Lax", Ci.nsICookie.SAMESITE_LAX], + ["Strict", Ci.nsICookie.SAMESITE_STRICT], + ]); + + let success = true; + try { + Services.cookies.add( + hostname, + cookie.path, + cookie.name, + cookie.value, + cookie.secure, + cookie.httpOnly || false, + isSession, + cookie.expires, + {} /* originAttributes */, + sameSiteMap.get(cookie.sameSite), + schemeType + ); + } catch (e) { + success = false; + } + + return { success }; + } + + /** + * Sets given cookies. + * + * @param {Object} options + * @param {Array.<Cookie>} cookies + * Cookies to be set. + */ + setCookies(options = {}) { + const { cookies } = options; + + if (!Array.isArray(cookies)) { + throw new TypeError("Invalid parameters (cookies: array expected)"); + } + + cookies.forEach(cookie => { + const { success } = this.setCookie(cookie); + if (!success) { + throw new Error("Invalid cookie fields"); + } + }); + } + + /** + * Toggles ignoring cache for each request. If true, cache will not be used. + * + * @param {Object} options + * @param {boolean} options.cacheDisabled + * Cache disabled state. + */ + async setCacheDisabled(options = {}) { + const { cacheDisabled = false } = options; + + const { INHIBIT_CACHING, LOAD_BYPASS_CACHE, LOAD_NORMAL } = Ci.nsIRequest; + + let loadFlags = LOAD_NORMAL; + if (cacheDisabled) { + loadFlags = LOAD_BYPASS_CACHE | INHIBIT_CACHING; + } + + await this.executeInChild("_updateLoadFlags", loadFlags); + } + + /** + * Allows overriding user agent with the given string. + * + * Redirected to Emulation.setUserAgentOverride. + */ + setUserAgentOverride(options = {}) { + const { id } = this.session; + this.session.execute(id, "Emulation", "setUserAgentOverride", options); + } + + _onRequest(eventName, httpChannel, data) { + const wrappedChannel = ChannelWrapper.get(httpChannel); + + const request = { + url: httpChannel.URI.spec, + urlFragment: undefined, + method: httpChannel.requestMethod, + headers: headersAsObject(data.headers), + postData: undefined, + hasPostData: false, + mixedContentType: undefined, + initialPriority: undefined, + referrerPolicy: undefined, + isLinkPreload: false, + }; + this.emit("Network.requestWillBeSent", { + requestId: data.requestId, + loaderId: data.loaderId, + documentURL: wrappedChannel.documentURL || httpChannel.URI.spec, + request, + timestamp: Date.now() / 1000, + wallTime: undefined, + initiator: undefined, + redirectResponse: undefined, + type: LOAD_CAUSE_STRINGS[data.cause] || "unknown", + frameId: data.frameId.toString(), + hasUserGesture: undefined, + }); + } + + _onResponse(eventName, httpChannel, data) { + const wrappedChannel = ChannelWrapper.get(httpChannel); + const headers = headersAsObject(data.headers); + + this.emit("Network.responseReceived", { + requestId: data.requestId, + loaderId: data.loaderId, + timestamp: Date.now() / 1000, + type: LOAD_CAUSE_STRINGS[data.cause] || "unknown", + response: { + url: httpChannel.URI.spec, + status: data.status, + statusText: data.statusText, + headers, + mimeType: wrappedChannel.contentType, + requestHeaders: headersAsObject(data.requestHeaders), + connectionReused: undefined, + connectionId: undefined, + remoteIPAddress: data.remoteIPAddress, + remotePort: data.remotePort, + fromDiskCache: data.fromCache, + encodedDataLength: undefined, + protocol: httpChannel.protocolVersion, + securityDetails: data.securityDetails, + // unknown, neutral, insecure, secure, info, insecure-broken + securityState: "unknown", + }, + frameId: data.frameId.toString(), + }); + } +} + +/** + * Creates a CDP Network.Cookie from our internal cookie values + * + * @param {nsICookie} cookie + * + * @returns {Network.Cookie} + * A CDP Cookie + */ +function _buildCookie(cookie) { + const data = { + name: cookie.name, + value: cookie.value, + domain: cookie.host, + path: cookie.path, + expires: cookie.isSession ? -1 : cookie.expiry, + // The size is the combined length of both the cookie name and value + size: cookie.name.length + cookie.value.length, + httpOnly: cookie.isHttpOnly, + secure: cookie.isSecure, + session: cookie.isSession, + }; + + if (cookie.sameSite) { + const sameSiteMap = new Map([ + [Ci.nsICookie.SAMESITE_LAX, "Lax"], + [Ci.nsICookie.SAMESITE_STRICT, "Strict"], + ]); + + data.sameSite = sameSiteMap.get(cookie.sameSite); + } + + return data; +} + +/** + * Given a array of possibly repeating header names, merge the values for + * duplicate headers into a comma-separated list, or in some cases a + * newline-separated list. + * + * e.g. { "Cache-Control": "no-cache,no-store" } + * + * Based on + * https://hg.mozilla.org/mozilla-central/file/56c09d42f411246e407fe30418c27e67a6a44d29/netwerk/protocol/http/nsHttpHeaderArray.h + * + * @param {Array} headers + * Array of {name, value} + * @returns {Object} + * Object where each key is a header name. + */ +function headersAsObject(headers) { + const rv = {}; + headers.forEach(({ name, value }) => { + name = name.toLowerCase(); + if (rv[name]) { + const separator = [ + "set-cookie", + "www-authenticate", + "proxy-authenticate", + ].includes(name) + ? "\n" + : ","; + rv[name] += `${separator}${value}`; + } else { + rv[name] = value; + } + }); + return rv; +} |