/* * Created on 03-Mar-2005 * Created by Paul Gardner * Copyright (C) 2004, 2005, 2006 Aelitis, 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; either version 2 * of the License, or (at your option) any later version. * 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. * * AELITIS, SAS au capital de 46,603.30 euros * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France. * */ package com.aelitis.azureus.plugins.magnet; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.net.InetSocketAddress; import org.eclipse.swt.graphics.Image; import org.gudy.azureus2.core3.torrent.TOTorrent; import org.gudy.azureus2.core3.torrent.TOTorrentFactory; import org.gudy.azureus2.core3.util.AEMonitor; import org.gudy.azureus2.core3.util.AERunnable; import org.gudy.azureus2.core3.util.AESemaphore; import org.gudy.azureus2.core3.util.AEThread2; import org.gudy.azureus2.core3.util.BEncoder; import org.gudy.azureus2.core3.util.Base32; 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.SystemTime; import org.gudy.azureus2.core3.util.TorrentUtils; import org.gudy.azureus2.core3.util.UrlUtils; import org.gudy.azureus2.plugins.*; import org.gudy.azureus2.plugins.ddb.DistributedDatabase; import org.gudy.azureus2.plugins.ddb.DistributedDatabaseContact; import org.gudy.azureus2.plugins.ddb.DistributedDatabaseEvent; import org.gudy.azureus2.plugins.ddb.DistributedDatabaseListener; import org.gudy.azureus2.plugins.ddb.DistributedDatabaseProgressListener; import org.gudy.azureus2.plugins.ddb.DistributedDatabaseTransferType; import org.gudy.azureus2.plugins.ddb.DistributedDatabaseValue; import org.gudy.azureus2.plugins.download.Download; import org.gudy.azureus2.plugins.download.DownloadException; import org.gudy.azureus2.plugins.torrent.Torrent; import org.gudy.azureus2.plugins.ui.UIInstance; import org.gudy.azureus2.plugins.ui.UIManagerListener; import org.gudy.azureus2.plugins.ui.config.BooleanParameter; import org.gudy.azureus2.plugins.ui.config.ConfigSection; import org.gudy.azureus2.plugins.ui.menus.MenuItem; import org.gudy.azureus2.plugins.ui.menus.MenuItemListener; import org.gudy.azureus2.plugins.ui.model.BasicPluginConfigModel; import org.gudy.azureus2.plugins.ui.tables.TableContextMenuItem; import org.gudy.azureus2.plugins.ui.tables.TableManager; import org.gudy.azureus2.plugins.ui.tables.TableRow; import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader; import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderAdapter; import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderException; import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderFactory; import org.gudy.azureus2.ui.swt.plugins.UISWTInstance; import com.aelitis.azureus.core.util.CopyOnWriteList; import com.aelitis.azureus.core.util.FeatureAvailability; import com.aelitis.net.magneturi.*; /** * @author parg * */ public class MagnetPlugin implements Plugin { private static final String SECONDARY_LOOKUP = "http://magnet.vuze.com/"; private static final int SECONDARY_LOOKUP_DELAY = 20*1000; private static final int SECONDARY_LOOKUP_MAX_TIME = 2*60*1000; private static final String PLUGIN_NAME = "Magnet URI Handler"; private static final String PLUGIN_CONFIGSECTION_ID = "plugins.magnetplugin"; private PluginInterface plugin_interface; private CopyOnWriteList listeners = new CopyOnWriteList(); private boolean first_download = true; private BooleanParameter secondary_lookup; public static void load( PluginInterface plugin_interface ) { plugin_interface.getPluginProperties().setProperty( "plugin.version", "1.0" ); plugin_interface.getPluginProperties().setProperty( "plugin.name", PLUGIN_NAME ); } public void initialize( PluginInterface _plugin_interface ) { plugin_interface = _plugin_interface; BasicPluginConfigModel config = plugin_interface.getUIManager().createBasicPluginConfigModel( ConfigSection.SECTION_PLUGINS, PLUGIN_CONFIGSECTION_ID); secondary_lookup = config.addBooleanParameter2( "MagnetPlugin.use.lookup.service", "MagnetPlugin.use.lookup.service", true ); MenuItemListener listener = new MenuItemListener() { public void selected( MenuItem _menu, Object _target ) { Download download = (Download)((TableRow)_target).getDataSource(); if ( download == null || download.getTorrent() == null ){ return; } Torrent torrent = download.getTorrent(); String cb_data = "magnet:?xt=urn:btih:" + Base32.encode( torrent.getHash()); // removed this as well - nothing wrong with allowing magnet copy // for private torrents - they still can't be tracked if you don't // have permission /*if ( torrent.isPrivate()){ cb_data = getMessageText( "private_torrent" ); }else if ( torrent.isDecentralised()){ */ // ok /* relaxed this as we allow such torrents to be downloaded via magnet links * (as opposed to tracked in the DHT) }else if ( torrent.isDecentralisedBackupEnabled()){ TorrentAttribute ta_peer_sources = plugin_interface.getTorrentManager().getAttribute( TorrentAttribute.TA_PEER_SOURCES ); String[] sources = download.getListAttribute( ta_peer_sources ); boolean ok = false; for (int i=0;i<sources.length;i++){ if ( sources[i].equalsIgnoreCase( "DHT")){ ok = true; break; } } if ( !ok ){ cb_data = getMessageText( "decentral_disabled" ); } }else{ cb_data = getMessageText( "decentral_backup_disabled" ); */ // } // System.out.println( "MagnetPlugin: export = " + url ); try{ plugin_interface.getUIManager().copyToClipBoard( cb_data ); }catch( Throwable e ){ e.printStackTrace(); } } }; final TableContextMenuItem menu1 = plugin_interface.getUIManager().getTableManager().addContextMenuItem(TableManager.TABLE_MYTORRENTS_INCOMPLETE, "MagnetPlugin.contextmenu.exporturi" ); final TableContextMenuItem menu2 = plugin_interface.getUIManager().getTableManager().addContextMenuItem(TableManager.TABLE_MYTORRENTS_COMPLETE, "MagnetPlugin.contextmenu.exporturi" ); menu1.addListener( listener ); menu2.addListener( listener ); MagnetURIHandler.getSingleton().addListener( new MagnetURIHandlerListener() { public byte[] badge() { InputStream is = getClass().getClassLoader().getResourceAsStream( "com/aelitis/azureus/plugins/magnet/Magnet.gif" ); if ( is == null ){ return( null ); } try{ ByteArrayOutputStream baos = new ByteArrayOutputStream(); try{ byte[] buffer = new byte[8192]; while( true ){ int len = is.read( buffer ); if ( len <= 0 ){ break; } baos.write( buffer, 0, len ); } }finally{ is.close(); } return( baos.toByteArray()); }catch( Throwable e ){ Debug.printStackTrace(e); return( null ); } } public byte[] download( final MagnetURIHandlerProgressListener muh_listener, final byte[] hash, final String args, final InetSocketAddress[] sources, final long timeout ) throws MagnetURIHandlerException { // see if we've already got it! try{ Download dl = plugin_interface.getDownloadManager().getDownload( hash ); if ( dl != null ){ Torrent torrent = dl.getTorrent(); if ( torrent != null ){ return( torrent.writeToBEncodedData()); } } }catch( Throwable e ){ Debug.printStackTrace(e); } return( MagnetPlugin.this.download( new MagnetPluginProgressListener() { public void reportSize( long size ) { muh_listener.reportSize( size ); } public void reportActivity( String str ) { muh_listener.reportActivity( str ); } public void reportCompleteness( int percent ) { muh_listener.reportCompleteness( percent ); } public void reportContributor( InetSocketAddress address ) { } public boolean verbose() { return( muh_listener.verbose()); } }, hash, args, sources, timeout )); } public boolean download( URL url ) throws MagnetURIHandlerException { try{ plugin_interface.getDownloadManager().addDownload( url, false ); return( true ); }catch( DownloadException e ){ throw( new MagnetURIHandlerException( "Operation failed", e )); } } public boolean set( String name, Map values ) { List l = listeners.getList(); for (int i=0;i<l.size();i++){ if (((MagnetPluginListener)l.get(i)).set( name, values )){ return( true ); } } return( false ); } public int get( String name, Map values ) { List l = listeners.getList(); for (int i=0;i<l.size();i++){ int res = ((MagnetPluginListener)l.get(i)).get( name, values ); if ( res != Integer.MIN_VALUE ){ return( res ); } } return( Integer.MIN_VALUE ); } }); plugin_interface.addListener( new PluginListener() { public void initializationComplete() { // make sure DDB is initialised as we need it to register its // transfer types AEThread2 t = new AEThread2( "MagnetPlugin:init", true ) { public void run() { plugin_interface.getDistributedDatabase(); } }; t.start(); } public void closedownInitiated(){} public void closedownComplete(){} }); plugin_interface.getUIManager().addUIListener( new UIManagerListener() { public void UIAttached( UIInstance instance ) { if ( instance instanceof UISWTInstance ){ UISWTInstance swt = (UISWTInstance)instance; Image image = swt.loadImage( "com/aelitis/azureus/plugins/magnet/icons/magnet.gif" ); menu1.setGraphic( swt.createGraphic( image )); menu2.setGraphic( swt.createGraphic( image )); } } public void UIDetached( UIInstance instance ) { } }); } public URL getMagnetURL( Download d ) { Torrent torrent = d.getTorrent(); if ( torrent == null ){ return( null ); } return( getMagnetURL( torrent.getHash())); } public URL getMagnetURL( byte[] hash ) { try{ return( new URL( "magnet:?xt=urn:btih:" + Base32.encode(hash))); }catch( Throwable e ){ Debug.printStackTrace(e); return( null ); } } public byte[] badge() { return( null ); } public byte[] download( MagnetPluginProgressListener listener, byte[] hash, String args, InetSocketAddress[] sources, long timeout ) throws MagnetURIHandlerException { byte[] torrent_data = downloadSupport( listener, hash, args, sources, timeout ); if ( args != null ){ String[] bits = args.split( "&" ); List<String> new_web_seeds = new ArrayList<String>(); for ( String bit: bits ){ String[] x = bit.split( "=" ); if ( x.length == 2 ){ if ( x[0].equalsIgnoreCase( "ws" )){ try{ new_web_seeds.add( new URL( UrlUtils.decode( x[1] )).toExternalForm()); }catch( Throwable e ){ } } } } if ( new_web_seeds.size() > 0 ){ try{ TOTorrent torrent = TOTorrentFactory.deserialiseFromBEncodedByteArray( torrent_data ); Object obj = torrent.getAdditionalProperty( "url-list" ); List<String> existing = new ArrayList<String>(); if ( obj instanceof byte[] ){ try{ new_web_seeds.remove( new URL( new String((byte[])obj, "UTF-8" )).toExternalForm()); }catch( Throwable e ){ } }else if ( obj instanceof List ){ List<byte[]> l = (List<byte[]>)obj; for ( byte[] b: l ){ try{ existing.add( new URL( new String((byte[])b, "UTF-8" )).toExternalForm()); }catch( Throwable e ){ } } } boolean update = false; for ( String e: new_web_seeds ){ if ( !existing.contains( e )){ existing.add( e ); update = true; } } if ( update ){ List<byte[]> l = new ArrayList<byte[]>(); for ( String s: existing ){ l.add( s.getBytes( "UTF-8" )); } torrent.setAdditionalProperty( "url-list", l ); torrent_data = BEncoder.encode( torrent.serialiseToMap()); } }catch( Throwable e ){ } } } return( torrent_data ); } private byte[] downloadSupport( final MagnetPluginProgressListener listener, final byte[] hash, final String args, final InetSocketAddress[] sources, final long timeout ) throws MagnetURIHandlerException { try{ if ( first_download ){ listener.reportActivity( getMessageText( "report.waiting_ddb" )); first_download = false; } final DistributedDatabase db = plugin_interface.getDistributedDatabase(); final List potential_contacts = new ArrayList(); final AESemaphore potential_contacts_sem = new AESemaphore( "MagnetPlugin:liveones" ); final AEMonitor potential_contacts_mon = new AEMonitor( "MagnetPlugin:liveones" ); final int[] outstanding = {0}; final boolean[] lookup_complete = {false}; listener.reportActivity( getMessageText( "report.searching" )); DistributedDatabaseListener ddb_listener = new DistributedDatabaseListener() { private Set found_set = new HashSet(); public void event( DistributedDatabaseEvent event ) { int type = event.getType(); if ( type == DistributedDatabaseEvent.ET_OPERATION_STARTS ){ // give live results a chance before kicking in explicit ones if ( sources.length > 0 ){ new DelayedEvent( "MP:sourceAdd", 10*1000, new AERunnable() { public void runSupport() { addExplicitSources(); } }); } }else if ( type == DistributedDatabaseEvent.ET_VALUE_READ ){ contactFound( event.getValue().getContact()); }else if ( type == DistributedDatabaseEvent.ET_OPERATION_COMPLETE || type == DistributedDatabaseEvent.ET_OPERATION_TIMEOUT ){ listener.reportActivity( getMessageText( "report.found", String.valueOf( found_set.size()))); // now inject any explicit sources addExplicitSources(); try{ potential_contacts_mon.enter(); lookup_complete[0] = true; }finally{ potential_contacts_mon.exit(); } potential_contacts_sem.release(); } } protected void addExplicitSources() { for (int i=0;i<sources.length;i++){ try{ contactFound( db.importContact(sources[i])); }catch( Throwable e ){ Debug.printStackTrace(e); } } } public void contactFound( final DistributedDatabaseContact contact ) { String key = contact.getAddress().toString(); synchronized( found_set ){ if ( found_set.contains( key )){ return; } found_set.add( key ); } if ( listener.verbose()){ listener.reportActivity( getMessageText( "report.found", contact.getName())); } try{ potential_contacts_mon.enter(); outstanding[0]++; }finally{ potential_contacts_mon.exit(); } contact.isAlive( 20*1000, new DistributedDatabaseListener() { public void event( DistributedDatabaseEvent event) { try{ boolean alive = event.getType() == DistributedDatabaseEvent.ET_OPERATION_COMPLETE; if ( listener.verbose()){ listener.reportActivity( getMessageText( alive?"report.alive":"report.dead", contact.getName())); } try{ potential_contacts_mon.enter(); Object[] entry = new Object[]{ new Boolean( alive ), contact}; boolean added = false; if ( alive ){ // try and place before first dead entry for (int i=0;i<potential_contacts.size();i++){ if (!((Boolean)((Object[])potential_contacts.get(i))[0]).booleanValue()){ potential_contacts.add(i, entry ); added = true; break; } } } if ( !added ){ potential_contacts.add( entry ); // dead at end } }finally{ potential_contacts_mon.exit(); } }finally{ try{ potential_contacts_mon.enter(); outstanding[0]--; }finally{ potential_contacts_mon.exit(); } potential_contacts_sem.release(); } } }); } }; db.read( ddb_listener, db.createKey( hash, "Torrent download lookup for '" + ByteFormatter.encodeString( hash ) + "'" ), timeout, DistributedDatabase.OP_EXHAUSTIVE_READ | DistributedDatabase.OP_PRIORITY_HIGH ); long remaining = timeout; long overall_start = SystemTime.getMonotonousTime(); boolean sl_enabled = secondary_lookup.getValue() && FeatureAvailability.isMagnetSLEnabled(); long secondary_lookup_time = -1; long last_found = -1; final Object[] secondary_result = { null }; while( remaining > 0 ){ try{ potential_contacts_mon.enter(); if ( lookup_complete[0] && potential_contacts.size() == 0 && outstanding[0] == 0 ){ break; } }finally{ potential_contacts_mon.exit(); } while( remaining > 0 ){ long wait_start = SystemTime.getMonotonousTime(); boolean got_sem = potential_contacts_sem.reserve( 1000 ); long now = SystemTime.getMonotonousTime(); remaining -= ( now - wait_start ); if ( got_sem ){ last_found = now; break; }else{ if ( sl_enabled ){ if ( secondary_lookup_time == -1 ){ long base_time; if ( last_found == -1 || now - overall_start > 60*1000 ){ base_time = overall_start; }else{ base_time = last_found; } long time_so_far = now - base_time; if ( time_so_far > SECONDARY_LOOKUP_DELAY ){ secondary_lookup_time = SystemTime.getMonotonousTime(); doSecondaryLookup( listener, secondary_result, hash, args ); } }else{ try{ byte[] torrent = getSecondaryLookupResult( secondary_result ); if ( torrent != null ){ return( torrent ); } }catch( ResourceDownloaderException e ){ // ignore, we just continue processing } } } continue; } } DistributedDatabaseContact contact; boolean live_contact; try{ potential_contacts_mon.enter(); // System.out.println( "rem=" + remaining + ",pot=" + potential_contacts.size() + ",out=" + outstanding[0] ); if ( potential_contacts.size() == 0 ){ if ( outstanding[0] == 0 ){ break; }else{ continue; } }else{ Object[] entry = (Object[])potential_contacts.remove(0); live_contact = ((Boolean)entry[0]).booleanValue(); contact = (DistributedDatabaseContact)entry[1]; } }finally{ potential_contacts_mon.exit(); } // System.out.println( "magnetDownload: " + contact.getName() + ", live = " + live_contact ); if ( !live_contact ){ listener.reportActivity( getMessageText( "report.tunnel", contact.getName())); contact.openTunnel(); } try{ listener.reportActivity( getMessageText( "report.downloading", contact.getName())); DistributedDatabaseValue value = contact.read( new DistributedDatabaseProgressListener() { public void reportSize( long size ) { listener.reportSize( size ); } public void reportActivity( String str ) { listener.reportActivity( str ); } public void reportCompleteness( int percent ) { listener.reportCompleteness( percent ); } }, db.getStandardTransferType( DistributedDatabaseTransferType.ST_TORRENT ), db.createKey ( hash , "Torrent download content for '" + ByteFormatter.encodeString( hash ) + "'"), timeout ); if ( value != null ){ // let's verify the torrent byte[] data = (byte[])value.getValue(byte[].class); try{ TOTorrent torrent = TOTorrentFactory.deserialiseFromBEncodedByteArray( data ); if ( Arrays.equals( hash, torrent.getHash())){ listener.reportContributor( contact.getAddress()); return( data ); }else{ listener.reportActivity( getMessageText( "report.error", "torrent invalid (hash mismatch)" )); } }catch( Throwable e ){ listener.reportActivity( getMessageText( "report.error", "torrent invalid (decode failed)" )); } } }catch( Throwable e ){ listener.reportActivity( getMessageText( "report.error", Debug.getNestedExceptionMessage(e))); Debug.printStackTrace(e); } } if ( sl_enabled ){ if ( secondary_lookup_time == -1 ){ secondary_lookup_time = SystemTime.getMonotonousTime(); doSecondaryLookup(listener, secondary_result, hash, args ); } while( SystemTime.getMonotonousTime() - secondary_lookup_time < SECONDARY_LOOKUP_MAX_TIME ){ try{ byte[] torrent = getSecondaryLookupResult( secondary_result ); if ( torrent != null ){ return( torrent ); } Thread.sleep( 500 ); }catch( ResourceDownloaderException e ){ break; } } } return( null ); // nothing found }catch( Throwable e ){ Debug.printStackTrace(e); listener.reportActivity( getMessageText( "report.error", Debug.getNestedExceptionMessage(e))); throw( new MagnetURIHandlerException( "MagnetURIHandler failed", e )); } } protected void doSecondaryLookup( final MagnetPluginProgressListener listener, final Object[] result, byte[] hash, String args ) { listener.reportActivity( getMessageText( "report.secondarylookup", null )); try{ ResourceDownloaderFactory rdf = plugin_interface.getUtilities().getResourceDownloaderFactory(); URL sl_url = new URL( SECONDARY_LOOKUP + "magnetLookup?hash=" + Base32.encode( hash ) + (args.length()==0?"":("&args=" + UrlUtils.encode( args )))); ResourceDownloader rd = rdf.create( sl_url ); rd.addListener( new ResourceDownloaderAdapter() { public boolean completed( ResourceDownloader downloader, InputStream data ) { listener.reportActivity( getMessageText( "report.secondarylookup.ok", null )); synchronized( result ){ result[0] = data; } return( true ); } public void failed( ResourceDownloader downloader, ResourceDownloaderException e ) { synchronized( result ){ result[0] = e; } listener.reportActivity( getMessageText( "report.secondarylookup.fail" )); } }); rd.asyncDownload(); }catch( Throwable e ){ listener.reportActivity( getMessageText( "report.secondarylookup.fail", Debug.getNestedExceptionMessage( e ) )); } } protected byte[] getSecondaryLookupResult( final Object[] result ) throws ResourceDownloaderException { Object x; synchronized( result ){ x = result[0]; result[0] = null; } if ( x instanceof InputStream ){ InputStream is = (InputStream)x; try{ TOTorrent t = TOTorrentFactory.deserialiseFromBEncodedInputStream( is ); TorrentUtils.setPeerCacheValid( t ); return( BEncoder.encode( t.serialiseToMap())); }catch( Throwable e ){ } }else if ( x instanceof ResourceDownloaderException ){ throw((ResourceDownloaderException)x); } return( null ); } protected String getMessageText( String resource ) { return( plugin_interface.getUtilities().getLocaleUtilities().getLocalisedMessageText( "MagnetPlugin." + resource )); } protected String getMessageText( String resource, String param ) { return( plugin_interface.getUtilities().getLocaleUtilities().getLocalisedMessageText( "MagnetPlugin." + resource, new String[]{ param })); } public void addListener( MagnetPluginListener listener ) { listeners.add( listener ); } public void removeListener( MagnetPluginListener listener ) { listeners.remove( listener ); } }