diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 6cef36f67b1cfd93109396b8525cca36eb736026..0433ffcbb28e3259fdbc679589f2f64d4f822d0a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -1,5 +1,6 @@ package org.klomp.snark; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -393,6 +394,46 @@ public class I2PSnarkUtil { } } + /** + * Fetch to memory + * @param retries if < 0, set timeout to a few seconds + * @param initialSize buffer size + * @param maxSize fails if greater + * @return null on error + * @since 0.9.4 + */ + public byte[] get(String url, boolean rewrite, int retries, int initialSize, int maxSize) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fetching [" + url + "] to memory"); + String fetchURL = url; + if (rewrite) + fetchURL = rewriteAnnounce(url); + int timeout; + if (retries < 0) { + if (!connected()) + return null; + timeout = EEPGET_CONNECT_TIMEOUT_SHORT; + retries = 0; + } else { + timeout = EEPGET_CONNECT_TIMEOUT; + if (!connected()) { + if (!connect()) + return null; + } + } + ByteArrayOutputStream out = new ByteArrayOutputStream(initialSize); + EepGet get = new I2PSocketEepGet(_context, _manager, retries, -1, maxSize, null, out, fetchURL); + if (get.fetch(timeout)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fetch successful [" + url + "]: size=" + out.size()); + return out.toByteArray(); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Fetch failed [" + url + "]"); + return null; + } + } + public I2PServerSocket getServerSocket() { I2PSocketManager mgr = _manager; if (mgr != null) @@ -523,6 +564,15 @@ public class I2PSnarkUtil { return Collections.EMPTY_LIST; return _openTrackers; } + + /** + * List of open trackers to use as backups even if disabled + * @return non-null + * @since 0.9.4 + */ + public List<String> getBackupTrackers() { + return _openTrackers; + } public void setUseOpenTrackers(boolean yes) { _shouldUseOT = yes; diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index dccfddbcd3513201e5c9fb6df89ede8b43c0c40b..6a821f2ba7a1c511739ec87a2ca5a65ee851049b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -20,6 +20,7 @@ package org.klomp.snark; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -43,6 +44,7 @@ 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; /** @@ -70,6 +72,8 @@ public class TrackerClient implements Runnable { private static final String COMPLETED_EVENT = "completed"; private static final String STOPPED_EVENT = "stopped"; private static final String NOT_REGISTERED = "torrent not registered"; //bytemonsoon + /** 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. private final static int DELAY_MIN = 2000; // 2 secs. @@ -78,7 +82,7 @@ public class TrackerClient implements Runnable { 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 = 10*60*1000; + private final static long MIN_TRACKER_ANNOUNCE_INTERVAL = 15*60*1000; private final static long MIN_DHT_ANNOUNCE_INTERVAL = 10*60*1000; private final I2PSnarkUtil _util; @@ -106,6 +110,7 @@ public class TrackerClient implements Runnable { private volatile boolean _fastUnannounce; private long lastDHTAnnounce; private final List<Tracker> trackers; + private final List<Tracker> backupTrackers; /** * Call start() to start it. @@ -131,6 +136,7 @@ public class TrackerClient implements Runnable { this.infoHash = urlencode(snark.getInfoHash()); this.peerID = urlencode(snark.getID()); this.trackers = new ArrayList(2); + this.backupTrackers = new ArrayList(2); } public synchronized void start() { @@ -233,7 +239,7 @@ public class TrackerClient implements Runnable { if (!_initialized) { _initialized = true; // FIXME only when starting everybody at once, not for a single torrent - long delay = I2PAppContext.getGlobalContext().random().nextInt(30*1000); + long delay = _util.getContext().random().nextInt(30*1000); try { Thread.sleep(delay); } catch (InterruptedException ie) {} @@ -267,18 +273,20 @@ public class TrackerClient implements Runnable { if (primary != null) { if (isValidAnnounce(primary)) { trackers.add(new Tracker(primary, true)); - _log.debug("Announce: [" + primary + "] infoHash: " + infoHash); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Announce: [" + primary + "] infoHash: " + infoHash); } else { - _log.warn("Skipping invalid or non-i2p announce: " + primary); + if (_log.shouldLog(Log.WARN)) + _log.warn("Skipping invalid or non-i2p announce: " + primary); } } else { _log.warn("No primary announce"); primary = ""; } - List tlist = _util.getOpenTrackers(); - if (tlist != null && (meta == null || !meta.isPrivate())) { + if (meta == null || !meta.isPrivate()) { + List<String> tlist = _util.getOpenTrackers(); for (int i = 0; i < tlist.size(); i++) { - String url = (String)tlist.get(i); + String url = tlist.get(i); if (!isValidAnnounce(url)) { _log.error("Bad announce URL: [" + url + "]"); continue; @@ -301,9 +309,37 @@ public class TrackerClient implements Runnable { continue; // opentrackers are primary if we don't have primary trackers.add(new Tracker(url, primary.equals(""))); - _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash); + 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 (!isValidAnnounce(url)) { + _log.error("Bad announce URL: [" + url + "]"); + continue; + } + int slash = url.indexOf('/', 7); + if (slash <= 7) { + _log.error("Bad announce URL: [" + url + "]"); + continue; + } + String dest = _util.lookup(url.substring(7, slash)); + if (dest == null) { + _log.error("Announce host unknown: [" + url.substring(7, slash) + "]"); + continue; + } + backupTrackers.add(new Tracker(url, false)); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Backup announce: [" + url + "] for infoHash: " + infoHash); + } + if (backupTrackers.isEmpty()) + backupTrackers.add(new Tracker(DEFAULT_BACKUP_TRACKER, false)); + } this.completed = coordinator.getLeft() == 0; } @@ -315,7 +351,7 @@ public class TrackerClient implements Runnable { private void loop() { try { - Random r = I2PAppContext.getGlobalContext().random(); + // normally this will only go once, then call queueLoop() and return while(!stop) { if (!verifyConnected()) { @@ -325,9 +361,71 @@ public class TrackerClient implements Runnable { // Local DHT tracker announce DHT dht = _util.getDHT(); - if (dht != null) + if (dht != null && (meta == null || !meta.isPrivate())) dht.announce(snark.getInfoHash()); + 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 + */ + private int getPeersFromTrackers(List<Tracker> trckrs) { long uploaded = coordinator.getUploaded(); long downloaded = coordinator.getDownloaded(); long left = coordinator.getLeft(); // -1 in magnet mode @@ -341,10 +439,10 @@ public class TrackerClient implements Runnable { } else event = NO_EVENT; - + // *** loop once for each tracker int maxSeenPeers = 0; - for (Tracker tr : trackers) { + for (Tracker tr : trckrs) { if ((!stop) && (!tr.stop) && (completed || coordinator.needOutboundPeers() || !tr.started) && (event.equals(COMPLETED_EVENT) || System.currentTimeMillis() > tr.lastRequestTime + tr.interval)) @@ -372,7 +470,7 @@ public class TrackerClient implements Runnable { snark.setTrackerSeenPeers(tr.seenPeers); // pass everybody over to our tracker - dht = _util.getDHT(); + DHT dht = _util.getDHT(); if (dht != null) { for (Peer peer : peers) { dht.announce(snark.getInfoHash(), peer.getPeerID().getDestHash()); @@ -383,6 +481,7 @@ public class TrackerClient implements Runnable { // we only want to talk to new people if we need things // from them (duh) List<Peer> ordered = new ArrayList(peers); + Random r = _util.getContext().random(); Collections.shuffle(ordered, r); Iterator<Peer> it = ordered.iterator(); while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) { @@ -409,13 +508,13 @@ public class TrackerClient implements Runnable { snark.setTrackerProblems(tr.trackerProblems); if (tr.trackerProblems.toLowerCase(Locale.US).startsWith(NOT_REGISTERED)) { // Give a guy some time to register it if using opentrackers too - if (trackers.size() == 1) { - stop = true; - snark.stopTorrent(); - } else { // hopefully each on the opentrackers list is really open + //if (trckrs.size() == 1) { + // stop = true; + // snark.stopTorrent(); + //} else { // hopefully each on the opentrackers list is really open if (tr.registerFails++ > MAX_REGISTER_FAILS) tr.stop = true; - } + // } if (++tr.consecutiveFails == MAX_CONSEC_FAILS) { tr.seenPeers = 0; @@ -432,7 +531,15 @@ public class TrackerClient implements Runnable { 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()) { @@ -442,6 +549,7 @@ public class TrackerClient implements Runnable { 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()) { @@ -451,19 +559,27 @@ public class TrackerClient implements Runnable { try { Thread.sleep(delay); } catch (InterruptedException ie) {} } } + rv = pids.size(); } } 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 - dht = _util.getDHT(); + int rv = 0; + DHT dht = _util.getDHT(); if (dht != null && (meta == null || !meta.isPrivate()) && (!stop) && _util.getContext().clock().now() > lastDHTAnnounce + MIN_DHT_ANNOUNCE_INTERVAL) { int numwant; - if (event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers()) + if (!coordinator.needOutboundPeers()) numwant = 1; else numwant = _util.getMaxConnections(); @@ -471,6 +587,7 @@ public class TrackerClient implements Runnable { if (!hashes.isEmpty()) { runStarted = true; lastDHTAnnounce = _util.getContext().clock().now(); + rv = hashes.size(); } if (_log.shouldLog(Log.INFO)) _log.info("Got " + hashes + " from DHT"); @@ -487,9 +604,12 @@ public class TrackerClient implements Runnable { if ((!stop) && !hashes.isEmpty()) { List<Peer> peers = new ArrayList(hashes.size()); for (Hash h : hashes) { - PeerID pID = new PeerID(h.getData(), _util); - peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo())); + 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()) { @@ -504,49 +624,10 @@ public class TrackerClient implements Runnable { if (_log.shouldLog(Log.INFO)) _log.info("Not getting DHT peers"); } - - - // 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; - 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 rv; } + /** * Creates a thread for each tracker in parallel if tunnel is still open * @since 0.9.1 @@ -630,7 +711,8 @@ public class TrackerClient implements Runnable { if (! event.equals(NO_EVENT)) buf.append("&event=").append(event); buf.append("&numwant="); - if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers()) + boolean small = left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers(); + if (small) buf.append('0'); else buf.append(_util.getMaxConnections()); @@ -641,14 +723,12 @@ public class TrackerClient implements Runnable { tr.lastRequestTime = System.currentTimeMillis(); // Don't wait for a response to stopped when shutting down boolean fast = _fastUnannounce && event.equals(STOPPED_EVENT); - File fetched = _util.get(s, true, fast ? -1 : 0); + 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); } - InputStream in = null; - try { - in = new FileInputStream(fetched); + InputStream in = new ByteArrayInputStream(fetched); TrackerInfo info = new TrackerInfo(in, snark.getID(), snark.getInfoHash(), snark.getMetaInfo(), _util); @@ -661,10 +741,6 @@ public class TrackerClient implements Runnable { tr.interval = Math.max(MIN_TRACKER_ANNOUNCE_INTERVAL, info.getInterval() * 1000l); return info; - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - fetched.delete(); - } } /** diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index 55521a7876f081fdaf00937c1ef801aa4643c969..b94c5f555fb5ff933ce5efca54c2a1e75be388f0 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -143,6 +143,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** how long since generated do we delete - BEP 5 says 10 minutes */ private static final long MAX_TOKEN_AGE = 10*60*1000; private static final long MAX_INBOUND_TOKEN_AGE = MAX_TOKEN_AGE - 2*60*1000; + private static final int MAX_OUTBOUND_TOKENS = 5000; /** how long since sent do we wait for a reply */ private static final long MAX_MSGID_AGE = 2*60*1000; /** how long since sent do we wait for a reply */ @@ -1208,7 +1209,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** * Handle and respond to the query. - * We have no node info here, it came on response port, we have to get it from the token + * We have no node info here, it came on response port, we have to get it from the token. + * So we can't verify that it came from the same peer, as BEP 5 specifies. */ private void receiveAnnouncePeer(MsgID msgID, InfoHash ih, byte[] tok) throws InvalidBEncodingException { Token token = new Token(tok); @@ -1216,8 +1218,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (nInfo == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Unknown token in announce_peer: " + token); - if (_log.shouldLog(Log.INFO)) - _log.info("Current known tokens: " + _outgoingTokens.keySet()); + //if (_log.shouldLog(Log.INFO)) + // _log.info("Current known tokens: " + _outgoingTokens.keySet()); return; } if (_log.shouldLog(Log.INFO)) @@ -1282,8 +1284,9 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @throws NPE, IllegalArgumentException, and others too */ private List<NodeInfo> receiveNodes(NodeInfo nInfo, byte[] ids) throws InvalidBEncodingException { - List<NodeInfo> rv = new ArrayList(ids.length / NodeInfo.LENGTH); - for (int off = 0; off < ids.length; off += NodeInfo.LENGTH) { + int max = Math.min(K, ids.length / NodeInfo.LENGTH); + List<NodeInfo> rv = new ArrayList(max); + for (int off = 0; off < ids.length && rv.size() < max; off += NodeInfo.LENGTH) { NodeInfo nInf = new NodeInfo(ids, off); if (_blacklist.contains(nInf.getNID())) { if (_log.shouldLog(Log.INFO)) @@ -1305,12 +1308,15 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private List<Hash> receivePeers(NodeInfo nInfo, List<BEValue> peers) throws InvalidBEncodingException { if (_log.shouldLog(Log.INFO)) _log.info("Rcvd peers from: " + nInfo); - List<Hash> rv = new ArrayList(peers.size()); + int max = Math.min(MAX_WANT, peers.size()); + List<Hash> rv = new ArrayList(max); for (BEValue bev : peers) { byte[] b = bev.getBytes(); //Hash h = new Hash(b); Hash h = Hash.create(b); rv.add(h); + if (rv.size() >= max) + break; } if (_log.shouldLog(Log.INFO)) _log.info("Rcvd peers from: " + nInfo + ": " + DataHelper.toString(rv)); @@ -1535,20 +1541,28 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _blacklist.size() + " in blacklist, " + _outgoingTokens.size() + " sent Tokens, " + _incomingTokens.size() + " rcvd Tokens"); + int cnt = 0; + long expire = now - MAX_TOKEN_AGE; for (Iterator<Token> iter = _outgoingTokens.keySet().iterator(); iter.hasNext(); ) { Token tok = iter.next(); - if (tok.lastSeen() < now - MAX_TOKEN_AGE) + // just delete at random if we have too many + // TODO reduce the expire time and iterate again? + if (tok.lastSeen() < expire || cnt >= MAX_OUTBOUND_TOKENS) iter.remove(); + else + cnt++; } + expire = now - MAX_INBOUND_TOKEN_AGE; for (Iterator<Token> iter = _incomingTokens.values().iterator(); iter.hasNext(); ) { Token tok = iter.next(); - if (tok.lastSeen() < now - MAX_INBOUND_TOKEN_AGE) + if (tok.lastSeen() < expire) iter.remove(); } + expire = now - BLACKLIST_CLEAN_TIME; for (Iterator<NID> iter = _blacklist.iterator(); iter.hasNext(); ) { NID nid = iter.next(); // lastSeen() is actually when-added - if (now > nid.lastSeen() + BLACKLIST_CLEAN_TIME) + if (nid.lastSeen() < expire) iter.remove(); } // TODO sent queries?