SessionCookies.sys.mjs (8177B)
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 PrivacyLevel: "resource://gre/modules/sessionstore/PrivacyLevel.sys.mjs", 9 }); 10 11 const MAX_EXPIRY = Number.MAX_SAFE_INTEGER; 12 13 /** 14 * The external API implemented by the SessionCookies module. 15 */ 16 export var SessionCookies = Object.freeze({ 17 collect() { 18 return SessionCookiesInternal.collect(); 19 }, 20 21 restore(cookies) { 22 SessionCookiesInternal.restore(cookies); 23 }, 24 }); 25 26 /** 27 * The internal API. 28 */ 29 var SessionCookiesInternal = { 30 /** 31 * Stores whether we're initialized, yet. 32 */ 33 _initialized: false, 34 35 /** 36 * Retrieve an array of all stored session cookies. 37 */ 38 collect() { 39 this._ensureInitialized(); 40 return CookieStore.toArray(); 41 }, 42 43 /** 44 * Restores a given list of session cookies. 45 */ 46 restore(cookies) { 47 for (let cookie of cookies) { 48 let expiry = "expiry" in cookie ? cookie.expiry : MAX_EXPIRY; 49 let exists = false; 50 try { 51 exists = Services.cookies.cookieExists( 52 cookie.host, 53 cookie.path || "", 54 cookie.name || "", 55 cookie.originAttributes || {} 56 ); 57 } catch (ex) { 58 console.error( 59 `CookieService::CookieExists failed with error '${ex}' for '${JSON.stringify( 60 cookie 61 )}'.` 62 ); 63 } 64 if (!exists) { 65 // Enforces isPartitioned if the partitionKey is set. We need to do this 66 // because the session store didn't store the isPartitioned flag. 67 // Otherwise, we'd end up setting partitioned cookies without 68 // isPartitioned flag. 69 let isPartitioned = 70 cookie.isPartitioned || 71 cookie.originAttributes?.partitionKey?.length > 0; 72 73 try { 74 const cv = Services.cookies.add( 75 cookie.host, 76 cookie.path || "", 77 cookie.name || "", 78 cookie.value, 79 !!cookie.secure, 80 !!cookie.httponly, 81 /* isSession = */ true, 82 expiry, 83 cookie.originAttributes || {}, 84 // If sameSite is undefined, we are migrating from a pre bug 1955685 session). 85 cookie.sameSite === undefined 86 ? Ci.nsICookie.SAMESITE_NONE 87 : cookie.sameSite, 88 cookie.schemeMap || Ci.nsICookie.SCHEME_HTTPS, 89 isPartitioned 90 ); 91 if (cv.result !== Ci.nsICookieValidation.eOK) { 92 console.error( 93 `CookieService::Add failed with error '${cv.result}' for cookie ${JSON.stringify( 94 cookie 95 )}.` 96 ); 97 } 98 } catch (ex) { 99 console.error( 100 `CookieService::Add failed with error '${ex}' for cookie ${JSON.stringify( 101 cookie 102 )}.` 103 ); 104 } 105 } 106 } 107 }, 108 109 /** 110 * Handles observers notifications that are sent whenever cookies are added, 111 * changed, or removed. Ensures that the storage is updated accordingly. 112 */ 113 observe(subject) { 114 let notification = subject.QueryInterface(Ci.nsICookieNotification); 115 116 let { 117 COOKIE_DELETED, 118 COOKIE_ADDED, 119 COOKIE_CHANGED, 120 ALL_COOKIES_CLEARED, 121 COOKIES_BATCH_DELETED, 122 } = Ci.nsICookieNotification; 123 124 switch (notification.action) { 125 case COOKIE_ADDED: 126 this._addCookie(notification.cookie); 127 break; 128 case COOKIE_CHANGED: 129 this._updateCookie(notification.cookie); 130 break; 131 case COOKIE_DELETED: 132 this._removeCookie(notification.cookie); 133 break; 134 case ALL_COOKIES_CLEARED: 135 CookieStore.clear(); 136 break; 137 case COOKIES_BATCH_DELETED: 138 this._removeCookies(notification.batchDeletedCookies); 139 break; 140 default: 141 throw new Error("Unhandled session-cookie-changed notification."); 142 } 143 }, 144 145 /** 146 * If called for the first time in a session, iterates all cookies in the 147 * cookies service and puts them into the store if they're session cookies. 148 */ 149 _ensureInitialized() { 150 if (this._initialized) { 151 return; 152 } 153 this._reloadCookies(); 154 this._initialized = true; 155 Services.obs.addObserver(this, "session-cookie-changed"); 156 157 // Listen for privacy level changes to reload cookies when needed. 158 Services.prefs.addObserver("browser.sessionstore.privacy_level", () => { 159 this._reloadCookies(); 160 }); 161 }, 162 163 /** 164 * Adds a given cookie to the store. 165 */ 166 _addCookie(cookie) { 167 cookie.QueryInterface(Ci.nsICookie); 168 169 // Store only session cookies, obey the privacy level. 170 if (cookie.isSession && lazy.PrivacyLevel.canSave(cookie.isSecure)) { 171 CookieStore.add(cookie); 172 } 173 }, 174 175 /** 176 * Updates a given cookie. 177 */ 178 _updateCookie(cookie) { 179 cookie.QueryInterface(Ci.nsICookie); 180 181 // Store only session cookies, obey the privacy level. 182 if (cookie.isSession && lazy.PrivacyLevel.canSave(cookie.isSecure)) { 183 CookieStore.add(cookie); 184 } else { 185 CookieStore.delete(cookie); 186 } 187 }, 188 189 /** 190 * Removes a given cookie from the store. 191 */ 192 _removeCookie(cookie) { 193 cookie.QueryInterface(Ci.nsICookie); 194 195 if (cookie.isSession) { 196 CookieStore.delete(cookie); 197 } 198 }, 199 200 /** 201 * Removes a given list of cookies from the store. 202 */ 203 _removeCookies(cookies) { 204 for (let i = 0; i < cookies.length; i++) { 205 this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie)); 206 } 207 }, 208 209 /** 210 * Iterates all cookies in the cookies service and puts them into the store 211 * if they're session cookies. Obeys the user's chosen privacy level. 212 */ 213 _reloadCookies() { 214 CookieStore.clear(); 215 216 // Bail out if we're not supposed to store cookies at all. 217 if (!lazy.PrivacyLevel.canSave(false)) { 218 return; 219 } 220 221 for (let cookie of Services.cookies.sessionCookies) { 222 this._addCookie(cookie); 223 } 224 }, 225 }; 226 227 /** 228 * The internal storage that keeps track of session cookies. 229 */ 230 var CookieStore = { 231 /** 232 * The internal map holding all known session cookies. 233 */ 234 _entries: new Map(), 235 236 /** 237 * Stores a given cookie. 238 * 239 * @param cookie 240 * The nsICookie object to add to the storage. 241 */ 242 add(cookie) { 243 let jscookie = { host: cookie.host, value: cookie.value }; 244 245 // Only add properties with non-default values to save a few bytes. 246 if (cookie.path) { 247 jscookie.path = cookie.path; 248 } 249 250 if (cookie.name) { 251 jscookie.name = cookie.name; 252 } 253 254 if (cookie.isSecure) { 255 jscookie.secure = true; 256 } 257 258 if (cookie.isHttpOnly) { 259 jscookie.httponly = true; 260 } 261 262 if (cookie.expiry < MAX_EXPIRY) { 263 jscookie.expiry = cookie.expiry; 264 } 265 266 if (cookie.originAttributes) { 267 jscookie.originAttributes = cookie.originAttributes; 268 } 269 270 jscookie.sameSite = cookie.sameSite; 271 272 if (cookie.schemeMap) { 273 jscookie.schemeMap = cookie.schemeMap; 274 } 275 276 if (cookie.isPartitioned) { 277 jscookie.isPartitioned = true; 278 } 279 280 this._entries.set(this._getKeyForCookie(cookie), jscookie); 281 }, 282 283 /** 284 * Removes a given cookie. 285 * 286 * @param cookie 287 * The nsICookie object to be removed from storage. 288 */ 289 delete(cookie) { 290 this._entries.delete(this._getKeyForCookie(cookie)); 291 }, 292 293 /** 294 * Removes all cookies. 295 */ 296 clear() { 297 this._entries.clear(); 298 }, 299 300 /** 301 * Return all cookies as an array. 302 */ 303 toArray() { 304 return [...this._entries.values()]; 305 }, 306 307 /** 308 * Returns the key needed to properly store and identify a given cookie. 309 * A cookie is uniquely identified by the combination of its host, name, 310 * path, and originAttributes properties. 311 * 312 * @param cookie 313 * The nsICookie object to compute a key for. 314 * @return string 315 */ 316 _getKeyForCookie(cookie) { 317 return JSON.stringify({ 318 host: cookie.host, 319 name: cookie.name, 320 path: cookie.path, 321 attr: ChromeUtils.originAttributesToSuffix(cookie.originAttributes), 322 }); 323 }, 324 };