diff --git a/chrome/content/zotero/xpcom/proxy.js b/chrome/content/zotero/xpcom/proxy.js index 294d51001..cff8a401d 100644 --- a/chrome/content/zotero/xpcom/proxy.js +++ b/chrome/content/zotero/xpcom/proxy.js @@ -41,9 +41,6 @@ Zotero.Proxies = new function() { */ this.init = Zotero.Promise.coroutine(function* () { if(!this.proxies) { - var me = this; - Zotero.MIMETypeHandler.addObserver(function(ch) { me.observe(ch) }); - var rows = yield Zotero.DB.queryAsync("SELECT * FROM proxies"); Zotero.Proxies.proxies = yield Zotero.Promise.all( rows.map(row => this.newProxyFromRow(row)) @@ -81,201 +78,6 @@ Zotero.Proxies = new function() { }); - /** - * Observe method to capture page loads and determine if they're going through an EZProxy. - * - * @param {nsIChannel} channel - */ - this.observe = Zotero.Promise.coroutine(function* (channel) { - // try to detect a proxy - channel.QueryInterface(Components.interfaces.nsIHttpChannel); - var url = channel.URI.spec; - - try { - var { browser, window } = _getBrowserAndWindow(channel); - } - catch (e) { - Zotero.logError(e); - } - if (!browser) { - Zotero.debug("Couldn't get browser from channel", 2); - } - - // see if there is a proxy we already know - var m = false; - var proxy; - for (proxy of 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 // if autoAssociate is on - && channel.responseStatus < 400 // and query was successful - && !Zotero.Proxies.hosts[host] // and host is not saved - && proxy.hosts.indexOf(host) === -1 - && !_isBlacklisted(host) // and host is not blacklisted - ) { - proxy.hosts.push(host); - yield proxy.save(true); - - if (!browser) return; - _showNotification( - browser, - window, - Zotero.getString('proxies.notification.associated.label', [host, channel.URI.hostPort]), - [ - { - label: "proxies.notification.settings.button", - callback: () => _prefsOpenCallback(window) - } - ] - ); - } - } else { - if (!browser) return; - - // otherwise, try to detect a proxy - var proxy = false; - for(var detectorName in Zotero.Proxies.Detectors) { - var detector = Zotero.Proxies.Detectors[detectorName]; - try { - proxy = detector(channel); - } catch(e) { - Zotero.logError(e); - } - - if(!proxy) continue; - Zotero.debug("Proxies: Detected "+detectorName+" proxy "+proxy.scheme+ - (proxy.multiHost ? " (multi-host)" : " for "+proxy.hosts[0])); - - var savedTransparent = false; - if(Zotero.Proxies.autoRecognize) { - // Ask to save only if automatic proxy recognition is on - savedTransparent = _showNotification( - browser, - window, - Zotero.getString('proxies.notification.recognized.label', [proxy.hosts[0], channel.URI.hostPort]), - [{ label: "proxies.notification.enable.button", callback: function() { _showDialog(proxy.hosts[0], channel.URI.hostPort, proxy); } }]); - } - - yield proxy.save(); - - break; - } - } - - // try to get an applicable proxy - var docShell = browser.docShell; - if (!docShell) { - Zotero.logError("Couldn't get docshell"); - return; - } - - if (!docShell || !docShell.allowMetaRedirects) return; - - // check that proxy redirection is actually enabled - if(!Zotero.Proxies.transparent) return; - - var proxied = Zotero.Proxies.properToProxy(url, true); - if(!proxied) return; - - if(Zotero.Proxies.disableByDomain) { - var now = new Date(); - - // IP update interval is every 15 minutes - if((now - Zotero.Proxies.lastIPCheck) > 900000) { - Zotero.Proxies.DNS.getHostnames().then(function (hosts) { - // if domains necessitate disabling, disable them - Zotero.Proxies.disabledByDomain = false; - for (var host of hosts) { - Zotero.Proxies.disabledByDomain = host.toLowerCase().indexOf(Zotero.Proxies.disableByDomain) != -1; - if (Zotero.Proxies.disabledByDomain) return; - } - _maybeRedirect(channel, browser, window, proxied); - }, function(e) { - _maybeRedirect(channel, browser, window, proxied); - }); - Zotero.Proxies.lastIPCheck = now; - return; - } - - if(Zotero.Proxies.disabledByDomain) return; - } - - _maybeRedirect(channel, browser, window, proxied); - }); - - function _maybeRedirect(channel, browser, window, proxied) { - channel.QueryInterface(Components.interfaces.nsIHttpChannel); - var proxiedURI = Services.io.newURI(proxied, null, null); - if(channel.referrer) { - // 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("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("Proxies: skipping redirect; redirect URI and referrer have same host"); - return; - } - } - - if(channel.originalURI) { - // 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("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("Proxies: skipping redirect; redirect URI and original URI have same host"); - return; - } - } - - // make sure that the top two domains (e.g. gmu.edu in foo.bar.gmu.edu) of the - // channel and the site to which we're redirecting don't match, to prevent loops. - const top2DomainsRe = /[^\.]+\.[^\.]+$/; - top21 = top2DomainsRe.exec(channel.URI.host); - top22 = top2DomainsRe.exec(proxiedURI.host); - if(!top21 || !top22 || top21[0] == top22[0]) { - Zotero.debug("Proxies: skipping redirect; redirect URI and URI have same top 2 domains"); - return; - } - - // Otherwise, redirect. Note that we save the URI we're redirecting from as the - // referrer, since we can't make a proper redirect - if(Zotero.Proxies.showRedirectNotification) { - _showNotification( - browser, - window, - Zotero.getString('proxies.notification.redirected.label', [channel.URI.hostPort, proxiedURI.hostPort]), - [ - { - label: "general.dontShowAgain", - callback: () => _disableRedirectNotification() - }, - { - label: "proxies.notification.settings.button", - callback: () => _prefsOpenCallback(window) - } - ] - ); - } - - browser.loadURIWithFlags(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 @@ -425,145 +227,6 @@ Zotero.Proxies = new function() { } return urlToProxy; }; - - /** - * 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 = [ - /edu$/, - /google\.com$/, - /wikipedia\.org$/, - /^[^.]*$/, - /doubleclick\.net$/ - ]; - /** - * Regular expression patterns of hosts that should always be proxied, regardless of whether - * they're on the blacklist - * @const - */ - const hostWhitelist = [ - /^scholar\.google\.com$/, - /^muse\.jhu\.edu$/ - ] - - for (let blackPattern of hostBlacklist) { - if(blackPattern.test(host)) { - for (let whitePattern of 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. - */ - var _showDialog = Zotero.Promise.coroutine(function* (proxiedHost, proxyHost, proxy) { - // 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.autoRecognize = false; - Zotero.Prefs.set("proxies.autoRecognize", false); - } - - if(io.add) { - yield proxy.erase(); - yield proxy.save(true); - } - }); - - /** - * Get browser and window from a channel - * @return {Object} Object containing the content browser as 'browser' and a ChromeWindow as 'window' - */ - function _getBrowserAndWindow(channel) { - let outerWindowID = channel.loadInfo.outerWindowID; - var wm = Cc["@mozilla.org/appshell/window-mediator;1"] - .getService(Ci.nsIWindowMediator); - let outerContentWin = wm.getOuterWindowWithId(outerWindowID); - if (!outerContentWin) { - return { browser: null, window: null }; - } - var browser = outerContentWin.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell).chromeEventHandler; - return { - browser, - window: browser.ownerDocument.defaultView - }; - } - - /** - * Show a proxy-related notification - * @param {Browser} browser - * @param {Window} window - * @param {String} label - notification text - * @param {Object[]} buttons - Array of objects with 'label' (for getString()) and 'callback' - */ - function _showNotification(browser, window, label, buttons) { - // Get localized button labels - buttons = buttons.map(function(button) { - return { - label: Zotero.getString(button.label), - callback: button.callback - } - }); - - var listener = function() { - var nb = window.gBrowser.getNotificationBox(); - nb.appendNotification(label, - 'zotero-proxy', 'chrome://browser/skin/Info.png', nb.PRIORITY_WARNING_MEDIUM, - buttons); - browser.removeEventListener("pageshow", listener, false); - } - - browser.addEventListener("pageshow", listener, false); - } - - /** - * Disables proxy redirection notification - */ - function _disableRedirectNotification() { - Zotero.Proxies.showRedirectNotification = false; - Zotero.Prefs.set("proxies.showRedirectNotification",false); - } - - /** - * Opens preferences window - */ - function _prefsOpenCallback(window) { - window.openDialog('chrome://zotero/content/preferences/preferences.xul', - 'zotero-prefs', - 'chrome,titlebar,toolbar,' - + Zotero.Prefs.get('browser.preferences.instantApply', true) ? 'dialog=no' : 'modal', - {"pane":"zotero-prefpane-proxies"} - ); - } } /** @@ -864,190 +527,6 @@ Zotero.Proxy.prototype.loadHosts = Zotero.Promise.coroutine(function* () { ); }); -/** - * 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) { - // 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 = Services.io.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 (proxy of 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 = Services.io.newChannelFromURI(toProxy); - newChannel.originalURI = channel.originalURI ? channel.originalURI : channel.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); - - new Zotero.Proxies.Detectors.EZProxy.Observer(newChannel); - newChannel.asyncOpen(new Zotero.Proxies.Detectors.EZProxy.DummyStreamListener(), null); - return false; - } - } - } - - // 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 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 = /\?(?:.+&)?(url|qurl)=([^&]+)/i.exec(loginURI.path); - 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 = Services.io.newURI(properURL, null, null); - - var proxy = false; - 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 != loginURI.host && proxiedURI.hostPort.indexOf(properURI.host) != -1) { - // Proxy by host - 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; -} - -/** - * @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; - 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 = Zotero.Promise.coroutine(function* (aSubject, aTopic, aData) { - if (aSubject == this.channel) { - 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]); - yield 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) { - 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; -} - Zotero.Proxies.DNS = new function() { this.getHostnames = function() { if (!Zotero.isWin && !Zotero.isMac && !Zotero.isLinux) return Zotero.Promise.resolve([]);