I2P Address: [http://git.idk.i2p]

Skip to content
Snippets Groups Projects
TrackerClient.java 31.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • /* TrackerClient - Class that informs a tracker and gets new peers.
       Copyright (C) 2003 Mark J. Wielaard
    
       This file is part of Snark.
       
       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, 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.
    */
    
    package org.klomp.snark;
    
    
    zzz's avatar
    zzz committed
    import java.io.ByteArrayInputStream;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    zzz's avatar
    zzz committed
    import java.net.MalformedURLException;
    import java.net.URL;
    
    import java.util.ArrayList;
    
    zzz's avatar
    zzz committed
    import java.util.Collection;
    
    import java.util.Collections;
    
    zzz's avatar
    zzz committed
    import java.util.Date;
    
    import java.util.Iterator;
    import java.util.List;
    
    import java.util.Locale;
    
    import java.util.Random;
    import java.util.Set;
    
    import net.i2p.I2PAppContext;
    
    zzz's avatar
    zzz committed
    import net.i2p.data.DataHelper;
    
    import net.i2p.data.Hash;
    
    import net.i2p.util.ConvertToHash;
    
    zzz's avatar
    zzz committed
    import net.i2p.util.I2PAppThread;
    
    jrandom's avatar
    jrandom committed
    import net.i2p.util.Log;
    
    zzz's avatar
    zzz committed
    import net.i2p.util.SimpleTimer2;
    
    zzz's avatar
    zzz committed
    import org.klomp.snark.bencode.InvalidBEncodingException;
    
    import org.klomp.snark.dht.DHT;
    
    
    /**
     * Informs metainfo tracker of events and gets new peers for peer
     * coordinator.
     *
    
    zzz's avatar
    zzz committed
     * start() creates a thread and starts it.
     * At the end of each run, a TimedEvent is queued on the SimpleTimer2 queue.
     * The TimedEvent creates a new thread and starts it, so it does not
     * clog SimpleTimer2.
     *
     * The thread runs one pass through the trackers, the PEX, and the DHT,
     * then queues a new TimedEvent and exits.
     *
     * Thus there are only threads that are actively announcing, not one thread per torrent forever.
     *
     * start() may be called again after halt().
     *
    
     * @author Mark Wielaard (mark@klomp.org)
     */
    
    zzz's avatar
    zzz committed
    public class TrackerClient implements Runnable {
    
      private final Log _log;
    
      private static final String NO_EVENT = "";
      private static final String STARTED_EVENT = "started";
      private static final String COMPLETED_EVENT = "completed";
      private static final String STOPPED_EVENT = "stopped";
    
    zzz's avatar
    zzz committed
      private static final String NOT_REGISTERED  = "torrent not registered"; //bytemonsoon
    
    zzz's avatar
    zzz committed
      /** this is our equivalent to router.utorrent.com for bootstrap */
      private static final String DEFAULT_BACKUP_TRACKER = "http://tracker.welterde.i2p/a";
    
    
      private final static int SLEEP = 5; // 5 minutes.
    
    zzz's avatar
    zzz committed
      private final static int DELAY_MIN = 2000; // 2 secs.
    
    zzz's avatar
    zzz committed
      private final static int DELAY_RAND = 6*1000;
    
    zzz's avatar
    zzz committed
      private final static int MAX_REGISTER_FAILS = 10; // * INITIAL_SLEEP = 15m to register
      private final static int INITIAL_SLEEP = 90*1000;
      private final static int MAX_CONSEC_FAILS = 5;    // slow down after this
      private final static int LONG_SLEEP = 30*60*1000; // sleep a while after lots of fails
    
    zzz's avatar
    zzz committed
      private final static long MIN_TRACKER_ANNOUNCE_INTERVAL = 15*60*1000;
    
      private final static long MIN_DHT_ANNOUNCE_INTERVAL = 10*60*1000;
    
    zzz's avatar
    zzz committed
      private final I2PSnarkUtil _util;
    
    zzz's avatar
    zzz committed
      private final String infoHash;
      private final String peerID;
    
    zzz's avatar
    zzz committed
      private final String additionalTrackerURL;
    
      private final PeerCoordinator coordinator;
    
      private final Snark snark;
    
    zzz's avatar
    zzz committed
      private final String _threadName;
    
      private volatile boolean stop = true;
      private volatile boolean started;
      private volatile boolean _initialized;
      private volatile int _runCount;
      // running thread so it can be interrupted
      private volatile Thread _thread;
      // queued event so it can be cancelled
      private volatile SimpleTimer2.TimedEvent _event;
      // these 2 used in loop()
      private volatile boolean runStarted;
      private volatile  int consecutiveFails;
    
    zzz's avatar
    zzz committed
      private boolean completed;
    
    zzz's avatar
    zzz committed
      private volatile boolean _fastUnannounce;
    
      private long lastDHTAnnounce;
    
    zzz's avatar
    zzz committed
      private final List<TCTracker> trackers;
      private final List<TCTracker> backupTrackers;
    
    zzz's avatar
    zzz committed
      /**
    
    zzz's avatar
    zzz committed
       * Call start() to start it.
       *
    
    zzz's avatar
    zzz committed
       * @param meta null if in magnet mode
    
    zzz's avatar
    zzz committed
       * @param additionalTrackerURL may be null, from the ?tr= param in magnet mode, otherwise ignored
    
    zzz's avatar
    zzz committed
      public TrackerClient(I2PSnarkUtil util, MetaInfo meta, String additionalTrackerURL,
                           PeerCoordinator coordinator, Snark snark)
    
    zzz's avatar
    zzz committed
        super();
    
        String id = urlencode(snark.getID());
    
    zzz's avatar
    zzz committed
        _threadName = "TrackerClient " + id.substring(id.length() - 12);
    
    zzz's avatar
    zzz committed
        _util = util;
    
        _log = util.getContext().logManager().getLog(TrackerClient.class);
    
    zzz's avatar
    zzz committed
        this.additionalTrackerURL = additionalTrackerURL;
    
        this.snark = snark;
    
    
        this.port = 6881; //(port == -1) ? 9 : port;
    
    zzz's avatar
    zzz committed
        this.infoHash = urlencode(snark.getInfoHash());
        this.peerID = urlencode(snark.getID());
        this.trackers = new ArrayList(2);
    
    zzz's avatar
    zzz committed
        this.backupTrackers = new ArrayList(2);
    
    zzz's avatar
    zzz committed
      public synchronized void start() {
          if (!stop) {
              if (_log.shouldLog(Log.WARN))
                  _log.warn("Already started: " + _threadName);
              return;
          }
          stop = false;
          consecutiveFails = 0;
          runStarted = false;
    
    zzz's avatar
    zzz committed
          _fastUnannounce = false;
    
    zzz's avatar
    zzz committed
          _thread = new I2PAppThread(this, _threadName + " #" + (++_runCount), true);
          _thread.start();
    
    jrandom's avatar
    jrandom committed
          started = true;
    
    jrandom's avatar
    jrandom committed
      }
      
      public boolean halted() { return stop; }
    
    jrandom's avatar
    jrandom committed
      public boolean started() { return started; }
    
    jrandom's avatar
    jrandom committed
      
    
    zzz's avatar
    zzz committed
       * @param fast if true, limit the life of the unannounce threads
    
    zzz's avatar
    zzz committed
      public synchronized void halt(boolean fast) {
    
    zzz's avatar
    zzz committed
        boolean wasStopped = stop;
        if (wasStopped) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Already stopped: " + _threadName);
        } else {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Stopping: " + _threadName);
            stop = true;
        }
        SimpleTimer2.TimedEvent e = _event;
        if (e != null) {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Cancelling next announce " + _threadName);
            e.cancel();
            _event = null;
        }
        Thread t = _thread;
        if (t != null) {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Interrupting " + t.getName());
            t.interrupt();
        }
    
    zzz's avatar
    zzz committed
        _fastUnannounce = true;
    
    zzz's avatar
    zzz committed
        if (!wasStopped)
            unannounce();
      }
    
      private void queueLoop(long delay) {
          _event = new Runner(delay);
      }
    
      private class Runner extends SimpleTimer2.TimedEvent {
          public Runner(long delay) {
    
              super(_util.getContext().simpleTimer2(), delay);
    
    zzz's avatar
    zzz committed
          }
    
          public void timeReached() {
              _event = null;
              _thread = new I2PAppThread(TrackerClient.this, _threadName + " #" + (++_runCount), true);
              _thread.start();
          }
    
    jrandom's avatar
    jrandom committed
      private boolean verifyConnected() {
    
    zzz's avatar
    zzz committed
        while (!stop && !_util.connected()) {
            boolean ok = _util.connect();
    
    jrandom's avatar
    jrandom committed
            if (!ok) {
                try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
            }
        }
    
    zzz's avatar
    zzz committed
        return !stop && _util.connected();
    
    jrandom's avatar
    jrandom committed
      }
      
    
    zzz's avatar
    zzz committed
      /**
       *  Setup the first time only,
       *  then one pass (usually) through the trackers, PEX, and DHT.
       *  This will take several seconds to several minutes.
       */
      public void run() {
    
          long begin = _util.getContext().clock().now();
    
    zzz's avatar
    zzz committed
          if (_log.shouldLog(Log.DEBUG))
              _log.debug("Start " + Thread.currentThread().getName());
          try {
              if (!_initialized) {
                  setup();
    
    zzz's avatar
    zzz committed
              }
              if (trackers.isEmpty() && _util.getDHT() == null) {
                  stop = true;
                  this.snark.addMessage(_util.getString("No valid trackers for {0} - enable opentrackers or DHT?",
                                                  this.snark.getBaseName()));
                  _log.error("No valid trackers for " + this.snark.getBaseName());
                  this.snark.stopTorrent();
                  return;
              }
              if (!_initialized) {
    
    zzz's avatar
    zzz committed
                  _initialized = true;
                  // FIXME only when starting everybody at once, not for a single torrent
    
    zzz's avatar
    zzz committed
                  long delay = _util.getContext().random().nextInt(30*1000);
    
    zzz's avatar
    zzz committed
                  try {
                      Thread.sleep(delay);
                  } catch (InterruptedException ie) {}
              }
              loop();
          } finally {
              // don't hold ref
              _thread = null;
              if (_log.shouldLog(Log.DEBUG))
                  _log.debug("Finish " + Thread.currentThread().getName() +
    
                             " after " + DataHelper.formatDuration(_util.getContext().clock().now() - begin));
    
    zzz's avatar
    zzz committed
          }
      }
    
    zzz's avatar
    zzz committed
      /**
       *  Do this one time only (not every time it is started).
       *  @since 0.9.1
       */
    
    zzz's avatar
    zzz committed
      private void setup() {
    
    zzz's avatar
    zzz committed
        // Construct the list of trackers for this torrent,
        // starting with the primary one listed in the metainfo,
        // followed by the secondary open trackers
        // It's painful, but try to make sure if an open tracker is also
        // the primary tracker, that we don't add it twice.
    
    zzz's avatar
    zzz committed
        // todo: check for b32 matches as well
    
        String primary = null;
    
    zzz's avatar
    zzz committed
        if (meta != null)
    
    zzz's avatar
    zzz committed
            primary = meta.getAnnounce();
    
    zzz's avatar
    zzz committed
        else if (additionalTrackerURL != null)
            primary = additionalTrackerURL;
    
        Set<Hash> trackerHashes = new HashSet(8);
    
        // primary tracker
    
    zzz's avatar
    zzz committed
        if (primary != null) {
    
            if (isNewValidTracker(trackerHashes, primary)) {
    
    zzz's avatar
    zzz committed
                trackers.add(new TCTracker(primary, true));
    
    zzz's avatar
    zzz committed
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Announce: [" + primary + "] infoHash: " + infoHash);
    
    zzz's avatar
    zzz committed
            } else {
    
    zzz's avatar
    zzz committed
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Skipping invalid or non-i2p announce: " + primary);
    
    zzz's avatar
    zzz committed
        } else {
            _log.warn("No primary announce");
        }
    
    
        // announce list
        if (meta != null && !meta.isPrivate()) {
            List<List<String>> list = meta.getAnnounceList();
            if (list != null) {
                for (List<String> llist : list) {
                    for (String url : llist) {
                        if (!isNewValidTracker(trackerHashes, url))
                            continue;
                        trackers.add(new TCTracker(url, trackers.isEmpty()));
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("Additional announce (list): [" + url + "] for infoHash: " + infoHash);
                    }
                }
            }
        }
    
        // configured open trackers
    
    zzz's avatar
    zzz committed
        if (meta == null || !meta.isPrivate()) {
            List<String> tlist = _util.getOpenTrackers();
    
    zzz's avatar
    zzz committed
            for (int i = 0; i < tlist.size(); i++) {
    
                String url = tlist.get(i);
                if (!isNewValidTracker(trackerHashes, url))
    
    zzz's avatar
    zzz committed
                    continue;
    
                // opentrackers are primary if we don't have primary
                trackers.add(new TCTracker(url, trackers.isEmpty()));
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
    
    zzz's avatar
    zzz committed
            }
        }
    
    zzz's avatar
    zzz committed
    
        // backup trackers if DHT needs bootstrapping
        if (trackers.isEmpty() && (meta == null || !meta.isPrivate())) {
            List<String> tlist = _util.getBackupTrackers();
            for (int i = 0; i < tlist.size(); i++) {
    
                String url = tlist.get(i);
                if (!isNewValidTracker(trackerHashes, url))
    
    zzz's avatar
    zzz committed
                    continue;
    
                backupTrackers.add(new TCTracker(url, false));
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Backup announce: [" + url + "] for infoHash: " + infoHash);
    
    zzz's avatar
    zzz committed
            }
    
            if (backupTrackers.isEmpty()) {
    
    zzz's avatar
    zzz committed
                backupTrackers.add(new TCTracker(DEFAULT_BACKUP_TRACKER, false));
    
    zzz's avatar
    zzz committed
        }
    
    zzz's avatar
    zzz committed
        this.completed = coordinator.getLeft() == 0;
    
    zzz's avatar
    zzz committed
      }
    
    zzz's avatar
    zzz committed
    
    
      /**
       *  @param existing the ones we already know about
       *  @param ann an announce URL non-null
       *  @return true if ann is valid and new; adds to existing if returns true
       *  @since 0.9.5
       */
      private boolean isNewValidTracker(Set<Hash> existing, String ann) {
          Hash h = getHostHash(ann);
          if (h == null) {
             _log.error("Bad announce URL: [" + ann + ']');
             return false;
          }
          boolean rv = existing.add(h);
          if (!rv) {
              if (_log.shouldLog(Log.INFO))
                 _log.info("Dup announce URL: [" + ann + ']');
          }
          return rv;
      }
    
    
    zzz's avatar
    zzz committed
      /**
       *  Announce to all the trackers, get peers from PEX and DHT, then queue up a SimpleTimer2 event.
       *  This will take several seconds to several minutes.
       *  @since 0.9.1
       */
      private void loop() {
    
    zzz's avatar
    zzz committed
            // normally this will only go once, then call queueLoop() and return
    
    zzz's avatar
    zzz committed
                if (!verifyConnected()) {
                    stop = true;
                    return;
                }
    
    
    zzz's avatar
    zzz committed
                // Local DHT tracker announce
    
                DHT dht = _util.getDHT();
    
    zzz's avatar
    zzz committed
                if (dht != null && (meta == null || !meta.isPrivate()))
    
                    dht.announce(snark.getInfoHash());
    
    zzz's avatar
    zzz committed
                int maxSeenPeers = 0;
                if (!trackers.isEmpty())
                    maxSeenPeers = getPeersFromTrackers(trackers);
                int p = getPeersFromPEX();
                if (p > maxSeenPeers)
                    maxSeenPeers = p;
                p = getPeersFromDHT();
                if (p > maxSeenPeers)
                    maxSeenPeers = p;
                // backup if DHT needs bootstrapping
                if (trackers.isEmpty() && !backupTrackers.isEmpty() && dht != null && dht.size() < 16) {
                    p = getPeersFromTrackers(backupTrackers);
                    if (p > maxSeenPeers)
                        maxSeenPeers = p;
                }
    
                // we could try and total the unique peers but that's too hard for now
                snark.setTrackerSeenPeers(maxSeenPeers);
    
                if (stop)
                    return;
    
                try {
                    // Sleep some minutes...
                    // Sleep the minimum interval for all the trackers, but 60s minimum
                    int delay;
                    Random r = _util.getContext().random();
                    int random = r.nextInt(120*1000);
                    if (completed && runStarted)
                      delay = 3*SLEEP*60*1000 + random;
                    else if (snark.getTrackerProblems() != null && ++consecutiveFails < MAX_CONSEC_FAILS)
                      delay = INITIAL_SLEEP;
                    else if ((!runStarted) && _runCount < MAX_CONSEC_FAILS)
                      delay = INITIAL_SLEEP;
                    else
                      // sleep a while, when we wake up we will contact only the trackers whose intervals have passed
                      delay = SLEEP*60*1000 + random;
    
                    if (delay > 20*1000) {
                      // put ourselves on SimpleTimer2
                      if (_log.shouldLog(Log.DEBUG))
                          _log.debug("Requeueing in " + DataHelper.formatDuration(delay) + ": " + Thread.currentThread().getName());
                      queueLoop(delay);
                      return;
                    } else if (delay > 0) {
                      Thread.sleep(delay);
                    }
                  } catch(InterruptedException interrupt) {}
              } // *** end of while loop
          } // try
        catch (Throwable t)
          {
            _log.error("TrackerClient: " + t, t);
            if (t instanceof OutOfMemoryError)
                throw (OutOfMemoryError)t;
          }
      }
    
      /**
       *  @return max peers seen
       */
    
    zzz's avatar
    zzz committed
      private int getPeersFromTrackers(List<TCTracker> trckrs) {
    
    zzz's avatar
    zzz committed
                long uploaded = coordinator.getUploaded();
                long downloaded = coordinator.getDownloaded();
                long left = coordinator.getLeft();   // -1 in magnet mode
    
                
                // First time we got a complete download?
                String event;
                if (!completed && left == 0)
                  {
                    completed = true;
                    event = COMPLETED_EVENT;
                  }
                else
                  event = NO_EVENT;
    
    zzz's avatar
    zzz committed
    
    
    zzz's avatar
    zzz committed
                // *** loop once for each tracker
                int maxSeenPeers = 0;
    
    zzz's avatar
    zzz committed
                for (TCTracker tr : trckrs) {
    
    zzz's avatar
    zzz committed
                  if ((!stop) && (!tr.stop) &&
    
                      (completed || coordinator.needOutboundPeers() || !tr.started) &&
    
    sponge's avatar
    sponge committed
                      (event.equals(COMPLETED_EVENT) || System.currentTimeMillis() > tr.lastRequestTime + tr.interval))
    
    zzz's avatar
    zzz committed
                        if (!tr.started)
                          event = STARTED_EVENT;
                        TrackerInfo info = doRequest(tr, infoHash, peerID,
    
                        snark.setTrackerProblems(null);
    
    zzz's avatar
    zzz committed
                        tr.trackerProblems = null;
                        tr.registerFails = 0;
                        tr.consecutiveFails = 0;
                        if (tr.isPrimary)
                            consecutiveFails = 0;
    
    sponge's avatar
    sponge committed
                        runStarted = true;
    
    zzz's avatar
    zzz committed
                        tr.started = true;
    
    
                        Set<Peer> peers = info.getPeers();
    
    zzz's avatar
    zzz committed
                        tr.seenPeers = info.getPeerCount();
    
                        if (snark.getTrackerSeenPeers() < tr.seenPeers) // update rising number quickly
                            snark.setTrackerSeenPeers(tr.seenPeers);
    
    
                        // pass everybody over to our tracker
    
    zzz's avatar
    zzz committed
                        DHT dht = _util.getDHT();
    
                        if (dht != null) {
    
                            for (Peer peer : peers) {
    
                                dht.announce(snark.getInfoHash(), peer.getPeerID().getDestHash());
    
    zzz's avatar
    zzz committed
                        if (coordinator.needOutboundPeers()) {
    
    jrandom's avatar
    jrandom committed
                            // we only want to talk to new people if we need things
                            // from them (duh)
    
                            List<Peer> ordered = new ArrayList(peers);
    
    zzz's avatar
    zzz committed
                            Random r = _util.getContext().random();
    
                            Collections.shuffle(ordered, r);
    
                            Iterator<Peer> it = ordered.iterator();
    
    zzz's avatar
    zzz committed
                            while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
    
                              Peer cur = it.next();
    
    zzz's avatar
    zzz committed
                              // FIXME if id == us || dest == us continue;
    
    zzz's avatar
    zzz committed
                              // only delay if we actually make an attempt to add peer
    
    zzz's avatar
    zzz committed
                              if(coordinator.addPeer(cur) && it.hasNext()) {
    
    zzz's avatar
    zzz committed
                                int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
    
    zzz's avatar
    zzz committed
                                try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                              }
    
    jrandom's avatar
    jrandom committed
                            }
                        }
    
                      }
                    catch (IOException ioe)
                      {
                        // Probably not fatal (if it doesn't last to long...)
    
                        if (_log.shouldLog(Log.WARN))
                            _log.warn
    
                          ("WARNING: Could not contact tracker at '"
    
    zzz's avatar
    zzz committed
                        tr.trackerProblems = ioe.getMessage();
                        // don't show secondary tracker problems to the user
                        if (tr.isPrimary)
    
                          snark.setTrackerProblems(tr.trackerProblems);
    
                        if (tr.trackerProblems.toLowerCase(Locale.US).startsWith(NOT_REGISTERED)) {
    
    zzz's avatar
    zzz committed
                          // Give a guy some time to register it if using opentrackers too
    
    zzz's avatar
    zzz committed
                          //if (trckrs.size() == 1) {
                          //  stop = true;
                          //  snark.stopTorrent();
                          //} else { // hopefully each on the opentrackers list is really open
    
    zzz's avatar
    zzz committed
                            if (tr.registerFails++ > MAX_REGISTER_FAILS)
                              tr.stop = true;
    
    zzz's avatar
    zzz committed
                          //
    
    zzz's avatar
    zzz committed
                        }
    
                        if (++tr.consecutiveFails == MAX_CONSEC_FAILS) {
                            tr.seenPeers = 0;
                            if (tr.interval < LONG_SLEEP)
                                tr.interval = LONG_SLEEP;  // slow down
                        }
    
    zzz's avatar
    zzz committed
                  } else {
    
                      if (_log.shouldLog(Log.INFO))
                          _log.info("Not announcing to " + tr.announce + " last announce was " +
                                   new Date(tr.lastRequestTime) + " interval is " + DataHelper.formatDuration(tr.interval));
    
    zzz's avatar
    zzz committed
                  if ((!tr.stop) && maxSeenPeers < tr.seenPeers)
                      maxSeenPeers = tr.seenPeers;
                }  // *** end of trackers loop here
    
    
    zzz's avatar
    zzz committed
                return maxSeenPeers;
      }
    
      /**
       *  @return max peers seen
       */
      private int getPeersFromPEX() {
    
    zzz's avatar
    zzz committed
                // Get peers from PEX
    
    zzz's avatar
    zzz committed
                int rv = 0;
    
    zzz's avatar
    zzz committed
                if (coordinator.needOutboundPeers() && (meta == null || !meta.isPrivate()) && !stop) {
    
    zzz's avatar
    zzz committed
                    Set<PeerID> pids = coordinator.getPEXPeers();
                    if (!pids.isEmpty()) {
    
                        if (_log.shouldLog(Log.INFO))
                            _log.info("Got " + pids.size() + " from PEX");
    
    zzz's avatar
    zzz committed
                        List<Peer> peers = new ArrayList(pids.size());
                        for (PeerID pID : pids) {
                            peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
                        }
    
    zzz's avatar
    zzz committed
                        Random r = _util.getContext().random();
    
    zzz's avatar
    zzz committed
                        Collections.shuffle(peers, r);
                        Iterator<Peer> it = peers.iterator();
    
    zzz's avatar
    zzz committed
                        while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
    
    zzz's avatar
    zzz committed
                            Peer cur = it.next();
                            if (coordinator.addPeer(cur) && it.hasNext()) {
    
    zzz's avatar
    zzz committed
                                int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
    
    zzz's avatar
    zzz committed
                                try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                             }
                        }
    
    zzz's avatar
    zzz committed
                        rv = pids.size();
    
    zzz's avatar
    zzz committed
                    }
    
    zzz's avatar
    zzz committed
                } else {
    
                    if (_log.shouldLog(Log.INFO))
                        _log.info("Not getting PEX peers");
    
    zzz's avatar
    zzz committed
                }
    
    zzz's avatar
    zzz committed
                return rv;
        }
    
    zzz's avatar
    zzz committed
    
    
    zzz's avatar
    zzz committed
      /**
       *  @return max peers seen
       */
      private int getPeersFromDHT() {
    
    zzz's avatar
    zzz committed
                // Get peers from DHT
                // FIXME this needs to be in its own thread
    
    zzz's avatar
    zzz committed
                int rv = 0;
                DHT dht = _util.getDHT();
    
                if (dht != null && (meta == null || !meta.isPrivate()) && (!stop) &&
                    _util.getContext().clock().now() >  lastDHTAnnounce + MIN_DHT_ANNOUNCE_INTERVAL) {
    
    zzz's avatar
    zzz committed
                    int numwant;
    
    zzz's avatar
    zzz committed
                    if (!coordinator.needOutboundPeers())
    
    zzz's avatar
    zzz committed
                        numwant = 1;
                    else
                        numwant = _util.getMaxConnections();
    
    zzz's avatar
    zzz committed
                    Collection<Hash> hashes = dht.getPeers(snark.getInfoHash(), numwant, 2*60*1000);
    
                    if (!hashes.isEmpty()) {
    
    zzz's avatar
    zzz committed
                        runStarted = true;
    
                        lastDHTAnnounce = _util.getContext().clock().now();
    
    zzz's avatar
    zzz committed
                        rv = hashes.size();
    
                    if (_log.shouldLog(Log.INFO))
                        _log.info("Got " + hashes + " from DHT");
    
    zzz's avatar
    zzz committed
                    // announce  ourselves while the token is still good
                    // FIXME this needs to be in its own thread
                    if (!stop) {
    
                        int good = dht.announce(snark.getInfoHash(), 1, 5*60*1000);
    
                        if (_log.shouldLog(Log.INFO))
                            _log.info("Sent " + good + " good announces to DHT");
    
    zzz's avatar
    zzz committed
                    }
    
                    // now try these peers
                    if ((!stop) && !hashes.isEmpty()) {
                        List<Peer> peers = new ArrayList(hashes.size());
                        for (Hash h : hashes) {
    
    zzz's avatar
    zzz committed
                            try {
                                PeerID pID = new PeerID(h.getData(), _util);
                                peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
                            } catch (InvalidBEncodingException ibe) {}
    
    zzz's avatar
    zzz committed
                        Random r = _util.getContext().random();
    
    zzz's avatar
    zzz committed
                        Collections.shuffle(peers, r);
                        Iterator<Peer> it = peers.iterator();
    
    zzz's avatar
    zzz committed
                        while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
    
    zzz's avatar
    zzz committed
                            Peer cur = it.next();
    
    zzz's avatar
    zzz committed
                            if (coordinator.addPeer(cur) && it.hasNext()) {
    
    zzz's avatar
    zzz committed
                                int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
    
    zzz's avatar
    zzz committed
                                try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                             }
                        }
                    }
    
    zzz's avatar
    zzz committed
                } else {
    
                    if (_log.shouldLog(Log.INFO))
                        _log.info("Not getting DHT peers");
    
    zzz's avatar
    zzz committed
                return rv;
    
    zzz's avatar
    zzz committed
      }
    
    
    zzz's avatar
    zzz committed
    
    
    zzz's avatar
    zzz committed
      /**
       *  Creates a thread for each tracker in parallel if tunnel is still open
       *  @since 0.9.1
       */
      private void unannounce() {
          // Local DHT tracker unannounce
    
          DHT dht = _util.getDHT();
          if (dht != null)
              dht.unannounce(snark.getInfoHash());
    
    zzz's avatar
    zzz committed
          int i = 0;
    
    zzz's avatar
    zzz committed
          for (TCTracker tr : trackers) {
    
    zzz's avatar
    zzz committed
              if (_util.connected() &&
                  tr.started && (!tr.stop) && tr.trackerProblems == null) {
                  try {
    
    zzz's avatar
    zzz committed
                      (new I2PAppThread(new Unannouncer(tr), _threadName + " U" + (++i), true)).start();
    
    zzz's avatar
    zzz committed
                  } catch (OutOfMemoryError oom) {
                      // probably ran out of threads, ignore
                      tr.reset();
                  }
              } else {
                  tr.reset();
              }
          }
      }
    
      /**
       *  Send "stopped" to a single tracker
       *  @since 0.9.1
       */
      private class Unannouncer implements Runnable {
    
    zzz's avatar
    zzz committed
         private final TCTracker tr;
    
    zzz's avatar
    zzz committed
    
    
    zzz's avatar
    zzz committed
         public Unannouncer(TCTracker tr) {
    
    zzz's avatar
    zzz committed
             this.tr = tr;
         }
    
         public void run() {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Running unannounce " + _threadName + " to " + tr.announce);
            long uploaded = coordinator.getUploaded();
            long downloaded = coordinator.getDownloaded();
            long left = coordinator.getLeft();
    
                // Don't try to restart I2CP connection just to say goodbye
    
    zzz's avatar
    zzz committed
                  if (_util.connected()) {
                      if (tr.started && (!tr.stop) && tr.trackerProblems == null)
                          doRequest(tr, infoHash, peerID, uploaded,
    
    zzz's avatar
    zzz committed
                  }
    
              }
            catch(IOException ioe) { /* ignored */ }
    
    zzz's avatar
    zzz committed
            tr.reset();
         }
    
    zzz's avatar
    zzz committed
      private TrackerInfo doRequest(TCTracker tr, String infoHash,
    
                                    String peerID, long uploaded,
                                    long downloaded, long left, String event)
        throws IOException
      {
    
        StringBuilder buf = new StringBuilder(512);
        buf.append(tr.announce);
        if (tr.announce.contains("?"))
            buf.append('&');
        else
            buf.append('?');
        buf.append("info_hash=").append(infoHash)
           .append("&peer_id=").append(peerID)
           .append("&port=").append(port)
           .append("&ip=" ).append(_util.getOurIPString()).append(".i2p")
           .append("&uploaded=").append(uploaded)
           .append("&downloaded=").append(downloaded)
           .append("&left=");
    
        // What do we send for left in magnet mode? Can we omit it?
    
        if (left >= 0)
            buf.append(left);
        else
            buf.append('1');
        buf.append("&compact=1");  // NOTE: opentracker will return 400 for &compact alone
        if (! event.equals(NO_EVENT))
            buf.append("&event=").append(event);
        buf.append("&numwant=");
    
    zzz's avatar
    zzz committed
        boolean small = left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers();
        if (small)
    
    zzz's avatar
    zzz committed
        else
    
            buf.append(_util.getMaxConnections());
        String s = buf.toString();
    
        if (_log.shouldLog(Log.INFO))
            _log.info("Sending TrackerClient request: " + s);
    
    zzz's avatar
    zzz committed
        tr.lastRequestTime = System.currentTimeMillis();
    
    zzz's avatar
    zzz committed
        // Don't wait for a response to stopped when shutting down
        boolean fast = _fastUnannounce && event.equals(STOPPED_EVENT);
    
    zzz's avatar
    zzz committed
        byte[] fetched = _util.get(s, true, fast ? -1 : 0, small ? 128 : 1024, small ? 1024 : 8*1024);
    
        if (fetched == null) {
            throw new IOException("Error fetching " + s);
        }
        
    
    zzz's avatar
    zzz committed
            InputStream in = new ByteArrayInputStream(fetched);
    
            TrackerInfo info = new TrackerInfo(in, snark.getID(),
    
                                               snark.getInfoHash(), snark.getMetaInfo(), _util);
    
            if (_log.shouldLog(Log.INFO))
                _log.info("TrackerClient response: " + info);
    
    jrandom's avatar
    jrandom committed
    
            String failure = info.getFailureReason();
            if (failure != null)
              throw new IOException(failure);
    
    
            tr.interval = Math.max(MIN_TRACKER_ANNOUNCE_INTERVAL, info.getInterval() * 1000l);
    
    jrandom's avatar
    jrandom committed
            return info;
    
    zzz's avatar
    zzz committed
       * Very lazy byte[] to URL encoder.  Just encodes almost everything, even
       * some "normal" chars.
       * By not encoding about 1/4 of the chars, we make random data like hashes about 16% smaller.
       *
       * RFC1738: 0-9a-zA-Z$-_.+!*'(),
       * Us:      0-9a-zA-Z
       *
    
    zzz's avatar
    zzz committed
      public static String urlencode(byte[] bs)
    
        StringBuilder sb = new StringBuilder(bs.length*3);
    
        for (int i = 0; i < bs.length; i++)
          {
            int c = bs[i] & 0xFF;
    
    zzz's avatar
    zzz committed
            if ((c >= '0' && c <= '9') ||
                (c >= 'A' && c <= 'Z') ||
                (c >= 'a' && c <= 'z')) {
                sb.append((char)c);
            } else {
                sb.append('%');
                if (c < 16)
                  sb.append('0');
                sb.append(Integer.toHexString(c));
            }
    
    zzz's avatar
    zzz committed
    
    
    zzz's avatar
    zzz committed
      /**
    
       *  @param ann an announce URL
    
    zzz's avatar
    zzz committed
       *  @return true for i2p hosts only
       *  @since 0.7.12
       */
    
    zzz's avatar
    zzz committed
      public static boolean isValidAnnounce(String ann) {
    
    zzz's avatar
    zzz committed
        URL url;
        try {
           url = new URL(ann);
        } catch (MalformedURLException mue) {
           return false;
        }
        return url.getProtocol().equals("http") &&
               (url.getHost().endsWith(".i2p") || url.getHost().equals("i2p")) &&
               url.getPort() < 0;
      }
    
    
      /**
       *  @param ann an announce URL non-null
       *  @return a Hash for i2p hosts only, null otherwise
       *  @since 0.9.5
       */
      private static Hash getHostHash(String ann) {
        URL url;
        try {
            url = new URL(ann);
        } catch (MalformedURLException mue) {
            return null;
        }
        if (url.getPort() >= 0 || !url.getProtocol().equals("http"))
            return null;
        String host = url.getHost();
        if (host.endsWith(".i2p"))
            return ConvertToHash.getHash(host);
        if (host.equals("i2p")) {
            String path = url.getPath();
            if (path == null || path.length() < 517 ||
                !path.startsWith("/"))
                return null;
            String[] parts = path.substring(1).split("/?&;", 2);
            return ConvertToHash.getHash(parts[0]);
        }
        return null;
      }
    
    
    zzz's avatar
    zzz committed
      private static class TCTracker
    
    zzz's avatar
    zzz committed
      {
    
    zzz's avatar
    zzz committed
          final String announce;
          final boolean isPrimary;
    
    zzz's avatar
    zzz committed
          long interval;
          long lastRequestTime;
          String trackerProblems;
          boolean stop;
          boolean started;
          int registerFails;
          int consecutiveFails;
          int seenPeers;
    
    
    zzz's avatar
    zzz committed
          public TCTracker(String a, boolean p)
    
    zzz's avatar
    zzz committed
          {
              announce = a;
              isPrimary = p;
              interval = INITIAL_SLEEP;
    
    zzz's avatar
    zzz committed
          }
    
          /**
           *  Call before restarting
           *  @since 0.9.1
           */
          public void reset() {
    
    zzz's avatar
    zzz committed
              lastRequestTime = 0;
              trackerProblems = null;
              stop = false;
              started = false;
              registerFails = 0;
              consecutiveFails = 0;
              seenPeers = 0;
          }
      }