From 8f8022347dee80bdb096c813f79f6a9c2dd1ca4c Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 26 Oct 2013 14:21:26 +0000 Subject: [PATCH] * I2PTunnel HTTPServer: New POST limiter --- .../src/net/i2p/i2ptunnel/ConnThrottler.java | 203 ++++++++++++++++++ .../i2p/i2ptunnel/I2PTunnelHTTPServer.java | 103 ++++++++- .../src/net/i2p/i2ptunnel/web/EditBean.java | 25 +++ .../src/net/i2p/i2ptunnel/web/IndexBean.java | 47 +++- apps/i2ptunnel/jsp/editServer.jsp | 36 +++- history.txt | 4 + .../src/net/i2p/router/RouterVersion.java | 2 +- 7 files changed, 411 insertions(+), 9 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ConnThrottler.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ConnThrottler.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ConnThrottler.java new file mode 100644 index 0000000000..3853100556 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ConnThrottler.java @@ -0,0 +1,203 @@ +package net.i2p.i2ptunnel; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.SimpleTimer2; + +/** + * Count how often something happens with a particular peer and all peers. + * This offers basic DOS protection but is not a complete solution. + * + * This is a little different from the one in streaming, in that the + * ban time is different from the check time, and we keep a separate + * map of throttled peers with individual time stamps. + * The streaming version is lightweight but "sloppy" since it + * uses a single time bucket for all. + * + * @since 0.9.9 + */ +class ConnThrottler { + private int _max; + private int _totalMax; + private long _checkPeriod; + private long _throttlePeriod; + private long _totalThrottlePeriod; + private int _currentTotal; + private final Map _peers; + private long _totalThrottleUntil; + private final String _action; + private final Log _log; + private final DateFormat _fmt; + + /* + * @param max per-peer, 0 for unlimited + * @param totalMax for all peers, 0 for unlimited + * @param period check window (ms) + * @param throttlePeriod how long to ban a peer (ms) + * @param totalThrottlePeriod how long to ban all peers (ms) + * @param action just a name to note in the log + */ + public ConnThrottler(int max, int totalMax, long period, + long throttlePeriod, long totalThrottlePeriod, String action, Log log) { + updateLimits(max, totalMax, period, throttlePeriod, totalThrottlePeriod); + _peers = new HashMap(4); + _action = action; + _log = log; + // for logging + _fmt = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM); + String systemTimeZone = I2PAppContext.getGlobalContext().getProperty("i2p.systemTimeZone"); + if (systemTimeZone != null) + _fmt.setTimeZone(TimeZone.getTimeZone(systemTimeZone)); + new Cleaner(); + } + + /* + * All periods in ms + * @param max per-peer, 0 for unlimited + * @param totalMax for all peers, 0 for unlimited + * @since 0.9.3 + */ + public synchronized void updateLimits(int max, int totalMax, long checkPeriod, long throttlePeriod, long totalThrottlePeriod) { + _max = max; + _totalMax = totalMax; + _checkPeriod = Math.max(checkPeriod, 10*1000); + _throttlePeriod = Math.max(throttlePeriod, 10*1000); + _totalThrottlePeriod = Math.max(totalThrottlePeriod, 10*1000); + } + + /** + * Checks both individual and total. Increments before checking. + */ + public synchronized boolean shouldThrottle(Hash h) { + // all throttled already? + if (_totalMax > 0) { + if (_totalThrottleUntil > 0) { + if (_totalThrottleUntil > Clock.getInstance().now()) + return true; + _totalThrottleUntil = 0; + } + } + // do this first, so we don't increment total if individual throttled + if (_max > 0) { + Record rec = _peers.get(h); + if (rec != null) { + // peer throttled already? + if (rec.getUntil() > 0) + return true; + rec.increment(); + long now = Clock.getInstance().now(); + if (rec.countSince(now - _checkPeriod) > _max) { + long until = now + _throttlePeriod; + String date = _fmt.format(new Date(until)); + _log.logAlways(Log.WARN, "Throttling " + _action + " until " + date + + " after exceeding max of " + _max + + " in " + DataHelper.formatDuration(_checkPeriod) + + ": " + h.toBase64()); + rec.ban(until); + return true; + } + } else { + _peers.put(h, new Record()); + } + } + if (_totalMax > 0 && ++_currentTotal > _totalMax) { + if (_totalThrottleUntil == 0) { + _totalThrottleUntil = Clock.getInstance().now() + _totalThrottlePeriod; + String date = _fmt.format(new Date(_totalThrottleUntil)); + _log.logAlways(Log.WARN, "*** Throttling " + _action + " from ALL peers until " + date + + " after exceeding max of " + _max + + " in " + DataHelper.formatDuration(_checkPeriod)); + } + return true; + } + return false; + } + + /** + * start over + */ + public synchronized void clear() { + _currentTotal = 0; + _totalThrottleUntil = 0; + _peers.clear(); + } + + /** + * Keep a list of seen times, and a ban-until time. + * Caller must sync all methods. + */ + private static class Record { + private final List times; + private long until; + + public Record() { + times = new ArrayList(8); + increment(); + } + + /** Caller must synch */ + public int countSince(long time) { + for (Iterator iter = times.iterator(); iter.hasNext(); ) { + if (iter.next().longValue() < time) + iter.remove(); + else + break; + } + return times.size(); + } + + /** Caller must synch */ + public void increment() { + times.add(Long.valueOf(Clock.getInstance().now())); + } + + /** Caller must synch */ + public void ban(long untilTime) { + until = untilTime; + // don't need to save times if banned + times.clear(); + } + + /** Caller must synch */ + public long getUntil() { + if (until < Clock.getInstance().now()) + until = 0; + return until; + } + } + + private class Cleaner extends SimpleTimer2.TimedEvent { + /** schedules itself */ + public Cleaner() { + super(SimpleTimer2.getInstance(), _checkPeriod); + } + + public void timeReached() { + synchronized(ConnThrottler.this) { + if (_totalMax > 0) + _currentTotal = 0; + if (_max > 0 && !_peers.isEmpty()) { + long then = Clock.getInstance().now() - _checkPeriod; + for (Iterator iter = _peers.values().iterator(); iter.hasNext(); ) { + Record rec = iter.next(); + if (rec.getUntil() <= 0 && rec.countSince(then) <= 0) + iter.remove(); + } + } + } + schedule(_checkPeriod); + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index d58a3364f0..864f69181d 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -25,6 +25,7 @@ import net.i2p.client.streaming.I2PSocket; import net.i2p.I2PAppContext; import net.i2p.data.ByteArray; import net.i2p.data.DataHelper; +import net.i2p.data.Hash; import net.i2p.util.ByteCache; import net.i2p.util.EventDispatcher; import net.i2p.util.I2PAppThread; @@ -41,6 +42,18 @@ import net.i2p.data.Base32; */ public class I2PTunnelHTTPServer extends I2PTunnelServer { + /** all of these in SECONDS */ + public static final String OPT_POST_WINDOW = "postCheckTime"; + public static final String OPT_POST_BAN_TIME = "postBanTime"; + public static final String OPT_POST_TOTAL_BAN_TIME = "postTotalBanTime"; + public static final String OPT_POST_MAX = "maxPosts"; + public static final String OPT_POST_TOTAL_MAX = "maxTotalPosts"; + public static final int DEFAULT_POST_WINDOW = 5*60; + public static final int DEFAULT_POST_BAN_TIME = 30*60; + public static final int DEFAULT_POST_TOTAL_BAN_TIME = 10*60; + public static final int DEFAULT_POST_MAX = 3; + public static final int DEFAULT_POST_TOTAL_MAX = 10; + /** what Host: should we seem to be to the webserver? */ private String _spoofHost; private static final String HASH_HEADER = "X-I2P-DestHash"; @@ -53,6 +66,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { private static final long TOTAL_HEADER_TIMEOUT = 2 * HEADER_TIMEOUT; private static final long START_INTERVAL = (60 * 1000) * 3; private long _startedOn = 0L; + private ConnThrottler _postThrottler; private final static byte[] ERR_UNAVAILABLE = ("HTTP/1.1 503 Service Unavailable\r\n"+ @@ -67,6 +81,19 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { "") .getBytes(); + private final static byte[] ERR_DENIED = + ("HTTP/1.1 403 Denied\r\n"+ + "Content-Type: text/html; charset=iso-8859-1\r\n"+ + "Cache-control: no-cache\r\n"+ + "Connection: close\r\n"+ + "Proxy-Connection: close\r\n"+ + "\r\n"+ + "403 Denied\n"+ + "

403 Denied

\n" + + "

Denied due to excessive requests. Please try again later.

\n" + + "") + .getBytes(); + public I2PTunnelHTTPServer(InetAddress host, int port, String privData, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super(host, port, privData, l, notifyThis, tunnel); setupI2PTunnelHTTPServer(spoofHost); @@ -91,8 +118,57 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { @Override public void startRunning() { super.startRunning(); - _startedOn = getTunnel().getContext().clock().now(); // Would be better if this was set when the inbound tunnel becomes alive. + _startedOn = getTunnel().getContext().clock().now(); + setupPostThrottle(); + } + + /** @since 0.9.9 */ + private void setupPostThrottle() { + int pp = getIntOption(OPT_POST_MAX, 0); + int pt = getIntOption(OPT_POST_TOTAL_MAX, 0); + synchronized(this) { + if (pp != 0 || pp != 0 || _postThrottler != null) { + long pw = 1000L * getIntOption(OPT_POST_WINDOW, DEFAULT_POST_WINDOW); + long pb = 1000L * getIntOption(OPT_POST_BAN_TIME, DEFAULT_POST_BAN_TIME); + long px = 1000L * getIntOption(OPT_POST_TOTAL_BAN_TIME, DEFAULT_POST_TOTAL_BAN_TIME); + if (_postThrottler == null) + _postThrottler = new ConnThrottler(pp, pt, pw, pb, px, "POST", _log); + else + _postThrottler.updateLimits(pp, pt, pw, pb, px); + } + } + } + + /** @since 0.9.9 */ + private int getIntOption(String opt, int dflt) { + Properties opts = getTunnel().getClientOptions(); + String o = opts.getProperty(opt); + if (o != null) { + try { + return Integer.parseInt(o); + } catch (NumberFormatException nfe) {} + } + return dflt; + } + + /** @since 0.9.9 */ + @Override + public boolean close(boolean forced) { + synchronized(this) { + if (_postThrottler != null) + _postThrottler.clear(); + } + return super.close(forced); + } + + /** @since 0.9.9 */ + @Override + public void optionsUpdated(I2PTunnel tunnel) { + if (getTunnel() != tunnel) + return; + setupPostThrottle(); + super.optionsUpdated(tunnel); } @@ -102,9 +178,10 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { */ @Override protected void blockingHandle(I2PSocket socket) { + Hash peerHash = socket.getPeerDestination().calculateHash(); if (_log.shouldLog(Log.INFO)) _log.info("Incoming connection to '" + toString() + "' port " + socket.getLocalPort() + - " from: " + socket.getPeerDestination().calculateHash() + " port " + socket.getPort()); + " from: " + peerHash + " port " + socket.getPort()); //local is fast, so synchronously. Does not need that many //threads. try { @@ -119,9 +196,27 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { Map> headers = readHeaders(in, command, CLIENT_SKIPHEADERS, getTunnel().getContext()); long afterHeaders = getTunnel().getContext().clock().now(); + + if (_postThrottler != null && + command.length() >= 5 && + command.substring(0, 5).toUpperCase(Locale.US).equals("POST ")) { + if (_postThrottler.shouldThrottle(peerHash)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Refusing POST since peer is throttled: " + peerHash.toBase64()); + try { + // Send a 503, so the user doesn't get an HTTP Proxy error message + // and blame his router or the network. + socket.getOutputStream().write(ERR_DENIED); + } catch (IOException ioe) {} + try { + socket.close(); + } catch (IOException ioe) {} + return; + } + } - addEntry(headers, HASH_HEADER, socket.getPeerDestination().calculateHash().toBase64()); - addEntry(headers, DEST32_HEADER, Base32.encode(socket.getPeerDestination().calculateHash().getData()) + ".b32.i2p"); + addEntry(headers, HASH_HEADER, peerHash.toBase64()); + addEntry(headers, DEST32_HEADER, Base32.encode(peerHash.getData()) + ".b32.i2p"); addEntry(headers, DEST64_HEADER, socket.getPeerDestination().toBase64()); // Port-specific spoofhost diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java index e74912a326..6afdb83b3e 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -21,6 +21,7 @@ import net.i2p.data.Signature; import net.i2p.data.SigningPrivateKey; import net.i2p.i2ptunnel.I2PTunnelHTTPClient; import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase; +import net.i2p.i2ptunnel.I2PTunnelHTTPServer; import net.i2p.i2ptunnel.I2PTunnelIRCClient; import net.i2p.i2ptunnel.TunnelController; import net.i2p.i2ptunnel.TunnelControllerGroup; @@ -274,6 +275,30 @@ public class EditBean extends IndexBean { return getProperty(tunnel, PROP_MAX_STREAMS, "0"); } + /** + * POST limits + * @since 0.9.9 + */ + public String getPostMax(int tunnel) { + return getProperty(tunnel, I2PTunnelHTTPServer.OPT_POST_MAX, "0"); + } + + public String getPostTotalMax(int tunnel) { + return getProperty(tunnel, I2PTunnelHTTPServer.OPT_POST_TOTAL_MAX, "0"); + } + + public int getPostCheckTime(int tunnel) { + return getProperty(tunnel, I2PTunnelHTTPServer.OPT_POST_WINDOW, I2PTunnelHTTPServer.DEFAULT_POST_WINDOW) / 60; + } + + public int getPostBanTime(int tunnel) { + return getProperty(tunnel, I2PTunnelHTTPServer.OPT_POST_BAN_TIME, I2PTunnelHTTPServer.DEFAULT_POST_BAN_TIME) / 60; + } + + public int getPostTotalBanTime(int tunnel) { + return getProperty(tunnel, I2PTunnelHTTPServer.OPT_POST_TOTAL_BAN_TIME, I2PTunnelHTTPServer.DEFAULT_POST_TOTAL_BAN_TIME) / 60; + } + private int getProperty(int tunnel, String prop, int def) { TunnelController tun = getController(tunnel); if (tun != null) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 10316f999f..7cdf3e6e2a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -30,6 +30,7 @@ import net.i2p.data.SessionKey; import net.i2p.i2ptunnel.I2PTunnelConnectClient; import net.i2p.i2ptunnel.I2PTunnelHTTPClient; import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase; +import net.i2p.i2ptunnel.I2PTunnelHTTPServer; import net.i2p.i2ptunnel.I2PTunnelIRCClient; import net.i2p.i2ptunnel.I2PTunnelServer; import net.i2p.i2ptunnel.TunnelController; @@ -809,7 +810,7 @@ public class IndexBean { public void setReduceTime(String val) { if (val != null) { try { - _otherOptions.put("i2cp.reduceIdleTime", "" + (Integer.parseInt(val.trim()) * 60*1000)); + _otherOptions.put("i2cp.reduceIdleTime", Integer.toString(Integer.parseInt(val.trim()) * 60*1000)); } catch (NumberFormatException nfe) {} } } @@ -835,7 +836,7 @@ public class IndexBean { public void setCloseTime(String val) { if (val != null) { try { - _otherOptions.put("i2cp.closeIdleTime", "" + (Integer.parseInt(val.trim()) * 60*1000)); + _otherOptions.put("i2cp.closeIdleTime", Integer.toString(Integer.parseInt(val.trim()) * 60*1000)); } catch (NumberFormatException nfe) {} } } @@ -914,6 +915,35 @@ public class IndexBean { _otherOptions.put(PROP_MAX_STREAMS, s.trim()); } + /** + * POST limits + * @since 0.9.9 + */ + public void setPostMax(String s) { + if (s != null) + _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_MAX, s.trim()); + } + + public void setPostTotalMax(String s) { + if (s != null) + _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_TOTAL_MAX, s.trim()); + } + + public void setPostCheckTime(String s) { + if (s != null) + _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_WINDOW, Integer.toString(Integer.parseInt(s.trim()) * 60)); + } + + public void setPostBanTime(String s) { + if (s != null) + _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_BAN_TIME, Integer.toString(Integer.parseInt(s.trim()) * 60)); + } + + public void setPostTotalBanTime(String s) { + if (s != null) + _otherOptions.put(I2PTunnelHTTPServer.OPT_POST_TOTAL_BAN_TIME, Integer.toString(Integer.parseInt(s.trim()) * 60)); + } + /** params needed for hashcash and dest modification */ public void setEffort(String val) { if (val != null) { @@ -1124,6 +1154,9 @@ public class IndexBean { } else if ("httpserver".equals(_type) || "httpbidirserver".equals(_type)) { if (_spoofedHost != null) config.setProperty("spoofedHost", _spoofedHost); + for (String p : _httpServerOpts) + if (_otherOptions.containsKey(p)) + config.setProperty("option." + p, _otherOptions.get(p)); } if ("httpbidirserver".equals(_type)) { if (_port != null) @@ -1180,6 +1213,13 @@ public class IndexBean { PROP_MAX_TOTAL_CONNS_MIN, PROP_MAX_TOTAL_CONNS_HOUR, PROP_MAX_TOTAL_CONNS_DAY, PROP_MAX_STREAMS }; + private static final String _httpServerOpts[] = { + I2PTunnelHTTPServer.OPT_POST_WINDOW, + I2PTunnelHTTPServer.OPT_POST_BAN_TIME, + I2PTunnelHTTPServer.OPT_POST_TOTAL_BAN_TIME, + I2PTunnelHTTPServer.OPT_POST_MAX, + I2PTunnelHTTPServer.OPT_POST_TOTAL_MAX + }; /** * do NOT add these to noShoOpts, we must leave them in for HTTPClient and ConnectCLient @@ -1190,7 +1230,7 @@ public class IndexBean { "proxyUsername", "proxyPassword" }; - protected static final Set _noShowSet = new HashSet(64); + protected static final Set _noShowSet = new HashSet(128); protected static final Set _nonProxyNoShowSet = new HashSet(4); static { _noShowSet.addAll(Arrays.asList(_noShowOpts)); @@ -1199,6 +1239,7 @@ public class IndexBean { _noShowSet.addAll(Arrays.asList(_booleanServerOpts)); _noShowSet.addAll(Arrays.asList(_otherClientOpts)); _noShowSet.addAll(Arrays.asList(_otherServerOpts)); + _noShowSet.addAll(Arrays.asList(_httpServerOpts)); _nonProxyNoShowSet.addAll(Arrays.asList(_otherProxyOpts)); } diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index 96240168af..a6c8e74138 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -425,7 +425,41 @@ input.default { width: 1px; height: 1px; visibility: hidden; } -
+ <% if (("httpserver".equals(tunnelType)) || ("httpbidirserver".equals(tunnelType))) { + %>
+
+ +
+
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ <% } // httpserver + %>

diff --git a/history.txt b/history.txt index 95fc709d2e..815f382d37 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,8 @@ +2013-10-26 zzz + * I2PTunnel HTTP server: New POST limiter + 2013-10-25 zzz + * Router: Only log ping file error once (ticket #1086) * Streaming: - Check blacklist/whitelist before connection limits, so a blacklisted peer does not increment the counters diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 7ca1a3ec2c..5a58b19bc2 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 10; + public final static long BUILD = 11; /** for example "-test" */ public final static String EXTRA = "";