www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 64f77108775d753baf2b38d6eb917f008bb5e3e5
parent 161733b207e7effc152447afb8c3b45350269154
Author: Simon Kornblith <simon@simonster.com>
Date:   Sat, 14 Feb 2015 13:24:58 -0500

Merge pull request #409 from aurimasv/cookies

Manage cookies received from other hosts.
Diffstat:
Mchrome/content/zotero/xpcom/connector/connector.js | 53++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mchrome/content/zotero/xpcom/connector/translate_item.js | 3+--
Mchrome/content/zotero/xpcom/cookieSandbox.js | 218+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mchrome/content/zotero/xpcom/server_connector.js | 6+++++-
4 files changed, 250 insertions(+), 30 deletions(-)

diff --git a/chrome/content/zotero/xpcom/connector/connector.js b/chrome/content/zotero/xpcom/connector/connector.js @@ -143,7 +143,7 @@ Zotero.Connector = new function() { * @param {Object} data RPC data. See documentation above. * @param {Function} callback Function to be called when requests complete. */ - this.callMethod = function(method, data, callback) { + this.callMethod = function(method, data, callback, tab) { // Don't bother trying if not online in bookmarklet if(Zotero.isBookmarklet && this.isOnline === false) { callback(false, 0); @@ -211,6 +211,57 @@ Zotero.Connector = new function() { "X-Zotero-Connector-API-Version":CONNECTOR_API_VERSION }); } + }, + + /** + * Adds detailed cookies to the data before sending "saveItems" request to + * the server/Standalone + * + * @param {Object} data RPC data. See documentation above. + * @param {Function} callback Function to be called when requests complete. + */ + this.setCookiesThenSaveItems = function(data, callback, tab) { + if(Zotero.isFx && !Zotero.isBookmarklet && data.uri) { + var host = Services.ios.newURI(data.uri, null, null).host; + var cookieEnum = Services.cookies.getCookiesFromHost(host); + var cookieHeader = ''; + while(cookieEnum.hasMoreElements()) { + var cookie = cookieEnum.getNext().QueryInterface(Components.interfaces.nsICookie2); + cookieHeader += '\n' + cookie.name + '=' + cookie.value + + ';Domain=' + cookie.host + + (cookie.path ? ';Path=' + cookie.path : '') + + (!cookie.isDomain ? ';hostOnly' : '') //not a legit flag, but we have to use it internally + + (cookie.isSecure ? ';secure' : ''); + } + + if(cookieHeader) { + data.detailedCookies = cookieHeader.substr(1); + } + + this.callMethod("saveItems", data, callback, tab); + return; + } else if(Zotero.isChrome && !Zotero.isBookmarklet) { + var self = this; + chrome.cookies.getAll({url: tab.url}, function(cookies) { + var cookieHeader = ''; + for(var i=0, n=cookies.length; i<n; i++) { + cookieHeader += '\n' + cookies[i].name + '=' + cookies[i].value + + ';Domain=' + cookies[i].domain + + (cookies[i].path ? ';Path=' + cookies[i].path : '') + + (cookies[i].hostOnly ? ';hostOnly' : '') //not a legit flag, but we have to use it internally + + (cookies[i].secure ? ';secure' : ''); + } + + if(cookieHeader) { + data.detailedCookies = cookieHeader.substr(1); + } + + self.callMethod("saveItems", data, callback, tab); + }); + return; + } + + this.callMethod("saveItems", data, callback, tab); } } diff --git a/chrome/content/zotero/xpcom/connector/translate_item.js b/chrome/content/zotero/xpcom/connector/translate_item.js @@ -80,8 +80,7 @@ Zotero.Translate.ItemSaver.prototype = { payload.uri = this._uri; payload.cookie = this._cookie; } - - Zotero.Connector.callMethod("saveItems", payload, function(data, status) { + Zotero.Connector.setCookiesThenSaveItems(payload, function(data, status) { if(data !== false) { Zotero.debug("Translate: Save via Standalone succeeded"); var haveAttachments = false; diff --git a/chrome/content/zotero/xpcom/cookieSandbox.js b/chrome/content/zotero/xpcom/cookieSandbox.js @@ -47,11 +47,9 @@ Zotero.CookieSandbox = function(browser, uri, cookieData, userAgent) { this._cookies = {}; if(cookieData) { - var splitCookies = cookieData.split(/; ?/); + var splitCookies = cookieData.split(/;\s*/); for each(var cookie in splitCookies) { - var key = cookie.substr(0, cookie.indexOf("=")); - var value = cookie.substr(cookie.indexOf("=")+1); - this._cookies[key] = value; + this.setCookie(cookie, this.URI.host); } } @@ -63,29 +61,93 @@ Zotero.CookieSandbox = function(browser, uri, cookieData, userAgent) { } } +/** + * Normalizes the host string: lower-case, remove leading period, some more cleanup + * @param {String} host; + */ +Zotero.CookieSandbox.normalizeHost = function(host) { + return host.trim().toLowerCase().replace(/^\.+|[:\/].*/g, ''); +} + +/** + * Normalizes the path string + * @param {String} path; + */ +Zotero.CookieSandbox.normalizePath = function(path) { + return '/' + path.trim().replace(/^\/+|[?#].*/g, ''); +} + +/** + * Generates a semicolon-separated string of cookie values from a list of cookies + * @param {Object} cookies Object containing key: value cookie pairs + */ +Zotero.CookieSandbox.generateCookieString = function(cookies) { + var str = ''; + for(var key in cookies) { + str += '; ' + key + '=' + cookies[key]; + } + + return str ? str.substr(2) : ''; +} + Zotero.CookieSandbox.prototype = { /** * Adds cookies to this CookieSandbox based on a cookie header * @param {String} cookieString; + * @param {nsIURI} [uri] URI of the header origin. + Used to verify same origin. If omitted validation is not performed */ - "addCookiesFromHeader":function(cookieString) { + "addCookiesFromHeader":function(cookieString, uri) { var cookies = cookieString.split("\n"); + if(uri) { + var validDomain = '.' + Zotero.CookieSandbox.normalizeHost(uri.host); + } + for(var i=0, n=cookies.length; i<n; i++) { - var cookieInfo = cookies[i].split(/; ?/); - var secure = false; + var cookieInfo = cookies[i].split(/;\s*/); + var secure = false, path = '', domain = '', hostOnly = false; for(var j=1, m=cookieInfo.length; j<m; j++) { - if(cookieInfo[j].substr(0, cookieInfo[j].indexOf("=")).toLowerCase() === "secure") { - secure = true; - break; + var pair = cookieInfo[j].split(/\s*=\s*/); + switch(pair[0].trim().toLowerCase()) { + case 'secure': + secure = true; + break; + case 'domain': + domain = pair[1]; + break; + case 'path': + path = pair[1]; + break; + case 'hostonly': + hostOnly = true; + break; } + + if(secure && domain && path && hostOnly) break; } - if(!secure) { - var key = cookieInfo[0].substr(0, cookieInfo[0].indexOf("=")); - var value = cookieInfo[0].substr(cookieInfo[0].indexOf("=")+1); - this._cookies[key] = value; + // Domain must be a suffix of the host setting the cookie + if(validDomain && domain) { + var normalizedDomain = Zotero.CookieSandbox.normalizeHost(domain); + var substrMatch = validDomain.lastIndexOf(normalizedDomain); + var publicSuffix; + try { publicSuffix = Services.eTLD.getPublicSuffix(uri) } catch(e) {} + if(substrMatch == -1 || !publicSuffix || publicSuffix == normalizedDomain + || (substrMatch + normalizedDomain.length != validDomain.length) + || (validDomain.charAt(substrMatch-1) != '.')) { + Zotero.debug("CookieSandbox: Ignoring attempt to set a cookie for different host"); + continue; + } } + + // When no domain is set, use requestor's host (hostOnly cookie) + if(validDomain && !domain) { + domain = validDomain.substr(1); + hostOnly = true; + } + + this.setCookie(cookieInfo[0], domain, path, secure, hostOnly); } }, @@ -104,13 +166,112 @@ Zotero.CookieSandbox.prototype = { "attachToInterfaceRequestor": function(ir) { Zotero.CookieSandbox.Observer.trackedInterfaceRequestors.push(Components.utils.getWeakReference(ir.QueryInterface(Components.interfaces.nsIInterfaceRequestor))); Zotero.CookieSandbox.Observer.trackedInterfaceRequestorSandboxes.push(this); + }, + + /** + * Set a cookie for a specified host + * @param {String} cookiePair A single cookie pair in the form key=value + * @param {String} [host] Host to bind the cookie to. + * Defaults to the host set on this.URI + * @param {String} [path] + * @param {Boolean} [secure] Whether the cookie has the secure attribute set + * @param {Boolean} [hostOnly] Whether the cookie is a host-only cookie + */ + "setCookie": function(cookiePair, host, path, secure, hostOnly) { + var splitAt = cookiePair.indexOf('='); + if(splitAt === -1) { + Zotero.debug("CookieSandbox: Not setting invalid cookie."); + return; + } + var pair = [cookiePair.substring(0,splitAt), cookiePair.substring(splitAt+1)]; + var name = pair[0].trim(); + var value = pair[1].trim(); + if(!name) { + Zotero.debug("CookieSandbox: Ignoring attempt to set cookie with no name"); + return; + } + + host = '.' + Zotero.CookieSandbox.normalizeHost(host); + + if(!path) path = '/'; + path = Zotero.CookieSandbox.normalizePath(path); + + if(!this._cookies[host]) { + this._cookies[host] = {}; + } + + if(!this._cookies[host][path]) { + this._cookies[host][path] = {}; + } + + /*Zotero.debug("CookieSandbox: adding cookie " + name + '=' + + value + ' for host ' + host + ' and path ' + path + + '[' + (hostOnly?'hostOnly,':'') + (secure?'secure':'') + ']');*/ + + this._cookies[host][path][name] = { + value: value, + secure: !!secure, + hostOnly: !!hostOnly + }; + }, + + /** + * Returns a list of cookies that should be sent to the given URI + * @param {nsIURI} uri + */ + "getCookiesForURI": function(uri) { + var hostParts = Zotero.CookieSandbox.normalizeHost(uri.host).split('.'), + pathParts = Zotero.CookieSandbox.normalizePath(uri.path).split('/'), + cookies = {}, found = false, secure = uri.scheme.toUpperCase() == 'HTTPS'; + + // Fetch cookies starting from the highest level domain + var cookieHost = '.' + hostParts[hostParts.length-1]; + for(var i=hostParts.length-2; i>=0; i--) { + cookieHost = '.' + hostParts[i] + cookieHost; + if(this._cookies[cookieHost]) { + found = this._getCookiesForPath(cookies, this._cookies[cookieHost], pathParts, secure, i==0) || found; + } + } + + //Zotero.debug("CookieSandbox: returning cookies:"); + //Zotero.debug(cookies); + + return found ? cookies : null; + }, + + "_getCookiesForPath": function(cookies, cookiePaths, pathParts, secure, isHost) { + var found = false; + var path = ''; + for(var i=0, n=pathParts.length; i<n; i++) { + path += pathParts[i]; + var cookiesForPath = cookiePaths[path]; + if(cookiesForPath) { + for(var key in cookiesForPath) { + if(cookiesForPath[key].secure && !secure) continue; + if(cookiesForPath[key].hostOnly && !isHost) continue; + + found = true; + cookies[key] = cookiesForPath[key].value; + } + } + + // Also check paths with trailing / (but not for last part) + path += '/'; + cookiesForPath = cookiePaths[path]; + if(cookiesForPath && i != n-1) { + for(var key in cookiesForPath) { + if(cookiesForPath[key].secure && !secure) continue; + if(cookiesForPath[key].hostOnly && !isHost) continue; + + found = true; + cookies[key] = cookiesForPath[key].value; + } + } + } + return found; } } -Zotero.CookieSandbox.prototype.__defineGetter__("cookieString", function() { - return [key+"="+this._cookies[key] for(key in this._cookies)].join("; "); -}); - /** * nsIObserver implementation for adding, clearing, and slurping cookies */ @@ -218,8 +379,12 @@ Zotero.CookieSandbox.Observer = new function() { } if(topic == "http-on-modify-request") { - // clear cookies to be sent to other domains - if(!trackedBy || channel.URI.host != trackedBy.URI.host) { + // Clear cookies to be sent to other domains if we're not explicitly managing them + if(trackedBy) { + var cookiesForURI = trackedBy.getCookiesForURI(channel.URI); + } + + if(!trackedBy || !cookiesForURI) { channel.setRequestHeader("Cookie", "", false); channel.setRequestHeader("Cookie2", "", false); Zotero.debug("CookieSandbox: Cleared cookies to be sent to "+channelURI, 5); @@ -231,26 +396,27 @@ Zotero.CookieSandbox.Observer = new function() { } // add cookies to be sent to this domain - channel.setRequestHeader("Cookie", trackedBy.cookieString, false); + channel.setRequestHeader("Cookie", Zotero.CookieSandbox.generateCookieString(cookiesForURI), false); Zotero.debug("CookieSandbox: Added cookies for request to "+channelURI, 5); } else if(topic == "http-on-examine-response") { // clear cookies being received try { var cookieHeader = channel.getResponseHeader("Set-Cookie"); } catch(e) { + Zotero.debug("CookieSandbox: No Set-Cookie header received for "+channelURI, 5); return; } + channel.setResponseHeader("Set-Cookie", "", false); channel.setResponseHeader("Set-Cookie2", "", false); - // don't process further if these cookies are for another set of domains - if(!trackedBy || channel.URI.host != trackedBy.URI.host) { - Zotero.debug("CookieSandbox: Rejected cookies from "+channelURI, 5); + if(!cookieHeader || !trackedBy) { + Zotero.debug("CookieSandbox: Not tracking received cookies for "+channelURI, 5); return; } - // put new cookies into our sandbox - if(cookieHeader) trackedBy.addCookiesFromHeader(cookieHeader); + // Put new cookies into our sandbox + trackedBy.addCookiesFromHeader(cookieHeader, channel.URI); Zotero.debug("CookieSandbox: Slurped cookies from "+channelURI, 5); } diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js @@ -339,7 +339,11 @@ Zotero.Server.Connector.SaveItem.prototype = { } catch(e) {} var cookieSandbox = data["uri"] ? new Zotero.CookieSandbox(null, data["uri"], - data["cookie"] || "", url.userAgent) : null; + data["detailedCookies"] ? "" : data["cookie"] || "", url.userAgent) : null; + if(cookieSandbox && data.detailedCookies) { + cookieSandbox.addCookiesFromHeader(data.detailedCookies); + } + for(var i=0; i<data.items.length; i++) { Zotero.Server.Connector.AttachmentProgressManager.add(data.items[i].attachments); }