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.BufferedInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.CoreVersion;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionListener;
import net.i2p.data.Base32;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.PrivateKey;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.i2cp.DestLookupMessage;
import net.i2p.data.i2cp.DestReplyMessage;
import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
import net.i2p.data.i2cp.GetDateMessage;
import net.i2p.data.i2cp.HostLookupMessage;
import net.i2p.data.i2cp.HostReplyMessage;
import net.i2p.data.i2cp.I2CPMessage;
import net.i2p.data.i2cp.I2CPMessageReader;
import net.i2p.data.i2cp.MessagePayloadMessage;
import net.i2p.data.i2cp.SessionId;
import net.i2p.data.i2cp.SessionStatusMessage;
import net.i2p.internal.I2CPMessageQueue;
import net.i2p.internal.InternalClientManager;
import net.i2p.internal.QueuedI2CPMessageReader;
import net.i2p.util.I2PAppThread;
import net.i2p.util.I2PSSLSocketFactory;
import net.i2p.util.LHMCache;
import net.i2p.util.Log;
import net.i2p.util.OrderedProperties;
import net.i2p.util.SimpleTimer2;
import net.i2p.util.SystemVersion;
import net.i2p.util.VersionComparator;
/**
* Implementation of an I2P session running over TCP. This class is NOT thread safe -
* only one thread should send messages at any given time
*
* Public only for clearCache().
* Except for methods defined in I2PSession and I2CPMessageEventListener,
* not maintained as a public API, not for external use.
* Use I2PClientFactory to get an I2PClient and then createSession().
*
* @author jrandom
*/
public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessageEventListener {
protected final Log _log;
/** who we are */
private final Destination _myDestination;
/** private key for decryption */
private final PrivateKey _privateKey;
/** private key for signing */
private /* final */ SigningPrivateKey _signingPrivateKey;
/** configuration options */
private final Properties _options;
/** this session's Id */
private SessionId _sessionId;
/** currently granted lease set, or null */
protected volatile LeaseSet _leaseSet;
// subsession stuff
// registered subsessions
private final List<SubSession> _subsessions;
// established subsessions
private final ConcurrentHashMap<SessionId, SubSession> _subsessionMap;
private final Object _subsessionLock = new Object();
private static final String MIN_SUBSESSION_VERSION = "0.9.21";
private volatile boolean _routerSupportsSubsessions;
/** hostname of router - will be null if in RouterContext */
protected final String _hostname;
/** port num to router - will be 0 if in RouterContext */
protected final int _portNum;
/** socket for comm */
protected Socket _socket;
/** reader that always searches for messages */
protected I2CPMessageReader _reader;
/** writer message queue */
protected ClientWriterRunner _writer;
/**
* Used for internal connections to the router.
* If this is set, _socket and _writer will be null.
* @since 0.8.3
*/
protected I2CPMessageQueue _queue;
/** who we send events to */
protected I2PSessionListener _sessionListener;
/** class that generates new messages */
protected final I2CPMessageProducer _producer;
/** map of Long --> MessagePayloadMessage */
protected Map<Long, MessagePayloadMessage> _availableMessages;
/** hashes of lookups we are waiting for */
protected final LinkedBlockingQueue<LookupWaiter> _pendingLookups = new LinkedBlockingQueue<LookupWaiter>();
private final AtomicInteger _lookupID = new AtomicInteger();
protected final Object _bwReceivedLock = new Object();
protected volatile int[] _bwLimits;
protected final I2PClientMessageHandlerMap _handlerMap;
/** used to separate things out so we can get rid of singletons */
protected final I2PAppContext _context;
/** monitor for waiting until a lease set has been granted */
protected final Object _leaseSetWait = new Object();
/**
* @since 0.9.8
*/
protected enum State {
/** @since 0.9.20 */
INIT,
OPENING,
/** @since 0.9.11 */
GOTDATE,
OPEN,
CLOSING,
CLOSED
}
protected State _state = State.INIT;
protected final Object _stateLock = new Object();
/**
* thread that we tell when new messages are available who then tells us
* to fetch them. The point of this is so that the fetch doesn't block the
* reading of other messages (in turn, potentially leading to deadlock)
*
*/
protected AvailabilityNotifier _availabilityNotifier;
private long _lastActivity;
private boolean _isReduced;
private final boolean _fastReceive;
private volatile boolean _routerSupportsFastReceive;
private volatile boolean _routerSupportsHostLookup;
protected static final int CACHE_MAX_SIZE = SystemVersion.isAndroid() ? 32 : 128;
/**
* Since 0.9.11, key is either a Hash or a String
* @since 0.8.9
*/
private static final Map<Object, Destination> _lookupCache = new LHMCache<Object, Destination>(CACHE_MAX_SIZE);
private static final String MIN_HOST_LOOKUP_VERSION = "0.9.11";
private static final boolean TEST_LOOKUP = false;
/** SSL interface (only) @since 0.8.3 */
protected static final String PROP_ENABLE_SSL = "i2cp.SSL";
protected static final String PROP_USER = "i2cp.username";
protected static final String PROP_PW = "i2cp.password";
/**
* Use Unix domain socket (or similar) to connect to a router
* @since 0.9.14
*/
protected static final String PROP_DOMAIN_SOCKET = "i2cp.domainSocket";
private static final long VERIFY_USAGE_TIME = 60*1000;
private static final long MAX_SEND_WAIT = 10*1000;
private static final String MIN_FAST_VERSION = "0.9.4";
/** @param routerVersion as rcvd in the SetDateMessage, may be null for very old routers */
void dateUpdated(String routerVersion) {
_routerSupportsFastReceive = _context.isRouterContext() ||
(routerVersion != null && routerVersion.length() > 0 &&
VersionComparator.comp(routerVersion, MIN_FAST_VERSION) >= 0);
_routerSupportsHostLookup = _context.isRouterContext() ||
TEST_LOOKUP ||
(routerVersion != null && routerVersion.length() > 0 &&
VersionComparator.comp(routerVersion, MIN_HOST_LOOKUP_VERSION) >= 0);
_routerSupportsSubsessions = _context.isRouterContext() ||
(routerVersion != null && routerVersion.length() > 0 &&
VersionComparator.comp(routerVersion, MIN_SUBSESSION_VERSION) >= 0);
synchronized (_stateLock) {
if (_state == State.OPENING) {
changeState(State.GOTDATE);
}
}
}
public static final int LISTEN_PORT = 7654;
private static final int BUF_SIZE = 32*1024;
/**
* for extension by SimpleSession (no dest)
*/
protected I2PSessionImpl(I2PAppContext context, Properties options,
I2PClientMessageHandlerMap handlerMap) {
this(context, options, handlerMap, null, false);
}
/*
* For extension by SubSession via I2PSessionMuxedImpl and I2PSessionImpl2
*
* @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 I2PSessionImpl(I2PSessionImpl primary, InputStream destKeyStream, Properties options) throws I2PSessionException {
this(primary.getContext(), options, primary.getHandlerMap(), primary.getProducer(), true);
_availabilityNotifier = new AvailabilityNotifier();
try {
readDestination(destKeyStream);
} catch (DataFormatException dfe) {
throw new I2PSessionException("Error reading the destination key stream", dfe);
} catch (IOException ioe) {
throw new I2PSessionException("Error reading the destination key stream", ioe);
}
}
/**
* Basic setup of finals
* @since 0.9.7
*/
private I2PSessionImpl(I2PAppContext context, Properties options,
I2PClientMessageHandlerMap handlerMap,
I2CPMessageProducer producer,
boolean hasDest) {
_context = context;
_handlerMap = handlerMap;
_log = context.logManager().getLog(getClass());
_subsessions = new CopyOnWriteArrayList<SubSession>();
_subsessionMap = new ConcurrentHashMap<SessionId, SubSession>(4);
if (options == null)
options = (Properties) System.getProperties().clone();
_options = loadConfig(options);
_hostname = getHost();
_portNum = getPort();
_fastReceive = Boolean.parseBoolean(_options.getProperty(I2PClient.PROP_FAST_RECEIVE));
if (hasDest) {
_producer = producer;
_availableMessages = new ConcurrentHashMap<Long, MessagePayloadMessage>();
_myDestination = new Destination();
_privateKey = new PrivateKey();
_signingPrivateKey = new SigningPrivateKey();
} else {
_producer = null;
_availableMessages = null;
_myDestination = null;
_privateKey = null;
_signingPrivateKey = null;
}
_routerSupportsFastReceive = _context.isRouterContext();
_routerSupportsHostLookup = _context.isRouterContext();
_routerSupportsSubsessions = _context.isRouterContext();
}
/**
* Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey
* from the destKeyStream, and using the specified options to connect to the router
*
* As of 0.9.19, defaults in options are honored.
*
* @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
*/
public I2PSessionImpl(I2PAppContext context, InputStream destKeyStream, Properties options) throws I2PSessionException {
this(context, options, new I2PClientMessageHandlerMap(context), new I2CPMessageProducer(context), true);
_availabilityNotifier = new AvailabilityNotifier();
try {
readDestination(destKeyStream);
} catch (DataFormatException dfe) {
throw new I2PSessionException("Error reading the destination key stream", dfe);
} catch (IOException ioe) {
throw new I2PSessionException("Error reading the destination key stream", ioe);
}
}
/**
* Router must be connected or was connected... for now.
*
* @return a new subsession, non-null
* @param privateKeyStream null for transient, if non-null must have same encryption keys as primary session
* and different signing keys
* @param opts subsession options if any, may be null
* @since 0.9.21
*/
public I2PSession addSubsession(InputStream privateKeyStream, Properties opts) throws I2PSessionException {
if (!_routerSupportsSubsessions)
throw new I2PSessionException("Router does not support subsessions");
SubSession sub;
synchronized(_subsessionLock) {
if (_subsessions.size() > _subsessionMap.size())
throw new I2PSessionException("Subsession request already pending");
sub = new SubSession(this, privateKeyStream, opts);
for (SubSession ss : _subsessions) {
if (ss.getDecryptionKey().equals(sub.getDecryptionKey()) &&
ss.getPrivateKey().equals(sub.getPrivateKey())) {
throw new I2PSessionException("Dup subsession");
}
}
_subsessions.add(sub);
}
synchronized (_stateLock) {
if (_state == State.OPEN) {
_producer.connect(sub);
} // else will be called in connect()
}
return sub;
}
/**
* @since 0.9.21
*/
public void removeSubsession(I2PSession session) {
if (!(session instanceof SubSession))
return;
synchronized(_subsessionLock) {
_subsessions.remove(session);
SessionId id = ((SubSession) session).getSessionId();
if (id != null)
_subsessionMap.remove(id);
/// tell the subsession
try {
// doesn't really throw
session.destroySession();
} catch (I2PSessionException ise) {}
}
}
/**
* @return a list of subsessions, non-null, does not include the primary session
* @since 0.9.21
*/
public List<I2PSession> getSubsessions() {
synchronized(_subsessionLock) {
return new ArrayList<I2PSession>(_subsessions);
}
}
/**
* Parse the config for anything we know about.
* Also fill in the authorization properties if missing.
*/
private final Properties loadConfig(Properties opts) {
Properties options = new Properties();
options.putAll(filter(opts));
// auto-add auth if required, not set in the options, and we are not in the same JVM
if ((!_context.isRouterContext()) &&
_context.getBooleanProperty("i2cp.auth") &&
((!opts.containsKey(PROP_USER)) || (!opts.containsKey(PROP_PW)))) {
String configUser = _context.getProperty(PROP_USER);
String configPW = _context.getProperty(PROP_PW);
if (configUser != null && configPW != null) {
options.setProperty(PROP_USER, configUser);
options.setProperty(PROP_PW, configPW);
}
}
if (options.getProperty(I2PClient.PROP_FAST_RECEIVE) == null)
options.setProperty(I2PClient.PROP_FAST_RECEIVE, "true");
if (options.getProperty(I2PClient.PROP_RELIABILITY) == null)
options.setProperty(I2PClient.PROP_RELIABILITY, "none");
return options;
}
/**
* Get I2CP host from the config
* @since 0.9.7 was in loadConfig()
*/
private String getHost() {
if (_context.isRouterContext())
// just for logging
return "[internal connection]";
else if (SystemVersion.isAndroid() &&
Boolean.parseBoolean(_options.getProperty(PROP_DOMAIN_SOCKET)))
// just for logging
return "[Domain socket connection]";
return _options.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
}
/**
* Get I2CP port from the config
* @since 0.9.7 was in loadConfig()
*/
private int getPort() {
if (_context.isRouterContext() ||
(SystemVersion.isAndroid() &&
Boolean.parseBoolean(_options.getProperty(PROP_DOMAIN_SOCKET))))
// just for logging
return 0;
String portNum = _options.getProperty(I2PClient.PROP_TCP_PORT, LISTEN_PORT + "");
try {
return Integer.parseInt(portNum);
} catch (NumberFormatException nfe) {
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix() + "Invalid port number specified, defaulting to "
+ LISTEN_PORT, nfe);
return LISTEN_PORT;
}
}
/**
* Save some memory, don't pass along the pointless properties.
* As of 0.9.19, defaults from options will be promoted to real values in rv.
* @return a new Properties without defaults
*/
private Properties filter(Properties options) {
Properties rv = new Properties();
for (String key : options.stringPropertyNames()) {
if (key.startsWith("java.") ||
key.startsWith("user.") ||
key.startsWith("os.") ||
key.startsWith("sun.") ||
key.startsWith("awt.") ||
key.startsWith("file.") ||
key.equals("line.separator") ||
key.equals("path.separator") ||
key.equals("prng.buffers") ||
key.equals("router.trustedUpdateKeys") ||
key.startsWith("router.update") ||
key.startsWith("routerconsole.") ||
key.startsWith("time.") ||
key.startsWith("stat.") ||
key.startsWith("gnu.") || // gnu JVM
key.startsWith("net.i2p.router.web.") || // console nonces
key.equals("loggerFilenameOverride") ||
key.equals("router.version") ||
key.equals("i2p.dir.base") ||
key.startsWith("networkaddress.cache.") ||
key.startsWith("http.") ||
key.startsWith("jetty.") ||
key.startsWith("org.mortbay.") ||
key.startsWith("wrapper.")) {
if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping property: " + key);
continue;
}
String val = options.getProperty(key);
// Long strings MUST be removed, even in router context,
// as the session config properties must be serialized to be signed.
// fixme, bytes could still be over 255 (unlikely)
if (key.length() > 255 || val.length() > 255) {
if (_log.shouldLog(Log.WARN))
_log.warn("Not passing on property ["
+ key
+ "] in the session config, key or value is too long (max = 255): "
+ val);
} else {
rv.setProperty(key, val);
}
}
return rv;
}
/**
* Update the tunnel and bandwidth settings
* @since 0.8.4
*/
public void updateOptions(Properties options) {
_options.putAll(filter(options));
_producer.updateBandwidth(this);
try {
_producer.updateTunnels(this, 0);
} catch (I2PSessionException ise) {}
}
/**
* @since 0.9.4
*/
public boolean getFastReceive() {
return _fastReceive && _routerSupportsFastReceive;
}
void setLeaseSet(LeaseSet ls) {
_leaseSet = ls;
if (ls != null) {
synchronized (_leaseSetWait) {
_leaseSetWait.notifyAll();
}
}
}
LeaseSet getLeaseSet() {
return _leaseSet;
}
protected void changeState(State state) {
if (_log.shouldInfo())
_log.info(getPrefix() + "Change state to " + state);
synchronized (_stateLock) {
_state = state;
_stateLock.notifyAll();
}
}
/**
* Load up the destKeyFile for our Destination, PrivateKey, and SigningPrivateKey
*
* @throws DataFormatException if the file is in the wrong format or keys are invalid
* @throws IOException if there is a problem reading the file
*/
private void readDestination(InputStream destKeyStream) throws DataFormatException, IOException {
_myDestination.readBytes(destKeyStream);
_privateKey.readBytes(destKeyStream);
_signingPrivateKey = new SigningPrivateKey(_myDestination.getSigningPublicKey().getType());
_signingPrivateKey.readBytes(destKeyStream);
}
/**
* Connect to the router and establish a session. This call blocks until
* a session is granted.
*
* Should be threadsafe, other threads will block until complete.
* Disconnect / destroy from another thread may be called simultaneously and
* will (should?) interrupt the connect.
*
* Connecting a primary session will not automatically connect subsessions.
* Connecting a subsession will automatically connect the primary session
* if not previously connected.
*
* @throws I2PSessionException if there is a configuration error or the router is
* not reachable
*/
public void connect() throws I2PSessionException {
synchronized(_stateLock) {
boolean wasOpening = false;
boolean loop = true;
while (loop) {
switch (_state) {
case INIT:
loop = false;
break;
case CLOSED:
if (wasOpening)
throw new I2PSessionException("connect by other thread failed");
loop = false;
break;
case OPENING:
case GOTDATE:
wasOpening = true;
try {
_stateLock.wait(10*1000);
} catch (InterruptedException ie) {
throw new I2PSessionException("Interrupted", ie);
}
break;
case CLOSING:
throw new I2PSessionException("close in progress");
case OPEN:
return;
}
}
changeState(State.OPENING);
}
_availabilityNotifier.stopNotifying();
if ( (_options != null) &&
(I2PClient.PROP_RELIABILITY_GUARANTEED.equals(_options.getProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT))) ) {
if (_log.shouldLog(Log.ERROR))
_log.error("I2CP guaranteed delivery mode has been removed, using best effort.");
}
boolean success = false;
long startConnect = _context.clock().now();
try {
// protect w/ closeSocket()
synchronized(_stateLock) {
// If we are in the router JVM, connect using the internal queue
if (_context.isRouterContext()) {
// _socket and _writer remain null
InternalClientManager mgr = _context.internalClientManager();
if (mgr == null)
throw new I2PSessionException("Router is not ready for connections");
// the following may throw an I2PSessionException
_queue = mgr.connect();
_reader = new QueuedI2CPMessageReader(_queue, this);
} else {
if (SystemVersion.isAndroid() &&
_options.getProperty(PROP_DOMAIN_SOCKET) != null) {
try {
Class<?> clazz = Class.forName("net.i2p.client.DomainSocketFactory");
Constructor<?> ctor = clazz.getDeclaredConstructor(I2PAppContext.class);
Object fact = ctor.newInstance(_context);
Method createSocket = clazz.getDeclaredMethod("createSocket", String.class);
try {
_socket = (Socket) createSocket.invoke(fact, _options.getProperty(PROP_DOMAIN_SOCKET));
} catch (InvocationTargetException e) {
throw new I2PSessionException("Cannot create domain socket", e);
}
} catch (ClassNotFoundException e) {
throw new I2PSessionException("Cannot load DomainSocketFactory", e);
} catch (NoSuchMethodException e) {
throw new I2PSessionException("Cannot load DomainSocketFactory", e);
} catch (InstantiationException e) {
throw new I2PSessionException("Cannot load DomainSocketFactory", e);
} catch (IllegalAccessException e) {
throw new I2PSessionException("Cannot load DomainSocketFactory", e);
} catch (InvocationTargetException e) {
throw new I2PSessionException("Cannot load DomainSocketFactory", e);
}
} else if (Boolean.parseBoolean(_options.getProperty(PROP_ENABLE_SSL))) {
try {
I2PSSLSocketFactory fact = new I2PSSLSocketFactory(_context, false, "certificates/i2cp");
_socket = fact.createSocket(_hostname, _portNum);
_socket.setKeepAlive(true);
} catch (GeneralSecurityException gse) {
IOException ioe = new IOException("SSL Fail");
ioe.initCause(gse);
throw ioe;
}
} else {
_socket = new Socket(_hostname, _portNum);
_socket.setKeepAlive(true);
}
// _socket.setSoTimeout(1000000); // Uhmmm we could really-really use a real timeout, and handle it.
OutputStream out = _socket.getOutputStream();
out.write(I2PClient.PROTOCOL_BYTE);
out.flush();
_writer = new ClientWriterRunner(out, this);
_writer.startWriting();
InputStream in = new BufferedInputStream(_socket.getInputStream(), BUF_SIZE);
_reader = new I2CPMessageReader(in, this);
}
}
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "before startReading");
_reader.startReading();
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Before getDate");
Properties auth = null;
if ((!_context.isRouterContext()) && _options.containsKey(PROP_USER) && _options.containsKey(PROP_PW)) {
// Only supported by routers 0.9.11 or higher, but we don't know the version yet.
// Auth will also be sent in the SessionConfig.
auth = new OrderedProperties();
auth.setProperty(PROP_USER, _options.getProperty(PROP_USER));
auth.setProperty(PROP_PW, _options.getProperty(PROP_PW));
}
sendMessage_unchecked(new GetDateMessage(CoreVersion.VERSION, auth));
waitForDate();
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Before producer.connect()");
_producer.connect(this);
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After producer.connect()");
// wait until we have created a lease set
int waitcount = 0;
while (_leaseSet == null) {
if (waitcount++ > 5*60) {
throw new IOException("No tunnels built after waiting 5 minutes. Your network connection may be down, or there is severe network congestion.");
}
synchronized (_leaseSetWait) {
// InterruptedException caught below
_leaseSetWait.wait(1000);
}
// if we got a disconnect message while waiting
if (isClosed())
throw new IOException("Disconnected from router while waiting for tunnels");
}
if (_log.shouldLog(Log.INFO)) {
long connected = _context.clock().now();
_log.info(getPrefix() + "Lease set created with inbound tunnels after "
+ (connected - startConnect)
+ "ms - ready to participate in the network!");
}
Thread notifier = new I2PAppThread(_availabilityNotifier, "ClientNotifier " + getPrefix(), true);
notifier.start();
startIdleMonitor();
startVerifyUsage();
success = true;
// now send CreateSessionMessages for all subsessions, one at a time, must wait for each response
synchronized(_subsessionLock) {
for (SubSession ss : _subsessions) {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix() + "Connecting subsession " + ss);
_producer.connect(ss);
}
}
} catch (InterruptedException ie) {
throw new I2PSessionException("Interrupted", ie);
} catch (UnknownHostException uhe) {
throw new I2PSessionException(getPrefix() + "Cannot connect to the router on " + _hostname + ':' + _portNum, uhe);
} catch (IOException ioe) {
// Generate the best error message as this will be logged
String msg;
if (_context.isRouterContext())
msg = "Failed to build tunnels";
else if (SystemVersion.isAndroid() &&
_options.getProperty(PROP_DOMAIN_SOCKET) != null)
msg = "Failed to bind to the router on " + _options.getProperty(PROP_DOMAIN_SOCKET) + " and build tunnels";
else
msg = "Cannot connect to the router on " + _hostname + ':' + _portNum + " and build tunnels";
throw new I2PSessionException(getPrefix() + msg, ioe);
} finally {
if (success) {
changeState(State.OPEN);
} else {
_availabilityNotifier.stopNotifying();
synchronized(_stateLock) {
changeState(State.CLOSING);
try {
_producer.disconnect(this);
} catch (I2PSessionException ipe) {}
closeSocket();
}
}
}
}
/**
* @since 0.9.11 moved from connect()
*/
protected void waitForDate() throws InterruptedException, IOException {
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After getDate / begin waiting for a response");
int waitcount = 0;
while (true) {
if (waitcount++ > 30) {
throw new IOException("No handshake received from the router");
}
synchronized(_stateLock) {
if (_state == State.GOTDATE)
break;
if (_state != State.OPENING && _state != State.INIT)
throw new IOException("Socket closed, state=" + _state);
// InterruptedException caught by caller
_stateLock.wait(1000);
}
}
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After received a SetDate response");
}
/**
* Pull the unencrypted data from the message that we've already prefetched and
* notified the user that its available.
*
*/
public byte[] receiveMessage(int msgId) throws I2PSessionException {
MessagePayloadMessage msg = _availableMessages.remove(Long.valueOf(msgId));
if (msg == null) {
_log.error("Receive message " + msgId + " had no matches");
return null;
}
updateActivity();
return msg.getPayload().getUnencryptedData();
}
/**
* Report abuse with regards to the given messageId
*/
public void reportAbuse(int msgId, int severity) throws I2PSessionException {
verifyOpen();
_producer.reportAbuse(this, msgId, severity);
}
public abstract void receiveStatus(int msgId, long nonce, int status);
/****** no end-to-end crypto
protected static final Set createNewTags(int num) {
Set tags = new HashSet();
for (int i = 0; i < num; i++)
tags.add(new SessionTag(true));
return tags;
}
*******/
/**
* Recieve a payload message and let the app know its available
*/
public void addNewMessage(MessagePayloadMessage msg) {
Long mid = Long.valueOf(msg.getMessageId());
_availableMessages.put(mid, msg);
long id = msg.getMessageId();
byte data[] = msg.getPayload().getUnencryptedData();
if ((data == null) || (data.length <= 0)) {
if (_log.shouldLog(Log.CRIT))
_log.log(Log.CRIT, getPrefix() + "addNewMessage of a message with no unencrypted data",
new Exception("Empty message"));
} else {
int size = data.length;
_availabilityNotifier.available(id, size);
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix() + "Notified availability for session " + _sessionId + ", message " + id);
}
}
/**
* Fire up a periodic task to check for unclaimed messages
* @since 0.9.1
*/
protected void startVerifyUsage() {
new VerifyUsage();
}
/**
* Check for unclaimed messages, without wastefully setting a timer for each
* message. Just copy all unclaimed ones and check some time later.
*/
private class VerifyUsage extends SimpleTimer2.TimedEvent {
private final List<Long> toCheck = new ArrayList<Long>();
public VerifyUsage() {
super(_context.simpleTimer2(), VERIFY_USAGE_TIME);
}
public void timeReached() {
if (isClosed())
return;
//if (_log.shouldLog(Log.DEBUG))
// _log.debug(getPrefix() + " VerifyUsage of " + toCheck.size());
if (!toCheck.isEmpty()) {
for (Long msgId : toCheck) {
MessagePayloadMessage removed = _availableMessages.remove(msgId);
if (removed != null)
_log.error(getPrefix() + " Client not responding? Message not processed! id=" + msgId + ": " + removed);
}
toCheck.clear();
}
toCheck.addAll(_availableMessages.keySet());
schedule(VERIFY_USAGE_TIME);
}
}
/**
* This notifies the client of payload messages.
* Needs work.
*/
protected class AvailabilityNotifier implements Runnable {
private final List<Long> _pendingIds;
private final List<Integer> _pendingSizes;
private volatile boolean _alive;
public AvailabilityNotifier() {
_pendingIds = new ArrayList<Long>(2);
_pendingSizes = new ArrayList<Integer>(2);
}
public void stopNotifying() {
_alive = false;
synchronized (AvailabilityNotifier.this) {
AvailabilityNotifier.this.notifyAll();
}
}
public void available(long msgId, int size) {
synchronized (AvailabilityNotifier.this) {
_pendingIds.add(Long.valueOf(msgId));
_pendingSizes.add(Integer.valueOf(size));
AvailabilityNotifier.this.notifyAll();
}
}
public void run() {
_alive = true;
while (_alive) {
Long msgId = null;
Integer size = null;
synchronized (AvailabilityNotifier.this) {
if (_pendingIds.isEmpty()) {
try {
AvailabilityNotifier.this.wait();
} catch (InterruptedException ie) { // nop
}
}
if (!_pendingIds.isEmpty()) {
msgId = _pendingIds.remove(0);
size = _pendingSizes.remove(0);
}
}
if ( (msgId != null) && (size != null) ) {
if (_sessionListener != null) {
try {
long before = System.currentTimeMillis();
_sessionListener.messageAvailable(I2PSessionImpl.this, msgId.intValue(), size.intValue());
long duration = System.currentTimeMillis() - before;
if ((duration > 100) && _log.shouldLog(Log.INFO))
_log.info("Message availability notification for " + msgId.intValue() + " took "
+ duration + " to " + _sessionListener);
} catch (RuntimeException e) {
_log.log(Log.CRIT, "Error notifying app of message availability", e);
}
} else {
_log.log(Log.CRIT, "Unable to notify an app that " + msgId + " of size " + size + " is available!");
}
}
}
}
}
/**
* The I2CPMessageEventListener callback.
* Recieve notification of some I2CP message and handle it if possible.
*
* We route the message based on message type AND session ID.
*
* The following types never contain a session ID and are not routable to
* a subsession:
* BandwidthLimitsMessage, DestReplyMessage
*
* The following types may not contain a valid session ID
* even when intended for a subsession, so we must take special care:
* SessionStatusMessage
*
* @param reader unused
*/
public void messageReceived(I2CPMessageReader reader, I2CPMessage message) {
int type = message.getType();
SessionId id = message.sessionId();
SessionId currId = _sessionId;
if (id == null || id.equals(currId) ||
(currId == null && id != null && type == SessionStatusMessage.MESSAGE_TYPE) ||
((id == null || id.getSessionId() == 65535) &&
(type == HostReplyMessage.MESSAGE_TYPE || type == DestReplyMessage.MESSAGE_TYPE))) {
// it's for us
I2CPMessageHandler handler = _handlerMap.getHandler(type);
if (handler != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug(getPrefix() + "Message received of type " + type
+ " to be handled by " + handler.getClass().getSimpleName());
handler.handleMessage(message, this);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix() + "Unknown message or unhandleable message received: type = "
+ type);
}
} else {
SubSession sub = _subsessionMap.get(id);
if (sub != null) {
// it's for a subsession
if (_log.shouldLog(Log.DEBUG))
_log.debug(getPrefix() + "Message received of type " + type
+ " to be handled by " + sub);
sub.messageReceived(reader, message);
} else if (id != null && type == SessionStatusMessage.MESSAGE_TYPE) {
// look for a subsession without a session
synchronized (_subsessionLock) {
for (SubSession sess : _subsessions) {
if (sess.getSessionId() == null) {
sess.messageReceived(reader, message);
id = sess.getSessionId();
if (id != null) {
if (id.equals(_sessionId)) {
// shouldnt happen
sess.setSessionId(null);
if (_log.shouldLog(Log.WARN))
_log.warn("Dup or our session id " + id);
} else {
SubSession old = _subsessionMap.putIfAbsent(id, sess);
if (old != null) {
// shouldnt happen
sess.setSessionId(null);
if (_log.shouldLog(Log.WARN))
_log.warn("Dup session id " + id);
}
}
}
return;
}
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix() + "No session " + id + " to handle message: type = "
+ type);
}
}
} else {
// it's for nobody
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix() + "No session " + id + " to handle message: type = "
+ type);
}
}
}
/**
* The I2CPMessageEventListener callback.
* Recieve notifiation of an error reading the I2CP stream.
* @param reader unused
* @param error non-null
*/
public void readError(I2CPMessageReader reader, Exception error) {
propogateError("There was an error reading data", error);
disconnect();
}
/**
* Retrieve the destination of the session
*/
public Destination getMyDestination() { return _myDestination; }
/**
* Retrieve the decryption PrivateKey
*/
public PrivateKey getDecryptionKey() { return _privateKey; }
/**
* Retrieve the signing SigningPrivateKey
*/
public SigningPrivateKey getPrivateKey() { return _signingPrivateKey; }
/**
* Retrieve the helper that generates I2CP messages
*/
I2CPMessageProducer getProducer() { return _producer; }
/**
* For Subsessions
* @since 0.9.21
*/
I2PClientMessageHandlerMap getHandlerMap() { return _handlerMap; }
/**
* For Subsessions
* @since 0.9.21
*/
I2PAppContext getContext() { return _context; }
/**
* Retrieve the configuration options, filtered.
* All defaults passed in via constructor have been promoted to the primary map.
*
* @return non-null, if insantiated with null options, this will be the System properties.
*/
Properties getOptions() { return _options; }
/**
* Retrieve the session's ID
*/
SessionId getSessionId() { return _sessionId; }
void setSessionId(SessionId id) { _sessionId = id; }
/** configure the listener */
public void setSessionListener(I2PSessionListener lsnr) { _sessionListener = lsnr; }
/**
* Has the session been closed (or not yet connected)?
* False when open and during transitions. Synchronized.
*/
public boolean isClosed() {
synchronized (_stateLock) {
return _state == State.CLOSED || _state == State.INIT;
}
}
/**
* Throws I2PSessionException if uninitialized, closed or closing.
* Blocks if opening.
*
* @since 0.9.23
*/
protected void verifyOpen() throws I2PSessionException {
synchronized (_stateLock) {
while (true) {
switch (_state) {
case INIT:
throw new I2PSessionException("Not open, must call connect() first");
case OPENING: // fall thru
case GOTDATE:
try {
_stateLock.wait(5*1000);
continue;
} catch (InterruptedException ie) {
throw new I2PSessionException("Interrupted", ie);
}
case OPEN:
return;
case CLOSING: // fall thru
case CLOSED:
throw new I2PSessionException("Already closed");
}
}
}
}
/**
* Deliver an I2CP message to the router
* As of 0.9.3, may block for several seconds if the write queue to the router is full
*
* @throws I2PSessionException if the message is malformed or there is an error writing it out
*/
void sendMessage(I2CPMessage message) throws I2PSessionException {
verifyOpen();
sendMessage_unchecked(message);
}
/**
* Deliver an I2CP message to the router.
* Does NOT check state. Call only from connect() or other methods that need to
* send messages when not in OPEN state.
*
* @throws I2PSessionException if the message is malformed or there is an error writing it out
* @since 0.9.23
*/
void sendMessage_unchecked(I2CPMessage message) throws I2PSessionException {
if (_queue != null) {
// internal
try {
if (!_queue.offer(message, MAX_SEND_WAIT))
throw new I2PSessionException("Timed out waiting while write queue was full");
} catch (InterruptedException ie) {
throw new I2PSessionException("Interrupted", ie);
}
} else {
ClientWriterRunner writer = _writer;
if (writer == null) {
throw new I2PSessionException("Already closed or not open");
} else {
writer.addMessage(message);
}
}
}
/**
* Pass off the error to the listener
* Misspelled, oh well.
* @param error non-null
*/
void propogateError(String msg, Throwable error) {
// Only log as WARN if the router went away
int level;
String msgpfx;
if (error instanceof EOFException) {
level = Log.WARN;
msgpfx = "Router closed connection: ";
} else {
level = Log.ERROR;
msgpfx = "Error occurred communicating with router: ";
}
if (_log.shouldLog(level))
_log.log(level, getPrefix() + msgpfx + msg, error);
if (_sessionListener != null) _sessionListener.errorOccurred(this, msg, error);
}
/**
* Tear down the session, and do NOT reconnect.
*
* Blocks if session has not been fully started.
*/
public void destroySession() {
destroySession(true);
}
/**
* Tear down the session, and do NOT reconnect.
*
* Will interrupt an open in progress.
*/
public void destroySession(boolean sendDisconnect) {
synchronized(_stateLock) {
if (_state == State.CLOSING || _state == State.CLOSED || _state == State.INIT)
return;
changeState(State.CLOSING);
}
if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Destroy the session", new Exception("DestroySession()"));
if (sendDisconnect && _producer != null) { // only null if overridden by I2PSimpleSession
try {
_producer.disconnect(this);
} catch (I2PSessionException ipe) {
//propogateError("Error destroying the session", ipe);
if (_log.shouldLog(Log.WARN))
_log.warn("Error destroying the session", ipe);
}
}
// SimpleSession does not initialize
if (_availabilityNotifier != null)
_availabilityNotifier.stopNotifying();
closeSocket();
_subsessionMap.clear();
if (_sessionListener != null) _sessionListener.disconnected(this);
}
/**
* Close the socket carefully.
*/
private void closeSocket() {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix() + "Closing the socket", new Exception("closeSocket"));
synchronized(_stateLock) {
changeState(State.CLOSING);
locked_closeSocket();
changeState(State.CLOSED);
}
synchronized (_subsessionLock) {
for (SubSession sess : _subsessions) {
sess.changeState(State.CLOSED);
sess.setSessionId(null);
sess.setLeaseSet(null);
}
}
}
/**
* Close the socket carefully.
* Caller must change state.
*/
private void locked_closeSocket() {
if (_reader != null) {
_reader.stopReading();
_reader = null;
}
if (_queue != null) {
// internal
_queue.close();
}
if (_writer != null) {
_writer.stopWriting();
_writer = null;
}
if (_socket != null) {
try {
_socket.close();
} catch (IOException ioe) {
propogateError("Caught an IO error closing the socket. ignored", ioe);
} finally {
_socket = null; // so when propogateError calls closeSocket, it doesnt loop
}
}
setSessionId(null);
setLeaseSet(null);
}
/**
* The I2CPMessageEventListener callback.
* Recieve notification that the I2CP connection was disconnected.
* @param reader unused
*/
public void disconnected(I2CPMessageReader reader) {
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Disconnected", new Exception("Disconnected"));
disconnect();
}
/**
* Will interrupt a connect in progress.
*/
protected void disconnect() {
State oldState;
synchronized(_stateLock) {
if (_state == State.CLOSING || _state == State.CLOSED || _state == State.INIT)
return;
oldState = _state;
changeState(State.CLOSING);
}
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Disconnect() called", new Exception("Disconnect"));
// don't try to reconnect if it failed before GETTDATE
if (oldState != State.OPENING && shouldReconnect()) {
if (reconnect()) {
if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "I2CP reconnection successful");
return;
}
if (_log.shouldLog(Log.ERROR)) _log.error(getPrefix() + "I2CP reconnection failed");
}
if (_log.shouldLog(Log.ERROR))
_log.error(getPrefix() + "Disconned from the router, and not trying to reconnect");
if (_sessionListener != null) _sessionListener.disconnected(this);
closeSocket();
changeState(State.CLOSED);
// break out of wait for initial LS in connect()
synchronized (_leaseSetWait) {
_leaseSetWait.notifyAll();
}
}
private final static int MAX_RECONNECT_DELAY = 320*1000;
private final static int BASE_RECONNECT_DELAY = 10*1000;
protected boolean shouldReconnect() {
return true;
}
protected boolean reconnect() {
closeSocket();
if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Reconnecting...");
int i = 0;
while (true) {
long delay = BASE_RECONNECT_DELAY << i;
i++;
if ( (delay > MAX_RECONNECT_DELAY) || (delay <= 0) )
delay = MAX_RECONNECT_DELAY;
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
return false;
}
try {
connect();
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix() + "Reconnected on attempt " + i);
return true;
} catch (I2PSessionException ise) {
if (_log.shouldLog(Log.ERROR))
_log.error(getPrefix() + "Error reconnecting on attempt " + i, ise);
}
}
}
/**
* try hard to make a decent identifier as this will appear in error logs
*/
protected String getPrefix() {
StringBuilder buf = new StringBuilder();
buf.append('[');
buf.append(_state.toString()).append(' ');
String s = _options.getProperty("inbound.nickname");
if (s != null)
buf.append(s);
else
buf.append(getClass().getSimpleName());
SessionId id = _sessionId;
if (id != null)
buf.append(" #").append(id.getSessionId());
buf.append("]: ");
return buf.toString();
}
/**
* Called by the message handler
* on reception of DestReplyMessage
* @param d non-null
*/
void destReceived(Destination d) {
Hash h = d.calculateHash();
synchronized (_lookupCache) {
_lookupCache.put(h, d);
}
for (LookupWaiter w : _pendingLookups) {
if (h.equals(w.hash)) {
synchronized (w) {
w.destination = d;
w.notifyAll();
}
}
}
}
/**
* Called by the message handler
* on reception of DestReplyMessage
* @param h non-null
*/
void destLookupFailed(Hash h) {
for (LookupWaiter w : _pendingLookups) {
if (h.equals(w.hash)) {
synchronized (w) {
w.notifyAll();
}
}
}
}
/**
* Called by the message handler
* on reception of HostReplyMessage
* @param d non-null
* @since 0.9.11
*/
void destReceived(long nonce, Destination d) {
// notify by nonce and hash
Hash h = d.calculateHash();
for (LookupWaiter w : _pendingLookups) {
if (nonce == w.nonce || h.equals(w.hash)) {
synchronized (_lookupCache) {
if (w.name != null)
_lookupCache.put(w.name, d);
_lookupCache.put(h, d);
}
synchronized (w) {
w.destination = d;
w.notifyAll();
}
}
}
}
/**
* Called by the message handler
* on reception of HostReplyMessage
* @since 0.9.11
*/
void destLookupFailed(long nonce) {
for (LookupWaiter w : _pendingLookups) {
if (nonce == w.nonce) {
synchronized (w) {
w.notifyAll();
}
}
}
}
/** called by the message handler */
void bwReceived(int[] i) {
_bwLimits = i;
synchronized (_bwReceivedLock) {
_bwReceivedLock.notifyAll();
}
}
/**
* Simple object to wait for lookup replies
* @since 0.8.3
*/
private static class LookupWaiter {
/** the request (Hash mode) */
public final Hash hash;
/** the request (String mode) */
public final String name;
/** the request (nonce mode) */
public final long nonce;
/** the reply; synch on this */
public Destination destination;
public LookupWaiter(Hash h) {
this(h, -1);
}
/** @since 0.9.11 */
public LookupWaiter(Hash h, long nonce) {
this.hash = h;
this.name = null;
this.nonce = nonce;
}
/** @since 0.9.11 */
public LookupWaiter(String name, long nonce) {
this.hash = null;
this.name = name;
this.nonce = nonce;
}
}
/** @since 0.9.20 */
public static void clearCache() {
synchronized (_lookupCache) {
_lookupCache.clear();
}
}
/**
* Blocking. Waits a max of 10 seconds by default.
* See lookupDest with maxWait parameter to change.
* Implemented in 0.8.3 in I2PSessionImpl;
* previously was available only in I2PSimpleSession.
* Multiple outstanding lookups are now allowed.
* @return null on failure
*/
public Destination lookupDest(Hash h) throws I2PSessionException {
return lookupDest(h, 10*1000);
}
/**
* Blocking.
* @param maxWait ms
* @since 0.8.3
* @return null on failure
*/
public Destination lookupDest(Hash h, long maxWait) throws I2PSessionException {
synchronized (_lookupCache) {
Destination rv = _lookupCache.get(h);
if (rv != null)
return rv;
}
synchronized (_stateLock) {
// not before GOTDATE
if (_state == State.CLOSED ||
_state == State.INIT ||
_state == State.OPENING) {
if (_log.shouldLog(Log.INFO))
_log.info("Session closed, cannot lookup " + h);
return null;
}
}
LookupWaiter waiter;
long nonce;
if (_routerSupportsHostLookup) {
nonce = _lookupID.incrementAndGet() & 0x7fffffff;
waiter = new LookupWaiter(h, nonce);
} else {
nonce = 0; // won't be used
waiter = new LookupWaiter(h);
}
_pendingLookups.offer(waiter);
Destination rv = null;
try {
if (_routerSupportsHostLookup) {
if (_log.shouldLog(Log.INFO))
_log.info("Sending HostLookup for " + h);
SessionId id = _sessionId;
if (id == null)
id = new SessionId(65535);
sendMessage_unchecked(new HostLookupMessage(id, h, nonce, maxWait));
} else {
if (_log.shouldLog(Log.INFO))
_log.info("Sending DestLookup for " + h);
sendMessage_unchecked(new DestLookupMessage(h));
}
try {
synchronized (waiter) {
waiter.wait(maxWait);
rv = waiter.destination;
}
} catch (InterruptedException ie) {
throw new I2PSessionException("Interrupted", ie);
}
} finally {
_pendingLookups.remove(waiter);
}
return rv;
}
/**
* Ask the router to lookup a Destination by host name.
* Blocking. Waits a max of 10 seconds by default.
*
* This only makes sense for a b32 hostname, OR outside router context.
* Inside router context, just query the naming service.
* Outside router context, this does NOT query the context naming service.
* Do that first if you expect a local addressbook.
*
* This will log a warning for non-b32 in router context.
*
* See interface for suggested implementation.
*
* Requires router side to be 0.9.11 or higher. If the router is older,
* this will return null immediately.
*
* @since 0.9.11
*/
public Destination lookupDest(String name) throws I2PSessionException {
return lookupDest(name, 10*1000);
}
/**
* Ask the router to lookup a Destination by host name.
* Blocking. See above for details.
* @param maxWait ms
* @since 0.9.11
* @return null on failure
*/
public Destination lookupDest(String name, long maxWait) throws I2PSessionException {
if (name.length() == 0)
return null;
// Shortcut for b64
if (name.length() >= 516) {
try {
return new Destination(name);
} catch (DataFormatException dfe) {
return null;
}
}
// won't fit in Mapping
if (name.length() >= 256 && !_context.isRouterContext())
return null;
synchronized (_lookupCache) {
Destination rv = _lookupCache.get(name);
if (rv != null)
return rv;
}
if (isClosed()) {
if (_log.shouldLog(Log.INFO))
_log.info("Session closed, cannot lookup " + name);
return null;
}
if (!_routerSupportsHostLookup) {
// do them a favor and convert to Hash lookup
if (name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p"))
return lookupDest(Hash.create(Base32.decode(name.toLowerCase(Locale.US).substring(0, 52))), maxWait);
// else unsupported
if (_log.shouldLog(Log.WARN))
_log.warn("Router does not support HostLookup for " + name);
return null;
}
int nonce = _lookupID.incrementAndGet() & 0x7fffffff;
LookupWaiter waiter = new LookupWaiter(name, nonce);
_pendingLookups.offer(waiter);
Destination rv = null;
try {
if (_log.shouldLog(Log.INFO))
_log.info("Sending HostLookup for " + name);
SessionId id = _sessionId;
if (id == null)
id = new SessionId(65535);
sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait));
try {
synchronized (waiter) {
waiter.wait(maxWait);
rv = waiter.destination;
}
} catch (InterruptedException ie) {
throw new I2PSessionException("Interrupted", ie);
}
} finally {
_pendingLookups.remove(waiter);
}
return rv;
}
/**
* Blocking. Waits a max of 5 seconds.
* But shouldn't take long.
* Implemented in 0.8.3 in I2PSessionImpl;
* previously was available only in I2PSimpleSession.
* Multiple outstanding lookups are now allowed.
* @return null on failure
*/
public int[] bandwidthLimits() throws I2PSessionException {
synchronized (_stateLock) {
// not before GOTDATE
if (_state == State.CLOSED ||
_state == State.INIT ||
_state == State.OPENING) {
if (_log.shouldLog(Log.INFO))
_log.info("Session closed, cannot get bw limits");
return null;
}
}
sendMessage_unchecked(new GetBandwidthLimitsMessage());
try {
synchronized (_bwReceivedLock) {
_bwReceivedLock.wait(5*1000);
}
} catch (InterruptedException ie) {
throw new I2PSessionException("Interrupted", ie);
}
return _bwLimits;
}
protected void updateActivity() {
_lastActivity = _context.clock().now();
if (_isReduced) {
_isReduced = false;
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix() + "Restoring original tunnel quantity");
try {
_producer.updateTunnels(this, 0);
} catch (I2PSessionException ise) {
_log.error(getPrefix() + "bork restore from reduced");
}
}
}
public long lastActivity() {
return _lastActivity;
}
public void setReduced() {
_isReduced = true;
}
private void startIdleMonitor() {
_isReduced = false;
boolean reduce = Boolean.parseBoolean(_options.getProperty("i2cp.reduceOnIdle"));
boolean close = Boolean.parseBoolean(_options.getProperty("i2cp.closeOnIdle"));
if (reduce || close) {
updateActivity();
_context.simpleTimer2().addEvent(new SessionIdleTimer(_context, this, reduce, close), SessionIdleTimer.MINIMUM_TIME);
}
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder(32);
buf.append("Session: ");
if (_myDestination != null)
buf.append(_myDestination.calculateHash().toBase64().substring(0, 4));
else
buf.append("[null dest]");
buf.append(getPrefix());
return buf.toString();
}
}