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.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.ConcurrentHashMap; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import net.i2p.client.I2PClient; 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.DisconnectMessage; import net.i2p.data.i2cp.I2CPMessage; import net.i2p.data.i2cp.I2CPMessageException; import net.i2p.data.i2cp.I2CPMessageReader; import net.i2p.data.i2cp.MessageId; import net.i2p.data.i2cp.MessageStatusMessage; import net.i2p.data.i2cp.SendMessageMessage; import net.i2p.data.i2cp.SendMessageExpiresMessage; import net.i2p.data.i2cp.SessionConfig; import net.i2p.data.i2cp.SessionId; import net.i2p.data.i2cp.SessionStatusMessage; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; import net.i2p.router.crypto.TransientSessionKeyManager; import net.i2p.util.ConcurrentHashSet; import net.i2p.util.I2PThread; import net.i2p.util.Log; import net.i2p.util.SimpleTimer; /** * Bridge the router and the client - managing state for a client. * * As of release 0.9.21, multiple sessions are supported on a single * I2CP connection. These sessions share tunnels and some configuration. * * @author jrandom */ class ClientConnectionRunner { protected final Log _log; protected final RouterContext _context; protected final ClientManager _manager; /** socket for this particular peer connection */ private final Socket _socket; /** output stream of the socket that I2CP messages bound to the client should be written to */ private OutputStream _out; private final ConcurrentHashMap<Hash, SessionParams> _sessions; private String _clientVersion; /** * Mapping of MessageId to Payload, storing messages for retrieval. * Unused for i2cp.fastReceive = "true" (_dontSendMSMOnRecive = true) */ private final Map<MessageId, Payload> _messages; private int _consecutiveLeaseRequestFails; /** * Set of messageIds created but not yet ACCEPTED. * Unused for i2cp.messageReliability = "none" (_dontSendMSM = true) */ private final Set<MessageId> _acceptedPending; /** thingy that does stuff */ protected I2CPMessageReader _reader; /** Used for all sessions, which must all have the same crypto keys */ private SessionKeyManager _sessionKeyManager; /** * This contains the last 10 MessageIds that have had their (non-ack) status * delivered to the client (so that we can be sure only to update when necessary) */ private final List<MessageId> _alreadyProcessed; private ClientWriterRunner _writer; /** are we, uh, dead */ private volatile boolean _dead; /** For outbound traffic. true if i2cp.messageReliability = "none"; @since 0.8.1 */ private boolean _dontSendMSM; /** For inbound traffic. true if i2cp.fastReceive = "true"; @since 0.9.4 */ private boolean _dontSendMSMOnReceive; private final AtomicInteger _messageId; // messageId counter // Was 32767 since the beginning (04-2004). // But it's 4 bytes in the I2CP spec and stored as a long in MessageID.... // If this is too low and wraps around, I2CP VerifyUsage could delete the wrong message, // e.g. on local access private static final int MAX_MESSAGE_ID = 0x4000000; private static final int MAX_LEASE_FAILS = 5; private static final int BUF_SIZE = 32*1024; private static final int MAX_SESSIONS = 4; /** @since 0.9.2 */ private static final String PROP_TAGS = "crypto.tagsToSend"; private static final String PROP_THRESH = "crypto.lowTagThreshold"; /** * For multisession * @since 0.9.21 */ private static class SessionParams { final Destination dest; final boolean isPrimary; SessionId sessionId; SessionConfig config; LeaseRequestState leaseRequest; Rerequest rerequestTimer; LeaseSet currentLeaseSet; SessionParams(Destination d, boolean isPrimary) { dest = d; this.isPrimary = isPrimary; } } /** * Create a new runner against the given socket * */ public ClientConnectionRunner(RouterContext context, ClientManager manager, Socket socket) { _context = context; _log = _context.logManager().getLog(ClientConnectionRunner.class); _manager = manager; _socket = socket; // unused for fastReceive _messages = new ConcurrentHashMap<MessageId, Payload>(); _sessions = new ConcurrentHashMap<Hash, SessionParams>(4); _alreadyProcessed = new ArrayList<MessageId>(); _acceptedPending = new ConcurrentHashSet<MessageId>(); _messageId = new AtomicInteger(_context.random().nextInt()); } private static final AtomicInteger __id = new AtomicInteger(); /** * Actually run the connection - listen for I2CP messages and respond. This * is the main driver for this class, though it gets all its meat from the * {@link net.i2p.data.i2cp.I2CPMessageReader I2CPMessageReader} * */ public synchronized void startRunning() throws IOException { if (_dead || _reader != null) throw new IllegalStateException(); _reader = new I2CPMessageReader(new BufferedInputStream(_socket.getInputStream(), BUF_SIZE), createListener()); _writer = new ClientWriterRunner(_context, this); I2PThread t = new I2PThread(_writer); t.setName("I2CP Writer " + __id.incrementAndGet()); t.setDaemon(true); t.start(); _out = new BufferedOutputStream(_socket.getOutputStream()); _reader.startReading(); // TODO need a cleaner for unclaimed items in _messages, but we have no timestamps... } /** * Allow override for testing * @since 0.9.8 */ protected I2CPMessageReader.I2CPMessageEventListener createListener() { return new ClientMessageEventListener(_context, this, true); } /** * Die a horrible death. Cannot be restarted. */ public synchronized void stopRunning() { if (_dead) return; // router may be null in unit tests if ((_context.router() == null || _context.router().isAlive()) && _log.shouldWarn()) _log.warn("Stop the I2CP connection!", new Exception("Stop client connection")); _dead = true; // we need these keys to unpublish the leaseSet if (_reader != null) _reader.stopReading(); if (_writer != null) _writer.stopWriting(); if (_socket != null) try { _socket.close(); } catch (IOException ioe) { } _messages.clear(); _acceptedPending.clear(); if (_sessionKeyManager != null) _sessionKeyManager.shutdown(); _manager.unregisterConnection(this); // netdb may be null in unit tests if (_context.netDb() != null) { for (SessionParams sp : _sessions.values()) { LeaseSet ls = sp.currentLeaseSet; if (ls != null) _context.netDb().unpublish(ls); if (!sp.isPrimary) _context.tunnelManager().removeAlias(sp.dest); } } synchronized (_alreadyProcessed) { _alreadyProcessed.clear(); } _sessions.clear(); } /** * Current client's config, * will be null if session not found * IS subsession aware. * @since 0.9.21 added hash param */ public SessionConfig getConfig(Hash h) { SessionParams sp = _sessions.get(h); if (sp == null) return null; return sp.config; } /** * Current client's config, * will be null if session not found * IS subsession aware. * Returns null if id is null. * @since 0.9.21 added id param */ public SessionConfig getConfig(SessionId id) { if (id == null) return null; for (SessionParams sp : _sessions.values()) { if (id.equals(sp.sessionId)) return sp.config; } return null; } /** * Primary client's config, * will be null if session not set up * @since 0.9.21 */ public SessionConfig getPrimaryConfig() { for (SessionParams sp : _sessions.values()) { if (sp.isPrimary) return sp.config; } return null; } /** * The client version. * @since 0.9.7 */ public void setClientVersion(String version) { _clientVersion = version; } /** * The client version. * @return null if unknown or less than 0.8.7 * @since 0.9.7 */ public String getClientVersion() { return _clientVersion; } /** current client's sessionkeymanager */ public SessionKeyManager getSessionKeyManager() { return _sessionKeyManager; } /** * Currently allocated leaseSet. * IS subsession aware. Returns primary leaseset only. * @return leaseSet or null if not yet set or unknown hash * @since 0.9.21 added hash parameter */ public LeaseSet getLeaseSet(Hash h) { SessionParams sp = _sessions.get(h); if (sp == null) return null; return sp.currentLeaseSet; } /** * Currently allocated leaseSet. * IS subsession aware. */ /**** void setLeaseSet(LeaseSet ls) { Hash h = ls.getDestination().calculateHash(); SessionParams sp = _sessions.get(h); if (sp == null) return; sp.currentLeaseSet = ls; } ****/ /** * Equivalent to getConfig().getDestination().calculateHash(); * will be null before session is established * Not subsession aware. Returns primary session hash. * Don't use if you can help it. * * @return primary hash or null if not yet set */ public Hash getDestHash() { SessionConfig cfg = getPrimaryConfig(); if (cfg != null) return cfg.getDestination().calculateHash(); return null; } /** * Return the hash for the given ID * @return hash or null if unknown * @since 0.9.21 */ public Hash getDestHash(SessionId id) { if (id == null) return null; for (Map.Entry<Hash, SessionParams> e : _sessions.entrySet()) { if (id.equals(e.getValue().sessionId)) return e.getKey(); } return null; } /** * Return the dest for the given ID * @return dest or null if unknown * @since 0.9.21 */ public Destination getDestination(SessionId id) { if (id == null) return null; for (SessionParams sp : _sessions.values()) { if (id.equals(sp.sessionId)) return sp.dest; } return null; } /** * Subsession aware. * * @param h the local target * @return current client's sessionId or null if not yet set or not a valid hash * @since 0.9.21 */ SessionId getSessionId(Hash h) { SessionParams sp = _sessions.get(h); if (sp == null) return null; return sp.sessionId; } /** * Subsession aware. * * @return all current client's sessionIds, non-null * @since 0.9.21 */ List<SessionId> getSessionIds() { List<SessionId> rv = new ArrayList<SessionId>(_sessions.size()); for (SessionParams sp : _sessions.values()) { SessionId id = sp.sessionId; if (id != null) rv.add(id); } return rv; } /** * Subsession aware. * * @return all current client's destinations, non-null * @since 0.9.21 */ List<Destination> getDestinations() { List<Destination> rv = new ArrayList<Destination>(_sessions.size()); for (SessionParams sp : _sessions.values()) { rv.add(sp.dest); } return rv; } /** * To be called only by ClientManager. * * @param hash for the session * @throws IllegalStateException if already set * @since 0.9.21 added hash param */ void setSessionId(Hash hash, SessionId id) { if (hash == null) throw new IllegalStateException(); if (id == null) throw new NullPointerException(); SessionParams sp = _sessions.get(hash); if (sp == null || sp.sessionId != null) throw new IllegalStateException(); sp.sessionId = id; } /** * Kill the session. Caller must kill runner if none left. * * @since 0.9.21 */ void removeSession(SessionId id) { if (id == null) return; boolean isPrimary = false; for (Iterator<SessionParams> iter = _sessions.values().iterator(); iter.hasNext(); ) { SessionParams sp = iter.next(); if (id.equals(sp.sessionId)) { if (_log.shouldLog(Log.INFO)) _log.info("Destroying client session " + id); iter.remove(); // Tell client manger _manager.unregisterSession(id, sp.dest); LeaseSet ls = sp.currentLeaseSet; if (ls != null) _context.netDb().unpublish(ls); isPrimary = sp.isPrimary; if (!isPrimary) _context.tunnelManager().removeAlias(sp.dest); break; } } if (isPrimary && !_sessions.isEmpty()) { // kill all the others also for (SessionParams sp : _sessions.values()) { if (_log.shouldLog(Log.INFO)) _log.info("Destroying remaining client subsession " + sp.sessionId); _manager.unregisterSession(sp.sessionId, sp.dest); LeaseSet ls = sp.currentLeaseSet; if (ls != null) _context.netDb().unpublish(ls); _context.tunnelManager().removeAlias(sp.dest); } } } /** * Data for the current leaseRequest, or null if there is no active leaseSet request. * Not subsession aware. Returns primary ID only. * @since 0.9.21 added hash param */ LeaseRequestState getLeaseRequest(Hash h) { SessionParams sp = _sessions.get(h); if (sp == null) return null; return sp.leaseRequest; } /** @param req non-null */ public void failLeaseRequest(LeaseRequestState req) { boolean disconnect = false; Hash h = req.getRequested().getDestination().calculateHash(); SessionParams sp = _sessions.get(h); if (sp == null) return; synchronized (this) { if (sp.leaseRequest == req) { sp.leaseRequest = null; disconnect = ++_consecutiveLeaseRequestFails > MAX_LEASE_FAILS; } } if (disconnect) disconnectClient("Too many leaseset request fails"); } /** already closed? */ boolean isDead() { return _dead; } /** * Only call if _dontSendMSMOnReceive is false, otherwise will always be null */ Payload getPayload(MessageId id) { return _messages.get(id); } /** * Only call if _dontSendMSMOnReceive is false */ void setPayload(MessageId id, Payload payload) { if (!_dontSendMSMOnReceive) _messages.put(id, payload); } /** * Only call if _dontSendMSMOnReceive is false */ void removePayload(MessageId id) { _messages.remove(id); } /** * Caller must send a SessionStatusMessage to the client with the returned code. * Caller must call disconnectClient() on failure. * Side effect: Sets the session ID. * * @return SessionStatusMessage return code, 1 for success, != 1 for failure */ public int sessionEstablished(SessionConfig config) { Destination dest = config.getDestination(); Hash destHash = dest.calculateHash(); if (_log.shouldLog(Log.DEBUG)) _log.debug("SessionEstablished called for destination " + destHash); if (_sessions.size() > MAX_SESSIONS) return SessionStatusMessage.STATUS_REFUSED; boolean isPrimary = _sessions.isEmpty(); if (!isPrimary) { // all encryption keys must be the same for (SessionParams sp : _sessions.values()) { if (!dest.getPublicKey().equals(sp.dest.getPublicKey())) return SessionStatusMessage.STATUS_INVALID; } } SessionParams sp = new SessionParams(dest, isPrimary); sp.config = config; SessionParams old = _sessions.putIfAbsent(destHash, sp); if (old != null) return SessionStatusMessage.STATUS_INVALID; // We process a few options here, but most are handled by the tunnel manager. // The ones here can't be changed later. Properties opts = config.getOptions(); if (isPrimary && opts != null) { _dontSendMSM = "none".equals(opts.getProperty(I2PClient.PROP_RELIABILITY, "").toLowerCase(Locale.US)); _dontSendMSMOnReceive = Boolean.parseBoolean(opts.getProperty(I2PClient.PROP_FAST_RECEIVE)); } // per-destination session key manager to prevent rather easy correlation if (isPrimary && _sessionKeyManager == null) { int tags = TransientSessionKeyManager.DEFAULT_TAGS; int thresh = TransientSessionKeyManager.LOW_THRESHOLD; if (opts != null) { String ptags = opts.getProperty(PROP_TAGS); if (ptags != null) { try { tags = Integer.parseInt(ptags); } catch (NumberFormatException nfe) {} } String pthresh = opts.getProperty(PROP_THRESH); if (pthresh != null) { try { thresh = Integer.parseInt(pthresh); } catch (NumberFormatException nfe) {} } } _sessionKeyManager = new TransientSessionKeyManager(_context, tags, thresh); } return _manager.destinationEstablished(this, dest); } /** * Send a notification to the client that their message (id specified) was * delivered (or failed delivery) * Note that this sends the Guaranteed status codes, even though we only support best effort. * Doesn't do anything if i2cp.messageReliability = "none" * * Do not use for status = STATUS_SEND_ACCEPTED; use ackSendMessage() for that. * * @param dest the client * @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 */ void updateMessageDeliveryStatus(Destination dest, MessageId id, long messageNonce, int status) { if (_dead || messageNonce <= 0) return; SessionParams sp = _sessions.get(dest.calculateHash()); if (sp == null) return; SessionId sid = sp.sessionId; if (sid == null) return; // sid = new SessionId(foo) ??? _context.jobQueue().addJob(new MessageDeliveryStatusUpdate(sid, id, messageNonce, status)); } /** * called after a new leaseSet is granted by the client, the NetworkDb has been * updated. This takes care of all the LeaseRequestState stuff (including firing any jobs) */ void leaseSetCreated(LeaseSet ls) { Hash h = ls.getDestination().calculateHash(); SessionParams sp = _sessions.get(h); if (sp == null) return; LeaseRequestState state; synchronized (this) { sp.currentLeaseSet = ls; state = sp.leaseRequest; if (state == null) { // We got the LS after the timeout? // ClientMessageEventListener told the router to publish. if (_log.shouldLog(Log.WARN)) _log.warn("LeaseRequest is null and we've received a new lease? " + ls); return; } else { state.setIsSuccessful(true); if (_log.shouldLog(Log.DEBUG)) _log.debug("LeaseSet created fully: " + state + " / " + ls); sp.leaseRequest = null; _consecutiveLeaseRequestFails = 0; } } if ( (state != null) && (state.getOnGranted() != null) ) _context.jobQueue().addJob(state.getOnGranted()); } /** * Send a DisconnectMessage and log with level Log.ERROR. * This is always bad. * See ClientMessageEventListener.handleCreateSession() * for why we don't send a SessionStatusMessage when we do this. * @param reason will be truncated to 255 bytes */ void disconnectClient(String reason) { disconnectClient(reason, Log.ERROR); } /** * @param reason will be truncated to 255 bytes * @param logLevel e.g. Log.WARN * @since 0.8.2 */ void disconnectClient(String reason, int logLevel) { if (_log.shouldLog(logLevel)) _log.log(logLevel, "Disconnecting the client - " + reason); DisconnectMessage msg = new DisconnectMessage(); if (reason.length() > 255) reason = reason.substring(0, 255); msg.setReason(reason); try { doSend(msg); } catch (I2CPMessageException ime) { if (_log.shouldLog(Log.WARN)) _log.warn("Error writing out the disconnect message", ime); } // give it a little time to get sent out... // even better would be to have stopRunning() flush it? try { Thread.sleep(50); } catch (InterruptedException ie) {} stopRunning(); } /** * Distribute the message. If the dest is local, it blocks until its passed * to the target ClientConnectionRunner (which then fires it into a MessageReceivedJob). * If the dest is remote, it blocks until it is added into the ClientMessagePool * */ MessageId distributeMessage(SendMessageMessage message) { Payload payload = message.getPayload(); Destination dest = message.getDestination(); MessageId id = new MessageId(); id.setMessageId(getNextMessageId()); long expiration = 0; int flags = 0; if (message.getType() == SendMessageExpiresMessage.MESSAGE_TYPE) { SendMessageExpiresMessage msg = (SendMessageExpiresMessage) message; expiration = msg.getExpirationTime(); flags = msg.getFlags(); } if ((!_dontSendMSM) && message.getNonce() != 0) _acceptedPending.add(id); if (_log.shouldLog(Log.DEBUG)) _log.debug("** Receiving message " + id.getMessageId() + " with payload of size " + payload.getSize() + " for session " + message.getSessionId()); //long beforeDistribute = _context.clock().now(); // the following blocks as described above Destination fromDest = getDestination(message.getSessionId()); if (fromDest != null) _manager.distributeMessage(fromDest, dest, payload, id, message.getNonce(), expiration, flags); // else log error? //long timeToDistribute = _context.clock().now() - beforeDistribute; //if (_log.shouldLog(Log.DEBUG)) // _log.warn("Time to distribute in the manager to " // + dest.calculateHash().toBase64() + ": " // + timeToDistribute); return id; } /** * Send a notification to the client that their message (id specified) was accepted * for delivery (but not necessarily delivered) * Doesn't do anything if i2cp.messageReliability = "none" * or if the nonce is 0. * * @param id OUR id for the message * @param nonce HIS id for the message */ void ackSendMessage(SessionId sid, MessageId id, long nonce) { if (_dontSendMSM || nonce == 0) return; if (_log.shouldLog(Log.DEBUG)) _log.debug("Acking message send [accepted]" + id + " / " + nonce + " for sessionId " + sid); MessageStatusMessage status = new MessageStatusMessage(); status.setMessageId(id.getMessageId()); status.setSessionId(sid.getSessionId()); status.setSize(0L); status.setNonce(nonce); status.setStatus(MessageStatusMessage.STATUS_SEND_ACCEPTED); try { doSend(status); _acceptedPending.remove(id); } catch (I2CPMessageException ime) { if (_log.shouldLog(Log.WARN)) _log.warn("Error writing out the message status message", ime); } } /** * Synchronously deliver the message to the current runner * * Failure indication is available as of 0.9.29. * Fails on e.g. queue overflow to client, client dead, etc. * * @param toDest non-null * @param fromDest generally null when from remote, non-null if from local * @return success */ boolean receiveMessage(Destination toDest, Destination fromDest, Payload payload) { if (_dead) return false; MessageReceivedJob j = new MessageReceivedJob(_context, this, toDest, fromDest, payload, _dontSendMSMOnReceive); // This is fast and non-blocking, run in-line //_context.jobQueue().addJob(j); //j.runJob(); return j.receiveMessage(); } /** * Synchronously deliver the message to the current runner * * Failure indication is available as of 0.9.29. * Fails on e.g. queue overflow to client, client dead, etc. * * @param toHash non-null * @param fromDest generally null when from remote, non-null if from local * @return success * @since 0.9.21 */ boolean receiveMessage(Hash toHash, Destination fromDest, Payload payload) { SessionParams sp = _sessions.get(toHash); if (sp == null) { if (_log.shouldLog(Log.WARN)) _log.warn("No session found for receiveMessage()"); return false; } return receiveMessage(sp.dest, fromDest, payload); } /** * Send async abuse message to the client * */ public void reportAbuse(Destination dest, String reason, int severity) { if (_dead) return; _context.jobQueue().addJob(new ReportAbuseJob(_context, this, dest, reason, severity)); } /** * 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. * * Job args are always null, may need some fixups if we start using them. * * @param h the Destination's hash * @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 and destination only, it is unsigned. * @param expirationTime ms to wait before failing * @param onCreateJob Job to run after the LeaseSet is authorized, null OK * @param onFailedJob Job to run after the timeout passes without receiving authorization, null OK */ void requestLeaseSet(Hash h, LeaseSet set, long expirationTime, Job onCreateJob, Job onFailedJob) { if (_dead) { if (_log.shouldLog(Log.WARN)) _log.warn("Requesting leaseSet from a dead client: " + set); if (onFailedJob != null) _context.jobQueue().addJob(onFailedJob); return; } SessionParams sp = _sessions.get(h); if (sp == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Requesting leaseSet for an unknown sesssion"); return; } // We can't use LeaseSet.equals() here because the dest, keys, and sig on // the new LeaseSet are null. So we compare leases one by one. // In addition, the client rewrites the expiration time of all the leases to // the earliest one, so we can't use Lease.equals() or Lease.getEndDate(). // So compare by tunnel ID, and then by gateway. // (on the remote possibility that two gateways are using the same ID). // TunnelPool.locked_buildNewLeaseSet() ensures that leases are sorted, // so the comparison will always work. int leases = set.getLeaseCount(); // synch so _currentLeaseSet isn't changed out from under us LeaseSet current = null; Destination dest = sp.dest; LeaseRequestState state; synchronized (this) { current = sp.currentLeaseSet; if (current != null && current.getLeaseCount() == leases) { for (int i = 0; i < leases; i++) { if (! current.getLease(i).getTunnelId().equals(set.getLease(i).getTunnelId())) break; if (! current.getLease(i).getGateway().equals(set.getLease(i).getGateway())) break; if (i == leases - 1) { if (_log.shouldLog(Log.INFO)) _log.info("Requested leaseSet hasn't changed"); if (onCreateJob != null) _context.jobQueue().addJob(onCreateJob); return; // no change } } } if (_log.shouldLog(Log.INFO)) _log.info("Current leaseSet " + current + "\nNew leaseSet " + set); state = sp.leaseRequest; if (state != null) { LeaseSet requested = state.getRequested(); LeaseSet granted = state.getGranted(); long ours = set.getEarliestLeaseDate(); if ( ( (requested != null) && (requested.getEarliestLeaseDate() > ours) ) || ( (granted != null) && (granted.getEarliestLeaseDate() > ours) ) ) { // theirs is newer if (_log.shouldLog(Log.DEBUG)) _log.debug("Already requesting, theirs newer, do nothing: " + state); } else { // ours is newer, so wait a few secs and retry set.setDestination(dest); Rerequest timer = new Rerequest(set, expirationTime, onCreateJob, onFailedJob); sp.rerequestTimer = timer; _context.simpleTimer2().addEvent(timer, 3*1000); if (_log.shouldLog(Log.DEBUG)) _log.debug("Already requesting, ours newer, wait 3 sec: " + state); } // fire onCreated? return; // already requesting } else { set.setDestination(dest); if (current == null && _context.tunnelManager().getOutboundClientTunnelCount(h) <= 0) { // at startup of a client, where we don't have a leaseset, wait for // an outbound tunnel also, so the client doesn't start sending data // before we are ready Rerequest timer = new Rerequest(set, expirationTime, onCreateJob, onFailedJob); sp.rerequestTimer = timer; _context.simpleTimer2().addEvent(timer, 1000); if (_log.shouldLog(Log.DEBUG)) _log.debug("No current LS but no OB tunnels, wait 1 sec for " + h); return; } else { // so the timer won't fire off with an older LS request sp.rerequestTimer = null; sp.leaseRequest = state = new LeaseRequestState(onCreateJob, onFailedJob, _context.clock().now() + expirationTime, set); if (_log.shouldLog(Log.DEBUG)) _log.debug("New request: " + state); } } } _context.jobQueue().addJob(new RequestLeaseSetJob(_context, this, state)); } private class Rerequest implements SimpleTimer.TimedEvent { private final LeaseSet _ls; private final long _expirationTime; private final Job _onCreate; private final Job _onFailed; /** @param ls dest must be set */ public Rerequest(LeaseSet ls, long expirationTime, Job onCreate, Job onFailed) { _ls = ls; _expirationTime = expirationTime; _onCreate = onCreate; _onFailed = onFailed; } public void timeReached() { Hash h = _ls.getDestination().calculateHash(); SessionParams sp = _sessions.get(h); if (sp == null) { if (_log.shouldLog(Log.WARN)) _log.warn("cancelling rerequest, session went away: " + h); return; } synchronized(ClientConnectionRunner.this) { if (sp.rerequestTimer != Rerequest.this) { if (_log.shouldLog(Log.WARN)) _log.warn("cancelling rerequest, newer request came in: " + h); return; } } requestLeaseSet(h, _ls, _expirationTime, _onCreate, _onFailed); } } void disconnected() { if (_log.shouldLog(Log.WARN)) _log.warn("Disconnected", new Exception("Disconnected?")); stopRunning(); } //// //// boolean getIsDead() { return _dead; } /** * Not thread-safe. Blocking. Only used for external sockets. * ClientWriterRunner thread is the only caller. * Others must use doSend(). */ void writeMessage(I2CPMessage msg) { //long before = _context.clock().now(); try { // We don't need synchronization here, ClientWriterRunner is the only writer. //synchronized (_out) { msg.writeMessage(_out); _out.flush(); //} //if (_log.shouldLog(Log.DEBUG)) // _log.debug("after writeMessage("+ msg.getClass().getName() + "): " // + (_context.clock().now()-before) + "ms"); } catch (I2CPMessageException ime) { _log.error("Error sending I2CP message to client", ime); stopRunning(); } catch (EOFException eofe) { // only warn if client went away if (_log.shouldLog(Log.WARN)) _log.warn("Error sending I2CP message - client went away", eofe); stopRunning(); } catch (IOException ioe) { if (_log.shouldLog(Log.ERROR)) _log.error("IO Error sending I2CP message to client", ioe); stopRunning(); } catch (Throwable t) { _log.log(Log.CRIT, "Unhandled exception sending I2CP message to client", t); stopRunning(); //} finally { // long after = _context.clock().now(); // long lag = after - before; // if (lag > 300) { // if (_log.shouldLog(Log.WARN)) // _log.warn("synchronization on the i2cp message send took too long (" + lag // + "ms): " + msg); // } } } /** * Actually send the I2CPMessage to the peer through the socket * */ void doSend(I2CPMessage msg) throws I2CPMessageException { if (_out == null) throw new I2CPMessageException("Output stream is not initialized"); if (msg == null) throw new I2CPMessageException("Null message?!"); //if (_log.shouldLog(Log.DEBUG)) { // if ( (_config == null) || (_config.getDestination() == null) ) // _log.debug("before doSend of a "+ msg.getClass().getName() // + " message on for establishing i2cp con"); // else // _log.debug("before doSend of a "+ msg.getClass().getName() // + " message on for " // + _config.getDestination().calculateHash().toBase64()); //} _writer.addMessage(msg); //if (_log.shouldLog(Log.DEBUG)) { // if ( (_config == null) || (_config.getDestination() == null) ) // _log.debug("after doSend of a "+ msg.getClass().getName() // + " message on for establishing i2cp con"); // else // _log.debug("after doSend of a "+ msg.getClass().getName() // + " message on for " // + _config.getDestination().calculateHash().toBase64()); //} } public int getNextMessageId() { // Don't % so we don't get negative IDs return _messageId.incrementAndGet() & (MAX_MESSAGE_ID - 1); } /** * True if the client has already been sent the ACCEPTED state for the given * message id, false otherwise. * */ private boolean alreadyAccepted(MessageId id) { if (_dead) return false; return !_acceptedPending.contains(id); } /** * If the message hasn't been state=ACCEPTED yet, we shouldn't send an update * since the client doesn't know the message id (and we don't know the nonce). * So, we just wait REQUEUE_DELAY ms before trying again. * */ private final static long REQUEUE_DELAY = 500; private static final int MAX_REQUEUE = 60; // 30 sec. private class MessageDeliveryStatusUpdate extends JobImpl { private final SessionId _sessId; private final MessageId _messageId; private final long _messageNonce; private final int _status; private long _lastTried; private int _requeueCount; /** * Do not use for status = STATUS_SEND_ACCEPTED; use ackSendMessage() for that. * * @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 MessageDeliveryStatusUpdate(SessionId sid, MessageId id, long messageNonce, int status) { super(ClientConnectionRunner.this._context); _sessId = sid; _messageId = id; _messageNonce = messageNonce; _status = status; } public String getName() { return "Update Delivery Status"; } /** * Note that this sends the Guaranteed status codes, even though we only support best effort. */ public void runJob() { if (_dead) return; MessageStatusMessage msg = new MessageStatusMessage(); msg.setMessageId(_messageId.getMessageId()); msg.setSessionId(_sessId.getSessionId()); // has to be >= 0, it is initialized to -1 msg.setNonce(_messageNonce); msg.setSize(0); msg.setStatus(_status); if (!alreadyAccepted(_messageId)) { if (_requeueCount++ > MAX_REQUEUE) { // bug requeueing forever? failsafe _log.error("Abandon update for message " + _messageId + " to " + MessageStatusMessage.getStatusString(msg.getStatus()) + " for " + _sessId); } else { if (_log.shouldLog(Log.WARN)) _log.warn("Almost send an update for message " + _messageId + " to " + MessageStatusMessage.getStatusString(msg.getStatus()) + " for " + _sessId + " before they knew the messageId! delaying .5s"); _lastTried = _context.clock().now(); requeue(REQUEUE_DELAY); } return; } boolean alreadyProcessed = false; long beforeLock = _context.clock().now(); long inLock = 0; synchronized (_alreadyProcessed) { inLock = _context.clock().now(); if (_alreadyProcessed.contains(_messageId)) { _log.info("Status already updated"); alreadyProcessed = true; } else { _alreadyProcessed.add(_messageId); while (_alreadyProcessed.size() > 10) _alreadyProcessed.remove(0); } } long afterLock = _context.clock().now(); if (afterLock - beforeLock > 50) { _log.warn("MessageDeliveryStatusUpdate.locking took too long: " + (afterLock-beforeLock) + " overall, synchronized took " + (inLock - beforeLock)); } if (alreadyProcessed) return; if (_lastTried > 0) { if (_log.shouldLog(Log.DEBUG)) _log.info("Updating message status for message " + _messageId + " to " + MessageStatusMessage.getStatusString(msg.getStatus()) + " for " + _sessId + " (with nonce=2), retrying after " + (_context.clock().now() - _lastTried)); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating message status for message " + _messageId + " to " + MessageStatusMessage.getStatusString(msg.getStatus()) + " for " + _sessId + " (with nonce=2)"); } try { doSend(msg); } catch (I2CPMessageException ime) { if (_log.shouldLog(Log.WARN)) _log.warn("Error updating the status for message ID " + _messageId, ime); } } } }