/* * Created on Feb 15, 2008 * Created by Paul Gardner * * Copyright 2008 Vuze, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package com.aelitis.azureus.plugins.net.netstatus; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.*; import org.gudy.azureus2.core3.util.AERunnable; import org.gudy.azureus2.core3.util.AESemaphore; import org.gudy.azureus2.core3.util.ByteFormatter; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.DelayedEvent; import org.gudy.azureus2.core3.util.DirectByteBuffer; import org.gudy.azureus2.core3.util.HashWrapper; import org.gudy.azureus2.core3.util.SystemTime; import com.aelitis.azureus.core.networkmanager.ConnectionEndpoint; import com.aelitis.azureus.core.networkmanager.IncomingMessageQueue; import com.aelitis.azureus.core.networkmanager.NetworkConnection; import com.aelitis.azureus.core.networkmanager.NetworkManager; import com.aelitis.azureus.core.networkmanager.OutgoingMessageQueue; import com.aelitis.azureus.core.networkmanager.ProtocolEndpoint; import com.aelitis.azureus.core.networkmanager.impl.tcp.ProtocolEndpointTCP; import com.aelitis.azureus.core.peermanager.PeerManager; import com.aelitis.azureus.core.peermanager.PeerManagerRegistration; import com.aelitis.azureus.core.peermanager.PeerManagerRegistrationAdapter; import com.aelitis.azureus.core.peermanager.messaging.Message; import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTBitfield; import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTHandshake; import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTHave; import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTMessage; import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTMessageDecoder; import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTMessageEncoder; import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTMessageFactory; import com.aelitis.azureus.core.util.CopyOnWriteList; public class NetStatusProtocolTesterBT { private static Random random = new SecureRandom(); private NetStatusProtocolTester tester; private boolean test_initiator; private CopyOnWriteList listeners = new CopyOnWriteList(); private byte[] my_hash; private byte[] peer_id; private PeerManagerRegistration pm_reg; private long start_time = SystemTime.getCurrentTime(); private List sessions = new ArrayList(); private int session_id_next; private int outbound_attempts = 0; private int outbound_connects = 0; private int inbound_connects = 0; private boolean outbound_connections_complete; private AESemaphore completion_sem = new AESemaphore( "Completion" ); private boolean destroyed; protected NetStatusProtocolTesterBT( NetStatusProtocolTester _tester, boolean _test_initiator ) { tester = _tester; test_initiator = _test_initiator; } protected void start() { my_hash = new byte[20]; random.nextBytes( my_hash ); peer_id = new byte[20]; random.nextBytes( peer_id ); pm_reg = PeerManager.getSingleton().registerLegacyManager( new HashWrapper( my_hash ), new PeerManagerRegistrationAdapter() { public byte[][] getSecrets() { return( new byte[][]{ my_hash }); } public boolean manualRoute( NetworkConnection connection ) { log( "Got incoming connection from " + connection.getEndpoint().getNotionalAddress()); new Session( connection, null ); return( true ); } public boolean isPeerSourceEnabled( String peer_source ) { return( true ); } public boolean activateRequest( InetSocketAddress remote_address ) { return( true ); } public void deactivateRequest( InetSocketAddress remote_address ) { } public String getDescription() { return( "NetStatusPlugin - router" ); } }); log( "Incoming routing established for " + ByteFormatter.encodeString( my_hash )); } protected byte[] getServerHash() { return( my_hash ); } protected long getStartTime( long now ) { if ( now < start_time ){ start_time = now; } return( start_time ); } protected void testOutbound( InetSocketAddress address, final byte[] their_hash, boolean use_crypto ) { // regardless of the caller's desires, override with crypto if we're using it if ( NetworkManager.getCryptoRequired( NetworkManager.CRYPTO_OVERRIDE_NONE )){ use_crypto = true; } log( "Making outbound connection to " + address ); synchronized( this ){ outbound_attempts++; } boolean allow_fallback = false; ProtocolEndpoint pe = new ProtocolEndpointTCP( address ); ConnectionEndpoint connection_endpoint = new ConnectionEndpoint( address ); connection_endpoint.addProtocol( pe ); final NetworkConnection connection = NetworkManager.getSingleton().createConnection( connection_endpoint, new BTMessageEncoder(), new BTMessageDecoder(), use_crypto, allow_fallback, new byte[][]{ their_hash }); new Session( connection, their_hash ); } public void destroy() { List to_close = new ArrayList(); synchronized( sessions ){ if ( destroyed ){ return; } destroyed = true; to_close.addAll( sessions ); sessions.clear(); } for (int i=0;i<to_close.size();i++){ Session session = (Session)to_close.get(i); session.close(); } pm_reg.unregister(); checkCompletion(); log( "Incoming routing destroyed for " + ByteFormatter.encodeString( my_hash )); } protected boolean isDestroyed() { return( destroyed ); } public void setOutboundConnectionsComplete() { synchronized( sessions ){ outbound_connections_complete = true; } checkCompletion(); } protected void checkCompletion() { boolean inform = false; synchronized( sessions ){ if ( completion_sem.isReleasedForever()){ return; } if ( destroyed || ( outbound_connections_complete && sessions.size() == 0 )){ inform = true; completion_sem.releaseForever(); } } if ( inform ){ Iterator it = listeners.iterator(); while( it.hasNext()){ try{ ((NetStatusProtocolTesterListener)it.next()).complete( this ); }catch( Throwable e ){ Debug.printStackTrace(e); } } } } public boolean waitForCompletion( long max_millis ) { if ( max_millis == 0 ){ completion_sem.reserve(); return( true ); }else{ return( completion_sem.reserve( max_millis )); } } public void addListener( NetStatusProtocolTesterListener l ) { listeners.add( l ); } public void removeListener( NetStatusProtocolTesterListener l ) { listeners.remove( l ); } public int getOutboundConnects() { return( outbound_connects ); } public int getInboundConnects() { return( inbound_connects ); } public String getStatus() { return( "sessions=" + sessions.size() + ", out_attempts=" + outbound_attempts + ", out_connect=" + outbound_connects + ", in_connect=" + inbound_connects ); } protected void log( String str ) { Iterator it = listeners.iterator(); while( it.hasNext()){ try{ ((NetStatusProtocolTesterListener)it.next()).log( str ); }catch( Throwable e ){ Debug.printStackTrace(e); } } tester.log( str ); } protected void logError( String str ) { Iterator it = listeners.iterator(); while( it.hasNext()){ try{ ((NetStatusProtocolTesterListener)it.next()).logError( str ); }catch( Throwable e ){ Debug.printStackTrace(e); } } tester.log( str ); } protected void logError( String str, Throwable e ) { Iterator it = listeners.iterator(); while( it.hasNext()){ try{ ((NetStatusProtocolTesterListener)it.next()).logError( str, e ); }catch( Throwable f ){ Debug.printStackTrace(f); } } tester.log( str, e ); } public class Session { private NetworkConnection connection; private int session_id; private boolean initiator; private byte[] info_hash; private boolean handshake_sent; private boolean handshake_received; private boolean bitfield_sent; private boolean bitfield_received; private int num_pieces; private boolean is_seed; private Set missing_pieces = new HashSet(); private boolean connected; private boolean closing; private boolean closed; protected Session( NetworkConnection _connection, byte[] _info_hash ) { connection = _connection; info_hash = _info_hash; initiator = info_hash != null; synchronized( sessions ){ session_id_next++; session_id = session_id_next; if ( destroyed ){ log( "Already destroyed" ); close(); return; }else{ // if we're a responder then we limit connections as we should only // receive 1 (be generous, give them 3 in case we decide to reconnect) if ( !( test_initiator || initiator )){ int responder_sessions = 0; for (int i=0;i<sessions.size();i++){ Session existing_session = (Session)sessions.get(i); if ( !existing_session.isInitiator()){ responder_sessions++; } } if ( responder_sessions >= 2 ){ log( "Too many responder sessions" ); close(); return; } } sessions.add( this ); is_seed = initiator && sessions.size()%2 == 0; } } Iterator it = listeners.iterator(); while( it.hasNext()){ try{ ((NetStatusProtocolTesterListener)it.next()).sessionAdded( this ); }catch( Throwable e ){ Debug.printStackTrace(e); } } connection.connect( true, new NetworkConnection.ConnectionListener() { final String type = initiator?"Outbound":"Inbound"; public final void connectStarted() { log( type + " connect start" ); } public final void connectSuccess( ByteBuffer remaining_initial_data ) { log( type + " connect success" ); connected = true; synchronized( NetStatusProtocolTesterBT.this ){ if ( initiator ){ outbound_connects++; }else{ inbound_connects++; } } connected(); } public final void connectFailure( Throwable e ) { if ( !closing ){ logError( type + " connection fail", e ); } close(); } public final void exceptionThrown( Throwable e ) { if ( !closing ){ logError( type + " connection fail", e ); } close(); } public String getDescription() { return( "NetStatusPlugin - " + type ); } }); } public boolean isInitiator() { return( initiator ); } public boolean isConnected() { return( connected ); } public boolean isSeed() { return( is_seed ); } public boolean isOK() { return( bitfield_received ); } protected void connected() { connection.getIncomingMessageQueue().registerQueueListener( new IncomingMessageQueue.MessageQueueListener() { public boolean messageReceived( Message message ) { try{ String message_id = message.getID(); log( "Incoming message received: " + message.getID()); if ( message_id.equals( BTMessage.ID_BT_HANDSHAKE )){ handshake_received = true; BTHandshake handshake = (BTHandshake)message; info_hash = handshake.getDataHash(); num_pieces = 500 + (info_hash[0]&0xff); // we use the piece at 'n' + 1 to indicate a close request by sending a HAVE for it // this helps us tidily close things if ( num_pieces%8 == 0 ){ num_pieces--; } if ( !is_seed ){ int missing = random.nextInt( num_pieces/2 ) + 5; for (int i=0;i<missing;i++){ missing_pieces.add( new Integer( random.nextInt( num_pieces ))); } } sendHandshake(); sendBitfield(); connection.getIncomingMessageQueue().getDecoder().resumeDecoding(); }else if ( message_id.equals( BTMessage.ID_BT_BITFIELD )){ bitfield_received = true; BTBitfield bitfield = (BTBitfield)message; ByteBuffer bb = bitfield.getBitfield().getBuffer((byte)0); byte[] contents = new byte[bb.remaining()]; bb.get( contents ); }else if ( message_id.equals( BTMessage.ID_BT_HAVE )){ BTHave have = (BTHave)message; if ( have.getPieceNumber() == num_pieces ){ synchronized( sessions ){ closing = true; } close(); } } return( true ); }finally{ message.destroy(); } } public final void protocolBytesReceived( int byte_count ) { } public final void dataBytesReceived( int byte_count ) { } }); connection.getOutgoingMessageQueue().registerQueueListener( new OutgoingMessageQueue.MessageQueueListener() { public final boolean messageAdded( Message message ) { return( true ); } public final void messageQueued( Message message ) { } public final void messageRemoved( Message message ) { } public final void messageSent( Message message ) { log( "Outgoing message sent: " + message.getID()); } public final void protocolBytesSent( int byte_count ) { } public final void dataBytesSent( int byte_count ) { } public void flush() { } }); connection.startMessageProcessing(); if ( initiator ){ sendHandshake(); } } protected void sendHandshake() { if ( !handshake_sent ){ handshake_sent = true; connection.getOutgoingMessageQueue().addMessage( new BTHandshake( info_hash, peer_id, false, BTMessageFactory.MESSAGE_VERSION_INITIAL ), false ); } } protected void sendHave( int piece_number ) { BTHave message = new BTHave( piece_number, BTMessageFactory.MESSAGE_VERSION_INITIAL ); OutgoingMessageQueue out_q = connection.getOutgoingMessageQueue(); out_q.addMessage( message, false ); out_q.flush(); } protected void sendBitfield() { if ( !bitfield_sent ){ bitfield_sent = true; byte[] bits = new byte[( num_pieces + 7 ) /8]; int pos = 0; int i = 0; int bToSend = 0; for (; i <num_pieces; i++ ){ if ((i %8) ==0){ bToSend =0; } bToSend = bToSend << 1; boolean has_piece = !missing_pieces.contains( new Integer(i)); if ( has_piece ){ bToSend += 1; } if ((i %8) ==7){ bits[pos++] = (byte)bToSend; } } if ((i %8) !=0){ bToSend = bToSend << (8 - (i % 8)); bits[pos++] = (byte)bToSend; } DirectByteBuffer buffer = new DirectByteBuffer( ByteBuffer.wrap( bits )); connection.getOutgoingMessageQueue().addMessage( new BTBitfield( buffer, BTMessageFactory.MESSAGE_VERSION_INITIAL ), false ); } } protected void close() { synchronized( sessions ){ sessions.remove( this ); if ( !closing ){ closing = true; }else{ closed = true; } } if ( closed ){ log( "Closing connection" ); connection.close(); }else{ sendHave( num_pieces ); new DelayedEvent( "NetStatus:delayClose", 5000, new AERunnable() { public void runSupport() { if ( !closed ){ close(); } } }); } checkCompletion(); } public String getProtocolString() { String str = ""; if ( connected ){ str = "connected"; str += addSent( "hand", handshake_sent ); str += addRecv( "hand", handshake_received ); str += addSent( "bitf", bitfield_sent ); str += addRecv( "bitf", bitfield_received ); }else{ str = "not connected"; } return( str ); } protected String addSent( String str, boolean ok ) { if ( ok ){ return( ", " + str + " sent" ); }else{ return( ", " + str + " !sent" ); } } protected String addRecv( String str, boolean ok ) { if ( ok ){ return( ", " + str + " recv" ); }else{ return( ", " + str + " !recv" ); } } protected String getLogPrefix() { return( "(" + (initiator?"L":"R") + (is_seed?"S":"L") + " " + session_id + ") " ); } protected void log( String str ) { NetStatusProtocolTesterBT.this.log( getLogPrefix() + str ); } protected void logError( String str ) { NetStatusProtocolTesterBT.this.logError( getLogPrefix() + str ); } protected void logError( String str, Throwable e ) { NetStatusProtocolTesterBT.this.logError( getLogPrefix() + str, e ); } } }