zotero/chrome/content/zotero/xpcom/proxy.js
Simon Kornblith 9ca461c59b Proxy system overhaul
- Only one preference for recognizing proxies for transparent redirection
- Blacklists sites and http://%h/%p
- Fixes to EZProxy

This will clear all existing proxies from Zotero
2009-03-23 19:55:57 +00:00

700 lines
22 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright (c) 2006 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://chnm.gmu.edu
Licensed under the Educational Community License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.opensource.org/licenses/ecl1.php
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***** 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 = {};
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var lastRecognizedURI = false;
var lastButton = false;
/**
* Initializes http-on-examine-response observer to intercept page loads and gets preferences
*/
this.init = function() {
if(!this.proxies) {
var me = this;
Zotero.MIMETypeHandler.addObserver(function(ch) { me.observe(ch) });
var rows = Zotero.DB.query("SELECT * FROM proxies");
Zotero.Proxies.proxies = [new Zotero.Proxy(row) for each(row in rows)];
for each(var proxy in Zotero.Proxies.proxies) {
for each(var host in proxy.hosts) {
Zotero.Proxies.hosts[host] = proxy;
}
}
}
Zotero.Proxies.transparent = Zotero.Prefs.get("proxies.transparent");
}
/**
* Observe method to capture page loads and determine if they're going through an EZProxy.
* At the moment, also clears Content-Disposition header on requests for EndNote files so we
* can capture them instead of letting them get saved as attachments
*
* @param {nsIChannel} channel
*/
this.observe = function(channel) {
// try to detect a proxy
channel.QueryInterface(Components.interfaces.nsIHttpChannel);
var url = channel.URI.spec;
// see if there is a proxy we already know
var m = false;
var proxy;
for each(proxy in Zotero.Proxies.proxies) {
if(proxy.proxyID && proxy.regexp && proxy.multiHost) {
m = proxy.regexp.exec(url);
if(m) break;
}
}
if(m) {
var host = m[proxy.parameters.indexOf("%h")+1];
// add this host if we know a proxy
if(proxy.autoAssociate && !Zotero.Proxies.hosts[host] && proxy.hosts.indexOf(host) == -1 && !_isBlacklisted(host)) {
// if no saved host and host is not blacklisted, auto associate
proxy.hosts.push(host);
proxy.save(true);
}
} else {
// otherwise, try to detect a proxy
var proxy = false;
for each(var detector in Zotero.Proxies.Detectors) {
try {
proxy = detector(channel);
} catch(e) {
Components.utils.reportError(e);
}
if(!proxy) continue;
var transparent = _showDialog(proxy.hosts[0], channel.URI.hostPort);
proxy.save(transparent);
break;
}
}
// try to get an applicable proxy
var webNav = null;
try {
webNav = channel.notificationCallbacks.QueryInterface(Components.interfaces.nsIWebNavigation);
} catch(e) {}
if(webNav) {
var proxied = Zotero.Proxies.properToProxy(url, true);
if(proxied) {
channel.QueryInterface(Components.interfaces.nsIHttpChannel);
// If the referrer is a proxiable host, we already have access
// (e.g., we're on-campus) and shouldn't redirect
if (channel.referrer) {
if (Zotero.Proxies.properToProxy(channel.referrer.spec, true)) {
return;
}
}
webNav.loadURI(proxied, 0, channel.URI, null, null);
}
}
}
/**
* 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 each(var host in 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 each(var proxy in Zotero.Proxies.proxies) {
if(proxy.regexp) {
var m = proxy.regexp.exec(url);
if(m) {
var toProper = proxy.toProper(m);
Zotero.debug("Zotero.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 = ioService.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("Zotero.Proxies.properToProxy: "+url+" to "+toProxy);
return toProxy;
}
return (onlyReturnIfProxied ? false : url);
}
/**
* Determines whether a host is blacklisted, i.e., whether we should refuse to save transparent
* proxy entries for this host. This is necessary because EZProxy offers to proxy all Google and
* Wikipedia subdomains, but in practice, this would get really annoying.
*
* @type Boolean
* @private
*/
function _isBlacklisted(host) {
/**
* Regular expression patterns of hosts never to proxy
* @const
*/
const hostBlacklist = [
/google\.com$/,
/wikipedia\.org$/
];
/**
* Regular expression patterns of hosts that should always be proxied, regardless of whether
* they're on the blacklist
* @const
*/
const hostWhitelist = [
/^scholar\.google\.com$/
]
for each(var blackPattern in hostBlacklist) {
if(blackPattern.test(host)) {
for each(var whitePattern in hostWhitelist) {
if(whitePattern.test(host)) {
return false;
}
}
return true;
}
}
return false;
}
/**
* If transparent is enabled, shows a dialog asking user whether to add a proxy to the
* transparent proxy list.
*
* @param {String} proxiedHost The host that would be redirected through the proxy.
* @param {String} proxyHost The host through which the given site would be redirected.
* @returns {Boolean} True if proxy should be added; false if it should not be.
*/
function _showDialog(proxiedHost, proxyHost) {
if(!Zotero.Proxies.transparent) return false;
// ask user whether to add this proxy
var io = {site:proxiedHost, proxy:proxyHost};
var window = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator)
.getMostRecentWindow("navigator:browser");
window.openDialog('chrome://zotero/content/proxy.xul', '', 'chrome,modal', io);
// disable transparent if checkbox checked
if(io.disable) {
Zotero.Proxies.transparent = false;
Zotero.Prefs.set("proxies.transparent", false);
}
return io.add;
}
}
/**
* Creates a Zotero.Proxy object from a DB row
*
* @constructor
* @class Represents an individual proxy server
*/
Zotero.Proxy = function(row) {
if(row) {
this._loadFromRow(row);
} else {
this.hosts = [];
this.multiHost = false;
}
}
/**
* 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/
};
const Zotero_Proxy_metaRegexp = /[-[\]{}()*+?.\\^$|,#\s]/g;
/**
* 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
var re = "^"+this.scheme.replace(Zotero_Proxy_metaRegexp, "\\$&")+"$";
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.multiSite && (!this.hosts.length || !this.hosts[0])) {
return ["host.invalid"];
} else if(this.multiSite && !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 each(var host in 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 = function(transparent) {
// ensure this proxy is valid
Zotero.debug(this);
var hasErrors = this.validate();
if(hasErrors) throw "Zotero.Proxy: could not be saved because it is invalid: error "+hasErrors[0];
this.autoAssociate = this.multiHost && this.autoAssociate;
this.compileRegexp();
if(this.proxyID) {
Zotero.Proxies.refreshHostMap(this);
if(!transparent) throw "Zotero.Proxy: cannot save transparent proxy without transparent param";
} else {
Zotero.Proxies.save(this);
}
if(transparent) {
try {
Zotero.DB.beginTransaction();
if(this.proxyID) {
Zotero.DB.query("UPDATE proxies SET multiHost = ?, autoAssociate = ?, scheme = ? WHERE proxyID = ?",
[this.multiHost ? 1 : 0, this.autoAssociate ? 1 : 0, this.scheme, this.proxyID]);
Zotero.DB.query("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]);
} else {
this.proxyID = Zotero.DB.query("INSERT INTO proxies (multiHost, autoAssociate, scheme) VALUES (?, ?, ?)",
[this.multiHost ? 1 : 0, this.autoAssociate ? 1 : 0, this.scheme])
}
this.hosts = this.hosts.sort();
var host;
for(var i in this.hosts) {
host = this.hosts[i] = this.hosts[i].toLowerCase();
Zotero.DB.query("INSERT INTO proxyHosts (proxyID, hostname) VALUES (?, ?)",
[this.proxyID, host]);
}
Zotero.DB.commitTransaction();
} catch(e) {
Zotero.DB.rollbackTransaction();
throw(e);
}
}
}
/**
* Reverts to the previously saved version of this proxy
*/
Zotero.Proxy.prototype.revert = function() {
if(!this.proxyID) throw "Cannot revert an unsaved proxy";
this._loadFromRow(Zotero.DB.rowQuery("SELECT * FROM proxies WHERE proxyID = ?", [this.proxyID]));
}
/**
* Deletes this proxy
*/
Zotero.Proxy.prototype.erase = function() {
Zotero.Proxies.remove(this);
if(this.proxyID) {
try {
Zotero.DB.beginTransaction();
Zotero.DB.query("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]);
Zotero.DB.query("DELETE FROM proxies WHERE proxyID = ?", [this.proxyID]);
Zotero.DB.commitTransaction();
} catch(e) {
Zotero.DB.rollbackTransaction();
throw(e);
}
}
}
/**
* Converts a proxied URL to an unproxied URL using this proxy
*
* @param m {Array} The match from running this proxy's regexp against a URL spec
* @type String
*/
Zotero.Proxy.prototype.toProper = function(m) {
if(this.multiHost) {
var properURL = "http://"+m[this.parameters.indexOf("%h")+1]+"/";
} else {
var properURL = "http://"+this.hosts[0]+"/";
}
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 {nsIURI} uri The nsIURI corresponding to the unproxied URL
* @type String
*/
Zotero.Proxy.prototype.toProxy = function(uri) {
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 = uri.hostPort;
} else if(param == "%p") {
value = uri.path.substr(1);
} else if(param == "%d") {
value = uri.path.substr(0, uri.path.lastIndexOf("/"));
} else if(param == "%f") {
value = uri.path.substr(uri.path.lastIndexOf("/")+1)
}
proxyURL = proxyURL.substr(0, this.indices[param])+value+proxyURL.substr(this.indices[param]+2);
}
return proxyURL;
}
/**
* Loads a proxy object from a DB row
* @private
*/
Zotero.Proxy.prototype._loadFromRow = function(row) {
this.proxyID = row.proxyID;
this.multiHost = !!row.multiHost;
this.autoAssociate = !!row.autoAssociate;
this.scheme = row.scheme;
this.hosts = Zotero.DB.columnQuery("SELECT hostname FROM proxyHosts WHERE proxyID = ? ORDER BY hostname", row.proxyID);
this.compileRegexp();
}
/**
* Detectors for various proxy systems
* @namespace
*/
Zotero.Proxies.Detectors = new Object();
/**
* Detector for OCLC EZProxy
* @param {nsIChannel} channel
* @type Boolean|Zotero.Proxy
*/
Zotero.Proxies.Detectors.EZProxy = function(channel) {
const ezProxyRe = /\?(?:.+&)?(url|qurl)=([^&]+)/i;
// Try to catch links from one proxy-by-port site to another
if([80, 443, -1].indexOf(channel.URI.port) == -1) {
// Two options here: we could have a redirect from an EZProxy site to another, or a link
// If it's a redirect, we'll have to catch the Location: header
var toProxy = false;
var fromProxy = false;
if([301, 302, 303].indexOf(channel.responseStatus) !== -1) {
try {
toProxy = Zotero.Proxies.Detectors.EZProxy.ios.newURI(
channel.getResponseHeader("Location"), null, null);
fromProxy = channel.URI;
} catch(e) {}
} else {
toProxy = channel.URI;
fromProxy = channel.referrer;
}
if(fromProxy && toProxy && fromProxy.host == toProxy.host && fromProxy.port != toProxy.port
&& [80, 443, -1].indexOf(toProxy.port) == -1) {
var proxy;
for each(proxy in Zotero.Proxies.proxies) {
if(proxy.regexp) {
var m = proxy.regexp.exec(fromProxy.spec);
if(m) break;
}
}
if(m) {
// Make sure caught proxy is not multi-host and that we don't have this new proxy already
if(proxy.multiHost || Zotero.Proxies.proxyToProper(toProxy.spec, true)) return false;
// Create a new nsIObserver and nsIChannel to figure out real URL (by failing to
// send cookies, so we get back to the login page)
var newChannel = Zotero.Proxies.Detectors.EZProxy.ios.newChannelFromURI(toProxy);
newChannel.originalURI = channel.originalURI ? channel.originalURI : channel.URI;
newChannel.QueryInterface(Components.interfaces.nsIRequest).loadFlags = newChannel.loadFlags |
Components.interfaces.nsIHttpChannel.LOAD_DOCUMENT_URI;
Zotero.Proxies.Detectors.EZProxy.obs.addObserver(
new Zotero.Proxies.Detectors.EZProxy.Observer(newChannel),
"http-on-modify-request", false);
newChannel.asyncOpen(new Zotero.Proxies.Detectors.EZProxy.DummyStreamListener(), null);
return false;
}
}
}
// Now try to catch redirects
try {
if(channel.getResponseHeader("Server") != "EZproxy") return false;
} catch(e) {
return false
}
// Get the new URL
if(channel.responseStatus != 302) return false;
var proxiedURL = channel.getResponseHeader("Location");
if(!proxiedURL) return false;
var proxiedURI = Zotero.Proxies.Detectors.EZProxy.ios.newURI(proxiedURL, null, null);
// look for query
var m = ezProxyRe.exec(channel.URI.spec);
if(!m) return false;
// Ignore if we already know about it
if(Zotero.Proxies.proxyToProper(proxiedURI.spec, true)) return false;
// Found URL
var properURL = (m[1].toLowerCase() == "qurl" ? unescape(m[2]) : m[2]);
var properURI = Zotero.Proxies.Detectors.EZProxy.ios.newURI(properURL, null, null);
if(channel.URI.host == proxiedURI.host && [channel.URI.port, 80, 443, -1].indexOf(proxiedURI.port) == -1) {
// Proxy by port
var proxy = new Zotero.Proxy();
proxy.multiHost = false;
proxy.scheme = proxiedURI.scheme+"://"+proxiedURI.hostPort+"/%p";
proxy.hosts = [properURI.hostPort];
} else if(proxiedURI.host != channel.URI.host && proxiedURI.hostPort.indexOf(properURI.host) != -1) {
// Proxy by host
var proxy = new Zotero.Proxy();
proxy.multiHost = proxy.autoAssociate = true;
proxy.scheme = proxiedURI.scheme+"://"+proxiedURI.hostPort.replace(properURI.host, "%h")+"/%p";
proxy.hosts = [properURI.hostPort];
}
return proxy;
}
Zotero.Proxies.Detectors.EZProxy.ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
Zotero.Proxies.Detectors.EZProxy.obs = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
/**
* @class Do-nothing stream listener
* @private
*/
Zotero.Proxies.Detectors.EZProxy.DummyStreamListener = function() {}
Zotero.Proxies.Detectors.EZProxy.DummyStreamListener.prototype.onDataAvailable = function(request,
context, inputStream, offset, count) {}
Zotero.Proxies.Detectors.EZProxy.DummyStreamListener.prototype.onStartRequest = function(request, context) {}
Zotero.Proxies.Detectors.EZProxy.DummyStreamListener.prototype.onStopRequest = function(request, context, status) {}
/**
* @class Observer to clear cookies on an HTTP request, then remove itself
* @private
*/
Zotero.Proxies.Detectors.EZProxy.Observer = function(newChannel) {
this.channel = newChannel;
}
Zotero.Proxies.Detectors.EZProxy.Observer.prototype.observe = function(aSubject, aTopic, aData) {
if (aSubject == this.channel) {
aSubject.QueryInterface(Components.interfaces.nsIHttpChannel).setRequestHeader("Cookie", "", false);
Zotero.Proxies.Detectors.EZProxy.obs.removeObserver(this, "http-on-modify-request");
}
}
Zotero.Proxies.Detectors.EZProxy.Observer.prototype.QueryInterface = function(aIID) {
if (aIID.equals(Components.interfaces.nsISupports) ||
aIID.equals(Components.interfaces.nsIObserver)) return this;
throw Components.results.NS_NOINTERFACE;
}
/**
* Detector for Juniper Networks WebVPN
* @param {nsIChannel} channel
* @type Boolean|Zotero.Proxy
*/
Zotero.Proxies.Detectors.Juniper = function(channel) {
const juniperRe = /^(https?:\/\/[^\/:]+(?:\:[0-9]+)?)\/(.*),DanaInfo=([^+,]*)([^+]*)(?:\+(.*))?$/;
try {
var url = channel.URI.spec;
var m = juniperRe.exec(url);
} catch(e) {
return false;
}
if(!m) return false;
var proxy = new Zotero.Proxy();
proxy.multiHost = true;
proxy.autoAssociate = false;
proxy.scheme = m[1]+"/%d"+",DanaInfo=%h%a+%f";
proxy.hosts = [m[3]];
return proxy;
}