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.util.List;
import java.util.Properties;
import net.i2p.CoreVersion;
import net.i2p.crypto.SigType;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.Payload;
import net.i2p.data.PublicKey;
import net.i2p.data.i2cp.BandwidthLimitsMessage;
import net.i2p.data.i2cp.CreateLeaseSetMessage;
import net.i2p.data.i2cp.CreateSessionMessage;
import net.i2p.data.i2cp.DestLookupMessage;
import net.i2p.data.i2cp.DestroySessionMessage;
import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
import net.i2p.data.i2cp.GetDateMessage;
import net.i2p.data.i2cp.HostLookupMessage;
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.MessagePayloadMessage;
import net.i2p.data.i2cp.MessageStatusMessage;
import net.i2p.data.i2cp.ReceiveMessageBeginMessage;
import net.i2p.data.i2cp.ReceiveMessageEndMessage;
import net.i2p.data.i2cp.ReconfigureSessionMessage;
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.data.i2cp.SetDateMessage;
import net.i2p.router.ClientTunnelSettings;
import net.i2p.router.LeaseSetKeys;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
import net.i2p.util.RandomSource;
/**
* Receive events from the client and handle them accordingly (updating the runner when
* necessary)
*
*/
class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventListener {
private final Log _log;
protected final RouterContext _context;
protected final ClientConnectionRunner _runner;
private final boolean _enforceAuth;
private volatile boolean _authorized;
private static final String PROP_AUTH = "i2cp.auth";
/** if true, user/pw must be in GetDateMessage */
private static final String PROP_AUTH_STRICT = "i2cp.strictAuth";
/**
* @param enforceAuth set false for in-JVM, true for socket access
*/
public ClientMessageEventListener(RouterContext context, ClientConnectionRunner runner, boolean enforceAuth) {
_context = context;
_log = _context.logManager().getLog(ClientMessageEventListener.class);
_runner = runner;
_enforceAuth = enforceAuth;
if ((!_enforceAuth) || !_context.getBooleanProperty(PROP_AUTH))
_authorized = true;
_context.statManager().createRateStat("client.distributeTime", "How long it took to inject the client message into the router", "ClientMessages", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
}
/**
* Handle an incoming message and dispatch it to the appropriate handler
*
*/
public void messageReceived(I2CPMessageReader reader, I2CPMessage message) {
if (_runner.isDead()) {
if (_log.shouldLog(Log.WARN))
_log.warn("Received but runner dead: \n" + message);
return;
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Message received: \n" + message);
int type = message.getType();
if (!_authorized) {
// Default true as of 0.9.16
boolean strict = _context.getBooleanPropertyDefaultTrue(PROP_AUTH_STRICT);
if ((strict && type != GetDateMessage.MESSAGE_TYPE) ||
(type != CreateSessionMessage.MESSAGE_TYPE &&
type != GetDateMessage.MESSAGE_TYPE &&
type != DestLookupMessage.MESSAGE_TYPE &&
type != GetBandwidthLimitsMessage.MESSAGE_TYPE)) {
_log.error("Received message type " + type + " without required authentication");
_runner.disconnectClient("Authorization required");
return;
}
}
switch (message.getType()) {
case GetDateMessage.MESSAGE_TYPE:
handleGetDate((GetDateMessage)message);
break;
case SetDateMessage.MESSAGE_TYPE:
handleSetDate((SetDateMessage)message);
break;
case CreateSessionMessage.MESSAGE_TYPE:
handleCreateSession((CreateSessionMessage)message);
break;
case SendMessageMessage.MESSAGE_TYPE:
handleSendMessage((SendMessageMessage)message);
break;
case SendMessageExpiresMessage.MESSAGE_TYPE:
handleSendMessage((SendMessageExpiresMessage)message);
break;
case ReceiveMessageBeginMessage.MESSAGE_TYPE:
handleReceiveBegin((ReceiveMessageBeginMessage)message);
break;
case ReceiveMessageEndMessage.MESSAGE_TYPE:
handleReceiveEnd((ReceiveMessageEndMessage)message);
break;
case CreateLeaseSetMessage.MESSAGE_TYPE:
handleCreateLeaseSet((CreateLeaseSetMessage)message);
break;
case DestroySessionMessage.MESSAGE_TYPE:
handleDestroySession((DestroySessionMessage)message);
break;
case DestLookupMessage.MESSAGE_TYPE:
handleDestLookup((DestLookupMessage)message);
break;
case HostLookupMessage.MESSAGE_TYPE:
handleHostLookup((HostLookupMessage)message);
break;
case ReconfigureSessionMessage.MESSAGE_TYPE:
handleReconfigureSession((ReconfigureSessionMessage)message);
break;
case GetBandwidthLimitsMessage.MESSAGE_TYPE:
handleGetBWLimits((GetBandwidthLimitsMessage)message);
break;
default:
if (_log.shouldLog(Log.ERROR))
_log.error("Unhandled I2CP type received: " + message.getType());
}
}
/**
* Handle notification that there was an error
*
*/
public void readError(I2CPMessageReader reader, Exception error) {
if (_runner.isDead()) return;
if (_log.shouldLog(Log.ERROR))
_log.error("Error occurred", error);
// Is this is a little drastic for an unknown message type?
// Send the whole exception string over for diagnostics
_runner.disconnectClient(error.toString());
_runner.stopRunning();
}
public void disconnected(I2CPMessageReader reader) {
if (_runner.isDead()) return;
_runner.disconnected();
}
/**
* Defaults in GetDateMessage options are NOT honored.
* Defaults are not serialized out-of-JVM, and the router does not recognize defaults in-JVM.
* Client side must promote defaults to the primary map.
*/
private void handleGetDate(GetDateMessage message) {
// sent by clients >= 0.8.7
String clientVersion = message.getVersion();
if (clientVersion != null)
_runner.setClientVersion(clientVersion);
Properties props = message.getOptions();
if (!checkAuth(props))
return;
try {
// only send version if the client can handle it (0.8.7 or greater)
_runner.doSend(new SetDateMessage(clientVersion != null ? CoreVersion.VERSION : null));
} catch (I2CPMessageException ime) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error writing out the setDate message", ime);
}
}
/**
* As of 0.8.7, does nothing. Do not allow a client to set the router's clock.
*/
private void handleSetDate(SetDateMessage message) {
//_context.clock().setNow(message.getDate().getTime());
}
/**
* Handle a CreateSessionMessage.
* On errors, we could perhaps send a SessionStatusMessage with STATUS_INVALID before
* sending the DisconnectMessage... but right now the client will send _us_ a
* DisconnectMessage in return, and not wait around for our DisconnectMessage.
* So keep it simple.
*
* Defaults in SessionConfig options are, in general, NOT honored.
* In-JVM client side must promote defaults to the primary map.
*/
private void handleCreateSession(CreateSessionMessage message) {
SessionConfig in = message.getSessionConfig();
Destination dest = in.getDestination();
if (in.verifySignature()) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Signature verified correctly on create session message");
} else {
// For now, we do NOT send a SessionStatusMessage - see javadoc above
int itype = dest.getCertificate().getCertificateType();
SigType stype = SigType.getByCode(itype);
if (stype == null || !stype.isAvailable()) {
_log.error("Client requested unsupported signature type " + itype);
_runner.disconnectClient("Unsupported signature type " + itype);
} else if (in.tooOld()) {
long skew = _context.clock().now() - in.getCreationDate().getTime();
String msg = "Create session message client clock skew? ";
if (skew >= 0)
msg += DataHelper.formatDuration(skew) + " in the past";
else
msg += DataHelper.formatDuration(0 - skew) + " in the future";
_log.error(msg);
_runner.disconnectClient(msg);
} else {
_log.error("Signature verification failed on a create session message");
_runner.disconnectClient("Invalid signature on CreateSessionMessage");
}
return;
}
// Auth, since 0.8.2
Properties inProps = in.getOptions();
if (!checkAuth(inProps))
return;
SessionId id = _runner.getSessionId(dest.calculateHash());
if (id != null) {
_runner.disconnectClient("Already have session " + id);
return;
}
// Copy over the whole config structure so we don't later corrupt it on
// the client side if we change settings or later get a
// ReconfigureSessionMessage
SessionConfig cfg = new SessionConfig(dest);
cfg.setSignature(in.getSignature());
Properties props = new Properties();
boolean isPrimary = _runner.getSessionIds().isEmpty();
if (!isPrimary) {
// all the primary options, then the overrides from the alias
SessionConfig pcfg = _runner.getPrimaryConfig();
if (pcfg != null) {
props.putAll(pcfg.getOptions());
} else {
_log.error("no primary config?");
}
}
props.putAll(inProps);
cfg.setOptions(props);
// this sets the session id
int status = _runner.sessionEstablished(cfg);
if (status != SessionStatusMessage.STATUS_CREATED) {
// For now, we do NOT send a SessionStatusMessage - see javadoc above
if (_log.shouldLog(Log.ERROR))
_log.error("Session establish failed: code = " + status);
String msg;
if (status == SessionStatusMessage.STATUS_INVALID)
msg = "duplicate destination";
else if (status == SessionStatusMessage.STATUS_REFUSED)
msg = "session limit exceeded";
else
msg = "unknown error";
_runner.disconnectClient(msg);
return;
}
// get the new session ID
id = _runner.getSessionId(dest.calculateHash());
if (_log.shouldLog(Log.INFO))
_log.info("Session " + id + " established for " + dest.calculateHash());
if (isPrimary) {
sendStatusMessage(id, status);
startCreateSessionJob(cfg);
} else {
SessionConfig pcfg = _runner.getPrimaryConfig();
if (pcfg != null) {
ClientTunnelSettings settings = new ClientTunnelSettings(dest.calculateHash());
settings.readFromProperties(props);
// addAlias() sends the create lease set msg, so we have to send the SMS first
sendStatusMessage(id, status);
boolean ok = _context.tunnelManager().addAlias(dest, settings, pcfg.getDestination());
if (!ok) {
_log.error("Add alias failed");
// FIXME cleanup
}
} else {
_log.error("no primary config?");
status = SessionStatusMessage.STATUS_INVALID;
sendStatusMessage(id, status);
// FIXME cleanup
}
}
}
/**
* Side effect - sets _authorized.
* Side effect - disconnects session if not authorized.
*
* @param props contains i2cp.username and i2cp.password, may be null
* @return success
* @since 0.9.11
*/
private boolean checkAuth(Properties props) {
if (_authorized)
return true;
if (_enforceAuth && _context.getBooleanProperty(PROP_AUTH)) {
String user = null;
String pw = null;
if (props != null) {
user = props.getProperty("i2cp.username");
pw = props.getProperty("i2cp.password");
}
if (user == null || user.length() == 0 || pw == null || pw.length() == 0) {
_log.logAlways(Log.WARN, "I2CP authentication failed");
_runner.disconnectClient("Authorization required, specify i2cp.username and i2cp.password in options");
_authorized = false;
return false;
}
PasswordManager mgr = new PasswordManager(_context);
if (!mgr.checkHash(PROP_AUTH, user, pw)) {
_log.logAlways(Log.WARN, "I2CP authentication failed, user: " + user);
_runner.disconnectClient("Authorization failed, user = " + user);
_authorized = false;
return false;
}
if (_log.shouldLog(Log.INFO))
_log.info("I2CP auth success user: " + user);
}
_authorized = true;
return true;
}
/**
* Override for testing
* @since 0.9.8
*
*/
protected void startCreateSessionJob(SessionConfig config) {
_context.jobQueue().addJob(new CreateSessionJob(_context, config));
}
/**
* Handle a SendMessageMessage: give it a message Id, have the ClientManager distribute
* it, and send the client an ACCEPTED message
*
*/
private void handleSendMessage(SendMessageMessage message) {
SessionId sid = message.getSessionId();
SessionConfig cfg = _runner.getConfig(sid);
if (cfg == null) {
List<SessionId> current = _runner.getSessionIds();
String msg = "SendMessage invalid session: " + sid + " current: " + current;
if (_log.shouldLog(Log.ERROR))
_log.error(msg);
// Just drop the message for now, don't kill the whole socket...
// bugs on client side, esp. prior to 0.9.21, may cause sending
// of messages before the session is established
//_runner.disconnectClient(msg);
// do this instead:
if (sid != null && message.getNonce() > 0) {
MessageStatusMessage status = new MessageStatusMessage();
status.setMessageId(_runner.getNextMessageId());
status.setSessionId(sid.getSessionId());
status.setSize(0);
status.setNonce(message.getNonce());
status.setStatus(MessageStatusMessage.STATUS_SEND_FAILURE_BAD_SESSION);
try {
_runner.doSend(status);
} catch (I2CPMessageException ime) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error writing out the message status message", ime);
}
}
return;
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("handleSendMessage called");
long beforeDistribute = _context.clock().now();
MessageId id = _runner.distributeMessage(message);
long timeToDistribute = _context.clock().now() - beforeDistribute;
// TODO validate session id
_runner.ackSendMessage(sid, id, message.getNonce());
_context.statManager().addRateData("client.distributeTime", timeToDistribute);
if ( (timeToDistribute > 50) && (_log.shouldLog(Log.DEBUG)) )
_log.debug("Took too long to distribute the message (which holds up the ack): " + timeToDistribute);
}
/**
* The client asked for a message, so we send it to them.
*
* This is only when not in fast receive mode.
* In the default fast receive mode, data is sent in MessageReceivedJob.
*/
private void handleReceiveBegin(ReceiveMessageBeginMessage message) {
if (_runner.isDead()) return;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Handling receive begin: id = " + message.getMessageId());
MessagePayloadMessage msg = new MessagePayloadMessage();
msg.setMessageId(message.getMessageId());
// TODO validate session id
msg.setSessionId(message.getSessionId());
Payload payload = _runner.getPayload(new MessageId(message.getMessageId()));
if (payload == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Payload for message id [" + message.getMessageId()
+ "] is null! Dropped or Unknown message id");
return;
}
msg.setPayload(payload);
try {
_runner.doSend(msg);
} catch (I2CPMessageException ime) {
String emsg = "Error sending data to client " + _runner.getDestHash();
if (_log.shouldWarn())
_log.warn(emsg, ime);
else
_log.logAlways(Log.WARN, emsg);
_runner.removePayload(new MessageId(message.getMessageId()));
}
}
/**
* The client told us that the message has been received completely. This currently
* does not do any security checking prior to removing the message from the
* pending queue, though it should.
*
*/
private void handleReceiveEnd(ReceiveMessageEndMessage message) {
_runner.removePayload(new MessageId(message.getMessageId()));
}
private void handleDestroySession(DestroySessionMessage message) {
SessionId id = message.getSessionId();
if (id != null) {
_runner.removeSession(id);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("destroy session with null ID");
}
int left = _runner.getSessionIds().size();
if (left <= 0 || id == null) {
_runner.stopRunning();
} else {
if (_log.shouldLog(Log.INFO))
_log.info("Still " + left + " sessions left");
}
}
/** override for testing */
protected void handleCreateLeaseSet(CreateLeaseSetMessage message) {
if ( (message.getLeaseSet() == null) || (message.getPrivateKey() == null) || (message.getSigningPrivateKey() == null) ) {
if (_log.shouldLog(Log.ERROR))
_log.error("Null lease set granted: " + message);
_runner.disconnectClient("Invalid CreateLeaseSetMessage");
return;
}
SessionId id = message.getSessionId();
SessionConfig cfg = _runner.getConfig(id);
if (cfg == null) {
List<SessionId> current = _runner.getSessionIds();
String msg = "CreateLeaseSet invalid session: " + id + " current: " + current;
if (_log.shouldLog(Log.ERROR))
_log.error(msg);
_runner.disconnectClient(msg);
return;
}
Destination dest = cfg.getDestination();
Destination ndest = message.getLeaseSet().getDestination();
if (!dest.equals(ndest)) {
if (_log.shouldLog(Log.ERROR))
_log.error("Different destination in LS");
_runner.disconnectClient("Different destination in LS");
return;
}
LeaseSetKeys keys = _context.keyManager().getKeys(dest);
if (keys == null ||
!message.getPrivateKey().equals(keys.getDecryptionKey())) {
// Verify and register crypto keys if new or if changed
// Private crypto key should never change, and if it does,
// one of the checks below will fail
PublicKey pk;
try {
pk = message.getPrivateKey().toPublic();
} catch (IllegalArgumentException iae) {
if (_log.shouldLog(Log.ERROR))
_log.error("Bad private key in LS");
_runner.disconnectClient("Bad private key in LS");
return;
}
if (!pk.equals(message.getLeaseSet().getEncryptionKey())) {
if (_log.shouldLog(Log.ERROR))
_log.error("Private/public crypto key mismatch in LS");
_runner.disconnectClient("Private/public crypto key mismatch in LS");
return;
}
// just register new SPK, don't verify, unused
_context.keyManager().registerKeys(dest, message.getSigningPrivateKey(), message.getPrivateKey());
} else if (!message.getSigningPrivateKey().equals(keys.getRevocationKey())) {
// just register new SPK, don't verify, unused
_context.keyManager().registerKeys(dest, message.getSigningPrivateKey(), message.getPrivateKey());
}
try {
_context.netDb().publish(message.getLeaseSet());
} catch (IllegalArgumentException iae) {
if (_log.shouldLog(Log.ERROR))
_log.error("Invalid leaseset from client", iae);
_runner.disconnectClient("Invalid leaseset: " + iae);
return;
}
if (_log.shouldLog(Log.INFO))
_log.info("New lease set granted for destination " + dest);
// leaseSetCreated takes care of all the LeaseRequestState stuff (including firing any jobs)
_runner.leaseSetCreated(message.getLeaseSet());
}
/** override for testing */
protected void handleDestLookup(DestLookupMessage message) {
// no session id in DLM
_context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash(),
_runner.getDestHash()));
}
/**
* override for testing
* @since 0.9.11
*/
protected void handleHostLookup(HostLookupMessage message) {
SessionId sid = message.getSessionId();
Hash h;
if (sid != null) {
h = _runner.getDestHash(sid);
} else {
// fixup if necessary
if (message.getReqID() >= 0)
sid = new SessionId(65535);
h = null;
}
if (h == null) {
h = _runner.getDestHash();
// h may still be null, an LS lookup for b32 will go out expl. tunnels
}
_context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getReqID(),
message.getTimeout(), sid,
message.getHash(), message.getHostname(), h));
}
/**
* Message's Session ID ignored. This doesn't support removing previously set options.
* Nor do we bother with message.getSessionConfig().verifySignature() ... should we?
* Nor is the Date checked.
*
* Note that this does NOT update the few options handled in
* ClientConnectionRunner.sessionEstablished(). Those can't be changed later.
*
* Defaults in SessionConfig options are, in general, NOT honored.
* In-JVM client side must promote defaults to the primary map.
*/
private void handleReconfigureSession(ReconfigureSessionMessage message) {
SessionId id = message.getSessionId();
SessionConfig cfg = _runner.getConfig(id);
if (cfg == null) {
List<SessionId> current = _runner.getSessionIds();
String msg = "ReconfigureSession invalid session: " + id + " current: " + current;
if (_log.shouldLog(Log.ERROR))
_log.error(msg);
//sendStatusMessage(id, SessionStatusMessage.STATUS_INVALID);
_runner.disconnectClient(msg);
return;
}
if (_log.shouldLog(Log.INFO))
_log.info("Updating options - old: " + cfg + " new: " + message.getSessionConfig());
if (!message.getSessionConfig().getDestination().equals(cfg.getDestination())) {
_log.error("Dest mismatch");
sendStatusMessage(id, SessionStatusMessage.STATUS_INVALID);
_runner.stopRunning();
return;
}
Hash dest = cfg.getDestination().calculateHash();
cfg.getOptions().putAll(message.getSessionConfig().getOptions());
ClientTunnelSettings settings = new ClientTunnelSettings(dest);
Properties props = new Properties();
props.putAll(cfg.getOptions());
settings.readFromProperties(props);
_context.tunnelManager().setInboundSettings(dest,
settings.getInboundSettings());
_context.tunnelManager().setOutboundSettings(dest,
settings.getOutboundSettings());
sendStatusMessage(id, SessionStatusMessage.STATUS_UPDATED);
}
private void sendStatusMessage(SessionId id, int status) {
SessionStatusMessage msg = new SessionStatusMessage();
msg.setSessionId(id);
msg.setStatus(status);
try {
_runner.doSend(msg);
} catch (I2CPMessageException ime) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error writing out the session status message", ime);
}
}
/**
* Divide router limit by 1.75 for overhead.
* This could someday give a different answer to each client.
* But it's not enforced anywhere.
*/
protected void handleGetBWLimits(GetBandwidthLimitsMessage message) {
if (_log.shouldLog(Log.INFO))
_log.info("Got BW Limits request");
int in = _context.bandwidthLimiter().getInboundKBytesPerSecond() * 4 / 7;
int out = _context.bandwidthLimiter().getOutboundKBytesPerSecond() * 4 / 7;
BandwidthLimitsMessage msg = new BandwidthLimitsMessage(in, out);
try {
_runner.doSend(msg);
} catch (I2CPMessageException ime) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error writing bw limits msg", ime);
}
}
}