www

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

cookieSandbox.js (13792B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2011 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 /**
     27  * Manage cookies in a sandboxed fashion
     28  *
     29  * @constructor
     30  * @param {browser} [browser] Hidden browser object
     31  * @param {String|nsIURI} uri URI of page to manage cookies for (cookies for domains that are not 
     32  *                     subdomains of this URI are ignored)
     33  * @param {String} cookieData Cookies with which to initiate the sandbox
     34  * @param {String} userAgent User agent to use for sandboxed requests
     35  */
     36 Zotero.CookieSandbox = function(browser, uri, cookieData, userAgent) {
     37 	this._observerService = Components.classes["@mozilla.org/observer-service;1"].
     38 		getService(Components.interfaces.nsIObserverService);
     39 	
     40 	if(uri instanceof Components.interfaces.nsIURI) {
     41 		this.URI = uri;
     42 	} else {
     43 		this.URI = Components.classes["@mozilla.org/network/io-service;1"]
     44 			.getService(Components.interfaces.nsIIOService)
     45 			.newURI(uri, null, null);
     46 	}
     47 	
     48 	this._cookies = {};
     49 	if(cookieData) {
     50 		var splitCookies = cookieData.split(/;\s*/);
     51 		for (let cookie of splitCookies) {
     52 			this.setCookie(cookie, this.URI.host);
     53 		}
     54 	}
     55 	
     56 	if(userAgent) this.userAgent = userAgent;
     57 	
     58 	Zotero.CookieSandbox.Observer.register();
     59 	if(browser) {
     60 		this.attachToBrowser(browser);
     61 	}
     62 }
     63 
     64 /**
     65  * Normalizes the host string: lower-case, remove leading period, some more cleanup
     66  * @param {String} host;
     67  */
     68 Zotero.CookieSandbox.normalizeHost = function(host) {
     69 	return host.trim().toLowerCase().replace(/^\.+|[:\/].*/g, '');
     70 }
     71 
     72 /**
     73  * Normalizes the path string
     74  * @param {String} path;
     75  */
     76 Zotero.CookieSandbox.normalizePath = function(path) {
     77 	return '/' + path.trim().replace(/^\/+|[?#].*/g, '');
     78 }
     79 
     80 /**
     81  * Generates a semicolon-separated string of cookie values from a list of cookies
     82  * @param {Object} cookies Object containing key: value cookie pairs
     83  */
     84 Zotero.CookieSandbox.generateCookieString = function(cookies) {
     85 	var str = '';
     86 	for(var key in cookies) {
     87 		str += '; ' + key + '=' + cookies[key];
     88 	}
     89 	
     90 	return str ? str.substr(2) : '';
     91 }
     92 
     93 Zotero.CookieSandbox.prototype = {
     94 	/**
     95 	 * Adds cookies to this CookieSandbox based on a cookie header
     96 	 * @param {String} cookieString;
     97 	 * @param {nsIURI} [uri] URI of the header origin.
     98 	                   Used to verify same origin. If omitted validation is not performed
     99 	 */
    100 	"addCookiesFromHeader":function(cookieString, uri) {
    101 		var cookies = cookieString.split("\n");
    102 		if(uri) {
    103 			var validDomain = '.' + Zotero.CookieSandbox.normalizeHost(uri.host);
    104 		}
    105 		
    106 		for(var i=0, n=cookies.length; i<n; i++) {
    107 			var cookieInfo = cookies[i].split(/;\s*/);
    108 			var secure = false, path = '', domain = '', hostOnly = false;
    109 			
    110 			for(var j=1, m=cookieInfo.length; j<m; j++) {
    111 				var pair = cookieInfo[j].split(/\s*=\s*/);
    112 				switch(pair[0].trim().toLowerCase()) {
    113 					case 'secure':
    114 						secure = true;
    115 						break;
    116 					case 'domain':
    117 						domain = pair[1];
    118 						break;
    119 					case 'path':
    120 						path = pair[1];
    121 						break;
    122 					case 'hostonly':
    123 						hostOnly = true;
    124 						break;
    125 				}
    126 				
    127 				if(secure && domain && path && hostOnly) break;
    128 			}
    129 			
    130 			// Domain must be a suffix of the host setting the cookie
    131 			if(validDomain && domain) {
    132 				var normalizedDomain = Zotero.CookieSandbox.normalizeHost(domain);
    133 				var substrMatch = validDomain.lastIndexOf(normalizedDomain);
    134 				var publicSuffix;
    135 				try { publicSuffix = Services.eTLD.getPublicSuffix(uri) }  catch(e) {}
    136 				if(substrMatch == -1 || !publicSuffix || publicSuffix == normalizedDomain
    137 					|| (substrMatch + normalizedDomain.length != validDomain.length)
    138 					|| (validDomain.charAt(substrMatch-1) != '.')) {
    139 					Zotero.debug("CookieSandbox: Ignoring attempt to set a cookie for different host");
    140 					continue;
    141 				}
    142 			}
    143 			
    144 			// When no domain is set, use requestor's host (hostOnly cookie)
    145 			if(validDomain && !domain) {
    146 				domain = validDomain.substr(1);
    147 				hostOnly = true;
    148 			}
    149 			
    150 			this.setCookie(cookieInfo[0], domain, path, secure, hostOnly);
    151 		}
    152 	},
    153 	
    154 	/**
    155 	 * Attach CookieSandbox to a specific browser
    156 	 * @param {Browser} browser
    157 	 */
    158 	"attachToBrowser":function(browser) {
    159 		Zotero.CookieSandbox.Observer.trackedBrowsers.set(browser, this);
    160 	},
    161 	
    162 	/**
    163 	 * Attach CookieSandbox to a specific XMLHttpRequest
    164 	 * @param {nsIInterfaceRequestor} ir
    165 	 */
    166 	"attachToInterfaceRequestor": function(ir) {
    167 		Zotero.CookieSandbox.Observer.trackedInterfaceRequestors.push(Components.utils.getWeakReference(ir.QueryInterface(Components.interfaces.nsIInterfaceRequestor)));
    168 		Zotero.CookieSandbox.Observer.trackedInterfaceRequestorSandboxes.push(this);
    169 	},
    170 	
    171 	/**
    172 	 * Set a cookie for a specified host
    173 	 * @param {String} cookiePair A single cookie pair in the form key=value
    174 	 * @param {String} [host] Host to bind the cookie to.
    175 	 *                        Defaults to the host set on this.URI
    176 	 * @param {String} [path]
    177 	 * @param {Boolean} [secure] Whether the cookie has the secure attribute set
    178 	 * @param {Boolean} [hostOnly] Whether the cookie is a host-only cookie
    179 	 */
    180 	"setCookie": function(cookiePair, host, path, secure, hostOnly) {
    181 		var splitAt = cookiePair.indexOf('=');
    182 		if(splitAt === -1) {
    183 			Zotero.debug("CookieSandbox: Not setting invalid cookie.");
    184 			return;
    185 		}
    186 		var pair = [cookiePair.substring(0,splitAt), cookiePair.substring(splitAt+1)];
    187 		var name = pair[0].trim();
    188 		var value = pair[1].trim();
    189 		if(!name) {
    190 			Zotero.debug("CookieSandbox: Ignoring attempt to set cookie with no name");
    191 			return;
    192 		}
    193 		
    194 		host = '.' + Zotero.CookieSandbox.normalizeHost(host);
    195 		
    196 		if(!path) path = '/';
    197 		path = Zotero.CookieSandbox.normalizePath(path);
    198 		
    199 		if(!this._cookies[host]) {
    200 			this._cookies[host] = {};
    201 		}
    202 		
    203 		if(!this._cookies[host][path]) {
    204 			this._cookies[host][path] = {};
    205 		}
    206 		
    207 		/*Zotero.debug("CookieSandbox: adding cookie " + name + '='
    208 			+ value + ' for host ' + host + ' and path ' + path 
    209 			+ '[' + (hostOnly?'hostOnly,':'') + (secure?'secure':'') + ']');*/
    210 		
    211 		this._cookies[host][path][name] = {
    212 			value: value,
    213 			secure: !!secure,
    214 			hostOnly: !!hostOnly
    215 		};
    216 	},
    217 	
    218 	/**
    219 	 * Returns a list of cookies that should be sent to the given URI
    220 	 * @param {nsIURI} uri
    221 	 */
    222 	"getCookiesForURI": function(uri) {
    223 		var hostParts = Zotero.CookieSandbox.normalizeHost(uri.host).split('.'),
    224 			pathParts = Zotero.CookieSandbox.normalizePath(uri.path).split('/'),
    225 			cookies = {}, found = false, secure = uri.scheme.toUpperCase() == 'HTTPS';
    226 		
    227 		// Fetch cookies starting from the highest level domain
    228 		var cookieHost = '.' + hostParts[hostParts.length-1];
    229 		for(var i=hostParts.length-2; i>=0; i--) {
    230 			cookieHost = '.' + hostParts[i] + cookieHost;
    231 			if(this._cookies[cookieHost]) {
    232 				found = this._getCookiesForPath(cookies, this._cookies[cookieHost], pathParts, secure, i==0) || found;
    233 			}
    234 		}
    235 		
    236 		//Zotero.debug("CookieSandbox: returning cookies:");
    237 		//Zotero.debug(cookies);
    238 		
    239 		return found ? cookies : null;
    240 	},
    241 	
    242 	"_getCookiesForPath": function(cookies, cookiePaths, pathParts, secure, isHost) {
    243 		var found = false;
    244 		var path = '';
    245 		for(var i=0, n=pathParts.length; i<n; i++) {
    246 			path += pathParts[i];
    247 			var cookiesForPath = cookiePaths[path];
    248 			if(cookiesForPath) {
    249 				for(var key in cookiesForPath) {
    250 					if(cookiesForPath[key].secure && !secure) continue;
    251 					if(cookiesForPath[key].hostOnly && !isHost) continue;
    252 					
    253 					found = true;
    254 					cookies[key] = cookiesForPath[key].value;
    255 				}
    256 			}
    257 			
    258 			// Also check paths with trailing / (but not for last part)
    259 			path += '/';
    260 			cookiesForPath = cookiePaths[path];
    261 			if(cookiesForPath && i != n-1) {
    262 				for(var key in cookiesForPath) {
    263 					if(cookiesForPath[key].secure && !secure) continue;
    264 					if(cookiesForPath[key].hostOnly && !isHost) continue;
    265 					
    266 					found = true;
    267 					cookies[key] = cookiesForPath[key].value;
    268 				}
    269 			}
    270 		}
    271 		return found;
    272 	}
    273 }
    274 
    275 /**
    276  * nsIObserver implementation for adding, clearing, and slurping cookies
    277  */
    278 Zotero.CookieSandbox.Observer = new function() {
    279 	const observeredTopics = ["http-on-examine-response", "http-on-modify-request", "quit-application"];
    280 	
    281 	var observerService = Components.classes["@mozilla.org/observer-service;1"].
    282 			getService(Components.interfaces.nsIObserverService),
    283 		observing = false;
    284 	
    285 	/**
    286 	 * Registers cookie manager and observer, if necessary
    287 	 */
    288 	this.register = function(CookieSandbox) {
    289 		this.trackedBrowsers = new WeakMap();
    290 		this.trackedInterfaceRequestors = [];
    291 		this.trackedInterfaceRequestorSandboxes = [];
    292 		
    293 		if(!observing) {
    294 			Zotero.debug("CookieSandbox: Registering observers");
    295 			for (let topic of observeredTopics) observerService.addObserver(this, topic, false);
    296 			observing = true;
    297 		}
    298 	};
    299 	
    300 	/**
    301 	 * Implements nsIObserver to watch for new cookies and to add sandboxed cookies
    302 	 */
    303 	this.observe = function(channel, topic) {
    304 		channel.QueryInterface(Components.interfaces.nsIHttpChannel);
    305 		var trackedBy, tested, browser, callbacks,
    306 			channelURI = channel.URI.hostPort,
    307 			notificationCallbacks = channel.notificationCallbacks;
    308 		
    309 		// try the notification callbacks
    310 		if(notificationCallbacks) {
    311 			for(var i=0; i<this.trackedInterfaceRequestors.length; i++) {
    312 				// Interface requestors are stored as weak references, so we have to see
    313 				// if they still point to something
    314 				var ir = this.trackedInterfaceRequestors[i].get();
    315 				if(!ir) {
    316 					// The interface requestor is gone, so remove it from the list
    317 					this.trackedInterfaceRequestors.splice(i, 1);
    318 					this.trackedInterfaceRequestorSandboxes.splice(i, 1);
    319 					i--;
    320 				} else if(ir == notificationCallbacks) {
    321 					// We are tracking this interface requestor
    322 					trackedBy = this.trackedInterfaceRequestorSandboxes[i];
    323 					break;
    324 				}
    325 			}
    326 			
    327 			if(trackedBy) {
    328 				tested = true;
    329 			} else {
    330 				// try the browser
    331 				try {
    332 					browser = notificationCallbacks.getInterface(Ci.nsIWebNavigation)
    333 						.QueryInterface(Ci.nsIDocShell).chromeEventHandler;
    334 				} catch(e) {}
    335 				if(browser) {
    336 					tested = true;
    337 					trackedBy = this.trackedBrowsers.get(browser);
    338 				} else {
    339 					// try the document for the load group
    340 					try {
    341 						browser = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsIWebNavigation)
    342 							.QueryInterface(Ci.nsIDocShell).chromeEventHandler;
    343 					} catch(e) {}
    344 					if(browser) {
    345 						tested = true;
    346 						trackedBy = this.trackedBrowsers.get(browser);
    347 					} else {
    348 						// try getting as an XHR or nsIWBP
    349 						try {
    350 							notificationCallbacks.QueryInterface(Components.interfaces.nsIXMLHttpRequest);
    351 							tested = true;
    352 						} catch(e) {}
    353 						if(!tested) {
    354 							try {
    355 								notificationCallbacks.QueryInterface(Components.interfaces.nsIWebBrowserPersist);
    356 								tested = true;
    357 							} catch(e) {}
    358 						}
    359 					}
    360 				}
    361 			}
    362 		}
    363 		
    364 		// trackedBy => we should manage cookies for this request
    365 		// tested && !trackedBy => we should not manage cookies for this request
    366 		// !tested && !trackedBy => this request is of a type we couldn't match to this request.
    367 		//         one such type is a link prefetch (nsPrefetchNode) but there might be others as
    368 		//         well. for now, we are paranoid and reject these.
    369 		
    370 		if(tested) {
    371 			if(trackedBy) {
    372 				Zotero.debug("CookieSandbox: Managing cookies for "+channelURI, 5);
    373 			} else {
    374 				Zotero.debug("CookieSandbox: Not touching channel for "+channelURI, 5);
    375 				return;
    376 			}
    377 		} else {
    378 			Zotero.debug("CookieSandbox: Being paranoid about channel for "+channelURI, 5);
    379 		}
    380 		
    381 		if(topic == "http-on-modify-request") {
    382 			// Clear cookies to be sent to other domains if we're not explicitly managing them
    383 			if(trackedBy) {
    384 				var cookiesForURI = trackedBy.getCookiesForURI(channel.URI);
    385 			}
    386 			
    387 			if(!trackedBy || !cookiesForURI) {
    388 				channel.setRequestHeader("Cookie", "", false);
    389 				channel.setRequestHeader("Cookie2", "", false);
    390 				Zotero.debug("CookieSandbox: Cleared cookies to be sent to "+channelURI, 5);
    391 				return;
    392 			}
    393 			
    394 			if(trackedBy.userAgent) {
    395 				channel.setRequestHeader("User-Agent", trackedBy.userAgent, false);
    396 			}
    397 			
    398 			// add cookies to be sent to this domain
    399 			channel.setRequestHeader("Cookie", Zotero.CookieSandbox.generateCookieString(cookiesForURI), false);
    400 			Zotero.debug("CookieSandbox: Added cookies for request to "+channelURI, 5);
    401 		} else if(topic == "http-on-examine-response") {
    402 			// clear cookies being received
    403 			try {
    404 				var cookieHeader = channel.getResponseHeader("Set-Cookie");
    405 			} catch(e) {
    406 				Zotero.debug("CookieSandbox: No Set-Cookie header received for "+channelURI, 5);
    407 				return;
    408 			}
    409 			
    410 			channel.setResponseHeader("Set-Cookie", "", false);
    411 			channel.setResponseHeader("Set-Cookie2", "", false);
    412 			
    413 			if(!cookieHeader || !trackedBy) {
    414 				Zotero.debug("CookieSandbox: Not tracking received cookies for "+channelURI, 5);
    415 				return;
    416 			}
    417 			
    418 			// Put new cookies into our sandbox
    419 			trackedBy.addCookiesFromHeader(cookieHeader, channel.URI);
    420 			
    421 			Zotero.debug("CookieSandbox: Slurped cookies from "+channelURI, 5);
    422 		}
    423 	}
    424 }