package net.i2p.client.impl;
/*
* 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.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionListener;
import net.i2p.client.I2PSessionMuxedListener;
import net.i2p.client.SendMessageOptions;
import net.i2p.client.SendMessageStatusListener;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import net.i2p.data.i2cp.MessageId;
import net.i2p.data.i2cp.MessageStatusMessage;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;
/**
* Thread safe implementation of an I2P session running over TCP.
*
* Unused directly, see I2PSessionMuxedImpl extension.
*
* @author jrandom
*/
class I2PSessionImpl2 extends I2PSessionImpl {
/** set of MessageState objects, representing all of the messages in the process of being sent */
protected final Map<Long, MessageState> _sendingStates;
protected final AtomicLong _sendMessageNonce;
/** max # seconds to wait for confirmation of the message send */
private final static long SEND_TIMEOUT = 60 * 1000; // 60 seconds to send
/** should we gzip each payload prior to sending it? */
private final static boolean SHOULD_COMPRESS = true;
private final static boolean SHOULD_DECOMPRESS = true;
/** Don't expect any MSMs from the router for outbound traffic @since 0.8.1 */
protected boolean _noEffort;
private static final long REMOVE_EXPIRED_TIME = 63*1000;
/**
* for extension by SimpleSession (no dest)
*/
protected I2PSessionImpl2(I2PAppContext context, Properties options,
I2PClientMessageHandlerMap handlerMap) {
super(context, options, handlerMap);
_sendingStates = null;
_sendMessageNonce = null;
}
/**
* for extension by I2PSessionMuxedImpl
*
* Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey
* from the destKeyStream, and using the specified options to connect to the router
*
* @param destKeyStream stream containing the private key data,
* format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
* @param options set of options to configure the router with, if null will use System properties
* @throws I2PSessionException if there is a problem loading the private keys
*/
protected I2PSessionImpl2(I2PAppContext ctx, InputStream destKeyStream, Properties options) throws I2PSessionException {
super(ctx, destKeyStream, options);
_sendingStates = new ConcurrentHashMap<Long, MessageState>(32);
_sendMessageNonce = new AtomicLong();
// default is BestEffort
_noEffort = "none".equals(getOptions().getProperty(I2PClient.PROP_RELIABILITY, "").toLowerCase(Locale.US));
//ctx.statManager().createRateStat("i2cp.sendBestEffortTotalTime", "how long to do the full sendBestEffort call?", "i2cp", new long[] { 10*60*1000 } );
//ctx.statManager().createRateStat("i2cp.sendBestEffortStage0", "first part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
//ctx.statManager().createRateStat("i2cp.sendBestEffortStage1", "second part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
//ctx.statManager().createRateStat("i2cp.sendBestEffortStage2", "third part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
//ctx.statManager().createRateStat("i2cp.sendBestEffortStage3", "fourth part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
//ctx.statManager().createRateStat("i2cp.sendBestEffortStage4", "fifth part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
//_context.statManager().createRateStat("i2cp.receiveStatusTime.0", "How long it took to get status=0 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
_context.statManager().createRateStat("i2cp.receiveStatusTime.1", "How long it took to get status=1 back", "i2cp", new long[] { 10*60*1000 });
// best effort codes unused
//_context.statManager().createRateStat("i2cp.receiveStatusTime.2", "How long it took to get status=2 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
//_context.statManager().createRateStat("i2cp.receiveStatusTime.3", "How long it took to get status=3 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
_context.statManager().createRateStat("i2cp.receiveStatusTime.4", "How long it took to get status=4 back", "i2cp", new long[] { 10*60*1000 });
_context.statManager().createRateStat("i2cp.receiveStatusTime.5", "How long it took to get status=5 back", "i2cp", new long[] { 10*60*1000 });
//_context.statManager().createRateStat("i2cp.receiveStatusTime", "How long it took to get any status", "i2cp", new long[] { 10*60*1000 });
_context.statManager().createRateStat("i2cp.tx.msgCompressed", "compressed size transferred", "i2cp", new long[] { 30*60*1000 });
_context.statManager().createRateStat("i2cp.tx.msgExpanded", "size before compression", "i2cp", new long[] { 30*60*1000 });
}
/*
* For extension by SubSession via I2PSessionMuxedImpl
*
* @param destKeyStream stream containing the private key data,
* format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
* @param options set of options to configure the router with, if null will use System properties
* @since 0.9.21
*/
protected I2PSessionImpl2(I2PSessionImpl primary, InputStream destKeyStream, Properties options) throws I2PSessionException {
super(primary, destKeyStream, options);
_sendingStates = new ConcurrentHashMap<Long, MessageState>(32);
_sendMessageNonce = new AtomicLong();
_noEffort = "none".equals(getOptions().getProperty(I2PClient.PROP_RELIABILITY, "").toLowerCase(Locale.US));
_context.statManager().createRateStat("i2cp.receiveStatusTime.1", "How long it took to get status=1 back", "i2cp", new long[] { 10*60*1000 });
_context.statManager().createRateStat("i2cp.receiveStatusTime.4", "How long it took to get status=4 back", "i2cp", new long[] { 10*60*1000 });
_context.statManager().createRateStat("i2cp.receiveStatusTime.5", "How long it took to get status=5 back", "i2cp", new long[] { 10*60*1000 });
_context.statManager().createRateStat("i2cp.tx.msgCompressed", "compressed size transferred", "i2cp", new long[] { 30*60*1000 });
_context.statManager().createRateStat("i2cp.tx.msgExpanded", "size before compression", "i2cp", new long[] { 30*60*1000 });
}
/**
* Fire up a periodic task to check for unclaimed messages
* @since 0.9.14
*/
@Override
protected void startVerifyUsage() {
super.startVerifyUsage();
new RemoveExpired();
}
/**
* Check for expired message states, without wastefully setting a timer for each
* message.
* @since 0.9.14
*/
private class RemoveExpired extends SimpleTimer2.TimedEvent {
public RemoveExpired() {
super(_context.simpleTimer2(), REMOVE_EXPIRED_TIME);
}
public void timeReached() {
if (isClosed())
return;
if (!_sendingStates.isEmpty()) {
long now = _context.clock().now();
for (Iterator<MessageState> iter = _sendingStates.values().iterator(); iter.hasNext(); ) {
MessageState state = iter.next();
if (state.getExpires() < now)
iter.remove();
}
}
schedule(REMOVE_EXPIRED_TIME);
}
}
protected long getTimeout() {
return SEND_TIMEOUT;
}
@Override
public void destroySession(boolean sendDisconnect) {
clearStates();
super.destroySession(sendDisconnect);
}
/** Don't bother if really small.
* Three 66-byte messages will fit in one tunnel message.
* Four messages don't fit no matter how small. So below 66 it isn't worth it.
* See ConnectionOptions.java in the streaming lib for similar calculations.
* Since we still have to pass it through gzip -0 the CPU savings
* is trivial but it's the best we can do for now. See below.
* i2cp.gzip defaults to SHOULD_COMPRESS = true.
* Perhaps the http server (which does its own compression)
* and P2P apps (with generally uncompressible data) should
* set to false.
*
* Todo: don't compress if destination is local?
*/
private static final int DONT_COMPRESS_SIZE = 66;
protected boolean shouldCompress(int size) {
if (size <= DONT_COMPRESS_SIZE)
return false;
String p = getOptions().getProperty("i2cp.gzip");
if (p != null)
return Boolean.parseBoolean(p);
return SHOULD_COMPRESS;
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public void addSessionListener(I2PSessionListener lsnr, int proto, int port) {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public void addMuxedSessionListener(I2PSessionMuxedListener l, int proto, int port) {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public void removeListener(int proto, int port) {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public boolean sendMessage(Destination dest, byte[] payload, int proto, int fromport, int toport) throws I2PSessionException {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set<SessionTag> tagsSent,
int proto, int fromport, int toport) throws I2PSessionException {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set<SessionTag> tagsSent, long expire,
int proto, int fromport, int toport) throws I2PSessionException {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set<SessionTag> tagsSent, long expire,
int proto, int fromport, int toport, int flags) throws I2PSessionException {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size,
int proto, int fromport, int toport, SendMessageOptions options) throws I2PSessionException {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** @throws UnsupportedOperationException always, use MuxedImpl */
public long sendMessage(Destination dest, byte[] payload, int offset, int size,
int proto, int fromport, int toport,
SendMessageOptions options, SendMessageStatusListener listener) throws I2PSessionException {
throw new UnsupportedOperationException("Use MuxedImpl");
}
/** unused, see MuxedImpl override */
@Override
public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException {
return sendMessage(dest, payload, 0, payload.length);
}
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size) throws I2PSessionException {
// we don't do end-to-end crypto any more
//return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64), 0);
return sendMessage(dest, payload, offset, size, null, null, 0);
}
/**
* @param keyUsed unused - no end-to-end crypto
* @param tagsSent unused - no end-to-end crypto
*/
@Override
public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set<SessionTag> tagsSent) throws I2PSessionException {
return sendMessage(dest, payload, 0, payload.length, keyUsed, tagsSent, 0);
}
/**
* @param keyUsed unused - no end-to-end crypto
* @param tagsSent unused - no end-to-end crypto
*/
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set<SessionTag> tagsSent)
throws I2PSessionException {
return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0);
}
/**
* Unused? see MuxedImpl override
*
* @param keyUsed unused - no end-to-end crypto
* @param tagsSent unused - no end-to-end crypto
*/
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set<SessionTag> tagsSent, long expires)
throws I2PSessionException {
if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message");
verifyOpen();
updateActivity();
// Sadly there is no way to send something completely uncompressed in a backward-compatible way,
// so we have to still send it in a gzip format, which adds 23 bytes (2.4% for a 960-byte msg)
// (10 byte header + 5 byte block header + 8 byte trailer)
// In the future we can add a one-byte magic number != 0x1F to signal an uncompressed msg
// (Gzip streams start with 0x1F 0x8B 0x08)
// assuming we don't need the CRC-32 that comes with gzip (do we?)
// Maybe implement this soon in receiveMessage() below so we are ready
// in case we ever make an incompatible network change.
// This would save 22 of the 23 bytes and a little CPU.
boolean sc = shouldCompress(size);
if (sc)
payload = DataHelper.compress(payload, offset, size);
else
payload = DataHelper.compress(payload, offset, size, DataHelper.NO_COMPRESSION);
//else throw new IllegalStateException("we need to update sendGuaranteed to support partial send");
int compressed = payload.length;
if (_log.shouldLog(Log.INFO)) {
String d = dest.calculateHash().toBase64().substring(0,4);
_log.info("sending message to: " + d + " compress? " + sc + " sizeIn=" + size + " sizeOut=" + compressed);
}
_context.statManager().addRateData("i2cp.tx.msgCompressed", compressed);
_context.statManager().addRateData("i2cp.tx.msgExpanded", size);
if (_noEffort)
return sendNoEffort(dest, payload, expires, 0);
else
return sendBestEffort(dest, payload, keyUsed, tagsSent, expires);
}
/**
* pull the unencrypted AND DECOMPRESSED data
*/
@Override
public byte[] receiveMessage(int msgId) throws I2PSessionException {
byte compressed[] = super.receiveMessage(msgId);
if (compressed == null) {
_log.error("Error: message " + msgId + " already received!");
return null;
}
// future - check magic number to see whether to decompress
if (SHOULD_DECOMPRESS) {
try {
return DataHelper.decompress(compressed);
} catch (IOException ioe) {
//throw new I2PSessionException("Error decompressing message", ioe);
if (_log.shouldWarn())
_log.warn("Error decompressing message", ioe);
return null;
}
}
return compressed;
}
/**
* @param keyUsed unused - no end-to-end crypto
* @param tagsSent unused - no end-to-end crypto
*/
protected boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set<SessionTag> tagsSent, long expires)
throws I2PSessionException {
return sendBestEffort(dest, payload, expires, 0);
}
/**
* TODO - Don't need to save MessageState since actuallyWait is false...
* But for now just use sendNoEffort() instead.
*
* @param flags to be passed to the router
* @since 0.8.4
*/
protected boolean sendBestEffort(Destination dest, byte payload[], long expires, int flags)
throws I2PSessionException {
long nonce = _sendMessageNonce.incrementAndGet();
MessageState state = new MessageState(_context, nonce, getPrefix());
// 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
// immediately, but in practice can take up to a second (though usually
// much quicker). setting this to false will short-circuit that delay
boolean actuallyWait = false; // true;
if (actuallyWait)
_sendingStates.put(Long.valueOf(nonce), state);
_producer.sendMessage(this, dest, nonce, payload, expires, flags);
if (actuallyWait) {
try {
state.waitForAccept(_context.clock().now() + getTimeout());
} catch (InterruptedException ie) {
throw new I2PSessionException("interrupted");
} finally {
_sendingStates.remove(Long.valueOf(nonce));
}
}
boolean found = !actuallyWait || state.wasAccepted();
if (found) {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix() + "Message sent after " + state.getElapsed() + "ms with "
+ payload.length + " bytes");
} else {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix() + "Message send failed after " + state.getElapsed() + "ms with "
+ payload.length + " bytes");
//if (_log.shouldLog(Log.ERROR))
// _log.error(getPrefix() + "Never received *accepted* from the router! dropping and reconnecting");
//disconnect();
return false;
}
return found;
}
/**
* Same as sendBestEffort(), except we do not expect any MessageStatusMessage responses -
* not for accepted, or success, or failure.
* So we don't create a MessageState and save it on the _sendingStates HashSet
*
* @return true always
* @since 0.8.1
*/
protected boolean sendNoEffort(Destination dest, byte payload[], long expires, int flags)
throws I2PSessionException {
// nonce always 0
_producer.sendMessage(this, dest, 0, payload, expires, flags);
return true;
}
/**
* Only call this with nonzero status, i.e. for outbound messages
* whose MessageState may be queued on _sendingStates.
*
* Even when using sendBestEffort(), this is a waste, because the
* MessageState is removed from _sendingStates immediately and
* so the lookup here fails.
*
* This is now pretty much avoided since streaming now sets
* i2cp.messageReliability = none, which forces sendNoEffort() instead of sendBestEffort(),
* so the router won't send us any MSM's for outbound traffic.
*
* @param status != 0
*/
@Override
public void receiveStatus(int msgId, long nonce, int status) {
if (_log.shouldLog(Log.DEBUG))
_log.debug(getPrefix() + "Received status " + status + " for msgId " + msgId + " / " + nonce);
MessageState state = null;
if ((state = _sendingStates.get(Long.valueOf(nonce))) != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug(getPrefix() + "Found a matching state");
} else if (!_sendingStates.isEmpty()) {
// O(n**2)
// shouldn't happen, router sends good nonce for all statuses as of 0.9.14
for (MessageState s : _sendingStates.values()) {
if (s.getMessageId() != null && s.getMessageId().getMessageId() == msgId) {
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Found a matching state by msgId");
state = s;
break;
}
}
}
if (state != null) {
if (state.getMessageId() == null) {
MessageId id = new MessageId();
id.setMessageId(msgId);
state.setMessageId(id);
}
state.receive(status);
if (state.wasSuccessful())
_sendingStates.remove(Long.valueOf(nonce));
long lifetime = state.getElapsed();
switch (status) {
case 1:
_context.statManager().addRateData("i2cp.receiveStatusTime.1", lifetime);
break;
// best effort codes unused
//case 2:
// _context.statManager().addRateData("i2cp.receiveStatusTime.2", lifetime, 0);
// break;
//case 3:
// _context.statManager().addRateData("i2cp.receiveStatusTime.3", lifetime, 0);
// break;
case 4:
_context.statManager().addRateData("i2cp.receiveStatusTime.4", lifetime);
break;
case 5:
_context.statManager().addRateData("i2cp.receiveStatusTime.5", lifetime);
break;
}
} else {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix() + "No matching state for messageId " + msgId + " / " + nonce
+ " w/ status = " + status);
}
}
/**
* Called whenever we want to reconnect (used only in the superclass). We need
* to override this to clear out the message state
*
*/
@Override
protected boolean reconnect() {
// even if we succeed in reconnecting, we want to clear the old states,
// since this will be a new sessionId
clearStates();
return super.reconnect();
}
private void clearStates() {
if (_sendingStates == null) // only null if overridden by I2PSimpleSession
return;
for (MessageState state : _sendingStates.values()) {
state.cancel();
}
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix() + "Disconnecting " + _sendingStates.size() + " states");
_sendingStates.clear();
}
}