Source code

Revision control

Copy as Markdown

Other Tools

/* 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.)
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{
DevToolsInfaillibleUtils:
},
{ global: "contextual" }
);
// It would make sense to put this in the above
// ChromeUtils.defineESModuleGetters, but that doesn't seem to work.
ChromeUtils.defineLazyGetter(lazy, "certDecoder", () => {
const { parse, pemToDER } = ChromeUtils.importESModule(
{ global: "contextual" }
);
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;
},
/**
* 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;
},
/**
* 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
* 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
// /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;
},
};