/* * This file is part of mlDHT. * * mlDHT 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 of the License, or * (at your option) any later version. * * mlDHT 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 mlDHT. If not, see <http://www.gnu.org/licenses/>. */ package lbms.plugins.mldht.kad.tasks; import java.net.InetSocketAddress; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.stream.Collectors; import static java.lang.Math.min; import lbms.plugins.mldht.kad.AnnounceNodeCache; import lbms.plugins.mldht.kad.DBItem; import lbms.plugins.mldht.kad.DHT; import lbms.plugins.mldht.kad.DHT.DHTtype; import lbms.plugins.mldht.kad.DHT.LogLevel; import lbms.plugins.mldht.kad.DHTConstants; import lbms.plugins.mldht.kad.KBucketEntry; import lbms.plugins.mldht.kad.KClosestNodesSearch; import lbms.plugins.mldht.kad.Key; import lbms.plugins.mldht.kad.Node; import lbms.plugins.mldht.kad.NodeList; import lbms.plugins.mldht.kad.PeerAddressDBItem; import lbms.plugins.mldht.kad.RPCCall; import lbms.plugins.mldht.kad.RPCServer; import lbms.plugins.mldht.kad.ScrapeResponseHandler; import lbms.plugins.mldht.kad.messages.GetPeersRequest; import lbms.plugins.mldht.kad.messages.GetPeersResponse; import lbms.plugins.mldht.kad.messages.MessageBase; import lbms.plugins.mldht.kad.messages.MessageBase.Method; import lbms.plugins.mldht.kad.utils.AddressUtils; /** * @author Damokles * */ public class PeerLookupTask extends IteratingTask { private boolean noAnnounce; private boolean noSeeds; private boolean fastTerminate; // nodes which have answered with tokens private Map<KBucketEntry, byte[]> announceCanidates; private ScrapeResponseHandler scrapeHandler; BiConsumer<KBucketEntry, PeerAddressDBItem> resultHandler = (x,y) -> {}; private Set<PeerAddressDBItem> returnedItems; AnnounceNodeCache cache; boolean useCache = true; public PeerLookupTask (RPCServer rpc, Node node, Key info_hash) { super(info_hash, rpc, node); announceCanidates = new ConcurrentHashMap<>(); returnedItems = Collections.newSetFromMap(new ConcurrentHashMap<PeerAddressDBItem, Boolean>()); cache = rpc.getDHT().getCache(); // register key even before the task is started so the cache can already accumulate entries cache.register(targetKey,false); addListener(t -> updatePopulationEstimator()); } public void setScrapeHandler(ScrapeResponseHandler scrapeHandler) { this.scrapeHandler = scrapeHandler; } public void useCache(boolean c) { useCache = c; } public void setResultHandler(BiConsumer<KBucketEntry,PeerAddressDBItem> handler) { resultHandler = handler; } public void setNoSeeds(boolean avoidSeeds) { noSeeds = avoidSeeds; } /** * enabling this also enables noAnnounce */ public void setFastTerminate(boolean fastTerminate) { if(!state.get().preStart()) throw new IllegalStateException("cannot change lookup mode after startup"); this.fastTerminate = fastTerminate; todo.allowRetransmits(!fastTerminate); if(fastTerminate) setNoAnnounce(true); } public void filterKnownUnreachableNodes(boolean toggle) { if(toggle) todo.setNonReachableCache(node.getDHT().getUnreachableCache()); else todo.setNonReachableCache(null); } public void setNoAnnounce(boolean noAnnounce) { this.noAnnounce = noAnnounce; } public boolean isNoAnnounce() { return noAnnounce; } /* (non-Javadoc) * @see lbms.plugins.mldht.kad.Task#callFinished(lbms.plugins.mldht.kad.RPCCall, lbms.plugins.mldht.kad.messages.MessageBase) */ @Override void callFinished (RPCCall c, MessageBase rsp) { if (c.getMessageMethod() != Method.GET_PEERS) { return; } GetPeersResponse gpr = (GetPeersResponse) rsp; KBucketEntry match = todo.acceptResponse(c); if(match == null) return; Set<KBucketEntry> returnedNodes = new HashSet<>(); NodeList nodes = gpr.getNodes(rpc.getDHT().getType()); if (nodes != null) { nodes.entries().filter(e -> !AddressUtils.isBogon(e.getAddress()) && !node.isLocalId(e.getID())).forEach(e -> { returnedNodes.add(e); }); } todo.addCandidates(match, returnedNodes); List<DBItem> items = gpr.getPeerItems(); //if(items.size() > 0) // System.out.println("unique:"+new HashSet<DBItem>(items).size()+" all:"+items.size()+" ver:"+gpr.getVersion()+" entries:"+items); for (DBItem item : items) { if(!(item instanceof PeerAddressDBItem)) continue; PeerAddressDBItem it = (PeerAddressDBItem) item; // also add the items to the returned_items list if(!AddressUtils.isBogon(it)) { resultHandler.accept(match, it); returnedItems.add(it); } } if(returnedItems.size() > 0 && firstResultTime == 0) firstResultTime = System.currentTimeMillis(); // if someone has peers he might have filters, collect for scrape if (!items.isEmpty() && scrapeHandler != null) synchronized (scrapeHandler) { scrapeHandler.addGetPeersRespone(gpr); } // add the peer who responded to the closest nodes list, so we can do an announce if (gpr.getToken() != null && !noAnnounce) announceCanidates.put(match, gpr.getToken()); // if we scrape we don't care about tokens. // otherwise we're only done if we have found the closest nodes that also returned tokens if (noAnnounce || gpr.getToken() != null) { closest.insert(match); } } /* (non-Javadoc) * @see lbms.plugins.mldht.kad.Task#callTimeout(lbms.plugins.mldht.kad.RPCCall) */ @Override void callTimeout (RPCCall c) { } @Override void update () { // check if the cache has any closer nodes after the initial query if(useCache) { Collection<KBucketEntry> cacheResults = cache.get(targetKey, requestConcurrency()); todo.addCandidates(null, cacheResults); } for(;;) { synchronized (this) { RequestPermit p = checkFreeSlot(); if(p == RequestPermit.NONE_ALLOWED) break; KBucketEntry e = todo.next2(kbe -> { RequestCandidateEvaluator eval = new RequestCandidateEvaluator(this, closest, todo, kbe, inFlight); return eval.goodForRequest(p); }).orElse(null); if(e == null) break; GetPeersRequest gpr = new GetPeersRequest(targetKey); // we only request cross-seeding on find-node gpr.setWant4(rpc.getDHT().getType() == DHTtype.IPV4_DHT); gpr.setWant6(rpc.getDHT().getType() == DHTtype.IPV6_DHT); gpr.setDestination(e.getAddress()); gpr.setScrape(scrapeHandler != null); gpr.setNoSeeds(noSeeds); if(!rpcCall(gpr, e.getID(), call -> { if(useCache) call.addListener(cache.getRPCListener()); call.builtFromEntry(e); long rtt = e.getRTT(); long defaultTimeout = rpc.getTimeoutFilter().getStallTimeout(); if(rtt < DHTConstants.RPC_CALL_TIMEOUT_MAX) { // the measured RTT is a mean and not the 90th percentile unlike the RPCServer's timeout filter // -> add some safety margin to account for variance rtt = (long) (rtt * (rtt < defaultTimeout ? 2 : 1.5)); call.setExpectedRTT(min(rtt, DHTConstants.RPC_CALL_TIMEOUT_MAX)); } if(DHT.isLogLevelEnabled(LogLevel.Verbose)) { List<InetSocketAddress> sources = todo.getSources(e).stream().map(KBucketEntry::getAddress).collect(Collectors.toList()); DHT.log("Task "+getTaskID()+" sending call to "+ e + " sources:" + sources, LogLevel.Verbose); } todo.addCall(call, e); })) { break; } } } } @Override protected boolean isDone() { int waitingFor = fastTerminate ? getNumOutstandingRequestsExcludingStalled() : getNumOutstandingRequests(); if(waitingFor > 0) return false; KBucketEntry closest = todo.next().orElse(null); if (closest == null) { return true; } RequestCandidateEvaluator eval = new RequestCandidateEvaluator(this, this.closest, todo, closest, inFlight); return eval.terminationPrecondition(); } private void updatePopulationEstimator() { synchronized (this) { // feed the estimator if we're sure that we haven't skipped anything in the closest-set if(!todo.next().isPresent() && noAnnounce && !fastTerminate && closest.reachedTargetCapacity()) { Set<Key> toEstimate = closest.ids().collect(Collectors.toCollection(HashSet::new)); rpc.getDHT().getEstimator().update(toEstimate,targetKey); } } } public Map<KBucketEntry, byte[]> getAnnounceCanidates() { if(fastTerminate || noAnnounce) throw new IllegalStateException("cannot use fast lookups for announces"); return announceCanidates; } /** * @return the returned_items */ public Set<PeerAddressDBItem> getReturnedItems () { return Collections.unmodifiableSet(returnedItems); } /** * @return the info_hash */ public Key getInfoHash () { return targetKey; } /* (non-Javadoc) * @see lbms.plugins.mldht.kad.Task#start() */ @Override public void start () { //delay the filling of the todo list until we actually start the task KClosestNodesSearch kns = new KClosestNodesSearch(targetKey, DHTConstants.MAX_ENTRIES_PER_BUCKET * 4,rpc.getDHT()); // unlike NodeLookups we do not use unverified nodes here. this avoids rewarding spoofers with useful lookup target IDs kns.fill(); todo.addCandidates(null, kns.getEntries()); if(useCache) { // re-register once we actually started cache.register(targetKey,fastTerminate); todo.addCandidates(null, cache.get(targetKey,DHTConstants.MAX_CONCURRENT_REQUESTS * 2)); } addListener(unused -> { logClosest(); }); super.start(); } }