cookie.sys.mjs (10167B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", 9 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 10 pprint: "chrome://remote/content/shared/Format.sys.mjs", 11 }); 12 13 const IPV4_PORT_EXPR = /:\d+$/; 14 15 const SAMESITE_MAP = new Map([ 16 [Ci.nsICookie.SAMESITE_NONE, "None"], 17 [Ci.nsICookie.SAMESITE_LAX, "Lax"], 18 [Ci.nsICookie.SAMESITE_STRICT, "Strict"], 19 [Ci.nsICookie.SAMESITE_UNSET, "None"], 20 ]); 21 22 /** @namespace */ 23 export const cookie = { 24 manager: Services.cookies, 25 }; 26 27 /** 28 * @name Cookie 29 * 30 * @returns {Record<string, (number|boolean|string)>} 31 */ 32 33 /** 34 * Unmarshal a JSON Object to a cookie representation. 35 * 36 * Effectively this will run validation checks on ``json``, which 37 * will produce the errors expected by WebDriver if the input is 38 * not valid. 39 * 40 * @param {Record<string, (number | boolean | string)>} json 41 * Cookie to be deserialised. ``name`` and ``value`` are required 42 * fields which must be strings. The ``path`` and ``domain`` fields 43 * are optional, but must be a string if provided. The ``secure``, 44 * and ``httpOnly`` are similarly optional, but must be booleans. 45 * Likewise, the ``expiry`` field is optional but must be 46 * unsigned integer. 47 * 48 * @returns {Cookie} 49 * Valid cookie object. 50 * 51 * @throws {InvalidArgumentError} 52 * If any of the properties are invalid. 53 */ 54 cookie.fromJSON = function (json) { 55 let newCookie = {}; 56 57 lazy.assert.object( 58 json, 59 lazy.pprint`Expected "cookie" to be an object, got ${json}` 60 ); 61 62 newCookie.name = lazy.assert.string( 63 json.name, 64 lazy.pprint`Expected cookie "name" to be a string, got ${json.name}` 65 ); 66 newCookie.value = lazy.assert.string( 67 json.value, 68 lazy.pprint`Expected cookie "value" to be a string, got ${json.value}` 69 ); 70 71 if (typeof json.path != "undefined") { 72 newCookie.path = lazy.assert.string( 73 json.path, 74 lazy.pprint`Expected cookie "path" to be a string, got ${json.path}` 75 ); 76 } 77 if (typeof json.domain != "undefined") { 78 newCookie.domain = lazy.assert.string( 79 json.domain, 80 lazy.pprint`Expected cookie "domain" to be a string, got ${json.domain}` 81 ); 82 } 83 if (typeof json.secure != "undefined") { 84 newCookie.secure = lazy.assert.boolean( 85 json.secure, 86 lazy.pprint`Expected cookie "secure" to be a boolean, got ${json.secure}` 87 ); 88 } 89 if (typeof json.httpOnly != "undefined") { 90 newCookie.httpOnly = lazy.assert.boolean( 91 json.httpOnly, 92 lazy.pprint`Expected cookie "httpOnly" to be a boolean, got ${json.httpOnly}` 93 ); 94 } 95 if (typeof json.expiry != "undefined") { 96 newCookie.expiry = lazy.assert.positiveInteger( 97 json.expiry, 98 lazy.pprint`Expected cookie "expiry" to be a positive integer, got ${json.expiry}` 99 ); 100 } 101 if (typeof json.sameSite != "undefined") { 102 const validOptions = Array.from(SAMESITE_MAP.values()); 103 newCookie.sameSite = lazy.assert.in( 104 json.sameSite, 105 validOptions, 106 `Expected cookie "sameSite" to be one of ${validOptions.toString()}, ` + 107 lazy.pprint`got ${json.sameSite}` 108 ); 109 } 110 111 return newCookie; 112 }; 113 114 /** 115 * Insert cookie to the cookie store. 116 * 117 * @param {Cookie} newCookie 118 * Cookie to add. 119 * @param {object} options 120 * @param {string=} options.restrictToHost 121 * Perform test that ``newCookie``'s domain matches this. 122 * @param {string=} options.protocol 123 * The protocol of the caller. It can be `http:` or `https:`. 124 * 125 * @throws {TypeError} 126 * If ``name``, ``value``, or ``domain`` are not present and 127 * of the correct type. 128 * @throws {InvalidCookieDomainError} 129 * If ``restrictToHost`` is set and ``newCookie``'s domain does 130 * not match. 131 * @throws {UnableToSetCookieError} 132 * If an error occurred while trying to save the cookie. 133 */ 134 cookie.add = function ( 135 newCookie, 136 { restrictToHost = null, protocol = null } = {} 137 ) { 138 lazy.assert.string( 139 newCookie.name, 140 lazy.pprint`Expected cookie "name" to be a string, got ${newCookie.name}` 141 ); 142 lazy.assert.string( 143 newCookie.value, 144 lazy.pprint`Expected cookie "value" to be a string, got ${newCookie.value}` 145 ); 146 147 if (typeof newCookie.path == "undefined") { 148 newCookie.path = "/"; 149 } 150 151 let hostOnly = false; 152 if (typeof newCookie.domain == "undefined") { 153 hostOnly = true; 154 newCookie.domain = restrictToHost; 155 } 156 lazy.assert.string( 157 newCookie.domain, 158 lazy.pprint`Expected cookie "domain" to be a string, got ${newCookie.domain}` 159 ); 160 if (newCookie.domain.substring(0, 1) === ".") { 161 newCookie.domain = newCookie.domain.substring(1); 162 } 163 164 if (typeof newCookie.secure == "undefined") { 165 newCookie.secure = false; 166 } 167 if (typeof newCookie.httpOnly == "undefined") { 168 newCookie.httpOnly = false; 169 } 170 171 if (typeof newCookie.expiry == "undefined") { 172 // The XPCOM interface requires the expiry field even for session cookies. 173 newCookie.expiry = Number.MAX_SAFE_INTEGER; 174 newCookie.session = true; 175 } else { 176 newCookie.session = false; 177 // Gecko expects the expiry value to be in milliseconds, WebDriver uses seconds. 178 // The maximum allowed value is capped at 400 days. 179 newCookie.expiry = Services.cookies.maybeCapExpiry(newCookie.expiry * 1000); 180 } 181 182 let sameSite = [...SAMESITE_MAP].find( 183 ([, value]) => newCookie.sameSite === value 184 ); 185 newCookie.sameSite = sameSite ? sameSite[0] : Ci.nsICookie.SAMESITE_UNSET; 186 187 let isIpAddress = false; 188 try { 189 Services.eTLD.getPublicSuffixFromHost(newCookie.domain); 190 } catch (e) { 191 switch (e.result) { 192 case Cr.NS_ERROR_HOST_IS_IP_ADDRESS: 193 isIpAddress = true; 194 break; 195 default: 196 throw new lazy.error.InvalidCookieDomainError(newCookie.domain); 197 } 198 } 199 200 if (!hostOnly && !isIpAddress) { 201 // only store this as a domain cookie if the domain was specified in the 202 // request and it wasn't an IP address. 203 newCookie.domain = "." + newCookie.domain; 204 } 205 206 if (restrictToHost) { 207 if ( 208 !restrictToHost.endsWith(newCookie.domain) && 209 "." + restrictToHost !== newCookie.domain && 210 restrictToHost !== newCookie.domain 211 ) { 212 throw new lazy.error.InvalidCookieDomainError( 213 `Cookies may only be set ` + 214 `for the current domain (${restrictToHost})` 215 ); 216 } 217 } 218 219 let schemeType = Ci.nsICookie.SCHEME_UNSET; 220 switch (protocol) { 221 case "http:": 222 schemeType = Ci.nsICookie.SCHEME_HTTP; 223 break; 224 case "https:": 225 schemeType = Ci.nsICookie.SCHEME_HTTPS; 226 break; 227 default: 228 // Any other protocol that is supported by the cookie service. 229 break; 230 } 231 232 // remove port from domain, if present. 233 // unfortunately this catches IPv6 addresses by mistake 234 // TODO: Bug 814416 235 newCookie.domain = newCookie.domain.replace(IPV4_PORT_EXPR, ""); 236 237 let cv; 238 try { 239 cv = cookie.manager.add( 240 newCookie.domain, 241 newCookie.path, 242 newCookie.name, 243 newCookie.value, 244 newCookie.secure, 245 newCookie.httpOnly, 246 newCookie.session, 247 newCookie.expiry, 248 {} /* origin attributes */, 249 newCookie.sameSite, 250 schemeType 251 ); 252 } catch (e) { 253 throw new lazy.error.UnableToSetCookieError(e); 254 } 255 256 if (cv.result !== Ci.nsICookieValidation.eOK) { 257 throw new lazy.error.UnableToSetCookieError( 258 `Invalid cookie: ${cv.errorString}` 259 ); 260 } 261 }; 262 263 /** 264 * Remove cookie from the cookie store. 265 * 266 * @param {Cookie} toDelete 267 * Cookie to remove. 268 */ 269 cookie.remove = function (toDelete) { 270 cookie.manager.remove( 271 toDelete.domain, 272 toDelete.name, 273 toDelete.path, 274 {} /* originAttributes */ 275 ); 276 }; 277 278 /** 279 * Iterates over the cookies for the current ``host``. You may 280 * optionally filter for specific paths on that ``host`` by specifying 281 * a path in ``currentPath``. 282 * 283 * @param {string} host 284 * Hostname to retrieve cookies for. 285 * @param {BrowsingContext=} [browsingContext=undefined] browsingContext 286 * The BrowsingContext that is reading these cookies. 287 * Used to get the correct partitioned cookies. 288 * @param {string=} [currentPath="/"] currentPath 289 * Optionally filter the cookies for ``host`` for the specific path. 290 * Defaults to ``/``, meaning all cookies for ``host`` are included. 291 * 292 * @returns {Iterable.<Cookie>} 293 * Iterator. 294 */ 295 cookie.iter = function* (host, browsingContext = undefined, currentPath = "/") { 296 lazy.assert.string( 297 host, 298 lazy.pprint`Expected "host" to be a string, got ${host}` 299 ); 300 lazy.assert.string( 301 currentPath, 302 lazy.pprint`Expected "currentPath" to be a string, got ${currentPath}` 303 ); 304 305 const isForCurrentPath = path => currentPath.includes(path); 306 307 let cookies = cookie.manager.getCookiesFromHost(host, {}); 308 if (browsingContext) { 309 let partitionedOriginAttributes = { 310 partitionKey: 311 browsingContext.currentWindowGlobal?.cookieJarSettings?.partitionKey, 312 }; 313 let cookiesPartitioned = cookie.manager.getCookiesFromHost( 314 host, 315 partitionedOriginAttributes 316 ); 317 cookies.push(...cookiesPartitioned); 318 } 319 for (let cookie of cookies) { 320 // take the hostname and progressively shorten 321 let hostname = host; 322 do { 323 if ( 324 (cookie.host == "." + hostname || cookie.host == hostname) && 325 isForCurrentPath(cookie.path) 326 ) { 327 let data = { 328 name: cookie.name, 329 value: cookie.value, 330 path: cookie.path, 331 domain: cookie.host, 332 secure: cookie.isSecure, 333 httpOnly: cookie.isHttpOnly, 334 }; 335 336 if (!cookie.isSession) { 337 // Internally expiry is in ms, WebDriver expects seconds. 338 data.expiry = Math.round(cookie.expiry / 1000); 339 } 340 341 data.sameSite = SAMESITE_MAP.get(cookie.sameSite) || "None"; 342 343 yield data; 344 } 345 hostname = hostname.replace(/^.*?\./, ""); 346 } while (hostname.includes(".")); 347 } 348 };