/* * Created on May 30, 2014 * Created by Paul Gardner * * Copyright 2014 Azureus Software, 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.tracker.dht; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.*; import org.gudy.azureus2.core3.util.AERunnable; import org.gudy.azureus2.core3.util.AEThread2; import org.gudy.azureus2.core3.util.AsyncDispatcher; import org.gudy.azureus2.core3.util.BDecoder; import org.gudy.azureus2.core3.util.BEncoder; import org.gudy.azureus2.core3.util.ByteArrayHashMap; import org.gudy.azureus2.core3.util.ByteFormatter; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.DisplayFormatters; import org.gudy.azureus2.core3.util.RandomUtils; import org.gudy.azureus2.core3.util.SimpleTimer; import org.gudy.azureus2.core3.util.SystemTime; import org.gudy.azureus2.core3.util.TimerEvent; import org.gudy.azureus2.core3.util.TimerEventPerformer; import org.gudy.azureus2.core3.util.TimerEventPeriodic; import com.aelitis.azureus.core.dht.transport.DHTTransportAlternativeContact; import com.aelitis.azureus.core.dht.transport.DHTTransportAlternativeNetwork; import com.aelitis.azureus.core.dht.transport.udp.impl.DHTUDPUtils; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin; public class DHTTrackerPluginAlt { private static final long startup_time = SystemTime.getMonotonousTime(); private static final int startup_grace = 60*1000; private static final int INITAL_DELAY = 5*1000; private static final int RPC_TIMEOUT = 15*1000; private static final int LOOKUP_TIMEOUT = 90*1000; private static final int LOOKUP_LINGER = 5*1000; private static final int CONC_LOOKUPS = 8; private static final int NUM_WANT = 32; private static final int NID_CLOSENESS_LIMIT = 10; private final byte[] NID = new byte[20]; private DatagramSocket current_server; private ByteArrayHashMap<Object[]> tid_map = new ByteArrayHashMap<Object[]>(); private TimerEventPeriodic timer_event; private AsyncDispatcher dispatcher = new AsyncDispatcher(); private volatile long lookup_count; private volatile long hit_count; private volatile long packets_out; private volatile long packets_in; private volatile long bytes_out; private volatile long bytes_in; protected DHTTrackerPluginAlt() { // there is no node id restriction for requests RandomUtils.nextBytes( NID ); } private DatagramSocket getServer() { synchronized( this ){ if ( current_server != null ){ if ( current_server.isClosed()){ current_server = null; }else{ return( current_server ); } } try{ final DatagramSocket server = new DatagramSocket(null); server.setReuseAddress(true); InetAddress bind_ip = NetworkAdmin.getSingleton().getSingleHomedServiceBindAddress(); if ( bind_ip == null ){ bind_ip = InetAddress.getByName( "127.0.0.1" ); } server.bind( new InetSocketAddress(bind_ip, 0)); current_server = server; new AEThread2( "DHTPluginAlt:server" ) { public void run() { try{ while( true ){ byte[] buffer = new byte[5120]; DatagramPacket packet = new DatagramPacket( buffer, buffer.length ); server.receive( packet ); packets_in++; bytes_in += packet.getLength(); Map<String, Object> map = new BDecoder().decodeByteArray(packet.getData(), 0, packet.getLength() ,false); //System.out.println( "got " + map ); byte[] tid = (byte[])map.get( "t" ); if ( tid != null ){ Object[] task; synchronized( tid_map ){ task = tid_map.remove( tid ); } if ( task != null ){ ((GetPeersTask)task[0]).handleReply((InetSocketAddress)packet.getSocketAddress(), tid, map ); } } } }catch( Throwable e ){ }finally{ try{ server.close(); }catch( Throwable f ){ } synchronized( DHTTrackerPluginAlt.this ){ if ( current_server == server ){ current_server = null; } } } } }.start(); return( server ); }catch( Throwable e ){ return( null ); } } } protected void get( final byte[] hash, final boolean no_seeds, final LookupListener listener ) { SimpleTimer.addEvent( "altlookup.delay", SystemTime.getCurrentTime() + INITAL_DELAY, new TimerEventPerformer() { public void perform( TimerEvent event) { if ( listener.isComplete()){ return; } if ( dispatcher.getQueueSize() > 100 ){ return; } dispatcher.dispatch( new AERunnable() { @Override public void runSupport() { getSupport( hash, no_seeds, listener ); } }); } }); } private void getSupport( final byte[] hash, final boolean no_seeds, final LookupListener listener ) { List<DHTTransportAlternativeContact> contacts; while( true ){ if ( listener.isComplete()){ return; } contacts = DHTUDPUtils.getAlternativeContacts( DHTTransportAlternativeNetwork.AT_MLDHT_IPV4, 16 ); if ( contacts.size() == 0 ){ long now = SystemTime.getMonotonousTime(); if ( now - startup_time < startup_grace ){ try{ Thread.sleep(5000); }catch( Throwable e ){ } continue; } return; }else{ break; } } DatagramSocket server = getServer(); if ( server == null ){ return; } lookup_count++; new GetPeersTask( server, contacts, hash, no_seeds, listener ); } private byte[] send( GetPeersTask task, DatagramSocket server, InetSocketAddress address, Map<String,Object> map ) throws IOException { byte[] tid; while( true ){ tid = new byte[4]; RandomUtils.nextBytes( tid ); synchronized( tid_map ){ if ( tid_map.containsKey( tid )){ continue; } tid_map.put( tid, new Object[]{ task, SystemTime.getMonotonousTime() }); if ( timer_event == null ){ timer_event = SimpleTimer.addPeriodicEvent( "dhtalttimer", 2500, new TimerEventPerformer() { public void perform( TimerEvent event) { checkTimeouts(); synchronized( tid_map ){ if ( tid_map.size() == 0 ){ timer_event.cancel(); timer_event = null; } } } }); } } try{ map.put( "t", tid ); // System.out.println( "Sending: " + map ); byte[] data_out = BEncoder.encode( map ); DatagramPacket packet = new DatagramPacket( data_out, data_out.length); packet.setSocketAddress( address ); packets_out++; bytes_out += data_out.length; server.send( packet ); return( tid ); }catch( Throwable e ){ try{ server.close(); }catch( Throwable f ){ } synchronized( tid_map ){ tid_map.remove( tid ); } if ( e instanceof IOException ){ throw((IOException)e); }else{ throw(new IOException(Debug.getNestedExceptionMessage(e))); } } } } private void checkTimeouts() { long now = SystemTime.getMonotonousTime(); List<Object[]> timeouts = null; synchronized( tid_map ){ Iterator<byte[]> it = tid_map.keys().iterator(); while( it.hasNext()){ byte[] key = it.next(); Object[] value = tid_map.get( key ); long time = (Long)value[1]; if ( now - time > RPC_TIMEOUT ){ tid_map.remove( key ); if ( timeouts == null ){ timeouts = new ArrayList<Object[]>(); } timeouts.add(new Object[]{ key, value[0] }); } } } if ( timeouts != null ){ for ( Object[] entry: timeouts ){ try{ ((GetPeersTask)entry[1]).handleTimeout((byte[])entry[0]); }catch( Throwable e ){ Debug.out( e ); } } } } protected String getString() { return( "lookups=" + lookup_count + ", hits=" + hit_count + ", out=" + packets_out + "/" + DisplayFormatters.formatByteCountToKiBEtc( bytes_out ) + ", in=" + packets_in + "/" + DisplayFormatters.formatByteCountToKiBEtc( bytes_in )); } private class GetPeersTask { private long start_time = SystemTime.getMonotonousTime(); private DatagramSocket server; private byte[] torrent_hash; private boolean no_seeds; private LookupListener listener; private List<DHTTransportAlternativeContact> initial_contacts; private ByteArrayHashMap<InetSocketAddress> active_queries = new ByteArrayHashMap<InetSocketAddress>(); private Set<InetSocketAddress> queried_nodes = new HashSet<InetSocketAddress>(); Comparator<byte[]> comparator = new Comparator<byte[]>() { public int compare( byte[] o1, byte[] o2) { for ( int i=0; i < o1.length;i++ ){ byte b1 = o1[i]; byte b2 = o2[i]; if ( b1 == b2 ){ continue; } byte t = torrent_hash[i]; int d1 = (b1^t)&0xff; int d2 = (b2^t)&0xff; if ( d1 == d2 ){ continue; } if ( d1 < d2 ){ return( -1 ); }else{ return( 1 ); } } return( 0 ); } }; private TreeMap<byte[],InetSocketAddress> to_query = new TreeMap<byte[], InetSocketAddress>( comparator ); private TreeMap<byte[],InetSocketAddress> heard_from = new TreeMap<byte[], InetSocketAddress>( new Comparator<byte[]>() { public int compare( byte[] o1, byte[] o2) { return( -comparator.compare(o1, o2)); } }); private long found_peer_time; private Set<InetSocketAddress> found_peers = new HashSet<InetSocketAddress>(); private int query_count; private int timeout_count; private int reply_count; private boolean failed; private GetPeersTask( DatagramSocket _server, List<DHTTransportAlternativeContact> _contacts, byte[] _torrent_hash, boolean _no_seeds, LookupListener _listener ) { server = _server; torrent_hash = _torrent_hash; no_seeds = _no_seeds; listener = _listener; initial_contacts = _contacts; tryQuery(); } private void search( InetSocketAddress address ) throws IOException { if ( queried_nodes.contains( address )){ return; } queried_nodes.add( address ); Map<String,Object> map = new HashMap<String,Object>(); map.put( "q", "get_peers" ); map.put( "y", "q" ); Map<String,Object> args = new HashMap<String,Object>(); map.put( "a", args ); args.put( "id", NID ); args.put( "info_hash", torrent_hash ); args.put( "noseed", new Long( no_seeds?1:0 )); byte[] tid = send( this, server, address, map ); query_count++; active_queries.put( tid, address ); } private void tryQuery() { if ( listener.isComplete()){ return; } try{ synchronized( this ){ if ( failed || active_queries.size() >= CONC_LOOKUPS ){ return; } long now = SystemTime.getMonotonousTime(); if ( now - start_time > LOOKUP_TIMEOUT ){ return; } if ( found_peer_time > 0 ){ if ( found_peers.size() > NUM_WANT ){ return; } if ( now - found_peer_time > LOOKUP_LINGER ){ return; } } byte[] limit_nid; if ( heard_from.size() >= NID_CLOSENESS_LIMIT ){ limit_nid = heard_from.keySet().iterator().next(); }else{ limit_nid = null; } Iterator<Map.Entry<byte[],InetSocketAddress>> query_it = to_query.entrySet().iterator(); while( query_it.hasNext()){ Map.Entry<byte[],InetSocketAddress> entry = query_it.next(); query_it.remove(); byte[] nid = entry.getKey(); if ( limit_nid != null && comparator.compare( limit_nid, nid ) <= 0 ){ // System.out.println( "skipping " + ByteFormatter.encodeString( nid ) + ": limit=" + ByteFormatter.encodeString( limit_nid ) + "/" + ByteFormatter.encodeString( torrent_hash )); continue; } // System.out.println( "searching " + ByteFormatter.encodeString( nid )); InetSocketAddress address = entry.getValue(); search( address ); if ( active_queries.size() >= CONC_LOOKUPS ){ return; } } if ( heard_from.size() < 10 ){ Iterator<DHTTransportAlternativeContact> contact_it = initial_contacts.iterator(); while( contact_it.hasNext()){ DHTTransportAlternativeContact contact = contact_it.next(); contact_it.remove(); Map<String,Object> properties = contact.getProperties(); byte[] _a = (byte[])properties.get( "a" ); Long _p = (Long)properties.get( "p" ); if ( _a != null && _p != null ){ try{ InetSocketAddress address = new InetSocketAddress( InetAddress.getByAddress( _a ), _p.intValue()); search( address ); if ( active_queries.size() >= CONC_LOOKUPS ){ return; } }catch( Throwable e ){ } } } } } }catch( Throwable e ){ failed = true; }finally{ // log(); } } private void handleTimeout( byte[] tid ) { synchronized( this ){ active_queries.remove( tid ); timeout_count++; } tryQuery(); } private void handleReply( InetSocketAddress from, byte[] tid, Map<String,Object> map ) throws IOException { Map<String,Object> reply = (Map<String,Object>)map.get( "r" ); synchronized( this ){ active_queries.remove( tid ); reply_count++; if ( reply == null ){ // error response, ignore return; } heard_from.put((byte[])reply.get( "id" ), from ); if ( heard_from.size() > NID_CLOSENESS_LIMIT ){ Iterator<byte[]> it = heard_from.keySet().iterator(); it.next(); it.remove(); } } ArrayList<byte[]> values = ( ArrayList<byte[]>)reply.get( "values" ); if ( values != null ){ synchronized( this ){ if ( found_peer_time == 0 ){ found_peer_time = SystemTime.getMonotonousTime(); } } for ( byte[] value: values ){ try{ ByteBuffer bb = ByteBuffer.wrap( value ); byte[] address = new byte[value.length-2]; bb.get( address ); int port = bb.getShort()&0xffff; InetSocketAddress addr = new InetSocketAddress( InetAddress.getByAddress(address), port ); synchronized( this ){ if ( found_peers.contains( addr )){ continue; } found_peers.add( addr ); } hit_count++; listener.foundPeer( addr ); }catch( Throwable e ){ } } } byte[] nodes = (byte[])reply.get( "nodes" ); byte[] nodes6 = (byte[])reply.get( "nodes6" ); if ( nodes != null ){ int entry_size = 20+4+2; for ( int i=0;i<nodes.length;i+=entry_size ){ ByteBuffer bb = ByteBuffer.wrap(nodes, i, entry_size ); byte[] nid = new byte[20]; bb.get(nid); byte[] address = new byte[ 4 ]; bb.get( address ); int port = bb.getShort()&0xffff; try{ InetSocketAddress addr = new InetSocketAddress( InetAddress.getByAddress(address), port ); synchronized( this ){ if ( !queried_nodes.contains( addr )){ to_query.put( nid, addr ); } } }catch( Throwable e ){ } } tryQuery(); }else{ // log(); } } private void log() { System.out.println( ByteFormatter.encodeString( torrent_hash ) + ": send=" + query_count + ", recv=" + reply_count + ", t/o=" + timeout_count + ", elapsed=" + (SystemTime.getMonotonousTime() - start_time ) + ", toq=" + to_query.size() + ", found=" + found_peers.size()); synchronized( this ){ for ( byte[] nid: heard_from.keySet()){ System.out.println( " " + ByteFormatter.encodeString( nid )); } } } } protected interface LookupListener { public void foundPeer( InetSocketAddress address ); public boolean isComplete(); } }