/* * File : TRTrackerBTAnnouncerImpl.java * Created : 5 Oct. 2003 * By : Parg * * Azureus - a Java Bittorrent client * * 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. * * 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 ( see the LICENSE file ). * * 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 org.gudy.azureus2.core3.tracker.client.impl.bt; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.*; import java.util.*; import java.util.zip.GZIPInputStream; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import org.gudy.azureus2.core3.config.COConfigurationManager; import org.gudy.azureus2.core3.config.ParameterListener; import org.gudy.azureus2.core3.internat.MessageText; import org.gudy.azureus2.core3.logging.LogAlert; import org.gudy.azureus2.core3.logging.LogEvent; import org.gudy.azureus2.core3.logging.LogIDs; import org.gudy.azureus2.core3.logging.Logger; import org.gudy.azureus2.core3.peer.PEPeerSource; import org.gudy.azureus2.core3.security.SESecurityManager; import org.gudy.azureus2.core3.torrent.TOTorrent; import org.gudy.azureus2.core3.torrent.TOTorrentAnnounceURLSet; import org.gudy.azureus2.core3.torrent.TOTorrentException; import org.gudy.azureus2.core3.tracker.client.*; import org.gudy.azureus2.core3.tracker.client.impl.*; import org.gudy.azureus2.core3.tracker.protocol.PRHelpers; import org.gudy.azureus2.core3.tracker.protocol.udp.*; import org.gudy.azureus2.core3.tracker.util.TRTrackerUtils; import org.gudy.azureus2.core3.util.*; import org.gudy.azureus2.core3.util.Timer; import org.gudy.azureus2.plugins.clientid.ClientIDException; import org.gudy.azureus2.plugins.clientid.ClientIDGenerator; import org.gudy.azureus2.plugins.download.DownloadAnnounceResult; import org.gudy.azureus2.plugins.download.DownloadAnnounceResultPeer; import org.gudy.azureus2.pluginsimpl.local.clientid.ClientIDManagerImpl; import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPosition; import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPositionManager; import com.aelitis.azureus.core.networkmanager.NetworkManager; import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin; import com.aelitis.azureus.core.networkmanager.impl.udp.UDPNetworkManager; import com.aelitis.azureus.core.peermanager.utils.PeerClassifier; import com.aelitis.azureus.core.proxy.AEProxyFactory; import com.aelitis.azureus.core.proxy.AEProxySelector; import com.aelitis.azureus.core.proxy.AEProxyFactory.PluginProxy; import com.aelitis.azureus.core.tracker.TrackerPeerSource; import com.aelitis.net.udp.uc.*; /** * * This class handles communication with the tracker * * @author Olivier * */ public class TRTrackerBTAnnouncerImpl implements TRTrackerAnnouncerHelper { public final static LogIDs LOGID = LogIDs.TRACKER; private static final int OVERRIDE_PERIOD = 10*1000; protected static Timer tracker_timer = new Timer( "Tracker Timer", 32); public static String UDP_REALM = "UDP Tracker"; private static int userMinInterval = 0; private static int userMaxNumwant = 100; private static boolean tcpAnnounceEnabled; private static boolean udpAnnounceEnabled; private static boolean udpProbeEnabled; static{ PRUDPTrackerCodecs.registerCodecs(); COConfigurationManager.addAndFireParameterListeners( new String[] { "Tracker Client Min Announce Interval", "Tracker Client Numwant Limit", "Tracker Client Enable TCP", "Server Enable UDP", "Tracker UDP Probe Enable" }, new ParameterListener() { public void parameterChanged(final String parameterName) { userMinInterval = COConfigurationManager.getIntParameter("Tracker Client Min Announce Interval"); userMaxNumwant = COConfigurationManager.getIntParameter("Tracker Client Numwant Limit"); tcpAnnounceEnabled = COConfigurationManager.getBooleanParameter("Tracker Client Enable TCP"); udpAnnounceEnabled = COConfigurationManager.getBooleanParameter("Server Enable UDP"); udpProbeEnabled = COConfigurationManager.getBooleanParameter("Tracker UDP Probe Enable"); } }); } private static AEMonitor class_mon = new AEMonitor( "TRTrackerBTAnnouncer:class" ); private static Map tracker_report_map = new HashMap(); private TOTorrent torrent; private TOTorrentAnnounceURLSet[] announce_urls; private TRTrackerAnnouncerImpl.Helper helper; private TimerEvent current_timer_event; private TimerEventPerformer timer_event_action; protected int tracker_state = TRTrackerAnnouncer.TS_INITIALISED; private String tracker_status_str = ""; private TRTrackerAnnouncerResponseImpl last_response = null; private long last_update_time_secs; private long current_time_to_wait_secs; private boolean manual_control; private long tracker_interval; private long tracker_min_interval; private long min_interval = 0; private int failure_added_time = 0; private long failure_time_last_updated = 0; private boolean stopped; private boolean stopped_for_queue; private boolean completed; private boolean complete_reported = false; private boolean update_in_progress = false; private long rd_last_override = 0; private int rd_override_percentage = 100; private long min_interval_override = 0; private List trackerUrlLists; private URL lastUsedUrl; private URL lastAZTrackerCheckedURL; private HashWrapper torrent_hash; private String last_tracker_message; // per torrent memory private String info_hash = "info_hash="; private byte[] tracker_peer_id; private String tracker_peer_id_str = "&peer_id="; private byte[] data_peer_id; private int announceCount; private int announceFailCount; private byte autoUDPprobeEvery = 1; private int autoUDPProbeSuccessCount; private String tracker_id = ""; private String ip_override; private String[] peer_networks; private TRTrackerAnnouncerDataProvider announce_data_provider; protected AEMonitor this_mon = new AEMonitor( "TRTrackerBTAnnouncer" ); private boolean az_tracker; private boolean destroyed; public TRTrackerBTAnnouncerImpl( TOTorrent _torrent, TOTorrentAnnounceURLSet[] _announce_urls, String[] _peer_networks, boolean _manual, TRTrackerAnnouncerImpl.Helper _helper ) throws TRTrackerAnnouncerException { torrent = _torrent; announce_urls = _announce_urls; peer_networks = _peer_networks; manual_control = _manual; helper = _helper; //Get the Tracker url constructTrackerUrlLists( true ); //Create our unique peerId try{ data_peer_id = helper.getPeerID(); if ( COConfigurationManager.getBooleanParameter("Tracker Separate Peer IDs")){ tracker_peer_id = ClientIDManagerImpl.getSingleton().generatePeerID( torrent, true ); }else{ tracker_peer_id = data_peer_id; } }catch( ClientIDException e ){ throw( new TRTrackerAnnouncerException( "TRTrackerAnnouncer: Peer ID generation fails", e )); } try { torrent_hash = _torrent.getHashWrapper(); this.info_hash += URLEncoder.encode(new String(torrent_hash.getBytes(), Constants.BYTE_ENCODING), Constants.BYTE_ENCODING).replaceAll("\\+", "%20"); this.tracker_peer_id_str += URLEncoder.encode(new String(tracker_peer_id, Constants.BYTE_ENCODING), Constants.BYTE_ENCODING).replaceAll("\\+", "%20"); }catch (UnsupportedEncodingException e){ Logger.log(new LogEvent(torrent, LOGID, "URL encode fails", e)); throw( new TRTrackerAnnouncerException( "TRTrackerAnnouncer: URL encode fails")); }catch( TOTorrentException e ){ Logger.log(new LogEvent(torrent, LOGID, "Torrent hash retrieval fails", e)); throw( new TRTrackerAnnouncerException( "TRTrackerAnnouncer: URL encode fails")); } timer_event_action = new TimerEventPerformer() { public void perform( TimerEvent this_event ) { if ( manual_control ){ requestUpdateSupport(); return; } long secs_to_wait = getErrorRetryInterval(); try{ secs_to_wait = requestUpdateSupport(); if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Next tracker announce (unadjusted) will be in " + secs_to_wait + "s")); }finally{ current_time_to_wait_secs = secs_to_wait; if ( tracker_state == TRTrackerAnnouncer.TS_STOPPED ){ // System.out.println( "\tperform: stopped so no more events"); }else{ try{ this_mon.enter(); // it is possible that the current event was being processed // when another thread cancelled it and created a further timer // event. if this is the case we don't want to go ahead and // create another timer as one already exists if ( this_event.isCancelled()){ // System.out.println( "\tperform: cancelled so no new event"); }else{ secs_to_wait = getAdjustedSecsToWait(); if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Next tracker announce (adjusted) will be in " + secs_to_wait + "s")); long target_time = SystemTime.getCurrentTime() + (secs_to_wait*1000); if ( current_timer_event != null && !current_timer_event.isCancelled()){ if ( current_timer_event != this_event && current_timer_event.getWhen() < target_time ){ // existing event is earlier then current target, use it return; } current_timer_event.cancel(); } if ( !destroyed ){ current_timer_event = tracker_timer.addEvent( target_time, this ); } } }finally{ this_mon.exit(); } } } } }; if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Tracker Announcer Created using url : " + trackerURLListToString())); } public void cloneFrom( TRTrackerBTAnnouncerImpl other ) { helper = other.helper; data_peer_id = other.data_peer_id; tracker_peer_id = other.tracker_peer_id; tracker_peer_id_str = other.tracker_peer_id_str; tracker_id = other.tracker_id; announce_data_provider = other.announce_data_provider; } protected long getAdjustedSecsToWait() { long secs_to_wait = current_time_to_wait_secs; if( last_response != null && last_response.getStatus() != TRTrackerAnnouncerResponse.ST_ONLINE ) { if( last_response.getStatus() == TRTrackerAnnouncerResponse.ST_REPORTED_ERROR ) { //the tracker has explicitly reported an error (torrent is unauthorized for example), //so there's no need to keep trying to re-announce as if it were actually offline //there's no "min interval" returned, so start the re-announce backoff timings at 15min if( failure_added_time < 900 ) failure_added_time = 900; secs_to_wait = getErrorRetryInterval(); if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, "MIN INTERVAL CALC: tracker reported error, " + "adjusting to error retry interval")); } } else { //tracker is OFFLINE secs_to_wait = getErrorRetryInterval(); if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, "MIN INTERVAL CALC: tracker seems to be offline, " + "adjusting to error retry interval")); } } } else{ if( rd_override_percentage == 0 ) { if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "MIN INTERVAL CALC: override, perc = 0")); return TRTrackerAnnouncer.REFRESH_MINIMUM_SECS; } if (rd_override_percentage != 100) { secs_to_wait = (secs_to_wait * rd_override_percentage) /100; if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, "MIN INTERVAL CALC: override, perc = " + rd_override_percentage)); } } if ( secs_to_wait < TRTrackerAnnouncer.REFRESH_MINIMUM_SECS ){ secs_to_wait = TRTrackerAnnouncer.REFRESH_MINIMUM_SECS; } //use 'min interval' for calculation if( min_interval != 0 && secs_to_wait < min_interval ) { float percentage = (float)min_interval / current_time_to_wait_secs; //percentage of original interval //long orig_override = secs_to_wait; int added_secs = (int)((min_interval - secs_to_wait) * percentage); //increase by x percentage of difference secs_to_wait += added_secs; if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "MIN INTERVAL CALC: min_interval=" + min_interval + ", interval=" + current_time_to_wait_secs + ", orig=" + current_time_to_wait_secs + ", new=" + secs_to_wait + ", added=" + added_secs + ", perc=" + percentage)); } } return( secs_to_wait ); } public int getStatus() { return( tracker_state ); } public String getStatusString() { return( tracker_status_str ); } public TRTrackerAnnouncer getBestAnnouncer() { return( this ); } public void setRefreshDelayOverrides( int percentage ) { if ( percentage > 100 ){ percentage = 100; }else if ( percentage < 50 ){ percentage = 50; } long now = SystemTime.getCurrentTime(); //only start overriding once the tracker announce update has been called boolean override_allowed = rd_last_override > 0 && now - rd_last_override > OVERRIDE_PERIOD; if( now < rd_last_override ) override_allowed = true; //time went backwards if ( override_allowed && rd_override_percentage != percentage ){ try{ this_mon.enter(); rd_last_override = now; rd_override_percentage = percentage; if ( current_timer_event != null && !current_timer_event.isCancelled()){ long start = current_timer_event.getCreatedTime(); long expiry = current_timer_event.getWhen(); long secs_to_wait = getAdjustedSecsToWait(); long target_time = start + (secs_to_wait*1000); if ( target_time != expiry ){ current_timer_event.cancel(); if ( !destroyed ){ if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Changed next tracker announce to " + secs_to_wait + "s via " + Debug.getStackTrace(true, false, 0, 3))); current_timer_event = tracker_timer.addEvent( start, target_time, timer_event_action ); } } } }finally{ this_mon.exit(); } } } public boolean isUpdating() { return( update_in_progress ); } public long getInterval() { return( tracker_interval ); } public long getMinInterval() { return( tracker_min_interval ); } public int getTimeUntilNextUpdate() { try{ this_mon.enter(); if ( current_timer_event == null ){ return( getErrorRetryInterval() ); } int rem = (int)((current_timer_event.getWhen() - SystemTime.getCurrentTime())/1000); return( rem ); }finally{ this_mon.exit(); } } public int getLastUpdateTime() { return( (int)last_update_time_secs ); } public void update( boolean force ) { long now = SystemTime.getCurrentTime() / 1000; if ( now < last_update_time_secs ) force = true; //time went backwards long effective_min = min_interval_override>0?min_interval_override:TRTrackerAnnouncer.REFRESH_MINIMUM_SECS; if ( manual_control || force || ( now - last_update_time_secs >= effective_min )){ requestUpdate(); } } public void complete( boolean already_reported ) { complete_reported = (complete_reported || already_reported ); completed = true; requestUpdate(); } public void stop( boolean for_queue ) { stopped = true; stopped_for_queue = for_queue; requestUpdate(); } protected void requestUpdate() { try{ this_mon.enter(); if ( current_timer_event != null ){ current_timer_event.cancel(); } rd_last_override = SystemTime.getCurrentTime(); //"pause" overrides for 10s if ( !destroyed ){ if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Forcing tracker announce now via " + Debug.getStackTrace(true, false, 0, 3))); current_timer_event = tracker_timer.addEvent( SystemTime.getCurrentTime(), timer_event_action ); } }finally{ this_mon.exit(); } } protected long requestUpdateSupport() { boolean clear_progress = true; try{ try{ this_mon.enter(); // can't continue if the data provider hasn't been set yet... if ( update_in_progress || announce_data_provider == null ){ clear_progress = false; return( getErrorRetryInterval() ); } update_in_progress = true; }finally{ this_mon.exit(); } last_update_time_secs = SystemTime.getCurrentTime()/1000; tracker_status_str = MessageText.getString("PeerManager.status.checking") + "..."; //$NON-NLS-1$ //$NON-NLS-2$ TRTrackerAnnouncerResponseImpl response = null; if ( stopped ){ // if manual control then we assume that a stop request is required, even if we // are in an init state. needed for explicit stop based on URL if ( tracker_state == TRTrackerAnnouncer.TS_INITIALISED && !manual_control ){ // never started tracker_state = TRTrackerAnnouncer.TS_STOPPED; }else if ( tracker_state != TRTrackerAnnouncer.TS_STOPPED ){ response = stopSupport(); if ( response.getStatus() == TRTrackerAnnouncerResponse.ST_ONLINE ){ tracker_state = TRTrackerAnnouncer.TS_STOPPED; }else{ // just have one go at sending a stop event as we don't want to sit here // forever trying to send stop to a stuffed tracker tracker_state = TRTrackerAnnouncer.TS_STOPPED; } } }else if ( tracker_state == TRTrackerAnnouncer.TS_INITIALISED ){ // always go through the "start" phase, even if we're already complete // as some trackers insist on the initial "start" response = startSupport(); if ( response.getStatus() == TRTrackerAnnouncerResponse.ST_ONLINE ){ tracker_state = TRTrackerAnnouncer.TS_DOWNLOADING; } }else if ( completed ){ if ( !complete_reported ){ response = completeSupport(); // treat the "complete" as processed if the tracker replies either OK or an explicit // error. In particular, if the tracker has returned an error to control seed limits // we don't want to treat this as an error and report completed again next time around // as this causes the tracker to double count stats if ( response.getStatus() != TRTrackerAnnouncerResponse.ST_OFFLINE ){ complete_reported = true; tracker_state = TRTrackerAnnouncer.TS_COMPLETED; } }else{ tracker_state = TRTrackerAnnouncer.TS_COMPLETED; response = updateSupport(); } }else{ response = updateSupport(); } if ( response != null ){ int rs = response.getStatus(); if ( rs == TRTrackerAnnouncerResponse.ST_OFFLINE ){ tracker_status_str = MessageText.getString("PeerManager.status.offline"); }else if ( rs == TRTrackerAnnouncerResponse.ST_REPORTED_ERROR ){ tracker_status_str = MessageText.getString("PeerManager.status.error"); // move state back to initialised to next time around a "started" // event it resent. Required for trackers like 123torrents.com that // will fail peers that don't start with a "started" event after a // tracker restart tracker_state = TRTrackerAnnouncer.TS_INITIALISED; }else{ if ( announce_data_provider.isPeerSourceEnabled( PEPeerSource.PS_BT_TRACKER )){ tracker_status_str = MessageText.getString("PeerManager.status.ok"); if ( response.wasProbe()){ tracker_status_str += " (" + MessageText.getString( "label.udp_probe" ) + ")"; } }else{ tracker_status_str = MessageText.getString("PeerManager.status.ps_disabled"); response.setPeers(new TRTrackerAnnouncerResponsePeerImpl[0]); } } String reason = response.getAdditionalInfo(); if ( reason != null ){ tracker_status_str += " (" + reason + ")"; } last_response = response; helper.informResponse( this, response ); return( response.getTimeToWait()); } tracker_status_str = ""; return( getErrorRetryInterval() ); }catch( Throwable e ){ Debug.printStackTrace( e ); return( getErrorRetryInterval() ); }finally{ try{ this_mon.enter(); if ( clear_progress ){ update_in_progress = false; } }finally{ this_mon.exit(); } } } protected TRTrackerAnnouncerResponseImpl startSupport() { if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Tracker Announcer is sending " + "a start Request")); return (update("started")); } protected TRTrackerAnnouncerResponseImpl completeSupport() { if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Tracker Announcer is sending " + "a completed Request")); return (update("completed")); } protected TRTrackerAnnouncerResponseImpl stopSupport() { if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Tracker Announcer is sending " + "a stopped Request")); return (update("stopped")); } protected TRTrackerAnnouncerResponseImpl updateSupport() { if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Tracker Announcer is sending " + "an update Request")); return update(""); } private TRTrackerAnnouncerResponseImpl update( String evt ) { // this method filters out any responses incompatible with the network selection TRTrackerAnnouncerResponseImpl resp = update2( evt ); TRTrackerAnnouncerResponsePeer[] peers = resp.getPeers(); if ( peers != null ){ List p = new ArrayList(); for (int i=0;i<peers.length;i++){ TRTrackerAnnouncerResponsePeer peer = peers[i]; if ( peer_networks == null ){ p.add( peer ); }else{ String peer_address = peer.getAddress(); String peer_network = AENetworkClassifier.categoriseAddress( peer_address ); boolean added = false; for (int j=0;j<peer_networks.length;j++){ if ( peer_networks[j] == peer_network ){ p.add( peer ); added = true; break; } } if (!added && Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_WARNING, "Tracker Announcer dropped peer '" + peer_address + "' as incompatible " + "with network selection")); } } peers = new TRTrackerAnnouncerResponsePeer[ p.size()]; p.toArray( peers ); resp.setPeers( peers ); } return( resp ); } private TRTrackerAnnouncerResponseImpl update2(String evt) { TRTrackerAnnouncerResponseImpl last_failure_resp = null; String skip_host = null; outer: for (int i = 0 ; i < trackerUrlLists.size() ; i++) { List urls = (List) trackerUrlLists.get(i); for (int j = 0 ; j < urls.size() ; j++) { final URL original_url = (URL)urls.get(j); if ( skip_host != null && skip_host.equals( original_url.getHost())){ if (Logger.isEnabled()) Logger.log( new LogEvent( torrent, LOGID, LogEvent.LT_WARNING, "Tracker Announcer is ignoring '" + original_url + "' as already received overloaded response from this host" )); continue; } lastUsedUrl = original_url; if ( lastUsedUrl != lastAZTrackerCheckedURL ){ az_tracker = TRTrackerUtils.isAZTracker( lastUsedUrl ); } URL request_url = null; if ( last_failure_resp != null ){ // report this now as it is about to be lost helper.informResponse( this, last_failure_resp ); } try{ request_url = constructUrl(evt,original_url); URL[] tracker_url = { original_url }; int prev_udp_probes_ok = autoUDPProbeSuccessCount; byte[] result_bytes = updateOld( tracker_url, request_url); lastUsedUrl = tracker_url[0]; // url may have redirected, use this value as it will be correct TRTrackerAnnouncerResponseImpl resp = decodeTrackerResponse( lastUsedUrl, result_bytes ); int resp_status = resp.getStatus(); if ( resp_status == TRTrackerAnnouncerResponse.ST_ONLINE ){ if ( autoUDPProbeSuccessCount > prev_udp_probes_ok ){ resp.setWasProbe(); } try{ // tracker looks ok, make any redirection permanent if ( !original_url.toString().equals(lastUsedUrl.toString())){ if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "announce url permanently redirected: old = " + original_url + ", new = " + lastUsedUrl )); TorrentUtils.replaceAnnounceURL( torrent, original_url, lastUsedUrl ); } }catch( Throwable e ){ Debug.printStackTrace(e); } urls.remove(j); urls.add(0, lastUsedUrl ); trackerUrlLists.remove(i); trackerUrlLists.add(0,urls); informURLChange( original_url, lastUsedUrl, false ); //and return the result return( resp ); }else if ( resp_status == TRTrackerAnnouncerResponse.ST_REPORTED_ERROR ){ last_failure_resp = resp; String reason = resp.getAdditionalInfo(); // avoid re-hitting a host with multiple ports if reporting overloaded. This is // particularly "interesting" when reporting the "completed" event and we get a // "overloaded" response - when we hit another port we record the event twice // as the tracker has discarded this peer and therefore doesn't know to ignore the // second "completed" event... if ( reason != null && ( reason.indexOf( "too many seeds" ) != -1 || reason.indexOf( "too many peers" ) != -1 )){ skip_host = original_url.getHost(); } }else{ announceFailCount++; last_failure_resp = resp; } }catch( MalformedURLException e ){ announceFailCount++; Debug.printStackTrace( e ); last_failure_resp = new TRTrackerAnnouncerResponseImpl( original_url, torrent_hash, TRTrackerAnnouncerResponse.ST_OFFLINE, getErrorRetryInterval(), "malformed URL '" + (request_url==null?"<null>":request_url.toString()) + "'" ); }catch( Throwable e ){ announceFailCount++; last_failure_resp = new TRTrackerAnnouncerResponseImpl( original_url, torrent_hash, TRTrackerAnnouncerResponse.ST_OFFLINE, getErrorRetryInterval(), e.getMessage()==null?e.toString():e.getMessage()); } if ( destroyed ){ break outer; } } } // things no good here if ( last_failure_resp == null ){ last_failure_resp = new TRTrackerAnnouncerResponseImpl( null, torrent_hash, TRTrackerAnnouncerResponse.ST_OFFLINE, getErrorRetryInterval(), "Reason Unknown" ); } // use 4* the num_want as no doubt a fair few connections will fail and // we want to get a decent reconnect rate int num_want = calculateNumWant() * 4; TRTrackerAnnouncerResponsePeer[] cached_peers = helper.getPeersFromCache(num_want); if ( cached_peers.length > 0 ){ // System.out.println( "cached peers used:" + cached_peers.length ); last_failure_resp.setPeers( cached_peers ); } return( last_failure_resp ); } private byte[] updateOld( URL[] tracker_url, URL reqUrl ) throws Exception { // set context in case authentication dialog is required boolean errorLevel = true; try{ TorrentUtils.setTLSTorrentHash( torrent_hash ); // loop to possibly retry update on SSL certificate install for (int i=0;i<2;i++){ String failure_reason = null; String protocol = reqUrl.getProtocol(); try{ if (Logger.isEnabled()){ Logger.log(new LogEvent(torrent, LOGID, "Tracker Announcer is Requesting: " + reqUrl)); } ByteArrayOutputStream message = new ByteArrayOutputStream(); URL udpAnnounceURL = null; boolean udp_probe = false; // if we have multiple tracker URLs then do something sensible about if ( protocol.equalsIgnoreCase("udp")){ if ( udpAnnounceEnabled ){ udpAnnounceURL = reqUrl; }else{ throw( new IOException( "UDP Tracker protocol disabled" )); } }else if ( protocol.equalsIgnoreCase("http") && !az_tracker && announceCount % autoUDPprobeEvery == 0 && udpProbeEnabled && udpAnnounceEnabled ){ // if we don't know this tracker supports UDP then don't probe on // first announce as we don't want a large delay on torrent startup // also if we are stopping we don't want to initiate a probe as // we want the stop instruction to get to tracker if possible if ( ( stopped || announceCount == 0 || ( announceCount < trackerUrlLists.size() && announceFailCount == announceCount )) && !TRTrackerUtils.isUDPProbeOK(reqUrl)){ // skip probe }else{ String tracker_network = AENetworkClassifier.categoriseAddress( reqUrl.getHost()); if ( tracker_network == AENetworkClassifier.AT_PUBLIC ){ udpAnnounceURL = new URL(reqUrl.toString().replaceFirst("^http", "udp")); udp_probe = true; } } } if ( udpAnnounceURL != null ){ failure_reason = announceUDP( reqUrl, message, udp_probe ); if ((failure_reason != null || message.size() == 0) && udp_probe){ // automatic UDP probe failed, use HTTP again udpAnnounceURL = null; if ( autoUDPprobeEvery < 16 ){ autoUDPprobeEvery <<= 1; }else{ // unregister in case the tracker somehow changed its capabilities TRTrackerUtils.setUDPProbeResult(reqUrl, false); } if (Logger.isEnabled()){ Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_INFORMATION, "redirection of http announce [" + tracker_url[0] + "] to udp failed, will retry in " + autoUDPprobeEvery + " announces")); } }else if (failure_reason == null && udp_probe){ TRTrackerUtils.setUDPProbeResult(reqUrl, true); if (Logger.isEnabled()){ Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_INFORMATION, "redirection of http announce [" + tracker_url[0] + "] to udp successful")); } autoUDPprobeEvery = 1; autoUDPProbeSuccessCount++; } } announceCount++; if ( udpAnnounceURL == null){ boolean failed = false; if ( !az_tracker && !tcpAnnounceEnabled ){ String tracker_network = AENetworkClassifier.categoriseAddress( reqUrl.getHost()); if ( tracker_network == AENetworkClassifier.AT_PUBLIC ){ failure_reason = "HTTP Tracker protocol disabled"; failed = true; } } if ( !failed ){ failure_reason = announceHTTP( tracker_url, reqUrl, message, i==0 ); } } // if we've got some kind of response then return it if ( message.size() > 0 ){ return( message.toByteArray()); } if ( failure_reason == null ){ failure_reason = "No data received from tracker"; if ( reqUrl.getProtocol().equalsIgnoreCase( "udp" )){ errorLevel = false; } } }catch( SSLException e ){ // e.printStackTrace(); // try and install certificate regardless of error (as this changed in JDK1.5 // and broke this...) if ( i == 0 ){//&& e.getMessage().indexOf("No trusted certificate found") != -1 ){ if ( SESecurityManager.installServerCertificates( reqUrl ) != null ){ // certificate has been installed continue; // retry with new certificate } failure_reason = exceptionToString( e ); }else{ failure_reason = exceptionToString( e ); } }catch( IOException e ){ if ( e instanceof UnknownHostException || e instanceof ConnectException ){ errorLevel = false; } if ( i == 0 && protocol.toLowerCase().startsWith( "http" )){ URL retry_url = UrlUtils.getIPV4Fallback( reqUrl ); if ( retry_url != null ){ reqUrl = retry_url; continue; } } failure_reason = exceptionToString( e ); }catch (Exception e){ // e.printStackTrace(); failure_reason = exceptionToString( e ); } if ( failure_reason != null && failure_reason.indexOf("401" ) != -1 ){ failure_reason = "Tracker authentication failed"; errorLevel = false; } if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, errorLevel ? LogEvent.LT_ERROR : LogEvent.LT_WARNING, "Exception while processing the Tracker Request for " + reqUrl + ": " + failure_reason)); throw( new Exception( failure_reason)); } // should never get here as second iteration of loop will always cause an exit throw( new Exception( "Internal Error: should never get here" )); }finally{ TorrentUtils.setTLSTorrentHash( null ); } } private String announceHTTP( URL[] tracker_url, // overwritten if redirected URL original_reqUrl, ByteArrayOutputStream message, boolean first_effort ) throws Exception { try{ return( announceHTTPSupport( tracker_url, original_reqUrl, null, message )); }catch( UnknownHostException e ){ if ( first_effort && AENetworkClassifier.categoriseAddress( original_reqUrl.getHost() ) != AENetworkClassifier.AT_PUBLIC ){ Map<String,Object> opts = new HashMap<String, Object>(); if ( peer_networks != null ){ opts.put( AEProxyFactory.PO_PEER_NETWORKS, peer_networks ); } PluginProxy proxy = AEProxyFactory.getPluginProxy( "Tracker update", original_reqUrl, opts, true ); if ( proxy != null ){ boolean ok = false; try{ String result = announceHTTPSupport( tracker_url, proxy.getURL(), proxy.getProxy(), message ); ok = true; return( result ); }catch( Throwable f ){ }finally{ proxy.setOK( ok ); } } } throw( e ); } } private String announceHTTPSupport( URL[] tracker_url, // overwritten if redirected URL original_reqUrl, Proxy proxy, ByteArrayOutputStream message ) throws IOException { TRTrackerUtils.checkForBlacklistedURLs( original_reqUrl ); URL reqUrl = TRTrackerUtils.adjustURLForHosting( original_reqUrl ); reqUrl = AddressUtils.adjustURL( reqUrl ); if ( reqUrl != original_reqUrl ){ if (Logger.isEnabled()){ Logger.log(new LogEvent(torrent, LOGID, " HTTP: url adjusted to " + reqUrl )); } } String failure_reason = null; HttpURLConnection con; Properties http_properties = new Properties(); http_properties.put( ClientIDGenerator.PR_URL, reqUrl ); try{ ClientIDManagerImpl.getSingleton().generateHTTPProperties( http_properties ); }catch( ClientIDException e ){ throw( new IOException( e.getMessage())); } reqUrl = (URL)http_properties.get( ClientIDGenerator.PR_URL ); if ( reqUrl.getProtocol().equalsIgnoreCase("https")){ // see ConfigurationChecker for SSL client defaults HttpsURLConnection ssl_con; if ( proxy == null ){ ssl_con = (HttpsURLConnection)reqUrl.openConnection(); }else{ ssl_con = (HttpsURLConnection)reqUrl.openConnection( proxy ); } // allow for certs that contain IP addresses rather than dns names ssl_con.setHostnameVerifier( new HostnameVerifier() { public boolean verify( String host, SSLSession session ) { return( true ); } }); con = ssl_con; }else{ if ( proxy == null ){ con = (HttpURLConnection) reqUrl.openConnection(); }else{ con = (HttpURLConnection) reqUrl.openConnection( proxy ); } } // we want this true but some plugins (grrr) set the global default not to follow // redirects con.setInstanceFollowRedirects( true ); String user_agent = (String)http_properties.get( ClientIDGenerator.PR_USER_AGENT ); if ( user_agent != null ){ con.setRequestProperty("User-Agent", user_agent ); } con.setRequestProperty("Connection", "close" ); // some trackers support gzip encoding of replies con.addRequestProperty("Accept-Encoding","gzip"); try{ try{ con.connect(); }catch( AEProxyFactory.UnknownHostException e ){ throw( new UnknownHostException( e.getMessage())); } InputStream is = null; try{ is = con.getInputStream(); String resulting_url_str = con.getURL().toString(); if ( !reqUrl.toString().equals( resulting_url_str )){ // some kind of redirect has occurred. Unfortunately we can't get at the underlying // redirection reason (temp, perm etc) so we support the use of an explicit indicator // in the resulting url String marker = "permredirect=1"; int pos = resulting_url_str.indexOf( marker ); if ( pos != -1 ){ pos = pos-1; // include the '&' or '?' try{ URL redirect_url = new URL( resulting_url_str.substring(0,pos)); tracker_url[0] = redirect_url; }catch( Throwable e ){ Debug.printStackTrace(e); } } } String encoding = con.getHeaderField( "content-encoding"); boolean gzip = encoding != null && encoding.equalsIgnoreCase("gzip"); // System.out.println( "encoding = " + encoding ); if ( gzip ){ is = new GZIPInputStream( is ); } // there are some trackers out there that don't set content length correctly // so we can't reliably use it :( int content_length = -1; //con.getContentLength(); // System.out.println(length); byte[] data = new byte[1024]; int num_read = 0; // some trackers don't return content-length while ( content_length <= 0 || num_read < content_length ){ try{ int len = is.read(data); if ( len > 0 ){ message.write(data, 0, len); num_read += len; if ( num_read > 128*1024 ){ // someone's sending us junk, bail out message.reset(); throw( new Exception( "Tracker response invalid (too large)" )); } }else if ( len == 0 ){ Thread.sleep(20); }else{ break; } }catch (Exception e){ if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, "Exception while Requesting Tracker", e)); Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_ERROR, "Message Received was : " + message)); } failure_reason = exceptionToString( e ); break; } } if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Tracker Announcer [" + lastUsedUrl + "] has received : " + message)); }catch (Exception e){ // e.printStackTrace(); failure_reason = exceptionToString( e ); }finally{ if (is != null) { try { is.close(); }catch (Exception e) { } is = null; } } }finally{ con.disconnect(); } return( failure_reason ); } protected String announceUDP( URL original_reqUrl, ByteArrayOutputStream message, boolean is_probe ) throws IOException { long timeout = is_probe?10000:PRUDPPacket.DEFAULT_UDP_TIMEOUT; URL reqUrl = TRTrackerUtils.adjustURLForHosting( original_reqUrl ); if ( reqUrl != original_reqUrl ){ if (Logger.isEnabled()){ Logger.log(new LogEvent(torrent, LOGID, " UDP: url adjusted to " + reqUrl )); } } String failure_reason = null; PasswordAuthentication auth = null; try{ if ( (!is_probe) && UrlUtils.queryHasParameter(reqUrl.getQuery(), "auth", false)) { auth = SESecurityManager.getPasswordAuthentication( UDP_REALM, reqUrl ); } PRUDPPacketHandler handler = PRUDPPacketHandlerFactory.getHandler( UDPNetworkManager.getSingleton().getUDPNonDataListeningPortNumber()); InetSocketAddress destination = new InetSocketAddress(reqUrl.getHost(),reqUrl.getPort()==-1?80:reqUrl.getPort()); handler = handler.openSession( destination ); try{ for (int retry_loop=0;retry_loop<PRUDPPacketTracker.DEFAULT_RETRY_COUNT;retry_loop++){ try{ PRUDPPacket connect_request = new PRUDPPacketRequestConnect(); PRUDPPacket reply = handler.sendAndReceive( auth, connect_request, destination, timeout ); if ( reply.getAction() == PRUDPPacketTracker.ACT_REPLY_CONNECT ){ PRUDPPacketReplyConnect connect_reply = (PRUDPPacketReplyConnect)reply; long my_connection = connect_reply.getConnectionId(); PRUDPPacketRequest request; if ( PRUDPPacketTracker.VERSION == 1 ){ PRUDPPacketRequestAnnounce announce_request = new PRUDPPacketRequestAnnounce( my_connection ); request = announce_request; // bit of a hack this... String url_str = reqUrl.toString(); int p_pos = url_str.indexOf("?"); url_str = url_str.substring(p_pos+1); String event_str = getURLParam( url_str, "event" ); int event = PRUDPPacketRequestAnnounce.EV_UPDATE; if ( event_str != null ){ if ( event_str.equals( "started" )){ event = PRUDPPacketRequestAnnounce.EV_STARTED; }else if ( event_str.equals( "stopped" )){ event = PRUDPPacketRequestAnnounce.EV_STOPPED; }else if ( event_str.equals( "completed" )){ event = PRUDPPacketRequestAnnounce.EV_COMPLETED; } } String ip_str = getURLParam( url_str, "ip" ); int ip = 0; if ( ip_str != null ){ ip = PRHelpers.addressToInt( ip_str); } announce_request.setDetails( torrent_hash.getBytes(), tracker_peer_id, getLongURLParam( url_str, "downloaded" ), event, ip, (int)getLongURLParam( url_str, "numwant" ), getLongURLParam( url_str, "left" ), (short)getLongURLParam( url_str, "port" ), getLongURLParam( url_str, "uploaded" )); }else{ PRUDPPacketRequestAnnounce2 announce_request = new PRUDPPacketRequestAnnounce2( my_connection ); request = announce_request; // bit of a hack this... String url_str = reqUrl.toString(); int p_pos = url_str.indexOf("?"); url_str = url_str.substring(p_pos+1); String event_str = getURLParam( url_str, "event" ); int event = PRUDPPacketRequestAnnounce.EV_UPDATE; if ( event_str != null ){ if ( event_str.equals( "started" )){ event = PRUDPPacketRequestAnnounce.EV_STARTED; }else if ( event_str.equals( "stopped" )){ event = PRUDPPacketRequestAnnounce.EV_STOPPED; }else if ( event_str.equals( "completed" )){ event = PRUDPPacketRequestAnnounce.EV_COMPLETED; } } String ip_str = getURLParam( url_str, "ip" ); int ip = 0; if ( ip_str != null ){ ip = PRHelpers.addressToInt( ip_str); } announce_request.setDetails( torrent_hash.getBytes(), tracker_peer_id, getLongURLParam( url_str, "downloaded" ), event, ip, helper.getUDPKey(), (int)getLongURLParam( url_str, "numwant" ), getLongURLParam( url_str, "left" ), (short)getLongURLParam( url_str, "port" ), getLongURLParam( url_str, "uploaded" )); } reply = handler.sendAndReceive( auth, request, destination ); if ( reply.getAction() == PRUDPPacketTracker.ACT_REPLY_ANNOUNCE ){ if ( auth != null ){ SESecurityManager.setPasswordAuthenticationOutcome( UDP_REALM, reqUrl, true ); } if ( PRUDPPacketTracker.VERSION == 1 ){ PRUDPPacketReplyAnnounce announce_reply = (PRUDPPacketReplyAnnounce)reply; Map map = new HashMap(); map.put( "interval", new Long( announce_reply.getInterval())); int[] addresses = announce_reply.getAddresses(); short[] ports = announce_reply.getPorts(); List peers = new ArrayList(); map.put( "peers", peers ); for (int i=0;i<addresses.length;i++){ Map peer = new HashMap(); peers.add( peer ); peer.put( "ip", PRHelpers.intToAddress(addresses[i]).getBytes()); peer.put( "port", new Long( ports[i])); } byte[] data = BEncoder.encode( map ); message.write( data ); return( null ); } PRUDPPacketReplyAnnounce2 announce_reply = (PRUDPPacketReplyAnnounce2)reply; Map map = new HashMap(); map.put( "interval", new Long( announce_reply.getInterval())); int[] addresses = announce_reply.getAddresses(); short[] ports = announce_reply.getPorts(); map.put( "complete", new Long(announce_reply.getSeeders())); map.put( "incomplete", new Long(announce_reply.getLeechers())); List peers = new ArrayList(); map.put( "peers", peers ); for (int i=0;i<addresses.length;i++){ Map peer = new HashMap(); peers.add( peer ); peer.put( "ip", PRHelpers.intToAddress(addresses[i]).getBytes()); peer.put( "port", new Long( ports[i])); } byte[] data = BEncoder.encode( map ); message.write( data ); return( null ); } failure_reason = ((PRUDPPacketReplyError)reply).getMessage(); }else{ failure_reason = ((PRUDPPacketReplyError)reply).getMessage(); } }catch( PRUDPPacketHandlerException e ){ if ( e.getMessage() == null || e.getMessage().indexOf("timed out") == -1 ){ throw( e ); } } } }finally{ handler.closeSession(); } }catch( Throwable e ){ failure_reason = exceptionToString(e); } if ( auth != null ){ SESecurityManager.setPasswordAuthenticationOutcome( UDP_REALM, reqUrl, false ); } return( failure_reason ); } protected long getLongURLParam( String url, String param ) { String val = getURLParam( url, param ); if( val == null ){ return(0); } return( Long.parseLong( val )); } protected String getURLParam( String url, String param ) { int p1 = url.indexOf( param + "=" ); if ( p1 == -1 ){ return( null ); } int p2 = url.indexOf( "&", p1 ); if ( p2 == -1 ){ return( url.substring(p1+param.length()+1)); } return( url.substring(p1+param.length()+1,p2)); } protected String exceptionToString( Throwable e ) { String class_name = e.getClass().getName(); int pos = class_name.lastIndexOf( '.' ); if ( pos != -1 ){ class_name = class_name.substring(pos+1); } pos = class_name.indexOf( '$' ); if ( pos != -1 ){ class_name = class_name.substring(pos+1); } String str = class_name + ": " + Debug.getNestedExceptionMessage(e); if ( str.indexOf( "timed out") != -1 ){ str = "timeout"; } return( str ); } public URL constructUrl( String evt, URL _url) throws Exception { String url = _url.toString(); StringBuffer request = new StringBuffer(url); // if url already has a query component then just append our parameters on the end if ( url.indexOf('?') != -1 ){ request.append('&'); }else{ request.append('?'); } // the client-id stuff RELIES on info_hash being the FIRST parameter added by // us to the URL, so don't change it! request.append(info_hash); request.append(tracker_peer_id_str); String port_details = announce_data_provider.getCryptoLevel()==NetworkManager.CRYPTO_OVERRIDE_REQUIRED? TRTrackerUtils.getPortsForURLFullCrypto(): TRTrackerUtils.getPortsForURL(); request.append(port_details); request.append("&uploaded=").append(announce_data_provider.getTotalSent()); request.append("&downloaded=").append(announce_data_provider.getTotalReceived()); if ( Constants.DOWNLOAD_SOURCES_PRETEND_COMPLETE ){ request.append("&left=0"); }else{ request.append("&left=").append(announce_data_provider.getRemaining()); } // 3017: added at request of tracker admins who want to be able to monitor swarm poisoning request.append("&corrupt=").append(announce_data_provider.getFailedHashCheck()); //TrackerID extension if( tracker_id.length() > 0 ) { request.append( "&trackerid=").append(tracker_id ); } if (evt.length() != 0) { request.append("&event=").append(evt); } boolean stopped = evt.equals("stopped"); if ( stopped ){ request.append("&numwant=0"); if ( stopped_for_queue ){ request.append( "&azq=1" ); } }else { //calculate how many peers we should ask for int numwant = Math.min(calculateNumWant(),userMaxNumwant); request.append("&numwant=").append(numwant); //no_peer_id has been made obsolete by 'compact' } // actually, leave this in, ask PARG why! request.append("&no_peer_id=1"); String tracker_network = AENetworkClassifier.categoriseAddress( _url.getHost()); // latest space saving measure, a compact return type where peers are returned // as 6 byte entries in a single byte[] (4 bytes ip, 2 byte port) // leave this as always supplied, ask PARG why request.append( "&compact=1" ); // any explicit override takes precedence over any implicit override added // when hosting torrents String explicit_ips = COConfigurationManager.getStringParameter( "Override Ip", "" ); String ip = null; // make sure this tracker network is enabled boolean network_ok = false; boolean normal_network_ok = false; if ( peer_networks == null ){ network_ok = true; normal_network_ok = true; }else{ for (int i=0;i<peer_networks.length;i++){ if ( peer_networks[i] == AENetworkClassifier.AT_PUBLIC ){ normal_network_ok = true; } if ( peer_networks[i] == tracker_network ){ network_ok = true; } } } if ( !network_ok ){ throw( new Exception( "Network not enabled for url '" + _url + "'" )); } String normal_explicit = null; if ( explicit_ips.length() > 0 ){ // gotta select an appropriate override based on network type StringTokenizer tok = new StringTokenizer( explicit_ips, ";" ); while( tok.hasMoreTokens()){ String this_address = tok.nextToken().trim(); if ( this_address.length() > 0 ){ String cat = AENetworkClassifier.categoriseAddress( this_address ); if ( cat == AENetworkClassifier.AT_PUBLIC ){ normal_explicit = this_address; } if ( tracker_network == cat ){ ip = this_address; break; } } } } if ( ip == null ){ // if we have a normal explicit override and this is enabled then use it if ( normal_network_ok && normal_explicit != null ){ ip = normal_explicit; }else{ if ( ip_override != null ){ ip = ip_override; } } } if ( ip != null ){ if ( tracker_network == AENetworkClassifier.AT_PUBLIC ){ try{ ip = PRHelpers.DNSToIPAddress( ip ); }catch( UnknownHostException e){ if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_ERROR, "IP Override host resolution of '" + ip + "' fails, using unresolved address")); } } request.append("&ip=").append(ip); } if ( COConfigurationManager.getBooleanParameter("Tracker Key Enable Client", true )){ request.append( "&key=").append( helper.getTrackerKey()); } String ext = announce_data_provider.getExtensions(); if ( ext != null ){ // sanitise it while ( ext.startsWith("&")){ ext = ext.substring( 1 ); } request.append( "&" ); request.append( ext ); } request.append( "&azver=" + TRTrackerAnnouncer.AZ_TRACKER_VERSION_CURRENT ); if ( az_tracker && !stopped ){ int up = announce_data_provider.getUploadSpeedKBSec( evt.equals( "started" )); if ( up > 0 ){ request.append( "&azup=" + up ); } String as = NetworkAdmin.getSingleton().getCurrentASN().getAS(); if ( as.length() > 0 ){ request.append( "&azas=" + URLEncoder.encode( as, "UTF8" )); } DHTNetworkPosition best_position = DHTNetworkPositionManager.getBestLocalPosition(); if ( best_position != null ){ try{ byte[] bytes = DHTNetworkPositionManager.serialisePosition( best_position ); request.append( "&aznp=" + Base32.encode( bytes )); }catch( Throwable e ){ Debug.printStackTrace( e ); } } } // bah, issue with an i2p tracker regarding what is passed it seems: truncate to minimum required and order for the moment... if ( tracker_network == AENetworkClassifier.AT_I2P ){ String temp = request.toString(); int pos = temp.indexOf( '?' ); String head = temp.substring( 0, pos ); String tail = temp.substring( pos+1 ); String[] bits = tail.split( "&" ); Map<String,String> map = new HashMap<String, String>(); for ( String bit: bits ){ String[] arg = bit.split( "=" ); map.put( arg[0], arg[1] ); } tail = ""; for ( String str: new String[]{ "info_hash", "peer_id", "port", "ip", "uploaded", "downloaded", "left", "compact", "event", "numwant" }){ String val = map.get( str ); if ( val != null ){ tail += (tail.length()==0?"":"&") + str + "=" + map.get( str ); } } request = new StringBuffer( head + "?" + tail ); } return new URL( request.toString()); } protected int calculateNumWant() { if ( !announce_data_provider.isPeerSourceEnabled( PEPeerSource.PS_BT_TRACKER )){ return( 0 ); } int MAX_PEERS = 100; // ask for a bit more than our max to allow for connections failures int maxAllowed = 3*announce_data_provider.getMaxNewConnectionsAllowed()/2; if ( maxAllowed < 0 || maxAllowed > MAX_PEERS ) { maxAllowed = MAX_PEERS; } return maxAllowed; } public byte[] getPeerId() { return( data_peer_id ); } public void setAnnounceDataProvider( TRTrackerAnnouncerDataProvider _provider) { try{ this_mon.enter(); announce_data_provider = _provider; }finally{ this_mon.exit(); } } public TOTorrent getTorrent() { return( torrent ); } public URL getTrackerURL() { return( lastUsedUrl ); } public void setTrackerURL( URL new_url ) { try{ new_url = new URL( new_url.toString().replaceAll(" ", "")); List list = new ArrayList(1); list.add( new_url ); trackerUrlLists.clear(); trackerUrlLists.add( list ); informURLChange( lastUsedUrl, new_url, true ); }catch( Throwable e ){ Debug.printStackTrace(e); } } public TOTorrentAnnounceURLSet[] getAnnounceSets() { return( announce_urls ); } public void resetTrackerUrl( boolean shuffle ) { String old_list = trackerURLListToString(); constructTrackerUrlLists(shuffle); if ( trackerUrlLists.size() == 0 ){ return; } if ( !old_list.equals(trackerURLListToString())){ URL first_url = (URL)((List)trackerUrlLists.get(0)).get(0); informURLChange( lastUsedUrl, first_url, true ); } } public void refreshListeners() { informURLRefresh(); } public void setIPOverride( String override ) { ip_override = override; } public void clearIPOverride() { ip_override = null; } private void constructTrackerUrlLists( boolean shuffle ) { try{ trackerUrlLists = new ArrayList(1); //This entry is present on multi-tracker torrents if ( announce_urls.length == 0 ){ //If not present, we use the default specification URL url = torrent.getAnnounceURL(); //We then contruct a list of one element, containing this url, and put this list //into the list of lists of urls. List list = new ArrayList(); list.add(url); trackerUrlLists.add(list); }else{ //Ok we have a multi-tracker torrent for(int i = 0 ; i < announce_urls.length ; i++){ //Each list contains a list of urls URL[] urls = announce_urls[i].getAnnounceURLs(); List random_urls = new ArrayList(); for(int j = 0 ; j < urls.length; j++){ //System.out.println(urls.get(j).getClass()); URL url = urls[j]; //Shuffle int pos = shuffle?(int)(Math.random() * (random_urls.size()+1)):j; random_urls.add(pos,url); } //Add this list to the list trackerUrlLists.add(random_urls); } } }catch(Exception e){ Debug.printStackTrace( e ); } } protected String trackerURLListToString() { String trackerUrlListString = "["; for (int i=0;i<trackerUrlLists.size();i++){ List group = (List)trackerUrlLists.get(i); trackerUrlListString += (i==0?"":",") + "["; for (int j=0;j<group.size();j++){ URL u = (URL)group.get(j); trackerUrlListString += (j==0?"":",") + u.toString(); } trackerUrlListString += "]"; } trackerUrlListString += "]"; return( trackerUrlListString ); } protected TRTrackerAnnouncerResponseImpl decodeTrackerResponse( URL url, byte[] data ) { String failure_reason; if ( data == null ){ failure_reason = "no response"; }else{ try{ //parse the metadata Map metaData = null; try{ metaData = BDecoder.decode(data); // obey any peers source restrictions Object o = metaData.get( "az_ps" ); if ( o instanceof List ){ List peer_sources = (List)o; List x = new ArrayList(); for (int i=0;i<peer_sources.size();i++){ Object o1 = peer_sources.get(i); if ( o1 instanceof byte[] ){ x.add( new String((byte[])o1)); } } String[] y = new String[x.size()]; x.toArray( y ); announce_data_provider.setPeerSources( y ); } // handle any user warnings in the response try{ byte[] b_warning_message = (byte[])metaData.get( "warning message" ); if ( b_warning_message != null && COConfigurationManager.getBooleanParameter( "Tracker Client Show Warnings" )){ String warning_message = new String(b_warning_message); // don't report the same message twice per torrent if ( !warning_message.equals( last_tracker_message )){ last_tracker_message = warning_message; boolean log_it = false; // only report a given message once per tracker try{ class_mon.enter(); String last_warning_message = (String)tracker_report_map.get( url.getHost()); if ( last_warning_message == null || !warning_message.equals( last_warning_message )){ log_it = true; tracker_report_map.put( url.getHost(), warning_message ); } }finally{ class_mon.exit(); } if ( log_it ){ Logger.logTextResource(new LogAlert(torrent, LogAlert.UNREPEATABLE, LogAlert.AT_WARNING, "TrackerClient.announce.warningmessage"), new String[] { announce_data_provider.getName(), warning_message }); } } } }catch( Throwable e ){ Debug.printStackTrace( e ); } long time_to_wait; try { if ( !metaData.containsKey( "interval" )){ throw( new Exception( "interval missing" )); } tracker_interval = time_to_wait = ((Long) metaData.get("interval")).longValue(); Long raw_min_interval = (Long) metaData.get("min interval"); if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_INFORMATION, "Received from announce: 'interval' = " + time_to_wait + "; 'min interval' = " + raw_min_interval)); } // guard against crazy return values if (time_to_wait < 0 || time_to_wait > 0xffffffffL) { time_to_wait = 0xffffffffL; } if (raw_min_interval != null) { tracker_min_interval = min_interval = raw_min_interval.longValue(); // ignore useless values // Note: Many trackers set min_interval and interval the same. if (min_interval < 1) { if (Logger.isEnabled()) { Logger.log(new LogEvent( torrent, LOGID, LogEvent.LT_INFORMATION, "Tracker being silly and " + "returning a 'min interval' of less than 1 second (" + min_interval + ")")); } min_interval = 0; } else if (min_interval > time_to_wait) { if (Logger.isEnabled()) { Logger.log(new LogEvent( torrent, LOGID, LogEvent.LT_INFORMATION, "Tracker being silly and " + "returning a 'min interval' (" + min_interval + ") greater than recommended announce 'interval'" + " (" + time_to_wait + ")")); } min_interval = 0; } } else { // tracker owners complain we announce too much but then never // implement "min interval". So take it into our own hands // and enforce a min_interval of interval when there is no // "min interval" min_interval = time_to_wait > 30 ? time_to_wait - 10 : time_to_wait; } if(userMinInterval != 0) { time_to_wait = Math.max(userMinInterval, time_to_wait); min_interval = Math.max(min_interval, userMinInterval); if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_INFORMATION, "Overriding with user settings: 'interval' = " + time_to_wait + "; 'min interval' = " + min_interval)); } } // roll back 10 seconds to make sure we announce before the tracker // times us out. This is done after min_interval in order not to // mess up the "ignore useless values" if (time_to_wait > 30) time_to_wait -= 10; } catch (Exception e) { byte[] failure_reason_bytes = (byte[]) metaData.get("failure reason"); if ( failure_reason_bytes == null ){ if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_WARNING, "Problems with Tracker, will retry in " + getErrorRetryInterval() + "ms")); return( new TRTrackerAnnouncerResponseImpl( url, torrent_hash, TRTrackerAnnouncerResponse.ST_OFFLINE, getErrorRetryInterval(), "Unknown cause" )); } // explicit failure from the tracker failure_reason = new String( failure_reason_bytes, Constants.DEFAULT_ENCODING); return( new TRTrackerAnnouncerResponseImpl( url, torrent_hash, TRTrackerAnnouncerResponse.ST_REPORTED_ERROR, getErrorRetryInterval(), failure_reason )); } //System.out.println("Response from Announce: " + new String(data)); Long incomplete_l = getLong( metaData, "incomplete"); Long complete_l = getLong( metaData, "complete"); Long downloaded_l = getLong( metaData, "downloaded"); if ( incomplete_l != null || complete_l != null ){ if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "ANNOUNCE SCRAPE1: seeds=" + complete_l + " peers=" + incomplete_l)); } //TrackerID extension, used by phpbt trackers. //We reply with '&trackerid=1234' when we receive //'10:tracker id4:1234e' on announce reply. //NOTE: we receive as 'tracker id' but reply as 'trackerid' byte[] trackerid = (byte[])metaData.get( "tracker id" ); if( trackerid != null ) { tracker_id = new String( trackerid ); } byte[] crypto_flags = (byte[])metaData.get( "crypto_flags" ); //build the list of peers List valid_meta_peers = new ArrayList(); Object meta_peers_peek = metaData.get( "peers" ); Long az_compact_l = (Long)metaData.get( "azcompact" ); long az_compact = az_compact_l==null?0:az_compact_l.longValue(); boolean this_is_az_tracker = az_compact == 2; if ( az_tracker != this_is_az_tracker || lastUsedUrl != lastAZTrackerCheckedURL ){ lastAZTrackerCheckedURL = lastUsedUrl; az_tracker = this_is_az_tracker; TRTrackerUtils.setAZTracker( url, az_tracker ); } if ( az_compact == 2 ){ // latest return to dictionary based data List meta_peers = (List)meta_peers_peek; int peers_length = meta_peers.size(); if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, "ANNOUNCE CompactPeers2: num=" + peers_length)); } if ( peers_length > 1 ){ // calculate average rtt to use for those with no rtt long total_rtt = 0; int rtt_count = 0; for ( int i = 0; i < peers_length; i++ ){ Map peer = (Map) meta_peers.get(i); Long l_rtt = (Long)peer.get( "r" ); if ( l_rtt != null ){ long rtt = l_rtt.longValue(); if ( rtt <= 0 ){ // invalid, remove peer.remove( "r" ); }else{ total_rtt += rtt; } rtt_count++; } } final int average_rtt = (int)( rtt_count==0?0:(total_rtt/rtt_count)); // sort into smallest rtt order with biased at front Collections.sort( meta_peers, new Comparator() { public int compare( Object o1, Object o2 ) { Map map1 = (Map)o1; Map map2 = (Map)o2; Long l_rtt1 = (Long)map1.get( "r" ); Long l_rtt2 = (Long)map2.get( "r" ); boolean biased_1 = map1.containsKey( "b" ); boolean biased_2 = map2.containsKey( "b" ); if ( biased_1 == biased_2 ){ int rtt1 = l_rtt1==null?average_rtt:l_rtt1.intValue(); int rtt2 = l_rtt2==null?average_rtt:l_rtt2.intValue(); return( rtt1 - rtt2 ); }else if ( biased_1 ){ return( -1 ); }else{ return( +1 ); } } }); // interleave non-biased peers with good rtt int biased_pos = peers_length; int non_biased_pos = peers_length; for ( int i = 0; i < peers_length; i++ ){ Map peer = (Map) meta_peers.get(i); if ( peer.containsKey( "b" )){ if ( i == 0 ){ biased_pos = i; } }else{ non_biased_pos = i; break; } } List new_peers = new ArrayList(peers_length); int non_biased_start = non_biased_pos; boolean last_biased = true; while( biased_pos < non_biased_start || non_biased_pos < peers_length ){ if ( biased_pos < non_biased_start ){ if ( non_biased_pos < peers_length ){ Map biased = (Map) meta_peers.get(biased_pos); Map non_biased = (Map) meta_peers.get(non_biased_pos); boolean use_biased; if ( !last_biased ){ use_biased = true; }else{ Long l_rtt_biased = (Long)biased.get( "r" ); Long l_rtt_non_biased = (Long)non_biased.get( "r" ); int biased_rtt = l_rtt_biased==null?average_rtt:l_rtt_biased.intValue(); int non_biased_rtt = l_rtt_non_biased==null?average_rtt:l_rtt_non_biased.intValue(); use_biased = non_biased_rtt >= biased_rtt; } if ( use_biased ){ new_peers.add( biased ); biased_pos++; }else{ new_peers.add( non_biased ); non_biased_pos++; } last_biased = use_biased; }else{ new_peers.add( meta_peers.get( biased_pos++ )); } }else{ new_peers.add( meta_peers.get( non_biased_pos++ )); } } meta_peers = new_peers; } for ( int i = 0; i < peers_length; i++ ){ Map peer = (Map) meta_peers.get(i); try{ byte[] ip_bytes = (byte[])peer.get("i"); String ip; if ( ip_bytes.length == 4 ){ int ip1 = 0xff & ip_bytes[0]; int ip2 = 0xff & ip_bytes[1]; int ip3 = 0xff & ip_bytes[2]; int ip4 = 0xff & ip_bytes[3]; ip = ip1 + "." + ip2 + "." + ip3 + "." + ip4; }else{ StringBuffer sb = new StringBuffer(39); for ( int j=0; j<16; j+=2 ){ sb.append( Integer.toHexString(((ip_bytes[j]<<8) & 0xff00) | (ip_bytes[j+1]&0x00ff))); if (j < 14 ){ sb.append( ":" ); } } ip = sb.toString(); } byte[] tcp_bytes = (byte[])peer.get("t"); int tcp_port = ((tcp_bytes[0]&0xff) << 8 ) + (tcp_bytes[1]&0xff ); byte[] peer_peer_id = TRTrackerAnnouncerImpl.getAnonymousPeerId( ip, tcp_port ); int udp_port = 0; byte[] udp_bytes = (byte[])peer.get("u"); if ( udp_bytes != null ){ if ( udp_bytes.length == 0 ){ udp_port = tcp_port; }else{ udp_port = ((udp_bytes[0]&0xff) << 8 ) + (udp_bytes[1]&0xff ); } } int http_port = 0; byte[] http_bytes = (byte[])peer.get("h"); if ( http_bytes != null ){ http_port = ((http_bytes[0]&0xff) << 8 ) + (http_bytes[1]&0xff ); } short protocol = DownloadAnnounceResultPeer.PROTOCOL_NORMAL; byte[] protocol_bytes = (byte[])peer.get("c"); if ( protocol_bytes != null ){ protocol = (protocol_bytes[0]&0x01)==0?DownloadAnnounceResultPeer.PROTOCOL_NORMAL:DownloadAnnounceResultPeer.PROTOCOL_CRYPT; } Long l_azver = (Long)peer.get("v" ); byte az_ver = l_azver==null?TRTrackerAnnouncer.AZ_TRACKER_VERSION_1:l_azver.byteValue(); Long l_up_speed = (Long)peer.get( "s" ); boolean biased = peer.containsKey("b"); if ( biased ){ PeerClassifier.setAzureusIP( ip ); } TRTrackerAnnouncerResponsePeerImpl new_peer = new TRTrackerAnnouncerResponsePeerImpl( PEPeerSource.PS_BT_TRACKER, peer_peer_id, ip, tcp_port, udp_port, http_port, protocol, az_ver, l_up_speed==null?0:l_up_speed.shortValue()); if (Logger.isEnabled()){ String extra = ""; Long l_rtt = (Long)peer.get( "r" ); if ( l_rtt != null ){ extra = ",rtt=" + l_rtt; } if ( biased ){ extra += ",biased"; } Logger.log(new LogEvent(torrent, LOGID, "AZ2-COMPACT PEER: " + new_peer.getString() + extra)); } valid_meta_peers.add( new_peer ); }catch( Throwable e ){ if (Logger.isEnabled()) Logger.log( new LogEvent( torrent, LOGID, LogEvent.LT_ERROR, "Invalid az2 peer received: " + peer )); } } }else if ( meta_peers_peek instanceof List ){ // old style non-compact List meta_peers = (List)meta_peers_peek; //for every peer int peers_length = meta_peers.size(); if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, "ANNOUNCE old style non-compact: num=" + peers_length)); } if ( crypto_flags != null && peers_length != crypto_flags.length ){ crypto_flags = null; if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_ERROR, "Invalid crypto_flags returned: length mismatch" )); } for (int i = 0; i < peers_length; i++) { Map peer = (Map) meta_peers.get(i); Object s_peerid = peer.get("peer id"); Object s_ip = peer.get("ip"); Object s_port = peer.get("port"); // Assert that all ip and port are available if ( s_ip != null && s_port != null ){ //get the peer ip address String ip = new String((byte[]) s_ip, Constants.DEFAULT_ENCODING); ip = AddressUtils.convertToShortForm( ip ); //get the peer port number - should be Long but have seen byte[] on occasion int peer_port = s_port instanceof byte[]?Integer.parseInt(new String((byte[])s_port)):((Long) s_port).intValue(); // try to repair invalid peer ports; worst that can happen is we // still can't make outgoing connections that we already can't make if (peer_port >65535) peer_port -=65536; if (peer_port <0) peer_port +=65536; if (peer_port < 0 || peer_port > 65535) { if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_ERROR, "Invalid peer port given: " + ip + ": " + peer_port)); continue; } byte[] peer_peer_id; // extension - if peer id is missing then the tracker isn't sending // peer ids to save on bandwidth. However, we need something "unique" to // work on internally so make an ID up from the ip and port if ( s_peerid == null ){ // Debug.out(ip + ": tracker did not give peerID in reply"); peer_peer_id = TRTrackerAnnouncerImpl.getAnonymousPeerId( ip, peer_port ); // System.out.println("generated peer id" + new String(peerId) + "/" + ByteFormatter.nicePrint( peerId, true )); }else{ peer_peer_id = (byte[])s_peerid ; } short protocol; if ( crypto_flags == null ){ protocol = DownloadAnnounceResultPeer.PROTOCOL_NORMAL; }else{ protocol = crypto_flags[i]==0?DownloadAnnounceResultPeer.PROTOCOL_NORMAL:DownloadAnnounceResultPeer.PROTOCOL_CRYPT; } int udp_port = 0; int http_port = 0; TRTrackerAnnouncerResponsePeerImpl new_peer = new TRTrackerAnnouncerResponsePeerImpl( PEPeerSource.PS_BT_TRACKER, peer_peer_id, ip, peer_port, udp_port, http_port, protocol, TRTrackerAnnouncer.AZ_TRACKER_VERSION_1, (short)0 ); if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "NON-COMPACT PEER: " + new_peer.getString())); valid_meta_peers.add( new_peer ); } } }else if ( meta_peers_peek instanceof byte[] ){ // byte[] for compact returns byte[] meta_peers = (byte[])meta_peers_peek; String tracker_network = AENetworkClassifier.categoriseAddress( url.getHost()); if ( tracker_network == AENetworkClassifier.AT_I2P && ( meta_peers.length % 32 == 0 )){ // 32 byte SHA256 b32 addresses for ( int i=0;i<meta_peers.length;i+=32 ){ byte[] i2p_id = new byte[32]; byte[] peer_peer_id = new byte[20]; System.arraycopy( meta_peers, i, i2p_id, 0, 32 ); System.arraycopy( meta_peers, i, peer_peer_id, 0, 20 ); String hostname = Base32.encode( i2p_id ).toLowerCase(Locale.US) + ".b32.i2p"; TRTrackerAnnouncerResponsePeerImpl peer = new TRTrackerAnnouncerResponsePeerImpl( PEPeerSource.PS_BT_TRACKER, peer_peer_id, hostname, 6881, 0, 0, DownloadAnnounceResultPeer.PROTOCOL_NORMAL, TRTrackerAnnouncer.AZ_TRACKER_VERSION_1, (short)0 ); if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "COMPACT PEER: " + peer.getString())); valid_meta_peers.add( peer ); } }else{ int entry_size = az_compact==1?9:6; if ( crypto_flags != null && meta_peers.length/entry_size != crypto_flags.length ){ crypto_flags = null; if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_ERROR, "Invalid crypto_flags returned: length mismatch" )); } int peer_number = 0; if (Logger.isEnabled()) { Logger.log(new LogEvent(torrent, LOGID, "ANNOUNCE CompactPeers: num=" + (meta_peers.length/entry_size))); } int peers_length = meta_peers.length; for (int i=0;i<(peers_length/entry_size)*entry_size;i+=entry_size){ peer_number++; int ip1 = 0xFF & meta_peers[i]; int ip2 = 0xFF & meta_peers[i+1]; int ip3 = 0xFF & meta_peers[i+2]; int ip4 = 0xFF & meta_peers[i+3]; int po1 = 0xFF & meta_peers[i+4]; int po2 = 0xFF & meta_peers[i+5]; String ip = "" + ip1 + "." + ip2 + "." + ip3 + "." + ip4; int tcp_port = po1*256+po2; if (tcp_port < 0 || tcp_port > 65535) { if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_ERROR, "Invalid compact peer port given: " + ip + ": " + tcp_port)); continue; } byte[] peer_peer_id = TRTrackerAnnouncerImpl.getAnonymousPeerId( ip, tcp_port ); short protocol; int udp_port; if ( az_compact == 1 ){ int upo1 = 0xFF & meta_peers[i+6]; int upo2 = 0xFF & meta_peers[i+7]; udp_port = upo1*256+upo2; byte flags = meta_peers[i+8]; protocol = (flags&0x01)==0?DownloadAnnounceResultPeer.PROTOCOL_NORMAL:DownloadAnnounceResultPeer.PROTOCOL_CRYPT; }else{ if ( crypto_flags == null ){ protocol = DownloadAnnounceResultPeer.PROTOCOL_NORMAL; }else{ protocol = crypto_flags[peer_number-1]==0?DownloadAnnounceResultPeer.PROTOCOL_NORMAL:DownloadAnnounceResultPeer.PROTOCOL_CRYPT; } udp_port = 0; } int http_port = 0; TRTrackerAnnouncerResponsePeerImpl peer = new TRTrackerAnnouncerResponsePeerImpl( PEPeerSource.PS_BT_TRACKER, peer_peer_id, ip, tcp_port, udp_port, http_port, protocol, TRTrackerAnnouncer.AZ_TRACKER_VERSION_1, (short)0 ); if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "COMPACT PEER: " + peer.getString())); valid_meta_peers.add( peer ); } } }else if ( meta_peers_peek instanceof Map ){ // some trackers incorrectly return an empty Map when no peers available if (((Map)meta_peers_peek).size() != 0 ){ throw( new IOException( "peers missing from response" )); } }else if (!metaData.containsKey("peers6")){ // we got nothing useful under peers and no peers6 either throw( new IOException( "peers missing from response" )); } final byte[] v6peers = (byte[])metaData.get("peers6"); if ( v6peers != null ){ // 16 bytes for v6 + 2 bytes for port final int entry_size = 18; final byte[] rawAddr = new byte[16]; for (int i=0; i<v6peers.length; i+=entry_size ){ System.arraycopy( v6peers, i, rawAddr, 0, 16 ); String ip = InetAddress.getByAddress(rawAddr).getHostAddress(); int po1 = 0xFF & v6peers[i+16]; int po2 = 0xFF & v6peers[i+17]; int tcp_port = po1*256 + po2; if (tcp_port < 0 || tcp_port > 65535){ if (Logger.isEnabled()){ Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_ERROR, "Invalid compactv6 peer port given: " + ip + ": " + tcp_port)); } continue; } byte[] peer_peer_id = TRTrackerAnnouncerImpl.getAnonymousPeerId( ip, tcp_port ); short protocol = DownloadAnnounceResultPeer.PROTOCOL_NORMAL; TRTrackerAnnouncerResponsePeerImpl peer = new TRTrackerAnnouncerResponsePeerImpl(PEPeerSource.PS_BT_TRACKER, peer_peer_id, ip, tcp_port, 0, 0, protocol, TRTrackerAnnouncer.AZ_TRACKER_VERSION_1, (short) 0); if (Logger.isEnabled()){ Logger.log(new LogEvent(torrent, LOGID, "COMPACTv6 PEER: " + peer.getString())); } valid_meta_peers.add(peer); } } TRTrackerAnnouncerResponsePeerImpl[] peers=new TRTrackerAnnouncerResponsePeerImpl[valid_meta_peers.size()]; valid_meta_peers.toArray(peers); helper.addToTrackerCache( peers); TRTrackerAnnouncerResponseImpl resp = new TRTrackerAnnouncerResponseImpl( url, torrent_hash, TRTrackerAnnouncerResponse.ST_ONLINE, time_to_wait, peers ); //reset failure retry interval on successful connect failure_added_time = 0; Map extensions = (Map)metaData.get( "extensions" ); resp.setExtensions(extensions); if ( extensions != null ){ if ( complete_l == null) { complete_l = (Long)extensions.get("complete"); } if ( incomplete_l == null) { incomplete_l = (Long)extensions.get("incomplete"); } if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "ANNOUNCE SCRAPE2: seeds=" + complete_l + " peers=" + incomplete_l)); Object override = extensions.get( "min interval override" ); if ( override != null && override instanceof Long ){ // this is to allow specific torrents to be refreshed more quickly // if the tracker permits. Parg min_interval_override = ((Long)override).longValue(); } } if (complete_l != null || incomplete_l != null || downloaded_l != null ) { int complete = complete_l == null ? 0 : complete_l.intValue(); int incomplete = incomplete_l == null ? 0 : incomplete_l.intValue(); int downloaded = downloaded_l == null ? -1 : downloaded_l.intValue(); if (complete < 0 || incomplete < 0) { resp.setFailureReason(MessageText.getString( "Tracker.announce.ignorePeerSeed", new String[] { (complete < 0 ? MessageText.getString("MyTorrentsView.seeds") + " == " + complete + ". " : "") + (incomplete < 0 ? MessageText.getString("MyTorrentsView.peers") + " == " + incomplete + ". " : "") })); } else { resp.setScrapeResult( complete, incomplete, downloaded ); TRTrackerScraper scraper = TRTrackerScraperFactory.getSingleton(); if (scraper != null) { TRTrackerScraperResponse scrapeResponse = scraper.scrape( torrent, getTrackerURL()); if (scrapeResponse != null) { long lNextScrapeTime = scrapeResponse.getNextScrapeStartTime(); long now = SystemTime.getCurrentTime(); long lNewNextScrapeTime = now + TRTrackerScraperResponseImpl.calcScrapeIntervalSecs( 0, complete) * 1000L; // make it look as if the scrape has just run. Important // as seeding rules may make calculations on when the // scrape value were set scrapeResponse.setScrapeStartTime( now ); if ( lNextScrapeTime < lNewNextScrapeTime ){ scrapeResponse.setNextScrapeStartTime(lNewNextScrapeTime); } scrapeResponse.setSeedsPeers(complete, incomplete); if ( downloaded >= 0 ){ scrapeResponse.setCompleted( downloaded ); } } } } } return (resp); }catch( IOException e ){ if ( metaData != null ){ byte[] failure_reason_bytes = (byte[]) metaData.get("failure reason"); // explicit failure from the tracker failure_reason = new String( failure_reason_bytes, Constants.DEFAULT_ENCODING); return( new TRTrackerAnnouncerResponseImpl( url, torrent_hash, TRTrackerAnnouncerResponse.ST_REPORTED_ERROR, Math.max( tracker_interval, getErrorRetryInterval()), failure_reason )); } // decode could fail if the tracker's returned, say, an HTTP response // indicating server overload String trace_data; if ( data.length <= 150 ){ trace_data = new String(data); }else{ trace_data = new String(data,0,150) + "..."; } if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, LogEvent.LT_ERROR, "TRTrackerAnnouncer::invalid reply: " + trace_data)); failure_reason = "invalid reply: " + trace_data; } }catch( Throwable e ){ Debug.printStackTrace( e ); failure_reason = "error: " + e.getMessage(); } } return( new TRTrackerAnnouncerResponseImpl( url, torrent_hash, TRTrackerAnnouncerResponse.ST_OFFLINE, getErrorRetryInterval(), failure_reason )); } private Long getLong( Map map, String key ) { Object o = map.get( key ); if ( o instanceof Long ){ return((Long)o); } return( null ); } protected void informURLChange( URL old_url, URL new_url, boolean explicit ) { helper.informURLChange( old_url, new_url, explicit ); } protected void informURLRefresh() { helper.informURLRefresh(); } public TRTrackerAnnouncerResponse getLastResponse() { if( last_response == null ){ return new TRTrackerAnnouncerResponseImpl( null, torrent_hash, TRTrackerAnnouncerResponse.ST_OFFLINE, TRTrackerAnnouncer.REFRESH_MINIMUM_SECS, "Initialising" ); } return( last_response ); } public boolean isManual() { return( manual_control ); } public void destroy() { destroyed = true; try{ this_mon.enter(); if ( current_timer_event != null ){ // cancel any events that are a way off being triggered. note that // we don't want to cancel all events as the "stopped" event that // is scheduled on stop of a download may still be lurking if ( current_timer_event.getWhen() - SystemTime.getCurrentTime() > 10*1000 ){ if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "Canceling announce trigger")); current_timer_event.cancel(); } } }finally{ this_mon.exit(); } } /** * Retrieve the retry interval to use on announce errors. */ protected int getErrorRetryInterval() { long currentTime = SystemTime.getCurrentTime() /1000; long diff = currentTime - failure_time_last_updated; //use previously calculated interval if it's not time to update if( diff < failure_added_time && !(diff < 0) ) { return failure_added_time; } //update previous change time failure_time_last_updated = currentTime; if( failure_added_time == 0 ) { //start failure_added_time = 10; } else if( failure_added_time < 30 ) { //three 10-sec retries failure_added_time += 10; } else if( failure_added_time < 60 ) { //two 15-sec retries failure_added_time += 15; } else if( failure_added_time < 120 ) { //two 30-sec retries failure_added_time += 30; } else if( failure_added_time < 600 ) { //eight 60-sec retries failure_added_time += 60; } else { //2-3min random retry failure_added_time += 120 + new Random().nextInt( 60 ); } boolean is_seed = (announce_data_provider == null) ? false : announce_data_provider.getRemaining() == 0; if( is_seed ) failure_added_time = failure_added_time * 2; //no need to retry as often //make sure we're not waiting longer than 30min if( !is_seed && failure_added_time > 1800) { failure_added_time = 1800; } else if ( is_seed && failure_added_time > 3600) { //or 60min if seed failure_added_time = 3600; } return failure_added_time; } public void setAnnounceResult( DownloadAnnounceResult result ) { // this is how the results from "external" announces get into the system // really should refactor so that "normal" and "external" mechanisms are // just instances of the same generic approach TRTrackerAnnouncerResponseImpl response; String status; if ( result.getResponseType() == DownloadAnnounceResult.RT_ERROR ){ status = MessageText.getString("PeerManager.status.error"); String reason = result.getError(); if ( reason != null ){ status += " (" + reason + ")"; } response = new TRTrackerAnnouncerResponseImpl( result.getURL(), torrent_hash, TRTrackerAnnouncerResponse.ST_OFFLINE, result.getTimeToWait(), reason ); }else{ DownloadAnnounceResultPeer[] ext_peers = result.getPeers(); List l_peers = new ArrayList(ext_peers.length); boolean ps_enabled = announce_data_provider != null && announce_data_provider.isPeerSourceEnabled( PEPeerSource.PS_BT_TRACKER ); for (int i=0;i<ext_peers.length;i++){ DownloadAnnounceResultPeer ext_peer = ext_peers[i]; String ps = ext_peer.getSource(); // filter out any disabled peers if ( !ps_enabled && ps.equals( PEPeerSource.PS_BT_TRACKER )){ continue; }else{ int http_port = 0; byte az_version = TRTrackerAnnouncer.AZ_TRACKER_VERSION_1; TRTrackerAnnouncerResponsePeerImpl p = new TRTrackerAnnouncerResponsePeerImpl( ext_peer.getSource(), ext_peer.getPeerID(), ext_peer.getAddress(), ext_peer.getPort(), ext_peer.getUDPPort(), http_port, ext_peer.getProtocol(), az_version, (short)0 ); l_peers.add( p ); if (Logger.isEnabled()) Logger.log(new LogEvent(torrent, LOGID, "EXTERNAL PEER: " + p.getString())); } } TRTrackerAnnouncerResponsePeerImpl[] peers = new TRTrackerAnnouncerResponsePeerImpl[ l_peers.size()]; l_peers.toArray( peers ); helper.addToTrackerCache( peers); if ( ps_enabled || peers.length > 0 || ext_peers.length == 0 ){ status = MessageText.getString("PeerManager.status.ok"); }else{ status = MessageText.getString("PeerManager.status.ps_disabled"); peers = new TRTrackerAnnouncerResponsePeerImpl[0]; } response = new TRTrackerAnnouncerResponseImpl( result.getURL(), torrent_hash, TRTrackerAnnouncerResponse.ST_ONLINE, result.getTimeToWait(), peers ); } // only make the user aware of the status if the underlying announce is // failing if ( last_response == null || last_response.getStatus() != TRTrackerAnnouncerResponse.ST_ONLINE ){ URL result_url = result.getURL(); boolean update_is_dht = TorrentUtils.isDecentralised( result_url ); tracker_status_str = status + " (" + (update_is_dht?MessageText.getString( "dht.backup.only" ):(result_url==null?"<null>":result_url.getHost())) + ")"; } helper.informResponse( this, response ); } public void addListener( TRTrackerAnnouncerListener l ) { helper.addListener( l ); } public void removeListener( TRTrackerAnnouncerListener l ) { helper.removeListener( l ); } public void setTrackerResponseCache( Map map ) { helper.setTrackerResponseCache( map ); } public void removeFromTrackerResponseCache( String ip, int tcpPort) { helper.removeFromTrackerResponseCache( ip, tcpPort ); } public Map getTrackerResponseCache() { return( helper.getTrackerResponseCache()); } public TrackerPeerSource getTrackerPeerSource( TOTorrentAnnounceURLSet set) { Debug.out( "not implemented" ); return null; } public TrackerPeerSource getCacheTrackerPeerSource() { Debug.out( "not implemented" ); return null; } public void generateEvidence( IndentWriter writer ) { writer.println( "BT announce:" ); try{ writer.indent(); writer.println( "state: " + tracker_state + ", in_progress=" + update_in_progress ); writer.println( "current: " + (lastUsedUrl==null?"null":lastUsedUrl.toString())); writer.println( "last: " + (last_response==null?"null":last_response.getString())); writer.println( "last_update_secs: " + last_update_time_secs ); writer.println( "secs_to_wait: " + current_time_to_wait_secs + (manual_control?" - manual":"")); writer.println( "t_interval: " + tracker_interval ); writer.println( "t_min_interval: " + tracker_min_interval ); writer.println( "min_interval: " + min_interval ); writer.println( "min_interval_override: " + min_interval_override ); writer.println( "rd: last_override=" + rd_last_override + ",percentage=" + rd_override_percentage ); writer.println( "event: " + ( current_timer_event==null?"null":current_timer_event.getString())); writer.println( "stopped: " + stopped + ", for_q=" + stopped_for_queue ); writer.println( "complete: " + completed + ", reported=" + complete_reported ); }finally{ writer.exdent(); } } }