/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) * (c) 2003 - 2004 mihi */ package net.i2p.i2ptunnel; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.Socket; import java.net.SocketException; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import javax.net.ssl.SSLException; import net.i2p.I2PAppContext; 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.Clock; import net.i2p.util.I2PAppThread; import net.i2p.util.InternalSocket; import net.i2p.util.Log; /** * A thread that starts two more threads, one to forward traffic in each direction. * * Warning - not maintained as a stable API for external use. */ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErrorListener { protected final Log _log; private static final AtomicLong __runnerId = new AtomicLong(); private final long _runnerId; /** * max bytes streamed in a packet - smaller ones might be filled * up to this size. Larger ones are not split (at least not on * Sun's impl of BufferedOutputStream), but that is the streaming * api's job... */ static int MAX_PACKET_SIZE = 1024 * 4; static final int NETWORK_BUFFER_SIZE = MAX_PACKET_SIZE; private final Socket s; private final I2PSocket i2ps; private final Object slock, finishLock = new Object(); private volatile boolean finished; private final byte[] initialI2PData; private final byte[] initialSocketData; /** when the last data was sent/received (or -1 if never) */ private long lastActivityOn; /** when the runner started up */ private final long startedOn; private final List<I2PSocket> sockList; /** if we die before receiving any data, run this job */ private final Runnable onTimeout; private final FailCallback _onFail; private long totalSent; private long totalReceived; /** * For use in new constructor * @since 0.9.14 */ public interface FailCallback { /** * @param e may be null */ public void onFail(Exception e); } /** * Starts itself * * @param slock the socket lock, non-null * @param initialI2PData may be null * @param sockList may be null. Caller must add i2ps to the list! It will be removed here on completion. * Will synchronize on slock when removing. * @deprecated use FailCallback constructor */ @Deprecated public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, List<I2PSocket> sockList) { this(s, i2ps, slock, initialI2PData, null, sockList, null, null, true); } /** * Starts itself * * @param slock the socket lock, non-null * @param initialI2PData may be null * @param initialSocketData may be null * @param sockList may be null. Caller must add i2ps to the list! It will be removed here on completion. * Will synchronize on slock when removing. * @deprecated use FailCallback constructor */ @Deprecated public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList) { this(s, i2ps, slock, initialI2PData, initialSocketData, sockList, null, null, true); } /** * Starts itself * * @param slock the socket lock, non-null * @param initialI2PData may be null * @param sockList may be null. Caller must add i2ps to the list! It will be removed here on completion. * Will synchronize on slock when removing. * @param onTimeout May be null. If non-null and no data (except initial data) was received, * it will be run before closing s. * @deprecated use FailCallback constructor */ @Deprecated public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, List<I2PSocket> sockList, Runnable onTimeout) { this(s, i2ps, slock, initialI2PData, null, sockList, onTimeout, null, true); } /** * Starts itself * * @param slock the socket lock, non-null * @param initialI2PData may be null * @param initialSocketData may be null * @param sockList may be null. Caller must add i2ps to the list! It will be removed here on completion. * Will synchronize on slock when removing. * @param onTimeout May be null. If non-null and no data (except initial data) was received, * it will be run before closing s. * @deprecated use FailCallback constructor */ @Deprecated public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, Runnable onTimeout) { this(s, i2ps, slock, initialI2PData, initialSocketData, sockList, onTimeout, null, true); } /** * Recommended new constructor. Does NOT start itself. Caller must call start(). * * @param slock the socket lock, non-null * @param initialI2PData may be null * @param initialSocketData may be null * @param sockList may be null. Caller must add i2ps to the list! It will be removed here on completion. * Will synchronize on slock when removing. * @param onFail May be null. If non-null and no data (except initial data) was received, * it will be run before closing s. */ public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, FailCallback onFail) { this(s, i2ps, slock, initialI2PData, initialSocketData, sockList, null, onFail, false); } /** * Base constructor * * @param slock the socket lock, non-null * @param initialI2PData may be null * @param initialSocketData may be null * @param sockList may be null. Caller must add i2ps to the list! It will be removed here on completion. * Will synchronize on slock when removing. * @param onTimeout May be null. If non-null and no data (except initial data) was received, * it will be run before closing s. * @param onFail Trumps onTimeout * @param shouldStart should thread be started in constructor (bad, false recommended) */ private I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, Runnable onTimeout, FailCallback onFail, boolean shouldStart) { this.sockList = sockList; this.s = s; this.i2ps = i2ps; this.slock = slock; this.initialI2PData = initialI2PData; this.initialSocketData = initialSocketData; this.onTimeout = onTimeout; _onFail = onFail; lastActivityOn = -1; startedOn = Clock.getInstance().now(); _log = I2PAppContext.getGlobalContext().logManager().getLog(getClass()); if (_log.shouldLog(Log.INFO)) _log.info("I2PTunnelRunner started"); _runnerId = __runnerId.incrementAndGet(); setName("I2PTunnelRunner " + _runnerId); if (shouldStart) start(); } /** * have we closed at least one (if not both) of the streams * [aka we're done running the streams]? * * @deprecated unused */ @Deprecated public boolean isFinished() { return finished; } /** * When was the last data for this runner sent or received? * As of 0.9.20, returns -1 always! * * @return date (ms since the epoch), or -1 if no data has been transferred yet * @deprecated unused */ @Deprecated public long getLastActivityOn() { return lastActivityOn; } /**** private void updateActivity() { lastActivityOn = Clock.getInstance().now(); } ****/ /** * When this runner started up transferring data * */ public long getStartedOn() { return startedOn; } protected InputStream getSocketIn() throws IOException { return s.getInputStream(); } protected OutputStream getSocketOut() throws IOException { return s.getOutputStream(); } private static final byte[] POST = { 'P', 'O', 'S', 'T', ' ' }; @Override public void run() { try { InputStream in = getSocketIn(); OutputStream out = getSocketOut(); // = new BufferedOutputStream(s.getOutputStream(), NETWORK_BUFFER_SIZE); // unimplemented in streaming //i2ps.setSocketErrorListener(this); InputStream i2pin = i2ps.getInputStream(); OutputStream i2pout = i2ps.getOutputStream(); //new BufferedOutputStream(i2ps.getOutputStream(), MAX_PACKET_SIZE); if (initialI2PData != null) { // why synchronize this? we could be in here a LONG time for large initial data //synchronized (slock) { // this does not increment totalSent i2pout.write(initialI2PData); // do NOT flush here, it will block and then onTimeout.run() won't happen on fail. // But if we don't flush, then we have to wait for the connectDelay timer to fire // in i2p socket? To be researched and/or fixed. // // AS OF 0.8.1, MessageOutputStream.flush() is fixed to only wait for accept, // not for "completion" (i.e. an ACK from the far end). // So we now get a fast return from flush(), and can do it here to save 250 ms. // To make sure we are under the initial window size and don't hang waiting for accept, // only flush if it fits in one message. if (initialI2PData.length <= 1730) { // ConnectionOptions.DEFAULT_MAX_MESSAGE_SIZE // Don't flush if POST, so we can get POST data into the initial packet if (initialI2PData.length < 5 || !DataHelper.eq(POST, 0, initialI2PData, 0, 5)) i2pout.flush(); } //} } if (initialSocketData != null) { // this does not increment totalReceived out.write(initialSocketData); } if (_log.shouldLog(Log.DEBUG)) _log.debug("Initial data " + (initialI2PData != null ? initialI2PData.length : 0) + " written to I2P, " + (initialSocketData != null ? initialSocketData.length : 0) + " written to the socket, starting forwarders"); if (!(s instanceof InternalSocket)) in = new BufferedInputStream(in, 2*NETWORK_BUFFER_SIZE); StreamForwarder toI2P = new StreamForwarder(in, i2pout, true); StreamForwarder fromI2P = new StreamForwarder(i2pin, out, false); toI2P.start(); // We are already a thread, so run the second one inline //fromI2P.start(); fromI2P.run(); synchronized (finishLock) { while (!finished) { finishLock.wait(); } } if (_log.shouldLog(Log.DEBUG)) _log.debug("At least one forwarder completed, closing and joining"); // this task is useful for the httpclient if (onTimeout != null || _onFail != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("runner has a timeout job, totalReceived = " + totalReceived + " totalSent = " + totalSent + " job = " + onTimeout); // Run even if totalSent > 0, as that's probably POST data. // This will be run even if initialSocketData != null, it's the timeout job's // responsibility to know that and decide whether or not to write to the socket. // HTTPClient never sets initialSocketData. if (totalReceived <= 0) { if (_onFail != null) { Exception e = fromI2P.getFailure(); if (e == null) e = toI2P.getFailure(); _onFail.onFail(e); } else { onTimeout.run(); } } } // now one connection is dead - kill the other as well, after making sure we flush close(out, in, i2pout, i2pin, s, i2ps, toI2P, fromI2P); } catch (InterruptedException ex) { if (_log.shouldLog(Log.ERROR)) _log.error("Interrupted", ex); } catch (SSLException she) { _log.error("SSL error", she); } catch (IOException ex) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Error forwarding", ex); } catch (IllegalStateException ise) { // JamVM (Gentoo: jamvm-1.5.4, gnu-classpath-0.98+gmp) //java.nio.channels.NotYetConnectedException // at gnu.java.nio.SocketChannelImpl.write(SocketChannelImpl.java:240) // at gnu.java.net.PlainSocketImpl$SocketOutputStream.write(PlainSocketImpl.java:668) // at java.io.OutputStream.write(OutputStream.java:86) // at net.i2p.i2ptunnel.I2PTunnelHTTPClient.writeFooter(I2PTunnelHTTPClient.java:1029) // at net.i2p.i2ptunnel.I2PTunnelHTTPClient.writeErrorMessage(I2PTunnelHTTPClient.java:1114) // at net.i2p.i2ptunnel.I2PTunnelHTTPClient.handleHTTPClientException(I2PTunnelHTTPClient.java:1131) // at net.i2p.i2ptunnel.I2PTunnelHTTPClient.access$000(I2PTunnelHTTPClient.java:67) // at net.i2p.i2ptunnel.I2PTunnelHTTPClient$OnTimeout.run(I2PTunnelHTTPClient.java:1052) // at net.i2p.i2ptunnel.I2PTunnelRunner.run(I2PTunnelRunner.java:167) if (_log.shouldLog(Log.WARN)) _log.warn("gnu?", ise); } catch (RuntimeException e) { if (_log.shouldLog(Log.ERROR)) _log.error("Internal error", e); } finally { removeRef(); try { if (s != null) s.close(); } catch (IOException ex) { if (_log.shouldLog(Log.WARN)) _log.warn("Could not close java socket", ex); } if (i2ps != null) { try { i2ps.close(); } catch (IOException ex) { if (_log.shouldLog(Log.WARN)) _log.warn("Could not close I2PSocket", ex); } // unimplemented in streaming //i2ps.setSocketErrorListener(null); } } } protected void close(OutputStream out, InputStream in, OutputStream i2pout, InputStream i2pin, Socket s, I2PSocket i2ps, Thread t1, Thread t2) throws InterruptedException { try { out.flush(); } catch (IOException ioe) { // ignore } try { i2pout.flush(); } catch (IOException ioe) { // ignore } try { in.close(); } catch (IOException ioe) { // ignore } try { i2pin.close(); } catch (IOException ioe) { // ignore } // ok, yeah, there's a race here in theory, if data comes in after flushing and before // closing, but its better than before... try { s.close(); } catch (IOException ioe) { // ignore } try { i2ps.close(); } catch (IOException ioe) { // ignore } t1.join(30*1000); // t2 = fromI2P now run inline //t2.join(30*1000); } /** * Deprecated, unimplemented in streaming, never called. */ public void errorOccurred() { synchronized (finishLock) { finished = true; finishLock.notifyAll(); } } private void removeRef() { if (sockList != null) { synchronized (slock) { sockList.remove(i2ps); } } } /** * Forward data in one direction */ private class StreamForwarder extends I2PAppThread { private final InputStream in; private final OutputStream out; private final String direction; private final boolean _toI2P; private final ByteCache _cache; private volatile Exception _failure; /** * Does not start itself. Caller must start() */ public StreamForwarder(InputStream in, OutputStream out, boolean toI2P) { this.in = in; this.out = out; _toI2P = toI2P; direction = (toI2P ? "toI2P" : "fromI2P"); _cache = ByteCache.getInstance(32, NETWORK_BUFFER_SIZE); setName("StreamForwarder " + _runnerId + '.' + direction); } @Override public void run() { String from = i2ps.getThisDestination().calculateHash().toBase64().substring(0,6); String to = i2ps.getPeerDestination().calculateHash().toBase64().substring(0,6); if (_log.shouldLog(Log.DEBUG)) { _log.debug(direction + ": Forwarding between " + from + " and " + to); } // boo, hiss! shouldn't need this - the streaming lib should be configurable, but // somehow the inactivity timer is sometimes failing to get triggered properly //i2ps.setReadTimeout(2*60*1000); ByteArray ba = _cache.acquire(); byte[] buffer = ba.getData(); // new byte[NETWORK_BUFFER_SIZE]; try { int len; while ((len = in.read(buffer)) != -1) { if (len > 0) { out.write(buffer, 0, len); if (_toI2P) totalSent += len; else totalReceived += len; //updateActivity(); } if (in.available() == 0) { //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Flushing after sending " + len + " bytes through"); if (_log.shouldLog(Log.DEBUG)) _log.debug(direction + ": " + len + " bytes flushed through " + (_toI2P ? "to " : "from ") + to); if (_toI2P) { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } if (in.available() <= 0) out.flush(); } else { out.flush(); } } } //out.flush(); // close() flushes } catch (SocketException ex) { // this *will* occur when the other threads closes the socket synchronized (finishLock) { if (!finished) { if (_log.shouldLog(Log.DEBUG)) _log.debug(direction + ": Socket closed - error reading and writing", ex); } } _failure = ex; } catch (InterruptedIOException ex) { if (_log.shouldLog(Log.WARN)) _log.warn(direction + ": Closing connection due to timeout (error: \"" + ex.getMessage() + "\")"); _failure = ex; } catch (IOException ex) { if (!finished) { if (_log.shouldLog(Log.WARN)) _log.warn(direction + ": Error forwarding", ex); } //else // _log.warn("You may ignore this", ex); _failure = ex; } finally { _cache.release(ba); if (_log.shouldLog(Log.INFO)) { _log.info(direction + ": done forwarding between " + from + " and " + to); } try { in.close(); } catch (IOException ex) { if (_log.shouldLog(Log.WARN)) _log.warn(direction + ": Error closing input stream", ex); } try { // Thread must close() before exiting for a PipedOutputStream, // or else input end gives up and we have data loss. // http://techtavern.wordpress.com/2008/07/16/whats-this-ioexception-write-end-dead/ //out.flush(); // DON'T close if we have a timeout job and we haven't received anything, // or else the timeout job can't write the error message to the stream. // close() above will close it after the timeout job is run. if (!((onTimeout != null || _onFail != null) && (!_toI2P) && totalReceived <= 0)) out.close(); else if (_log.shouldLog(Log.INFO)) _log.info(direction + ": not closing so we can write the error message"); } catch (IOException ioe) { if (_log.shouldLog(Log.WARN)) _log.warn(direction + ": Error flushing to close", ioe); } synchronized (finishLock) { finished = true; finishLock.notifyAll(); // the main thread will close sockets etc. now } } } /** * @since 0.9.14 */ public Exception getFailure() { return _failure; } } }