forked from I2P_Developers/i2p.i2p
981 lines
37 KiB
Java
981 lines
37 KiB
Java
/* 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.ByteArrayInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Random;
|
|
import java.util.Set;
|
|
|
|
import net.i2p.crypto.SigType;
|
|
import net.i2p.data.DataHelper;
|
|
import net.i2p.data.Destination;
|
|
import net.i2p.data.Hash;
|
|
import net.i2p.util.ConvertToHash;
|
|
import net.i2p.util.I2PAppThread;
|
|
import net.i2p.util.Log;
|
|
import net.i2p.util.SimpleTimer2;
|
|
|
|
import org.klomp.snark.bencode.InvalidBEncodingException;
|
|
import org.klomp.snark.dht.DHT;
|
|
|
|
/**
|
|
* Informs metainfo tracker of events and gets new peers for peer
|
|
* coordinator.
|
|
*
|
|
* 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)
|
|
*/
|
|
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";
|
|
private static final String NOT_REGISTERED = "torrent not registered"; //bytemonsoon
|
|
private static final String NOT_REGISTERED_2 = "torrent not found"; // diftracker
|
|
private static final String NOT_REGISTERED_3 = "torrent unauthorised"; // vuze
|
|
private static final String ERROR_GOT_HTML = "received html"; // fake return
|
|
|
|
private final static int SLEEP = 5; // 5 minutes.
|
|
private final static int DELAY_MIN = 2000; // 2 secs.
|
|
private final static int DELAY_RAND = 6*1000;
|
|
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
|
|
private final static long MIN_TRACKER_ANNOUNCE_INTERVAL = 15*60*1000;
|
|
private final static long MIN_DHT_ANNOUNCE_INTERVAL = 39*60*1000;
|
|
/** No guidance in BEP 5; standard practice is K (=8) */
|
|
private static final int DHT_ANNOUNCE_PEERS = 4;
|
|
public static final int PORT = 6881;
|
|
private static final int MAX_TRACKERS = 12;
|
|
// tracker.welterde.i2p
|
|
private static final Hash DSA_ONLY_TRACKER = ConvertToHash.getHash("cfmqlafjfmgkzbt4r3jsfyhgsr5abgxryl6fnz3d3y5a365di5aa.b32.i2p");
|
|
|
|
private final I2PSnarkUtil _util;
|
|
private final MetaInfo meta;
|
|
private final String infoHash;
|
|
private final String peerID;
|
|
private final String additionalTrackerURL;
|
|
private final PeerCoordinator coordinator;
|
|
private final Snark snark;
|
|
private final int port;
|
|
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;
|
|
// if we don't want anything else.
|
|
// Not necessarily seeding, as we may have skipped some files.
|
|
private boolean completed;
|
|
private volatile boolean _fastUnannounce;
|
|
private long lastDHTAnnounce;
|
|
private final List<TCTracker> trackers;
|
|
private final List<TCTracker> backupTrackers;
|
|
private long _startedOn;
|
|
|
|
/**
|
|
* Call start() to start it.
|
|
*
|
|
* @param meta null if in magnet mode
|
|
* @param additionalTrackerURL may be null, from the ?tr= param in magnet mode, otherwise ignored
|
|
*/
|
|
public TrackerClient(I2PSnarkUtil util, MetaInfo meta, String additionalTrackerURL,
|
|
PeerCoordinator coordinator, Snark snark)
|
|
{
|
|
super();
|
|
// Set unique name.
|
|
String id = urlencode(snark.getID());
|
|
_threadName = "TrackerClient " + id.substring(id.length() - 12);
|
|
_util = util;
|
|
_log = util.getContext().logManager().getLog(TrackerClient.class);
|
|
this.meta = meta;
|
|
this.additionalTrackerURL = additionalTrackerURL;
|
|
this.coordinator = coordinator;
|
|
this.snark = snark;
|
|
|
|
this.port = PORT; //(port == -1) ? 9 : port;
|
|
this.infoHash = urlencode(snark.getInfoHash());
|
|
this.peerID = urlencode(snark.getID());
|
|
this.trackers = new ArrayList<TCTracker>(2);
|
|
this.backupTrackers = new ArrayList<TCTracker>(2);
|
|
}
|
|
|
|
public synchronized void start() {
|
|
if (!stop) {
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("Already started: " + _threadName);
|
|
return;
|
|
}
|
|
stop = false;
|
|
consecutiveFails = 0;
|
|
runStarted = false;
|
|
_fastUnannounce = false;
|
|
snark.setTrackerProblems(null);
|
|
_thread = new I2PAppThread(this, _threadName + " #" + (++_runCount), true);
|
|
_thread.start();
|
|
started = true;
|
|
}
|
|
|
|
public boolean halted() { return stop; }
|
|
public boolean started() { return started; }
|
|
|
|
/**
|
|
* Interrupts this Thread to stop it.
|
|
* @param fast if true, limit the life of the unannounce threads
|
|
*/
|
|
public synchronized void halt(boolean fast) {
|
|
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();
|
|
}
|
|
_fastUnannounce = true;
|
|
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);
|
|
}
|
|
|
|
public void timeReached() {
|
|
_event = null;
|
|
_thread = new I2PAppThread(TrackerClient.this, _threadName + " #" + (++_runCount), true);
|
|
_thread.start();
|
|
}
|
|
}
|
|
|
|
private boolean verifyConnected() {
|
|
while (!stop && !_util.connected()) {
|
|
boolean ok = _util.connect();
|
|
if (!ok) {
|
|
try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
|
|
}
|
|
}
|
|
return !stop && _util.connected();
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
if (_log.shouldLog(Log.DEBUG))
|
|
_log.debug("Start " + Thread.currentThread().getName());
|
|
try {
|
|
if (!_initialized) {
|
|
setup();
|
|
}
|
|
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) {
|
|
_initialized = true;
|
|
// FIXME only when starting everybody at once, not for a single torrent
|
|
long delay = _util.getContext().random().nextInt(30*1000);
|
|
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));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Do this one time only (not every time it is started).
|
|
* @since 0.9.1
|
|
*/
|
|
private void setup() {
|
|
// 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.
|
|
// todo: check for b32 matches as well
|
|
String primary = null;
|
|
if (meta != null)
|
|
primary = meta.getAnnounce();
|
|
else if (additionalTrackerURL != null)
|
|
primary = additionalTrackerURL;
|
|
Set<Hash> trackerHashes = new HashSet<Hash>(8);
|
|
|
|
// primary tracker
|
|
if (primary != null) {
|
|
if (isNewValidTracker(trackerHashes, primary)) {
|
|
trackers.add(new TCTracker(primary, true));
|
|
if (_log.shouldLog(Log.DEBUG))
|
|
_log.debug("Announce: [" + primary + "] infoHash: " + infoHash);
|
|
} else {
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("Skipping invalid or non-i2p announce: " + primary);
|
|
}
|
|
} else {
|
|
_log.warn("No primary announce");
|
|
}
|
|
|
|
// announce list
|
|
// We completely ignore the BEP 12 processing rules
|
|
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);
|
|
}
|
|
}
|
|
if (trackers.size() > 2) {
|
|
// shuffle everything but the primary
|
|
TCTracker pri = trackers.remove(0);
|
|
Collections.shuffle(trackers, _util.getContext().random());
|
|
trackers.add(0, pri);
|
|
}
|
|
}
|
|
}
|
|
|
|
// configured open trackers
|
|
if (meta == null || !meta.isPrivate()) {
|
|
List<String> tlist = _util.getOpenTrackers();
|
|
for (int i = 0; i < tlist.size(); i++) {
|
|
String url = tlist.get(i);
|
|
if (!isNewValidTracker(trackerHashes, url))
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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))
|
|
continue;
|
|
backupTrackers.add(new TCTracker(url, false));
|
|
if (_log.shouldLog(Log.DEBUG))
|
|
_log.debug("Backup announce: [" + url + "] for infoHash: " + infoHash);
|
|
}
|
|
if (backupTrackers.isEmpty()) {
|
|
backupTrackers.add(new TCTracker(SnarkManager.DEFAULT_BACKUP_TRACKER, false));
|
|
} else if (trackers.size() > 1) {
|
|
Collections.shuffle(backupTrackers, _util.getContext().random());
|
|
}
|
|
}
|
|
this.completed = coordinator.getLeft() == 0;
|
|
_startedOn = _util.getContext().clock().now();
|
|
}
|
|
|
|
/**
|
|
* @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) {
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("Bad announce URL: [" + ann + ']');
|
|
return false;
|
|
}
|
|
// comment this out if tracker.welterde.i2p upgrades
|
|
if (h.equals(DSA_ONLY_TRACKER)) {
|
|
Destination dest = _util.getMyDestination();
|
|
if (dest != null && dest.getSigType() != SigType.DSA_SHA1) {
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("Skipping incompatible tracker: " + ann);
|
|
return false;
|
|
}
|
|
}
|
|
if (existing.size() >= MAX_TRACKERS) {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info("Not using announce URL, we have enough: [" + ann + ']');
|
|
return false;
|
|
}
|
|
boolean rv = existing.add(h);
|
|
if (!rv) {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info("Dup announce URL: [" + ann + ']');
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
try
|
|
{
|
|
// normally this will only go once, then call queueLoop() and return
|
|
while(!stop)
|
|
{
|
|
if (!verifyConnected()) {
|
|
stop = true;
|
|
return;
|
|
}
|
|
|
|
// Local DHT tracker announce
|
|
DHT dht = _util.getDHT();
|
|
if (dht != null && (meta == null || !meta.isPrivate()))
|
|
dht.announce(snark.getInfoHash(), coordinator.completed());
|
|
|
|
int oldSeenPeers = snark.getTrackerSeenPeers();
|
|
int maxSeenPeers = 0;
|
|
if (!trackers.isEmpty()) {
|
|
maxSeenPeers = getPeersFromTrackers(trackers);
|
|
// fast update for UI at startup
|
|
if (maxSeenPeers > oldSeenPeers)
|
|
snark.setTrackerSeenPeers(maxSeenPeers);
|
|
}
|
|
int p = getPeersFromPEX();
|
|
if (p > maxSeenPeers)
|
|
maxSeenPeers = p;
|
|
p = getPeersFromDHT();
|
|
if (p > maxSeenPeers) {
|
|
maxSeenPeers = p;
|
|
// fast update for UI at startup
|
|
if (maxSeenPeers > oldSeenPeers)
|
|
snark.setTrackerSeenPeers(maxSeenPeers);
|
|
}
|
|
// 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
|
|
*/
|
|
private int getPeersFromTrackers(List<TCTracker> trckrs) {
|
|
long left = coordinator.getLeft(); // -1 in magnet mode
|
|
|
|
// First time we got a complete download?
|
|
boolean newlyCompleted;
|
|
if (!completed && left == 0) {
|
|
completed = true;
|
|
newlyCompleted = true;
|
|
} else {
|
|
newlyCompleted = false;
|
|
}
|
|
|
|
// *** loop once for each tracker
|
|
int maxSeenPeers = 0;
|
|
for (TCTracker tr : trckrs) {
|
|
if ((!stop) && (!tr.stop) &&
|
|
(completed || coordinator.needOutboundPeers() || !tr.started) &&
|
|
(newlyCompleted || System.currentTimeMillis() > tr.lastRequestTime + tr.interval))
|
|
{
|
|
try
|
|
{
|
|
long uploaded = coordinator.getUploaded();
|
|
long downloaded = coordinator.getDownloaded();
|
|
long len = snark.getTotalLength();
|
|
if (len > 0 && downloaded > len)
|
|
downloaded = len;
|
|
left = coordinator.getLeft();
|
|
String event;
|
|
if (!tr.started) {
|
|
event = STARTED_EVENT;
|
|
} else if (newlyCompleted) {
|
|
event = COMPLETED_EVENT;
|
|
} else {
|
|
event = NO_EVENT;
|
|
}
|
|
TrackerInfo info = doRequest(tr, infoHash, peerID,
|
|
uploaded, downloaded, left,
|
|
event);
|
|
|
|
snark.setTrackerProblems(null);
|
|
tr.trackerProblems = null;
|
|
tr.registerFails = 0;
|
|
tr.consecutiveFails = 0;
|
|
if (tr.isPrimary)
|
|
consecutiveFails = 0;
|
|
runStarted = true;
|
|
tr.started = true;
|
|
tr.seenPeers = info.getPeerCount();
|
|
if (snark.getTrackerSeenPeers() < tr.seenPeers) // update rising number quickly
|
|
snark.setTrackerSeenPeers(tr.seenPeers);
|
|
|
|
// auto stop
|
|
// These are very high thresholds for now, not configurable,
|
|
// just for update torrent
|
|
if (completed &&
|
|
tr.isPrimary &&
|
|
snark.isAutoStoppable() &&
|
|
!snark.isChecking() &&
|
|
info.getSeedCount() > 100 &&
|
|
coordinator.getPeerCount() <= 0 &&
|
|
_util.getContext().clock().now() > _startedOn + 30*60*1000 &&
|
|
snark.getTotalLength() > 0 &&
|
|
uploaded >= snark.getTotalLength() / 2) {
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("Auto stopping " + snark.getBaseName());
|
|
snark.setAutoStoppable(false);
|
|
snark.stopTorrent();
|
|
return tr.seenPeers;
|
|
}
|
|
|
|
Set<Peer> peers = info.getPeers();
|
|
|
|
// pass everybody over to our tracker
|
|
DHT dht = _util.getDHT();
|
|
if (dht != null) {
|
|
for (Peer peer : peers) {
|
|
dht.announce(snark.getInfoHash(), peer.getPeerID().getDestHash(),
|
|
false); // TODO actual seed/leech status
|
|
}
|
|
}
|
|
|
|
if (coordinator.needOutboundPeers()) {
|
|
// we only want to talk to new people if we need things
|
|
// from them (duh)
|
|
List<Peer> ordered = new ArrayList<Peer>(peers);
|
|
Random r = _util.getContext().random();
|
|
Collections.shuffle(ordered, r);
|
|
Iterator<Peer> it = ordered.iterator();
|
|
while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
|
|
Peer cur = it.next();
|
|
// FIXME if id == us || dest == us continue;
|
|
// only delay if we actually make an attempt to add peer
|
|
if(coordinator.addPeer(cur) && it.hasNext()) {
|
|
int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
|
|
try { Thread.sleep(delay); } catch (InterruptedException ie) {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (IOException ioe)
|
|
{
|
|
// Probably not fatal (if it doesn't last to long...)
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("Could not contact tracker at '" + tr.announce + "': " + ioe);
|
|
tr.trackerProblems = ioe.getMessage();
|
|
// don't show secondary tracker problems to the user
|
|
// ... and only if we don't have any peers at all. Otherwise, PEX/DHT will save us.
|
|
if (tr.isPrimary && coordinator.getPeers() <= 0 &&
|
|
(!completed || _util.getDHT() == null || _util.getDHT().size() <= 0))
|
|
snark.setTrackerProblems(tr.trackerProblems);
|
|
String tplc = tr.trackerProblems.toLowerCase(Locale.US);
|
|
if (tplc.startsWith(NOT_REGISTERED) || tplc.startsWith(NOT_REGISTERED_2) ||
|
|
tplc.startsWith(NOT_REGISTERED_3) || tplc.startsWith(ERROR_GOT_HTML)) {
|
|
// Give a guy some time to register it if using opentrackers too
|
|
//if (trckrs.size() == 1) {
|
|
// stop = true;
|
|
// snark.stopTorrent();
|
|
//} else { // hopefully each on the opentrackers list is really open
|
|
if (tr.registerFails++ > MAX_REGISTER_FAILS ||
|
|
!completed || // no use retrying if we aren't seeding
|
|
tplc.startsWith(ERROR_GOT_HTML) || // fake msg from doRequest()
|
|
(!tr.isPrimary && tr.registerFails > MAX_REGISTER_FAILS / 2))
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn("Not longer announcing to " + tr.announce + " : " +
|
|
tr.trackerProblems + " after " + tr.registerFails + " failures");
|
|
tr.stop = true;
|
|
//
|
|
}
|
|
if (++tr.consecutiveFails == MAX_CONSEC_FAILS) {
|
|
tr.seenPeers = 0;
|
|
if (tr.interval < LONG_SLEEP)
|
|
tr.interval = LONG_SLEEP; // slow down
|
|
}
|
|
}
|
|
} 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));
|
|
}
|
|
if ((!tr.stop) && maxSeenPeers < tr.seenPeers)
|
|
maxSeenPeers = tr.seenPeers;
|
|
} // *** end of trackers loop here
|
|
|
|
return maxSeenPeers;
|
|
}
|
|
|
|
/**
|
|
* @return max peers seen
|
|
*/
|
|
private int getPeersFromPEX() {
|
|
// Get peers from PEX
|
|
int rv = 0;
|
|
if (coordinator.needOutboundPeers() && (meta == null || !meta.isPrivate()) && !stop) {
|
|
Set<PeerID> pids = coordinator.getPEXPeers();
|
|
if (!pids.isEmpty()) {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info("Got " + pids.size() + " from PEX");
|
|
List<Peer> peers = new ArrayList<Peer>(pids.size());
|
|
for (PeerID pID : pids) {
|
|
peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
|
|
}
|
|
Random r = _util.getContext().random();
|
|
Collections.shuffle(peers, r);
|
|
Iterator<Peer> it = peers.iterator();
|
|
while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
|
|
Peer cur = it.next();
|
|
if (coordinator.addPeer(cur) && it.hasNext()) {
|
|
int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
|
|
try { Thread.sleep(delay); } catch (InterruptedException ie) {}
|
|
}
|
|
}
|
|
rv = pids.size();
|
|
pids.clear();
|
|
}
|
|
} else {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info("Not getting PEX peers");
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
/**
|
|
* @return max peers seen
|
|
*/
|
|
private int getPeersFromDHT() {
|
|
// Get peers from DHT
|
|
// FIXME this needs to be in its own thread
|
|
int rv = 0;
|
|
DHT dht = _util.getDHT();
|
|
if (dht != null &&
|
|
(meta == null || !meta.isPrivate()) &&
|
|
(!stop) &&
|
|
(meta == null || _util.getContext().clock().now() > lastDHTAnnounce + MIN_DHT_ANNOUNCE_INTERVAL)) {
|
|
int numwant;
|
|
if (!coordinator.needOutboundPeers())
|
|
numwant = 1;
|
|
else
|
|
numwant = _util.getMaxConnections();
|
|
Collection<Hash> hashes = dht.getPeersAndAnnounce(snark.getInfoHash(), numwant,
|
|
5*60*1000, DHT_ANNOUNCE_PEERS, 3*60*1000,
|
|
coordinator.completed(), numwant <= 1);
|
|
if (!hashes.isEmpty()) {
|
|
runStarted = true;
|
|
lastDHTAnnounce = _util.getContext().clock().now();
|
|
rv = hashes.size();
|
|
} else {
|
|
lastDHTAnnounce = 0;
|
|
}
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info("Got " + hashes + " from DHT");
|
|
|
|
// now try these peers
|
|
if ((!stop) && !hashes.isEmpty()) {
|
|
List<Peer> peers = new ArrayList<Peer>(hashes.size());
|
|
for (Hash h : hashes) {
|
|
try {
|
|
PeerID pID = new PeerID(h.getData(), _util);
|
|
peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
|
|
} catch (InvalidBEncodingException ibe) {}
|
|
}
|
|
Random r = _util.getContext().random();
|
|
Collections.shuffle(peers, r);
|
|
Iterator<Peer> it = peers.iterator();
|
|
while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
|
|
Peer cur = it.next();
|
|
if (coordinator.addPeer(cur) && it.hasNext()) {
|
|
int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
|
|
try { Thread.sleep(delay); } catch (InterruptedException ie) {}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info("Not getting DHT peers");
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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());
|
|
int i = 0;
|
|
for (TCTracker tr : trackers) {
|
|
if (_util.connected() &&
|
|
tr.started && (!tr.stop) && tr.trackerProblems == null) {
|
|
try {
|
|
(new I2PAppThread(new Unannouncer(tr), _threadName + " U" + (++i), true)).start();
|
|
} 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 {
|
|
private final TCTracker tr;
|
|
|
|
public Unannouncer(TCTracker tr) {
|
|
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 len = snark.getTotalLength();
|
|
if (len > 0 && downloaded > len)
|
|
downloaded = len;
|
|
long left = coordinator.getLeft();
|
|
try
|
|
{
|
|
// Don't try to restart I2CP connection just to say goodbye
|
|
if (_util.connected()) {
|
|
if (tr.started && (!tr.stop) && tr.trackerProblems == null)
|
|
doRequest(tr, infoHash, peerID, uploaded,
|
|
downloaded, left, STOPPED_EVENT);
|
|
}
|
|
}
|
|
catch(IOException ioe) { /* ignored */ }
|
|
tr.reset();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Note: IOException message text gets displayed in the UI
|
|
*
|
|
*/
|
|
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=");
|
|
boolean small = left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers();
|
|
if (small)
|
|
buf.append('0');
|
|
else
|
|
buf.append(_util.getMaxConnections());
|
|
String s = buf.toString();
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info("Sending TrackerClient request: " + s);
|
|
|
|
tr.lastRequestTime = System.currentTimeMillis();
|
|
// Don't wait for a response to stopped when shutting down
|
|
boolean fast = _fastUnannounce && event.equals(STOPPED_EVENT);
|
|
byte[] fetched = _util.get(s, true, fast ? -1 : 0, small ? 128 : 1024, small ? 1024 : 32*1024);
|
|
if (fetched == null)
|
|
throw new IOException("No response from " + tr.host);
|
|
if (fetched.length == 0)
|
|
throw new IOException("No data from " + tr.host);
|
|
// The HTML check only works if we didn't exceed the maxium fetch size specified in get(),
|
|
// otherwise we already threw an IOE.
|
|
if (fetched[0] == '<')
|
|
throw new IOException(ERROR_GOT_HTML + " from " + tr.host);
|
|
|
|
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 " + tr.host + " response: " + info);
|
|
|
|
String failure = info.getFailureReason();
|
|
if (failure != null)
|
|
throw new IOException("Tracker " + tr.host + " responded with: " + failure);
|
|
|
|
tr.interval = Math.max(MIN_TRACKER_ANNOUNCE_INTERVAL, info.getInterval() * 1000l);
|
|
return info;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
*/
|
|
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;
|
|
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));
|
|
}
|
|
}
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* @param ann an announce URL, may be null, returns false if null
|
|
* @return true for i2p hosts only
|
|
* @since 0.7.12
|
|
*/
|
|
public static boolean isValidAnnounce(String ann) {
|
|
if (ann == null)
|
|
return false;
|
|
URI url;
|
|
try {
|
|
url = new URI(ann);
|
|
} catch (URISyntaxException use) {
|
|
return false;
|
|
}
|
|
String path = url.getPath();
|
|
if (path == null || !path.startsWith("/"))
|
|
return false;
|
|
return "http".equals(url.getScheme()) && url.getHost() != null &&
|
|
(url.getHost().endsWith(".i2p") || url.getHost().equals("i2p"));
|
|
}
|
|
|
|
/**
|
|
* This also validates the URL.
|
|
*
|
|
* @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) {
|
|
URI url;
|
|
try {
|
|
url = new URI(ann);
|
|
} catch (URISyntaxException use) {
|
|
return null;
|
|
}
|
|
if (!"http".equals(url.getScheme()))
|
|
return null;
|
|
String host = url.getHost();
|
|
if (host == null)
|
|
return null;
|
|
if (host.endsWith(".i2p")) {
|
|
String path = url.getPath();
|
|
if (path == null || !path.startsWith("/"))
|
|
return null;
|
|
return ConvertToHash.getHash(host);
|
|
}
|
|
if (host.equals("i2p")) {
|
|
String path = url.getPath();
|
|
if (path == null || path.length() < 517 ||
|
|
!path.startsWith("/"))
|
|
return null;
|
|
String[] parts = DataHelper.split(path.substring(1), "[/\\?&;]", 2);
|
|
return ConvertToHash.getHash(parts[0]);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static class TCTracker
|
|
{
|
|
final String announce;
|
|
final String host;
|
|
final boolean isPrimary;
|
|
long interval;
|
|
long lastRequestTime;
|
|
String trackerProblems;
|
|
boolean stop;
|
|
boolean started;
|
|
int registerFails;
|
|
int consecutiveFails;
|
|
int seenPeers;
|
|
|
|
/**
|
|
* @param a must be a valid http URL with a path
|
|
* @param p true if primary
|
|
*/
|
|
public TCTracker(String a, boolean p)
|
|
{
|
|
announce = a;
|
|
String s = a.substring(7);
|
|
host = s.substring(0, s.indexOf('/'));
|
|
isPrimary = p;
|
|
interval = INITIAL_SLEEP;
|
|
}
|
|
|
|
/**
|
|
* Call before restarting
|
|
* @since 0.9.1
|
|
*/
|
|
public void reset() {
|
|
lastRequestTime = 0;
|
|
trackerProblems = null;
|
|
stop = false;
|
|
started = false;
|
|
registerFails = 0;
|
|
consecutiveFails = 0;
|
|
seenPeers = 0;
|
|
}
|
|
}
|
|
}
|