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 }