proxy.js (17554B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2009 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 * A singleton to handle URL rewriting proxies 28 * @namespace 29 * @property transparent {Boolean} Whether transparent proxy functionality is enabled 30 * @property proxies {Zotero.Proxy[]} All loaded proxies 31 * @property hosts {Zotero.Proxy{}} Object mapping hosts to proxies 32 */ 33 Zotero.Proxies = new function() { 34 this.proxies = false; 35 this.transparent = false; 36 this.hosts = {}; 37 38 39 /** 40 * Initializes http-on-examine-response observer to intercept page loads and gets preferences 41 */ 42 this.init = Zotero.Promise.coroutine(function* () { 43 if(!this.proxies) { 44 var rows = yield Zotero.DB.queryAsync("SELECT * FROM proxies"); 45 Zotero.Proxies.proxies = yield Zotero.Promise.all( 46 rows.map(row => this.newProxyFromRow(row)) 47 ); 48 49 for (let proxy of Zotero.Proxies.proxies) { 50 for (let host of proxy.hosts) { 51 Zotero.Proxies.hosts[host] = proxy; 52 } 53 } 54 } 55 56 Zotero.Proxies.transparent = !Zotero.isConnector && Zotero.Prefs.get("proxies.transparent"); 57 Zotero.Proxies.autoRecognize = Zotero.Proxies.transparent && Zotero.Prefs.get("proxies.autoRecognize"); 58 59 var disableByDomainPref = Zotero.Prefs.get("proxies.disableByDomain"); 60 Zotero.Proxies.disableByDomain = (Zotero.Proxies.transparent && disableByDomainPref ? Zotero.Prefs.get("proxies.disableByDomainString") : null); 61 62 Zotero.Proxies.lastIPCheck = 0; 63 Zotero.Proxies.lastIPs = ""; 64 Zotero.Proxies.disabledByDomain = false; 65 66 Zotero.Proxies.showRedirectNotification = Zotero.Prefs.get("proxies.showRedirectNotification"); 67 }); 68 69 70 /** 71 * @param {Object} row - Database row with proxy data 72 * @return {Promise<Zotero.Proxy>} 73 */ 74 this.newProxyFromRow = Zotero.Promise.coroutine(function* (row) { 75 var proxy = new Zotero.Proxy(row); 76 yield proxy.loadHosts(); 77 return proxy; 78 }); 79 80 81 /** 82 * Removes a proxy object from the list of proxy objects 83 * @returns {Boolean} True if the proxy was in the list, false if it was not 84 */ 85 this.remove = function(proxy) { 86 var index = Zotero.Proxies.proxies.indexOf(proxy); 87 if(index == -1) return false; 88 // remove proxy from proxy list 89 Zotero.Proxies.proxies.splice(index, 1); 90 // remove hosts from host list 91 for(var host in Zotero.Proxies.hosts) { 92 if(Zotero.Proxies.hosts[host] == proxy) { 93 delete Zotero.Proxies.hosts[host]; 94 } 95 } 96 return true; 97 } 98 99 /** 100 * Inserts a proxy into the host map; necessary when proxies are added 101 */ 102 this.save = function(proxy) { 103 // add to list of proxies 104 if(Zotero.Proxies.proxies.indexOf(proxy) == -1) Zotero.Proxies.proxies.push(proxy); 105 106 // if there is a proxy ID (i.e., if this is a persisting, transparent proxy), add to host 107 // list to do reverse mapping 108 if(proxy.proxyID) { 109 for (let host of proxy.hosts) { 110 Zotero.Proxies.hosts[host] = proxy; 111 } 112 } 113 } 114 115 /** 116 * Refreshes host map; necessary when proxies are changed or deleted 117 */ 118 this.refreshHostMap = function(proxy) { 119 // if there is no proxyID, then return immediately, since there is no need to update 120 if(!proxy.proxyID) return; 121 122 // delete hosts that point to this proxy if they no longer exist 123 for(var host in Zotero.Proxies.hosts) { 124 if(Zotero.Proxies.hosts[host] == proxy && proxy.hosts.indexOf(host) == -1) { 125 delete Zotero.Proxies.hosts[host]; 126 } 127 } 128 // add new hosts for this proxy 129 Zotero.Proxies.save(proxy); 130 } 131 132 /** 133 * Returns a page's proper URL from a proxied URL. Uses both transparent and opaque proxies. 134 * @param {String} url 135 * @param {Boolean} onlyReturnIfProxied Controls behavior if the given URL is not proxied. If 136 * it is false or unspecified, unproxied URLs are returned verbatim. If it is true, the 137 * function will return "false" if the given URL is unproxied. 138 * @type String 139 */ 140 this.proxyToProper = function(url, onlyReturnIfProxied) { 141 for (let proxy of Zotero.Proxies.proxies) { 142 if(proxy.regexp) { 143 var m = proxy.regexp.exec(url); 144 if(m) { 145 var toProper = proxy.toProper(m); 146 Zotero.debug("Proxies.proxyToProper: "+url+" to "+toProper); 147 return toProper; 148 } 149 } 150 } 151 return (onlyReturnIfProxied ? false : url); 152 } 153 154 /** 155 * Returns a page's proxied URL from the proper URL. Uses only transparent proxies. 156 * @param {String} url 157 * @param {Boolean} onlyReturnIfProxied Controls behavior if the given URL is not proxied. If 158 * it is false or unspecified, unproxied URLs are returned verbatim. If it is true, the 159 * function will return "false" if the given URL is unproxied. 160 * @type String 161 */ 162 this.properToProxy = function(url, onlyReturnIfProxied) { 163 var uri = Services.io.newURI(url, null, null); 164 if(Zotero.Proxies.hosts[uri.hostPort] && Zotero.Proxies.hosts[uri.hostPort].proxyID) { 165 var toProxy = Zotero.Proxies.hosts[uri.hostPort].toProxy(uri); 166 Zotero.debug("Proxies.properToProxy: "+url+" to "+toProxy); 167 return toProxy; 168 } 169 return (onlyReturnIfProxied ? false : url); 170 } 171 172 /** 173 * Check the url for potential proxies and deproxify, providing a scheme to build 174 * a proxy object. 175 * 176 * @param URL 177 * @returns {Object} Unproxied url to proxy object 178 */ 179 this.getPotentialProxies = function(URL) { 180 var urlToProxy = {}; 181 // If it's a known proxied URL just return it 182 if (Zotero.Proxies.transparent) { 183 for (var proxy of Zotero.Proxies.proxies) { 184 if (proxy.regexp) { 185 var m = proxy.regexp.exec(URL); 186 if (m) { 187 let proper = proxy.toProper(m); 188 urlToProxy[proper] = proxy.toJSON(); 189 return urlToProxy; 190 } 191 } 192 } 193 } 194 urlToProxy[URL] = null; 195 196 // if there is a subdomain that is also a TLD, also test against URI with the domain 197 // dropped after the TLD 198 // (i.e., www.nature.com.mutex.gmu.edu => www.nature.com) 199 var m = /^(https?:\/\/)([^\/]+)/i.exec(URL); 200 if (m) { 201 // First, drop the 0- if it exists (this is an III invention) 202 var host = m[2]; 203 if (host.substr(0, 2) === "0-") host = host.substr(2); 204 var hostnameParts = [host.split(".")]; 205 if (m[1] == 'https://' && host.replace(/-/g, '.') != host) { 206 // try replacing hyphens with dots for https protocol 207 // to account for EZProxy HttpsHypens mode 208 hostnameParts.push(host.replace(/-/g, '.').split('.')); 209 } 210 211 for (let i=0; i < hostnameParts.length; i++) { 212 let parts = hostnameParts[i]; 213 // If hostnameParts has two entries, then the second one is with replaced hyphens 214 let dotsToHyphens = i == 1; 215 // skip the lowest level subdomain, domain and TLD 216 for (let j=1; j<parts.length-2; j++) { 217 // if a part matches a TLD, everything up to it is probably the true URL 218 if (TLDS[parts[j].toLowerCase()]) { 219 var properHost = parts.slice(0, j+1).join("."); 220 // protocol + properHost + /path 221 var properURL = m[1]+properHost+URL.substr(m[0].length); 222 var proxyHost = parts.slice(j+1).join('.'); 223 urlToProxy[properURL] = {scheme: m[1] + '%h.' + proxyHost + '/%p', dotsToHyphens}; 224 } 225 } 226 } 227 } 228 return urlToProxy; 229 }; 230 } 231 232 /** 233 * Creates a Zotero.Proxy object from a DB row 234 * 235 * @constructor 236 * @class Represents an individual proxy server 237 */ 238 Zotero.Proxy = function (row) { 239 this.hosts = []; 240 this._loadFromRow(row); 241 } 242 243 /** 244 * Loads a proxy object from a DB row 245 * @private 246 */ 247 Zotero.Proxy.prototype._loadFromRow = function (row) { 248 this.proxyID = row.proxyID; 249 this.multiHost = row.scheme && row.scheme.indexOf('%h') != -1 || !!row.multiHost; 250 this.autoAssociate = !!row.autoAssociate; 251 this.scheme = row.scheme; 252 // Database query results will throw as this option is only present when the proxy comes along with the translator 253 if ('dotsToHyphens' in row) { 254 this.dotsToHyphens = !!row.dotsToHyphens; 255 } 256 257 if (this.scheme) { 258 this.compileRegexp(); 259 } 260 }; 261 262 Zotero.Proxy.prototype.toJSON = function() { 263 if (!this.scheme) { 264 throw Error('Cannot convert proxy to JSON - no scheme'); 265 } 266 return {id: this.id, scheme: this.scheme, dotsToHyphens: this.dotsToHyphens}; 267 } 268 269 /** 270 * Regexps to match the URL contents corresponding to proxy scheme parameters 271 * @const 272 */ 273 const Zotero_Proxy_schemeParameters = { 274 "%p":"(.*?)", // path 275 "%d":"(.*?)", // directory 276 "%f":"(.*?)", // filename 277 "%a":"(.*?)" // anything 278 }; 279 280 /** 281 * Regexps to match proxy scheme parameters in the proxy scheme URL 282 * @const 283 */ 284 const Zotero_Proxy_schemeParameterRegexps = { 285 "%p":/([^%])%p/, 286 "%d":/([^%])%d/, 287 "%f":/([^%])%f/, 288 "%h":/([^%])%h/, 289 "%a":/([^%])%a/ 290 }; 291 292 /** 293 * Compiles the regular expression against which we match URLs to determine if this proxy is in use 294 * and saves it in this.regexp 295 */ 296 Zotero.Proxy.prototype.compileRegexp = function() { 297 // take host only if flagged as multiHost 298 var parametersToCheck = Zotero_Proxy_schemeParameters; 299 if(this.multiHost) parametersToCheck["%h"] = "([a-zA-Z0-9]+[.\\-][a-zA-Z0-9.\\-]+)"; 300 301 var indices = this.indices = {}; 302 this.parameters = []; 303 for(var param in parametersToCheck) { 304 var index = this.scheme.indexOf(param); 305 306 // avoid escaped matches 307 while(this.scheme[index-1] && (this.scheme[index-1] == "%")) { 308 this.scheme = this.scheme.substr(0, index-1)+this.scheme.substr(index); 309 index = this.scheme.indexOf(param, index+1); 310 } 311 312 if(index != -1) { 313 this.indices[param] = index; 314 this.parameters.push(param); 315 } 316 } 317 318 // sort params by index 319 this.parameters = this.parameters.sort(function(a, b) { 320 return indices[a]-indices[b]; 321 }) 322 323 // now replace with regexp fragment in reverse order 324 if (this.scheme.includes('://')) { 325 re = "^"+Zotero.Utilities.quotemeta(this.scheme)+"$"; 326 } else { 327 re = "^https?"+Zotero.Utilities.quotemeta('://'+this.scheme)+"$"; 328 } 329 for(var i=this.parameters.length-1; i>=0; i--) { 330 var param = this.parameters[i]; 331 re = re.replace(Zotero_Proxy_schemeParameterRegexps[param], "$1"+parametersToCheck[param]); 332 } 333 334 this.regexp = new RegExp(re); 335 } 336 337 /** 338 * Ensures that the proxy scheme and host settings are valid for this proxy type 339 * 340 * @returns {String|Boolean} An error type if a validation error occurred, or "false" if there was 341 * no error. 342 */ 343 Zotero.Proxy.prototype.validate = function() { 344 if(this.scheme.length < 8 || (this.scheme.substr(0, 7) != "http://" && this.scheme.substr(0, 8) != "https://")) { 345 return ["scheme.noHTTP"]; 346 } 347 348 if(!this.multiHost && (!this.hosts.length || !this.hosts[0])) { 349 return ["host.invalid"]; 350 } else if(this.multiHost && !Zotero_Proxy_schemeParameterRegexps["%h"].test(this.scheme)) { 351 return ["scheme.noHost"]; 352 } 353 354 if(!Zotero_Proxy_schemeParameterRegexps["%p"].test(this.scheme) && 355 (!Zotero_Proxy_schemeParameterRegexps["%d"].test(this.scheme) || 356 !Zotero_Proxy_schemeParameterRegexps["%f"].test(this.scheme))) { 357 return ["scheme.noPath"]; 358 } 359 360 if(this.scheme.substr(0, 10) == "http://%h/" || this.scheme.substr(0, 11) == "https://%h/") { 361 return ["scheme.invalid"]; 362 } 363 364 for (let host of this.hosts) { 365 var oldHost = Zotero.Proxies.hosts[host]; 366 if(oldHost && oldHost.proxyID && oldHost != this) { 367 return ["host.proxyExists", host]; 368 } 369 } 370 371 return false; 372 } 373 374 /** 375 * Saves any changes to this proxy 376 * 377 * @param {Boolean} transparent True if proxy should be saved as a persisting, transparent proxy 378 */ 379 Zotero.Proxy.prototype.save = Zotero.Promise.coroutine(function* (transparent) { 380 // ensure this proxy is valid 381 var hasErrors = this.validate(); 382 if(hasErrors) throw "Proxy: could not be saved because it is invalid: error "+hasErrors[0]; 383 384 // we never save any changes to non-persisting proxies, so this works 385 var newProxy = !this.proxyID; 386 387 this.autoAssociate = this.multiHost && this.autoAssociate; 388 this.compileRegexp(); 389 390 if(transparent) { 391 yield Zotero.DB.executeTransaction(function* () { 392 if(this.proxyID) { 393 yield Zotero.DB.queryAsync( 394 "UPDATE proxies SET multiHost = ?, autoAssociate = ?, scheme = ? WHERE proxyID = ?", 395 [this.multiHost ? 1 : 0, this.autoAssociate ? 1 : 0, this.scheme, this.proxyID] 396 ); 397 yield Zotero.DB.queryAsync("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]); 398 } else { 399 let id = Zotero.ID.get('proxies'); 400 yield Zotero.DB.queryAsync( 401 "INSERT INTO proxies (proxyID, multiHost, autoAssociate, scheme) VALUES (?, ?, ?, ?)", 402 [id, this.multiHost ? 1 : 0, this.autoAssociate ? 1 : 0, this.scheme] 403 ); 404 this.proxyID = id; 405 } 406 407 this.hosts = this.hosts.sort(); 408 var host; 409 for(var i in this.hosts) { 410 host = this.hosts[i] = this.hosts[i].toLowerCase(); 411 yield Zotero.DB.queryAsync( 412 "INSERT INTO proxyHosts (proxyID, hostname) VALUES (?, ?)", 413 [this.proxyID, host] 414 ); 415 } 416 }.bind(this)); 417 } 418 419 if(newProxy) { 420 Zotero.Proxies.save(this); 421 } else { 422 Zotero.Proxies.refreshHostMap(this); 423 if(!transparent) throw "Proxy: cannot save transparent proxy without transparent param"; 424 } 425 }); 426 427 /** 428 * Reverts to the previously saved version of this proxy 429 */ 430 Zotero.Proxy.prototype.revert = Zotero.Promise.coroutine(function* () { 431 if (!this.proxyID) throw new Error("Cannot revert an unsaved proxy"); 432 var row = yield Zotero.DB.rowQueryAsync("SELECT * FROM proxies WHERE proxyID = ?", [this.proxyID]); 433 this._loadFromRow(row); 434 yield this.loadHosts(); 435 }); 436 437 /** 438 * Deletes this proxy 439 */ 440 Zotero.Proxy.prototype.erase = Zotero.Promise.coroutine(function* () { 441 Zotero.Proxies.remove(this); 442 443 if(this.proxyID) { 444 yield Zotero.DB.executeTransaction(function* () { 445 yield Zotero.DB.queryAsync("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]); 446 yield Zotero.DB.queryAsync("DELETE FROM proxies WHERE proxyID = ?", [this.proxyID]); 447 }.bind(this)); 448 } 449 }); 450 451 /** 452 * Converts a proxied URL to an unproxied URL using this proxy 453 * 454 * @param m {String|Array} The URL or the match from running this proxy's regexp against a URL spec 455 * @return {String} The unproxified URL if was proxified or the unchanged URL 456 */ 457 Zotero.Proxy.prototype.toProper = function(m) { 458 if (!Array.isArray(m)) { 459 let match = this.regexp.exec(m); 460 if (!match) { 461 return m 462 } else { 463 m = match; 464 } 465 } 466 let scheme = this.scheme.indexOf('https') == -1 ? 'http://' : 'https://'; 467 if(this.multiHost) { 468 var properURL = scheme+m[this.parameters.indexOf("%h")+1]+"/"; 469 } else { 470 var properURL = scheme+this.hosts[0]+"/"; 471 } 472 473 // Replace `-` with `.` in https to support EZProxy HttpsHyphens. 474 // Potentially troublesome with domains that contain dashes 475 if (this.dotsToHyphens) { 476 properURL = properURL.replace(/-/g, '.'); 477 } 478 479 if(this.indices["%p"]) { 480 properURL += m[this.parameters.indexOf("%p")+1]; 481 } else { 482 var dir = m[this.parameters.indexOf("%d")+1]; 483 var file = m[this.parameters.indexOf("%f")+1]; 484 if(dir !== "") properURL += dir+"/"; 485 properURL += file; 486 } 487 488 return properURL; 489 } 490 491 /** 492 * Converts an unproxied URL to a proxied URL using this proxy 493 * 494 * @param {String|nsIURI} uri The URL as a string or the nsIURI corresponding to the unproxied URL 495 * @return {String} The proxified URL if was unproxified or the unchanged url 496 */ 497 Zotero.Proxy.prototype.toProxy = function(uri) { 498 if (typeof uri == "string") { 499 uri = Services.io.newURI(uri, null, null); 500 } 501 if (this.regexp.exec(uri.spec)) { 502 return uri.spec; 503 } 504 var proxyURL = this.scheme; 505 506 for(var i=this.parameters.length-1; i>=0; i--) { 507 var param = this.parameters[i]; 508 var value = ""; 509 if(param == "%h") { 510 value = this.dotsToHyphens ? uri.hostPort.replace(/-/g, '.') : uri.hostPort; 511 } else if(param == "%p") { 512 value = uri.path.substr(1); 513 } else if(param == "%d") { 514 value = uri.path.substr(0, uri.path.lastIndexOf("/")); 515 } else if(param == "%f") { 516 value = uri.path.substr(uri.path.lastIndexOf("/")+1) 517 } 518 519 proxyURL = proxyURL.substr(0, this.indices[param])+value+proxyURL.substr(this.indices[param]+2); 520 } 521 522 return proxyURL; 523 } 524 525 Zotero.Proxy.prototype.loadHosts = Zotero.Promise.coroutine(function* () { 526 if (!this.proxyID) { 527 throw Error("Cannot load hosts without a proxyID") 528 } 529 this.hosts = yield Zotero.DB.columnQueryAsync( 530 "SELECT hostname FROM proxyHosts WHERE proxyID = ? ORDER BY hostname", this.proxyID 531 ); 532 }); 533 534 Zotero.Proxies.DNS = new function() { 535 this.getHostnames = function() { 536 if (!Zotero.isWin && !Zotero.isMac && !Zotero.isLinux) return Zotero.Promise.resolve([]); 537 var deferred = Zotero.Promise.defer(); 538 var worker = new ChromeWorker("chrome://zotero/content/xpcom/dns_worker.js"); 539 Zotero.debug("Proxies.DNS: Performing reverse lookup"); 540 worker.onmessage = function(e) { 541 Zotero.debug("Proxies.DNS: Got hostnames "+e.data); 542 deferred.resolve(e.data); 543 }; 544 worker.onerror = function(e) { 545 Zotero.debug("Proxies.DNS: Reverse lookup failed"); 546 deferred.reject(e.message); 547 }; 548 worker.postMessage(Zotero.isWin ? "win" : Zotero.isMac ? "mac" : Zotero.isLinux ? "linux" : "unix"); 549 return deferred.promise; 550 } 551 };