www

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

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 };