/* 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.data.ByteArray; 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; /** * Like I2PTunnelRunner but socket-to-socket * * Warning - not maintained as a stable API for external use. * * @since 0.9.11 */ public class I2PTunnelOutproxyRunner extends I2PAppThread { 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... */ private static final int MAX_PACKET_SIZE = 1024 * 4; private static final int NETWORK_BUFFER_SIZE = MAX_PACKET_SIZE; private final Socket s; private final Socket i2ps; private final Object slock, finishLock = new Object(); volatile boolean finished = false; 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; /** if we die before receiving any data, run this job */ private final I2PTunnelRunner.FailCallback onTimeout; private long totalSent; private long totalReceived; private static final AtomicLong __forwarderId = new AtomicLong(); /** * Does NOT start itself. Caller must call start(). * * @param slock the socket lock, non-null * @param initialI2PData may be null * @param onTimeout May be null. If non-null and no data (except initial data) was received, it will be run before closing s. */ public I2PTunnelOutproxyRunner(Socket s, Socket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, I2PTunnelRunner.FailCallback onTimeout) { this.s = s; this.i2ps = i2ps; this.slock = slock; this.initialI2PData = initialI2PData; this.initialSocketData = initialSocketData; this.onTimeout = onTimeout; lastActivityOn = -1; startedOn = Clock.getInstance().now(); _log = I2PAppContext.getGlobalContext().logManager().getLog(getClass()); if (_log.shouldLog(Log.INFO)) _log.info("OutproxyRunner started"); _runnerId = __runnerId.incrementAndGet(); setName("OutproxyRunner " + _runnerId); } /** * 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(); } @Override public void run() { try { InputStream in = getSocketIn(); OutputStream out = getSocketOut(); InputStream i2pin = i2ps.getInputStream(); OutputStream i2pout = i2ps.getOutputStream(); if (initialI2PData != null) { i2pout.write(initialI2PData); 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 the outproxy, " + (initialSocketData != null ? initialSocketData.length : 0) + " written to the socket, starting forwarders"); if (!(s instanceof InternalSocket)) in = new BufferedInputStream(in, 2*NETWORK_BUFFER_SIZE); Thread t1 = new StreamForwarder(in, i2pout, true); Thread t2 = new StreamForwarder(i2pin, out, false); // TODO can we run one of these inline and save a thread? t1.start(); t2.start(); 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) { 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. if (totalReceived <= 0) onTimeout.onFail(null); } // now one connection is dead - kill the other as well, after making sure we flush close(out, in, i2pout, i2pin, s, i2ps, t1, t2); } 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) { if (_log.shouldLog(Log.WARN)) _log.warn("gnu?", ise); } catch (RuntimeException e) { if (_log.shouldLog(Log.ERROR)) _log.error("Internal error", e); } finally { 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 Socket", ex); } } } } protected void close(OutputStream out, InputStream in, OutputStream i2pout, InputStream i2pin, Socket s, Socket 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.join(30*1000); } public void errorOccurred() { synchronized (finishLock) { finished = true; finishLock.notifyAll(); } } /** * 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; /** * Does not start itself. Caller must start() */ private StreamForwarder(InputStream in, OutputStream out, boolean toI2P) { this.in = in; this.out = out; _toI2P = toI2P; direction = (toI2P ? "toOutproxy" : "fromOutproxy"); _cache = ByteCache.getInstance(32, NETWORK_BUFFER_SIZE); setName("OutproxyForwarder " + _runnerId + '.' + __forwarderId.incrementAndGet()); } @Override public void run() { String from = "todo"; String to = "todo"; if (_log.shouldLog(Log.DEBUG)) { _log.debug(direction + ": Forwarding between " + from + " and " + to); } 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(direction + ": " + len + " bytes flushed through " + (_toI2P ? "to " : "from ") + "outproxy"); 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); } } } catch (InterruptedIOException ex) { if (_log.shouldLog(Log.WARN)) _log.warn(direction + ": Closing connection due to timeout (error: \"" + ex.getMessage() + "\")"); } catch (IOException ex) { if (!finished) { if (_log.shouldLog(Log.WARN)) _log.warn(direction + ": Error forwarding", 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 { if (!(onTimeout != 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 } } } } }