package net.i2p.router.client;
/*
* 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.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.CoreVersion;
import net.i2p.client.I2PSessionException;
import net.i2p.crypto.SessionKeyManager;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.Payload;
import net.i2p.data.i2cp.I2CPMessage;
import net.i2p.data.i2cp.I2CPMessageException;
import net.i2p.data.i2cp.MessageId;
import net.i2p.data.i2cp.MessageStatusMessage;
import net.i2p.data.i2cp.SessionConfig;
import net.i2p.data.i2cp.SessionId;
import net.i2p.data.i2cp.SessionStatusMessage;
import net.i2p.data.i2cp.SetDateMessage;
import net.i2p.internal.I2CPMessageQueue;
import net.i2p.router.ClientManagerFacade;
import net.i2p.router.ClientMessage;
import net.i2p.router.Job;
import net.i2p.router.JobImpl;
import net.i2p.router.RouterContext;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;
import net.i2p.util.SystemVersion;
/**
* Coordinate connections and various tasks
*
* @author jrandom
*/
class ClientManager {
private final Log _log;
protected final List<ClientListenerRunner> _listeners;
// Destination --> ClientConnectionRunner
// Locked for adds/removes but not lookups
// If a runner has multiple sessions it will be in here multiple times, one for each dest
private final Map<Destination, ClientConnectionRunner> _runners;
// Same as what's in _runners, but for fast lookup by Hash
// Locked for adds/removes but not lookups
// If a runner has multiple sessions it will be in here multiple times, one for each dest
private final Map<Hash, ClientConnectionRunner> _runnersByHash;
// ClientConnectionRunner for clients w/out a Dest yet
private final Set<ClientConnectionRunner> _pendingRunners;
private final Set<SessionId> _runnerSessionIds;
protected final RouterContext _ctx;
protected final int _port;
protected volatile boolean _isStarted;
private final SimpleTimer2.TimedEvent _clientTimestamper;
/** Disable external interface, allow internal clients only @since 0.8.3 */
private static final String PROP_DISABLE_EXTERNAL = "i2cp.disableInterface";
/** SSL interface (only) @since 0.8.3 */
private static final String PROP_ENABLE_SSL = "i2cp.SSL";
private static final int INTERNAL_QUEUE_SIZE = 256;
private static final long REQUEST_LEASESET_TIMEOUT = 60*1000;
/** 2 bytes, save 65535 for unknown */
private static final int MAX_SESSION_ID = 65534;
private static final String PROP_MAX_SESSIONS = "i2cp.maxSessions";
private static final int DEFAULT_MAX_SESSIONS = 100;
/** 65535 */
public static final SessionId UNKNOWN_SESSION_ID = new SessionId(MAX_SESSION_ID + 1);
/**
* Does not start the listeners.
* Caller must call start()
*/
public ClientManager(RouterContext context, int port) {
_ctx = context;
_log = context.logManager().getLog(ClientManager.class);
//_ctx.statManager().createRateStat("client.receiveMessageSize",
// "How large are messages received by the client?",
// "ClientMessages",
// new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
_listeners = new ArrayList<ClientListenerRunner>();
_runners = new ConcurrentHashMap<Destination, ClientConnectionRunner>();
_runnersByHash = new ConcurrentHashMap<Hash, ClientConnectionRunner>();
_pendingRunners = new HashSet<ClientConnectionRunner>();
_runnerSessionIds = new HashSet<SessionId>();
_port = port;
_clientTimestamper = new ClientTimestamper();
// following are for RequestLeaseSetJob
_ctx.statManager().createRateStat("client.requestLeaseSetSuccess", "How frequently the router requests successfully a new leaseSet?", "ClientMessages", new long[] { 60*60*1000 });
_ctx.statManager().createRateStat("client.requestLeaseSetTimeout", "How frequently the router requests a new leaseSet but gets no reply?", "ClientMessages", new long[] { 60*60*1000 });
_ctx.statManager().createRateStat("client.requestLeaseSetDropped", "How frequently the router requests a new leaseSet but the client drops?", "ClientMessages", new long[] { 60*60*1000 });
}
/** @since 0.9.8 */
public synchronized void start() {
startListeners();
}
/**
* Call from synchronized method
* Todo: Start a 3rd listener for IPV6?
*/
protected void startListeners() {
ClientListenerRunner listener;
if (SystemVersion.isAndroid()) {
try {
Class<? extends ClientListenerRunner> clazz = Class.forName(
"net.i2p.router.client.DomainClientListenerRunner"
).asSubclass(ClientListenerRunner.class);
Constructor<? extends ClientListenerRunner> ctor =
clazz.getDeclaredConstructor(RouterContext.class,
ClientManager.class);
listener = ctor.newInstance(_ctx, this);
Thread t = new I2PThread(listener, "DomainClientListener", true);
t.start();
_listeners.add(listener);
} catch (ClassNotFoundException e) {
_log.warn("Could not find DomainClientListenerRunner class", e);
} catch (ClassCastException e) {
_log.error("Error creating DomainClientListenerRunner", e);
} catch (NoSuchMethodException e) {
_log.error("Error creating DomainClientListenerRunner", e);
} catch (InstantiationException e) {
_log.error("Error creating DomainClientListenerRunner", e);
} catch (IllegalAccessException e) {
_log.error("Error creating DomainClientListenerRunner", e);
} catch (InvocationTargetException e) {
_log.error("Error creating DomainClientListenerRunner", e);
}
}
if (!_ctx.getBooleanProperty(PROP_DISABLE_EXTERNAL)) {
// there's no option to start both an SSL and non-SSL listener
if (_ctx.getBooleanProperty(PROP_ENABLE_SSL))
listener = new SSLClientListenerRunner(_ctx, this, _port);
else
listener = new ClientListenerRunner(_ctx, this, _port);
Thread t = new I2PThread(listener, "ClientListener:" + _port, true);
t.start();
_listeners.add(listener);
_clientTimestamper.schedule(ClientTimestamper.LOOP_TIME);
}
_isStarted = true;
}
public synchronized void restart() {
shutdown("Router restart");
// to let the old listener die
try { Thread.sleep(2*1000); } catch (InterruptedException ie) {}
startListeners();
}
/**
* @param msg message to send to the clients
*/
public synchronized void shutdown(String msg) {
_isStarted = false;
_log.info("Shutting down the ClientManager");
for (ClientListenerRunner listener : _listeners)
listener.stopListening();
_listeners.clear();
Set<ClientConnectionRunner> runners = new HashSet<ClientConnectionRunner>();
synchronized (_runners) {
for (ClientConnectionRunner runner : _runners.values()) {
runners.add(runner);
}
}
synchronized (_pendingRunners) {
for (ClientConnectionRunner runner : _pendingRunners) {
runners.add(runner);
}
}
for (ClientConnectionRunner runner : runners) {
runner.disconnectClient(msg, Log.WARN);
}
_runnersByHash.clear();
_clientTimestamper.cancel();
}
/**
* The InternalClientManager interface.
* Connects to the router, receiving a message queue to talk to the router with.
* @throws I2PSessionException if the router isn't ready
* @since 0.8.3
*/
public I2CPMessageQueue internalConnect() throws I2PSessionException {
if (!_isStarted)
throw new I2PSessionException("Router client manager is shut down");
LinkedBlockingQueue<I2CPMessage> in = new LinkedBlockingQueue<I2CPMessage>(INTERNAL_QUEUE_SIZE);
LinkedBlockingQueue<I2CPMessage> out = new LinkedBlockingQueue<I2CPMessage>(INTERNAL_QUEUE_SIZE);
I2CPMessageQueue myQueue = new I2CPMessageQueueImpl(in, out);
I2CPMessageQueue hisQueue = new I2CPMessageQueueImpl(out, in);
ClientConnectionRunner runner = new QueuedClientConnectionRunner(_ctx, this, myQueue);
registerConnection(runner);
return hisQueue;
}
public synchronized boolean isAlive() {
boolean listening = true;
if (!_listeners.isEmpty()) {
for (ClientListenerRunner listener : _listeners)
listening = listening && listener.isListening();
}
return _isStarted && (_listeners.isEmpty() || listening);
}
public void registerConnection(ClientConnectionRunner runner) {
try {
runner.startRunning();
synchronized (_pendingRunners) {
_pendingRunners.add(runner);
}
} catch (IOException ioe) {
_log.error("Error starting up the runner", ioe);
runner.stopRunning();
}
}
/**
* Remove all sessions for this runner.
*/
public void unregisterConnection(ClientConnectionRunner runner) {
synchronized (_pendingRunners) {
_pendingRunners.remove(runner);
}
List<SessionId> ids = runner.getSessionIds();
List<Destination> dests = runner.getDestinations();
if (_log.shouldLog(Log.WARN))
_log.warn("Unregistering (dropping) a client connection with ids: " + ids);
synchronized (_runners) {
for (SessionId id : ids) {
_runnerSessionIds.remove(id);
}
for (Destination dest : dests) {
_runners.remove(dest);
_runnersByHash.remove(dest.calculateHash());
}
// just in case
for (Iterator<ClientConnectionRunner> iter = _runners.values().iterator(); iter.hasNext(); ) {
ClientConnectionRunner r = iter.next();
if (r.equals(runner))
iter.remove();
}
for (Iterator<ClientConnectionRunner> iter = _runnersByHash.values().iterator(); iter.hasNext(); ) {
ClientConnectionRunner r = iter.next();
if (r.equals(runner))
iter.remove();
}
}
}
/**
* Remove only the following session. Does not remove the runner if it has more.
*
* @since 0.9.21
*/
public void unregisterSession(SessionId id, Destination dest) {
if (_log.shouldLog(Log.WARN))
_log.warn("Unregistering client session " + id);
synchronized (_runners) {
_runnerSessionIds.remove(id);
_runners.remove(dest);
_runnersByHash.remove(dest.calculateHash());
}
}
/**
* Add to the clients list. Check for a dup destination.
* Side effect: Sets the session ID of the runner.
* Caller must call runner.disconnectClient() on failure.
*
* @return SessionStatusMessage return code, 1 for success, != 1 for failure
*/
public int destinationEstablished(ClientConnectionRunner runner, Destination dest) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("DestinationEstablished called for destination " + dest.calculateHash().toBase64());
synchronized (_pendingRunners) {
_pendingRunners.remove(runner);
}
int rv;
synchronized (_runners) {
boolean fail = _runnersByHash.containsKey(dest.calculateHash());
if (fail) {
rv = SessionStatusMessage.STATUS_INVALID;
} else {
SessionId id = locked_getNextSessionId();
if (id != null) {
Hash h = dest.calculateHash();
runner.setSessionId(h, id);
_runners.put(dest, runner);
_runnersByHash.put(h, runner);
rv = SessionStatusMessage.STATUS_CREATED;
} else {
rv = SessionStatusMessage.STATUS_REFUSED;
}
}
}
if (rv == SessionStatusMessage.STATUS_INVALID) {
_log.log(Log.CRIT, "Client attempted to register duplicate destination " + dest.calculateHash().toBase64());
} else if (rv == SessionStatusMessage.STATUS_REFUSED) {
_log.error("Max sessions exceeded " + dest.calculateHash().toBase64());
}
return rv;
}
/**
* Generate a new random, unused sessionId. Caller must synch on _runners.
* @return null on failure
* @since 0.9.12
*/
private SessionId locked_getNextSessionId() {
int max = Math.max(1, Math.min(2048, _ctx.getProperty(PROP_MAX_SESSIONS, DEFAULT_MAX_SESSIONS)));
if (_runnerSessionIds.size() >= max) {
_log.logAlways(Log.WARN, "Session refused, max is " + max + ", increase " + PROP_MAX_SESSIONS);
return null;
}
for (int i = 0; i < 100; i++) {
SessionId id = new SessionId(_ctx.random().nextInt(MAX_SESSION_ID + 1));
if (_runnerSessionIds.add(id))
return id;
}
_log.logAlways(Log.WARN, "Session refused, can't find id slot");
return null;
}
/**
* Distribute message to a local or remote destination.
* @param msgId the router's ID for this message
* @param messageNonce the client's ID for this message
* @param flags ignored for local
*/
void distributeMessage(Destination fromDest, Destination toDest, Payload payload,
MessageId msgId, long messageNonce, long expiration, int flags) {
// check if there is a runner for it
ClientConnectionRunner runner = getRunner(toDest);
if (runner != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Message " + msgId + " is targeting a local destination. distribute it as such");
ClientConnectionRunner sender = getRunner(fromDest);
if (sender == null) {
// sender went away
return;
}
// run this inline so we don't clog up the job queue
Job j = new DistributeLocal(toDest, runner, sender, fromDest, payload, msgId, messageNonce);
//_ctx.jobQueue().addJob(j);
j.runJob();
} else {
// remote. w00t
if (_log.shouldLog(Log.DEBUG))
_log.debug("Message " + msgId + " is targeting a REMOTE destination! Added to the client message pool");
runner = getRunner(fromDest);
if (runner == null) {
// sender went away
return;
}
SessionConfig config = runner.getConfig(fromDest.calculateHash());
if (config == null)
return;
ClientMessage msg = new ClientMessage(toDest, payload, config,
fromDest, msgId,
messageNonce, expiration, flags);
_ctx.clientMessagePool().add(msg, true);
}
}
private class DistributeLocal extends JobImpl {
private final Destination _toDest;
private final ClientConnectionRunner _to;
private final ClientConnectionRunner _from;
private final Destination _fromDest;
private final Payload _payload;
private final MessageId _msgId;
private final long _messageNonce;
/**
* @param msgId the router's ID for this message
* @param messageNonce the client's ID for this message
*/
public DistributeLocal(Destination toDest, ClientConnectionRunner to, ClientConnectionRunner from,
Destination fromDest, Payload payload, MessageId id, long messageNonce) {
super(_ctx);
_toDest = toDest;
_to = to;
_from = from;
_fromDest = fromDest;
_payload = payload;
_msgId = id;
_messageNonce = messageNonce;
}
public String getName() { return "Distribute local message"; }
public void runJob() {
boolean ok = _to.receiveMessage(_toDest, _fromDest, _payload);
if (_from != null) {
int rc = ok ? MessageStatusMessage.STATUS_SEND_SUCCESS_LOCAL : MessageStatusMessage.STATUS_SEND_FAILURE_LOCAL;
_from.updateMessageDeliveryStatus(_fromDest, _msgId, _messageNonce, rc);
}
}
}
/**
* Request that a particular client authorize the Leases contained in the
* LeaseSet, after which the onCreateJob is queued up. If that doesn't occur
* within the timeout specified, queue up the onFailedJob. This call does not
* block.
*
* UNUSED, the call below without jobs is always used.
*
* @param dest Destination from which the LeaseSet's authorization should be requested
* @param set LeaseSet with requested leases - this object must be updated to contain the
* signed version (as well as any changed/added/removed Leases).
* The LeaseSet contains Leases only; it is unsigned and does not have the destination set.
* @param timeout ms to wait before failing
* @param onCreateJob Job to run after the LeaseSet is authorized
* @param onFailedJob Job to run after the timeout passes without receiving authorization
*/
public void requestLeaseSet(Destination dest, LeaseSet set, long timeout, Job onCreateJob, Job onFailedJob) {
ClientConnectionRunner runner = getRunner(dest);
if (runner == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Cannot request the lease set, as we can't find a client runner for "
+ dest.calculateHash().toBase64() + ". disconnected?");
_ctx.jobQueue().addJob(onFailedJob);
} else {
runner.requestLeaseSet(dest.calculateHash(), set, timeout, onCreateJob, onFailedJob);
}
}
/**
* Request that a particular client authorize the Leases contained in the
* LeaseSet.
*
* @param dest Destination from which the LeaseSet's authorization should be requested
* @param ls LeaseSet with requested leases - this object must be updated to contain the
* signed version (as well as any changed/added/removed Leases).
* The LeaseSet contains Leases only; it is unsigned and does not have the destination set.
*/
public void requestLeaseSet(Hash dest, LeaseSet ls) {
ClientConnectionRunner runner = getRunner(dest);
if (runner != null) {
// no need to fire off any jobs...
runner.requestLeaseSet(dest, ls, REQUEST_LEASESET_TIMEOUT, null, null);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Cannot request the lease set, as we can't find a client runner for "
+ dest + ". disconnected?");
}
}
/**
* Unsynchronized
*/
public boolean isLocal(Destination dest) {
return _runners.containsKey(dest);
}
/**
* Unsynchronized
*/
public boolean isLocal(Hash destHash) {
if (destHash == null) return false;
return _runnersByHash.containsKey(destHash);
}
/**
* @return true if we don't know about this destination at all
*/
public boolean shouldPublishLeaseSet(Hash destHash) {
if (destHash == null) return true;
ClientConnectionRunner runner = getRunner(destHash);
if (runner == null) return true;
SessionConfig config = runner.getConfig(destHash);
if (config == null) return true;
return !Boolean.parseBoolean(config.getOptions().getProperty(ClientManagerFacade.PROP_CLIENT_ONLY));
}
/**
* Unsynchronized
*/
public Set<Destination> listClients() {
Set<Destination> rv = new HashSet<Destination>();
rv.addAll(_runners.keySet());
return rv;
}
/**
* Unsynchronized
*/
ClientConnectionRunner getRunner(Destination dest) {
return _runners.get(dest);
}
/**
* Return the client's current config, or null if not connected
*
*/
public SessionConfig getClientSessionConfig(Destination dest) {
ClientConnectionRunner runner = getRunner(dest);
if (runner != null)
return runner.getConfig(dest.calculateHash());
else
return null;
}
/**
* Return the client's SessionKeyManager
* Use this instead of the RouterContext.sessionKeyManager()
* to prevent correlation attacks across destinations
*/
public SessionKeyManager getClientSessionKeyManager(Hash dest) {
ClientConnectionRunner runner = getRunner(dest);
if (runner != null)
return runner.getSessionKeyManager();
else
return null;
}
/**
* Unsynchronized
*/
private ClientConnectionRunner getRunner(Hash destHash) {
if (destHash == null)
return null;
return _runnersByHash.get(destHash);
}
/**
* @param id the router's ID for this message
* @param messageNonce the client's ID for this message
* @param status see I2CP MessageStatusMessage for success/failure codes
*/
public void messageDeliveryStatusUpdate(Destination fromDest, MessageId id, long messageNonce, int status) {
ClientConnectionRunner runner = getRunner(fromDest);
if (runner != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Delivering status " + status + " to "
+ fromDest.calculateHash() + " for message " + id);
runner.updateMessageDeliveryStatus(fromDest, id, messageNonce, status);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Cannot deliver status " + status + " to "
+ fromDest.calculateHash() + " for message " + id);
}
}
/**
* @return unmodifiable, not a copy
*/
Set<Destination> getRunnerDestinations() {
return Collections.unmodifiableSet(_runners.keySet());
}
/**
* Unused
*
* @param dest null for all local destinations
*/
public void reportAbuse(Destination dest, String reason, int severity) {
if (dest != null) {
ClientConnectionRunner runner = getRunner(dest);
if (runner != null) {
runner.reportAbuse(dest, reason, severity);
}
} else {
for (Destination d : _runners.keySet()) {
reportAbuse(d, reason, severity);
}
}
}
/** @deprecated unused */
@Deprecated
public void renderStatusHTML(Writer out) throws IOException {
/******
StringBuilder buf = new StringBuilder(8*1024);
buf.append("<u><b>Local destinations</b></u><br>");
Map<Destination, ClientConnectionRunner> runners = null;
synchronized (_runners) {
runners = (Map)_runners.clone();
}
for (Iterator<Destination> iter = runners.keySet().iterator(); iter.hasNext(); ) {
Destination dest = iter.next();
ClientConnectionRunner runner = runners.get(dest);
buf.append("<b>*</b> ").append(dest.calculateHash().toBase64().substring(0,6)).append("<br>\n");
LeaseSet ls = runner.getLeaseSet();
if (ls == null) {
buf.append("<font color=\"red\"><i>No lease</i></font><br>\n");
} else {
long leaseAge = ls.getEarliestLeaseDate() - _ctx.clock().now();
if (leaseAge <= 0) {
buf.append("<font color=\"red\"><i>Lease expired ");
buf.append(DataHelper.formatDuration(0-leaseAge)).append(" ago</i></font><br>\n");
} else {
int count = ls.getLeaseCount();
if (count <= 0) {
buf.append("<font color=\"red\"><i>No tunnels</i></font><br>\n");
} else {
TunnelId id = ls.getLease(0).getTunnelId();
TunnelInfo info = _ctx.tunnelManager().getTunnelInfo(id);
if (info == null) {
buf.append("<font color=\"red\"><i>Failed tunnels</i></font><br>\n");
} else {
buf.append(count).append(" x ");
buf.append(info.getLength() - 1).append(" hop tunnel");
if (count != 1)
buf.append('s');
buf.append("<br>\n");
buf.append("Expiring in ").append(DataHelper.formatDuration(leaseAge));
buf.append("<br>\n");
}
}
}
}
}
buf.append("\n<hr>\n");
out.write(buf.toString());
out.flush();
******/
}
public void messageReceived(ClientMessage msg) {
// This is fast and non-blocking, run in-line
//_ctx.jobQueue().addJob(new HandleJob(msg));
(new HandleJob(msg)).runJob();
}
private class HandleJob extends JobImpl {
private final ClientMessage _msg;
public HandleJob(ClientMessage msg) {
super(_ctx);
_msg = msg;
}
public String getName() { return "Handle Inbound Client Messages"; }
public void runJob() {
ClientConnectionRunner runner;
Destination dest = _msg.getDestination();
if (dest != null)
runner = getRunner(dest);
else
runner = getRunner(_msg.getDestinationHash());
if (runner != null) {
//_ctx.statManager().addRateData("client.receiveMessageSize",
// _msg.getPayload().getSize(), 0);
if (dest != null)
runner.receiveMessage(dest, null, _msg.getPayload());
else
runner.receiveMessage(_msg.getDestinationHash(), null, _msg.getPayload());
} else {
// no client connection...
// we should pool these somewhere...
if (_log.shouldLog(Log.WARN))
_log.warn("Message received but we don't have a connection to "
+ dest + "/" + _msg.getDestinationHash()
+ " currently. DROPPED");
}
}
}
/**
* Tell external clients the time periodically
*
* @since 0.9.20
*/
private class ClientTimestamper extends SimpleTimer2.TimedEvent {
public static final long LOOP_TIME = 10*60*1000;
/** must call schedule() later */
public ClientTimestamper() {
super(_ctx.simpleTimer2());
}
public void timeReached() {
if (!_isStarted)
return;
for (ClientConnectionRunner runner : _runners.values()) {
if (runner instanceof QueuedClientConnectionRunner)
continue;
if (runner.isDead())
continue;
SessionConfig cfg = runner.getPrimaryConfig();
if (cfg == null)
continue; // simple session or no session yet
if (runner.getLeaseSet(cfg.getDestination().calculateHash()) == null)
continue; // don't confuse client while waiting for CreateLeaseSet msg
try {
// only send version if the client can handle it (0.8.7 or greater)
runner.doSend(new SetDateMessage(runner.getClientVersion() != null ?
CoreVersion.VERSION : null));
} catch (I2CPMessageException ime) {}
}
if (_isStarted)
schedule(LOOP_TIME);
}
}
}