From e394d3d4c5852910bd97748f478d6720018b1edc Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Sun, 26 May 2013 17:25:02 +0000 Subject: [PATCH] * DatabaseLookupmessage: - Add support for requesting an encrypted reply * NetDB: - Add support for encrypted DatabaseSearchReplyMessage and DatabaseStoreMessage in response to a DatabaseLookupMessage * PRNG: Cleanups using Collections.singletonMap() * Router utils: New RemovableSingletonSet * TransientSessionKeyManager: - Support variable expiration for inbound tag sets - Several efficiency improvements * VersionComparator: Add static method, use most places --- .../router/update/ConsoleUpdateManager.java | 3 +- .../i2p/router/update/PluginUpdateRunner.java | 14 +- .../net/i2p/router/update/UpdateRunner.java | 2 +- .../i2p/router/web/ConfigServiceHandler.java | 2 +- .../src/net/i2p/router/web/LogsHelper.java | 2 +- .../src/net/i2p/router/web/PluginStarter.java | 10 +- .../crypto/prng/AsyncFortunaStandalone.java | 5 +- .../gnu/crypto/prng/FortunaStandalone.java | 5 +- .../src/net/i2p/crypto/SessionKeyManager.java | 14 +- .../crypto/TransientSessionKeyManager.java | 132 ++++++++++-------- .../src/net/i2p/crypto/TrustedUpdate.java | 2 +- core/java/src/net/i2p/util/SystemVersion.java | 2 +- .../src/net/i2p/util/VersionComparator.java | 8 +- .../i2p/data/i2np/DatabaseLookupMessage.java | 87 +++++++++++- .../HandleDatabaseLookupMessageJob.java | 26 +++- .../kademlia/FloodfillVerifyStoreJob.java | 4 + .../kademlia/IterativeSearchJob.java | 6 + .../networkdb/kademlia/MessageWrapper.java | 63 ++++++++- .../router/networkdb/kademlia/StoreJob.java | 2 +- .../router/tunnel/pool/BuildRequestor.java | 3 +- .../net/i2p/router/tunnel/pool/TestJob.java | 6 +- .../tunnel/pool/TunnelPeerSelector.java | 3 +- .../i2p/router/tunnel/pool/TunnelPool.java | 3 +- .../router/util/RemovableSingletonSet.java | 78 +++++++++++ 24 files changed, 372 insertions(+), 110 deletions(-) create mode 100644 router/java/src/net/i2p/router/util/RemovableSingletonSet.java diff --git a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java index 1b295c68ea..fbf3525fa8 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java @@ -70,7 +70,6 @@ public class ConsoleUpdateManager implements UpdateManager { /** downloaded AND installed */ private final Map<UpdateItem, Version> _installed; private static final DecimalFormat _pct = new DecimalFormat("0.0%"); - private static final VersionComparator _versionComparator = new VersionComparator(); private volatile String _status; @@ -1289,7 +1288,7 @@ public class ConsoleUpdateManager implements UpdateManager { } public int compareTo(Version r) { - return _versionComparator.compare(version, r.version); + return VersionComparator.comp(version, r.version); } @Override diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java index 4c8c394818..1f3f7fde41 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java @@ -252,7 +252,7 @@ class PluginUpdateRunner extends UpdateRunner { String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version"); if (minVersion != null && - (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) { + VersionComparator.comp(CoreVersion.VERSION, minVersion) < 0) { to.delete(); statusDone("<b>" + _("This plugin requires I2P version {0} or higher", minVersion) + "</b>"); return; @@ -260,7 +260,7 @@ class PluginUpdateRunner extends UpdateRunner { minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version"); if (minVersion != null && - (new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) { + VersionComparator.comp(System.getProperty("java.version"), minVersion) < 0) { to.delete(); statusDone("<b>" + _("This plugin requires Java version {0} or higher", minVersion) + "</b>"); return; @@ -295,21 +295,21 @@ class PluginUpdateRunner extends UpdateRunner { } String oldVersion = oldProps.getProperty("version"); if (oldVersion == null || - (new VersionComparator()).compare(oldVersion, version) >= 0) { + VersionComparator.comp(oldVersion, version) >= 0) { to.delete(); statusDone("<b>" + _("Downloaded plugin version {0} is not newer than installed plugin", version) + "</b>"); return; } minVersion = ConfigClientsHelper.stripHTML(props, "min-installed-version"); if (minVersion != null && - (new VersionComparator()).compare(minVersion, oldVersion) > 0) { + VersionComparator.comp(minVersion, oldVersion) > 0) { to.delete(); statusDone("<b>" + _("Plugin update requires installed plugin version {0} or higher", minVersion) + "</b>"); return; } String maxVersion = ConfigClientsHelper.stripHTML(props, "max-installed-version"); if (maxVersion != null && - (new VersionComparator()).compare(maxVersion, oldVersion) < 0) { + VersionComparator.comp(maxVersion, oldVersion) < 0) { to.delete(); statusDone("<b>" + _("Plugin update requires installed plugin version {0} or lower", maxVersion) + "</b>"); return; @@ -317,14 +317,14 @@ class PluginUpdateRunner extends UpdateRunner { oldVersion = LogsHelper.jettyVersion(); minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version"); if (minVersion != null && - (new VersionComparator()).compare(minVersion, oldVersion) > 0) { + VersionComparator.comp(minVersion, oldVersion) > 0) { to.delete(); statusDone("<b>" + _("Plugin requires Jetty version {0} or higher", minVersion) + "</b>"); return; } maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version"); if (maxVersion != null && - (new VersionComparator()).compare(maxVersion, oldVersion) < 0) { + VersionComparator.comp(maxVersion, oldVersion) < 0) { to.delete(); statusDone("<b>" + _("Plugin requires Jetty version {0} or lower", maxVersion) + "</b>"); return; diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java index bb952bc50b..937f235fed 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java @@ -184,7 +184,7 @@ class UpdateRunner extends I2PAppThread implements UpdateTask, EepGet.StatusList if (_isPartial) { // Compare version with what we have now String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray())); - boolean newer = (new VersionComparator()).compare(newVersion, RouterVersion.VERSION) > 0; + boolean newer = VersionComparator.comp(newVersion, RouterVersion.VERSION) > 0; if (newer) { _newVersion = newVersion; } else { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java index 5a1b6c807b..54bc9bc3c4 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java @@ -144,7 +144,7 @@ public class ConfigServiceHandler extends FormHandler { if (ctx.hasWrapper() && _wrapperListener == null && !SystemVersion.isWindows()) { String wv = System.getProperty("wrapper.version"); - if (wv != null && (new VersionComparator()).compare(wv, LISTENER_AVAILABLE) >= 0) { + if (wv != null && VersionComparator.comp(wv, LISTENER_AVAILABLE) >= 0) { try { _wrapperListener = new WrapperListener(ctx); } catch (Throwable t) {} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java index 6e92ff9d84..651ace5728 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java @@ -48,7 +48,7 @@ public class LogsHelper extends HelperBase { File f = null; if (ctx.hasWrapper()) { String wv = System.getProperty("wrapper.version"); - if (wv != null && (new VersionComparator()).compare(wv, LOCATION_AVAILABLE) >= 0) { + if (wv != null && VersionComparator.comp(wv, LOCATION_AVAILABLE) >= 0) { try { f = WrapperManager.getWrapperLogFile(); } catch (Throwable t) {} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 08b198dd21..b7018d37fa 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -73,7 +73,7 @@ public class PluginStarter implements Runnable { !NewsHelper.isUpdateInProgress()) { String prev = _context.getProperty("router.previousVersion"); if (prev != null && - (new VersionComparator()).compare(RouterVersion.VERSION, prev) > 0) { + VersionComparator.comp(RouterVersion.VERSION, prev) > 0) { updateAll(_context, true); } } @@ -236,7 +236,7 @@ public class PluginStarter implements Runnable { String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version"); if (minVersion != null && - (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) { + VersionComparator.comp(CoreVersion.VERSION, minVersion) < 0) { String foo = "Plugin " + appName + " requires I2P version " + minVersion + " or higher"; log.error(foo); disablePlugin(appName); @@ -245,7 +245,7 @@ public class PluginStarter implements Runnable { minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version"); if (minVersion != null && - (new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) { + VersionComparator.comp(System.getProperty("java.version"), minVersion) < 0) { String foo = "Plugin " + appName + " requires Java version " + minVersion + " or higher"; log.error(foo); disablePlugin(appName); @@ -255,7 +255,7 @@ public class PluginStarter implements Runnable { String jVersion = LogsHelper.jettyVersion(); minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version"); if (minVersion != null && - (new VersionComparator()).compare(minVersion, jVersion) > 0) { + VersionComparator.comp(minVersion, jVersion) > 0) { String foo = "Plugin " + appName + " requires Jetty version " + minVersion + " or higher"; log.error(foo); disablePlugin(appName); @@ -264,7 +264,7 @@ public class PluginStarter implements Runnable { String maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version"); if (maxVersion != null && - (new VersionComparator()).compare(maxVersion, jVersion) < 0) { + VersionComparator.comp(maxVersion, jVersion) < 0) { String foo = "Plugin " + appName + " requires Jetty version " + maxVersion + " or lower"; log.error(foo); disablePlugin(appName); diff --git a/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java b/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java index 1a2ec48921..ba68e3d148 100644 --- a/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java +++ b/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java @@ -1,6 +1,6 @@ package gnu.crypto.prng; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; @@ -77,8 +77,7 @@ public class AsyncFortunaStandalone extends FortunaStandalone implements Runnabl /** the seed is only propogated once the prng is started with startup() */ @Override public void seed(byte val[]) { - Map props = new HashMap(1); - props.put(SEED, val); + Map props = Collections.singletonMap(SEED, val); init(props); //fillBlock(); } diff --git a/core/java/src/gnu/crypto/prng/FortunaStandalone.java b/core/java/src/gnu/crypto/prng/FortunaStandalone.java index 927c6ef8d3..8128794daf 100644 --- a/core/java/src/gnu/crypto/prng/FortunaStandalone.java +++ b/core/java/src/gnu/crypto/prng/FortunaStandalone.java @@ -48,7 +48,7 @@ import java.io.Serializable; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.util.Arrays; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; import net.i2p.crypto.CryptixAESKeyCache; @@ -131,8 +131,7 @@ public class FortunaStandalone extends BasePRNGStandalone implements Serializabl } public void seed(byte val[]) { - Map props = new HashMap(1); - props.put(SEED, val); + Map props = Collections.singletonMap(SEED, val); init(props); fillBlock(); } diff --git a/core/java/src/net/i2p/crypto/SessionKeyManager.java b/core/java/src/net/i2p/crypto/SessionKeyManager.java index b9a00b9988..53ad0c02c3 100644 --- a/core/java/src/net/i2p/crypto/SessionKeyManager.java +++ b/core/java/src/net/i2p/crypto/SessionKeyManager.java @@ -150,11 +150,19 @@ public class SessionKeyManager { } /** - * Accept the given tags and associate them with the given key for decryption + * Accept the given tags and associate them with the given key for decryption, + * with the default expiration. + */ + public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) {} + + /** + * Accept the given tags and associate them with the given key for decryption, + * with specified expiration. * + * @param expire time from now + * @since 0.9.7 */ - public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) { // nop - } + public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags, long expire) {} /** * Determine if we have received a session key associated with the given session tag, diff --git a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java index 190004b574..dc9bdd67d4 100644 --- a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java +++ b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java @@ -89,9 +89,8 @@ public class TransientSessionKeyManager extends SessionKeyManager { private final int _lowThreshold; /** - * Let session tags sit around for this long before expiring them. We can now have such a large - * value since there is the persistent session key manager. This value is for outbound tags - - * inbound tags are managed by SESSION_LIFETIME_MAX_MS + * Let outbound session tags sit around for this long before expiring them. + * Inbound tag expiration is set by SESSION_LIFETIME_MAX_MS */ private final static long SESSION_TAG_DURATION_MS = 12 * 60 * 1000; @@ -99,6 +98,8 @@ public class TransientSessionKeyManager extends SessionKeyManager { * Keep unused inbound session tags around for this long (a few minutes longer than * session tags are used on the outbound side so that no reasonable network lag * can cause failed decrypts) + * + * This is also the max idle time for an outbound session. */ private final static long SESSION_LIFETIME_MAX_MS = SESSION_TAG_DURATION_MS + 3 * 60 * 1000; @@ -499,11 +500,22 @@ public class TransientSessionKeyManager extends SessionKeyManager { /** * Accept the given tags and associate them with the given key for decryption * + * @param sessionTags modifiable; NOT copied */ @Override public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) { - int overage = 0; - TagSet tagSet = new TagSet(sessionTags, key, _context.clock().now(), _rcvTagSetID.incrementAndGet()); + tagsReceived(key, sessionTags, SESSION_LIFETIME_MAX_MS); + } + + /** + * Accept the given tags and associate them with the given key for decryption + * + * @param sessionTags modifiable; NOT copied + */ + @Override + public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags, long expire) { + TagSet tagSet = new TagSet(sessionTags, key, _context.clock().now() + expire, + _rcvTagSetID.incrementAndGet()); if (_log.shouldLog(Log.INFO)) _log.info("Received " + tagSet); TagSet old = null; @@ -513,7 +525,6 @@ public class TransientSessionKeyManager extends SessionKeyManager { //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Receiving tag " + tag + " in tagSet: " + tagSet); old = _inboundTagSets.put(tag, tagSet); - overage = _inboundTagSets.size() - MAX_INBOUND_SESSION_TAGS; if (old != null) { if (!old.getAssociatedKey().equals(tagSet.getAssociatedKey())) { _inboundTagSets.remove(tag); @@ -546,6 +557,7 @@ public class TransientSessionKeyManager extends SessionKeyManager { } } + int overage = _inboundTagSets.size() - MAX_INBOUND_SESSION_TAGS; if (overage > 0) clearExcess(overage); @@ -573,25 +585,23 @@ public class TransientSessionKeyManager extends SessionKeyManager { _log.log(Log.CRIT, "TOO MANY SESSION TAGS! Starting cleanup, overage = " + overage); List<TagSet> removed = new ArrayList(toRemove); synchronized (_inboundTagSets) { - for (Iterator<TagSet> iter = _inboundTagSets.values().iterator(); iter.hasNext(); ) { - TagSet set = iter.next(); + for (TagSet set : _inboundTagSets.values()) { int size = set.getTags().size(); if (size > 1000) absurd++; if (size > 100) large++; - if (now - set.getDate() > SESSION_LIFETIME_MAX_MS) + if (now >= set.getDate()) old++; - else if (now - set.getDate() < 1*60*1000) + else if (set.getDate() - now > 10*60*1000) recent++; - if ((removed.size() < (toRemove)) || (now - set.getDate() > SESSION_LIFETIME_MAX_MS)) + if ((removed.size() < (toRemove)) || (now >= set.getDate())) removed.add(set); } for (int i = 0; i < removed.size(); i++) { TagSet cur = removed.get(i); - for (Iterator<SessionTag> iter = cur.getTags().iterator(); iter.hasNext(); ) { - SessionTag tag = iter.next(); + for (SessionTag tag : cur.getTags()) { _inboundTagSets.remove(tag); tags++; } @@ -616,21 +626,21 @@ public class TransientSessionKeyManager extends SessionKeyManager { */ @Override public SessionKey consumeTag(SessionTag tag) { - //if (false) aggressiveExpire(); + TagSet tagSet; synchronized (_inboundTagSets) { - TagSet tagSet = _inboundTagSets.remove(tag); + tagSet = _inboundTagSets.remove(tag); if (tagSet == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Cannot consume IB " + tag + " as it is not known"); return null; } tagSet.consume(tag); - - SessionKey key = tagSet.getAssociatedKey(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("IB Tag consumed: " + tag + " from: " + tagSet); - return key; } + + SessionKey key = tagSet.getAssociatedKey(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("IB Tag consumed: " + tag + " from: " + tagSet); + return key; } private OutboundSession getSession(PublicKey target) { @@ -660,44 +670,28 @@ public class TransientSessionKeyManager extends SessionKeyManager { /** * Aggressively expire inbound tag sets and outbound sessions * - * @return number of tag sets expired + * @return number of tag sets expired (bogus as it overcounts inbound) */ private int aggressiveExpire() { int removed = 0; int remaining = 0; long now = _context.clock().now(); - StringBuilder buf = null; - //StringBuilder bufSummary = null; - if (_log.shouldLog(Log.DEBUG)) { - buf = new StringBuilder(128); - buf.append("Expiring inbound: "); - //bufSummary = new StringBuilder(1024); - } + synchronized (_inboundTagSets) { - for (Iterator<SessionTag> iter = _inboundTagSets.keySet().iterator(); iter.hasNext();) { - SessionTag tag = iter.next(); - TagSet ts = _inboundTagSets.get(tag); - long age = now - ts.getDate(); - if (age > SESSION_LIFETIME_MAX_MS) { - //if (ts.getDate() < now - SESSION_LIFETIME_MAX_MS) { + for (Iterator<TagSet> iter = _inboundTagSets.values().iterator(); iter.hasNext();) { + TagSet ts = iter.next(); + // for inbound tagsets, getDate() is the expire time + if (ts.getDate() <= now) { iter.remove(); + // bug, this counts inbound tags, not tag sets removed++; - if (buf != null) - buf.append(tag).append(" @ age ").append(DataHelper.formatDuration(age)); - //} else if (false && (bufSummary != null) ) { - // bufSummary.append("\nTagSet: " + ts + ", key: " + ts.getAssociatedKey() - // + ": tag: " + tag); } } remaining = _inboundTagSets.size(); } _context.statManager().addRateData("crypto.sessionTagsRemaining", remaining, 0); - if ( (buf != null) && (removed > 0) ) - _log.debug(buf.toString()); - //if (bufSummary != null) - // _log.debug("Cleaning up with remaining: " + bufSummary.toString()); - - //_log.warn("Expiring tags: [" + tagsToDrop + "]"); + if (removed > 0 && _log.shouldLog(Log.DEBUG)) + _log.debug("Expired inbound: " + removed); synchronized (_outboundSessions) { for (Iterator<OutboundSession> iter = _outboundSessions.values().iterator(); iter.hasNext();) { @@ -722,10 +716,12 @@ public class TransientSessionKeyManager extends SessionKeyManager { Set<TagSet> inbound = getInboundTagSets(); Map<SessionKey, Set<TagSet>> inboundSets = new HashMap(inbound.size()); // Build a map of the inbound tag sets, grouped by SessionKey - for (Iterator<TagSet> iter = inbound.iterator(); iter.hasNext();) { - TagSet ts = iter.next(); - if (!inboundSets.containsKey(ts.getAssociatedKey())) inboundSets.put(ts.getAssociatedKey(), new HashSet()); + for (TagSet ts : inbound) { Set<TagSet> sets = inboundSets.get(ts.getAssociatedKey()); + if (sets == null) { + sets = new HashSet(); + inboundSets.put(ts.getAssociatedKey(), sets); + } sets.add(ts); } int total = 0; @@ -741,8 +737,12 @@ public class TransientSessionKeyManager extends SessionKeyManager { TagSet ts = siter.next(); int size = ts.getTags().size(); total += size; - buf.append("<li><b>ID: ").append(ts.getID()) - .append(" Received:</b> ").append(DataHelper.formatDuration2(now - ts.getDate())).append(" ago with "); + buf.append("<li><b>ID: ").append(ts.getID()); + long expires = ts.getDate() - now; + if (expires > 0) + buf.append(" Expires in:</b> ").append(DataHelper.formatDuration2(expires)).append(" with "); + else + buf.append(" Expired:</b> ").append(DataHelper.formatDuration2(0 - expires)).append(" ago with "); buf.append(size).append('/').append(ts.getOriginalSize()).append(" tags remaining</li>"); } buf.append("</ul></td></tr>\n"); @@ -802,7 +802,10 @@ public class TransientSessionKeyManager extends SessionKeyManager { */ private static class TagSetComparator implements Comparator<TagSet> { public int compare(TagSet l, TagSet r) { - return (int) (l.getDate() - r.getDate()); + int rv = (int) (l.getDate() - r.getDate()); + if (rv != 0) + return rv; + return l.hashCode() - r.hashCode(); } } @@ -1088,6 +1091,9 @@ public class TransientSessionKeyManager extends SessionKeyManager { /** did we get an ack for this tagset? Only for outbound tagsets */ private boolean _acked; + /** + * @param date For inbound: when the TagSet will expire; for outbound: creation time + */ public TagSet(Set<SessionTag> tags, SessionKey key, long date, int id) { if (key == null) throw new IllegalArgumentException("Missing key"); if (tags == null) throw new IllegalArgumentException("Missing tags"); @@ -1104,7 +1110,9 @@ public class TransientSessionKeyManager extends SessionKeyManager { //} } - /** when the tag set was created */ + /** + * For inbound: when the TagSet will expire; for outbound: creation time + */ public long getDate() { return _date; } @@ -1135,23 +1143,29 @@ public class TransientSessionKeyManager extends SessionKeyManager { } /** - * Let's do this without counting the elements first. + * For outbound only. * Caller must synch. + * @return a tag or null */ public SessionTag consumeNext() { - SessionTag first; - try { - first = _sessionTags.iterator().next(); - } catch (NoSuchElementException nsee) { + Iterator<SessionTag> iter = _sessionTags.iterator(); + if (!iter.hasNext()) return null; - } - _sessionTags.remove(first); + SessionTag first = iter.next(); + iter.remove(); return first; } //public Exception getCreatedBy() { return _createdBy; } + /** + * For outbound only. + */ public void setAcked() { _acked = true; } + + /** + * For outbound only. + */ public boolean getAcked() { return _acked; } /****** this will return a dup if two in the same ms, so just use java diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java index 90b6b301ca..8ad2d14ed2 100644 --- a/core/java/src/net/i2p/crypto/TrustedUpdate.java +++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java @@ -302,7 +302,7 @@ riCe6OlAEiNpcc6mMyIYYWFICbrDFTrDR3wXqwc/Jkcx6L5VVWoagpSzbo3yGhc= * version, otherwise <code>false</code>. */ public static final boolean needsUpdate(String currentVersion, String newVersion) { - return (new VersionComparator()).compare(currentVersion, newVersion) < 0; + return VersionComparator.comp(currentVersion, newVersion) < 0; } /** @return success */ diff --git a/core/java/src/net/i2p/util/SystemVersion.java b/core/java/src/net/i2p/util/SystemVersion.java index 57ee79d75b..4761731a85 100644 --- a/core/java/src/net/i2p/util/SystemVersion.java +++ b/core/java/src/net/i2p/util/SystemVersion.java @@ -56,7 +56,7 @@ public abstract class SystemVersion { if (_isAndroid) { _oneDotSix = _androidSDK >= 9; } else { - _oneDotSix = (new VersionComparator()).compare(System.getProperty("java.version"), "1.6") >= 0; + _oneDotSix = VersionComparator.comp(System.getProperty("java.version"), "1.6") >= 0; } } diff --git a/core/java/src/net/i2p/util/VersionComparator.java b/core/java/src/net/i2p/util/VersionComparator.java index f65cf33d95..6ae87247ba 100644 --- a/core/java/src/net/i2p/util/VersionComparator.java +++ b/core/java/src/net/i2p/util/VersionComparator.java @@ -11,9 +11,15 @@ import java.util.Comparator; */ public class VersionComparator implements Comparator<String> { - @Override public int compare(String l, String r) { + return comp(l, r); + } + /** + * To avoid churning comparators + * @since 0.9.7 + */ + public static int comp(String l, String r) { if (l.equals(r)) return 0; diff --git a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java index d889fce74d..d8ac613633 100644 --- a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java +++ b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java @@ -17,8 +17,12 @@ import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.data.Hash; +import net.i2p.data.RouterInfo; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; import net.i2p.data.TunnelId; //import net.i2p.util.Log; +import net.i2p.util.VersionComparator; /** * Defines the message a router sends to another router to search for a @@ -34,6 +38,8 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { private TunnelId _replyTunnel; /** this must be kept as a list to preserve the order and not break the checksum */ private List<Hash> _dontIncludePeers; + private SessionKey _replyKey; + private SessionTag _replyTag; //private static volatile long _currentLookupPeriod = 0; //private static volatile int _currentLookupCount = 0; @@ -45,6 +51,11 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { Have to prevent a huge alloc on rcv of a malicious msg though */ private static final int MAX_NUM_PEERS = 512; + private static final byte FLAG_TUNNEL = 0x01; + private static final byte FLAG_ENCRYPT = 0x02; + + private static final String MIN_ENCRYPTION_VERSION = "0.9.8"; + public DatabaseLookupMessage(I2PAppContext context) { this(context, false); } @@ -144,6 +155,49 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { _replyTunnel = replyTunnel; } + /** + * Does this router support encrypted replies? + * + * @param to null OK + * @since 0.9.7 + */ + public static boolean supportsEncryptedReplies(RouterInfo to) { + if (to == null) + return false; + String v = to.getOption("router.version"); + return v != null && + VersionComparator.comp(v, MIN_ENCRYPTION_VERSION) >= 0; + } + + /** + * The included session key or null if unset + * + * @since 0.9.7 + */ + public SessionKey getReplyKey() { return _replyKey; } + + /** + * The included session tag or null if unset + * + * @since 0.9.7 + */ + public SessionTag getReplyTag() { return _replyTag; } + + /** + * Only worthwhile if sending reply via tunnel + * + * @throws IllegalStateException if key or tag previously set, to protect saved checksum + * @param encryptKey non-null + * @param encryptTag non-null + * @since 0.9.7 + */ + public void setReplySession(SessionKey encryptKey, SessionTag encryptTag) { + if (_replyKey != null || _replyTag != null) + throw new IllegalStateException(); + _replyKey = encryptKey; + _replyTag = encryptTag; + } + /** * Set of peers that a lookup reply should NOT include. * WARNING - returns a copy. @@ -224,7 +278,8 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { // as of 0.9.6, ignore other 7 bits of the flag byte // TODO store the whole flag byte - boolean tunnelSpecified = (data[curIndex] & 0x01) != 0; + boolean tunnelSpecified = (data[curIndex] & FLAG_TUNNEL) != 0; + boolean replyKeySpecified = (data[curIndex] & FLAG_ENCRYPT) != 0; curIndex++; if (tunnelSpecified) { @@ -246,6 +301,15 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { peers.add(p); } _dontIncludePeers = peers; + if (replyKeySpecified) { + byte[] rk = new byte[SessionKey.KEYSIZE_BYTES]; + System.arraycopy(data, curIndex, rk, 0, SessionKey.KEYSIZE_BYTES); + _replyKey = new SessionKey(rk); + curIndex += SessionKey.KEYSIZE_BYTES; + byte[] rt = new byte[SessionTag.BYTE_LENGTH]; + System.arraycopy(data, curIndex, rt, 0, SessionTag.BYTE_LENGTH); + _replyTag = new SessionTag(rt); + } } @@ -258,6 +322,8 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { totalLength += 2; // numPeers if (_dontIncludePeers != null) totalLength += Hash.HASH_LENGTH * _dontIncludePeers.size(); + if (_replyKey != null) + totalLength += SessionKey.KEYSIZE_BYTES + SessionTag.BYTE_LENGTH; return totalLength; } @@ -269,12 +335,17 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { curIndex += Hash.HASH_LENGTH; System.arraycopy(_fromHash.getData(), 0, out, curIndex, Hash.HASH_LENGTH); curIndex += Hash.HASH_LENGTH; - // TODO allow specification of the other 7 bits of the flag byte + // Generate the flag byte if (_replyTunnel != null) { - out[curIndex++] = 0x01; + byte flag = FLAG_TUNNEL; + if (_replyKey != null) + flag |= FLAG_ENCRYPT; + out[curIndex++] = flag; byte id[] = DataHelper.toLong(4, _replyTunnel.getTunnelId()); System.arraycopy(id, 0, out, curIndex, 4); curIndex += 4; + } else if (_replyKey != null) { + out[curIndex++] = FLAG_ENCRYPT; } else { out[curIndex++] = 0x00; } @@ -293,6 +364,12 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { curIndex += Hash.HASH_LENGTH; } } + if (_replyKey != null) { + System.arraycopy(_replyKey.getData(), 0, out, curIndex, SessionKey.KEYSIZE_BYTES); + curIndex += SessionKey.KEYSIZE_BYTES; + System.arraycopy(_replyTag.getData(), 0, out, curIndex, SessionTag.BYTE_LENGTH); + curIndex += SessionTag.BYTE_LENGTH; + } return curIndex; } @@ -326,6 +403,10 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { buf.append("\n\tSearch Key: ").append(_key); buf.append("\n\tFrom: ").append(_fromHash); buf.append("\n\tReply Tunnel: ").append(_replyTunnel); + if (_replyKey != null) + buf.append("\n\tReply Key: ").append(_replyKey); + if (_replyTag != null) + buf.append("\n\tReply Tag: ").append(_replyTag); buf.append("\n\tDont Include Peers: "); if (_dontIncludePeers != null) buf.append(_dontIncludePeers.size()); diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java index 37d5f0fc36..22bdb88890 100644 --- a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java @@ -8,6 +8,7 @@ package net.i2p.router.networkdb; * */ +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -16,6 +17,7 @@ import net.i2p.data.Hash; import net.i2p.data.LeaseSet; import net.i2p.data.RouterIdentity; import net.i2p.data.RouterInfo; +import net.i2p.data.SessionKey; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; @@ -27,6 +29,7 @@ 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; @@ -36,8 +39,9 @@ import net.i2p.util.Log; * Unused directly - see kademlia/ for extension */ public class HandleDatabaseLookupMessageJob extends JobImpl { - private Log _log; - private DatabaseLookupMessage _message; + private final Log _log; + private final DatabaseLookupMessage _message; + 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; @@ -149,8 +153,7 @@ public class HandleDatabaseLookupMessageJob extends JobImpl { if ( (info.getIdentity().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 = new HashSet<Hash>(1); - us.add(getContext().routerHash()); + Set<Hash> us = Collections.singleton(getContext().routerHash()); sendClosest(_message.getSearchKey(), us, fromKey, _message.getReplyTunnel()); //} else if (info.isHidden()) { // // Don't return hidden nodes @@ -203,9 +206,11 @@ public class HandleDatabaseLookupMessageJob extends JobImpl { */ private Set<Hash> getNearestRouters() { Set<Hash> dontInclude = _message.getDontIncludePeers(); + Hash us = getContext().routerHash(); if (dontInclude == null) - dontInclude = new HashSet(1); - dontInclude.add(getContext().routerHash()); + dontInclude = Collections.singleton(us); + else + dontInclude.add(us); // Honor flag to exclude all floodfills //if (dontInclude.contains(Hash.FAKE_HASH)) { // This is handled in FloodfillPeerSelector @@ -289,6 +294,15 @@ public class HandleDatabaseLookupMessageJob extends JobImpl { getContext().tunnelDispatcher().dispatch(m); } else { // if we aren't the gateway, forward it on + SessionKey replyKey = _message.getReplyKey(); + if (replyKey != null) { + // encrypt the reply + message = MessageWrapper.wrap(getContext(), message, replyKey, _message.getReplyTag()); + if (message == null) { + _log.error("Encryption error"); + return; + } + } TunnelGatewayMessage m = new TunnelGatewayMessage(getContext()); m.setMessage(message); m.setMessageExpiration(message.getMessageExpiration()); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java index 14aeaac13d..79e1d13d01 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java @@ -111,6 +111,10 @@ class FloodfillVerifyStoreJob extends JobImpl { _facade.verifyFinished(_key); return; } + if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) { + MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(getContext()); + lookup.setReplySession(sess.key, sess.tag); + } Hash fromKey; if (_isRouterInfo) fromKey = null; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java index 25d57f6f83..c2d7bfb5cb 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java @@ -24,6 +24,7 @@ import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; import net.i2p.router.util.RandomIterator; import net.i2p.util.Log; +import net.i2p.util.VersionComparator; /** * A traditional Kademlia search that continues to search @@ -233,6 +234,11 @@ class IterativeSearchJob extends FloodSearchJob { // if we have the ff RI, garlic encrypt it RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(peer); if (ri != null) { + // request encrypted reply + if (DatabaseLookupMessage.supportsEncryptedReplies(ri)) { + MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(getContext()); + dlm.setReplySession(sess.key, sess.tag); + } outMsg = MessageWrapper.wrap(getContext(), dlm, ri); if (_log.shouldLog(Log.DEBUG)) _log.debug(getJobId() + ": Encrypted DLM for " + _key + " to " + peer); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java index 4359b840de..44a1b61ad7 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java @@ -17,14 +17,16 @@ import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.RouterContext; import net.i2p.router.message.GarlicMessageBuilder; import net.i2p.router.message.PayloadGarlicConfig; +import net.i2p.router.util.RemovableSingletonSet; /** * Method and class for garlic encrypting outbound netdb traffic, - * including management of the ElGamal/AES tags + * and sending keys and tags for others to encrypt inbound netdb traffic, + * including management of the ElGamal/AES tags. * * @since 0.7.10 */ -class MessageWrapper { +public class MessageWrapper { //private static final Log _log = RouterContext.getGlobalContext().logManager().getLog(MessageWrapper.class); @@ -142,4 +144,61 @@ class MessageWrapper { key, sentKey, null); return msg; } + + /** + * A single key and tag, for receiving a single message. + * + * @since 0.9.7 + */ + public static class OneTimeSession { + public final SessionKey key; + public final SessionTag tag; + + public OneTimeSession(SessionKey key, SessionTag tag) { + this.key = key; this.tag = tag; + } + } + + /** + * Create a single key and tag, for receiving a single encrypted message, + * and register it with the router's session key manager, to expire in two minutes. + * The recipient can then send us an AES-encrypted message, + * avoiding ElGamal. + * + * @since 0.9.7 + */ + public static OneTimeSession generateSession(RouterContext ctx) { + SessionKey key = ctx.keyGenerator().generateSessionKey(); + SessionTag tag = new SessionTag(true); + Set<SessionTag> tags = new RemovableSingletonSet(tag); + ctx.sessionKeyManager().tagsReceived(key, tags, 2*60*1000); + return new OneTimeSession(key, tag); + } + + /** + * Garlic wrap a message from nobody, destined for an unknown router, + * to hide the contents from the IBGW. + * Uses a supplied session key and session tag for AES encryption, + * avoiding ElGamal. + * + * @param encryptKey non-null + * @param encryptTag non-null + * @return null on encrypt failure + * @since 0.9.7 + */ + public static GarlicMessage wrap(RouterContext ctx, I2NPMessage m, SessionKey encryptKey, SessionTag encryptTag) { + DeliveryInstructions instructions = new DeliveryInstructions(); + instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_LOCAL); + + PayloadGarlicConfig payload = new PayloadGarlicConfig(); + payload.setCertificate(Certificate.NULL_CERT); + payload.setId(ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE)); + payload.setPayload(m); + payload.setDeliveryInstructions(instructions); + payload.setExpiration(m.getMessageExpiration()); + + GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, null, null, + null, encryptKey, encryptTag); + return msg; + } } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java index d6e0f1ae7f..3c47769f4e 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java @@ -489,7 +489,7 @@ class StoreJob extends JobImpl { String v = ri.getOption("router.version"); if (v == null) return false; - return (new VersionComparator()).compare(v, MIN_ENCRYPTION_VERSION) >= 0; + return VersionComparator.comp(v, MIN_ENCRYPTION_VERSION) >= 0; } /** diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java index 80bd1803af..1c71e7bb71 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java @@ -202,7 +202,6 @@ abstract class BuildRequestor { private static final boolean SEND_VARIABLE = true; /** 5 (~2600 bytes) fits nicely in 3 tunnel messages */ private static final int SHORT_RECORDS = 5; - private static final VersionComparator _versionComparator = new VersionComparator(); private static final List<Integer> SHORT_ORDER = new ArrayList(SHORT_RECORDS); static { for (int i = 0; i < SHORT_RECORDS; i++) @@ -217,7 +216,7 @@ abstract class BuildRequestor { String v = ri.getOption("router.version"); if (v == null) return false; - return _versionComparator.compare(v, MIN_VARIABLE_VERSION) >= 0; + return VersionComparator.comp(v, MIN_VARIABLE_VERSION) >= 0; } /** diff --git a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java index 93b43a1fab..f5f6b8b425 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java @@ -1,6 +1,5 @@ package net.i2p.router.tunnel.pool; -import java.util.HashSet; import java.util.Set; import net.i2p.crypto.SessionKeyManager; @@ -19,6 +18,7 @@ import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; import net.i2p.router.message.GarlicMessageBuilder; import net.i2p.router.message.PayloadGarlicConfig; +import net.i2p.router.util.RemovableSingletonSet; import net.i2p.stat.Rate; import net.i2p.stat.RateStat; import net.i2p.util.Log; @@ -140,9 +140,7 @@ class TestJob extends JobImpl { scheduleRetest(); return; } - // can't be a singleton, the SKM modifies it - Set encryptTags = new HashSet(1); - encryptTags.add(encryptTag); + Set<SessionTag> encryptTags = new RemovableSingletonSet(encryptTag); // Register the single tag with the appropriate SKM if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) { SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination()); diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java index f125cf42e8..dd449949b7 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java @@ -345,7 +345,6 @@ public abstract class TunnelPeerSelector { /** 0.7.8 and earlier had major message corruption bugs */ private static final String MIN_VERSION = "0.7.9"; - private static final VersionComparator _versionComparator = new VersionComparator(); private static boolean shouldExclude(RouterContext ctx, Log log, RouterInfo peer, char excl[]) { String cap = peer.getCapabilities(); @@ -371,7 +370,7 @@ public abstract class TunnelPeerSelector { // minimum version check String v = peer.getOption("router.version"); - if (v == null || _versionComparator.compare(v, MIN_VERSION) < 0) + if (v == null || VersionComparator.comp(v, MIN_VERSION) < 0) return true; // uptime is always spoofed to 90m, so just remove all this diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java index 924486e6a2..9e73535e45 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java @@ -1114,8 +1114,7 @@ public class TunnelPool { return null; } } else { - peers = new ArrayList(1); - peers.add(_context.routerHash()); + peers = Collections.singletonList(_context.routerHash()); } PooledTunnelCreatorConfig cfg = new PooledTunnelCreatorConfig(_context, peers.size(), settings.isInbound(), settings.getDestination()); diff --git a/router/java/src/net/i2p/router/util/RemovableSingletonSet.java b/router/java/src/net/i2p/router/util/RemovableSingletonSet.java new file mode 100644 index 0000000000..57aa1e9795 --- /dev/null +++ b/router/java/src/net/i2p/router/util/RemovableSingletonSet.java @@ -0,0 +1,78 @@ +package net.i2p.router.util; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * Like Collections.singleton() but item is removable, + * clear() is supported, and the iterator supports remove(). + * Item may not be null. add() and addAll() unsupported. + * Unsynchronized. + * + * @since 0.9.7 + */ +public class RemovableSingletonSet<E> extends AbstractSet<E> { + private E _elem; + + public RemovableSingletonSet(E element) { + if (element == null) + throw new NullPointerException(); + _elem = element; + } + + @Override + public void clear() { + _elem = null; + } + + @Override + public boolean contains(Object o) { + return o != null && o.equals(_elem); + } + + @Override + public boolean isEmpty() { + return _elem == null; + } + + @Override + public boolean remove(Object o) { + boolean rv = o.equals(_elem); + if (rv) + _elem = null; + return rv; + } + + public int size() { + return _elem != null ? 1 : 0; + } + + public Iterator<E> iterator() { + return new RSSIterator(); + } + + private class RSSIterator implements Iterator<E> { + boolean done; + + public boolean hasNext() { + return _elem != null && !done; + } + + public E next() { + if (!hasNext()) + throw new NoSuchElementException(); + done = true; + return _elem; + } + + public void remove() { + if (_elem == null || !done) + throw new IllegalStateException(); + _elem = null; + } + } +} + -- GitLab