Mypal/devtools/shared/webconsole/network-helper.js

843 lines
27 KiB
JavaScript

/* vim:set ts=2 sw=2 sts=2 et: */
/*
* 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.)
*/
"use strict";
const {components, Cc, Ci} = require("chrome");
loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const Services = require("Services");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/netmonitor.properties");
// The cache used in the `nsIURL` function.
const gNSURLStore = new Map();
/**
* 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.
*/
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: function (text, charset) {
let 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: function (stream, charset) {
let text = null;
try {
text = 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: function (request, charset) {
if (request instanceof Ci.nsIUploadChannel) {
let 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.
let 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: function (docShell, charset) {
let 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: function (webNav, charset) {
if (webNav instanceof Ci.nsIWebPageDescriptor) {
let 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 web appId that is associated with request.
*
* @param nsIHttpChannel request
* @returns number|null
* The appId for the given request, if available.
*/
getAppIdForRequest: function (request) {
try {
return this.getRequestLoadContext(request).appId;
} catch (ex) {
// request loadContent is not always available.
}
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 nsIDOMElement|null
* The top frame element for the given request.
*/
getTopFrameForRequest: function (request) {
try {
return this.getRequestLoadContext(request).topFrameElement;
} catch (ex) {
// request loadContent is not always available.
}
return null;
},
/**
* Gets the nsIDOMWindow that is associated with request.
*
* @param nsIHttpChannel request
* @returns nsIDOMWindow or null
*/
getWindowForRequest: function (request) {
try {
return this.getRequestLoadContext(request).associatedWindow;
} catch (ex) {
// TODO: bug 802246 - getWindowForRequest() throws on b2g: there is no
// associatedWindow property.
}
return null;
},
/**
* Gets the nsILoadContext that is associated with request.
*
* @param nsIHttpChannel request
* @returns nsILoadContext or null
*/
getRequestLoadContext: function (request) {
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: function (request) {
if (request instanceof Ci.nsIChannel) {
let loadInfo = request.loadInfo;
if (loadInfo && 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: function (url, charset, callback) {
let channel = 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;
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.
let requestChannel = request.QueryInterface(Ci.nsIChannel);
let 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: function (header) {
let cookies = header.split(";");
let result = [];
cookies.forEach(function (cookie) {
let equal = cookie.indexOf("=");
let name = cookie.substr(0, equal);
let 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 string header
* The raw Set-Cookie header value.
* @return array
* Array holding an object for each cookie. Each object holds the
* following properties: name, value, secure (boolean), httpOnly
* (boolean), path, domain and expires (ISO date string).
*/
parseSetCookieHeader: function (header) {
let rawCookies = header.split(/\r\n|\n|\r/);
let cookies = [];
rawCookies.forEach(function (cookie) {
let equal = cookie.indexOf("=");
let name = unescape(cookie.substr(0, equal).trim());
let parts = cookie.substr(equal + 1).split(";");
let value = unescape(parts.shift().trim());
cookie = {name: name, value: 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) {
let pair = part.split("=");
pair[0] = pair[0].toLowerCase();
if (pair[0] == "path" || pair[0] == "domain") {
cookie[pair[0]] = 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/vnd.mozilla.xul+xml": "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: function (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;
}
let 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 httpActivity
* The httpActivity object for the request with at least members
* { private, hostname }.
*
* @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: full error message from
* nsITransportSecurityInfo.
* 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.
*/
parseSecurityInfo: function (securityInfo, httpActivity) {
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
* http://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"
*/
securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
securityInfo.QueryInterface(Ci.nsISSLStatusProvider);
const wpl = Ci.nsIWebProgressListener;
const NSSErrorsService = Cc["@mozilla.org/nss_errors_service;1"]
.getService(Ci.nsINSSErrorsService);
const SSLStatus = securityInfo.SSLStatus;
if (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
const state = securityInfo.securityState;
let uri = null;
if (httpActivity.channel && 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 {
DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo",
"Security state " + state + " has no known STATE_IS_* flags.");
return info;
}
// Cipher suite.
info.cipherSuite = SSLStatus.cipherName;
// Key exchange group name.
info.keaGroupName = SSLStatus.keaGroupName;
// Localise two special values.
if (info.keaGroupName == "none") {
info.keaGroupName = L10N.getStr("netmonitor.security.keaGroup.none");
}
if (info.keaGroupName == "custom") {
info.keaGroupName = L10N.getStr("netmonitor.security.keaGroup.custom");
}
if (info.keaGroupName == "unknown group") {
info.keaGroupName = L10N.getStr("netmonitor.security.keaGroup.unknown");
}
// Certificate signature scheme.
info.signatureSchemeName = SSLStatus.signatureSchemeName;
// Localise two special values.
if (info.signatureSchemeName == "none") {
info.signatureSchemeName =
L10N.getStr("netmonitor.security.signatureScheme.none");
}
if (info.signatureSchemeName == "unknown signature") {
info.signatureSchemeName =
L10N.getStr("netmonitor.security.signatureScheme.unknown");
}
// Protocol version.
info.protocolVersion =
this.formatSecurityProtocol(SSLStatus.protocolVersion);
// Certificate.
info.cert = this.parseCertificateInfo(SSLStatus.serverCert);
// HSTS and HPKP if available.
if (httpActivity.hostname) {
const sss = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
// SiteSecurityService uses different storage if the channel is
// private. Thus we must give isSecureHost correct flags or we
// might get incorrect results.
let flags = (httpActivity.private) ?
Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0;
let host = httpActivity.hostname;
info.hsts = sss.isSecureHost(sss.HEADER_HSTS, host, flags);
// HPKP is no longer supported.
info.hpkp = false;
} else {
DevToolsUtils.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.errorMessage;
}
return info;
},
/**
* Takes an nsIX509Cert and returns an object with certificate information.
*
* @param nsIX509Cert cert
* The certificate to extract the information from.
* @return object
* An object with following format:
* {
* subject: { commonName, organization, organizationalUnit },
* issuer: { commonName, organization, organizationUnit },
* validity: { start, end },
* fingerprint: { sha1, sha256 }
* }
*/
parseCertificateInfo: function (cert) {
let info = {};
if (cert) {
info.subject = {
commonName: cert.commonName,
organization: cert.organization,
organizationalUnit: cert.organizationalUnit,
};
info.issuer = {
commonName: cert.issuerCommonName,
organization: cert.issuerOrganization,
organizationUnit: cert.issuerOrganizationUnit,
};
info.validity = {
start: cert.validity.notBeforeLocalDay,
end: cert.validity.notAfterLocalDay,
};
info.fingerprint = {
sha1: cert.sha1Fingerprint,
sha256: cert.sha256Fingerprint,
};
} else {
DevToolsUtils.reportException("NetworkHelper.parseCertificateInfo",
"Secure connection established without certificate.");
}
return info;
},
/**
* Takes protocolVersion of SSLStatus object and returns human readable
* description.
*
* @param Number version
* One of nsISSLStatus version constants.
* @return string
* One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if @param version
* is valid, Unknown otherwise.
*/
formatSecurityProtocol: function (version) {
switch (version) {
case Ci.nsISSLStatus.TLS_VERSION_1:
return "TLSv1";
case Ci.nsISSLStatus.TLS_VERSION_1_1:
return "TLSv1.1";
case Ci.nsISSLStatus.TLS_VERSION_1_2:
return "TLSv1.2";
case Ci.nsISSLStatus.TLS_VERSION_1_3:
return "TLSv1.3";
default:
DevToolsUtils.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: function (state) {
const wpl = Ci.nsIWebProgressListener;
// If there's non-fatal security issues the request has STATE_IS_BROKEN
// flag set. See http://hg.mozilla.org/mozilla-central/file/44344099d119
// /security/manager/ssl/nsNSSCallbacks.cpp#l1233
let reasons = [];
if (state & wpl.STATE_IS_BROKEN) {
let isCipher = state & wpl.STATE_USES_WEAK_CRYPTO;
if (isCipher) {
reasons.push("cipher");
}
if (!isCipher) {
DevToolsUtils.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: function (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.
let paramsArray = queryString.replace(/^[?&]/, "").split("&").map(e => {
let param = e.split("=");
return {
name: param[0] ?
NetworkHelper.convertToUnicode(unescape(param[0])) : "",
value: param[1] ?
NetworkHelper.convertToUnicode(unescape(param[1])) : ""
};
});
return paramsArray;
},
/**
* Helper for getting an nsIURL instance out of a string.
*/
nsIURL: function (url, store = gNSURLStore) {
if (store.has(url)) {
return store.get(url);
}
let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
store.set(url, uri);
return uri;
}
};
for (let prop of Object.getOwnPropertyNames(NetworkHelper)) {
exports[prop] = NetworkHelper[prop];
}