package net.i2p.sam;
/*
* free (adj.): unencumbered; not under the control of others
* Written by human in 2004 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.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.ByteBuffer;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import net.i2p.I2PException;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketOptions;
import net.i2p.data.ByteArray;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.util.ByteCache;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
/**
* SAMv2 STREAM session class.
*
* @author mkvore
*/
class SAMv2StreamSession extends SAMStreamSession
{
/**
* Create a new SAM STREAM session.
*
* Caller MUST call start().
*
* @param dest Base64-encoded destination and private keys (same format as PrivateKeyFile)
* @param dir Session direction ("RECEIVE", "CREATE" or "BOTH")
* @param props Properties to setup the I2P session
* @param recv Object that will receive incoming data
* @throws IOException
* @throws DataFormatException
* @throws SAMException
*/
public SAMv2StreamSession ( String dest, String dir, Properties props,
SAMStreamReceiver recv ) throws IOException, DataFormatException, SAMException
{
super ( dest, dir, props, recv );
}
/**
* Create a new SAM STREAM session.
*
* Caller MUST call start().
*
* @param destStream Input stream containing the destination and private keys (same format as PrivateKeyFile)
* @param dir Session direction ("RECEIVE", "CREATE" or "BOTH")
* @param props Properties to setup the I2P session
* @param recv Object that will receive incoming data
* @throws IOException
* @throws DataFormatException
* @throws SAMException
*/
public SAMv2StreamSession ( InputStream destStream, String dir,
Properties props, SAMStreamReceiver recv ) throws IOException, DataFormatException, SAMException
{
super ( destStream, dir, props, recv );
}
/**
* Connect the SAM STREAM session to the specified Destination
*
* @param id Unique id for the connection
* @param dest Base64-encoded Destination to connect to
* @param props Options to be used for connection
*
* @throws DataFormatException if the destination is not valid
* @throws SAMInvalidDirectionException if trying to connect through a
* receive-only session
* @return true if the communication with the SAM client is ok
*/
@Override
public boolean connect ( int id, String dest, Properties props )
throws DataFormatException, SAMInvalidDirectionException
{
if ( !canCreate )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Trying to create an outgoing connection using a receive-only session" );
throw new SAMInvalidDirectionException ( "Trying to create connections through a receive-only session" );
}
if ( checkSocketHandlerId ( id ) )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "The specified id (" + id + ") is already in use" );
return false ;
}
Destination d = SAMUtils.getDest(dest);
I2PSocketOptions opts = socketMgr.buildOptions ( props );
if ( props.getProperty ( I2PSocketOptions.PROP_CONNECT_TIMEOUT ) == null )
opts.setConnectTimeout ( 60 * 1000 );
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Connecting new I2PSocket..." );
// non-blocking connection (SAMv2)
StreamConnector connector = new StreamConnector ( id, d, opts );
I2PAppThread connectThread = new I2PAppThread ( connector, "StreamConnector" + id ) ;
connectThread.start() ;
return true ;
}
/**
* SAM STREAM socket connecter, running in its own thread.
*
* @author mkvore
*/
private class StreamConnector implements Runnable
{
private final int id;
private final Destination dest ;
private final I2PSocketOptions opts ;
/**
* Create a new SAM STREAM session socket reader
*
* @param id Unique id assigned to the handler
* @param dest Destination to reach
* @param opts Socket options (I2PSocketOptions)
*/
public StreamConnector ( int id, Destination dest, I2PSocketOptions opts )// throws IOException
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Instantiating new SAM STREAM connector" );
this.id = id ;
this.opts = opts ;
this.dest = dest ;
}
public void run()
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "run() called for socket connector " + id );
try
{
try
{
I2PSocket i2ps = socketMgr.connect ( dest, opts );
createSocketHandler ( i2ps, id );
recv.notifyStreamOutgoingConnection ( id, "OK", null );
}
catch ( DataFormatException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Invalid destination in STREAM CONNECT message" );
recv.notifyStreamOutgoingConnection ( id, "INVALID_KEY", e.getMessage() );
}
catch ( ConnectException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug("STREAM CONNECT failed", e);
recv.notifyStreamOutgoingConnection ( id, "CONNECTION_REFUSED", e.getMessage() );
}
catch ( NoRouteToHostException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug("STREAM CONNECT failed", e);
recv.notifyStreamOutgoingConnection ( id, "CANT_REACH_PEER", e.getMessage() );
}
catch ( InterruptedIOException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug("STREAM CONNECT failed", e);
recv.notifyStreamOutgoingConnection ( id, "TIMEOUT", e.getMessage() );
}
catch ( I2PException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug("STREAM CONNECT failed", e);
recv.notifyStreamOutgoingConnection ( id, "I2P_ERROR", e.getMessage() );
}
}
catch ( IOException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Error sending disconnection notice for handler "
+ id, e );
}
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Shutting down SAM STREAM session connector " + id );
}
}
/**
* Lets us push data through the stream without blocking, (even after exceeding
* the I2PSocket's buffer)
*
* @param s I2PSocket
* @param id Socket ID
* @return v2StreamSender
* @throws IOException
*/
@Override
protected StreamSender newStreamSender ( I2PSocket s, int id ) throws IOException
{
return new V2StreamSender ( s, id ) ;
}
@Override
protected SAMStreamSessionSocketReader
newSAMStreamSessionSocketReader(I2PSocket s, int id ) throws IOException
{
return new SAMv2StreamSessionSocketReader(s,id);
}
private class V2StreamSender extends StreamSender
{
private final List<ByteArray> _data;
private int _dataSize;
private final ByteCache _cache;
private final OutputStream _out;
private volatile boolean _stillRunning, _shuttingDownGracefully;
private final Object runningLock = new Object();
public V2StreamSender ( I2PSocket s, int id ) throws IOException
{
super ( s, id );
_data = new ArrayList<ByteArray> ( 1 );
_cache = ByteCache.getInstance ( 10, 32 * 1024 );
_out = s.getOutputStream();
_stillRunning = true;
}
/**
* Send bytes through the SAM STREAM session socket sender
*
* @param in Data stream of data to send
* @param size Count of bytes to send
* @throws IOException if the client didnt provide enough data
*/
@Override
public void sendBytes ( InputStream in, int size ) throws IOException
{
if ( _log.shouldLog ( Log.DEBUG ) )
_log.debug ( "Handler " + _id + ": sending " + size + " bytes" );
ByteArray ba = _cache.acquire();
int read = DataHelper.read ( in, ba.getData(), 0, size );
if ( read != size )
throw new IOException ( "Insufficient data from the SAM client (" + read + "/" + size + ")" );
ba.setValid ( read );
synchronized ( _data )
{
if ( _dataSize >= SOCKET_HANDLER_BUF_SIZE )
{
_cache.release ( ba, false );
recv.streamSendAnswer ( _id, "FAILED", "BUFFER_FULL" ) ;
}
else
{
_dataSize += size ;
_data.add ( ba );
_data.notifyAll();
if ( _dataSize >= SOCKET_HANDLER_BUF_SIZE )
{
recv.streamSendAnswer ( _id, "OK", "BUFFER_FULL" ) ;
}
else
{
recv.streamSendAnswer ( _id, "OK", "READY" );
}
}
}
}
/**
* Stop a SAM STREAM session socket sender thread immediately
*
*/
@Override
public void stopRunning()
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "stopRunning() invoked on socket sender " + _id );
synchronized ( runningLock )
{
if ( _stillRunning )
{
_stillRunning = false;
try
{
i2pSocket.close();
}
catch ( IOException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Caught IOException", e );
}
synchronized ( _data )
{
_data.clear();
_data.notifyAll();
}
}
}
}
/**
* Stop a SAM STREAM session socket sender gracefully: stop the
* sender thread once all pending data has been sent.
*/
@Override
public void shutDownGracefully()
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "shutDownGracefully() invoked on socket sender " + _id );
_shuttingDownGracefully = true;
}
@Override
public void run()
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "run() called for socket sender " + _id );
ByteArray data = null;
while ( _stillRunning )
{
data = null;
try
{
synchronized ( _data )
{
if ( !_data.isEmpty() )
{
int formerSize = _dataSize ;
data = _data.remove ( 0 );
_dataSize -= data.getValid();
if ( ( formerSize >= SOCKET_HANDLER_BUF_SIZE ) && ( _dataSize < SOCKET_HANDLER_BUF_SIZE ) )
recv.notifyStreamSendBufferFree ( _id );
}
else if ( _shuttingDownGracefully )
{
/* No data left and shutting down gracefully?
If so, stop the sender. */
stopRunning();
break;
}
else
{
/* Wait for data. */
_data.wait ( 5000 );
}
}
if ( data != null )
{
try
{
_out.write ( data.getData(), 0, data.getValid() );
if ( forceFlush )
{
// i dont like doing this, but it clears the buffer issues
_out.flush();
}
}
catch ( IOException ioe )
{
// ok, the stream failed, but the SAM client didn't
if ( _log.shouldLog ( Log.WARN ) )
_log.warn ( "Stream failed", ioe );
removeSocketHandler ( _id );
stopRunning();
}
finally
{
_cache.release ( data, false );
}
}
}
catch ( InterruptedException ie ) {}
catch ( IOException e ) {}}
synchronized ( _data )
{
_data.clear();
}
}
}
/**
* Send bytes through a SAM STREAM session.
*
* @param id Stream ID
* @param limit limitation
* @param nolimit true to limit
* @return True if the data was queued for sending, false otherwise
*/
@Override
public boolean setReceiveLimit ( int id, long limit, boolean nolimit )
{
SAMStreamSessionSocketReader reader = getSocketReader ( id );
if ( reader == null )
{
if ( _log.shouldLog ( Log.WARN ) )
_log.warn ( "Trying to set a limit to a nonexistent reader " + id );
return false;
}
( (SAMv2StreamSessionSocketReader) reader).setLimit ( limit, nolimit );
return true;
}
/**
* SAM STREAM socket reader, running in its own thread. It forwards
* forward data to/from an I2P socket.
*
* @author human
*/
public class SAMv2StreamSessionSocketReader extends SAMv1StreamSessionSocketReader
{
protected boolean nolimit ;
protected long limit ;
protected long totalReceived ;
/**
* Create a new SAM STREAM session socket reader
*
* @param s Socket to be handled
* @param id Unique id assigned to the handler
*/
public SAMv2StreamSessionSocketReader ( I2PSocket s, int id ) throws IOException
{
super ( s, id );
}
public void setLimit ( long limit, boolean nolimit )
{
synchronized (runningLock)
{
this.limit = limit ;
this.nolimit = nolimit ;
runningLock.notify() ;
}
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "new limit set for socket reader " + id + " : " + (nolimit ? "NOLIMIT" : limit + " bytes" ) );
}
@Override
public void run()
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "run() called for socket reader " + id );
int read = -1;
ByteBuffer data = ByteBuffer.allocate(SOCKET_HANDLER_BUF_SIZE);
try
{
InputStream in = i2pSocket.getInputStream();
while ( stillRunning )
{
synchronized (runningLock)
{
while ( stillRunning && ( !nolimit && totalReceived >= limit) )
{
try{
runningLock.wait() ;
}
catch (InterruptedException ie)
{}
}
if ( !stillRunning )
break ;
}
data.clear();
read = Channels.newChannel(in).read ( data );
if ( read == -1 )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Handler " + id + ": connection closed" );
break;
}
totalReceived += read ;
data.flip();
recv.receiveStreamBytes ( id, data );
}
}
catch ( IOException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Caught IOException", e );
}
try
{
i2pSocket.close();
}
catch ( IOException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Caught IOException", e );
}
if ( stillRunning )
{
removeSocketHandler ( id );
// FIXME: we need error reporting here!
try
{
recv.notifyStreamDisconnection ( id, "OK", null );
}
catch ( IOException e )
{
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Error sending disconnection notice for handler "
+ id, e );
}
}
if (_log.shouldLog(Log.DEBUG))
_log.debug ( "Shutting down SAM STREAM session socket handler " + id );
}
}
}