+ The router keyring is used to decrypt encrypted leaseSets.
+ The keyring may contain keys for local or remote encrypted destinations.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/routerconsole/jsp/confignav.jsp b/apps/routerconsole/jsp/confignav.jsp
index b6a5ce6df..851ab79b5 100644
--- a/apps/routerconsole/jsp/confignav.jsp
+++ b/apps/routerconsole/jsp/confignav.jsp
@@ -10,6 +10,8 @@
%>Clients | <% } else { %>Clients | <% }
if (request.getRequestURI().indexOf("configpeer.jsp") != -1) {
%>Peers | <% } else { %>Peers | <% }
+ if (request.getRequestURI().indexOf("configkeyring.jsp") != -1) {
+ %>Keyring | <% } else { %>Keyring | <% }
if (request.getRequestURI().indexOf("configlogging.jsp") != -1) {
%>Logging | <% } else { %>Logging | <% }
if (request.getRequestURI().indexOf("configstats.jsp") != -1) {
diff --git a/apps/routerconsole/jsp/help.jsp b/apps/routerconsole/jsp/help.jsp
index 8580f6e65..d53f93a88 100644
--- a/apps/routerconsole/jsp/help.jsp
+++ b/apps/routerconsole/jsp/help.jsp
@@ -34,9 +34,8 @@ licenses and dependencies. This webpage is being served as part of the I2P rout
client application, which is built off a trimmed down Jetty
instance (trimmed down, as in, we do not include the demo apps or other add-ons, and we simplify configuration),
allowing you to deploy standard JSP/Servlet web applications into your router. Jetty in turn makes use of
-Apache's javax.servlet (javax.servlet.jar) implementation, as well as their xerces-j XML parser (xerces.jar).
-Their XML parser requires the Sun XML APIs (JAXP) which is included in binary form (xml-apis.jar) as required
-by their binary code license. This product includes software developed by the Apache Software Foundation
+Apache's javax.servlet (javax.servlet.jar) implementation.
+This product includes software developed by the Apache Software Foundation
(http://www.apache.org/).
Another application you can see on this webpage is I2PTunnel
diff --git a/apps/routerconsole/jsp/netdb.jsp b/apps/routerconsole/jsp/netdb.jsp
index 89c2bdec2..08a1377d3 100644
--- a/apps/routerconsole/jsp/netdb.jsp
+++ b/apps/routerconsole/jsp/netdb.jsp
@@ -14,6 +14,7 @@
" />
+ " />
" />
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
index 184e4a545..431540d46 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
@@ -45,6 +45,7 @@ public class Connection {
private long _congestionWindowEnd;
private long _highestAckedThrough;
private boolean _isInbound;
+ private boolean _updatedShareOpts;
/** Packet ID (Long) to PacketLocal for sent but unacked packets */
private Map _outboundPackets;
private PacketQueue _outboundQueue;
@@ -120,6 +121,7 @@ public class Connection {
_activeResends = 0;
_resetSentOn = -1;
_isInbound = false;
+ _updatedShareOpts = false;
_connectionEvent = new ConEvent();
_hardDisconnected = false;
_context.statManager().createRateStat("stream.con.windowSizeAtCongestion", "How large was our send window when we send a dup?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
@@ -586,6 +588,8 @@ public class Connection {
if (_remotePeerSet) throw new RuntimeException("Remote peer already set [" + _remotePeer + ", " + peer + "]");
_remotePeerSet = true;
_remotePeer = peer;
+ // now that we know who the other end is, get the rtt etc. from the cache
+ _connectionManager.updateOptsFromShare(this);
}
private boolean _sendStreamIdSet = false;
@@ -709,7 +713,13 @@ public class Connection {
}
public long getCloseReceivedOn() { return _closeReceivedOn; }
public void setCloseReceivedOn(long when) { _closeReceivedOn = when; }
-
+
+ public void updateShareOpts() {
+ if (_closeSentOn > 0 && !_updatedShareOpts) {
+ _connectionManager.updateShareOpts(this);
+ _updatedShareOpts = true;
+ }
+ }
public void incrementUnackedPacketsReceived() { _unackedPacketsReceived++; }
public int getUnackedPacketsReceived() { return _unackedPacketsReceived; }
/** how many packets have we sent but not yet received an ACK for?
@@ -998,7 +1008,7 @@ public class Connection {
/**
* Coordinate the resends of a given packet
*/
- private class ResendPacketEvent implements SimpleTimer.TimedEvent {
+ public class ResendPacketEvent implements SimpleTimer.TimedEvent {
private PacketLocal _packet;
private long _nextSendTime;
public ResendPacketEvent(PacketLocal packet, long sendTime) {
@@ -1104,26 +1114,6 @@ public class Connection {
_context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
}
- if (numSends - 1 <= _options.getMaxResends()) {
- if (_log.shouldLog(Log.INFO))
- _log.info("Resend packet " + _packet + " time " + numSends +
- " activeResends: " + _activeResends +
- " (wsize "
- + newWindowSize + " lifetime "
- + (_context.clock().now() - _packet.getCreatedOn()) + "ms)");
- _outboundQueue.enqueue(_packet);
- _lastSendTime = _context.clock().now();
- }
-
- // acked during resending (... or somethin')
- if ( (_packet.getAckTime() > 0) && (_packet.getNumSends() > 1) ) {
- _activeResends--;
- synchronized (_outboundPackets) {
- _outboundPackets.notifyAll();
- }
- return true;
- }
-
if (numSends - 1 > _options.getMaxResends()) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Too many resends");
@@ -1137,11 +1127,32 @@ public class Connection {
long timeout = rto << (numSends-1);
if ( (timeout > MAX_RESEND_DELAY) || (timeout <= 0) )
timeout = MAX_RESEND_DELAY;
+ // set this before enqueue() as it passes it on to the router
+ _nextSendTime = timeout + _context.clock().now();
+
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Resend packet " + _packet + " time " + numSends +
+ " activeResends: " + _activeResends +
+ " (wsize "
+ + newWindowSize + " lifetime "
+ + (_context.clock().now() - _packet.getCreatedOn()) + "ms)");
+ _outboundQueue.enqueue(_packet);
+ _lastSendTime = _context.clock().now();
+
if (_log.shouldLog(Log.DEBUG))
_log.debug("Scheduling resend in " + timeout + "ms for " + _packet);
RetransmissionTimer.getInstance().addEvent(ResendPacketEvent.this, timeout);
- _nextSendTime = timeout + _context.clock().now();
}
+
+ // acked during resending (... or somethin')
+ if ( (_packet.getAckTime() > 0) && (_packet.getNumSends() > 1) ) {
+ _activeResends--;
+ synchronized (_outboundPackets) {
+ _outboundPackets.notifyAll();
+ }
+ return true;
+ }
+
return true;
} else {
//if (_log.shouldLog(Log.DEBUG))
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java
index da2b1ab12..7826ba2a8 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java
@@ -30,6 +30,7 @@ public class ConnectionManager {
private PacketQueue _outboundQueue;
private SchedulerChooser _schedulerChooser;
private ConnectionPacketHandler _conPacketHandler;
+ private TCBShare _tcbShare;
/** Inbound stream ID (Long) to Connection map */
private Map _connectionByInboundId;
/** Ping ID (Long) to PingRequest */
@@ -52,6 +53,7 @@ public class ConnectionManager {
_connectionHandler = new ConnectionHandler(context, this);
_schedulerChooser = new SchedulerChooser(context);
_conPacketHandler = new ConnectionPacketHandler(context);
+ _tcbShare = new TCBShare(context);
_session = session;
session.setSessionListener(_messageHandler);
_outboundQueue = new PacketQueue(context, session, this);
@@ -127,6 +129,7 @@ public class ConnectionManager {
*/
public Connection receiveConnection(Packet synPacket) {
Connection con = new Connection(_context, this, _schedulerChooser, _outboundQueue, _conPacketHandler, new ConnectionOptions(_defaultOptions));
+ _tcbShare.updateOptsFromShare(con);
con.setInbound();
long receiveId = _context.random().nextLong(Packet.MAX_STREAM_ID-1)+1;
boolean reject = false;
@@ -277,6 +280,8 @@ public class ConnectionManager {
public ConnectionHandler getConnectionHandler() { return _connectionHandler; }
public I2PSession getSession() { return _session; }
public PacketQueue getPacketQueue() { return _outboundQueue; }
+ public void updateOptsFromShare(Connection con) { _tcbShare.updateOptsFromShare(con); }
+ public void updateShareOpts(Connection con) { _tcbShare.updateShareOpts(con); }
/**
* Something b0rked hard, so kill all of our connections without mercy.
@@ -292,6 +297,7 @@ public class ConnectionManager {
_connectionByInboundId.clear();
_connectionLock.notifyAll();
}
+ _tcbShare.stop();
}
/**
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
index 6a062d4a6..7c445f038 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
@@ -213,6 +213,10 @@ public class ConnectionPacketHandler {
packet.releasePayload();
}
+ // update the TCB Cache now that we've processed the acks and updated our rtt etc.
+ if (isNew && packet.isFlagSet(Packet.FLAG_CLOSE) && packet.isFlagSet(Packet.FLAG_SIGNATURE_INCLUDED))
+ con.updateShareOpts();
+
//if (choke)
// con.fastRetransmit();
}
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
index 2d22226d3..a56e7753d 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
@@ -82,7 +82,16 @@ class PacketQueue {
// this should not block!
begin = _context.clock().now();
- sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent);
+ long expires = 0;
+ Connection.ResendPacketEvent rpe = (Connection.ResendPacketEvent) packet.getResendEvent();
+ if (rpe != null)
+ // we want the router to expire it a little before we do,
+ // so if we retransmit it will use a new tunnel/lease combo
+ expires = rpe.getNextSendTime() - 500;
+ if (expires > 0)
+ sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires);
+ else
+ sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent);
end = _context.clock().now();
if ( (end-begin > 1000) && (_log.shouldLog(Log.WARN)) )
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java b/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java
new file mode 100644
index 000000000..1562f948e
--- /dev/null
+++ b/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java
@@ -0,0 +1,137 @@
+package net.i2p.client.streaming;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Destination;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleTimer;
+
+/**
+ * Share important TCP Control Block parameters across Connections
+ * to the same remote peer.
+ * This is intended for "temporal" sharing at connection open/close time,
+ * not "ensemble" sharing during a connection. Ref. RFC 2140.
+ *
+ * There is a TCB share per ConnectionManager (i.e. per local Destination)
+ * so that there is no information leakage to other Destinations on the
+ * same router.
+ *
+ */
+public class TCBShare {
+ private I2PAppContext _context;
+ private Log _log;
+ private Map _cache;
+ private CleanEvent _cleaner;
+
+ private static final long EXPIRE_TIME = 30*60*1000;
+ private static final long CLEAN_TIME = 10*60*1000;
+ private static final double RTT_DAMPENING = 0.75;
+ private static final double WDW_DAMPENING = 0.75;
+ private static final int MAX_RTT = ((int) Connection.MAX_RESEND_DELAY) / 2;
+ private static final int MAX_WINDOW_SIZE = Connection.MAX_WINDOW_SIZE / 4;
+
+ public TCBShare(I2PAppContext ctx) {
+ _context = ctx;
+ _log = ctx.logManager().getLog(TCBShare.class);
+ _cache = new ConcurrentHashMap(4);
+ _cleaner = new CleanEvent();
+ SimpleTimer.getInstance().addEvent(_cleaner, CLEAN_TIME);
+ }
+
+ public void stop() {
+ SimpleTimer.getInstance().removeEvent(_cleaner);
+ }
+
+ public void updateOptsFromShare(Connection con) {
+ Destination dest = con.getRemotePeer();
+ if (dest == null)
+ return;
+ ConnectionOptions opts = con.getOptions();
+ if (opts == null)
+ return;
+ Entry e = _cache.get(dest);
+ if (e == null || e.isExpired())
+ return;
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("From cache: " +
+ con.getSession().getMyDestination().calculateHash().toBase64().substring(0, 4) +
+ '-' +
+ dest.calculateHash().toBase64().substring(0, 4) +
+ " RTT: " + e.getRTT() + " wdw: " + e.getWindowSize());
+ opts.setRTT(e.getRTT());
+ opts.setWindowSize(e.getWindowSize());
+ }
+
+ public void updateShareOpts(Connection con) {
+ Destination dest = con.getRemotePeer();
+ if (dest == null)
+ return;
+ if (con.getAckedPackets() <= 0)
+ return;
+ ConnectionOptions opts = con.getOptions();
+ if (opts == null)
+ return;
+ int old = -1;
+ int oldw = -1;
+ Entry e = _cache.get(dest);
+ if (e == null || e.isExpired()) {
+ e = new Entry(opts.getRTT(), opts.getWindowSize());
+ _cache.put(dest, e);
+ } else {
+ old = e.getRTT();
+ oldw = e.getWindowSize();
+ e.setRTT(opts.getRTT());
+ e.setWindowSize(opts.getWindowSize());
+ }
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("To cache: " +
+ con.getSession().getMyDestination().calculateHash().toBase64().substring(0, 4) +
+ '-' +
+ dest.calculateHash().toBase64().substring(0, 4) +
+ " old: " + old + " con: " + opts.getRTT() + " new: " + e.getRTT() +
+ " oldw: " + oldw + " conw: " + opts.getWindowSize() + " neww: " + e.getWindowSize());
+ }
+
+ private class Entry {
+ int _rtt;
+ int _wdw;
+ long _updated;
+
+ public Entry(int ms, int wdw) {
+ _rtt = ms;
+ _wdw = wdw;
+ _updated = _context.clock().now();
+ }
+ public int getRTT() { return _rtt; }
+ public void setRTT(int ms) {
+ _rtt = (int)(RTT_DAMPENING*_rtt + (1-RTT_DAMPENING)*ms);
+ if (_rtt > MAX_RTT)
+ _rtt = MAX_RTT;
+ _updated = _context.clock().now();
+ }
+ public int getWindowSize() { return _wdw; }
+ public void setWindowSize(int wdw) {
+ _wdw = (int)(0.5 + WDW_DAMPENING*_wdw + (1-WDW_DAMPENING)*wdw);
+ if (_wdw > MAX_WINDOW_SIZE)
+ _wdw = MAX_WINDOW_SIZE;
+ _updated = _context.clock().now();
+ }
+ public boolean isExpired() {
+ return _updated < _context.clock().now() - EXPIRE_TIME;
+ }
+ }
+
+ private class CleanEvent implements SimpleTimer.TimedEvent {
+ public CleanEvent() {}
+ public void timeReached() {
+ for (Iterator iter = _cache.keySet().iterator(); iter.hasNext(); ) {
+ if (_cache.get(iter.next()).isExpired())
+ iter.remove();
+ }
+ SimpleTimer.getInstance().addEvent(CleanEvent.this, CLEAN_TIME);
+ }
+ }
+}
diff --git a/build.xml b/build.xml
index f9a015a6e..9b69a13f3 100644
--- a/build.xml
+++ b/build.xml
@@ -60,7 +60,6 @@
-
@@ -87,7 +86,7 @@
-
+
@@ -219,7 +218,6 @@
-
@@ -371,17 +369,16 @@
-
-
+
-
+
-
+
@@ -452,7 +449,7 @@
-
+
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 3e514ea18..6b3b0fd5b 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -24,6 +24,7 @@ import net.i2p.data.RoutingKeyGenerator;
import net.i2p.stat.StatManager;
import net.i2p.util.Clock;
import net.i2p.util.FortunaRandomSource;
+import net.i2p.util.KeyRing;
import net.i2p.util.LogManager;
import net.i2p.util.PooledRandomSource;
import net.i2p.util.RandomSource;
@@ -75,6 +76,7 @@ public class I2PAppContext {
private RoutingKeyGenerator _routingKeyGenerator;
private RandomSource _random;
private KeyGenerator _keyGenerator;
+ protected KeyRing _keyRing; // overridden in RouterContext
private volatile boolean _statManagerInitialized;
private volatile boolean _sessionKeyManagerInitialized;
private volatile boolean _namingServiceInitialized;
@@ -91,6 +93,7 @@ public class I2PAppContext {
private volatile boolean _routingKeyGeneratorInitialized;
private volatile boolean _randomInitialized;
private volatile boolean _keyGeneratorInitialized;
+ protected volatile boolean _keyRingInitialized; // used in RouterContext
/**
@@ -141,12 +144,14 @@ public class I2PAppContext {
_elGamalEngine = null;
_elGamalAESEngine = null;
_logManager = null;
+ _keyRing = null;
_statManagerInitialized = false;
_sessionKeyManagerInitialized = false;
_namingServiceInitialized = false;
_elGamalEngineInitialized = false;
_elGamalAESEngineInitialized = false;
_logManagerInitialized = false;
+ _keyRingInitialized = false;
}
/**
@@ -512,6 +517,23 @@ public class I2PAppContext {
}
}
+ /**
+ * Basic hash map
+ */
+ public KeyRing keyRing() {
+ if (!_keyRingInitialized)
+ initializeKeyRing();
+ return _keyRing;
+ }
+
+ protected void initializeKeyRing() {
+ synchronized (this) {
+ if (_keyRing == null)
+ _keyRing = new KeyRing();
+ _keyRingInitialized = true;
+ }
+ }
+
/**
* [insert snarky comment here]
*
diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java
index 9af1fbd19..5b45ee7a3 100644
--- a/core/java/src/net/i2p/client/I2CPMessageProducer.java
+++ b/core/java/src/net/i2p/client/I2CPMessageProducer.java
@@ -9,6 +9,7 @@ package net.i2p.client;
*
*/
+import java.util.Date;
import java.util.Set;
import net.i2p.I2PAppContext;
@@ -28,6 +29,7 @@ import net.i2p.data.i2cp.DestroySessionMessage;
import net.i2p.data.i2cp.MessageId;
import net.i2p.data.i2cp.ReportAbuseMessage;
import net.i2p.data.i2cp.SendMessageMessage;
+import net.i2p.data.i2cp.SendMessageExpiresMessage;
import net.i2p.data.i2cp.SessionConfig;
import net.i2p.util.Log;
@@ -91,8 +93,13 @@ class I2CPMessageProducer {
*
*/
public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SessionTag tag,
- SessionKey key, Set tags, SessionKey newKey) throws I2PSessionException {
- SendMessageMessage msg = new SendMessageMessage();
+ SessionKey key, Set tags, SessionKey newKey, long expires) throws I2PSessionException {
+ SendMessageMessage msg;
+ if (expires > 0) {
+ msg = new SendMessageExpiresMessage();
+ ((SendMessageExpiresMessage)msg).setExpiration(new Date(expires));
+ } else
+ msg = new SendMessageMessage();
msg.setDestination(dest);
msg.setSessionId(session.getSessionId());
msg.setNonce(nonce);
diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java
index 627d1775a..d8c64f222 100644
--- a/core/java/src/net/i2p/client/I2PSession.java
+++ b/core/java/src/net/i2p/client/I2PSession.java
@@ -70,6 +70,7 @@ public interface I2PSession {
*/
public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException;
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent) throws I2PSessionException;
+ public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire) throws I2PSessionException;
/** Receive a message that the router has notified the client about, returning
* the payload.
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index 78f4ba763..a57957107 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -550,10 +550,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
* Pass off the error to the listener
*/
void propogateError(String msg, Throwable error) {
- if (_log.shouldLog(Log.WARN))
- _log.warn(getPrefix() + "Error occurred: " + msg + " - " + error.getMessage());
- if (_log.shouldLog(Log.WARN))
- _log.warn(getPrefix() + " cause", error);
+ if (_log.shouldLog(Log.ERROR))
+ _log.error(getPrefix() + "Error occurred: " + msg + " - " + error.getMessage());
+ if (_log.shouldLog(Log.ERROR))
+ _log.error(getPrefix() + " cause", error);
if (_sessionListener != null) _sessionListener.errorOccurred(this, msg, error);
}
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java
index 81c6ef22f..6a90952a5 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl2.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java
@@ -107,15 +107,19 @@ class I2PSessionImpl2 extends I2PSessionImpl {
return sendMessage(dest, payload, 0, payload.length);
}
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size) throws I2PSessionException {
- return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64));
+ return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64), 0);
}
@Override
public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException {
- return sendMessage(dest, payload, 0, payload.length, keyUsed, tagsSent);
+ return sendMessage(dest, payload, 0, payload.length, keyUsed, tagsSent, 0);
}
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent)
throws I2PSessionException {
+ return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0);
+ }
+ public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expires)
+ throws I2PSessionException {
if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message");
if (isClosed()) throw new I2PSessionException("Already closed");
@@ -142,7 +146,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
}
_context.statManager().addRateData("i2cp.tx.msgCompressed", compressed, 0);
_context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0);
- return sendBestEffort(dest, payload, keyUsed, tagsSent);
+ return sendBestEffort(dest, payload, keyUsed, tagsSent, expires);
}
/**
@@ -168,7 +172,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
private static final int NUM_TAGS = 50;
- private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent)
+ private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent, long expires)
throws I2PSessionException {
SessionKey key = null;
SessionKey newKey = null;
@@ -176,6 +180,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
Set sentTags = null;
int oldTags = 0;
long begin = _context.clock().now();
+ /***********
if (I2CPMessageProducer.END_TO_END_CRYPTO) {
if (_log.shouldLog(Log.DEBUG)) _log.debug("begin sendBestEffort");
key = _context.sessionKeyManager().getCurrentKey(dest.getPublicKey());
@@ -220,6 +225,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
} else {
// not using end to end crypto, so don't ever bundle any tags
}
+ **********/
if (_log.shouldLog(Log.DEBUG)) _log.debug("before creating nonce");
@@ -233,14 +239,14 @@ class I2PSessionImpl2 extends I2PSessionImpl {
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Setting key = " + key);
if (keyUsed != null) {
- if (I2CPMessageProducer.END_TO_END_CRYPTO) {
- if (newKey != null)
- keyUsed.setData(newKey.getData());
- else
- keyUsed.setData(key.getData());
- } else {
+ //if (I2CPMessageProducer.END_TO_END_CRYPTO) {
+ // if (newKey != null)
+ // keyUsed.setData(newKey.getData());
+ // else
+ // keyUsed.setData(key.getData());
+ //} else {
keyUsed.setData(SessionKey.INVALID_KEY.getData());
- }
+ //}
}
if (tagsSent != null) {
if (sentTags != null) {
@@ -261,7 +267,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
+ state.getNonce() + " for best effort "
+ " sync took " + (inSendingSync-beforeSendingSync)
+ " add took " + (afterSendingSync-inSendingSync));
- _producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey);
+ _producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey, expires);
// since this is 'best effort', all we're waiting for is a status update
// saying that the router received it - in theory, that should come back
diff --git a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java
index 7d6d816c1..6163771e3 100644
--- a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java
+++ b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java
@@ -21,6 +21,7 @@ import net.i2p.data.Lease;
import net.i2p.data.LeaseSet;
import net.i2p.data.PrivateKey;
import net.i2p.data.PublicKey;
+import net.i2p.data.SessionKey;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.i2cp.I2CPMessage;
@@ -78,6 +79,17 @@ class RequestLeaseSetMessageHandler extends HandlerImpl {
leaseSet.setEncryptionKey(li.getPublicKey());
leaseSet.setSigningKey(li.getSigningPublicKey());
+ String sk = session.getOptions().getProperty("i2cp.sessionKey");
+ if (sk != null) {
+ SessionKey key = new SessionKey();
+ try {
+ key.fromBase64(sk);
+ leaseSet.encrypt(key);
+ _context.keyRing().put(session.getMyDestination().calculateHash(), key);
+ } catch (DataFormatException dfe) {
+ _log.error("Bad session key: " + sk);
+ }
+ }
try {
leaseSet.sign(session.getPrivateKey());
session.getProducer().createLeaseSet(session, leaseSet, li.getSigningPrivateKey(), li.getPrivateKey());
@@ -137,4 +149,4 @@ class RequestLeaseSetMessageHandler extends HandlerImpl {
&& DataHelper.eq(_signingPrivKey, li.getSigningPrivateKey());
}
}
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index 835e6a0dd..4a074f17c 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -25,6 +25,7 @@ import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
+import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -235,7 +236,7 @@ public class DataHelper {
int split = line.indexOf('=');
if (split <= 0) continue;
String key = line.substring(0, split);
- String val = line.substring(split+1);
+ String val = line.substring(split+1); //.trim() ??????????????
// Unescape line breaks after loading.
// Remember: "\" needs escaping both for regex and string.
val = val.replaceAll("\\\\r","\r");
@@ -842,6 +843,29 @@ public class DataHelper {
}
}
+ /**
+ * Caller should append 'B' or 'b' as appropriate
+ */
+ public static String formatSize(long bytes) {
+ double val = bytes;
+ int scale = 0;
+ while (val >= 1024) {
+ scale++;
+ val /= 1024;
+ }
+
+ DecimalFormat fmt = new DecimalFormat("##0.00");
+
+ String str = fmt.format(val);
+ switch (scale) {
+ case 1: return str + "K";
+ case 2: return str + "M";
+ case 3: return str + "G";
+ case 4: return str + "T";
+ default: return bytes + "";
+ }
+ }
+
/**
* Strip out any HTML (simply removing any less than / greater than symbols)
*/
diff --git a/core/java/src/net/i2p/data/LeaseSet.java b/core/java/src/net/i2p/data/LeaseSet.java
index 7dd74a9d7..8a05dd956 100644
--- a/core/java/src/net/i2p/data/LeaseSet.java
+++ b/core/java/src/net/i2p/data/LeaseSet.java
@@ -9,6 +9,7 @@ package net.i2p.data;
*
*/
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -17,13 +18,34 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import net.i2p.I2PAppContext;
import net.i2p.crypto.DSAEngine;
import net.i2p.util.Clock;
import net.i2p.util.Log;
+import net.i2p.util.RandomSource;
/**
* Defines the set of leases a destination currently has.
*
+ * Support encryption and decryption with a supplied key.
+ * Only the gateways and tunnel IDs in the individual
+ * leases are encrypted.
+ *
+ * Encrypted leases are not indicated as such.
+ * The only way to tell a lease is encrypted is to
+ * determine that the listed gateways do not exist.
+ * Routers wishing to decrypt a leaseset must have the
+ * desthash and key in their keyring.
+ * This is required for the local router as well, since
+ * the encryption is done on the client side of I2CP, the
+ * router must decrypt it back again for local usage
+ * (but not for transmission to the floodfills)
+ *
+ * Decrypted leases are only available through the getLease()
+ * method, so that storage and network transmission via
+ * writeBytes() will output the original encrypted
+ * leases and the original leaseset signature.
+ *
* @author jrandom
*/
public class LeaseSet extends DataStructureImpl {
@@ -40,6 +62,9 @@ public class LeaseSet extends DataStructureImpl {
// Store these since isCurrent() and getEarliestLeaseDate() are called frequently
private long _firstExpiration;
private long _lastExpiration;
+ private List _decryptedLeases;
+ private boolean _decrypted;
+ private boolean _checked;
/** This seems like plenty */
private final static int MAX_LEASES = 6;
@@ -55,6 +80,8 @@ public class LeaseSet extends DataStructureImpl {
_receivedAsPublished = false;
_firstExpiration = Long.MAX_VALUE;
_lastExpiration = 0;
+ _decrypted = false;
+ _checked = false;
}
public Destination getDestination() {
@@ -104,11 +131,17 @@ public class LeaseSet extends DataStructureImpl {
}
public int getLeaseCount() {
- return _leases.size();
+ if (isEncrypted())
+ return _leases.size() - 1;
+ else
+ return _leases.size();
}
public Lease getLease(int index) {
- return (Lease) _leases.get(index);
+ if (isEncrypted())
+ return (Lease) _decryptedLeases.get(index);
+ else
+ return (Lease) _leases.get(index);
}
public Signature getSignature() {
@@ -335,4 +368,139 @@ public class LeaseSet extends DataStructureImpl {
buf.append("]");
return buf.toString();
}
+
+ private static final int DATA_LEN = Hash.HASH_LENGTH + 4;
+ private static final int IV_LEN = 16;
+
+ /**
+ * Encrypt the gateway and tunnel ID of each lease, leaving the expire dates unchanged.
+ * This adds an extra dummy lease, because AES data must be padded to 16 bytes.
+ * The fact that it is encrypted is not stored anywhere.
+ * Must be called after all the leases are in place, but before sign().
+ */
+ public void encrypt(SessionKey key) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("encrypting lease: " + _destination.calculateHash());
+ try {
+ encryp(key);
+ } catch (DataFormatException dfe) {
+ _log.error("Error encrypting lease: " + _destination.calculateHash());
+ } catch (IOException ioe) {
+ _log.error("Error encrypting lease: " + _destination.calculateHash());
+ }
+ }
+
+ /**
+ * - Put the {Gateway Hash, TunnelID} pairs for all the leases in a buffer
+ * - Pad with random data to a multiple of 16 bytes
+ * - Use the first part of the dest's public key as an IV
+ * - Encrypt
+ * - Pad with random data to a multiple of 36 bytes
+ * - Add an extra lease
+ * - Replace the Hash and TunnelID in each Lease
+ */
+ private void encryp(SessionKey key) throws DataFormatException, IOException {
+ int size = _leases.size();
+ if (size < 1 || size > MAX_LEASES-1)
+ throw new IllegalArgumentException("Bad number of leases for encryption");
+ int datalen = ((DATA_LEN * size / 16) + 1) * 16;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(datalen);
+ for (int i = 0; i < size; i++) {
+ ((Lease)_leases.get(i)).getGateway().writeBytes(baos);
+ ((Lease)_leases.get(i)).getTunnelId().writeBytes(baos);
+ }
+ // pad out to multiple of 16 with random data before encryption
+ int padlen = datalen - (DATA_LEN * size);
+ byte[] pad = new byte[padlen];
+ RandomSource.getInstance().nextBytes(pad);
+ baos.write(pad);
+ byte[] iv = new byte[IV_LEN];
+ System.arraycopy(_destination.getPublicKey().getData(), 0, iv, 0, IV_LEN);
+ byte[] enc = new byte[DATA_LEN * (size + 1)];
+ I2PAppContext.getGlobalContext().aes().encrypt(baos.toByteArray(), 0, enc, 0, key, iv, datalen);
+ // pad out to multiple of 36 with random data after encryption
+ // (even for 4 leases, where 36*4 is a multiple of 16, we add another, just to be consistent)
+ padlen = enc.length - datalen;
+ pad = new byte[padlen];
+ RandomSource.getInstance().nextBytes(pad);
+ System.arraycopy(pad, 0, enc, datalen, padlen);
+ // add the padded lease...
+ Lease padLease = new Lease();
+ padLease.setEndDate(((Lease)_leases.get(0)).getEndDate());
+ _leases.add(padLease);
+ // ...and replace all the gateways and tunnel ids
+ ByteArrayInputStream bais = new ByteArrayInputStream(enc);
+ for (int i = 0; i < size+1; i++) {
+ Hash h = new Hash();
+ h.readBytes(bais);
+ ((Lease)_leases.get(i)).setGateway(h);
+ TunnelId t = new TunnelId();
+ t.readBytes(bais);
+ ((Lease)_leases.get(i)).setTunnelId(t);
+ }
+ }
+
+ /**
+ * Decrypt the leases, except for the last one which is partially padding.
+ * Store the new decrypted leases in a backing store,
+ * and keep the original leases so that verify() still works and the
+ * encrypted leaseset can be sent on to others (via writeBytes())
+ */
+ private void decrypt(SessionKey key) throws DataFormatException, IOException {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("decrypting lease: " + _destination.calculateHash());
+ int size = _leases.size();
+ if (size < 2)
+ throw new DataFormatException("Bad number of leases for decryption");
+ int datalen = DATA_LEN * size;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(datalen);
+ for (int i = 0; i < size; i++) {
+ ((Lease)_leases.get(i)).getGateway().writeBytes(baos);
+ ((Lease)_leases.get(i)).getTunnelId().writeBytes(baos);
+ }
+ byte[] iv = new byte[IV_LEN];
+ System.arraycopy(_destination.getPublicKey().getData(), 0, iv, 0, IV_LEN);
+ int enclen = ((DATA_LEN * (size - 1) / 16) + 1) * 16;
+ byte[] enc = new byte[enclen];
+ System.arraycopy(baos.toByteArray(), 0, enc, 0, enclen);
+ byte[] dec = new byte[enclen];
+ I2PAppContext.getGlobalContext().aes().decrypt(enc, 0, dec, 0, key, iv, enclen);
+ ByteArrayInputStream bais = new ByteArrayInputStream(dec);
+ _decryptedLeases = new ArrayList(size - 1);
+ for (int i = 0; i < size-1; i++) {
+ Lease l = new Lease();
+ Hash h = new Hash();
+ h.readBytes(bais);
+ l.setGateway(h);
+ TunnelId t = new TunnelId();
+ t.readBytes(bais);
+ l.setTunnelId(t);
+ l.setEndDate(((Lease)_leases.get(i)).getEndDate());
+ _decryptedLeases.add(l);
+ }
+ }
+
+ /**
+ * @return true if it was encrypted, and we decrypted it successfully.
+ * Decrypts on first call.
+ */
+ private synchronized boolean isEncrypted() {
+ if (_decrypted)
+ return true;
+ if (_checked || _destination == null)
+ return false;
+ SessionKey key = I2PAppContext.getGlobalContext().keyRing().get(_destination.calculateHash());
+ if (key != null) {
+ try {
+ decrypt(key);
+ _decrypted = true;
+ } catch (DataFormatException dfe) {
+ _log.error("Error decrypting lease: " + _destination.calculateHash() + dfe);
+ } catch (IOException ioe) {
+ _log.error("Error decrypting lease: " + _destination.calculateHash() + ioe);
+ }
+ }
+ _checked = true;
+ return _decrypted;
+ }
}
diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
index 128c312dc..15045028a 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
@@ -18,7 +18,7 @@ import net.i2p.data.DataHelper;
import net.i2p.util.Log;
/**
- * Handle messages from the server for the client
+ * Handle messages from the server for the client or vice versa
*
*/
public class I2CPMessageHandler {
@@ -75,6 +75,8 @@ public class I2CPMessageHandler {
return new RequestLeaseSetMessage();
case SendMessageMessage.MESSAGE_TYPE:
return new SendMessageMessage();
+ case SendMessageExpiresMessage.MESSAGE_TYPE:
+ return new SendMessageExpiresMessage();
case SessionStatusMessage.MESSAGE_TYPE:
return new SessionStatusMessage();
case GetDateMessage.MESSAGE_TYPE:
diff --git a/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java b/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java
new file mode 100644
index 000000000..7165f6d32
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java
@@ -0,0 +1,103 @@
+package net.i2p.data.i2cp;
+
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by jrandom in 2003 and released into the public domain
+ * with no warranty of any kind, either expressed or implied.
+ * It probably won't make your computer catch on fire, or eat
+ * your children, but it might. Use at your own risk.
+ *
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.util.Log;
+
+/**
+ * Defines the message a client sends to a router when
+ * updating the config on an existing session.
+ *
+ * @author zzz
+ */
+public class ReconfigureSessionMessage extends I2CPMessageImpl {
+ private final static Log _log = new Log(ReconfigureSessionMessage.class);
+ public final static int MESSAGE_TYPE = 2;
+ private SessionId _sessionId;
+ private SessionConfig _sessionConfig;
+
+ public ReconfigureSessionMessage() {
+ _sessionId = null;
+ _sessionConfig = null;
+ }
+
+ public SessionId getSessionId() {
+ return _sessionId;
+ }
+
+ public void setSessionId(SessionId id) {
+ _sessionId = id;
+ }
+
+ public SessionConfig getSessionConfig() {
+ return _sessionConfig;
+ }
+
+ public void setSessionConfig(SessionConfig config) {
+ _sessionConfig = config;
+ }
+
+ @Override
+ protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
+ try {
+ _sessionId = new SessionId();
+ _sessionId.readBytes(in);
+ _sessionConfig = new SessionConfig();
+ _sessionConfig.readBytes(in);
+ } catch (DataFormatException dfe) {
+ throw new I2CPMessageException("Unable to load the message data", dfe);
+ }
+ }
+
+ @Override
+ protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+ if (_sessionId == null || _sessionConfig == null)
+ throw new I2CPMessageException("Unable to write out the message as there is not enough data");
+ ByteArrayOutputStream os = new ByteArrayOutputStream(64);
+ try {
+ _sessionId.writeBytes(os);
+ _sessionConfig.writeBytes(os);
+ } catch (DataFormatException dfe) {
+ throw new I2CPMessageException("Error writing out the message data", dfe);
+ }
+ return os.toByteArray();
+ }
+
+ public int getType() {
+ return MESSAGE_TYPE;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if ((object != null) && (object instanceof ReconfigureSessionMessage)) {
+ ReconfigureSessionMessage msg = (ReconfigureSessionMessage) object;
+ return DataHelper.eq(getSessionId(), msg.getSessionId())
+ && DataHelper.eq(getSessionConfig(), msg.getSessionConfig());
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("[ReconfigureSessionMessage: ");
+ buf.append("\n\tSessionId: ").append(getSessionId());
+ buf.append("\n\tSessionConfig: ").append(getSessionConfig());
+ buf.append("]");
+ return buf.toString();
+ }
+}
diff --git a/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java b/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java
new file mode 100644
index 000000000..d15c1979c
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java
@@ -0,0 +1,117 @@
+package net.i2p.data.i2cp;
+
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by jrandom in 2003 and released into the public domain
+ * with no warranty of any kind, either expressed or implied.
+ * It probably won't make your computer catch on fire, or eat
+ * your children, but it might. Use at your own risk.
+ *
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Date;
+
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.data.Payload;
+import net.i2p.util.Log;
+
+/**
+ * Same as SendMessageMessage, but with an expiration to be passed to the router
+ *
+ * @author zzz
+ */
+public class SendMessageExpiresMessage extends SendMessageMessage {
+ private final static Log _log = new Log(SendMessageExpiresMessage.class);
+ public final static int MESSAGE_TYPE = 36;
+ private SessionId _sessionId;
+ private Destination _destination;
+ private Payload _payload;
+ private Date _expiration;
+
+ public SendMessageExpiresMessage() {
+ super();
+ setExpiration(null);
+ }
+
+ public Date getExpiration() {
+ return _expiration;
+ }
+
+ public void setExpiration(Date d) {
+ _expiration = d;
+ }
+
+ /**
+ * Read the body into the data structures
+ *
+ * @throws IOException
+ */
+ @Override
+ public void readMessage(InputStream in, int length, int type) throws I2CPMessageException, IOException {
+ super.readMessage(in, length, type);
+
+ try {
+ _expiration = DataHelper.readDate(in);
+ } catch (DataFormatException dfe) {
+ throw new I2CPMessageException("Unable to load the message data", dfe);
+ }
+ }
+
+ /**
+ * Write out the full message to the stream, including the 4 byte size and 1
+ * byte type header. Override the parent so we can be more mem efficient
+ *
+ * @throws IOException
+ */
+ @Override
+ public void writeMessage(OutputStream out) throws I2CPMessageException, IOException {
+ if ((getSessionId() == null) || (getDestination() == null) || (getPayload() == null) || (getNonce() <= 0) || (_expiration == null))
+ throw new I2CPMessageException("Unable to write out the message as there is not enough data");
+ int len = 2 + getDestination().size() + getPayload().getSize() + 4 + 4 + DataHelper.DATE_LENGTH;
+
+ try {
+ DataHelper.writeLong(out, 4, len);
+ DataHelper.writeLong(out, 1, getType());
+ getSessionId().writeBytes(out);
+ getDestination().writeBytes(out);
+ getPayload().writeBytes(out);
+ DataHelper.writeLong(out, 4, getNonce());
+ DataHelper.writeDate(out, _expiration);
+ } catch (DataFormatException dfe) {
+ throw new I2CPMessageException("Error writing the msg", dfe);
+ }
+ }
+
+ public int getType() {
+ return MESSAGE_TYPE;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if ((object != null) && (object instanceof SendMessageExpiresMessage)) {
+ SendMessageExpiresMessage msg = (SendMessageExpiresMessage) object;
+ return super.equals(object)
+ && DataHelper.eq(getExpiration(), msg.getExpiration());
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("[SendMessageMessage: ");
+ buf.append("\n\tSessionId: ").append(getSessionId());
+ buf.append("\n\tNonce: ").append(getNonce());
+ buf.append("\n\tDestination: ").append(getDestination());
+ buf.append("\n\tExpiration: ").append(getExpiration());
+ buf.append("\n\tPayload: ").append(getPayload());
+ buf.append("]");
+ return buf.toString();
+ }
+}
diff --git a/core/java/src/net/i2p/util/KeyRing.java b/core/java/src/net/i2p/util/KeyRing.java
new file mode 100644
index 000000000..6bbfb38de
--- /dev/null
+++ b/core/java/src/net/i2p/util/KeyRing.java
@@ -0,0 +1,20 @@
+package net.i2p.util;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+
+/**
+ * simple
+ */
+public class KeyRing extends ConcurrentHashMap {
+ public KeyRing() {
+ super(0);
+ }
+
+ public void renderStatusHTML(Writer out) throws IOException {}
+}
diff --git a/installer/resources/wrapper.config b/installer/resources/wrapper.config
index 2550f4e3d..177bc1c0b 100644
--- a/installer/resources/wrapper.config
+++ b/installer/resources/wrapper.config
@@ -47,14 +47,13 @@ wrapper.java.classpath.12=lib/jasper-runtime.jar
wrapper.java.classpath.13=lib/commons-logging.jar
wrapper.java.classpath.14=lib/commons-el.jar
wrapper.java.classpath.15=lib/ant.jar
-wrapper.java.classpath.16=lib/xercesImpl.jar
# java service wrapper, BSD
-wrapper.java.classpath.17=lib/wrapper.jar
+wrapper.java.classpath.16=lib/wrapper.jar
# systray, LGPL
-wrapper.java.classpath.18=lib/systray.jar
-wrapper.java.classpath.19=lib/systray4j.jar
+wrapper.java.classpath.17=lib/systray.jar
+wrapper.java.classpath.18=lib/systray4j.jar
# BOB
-wrapper.java.classpath.20=lib/BOB.jar
+wrapper.java.classpath.19=lib/BOB.jar
# Java Library Path (location of Wrapper.DLL or libwrapper.so)
wrapper.java.library.path.1=.
diff --git a/router/java/src/net/i2p/router/ClientMessage.java b/router/java/src/net/i2p/router/ClientMessage.java
index 005f69a2d..ec7820d69 100644
--- a/router/java/src/net/i2p/router/ClientMessage.java
+++ b/router/java/src/net/i2p/router/ClientMessage.java
@@ -27,6 +27,7 @@ public class ClientMessage {
private SessionConfig _senderConfig;
private Hash _destinationHash;
private MessageId _messageId;
+ private long _expiration;
public ClientMessage() {
setPayload(null);
@@ -36,6 +37,7 @@ public class ClientMessage {
setSenderConfig(null);
setDestinationHash(null);
setMessageId(null);
+ setExpiration(0);
}
/**
@@ -91,4 +93,12 @@ public class ClientMessage {
*/
public SessionConfig getSenderConfig() { return _senderConfig; }
public void setSenderConfig(SessionConfig config) { _senderConfig = config; }
+
+ /**
+ * Expiration requested by the client that sent the message. This will only be available
+ * for locally originated messages.
+ *
+ */
+ public long getExpiration() { return _expiration; }
+ public void setExpiration(long e) { _expiration = e; }
}
diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
index ed4fe1555..e4a5ce08b 100644
--- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
@@ -62,4 +62,5 @@ public abstract class NetworkDatabaseFacade implements Service {
public int getKnownRouters() { return 0; }
public int getKnownLeaseSets() { return 0; }
public void renderRouterInfoHTML(Writer out, String s) throws IOException {}
+ public void renderStatusHTML(Writer out, boolean b) throws IOException {}
}
diff --git a/router/java/src/net/i2p/router/PersistentKeyRing.java b/router/java/src/net/i2p/router/PersistentKeyRing.java
new file mode 100644
index 000000000..d02275ea2
--- /dev/null
+++ b/router/java/src/net/i2p/router/PersistentKeyRing.java
@@ -0,0 +1,103 @@
+package net.i2p.router;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+import net.i2p.data.Base64;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.LeaseSet;
+import net.i2p.data.SessionKey;
+import net.i2p.router.TunnelPoolSettings;
+import net.i2p.util.KeyRing;
+
+/**
+ * ConcurrentHashMap with backing in the router.config file.
+ * router.keyring.key.{base64 hash, with = replaced with $}={base64 session key}
+ * Caution - not all HashMap methods are overridden.
+ */
+public class PersistentKeyRing extends KeyRing {
+ private RouterContext _ctx;
+ private static final String PROP_PFX = "router.keyring.key.";
+
+ public PersistentKeyRing(RouterContext ctx) {
+ super();
+ _ctx = ctx;
+ addFromProperties();
+ }
+
+ public SessionKey put(Hash h, SessionKey sk) {
+ SessionKey old = super.put(h, sk);
+ if (!sk.equals(old)) {
+ _ctx.router().setConfigSetting(PROP_PFX + h.toBase64().replace("=", "$"),
+ sk.toBase64());
+ _ctx.router().saveConfig();
+ }
+ return old;
+ }
+
+ public SessionKey remove(Hash h) {
+ _ctx.router().removeConfigSetting(PROP_PFX + h.toBase64().replace("=", "$"));
+ _ctx.router().saveConfig();
+ return super.remove(h);
+ }
+
+ private void addFromProperties() {
+ for (Iterator iter = _ctx.getPropertyNames().iterator(); iter.hasNext(); ) {
+ String prop = (String) iter.next();
+ if (!prop.startsWith(PROP_PFX))
+ continue;
+ String key = _ctx.getProperty(prop);
+ if (key == null || key.length() != 44)
+ continue;
+ String hb = prop.substring(PROP_PFX.length());
+ hb.replace("$", "=");
+ Hash dest = new Hash();
+ SessionKey sk = new SessionKey();
+ try {
+ dest.fromBase64(hb);
+ sk.fromBase64(key);
+ super.put(dest, sk);
+ } catch (DataFormatException dfe) { continue; }
+ }
+ }
+
+ public void renderStatusHTML(Writer out) throws IOException {
+ StringBuffer buf = new StringBuffer(1024);
+ buf.append("\n
Destination Hash
Name or Dest.
Session Key
");
+ for (Entry e : entrySet()) {
+ buf.append("\n