/* 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; }, };