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

Skip to content
Snippets Groups Projects
TrackerClient.java 16.1 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;
    
    
    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;
    import java.util.Collections;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Random;
    import java.util.Set;
    
    import net.i2p.I2PAppContext;
    
    zzz's avatar
    zzz committed
    import net.i2p.util.I2PAppThread;
    
    jrandom's avatar
    jrandom committed
    import net.i2p.util.Log;
    
    
    /**
     * Informs metainfo tracker of events and gets new peers for peer
     * coordinator.
     *
     * @author Mark Wielaard (mark@klomp.org)
     */
    
    zzz's avatar
    zzz committed
    public class TrackerClient extends I2PAppThread
    
      private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(TrackerClient.class);
    
      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
    
    
      private final static int SLEEP = 5; // 5 minutes.
    
    zzz's avatar
    zzz committed
      private final static int DELAY_MIN = 2000; // 2 secs.
      private final static int DELAY_MUL = 1500; // 1.5 secs.
    
    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 I2PSnarkUtil _util;
    
      private final MetaInfo meta;
      private final PeerCoordinator coordinator;
      private final int port;
    
      private boolean stop;
    
    jrandom's avatar
    jrandom committed
      private boolean started;
    
    zzz's avatar
    zzz committed
      private List trackers;
    
    zzz's avatar
    zzz committed
      public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator)
    
      {
        // Set unique name.
        super("TrackerClient-" + urlencode(coordinator.getID()));
    
    zzz's avatar
    zzz committed
        _util = util;
    
        this.meta = meta;
        this.coordinator = coordinator;
    
        this.port = 6881; //(port == -1) ? 9 : port;
    
        stop = false;
    
    jrandom's avatar
    jrandom committed
        started = false;
    
    sponge's avatar
    sponge committed
        @Override
    
    jrandom's avatar
    jrandom committed
      public void start() {
    
    jrandom's avatar
    jrandom committed
          if (stop) throw new RuntimeException("Dont rerun me, create a copy");
    
    jrandom's avatar
    jrandom committed
          super.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
      
    
      /**
       * Interrupts this Thread to stop it.
       */
      public void halt()
      {
        stop = true;
        this.interrupt();
      }
    
    
    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
      }
      
    
    sponge's avatar
    sponge committed
        @Override
    
      public void run()
      {
        String infoHash = urlencode(meta.getInfoHash());
        String peerID = urlencode(coordinator.getID());
    
    
    zzz's avatar
    zzz committed
        _log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash);
    
    jrandom's avatar
    jrandom committed
        
    
    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
    
    zzz's avatar
    zzz committed
        trackers = new ArrayList(2);
    
    zzz's avatar
    zzz committed
        String primary = meta.getAnnounce();
        if (isValidAnnounce(primary)) {
            trackers.add(new Tracker(meta.getAnnounce(), true));
        } else {
            _log.warn("Skipping invalid or non-i2p announce: " + primary);
        }
    
    zzz's avatar
    zzz committed
        List tlist = _util.getOpenTrackers();
    
    zzz's avatar
    zzz committed
        if (tlist != null) {
            for (int i = 0; i < tlist.size(); i++) {
                 String url = (String)tlist.get(i);
    
    zzz's avatar
    zzz committed
                 if (!isValidAnnounce(url)) {
    
    zzz's avatar
    zzz committed
                    _log.error("Bad announce URL: [" + url + "]");
                    continue;
                 }
                 int slash = url.indexOf('/', 7);
                 if (slash <= 7) {
                    _log.error("Bad announce URL: [" + url + "]");
                    continue;
                 }
    
    zzz's avatar
    zzz committed
                 if (primary.startsWith(url.substring(0, slash)))
    
    zzz's avatar
    zzz committed
                    continue;
    
    zzz's avatar
    zzz committed
                 String dest = _util.lookup(url.substring(7, slash));
    
    zzz's avatar
    zzz committed
                 if (dest == null) {
    
    zzz's avatar
    zzz committed
                    _log.error("Announce host unknown: [" + url.substring(7, slash) + "]");
    
    zzz's avatar
    zzz committed
                    continue;
                 }
    
    zzz's avatar
    zzz committed
                 if (primary.startsWith("http://" + dest))
    
    zzz's avatar
    zzz committed
                    continue;
    
    zzz's avatar
    zzz committed
                 if (primary.startsWith("http://i2p/" + dest))
    
    zzz's avatar
    zzz committed
                    continue;
                 trackers.add(new Tracker(url, false));
                 _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
            }
        }
    
    
    zzz's avatar
    zzz committed
            // FIXME really need to get this message to the gui
            stop = true;
            _log.error("No valid trackers for infoHash: " + infoHash);
            return;
        }
    
    
        long uploaded = coordinator.getUploaded();
        long downloaded = coordinator.getDownloaded();
        long left = coordinator.getLeft();
    
        boolean completed = (left == 0);
    
    zzz's avatar
    zzz committed
        int sleptTime = 0;
    
    jrandom's avatar
    jrandom committed
            if (!verifyConnected()) return;
    
    sponge's avatar
    sponge committed
            boolean runStarted = false;
    
    zzz's avatar
    zzz committed
            boolean firstTime = true;
            int consecutiveFails = 0;
    
    zzz's avatar
    zzz committed
            Random r = I2PAppContext.getGlobalContext().random();
    
    zzz's avatar
    zzz committed
                    // Sleep the minimum interval for all the trackers, but 60s minimum
                    // except for the first time...
    
    zzz's avatar
    zzz committed
                    int delay;
    
    zzz's avatar
    zzz committed
                    int random = r.nextInt(120*1000);
                    if (firstTime) {
                      delay = r.nextInt(30*1000);
                      firstTime = false;
    
    sponge's avatar
    sponge committed
                    } else if (completed && runStarted)
    
    zzz's avatar
    zzz committed
                      delay = 3*SLEEP*60*1000 + random;
                    else if (coordinator.trackerProblems != null && ++consecutiveFails < 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;
    
    
    zzz's avatar
    zzz committed
                    if (delay > 0)
                      Thread.sleep(delay);
    
                  }
                catch(InterruptedException interrupt)
                  {
                    // ignore
                  }
    
                if (stop)
                  break;
    
    jrandom's avatar
    jrandom committed
           
                if (!verifyConnected()) return;
    
                
                uploaded = coordinator.getUploaded();
                downloaded = coordinator.getDownloaded();
                left = coordinator.getLeft();
                
                // 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
                // *** loop once for each tracker
    
                // Only do a request when necessary.
    
    zzz's avatar
    zzz committed
                sleptTime = 0;
    
    zzz's avatar
    zzz committed
                int maxSeenPeers = 0;
                for (Iterator iter = trackers.iterator(); iter.hasNext(); ) {
                  Tracker tr = (Tracker)iter.next();
                  if ((!stop) && (!tr.stop) &&
                      (completed || coordinator.needPeers()) &&
    
    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,
    
    zzz's avatar
    zzz committed
                        coordinator.trackerProblems = 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;
    
    
    jrandom's avatar
    jrandom committed
                        Set peers = info.getPeers();
    
    zzz's avatar
    zzz committed
                        tr.seenPeers = info.getPeerCount();
    
    zzz's avatar
    zzz committed
                        if (coordinator.trackerSeenPeers < tr.seenPeers) // update rising number quickly
                            coordinator.trackerSeenPeers = tr.seenPeers;
    
    jrandom's avatar
    jrandom committed
                        if ( (left > 0) && (!completed) ) {
                            // we only want to talk to new people if we need things
                            // from them (duh)
    
    jrandom's avatar
    jrandom committed
                            List ordered = new ArrayList(peers);
    
                            Collections.shuffle(ordered, r);
    
    jrandom's avatar
    jrandom committed
                            Iterator it = ordered.iterator();
    
    jrandom's avatar
    jrandom committed
                            while (it.hasNext()) {
                              Peer cur = (Peer)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
                              if(coordinator.addPeer(cur)) {
                                int delay = DELAY_MUL;
                                delay *= ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 10;
                                delay += DELAY_MIN;
                                sleptTime += delay;
                                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...)
    
    zzz's avatar
    zzz committed
                        _util.debug
    
                          ("WARNING: Could not contact tracker at '"
    
    zzz's avatar
    zzz committed
                           + tr.announce + "': " + ioe, Snark.WARNING);
                        tr.trackerProblems = ioe.getMessage();
                        // don't show secondary tracker problems to the user
                        if (tr.isPrimary)
                          coordinator.trackerProblems = tr.trackerProblems;
                        if (tr.trackerProblems.toLowerCase().startsWith(NOT_REGISTERED)) {
                          // Give a guy some time to register it if using opentrackers too
                          if (trackers.size() == 1) {
                            stop = true;
                            coordinator.snark.stopTorrent();
                          } else { // hopefully each on the opentrackers list is really open
                            if (tr.registerFails++ > MAX_REGISTER_FAILS)
                              tr.stop = true;
                          }
    
    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
                  if ((!tr.stop) && maxSeenPeers < tr.seenPeers)
                      maxSeenPeers = tr.seenPeers;
                }  // *** end of trackers loop here
    
                // we could try and total the unique peers but that's too hard for now
                coordinator.trackerSeenPeers = maxSeenPeers;
    
    sponge's avatar
    sponge committed
                if (!runStarted)
    
    zzz's avatar
    zzz committed
                    _util.debug("         Retrying in one minute...", Snark.DEBUG);
    
    zzz's avatar
    zzz committed
              } // *** end of while loop
          } // try
    
    zzz's avatar
    zzz committed
            _util.debug("TrackerClient: " + t, Snark.ERROR, t);
    
    jrandom's avatar
    jrandom committed
            if (t instanceof OutOfMemoryError)
                throw (OutOfMemoryError)t;
    
    zzz's avatar
    zzz committed
                // try to contact everybody we can
    
                // Don't try to restart I2CP connection just to say goodbye
    
    zzz's avatar
    zzz committed
                for (Iterator iter = trackers.iterator(); iter.hasNext(); ) {
    
                  if (!_util.connected()) return;
    
    zzz's avatar
    zzz committed
                  Tracker tr = (Tracker)iter.next();
                  if (tr.started && (!tr.stop) && tr.trackerProblems == null)
                      doRequest(tr, infoHash, peerID, uploaded,
    
    zzz's avatar
    zzz committed
                }
    
    zzz's avatar
    zzz committed
      private TrackerInfo doRequest(Tracker tr, String infoHash,
    
                                    String peerID, long uploaded,
                                    long downloaded, long left, String event)
        throws IOException
      {
    
    zzz's avatar
    zzz committed
        String s = tr.announce
    
          + "?info_hash=" + infoHash
          + "&peer_id=" + peerID
          + "&port=" + port
    
    zzz's avatar
    zzz committed
          + "&ip=" + _util.getOurIPString() + ".i2p"
    
          + "&uploaded=" + uploaded
          + "&downloaded=" + downloaded
          + "&left=" + left
    
    zzz's avatar
    zzz committed
          + "&compact"
    
    sponge's avatar
    sponge committed
          + ((! event.equals(NO_EVENT)) ? ("&event=" + event) : "");
    
    zzz's avatar
    zzz committed
        if (left <= 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
            s += "&numwant=0";
        else
            s += "&numwant=" + _util.getMaxConnections();
    
    zzz's avatar
    zzz committed
        _util.debug("Sending TrackerClient request: " + s, Snark.INFO);
    
    zzz's avatar
    zzz committed
        tr.lastRequestTime = System.currentTimeMillis();
    
    zzz's avatar
    zzz committed
        File fetched = _util.get(s);
    
        if (fetched == null) {
            throw new IOException("Error fetching " + s);
        }
        
    
    jrandom's avatar
    jrandom committed
        InputStream in = null;
    
    jrandom's avatar
    jrandom committed
        try {
    
    jrandom's avatar
    jrandom committed
            in = new FileInputStream(fetched);
    
    jrandom's avatar
    jrandom committed
            TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
                                               coordinator.getMetaInfo());
    
    zzz's avatar
    zzz committed
            _util.debug("TrackerClient response: " + info, Snark.INFO);
    
    jrandom's avatar
    jrandom committed
    
            String failure = info.getFailureReason();
            if (failure != null)
              throw new IOException(failure);
    
    
    zzz's avatar
    zzz committed
            tr.interval = info.getInterval() * 1000;
    
    jrandom's avatar
    jrandom committed
            return info;
        } finally {
    
    jrandom's avatar
    jrandom committed
            if (in != null) try { in.close(); } catch (IOException ioe) {}
    
    jrandom's avatar
    jrandom committed
            fetched.delete();
        }
    
    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
      /**
       *  @return true for i2p hosts only
       *  @since 0.7.12
       */
      static boolean isValidAnnounce(String ann) {
        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;
      }
    
    
    zzz's avatar
    zzz committed
      private static class Tracker
    
    zzz's avatar
    zzz committed
      {
          String announce;
          boolean isPrimary;
          long interval;
          long lastRequestTime;
          String trackerProblems;
          boolean stop;
          boolean started;
          int registerFails;
          int consecutiveFails;
          int seenPeers;
    
          public Tracker(String a, boolean p)
          {
              announce = a;
              isPrimary = p;
              interval = INITIAL_SLEEP;
              lastRequestTime = 0;
              trackerProblems = null;
              stop = false;
              started = false;
              registerFails = 0;
              consecutiveFails = 0;
              seenPeers = 0;
          }
      }