* I2PTunnel HTTPServer:

New POST limiter
This commit is contained in:
zzz
2013-10-26 14:21:26 +00:00
parent acc0ab66a3
commit 8f8022347d
7 changed files with 411 additions and 9 deletions

View File

@ -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<Hash, Record> _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<Long> times;
private long until;
public Record() {
times = new ArrayList(8);
increment();
}
/** Caller must synch */
public int countSince(long time) {
for (Iterator<Long> 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<Record> iter = _peers.values().iterator(); iter.hasNext(); ) {
Record rec = iter.next();
if (rec.getUntil() <= 0 && rec.countSince(then) <= 0)
iter.remove();
}
}
}
schedule(_checkPeriod);
}
}
}

View File

@ -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 {
"</body></html>")
.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"+
"<html><head><title>403 Denied</title></head>\n"+
"<body><h2>403 Denied</h2>\n" +
"<p>Denied due to excessive requests. Please try again later.</p>\n" +
"</body></html>")
.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<String, List<String>> 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

View File

@ -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) {

View File

@ -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));
}

View File

@ -425,7 +425,41 @@ input.default { width: 1px; height: 1px; visibility: hidden; }
</div>
</div>
<div class="subdivider">
<% if (("httpserver".equals(tunnelType)) || ("httpbidirserver".equals(tunnelType))) {
%><div class="rowItem">
<div id="optionsField" class="rowItem">
<label><%=intl._("POST limits (0=unlimited)")%><br /><%=intl._("Per client")%>:</label>
</div>
<div id="portField" class="rowItem">
<label><%=intl._("Per period")%>:</label>
<input type="text" id="port" name="postMax" value="<%=editBean.getPostMax(curTunnel)%>" class="freetext" />
</div>
<div id="portField" class="rowItem">
<label><%=intl._("Ban minutes")%>:</label>
<input type="text" id="port" name="postBanTime" value="<%=editBean.getPostBanTime(curTunnel)%>" class="freetext" />
</div>
</div>
<div class="rowItem">
<div id="optionsField" class="rowItem">
<label><%=intl._("Total")%>:</label>
</div>
<div id="portField" class="rowItem">
<input type="text" id="port" name="postTotalMax" value="<%=editBean.getPostTotalMax(curTunnel)%>" class="freetext" />
</div>
<div id="portField" class="rowItem">
<input type="text" id="port" name="postTotalBanTime" value="<%=editBean.getPostTotalBanTime(curTunnel)%>" class="freetext" />
</div>
</div>
<div class="rowItem">
<div id="optionsField" class="rowItem">
<label><%=intl._("POST limit period (minutes)")%>:</label>
</div>
<div id="portField" class="rowItem">
<input type="text" id="port" name="postCheckTime" value="<%=editBean.getPostCheckTime(curTunnel)%>" class="freetext" />
</div>
</div>
<% } // httpserver
%><div class="subdivider">
<hr />
</div>

View File

@ -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

View File

@ -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 = "";