storage.sys.mjs (28146B)
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 import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", 11 BytesValueType: 12 "chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs", 13 deserializeBytesValue: 14 "chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs", 15 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 16 pprint: "chrome://remote/content/shared/Format.sys.mjs", 17 UserContextManager: 18 "chrome://remote/content/shared/UserContextManager.sys.mjs", 19 }); 20 21 const PREF_COOKIE_CHIPS_ENABLED = "network.cookie.CHIPS.enabled"; 22 const PREF_COOKIE_BEHAVIOR = "network.cookie.cookieBehavior"; 23 24 // This is a static preference, so it cannot be modified during runtime and we can cache its value. 25 ChromeUtils.defineLazyGetter(lazy, "cookieCHIPSEnabled", () => 26 Services.prefs.getBoolPref(PREF_COOKIE_CHIPS_ENABLED) 27 ); 28 29 const CookieFieldsMapping = { 30 domain: "host", 31 expiry: "expiry", 32 httpOnly: "isHttpOnly", 33 name: "name", 34 path: "path", 35 sameSite: "sameSite", 36 secure: "isSecure", 37 size: "size", 38 value: "value", 39 }; 40 41 const MAX_COOKIE_EXPIRY = Number.MAX_SAFE_INTEGER; 42 43 /** 44 * Enum of possible partition types supported by the 45 * storage.getCookies command. 46 * 47 * @readonly 48 * @enum {PartitionType} 49 */ 50 const PartitionType = { 51 Context: "context", 52 StorageKey: "storageKey", 53 }; 54 55 const PartitionKeyAttributes = ["sourceOrigin", "userContext"]; 56 57 /** 58 * Enum of possible SameSite types supported by the 59 * storage.getCookies command. 60 * 61 * @readonly 62 * @enum {SameSiteType} 63 */ 64 const SameSiteType = { 65 [Ci.nsICookie.SAMESITE_NONE]: "none", 66 [Ci.nsICookie.SAMESITE_LAX]: "lax", 67 [Ci.nsICookie.SAMESITE_STRICT]: "strict", 68 [Ci.nsICookie.SAMESITE_UNSET]: "default", 69 }; 70 71 class StorageModule extends RootBiDiModule { 72 destroy() {} 73 74 /** 75 * Used as an argument for storage.getCookies command 76 * to represent fields which should be used to filter the output 77 * of the command. 78 * 79 * @typedef CookieFilter 80 * 81 * @property {string=} domain 82 * @property {number=} expiry 83 * @property {boolean=} httpOnly 84 * @property {string=} name 85 * @property {string=} path 86 * @property {SameSiteType=} sameSite 87 * @property {boolean=} secure 88 * @property {number=} size 89 * @property {Network.BytesValueType=} value 90 */ 91 92 /** 93 * Used as an argument for storage.getCookies command as one of the available variants 94 * {BrowsingContextPartitionDescriptor} or {StorageKeyPartitionDescriptor}, to represent 95 * fields should be used to build a partition key. 96 * 97 * @typedef PartitionDescriptor 98 */ 99 100 /** 101 * @typedef BrowsingContextPartitionDescriptor 102 * 103 * @property {PartitionType} [type=PartitionType.context] 104 * @property {string} context 105 */ 106 107 /** 108 * @typedef StorageKeyPartitionDescriptor 109 * 110 * @property {PartitionType} [type=PartitionType.storageKey] 111 * @property {string=} sourceOrigin 112 * @property {string=} userContext 113 */ 114 115 /** 116 * @typedef PartitionKey 117 * 118 * @property {string=} sourceOrigin 119 * @property {string=} userContext 120 */ 121 122 /** 123 * An object that holds the result of storage.getCookies command. 124 * 125 * @typedef GetCookiesResult 126 * 127 * @property {Array<Cookie>} cookies 128 * List of cookies. 129 * @property {PartitionKey} partitionKey 130 * An object which represent the partition key which was used 131 * to retrieve the cookies. 132 */ 133 134 /** 135 * Remove zero or more cookies which match a set of provided parameters. 136 * 137 * @param {object=} options 138 * @param {CookieFilter=} options.filter 139 * An object which holds field names and values, which 140 * should be used to filter the output of the command. 141 * @param {PartitionDescriptor=} options.partition 142 * An object which holds the information which 143 * should be used to build a partition key. 144 * 145 * @returns {PartitionKey} 146 * An object with the partition key which was used to 147 * retrieve cookies which had to be removed. 148 * @throws {InvalidArgumentError} 149 * If the provided arguments are not valid. 150 * @throws {NoSuchFrameError} 151 * If the provided browsing context cannot be found. 152 */ 153 async deleteCookies(options = {}) { 154 let { filter = {} } = options; 155 const { partition: partitionSpec = null } = options; 156 157 this.#assertPartition(partitionSpec); 158 filter = this.#assertCookieFilter(filter); 159 160 const partitionKey = this.#expandStoragePartitionSpec(partitionSpec); 161 const store = this.#getTheCookieStore(partitionKey); 162 const cookies = this.#getMatchingCookies(store, filter); 163 164 for (const cookie of cookies) { 165 Services.cookies.remove( 166 cookie.host, 167 cookie.name, 168 cookie.path, 169 cookie.originAttributes 170 ); 171 } 172 173 return { partitionKey: this.#formatPartitionKey(partitionKey) }; 174 } 175 176 /** 177 * Retrieve zero or more cookies which match a set of provided parameters. 178 * 179 * @param {object=} options 180 * @param {CookieFilter=} options.filter 181 * An object which holds field names and values, which 182 * should be used to filter the output of the command. 183 * @param {PartitionDescriptor=} options.partition 184 * An object which holds the information which 185 * should be used to build a partition key. 186 * 187 * @returns {GetCookiesResult} 188 * An object which holds a list of retrieved cookies and 189 * the partition key which was used. 190 * @throws {InvalidArgumentError} 191 * If the provided arguments are not valid. 192 * @throws {NoSuchFrameError} 193 * If the provided browsing context cannot be found. 194 */ 195 async getCookies(options = {}) { 196 let { filter = {} } = options; 197 const { partition: partitionSpec = null } = options; 198 199 this.#assertPartition(partitionSpec); 200 filter = this.#assertCookieFilter(filter); 201 202 const partitionKey = this.#expandStoragePartitionSpec(partitionSpec); 203 const store = this.#getTheCookieStore(partitionKey); 204 const cookies = this.#getMatchingCookies(store, filter); 205 const serializedCookies = []; 206 207 for (const cookie of cookies) { 208 serializedCookies.push(this.#serializeCookie(cookie)); 209 } 210 211 return { 212 cookies: serializedCookies, 213 partitionKey: this.#formatPartitionKey(partitionKey), 214 }; 215 } 216 217 /** 218 * An object representation of the cookie which should be set. 219 * 220 * @typedef PartialCookie 221 * 222 * @property {string} domain 223 * @property {number=} expiry 224 * @property {boolean=} httpOnly 225 * @property {string} name 226 * @property {string=} path 227 * @property {SameSiteType=} sameSite 228 * @property {boolean=} secure 229 * @property {number=} size 230 * @property {Network.BytesValueType} value 231 */ 232 233 /** 234 * Create a new cookie in a cookie store. 235 * 236 * @param {object=} options 237 * @param {PartialCookie} options.cookie 238 * An object representation of the cookie which 239 * should be set. 240 * @param {PartitionDescriptor=} options.partition 241 * An object which holds the information which 242 * should be used to build a partition key. 243 * 244 * @returns {PartitionKey} 245 * An object with the partition key which was used to 246 * add the cookie. 247 * @throws {InvalidArgumentError} 248 * If the provided arguments are not valid. 249 * @throws {NoSuchFrameError} 250 * If the provided browsing context cannot be found. 251 * @throws {UnableToSetCookieError} 252 * If the cookie was not added. 253 */ 254 async setCookie(options = {}) { 255 const { cookie: cookieSpec, partition: partitionSpec = null } = options; 256 lazy.assert.object( 257 cookieSpec, 258 lazy.pprint`Expected "cookie" to be an object, got ${cookieSpec}` 259 ); 260 261 const { 262 domain, 263 expiry = null, 264 httpOnly = null, 265 name, 266 path = null, 267 sameSite = null, 268 secure = null, 269 value, 270 } = cookieSpec; 271 this.#assertCookie({ 272 domain, 273 expiry, 274 httpOnly, 275 name, 276 path, 277 sameSite, 278 secure, 279 value, 280 }); 281 this.#assertPartition(partitionSpec); 282 283 const partitionKey = this.#expandStoragePartitionSpec(partitionSpec); 284 285 // The cookie store is defined by originAttributes. 286 const originAttributes = this.#getOriginAttributes(partitionKey, domain); 287 288 // The cookie value is a network.BytesValue. 289 const deserializedValue = lazy.deserializeBytesValue(value); 290 291 // The XPCOM interface requires to be specified if a cookie is session. 292 const isSession = expiry === null; 293 294 let schemeType; 295 if (secure) { 296 schemeType = Ci.nsICookie.SCHEME_HTTPS; 297 } else { 298 schemeType = Ci.nsICookie.SCHEME_HTTP; 299 } 300 301 const isPartitioned = originAttributes.partitionKey?.length > 0; 302 303 let cv; 304 try { 305 cv = Services.cookies.add( 306 domain, 307 path === null ? "/" : path, 308 name, 309 deserializedValue, 310 secure === null ? false : secure, 311 httpOnly === null ? false : httpOnly, 312 isSession, 313 // The XPCOM interface requires the expiry field even for session cookies. 314 // The expiry value must be passed in milliseconds and is capped at 400 315 // days. 316 expiry === null 317 ? MAX_COOKIE_EXPIRY 318 : Services.cookies.maybeCapExpiry(expiry * 1000), 319 originAttributes, 320 this.#getSameSitePlatformProperty(sameSite), 321 schemeType, 322 isPartitioned 323 ); 324 } catch (e) { 325 throw new lazy.error.UnableToSetCookieError(e); 326 } 327 328 if (cv.result !== Ci.nsICookieValidation.eOK) { 329 throw new lazy.error.UnableToSetCookieError( 330 `Invalid cookie: ${cv.errorString}` 331 ); 332 } 333 334 return { 335 partitionKey: this.#formatPartitionKey(partitionKey, originAttributes), 336 }; 337 } 338 339 #assertCookie(cookie) { 340 lazy.assert.object( 341 cookie, 342 lazy.pprint`Expected "cookie" to be an object, got ${cookie}` 343 ); 344 345 const { domain, expiry, httpOnly, name, path, sameSite, secure, value } = 346 cookie; 347 348 lazy.assert.string( 349 domain, 350 lazy.pprint`Expected cookie "domain" to be a string, got ${domain}` 351 ); 352 353 lazy.assert.string( 354 name, 355 lazy.pprint`Expected cookie "name" to be a string, got ${name}` 356 ); 357 358 this.#assertValue(value); 359 360 if (expiry !== null) { 361 lazy.assert.positiveInteger( 362 expiry, 363 lazy.pprint`Expected cookie "expiry" to be a positive integer, got ${expiry}` 364 ); 365 } 366 367 if (httpOnly !== null) { 368 lazy.assert.boolean( 369 httpOnly, 370 lazy.pprint`Expected cookie "httpOnly" to be a boolean, got ${httpOnly}` 371 ); 372 } 373 374 if (path !== null) { 375 lazy.assert.string( 376 path, 377 lazy.pprint`Expected cookie "path" to be a string, got ${path}` 378 ); 379 } 380 381 this.#assertSameSite(sameSite); 382 383 if (secure !== null) { 384 lazy.assert.boolean( 385 secure, 386 lazy.pprint`Expected cookie "secure" to be a boolean, got ${secure}` 387 ); 388 } 389 } 390 391 #assertCookieFilter(filter) { 392 lazy.assert.object( 393 filter, 394 lazy.pprint`Expected "filter" to be an object, got ${filter}` 395 ); 396 397 const { 398 domain = null, 399 expiry = null, 400 httpOnly = null, 401 name = null, 402 path = null, 403 sameSite = null, 404 secure = null, 405 size = null, 406 value = null, 407 } = filter; 408 409 if (domain !== null) { 410 lazy.assert.string( 411 domain, 412 lazy.pprint`Expected filter "domain" to be a string, got ${domain}` 413 ); 414 } 415 416 if (expiry !== null) { 417 lazy.assert.positiveInteger( 418 expiry, 419 lazy.pprint`Expected filter "expiry" to be a positive integer, got ${expiry}` 420 ); 421 } 422 423 if (httpOnly !== null) { 424 lazy.assert.boolean( 425 httpOnly, 426 lazy.pprint`Expected filter "httpOnly" to be a boolean, got ${httpOnly}` 427 ); 428 } 429 430 if (name !== null) { 431 lazy.assert.string( 432 name, 433 lazy.pprint`Expected filter "name" to be a string, got ${name}` 434 ); 435 } 436 437 if (path !== null) { 438 lazy.assert.string( 439 path, 440 lazy.pprint`Expected filter "path" to be a string, got ${path}` 441 ); 442 } 443 444 this.#assertSameSite(sameSite, "filter.sameSite"); 445 446 if (secure !== null) { 447 lazy.assert.boolean( 448 secure, 449 lazy.pprint`Expected filter "secure" to be a boolean, got ${secure}` 450 ); 451 } 452 453 if (size !== null) { 454 lazy.assert.positiveInteger( 455 size, 456 lazy.pprint`Expected filter "size" to be a positive integer, got ${size}` 457 ); 458 } 459 460 if (value !== null) { 461 this.#assertValue(value, "filter.value"); 462 } 463 464 return { 465 domain, 466 expiry, 467 httpOnly, 468 name, 469 path, 470 sameSite, 471 secure, 472 size, 473 value, 474 }; 475 } 476 477 #assertPartition(partitionSpec) { 478 if (partitionSpec === null) { 479 return; 480 } 481 lazy.assert.object( 482 partitionSpec, 483 lazy.pprint`Expected "partition" to be an object, got ${partitionSpec}` 484 ); 485 486 const { type } = partitionSpec; 487 lazy.assert.string( 488 type, 489 lazy.pprint`Expected partition "type" to be a string, got ${type}` 490 ); 491 492 switch (type) { 493 case PartitionType.Context: { 494 const { context } = partitionSpec; 495 lazy.assert.string( 496 context, 497 lazy.pprint`Expected partition "context" to be a string, got ${context}` 498 ); 499 500 break; 501 } 502 503 case PartitionType.StorageKey: { 504 const { sourceOrigin = null, userContext = null } = partitionSpec; 505 if (sourceOrigin !== null) { 506 lazy.assert.string( 507 sourceOrigin, 508 lazy.pprint`Expected partition "sourceOrigin" to be a string, got ${sourceOrigin}` 509 ); 510 lazy.assert.that( 511 sourceOrigin => URL.canParse(sourceOrigin), 512 lazy.pprint`Expected partition "sourceOrigin" to be a valid URL, got ${sourceOrigin}` 513 )(sourceOrigin); 514 515 const url = new URL(sourceOrigin); 516 lazy.assert.that( 517 url => url.pathname === "/" && url.hash === "" && url.search === "", 518 lazy.pprint`Expected partition "sourceOrigin" to contain only origin, got ${sourceOrigin}` 519 )(url); 520 } 521 if (userContext !== null) { 522 lazy.assert.string( 523 userContext, 524 lazy.pprint`Expected partition "userContext" to be a string, got ${userContext}` 525 ); 526 527 if (!lazy.UserContextManager.hasUserContextId(userContext)) { 528 throw new lazy.error.NoSuchUserContextError( 529 `User Context with id ${userContext} was not found` 530 ); 531 } 532 } 533 break; 534 } 535 536 default: { 537 throw new lazy.error.InvalidArgumentError( 538 `Expected "partition.type" to be one of ${Object.values( 539 PartitionType 540 )}, got ${type}` 541 ); 542 } 543 } 544 } 545 546 #assertSameSite(sameSite, fieldName = "sameSite") { 547 if (sameSite !== null) { 548 const sameSiteTypeValue = Object.values(SameSiteType); 549 lazy.assert.in( 550 sameSite, 551 sameSiteTypeValue, 552 `Expected "${fieldName}" to be one of ${sameSiteTypeValue}, ` + 553 lazy.pprint`got ${sameSite}` 554 ); 555 } 556 } 557 558 #assertValue(value, fieldName = "value") { 559 lazy.assert.object( 560 value, 561 `Expected "${fieldName}" to be an object, ` + lazy.pprint`got ${value}` 562 ); 563 564 const { type, value: protocolBytesValue } = value; 565 566 const bytesValueTypeValue = Object.values(lazy.BytesValueType); 567 lazy.assert.in( 568 type, 569 bytesValueTypeValue, 570 `Expected ${fieldName} "type" to be one of ${bytesValueTypeValue}, ` + 571 lazy.pprint`got ${type}` 572 ); 573 574 lazy.assert.string( 575 protocolBytesValue, 576 `Expected ${fieldName} "value" to be string, ` + 577 lazy.pprint`got ${protocolBytesValue}` 578 ); 579 } 580 581 /** 582 * Deserialize filter. 583 * 584 * @see https://w3c.github.io/webdriver-bidi/#deserialize-filter 585 */ 586 #deserializeFilter(filter) { 587 const deserializedFilter = {}; 588 for (const [fieldName, value] of Object.entries(filter)) { 589 if (value === null) { 590 continue; 591 } 592 593 const deserializedName = CookieFieldsMapping[fieldName]; 594 let deserializedValue; 595 596 switch (deserializedName) { 597 case "sameSite": 598 deserializedValue = this.#getSameSitePlatformProperty(value); 599 break; 600 601 case "value": 602 deserializedValue = lazy.deserializeBytesValue(value); 603 break; 604 605 case "expiry": 606 deserializedValue = value * 1000; 607 break; 608 609 default: 610 deserializedValue = value; 611 } 612 613 deserializedFilter[deserializedName] = deserializedValue; 614 } 615 616 return deserializedFilter; 617 } 618 619 /** 620 * Build a partition key. 621 * 622 * @see https://w3c.github.io/webdriver-bidi/#expand-a-storage-partition-spec 623 */ 624 #expandStoragePartitionSpec(partitionSpec) { 625 if (partitionSpec === null) { 626 partitionSpec = {}; 627 } 628 629 if (partitionSpec.type === PartitionType.Context) { 630 const { context: contextId } = partitionSpec; 631 const browsingContext = this._getNavigable(contextId); 632 const principal = Services.scriptSecurityManager.createContentPrincipal( 633 browsingContext.currentURI, 634 {} 635 ); 636 637 // Define browsing context’s associated storage partition as combination of user context id 638 // and the origin of the document in this browsing context. We also add here `isThirdPartyURI` 639 // which is required to filter out third-party cookies in case they are not allowed. 640 return { 641 // In case we have the browsing context of an iframe here, we perform a check 642 // if the URI of the top context is considered third-party to the URI of the iframe principal. 643 // It's considered a third-party if base domains or hosts (in case one or both base domains 644 // can not be determined) do not match. 645 isThirdPartyURI: browsingContext.parent 646 ? principal.isThirdPartyURI(browsingContext.top.currentURI) 647 : false, 648 sourceOrigin: browsingContext.currentURI.prePath, 649 userContext: browsingContext.originAttributes.userContextId, 650 }; 651 } 652 653 const partitionKey = {}; 654 for (const keyName of PartitionKeyAttributes) { 655 if (keyName in partitionSpec) { 656 // Retrieve a platform user context id. 657 if (keyName === "userContext") { 658 partitionKey[keyName] = lazy.UserContextManager.getInternalIdById( 659 partitionSpec.userContext 660 ); 661 } else { 662 partitionKey[keyName] = partitionSpec[keyName]; 663 } 664 } 665 } 666 667 return partitionKey; 668 } 669 670 /** 671 * Prepare the partition key in the right format for returning to a client. 672 */ 673 #formatPartitionKey(partitionKey, originAttributes) { 674 if ("userContext" in partitionKey) { 675 // Exchange platform id for Webdriver BiDi id for the user context to return it to the client. 676 partitionKey.userContext = lazy.UserContextManager.getIdByInternalId( 677 partitionKey.userContext 678 ); 679 } 680 681 // If sourceOrigin matches the cookie domain we don't set the partitionKey 682 // in the setCookie command. In that case we should also remove sourceOrigin 683 // from the returned partitionKey. 684 if ( 685 originAttributes && 686 "sourceOrigin" in partitionKey && 687 originAttributes.partitionKey === "" 688 ) { 689 delete partitionKey.sourceOrigin; 690 } 691 692 // This key is not used for partitioning and was required to only filter out third-party cookies. 693 delete partitionKey.isThirdPartyURI; 694 695 return partitionKey; 696 } 697 698 /** 699 * Since cookies retrieved from the platform API 700 * always contain expiry even for session cookies, 701 * we should check ourselves if it's a session cookie 702 * and do not return expiry in case it is. 703 */ 704 #getCookieExpiry(cookie) { 705 const { expiry, isSession } = cookie; 706 return isSession ? null : expiry; 707 } 708 709 #getCookieSize(cookie) { 710 const { name, value } = cookie; 711 return name.length + value.length; 712 } 713 714 /** 715 * Filter and serialize given cookies with provided filter. 716 * 717 * @see https://w3c.github.io/webdriver-bidi/#get-matching-cookies 718 */ 719 #getMatchingCookies(cookieStore, filter) { 720 const cookies = []; 721 const deserializedFilter = this.#deserializeFilter(filter); 722 723 for (const storedCookie of cookieStore) { 724 if (this.#matchCookie(storedCookie, deserializedFilter)) { 725 cookies.push(storedCookie); 726 } 727 } 728 return cookies; 729 } 730 731 /** 732 * Prepare the data in the required for platform API format. 733 */ 734 #getOriginAttributes(partitionKey, domain) { 735 const originAttributes = {}; 736 737 if (partitionKey.sourceOrigin) { 738 if ( 739 "isThirdPartyURI" in partitionKey && 740 domain && 741 !this.#shouldIncludePartitionedCookies() && 742 partitionKey.sourceOrigin !== "about:" 743 ) { 744 // This is a workaround until CHIPS support is enabled (see Bug 1898253). 745 // It handles the "context" type partitioning of the `setCookie` command 746 // (when domain is provided) and if partitioned cookies are disabled, 747 // but ignore `about` pаges. 748 const principal = 749 Services.scriptSecurityManager.createContentPrincipalFromOrigin( 750 partitionKey.sourceOrigin 751 ); 752 753 // Do not set partition key if the cookie domain matches the `sourceOrigin`. 754 if (principal.host.endsWith(domain)) { 755 originAttributes.partitionKey = ""; 756 } else { 757 originAttributes.partitionKey = ChromeUtils.getPartitionKeyFromURL( 758 partitionKey.sourceOrigin, 759 "", 760 false 761 ); 762 } 763 } else { 764 originAttributes.partitionKey = ChromeUtils.getPartitionKeyFromURL( 765 partitionKey.sourceOrigin, 766 "", 767 false 768 ); 769 } 770 } 771 if ("userContext" in partitionKey) { 772 originAttributes.userContextId = partitionKey.userContext; 773 } 774 775 return originAttributes; 776 } 777 778 #getSameSitePlatformProperty(sameSite) { 779 switch (sameSite) { 780 case "lax": { 781 return Ci.nsICookie.SAMESITE_LAX; 782 } 783 case "strict": { 784 return Ci.nsICookie.SAMESITE_STRICT; 785 } 786 case "none": { 787 return Ci.nsICookie.SAMESITE_NONE; 788 } 789 } 790 791 return Ci.nsICookie.SAMESITE_UNSET; 792 } 793 794 /** 795 * Return a cookie store of the storage partition for a given storage partition key. 796 * 797 * The implementation differs here from the spec, since in gecko there is no 798 * direct way to get all the cookies for a given partition key. 799 * 800 * @see https://w3c.github.io/webdriver-bidi/#get-the-cookie-store 801 */ 802 #getTheCookieStore(storagePartitionKey) { 803 let store = []; 804 805 // Prepare the data in the format required for the platform API. 806 const originAttributes = this.#getOriginAttributes(storagePartitionKey); 807 808 // Retrieve the cookies which exactly match a built partition attributes. 809 const cookiesWithOriginAttributes = 810 Services.cookies.getCookiesWithOriginAttributes( 811 JSON.stringify(originAttributes) 812 ); 813 814 const isFirstPartyOrCrossSiteAllowed = 815 !storagePartitionKey.isThirdPartyURI || 816 this.#shouldIncludeCrossSiteCookie(); 817 818 // Check if we accessing the first party storage or cross-site cookies are allowed. 819 if (isFirstPartyOrCrossSiteAllowed) { 820 // In case we want to get the cookies for a certain `sourceOrigin`, 821 // we have to separately retrieve cookies for a hostname built from `sourceOrigin`, 822 // and with `partitionKey` equal an empty string to retrieve the cookies that which were set 823 // by this hostname but without `partitionKey`, e.g. with `document.cookie`. 824 if (storagePartitionKey.sourceOrigin) { 825 const url = new URL(storagePartitionKey.sourceOrigin); 826 const hostname = url.hostname; 827 828 const principal = Services.scriptSecurityManager.createContentPrincipal( 829 url.URI, 830 {} 831 ); 832 const isSecureProtocol = principal.isOriginPotentiallyTrustworthy; 833 834 // We want to keep `userContext` id here, if it's present, 835 // but set the `partitionKey` to an empty string. 836 const cookiesMatchingHostname = 837 Services.cookies.getCookiesWithOriginAttributes( 838 JSON.stringify({ ...originAttributes, partitionKey: "" }), 839 hostname 840 ); 841 for (const cookie of cookiesMatchingHostname) { 842 // Ignore secure cookies for non-secure protocols. 843 if (cookie.isSecure && !isSecureProtocol) { 844 continue; 845 } 846 store.push(cookie); 847 } 848 } 849 850 store = store.concat(cookiesWithOriginAttributes); 851 } 852 // If we're trying to access the store in the third party context and 853 // the preferences imply that we shouldn't include cross site cookies, 854 // but we should include partitioned cookies, add only partitioned cookies. 855 else if (this.#shouldIncludePartitionedCookies()) { 856 for (const cookie of cookiesWithOriginAttributes) { 857 if (cookie.isPartitioned) { 858 store.push(cookie); 859 } 860 } 861 } 862 863 return store; 864 } 865 866 /** 867 * Match a provided cookie with provided filter. 868 * 869 * @see https://w3c.github.io/webdriver-bidi/#match-cookie 870 */ 871 #matchCookie(storedCookie, filter) { 872 for (const [fieldName, value] of Object.entries(filter)) { 873 // Since we set `null` to not specified values, we have to check for `null` here 874 // and not match on these values. 875 if (value === null) { 876 continue; 877 } 878 879 let storedCookieValue = storedCookie[fieldName]; 880 881 // The platform representation of cookie doesn't contain a size field, 882 // so we have to calculate it to match. 883 if (fieldName === "size") { 884 storedCookieValue = this.#getCookieSize(storedCookie); 885 } 886 887 if (storedCookieValue !== value) { 888 return false; 889 } 890 } 891 892 return true; 893 } 894 895 /** 896 * Serialize a cookie. 897 * 898 * @see https://w3c.github.io/webdriver-bidi/#serialize-cookie 899 */ 900 #serializeCookie(storedCookie) { 901 const cookie = {}; 902 for (const [serializedName, cookieName] of Object.entries( 903 CookieFieldsMapping 904 )) { 905 switch (serializedName) { 906 case "expiry": { 907 const expiry = this.#getCookieExpiry(storedCookie); 908 if (expiry !== null) { 909 cookie.expiry = Math.round(expiry / 1000); 910 } 911 break; 912 } 913 914 case "sameSite": 915 cookie.sameSite = SameSiteType[storedCookie.sameSite]; 916 break; 917 918 case "size": 919 cookie.size = this.#getCookieSize(storedCookie); 920 break; 921 922 case "value": 923 // Bug 1879309. Add support for non-UTF8 cookies, 924 // when a byte representation of value is available. 925 // For now, use a value field, which is returned as a string. 926 cookie.value = { 927 type: lazy.BytesValueType.String, 928 value: storedCookie.value, 929 }; 930 break; 931 932 default: 933 cookie[serializedName] = storedCookie[cookieName]; 934 } 935 } 936 937 return cookie; 938 } 939 940 #shouldIncludeCrossSiteCookie() { 941 const cookieBehavior = Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR); 942 943 if ( 944 cookieBehavior === Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN || 945 cookieBehavior === 946 Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN 947 ) { 948 return false; 949 } 950 951 return true; 952 } 953 954 #shouldIncludePartitionedCookies() { 955 const cookieBehavior = Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR); 956 957 return ( 958 cookieBehavior === 959 Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && 960 lazy.cookieCHIPSEnabled 961 ); 962 } 963 } 964 965 export const storage = StorageModule;