package org.klomp.snark.dht; /* * GPLv2 */ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import net.i2p.I2PAppContext; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.client.I2PSessionMuxedListener; import net.i2p.client.SendMessageOptions; import net.i2p.client.datagram.I2PDatagramDissector; import net.i2p.client.datagram.I2PDatagramMaker; import net.i2p.client.datagram.I2PInvalidDatagramException; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.util.ConcurrentHashSet; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.SimpleTimer2; import org.klomp.snark.I2PSnarkUtil; import org.klomp.snark.SnarkManager; import org.klomp.snark.TrackerClient; import org.klomp.snark.bencode.BDecoder; import org.klomp.snark.bencode.BEncoder; import org.klomp.snark.bencode.BEValue; import org.klomp.snark.bencode.InvalidBEncodingException; /** * Standard BEP 5 * Mods for I2P: * <pre> * - The UDP port need not be pinged after receiving a PORT message. * * - The UDP (datagram) port listed in the compact node info is used * to receive repliable (signed) datagrams. * This is used for queries, except for announces. * We call this the "query port". * In addition to that UDP port, we use a second datagram * port equal to the signed port + 1. This is used to receive * unsigned (raw) datagrams for replies, errors, and announce queries.. * We call this the "response port". * * - Compact peer info is 32 bytes (32 byte SHA256 Hash) * instead of 4 byte IP + 2 byte port. There is no peer port. * * - Compact node info is 54 bytes (20 byte SHA1 Hash + 32 byte SHA256 Hash + 2 byte port) * instead of 20 byte SHA1 Hash + 4 byte IP + 2 byte port. * Port is the query port, the response port is always the query port + 1. * * - The trackerless torrent dictionary "nodes" key is a list of * 32 byte binary strings (SHA256 Hashes) instead of a list of lists * containing a host string and a port integer. * </pre> * * Questions: * - nodes (in the find_node and get_peers response) is one concatenated string, not a list of strings, right? * - Node ID enforcement, keyspace rotation? * * @since 0.9.2 * @author zzz */ public class KRPC implements I2PSessionMuxedListener, DHT { private final I2PAppContext _context; private final Log _log; /** our tracker */ private final DHTTracker _tracker; /** who we know */ private final DHTNodes _knownNodes; /** index to sent queries awaiting reply */ private final ConcurrentHashMap<MsgID, ReplyWaiter> _sentQueries; /** index to outgoing tokens we generated, sent in reply to a get_peers query */ private final ConcurrentHashMap<Token, NodeInfo> _outgoingTokens; /** index to incoming opaque tokens, received in a peers or nodes reply */ private final ConcurrentHashMap<NID, Token> _incomingTokens; /** recently unreachable, with lastSeen() as the added-to-blacklist time */ private final Set<NID> _blacklist; /** hook to inject and receive datagrams */ private final I2PSession _session; /** 20 byte random id */ private final byte[] _myID; /** 20 byte random id */ private final NID _myNID; /** 20 byte random id + 32 byte Hash + 2 byte port */ private final NodeInfo _myNodeInfo; /** unsigned dgrams */ private final int _rPort; /** signed dgrams */ private final int _qPort; private final File _dhtFile; private final File _backupDhtFile; private volatile boolean _isRunning; private volatile boolean _hasBootstrapped; /** stats */ private final AtomicLong _rxPkts = new AtomicLong(); private final AtomicLong _txPkts = new AtomicLong(); private final AtomicLong _rxBytes = new AtomicLong(); private final AtomicLong _txBytes = new AtomicLong(); private long _started; private long _nodesLastSaved; /** all-zero NID used for pings */ public static final NID FAKE_NID = new NID(new byte[NID.HASH_LENGTH]); /** Max number of nodes to return. BEP 5 says 8 */ private static final int K = 8; /** Max number of peers to return. BEP 5 doesn't say. * We'll use more than I2PSnarkUtil.MAX_CONNECTIONS since lots could be old. */ private static final int MAX_WANT = I2PSnarkUtil.MAX_CONNECTIONS * 3 / 2; /** overloads error codes which start with 201 */ private static final int REPLY_NONE = 0; private static final int REPLY_PONG = 1; private static final int REPLY_PEERS = 2; private static final int REPLY_NODES = 3; private static final int REPLY_NETWORK_FAIL = 4; public static final boolean SECURE_NID = true; /** 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 */ private static final long DEFAULT_QUERY_TIMEOUT = 75*1000; private static final long DEST_LOOKUP_TIMEOUT = 10*1000; /** stagger with other cleaners */ private static final long CLEAN_TIME = 63*1000; private static final long EXPLORE_TIME = 877*1000; private static final long BLACKLIST_CLEAN_TIME = 17*60*1000; private static final long NODES_SAVE_TIME = 3*60*60*1000; public static final String DHT_FILE_SUFFIX = ".dht.dat"; private static final int SEND_CRYPTO_TAGS = 8; private static final int LOW_CRYPTO_TAGS = 4; /** * @param baseName generally "i2psnark" */ public KRPC(I2PAppContext ctx, String baseName, I2PSession session) { _context = ctx; _session = session; _log = ctx.logManager().getLog(KRPC.class); _tracker = new DHTTracker(ctx); _sentQueries = new ConcurrentHashMap<MsgID, ReplyWaiter>(); _outgoingTokens = new ConcurrentHashMap<Token, NodeInfo>(); _incomingTokens = new ConcurrentHashMap<NID, Token>(); _blacklist = new ConcurrentHashSet<NID>(); // Construct my NodeInfo // Pick ports over a big range to marginally increase security // If we add a search DHT, adjust to stay out of each other's way _qPort = TrackerClient.PORT + 10 + ctx.random().nextInt(65535 - 20 - TrackerClient.PORT); _rPort = _qPort + 1; if (SECURE_NID) { _myNID = NodeInfo.generateNID(session.getMyDestination().calculateHash(), _qPort, _context.random()); _myID = _myNID.getData(); } else { _myID = new byte[NID.HASH_LENGTH]; ctx.random().nextBytes(_myID); _myNID = new NID(_myID); } _myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort); File conf = new File(ctx.getConfigDir(), baseName + ".config" + SnarkManager.CONFIG_DIR_SUFFIX); _dhtFile = new File(conf, "i2psnark" + DHT_FILE_SUFFIX); if (baseName.equals("i2psnark")) { _backupDhtFile = null; } else { File bconf = new File(ctx.getConfigDir(), "i2psnark.config" + SnarkManager.CONFIG_DIR_SUFFIX); _backupDhtFile = new File(bconf, "i2psnark" + DHT_FILE_SUFFIX); } _knownNodes = new DHTNodes(ctx, _myNID); start(); } ///////////////// Public methods /** * Known nodes, not estimated total network size. */ public int size() { return _knownNodes.size(); } /** * @return The UDP query port */ public int getPort() { return _qPort; } /** * @return The UDP response port */ public int getRPort() { return _rPort; } /** * Ping. We don't have a NID yet so the node is presumed * to be absent from our DHT. * Non-blocking, does not wait for pong. * If and when the pong is received the node will be inserted in our DHT. */ public void ping(Destination dest, int port) { NodeInfo nInfo = new NodeInfo(dest, port); sendPing(nInfo); } /** * Bootstrapping or background thread. * Blocking! * This is almost the same as getPeers() * * @param target the key we are searching for * @param maxNodes how many to contact * @param maxWait how long to wait for each to reply (not total) must be > 0 * @param parallel how many outstanding at once (unimplemented, always 1) */ @SuppressWarnings("unchecked") private void explore(NID target, int maxNodes, long maxWait, int parallel) { List<NodeInfo> nodes = _knownNodes.findClosest(target, maxNodes); if (nodes.isEmpty()) { if (_log.shouldLog(Log.WARN)) _log.info("DHT is empty, cannot explore"); return; } SortedSet<NodeInfo> toTry = new TreeSet<NodeInfo>(new NodeInfoComparator(target)); toTry.addAll(nodes); Set<NodeInfo> tried = new HashSet<NodeInfo>(); if (_log.shouldLog(Log.INFO)) _log.info("Starting explore of " + target); for (int i = 0; i < maxNodes; i++) { if (!_isRunning) break; NodeInfo nInfo; try { nInfo = toTry.first(); } catch (NoSuchElementException nsee) { break; } toTry.remove(nInfo); tried.add(nInfo); ReplyWaiter waiter = sendFindNode(nInfo, target); if (waiter == null) continue; synchronized(waiter) { try { waiter.wait(maxWait); } catch (InterruptedException ie) {} } int replyType = waiter.getReplyCode(); if (replyType == REPLY_NONE) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Got no reply"); } else if (replyType == REPLY_NODES) { List<NodeInfo> reply = (List<NodeInfo>) waiter.getReplyObject(); // It seems like we are just going to get back ourselves all the time if (_log.shouldLog(Log.DEBUG)) _log.debug("Got " + reply.size() + " nodes"); for (NodeInfo ni : reply) { if (! (ni.equals(_myNodeInfo) || (toTry.contains(ni) && tried.contains(ni)))) toTry.add(ni); } } else if (replyType == REPLY_NETWORK_FAIL) { break; } else { if (_log.shouldLog(Log.INFO)) _log.info("Got unexpected reply " + replyType + ": " + waiter.getReplyObject()); } } if (_log.shouldLog(Log.INFO)) _log.info("Finished explore of " + target); } /** * Local lookup only * @param ih a 20-byte info hash * @param max max to return * @return list or empty list (never null) */ public List<NodeInfo> findClosest(byte[] ih, int max) { List<NodeInfo> nodes = _knownNodes.findClosest(new InfoHash(ih), max); return nodes; } /** * Get peers for a torrent, and announce to the closest annMax nodes we find. * This is an iterative lookup in the DHT. * Blocking! * Caller should run in a thread. * * @param ih the Info Hash (torrent) * @param max maximum number of peers to return * @param maxWait the maximum time to wait (ms) must be > 0 * @param annMax the number of peers to announce to * @param annMaxWait the maximum total time to wait for announces, may be 0 to return immediately without waiting for acks * @param isSeed true if seed, false if leech * @param noSeeds true if we do not want seeds in the result * @return possibly empty (never null) */ @SuppressWarnings("unchecked") public Collection<Hash> getPeersAndAnnounce(byte[] ih, int max, long maxWait, int annMax, long annMaxWait, boolean isSeed, boolean noSeeds) { // check local tracker first InfoHash iHash = new InfoHash(ih); Collection<Hash> rv = _tracker.getPeers(iHash, max, noSeeds); rv.remove(_myNodeInfo.getHash()); if (rv.size() >= max) return rv; rv = new HashSet<Hash>(rv); long endTime = _context.clock().now() + maxWait; // needs to be much higher than log(size) since many lookups will fail // at first and we will give up too early int maxNodes = 30; // Initial set to try, will get added to as we go List<NodeInfo> nodes = _knownNodes.findClosest(iHash, maxNodes); NodeInfoComparator comp = new NodeInfoComparator(iHash); SortedSet<NodeInfo> toTry = new TreeSet<NodeInfo>(comp); SortedSet<NodeInfo> heardFrom = new TreeSet<NodeInfo>(comp); toTry.addAll(nodes); SortedSet<NodeInfo> tried = new TreeSet<NodeInfo>(comp); if (_log.shouldLog(Log.INFO)) _log.info("Starting getPeers for " + iHash + " (b64: " + new NID(ih) + ") " + " with " + nodes.size() + " to try"); for (int i = 0; i < maxNodes; i++) { if (!_isRunning) break; if (_log.shouldLog(Log.DEBUG)) _log.debug("Now to try: " + toTry); NodeInfo nInfo; try { nInfo = toTry.first(); } catch (NoSuchElementException nsee) { break; } toTry.remove(nInfo); tried.add(nInfo); if (_log.shouldLog(Log.DEBUG)) _log.debug("Try " + i + ": " + nInfo); ReplyWaiter waiter = sendGetPeers(nInfo, iHash, noSeeds); if (waiter == null) continue; synchronized(waiter) { try { waiter.wait(Math.max(30*1000, (Math.min(45*1000, endTime - _context.clock().now())))); } catch (InterruptedException ie) {} } int replyType = waiter.getReplyCode(); if (replyType == REPLY_NONE) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Got no reply"); } else if (replyType == REPLY_PONG) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Got pong"); } else if (replyType == REPLY_PEERS) { heardFrom.add(waiter.getSentTo()); if (_log.shouldLog(Log.DEBUG)) _log.debug("Got peers"); List<Hash> reply = (List<Hash>) waiter.getReplyObject(); // shouldn't send us an empty peers list but through // 0.9.8.1 it will if (!reply.isEmpty()) { for (int j = 0; j < reply.size() && rv.size() < max; j++) { Hash h = reply.get(j); if (!h.equals(_myNodeInfo.getHash())) rv.add(h); } } if (_log.shouldLog(Log.INFO)) _log.info("Finished get Peers, got " + reply.size() + " from DHT, returning " + rv.size()); break; } else if (replyType == REPLY_NODES) { heardFrom.add(waiter.getSentTo()); List<NodeInfo> reply = (List<NodeInfo>) waiter.getReplyObject(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Got " + reply.size() + " nodes"); for (NodeInfo ni : reply) { if (! (ni.equals(_myNodeInfo) || tried.contains(ni) || toTry.contains(ni))) toTry.add(ni); } } else if (replyType == REPLY_NETWORK_FAIL) { break; } else { if (_log.shouldLog(Log.INFO)) _log.info("Got unexpected reply " + replyType + ": " + waiter.getReplyObject()); } if (_context.clock().now() > endTime) break; if (!toTry.isEmpty() && !heardFrom.isEmpty() && comp.compare(toTry.first(), heardFrom.first()) >= 0) { if (_log.shouldLog(Log.INFO)) _log.info("Finished get Peers, nothing closer to try after " + (i+1)); break; } } // now announce if (!heardFrom.isEmpty()) { announce(ih, isSeed); // announce to the closest we've heard from int annCnt = 0; long start = _context.clock().now(); for (Iterator<NodeInfo> iter = heardFrom.iterator(); iter.hasNext() && annCnt < annMax && _isRunning; ) { NodeInfo annTo = iter.next(); if (_log.shouldLog(Log.INFO)) _log.info("Announcing to closest from get peers: " + annTo); long toWait = annMaxWait > 0 ? Math.min(annMaxWait, 60*1000) : 0; if (announce(ih, annTo, toWait, isSeed)) annCnt++; if (annMaxWait > 0) { annMaxWait -= _context.clock().now() - start; if (annMaxWait < 1000) break; } } } else { // spray it, but unlikely to work, we just went through the kbuckets, // so this is essentially just a retry if (_log.shouldLog(Log.INFO)) _log.info("Announcing to closest in kbuckets after get peers failed"); announce(ih, annMax, annMaxWait, isSeed); } if (_log.shouldLog(Log.INFO)) { _log.info("Finished get Peers, returning " + rv.size()); _log.info("Tried: " + tried); _log.info("Heard from: " + heardFrom); _log.info("Not tried: " + toTry); } return rv; } /** * Announce to ourselves. * Non-blocking. * * @param ih the Info Hash (torrent) */ public void announce(byte[] ih, boolean isSeed) { InfoHash iHash = new InfoHash(ih); _tracker.announce(iHash, _myNodeInfo.getHash(), isSeed); } /** * Announce somebody else we know about to ourselves. * Non-blocking. * * @param ih the Info Hash (torrent) * @param peerHash the peer's Hash */ public void announce(byte[] ih, byte[] peerHash, boolean isSeed) { InfoHash iHash = new InfoHash(ih); _tracker.announce(iHash, new Hash(peerHash), isSeed); // Do NOT do this, corrupts the Hash cache and the Peer ID //_tracker.announce(iHash, Hash.create(peerHash)); } /** * Remove reference to ourselves in the local tracker. * Use when shutting down the torrent locally. * Non-blocking. * * @param ih the Info Hash (torrent) */ public void unannounce(byte[] ih) { InfoHash iHash = new InfoHash(ih); _tracker.unannounce(iHash, _myNodeInfo.getHash()); } /** * Not recommended - use getPeersAndAnnounce(). * * Announce to the closest peers in the local DHT. * This is NOT iterative - call getPeers() first to get the closest * peers into the local DHT. * Blocking unless maxWait <= 0 * Caller should run in a thread. * This also automatically announces ourself to our local tracker. * For best results do a getPeersAndAnnounce() instead, as this announces to * the closest in the kbuckets, it does NOT sort through the known nodes hashmap. * * @param ih the Info Hash (torrent) * @param max maximum number of peers to announce to * @param maxWait the maximum total time to wait (ms) or 0 to do all in parallel and return immediately. * @param isSeed true if seed, false if leech * @return the number of successful announces, not counting ourselves. */ public int announce(byte[] ih, int max, long maxWait, boolean isSeed) { announce(ih, isSeed); int rv = 0; long start = _context.clock().now(); InfoHash iHash = new InfoHash(ih); List<NodeInfo> nodes = _knownNodes.findClosest(iHash, max); if (_log.shouldLog(Log.INFO)) _log.info("Found " + nodes.size() + " to announce to for " + iHash); for (NodeInfo nInfo : nodes) { if (!_isRunning) break; if (announce(ih, nInfo, Math.min(maxWait, 60*1000), isSeed)) rv++; maxWait -= _context.clock().now() - start; if (maxWait < 1000) break; } return rv; } /** * Announce to a single DHT peer. * Blocking unless maxWait <= 0 * Caller should run in a thread. * For best results do a getPeers() first so we have a token. * * @param ih the Info Hash (torrent) * @param nInfo the peer to announce to * @param maxWait the maximum time to wait (ms) or 0 to return immediately. * @param isSeed true if seed, false if leech * @return success */ private boolean announce(byte[] ih, NodeInfo nInfo, long maxWait, boolean isSeed) { InfoHash iHash = new InfoHash(ih); // it isn't clear from BEP 5 if a token is bound to a single infohash? // for now, just bind to the NID //TokenKey tokenKey = new TokenKey(nInfo.getNID(), iHash); Token token = _incomingTokens.get(nInfo.getNID()); if (token != null && token.lastSeen() < _context.clock().now() - MAX_INBOUND_TOKEN_AGE) { // too old, cleaner will get it soon token = null; } if (token == null) { // we have no token, have to do a getPeers first to get a token if (maxWait <= 0) return false; if (_log.shouldLog(Log.INFO)) _log.info("No token for announce to " + nInfo + ", sending get_peers first"); ReplyWaiter waiter = sendGetPeers(nInfo, iHash, false); if (waiter == null) return false; long start = _context.clock().now(); synchronized(waiter) { try { waiter.wait(maxWait); } catch (InterruptedException ie) {} } int replyType = waiter.getReplyCode(); if (!(replyType == REPLY_PEERS || replyType == REPLY_NODES)) { if (_log.shouldLog(Log.INFO)) _log.info("Get_peers in announce() failed to " + nInfo); return false; } // we should have a token now token = _incomingTokens.get(nInfo.getNID()); if (token == null || token.lastSeen() < _context.clock().now() - MAX_INBOUND_TOKEN_AGE) { if (_log.shouldLog(Log.INFO)) _log.info("Huh? no token after get_peers in announce() succeeded to " + nInfo); return false; } maxWait -= _context.clock().now() - start; if (maxWait < 1000) { if (_log.shouldLog(Log.INFO)) _log.info("Ran out of time after get_peers in announce() succeeded to " + nInfo); return false; } } // send and wait on rcv msg lock unless maxWait <= 0 ReplyWaiter waiter = sendAnnouncePeer(nInfo, iHash, token, isSeed); if (waiter == null) return false; if (maxWait <= 0) return true; synchronized(waiter) { try { waiter.wait(maxWait); } catch (InterruptedException ie) {} } int replyType = waiter.getReplyCode(); return replyType == REPLY_PONG; } /** * Loads the DHT from file. * Can't be restarted after stopping? */ public synchronized void start() { if (_isRunning) return; _session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM_RAW, _rPort); _session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM, _qPort); _knownNodes.start(); _tracker.start(); PersistDHT.loadDHT(this, _dhtFile, _backupDhtFile); // start the explore thread _isRunning = true; // no need to keep ref, it will eventually stop new Cleaner(); new Explorer(5*1000); _txPkts.set(0); _rxPkts.set(0); _txBytes.set(0); _rxBytes.set(0); _started = _context.clock().now(); _nodesLastSaved = _started; } /** * Stop everything. */ public synchronized void stop() { if (!_isRunning) return; _isRunning = false; // FIXME stop the explore thread // unregister port listeners _session.removeListener(I2PSession.PROTO_DATAGRAM, _qPort); _session.removeListener(I2PSession.PROTO_DATAGRAM_RAW, _rPort); // clear the DHT and tracker _tracker.stop(); // don't lose all our peers if we didn't have time to check them boolean saveAll = _context.clock().now() - _started < 20*60*1000; PersistDHT.saveDHT(_knownNodes, saveAll, _dhtFile); _knownNodes.stop(); for (Iterator<ReplyWaiter> iter = _sentQueries.values().iterator(); iter.hasNext(); ) { ReplyWaiter waiter = iter.next(); iter.remove(); waiter.networkFail(); } _outgoingTokens.clear(); _incomingTokens.clear(); _blacklist.clear(); } /** * Clears the tracker and DHT data. * Call after saving DHT data to disk. */ public void clear() { _tracker.stop(); _knownNodes.clear(); } /** * Debug info, HTML formatted */ public String renderStatusHTML() { long uptime = Math.max(1000, _context.clock().now() - _started); StringBuilder buf = new StringBuilder(256); buf.append("<br><b>DHT DEBUG</b><br>TX: ").append(_txPkts.get()).append(" pkts / ") .append(DataHelper.formatSize2(_txBytes.get())).append("B / ") .append(DataHelper.formatSize2(_txBytes.get() * 1000 / uptime)).append("Bps<br>" + "RX: ").append(_rxPkts.get()).append(" pkts / ") .append(DataHelper.formatSize2(_rxBytes.get())).append("B / ") .append(DataHelper.formatSize2(_rxBytes.get() * 1000 / uptime)).append("Bps<br>" + "DHT Peers: ").append( _knownNodes.size()).append("<br>" + "Blacklisted: ").append(_blacklist.size()).append("<br>" + "Sent tokens: ").append(_outgoingTokens.size()).append("<br>" + "Rcvd tokens: ").append(_incomingTokens.size()).append("<br>" + "Pending queries: ").append(_sentQueries.size()).append("<br>"); _tracker.renderStatusHTML(buf); _knownNodes.renderStatusHTML(buf); return buf.toString(); } ////////// All private below here ///////////////////////////////////// ///// Sending..... // Queries..... // The first 3 queries use the query port. // Announces use the response port. /** * Blocking if we have to look up the dest for the nodeinfo * * @param nInfo who to send it to * @return null on error */ private ReplyWaiter sendPing(NodeInfo nInfo) { if (_log.shouldLog(Log.INFO)) _log.info("Sending ping to: " + nInfo); Map<String, Object> map = new HashMap<String, Object>(); map.put("q", "ping"); Map<String, Object> args = new HashMap<String, Object>(); map.put("a", args); return sendQuery(nInfo, map, true); } /** * Blocking if we have to look up the dest for the nodeinfo * * @param nInfo who to send it to * @param tID target ID we are looking for * @return null on error */ private ReplyWaiter sendFindNode(NodeInfo nInfo, NID tID) { if (_log.shouldLog(Log.INFO)) _log.info("Sending find node of " + tID + " to: " + nInfo); Map<String, Object> map = new HashMap<String, Object>(); map.put("q", "find_node"); Map<String, Object> args = new HashMap<String, Object>(); args.put("target", tID.getData()); map.put("a", args); return sendQuery(nInfo, map, true); } /** * Blocking if we have to look up the dest for the nodeinfo * * @param nInfo who to send it to * @param noSeeds true if we do not want seeds in the result * @return null on error */ private ReplyWaiter sendGetPeers(NodeInfo nInfo, InfoHash ih, boolean noSeeds) { if (_log.shouldLog(Log.INFO)) _log.info("Sending get peers of " + ih + " to: " + nInfo + " noseeds? " + noSeeds); Map<String, Object> map = new HashMap<String, Object>(); map.put("q", "get_peers"); Map<String, Object> args = new HashMap<String, Object>(); args.put("info_hash", ih.getData()); if (noSeeds) args.put("noseed", Integer.valueOf(1)); map.put("a", args); ReplyWaiter rv = sendQuery(nInfo, map, true); // save the InfoHash so we can get it later if (rv != null) rv.setSentObject(ih); return rv; } /** * Non-blocking, will fail if we don't have the dest for the nodeinfo * * @param nInfo who to send it to * @param isSeed true if seed, false if leech * @return null on error */ private ReplyWaiter sendAnnouncePeer(NodeInfo nInfo, InfoHash ih, Token token, boolean isSeed) { if (_log.shouldLog(Log.INFO)) _log.info("Sending announce of " + ih + " to: " + nInfo + " seed? " + isSeed); Map<String, Object> map = new HashMap<String, Object>(); map.put("q", "announce_peer"); Map<String, Object> args = new HashMap<String, Object>(); args.put("info_hash", ih.getData()); // port ignored args.put("port", Integer.valueOf(TrackerClient.PORT)); args.put("token", token.getData()); args.put("seed", Integer.valueOf(isSeed ? 1 : 0)); map.put("a", args); // an announce need not be signed, we have a token ReplyWaiter rv = sendQuery(nInfo, map, false); return rv; } // Responses..... // All responses use the response port. /** * @param nInfo who to send it to * @return success */ private boolean sendPong(NodeInfo nInfo, MsgID msgID) { if (_log.shouldLog(Log.INFO)) _log.info("Sending pong to: " + nInfo); Map<String, Object> map = new HashMap<String, Object>(); Map<String, Object> resps = new HashMap<String, Object>(); map.put("r", resps); return sendResponse(nInfo, msgID, map); } /** response to find_node (no token) */ private boolean sendNodes(NodeInfo nInfo, MsgID msgID, byte[] ids) { return sendNodes(nInfo, msgID, null, ids); } /** * response to find_node (token is null) or get_peers (has a token) * @param nInfo who to send it to * @return success */ private boolean sendNodes(NodeInfo nInfo, MsgID msgID, Token token, byte[] ids) { if (_log.shouldLog(Log.INFO)) _log.info("Sending nodes to: " + nInfo); Map<String, Object> map = new HashMap<String, Object>(); Map<String, Object> resps = new HashMap<String, Object>(); map.put("r", resps); if (token != null) resps.put("token", token.getData()); resps.put("nodes", ids); return sendResponse(nInfo, msgID, map); } /** @param token non-null */ private boolean sendPeers(NodeInfo nInfo, MsgID msgID, Token token, List<byte[]> peers) { if (_log.shouldLog(Log.INFO)) _log.info("Sending peers to: " + nInfo); Map<String, Object> map = new HashMap<String, Object>(); Map<String, Object> resps = new HashMap<String, Object>(); map.put("r", resps); resps.put("token", token.getData()); resps.put("values", peers); return sendResponse(nInfo, msgID, map); } // All errors use the response port. /** * Unused * * @param nInfo who to send it to * @return success */ private boolean sendError(NodeInfo nInfo, MsgID msgID, int err, String msg) { if (_log.shouldLog(Log.INFO)) _log.info("Sending error " + msg + " to: " + nInfo); Map<String, Object> map = new HashMap<String, Object>(4); List<Object> error = new ArrayList<Object>(2); error.add(Integer.valueOf(err)); error.add(msg); map.put("e", error); return sendError(nInfo, msgID, map); } // Low-level send methods // TODO sendQuery with onReply / onTimeout args /** * Blocking if repliable and we must lookup b32 * @param repliable true for all but announce * @return null on error */ @SuppressWarnings("unchecked") private ReplyWaiter sendQuery(NodeInfo nInfo, Map<String, Object> map, boolean repliable) { if (nInfo.equals(_myNodeInfo)) throw new IllegalArgumentException("don't send to ourselves"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending query to: " + nInfo); if (nInfo.getDestination() == null) { NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); if (newInfo != null && newInfo.getDestination() != null) { nInfo = newInfo; } else if (!repliable) { // Don't lookup for announce query, we should already have it if (_log.shouldLog(Log.WARN)) _log.warn("Dropping non-repliable query, no dest for " + nInfo); return null; } else { // Lookup the dest for the hash // TODO spin off into thread or queue? We really don't want to block here if (!lookupDest(nInfo)) { if (_log.shouldLog(Log.INFO)) _log.info("Dropping repliable query, no dest for " + nInfo); timeout(nInfo); return null; } } } map.put("y", "q"); MsgID mID = new MsgID(_context); map.put("t", mID.getData()); Map<String, Object> args = (Map<String, Object>) map.get("a"); if (args == null) throw new IllegalArgumentException("no args"); args.put("id", _myID); int port = nInfo.getPort(); if (!repliable) port++; boolean success = sendMessage(nInfo.getDestination(), port, map, repliable); if (success) { // save for the caller to get ReplyWaiter rv = new ReplyWaiter(mID, nInfo, null, null); _sentQueries.put(mID, rv); return rv; } return null; } /** * @param toPort the query port, we will increment here * @return success */ @SuppressWarnings("unchecked") private boolean sendResponse(NodeInfo nInfo, MsgID msgID, Map<String, Object> map) { if (nInfo.equals(_myNodeInfo)) throw new IllegalArgumentException("don't send to ourselves"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Sending response to: " + nInfo); if (nInfo.getDestination() == null) { NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); if (newInfo != null && newInfo.getDestination() != null) { nInfo = newInfo; } else { // lookup b32? if (_log.shouldLog(Log.WARN)) _log.warn("Dropping response, no dest for " + nInfo); return false; } } map.put("y", "r"); map.put("t", msgID.getData()); Map<String, Object> resps = (Map<String, Object>) map.get("r"); if (resps == null) throw new IllegalArgumentException("no resps"); resps.put("id", _myID); return sendMessage(nInfo.getDestination(), nInfo.getPort() + 1, map, false); } /** * Unused * * @return success */ private boolean sendError(NodeInfo nInfo, MsgID msgID, Map<String, Object> map) { if (nInfo.equals(_myNodeInfo)) throw new IllegalArgumentException("don't send to ourselves"); if (_log.shouldLog(Log.INFO)) _log.info("Sending error to: " + nInfo); if (nInfo.getDestination() == null) { NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); if (newInfo != null && newInfo.getDestination() != null) { nInfo = newInfo; } else { // lookup b32? if (_log.shouldLog(Log.WARN)) _log.warn("Dropping sendError, no dest for " + nInfo); return false; } } map.put("y", "e"); map.put("t", msgID.getData()); return sendMessage(nInfo.getDestination(), nInfo.getPort() + 1, map, false); } /** * Get the dest for a NodeInfo lacking it, and store it there. * Blocking. * @return success */ private boolean lookupDest(NodeInfo nInfo) { if (_log.shouldLog(Log.INFO)) _log.info("looking up dest for " + nInfo); try { // use a short timeout for now Destination dest = _session.lookupDest(nInfo.getHash(), DEST_LOOKUP_TIMEOUT); if (dest != null) { nInfo.setDestination(dest); if (_log.shouldLog(Log.INFO)) _log.info("lookup success for " + nInfo); return true; } } catch (I2PSessionException ise) { if (_log.shouldLog(Log.WARN)) _log.warn("lookup fail", ise); } if (_log.shouldLog(Log.INFO)) _log.info("lookup fail for " + nInfo); return false; } /** * Lowest-level send message call. * @param repliable true for all but announce * @return success */ private boolean sendMessage(Destination dest, int toPort, Map<String, Object> map, boolean repliable) { if (_session.isClosed()) { // Don't allow DHT to open a closed session if (_log.shouldLog(Log.WARN)) _log.warn("Not sending message, session is closed"); return false; } if (dest.calculateHash().equals(_myNodeInfo.getHash())) throw new IllegalArgumentException("don't send to ourselves"); byte[] payload = BEncoder.bencode(map); if (_log.shouldLog(Log.DEBUG)) { ByteArrayInputStream bais = new ByteArrayInputStream(payload); try { _log.debug("Sending to: " + dest.calculateHash() + ' ' + BDecoder.bdecode(bais).toString()); } catch (IOException ioe) {} } // Always send query port, peer will increment for unsigned replies int fromPort = _qPort; if (repliable) { I2PDatagramMaker dgMaker = new I2PDatagramMaker(_session); payload = dgMaker.makeI2PDatagram(payload); if (payload == null) { if (_log.shouldLog(Log.WARN)) _log.warn("DGM fail"); return false; } } SendMessageOptions opts = new SendMessageOptions(); opts.setDate(_context.clock().now() + 60*1000); opts.setTagsToSend(SEND_CRYPTO_TAGS); opts.setTagThreshold(LOW_CRYPTO_TAGS); if (!repliable) opts.setSendLeaseSet(false); try { boolean success = _session.sendMessage(dest, payload, 0, payload.length, repliable ? I2PSession.PROTO_DATAGRAM : I2PSession.PROTO_DATAGRAM_RAW, fromPort, toPort, opts); if (success) { _txPkts.incrementAndGet(); _txBytes.addAndGet(payload.length); } else { if (_log.shouldLog(Log.WARN)) _log.warn("sendMessage fail"); } return success; } catch (I2PSessionException ise) { if (_log.shouldLog(Log.WARN)) _log.warn("sendMessage fail", ise); return false; } } ///// Reception..... /** * @param from dest or null if it didn't come in on signed port */ private void receiveMessage(Destination from, int fromPort, byte[] payload) { try { InputStream is = new ByteArrayInputStream(payload); BDecoder dec = new BDecoder(is); BEValue bev = dec.bdecodeMap(); Map<String, BEValue> map = bev.getMap(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Got KRPC message " + bev.toString()); // Lazy here, just let missing Map entries throw NPEs, caught below byte[] msgIDBytes = map.get("t").getBytes(); MsgID mID = new MsgID(msgIDBytes); String type = map.get("y").getString(); if (type.equals("q")) { // queries must be repliable String method = map.get("q").getString(); Map<String, BEValue> args = map.get("a").getMap(); receiveQuery(mID, from, fromPort, method, args); } else if (type.equals("r") || type.equals("e")) { // get dest from id->dest map ReplyWaiter waiter = _sentQueries.remove(mID); if (waiter != null) { // TODO verify waiter NID and port? if (type.equals("r")) { Map<String, BEValue> response = map.get("r").getMap(); receiveResponse(waiter, response); } else { List<BEValue> error = map.get("e").getList(); receiveError(waiter, error); } } else { if (_log.shouldLog(Log.WARN)) _log.warn("Rcvd msg with no one waiting: " + bev.toString()); } } else { if (_log.shouldLog(Log.WARN)) _log.warn("Unknown msg type rcvd: " + bev.toString()); throw new InvalidBEncodingException("Unknown type: " + type); } // success /*** } catch (InvalidBEncodingException e) { } catch (IOException e) { } catch (ArrayIndexOutOfBoundsException e) { } catch (IllegalArgumentException e) { } catch (ClassCastException e) { } catch (NullPointerException e) { ***/ } catch (Exception e) { if (_log.shouldLog(Log.WARN)) _log.warn("Receive error for message", e); } } // Queries..... /** * Adds sender to our DHT. * @param dest may be null for announce_peer method only * @throws NPE too */ private void receiveQuery(MsgID msgID, Destination dest, int fromPort, String method, Map<String, BEValue> args) throws InvalidBEncodingException { if (dest == null && !method.equals("announce_peer")) { if (_log.shouldLog(Log.WARN)) _log.warn("Received non-announce_peer query method on reply port: " + method); return; } byte[] nid = args.get("id").getBytes(); NodeInfo nInfo; if (dest != null) { nInfo = new NodeInfo(new NID(nid), dest, fromPort); nInfo = heardFrom(nInfo); nInfo.setDestination(dest); // ninfo.checkport ? } else { nInfo = null; } if (method.equals("ping")) { receivePing(msgID, nInfo); } else if (method.equals("find_node")) { byte[] tid = args.get("target").getBytes(); NID tID = new NID(tid); receiveFindNode(msgID, nInfo, tID); } else if (method.equals("get_peers")) { byte[] hash = args.get("info_hash").getBytes(); InfoHash ih = new InfoHash(hash); boolean noSeeds = false; BEValue nos = args.get("noseed"); if (nos != null) noSeeds = nos.getInt() == 1; receiveGetPeers(msgID, nInfo, ih, noSeeds); } else if (method.equals("announce_peer")) { byte[] hash = args.get("info_hash").getBytes(); InfoHash ih = new InfoHash(hash); // this is the "TCP" port, we don't care //int port = args.get("port").getInt(); byte[] token = args.get("token").getBytes(); boolean isSeed = false; BEValue iss = args.get("seed"); if (iss != null) isSeed = iss.getInt() == 1; receiveAnnouncePeer(msgID, ih, token, isSeed); } else { if (_log.shouldLog(Log.WARN)) _log.warn("Unknown query method rcvd: " + method); } } /** * Called for a request or response * @return old NodeInfo or nInfo if none, use this to reduce object churn */ private NodeInfo heardFrom(NodeInfo nInfo) { // try to keep ourselves out of the DHT if (nInfo.equals(_myNodeInfo)) return _myNodeInfo; NID nID = nInfo.getNID(); NodeInfo oldInfo = _knownNodes.get(nID); if (oldInfo == null) { if (_log.shouldLog(Log.INFO)) _log.info("Adding node: " + nInfo); oldInfo = nInfo; NodeInfo nInfo2 = _knownNodes.putIfAbsent(nInfo); if (nInfo2 != null) oldInfo = nInfo2; } else { if (oldInfo.getDestination() == null && nInfo.getDestination() != null) oldInfo.setDestination(nInfo.getDestination()); } nID = oldInfo.getNID(); nID.setLastSeen(); if (_blacklist.remove(nID)) { if (_log.shouldLog(Log.INFO)) _log.info("UN-blacklisted: " + nID); } return oldInfo; } /** * Called for bootstrap or for all nodes in a receiveNodes reply. * Package private for PersistDHT. * @return non-null nodeInfo from DB if present, otherwise the nInfo parameter is returned */ NodeInfo heardAbout(NodeInfo nInfo) { // try to keep ourselves out of the DHT if (nInfo.equals(_myNodeInfo)) return _myNodeInfo; NodeInfo rv = _knownNodes.putIfAbsent(nInfo); if (rv == null) { rv = nInfo; // if we didn't know about it before, set the timestamp // so it isn't immediately removed by the DHTNodes cleaner rv.getNID().setLastSeen(); } return rv; } /** * Called when a reply times out */ private void timeout(NodeInfo nInfo) { NID nid = nInfo.getNID(); boolean remove = nid.timeout(); if (remove) { if (_knownNodes.remove(nid) != null) { if (_log.shouldLog(Log.INFO)) _log.info("Removed after consecutive timeouts: " + nInfo); } if (!_blacklist.contains(nid)) { // used as when-added time nid.setLastSeen(); _blacklist.add(nid); if (_log.shouldLog(Log.INFO)) _log.info("Blacklisted: " + nid); } } } /** * Handle and respond to the query */ private void receivePing(MsgID msgID, NodeInfo nInfo) throws InvalidBEncodingException { if (_log.shouldLog(Log.INFO)) _log.info("Rcvd ping from: " + nInfo); sendPong(nInfo, msgID); } /** * Handle and respond to the query * @param tID target ID they are looking for */ private void receiveFindNode(MsgID msgID, NodeInfo nInfo, NID tID) throws InvalidBEncodingException { if (_log.shouldLog(Log.INFO)) _log.info("Rcvd find_node from: " + nInfo + " for: " + tID); NodeInfo peer = _knownNodes.get(tID); if (peer != null) { // success, one answer sendNodes(nInfo, msgID, peer.getData()); } else { // get closest from DHT List<NodeInfo> nodes = _knownNodes.findClosest(tID, K); nodes.remove(nInfo); // him nodes.remove(_myNodeInfo); // me byte[] nodeArray = new byte[nodes.size() * NodeInfo.LENGTH]; for (int i = 0; i < nodes.size(); i ++) { System.arraycopy(nodes.get(i).getData(), 0, nodeArray, i * NodeInfo.LENGTH, NodeInfo.LENGTH); } sendNodes(nInfo, msgID, nodeArray); } } /** * Handle and respond to the query */ private void receiveGetPeers(MsgID msgID, NodeInfo nInfo, InfoHash ih, boolean noSeeds) throws InvalidBEncodingException { if (_log.shouldLog(Log.INFO)) _log.info("Rcvd get_peers from: " + nInfo + " for: " + ih + " noseeds? " + noSeeds); // generate and save random token Token token = new Token(_context); _outgoingTokens.put(token, nInfo); if (_log.shouldLog(Log.INFO)) _log.info("Stored new OB token: " + token + " for: " + nInfo); List<Hash> peers = _tracker.getPeers(ih, MAX_WANT, noSeeds); // Check this before removing him, so we don't needlessly send nodes // if he's the only one on the torrent. boolean noPeers = peers.isEmpty(); peers.remove(nInfo.getHash()); // him if (noPeers) { // similar to find node, but with token // get closest from DHT List<NodeInfo> nodes = _knownNodes.findClosest(ih, K); nodes.remove(nInfo); // him nodes.remove(_myNodeInfo); // me byte[] nodeArray = new byte[nodes.size() * NodeInfo.LENGTH]; for (int i = 0; i < nodes.size(); i ++) { System.arraycopy(nodes.get(i).getData(), 0, nodeArray, i * NodeInfo.LENGTH, NodeInfo.LENGTH); } sendNodes(nInfo, msgID, token, nodeArray); } else { List<byte[]> hashes; if (peers.isEmpty()) { hashes = Collections.emptyList(); } else { hashes = new ArrayList<byte[]>(peers.size()); for (Hash peer : peers) { hashes.add(peer.getData()); } } sendPeers(nInfo, msgID, token, hashes); } } /** * 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. * 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, boolean isSeed) throws InvalidBEncodingException { Token token = new Token(tok); NodeInfo nInfo = _outgoingTokens.get(token); 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()); return; } if (_log.shouldLog(Log.INFO)) _log.info("Rcvd announce from: " + nInfo + " for: " + ih + " seed? " + isSeed); _tracker.announce(ih, nInfo.getHash(), isSeed); // the reply for an announce is the same as the reply for a ping sendPong(nInfo, msgID); } // Responses..... /** * Handle the response and alert whoever sent the query it is responding to. * Adds sender nodeinfo to our DHT. * @throws NPE, IllegalArgumentException, and others too */ private void receiveResponse(ReplyWaiter waiter, Map<String, BEValue> response) throws InvalidBEncodingException { NodeInfo nInfo = waiter.getSentTo(); BEValue nodes = response.get("nodes"); BEValue values = response.get("values"); // token handling - save it for later announces if (nodes != null || values != null) { BEValue btok = response.get("token"); InfoHash ih = (InfoHash) waiter.getSentObject(); if (btok != null && ih != null) { byte[] tok = btok.getBytes(); Token token = new Token(_context, tok); _incomingTokens.put(nInfo.getNID(), token); if (_log.shouldLog(Log.DEBUG)) _log.debug("Got token: " + token + ", must be a response to get_peers"); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("No token and saved infohash, must be a response to find_node"); } } // now do the right thing if (nodes != null) { // find node or get peers response - concatenated NodeInfos byte[] ids = nodes.getBytes(); List<NodeInfo> rlist = receiveNodes(nInfo, ids); waiter.gotReply(REPLY_NODES, rlist); } else if (values != null) { // get peers response - list of Hashes List<BEValue> peers = values.getList(); List<Hash> rlist = receivePeers(nInfo, peers); waiter.gotReply(REPLY_PEERS, rlist); } else { // a ping response or an announce peer response byte[] nid = response.get("id").getBytes(); receivePong(nInfo, nid); waiter.gotReply(REPLY_PONG, null); } } /** * rcv concatenated 54 byte NodeInfos, return as a List * Adds all received nodeinfos to our DHT. * @throws NPE, IllegalArgumentException, and others too */ private List<NodeInfo> receiveNodes(NodeInfo nInfo, byte[] ids) throws InvalidBEncodingException { // Azureus sends 20 int max = Math.min(3 * K, ids.length / NodeInfo.LENGTH); List<NodeInfo> rv = new ArrayList<NodeInfo>(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)) _log.info("Ignoring blacklisted " + nInf.getNID() + " from: " + nInfo); continue; } nInf = heardAbout(nInf); rv.add(nInf); } if (_log.shouldLog(Log.INFO)) _log.info("Rcvd nodes from: " + nInfo + ": " + DataHelper.toString(rv)); return rv; } /** * rcv 32 byte Hashes, return as a List * @throws NPE, IllegalArgumentException, and others too */ private List<Hash> receivePeers(NodeInfo nInfo, List<BEValue> peers) throws InvalidBEncodingException { if (_log.shouldLog(Log.INFO)) _log.info("Rcvd peers from: " + nInfo); int max = Math.min(MAX_WANT * 2, peers.size()); List<Hash> rv = new ArrayList<Hash>(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.size() + " peers from: " + nInfo + ": " + DataHelper.toString(rv)); return rv; } /** * If node info was previously created with the dummy NID, * replace it with the received NID. */ private void receivePong(NodeInfo nInfo, byte[] nid) { if (nInfo.getNID().equals(FAKE_NID)) { NodeInfo newInfo = new NodeInfo(new NID(nid), nInfo.getHash(), nInfo.getPort()); Destination dest = nInfo.getDestination(); if (dest != null) newInfo.setDestination(dest); heardFrom(newInfo); } if (_log.shouldLog(Log.INFO)) _log.info("Rcvd pong from: " + nInfo); } // Errors..... /** * @param error 1st item is error code, 2nd is message string * @throws NPE, and others too */ private void receiveError(ReplyWaiter waiter, List<BEValue> error) throws InvalidBEncodingException { int errorCode = error.get(0).getInt(); String errorString = error.get(1).getString(); if (_log.shouldLog(Log.WARN)) _log.warn("Rcvd error from: " + waiter + " num: " + errorCode + " msg: " + errorString); // this calls heardFrom() waiter.gotReply(errorCode, errorString); } /** * Callback for replies */ private class ReplyWaiter extends SimpleTimer2.TimedEvent { private final MsgID mid; private final NodeInfo sentTo; private final Runnable onReply; private final Runnable onTimeout; private volatile int replyCode; private Object sentObject; private Object replyObject; /** * Either wait on this object with a timeout, or use non-null Runnables. * Any sent data to be remembered may be stored by setSentObject(). * Reply object may be in getReplyObject(). * @param onReply must be fast, otherwise set to null and wait on this UNUSED * @param onTimeout must be fast, otherwise set to null and wait on this UNUSED */ public ReplyWaiter(MsgID mID, NodeInfo nInfo, Runnable onReply, Runnable onTimeout) { super(SimpleTimer2.getInstance(), DEFAULT_QUERY_TIMEOUT); this.mid = mID; this.sentTo = nInfo; this.onReply = onReply; this.onTimeout = onTimeout; } public NodeInfo getSentTo() { return sentTo; } /** only used for get_peers, to save the Info Hash */ public void setSentObject(Object o) { sentObject = o; } /** @return that stored with setSentObject() */ public Object getSentObject() { return sentObject; } /** * Should contain null if getReplyCode is REPLY_PONG. * Should contain List<Hash> if getReplyCode is REPLY_PEERS. * Should contain List<NodeInfo> if getReplyCode is REPLY_NODES. * Should contain String if getReplyCode is > 200. * @return may be null depending on what happened. Cast to expected type. */ public Object getReplyObject() { return replyObject; } /** * If nonzero, we got a reply, and getReplyObject() may contain something. * @return code or 0 if no error */ public int getReplyCode() { return replyCode; } /** * Will notify this and run onReply. * Also removes from _sentQueries and calls heardFrom(). */ public void gotReply(int code, Object o) { cancel(); _sentQueries.remove(mid); replyObject = o; replyCode = code; // if it is fake, heardFrom is called by receivePong() if (!sentTo.getNID().equals(FAKE_NID)) heardFrom(sentTo); if (onReply != null) onReply.run(); synchronized(this) { this.notifyAll(); } } /** timer callback on timeout */ public void timeReached() { _sentQueries.remove(mid); if (onTimeout != null) onTimeout.run(); timeout(sentTo); if (_log.shouldLog(Log.INFO)) _log.warn("timeout waiting for reply from " + sentTo); synchronized(this) { this.notifyAll(); } } /** * Will notify this but not run onReply or onTimeout, * or remove from _sentQueries, or call heardFrom(). */ public void networkFail() { cancel(); replyCode = REPLY_NETWORK_FAIL; synchronized(this) { this.notifyAll(); } } } // I2PSessionMuxedListener interface ---------------- /** * Instruct the client that the given session has received a message * * Will be called only if you register via addMuxedSessionListener(). * Will be called only for the proto(s) and toPort(s) you register for. * * @param session session to notify * @param msgId message number available * @param size size of the message - why it's a long and not an int is a mystery * @param proto 1-254 or 0 for unspecified * @param fromPort 1-65535 or 0 for unspecified * @param toPort 1-65535 or 0 for unspecified */ public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromPort, int toPort) { // TODO throttle try { byte[] payload = session.receiveMessage(msgId); if (payload == null) return; _rxPkts.incrementAndGet(); _rxBytes.addAndGet(payload.length); if (toPort == _qPort) { // repliable I2PDatagramDissector dgDiss = new I2PDatagramDissector(); dgDiss.loadI2PDatagram(payload); payload = dgDiss.getPayload(); Destination from = dgDiss.getSender(); // TODO per-dest throttle receiveMessage(from, fromPort, payload); } else if (toPort == _rPort) { // raw receiveMessage(null, fromPort, payload); } else { if (_log.shouldLog(Log.WARN)) _log.warn("msg on bad port"); } } catch (DataFormatException e) { if (_log.shouldLog(Log.WARN)) _log.warn("bad msg"); } catch (I2PInvalidDatagramException e) { if (_log.shouldLog(Log.WARN)) _log.warn("bad msg"); } catch (I2PSessionException e) { if (_log.shouldLog(Log.WARN)) _log.warn("bad msg"); } } /** for non-muxed */ public void messageAvailable(I2PSession session, int msgId, long size) {} public void reportAbuse(I2PSession session, int severity) {} public void disconnected(I2PSession session) { if (_log.shouldLog(Log.WARN)) _log.warn("KRPC disconnected"); } public void errorOccurred(I2PSession session, String message, Throwable error) { if (_log.shouldLog(Log.WARN)) _log.warn("KRPC got error msg: ", error); } /** * Cleaner-upper */ private class Cleaner extends SimpleTimer2.TimedEvent { public Cleaner() { super(SimpleTimer2.getInstance(), 7 * CLEAN_TIME); } public void timeReached() { if (!_isRunning) return; long now = _context.clock().now(); if (_log.shouldLog(Log.DEBUG)) _log.debug("KRPC cleaner starting with " + _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(); // 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() < 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 (nid.lastSeen() < expire) iter.remove(); } if (now - _nodesLastSaved > NODES_SAVE_TIME) { PersistDHT.saveDHT(_knownNodes, false, _dhtFile); _nodesLastSaved = now; } // TODO sent queries? if (_log.shouldLog(Log.DEBUG)) _log.debug("KRPC cleaner done, now with " + _blacklist.size() + " in blacklist, " + _outgoingTokens.size() + " sent Tokens, " + _incomingTokens.size() + " rcvd Tokens, " + _knownNodes.size() + " known peers, " + _sentQueries.size() + " queries awaiting response"); schedule(CLEAN_TIME); } } /** * Fire off explorer thread */ private class Explorer extends SimpleTimer2.TimedEvent { public Explorer(long delay) { super(SimpleTimer2.getInstance(), delay); } public void timeReached() { if (!_isRunning) return; if (_knownNodes.size() > 0) (new I2PAppThread(new ExplorerThread(), "DHT Explore", true)).start(); else schedule(60*1000); } } /** * explorer thread */ private class ExplorerThread implements Runnable { public void run() { if (!_isRunning) return; if (!_hasBootstrapped) { if (_log.shouldLog(Log.INFO)) _log.info("Bootstrap start, size: " + _knownNodes.size()); explore(_myNID, 8, 60*1000, 1); if (_log.shouldLog(Log.INFO)) _log.info("Bootstrap done, size: " + _knownNodes.size()); _hasBootstrapped = true; } if (!_isRunning) return; if (_log.shouldLog(Log.INFO)) _log.info("Explore start. size: " + _knownNodes.size()); List<NID> keys = _knownNodes.getExploreKeys(); for (NID nid : keys) { explore(nid, 8, 60*1000, 1); if (!_isRunning) return; } if (_log.shouldLog(Log.INFO)) _log.info("Explore of " + keys.size() + " buckets done, new size: " + _knownNodes.size()); new Explorer(EXPLORE_TIME); } } }