forked from I2P_Developers/i2p.i2p
* 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
This commit is contained in:
@ -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());
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user