Fix duplicate requests to provider sites

When the user follows a link from a proxy-by-port EZProxy to another
proxy-by-port EZProxy, we make an additional request to the target site
with no cookies. This yields a redirect to the EZProxy login page. We
use the query parameters to determine the true domain of the site.

Previously, we allowed the redirect to continue, and used the same code we
use to detect EZProxy logins to map the proxied domain to the true domain.
This caused duplicate requests to providers and apparently infinite loops.

We now read the query parameter directly from the redirect to the EZProxy
login page and then cancel the request so that the redirect doesn't get
followed.

The changeset also improves logging of proxy-related information.

See https://forums.zotero.org/discussion/28505/ for further discussion.
This commit is contained in:
Simon Kornblith 2013-03-25 21:31:23 -04:00
parent 253c22f58c
commit 107dc081b9

View file

@ -112,14 +112,17 @@ Zotero.Proxies = new function() {
} else {
// otherwise, try to detect a proxy
var proxy = false;
for each(var detector in Zotero.Proxies.Detectors) {
for(var detectorName in Zotero.Proxies.Detectors) {
var detector = Zotero.Proxies.Detectors[detectorName];
try {
proxy = detector(channel);
} catch(e) {
Components.utils.reportError(e);
Zotero.logError(e);
}
if(!proxy) continue;
Zotero.debug("Proxies: Detected "+detectorName+" proxy "+proxy.scheme+
(proxy.multiHost ? " (multi-host)" : " for "+proxy.hosts[0]));
var bw = _getBrowserAndWindow(channel.notificationCallbacks);
if(!bw) return;
@ -191,13 +194,13 @@ Zotero.Proxies = new function() {
// If the referrer is a proxiable host, we already have access (e.g., we're
// on-campus) and shouldn't redirect
if(Zotero.Proxies.properToProxy(channel.referrer.spec, true)) {
Zotero.debug("Zotero.Proxies: skipping redirect; referrer was proxiable");
Zotero.debug("Proxies: skipping redirect; referrer was proxiable");
return;
}
// If the referrer is the same host as we're about to redirect to, we shouldn't
// or we risk a loop
if(channel.referrer.host == proxiedURI.host) {
Zotero.debug("Zotero.Proxies: skipping redirect; redirect URI and referrer have same host");
Zotero.debug("Proxies: skipping redirect; redirect URI and referrer have same host");
return;
}
}
@ -206,13 +209,13 @@ Zotero.Proxies = new function() {
// If the original URI was a proxied host, we also shouldn't redirect, since any
// links handed out by the proxy should already be proxied
if(Zotero.Proxies.proxyToProper(channel.originalURI.spec, true)) {
Zotero.debug("Zotero.Proxies: skipping redirect; original URI was proxied");
Zotero.debug("Proxies: skipping redirect; original URI was proxied");
return;
}
// Finally, if the original URI is the same as the host we're about to redirect
// to, then we also risk a loop
if(channel.originalURI.host == proxiedURI.host) {
Zotero.debug("Zotero.Proxies: skipping redirect; redirect URI and original URI have same host");
Zotero.debug("Proxies: skipping redirect; redirect URI and original URI have same host");
return;
}
}
@ -223,7 +226,7 @@ Zotero.Proxies = new function() {
top21 = top2DomainsRe.exec(channel.URI.host);
top22 = top2DomainsRe.exec(proxiedURI.host);
if(!top21 || !top22 || top21[0] == top22[0]) {
Zotero.debug("Zotero.Proxies: skipping redirect; redirect URI and URI have same top 2 domains");
Zotero.debug("Proxies: skipping redirect; redirect URI and URI have same top 2 domains");
return;
}
@ -300,7 +303,7 @@ Zotero.Proxies = new function() {
var m = proxy.regexp.exec(url);
if(m) {
var toProper = proxy.toProper(m);
Zotero.debug("Zotero.Proxies.proxyToProper: "+url+" to "+toProper);
Zotero.debug("Proxies.proxyToProper: "+url+" to "+toProper);
return toProper;
}
}
@ -320,7 +323,7 @@ Zotero.Proxies = new function() {
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);
Zotero.debug("Proxies.properToProxy: "+url+" to "+toProxy);
return toProxy;
}
return (onlyReturnIfProxied ? false : url);
@ -536,9 +539,9 @@ Zotero.Proxy.prototype.validate = function() {
return ["scheme.noHTTP"];
}
if(!this.multiSite && (!this.hosts.length || !this.hosts[0])) {
if(!this.multiHost && (!this.hosts.length || !this.hosts[0])) {
return ["host.invalid"];
} else if(this.multiSite && !Zotero_Proxy_schemeParameterRegexps["%h"].test(this.scheme)) {
} else if(this.multiHost && !Zotero_Proxy_schemeParameterRegexps["%h"].test(this.scheme)) {
return ["scheme.noHost"];
}
@ -570,7 +573,7 @@ Zotero.Proxy.prototype.validate = function() {
Zotero.Proxy.prototype.save = function(transparent) {
// ensure this proxy is valid
var hasErrors = this.validate();
if(hasErrors) throw "Zotero.Proxy: could not be saved because it is invalid: error "+hasErrors[0];
if(hasErrors) throw "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;
@ -610,7 +613,7 @@ Zotero.Proxy.prototype.save = function(transparent) {
Zotero.Proxies.save(this);
} else {
Zotero.Proxies.refreshHostMap(this);
if(!transparent) throw "Zotero.Proxy: cannot save transparent proxy without transparent param";
if(!transparent) throw "Proxy: cannot save transparent proxy without transparent param";
}
}
@ -719,8 +722,6 @@ Zotero.Proxies.Detectors = new Object();
* @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
@ -729,8 +730,7 @@ Zotero.Proxies.Detectors.EZProxy = function(channel) {
var fromProxy = false;
if([301, 302, 303].indexOf(channel.responseStatus) !== -1) {
try {
toProxy = Zotero.Proxies.Detectors.EZProxy.ios.newURI(
channel.getResponseHeader("Location"), null, null);
toProxy = Services.io.newURI(channel.getResponseHeader("Location"), null, null);
fromProxy = channel.URI;
} catch(e) {}
} else {
@ -753,14 +753,12 @@ Zotero.Proxies.Detectors.EZProxy = function(channel) {
// 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);
var newChannel = Services.io.newChannelFromURI(toProxy);
newChannel.originalURI = channel.originalURI ? channel.originalURI : channel.URI;
newChannel.QueryInterface(Components.interfaces.nsIRequest).loadFlags = newChannel.loadFlags |
Components.interfaces.nsIHttpChannel.LOAD_DOCUMENT_URI;
newChannel.QueryInterface(Components.interfaces.nsIRequest).loadFlags = newChannel.loadFlags;
Zotero.debug("Proxies: Identified putative port-by-port EZProxy link from "+fromProxy.hostPort+" to "+toProxy.hostPort);
Zotero.Proxies.Detectors.EZProxy.obs.addObserver(
new Zotero.Proxies.Detectors.EZProxy.Observer(newChannel),
"http-on-modify-request", false);
new Zotero.Proxies.Detectors.EZProxy.Observer(newChannel);
newChannel.asyncOpen(new Zotero.Proxies.Detectors.EZProxy.DummyStreamListener(), null);
return false;
}
@ -768,19 +766,25 @@ Zotero.Proxies.Detectors.EZProxy = function(channel) {
}
// Now try to catch redirects
if(channel.responseStatus != 302) return false;
try {
if(channel.getResponseHeader("Server") != "EZproxy") return false;
var proxiedURI = Services.io.newURI(channel.getResponseHeader("Location"), null, null);
} catch(e) {
return false
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);
return Zotero.Proxies.Detectors.EZProxy.learn(channel.URI, proxiedURI);
}
/**
* Learn about a mapping from an EZProxy to a normal proxy
* @param {nsIURI} loginURI The URL of the login page
* @param {nsIURI} proxiedURI The URI of the page
* @return {Zotero.Proxy | false}
*/
Zotero.Proxies.Detectors.EZProxy.learn = function(loginURI, proxiedURI) {
// look for query
var m = ezProxyRe.exec(channel.URI.spec);
var m = /\?(?:.+&)?(url|qurl)=([^&]+)/i.exec(loginURI.path);
if(!m) return false;
// Ignore if we already know about it
@ -788,16 +792,16 @@ Zotero.Proxies.Detectors.EZProxy = function(channel) {
// Found URL
var properURL = (m[1].toLowerCase() == "qurl" ? unescape(m[2]) : m[2]);
var properURI = Zotero.Proxies.Detectors.EZProxy.ios.newURI(properURL, null, null);
var properURI = Services.io.newURI(properURL, null, null);
var proxy = false;
if(channel.URI.host == proxiedURI.host && [channel.URI.port, 80, 443, -1].indexOf(proxiedURI.port) == -1) {
if(loginURI.host == proxiedURI.host && [loginURI.port, 80, 443, -1].indexOf(proxiedURI.port) == -1) {
// Proxy by port
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) {
} else if(proxiedURI.host != loginURI.host && proxiedURI.hostPort.indexOf(properURI.host) != -1) {
// Proxy by host
proxy = new Zotero.Proxy();
proxy.multiHost = proxy.autoAssociate = true;
@ -806,10 +810,6 @@ Zotero.Proxies.Detectors.EZProxy = function(channel) {
}
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
@ -827,11 +827,42 @@ Zotero.Proxies.Detectors.EZProxy.DummyStreamListener.prototype.onStopRequest = f
*/
Zotero.Proxies.Detectors.EZProxy.Observer = function(newChannel) {
this.channel = newChannel;
Services.obs.addObserver(this, "http-on-modify-request", false);
Services.obs.addObserver(this, "http-on-examine-response", false);
}
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");
if(aTopic === "http-on-modify-request") {
try {
aSubject.QueryInterface(Components.interfaces.nsIHttpChannel).setRequestHeader("Cookie", "", false);
} catch(e) {
Zotero.logError(e);
} finally {
Services.obs.removeObserver(this, "http-on-modify-request");
}
} else if(aTopic === "http-on-examine-response") {
try {
// Make sure this is a redirect involving an EZProxy
if(aSubject.responseStatus !== 302) return;
try {
if(aSubject.getResponseHeader("Server") !== "EZproxy") return;
var loginURL = aSubject.getResponseHeader("Location");
} catch(e) {
return;
}
var proxy = Zotero.Proxies.Detectors.EZProxy.learn(Services.io.newURI(loginURL, null, null), aSubject.URI);
if(proxy) {
Zotero.debug("Proxies: Proxy-by-port EZProxy "+aSubject.URI.hostPort+" corresponds to "+proxy.hosts[0]);
proxy.save();
}
} catch(e) {
Zotero.logError(e);
} finally {
Services.obs.removeObserver(this, "http-on-examine-response");
aSubject.cancel(0x80004004 /*NS_ERROR_ABORT*/);
}
}
}
}
Zotero.Proxies.Detectors.EZProxy.Observer.prototype.QueryInterface = function(aIID) {