From 4f892d63318564f2b001c1ea2340a2ffb8136ad6 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 25 Apr 2026 10:43:58 -0400 Subject: [PATCH] I2CP, SAM: Support lookup of ls2 options (proposal 167) --- .../router/naming/BlockfileNamingService.java | 8 +- .../java/src/net/i2p/sam/SAMMessageSess.java | 4 +- .../src/net/i2p/sam/SAMMessageSession.java | 19 +- .../src/net/i2p/sam/SAMStreamSession.java | 5 +- apps/sam/java/src/net/i2p/sam/SAMUtils.java | 26 ++- .../java/src/net/i2p/sam/SAMv1Handler.java | 54 +++++- core/java/src/net/i2p/client/I2PSession.java | 44 ++++- .../java/src/net/i2p/client/LookupResult.java | 21 +++ .../client/impl/HostReplyMessageHandler.java | 2 +- .../net/i2p/client/impl/I2PSessionImpl.java | 173 +++++++++++++++--- .../src/net/i2p/client/impl/LkupResult.java | 24 ++- .../src/net/i2p/client/impl/SubSession.java | 4 +- .../i2p/client/naming/DummyNamingService.java | 2 +- .../src/net/i2p/client/naming/LookupDest.java | 51 +++++- .../net/i2p/data/i2cp/HostLookupMessage.java | 102 +++++++++-- .../net/i2p/data/i2cp/HostReplyMessage.java | 5 +- .../client/ClientMessageEventListener.java | 5 +- .../net/i2p/router/client/LookupDestJob.java | 100 +++++++--- 18 files changed, 548 insertions(+), 101 deletions(-) diff --git a/apps/addressbook/java/src/net/i2p/router/naming/BlockfileNamingService.java b/apps/addressbook/java/src/net/i2p/router/naming/BlockfileNamingService.java index 2c3f8720c..cf0234853 100644 --- a/apps/addressbook/java/src/net/i2p/router/naming/BlockfileNamingService.java +++ b/apps/addressbook/java/src/net/i2p/router/naming/BlockfileNamingService.java @@ -791,13 +791,15 @@ public class BlockfileNamingService extends DummyNamingService { listname = lookupOptions.getProperty("list"); Destination d = null; + boolean isb32 = hostname.length() >= BASE32_HASH_LENGTH + 8 && hostname.toLowerCase(Locale.US).endsWith(".b32.i2p"); // only use cache if we aren't retreiving options or specifying the list - if (listname == null && storedOptions == null) { - d = super.lookup(hostname, null, null); + // except support ls2 options for b32 (prop. 167) + if (listname == null && (storedOptions == null || isb32)) { + d = super.lookup(hostname, null, storedOptions); if (d != null) return d; // Base32 failed? - if (hostname.length() >= BASE32_HASH_LENGTH + 8 && hostname.toLowerCase(Locale.US).endsWith(".b32.i2p")) + if (isb32) return null; } diff --git a/apps/sam/java/src/net/i2p/sam/SAMMessageSess.java b/apps/sam/java/src/net/i2p/sam/SAMMessageSess.java index 8aef9e473..b18ab0563 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMMessageSess.java +++ b/apps/sam/java/src/net/i2p/sam/SAMMessageSess.java @@ -1,6 +1,7 @@ package net.i2p.sam; import java.io.Closeable; +import java.util.Properties; import net.i2p.client.I2PSessionException; import net.i2p.data.DataFormatException; @@ -65,8 +66,9 @@ interface SAMMessageSess extends Closeable { * Lookup a destination through the I2CP session. * Blocking. * + * @param lsopts out parameter, or null * @return the Destination or null * @since 0.9.69 */ - public Destination lookupDest(String name) throws I2PSessionException ; + public Destination lookupDest(String name, Properties lsopts) throws I2PSessionException ; } diff --git a/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java b/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java index 259de3eda..37c81dcf9 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java @@ -20,6 +20,7 @@ import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.client.I2PSessionMuxedListener; +import net.i2p.client.LookupResult; import net.i2p.client.SendMessageOptions; import net.i2p.data.Base64; import net.i2p.data.DataFormatException; @@ -214,22 +215,34 @@ abstract class SAMMessageSession implements SAMMessageSess { * Lookup a destination through the I2CP session. * Blocking. * + * @param lsopts out parameter, or null * @return the Destination or null * @since 0.9.69 */ - public Destination lookupDest(String name) throws I2PSessionException { - return lookupDest(session, name); + public Destination lookupDest(String name, Properties lsopts) throws I2PSessionException { + return lookupDest(session, name, lsopts); } /** * Lookup a destination through the I2CP session. * Blocking. * + * @param lsopts out parameter, or null * @return the Destination or null * @since 0.9.69 */ - static Destination lookupDest(I2PSession session, String name) throws I2PSessionException { + static Destination lookupDest(I2PSession session, String name, Properties lsopts) throws I2PSessionException { // session will convert b32 to hash, no need to do it here + if (lsopts != null) { + LookupResult res = session.lookupDest2(name, 10*1000, true); + if (res.getResultCode() == LookupResult.RESULT_SUCCESS) { + Properties opts = res.getOptions(); + if (opts != null && !opts.isEmpty()) + lsopts.putAll(opts); + return res.getDestination(); + } + return null; + } return session.lookupDest(name, 10*1000); } diff --git a/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java b/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java index 497708777..fa64719ab 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java @@ -351,11 +351,12 @@ class SAMStreamSession implements SAMMessageSess { * Lookup a destination through the I2CP session. * Blocking. * + * @param lsopts out parameter, or null * @return the Destination or null * @since 0.9.69 */ - public Destination lookupDest(String name) throws I2PSessionException { - return SAMMessageSession.lookupDest(socketMgr.getSession(), name); + public Destination lookupDest(String name, Properties lsopts) throws I2PSessionException { + return SAMMessageSession.lookupDest(socketMgr.getSession(), name, lsopts); } /** diff --git a/apps/sam/java/src/net/i2p/sam/SAMUtils.java b/apps/sam/java/src/net/i2p/sam/SAMUtils.java index 8df46622f..c2280a653 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMUtils.java +++ b/apps/sam/java/src/net/i2p/sam/SAMUtils.java @@ -144,29 +144,41 @@ class SAMUtils { * Resolved the specified hostname. * * @param name Hostname to be resolved - * + * @param lsopts out parameter, or null * @return the Destination for the specified hostname, or null if not found */ - private static Destination lookupHost(String name) { + private static Destination lookupHost(String name, Properties lsopts) { NamingService ns = I2PAppContext.getGlobalContext().namingService(); - Destination dest = ns.lookup(name); + Destination dest = ns.lookup(name, null, lsopts); return dest; } - + /** * Resolve the destination from a key or a hostname * * @param s Hostname or key to be resolved - * * @return the Destination for the specified hostname, non-null * @throws DataFormatException on bad Base 64 or name not found */ - public static Destination getDest(String s) throws DataFormatException + public static Destination getDest(String s) throws DataFormatException { + return getDest(s, null); + } + + /** + * Resolve the destination from a key or a hostname + * + * @param s Hostname or key to be resolved + * @param lsopts out parameter, or null + * @return the Destination for the specified hostname, non-null + * @throws DataFormatException on bad Base 64 or name not found + * @since 0.9.70 + */ + public static Destination getDest(String s, Properties lsopts) throws DataFormatException { // NamingService caches b64 so just use it for everything // TODO: Add a static local cache here so SAM doesn't flush the // NamingService cache - Destination d = lookupHost(s); + Destination d = lookupHost(s, lsopts); if (d == null) { String msg; if (s.length() >= 516) diff --git a/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java index 7bf386ba0..5ba108503 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java +++ b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java @@ -20,6 +20,7 @@ import java.net.SocketTimeoutException; import java.nio.channels.SocketChannel; import java.nio.ByteBuffer; import java.util.Locale; +import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicLong; @@ -376,6 +377,9 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece return writeString("NAMING REPLY RESULT=KEY_NOT_FOUND NAME=\"\" MESSAGE=\"Must specify NAME\"\n"); } + // proposal 167 + boolean reqopts = "true".equals(props.getProperty("OPTIONS")); + Properties lsopts = reqopts ? new Properties() : null; Destination dest = null ; if (name.equals("ME")) { if (rawSession != null) { @@ -393,24 +397,28 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece try { if (name.length() >= 516 || !name.toLowerCase(Locale.US).endsWith(".b32.i2p")) { // out of session - dest = SAMUtils.getDest(name); + dest = SAMUtils.getDest(name, lsopts); } else if (streamSession != null) { // lookup in-session so the router will use the client tunnels to get the LS // and put the LS in the client's netdb - dest = streamSession.lookupDest(name); + dest = streamSession.lookupDest(name, lsopts); } else if (datagramSession != null) { - dest = datagramSession.lookupDest(name); + dest = datagramSession.lookupDest(name, lsopts); } else if (rawSession != null) { - dest = rawSession.lookupDest(name); + dest = rawSession.lookupDest(name, lsopts); } else { // out of session // leaseset will end up in the main netdb // and will have to be looked up again by the router if a message is sent to it. - dest = SAMUtils.getDest(name); + dest = SAMUtils.getDest(name, lsopts); } } catch (I2PSessionException e) { + if (_log.shouldWarn()) + _log.warn("lookup error", e); return writeString("NAMING REPLY RESULT=KEY_NOT_FOUND NAME=" + name, e.getMessage()); } catch (DataFormatException e) { + if (_log.shouldWarn()) + _log.warn("lookup error", e); return writeString("NAMING REPLY RESULT=KEY_NOT_FOUND NAME=" + name, e.getMessage()); } } @@ -419,6 +427,42 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece return writeString("NAMING REPLY RESULT=KEY_NOT_FOUND NAME=" + name + "\n"); } + if (lsopts != null && !lsopts.isEmpty()) { + // proposal 167 + StringBuilder buf = new StringBuilder(1024); + buf.append("NAMING REPLY RESULT=OK NAME=").append(name).append(" VALUE=").append(dest.toBase64()); + for (Map.Entry entry : lsopts.entrySet()) { + String k = (String) entry.getKey(); + if (k.indexOf('\n') >= 0 || + k.indexOf('\r') >= 0 || + k.indexOf('\t') >= 0 || + k.indexOf(' ') >= 0 || + k.indexOf('"') >= 0 || + k.indexOf('\\') >= 0 || + k.indexOf('=') >= 0 || // prefixed with = by addressbook Daemon for subscription params + k.length() == 0 || + (!name.toLowerCase(Locale.US).endsWith(".b32.i2p") && + (k.equals("a") || // blockfile builtins + k.equals("s") || + k.equals("v")))) { + // no facility for escaping keys + if (_log.shouldInfo()) + _log.info("Stripping LS option key \"" + k + '"'); + continue; + } + String v = (String) entry.getValue(); + v = v.replace("\n", " "); + v = v.replace("\r", " "); + v = v.replace("\\", "\\\\"); + v = v.replace("\"", "\\\""); + buf.append(" OPTION:").append(k).append("=\"").append(v).append('"'); + } + buf.append('\n'); + return writeString(buf.toString()); + } else if (lsopts != null) { + if (_log.shouldInfo()) + _log.info("Options requested for " + name + " but none received"); + } return writeString("NAMING REPLY RESULT=OK NAME=" + name + " VALUE=" + dest.toBase64() diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java index c787d3637..b9fa72807 100644 --- a/core/java/src/net/i2p/client/I2PSession.java +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -423,7 +423,7 @@ public interface I2PSession { public LookupResult lookupDest2(String name, long maxWait) throws I2PSessionException; /** - * Lookup a Destination by hostname. + * Lookup a Destination by hash. * Non-blocking. * If the result is cached or there is an immediate failure, * the result code will be something other than RESULT_DEFERRED, and the callback will NOT be called. @@ -436,7 +436,7 @@ public interface I2PSession { public LookupResult lookupDest(Hash h, long maxWait, LookupCallback callback) throws I2PSessionException; /** - * Lookup a Destination by hash. + * Lookup a Destination by hostname. * Non-blocking. * If the result is cached or there is an immediate failure, * the result code will be something other than RESULT_DEFERRED, and the callback will NOT be called. @@ -448,6 +448,46 @@ public interface I2PSession { */ public LookupResult lookupDest(String name, long maxWait, LookupCallback callback) throws I2PSessionException; + /** + * Ask the router to lookup a Destination by hostname. + * Blocking. See above for details. + * Same as lookupDest() but with a failure code in the return value + * + * @param maxWait ms + * @param withOptions see proposal 167 + * @return non-null + * @since 0.9.70 + */ + public LookupResult lookupDest2(String name, long maxWait, boolean withOptions) throws I2PSessionException; + + /** + * Lookup a Destination by hash. + * Non-blocking. + * If the result is cached or there is an immediate failure, + * the result code will be something other than RESULT_DEFERRED, and the callback will NOT be called. + * + * @param maxWait ms + * @param callback to return the result, non-null + * @param withOptions see proposal 167 + * @return non-null. If result code is RESULT_DEFERRED, callback will be called later + * @since 0.9.70 + */ + public LookupResult lookupDest(Hash h, long maxWait, LookupCallback callback, boolean withOptions) throws I2PSessionException; + + /** + * Lookup a Destination by hostname. + * Non-blocking. + * If the result is cached or there is an immediate failure, + * the result code will be something other than RESULT_DEFERRED, and the callback will NOT be called. + * + * @param maxWait ms + * @param callback to return the result, non-null + * @param withOptions see proposal 167 + * @return non-null. If result code is RESULT_DEFERRED, callback will be called later + * @since 0.9.70 + */ + public LookupResult lookupDest(String name, long maxWait, LookupCallback callback, boolean withOptions) throws I2PSessionException; + /** * Pass updated options to the router. * Does not remove properties previously present but missing from this options parameter. diff --git a/core/java/src/net/i2p/client/LookupResult.java b/core/java/src/net/i2p/client/LookupResult.java index e01ba8fc2..e2c075574 100644 --- a/core/java/src/net/i2p/client/LookupResult.java +++ b/core/java/src/net/i2p/client/LookupResult.java @@ -1,5 +1,7 @@ package net.i2p.client; +import java.util.Properties; + import net.i2p.data.Destination; import net.i2p.data.i2cp.HostReplyMessage; @@ -40,6 +42,18 @@ public interface LookupResult { */ public static final int RESULT_DECRYPTION_FAILURE = HostReplyMessage.RESULT_DECRYPTION_FAILURE; + /** + * See proposal 167 + * @since 0.9.70 + */ + public static final int RESULT_LEASESET_LOOKUP_FAILURE = HostReplyMessage.RESULT_LEASESET_LOOKUP_FAILURE; + + /** + * See proposal 167 + * @since 0.9.70 + */ + public static final int RESULT_LOOKUP_TYPE_UNSUPPORTED = HostReplyMessage.RESULT_LOOKUP_TYPE_UNSUPPORTED; + /** * For async calls only. Nonce will be non-zero and destination will be null. * Callback will be called later with the final result and the same nonce. @@ -58,6 +72,13 @@ public interface LookupResult { */ public Destination getDestination(); + /** + * See proposal 167 + * @return options from leaseset or null + * @since 0.9.70 + */ + public Properties getOptions(); + /** * For async calls only. Nonce will be non-zero. * Callback will be called later with the final result and the same nonce. diff --git a/core/java/src/net/i2p/client/impl/HostReplyMessageHandler.java b/core/java/src/net/i2p/client/impl/HostReplyMessageHandler.java index c1ebbad82..699843a70 100644 --- a/core/java/src/net/i2p/client/impl/HostReplyMessageHandler.java +++ b/core/java/src/net/i2p/client/impl/HostReplyMessageHandler.java @@ -30,7 +30,7 @@ class HostReplyMessageHandler extends HandlerImpl { Destination d = msg.getDestination(); long id = msg.getReqID(); if (d != null) { - session.destReceived(id, d); + session.destReceived(id, d, msg.getOptions()); } else { session.destLookupFailed(id, msg.getResultCode()); } diff --git a/core/java/src/net/i2p/client/impl/I2PSessionImpl.java b/core/java/src/net/i2p/client/impl/I2PSessionImpl.java index b68b21c92..44123f7c2 100644 --- a/core/java/src/net/i2p/client/impl/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/impl/I2PSessionImpl.java @@ -206,6 +206,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 private volatile boolean _routerSupportsHostLookup; private volatile boolean _routerSupportsLS2; private volatile boolean _routerSupportsBlindingInfo; + private volatile boolean _routerSupportsLookupOptions; protected static final int CACHE_MAX_SIZE = SystemVersion.isAndroid() ? 32 : 128; /** @@ -230,6 +231,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 private static final String MIN_FAST_VERSION = "0.9.4"; private static final String MIN_LS2_VERSION = "0.9.38"; private static final String MIN_BLINDINFO_VERSION = "0.9.43"; + private static final String MIN_LOOKUP_OPTIONS_VERSION = "0.9.70"; /** @param routerVersion as rcvd in the SetDateMessage, may be null for very old routers */ void dateUpdated(String routerVersion) { @@ -250,6 +252,9 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 _routerSupportsBlindingInfo = isrc || (routerVersion != null && routerVersion.length() > 0 && VersionComparator.comp(routerVersion, MIN_BLINDINFO_VERSION) >= 0); + _routerSupportsLookupOptions = isrc || + (routerVersion != null && routerVersion.length() > 0 && + VersionComparator.comp(routerVersion, MIN_LOOKUP_OPTIONS_VERSION) >= 0); synchronized (_stateLock) { if (_state == State.OPENING) { changeState(State.GOTDATE); @@ -327,6 +332,8 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 _routerSupportsHostLookup = isrc; _routerSupportsSubsessions = isrc; _routerSupportsLS2 = isrc; + _routerSupportsBlindingInfo = isrc; + _routerSupportsLookupOptions = isrc; } /** @@ -1611,7 +1618,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * @param d non-null * @since 0.9.11 */ - void destReceived(long nonce, Destination d) { + void destReceived(long nonce, Destination d, Properties opts) { // notify by nonce and hash Hash h = d.calculateHash(); for (LookupWaiter w : _pendingLookups) { @@ -1623,13 +1630,14 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 } if (w.callback != null) { // asynch - LkupResult result = new LkupResult(LookupResult.RESULT_SUCCESS, d, (int) w.nonce); + LkupResult result = new LkupResult(LookupResult.RESULT_SUCCESS, d, (int) w.nonce, opts); w.callback.complete(result); } else { // synch synchronized (w) { w.destination = d; w.code = LookupResult.RESULT_SUCCESS; + w.options = opts; w.notifyAll(); } } @@ -1691,6 +1699,11 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * @since 0.9.67 */ public final LookupCallback callback; + /** + * the leaseset options, see proposal 167 + * @since 0.9.70 + */ + public Properties options; public LookupWaiter(Hash h) { this(h, -1); @@ -1847,7 +1860,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * @return null on failure */ public Destination lookupDest(String name, long maxWait) throws I2PSessionException { - LookupWaiter waiter = x_lookupDest(name, maxWait); + LookupWaiter waiter = x_lookupDest(name, maxWait, false); if (waiter == null) return null; synchronized(waiter) { @@ -1865,7 +1878,21 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * @return non-null */ public LookupResult lookupDest2(String name, long maxWait) throws I2PSessionException { - LookupWaiter waiter = x_lookupDest(name, maxWait); + return lookupDest2(name, maxWait, false); + } + + /** + * Ask the router to lookup a Destination by hostname. + * Blocking. See above for details. + * Same as lookupDest() but with a failure code in the return value + * + * @param maxWait ms + * @param withOptions see proposal 167 + * @return non-null + * @since 0.9.70 + */ + public LookupResult lookupDest2(String name, long maxWait, boolean withOptions) throws I2PSessionException { + LookupWaiter waiter = x_lookupDest(name, maxWait, withOptions); if (waiter == null) return LOOKUP_FAILURE; synchronized(waiter) { @@ -1873,6 +1900,8 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 Destination d = waiter.destination; if (d == null && code == LookupResult.RESULT_SUCCESS) code = LookupResult.RESULT_FAILURE; + if (withOptions && code == LookupResult.RESULT_SUCCESS) + return new LkupResult(code, d, 0, waiter.options); return new LkupResult(code, d); } } @@ -1884,7 +1913,12 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * @since 0.9.11 * @return null on failure */ - private LookupWaiter x_lookupDest(String name, long maxWait) throws I2PSessionException { + private LookupWaiter x_lookupDest(String name, long maxWait, boolean withOptions) throws I2PSessionException { + if (!_routerSupportsLookupOptions) { + withOptions = false; + if (_log.shouldWarn()) + _log.warn("Lookup with options not supported by router"); + } if (name.length() == 0) return null; // Shortcut for b64 @@ -1898,10 +1932,13 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 // won't fit in Mapping if (name.length() >= 256 && !_context.isRouterContext()) return null; - synchronized (_lookupCache) { - Destination rv = _lookupCache.get(name); - if (rv != null) - return new LookupWaiter(rv); + // if options are requested, we need to ask the router + if (!withOptions) { + synchronized (_lookupCache) { + Destination rv = _lookupCache.get(name); + if (rv != null) + return new LookupWaiter(rv); + } } if (isClosed()) { if (_log.shouldLog(Log.INFO)) @@ -1910,8 +1947,48 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 } if (!_routerSupportsHostLookup) { // do them a favor and convert to Hash lookup - if (name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p")) - return new LookupWaiter(lookupDest(Hash.create(Base32.decode(name.toLowerCase(Locale.US).substring(0, 52))), maxWait)); + if (name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p")) { + Hash h = Hash.create(Base32.decode(name.toLowerCase(Locale.US).substring(0, 52))); + if (withOptions) { + // we only have an async call for hash-with-options + LkupCallback cb = new LkupCallback(); + LookupResult res = lookupDest(h, maxWait, cb, true); + LookupWaiter rv = new LookupWaiter((Destination) null); + int code = res.getResultCode(); + if (code == LookupResult.RESULT_SUCCESS) { + rv.destination = res.getDestination(); + rv.options = res.getOptions(); + rv.code = code; + return rv; + } + if (code != LookupResult.RESULT_DEFERRED) { + // immediate failure + rv.code = code; + return rv; + } + // blocking, sit and wait for it + synchronized(cb) { + try { cb.wait(maxWait); } catch (InterruptedException ie) {} + res = cb.getResult(); + } + if (res == null) { + // timeout + rv.code = LookupResult.RESULT_FAILURE; + return rv; + } + code = res.getResultCode(); + if (code == LookupResult.RESULT_SUCCESS) { + rv.destination = res.getDestination(); + rv.options = res.getOptions(); + rv.code = code; + return rv; + } + // failure + rv.code = code; + return rv; + } + return new LookupWaiter(lookupDest(h, maxWait)); + } // else unsupported if (_log.shouldLog(Log.WARN)) _log.warn("Router does not support HostLookup for " + name); @@ -1926,7 +2003,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 SessionId id = _sessionId; if (id == null) id = DUMMY_SESSION; - sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait)); + sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait, withOptions)); try { synchronized (waiter) { waiter.wait(maxWait); @@ -1941,7 +2018,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 } /** - * Lookup a Destination by hostname. + * Lookup a Destination by hash. * Non-blocking. * If the result is cached or there is an immediate failure, * the result code will be something other than RESULT_DEFERRED, and the callback will NOT be called. @@ -1952,10 +2029,34 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * @since 0.9.67 */ public LookupResult lookupDest(Hash h, long maxWait, LookupCallback callback) throws I2PSessionException { - synchronized (_lookupCache) { - Destination rv = _lookupCache.get(h); - if (rv != null) - return new LkupResult(LookupResult.RESULT_SUCCESS, rv); + return lookupDest(h, maxWait, callback, false); + } + + /** + * Lookup a Destination by hash. + * Non-blocking. + * If the result is cached or there is an immediate failure, + * the result code will be something other than RESULT_DEFERRED, and the callback will NOT be called. + * + * @param maxWait ms + * @param callback to return the result, non-null + * @param withOptions see proposal 167 + * @return non-null. If result code is RESULT_DEFERRED, callback will be called later + * @since 0.9.70 + */ + public LookupResult lookupDest(Hash h, long maxWait, LookupCallback callback, boolean withOptions) throws I2PSessionException { + if (!_routerSupportsLookupOptions) { + withOptions = false; + if (_log.shouldWarn()) + _log.warn("Lookup with options not supported by router"); + } + // if options are requested, we need to ask the router + if (!withOptions) { + synchronized (_lookupCache) { + Destination rv = _lookupCache.get(h); + if (rv != null) + return new LkupResult(LookupResult.RESULT_SUCCESS, rv); + } } synchronized (_stateLock) { // not before GOTDATE @@ -1977,7 +2078,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 if (maxWait > 60*1000) maxWait = 60*1000; try { - sendMessage_unchecked(new HostLookupMessage(id, h, nonce, maxWait)); + sendMessage_unchecked(new HostLookupMessage(id, h, nonce, maxWait, withOptions)); } catch (I2PSessionException ise) { _pendingLookups.remove(waiter); throw ise; @@ -1987,7 +2088,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 } /** - * Lookup a Destination by hash. + * Lookup a Destination by hostname. * Non-blocking. * If the result is cached or there is an immediate failure, * the result code will be something other than RESULT_DEFERRED, and the callback will NOT be called. @@ -1998,6 +2099,27 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * @since 0.9.67 */ public LookupResult lookupDest(String name, long maxWait, LookupCallback callback) throws I2PSessionException { + return lookupDest(name, maxWait, callback, false); + } + + /** + * Lookup a Destination by hostname. + * Non-blocking. + * If the result is cached or there is an immediate failure, + * the result code will be something other than RESULT_DEFERRED, and the callback will NOT be called. + * + * @param maxWait ms + * @param callback to return the result, non-null + * @param withOptions see proposal 167 + * @return non-null. If result code is RESULT_DEFERRED, callback will be called later + * @since 0.9.70 + */ + public LookupResult lookupDest(String name, long maxWait, LookupCallback callback, boolean withOptions) throws I2PSessionException { + if (!_routerSupportsLookupOptions) { + withOptions = false; + if (_log.shouldWarn()) + _log.warn("Lookup with options not supported by router"); + } if (name.length() == 0) return LOOKUP_FAILURE; // Shortcut for b64 @@ -2012,10 +2134,13 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 // won't fit in Mapping if (name.length() >= 256 && !_context.isRouterContext()) return LOOKUP_FAILURE; - synchronized (_lookupCache) { - Destination rv = _lookupCache.get(name); - if (rv != null) - return new LkupResult(LookupResult.RESULT_SUCCESS, rv); + // if options are requested, we need to ask the router + if (!withOptions) { + synchronized (_lookupCache) { + Destination rv = _lookupCache.get(name); + if (rv != null) + return new LkupResult(LookupResult.RESULT_SUCCESS, rv); + } } synchronized (_stateLock) { // not before GOTDATE @@ -2037,7 +2162,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 if (maxWait > 60*1000) maxWait = 60*1000; try { - sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait)); + sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait, withOptions)); } catch (I2PSessionException ise) { _pendingLookups.remove(waiter); throw ise; diff --git a/core/java/src/net/i2p/client/impl/LkupResult.java b/core/java/src/net/i2p/client/impl/LkupResult.java index 6507e9d37..797b39993 100644 --- a/core/java/src/net/i2p/client/impl/LkupResult.java +++ b/core/java/src/net/i2p/client/impl/LkupResult.java @@ -1,5 +1,7 @@ package net.i2p.client.impl; +import java.util.Properties; + import net.i2p.client.LookupResult; import net.i2p.data.Destination; @@ -13,9 +15,10 @@ public class LkupResult implements LookupResult { private final int _code; private final Destination _dest; private final int _nonce; + private final Properties _opts; LkupResult(int code, Destination dest) { - this(code, dest, 0); + this(code, dest, 0, null); } /** @@ -24,7 +27,7 @@ public class LkupResult implements LookupResult { * @since 0.9.67 */ LkupResult(int nonce) { - this(RESULT_DEFERRED, null, nonce); + this(RESULT_DEFERRED, null, nonce, null); } /** @@ -33,9 +36,19 @@ public class LkupResult implements LookupResult { * @since 0.9.67 */ LkupResult(int code, Destination dest, int nonce) { + this(code, dest, nonce, null); + } + + /** + * See proposal 167 + * + * @since 0.9.70 + */ + LkupResult(int code, Destination dest, int nonce, Properties opts) { _code = code; _dest = dest; _nonce = nonce; + _opts = opts; } /** @@ -48,6 +61,13 @@ public class LkupResult implements LookupResult { */ public Destination getDestination() { return _dest; } + /** + * See proposal 167 + * @return options from leaseset or null + * @since 0.9.70 + */ + public Properties getOptions() { return _opts; } + /** * For async calls only. Nonce will be non-zero. * Callback will be called later with the final result and the same nonce. diff --git a/core/java/src/net/i2p/client/impl/SubSession.java b/core/java/src/net/i2p/client/impl/SubSession.java index 9e18bde2a..f44e47833 100644 --- a/core/java/src/net/i2p/client/impl/SubSession.java +++ b/core/java/src/net/i2p/client/impl/SubSession.java @@ -257,8 +257,8 @@ class SubSession extends I2PSessionMuxedImpl { * on reception of HostReplyMessage * @param d non-null */ - void destReceived(long nonce, Destination d) { - _primary.destReceived(nonce, d); + void destReceived(long nonce, Destination d, Properties opts) { + _primary.destReceived(nonce, d, opts); } /** diff --git a/core/java/src/net/i2p/client/naming/DummyNamingService.java b/core/java/src/net/i2p/client/naming/DummyNamingService.java index a39ad112b..d535730c1 100644 --- a/core/java/src/net/i2p/client/naming/DummyNamingService.java +++ b/core/java/src/net/i2p/client/naming/DummyNamingService.java @@ -78,7 +78,7 @@ public class DummyNamingService extends NamingService { try { if (hostname.length() == BASE32_HASH_LENGTH + 8) { // b32 - d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH)); + d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH), storedOptions); } else { // b33 d = LookupDest.lookupHostname(_context, hostname); diff --git a/core/java/src/net/i2p/client/naming/LookupDest.java b/core/java/src/net/i2p/client/naming/LookupDest.java index d6b311163..f8c4a7662 100644 --- a/core/java/src/net/i2p/client/naming/LookupDest.java +++ b/core/java/src/net/i2p/client/naming/LookupDest.java @@ -11,6 +11,8 @@ import net.i2p.client.I2PSessionException; import net.i2p.client.I2PClient; import net.i2p.client.I2PSession; import net.i2p.client.I2PSimpleClient; +import net.i2p.client.LookupResult; +import net.i2p.client.impl.LkupCallback; import net.i2p.data.Base32; import net.i2p.data.Destination; import net.i2p.data.Hash; @@ -38,12 +40,22 @@ public class LookupDest { protected LookupDest(I2PAppContext context) {} - /** @param key 52 chars (do not include the .b32.i2p suffix) */ + /** + * @param key 52 chars (do not include the .b32.i2p suffix) + */ static Destination lookupBase32Hash(I2PAppContext ctx, String key) throws I2PSessionException { + return lookupBase32Hash(ctx, key, null); + } + + /** + * @param key 52 chars (do not include the .b32.i2p suffix) + * @param lsopts out property or null + */ + static Destination lookupBase32Hash(I2PAppContext ctx, String key, Properties lsopts) throws I2PSessionException { byte[] h = Base32.decode(key); if (h == null) return null; - return lookupHash(ctx, h); + return lookupHash(ctx, h, lsopts); } /* Might be useful but not in the context of urls due to upper/lower case */ @@ -56,8 +68,12 @@ public class LookupDest { } ****/ - /** @param h 32 byte hash */ - private static Destination lookupHash(I2PAppContext ctx, byte[] h) throws I2PSessionException { + /** + * @param h 32 byte hash + * @param lsopts out property or null + * @since 0.9.70 + */ + private static Destination lookupHash(I2PAppContext ctx, byte[] h, Properties lsopts) throws I2PSessionException { Hash key = Hash.create(h); Destination rv = null; I2PClient client = new I2PSimpleClient(); @@ -66,7 +82,32 @@ public class LookupDest { try { session = client.createSession(null, opts); session.connect(); - rv = session.lookupDest(key, DEFAULT_TIMEOUT); + if (lsopts != null) { + // prop. 167 + LkupCallback cb = new LkupCallback(); + LookupResult res = session.lookupDest(key, 10*1000, cb, true); + int code = res.getResultCode(); + if (code == LookupResult.RESULT_SUCCESS) { + rv = res.getDestination(); + Properties ropts = res.getOptions(); + if (ropts != null && !ropts.isEmpty()) + lsopts.putAll(ropts); + } else if (code == LookupResult.RESULT_DEFERRED) { + // blocking, sit and wait for it + synchronized(cb) { + try { cb.wait(10*1000); } catch (InterruptedException ie) {} + res = cb.getResult(); + } + if (res != null && res.getResultCode() == LookupResult.RESULT_SUCCESS) { + rv = res.getDestination(); + Properties ropts = res.getOptions(); + if (ropts != null && !ropts.isEmpty()) + lsopts.putAll(ropts); + } // else timeout or failure + } // else immediate failure + } else { + rv = session.lookupDest(key, DEFAULT_TIMEOUT); + } } finally { if (session != null) session.destroySession(); diff --git a/core/java/src/net/i2p/data/i2cp/HostLookupMessage.java b/core/java/src/net/i2p/data/i2cp/HostLookupMessage.java index 54fd3794b..1897f77e9 100644 --- a/core/java/src/net/i2p/data/i2cp/HostLookupMessage.java +++ b/core/java/src/net/i2p/data/i2cp/HostLookupMessage.java @@ -11,6 +11,7 @@ import java.io.InputStream; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.util.ByteArrayStream; @@ -28,6 +29,7 @@ public class HostLookupMessage extends I2CPMessageImpl { private int _lookupType; private Hash _hash; private String _host; + private Destination _dest; private SessionId _sessionId; public static final int LOOKUP_HASH = 0; @@ -96,6 +98,28 @@ public class HostLookupMessage extends I2CPMessageImpl { _lookupType = reqOpts ? LOOKUP_HOST_OPT : LOOKUP_HOST; } + /** + * Always requests LS2 options + * + * @param reqID 0 to 2**32 - 1 + * @param timeout ms 1 to 2**32 - 1 + * @since 0.9.70 + */ + public HostLookupMessage(SessionId id, Destination dest, long reqID, long timeout) { + if (id == null || dest == null) + throw new IllegalArgumentException(); + if (reqID < 0 || reqID > MAX_INT) + throw new IllegalArgumentException(); + if (timeout <= 0 || timeout > MAX_INT) + throw new IllegalArgumentException(); + _sessionId = id; + _dest = dest; + _hash = dest.calculateHash(); + _reqID = reqID; + _timeout = timeout; + _lookupType = LOOKUP_DEST_OPT; + } + public SessionId getSessionId() { return _sessionId; } @@ -125,26 +149,34 @@ public class HostLookupMessage extends I2CPMessageImpl { } /** - * @return 0 (hash) or 1 (host) + * @return 0-4 */ public int getLookupType() { return _lookupType; } /** - * @return only valid if lookup type == 0 + * @return only valid if lookup type == 0 or 2 or 4 */ public Hash getHash() { return _hash; } /** - * @return only valid if lookup type == 1 + * @return only valid if lookup type == 1 or 3 */ public String getHostname() { return _host; } + /** + * @return only valid if lookup type == 4 + * @since 0.9.70 + */ + public Destination getDestination() { + return _dest; + } + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { try { _sessionId = new SessionId(); @@ -154,13 +186,24 @@ public class HostLookupMessage extends I2CPMessageImpl { _lookupType = in.read(); if (_lookupType < 0) throw new EOFException(); - if (_lookupType == LOOKUP_HASH || _lookupType == LOOKUP_HASH_OPT) { + switch (_lookupType) { + case LOOKUP_HASH: + case LOOKUP_HASH_OPT: _hash = Hash.create(in); - } else if (_lookupType == LOOKUP_HOST || _lookupType == LOOKUP_HOST_OPT) { + break; + + case LOOKUP_HOST: + case LOOKUP_HOST_OPT: _host = DataHelper.readString(in); if (_host.length() == 0) throw new I2CPMessageException("bad host"); - } else { + break; + + case LOOKUP_DEST_OPT: + _dest = Destination.create(in); + break; + + default: throw new I2CPMessageException("bad type"); } } catch (DataFormatException dfe) { @@ -170,15 +213,26 @@ public class HostLookupMessage extends I2CPMessageImpl { protected byte[] doWriteMessage() throws I2CPMessageException, IOException { int len; - if (_lookupType == LOOKUP_HASH || _lookupType == LOOKUP_HASH_OPT) { + switch (_lookupType) { + case LOOKUP_HASH: + case LOOKUP_HASH_OPT: if (_hash == null) throw new I2CPMessageException("Unable to write out the message as there is not enough data"); len = 11 + Hash.HASH_LENGTH; - } else if (_lookupType == LOOKUP_HOST || _lookupType == LOOKUP_HOST_OPT) { + break; + + case LOOKUP_HOST: + case LOOKUP_HOST_OPT: if (_host == null) throw new I2CPMessageException("Unable to write out the message as there is not enough data"); len = 12 + _host.length(); - } else { + break; + + case LOOKUP_DEST_OPT: + len = 11 + _dest.size(); + break; + + default: throw new I2CPMessageException("bad type"); } ByteArrayStream os = new ByteArrayStream(len); @@ -187,10 +241,20 @@ public class HostLookupMessage extends I2CPMessageImpl { DataHelper.writeLong(os, 4, _reqID); DataHelper.writeLong(os, 4, _timeout); os.write((byte) _lookupType); - if (_lookupType == LOOKUP_HASH) { + switch (_lookupType) { + case LOOKUP_HASH: + case LOOKUP_HASH_OPT: _hash.writeBytes(os); - } else { + break; + + case LOOKUP_HOST: + case LOOKUP_HOST_OPT: DataHelper.writeString(os, _host); + break; + + case LOOKUP_DEST_OPT: + _dest.writeBytes(os); + break; } } catch (DataFormatException dfe) { throw new I2CPMessageException("bad data", dfe); @@ -209,10 +273,22 @@ public class HostLookupMessage extends I2CPMessageImpl { buf.append("\n\t").append(_sessionId); buf.append("\n\tReqID: ").append(_reqID); buf.append("\n\tTimeout: ").append(_timeout); - if (_lookupType == LOOKUP_HASH) + buf.append("\n\tType: ").append(_lookupType); + switch (_lookupType) { + case LOOKUP_HASH: + case LOOKUP_HASH_OPT: buf.append("\n\tHash: ").append(_hash); - else if (_lookupType == LOOKUP_HOST) + break; + + case LOOKUP_HOST: + case LOOKUP_HOST_OPT: buf.append("\n\tHost: ").append(_host); + break; + + case LOOKUP_DEST_OPT: + buf.append("\n\tDest: ").append(_dest); + break; + } buf.append("]"); return buf.toString(); } diff --git a/core/java/src/net/i2p/data/i2cp/HostReplyMessage.java b/core/java/src/net/i2p/data/i2cp/HostReplyMessage.java index 28a4fb525..f1986f4e9 100644 --- a/core/java/src/net/i2p/data/i2cp/HostReplyMessage.java +++ b/core/java/src/net/i2p/data/i2cp/HostReplyMessage.java @@ -196,8 +196,11 @@ public class HostReplyMessage extends I2CPMessageImpl { buf.append("\n\t").append(_sessionId); buf.append("\n\tReqID: ").append(_reqID); buf.append("\n\tResult: ").append(_code); - if (_code == RESULT_SUCCESS) + if (_code == RESULT_SUCCESS) { buf.append("\n\tDestination: ").append(_dest); + if (_options != null && !_options.isEmpty()) + buf.append("\n\tOptions: ").append(DataHelper.toString(_options)); + } buf.append("]"); return buf.toString(); } diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index 59dde1fb9..9056ce734 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -766,8 +766,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi // h may still be null, an LS lookup for b32 will go out expl. tunnels } _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getReqID(), - message.getTimeout(), sid, - message.getHash(), message.getHostname(), h)); + message.getTimeout(), sid, message.getLookupType(), + message.getHash(), message.getHostname(), + message.getDestination(), h)); } /** diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java index 22321c7af..67f1559de 100644 --- a/router/java/src/net/i2p/router/client/LookupDestJob.java +++ b/router/java/src/net/i2p/router/client/LookupDestJob.java @@ -5,17 +5,21 @@ package net.i2p.router.client; import java.util.Locale; +import java.util.Properties; import net.i2p.crypto.Blinding; import net.i2p.data.Base32; import net.i2p.data.BlindData; import net.i2p.data.DatabaseEntry; import net.i2p.data.Destination; +import net.i2p.data.EmptyProperties; import net.i2p.data.EncryptedLeaseSet; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; +import net.i2p.data.LeaseSet2; import net.i2p.data.SigningPublicKey; import net.i2p.data.i2cp.DestReplyMessage; +import net.i2p.data.i2cp.HostLookupMessage; import net.i2p.data.i2cp.HostReplyMessage; import net.i2p.data.i2cp.I2CPMessage; import net.i2p.data.i2cp.I2CPMessageException; @@ -34,35 +38,41 @@ class LookupDestJob extends JobImpl { private final ClientConnectionRunner _runner; private final long _reqID; private final long _timeout; - private final Hash _hash; + private Hash _hash; private final String _name; + private Destination _dest; private final SessionId _sessID; private final Hash _fromLocalDest; private final BlindData _blindData; + private final boolean _wantsOptions; private static final long DEFAULT_TIMEOUT = 15*1000; public LookupDestJob(RouterContext context, ClientConnectionRunner runner, Hash h, Hash fromLocalDest) { - this(context, runner, -1, DEFAULT_TIMEOUT, null, h, null, fromLocalDest); + this(context, runner, -1, DEFAULT_TIMEOUT, null, HostLookupMessage.LOOKUP_HASH, h, null, null, fromLocalDest); } /** - * One of h or name non-null. + * One of h or name or dest non-null. * * For hash or b32 name, the dest will be returned if the LS can be found, * even if the dest uses unsupported crypto. * * @param reqID must be >= 0 if name != null * @param sessID must non-null if reqID >= 0 + * @param type as defined in HostLookupMessage + * @param h only valid for type 0, 2, 4 + * @param name only valid for type 1, 3 + * @param dest only valid for type 4 * @param fromLocalDest use these tunnels for the lookup, or null for exploratory * @since 0.9.11 */ public LookupDestJob(RouterContext context, ClientConnectionRunner runner, - long reqID, long timeout, SessionId sessID, Hash h, String name, - Hash fromLocalDest) { + long reqID, long timeout, SessionId sessID, int type, Hash h, String name, + Destination dest, Hash fromLocalDest) { super(context); _log = context.logManager().getLog(LookupDestJob.class); - if ((h == null && name == null) || + if ((h == null && name == null && dest == null) || (h != null && name != null) || (reqID >= 0 && sessID == null) || (reqID < 0 && name != null)) { @@ -74,6 +84,7 @@ class LookupDestJob extends JobImpl { _timeout = timeout; _sessID = sessID; _fromLocalDest = fromLocalDest; + _wantsOptions = type >= HostLookupMessage.LOOKUP_HASH_OPT; BlindData bd = null; if (name != null && name.length() >= 60) { // convert a b32 lookup to a hash lookup @@ -134,6 +145,7 @@ class LookupDestJob extends JobImpl { } _hash = h; _name = name; + _dest = dest; _blindData = bd; } @@ -166,23 +178,28 @@ class LookupDestJob extends JobImpl { if (d != null) { if (_log.shouldDebug()) _log.debug("Found cached b33 lookup " + _blindData.getUnblindedPubKey() + " to " + d); - returnDest(d); + returnDest(d, null); return; } } if (_name != null) { // inline, ignore timeout - Destination d = getContext().namingService().lookup(_name); - if (d != null) { + _dest = getContext().namingService().lookup(_name); + if (_dest != null) { if (_log.shouldDebug()) - _log.debug("Found name lookup " + _name + " to " + d); - returnDest(d); + _log.debug("Found name lookup " + _name + " to " + _dest); + if (!_wantsOptions) + returnDest(_dest, null); + // else fall through, we need the LS + _hash = _dest.calculateHash(); } else { if (_log.shouldDebug()) _log.debug("Failed name lookup " + _name); returnFail(); } - } else if (_hash != null) { + } + + if (_hash != null) { DoneJob done = new DoneJob(getContext()); // shorten timeout so we can respond before the client side times out long timeout = _timeout; @@ -190,9 +207,11 @@ class LookupDestJob extends JobImpl { timeout -= 500; // TODO tell router this is an encrypted lookup, skip 38 or earlier ffs? NetworkDatabaseFacade db = _runner.getFloodfillNetworkDatabaseFacade(); - if (db == null) - db = getContext().netDb(); - db.lookupDestination(_hash, done, timeout, _fromLocalDest); + if (_dest == null) { + if (db == null) + db = getContext().netDb(); + db.lookupDestination(_hash, done, timeout, _fromLocalDest); + } } else { // blinding decode fail returnFail(HostReplyMessage.RESULT_DECRYPTION_FAILURE); @@ -208,37 +227,64 @@ class LookupDestJob extends JobImpl { NetworkDatabaseFacade db = _runner.getFloodfillNetworkDatabaseFacade(); if (db == null) db = getContext().netDb(); - Destination dest = db.lookupDestinationLocally(_hash); - if (dest == null && _blindData != null) { + LeaseSet ls = null; + if (_wantsOptions) { + ls = db.lookupLeaseSetLocally(_hash); + if (ls != null && _dest == null) + _dest = ls.getDestination(); + } else if (_dest == null) { + _dest = db.lookupDestinationLocally(_hash); + } + if (_dest == null && _blindData != null) { // TODO store and lookup original hash instead - LeaseSet ls = db.lookupLeaseSetLocally(_hash); + if (ls == null) + ls = db.lookupLeaseSetLocally(_hash); if (ls != null && ls.getType() == DatabaseEntry.KEY_TYPE_ENCRYPTED_LS2) { // already decrypted EncryptedLeaseSet encls = (EncryptedLeaseSet) ls; LeaseSet decls = encls.getDecryptedLeaseSet(); if (decls != null) { - dest = decls.getDestination(); + _dest = decls.getDestination(); } } } - if (dest != null) { + if (_dest != null) { if (_log.shouldDebug()) - _log.debug("Found hash lookup " + _hash + " to " + dest); - returnDest(dest); + _log.debug("Found hash lookup " + _hash + " to " + _dest); + if (_wantsOptions) { + Properties p = null; + if (ls != null && ls.getType() == DatabaseEntry.KEY_TYPE_LS2) { + LeaseSet2 ls2 = (LeaseSet2) ls; + p = ls2.getOptions(); + } + returnDest(_dest, p); + } else { + returnDest(_dest, null); + } } else { if (_log.shouldDebug()) _log.debug("Failed hash lookup " + _hash); - returnFail(); + if (_wantsOptions && ls == null) + returnFail(HostReplyMessage.RESULT_LEASESET_LOOKUP_FAILURE); + else + returnFail(); } } } - private void returnDest(Destination d) { + private void returnDest(Destination d, Properties p) { I2CPMessage msg; - if (_reqID >= 0) - msg = new HostReplyMessage(_sessID, d, _reqID); - else + if (_reqID >= 0) { + if (_wantsOptions) { + if (p == null) + p = EmptyProperties.INSTANCE; + msg = new HostReplyMessage(_sessID, d, _reqID, p); + } else { + msg = new HostReplyMessage(_sessID, d, _reqID); + } + } else { msg = new DestReplyMessage(d); + } try { _runner.doSend(msg); } catch (I2CPMessageException ime) {}