/* * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ /* * Copyright (c) 2010, Remko Tronçon. * All rights reserved. */ package com.isode.stroke.client; import com.isode.stroke.base.NotNull; import com.isode.stroke.elements.Message; import com.isode.stroke.elements.Presence; import com.isode.stroke.elements.Stanza; import com.isode.stroke.elements.StreamType; import com.isode.stroke.jid.JID; import com.isode.stroke.network.Connection; import com.isode.stroke.network.ConnectionFactory; import com.isode.stroke.network.Connector; import com.isode.stroke.network.DomainNameResolveError; import com.isode.stroke.network.NetworkFactories; import com.isode.stroke.parser.payloadparsers.FullPayloadParserFactoryCollection; import com.isode.stroke.queries.IQRouter; import com.isode.stroke.serializer.payloadserializers.FullPayloadSerializerCollection; import com.isode.stroke.session.BasicSessionStream; import com.isode.stroke.session.SessionStream; import com.isode.stroke.signals.Signal; import com.isode.stroke.signals.Signal1; import com.isode.stroke.signals.SignalConnection; import com.isode.stroke.signals.Slot; import com.isode.stroke.signals.Slot1; import com.isode.stroke.signals.Slot2; import com.isode.stroke.tls.Certificate; import com.isode.stroke.tls.CertificateTrustChecker; import com.isode.stroke.tls.CertificateVerificationError; import com.isode.stroke.tls.CertificateWithKey; import com.isode.stroke.tls.PlatformTLSFactories; /** * The central class for communicating with an XMPP server. * * This class is responsible for setting up the connection with the XMPP server, authenticating, and * initializing the session. * * This class can be used directly in your application, although the Client subclass provides more * functionality and interfaces, and is better suited for most needs. */ public class CoreClient { /** * The user should add a listener to this signal, which will be called when * the client was disconnected from tne network. * *

If the disconnection was due to a non-recoverable error, the type * of error will be passed as a parameter. */ public final Signal1 onDisconnected = new Signal1(); /** * The user should add a listener to this signal, which will be called when * the connection is established with the server. */ public final Signal onConnected = new Signal(); /** * The user may add a listener to this signal, which will be called when * data are received from the server. Useful for observing protocol exchange. */ public final Signal1 onDataRead = new Signal1(); /** * The user may add a listener to this signal, which will be called when * data are sent to the server. Useful for observing protocol exchange. */ public final Signal1 onDataWritten = new Signal1(); /** * Called when a message stanza is received. */ public final Signal1 onMessageReceived = new Signal1(); /** * Called when a presence stanza is received. */ public final Signal1 onPresenceReceived = new Signal1(); /** * Called when a stanza has been received and acked by a server supporting XEP-0198. */ public final Signal1 onStanzaAcked = new Signal1(); private JID jid_; private String password_; private ClientSessionStanzaChannel stanzaChannel_; private IQRouter iqRouter_; private Connector connector_; //private ConnectionFactory connectionFactory_; private FullPayloadParserFactoryCollection payloadParserFactories_ = new FullPayloadParserFactoryCollection(); private FullPayloadSerializerCollection payloadSerializers_ = new FullPayloadSerializerCollection(); private Connection connection_; private BasicSessionStream sessionStream_; private ClientSession session_; private CertificateWithKey certificate_; private boolean disconnectRequested_; private ClientOptions options; private CertificateTrustChecker certificateTrustChecker; private NetworkFactories networkFactories; private PlatformTLSFactories tlsFactories; private SignalConnection sessionStreamDataReadConnection_; private SignalConnection sessionStreamDataWrittenConnection_; private SignalConnection sessionFinishedConnection_; private SignalConnection sessionNeedCredentialsConnection_; private SignalConnection connectorConnectFinishedConnection_; /** * Constructor. * * @param eventLoop Event loop used by the class, must not be null. The * CoreClient creates threads to do certain tasks. However, it * posts events that it expects to be done in the application's * main thread to this eventLoop. The application should * use an appropriate EventLoop implementation for the application type. This * EventLoop is just a way for the CoreClient to pass these * events back to the main thread, and should not be used by the * application for its own purposes. * @param jid User JID used to connect to the server, must not be null * @param password User password to use, must not be null * @param networkFactories An implementation of network interaction, must * not be null. */ public CoreClient(final JID jid, final String password, final NetworkFactories networkFactories) { jid_ = jid; password_ = password; disconnectRequested_ = false; this.networkFactories = networkFactories; this.certificateTrustChecker = null; stanzaChannel_ = new ClientSessionStanzaChannel(); stanzaChannel_.onMessageReceived.connect(new Slot1() { public void call(Message p1) { onMessageReceived.emit(p1); } }); stanzaChannel_.onPresenceReceived.connect(new Slot1() { public void call(Presence p1) { onPresenceReceived.emit(p1); } }); stanzaChannel_.onStanzaAcked.connect(new Slot1() { public void call(Stanza p1) { onStanzaAcked.emit(p1); } }); stanzaChannel_.onAvailableChanged.connect(new Slot1() { public void call(Boolean p1) { handleStanzaChannelAvailableChanged(p1); } }); iqRouter_ = new IQRouter(stanzaChannel_); tlsFactories = new PlatformTLSFactories(); } /*CoreClient::~CoreClient() { if (session_ || connection_) { std::cerr << "Warning: Client not disconnected properly" << std::endl; } delete tlsLayerFactory_; delete timerFactory_; delete connectionFactory_; delete iqRouter_; stanzaChannel_->onAvailableChanged.disconnect(boost::bind(&CoreClient::handleStanzaChannelAvailableChanged, this, _1)); stanzaChannel_->onMessageReceived.disconnect(boost::ref(onMessageReceived)); stanzaChannel_->onPresenceReceived.disconnect(boost::ref(onPresenceReceived)); stanzaChannel_->onStanzaAcked.disconnect(boost::ref(onStanzaAcked)); delete stanzaChannel_; }*/ /** * Connect using the standard XMPP connection rules (i.e. SRV then A/AAAA). * * @param o Client options to use in the connection, must not be null */ public void connect(final ClientOptions o) { forceReset(); disconnectRequested_ = false; assert (connector_ == null); options = o; /* FIXME: Port Proxies */ String host = (o.manualHostname == null || o.manualHostname.isEmpty()) ? jid_.getDomain() : o.manualHostname; int port = o.manualPort; connector_ = Connector.create(host, port, o.manualHostname == null || o.manualHostname.isEmpty(), networkFactories.getDomainNameResolver(), networkFactories.getConnectionFactory(), networkFactories.getTimerFactory()); connectorConnectFinishedConnection_ = connector_.onConnectFinished.connect(new Slot2() { public void call(Connection p1, com.isode.stroke.base.Error p2) { handleConnectorFinished(p1, p2); } }); connector_.setTimeoutMilliseconds(60 * 1000); connector_.start(); } private void bindSessionToStream() { session_ = ClientSession.create(jid_, sessionStream_); session_.setCertificateTrustChecker(certificateTrustChecker); session_.setUseStreamCompression(options.useStreamCompression); session_.setAllowPLAINOverNonTLS(options.allowPLAINWithoutTLS); switch (options.useTLS) { case UseTLSWhenAvailable: session_.setUseTLS(ClientSession.UseTLS.UseTLSWhenAvailable); session_.setCertificateTrustChecker(certificateTrustChecker); break; case NeverUseTLS: session_.setUseTLS(ClientSession.UseTLS.NeverUseTLS); break; case RequireTLS: session_.setUseTLS(ClientSession.UseTLS.RequireTLS); break; } session_.setUseAcks(options.useAcks); stanzaChannel_.setSession(session_); sessionFinishedConnection_ = session_.onFinished.connect(new Slot1() { public void call(com.isode.stroke.base.Error p1) { handleSessionFinished(p1); } }); sessionNeedCredentialsConnection_ = session_.onNeedCredentials.connect(new Slot() { public void call() { handleNeedCredentials(); } }); session_.start(); } void handleConnectorFinished(final Connection connection, final com.isode.stroke.base.Error error) { resetConnector(); if (connection == null) { ClientError clientError = null; if (!disconnectRequested_) { if (error instanceof DomainNameResolveError) { clientError = new ClientError(ClientError.Type.DomainNameResolveError); } else { clientError = new ClientError(ClientError.Type.ConnectionError); } } onDisconnected.emit(clientError); } else { assert (connection_ == null); connection_ = connection; assert (sessionStream_ == null); sessionStream_ = new BasicSessionStream(StreamType.ClientStreamType, connection_, payloadParserFactories_, payloadSerializers_, tlsFactories.getTLSContextFactory(), networkFactories.getTimerFactory()); if (certificate_ != null && !certificate_.isNull()) { sessionStream_.setTLSCertificate(certificate_); } sessionStreamDataReadConnection_ = sessionStream_.onDataRead.connect(new Slot1() { public void call(String p1) { handleDataRead(p1); } }); sessionStreamDataWrittenConnection_ = sessionStream_.onDataWritten.connect(new Slot1() { public void call(String p1) { handleDataWritten(p1); } }); bindSessionToStream(); } } /** * Close the stream and disconnect from the server. */ public void disconnect() { // FIXME: We should be able to do without this boolean. We just have to make sure we can tell the difference between // connector finishing without a connection due to an error or because of a disconnect. disconnectRequested_ = true; if (session_ != null && !session_.isFinished()) { session_.finish(); } else if (connector_ != null) { connector_.stop(); } } public void setCertificate(final CertificateWithKey certificate) { certificate_ = certificate; } /** * Sets the certificate trust checker. If a server presents a certificate * which does not conform to the requirements of RFC 6120, then the * trust checker, if configured, will be called. If the trust checker * says the certificate is trusted, then connecting will proceed; if * not, the connection will end with an error. * * @param checker a CertificateTrustChecker that will be called when * the server sends a TLS certificate that does not validate. */ public void setCertificateTrustChecker(final CertificateTrustChecker checker) { certificateTrustChecker = checker; } private void handleSessionFinished(final com.isode.stroke.base.Error error) { sessionFinishedConnection_.disconnect(); sessionNeedCredentialsConnection_.disconnect(); session_ = null; sessionStreamDataReadConnection_.disconnect(); sessionStreamDataWrittenConnection_.disconnect(); sessionStream_ = null; connection_.disconnect(); connection_ = null; ClientError clientError = null; if (error != null) { if (error instanceof ClientSession.Error) { ClientSession.Error actualError = (ClientSession.Error) error; switch (actualError.type) { case AuthenticationFailedError: clientError = new ClientError(ClientError.Type.AuthenticationFailedError); break; case CompressionFailedError: clientError = new ClientError(ClientError.Type.CompressionFailedError); break; case ServerVerificationFailedError: clientError = new ClientError(ClientError.Type.ServerVerificationFailedError); break; case NoSupportedAuthMechanismsError: clientError = new ClientError(ClientError.Type.NoSupportedAuthMechanismsError); break; case UnexpectedElementError: clientError = new ClientError(ClientError.Type.UnexpectedElementError); break; case ResourceBindError: clientError = new ClientError(ClientError.Type.ResourceBindError); break; case SessionStartError: clientError = new ClientError(ClientError.Type.SessionStartError); break; case TLSError: clientError = new ClientError(ClientError.Type.TLSError); break; case TLSClientCertificateError: clientError = new ClientError(ClientError.Type.ClientCertificateError); break; /* Note: no case clause for "StreamError" */ } } else if (error instanceof SessionStream.Error) { SessionStream.Error actualError = (SessionStream.Error) error; switch (actualError.type) { case ParseError: clientError = new ClientError(ClientError.Type.XMLError); break; case TLSError: clientError = new ClientError(ClientError.Type.TLSError); break; case InvalidTLSCertificateError: clientError = new ClientError(ClientError.Type.ClientCertificateLoadError); break; case ConnectionReadError: clientError = new ClientError(ClientError.Type.ConnectionReadError); break; case ConnectionWriteError: clientError = new ClientError(ClientError.Type.ConnectionWriteError); break; } } else if (error instanceof CertificateVerificationError) { CertificateVerificationError verificationError = (CertificateVerificationError)error; switch (verificationError.type) { case UnknownError: clientError = new ClientError(ClientError.Type.UnknownCertificateError); break; case Expired: clientError = new ClientError(ClientError.Type.CertificateExpiredError); break; case NotYetValid: clientError = new ClientError(ClientError.Type.CertificateNotYetValidError); break; case SelfSigned: clientError = new ClientError(ClientError.Type.CertificateSelfSignedError); break; case Rejected: clientError = new ClientError(ClientError.Type.CertificateRejectedError); break; case Untrusted: clientError = new ClientError(ClientError.Type.CertificateUntrustedError); break; case InvalidPurpose: clientError = new ClientError(ClientError.Type.InvalidCertificatePurposeError); break; case PathLengthExceeded: clientError = new ClientError(ClientError.Type.CertificatePathLengthExceededError); break; case InvalidSignature: clientError = new ClientError(ClientError.Type.InvalidCertificateSignatureError); break; case InvalidCA: clientError = new ClientError(ClientError.Type.InvalidCAError); break; case InvalidServerIdentity: clientError = new ClientError(ClientError.Type.InvalidServerIdentityError); break; } } /* If "error" was non-null, we expect to be able to derive * a non-null "clientError". */ NotNull.exceptIfNull(clientError,"clientError"); } onDisconnected.emit(clientError); } private void handleNeedCredentials() { assert session_ != null; session_.sendCredentials(password_); } private void handleDataRead(final String data) { onDataRead.emit(data); } private void handleDataWritten(final String data) { onDataWritten.emit(data); } private void handleStanzaChannelAvailableChanged(final boolean available) { if (available) { onConnected.emit(); } } public void sendMessage(final Message message) { stanzaChannel_.sendMessage(message); } public void sendPresence(final Presence presence) { stanzaChannel_.sendPresence(presence); } /** * Get the IQRouter responsible for all IQs on this connection. * Use this to send IQs. */ public IQRouter getIQRouter() { return iqRouter_; } public StanzaChannel getStanzaChannel() { return stanzaChannel_; } /** * @return session is available for sending/receiving stanzas. */ public boolean isAvailable() { return stanzaChannel_.isAvailable(); } /** * Determine whether the underlying session is encrypted with TLS * @return true if the session is initialized and encrypted with TLS, * false otherwise. */ public boolean isSessionTLSEncrypted() { return (sessionStream_ != null && sessionStream_.isTLSEncrypted()); } /** * If the session is initialized and encrypted with TLS, then the * certificate presented by the peer is returned * @return the peer certificate, if one is available, otherwise null. */ public Certificate getSessionCertificate() { return (isSessionTLSEncrypted() ? sessionStream_.getPeerCertificate() : null); } private void resetConnector() { if (connectorConnectFinishedConnection_ != null) { connectorConnectFinishedConnection_.disconnect(); } connector_ = null; } private void resetSession() { session_.onFinished.disconnectAll(); session_.onNeedCredentials.disconnectAll(); sessionStream_.onDataRead.disconnectAll(); sessionStream_.onDataWritten.disconnectAll(); if (connection_ != null) { connection_.disconnect(); } sessionStream_ = null; connection_ = null; } /** * @return JID of the client, will never be null. After the session was * initialized, this returns the bound JID (the JID provided by * the server during resource binding). Prior to this it returns * the JID provided by the user. */ public JID getJID() { if (session_ != null) { return session_.getLocalJID(); } else { return jid_; } } private void forceReset() { if (connector_ != null) { resetConnector(); } if (sessionStream_ != null || connection_ != null) { resetSession(); } } @Override public String toString() { return "CoreClient for \"" + jid_ + "\"" + "; session " + (isAvailable() ? "" : "un") + "available"; } }