551 lines
No EOL
17 KiB
JavaScript
551 lines
No EOL
17 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2009 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
http://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
/**
|
|
* A singleton to handle URL rewriting proxies
|
|
* @namespace
|
|
* @property transparent {Boolean} Whether transparent proxy functionality is enabled
|
|
* @property proxies {Zotero.Proxy[]} All loaded proxies
|
|
* @property hosts {Zotero.Proxy{}} Object mapping hosts to proxies
|
|
*/
|
|
Zotero.Proxies = new function() {
|
|
this.proxies = false;
|
|
this.transparent = false;
|
|
this.hosts = {};
|
|
|
|
|
|
/**
|
|
* Initializes http-on-examine-response observer to intercept page loads and gets preferences
|
|
*/
|
|
this.init = Zotero.Promise.coroutine(function* () {
|
|
if(!this.proxies) {
|
|
var rows = yield Zotero.DB.queryAsync("SELECT * FROM proxies");
|
|
Zotero.Proxies.proxies = yield Zotero.Promise.all(
|
|
rows.map(row => this.newProxyFromRow(row))
|
|
);
|
|
|
|
for (let proxy of Zotero.Proxies.proxies) {
|
|
for (let host of proxy.hosts) {
|
|
Zotero.Proxies.hosts[host] = proxy;
|
|
}
|
|
}
|
|
}
|
|
|
|
Zotero.Proxies.transparent = !Zotero.isConnector && Zotero.Prefs.get("proxies.transparent");
|
|
Zotero.Proxies.autoRecognize = Zotero.Proxies.transparent && Zotero.Prefs.get("proxies.autoRecognize");
|
|
|
|
var disableByDomainPref = Zotero.Prefs.get("proxies.disableByDomain");
|
|
Zotero.Proxies.disableByDomain = (Zotero.Proxies.transparent && disableByDomainPref ? Zotero.Prefs.get("proxies.disableByDomainString") : null);
|
|
|
|
Zotero.Proxies.lastIPCheck = 0;
|
|
Zotero.Proxies.lastIPs = "";
|
|
Zotero.Proxies.disabledByDomain = false;
|
|
|
|
Zotero.Proxies.showRedirectNotification = Zotero.Prefs.get("proxies.showRedirectNotification");
|
|
});
|
|
|
|
|
|
/**
|
|
* @param {Object} row - Database row with proxy data
|
|
* @return {Promise<Zotero.Proxy>}
|
|
*/
|
|
this.newProxyFromRow = Zotero.Promise.coroutine(function* (row) {
|
|
var proxy = new Zotero.Proxy(row);
|
|
yield proxy.loadHosts();
|
|
return proxy;
|
|
});
|
|
|
|
|
|
/**
|
|
* Removes a proxy object from the list of proxy objects
|
|
* @returns {Boolean} True if the proxy was in the list, false if it was not
|
|
*/
|
|
this.remove = function(proxy) {
|
|
var index = Zotero.Proxies.proxies.indexOf(proxy);
|
|
if(index == -1) return false;
|
|
// remove proxy from proxy list
|
|
Zotero.Proxies.proxies.splice(index, 1);
|
|
// remove hosts from host list
|
|
for(var host in Zotero.Proxies.hosts) {
|
|
if(Zotero.Proxies.hosts[host] == proxy) {
|
|
delete Zotero.Proxies.hosts[host];
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Inserts a proxy into the host map; necessary when proxies are added
|
|
*/
|
|
this.save = function(proxy) {
|
|
// add to list of proxies
|
|
if(Zotero.Proxies.proxies.indexOf(proxy) == -1) Zotero.Proxies.proxies.push(proxy);
|
|
|
|
// if there is a proxy ID (i.e., if this is a persisting, transparent proxy), add to host
|
|
// list to do reverse mapping
|
|
if(proxy.proxyID) {
|
|
for (let host of proxy.hosts) {
|
|
Zotero.Proxies.hosts[host] = proxy;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refreshes host map; necessary when proxies are changed or deleted
|
|
*/
|
|
this.refreshHostMap = function(proxy) {
|
|
// if there is no proxyID, then return immediately, since there is no need to update
|
|
if(!proxy.proxyID) return;
|
|
|
|
// delete hosts that point to this proxy if they no longer exist
|
|
for(var host in Zotero.Proxies.hosts) {
|
|
if(Zotero.Proxies.hosts[host] == proxy && proxy.hosts.indexOf(host) == -1) {
|
|
delete Zotero.Proxies.hosts[host];
|
|
}
|
|
}
|
|
// add new hosts for this proxy
|
|
Zotero.Proxies.save(proxy);
|
|
}
|
|
|
|
/**
|
|
* Returns a page's proper URL from a proxied URL. Uses both transparent and opaque proxies.
|
|
* @param {String} url
|
|
* @param {Boolean} onlyReturnIfProxied Controls behavior if the given URL is not proxied. If
|
|
* it is false or unspecified, unproxied URLs are returned verbatim. If it is true, the
|
|
* function will return "false" if the given URL is unproxied.
|
|
* @type String
|
|
*/
|
|
this.proxyToProper = function(url, onlyReturnIfProxied) {
|
|
for (let proxy of Zotero.Proxies.proxies) {
|
|
if(proxy.regexp) {
|
|
var m = proxy.regexp.exec(url);
|
|
if(m) {
|
|
var toProper = proxy.toProper(m);
|
|
Zotero.debug("Proxies.proxyToProper: "+url+" to "+toProper);
|
|
return toProper;
|
|
}
|
|
}
|
|
}
|
|
return (onlyReturnIfProxied ? false : url);
|
|
}
|
|
|
|
/**
|
|
* Returns a page's proxied URL from the proper URL. Uses only transparent proxies.
|
|
* @param {String} url
|
|
* @param {Boolean} onlyReturnIfProxied Controls behavior if the given URL is not proxied. If
|
|
* it is false or unspecified, unproxied URLs are returned verbatim. If it is true, the
|
|
* function will return "false" if the given URL is unproxied.
|
|
* @type String
|
|
*/
|
|
this.properToProxy = function(url, onlyReturnIfProxied) {
|
|
var uri = Services.io.newURI(url, null, null);
|
|
if(Zotero.Proxies.hosts[uri.hostPort] && Zotero.Proxies.hosts[uri.hostPort].proxyID) {
|
|
var toProxy = Zotero.Proxies.hosts[uri.hostPort].toProxy(uri);
|
|
Zotero.debug("Proxies.properToProxy: "+url+" to "+toProxy);
|
|
return toProxy;
|
|
}
|
|
return (onlyReturnIfProxied ? false : url);
|
|
}
|
|
|
|
/**
|
|
* Check the url for potential proxies and deproxify, providing a scheme to build
|
|
* a proxy object.
|
|
*
|
|
* @param URL
|
|
* @returns {Object} Unproxied url to proxy object
|
|
*/
|
|
this.getPotentialProxies = function(URL) {
|
|
var urlToProxy = {};
|
|
// If it's a known proxied URL just return it
|
|
if (Zotero.Proxies.transparent) {
|
|
for (var proxy of Zotero.Proxies.proxies) {
|
|
if (proxy.regexp) {
|
|
var m = proxy.regexp.exec(URL);
|
|
if (m) {
|
|
let proper = proxy.toProper(m);
|
|
urlToProxy[proper] = proxy.toJSON();
|
|
return urlToProxy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
urlToProxy[URL] = null;
|
|
|
|
// if there is a subdomain that is also a TLD, also test against URI with the domain
|
|
// dropped after the TLD
|
|
// (i.e., www.nature.com.mutex.gmu.edu => www.nature.com)
|
|
var m = /^(https?:\/\/)([^\/]+)/i.exec(URL);
|
|
if (m) {
|
|
// First, drop the 0- if it exists (this is an III invention)
|
|
var host = m[2];
|
|
if (host.substr(0, 2) === "0-") host = host.substr(2);
|
|
var hostnameParts = [host.split(".")];
|
|
if (m[1] == 'https://' && host.replace(/-/g, '.') != host) {
|
|
// try replacing hyphens with dots for https protocol
|
|
// to account for EZProxy HttpsHypens mode
|
|
hostnameParts.push(host.replace(/-/g, '.').split('.'));
|
|
}
|
|
|
|
for (let i=0; i < hostnameParts.length; i++) {
|
|
let parts = hostnameParts[i];
|
|
// If hostnameParts has two entries, then the second one is with replaced hyphens
|
|
let dotsToHyphens = i == 1;
|
|
// skip the lowest level subdomain, domain and TLD
|
|
for (let j=1; j<parts.length-2; j++) {
|
|
// if a part matches a TLD, everything up to it is probably the true URL
|
|
if (TLDS[parts[j].toLowerCase()]) {
|
|
var properHost = parts.slice(0, j+1).join(".");
|
|
// protocol + properHost + /path
|
|
var properURL = m[1]+properHost+URL.substr(m[0].length);
|
|
var proxyHost = parts.slice(j+1).join('.');
|
|
urlToProxy[properURL] = {scheme: m[1] + '%h.' + proxyHost + '/%p', dotsToHyphens};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return urlToProxy;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a Zotero.Proxy object from a DB row
|
|
*
|
|
* @constructor
|
|
* @class Represents an individual proxy server
|
|
*/
|
|
Zotero.Proxy = function (row) {
|
|
this.hosts = [];
|
|
this._loadFromRow(row);
|
|
}
|
|
|
|
/**
|
|
* Loads a proxy object from a DB row
|
|
* @private
|
|
*/
|
|
Zotero.Proxy.prototype._loadFromRow = function (row) {
|
|
this.proxyID = row.proxyID;
|
|
this.multiHost = row.scheme && row.scheme.indexOf('%h') != -1 || !!row.multiHost;
|
|
this.autoAssociate = !!row.autoAssociate;
|
|
this.scheme = row.scheme;
|
|
// Database query results will throw as this option is only present when the proxy comes along with the translator
|
|
if ('dotsToHyphens' in row) {
|
|
this.dotsToHyphens = !!row.dotsToHyphens;
|
|
}
|
|
|
|
if (this.scheme) {
|
|
this.compileRegexp();
|
|
}
|
|
};
|
|
|
|
Zotero.Proxy.prototype.toJSON = function() {
|
|
if (!this.scheme) {
|
|
throw Error('Cannot convert proxy to JSON - no scheme');
|
|
}
|
|
return {id: this.id, scheme: this.scheme, dotsToHyphens: this.dotsToHyphens};
|
|
}
|
|
|
|
/**
|
|
* Regexps to match the URL contents corresponding to proxy scheme parameters
|
|
* @const
|
|
*/
|
|
const Zotero_Proxy_schemeParameters = {
|
|
"%p":"(.*?)", // path
|
|
"%d":"(.*?)", // directory
|
|
"%f":"(.*?)", // filename
|
|
"%a":"(.*?)" // anything
|
|
};
|
|
|
|
/**
|
|
* Regexps to match proxy scheme parameters in the proxy scheme URL
|
|
* @const
|
|
*/
|
|
const Zotero_Proxy_schemeParameterRegexps = {
|
|
"%p":/([^%])%p/,
|
|
"%d":/([^%])%d/,
|
|
"%f":/([^%])%f/,
|
|
"%h":/([^%])%h/,
|
|
"%a":/([^%])%a/
|
|
};
|
|
|
|
/**
|
|
* Compiles the regular expression against which we match URLs to determine if this proxy is in use
|
|
* and saves it in this.regexp
|
|
*/
|
|
Zotero.Proxy.prototype.compileRegexp = function() {
|
|
// take host only if flagged as multiHost
|
|
var parametersToCheck = Zotero_Proxy_schemeParameters;
|
|
if(this.multiHost) parametersToCheck["%h"] = "([a-zA-Z0-9]+[.\\-][a-zA-Z0-9.\\-]+)";
|
|
|
|
var indices = this.indices = {};
|
|
this.parameters = [];
|
|
for(var param in parametersToCheck) {
|
|
var index = this.scheme.indexOf(param);
|
|
|
|
// avoid escaped matches
|
|
while(this.scheme[index-1] && (this.scheme[index-1] == "%")) {
|
|
this.scheme = this.scheme.substr(0, index-1)+this.scheme.substr(index);
|
|
index = this.scheme.indexOf(param, index+1);
|
|
}
|
|
|
|
if(index != -1) {
|
|
this.indices[param] = index;
|
|
this.parameters.push(param);
|
|
}
|
|
}
|
|
|
|
// sort params by index
|
|
this.parameters = this.parameters.sort(function(a, b) {
|
|
return indices[a]-indices[b];
|
|
})
|
|
|
|
// now replace with regexp fragment in reverse order
|
|
if (this.scheme.includes('://')) {
|
|
re = "^"+Zotero.Utilities.quotemeta(this.scheme)+"$";
|
|
} else {
|
|
re = "^https?"+Zotero.Utilities.quotemeta('://'+this.scheme)+"$";
|
|
}
|
|
for(var i=this.parameters.length-1; i>=0; i--) {
|
|
var param = this.parameters[i];
|
|
re = re.replace(Zotero_Proxy_schemeParameterRegexps[param], "$1"+parametersToCheck[param]);
|
|
}
|
|
|
|
this.regexp = new RegExp(re);
|
|
}
|
|
|
|
/**
|
|
* Ensures that the proxy scheme and host settings are valid for this proxy type
|
|
*
|
|
* @returns {String|Boolean} An error type if a validation error occurred, or "false" if there was
|
|
* no error.
|
|
*/
|
|
Zotero.Proxy.prototype.validate = function() {
|
|
if(this.scheme.length < 8 || (this.scheme.substr(0, 7) != "http://" && this.scheme.substr(0, 8) != "https://")) {
|
|
return ["scheme.noHTTP"];
|
|
}
|
|
|
|
if(!this.multiHost && (!this.hosts.length || !this.hosts[0])) {
|
|
return ["host.invalid"];
|
|
} else if(this.multiHost && !Zotero_Proxy_schemeParameterRegexps["%h"].test(this.scheme)) {
|
|
return ["scheme.noHost"];
|
|
}
|
|
|
|
if(!Zotero_Proxy_schemeParameterRegexps["%p"].test(this.scheme) &&
|
|
(!Zotero_Proxy_schemeParameterRegexps["%d"].test(this.scheme) ||
|
|
!Zotero_Proxy_schemeParameterRegexps["%f"].test(this.scheme))) {
|
|
return ["scheme.noPath"];
|
|
}
|
|
|
|
if(this.scheme.substr(0, 10) == "http://%h/" || this.scheme.substr(0, 11) == "https://%h/") {
|
|
return ["scheme.invalid"];
|
|
}
|
|
|
|
for (let host of this.hosts) {
|
|
var oldHost = Zotero.Proxies.hosts[host];
|
|
if(oldHost && oldHost.proxyID && oldHost != this) {
|
|
return ["host.proxyExists", host];
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Saves any changes to this proxy
|
|
*
|
|
* @param {Boolean} transparent True if proxy should be saved as a persisting, transparent proxy
|
|
*/
|
|
Zotero.Proxy.prototype.save = Zotero.Promise.coroutine(function* (transparent) {
|
|
// ensure this proxy is valid
|
|
var hasErrors = this.validate();
|
|
if(hasErrors) throw new Error("Proxy: could not be saved because it is invalid: error "+hasErrors[0]);
|
|
|
|
// we never save any changes to non-persisting proxies, so this works
|
|
var newProxy = !this.proxyID;
|
|
|
|
this.autoAssociate = this.multiHost && this.autoAssociate;
|
|
this.compileRegexp();
|
|
|
|
if(transparent) {
|
|
yield Zotero.DB.executeTransaction(async function () {
|
|
if(this.proxyID) {
|
|
await Zotero.DB.queryAsync(
|
|
"UPDATE proxies SET multiHost = ?, autoAssociate = ?, scheme = ? WHERE proxyID = ?",
|
|
[this.multiHost ? 1 : 0, this.autoAssociate ? 1 : 0, this.scheme, this.proxyID]
|
|
);
|
|
await Zotero.DB.queryAsync("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]);
|
|
} else {
|
|
let id = Zotero.ID.get('proxies');
|
|
await Zotero.DB.queryAsync(
|
|
"INSERT INTO proxies (proxyID, multiHost, autoAssociate, scheme) VALUES (?, ?, ?, ?)",
|
|
[id, this.multiHost ? 1 : 0, this.autoAssociate ? 1 : 0, this.scheme]
|
|
);
|
|
this.proxyID = id;
|
|
}
|
|
|
|
this.hosts = this.hosts.sort();
|
|
var host;
|
|
for(var i in this.hosts) {
|
|
host = this.hosts[i] = this.hosts[i].toLowerCase();
|
|
await Zotero.DB.queryAsync(
|
|
"INSERT INTO proxyHosts (proxyID, hostname) VALUES (?, ?)",
|
|
[this.proxyID, host]
|
|
);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
if(newProxy) {
|
|
Zotero.Proxies.save(this);
|
|
} else {
|
|
Zotero.Proxies.refreshHostMap(this);
|
|
if(!transparent) throw new Error("Proxy: cannot save transparent proxy without transparent param");
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Reverts to the previously saved version of this proxy
|
|
*/
|
|
Zotero.Proxy.prototype.revert = Zotero.Promise.coroutine(function* () {
|
|
if (!this.proxyID) throw new Error("Cannot revert an unsaved proxy");
|
|
var row = yield Zotero.DB.rowQueryAsync("SELECT * FROM proxies WHERE proxyID = ?", [this.proxyID]);
|
|
this._loadFromRow(row);
|
|
yield this.loadHosts();
|
|
});
|
|
|
|
/**
|
|
* Deletes this proxy
|
|
*/
|
|
Zotero.Proxy.prototype.erase = Zotero.Promise.coroutine(function* () {
|
|
Zotero.Proxies.remove(this);
|
|
|
|
if(this.proxyID) {
|
|
yield Zotero.DB.executeTransaction(async function () {
|
|
await Zotero.DB.queryAsync("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]);
|
|
await Zotero.DB.queryAsync("DELETE FROM proxies WHERE proxyID = ?", [this.proxyID]);
|
|
}.bind(this));
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Converts a proxied URL to an unproxied URL using this proxy
|
|
*
|
|
* @param m {String|Array} The URL or the match from running this proxy's regexp against a URL spec
|
|
* @return {String} The unproxified URL if was proxified or the unchanged URL
|
|
*/
|
|
Zotero.Proxy.prototype.toProper = function(m) {
|
|
if (!Array.isArray(m)) {
|
|
let match = this.regexp.exec(m);
|
|
if (!match) {
|
|
return m
|
|
} else {
|
|
m = match;
|
|
}
|
|
}
|
|
let scheme = this.scheme.indexOf('https') == -1 ? 'http://' : 'https://';
|
|
if(this.multiHost) {
|
|
var properURL = scheme+m[this.parameters.indexOf("%h")+1]+"/";
|
|
} else {
|
|
var properURL = scheme+this.hosts[0]+"/";
|
|
}
|
|
|
|
// Replace `-` with `.` in https to support EZProxy HttpsHyphens.
|
|
// Potentially troublesome with domains that contain dashes
|
|
if (this.dotsToHyphens) {
|
|
properURL = properURL.replace(/-/g, '.');
|
|
}
|
|
|
|
if(this.indices["%p"]) {
|
|
properURL += m[this.parameters.indexOf("%p")+1];
|
|
} else {
|
|
var dir = m[this.parameters.indexOf("%d")+1];
|
|
var file = m[this.parameters.indexOf("%f")+1];
|
|
if(dir !== "") properURL += dir+"/";
|
|
properURL += file;
|
|
}
|
|
|
|
return properURL;
|
|
}
|
|
|
|
/**
|
|
* Converts an unproxied URL to a proxied URL using this proxy
|
|
*
|
|
* @param {String|nsIURI} uri The URL as a string or the nsIURI corresponding to the unproxied URL
|
|
* @return {String} The proxified URL if was unproxified or the unchanged url
|
|
*/
|
|
Zotero.Proxy.prototype.toProxy = function(uri) {
|
|
if (typeof uri == "string") {
|
|
uri = Services.io.newURI(uri, null, null);
|
|
}
|
|
if (this.regexp.exec(uri.spec)) {
|
|
return uri.spec;
|
|
}
|
|
var proxyURL = this.scheme;
|
|
|
|
for(var i=this.parameters.length-1; i>=0; i--) {
|
|
var param = this.parameters[i];
|
|
var value = "";
|
|
if(param == "%h") {
|
|
value = this.dotsToHyphens ? uri.hostPort.replace(/-/g, '.') : uri.hostPort;
|
|
} else if(param == "%p") {
|
|
value = uri.pathQueryRef.substr(1);
|
|
} else if(param == "%d") {
|
|
value = uri.pathQueryRef.substr(0, uri.pathQueryRef.lastIndexOf("/"));
|
|
} else if(param == "%f") {
|
|
value = uri.pathQueryRef.substr(uri.pathQueryRef.lastIndexOf("/")+1)
|
|
}
|
|
|
|
proxyURL = proxyURL.substr(0, this.indices[param])+value+proxyURL.substr(this.indices[param]+2);
|
|
}
|
|
|
|
return proxyURL;
|
|
}
|
|
|
|
Zotero.Proxy.prototype.loadHosts = Zotero.Promise.coroutine(function* () {
|
|
if (!this.proxyID) {
|
|
throw Error("Cannot load hosts without a proxyID")
|
|
}
|
|
this.hosts = yield Zotero.DB.columnQueryAsync(
|
|
"SELECT hostname FROM proxyHosts WHERE proxyID = ? ORDER BY hostname", this.proxyID
|
|
);
|
|
});
|
|
|
|
Zotero.Proxies.DNS = new function() {
|
|
this.getHostnames = function() {
|
|
if (!Zotero.isWin && !Zotero.isMac && !Zotero.isLinux) return Zotero.Promise.resolve([]);
|
|
var deferred = Zotero.Promise.defer();
|
|
var worker = new ChromeWorker("chrome://zotero/content/xpcom/dns_worker.js");
|
|
Zotero.debug("Proxies.DNS: Performing reverse lookup");
|
|
worker.onmessage = function(e) {
|
|
Zotero.debug("Proxies.DNS: Got hostnames "+e.data);
|
|
deferred.resolve(e.data);
|
|
};
|
|
worker.onerror = function(e) {
|
|
Zotero.debug("Proxies.DNS: Reverse lookup failed");
|
|
deferred.reject(e.message);
|
|
};
|
|
worker.postMessage(Zotero.isWin ? "win" : Zotero.isMac ? "mac" : Zotero.isLinux ? "linux" : "unix");
|
|
return deferred.promise;
|
|
}
|
|
}; |