package net.i2p.router.networkdb;
/*
* free (adj.): unencumbered; not under the control of others
* Written by jrandom in 2003 and released into the public domain
* with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk.
*
*/
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import net.i2p.data.DatabaseEntry;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.router.RouterIdentity;
import net.i2p.data.router.RouterInfo;
import net.i2p.data.SessionKey;
import net.i2p.data.TunnelId;
import net.i2p.data.i2np.DatabaseLookupMessage;
import net.i2p.data.i2np.DatabaseSearchReplyMessage;
import net.i2p.data.i2np.DatabaseStoreMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.TunnelGatewayMessage;
import net.i2p.router.Job;
import net.i2p.router.JobImpl;
import net.i2p.router.OutNetMessage;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.networkdb.kademlia.MessageWrapper;
import net.i2p.router.message.SendMessageDirectJob;
import net.i2p.util.Log;
/**
* Handle a lookup for a key received from a remote peer. Needs to be implemented
* to send back replies, etc
* Unused directly - see kademlia/ for extension
*/
public class HandleDatabaseLookupMessageJob extends JobImpl {
private final Log _log;
private final DatabaseLookupMessage _message;
private boolean _replyKeyConsumed;
private final static int MAX_ROUTERS_RETURNED = 3;
private final static int CLOSENESS_THRESHOLD = 8; // FNDF.MAX_TO_FLOOD + 1
private final static int REPLY_TIMEOUT = 60*1000;
private final static int MESSAGE_PRIORITY = OutNetMessage.PRIORITY_NETDB_REPLY;
/**
* If a routerInfo structure isn't this recent, don't send it out.
* Equal to KNDF.ROUTER_INFO_EXPIRATION_FLOODFILL.
*/
public final static long EXPIRE_DELAY = 60*60*1000;
public HandleDatabaseLookupMessageJob(RouterContext ctx, DatabaseLookupMessage receivedMessage, RouterIdentity from, Hash fromHash) {
super(ctx);
_log = getContext().logManager().getLog(HandleDatabaseLookupMessageJob.class);
_message = receivedMessage;
}
protected boolean answerAllQueries() { return false; }
public void runJob() {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Handling database lookup message for " + _message.getSearchKey());
Hash fromKey = _message.getFrom();
if (_log.shouldLog(Log.DEBUG)) {
if (_message.getReplyTunnel() != null)
_log.debug("dbLookup received with replies going to " + fromKey
+ " (tunnel " + _message.getReplyTunnel() + ")");
}
// If we are hidden we should not get queries, log and return
if (getContext().router().isHidden()) {
if (_log.shouldLog(Log.WARN)) {
_log.warn("Uninvited dbLookup received with replies going to " + fromKey
+ " (tunnel " + _message.getReplyTunnel() + ")");
}
return;
}
// i2pd bug?
if (_message.getSearchKey().equals(Hash.FAKE_HASH)) {
if (_log.shouldWarn())
_log.warn("Zero lookup", new Exception());
getContext().statManager().addRateData("netDb.DLMAllZeros", 1);
return;
}
DatabaseLookupMessage.Type lookupType = _message.getSearchType();
// only lookup once, then cast to correct type
DatabaseEntry dbe = getContext().netDb().lookupLocally(_message.getSearchKey());
if (dbe != null && dbe.getType() == DatabaseEntry.KEY_TYPE_LEASESET &&
(lookupType == DatabaseLookupMessage.Type.ANY || lookupType == DatabaseLookupMessage.Type.LS)) {
LeaseSet ls = (LeaseSet) dbe;
// We have to be very careful here to decide whether or not to send out the leaseSet,
// to avoid anonymity vulnerabilities.
// As this is complex, lots of comments follow...
boolean isLocal = getContext().clientManager().isLocal(ls.getDestination());
boolean shouldPublishLocal = isLocal && getContext().clientManager().shouldPublishLeaseSet(_message.getSearchKey());
// Only answer a request for a LeaseSet if it has been published
// to us, or, if its local, if we would have published to ourselves
// answerAllQueries: We are floodfill
// getReceivedAsPublished:
// false for local
// false for received over a tunnel
// false for received in response to our lookups
// true for received in a DatabaseStoreMessage unsolicited
if (ls.getReceivedAsPublished()) {
// Answer anything that was stored to us directly
// (i.e. "received as published" - not the result of a query, or received
// over a client tunnel).
// This is probably because we are floodfill, but also perhaps we used to be floodfill,
// so we don't check the answerAllQueries() flag.
// Local leasesets are not handled here
if (_log.shouldLog(Log.INFO))
_log.info("We have the published LS " + _message.getSearchKey() + ", answering query");
getContext().statManager().addRateData("netDb.lookupsMatchedReceivedPublished", 1);
sendData(_message.getSearchKey(), ls, fromKey, _message.getReplyTunnel());
} else if (shouldPublishLocal && answerAllQueries()) {
// We are floodfill, and this is our local leaseset, and we publish it.
// Only send it out if it is in our estimated keyspace.
// For this, we do NOT use their dontInclude list as it can't be trusted
// (i.e. it could mess up the closeness calculation)
Set<Hash> closestHashes = getContext().netDb().findNearestRouters(_message.getSearchKey(),
CLOSENESS_THRESHOLD, null);
if (weAreClosest(closestHashes)) {
// It's in our keyspace, so give it to them
if (_log.shouldLog(Log.INFO))
_log.info("We have local LS " + _message.getSearchKey() + ", answering query, in our keyspace");
getContext().statManager().addRateData("netDb.lookupsMatchedLocalClosest", 1);
sendData(_message.getSearchKey(), ls, fromKey, _message.getReplyTunnel());
} else {
// Lie, pretend we don't have it
if (_log.shouldLog(Log.INFO))
_log.info("We have local LS " + _message.getSearchKey() + ", NOT answering query, out of our keyspace");
getContext().statManager().addRateData("netDb.lookupsMatchedLocalNotClosest", 1);
Set<Hash> routerHashSet = getNearestRouters(lookupType);
sendClosest(_message.getSearchKey(), routerHashSet, fromKey, _message.getReplyTunnel());
}
} else {
// It was not published to us (we looked it up, for example)
// or it's local and we aren't floodfill,
// or it's local and we don't publish it.
// Lie, pretend we don't have it
if (_log.shouldLog(Log.INFO))
_log.info("We have LS " + _message.getSearchKey() +
", NOT answering query - local? " + isLocal + " shouldPublish? " + shouldPublishLocal +
" RAP? " + ls.getReceivedAsPublished() + " RAR? " + ls.getReceivedAsReply());
getContext().statManager().addRateData("netDb.lookupsMatchedRemoteNotClosest", 1);
Set<Hash> routerHashSet = getNearestRouters(lookupType);
sendClosest(_message.getSearchKey(), routerHashSet, fromKey, _message.getReplyTunnel());
}
} else if (dbe != null && dbe.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO &&
lookupType != DatabaseLookupMessage.Type.LS) {
RouterInfo info = (RouterInfo) dbe;
if (info.isCurrent(EXPIRE_DELAY)) {
if ( (info.isHidden()) || (isUnreachable(info) && !publishUnreachable()) ) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Not answering a query for a netDb peer who isn't reachable");
Set<Hash> us = Collections.singleton(getContext().routerHash());
sendClosest(_message.getSearchKey(), us, fromKey, _message.getReplyTunnel());
} else {
// send that routerInfo to the _message.getFromHash peer
if (_log.shouldLog(Log.DEBUG))
_log.debug("We do have key " + _message.getSearchKey()
+ " locally as a router info. sending to " + fromKey);
sendData(_message.getSearchKey(), info, fromKey, _message.getReplyTunnel());
}
} else {
// expired locally - return closest peer hashes
Set<Hash> routerHashSet = getNearestRouters(lookupType);
// ERR: see above
// // Remove hidden nodes from set..
// for (Iterator iter = routerInfoSet.iterator(); iter.hasNext();) {
// RouterInfo peer = (RouterInfo)iter.next();
// if (peer.isHidden()) {
// iter.remove();
// }
// }
if (_log.shouldLog(Log.DEBUG))
_log.debug("Expired " + _message.getSearchKey() +
" locally. sending back " + routerHashSet.size() + " peers to " + fromKey);
sendClosest(_message.getSearchKey(), routerHashSet, fromKey, _message.getReplyTunnel());
}
} else {
// not found locally - return closest peer hashes
Set<Hash> routerHashSet = getNearestRouters(lookupType);
if (_log.shouldLog(Log.DEBUG))
_log.debug("We do not have key " + _message.getSearchKey() +
" locally. sending back " + routerHashSet.size() + " peers to " + fromKey);
sendClosest(_message.getSearchKey(), routerHashSet, fromKey, _message.getReplyTunnel());
}
}
/**
* Closest to the message's search key,
* honoring the message's dontInclude set.
* Will not include us.
* Side effect - adds us to the message's dontInclude set.
*/
private Set<Hash> getNearestRouters(DatabaseLookupMessage.Type lookupType) {
// convert the new EXPL type flag to the old-style FAKE_HASH
// to pass to findNearestRouters()
Set<Hash> dontInclude = _message.getDontIncludePeers();
Hash us = getContext().routerHash();
if (dontInclude == null && lookupType == DatabaseLookupMessage.Type.EXPL) {
dontInclude = new HashSet<Hash>(2);
dontInclude.add(us);
dontInclude.add(Hash.FAKE_HASH);
} else if (dontInclude == null) {
dontInclude = Collections.singleton(us);
} else if (lookupType == DatabaseLookupMessage.Type.EXPL) {
dontInclude.add(us);
dontInclude.add(Hash.FAKE_HASH);
} else {
dontInclude.add(us);
}
// Honor flag to exclude all floodfills
//if (dontInclude.contains(Hash.FAKE_HASH)) {
// This is handled in FloodfillPeerSelector
return getContext().netDb().findNearestRouters(_message.getSearchKey(),
MAX_ROUTERS_RETURNED,
dontInclude);
}
private static boolean isUnreachable(RouterInfo info) {
if (info == null) return true;
String cap = info.getCapabilities();
return cap.indexOf(Router.CAPABILITY_REACHABLE) >= 0;
}
public static final String PROP_PUBLISH_UNREACHABLE = "router.publishUnreachableRouters";
public static final boolean DEFAULT_PUBLISH_UNREACHABLE = true;
private boolean publishUnreachable() {
return getContext().getProperty(PROP_PUBLISH_UNREACHABLE, DEFAULT_PUBLISH_UNREACHABLE);
}
private boolean weAreClosest(Set<Hash> routerHashSet) {
return routerHashSet.contains(getContext().routerHash());
}
private void sendData(Hash key, DatabaseEntry data, Hash toPeer, TunnelId replyTunnel) {
if (!key.equals(data.getHash())) {
_log.error("Hash mismatch HDLMJ");
return;
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Sending data matching key " + key + " to peer " + toPeer
+ " tunnel " + replyTunnel);
DatabaseStoreMessage msg = new DatabaseStoreMessage(getContext());
if (data.getType() == DatabaseEntry.KEY_TYPE_LEASESET) {
getContext().statManager().addRateData("netDb.lookupsMatchedLeaseSet", 1);
}
msg.setEntry(data);
getContext().statManager().addRateData("netDb.lookupsMatched", 1);
getContext().statManager().addRateData("netDb.lookupsHandled", 1);
sendMessage(msg, toPeer, replyTunnel);
}
protected void sendClosest(Hash key, Set<Hash> routerHashes, Hash toPeer, TunnelId replyTunnel) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Sending closest routers to key " + key + ": # peers = "
+ routerHashes.size() + " tunnel " + replyTunnel);
DatabaseSearchReplyMessage msg = new DatabaseSearchReplyMessage(getContext());
msg.setFromHash(getContext().routerHash());
msg.setSearchKey(key);
int i = 0;
for (Hash h : routerHashes) {
msg.addReply(h);
if (++i >= MAX_ROUTERS_RETURNED)
break;
}
getContext().statManager().addRateData("netDb.lookupsHandled", 1);
sendMessage(msg, toPeer, replyTunnel); // should this go via garlic messages instead?
}
protected void sendMessage(I2NPMessage message, Hash toPeer, TunnelId replyTunnel) {
if (replyTunnel != null) {
sendThroughTunnel(message, toPeer, replyTunnel);
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Sending reply directly to " + toPeer);
Job send = new SendMessageDirectJob(getContext(), message, toPeer, REPLY_TIMEOUT, MESSAGE_PRIORITY);
send.runJob();
//getContext().netDb().lookupRouterInfo(toPeer, send, null, REPLY_TIMEOUT);
}
}
private void sendThroughTunnel(I2NPMessage message, Hash toPeer, TunnelId replyTunnel) {
if (getContext().routerHash().equals(toPeer)) {
// if we are the gateway, act as if we received it
TunnelGatewayMessage m = new TunnelGatewayMessage(getContext());
m.setMessage(message);
m.setTunnelId(replyTunnel);
m.setMessageExpiration(message.getMessageExpiration());
getContext().tunnelDispatcher().dispatch(m);
} else {
// if we aren't the gateway, forward it on
if (!_replyKeyConsumed) {
// if we send a followup DSM w/ our RI, don't reuse key
SessionKey replyKey = _message.getReplyKey();
if (replyKey != null) {
// encrypt the reply
if (_log.shouldLog(Log.INFO))
_log.info("Sending encrypted reply to " + toPeer + ' ' + replyKey + ' ' + _message.getReplyTag());
message = MessageWrapper.wrap(getContext(), message, replyKey, _message.getReplyTag());
if (message == null) {
_log.error("Encryption error");
return;
}
_replyKeyConsumed = true;
}
}
TunnelGatewayMessage m = new TunnelGatewayMessage(getContext());
m.setMessage(message);
m.setMessageExpiration(message.getMessageExpiration());
m.setTunnelId(replyTunnel);
SendMessageDirectJob j = new SendMessageDirectJob(getContext(), m, toPeer, 10*1000, MESSAGE_PRIORITY);
j.runJob();
//getContext().jobQueue().addJob(j);
}
}
public String getName() { return "Handle Database Lookup Message"; }
@Override
public void dropped() {
getContext().messageHistory().messageProcessingError(_message.getUniqueId(),
_message.getClass().getName(),
"Dropped due to overload");
}
}