2005-03-04 jrandom

* Filter HTTP response headers in the eepproxy, forcing Connection: close
      so that broken (/malicious) webservers can't allow persistent
      connections.  All HTTP compliant browsers should now always close the
      socket.
    * Enabled the GZIPInputStream's cache (they were'nt cached before)
    * Make sure our first send is always a SYN (duh)
    * Workaround for some buggy compilers
This commit is contained in:
jrandom
2005-03-05 02:54:42 +00:00
committed by zzz
parent 7928ef83cc
commit 01979c08b3
16 changed files with 320 additions and 28 deletions

View File

@ -0,0 +1,232 @@
package net.i2p.i2ptunnel;
/*
* free (adj.): unencumbered; not under the control of others
* Written by jrandom in 2005 and released into the public domain
* with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk.
*
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.FilterOutputStream;
import java.io.OutputStream;
import java.util.Properties;
import java.util.Iterator;
import net.i2p.data.ByteArray;
import net.i2p.util.ByteCache;
import net.i2p.util.Log;
/**
* Simple stream for delivering an HTTP response to
* the client, trivially filtered to make sure "Connection: close"
* is always in the response.
*
*/
class HTTPResponseOutputStream extends FilterOutputStream {
private static final Log _log = new Log(HTTPResponseOutputStream.class);
private ByteCache _cache;
protected ByteArray _headerBuffer;
private boolean _headerWritten;
private byte _buf1[];
private static final int CACHE_SIZE = 4096;
public HTTPResponseOutputStream(OutputStream raw) {
super(raw);
_cache = ByteCache.getInstance(8, CACHE_SIZE);
_headerBuffer = _cache.acquire();
_headerWritten = false;
_buf1 = new byte[1];
}
public void write(int c) throws IOException {
_buf1[0] = (byte)c;
write(_buf1, 0, 1);
}
public void write(byte buf[]) throws IOException {
write(buf, 0, buf.length);
}
public void write(byte buf[], int off, int len) throws IOException {
if (_headerWritten) {
out.write(buf, off, len);
return;
}
for (int i = 0; i < len; i++) {
ensureCapacity();
_headerBuffer.getData()[_headerBuffer.getValid()] = buf[off+i];
_headerBuffer.setValid(_headerBuffer.getValid()+1);
if (headerReceived()) {
writeHeader();
_headerWritten = true;
if (i + 1 < len) // write out the remaining
out.write(buf, off+i+1, len-i-1);
return;
}
}
}
/**
* filter any headers (adding or removing as necessary), and tweak
* the first response line as necessary.
*
* @return response line ("200 OK", etc)
*/
protected String filterHeaders(String responseLine, Properties props) {
props.setProperty("Connection", "close");
props.setProperty("Proxy-Connection", "close");
return responseLine;
}
/** grow (and free) the buffer as necessary */
private void ensureCapacity() {
if (_headerBuffer.getValid() + 1 >= _headerBuffer.getData().length) {
int newSize = (int)(_headerBuffer.getData().length * 1.5);
ByteArray newBuf = new ByteArray(new byte[newSize]);
System.arraycopy(_headerBuffer.getData(), 0, newBuf, 0, _headerBuffer.getValid());
if (_headerBuffer.getData().length == CACHE_SIZE)
_cache.release(_headerBuffer);
_headerBuffer = newBuf;
}
}
/** are the headers finished? */
private boolean headerReceived() {
if (_headerBuffer.getValid() < 3) return false;
byte first = _headerBuffer.getData()[_headerBuffer.getValid()-3];
byte second = _headerBuffer.getData()[_headerBuffer.getValid()-2];
byte third = _headerBuffer.getData()[_headerBuffer.getValid()-1];
return (isNL(second) && isNL(third)) || // \n\n
(isNL(first) && isNL(third)); // \n\r\n
}
/** we ignore any potential \r, since we trim it on write anyway */
private static final byte NL = '\n';
private boolean isNL(byte b) { return (b == NL); }
/** ok, received, now munge & write it */
private void writeHeader() throws IOException {
Properties props = new Properties();
String responseLine = null;
int lastEnd = -1;
for (int i = 0; i < _headerBuffer.getValid(); i++) {
if (isNL(_headerBuffer.getData()[i])) {
if (lastEnd == -1) {
responseLine = new String(_headerBuffer.getData(), 0, i+1); // includes NL
} else {
for (int j = lastEnd+1; j < i; j++) {
if (_headerBuffer.getData()[j] == ':') {
String key = new String(_headerBuffer.getData(), lastEnd+1, j-(lastEnd+1));
String val = new String(_headerBuffer.getData(), j+2, i-(j+2));
props.setProperty(key, val);
break;
}
}
}
lastEnd = i;
}
}
if (responseLine == null)
throw new IOException("No HTTP response line, with props=" + props);
responseLine = filterHeaders(responseLine, props);
responseLine = (responseLine.trim() + "\n");
if (_log.shouldLog(Log.DEBUG)) {
StringBuffer msg = new StringBuffer(responseLine.length() + props.size() * 64);
msg.append("HTTP response: first line [").append(responseLine.trim());
msg.append("] options: \n");
for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) {
String key = (String)iter.next();
String val = props.getProperty(key);
msg.append('[').append(key.trim()).append("]=[").append(val.trim()).append("]\n");
}
_log.debug(msg.toString());
}
out.write(responseLine.getBytes());
for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) {
String key = (String)iter.next();
String val = props.getProperty(key);
String line = key.trim() + ": " + val.trim() + "\n";
out.write(line.getBytes());
}
out.write("\n".getBytes()); // end of the headers
// done, shove off
if (_headerBuffer.getData().length == CACHE_SIZE)
_cache.release(_headerBuffer);
else
_headerBuffer = null;
}
public static void main(String args[]) {
String simple = "HTTP/1.1 200 OK\n" +
"foo: bar\n" +
"baz: bat\n" +
"\n" +
"hi ho, this is the body";
String filtered = "HTTP/1.1 200 OK\n" +
"Connection: keep-alive\n" +
"foo: bar\n" +
"baz: bat\n" +
"\n" +
"hi ho, this is the body";
String winfilter= "HTTP/1.1 200 OK\r\n" +
"Connection: keep-alive\r\n" +
"foo: bar\r\n" +
"baz: bat\r\n" +
"\r\n" +
"hi ho, this is the body";
String minimal = "HTTP/1.1 200 OK\n" +
"\n" +
"hi ho, this is the body";
String winmin = "HTTP/1.1 200 OK\r\n" +
"\r\n" +
"hi ho, this is the body";
String invalid1 = "HTTP/1.1 200 OK\n";
String invalid2 = "HTTP/1.1 200 OK";
String invalid3 = "HTTP 200 OK\r\n";
String invalid4 = "HTTP 200 OK\r";
String large = "HTTP/1.1 200 OK\n" +
"Last-modified: Tue, 25 Nov 2003 12:05:38 GMT\n" +
"Expires: Tue, 25 Nov 2003 12:05:38 GMT\n" +
"Content-length: 32\n" +
"\n" +
"hi ho, this is the body";
/* */
test("Simple", simple);
test("Filtered", filtered);
test("Filtered windows", winfilter);
test("Minimal", minimal);
test("Windows", winmin);
test("Large", large);
test("Invalid (short headers)", invalid1);
test("Invalid (no headers)", invalid2);
test("Invalid (windows with short headers)", invalid3);
test("Invalid (windows no headers)", invalid4);
/* */
}
private static void test(String name, String orig) {
System.out.println("====Testing: " + name + "\n" + orig + "\n------------");
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
HTTPResponseOutputStream resp = new HTTPResponseOutputStream(baos);
resp.write(orig.getBytes());
resp.flush();
String received = new String(baos.toByteArray());
System.out.println(received);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -416,7 +416,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
I2PSocket i2ps = createI2PSocket(dest, getDefaultOptions(opts));
byte[] data = newRequest.toString().getBytes("ISO-8859-1");
Runnable onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId);
I2PTunnelRunner runner = new I2PTunnelRunner(s, i2ps, sockLock, data, mySockets, onTimeout);
I2PTunnelRunner runner = new I2PTunnelHTTPClientRunner(s, i2ps, sockLock, data, mySockets, onTimeout);
} catch (SocketException ex) {
_log.info(getPrefix(requestId) + "Error trying to connect", ex);
l.log(ex.getMessage());

View File

@ -0,0 +1,41 @@
/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
* (c) 2003 - 2004 mihi
*/
package net.i2p.i2ptunnel;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.FilterOutputStream;
import java.net.Socket;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.data.ByteArray;
import net.i2p.data.DataHelper;
import net.i2p.util.ByteCache;
import net.i2p.util.Log;
/**
* Override the response with a stream filtering the HTTP headers
* received. Specifically, this makes sure we get Connection: close,
* so the browser knows they really shouldn't try to use persistent
* connections. The HTTP server *should* already be setting this,
* since the HTTP headers sent by the browser specify Connection: close,
* and the server should echo it. However, both broken and malicious
* servers could ignore that, potentially confusing the user.
*
*/
public class I2PTunnelHTTPClientRunner extends I2PTunnelRunner {
public I2PTunnelHTTPClientRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, List sockList, Runnable onTimeout) {
super(s, i2ps, slock, initialI2PData, sockList, onTimeout);
}
protected OutputStream getSocketOut() throws IOException {
OutputStream raw = super.getSocketOut();
return new HTTPResponseOutputStream(raw);
}
}

View File

@ -111,10 +111,13 @@ public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorL
return startedOn;
}
protected InputStream getSocketIn() throws IOException { return s.getInputStream(); }
protected OutputStream getSocketOut() throws IOException { return s.getOutputStream(); }
public void run() {
try {
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream(); // = new BufferedOutputStream(s.getOutputStream(), NETWORK_BUFFER_SIZE);
InputStream in = getSocketIn();
OutputStream out = getSocketOut(); // = new BufferedOutputStream(s.getOutputStream(), NETWORK_BUFFER_SIZE);
i2ps.setSocketErrorListener(this);
InputStream i2pin = i2ps.getInputStream();
OutputStream i2pout = i2ps.getOutputStream(); //new BufferedOutputStream(i2ps.getOutputStream(), MAX_PACKET_SIZE);

View File

@ -584,6 +584,7 @@ public class Connection {
}
}
/** how many packets have we sent and the other side has ACKed? */
public long getAckedPackets() { return _ackedPackets; }
public long getCreatedOn() { return _createdOn; }
public long getCloseSentOn() { return _closeSentOn; }
@ -599,6 +600,7 @@ public class Connection {
public void incrementUnackedPacketsReceived() { _unackedPacketsReceived++; }
public int getUnackedPacketsReceived() { return _unackedPacketsReceived; }
/** how many packets have we sent but not yet received an ACK for? */
public int getUnackedPacketsSent() {
synchronized (_outboundPackets) {
return _outboundPackets.size();

View File

@ -132,6 +132,8 @@ class ConnectionDataReceiver implements MessageOutputStream.DataReceiver {
private PacketLocal buildPacket(Connection con, byte buf[], int off, int size, boolean forceIncrement) {
if (size > Packet.MAX_PAYLOAD_SIZE) throw new IllegalArgumentException("size is too large (" + size + ")");
boolean ackOnly = isAckOnly(con, size);
boolean isFirst = (con.getAckedPackets() <= 0) && (con.getUnackedPacketsSent() <= 0);
PacketLocal packet = new PacketLocal(_context, con.getRemotePeer(), con);
//ByteArray data = packet.acquirePayload();
ByteArray data = new ByteArray(new byte[size]);
@ -140,7 +142,7 @@ class ConnectionDataReceiver implements MessageOutputStream.DataReceiver {
data.setValid(size);
data.setOffset(0);
packet.setPayload(data);
if (ackOnly && !forceIncrement)
if ( (ackOnly && !forceIncrement) && (!isFirst) )
packet.setSequenceNum(0);
else
packet.setSequenceNum(con.getNextOutboundPacketNum());
@ -158,7 +160,8 @@ class ConnectionDataReceiver implements MessageOutputStream.DataReceiver {
packet.setFlag(Packet.FLAG_SIGNATURE_REQUESTED, con.getOptions().getRequireFullySigned());
if ( (!ackOnly) && (packet.getSequenceNum() <= 0) ) {
//if ( (!ackOnly) && (packet.getSequenceNum() <= 0) ) {
if (isFirst) {
packet.setFlag(Packet.FLAG_SYNCHRONIZE);
packet.setOptionalFrom(con.getSession().getMyDestination());
packet.setOptionalMaxSize(con.getOptions().getMaxMessageSize());

View File

@ -160,6 +160,7 @@ public class PacketLocal extends Packet implements MessageOutputStream.WriteStat
synchronized (this) {
if (_ackOn > 0) break;
if (_cancelledOn > 0) break;
if (!_connection.getIsConnected()) break;
if (timeRemaining > 60*1000)
timeRemaining = 60*1000;
else if (timeRemaining <= 0)

View File

@ -865,7 +865,7 @@ public class DataHelper {
ReusableGZIPInputStream in = ReusableGZIPInputStream.acquire();
in.initialize(new ByteArrayInputStream(orig, offset, length));
ByteCache cache = ByteCache.getInstance(16, MAX_UNCOMPRESSED);
ByteCache cache = ByteCache.getInstance(8, MAX_UNCOMPRESSED);
ByteArray outBuf = cache.acquire();
int written = 0;
while (true) {
@ -877,6 +877,7 @@ public class DataHelper {
byte rv[] = new byte[written];
System.arraycopy(outBuf.getData(), 0, rv, 0, written);
cache.release(outBuf);
ReusableGZIPInputStream.release(in);
return rv;
}

View File

@ -134,8 +134,8 @@ public class Timestamper implements Runnable {
try {
lastFailed = !queryTime(serverList);
} catch (IllegalArgumentException iae) {
if (!lastFailed)
_log.log(Log.CRIT, "Unable to reach any of the NTP servers - network disconnected?");
if ( (!lastFailed) && (_log.shouldLog(Log.ERROR)) )
_log.error("Unable to reach any of the NTP servers - network disconnected?");
lastFailed = true;
}
}

View File

@ -238,8 +238,8 @@ public class OrderedProperties extends Properties {
public int compareTo(Object o) {
if (o == null) return -1;
if (o instanceof StringMapEntry) return ((String) getKey()).compareTo(((StringMapEntry) o).getKey());
if (o instanceof String) return ((String) getKey()).compareTo(o);
if (o instanceof StringMapEntry) return ((String) getKey()).compareTo((String)((StringMapEntry) o).getKey());
if (o instanceof String) return ((String) getKey()).compareTo((String)o);
return -2;
}

View File

@ -15,7 +15,7 @@ import net.i2p.data.DataHelper;
*
*/
public class ReusableGZIPInputStream extends ResettableGZIPInputStream {
private static ArrayList _available = new ArrayList(16);
private static ArrayList _available = new ArrayList(8);
/**
* Pull a cached instance
*/
@ -36,7 +36,7 @@ public class ReusableGZIPInputStream extends ResettableGZIPInputStream {
*/
public static void release(ReusableGZIPInputStream released) {
synchronized (_available) {
if (_available.size() < 16)
if (_available.size() < 8)
_available.add(released);
}
}

View File

@ -1,4 +1,13 @@
$Id: history.txt,v 1.162 2005/03/02 22:36:52 jrandom Exp $
$Id: history.txt,v 1.163 2005/03/04 01:09:51 jrandom Exp $
2005-03-04 jrandom
* Filter HTTP response headers in the eepproxy, forcing Connection: close
so that broken (/malicious) webservers can't allow persistent
connections. All HTTP compliant browsers should now always close the
socket.
* Enabled the GZIPInputStream's cache (they were'nt cached before)
* Make sure our first send is always a SYN (duh)
* Workaround for some buggy compilers
2005-03-03 jrandom
* Loop while starting up the I2PTunnel instances, in case the I2CP

View File

@ -15,9 +15,9 @@ import net.i2p.CoreVersion;
*
*/
public class RouterVersion {
public final static String ID = "$Revision: 1.157 $ $Date: 2005/03/02 22:36:53 $";
public final static String ID = "$Revision: 1.158 $ $Date: 2005/03/04 01:09:20 $";
public final static String VERSION = "0.5.0.1";
public final static long BUILD = 9;
public final static long BUILD = 10;
public static void main(String args[]) {
System.out.println("I2P Router version: " + VERSION);
System.out.println("Router ID: " + RouterVersion.ID);

View File

@ -27,7 +27,7 @@ public class InboundGatewayReceiver implements TunnelGateway.Receiver {
if (_target == null) {
ReceiveJob j = null;
if (!alreadySearched)
j = new ReceiveJob(encrypted);
j = new ReceiveJob(_context, encrypted);
_context.netDb().lookupRouterInfo(_config.getSendTo(), j, j, 5*1000);
return;
}
@ -47,8 +47,8 @@ public class InboundGatewayReceiver implements TunnelGateway.Receiver {
private class ReceiveJob extends JobImpl {
private byte[] _encrypted;
public ReceiveJob(byte data[]) {
super(_context);
public ReceiveJob(RouterContext ctx, byte data[]) {
super(ctx);
_encrypted = data;
}
public String getName() { return "lookup first hop"; }

View File

@ -42,7 +42,7 @@ class OutboundReceiver implements TunnelGateway.Receiver {
if (_log.shouldLog(Log.DEBUG))
_log.debug("lookup of " + _config.getPeer(1).toBase64().substring(0,4)
+ " required for " + msg);
_context.netDb().lookupRouterInfo(_config.getPeer(1), new SendJob(msg), new FailedJob(), 10*1000);
_context.netDb().lookupRouterInfo(_config.getPeer(1), new SendJob(_context, msg), new FailedJob(_context), 10*1000);
}
}
@ -60,8 +60,8 @@ class OutboundReceiver implements TunnelGateway.Receiver {
private class SendJob extends JobImpl {
private TunnelDataMessage _msg;
public SendJob(TunnelDataMessage msg) {
super(_context);
public SendJob(RouterContext ctx, TunnelDataMessage msg) {
super(ctx);
_msg = msg;
}
public String getName() { return "forward a tunnel message"; }
@ -78,8 +78,8 @@ class OutboundReceiver implements TunnelGateway.Receiver {
}
private class FailedJob extends JobImpl {
public FailedJob() {
super(_context);
public FailedJob(RouterContext ctx) {
super(ctx);
}
public String getName() { return "failed looking for our outbound gateway"; }
public void runJob() {

View File

@ -77,7 +77,7 @@ public class TunnelParticipant {
if (_log.shouldLog(Log.WARN))
_log.warn("Lookup the nextHop (" + _config.getSendTo().toBase64().substring(0,4)
+ " for " + msg);
_context.netDb().lookupRouterInfo(_config.getSendTo(), new SendJob(msg), new TimeoutJob(msg), 10*1000);
_context.netDb().lookupRouterInfo(_config.getSendTo(), new SendJob(_context, msg), new TimeoutJob(_context, msg), 10*1000);
}
} else {
_inboundEndpointProcessor.getConfig().incrementProcessedMessages();
@ -112,8 +112,8 @@ public class TunnelParticipant {
private class SendJob extends JobImpl {
private TunnelDataMessage _msg;
public SendJob(TunnelDataMessage msg) {
super(_context);
public SendJob(RouterContext ctx, TunnelDataMessage msg) {
super(ctx);
_msg = msg;
}
public String getName() { return "forward a tunnel message"; }
@ -132,8 +132,8 @@ public class TunnelParticipant {
private class TimeoutJob extends JobImpl {
private TunnelDataMessage _msg;
public TimeoutJob(TunnelDataMessage msg) {
super(_context);
public TimeoutJob(RouterContext ctx, TunnelDataMessage msg) {
super(ctx);
_msg = msg;
}
public String getName() { return "timeout looking for next hop info"; }