NetworkHelper.sys.mjs (29249B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* 6 * Software License Agreement (BSD License) 7 * 8 * Copyright (c) 2007, Parakey Inc. 9 * All rights reserved. 10 * 11 * Redistribution and use of this software in source and binary forms, 12 * with or without modification, are permitted provided that the 13 * following conditions are met: 14 * 15 * * Redistributions of source code must retain the above 16 * copyright notice, this list of conditions and the 17 * following disclaimer. 18 * 19 * * Redistributions in binary form must reproduce the above 20 * copyright notice, this list of conditions and the 21 * following disclaimer in the documentation and/or other 22 * materials provided with the distribution. 23 * 24 * * Neither the name of Parakey Inc. nor the names of its 25 * contributors may be used to endorse or promote products 26 * derived from this software without specific prior 27 * written permission of Parakey Inc. 28 * 29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 30 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 31 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 32 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 33 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 34 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 35 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 36 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 37 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 38 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 39 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 40 * OF THE POSSIBILITY OF SUCH DAMAGE. 41 */ 42 43 /* 44 * Creator: 45 * Joe Hewitt 46 * Contributors 47 * John J. Barton (IBM Almaden) 48 * Jan Odvarko (Mozilla Corp.) 49 * Max Stepanov (Aptana Inc.) 50 * Rob Campbell (Mozilla Corp.) 51 * Hans Hillen (Paciello Group, Mozilla) 52 * Curtis Bartley (Mozilla Corp.) 53 * Mike Collins (IBM Almaden) 54 * Kevin Decker 55 * Mike Ratcliffe (Comartis AG) 56 * Hernan RodrÃguez Colmeiro 57 * Austin Andrews 58 * Christoph Dorn 59 * Steven Roussey (AppCenter Inc, Network54) 60 * Mihai Sucan (Mozilla Corp.) 61 */ 62 63 const lazy = {}; 64 65 ChromeUtils.defineESModuleGetters( 66 lazy, 67 { 68 DevToolsInfaillibleUtils: 69 "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs", 70 71 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 72 }, 73 { global: "contextual" } 74 ); 75 76 // It would make sense to put this in the above 77 // ChromeUtils.defineESModuleGetters, but that doesn't seem to work. 78 ChromeUtils.defineLazyGetter(lazy, "certDecoder", () => { 79 const { parse, pemToDER } = ChromeUtils.importESModule( 80 "chrome://global/content/certviewer/certDecoder.mjs", 81 { global: "contextual" } 82 ); 83 return { parse, pemToDER }; 84 }); 85 86 // "Lax", "Strict" and "None" are special values of the SameSite cookie 87 // attribute that should not be translated. 88 const COOKIE_SAMESITE = { 89 LAX: "Lax", 90 STRICT: "Strict", 91 NONE: "None", 92 }; 93 94 /** 95 * Helper object for networking stuff. 96 * 97 * Most of the following functions have been taken from the Firebug source. They 98 * have been modified to match the Firefox coding rules. 99 */ 100 export var NetworkHelper = { 101 /** 102 * Add charset to MIME type. 103 * 104 * @param string mimeType 105 * Initial MIME type. 106 * @param string charset 107 * Charset to add to the MIME type. 108 * @returns {string} 109 * Final MIME type. 110 */ 111 addCharsetToMimeType(mimeType, charset) { 112 if (mimeType && charset) { 113 mimeType += "; charset=" + charset; 114 } 115 116 return mimeType; 117 }, 118 119 /** 120 * Converts text with a given charset to unicode. 121 * 122 * @param string text 123 * Text to convert. 124 * @param string charset 125 * Charset to convert the text to. 126 * @param boolean throwOnFailure 127 * Whether exceptions should be bubbled up or swallowed. Defaults to 128 * false. 129 * @returns string 130 * Converted text. 131 */ 132 convertToUnicode(text, charset, throwOnFailure) { 133 // FIXME: We need to throw when text can't be converted e.g. the contents of 134 // an image. Until we have a way to do so with TextEncoder and TextDecoder 135 // we need to use nsIScriptableUnicodeConverter instead. 136 const conv = Cc[ 137 "@mozilla.org/intl/scriptableunicodeconverter" 138 ].createInstance(Ci.nsIScriptableUnicodeConverter); 139 try { 140 conv.charset = charset || "UTF-8"; 141 return conv.ConvertToUnicode(text); 142 } catch (ex) { 143 if (throwOnFailure) { 144 throw ex; 145 } 146 return text; 147 } 148 }, 149 150 /** 151 * Reads all available bytes from stream and converts them to charset. 152 * 153 * @param nsIInputStream stream 154 * @param string charset 155 * @returns string 156 * UTF-16 encoded string based on the content of stream and charset. 157 */ 158 readAndConvertFromStream(stream, charset) { 159 let text = null; 160 try { 161 text = lazy.NetUtil.readInputStreamToString(stream, stream.available()); 162 return this.convertToUnicode(text, charset); 163 } catch (err) { 164 return text; 165 } 166 }, 167 168 /** 169 * Reads the post data from request. 170 * 171 * @param nsIHttpChannel request 172 * @param string charset 173 * The content document charset, used when reading the POSTed data. 174 * 175 * @returns object or null 176 * Returns an object with the following properties: 177 * - {string|null} data: post data as string if it was possible to 178 * read from request, otherwise null. 179 * - {boolean} isDecodedAsText: if the data could be successfully read with 180 * the specified charset. 181 * Returns null if the channel does not implement nsIUploadChannel, 182 * and therefore cannot upload a data stream. 183 */ 184 readPostDataFromRequest(request, charset) { 185 if (!(request instanceof Ci.nsIUploadChannel)) { 186 return null; 187 } 188 const iStream = request.uploadStream; 189 190 let isSeekableStream = false; 191 if (iStream instanceof Ci.nsISeekableStream) { 192 isSeekableStream = true; 193 } 194 195 let prevOffset; 196 if (isSeekableStream) { 197 prevOffset = iStream.tell(); 198 iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); 199 } 200 201 let data = null; 202 let isDecodedAsText = true; 203 204 // Read data from the stream. 205 try { 206 data = lazy.NetUtil.readInputStreamToString(iStream, iStream.available()); 207 } catch { 208 // If we failed to read the stream, assume there is no valid request post 209 // data to display. 210 return null; 211 } 212 213 // Decode the data as text with the provided charset. 214 try { 215 data = this.convertToUnicode(data, charset, true); 216 } catch (err) { 217 isDecodedAsText = false; 218 } 219 220 // Seek locks the file, so seek to the beginning only if necko hasn't 221 // read it yet, since necko doesn't seek to 0 before reading (at lest 222 // not till 459384 is fixed). 223 if (isSeekableStream && prevOffset == 0) { 224 iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); 225 } 226 227 return { data, isDecodedAsText }; 228 }, 229 230 /** 231 * Gets the topFrameElement that is associated with request. This 232 * works in single-process and multiprocess contexts. It may cross 233 * the content/chrome boundary. 234 * 235 * @param nsIHttpChannel request 236 * @returns Element|null 237 * The top frame element for the given request. 238 */ 239 getTopFrameForRequest(request) { 240 try { 241 return this.getRequestLoadContext(request).topFrameElement; 242 } catch (ex) { 243 // request loadContext is not always available. 244 } 245 return null; 246 }, 247 248 /** 249 * Gets the nsIDOMWindow that is associated with request. 250 * 251 * @param nsIHttpChannel request 252 * @returns nsIDOMWindow or null 253 */ 254 getWindowForRequest(request) { 255 try { 256 return this.getRequestLoadContext(request).associatedWindow; 257 } catch (ex) { 258 // On some request notificationCallbacks and loadGroup are both null, 259 // so that we can't retrieve any nsILoadContext interface. 260 // Fallback on nsILoadInfo to try to retrieve the request's window. 261 // (this is covered by test_network_get.html and its CSS request) 262 return request.loadInfo.loadingDocument?.defaultView; 263 } 264 }, 265 266 /** 267 * Gets the nsILoadContext that is associated with request. 268 * 269 * @param nsIHttpChannel request 270 * @returns nsILoadContext or null 271 */ 272 getRequestLoadContext(request) { 273 try { 274 if (request.loadInfo.workerAssociatedBrowsingContext) { 275 return request.loadInfo.workerAssociatedBrowsingContext; 276 } 277 } catch (ex) { 278 // Ignore. 279 } 280 try { 281 return request.notificationCallbacks.getInterface(Ci.nsILoadContext); 282 } catch (ex) { 283 // Ignore. 284 } 285 286 try { 287 return request.loadGroup.notificationCallbacks.getInterface( 288 Ci.nsILoadContext 289 ); 290 } catch (ex) { 291 // Ignore. 292 } 293 294 return null; 295 }, 296 297 /** 298 * Loads the content of url from the cache. 299 * 300 * @param string url 301 * URL to load the cached content for. 302 * @param string charset 303 * Assumed charset of the cached content. Used if there is no charset 304 * on the channel directly. 305 * @param function callback 306 * Callback that is called with the loaded cached content if available 307 * or null if something failed while getting the cached content. 308 */ 309 loadFromCache(url, charset, callback) { 310 const channel = lazy.NetUtil.newChannel({ 311 uri: url, 312 loadUsingSystemPrincipal: true, 313 }); 314 315 // Ensure that we only read from the cache and not the server. 316 channel.loadFlags = 317 Ci.nsIRequest.LOAD_FROM_CACHE | 318 Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | 319 Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY; 320 321 lazy.NetUtil.asyncFetch(channel, (inputStream, statusCode, request) => { 322 if (!Components.isSuccessCode(statusCode)) { 323 callback(null); 324 return; 325 } 326 327 // Try to get the encoding from the channel. If there is none, then use 328 // the passed assumed charset. 329 const requestChannel = request.QueryInterface(Ci.nsIChannel); 330 const contentCharset = requestChannel.contentCharset || charset; 331 332 // Read the content of the stream using contentCharset as encoding. 333 callback(this.readAndConvertFromStream(inputStream, contentCharset)); 334 }); 335 }, 336 337 /** 338 * Parse a raw Cookie header value. 339 * 340 * @param string header 341 * The raw Cookie header value. 342 * @return array 343 * Array holding an object for each cookie. Each object holds the 344 * following properties: name and value. 345 */ 346 parseCookieHeader(header) { 347 const cookies = header.split(";"); 348 const result = []; 349 350 cookies.forEach(function (cookie) { 351 const equal = cookie.indexOf("="); 352 const name = cookie.substr(0, equal); 353 const value = cookie.substr(equal + 1); 354 result.push({ 355 name: unescape(name.trim()), 356 value: unescape(value.trim()), 357 }); 358 }); 359 360 return result; 361 }, 362 363 /** 364 * Parse a raw Set-Cookie header value. 365 * 366 * @param array headers 367 * Array of raw Set-Cookie header values. 368 * @return array 369 * Array holding an object for each cookie. Each object holds the 370 * following properties: name, value, secure (boolean), httpOnly 371 * (boolean), path, domain, samesite and expires (ISO date string). 372 */ 373 parseSetCookieHeaders(headers) { 374 function parseSameSiteAttribute(attribute) { 375 attribute = attribute.toLowerCase(); 376 switch (attribute) { 377 case COOKIE_SAMESITE.LAX.toLowerCase(): 378 return COOKIE_SAMESITE.LAX; 379 case COOKIE_SAMESITE.STRICT.toLowerCase(): 380 return COOKIE_SAMESITE.STRICT; 381 default: 382 return COOKIE_SAMESITE.NONE; 383 } 384 } 385 386 const cookies = []; 387 388 for (const header of headers) { 389 const rawCookies = header.split(/\r\n|\n|\r/); 390 391 rawCookies.forEach(function (cookie) { 392 const equal = cookie.indexOf("="); 393 const name = unescape(cookie.substr(0, equal).trim()); 394 const parts = cookie.substr(equal + 1).split(";"); 395 const value = unescape(parts.shift().trim()); 396 397 cookie = { name, value }; 398 399 parts.forEach(function (part) { 400 part = part.trim(); 401 if (part.toLowerCase() == "secure") { 402 cookie.secure = true; 403 } else if (part.toLowerCase() == "httponly") { 404 cookie.httpOnly = true; 405 } else if (part.indexOf("=") > -1) { 406 const pair = part.split("="); 407 pair[0] = pair[0].toLowerCase(); 408 if (pair[0] == "path" || pair[0] == "domain") { 409 cookie[pair[0]] = pair[1]; 410 } else if (pair[0] == "samesite") { 411 cookie[pair[0]] = parseSameSiteAttribute(pair[1]); 412 } else if (pair[0] == "expires") { 413 try { 414 pair[1] = pair[1].replace(/-/g, " "); 415 cookie.expires = new Date(pair[1]).toISOString(); 416 } catch (ex) { 417 // Ignore. 418 } 419 } 420 } 421 }); 422 423 cookies.push(cookie); 424 }); 425 } 426 427 return cookies; 428 }, 429 430 // This is a list of all the mime category maps jviereck could find in the 431 // firebug code base. 432 mimeCategoryMap: { 433 "text/plain": "txt", 434 "text/html": "html", 435 "text/xml": "xml", 436 "text/xsl": "txt", 437 "text/xul": "txt", 438 "text/css": "css", 439 "text/sgml": "txt", 440 "text/rtf": "txt", 441 "text/x-setext": "txt", 442 "text/richtext": "txt", 443 "text/javascript": "js", 444 "text/jscript": "txt", 445 "text/tab-separated-values": "txt", 446 "text/rdf": "txt", 447 "text/xif": "txt", 448 "text/ecmascript": "js", 449 "text/vnd.curl": "txt", 450 "text/x-json": "json", 451 "text/x-js": "txt", 452 "text/js": "txt", 453 "text/vbscript": "txt", 454 "view-source": "txt", 455 "view-fragment": "txt", 456 "application/xml": "xml", 457 "application/xhtml+xml": "xml", 458 "application/atom+xml": "xml", 459 "application/rss+xml": "xml", 460 "application/vnd.mozilla.maybe.feed": "xml", 461 "application/javascript": "js", 462 "application/x-javascript": "js", 463 "application/x-httpd-php": "txt", 464 "application/rdf+xml": "xml", 465 "application/ecmascript": "js", 466 "application/http-index-format": "txt", 467 "application/json": "json", 468 "application/x-js": "txt", 469 "application/x-mpegurl": "txt", 470 "application/vnd.apple.mpegurl": "txt", 471 "multipart/mixed": "txt", 472 "multipart/x-mixed-replace": "txt", 473 "image/svg+xml": "svg", 474 "application/octet-stream": "bin", 475 "image/jpeg": "image", 476 "image/jpg": "image", 477 "image/gif": "image", 478 "image/png": "image", 479 "image/bmp": "image", 480 "application/x-shockwave-flash": "flash", 481 "video/x-flv": "flash", 482 "audio/mpeg3": "media", 483 "audio/x-mpeg-3": "media", 484 "video/mpeg": "media", 485 "video/x-mpeg": "media", 486 "video/vnd.mpeg.dash.mpd": "xml", 487 "audio/ogg": "media", 488 "application/ogg": "media", 489 "application/x-ogg": "media", 490 "application/x-midi": "media", 491 "audio/midi": "media", 492 "audio/x-mid": "media", 493 "audio/x-midi": "media", 494 "music/crescendo": "media", 495 "audio/wav": "media", 496 "audio/x-wav": "media", 497 "text/json": "json", 498 "application/x-json": "json", 499 "application/json-rpc": "json", 500 "application/x-web-app-manifest+json": "json", 501 "application/manifest+json": "json", 502 }, 503 504 /** 505 * Check if the given MIME type is a text-only MIME type. 506 * 507 * @param string mimeType 508 * @return boolean 509 */ 510 isTextMimeType(mimeType) { 511 if (mimeType.indexOf("text/") == 0) { 512 return true; 513 } 514 515 // XML and JSON often come with custom MIME types, so in addition to the 516 // standard "application/xml" and "application/json", we also look for 517 // variants like "application/x-bigcorp+xml". For JSON we allow "+json" and 518 // "-json" as suffixes. 519 if (/^application\/\w+(?:[\.-]\w+)*(?:\+xml|[-+]json)$/.test(mimeType)) { 520 return true; 521 } 522 523 const category = this.mimeCategoryMap[mimeType] || null; 524 switch (category) { 525 case "txt": 526 case "js": 527 case "json": 528 case "css": 529 case "html": 530 case "svg": 531 case "xml": 532 return true; 533 534 default: 535 return false; 536 } 537 }, 538 539 /** 540 * Takes a securityInfo object of nsIRequest, the nsIRequest itself and 541 * extracts security information from them. 542 * 543 * @param object securityInfo 544 * The securityInfo object of a request. If null channel is assumed 545 * to be insecure. 546 * @param object originAttributes 547 * The OriginAttributes of the request. 548 * @param object httpActivity 549 * The httpActivity object for the request with at least members 550 * { private, hostname }. 551 * @param Map decodedCertificateCache 552 * A Map of certificate fingerprints to decoded certificates, to avoid 553 * repeatedly decoding previously-seen certificates. 554 * 555 * @return object 556 * Returns an object containing following members: 557 * - state: The security of the connection used to fetch this 558 * request. Has one of following string values: 559 * * "insecure": the connection was not secure (only http) 560 * * "weak": the connection has minor security issues 561 * * "broken": secure connection failed (e.g. expired cert) 562 * * "secure": the connection was properly secured. 563 * If state == broken: 564 * - errorMessage: error code string. 565 * If state == secure: 566 * - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3. 567 * - cipherSuite: the cipher suite used in this connection. 568 * - cert: information about certificate used in this connection. 569 * See parseCertificateInfo for the contents. 570 * - hsts: true if host uses Strict Transport Security, 571 * false otherwise 572 * - hpkp: true if host uses Public Key Pinning, false otherwise 573 * If state == weak: Same as state == secure and 574 * - weaknessReasons: list of reasons that cause the request to be 575 * considered weak. See getReasonsForWeakness. 576 */ 577 async parseSecurityInfo( 578 securityInfo, 579 originAttributes, 580 httpActivity, 581 decodedCertificateCache 582 ) { 583 const info = { 584 state: "insecure", 585 }; 586 587 // The request did not contain any security info. 588 if (!securityInfo) { 589 return info; 590 } 591 592 /** 593 * Different scenarios to consider here and how they are handled: 594 * - request is HTTP, the connection is not secure 595 * => securityInfo is null 596 * => state === "insecure" 597 * 598 * - request is HTTPS, the connection is secure 599 * => .securityState has STATE_IS_SECURE flag 600 * => state === "secure" 601 * 602 * - request is HTTPS, the connection has security issues 603 * => .securityState has STATE_IS_INSECURE flag 604 * => .errorCode is an NSS error code. 605 * => state === "broken" 606 * 607 * - request is HTTPS, the connection was terminated before the security 608 * could be validated 609 * => .securityState has STATE_IS_INSECURE flag 610 * => .errorCode is NOT an NSS error code. 611 * => .errorMessage is not available. 612 * => state === "insecure" 613 * 614 * - request is HTTPS but it uses a weak cipher or old protocol, see 615 * https://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/ 616 * security/manager/ssl/nsNSSCallbacks.cpp#l1233 617 * - request is mixed content (which makes no sense whatsoever) 618 * => .securityState has STATE_IS_BROKEN flag 619 * => .errorCode is NOT an NSS error code 620 * => .errorMessage is not available 621 * => state === "weak" 622 */ 623 624 const wpl = Ci.nsIWebProgressListener; 625 const NSSErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService( 626 Ci.nsINSSErrorsService 627 ); 628 if (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) { 629 const state = securityInfo.securityState; 630 631 let uri = null; 632 if (httpActivity.channel?.URI) { 633 uri = httpActivity.channel.URI; 634 } 635 if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) { 636 // it is not enough to look at the transport security info - 637 // schemes other than https and wss are subject to 638 // downgrade/etc at the scheme level and should always be 639 // considered insecure 640 info.state = "insecure"; 641 } else if (state & wpl.STATE_IS_SECURE) { 642 // The connection is secure if the scheme is sufficient 643 info.state = "secure"; 644 } else if (state & wpl.STATE_IS_BROKEN) { 645 // The connection is not secure, there was no error but there's some 646 // minor security issues. 647 info.state = "weak"; 648 info.weaknessReasons = this.getReasonsForWeakness(state); 649 } else if (state & wpl.STATE_IS_INSECURE) { 650 // This was most likely an https request that was aborted before 651 // validation. Return info as info.state = insecure. 652 return info; 653 } else { 654 lazy.DevToolsInfaillibleUtils.reportException( 655 "NetworkHelper.parseSecurityInfo", 656 "Security state " + state + " has no known STATE_IS_* flags." 657 ); 658 return info; 659 } 660 661 // Cipher suite. 662 info.cipherSuite = securityInfo.cipherName; 663 664 // Key exchange group name. 665 info.keaGroupName = securityInfo.keaGroupName; 666 667 // Certificate signature scheme. 668 info.signatureSchemeName = securityInfo.signatureSchemeName; 669 670 // Protocol version. 671 info.protocolVersion = this.formatSecurityProtocol( 672 securityInfo.protocolVersion 673 ); 674 675 // Certificate. 676 info.cert = await this.parseCertificateInfo( 677 securityInfo.serverCert, 678 decodedCertificateCache 679 ); 680 681 // Certificate transparency status. 682 info.certificateTransparency = securityInfo.certificateTransparencyStatus; 683 684 // HSTS and HPKP if available. 685 if (httpActivity.hostname) { 686 const sss = Cc["@mozilla.org/ssservice;1"].getService( 687 Ci.nsISiteSecurityService 688 ); 689 const pkps = Cc[ 690 "@mozilla.org/security/publickeypinningservice;1" 691 ].getService(Ci.nsIPublicKeyPinningService); 692 693 if (!uri) { 694 // isSecureURI only cares about the host, not the scheme. 695 const host = httpActivity.hostname; 696 uri = Services.io.newURI("https://" + host); 697 } 698 699 info.hsts = sss.isSecureURI(uri, originAttributes); 700 info.hpkp = pkps.hostHasPins(uri); 701 } else { 702 lazy.DevToolsInfaillibleUtils.reportException( 703 "NetworkHelper.parseSecurityInfo", 704 "Could not get HSTS/HPKP status as hostname is not available." 705 ); 706 info.hsts = false; 707 info.hpkp = false; 708 } 709 } else { 710 // The connection failed. 711 info.state = "broken"; 712 info.errorMessage = securityInfo.errorCodeString; 713 } 714 715 // These values can be unset in rare cases, e.g. when stashed connection 716 // data is deseralized from an older version of Firefox. 717 try { 718 info.usedEch = securityInfo.isAcceptedEch; 719 } catch { 720 info.usedEch = false; 721 } 722 try { 723 info.usedDelegatedCredentials = securityInfo.isDelegatedCredential; 724 } catch { 725 info.usedDelegatedCredentials = false; 726 } 727 info.usedOcsp = securityInfo.madeOCSPRequests; 728 info.usedPrivateDns = securityInfo.usedPrivateDNS; 729 730 return info; 731 }, 732 733 /** 734 * Takes an nsIX509Cert and returns an object with certificate information. 735 * 736 * @param nsIX509Cert cert 737 * The certificate to extract the information from. 738 * @param Map decodedCertificateCache 739 * A Map of certificate fingerprints to decoded certificates, to avoid 740 * repeatedly decoding previously-seen certificates. 741 * @return object 742 * An object with following format: 743 * { 744 * subject: { commonName, organization, organizationalUnit }, 745 * issuer: { commonName, organization, organizationUnit }, 746 * validity: { start, end }, 747 * fingerprint: { sha1, sha256 } 748 * } 749 */ 750 async parseCertificateInfo(cert, decodedCertificateCache) { 751 function getDNComponent(dn, componentType) { 752 for (const [type, value] of dn.entries) { 753 if (type == componentType) { 754 return value; 755 } 756 } 757 return undefined; 758 } 759 760 const info = {}; 761 if (cert) { 762 const certHash = cert.sha256Fingerprint; 763 let parsedCert = decodedCertificateCache.get(certHash); 764 if (!parsedCert) { 765 parsedCert = await lazy.certDecoder.parse( 766 lazy.certDecoder.pemToDER(cert.getBase64DERString()) 767 ); 768 decodedCertificateCache.set(certHash, parsedCert); 769 } 770 info.subject = { 771 commonName: getDNComponent(parsedCert.subject, "Common Name"), 772 organization: getDNComponent(parsedCert.subject, "Organization"), 773 organizationalUnit: getDNComponent( 774 parsedCert.subject, 775 "Organizational Unit" 776 ), 777 }; 778 779 info.issuer = { 780 commonName: getDNComponent(parsedCert.issuer, "Common Name"), 781 organization: getDNComponent(parsedCert.issuer, "Organization"), 782 organizationUnit: getDNComponent( 783 parsedCert.issuer, 784 "Organizational Unit" 785 ), 786 }; 787 788 info.validity = { 789 start: parsedCert.notBeforeUTC, 790 end: parsedCert.notAfterUTC, 791 }; 792 793 info.fingerprint = { 794 sha1: parsedCert.fingerprint.sha1, 795 sha256: parsedCert.fingerprint.sha256, 796 }; 797 } else { 798 lazy.DevToolsInfaillibleUtils.reportException( 799 "NetworkHelper.parseCertificateInfo", 800 "Secure connection established without certificate." 801 ); 802 } 803 804 return info; 805 }, 806 807 /** 808 * Takes protocolVersion of TransportSecurityInfo object and returns 809 * human readable description. 810 * 811 * @param Number version 812 * One of nsITransportSecurityInfo version constants. 813 * @return string 814 * One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if @param version 815 * is valid, Unknown otherwise. 816 */ 817 formatSecurityProtocol(version) { 818 switch (version) { 819 case Ci.nsITransportSecurityInfo.TLS_VERSION_1: 820 return "TLSv1"; 821 case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1: 822 return "TLSv1.1"; 823 case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2: 824 return "TLSv1.2"; 825 case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3: 826 return "TLSv1.3"; 827 default: 828 lazy.DevToolsInfaillibleUtils.reportException( 829 "NetworkHelper.formatSecurityProtocol", 830 "protocolVersion " + version + " is unknown." 831 ); 832 return "Unknown"; 833 } 834 }, 835 836 /** 837 * Takes the securityState bitfield and returns reasons for weak connection 838 * as an array of strings. 839 * 840 * @param Number state 841 * nsITransportSecurityInfo.securityState. 842 * 843 * @return Array[String] 844 * List of weakness reasons. A subset of { cipher } where 845 * * cipher: The cipher suite is consireded to be weak (RC4). 846 */ 847 getReasonsForWeakness(state) { 848 const wpl = Ci.nsIWebProgressListener; 849 850 // If there's non-fatal security issues the request has STATE_IS_BROKEN 851 // flag set. See https://hg.mozilla.org/mozilla-central/file/44344099d119 852 // /security/manager/ssl/nsNSSCallbacks.cpp#l1233 853 const reasons = []; 854 855 if (state & wpl.STATE_IS_BROKEN) { 856 const isCipher = state & wpl.STATE_USES_WEAK_CRYPTO; 857 858 if (isCipher) { 859 reasons.push("cipher"); 860 } 861 862 if (!isCipher) { 863 lazy.DevToolsInfaillibleUtils.reportException( 864 "NetworkHelper.getReasonsForWeakness", 865 "STATE_IS_BROKEN without a known reason. Full state was: " + state 866 ); 867 } 868 } 869 870 return reasons; 871 }, 872 873 /** 874 * Parse a url's query string into its components 875 * 876 * @param string queryString 877 * The query part of a url 878 * @return array 879 * Array of query params {name, value} 880 */ 881 parseQueryString(queryString) { 882 // Make sure there's at least one param available. 883 // Be careful here, params don't necessarily need to have values, so 884 // no need to verify the existence of a "=". 885 if (!queryString) { 886 return null; 887 } 888 889 // Turn the params string into an array containing { name: value } tuples. 890 const paramsArray = queryString 891 .replace(/^[?&]/, "") 892 .split("&") 893 .map(e => { 894 const param = e.split("="); 895 return { 896 name: param[0] 897 ? NetworkHelper.convertToUnicode(unescape(param[0])) 898 : "", 899 value: param[1] 900 ? NetworkHelper.convertToUnicode(unescape(param[1])) 901 : "", 902 }; 903 }); 904 905 return paramsArray; 906 }, 907 };