diff --git a/apps/jetty/java/src/net/i2p/jetty/JettyStart.java b/apps/jetty/java/src/net/i2p/jetty/JettyStart.java index 2c77a2ef885ad61cb39e15b078447f74a4a5ea89..13858c3a6c2d43dc428712d96d7db9b0910898c9 100644 --- a/apps/jetty/java/src/net/i2p/jetty/JettyStart.java +++ b/apps/jetty/java/src/net/i2p/jetty/JettyStart.java @@ -65,7 +65,6 @@ public class JettyStart implements ClientApp { private static final String GZIP_DIR = "eepsite-jetty9.3"; private static final String GZIP_CONFIG = "jetty-gzip.xml"; private static final String MIN_GZIP_HANDLER_VER = "9.3"; - /** * All args must be XML file names. * Does not support any of the other argument types from org.mortbay.start.Main. @@ -179,6 +178,10 @@ public class JettyStart implements ClientApp { public void run() { for (LifeCycle lc : _jettys) { if (!lc.isRunning()) { + if (lc instanceof Server) { + Server server = (Server) lc; + server.insertHandler(new XI2PLocationFilter()); + } try { lc.start(); if (_context != null) { diff --git a/apps/jetty/java/src/net/i2p/servlet/filters/XI2PLocationFilter.java b/apps/jetty/java/src/net/i2p/servlet/filters/XI2PLocationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..05441e28d69b73fc7239075b973894b558aeb40d --- /dev/null +++ b/apps/jetty/java/src/net/i2p/servlet/filters/XI2PLocationFilter.java @@ -0,0 +1,188 @@ +package net.i2p.jetty; + +import java.io.IOException; +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.HandlerWrapper; + +import net.i2p.I2PAppContext; +import net.i2p.I2PException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKeyFile; + + +import net.i2p.util.Log; + +/** + * Adds a header, X-I2P-Location, to requests when they do **not** come in on an I2P hostname. + * This header contains a URL that looks like: [scheme://][i2phostname.i2p][/path][?query] + * and expresses the I2P-Equivalent URL of the clearnet query. Clients can use this to prompt + * users to switch from a non-I2P host to an I2P host or to redirect them automatically. It + * automatically enabled on the default I2P site located on port 7658 by default. + * + * @since 0.9.51 + */ +public class XI2PLocationFilter extends HandlerWrapper { + private String X_I2P_Location = null; + private long lastFailure = -1; + private static final long failTimeout = 600000; + private static final String encodeUTF = StandardCharsets.UTF_8.toString(); + private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(XI2PLocationFilter.class); + + + private synchronized void setLocation(String xi2plocation) { + if (_log.shouldInfo()) + _log.info("Checking X-I2P-Location header prefix" + xi2plocation); + if (X_I2P_Location != null) + return ; + if (xi2plocation == null) + return ; + if (xi2plocation.equals("")) + return ; + X_I2P_Location = xi2plocation; + if (_log.shouldInfo()) + _log.info("Caching X-I2P-Location header prefix" + X_I2P_Location); + } + + private synchronized boolean shouldRecheck(){ + boolean settable = (X_I2P_Location == null); + if (!settable) return settable; + if (lastFailure == -1) { + lastFailure = System.currentTimeMillis(); + if (_log.shouldDebug()) + _log.debug("New instance, attempting to set X-I2P-Location header for the first time"); + return settable; + } + if ((System.currentTimeMillis() - lastFailure) > failTimeout){ + lastFailure = System.currentTimeMillis(); + if (_log.shouldDebug()) + _log.debug("More than ten minutes since failing attempt to re-check X-I2P-Location header"); + return settable; + } + if (_log.shouldDebug()) + _log.debug("Not attempting to re-check X-I2P-Location header"); + return false; + } + + private synchronized String getXI2PLocation(String host, String port) { + File configDir = I2PAppContext.getGlobalContext().getConfigDir(); + File tunnelConfig = new File(configDir, "i2ptunnel.config"); + boolean isSingleFile = tunnelConfig.exists(); + if (!isSingleFile) { + File tunnelConfigD = new File(configDir, "i2ptunnel.config.d"); + File[] configFiles = tunnelConfigD.listFiles(new net.i2p.util.FileSuffixFilter(".config")); + if (configFiles == null) + return null; + for (int fnum=0; fnum < configFiles.length; fnum++) { + Properties tunnelProps = new Properties(); + try { + DataHelper.loadProps(tunnelProps, configFiles[fnum]); + String targetHost = tunnelProps.getProperty("targetHost"); + boolean hostmatch = (host.equals(targetHost) || "0.0.0.0".equals(targetHost) || "::".equals(targetHost)); + if ( hostmatch && port.equals(tunnelProps.getProperty("targetPort")) ) { + String sh = tunnelProps.getProperty("spoofedHost"); + if (sh != null) { + if (sh.endsWith(".i2p")) + return sh; + } + String kf = tunnelProps.getProperty("privKeyFile"); + if (kf != null) { + File keyFile = new File(kf); + if (!keyFile.isAbsolute()) + keyFile = new File(configDir, kf); + if (keyFile.exists()) { + PrivateKeyFile pkf = new PrivateKeyFile(keyFile); + try { + Destination rv = pkf.getDestination(); + if (rv != null) + return rv.toBase32(); + } catch (I2PException e) { + if (_log.shouldWarn()) + _log.warn("I2PException Unable to set X-I2P-Location, keys arent ready. This is probably safe to ignore and will go away after the first run." + e); + return null; + } catch (IOException e) { + if (_log.shouldWarn()) + _log.warn("IOE Unable to set X-I2P-Location, location is uninitialized due file not found. This probably means the keys aren't ready. This is probably safe to ignore." + e); + return null; + } + } + } + if (_log.shouldWarn()) + _log.warn("Unable to set X-I2P-Location, location is target not found in any I2PTunnel config file. This should never happen."); + return null; + } + } catch (IOException ioe) { + if (_log.shouldWarn()) + _log.warn("IOE Unable to set X-I2P-Location, location is uninitialized. This is probably safe to ignore. location='" + ioe + "'"); + return null; + } + } + } else { + // don't bother + } + return null; + } + + private synchronized String headerContents(final HttpServletRequest httpRequest) { + if (X_I2P_Location != null) { + String scheme = httpRequest.getScheme(); + if (scheme == null) + scheme = ""; + String path = httpRequest.getPathInfo(); + if (path == null) + path = ""; + String query = httpRequest.getQueryString(); + if (query == null) + query = ""; + try { + if (query.equals("")) { + URI uri = new URI(scheme, X_I2P_Location, path, null); + String encodedURL = uri.toASCIIString(); + return encodedURL; + } else { + URI uri = new URI(scheme, X_I2P_Location, path, query, null); + String encodedURL = uri.toASCIIString(); + return encodedURL; + } + }catch(URISyntaxException use){ + return null; + } + } + return null; + } + + @Override + public void handle(final String target, final Request request, final HttpServletRequest httpRequest, HttpServletResponse httpResponse) + throws IOException, ServletException { + final String hashHeader = httpRequest.getHeader("X-I2P-DestHash"); + + if (hashHeader == null) { + if (shouldRecheck()) { + String xi2plocation = getXI2PLocation(request.getLocalAddr(), String.valueOf(request.getLocalPort())); + if (_log.shouldInfo()) + _log.info("Checking X-I2P-Location header IP " + request.getLocalAddr() + " port " + request.getLocalPort() + " prefix " + xi2plocation); + setLocation(xi2plocation); + } + String headerURL = headerContents(httpRequest); + if (headerURL != null) { + if (_log.shouldInfo()) + _log.info("Checking X-I2P-Location header" + headerURL); + httpResponse.addHeader("X-I2P-Location", headerURL); + } + } + + _handler.handle(target, request, httpRequest, httpResponse); + } +}