summaryrefslogtreecommitdiffstats
path: root/devtools/shared/network-observer/NetworkHelper.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/network-observer/NetworkHelper.sys.mjs')
-rw-r--r--devtools/shared/network-observer/NetworkHelper.sys.mjs917
1 files changed, 917 insertions, 0 deletions
diff --git a/devtools/shared/network-observer/NetworkHelper.sys.mjs b/devtools/shared/network-observer/NetworkHelper.sys.mjs
new file mode 100644
index 0000000000..3523c0854e
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkHelper.sys.mjs
@@ -0,0 +1,917 @@
+/* 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/. */
+
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2007, Parakey Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the
+ * following conditions are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer in the documentation and/or other
+ * materials provided with the distribution.
+ *
+ * * Neither the name of Parakey Inc. nor the names of its
+ * contributors may be used to endorse or promote products
+ * derived from this software without specific prior
+ * written permission of Parakey Inc.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * Creator:
+ * Joe Hewitt
+ * Contributors
+ * John J. Barton (IBM Almaden)
+ * Jan Odvarko (Mozilla Corp.)
+ * Max Stepanov (Aptana Inc.)
+ * Rob Campbell (Mozilla Corp.)
+ * Hans Hillen (Paciello Group, Mozilla)
+ * Curtis Bartley (Mozilla Corp.)
+ * Mike Collins (IBM Almaden)
+ * Kevin Decker
+ * Mike Ratcliffe (Comartis AG)
+ * Hernan Rodríguez Colmeiro
+ * Austin Andrews
+ * Christoph Dorn
+ * Steven Roussey (AppCenter Inc, Network54)
+ * Mihai Sucan (Mozilla Corp.)
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ DevToolsInfaillibleUtils:
+ "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+// It would make sense to put this in the above
+// ChromeUtils.defineESModuleGetters, but that doesn't seem to work.
+XPCOMUtils.defineLazyGetter(lazy, "certDecoder", () => {
+ const { parse, pemToDER } = ChromeUtils.importESModule(
+ "chrome://global/content/certviewer/certDecoder.mjs"
+ );
+ return { parse, pemToDER };
+});
+
+// "Lax", "Strict" and "None" are special values of the SameSite cookie
+// attribute that should not be translated.
+const COOKIE_SAMESITE = {
+ LAX: "Lax",
+ STRICT: "Strict",
+ NONE: "None",
+};
+
+/**
+ * Helper object for networking stuff.
+ *
+ * Most of the following functions have been taken from the Firebug source. They
+ * have been modified to match the Firefox coding rules.
+ */
+export var NetworkHelper = {
+ /**
+ * Converts text with a given charset to unicode.
+ *
+ * @param string text
+ * Text to convert.
+ * @param string charset
+ * Charset to convert the text to.
+ * @returns string
+ * Converted text.
+ */
+ convertToUnicode(text, charset) {
+ // FIXME: We need to throw when text can't be converted e.g. the contents of
+ // an image. Until we have a way to do so with TextEncoder and TextDecoder
+ // we need to use nsIScriptableUnicodeConverter instead.
+ const conv = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ try {
+ conv.charset = charset || "UTF-8";
+ return conv.ConvertToUnicode(text);
+ } catch (ex) {
+ return text;
+ }
+ },
+
+ /**
+ * Reads all available bytes from stream and converts them to charset.
+ *
+ * @param nsIInputStream stream
+ * @param string charset
+ * @returns string
+ * UTF-16 encoded string based on the content of stream and charset.
+ */
+ readAndConvertFromStream(stream, charset) {
+ let text = null;
+ try {
+ text = lazy.NetUtil.readInputStreamToString(stream, stream.available());
+ return this.convertToUnicode(text, charset);
+ } catch (err) {
+ return text;
+ }
+ },
+
+ /**
+ * Reads the posted text from request.
+ *
+ * @param nsIHttpChannel request
+ * @param string charset
+ * The content document charset, used when reading the POSTed data.
+ * @returns string or null
+ * Returns the posted string if it was possible to read from request
+ * otherwise null.
+ */
+ readPostTextFromRequest(request, charset) {
+ if (request instanceof Ci.nsIUploadChannel) {
+ const iStream = request.uploadStream;
+
+ let isSeekableStream = false;
+ if (iStream instanceof Ci.nsISeekableStream) {
+ isSeekableStream = true;
+ }
+
+ let prevOffset;
+ if (isSeekableStream) {
+ prevOffset = iStream.tell();
+ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+ }
+
+ // Read data from the stream.
+ const text = this.readAndConvertFromStream(iStream, charset);
+
+ // Seek locks the file, so seek to the beginning only if necko hasn't
+ // read it yet, since necko doesn't seek to 0 before reading (at lest
+ // not till 459384 is fixed).
+ if (isSeekableStream && prevOffset == 0) {
+ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+ }
+ return text;
+ }
+ return null;
+ },
+
+ /**
+ * Reads the posted text from the page's cache.
+ *
+ * @param nsIDocShell docShell
+ * @param string charset
+ * @returns string or null
+ * Returns the posted string if it was possible to read from
+ * docShell otherwise null.
+ */
+ readPostTextFromPage(docShell, charset) {
+ const webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ return this.readPostTextFromPageViaWebNav(webNav, charset);
+ },
+
+ /**
+ * Reads the posted text from the page's cache, given an nsIWebNavigation
+ * object.
+ *
+ * @param nsIWebNavigation webNav
+ * @param string charset
+ * @returns string or null
+ * Returns the posted string if it was possible to read from
+ * webNav, otherwise null.
+ */
+ readPostTextFromPageViaWebNav(webNav, charset) {
+ if (webNav instanceof Ci.nsIWebPageDescriptor) {
+ const descriptor = webNav.currentDescriptor;
+
+ if (
+ descriptor instanceof Ci.nsISHEntry &&
+ descriptor.postData &&
+ descriptor instanceof Ci.nsISeekableStream
+ ) {
+ descriptor.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+
+ return this.readAndConvertFromStream(descriptor, charset);
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Gets the topFrameElement that is associated with request. This
+ * works in single-process and multiprocess contexts. It may cross
+ * the content/chrome boundary.
+ *
+ * @param nsIHttpChannel request
+ * @returns Element|null
+ * The top frame element for the given request.
+ */
+ getTopFrameForRequest(request) {
+ try {
+ return this.getRequestLoadContext(request).topFrameElement;
+ } catch (ex) {
+ // request loadContext is not always available.
+ }
+ return null;
+ },
+
+ /**
+ * Gets the nsIDOMWindow that is associated with request.
+ *
+ * @param nsIHttpChannel request
+ * @returns nsIDOMWindow or null
+ */
+ getWindowForRequest(request) {
+ try {
+ return this.getRequestLoadContext(request).associatedWindow;
+ } catch (ex) {
+ // On some request notificationCallbacks and loadGroup are both null,
+ // so that we can't retrieve any nsILoadContext interface.
+ // Fallback on nsILoadInfo to try to retrieve the request's window.
+ // (this is covered by test_network_get.html and its CSS request)
+ return request.loadInfo.loadingDocument?.defaultView;
+ }
+ },
+
+ /**
+ * Gets the nsILoadContext that is associated with request.
+ *
+ * @param nsIHttpChannel request
+ * @returns nsILoadContext or null
+ */
+ getRequestLoadContext(request) {
+ try {
+ if (request.loadInfo.workerAssociatedBrowsingContext) {
+ return request.loadInfo.workerAssociatedBrowsingContext;
+ }
+ } catch (ex) {
+ // Ignore.
+ }
+ try {
+ return request.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (ex) {
+ // Ignore.
+ }
+
+ try {
+ return request.loadGroup.notificationCallbacks.getInterface(
+ Ci.nsILoadContext
+ );
+ } catch (ex) {
+ // Ignore.
+ }
+
+ return null;
+ },
+
+ /**
+ * Determines whether the request has been made for the top level document.
+ *
+ * @param nsIHttpChannel request
+ * @returns Boolean True if the request represents the top level document.
+ */
+ isTopLevelLoad(request) {
+ if (request instanceof Ci.nsIChannel) {
+ const loadInfo = request.loadInfo;
+ if (loadInfo?.isTopLevelLoad) {
+ return request.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Loads the content of url from the cache.
+ *
+ * @param string url
+ * URL to load the cached content for.
+ * @param string charset
+ * Assumed charset of the cached content. Used if there is no charset
+ * on the channel directly.
+ * @param function callback
+ * Callback that is called with the loaded cached content if available
+ * or null if something failed while getting the cached content.
+ */
+ loadFromCache(url, charset, callback) {
+ const channel = lazy.NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+
+ // Ensure that we only read from the cache and not the server.
+ channel.loadFlags =
+ Ci.nsIRequest.LOAD_FROM_CACHE |
+ Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
+ Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
+
+ lazy.NetUtil.asyncFetch(channel, (inputStream, statusCode, request) => {
+ if (!Components.isSuccessCode(statusCode)) {
+ callback(null);
+ return;
+ }
+
+ // Try to get the encoding from the channel. If there is none, then use
+ // the passed assumed charset.
+ const requestChannel = request.QueryInterface(Ci.nsIChannel);
+ const contentCharset = requestChannel.contentCharset || charset;
+
+ // Read the content of the stream using contentCharset as encoding.
+ callback(this.readAndConvertFromStream(inputStream, contentCharset));
+ });
+ },
+
+ /**
+ * Parse a raw Cookie header value.
+ *
+ * @param string header
+ * The raw Cookie header value.
+ * @return array
+ * Array holding an object for each cookie. Each object holds the
+ * following properties: name and value.
+ */
+ parseCookieHeader(header) {
+ const cookies = header.split(";");
+ const result = [];
+
+ cookies.forEach(function (cookie) {
+ const equal = cookie.indexOf("=");
+ const name = cookie.substr(0, equal);
+ const value = cookie.substr(equal + 1);
+ result.push({
+ name: unescape(name.trim()),
+ value: unescape(value.trim()),
+ });
+ });
+
+ return result;
+ },
+
+ /**
+ * Parse a raw Set-Cookie header value.
+ *
+ * @param array headers
+ * Array of raw Set-Cookie header values.
+ * @return array
+ * Array holding an object for each cookie. Each object holds the
+ * following properties: name, value, secure (boolean), httpOnly
+ * (boolean), path, domain, samesite and expires (ISO date string).
+ */
+ parseSetCookieHeaders(headers) {
+ function parseSameSiteAttribute(attribute) {
+ attribute = attribute.toLowerCase();
+ switch (attribute) {
+ case COOKIE_SAMESITE.LAX.toLowerCase():
+ return COOKIE_SAMESITE.LAX;
+ case COOKIE_SAMESITE.STRICT.toLowerCase():
+ return COOKIE_SAMESITE.STRICT;
+ default:
+ return COOKIE_SAMESITE.NONE;
+ }
+ }
+
+ const cookies = [];
+
+ for (const header of headers) {
+ const rawCookies = header.split(/\r\n|\n|\r/);
+
+ rawCookies.forEach(function (cookie) {
+ const equal = cookie.indexOf("=");
+ const name = unescape(cookie.substr(0, equal).trim());
+ const parts = cookie.substr(equal + 1).split(";");
+ const value = unescape(parts.shift().trim());
+
+ cookie = { name, value };
+
+ parts.forEach(function (part) {
+ part = part.trim();
+ if (part.toLowerCase() == "secure") {
+ cookie.secure = true;
+ } else if (part.toLowerCase() == "httponly") {
+ cookie.httpOnly = true;
+ } else if (part.indexOf("=") > -1) {
+ const pair = part.split("=");
+ pair[0] = pair[0].toLowerCase();
+ if (pair[0] == "path" || pair[0] == "domain") {
+ cookie[pair[0]] = pair[1];
+ } else if (pair[0] == "samesite") {
+ cookie[pair[0]] = parseSameSiteAttribute(pair[1]);
+ } else if (pair[0] == "expires") {
+ try {
+ pair[1] = pair[1].replace(/-/g, " ");
+ cookie.expires = new Date(pair[1]).toISOString();
+ } catch (ex) {
+ // Ignore.
+ }
+ }
+ }
+ });
+
+ cookies.push(cookie);
+ });
+ }
+
+ return cookies;
+ },
+
+ // This is a list of all the mime category maps jviereck could find in the
+ // firebug code base.
+ mimeCategoryMap: {
+ "text/plain": "txt",
+ "text/html": "html",
+ "text/xml": "xml",
+ "text/xsl": "txt",
+ "text/xul": "txt",
+ "text/css": "css",
+ "text/sgml": "txt",
+ "text/rtf": "txt",
+ "text/x-setext": "txt",
+ "text/richtext": "txt",
+ "text/javascript": "js",
+ "text/jscript": "txt",
+ "text/tab-separated-values": "txt",
+ "text/rdf": "txt",
+ "text/xif": "txt",
+ "text/ecmascript": "js",
+ "text/vnd.curl": "txt",
+ "text/x-json": "json",
+ "text/x-js": "txt",
+ "text/js": "txt",
+ "text/vbscript": "txt",
+ "view-source": "txt",
+ "view-fragment": "txt",
+ "application/xml": "xml",
+ "application/xhtml+xml": "xml",
+ "application/atom+xml": "xml",
+ "application/rss+xml": "xml",
+ "application/vnd.mozilla.maybe.feed": "xml",
+ "application/javascript": "js",
+ "application/x-javascript": "js",
+ "application/x-httpd-php": "txt",
+ "application/rdf+xml": "xml",
+ "application/ecmascript": "js",
+ "application/http-index-format": "txt",
+ "application/json": "json",
+ "application/x-js": "txt",
+ "application/x-mpegurl": "txt",
+ "application/vnd.apple.mpegurl": "txt",
+ "multipart/mixed": "txt",
+ "multipart/x-mixed-replace": "txt",
+ "image/svg+xml": "svg",
+ "application/octet-stream": "bin",
+ "image/jpeg": "image",
+ "image/jpg": "image",
+ "image/gif": "image",
+ "image/png": "image",
+ "image/bmp": "image",
+ "application/x-shockwave-flash": "flash",
+ "video/x-flv": "flash",
+ "audio/mpeg3": "media",
+ "audio/x-mpeg-3": "media",
+ "video/mpeg": "media",
+ "video/x-mpeg": "media",
+ "video/vnd.mpeg.dash.mpd": "xml",
+ "audio/ogg": "media",
+ "application/ogg": "media",
+ "application/x-ogg": "media",
+ "application/x-midi": "media",
+ "audio/midi": "media",
+ "audio/x-mid": "media",
+ "audio/x-midi": "media",
+ "music/crescendo": "media",
+ "audio/wav": "media",
+ "audio/x-wav": "media",
+ "text/json": "json",
+ "application/x-json": "json",
+ "application/json-rpc": "json",
+ "application/x-web-app-manifest+json": "json",
+ "application/manifest+json": "json",
+ },
+
+ /**
+ * Check if the given MIME type is a text-only MIME type.
+ *
+ * @param string mimeType
+ * @return boolean
+ */
+ isTextMimeType(mimeType) {
+ if (mimeType.indexOf("text/") == 0) {
+ return true;
+ }
+
+ // XML and JSON often come with custom MIME types, so in addition to the
+ // standard "application/xml" and "application/json", we also look for
+ // variants like "application/x-bigcorp+xml". For JSON we allow "+json" and
+ // "-json" as suffixes.
+ if (/^application\/\w+(?:[\.-]\w+)*(?:\+xml|[-+]json)$/.test(mimeType)) {
+ return true;
+ }
+
+ const category = this.mimeCategoryMap[mimeType] || null;
+ switch (category) {
+ case "txt":
+ case "js":
+ case "json":
+ case "css":
+ case "html":
+ case "svg":
+ case "xml":
+ return true;
+
+ default:
+ return false;
+ }
+ },
+
+ /**
+ * Takes a securityInfo object of nsIRequest, the nsIRequest itself and
+ * extracts security information from them.
+ *
+ * @param object securityInfo
+ * The securityInfo object of a request. If null channel is assumed
+ * to be insecure.
+ * @param object originAttributes
+ * The OriginAttributes of the request.
+ * @param object httpActivity
+ * The httpActivity object for the request with at least members
+ * { private, hostname }.
+ * @param Map decodedCertificateCache
+ * A Map of certificate fingerprints to decoded certificates, to avoid
+ * repeatedly decoding previously-seen certificates.
+ *
+ * @return object
+ * Returns an object containing following members:
+ * - state: The security of the connection used to fetch this
+ * request. Has one of following string values:
+ * * "insecure": the connection was not secure (only http)
+ * * "weak": the connection has minor security issues
+ * * "broken": secure connection failed (e.g. expired cert)
+ * * "secure": the connection was properly secured.
+ * If state == broken:
+ * - errorMessage: error code string.
+ * If state == secure:
+ * - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3.
+ * - cipherSuite: the cipher suite used in this connection.
+ * - cert: information about certificate used in this connection.
+ * See parseCertificateInfo for the contents.
+ * - hsts: true if host uses Strict Transport Security,
+ * false otherwise
+ * - hpkp: true if host uses Public Key Pinning, false otherwise
+ * If state == weak: Same as state == secure and
+ * - weaknessReasons: list of reasons that cause the request to be
+ * considered weak. See getReasonsForWeakness.
+ */
+ async parseSecurityInfo(
+ securityInfo,
+ originAttributes,
+ httpActivity,
+ decodedCertificateCache
+ ) {
+ const info = {
+ state: "insecure",
+ };
+
+ // The request did not contain any security info.
+ if (!securityInfo) {
+ return info;
+ }
+
+ /**
+ * Different scenarios to consider here and how they are handled:
+ * - request is HTTP, the connection is not secure
+ * => securityInfo is null
+ * => state === "insecure"
+ *
+ * - request is HTTPS, the connection is secure
+ * => .securityState has STATE_IS_SECURE flag
+ * => state === "secure"
+ *
+ * - request is HTTPS, the connection has security issues
+ * => .securityState has STATE_IS_INSECURE flag
+ * => .errorCode is an NSS error code.
+ * => state === "broken"
+ *
+ * - request is HTTPS, the connection was terminated before the security
+ * could be validated
+ * => .securityState has STATE_IS_INSECURE flag
+ * => .errorCode is NOT an NSS error code.
+ * => .errorMessage is not available.
+ * => state === "insecure"
+ *
+ * - request is HTTPS but it uses a weak cipher or old protocol, see
+ * https://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
+ * security/manager/ssl/nsNSSCallbacks.cpp#l1233
+ * - request is mixed content (which makes no sense whatsoever)
+ * => .securityState has STATE_IS_BROKEN flag
+ * => .errorCode is NOT an NSS error code
+ * => .errorMessage is not available
+ * => state === "weak"
+ */
+
+ const wpl = Ci.nsIWebProgressListener;
+ const NSSErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
+ Ci.nsINSSErrorsService
+ );
+ if (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
+ const state = securityInfo.securityState;
+
+ let uri = null;
+ if (httpActivity.channel?.URI) {
+ uri = httpActivity.channel.URI;
+ }
+ if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) {
+ // it is not enough to look at the transport security info -
+ // schemes other than https and wss are subject to
+ // downgrade/etc at the scheme level and should always be
+ // considered insecure
+ info.state = "insecure";
+ } else if (state & wpl.STATE_IS_SECURE) {
+ // The connection is secure if the scheme is sufficient
+ info.state = "secure";
+ } else if (state & wpl.STATE_IS_BROKEN) {
+ // The connection is not secure, there was no error but there's some
+ // minor security issues.
+ info.state = "weak";
+ info.weaknessReasons = this.getReasonsForWeakness(state);
+ } else if (state & wpl.STATE_IS_INSECURE) {
+ // This was most likely an https request that was aborted before
+ // validation. Return info as info.state = insecure.
+ return info;
+ } else {
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.parseSecurityInfo",
+ "Security state " + state + " has no known STATE_IS_* flags."
+ );
+ return info;
+ }
+
+ // Cipher suite.
+ info.cipherSuite = securityInfo.cipherName;
+
+ // Key exchange group name.
+ info.keaGroupName = securityInfo.keaGroupName;
+
+ // Certificate signature scheme.
+ info.signatureSchemeName = securityInfo.signatureSchemeName;
+
+ // Protocol version.
+ info.protocolVersion = this.formatSecurityProtocol(
+ securityInfo.protocolVersion
+ );
+
+ // Certificate.
+ info.cert = await this.parseCertificateInfo(
+ securityInfo.serverCert,
+ decodedCertificateCache
+ );
+
+ // Certificate transparency status.
+ info.certificateTransparency = securityInfo.certificateTransparencyStatus;
+
+ // HSTS and HPKP if available.
+ if (httpActivity.hostname) {
+ const sss = Cc["@mozilla.org/ssservice;1"].getService(
+ Ci.nsISiteSecurityService
+ );
+ const pkps = Cc[
+ "@mozilla.org/security/publickeypinningservice;1"
+ ].getService(Ci.nsIPublicKeyPinningService);
+
+ if (!uri) {
+ // isSecureURI only cares about the host, not the scheme.
+ const host = httpActivity.hostname;
+ uri = Services.io.newURI("https://" + host);
+ }
+
+ info.hsts = sss.isSecureURI(uri, originAttributes);
+ info.hpkp = pkps.hostHasPins(uri);
+ } else {
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.parseSecurityInfo",
+ "Could not get HSTS/HPKP status as hostname is not available."
+ );
+ info.hsts = false;
+ info.hpkp = false;
+ }
+ } else {
+ // The connection failed.
+ info.state = "broken";
+ info.errorMessage = securityInfo.errorCodeString;
+ }
+
+ // These values can be unset in rare cases, e.g. when stashed connection
+ // data is deseralized from an older version of Firefox.
+ try {
+ info.usedEch = securityInfo.isAcceptedEch;
+ } catch {
+ info.usedEch = false;
+ }
+ try {
+ info.usedDelegatedCredentials = securityInfo.isDelegatedCredential;
+ } catch {
+ info.usedDelegatedCredentials = false;
+ }
+ info.usedOcsp = securityInfo.madeOCSPRequests;
+ info.usedPrivateDns = securityInfo.usedPrivateDNS;
+
+ return info;
+ },
+
+ /**
+ * Takes an nsIX509Cert and returns an object with certificate information.
+ *
+ * @param nsIX509Cert cert
+ * The certificate to extract the information from.
+ * @param Map decodedCertificateCache
+ * A Map of certificate fingerprints to decoded certificates, to avoid
+ * repeatedly decoding previously-seen certificates.
+ * @return object
+ * An object with following format:
+ * {
+ * subject: { commonName, organization, organizationalUnit },
+ * issuer: { commonName, organization, organizationUnit },
+ * validity: { start, end },
+ * fingerprint: { sha1, sha256 }
+ * }
+ */
+ async parseCertificateInfo(cert, decodedCertificateCache) {
+ function getDNComponent(dn, componentType) {
+ for (const [type, value] of dn.entries) {
+ if (type == componentType) {
+ return value;
+ }
+ }
+ return undefined;
+ }
+
+ const info = {};
+ if (cert) {
+ const certHash = cert.sha256Fingerprint;
+ let parsedCert = decodedCertificateCache.get(certHash);
+ if (!parsedCert) {
+ parsedCert = await lazy.certDecoder.parse(
+ lazy.certDecoder.pemToDER(cert.getBase64DERString())
+ );
+ decodedCertificateCache.set(certHash, parsedCert);
+ }
+ info.subject = {
+ commonName: getDNComponent(parsedCert.subject, "Common Name"),
+ organization: getDNComponent(parsedCert.subject, "Organization"),
+ organizationalUnit: getDNComponent(
+ parsedCert.subject,
+ "Organizational Unit"
+ ),
+ };
+
+ info.issuer = {
+ commonName: getDNComponent(parsedCert.issuer, "Common Name"),
+ organization: getDNComponent(parsedCert.issuer, "Organization"),
+ organizationUnit: getDNComponent(
+ parsedCert.issuer,
+ "Organizational Unit"
+ ),
+ };
+
+ info.validity = {
+ start: parsedCert.notBeforeUTC,
+ end: parsedCert.notAfterUTC,
+ };
+
+ info.fingerprint = {
+ sha1: parsedCert.fingerprint.sha1,
+ sha256: parsedCert.fingerprint.sha256,
+ };
+ } else {
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.parseCertificateInfo",
+ "Secure connection established without certificate."
+ );
+ }
+
+ return info;
+ },
+
+ /**
+ * Takes protocolVersion of TransportSecurityInfo object and returns
+ * human readable description.
+ *
+ * @param Number version
+ * One of nsITransportSecurityInfo version constants.
+ * @return string
+ * One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if @param version
+ * is valid, Unknown otherwise.
+ */
+ formatSecurityProtocol(version) {
+ switch (version) {
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1:
+ return "TLSv1";
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1:
+ return "TLSv1.1";
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2:
+ return "TLSv1.2";
+ case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3:
+ return "TLSv1.3";
+ default:
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.formatSecurityProtocol",
+ "protocolVersion " + version + " is unknown."
+ );
+ return "Unknown";
+ }
+ },
+
+ /**
+ * Takes the securityState bitfield and returns reasons for weak connection
+ * as an array of strings.
+ *
+ * @param Number state
+ * nsITransportSecurityInfo.securityState.
+ *
+ * @return Array[String]
+ * List of weakness reasons. A subset of { cipher } where
+ * * cipher: The cipher suite is consireded to be weak (RC4).
+ */
+ getReasonsForWeakness(state) {
+ const wpl = Ci.nsIWebProgressListener;
+
+ // If there's non-fatal security issues the request has STATE_IS_BROKEN
+ // flag set. See https://hg.mozilla.org/mozilla-central/file/44344099d119
+ // /security/manager/ssl/nsNSSCallbacks.cpp#l1233
+ const reasons = [];
+
+ if (state & wpl.STATE_IS_BROKEN) {
+ const isCipher = state & wpl.STATE_USES_WEAK_CRYPTO;
+
+ if (isCipher) {
+ reasons.push("cipher");
+ }
+
+ if (!isCipher) {
+ lazy.DevToolsInfaillibleUtils.reportException(
+ "NetworkHelper.getReasonsForWeakness",
+ "STATE_IS_BROKEN without a known reason. Full state was: " + state
+ );
+ }
+ }
+
+ return reasons;
+ },
+
+ /**
+ * Parse a url's query string into its components
+ *
+ * @param string queryString
+ * The query part of a url
+ * @return array
+ * Array of query params {name, value}
+ */
+ parseQueryString(queryString) {
+ // Make sure there's at least one param available.
+ // Be careful here, params don't necessarily need to have values, so
+ // no need to verify the existence of a "=".
+ if (!queryString) {
+ return null;
+ }
+
+ // Turn the params string into an array containing { name: value } tuples.
+ const paramsArray = queryString
+ .replace(/^[?&]/, "")
+ .split("&")
+ .map(e => {
+ const param = e.split("=");
+ return {
+ name: param[0]
+ ? NetworkHelper.convertToUnicode(unescape(param[0]))
+ : "",
+ value: param[1]
+ ? NetworkHelper.convertToUnicode(unescape(param[1]))
+ : "",
+ };
+ });
+
+ return paramsArray;
+ },
+};