/* * 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 org.gudy.azureus2.pluginsimpl.local.ddb; import java.util.*; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.gudy.azureus2.core3.download.DownloadManager; import org.gudy.azureus2.core3.logging.LogAlert; import org.gudy.azureus2.core3.logging.Logger; import org.gudy.azureus2.core3.peer.PEPeerSource; import org.gudy.azureus2.core3.util.*; import org.gudy.azureus2.plugins.PluginInterface; import org.gudy.azureus2.plugins.ddb.*; import org.gudy.azureus2.plugins.download.Download; import org.gudy.azureus2.plugins.torrent.Torrent; import org.gudy.azureus2.plugins.torrent.TorrentAttribute; import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils; import org.gudy.azureus2.pluginsimpl.local.PluginInitializer; import com.aelitis.azureus.plugins.dht.DHTPluginProgressListener; /** * @author parg * */ public class DDBaseTTTorrent implements DistributedDatabaseTransferType, DistributedDatabaseTransferHandler { private static final boolean TRACE = false; private static final byte CRYPTO_VERSION = 1; static{ if ( TRACE ){ System.out.println( "**** Torrent xfer tracing on ****" ); } } private DDBaseImpl ddb; private TorrentAttribute ta_sha1; private boolean crypto_tested; private boolean crypto_available; private List external_downloads; private Map data_cache = new LinkedHashMap(5,0.75f,true) { protected boolean removeEldestEntry( Map.Entry eldest) { return size() > 5; } }; protected DDBaseTTTorrent( DDBaseImpl _ddb ) { ddb = _ddb; } public void addDownload( Download download ) { synchronized( this ){ if ( external_downloads == null ){ external_downloads = new ArrayList(); } external_downloads.add( download ); } } public void removeDownload( Download download ) { synchronized( this ){ if ( external_downloads != null ){ external_downloads.remove( download ); if ( external_downloads.size() == 0 ){ external_downloads = null; } } } } // server side read public DistributedDatabaseValue read( DistributedDatabaseContact contact, DistributedDatabaseTransferType type, DistributedDatabaseKey key ) throws DistributedDatabaseException { // We use sha1(hash) as the key for torrent downloads // and encrypt the torrent content using the hash as the basis for a key. This // prevents someone without the hash from downloading the torrent try{ byte[] search_key = ((DDBaseKeyImpl)key).getBytes(); Download download = null; PluginInterface pi = PluginInitializer.getDefaultInterface(); String search_sha1 = pi.getUtilities().getFormatters().encodeBytesToString( search_key ); if ( ta_sha1 == null ){ ta_sha1 = pi.getTorrentManager().getPluginAttribute( "DDBaseTTTorrent::sha1"); } // gotta look for the sha1(hash) Download[] downloads = pi.getDownloadManager().getDownloads(); for (int i=0;i<downloads.length;i++){ Download dl = downloads[i]; if ( dl.getTorrent() == null ){ continue; } String sha1 = dl.getAttribute( ta_sha1 ); if ( sha1 == null ){ sha1 = pi.getUtilities().getFormatters().encodeBytesToString( new SHA1Simple().calculateHash( dl.getTorrent().getHash())); dl.setAttribute( ta_sha1, sha1 ); } if ( sha1.equals( search_sha1 )){ download = dl; break; } } if ( download == null ){ synchronized( this ){ if ( external_downloads != null ){ for (int i=0;i<external_downloads.size();i++){ Download dl = (Download)external_downloads.get(i); if ( dl.getTorrent() == null ){ continue; } String sha1 = dl.getAttribute( ta_sha1 ); if ( sha1 == null ){ sha1 = pi.getUtilities().getFormatters().encodeBytesToString( new SHA1Simple().calculateHash( dl.getTorrent().getHash())); dl.setAttribute( ta_sha1, sha1 ); } if ( sha1.equals( search_sha1 )){ download = dl; break; } } } } } String originator = contact.getName(); if ( download == null ){ String msg = "TorrentDownload: request from " + originator + " for '" + pi.getUtilities().getFormatters().encodeBytesToString( search_key ) + "' not found"; if ( TRACE ){ System.out.println( msg ); } ddb.log( msg ); // torrent not found - probably been removed whilst info still published in DHT return( null ); } Torrent torrent = download.getTorrent(); if ( torrent.isPrivate()){ Debug.out( "Attempt to download private torrent" ); ddb.log( "TorrentDownload: request from " + originator + " for '" + download.getName() + "' denied as it is private" ); // should never happen as private torrents are not tracked so they can't be found for // download return( null ); } try{ // apparently there are some trackers using non-private torrents with passkeys. Crazy, however to give users at // least the opportunity to prevent .torrent transfer for these torrents we deny this if the DHT peer source has // been disabled by the user DownloadManager dm = PluginCoreUtils.unwrapIfPossible( download ); if ( dm != null && !dm.getDownloadState().isPeerSourceEnabled( PEPeerSource.PS_DHT )){ ddb.log( "TorrentDownload: request from " + originator + " for '" + download.getName() + "' denied as DHT peer source disabled" ); return( null ); } }catch( Throwable e ){ Debug.out( e ); } String msg = "TorrentDownload: request from " + originator + " for '" + download.getName() + "' OK"; if ( TRACE ){ System.out.println( msg ); } ddb.log( msg ); HashWrapper hw = new HashWrapper( torrent.getHash()); synchronized( data_cache ){ Object[] data = (Object[])data_cache.get( hw ); if ( data != null ){ data[1] = new Long( SystemTime.getCurrentTime()); return( ddb.createValue((byte[])data[0])); } } torrent = torrent.removeAdditionalProperties(); // when clients get a torrent from the DHT they take on // responsibility for tracking it too torrent.setDecentralisedBackupRequested( true ); byte[] data = torrent.writeToBEncodedData(); data = encrypt( torrent.getHash(), data ); if ( data == null ){ return( null ); } synchronized( data_cache ){ if ( data_cache.size() == 0 ){ final TimerEventPeriodic[]pe = { null }; pe[0] = SimpleTimer.addPeriodicEvent( "DDBTorrent:timeout", 30*1000, new TimerEventPerformer() { public void perform( TimerEvent event ) { long now = SystemTime.getCurrentTime(); synchronized( data_cache ){ Iterator it = data_cache.values().iterator(); while( it.hasNext()){ long time = ((Long)((Object[])it.next())[1]).longValue(); if ( now < time || now - time > 120*1000 ){ it.remove(); } } if ( data_cache.size() == 0 ){ pe[0].cancel(); } } } }); } data_cache.put( hw, new Object[]{ data, new Long( SystemTime.getCurrentTime())}); } return( ddb.createValue( data )); }catch( Throwable e ){ throw( new DistributedDatabaseException("Torrent write fails", e )); } } // server side write public void write( DistributedDatabaseContact contact, DistributedDatabaseTransferType type, DistributedDatabaseKey key, DistributedDatabaseValue value ) throws DistributedDatabaseException { throw( new DistributedDatabaseException( "not supported" )); } // client side read protected DistributedDatabaseValue read( DDBaseContactImpl contact, final DistributedDatabaseProgressListener listener, DistributedDatabaseTransferType type, DistributedDatabaseKey key, long timeout ) throws DistributedDatabaseException { byte[] torrent_hash = ((DDBaseKeyImpl)key).getBytes(); byte[] lookup_key = new SHA1Simple().calculateHash( torrent_hash ); if ( TRACE ){ System.out.println( "TorrentXfer: sending via sha1(hash)" ); } byte[] data = contact.getContact().read( new DHTPluginProgressListener() { public void reportSize( long size ) { listener.reportSize( size ); } public void reportActivity( String str ) { listener.reportActivity( str ); } public void reportCompleteness( int percent ) { listener.reportCompleteness( percent ); } }, DDBaseHelpers.getKey(type.getClass()).getHash(), lookup_key, timeout ); if ( data == null ){ return( null ); } data = decrypt( torrent_hash, data ); if ( data == null ){ return( null ); } return( new DDBaseValueImpl( contact, data, SystemTime.getCurrentTime(), -1)); } protected byte[] encrypt( byte[] hash, byte[] data ) { if ( !testCrypto()){ return( null ); } byte[] enc = doCrypt( Cipher.ENCRYPT_MODE, hash, data, 0 ); if ( enc == null ){ if ( TRACE ){ System.out.println( "TorrentXfer: encryption failed, using plain" ); } byte[] res = new byte[data.length+2]; res[0] = CRYPTO_VERSION; res[1] = 0; // not encrypted System.arraycopy( data, 0, res, 2, data.length ); return( res ); }else{ if ( TRACE ){ System.out.println( "TorrentXfer: encryption ok" ); } byte[] res = new byte[enc.length+2]; res[0] = CRYPTO_VERSION; res[1] = 1; // encrypted System.arraycopy( enc, 0, res, 2, enc.length ); return( res ); } } protected byte[] decrypt( byte[] hash, byte[] data ) { if ( !testCrypto()){ return( null ); } if ( data[0] != CRYPTO_VERSION ){ Debug.out( "Invalid crypto version received" ); return( data ); } if ( data[1] == 0 ){ // encryption failed, in plain if ( TRACE ){ System.out.println( "TorrentXfer: encryption failed, retrieving plain" ); } byte[] res = new byte[data.length-2]; System.arraycopy( data, 2, res, 0, res.length ); return( res ); }else{ if ( TRACE ){ System.out.println( "TorrentXfer: encryption ok, decrypting" ); } byte[] res = doCrypt( Cipher.DECRYPT_MODE, hash, data, 2 ); return( res ); } } protected byte[] doCrypt( int mode, byte[] hash, byte[] data, int data_offset ) { try{ byte[] key_data = new byte[24]; // hash is 20 bytes so we've got 4 zeros at the end. tough System.arraycopy( hash, 0, key_data, 0, hash.length ); SecretKey tdes_key = new SecretKeySpec( key_data, "DESede" ); Cipher cipher = Cipher.getInstance("DESede"); // Triple-DES encryption cipher.init(mode, tdes_key ); return( cipher.doFinal(data, data_offset, data.length - data_offset )); }catch( Throwable e ){ Debug.out( e ); return( null ); } } protected boolean testCrypto() { if ( !crypto_tested ){ crypto_tested = true; try{ Cipher.getInstance("DESede"); // Triple-DES encryption crypto_available = true; }catch( Throwable e ){ Logger.log(new LogAlert(LogAlert.UNREPEATABLE, "Unable to initialise cryptographic framework for magnet-based " + "torrent downloads, please re-install Java", e)); } } return( crypto_available ); } }