diff --git a/apps/i2psnark/i2psnark-appctx.config b/apps/i2psnark/i2psnark-appctx.config new file mode 100644 index 0000000000..ce716500a8 --- /dev/null +++ b/apps/i2psnark/i2psnark-appctx.config @@ -0,0 +1,4 @@ +# +# This is for app context configuration of standalone i2psnark. +# Almost all configuration settings are in i2psnark.config.d/i2psnark.config +# diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml index 94ef57c1da..fe84bebfdf 100644 --- a/apps/i2psnark/java/build.xml +++ b/apps/i2psnark/java/build.xml @@ -50,10 +50,11 @@ - + - + + @@ -319,6 +320,7 @@ + diff --git a/apps/i2psnark/java/src/org/klomp/snark/standalone/HostCheckHandler.java b/apps/i2psnark/java/src/org/klomp/snark/standalone/HostCheckHandler.java new file mode 100644 index 0000000000..ebf39ebecb --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/standalone/HostCheckHandler.java @@ -0,0 +1,136 @@ +package org.klomp.snark.standalone; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.StringTokenizer; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.i2p.I2PAppContext; +import net.i2p.util.Log; + +import org.apache.http.conn.util.InetAddressUtils; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +/** + * Block certain Host headers to prevent DNS rebinding attacks. + * + * Unlike in the console, this is an AbstractHandler, not a HandlerWrapper. + * + * @since 0.9.34 adapted from router console + */ +public class HostCheckHandler extends AbstractHandler { + private final I2PAppContext _context; + private final Set _listenHosts; + private static final String PROP_ALLOWED_HOSTS = "i2psnark.allowedHosts"; + + public HostCheckHandler() { + this(I2PAppContext.getGlobalContext()); + } + + public HostCheckHandler(I2PAppContext ctx) { + super(); + _context = ctx; + _listenHosts = new HashSet(8); + _listenHosts.add("127.0.0.1"); + _listenHosts.add("::1"); + _listenHosts.add("localhost"); + String allowed = _context.getProperty(PROP_ALLOWED_HOSTS); + if (allowed != null) { + StringTokenizer tok = new StringTokenizer(allowed, " ,"); + while (tok.hasMoreTokens()) { + _listenHosts.add(tok.nextToken()); + } + } + } + + /** + * Unused, we can't get here from RunStandalone + * + * @param hosts contains hostnames or IPs. But we allow all IPs anyway. + */ + public void setListenHosts(Set hosts) { + _listenHosts.clear(); + _listenHosts.addAll(hosts); + } + + /** + * Block by Host header, + * redirect HTTP to HTTPS, + * pass everything else to the delegate. + */ + public void handle(String pathInContext, + Request baseRequest, + HttpServletRequest httpRequest, + HttpServletResponse httpResponse) + throws IOException, ServletException + { + + String host = httpRequest.getHeader("Host"); + if (!allowHost(host)) { + Log log = _context.logManager().getLog(HostCheckHandler.class); + host = getHost(host); + String s = "Console request denied.\n" + + " To allow access using the hostname \"" + host + "\", add the line \"" + + PROP_ALLOWED_HOSTS + '=' + host + + "\" in the file " + RunStandalone.APP_CONFIG_FILE.getAbsolutePath() + " and restart."; + log.logAlways(Log.WARN, s); + httpResponse.sendError(403, s); + baseRequest.setHandled(true); + return; + } + } + + /** + * Should we allow a request with this Host header? + * + * ref: https://en.wikipedia.org/wiki/DNS_rebinding + * + * @param host the HTTP Host header, null ok + * @return true if OK + */ + private boolean allowHost(String host) { + if (host == null) + return true; + // common cases + if (host.equals("127.0.0.1:8002") || + host.equals("localhost:8002") || + host.equals("[::1]:8002")) + return true; + // all allowed? + if (_listenHosts.isEmpty()) + return true; + host = getHost(host); + if (_listenHosts.contains(host)) + return true; + // allow all IP addresses + if (InetAddressUtils.isIPv4Address(host) || InetAddressUtils.isIPv6Address(host)) + return true; + //System.out.println(host + " not found in " + s); + return false; + } + + /** + * Strip [] and port from a host header + * + * @param host the HTTP Host header non-null + */ + private static String getHost(String host) { + if (host.startsWith("[")) { + host = host.substring(1); + int brack = host.indexOf(']'); + if (brack >= 0) + host = host.substring(0, brack); + } else { + int colon = host.indexOf(':'); + if (colon >= 0) + host = host.substring(0, colon); + } + return host; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java b/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java index 2a954020a1..b5800a516b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java +++ b/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java @@ -1,9 +1,12 @@ package org.klomp.snark.standalone; import java.io.File; +import java.io.IOException; +import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.apps.systray.UrlLauncher; +import net.i2p.data.DataHelper; import net.i2p.jetty.JettyStart; /** @@ -16,9 +19,16 @@ public class RunStandalone { private int _port = 8002; private String _host = "127.0.0.1"; private static RunStandalone _instance; + static final File APP_CONFIG_FILE = new File("i2psnark-appctx.config"); private RunStandalone(String args[]) throws Exception { - _context = I2PAppContext.getGlobalContext(); + Properties p = new Properties(); + if (APP_CONFIG_FILE.exists()) { + try { + DataHelper.loadProps(p, APP_CONFIG_FILE); + } catch (IOException ioe) {} + } + _context = new I2PAppContext(p); File base = _context.getBaseDir(); File xml = new File(base, "jetty-i2psnark.xml"); _jettyStart = new JettyStart(_context, null, new String[] { xml.getAbsolutePath() } ); diff --git a/apps/i2psnark/java/src/org/klomp/snark/standalone/package.html b/apps/i2psnark/java/src/org/klomp/snark/standalone/package.html new file mode 100644 index 0000000000..096772850d --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/standalone/package.html @@ -0,0 +1,7 @@ + + +

+Classes only used for, and bundled with, the standalone installation. Since 0.9.27. +

+ + diff --git a/apps/i2psnark/jetty-i2psnark.xml b/apps/i2psnark/jetty-i2psnark.xml index 4a525ce8f6..f3562daa21 100644 --- a/apps/i2psnark/jetty-i2psnark.xml +++ b/apps/i2psnark/jetty-i2psnark.xml @@ -55,6 +55,9 @@ + + + diff --git a/apps/jetty/java/src/net/i2p/servlet/filters/XSSFilter.java b/apps/jetty/java/src/net/i2p/servlet/filters/XSSFilter.java index 9fcce1aba0..b300a03a32 100644 --- a/apps/jetty/java/src/net/i2p/servlet/filters/XSSFilter.java +++ b/apps/jetty/java/src/net/i2p/servlet/filters/XSSFilter.java @@ -33,7 +33,11 @@ public class XSSFilter implements Filter { // We need to send the error quickly, if we just throw a ServletException, // the data keeps coming and the connection gets reset. // This way we at least get the error to the browser. - ((HttpServletResponse)response).sendError(413, ise.getMessage()); + try { + ((HttpServletResponse)response).sendError(413, ise.getMessage()); + } catch (IllegalStateException ise2) { + // Committed, probably wasn't a multipart form error after all + } } } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java index ff862d3bba..1805f9f7e5 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java @@ -78,6 +78,7 @@ public class HostCheckHandler extends HandlerWrapper "\" to advanced configuration and restart."; log.logAlways(Log.WARN, s); httpResponse.sendError(403, s); + baseRequest.setHandled(true); return; } @@ -92,6 +93,7 @@ public class HostCheckHandler extends HandlerWrapper if (Boolean.valueOf(redir) || (redir == null && "1".equals(httpRequest.getHeader("Upgrade-Insecure-Requests")))) { sendRedirect(httpsPort, httpRequest, httpResponse); + baseRequest.setHandled(true); return; } }