diff options
Diffstat (limited to 'browser/components/sessionstore/SessionCookies.jsm')
-rw-r--r-- | browser/components/sessionstore/SessionCookies.jsm | 476 |
1 files changed, 476 insertions, 0 deletions
diff --git a/browser/components/sessionstore/SessionCookies.jsm b/browser/components/sessionstore/SessionCookies.jsm new file mode 100644 index 000000000..b99ab927b --- /dev/null +++ b/browser/components/sessionstore/SessionCookies.jsm @@ -0,0 +1,476 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["SessionCookies"]; + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); + +XPCOMUtils.defineLazyModuleGetter(this, "Utils", + "resource://gre/modules/sessionstore/Utils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", + "resource:///modules/sessionstore/PrivacyLevel.jsm"); + +// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision. +const MAX_EXPIRY = Math.pow(2, 62); + +/** + * The external API implemented by the SessionCookies module. + */ +this.SessionCookies = Object.freeze({ + update: function (windows) { + SessionCookiesInternal.update(windows); + }, + + getHostsForWindow: function (window, checkPrivacy = false) { + return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy); + }, + + restore(cookies) { + SessionCookiesInternal.restore(cookies); + } +}); + +/** + * The internal API. + */ +var SessionCookiesInternal = { + /** + * Stores whether we're initialized, yet. + */ + _initialized: false, + + /** + * Retrieve the list of all hosts contained in the given windows' session + * history entries (per window) and collect the associated cookies for those + * hosts, if any. The given state object is being modified. + * + * @param windows + * Array of window state objects. + * [{ tabs: [...], cookies: [...] }, ...] + */ + update: function (windows) { + this._ensureInitialized(); + + for (let window of windows) { + let cookies = []; + + // Collect all hosts for the current window. + let hosts = this.getHostsForWindow(window, true); + + for (let host of Object.keys(hosts)) { + let isPinned = hosts[host]; + + for (let cookie of CookieStore.getCookiesForHost(host)) { + // _getCookiesForHost() will only return hosts with the right privacy + // rules, so there is no need to do anything special with this call + // to PrivacyLevel.canSave(). + if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) { + cookies.push(cookie); + } + } + } + + // Don't include/keep empty cookie sections. + if (cookies.length) { + window.cookies = cookies; + } else if ("cookies" in window) { + delete window.cookies; + } + } + }, + + /** + * Returns a map of all hosts for a given window that we might want to + * collect cookies for. + * + * @param window + * A window state object containing tabs with history entries. + * @param checkPrivacy (bool) + * Whether to check the privacy level for each host. + * @return {object} A map of hosts for a given window state object. The keys + * will be hosts, the values are boolean and determine + * whether we will use the deferred privacy level when + * checking how much data to save on quitting. + */ + getHostsForWindow: function (window, checkPrivacy = false) { + let hosts = {}; + + for (let tab of window.tabs) { + for (let entry of tab.entries) { + this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned); + } + } + + return hosts; + }, + + /** + * Restores a given list of session cookies. + */ + restore(cookies) { + + for (let cookie of cookies) { + let expiry = "expiry" in cookie ? cookie.expiry : MAX_EXPIRY; + let cookieObj = { + host: cookie.host, + path: cookie.path || "", + name: cookie.name || "" + }; + if (!Services.cookies.cookieExists(cookieObj, cookie.originAttributes || {})) { + Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "", + cookie.value, !!cookie.secure, !!cookie.httponly, + /* isSession = */ true, expiry, cookie.originAttributes || {}); + } + } + }, + + /** + * Handles observers notifications that are sent whenever cookies are added, + * changed, or removed. Ensures that the storage is updated accordingly. + */ + observe: function (subject, topic, data) { + switch (data) { + case "added": + case "changed": + this._updateCookie(subject); + break; + case "deleted": + this._removeCookie(subject); + break; + case "cleared": + CookieStore.clear(); + break; + case "batch-deleted": + this._removeCookies(subject); + break; + case "reload": + CookieStore.clear(); + this._reloadCookies(); + break; + default: + throw new Error("Unhandled cookie-changed notification."); + } + }, + + /** + * If called for the first time in a session, iterates all cookies in the + * cookies service and puts them into the store if they're session cookies. + */ + _ensureInitialized: function () { + if (!this._initialized) { + this._reloadCookies(); + this._initialized = true; + Services.obs.addObserver(this, "cookie-changed", false); + } + }, + + /** + * Fill a given map with hosts found in the given entry's session history and + * any child entries. + * + * @param entry + * the history entry, serialized + * @param hosts + * the hash that will be used to store hosts eg, { hostname: true } + * @param checkPrivacy + * should we check the privacy level for https + * @param isPinned + * is the entry we're evaluating for a pinned tab; used only if + * checkPrivacy + */ + _extractHostsFromEntry: function (entry, hosts, checkPrivacy, isPinned) { + let host = entry._host; + let scheme = entry._scheme; + + // If host & scheme aren't defined, then we are likely here in the startup + // process via _splitCookiesFromWindow. In that case, we'll turn entry.url + // into an nsIURI and get host/scheme from that. This will throw for about: + // urls in which case we don't need to do anything. + if (!host && !scheme) { + try { + let uri = Utils.makeURI(entry.url); + host = uri.host; + scheme = uri.scheme; + this._extractHostsFromHostScheme(host, scheme, hosts, checkPrivacy, isPinned); + } + catch (ex) { } + } + + if (entry.children) { + for (let child of entry.children) { + this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned); + } + } + }, + + /** + * Add a given host to a given map of hosts if the privacy level allows + * saving cookie data for it. + * + * @param host + * the host of a uri (usually via nsIURI.host) + * @param scheme + * the scheme of a uri (usually via nsIURI.scheme) + * @param hosts + * the hash that will be used to store hosts eg, { hostname: true } + * @param checkPrivacy + * should we check the privacy level for https + * @param isPinned + * is the entry we're evaluating for a pinned tab; used only if + * checkPrivacy + */ + _extractHostsFromHostScheme: + function (host, scheme, hosts, checkPrivacy, isPinned) { + // host and scheme may not be set (for about: urls for example), in which + // case testing scheme will be sufficient. + if (/https?/.test(scheme) && !hosts[host] && + (!checkPrivacy || + PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) { + // By setting this to true or false, we can determine when looking at + // the host in update() if we should check for privacy. + hosts[host] = isPinned; + } else if (scheme == "file") { + hosts[host] = true; + } + }, + + /** + * Updates or adds a given cookie to the store. + */ + _updateCookie: function (cookie) { + cookie.QueryInterface(Ci.nsICookie2); + + if (cookie.isSession) { + CookieStore.set(cookie); + } else { + CookieStore.delete(cookie); + } + }, + + /** + * Removes a given cookie from the store. + */ + _removeCookie: function (cookie) { + cookie.QueryInterface(Ci.nsICookie2); + + if (cookie.isSession) { + CookieStore.delete(cookie); + } + }, + + /** + * Removes a given list of cookies from the store. + */ + _removeCookies: function (cookies) { + for (let i = 0; i < cookies.length; i++) { + this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2)); + } + }, + + /** + * Iterates all cookies in the cookies service and puts them into the store + * if they're session cookies. + */ + _reloadCookies: function () { + let iter = Services.cookies.enumerator; + while (iter.hasMoreElements()) { + this._updateCookie(iter.getNext()); + } + } +}; + +/** + * Generates all possible subdomains for a given host and prepends a leading + * dot to all variants. + * + * See http://tools.ietf.org/html/rfc6265#section-5.1.3 + * http://en.wikipedia.org/wiki/HTTP_cookie#Domain_and_Path + * + * All cookies belonging to a web page will be internally represented by a + * nsICookie object. nsICookie.host will be the request host if no domain + * parameter was given when setting the cookie. If a specific domain was given + * then nsICookie.host will contain that specific domain and prepend a leading + * dot to it. + * + * We thus generate all possible subdomains for a given domain and prepend a + * leading dot to them as that is the value that was used as the map key when + * the cookie was set. + */ +function* getPossibleSubdomainVariants(host) { + // Try given domain with a leading dot (.www.example.com). + yield "." + host; + + // Stop if there are only two parts left (e.g. example.com was given). + let parts = host.split("."); + if (parts.length < 3) { + return; + } + + // Remove the first subdomain (www.example.com -> example.com). + let rest = parts.slice(1).join("."); + + // Try possible parent subdomains. + yield* getPossibleSubdomainVariants(rest); +} + +/** + * The internal cookie storage that keeps track of every active session cookie. + * These are stored using maps per host, path, and cookie name. + */ +var CookieStore = { + /** + * The internal structure holding all known cookies. + * + * Host => + * Path => + * Name => {path: "/", name: "sessionid", secure: true} + * + * Maps are used for storage but the data structure is equivalent to this: + * + * this._hosts = { + * "www.mozilla.org": { + * "/": { + * "username": {name: "username", value: "my_name_is", etc...}, + * "sessionid": {name: "sessionid", value: "1fdb3a", etc...} + * } + * }, + * "tbpl.mozilla.org": { + * "/path": { + * "cookiename": {name: "cookiename", value: "value", etc...} + * } + * }, + * ".example.com": { + * "/path": { + * "cookiename": {name: "cookiename", value: "value", etc...} + * } + * } + * }; + */ + _hosts: new Map(), + + /** + * Returns the list of stored session cookies for a given host. + * + * @param host + * A string containing the host name we want to get cookies for. + */ + getCookiesForHost: function (host) { + let cookies = []; + + let appendCookiesForHost = host => { + if (!this._hosts.has(host)) { + return; + } + + for (let pathToNamesMap of this._hosts.get(host).values()) { + for (let nameToCookiesMap of pathToNamesMap.values()) { + cookies.push(...nameToCookiesMap.values()); + } + } + } + + // Try to find cookies for the given host, e.g. <www.example.com>. + // The full hostname will be in the map if the Set-Cookie header did not + // have a domain= attribute, i.e. the cookie will only be stored for the + // request domain. Also, try to find cookies for subdomains, e.g. + // <.example.com>. We will find those variants with a leading dot in the + // map if the Set-Cookie header had a domain= attribute, i.e. the cookie + // will be stored for a parent domain and we send it for any subdomain. + for (let variant of [host, ...getPossibleSubdomainVariants(host)]) { + appendCookiesForHost(variant); + } + + return cookies; + }, + + /** + * Stores a given cookie. + * + * @param cookie + * The nsICookie2 object to add to the storage. + */ + set: function (cookie) { + let jscookie = {host: cookie.host, value: cookie.value}; + + // Only add properties with non-default values to save a few bytes. + if (cookie.path) { + jscookie.path = cookie.path; + } + + if (cookie.name) { + jscookie.name = cookie.name; + } + + if (cookie.isSecure) { + jscookie.secure = true; + } + + if (cookie.isHttpOnly) { + jscookie.httponly = true; + } + + if (cookie.expiry < MAX_EXPIRY) { + jscookie.expiry = cookie.expiry; + } + + if (cookie.originAttributes) { + jscookie.originAttributes = cookie.originAttributes; + } + + this._ensureMap(cookie).set(cookie.name, jscookie); + }, + + /** + * Removes a given cookie. + * + * @param cookie + * The nsICookie2 object to be removed from storage. + */ + delete: function (cookie) { + this._ensureMap(cookie).delete(cookie.name); + }, + + /** + * Removes all cookies. + */ + clear: function () { + this._hosts.clear(); + }, + + /** + * Creates all maps necessary to store a given cookie. + * + * @param cookie + * The nsICookie2 object to create maps for. + * + * @return The newly created Map instance mapping cookie names to + * internal jscookies, in the given path of the given host. + */ + _ensureMap: function (cookie) { + if (!this._hosts.has(cookie.host)) { + this._hosts.set(cookie.host, new Map()); + } + + let originAttributesMap = this._hosts.get(cookie.host); + // If cookie.originAttributes is null, originAttributes will be an empty string. + let originAttributes = ChromeUtils.originAttributesToSuffix(cookie.originAttributes); + if (!originAttributesMap.has(originAttributes)) { + originAttributesMap.set(originAttributes, new Map()); + } + + let pathToNamesMap = originAttributesMap.get(originAttributes); + + if (!pathToNamesMap.has(cookie.path)) { + pathToNamesMap.set(cookie.path, new Map()); + } + + return pathToNamesMap.get(cookie.path); + } +}; |