diff options
author | Kevin Smith <git@kismith.co.uk> | 2011-07-01 09:19:49 (GMT) |
---|---|---|
committer | Kevin Smith <git@kismith.co.uk> | 2011-07-01 09:19:49 (GMT) |
commit | 2da71a8a85486a494343f1662d64fb5ae5a2a44e (patch) | |
tree | 23992f9f2a00bac23b345e5c2cc9c1194efc25be /src/com/isode/stroke/client | |
download | stroke-2da71a8a85486a494343f1662d64fb5ae5a2a44e.zip stroke-2da71a8a85486a494343f1662d64fb5ae5a2a44e.tar.bz2 |
Initial import
Diffstat (limited to 'src/com/isode/stroke/client')
-rw-r--r-- | src/com/isode/stroke/client/ClientError.java | 57 | ||||
-rw-r--r-- | src/com/isode/stroke/client/ClientOptions.java | 45 | ||||
-rw-r--r-- | src/com/isode/stroke/client/ClientSession.java | 608 | ||||
-rw-r--r-- | src/com/isode/stroke/client/ClientSessionStanzaChannel.java | 125 | ||||
-rw-r--r-- | src/com/isode/stroke/client/CoreClient.java | 385 | ||||
-rw-r--r-- | src/com/isode/stroke/client/IDGenerator.java | 22 | ||||
-rw-r--r-- | src/com/isode/stroke/client/StanzaChannel.java | 35 |
7 files changed, 1277 insertions, 0 deletions
diff --git a/src/com/isode/stroke/client/ClientError.java b/src/com/isode/stroke/client/ClientError.java new file mode 100644 index 0000000..9412e9a --- /dev/null +++ b/src/com/isode/stroke/client/ClientError.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.client; + +/** + * + */ +public class ClientError { + + private final Type type_; + + enum Type { + + UnknownError, + DomainNameResolveError, + ConnectionError, + ConnectionReadError, + ConnectionWriteError, + XMLError, + AuthenticationFailedError, + CompressionFailedError, + ServerVerificationFailedError, + NoSupportedAuthMechanismsError, + UnexpectedElementError, + ResourceBindError, + SessionStartError, + TLSError, + ClientCertificateLoadError, + ClientCertificateError, + // Certificate verification errors + UnknownCertificateError, + CertificateExpiredError, + CertificateNotYetValidError, + CertificateSelfSignedError, + CertificateRejectedError, + CertificateUntrustedError, + InvalidCertificatePurposeError, + CertificatePathLengthExceededError, + InvalidCertificateSignatureError, + InvalidCAError, + InvalidServerIdentityError, + }; + + ClientError(Type type) { + type_ = type; + } + + Type getType() { + return type_; + } +} diff --git a/src/com/isode/stroke/client/ClientOptions.java b/src/com/isode/stroke/client/ClientOptions.java new file mode 100644 index 0000000..dad4204 --- /dev/null +++ b/src/com/isode/stroke/client/ClientOptions.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2011 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.client; + +/** + * + */ +public class ClientOptions { + + enum UseTLS { + + NeverUseTLS, + UseTLSWhenAvailable + }; + + public ClientOptions() { + useStreamCompression = true; + useTLS = UseTLS.UseTLSWhenAvailable; + useStreamResumption = false; + } + /** + * Whether ZLib stream compression should be used when available. + * + * Default: true + */ + public boolean useStreamCompression; + /** + * Sets whether TLS encryption should be used. + * + * Default: UseTLSWhenAvailable + */ + public UseTLS useTLS; + /** + * Use XEP-196 stream resumption when available. + * + * Default: false + */ + public boolean useStreamResumption; +} diff --git a/src/com/isode/stroke/client/ClientSession.java b/src/com/isode/stroke/client/ClientSession.java new file mode 100644 index 0000000..59427b0 --- /dev/null +++ b/src/com/isode/stroke/client/ClientSession.java @@ -0,0 +1,608 @@ +/* + * Copyright (c) 2010-2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010-2011 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.client; + +import com.isode.stroke.elements.AuthChallenge; +import com.isode.stroke.elements.AuthFailure; +import com.isode.stroke.elements.AuthRequest; +import com.isode.stroke.elements.AuthResponse; +import com.isode.stroke.elements.AuthSuccess; +import com.isode.stroke.elements.CompressFailure; +import com.isode.stroke.elements.CompressRequest; +import com.isode.stroke.elements.Compressed; +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.EnableStreamManagement; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.elements.ProtocolHeader; +import com.isode.stroke.elements.ResourceBind; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.elements.StanzaAck; +import com.isode.stroke.elements.StanzaAckRequest; +import com.isode.stroke.elements.StartSession; +import com.isode.stroke.elements.StartTLSFailure; +import com.isode.stroke.elements.StreamFeatures; +import com.isode.stroke.elements.StartTLSRequest; +import com.isode.stroke.elements.StreamError; +import com.isode.stroke.elements.StreamManagementEnabled; +import com.isode.stroke.elements.StreamManagementFailed; +import com.isode.stroke.elements.TLSProceed; +import com.isode.stroke.jid.JID; +import com.isode.stroke.sasl.ClientAuthenticator; +import com.isode.stroke.sasl.PLAINClientAuthenticator; +import com.isode.stroke.sasl.SCRAMSHA1ClientAuthenticator; +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.streammanagement.StanzaAckRequester; +import com.isode.stroke.streammanagement.StanzaAckResponder; +import com.isode.stroke.tls.Certificate; +import com.isode.stroke.tls.CertificateTrustChecker; +import com.isode.stroke.tls.CertificateVerificationError; +import com.isode.stroke.tls.ServerIdentityVerifier; +import java.util.UUID; + +public class ClientSession { + private SignalConnection streamElementReceivedConnection; + private SignalConnection streamStreamStartReceivedConnection; + private SignalConnection streamClosedConnection; + private SignalConnection streamTLSEncryptedConnection; + private SignalConnection stanzaAckOnRequestConnection_; + private SignalConnection stanzaAckOnAckedConnection_; + private SignalConnection stanzaResponderAckConnection_; + + public enum State { + + Initial, + WaitingForStreamStart, + Negotiating, + Compressing, + WaitingForEncrypt, + Encrypting, + WaitingForCredentials, + Authenticating, + EnablingSessionManagement, + BindingResource, + StartingSession, + Initialized, + Finishing, + Finished + }; + + public static class Error implements com.isode.stroke.base.Error { + + public enum Type { + + AuthenticationFailedError, + CompressionFailedError, + ServerVerificationFailedError, + NoSupportedAuthMechanismsError, + UnexpectedElementError, + ResourceBindError, + SessionStartError, + TLSClientCertificateError, + TLSError, + StreamError + }; + + public Error(Type type) { + if (type == null) { + throw new IllegalStateException(); + } + this.type = type; + } + public final Type type; + }; + + public enum UseTLS { + NeverUseTLS, + UseTLSWhenAvailable + } + + private ClientSession(JID jid, SessionStream stream) { + localJID = jid; + state = State.Initial; + this.stream = stream; + allowPLAINOverNonTLS = true; /* FIXME: false */ + needSessionStart = false; + needResourceBind = false; + needAcking = false; + authenticator = null; + } + + public static ClientSession create(JID jid, SessionStream stream) { + return new ClientSession(jid, stream); + } + + public State getState() { + return state; + } + + public void setAllowPLAINOverNonTLS(boolean b) { + allowPLAINOverNonTLS = b; + } + + public void setUseStreamCompression(boolean b) { + useStreamCompression = b; + } + + public void setUseTLS(UseTLS use) { + useTLS = use; + } + + public boolean getStreamManagementEnabled() { + return stanzaAckRequester_ != null; + } + + public boolean getRosterVersioningSuported() { + return rosterVersioningSupported; + } + + public JID getLocalJID() { + return localJID; + } + + public boolean isFinished() { + return State.Finished.equals(getState()); + } + + public void setCertificateTrustChecker(CertificateTrustChecker checker) { + certificateTrustChecker = checker; + } + + public void start() { + streamStreamStartReceivedConnection = stream.onStreamStartReceived.connect(new Slot1<ProtocolHeader>(){ + public void call(ProtocolHeader p1) { + handleStreamStart(p1); + } + }); + streamElementReceivedConnection = stream.onElementReceived.connect(new Slot1<Element>(){ + public void call(Element p1) { + handleElement(p1); + } + }); + streamClosedConnection = stream.onClosed.connect(new Slot1<SessionStream.Error>(){ + public void call(SessionStream.Error p1) { + handleStreamClosed(p1); + } + }); + streamTLSEncryptedConnection = stream.onTLSEncrypted.connect(new Slot(){ + public void call() { + handleTLSEncrypted(); + } + }); + + assert state.equals(State.Initial); + state = State.WaitingForStreamStart; + sendStreamHeader(); + } + + private void sendStreamHeader() { + ProtocolHeader header = new ProtocolHeader(); + header.setTo(getRemoteJID().toString()); + stream.writeHeader(header); + } + + public void sendStanza(Stanza stanza) { + stream.writeElement(stanza); + if (stanzaAckRequester_ != null) { + stanzaAckRequester_.handleStanzaSent(stanza); + } + } + + private void handleStreamStart(ProtocolHeader header) { + if (!checkState(State.WaitingForStreamStart)) { + return; + } + state = State.Negotiating; + } + + private void handleElement(Element element) { + if (element instanceof Stanza) { + Stanza stanza = (Stanza) element; + if (stanzaAckResponder_ != null) { + stanzaAckResponder_.handleStanzaReceived(); + } + if (getState().equals(State.Initialized)) { + onStanzaReceived.emit(stanza); + } + else if (stanza instanceof IQ) { + IQ iq = (IQ)stanza; + if (getState().equals(State.BindingResource)) { + ResourceBind resourceBind = iq.getPayload(new ResourceBind()); + if (IQ.Type.Error.equals(iq.getType()) && iq.getID().equals("session-bind")) { + finishSession(Error.Type.ResourceBindError); + } + else if (resourceBind == null) { + finishSession(Error.Type.UnexpectedElementError); + } + else if (IQ.Type.Result.equals(iq.getType())) { + localJID = resourceBind.getJID(); + if (!localJID.isValid()) { + finishSession(Error.Type.ResourceBindError); + } + needResourceBind = false; + continueSessionInitialization(); + } + else { + finishSession(Error.Type.UnexpectedElementError); + } + } + else if (state.equals(State.StartingSession)) { + if (IQ.Type.Result.equals(iq.getType())) { + needSessionStart = false; + continueSessionInitialization(); + } + else if (IQ.Type.Error.equals(iq.getType())) { + finishSession(Error.Type.SessionStartError); + } + else { + finishSession(Error.Type.UnexpectedElementError); + } + } + else { + finishSession(Error.Type.UnexpectedElementError); + } + } + } + else if (element instanceof StanzaAckRequest) { + if (stanzaAckResponder_ != null) { + + stanzaAckResponder_.handleAckRequestReceived(); + } + } + else if (element instanceof StanzaAck) { + StanzaAck ack = (StanzaAck) element; + if (stanzaAckRequester_ != null) { + if (ack.isValid()) { + stanzaAckRequester_.handleAckReceived(ack.getHandledStanzasCount()); + } + else { + //logger_.warning("Got invalid ack from server"); /*FIXME: Do we want logging here? + } + } + else { + //logger_.warning("Ignoring ack"); /*FIXME: Do we want logging here?*/ + } + } + else if (element instanceof StreamError) { + finishSession(Error.Type.StreamError); + } + else if (State.Initialized.equals(getState())) { + Stanza stanza = element instanceof Stanza ? (Stanza)element : null; + if (stanza != null) { + if (stanzaAckResponder_ != null) { + stanzaAckResponder_.handleStanzaReceived(); + } + onStanzaReceived.emit(stanza); + } + } + else if (element instanceof StreamFeatures) { + StreamFeatures streamFeatures = (StreamFeatures) element; + if (!checkState(State.Negotiating)) { + return; + } + + if (streamFeatures.hasStartTLS() && stream.supportsTLSEncryption()) { + state = State.WaitingForEncrypt; + stream.writeElement(new StartTLSRequest()); + } + else if (false && streamFeatures.hasCompressionMethod("zlib")) { /*FIXME: test and enable!*/ + state = State.Compressing; + stream.writeElement(new CompressRequest("zlib")); + } + else if (streamFeatures.hasAuthenticationMechanisms()) { + if (stream.hasTLSCertificate()) { + if (streamFeatures.hasAuthenticationMechanism("EXTERNAL")) { + state = State.Authenticating; + stream.writeElement(new AuthRequest("EXTERNAL")); + } + else { + finishSession(Error.Type.TLSClientCertificateError); + } + } + else if (streamFeatures.hasAuthenticationMechanism("EXTERNAL")) { + state = State.Authenticating; + stream.writeElement(new AuthRequest("EXTERNAL")); + } + else if (streamFeatures.hasAuthenticationMechanism("SCRAM-SHA-1") || streamFeatures.hasAuthenticationMechanism("SCRAM-SHA-1-PLUS")) { + SCRAMSHA1ClientAuthenticator scramAuthenticator = new SCRAMSHA1ClientAuthenticator(UUID.randomUUID().toString(), streamFeatures.hasAuthenticationMechanism("SCRAM-SHA-1-PLUS")); + if (stream.isTLSEncrypted()) { + scramAuthenticator.setTLSChannelBindingData(stream.getTLSFinishMessage()); + } + authenticator = scramAuthenticator; + state = State.WaitingForCredentials; + onNeedCredentials.emit(); + } + else if ((stream.isTLSEncrypted() || allowPLAINOverNonTLS) && streamFeatures.hasAuthenticationMechanism("PLAIN")) { + authenticator = new PLAINClientAuthenticator(); + state = State.WaitingForCredentials; + onNeedCredentials.emit(); + } +// //FIXME: Port +// else if (streamFeatures.hasAuthenticationMechanism("DIGEST-MD5")) { +// // FIXME: Host should probably be the actual host +// authenticator = new DIGESTMD5ClientAuthenticator(localJID.getDomain(), UUID.randomUUID()); +// state = State.WaitingForCredentials; +// onNeedCredentials.emit(); +// } + else { + finishSession(Error.Type.NoSupportedAuthMechanismsError); + } + } + else { + // Start the session + rosterVersioningSupported = streamFeatures.hasRosterVersioning(); + stream.setWhitespacePingEnabled(true); + needSessionStart = streamFeatures.hasSession(); + needResourceBind = streamFeatures.hasResourceBind(); + needAcking = streamFeatures.hasStreamManagement(); + if (!needResourceBind) { + // Resource binding is a MUST + finishSession(Error.Type.ResourceBindError); + } else { + continueSessionInitialization(); + } + } + } + else if (element instanceof Compressed) { + checkState(State.Compressing); + state = State.WaitingForStreamStart; + stream.addZLibCompression(); + stream.resetXMPPParser(); + sendStreamHeader(); + } + else if (element instanceof CompressFailure) { + finishSession(Error.Type.CompressionFailedError); + } + else if (element instanceof StreamManagementEnabled) { + stanzaAckRequester_ = new StanzaAckRequester(); + stanzaAckOnRequestConnection_ = stanzaAckRequester_.onRequestAck.connect(new Slot() { + + public void call() { + requestAck(); + } + }); + stanzaAckOnAckedConnection_ = stanzaAckRequester_.onStanzaAcked.connect(new Slot1<Stanza>() { + + public void call(Stanza p1) { + handleStanzaAcked(p1); + } + }); + stanzaAckResponder_ = new StanzaAckResponder(); + stanzaResponderAckConnection_ = stanzaAckResponder_.onAck.connect(new Slot1<Long>() { + + public void call(Long p1) { + ack(p1); + } + }); + needAcking = false; + continueSessionInitialization(); + } + else if (element instanceof StreamManagementFailed) { + needAcking = false; + continueSessionInitialization(); + } + else if (element instanceof AuthChallenge) { + AuthChallenge challenge = (AuthChallenge) element; + checkState(State.Authenticating); + assert authenticator != null; + if (authenticator.setChallenge(challenge.getValue())) { + stream.writeElement(new AuthResponse(authenticator.getResponse())); + } + else { + finishSession(Error.Type.AuthenticationFailedError); + } + } + else if (element instanceof AuthSuccess) { + AuthSuccess authSuccess = (AuthSuccess)element; + checkState(State.Authenticating); + if (authenticator != null && !authenticator.setChallenge(authSuccess.getValue())) { + finishSession(Error.Type.ServerVerificationFailedError); + } + else { + state = State.WaitingForStreamStart; + authenticator = null; + stream.resetXMPPParser(); + sendStreamHeader(); + } + } + else if (element instanceof AuthFailure) { + authenticator = null; + finishSession(Error.Type.AuthenticationFailedError); + } + else if (element instanceof TLSProceed) { + if (!checkState(State.WaitingForEncrypt)) { + return; + } + state = State.Encrypting; + stream.addTLSEncryption(); + } + else if (element instanceof StartTLSFailure) { + finishSession(Error.Type.TLSError); + } + else { + // FIXME Not correct? + state = State.Initialized; + onInitialized.emit(); + } + } + + private void continueSessionInitialization() { + if (needResourceBind) { + state = State.BindingResource; + ResourceBind resourceBind = new ResourceBind(); + if (localJID.getResource().length() != 0) { + resourceBind.setResource(localJID.getResource()); + } + sendStanza(IQ.createRequest(IQ.Type.Set, new JID(), "session-bind", resourceBind)); + } + else if (needAcking) { + state = State.EnablingSessionManagement; + stream.writeElement(new EnableStreamManagement()); + } + else if (needSessionStart) { + state = State.StartingSession; + sendStanza(IQ.createRequest(IQ.Type.Set, new JID(), "session-start", new StartSession())); + } + else { + state = State.Initialized; + onInitialized.emit(); + } + } + + private boolean checkState(State state) { + State currentState = this.state; /* For symbol debugging, as the following overwrites it */ + if (!currentState.equals(state)) { + finishSession(Error.Type.UnexpectedElementError); + return false; + } + return true; + } + + public void sendCredentials(String password) { + if (!checkState(State.WaitingForCredentials)) { + throw new IllegalStateException("Asking for credentials when we shouldn't be asked."); + } + state = State.Authenticating; + authenticator.setCredentials(localJID.getNode(), password); + stream.writeElement(new AuthRequest(authenticator.getName(), authenticator.getResponse())); + } + + private void handleTLSEncrypted() { + if (!checkState(State.Encrypting)) { + return; + } + Certificate certificate = stream.getPeerCertificate(); + CertificateVerificationError verificationError = stream.getPeerCertificateVerificationError(); + if (verificationError != null) { + checkTrustOrFinish(certificate, verificationError); + } + else { + ServerIdentityVerifier identityVerifier = new ServerIdentityVerifier(localJID); + if (identityVerifier.certificateVerifies(certificate)) { + continueAfterTLSEncrypted(); + } + else { + checkTrustOrFinish(certificate, new CertificateVerificationError(CertificateVerificationError.Type.InvalidServerIdentity)); + } + } + } + + private void checkTrustOrFinish(Certificate certificate, CertificateVerificationError error) { + if (certificateTrustChecker != null && certificateTrustChecker.isCertificateTrusted(certificate)) { + continueAfterTLSEncrypted(); + } + else { + finishSession(error); + } + } + + private void continueAfterTLSEncrypted() { + state = State.WaitingForStreamStart; + stream.resetXMPPParser(); + sendStreamHeader(); + } + + private void handleStreamClosed(SessionStream.Error streamError) { + State previousState = state; + state = State.Finished; + + if (stanzaAckRequester_ != null) { + stanzaAckOnRequestConnection_.disconnect(); + stanzaAckOnAckedConnection_.disconnect(); + stanzaAckRequester_ = null; + } + if (stanzaAckResponder_ != null) { + stanzaResponderAckConnection_.disconnect(); + stanzaAckResponder_ = null; + } + stream.setWhitespacePingEnabled(false); + streamStreamStartReceivedConnection.disconnect(); + streamElementReceivedConnection.disconnect(); + streamClosedConnection.disconnect(); + streamTLSEncryptedConnection.disconnect(); + + if (State.Finishing.equals(previousState)) { + onFinished.emit(error_); + } + else { + onFinished.emit(streamError); + } + } + + public void finish() { + finishSession((Error.Type)null); + } + + private void finishSession(Error.Type error) { + Error localError = null; + if (error != null) { + localError = new Error(error); + } + finishSession(localError); + } + + private void finishSession(com.isode.stroke.base.Error error) { + state = State.Finishing; + error_ = error; + assert(stream.isOpen()); + if (stanzaAckResponder_ != null) { + stanzaAckResponder_.handleAckRequestReceived(); + } + stream.writeFooter(); + stream.close(); + } + + private void requestAck() { + stream.writeElement(new StanzaAckRequest()); + } + + private void handleStanzaAcked(Stanza stanza) { + onStanzaAcked.emit(stanza); + } + + private void ack(long handledStanzasCount) { + stream.writeElement(new StanzaAck(handledStanzasCount)); + } + + public final Signal onNeedCredentials = new Signal(); + public final Signal onInitialized = new Signal(); + public final Signal1<com.isode.stroke.base.Error> onFinished = new Signal1<com.isode.stroke.base.Error>(); + public final Signal1<Stanza> onStanzaReceived = new Signal1<Stanza>(); + public final Signal1<Stanza> onStanzaAcked = new Signal1<Stanza>(); + + + + + + + + private JID getRemoteJID() { + return new JID("", localJID.getDomain()); + } + + private JID localJID; + private State state; + private SessionStream stream; + private boolean allowPLAINOverNonTLS; + private boolean useStreamCompression; + private UseTLS useTLS; + private boolean needSessionStart; + private boolean needResourceBind; + private boolean needAcking; + private boolean rosterVersioningSupported; + private ClientAuthenticator authenticator; + private StanzaAckRequester stanzaAckRequester_; + private StanzaAckResponder stanzaAckResponder_; + private com.isode.stroke.base.Error error_; + private CertificateTrustChecker certificateTrustChecker; +} diff --git a/src/com/isode/stroke/client/ClientSessionStanzaChannel.java b/src/com/isode/stroke/client/ClientSessionStanzaChannel.java new file mode 100644 index 0000000..531ff62 --- /dev/null +++ b/src/com/isode/stroke/client/ClientSessionStanzaChannel.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010, 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.Error; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.elements.Message; +import com.isode.stroke.elements.Presence; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.signals.SignalConnection; +import com.isode.stroke.signals.Slot; +import com.isode.stroke.signals.Slot1; +import java.util.logging.Logger; + +/** + * StanzaChannel implementation around a ClientSession. + */ +public class ClientSessionStanzaChannel extends StanzaChannel { + private SignalConnection sessionInitializedConnection; + private SignalConnection sessionFinishedConnection; + private SignalConnection sessionStanzaReceivedConnection; + private SignalConnection sessionStanzaAckedConnection; + + public void setSession(ClientSession session) { + assert this.session == null; + this.session = session; + sessionInitializedConnection = session.onInitialized.connect(new Slot() { + + public void call() { + handleSessionInitialized(); + } + }); + sessionFinishedConnection = session.onFinished.connect(new Slot1<com.isode.stroke.base.Error>() { + + public void call(com.isode.stroke.base.Error p1) { + handleSessionFinished(p1); + } + }); + sessionStanzaReceivedConnection = session.onStanzaReceived.connect(new Slot1<Stanza>() { + + public void call(Stanza p1) { + handleStanza(p1); + } + }); + sessionStanzaAckedConnection = session.onStanzaAcked.connect(new Slot1<Stanza>() { + + public void call(Stanza p1) { + handleStanzaAcked(p1); + } + }); + } + + public void sendIQ(IQ iq) { + send(iq); + } + + public void sendMessage(Message message) { + send(message); + } + + public void sendPresence(Presence presence) { + send(presence); + } + + public boolean getStreamManagementEnabled() { + if (session != null) { + return session.getStreamManagementEnabled(); + } + return false; + } + + public boolean isAvailable() { + return session != null && ClientSession.State.Initialized.equals(session.getState()); + } + + public String getNewIQID() { + return idGenerator.generateID(); + } + + private void send(Stanza stanza) { + if (!isAvailable()) { + logger_.warning("Warning: Client: Trying to send a stanza while disconnected."); + return; + } + session.sendStanza(stanza); + } + + private void handleSessionFinished(Error error) { + sessionFinishedConnection.disconnect(); + sessionStanzaReceivedConnection.disconnect(); + sessionStanzaAckedConnection.disconnect(); + sessionInitializedConnection.disconnect(); + session = null; + onAvailableChanged.emit(false); + } + + private void handleStanza(Stanza stanza) { + if (stanza instanceof Message) { + onMessageReceived.emit((Message)stanza); + } + if (stanza instanceof Presence) { + onPresenceReceived.emit((Presence)stanza); + } + if (stanza instanceof IQ) { + onIQReceived.emit((IQ)stanza); + } + } + + private void handleStanzaAcked(Stanza stanza) { + onStanzaAcked.emit(stanza); + } + + private void handleSessionInitialized() { + onAvailableChanged.emit(true); + } + private IDGenerator idGenerator = new IDGenerator(); + private ClientSession session; + private static final Logger logger_ = Logger.getLogger(ClientSessionStanzaChannel.class.getName()); +} diff --git a/src/com/isode/stroke/client/CoreClient.java b/src/com/isode/stroke/client/CoreClient.java new file mode 100644 index 0000000..0ce1503 --- /dev/null +++ b/src/com/isode/stroke/client/CoreClient.java @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2010, 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.elements.Message; +import com.isode.stroke.elements.Presence; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.elements.StreamType; +import com.isode.stroke.eventloop.EventLoop; +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.NetworkFactories; +import com.isode.stroke.network.PlatformDomainNameResolver; +import com.isode.stroke.network.TimerFactory; +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.tls.CertificateTrustChecker; +import com.isode.stroke.tls.CertificateVerificationError; +import com.isode.stroke.tls.PKCS12Certificate; +import com.isode.stroke.tls.PlatformTLSFactories; +import com.isode.stroke.tls.TLSContextFactory; + +/** + * 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 { + private SignalConnection sessionStreamDataReadConnection_; + private SignalConnection sessionStreamDataWrittenConnection_; + private SignalConnection sessionFinishedConnection_; + private SignalConnection sessionNeedCredentialsConnection_; + private SignalConnection connectorConnectFinishedConnection_; + private final EventLoop eventLoop_; + + public CoreClient(EventLoop eventLoop, JID jid, String password, NetworkFactories networkFactories) { + jid_ = jid; + password_ = password; + disconnectRequested_ = false; + eventLoop_ = eventLoop; + this.networkFactories = networkFactories; + this.certificateTrustChecker = null; + resolver_ = new PlatformDomainNameResolver(eventLoop); + stanzaChannel_ = new ClientSessionStanzaChannel(); + stanzaChannel_.onMessageReceived.connect(new Slot1<Message>() { + + public void call(Message p1) { + onMessageReceived.emit(p1); + } + }); + stanzaChannel_.onPresenceReceived.connect(new Slot1<Presence>() { + + public void call(Presence p1) { + onPresenceReceived.emit(p1); + } + }); + stanzaChannel_.onStanzaAcked.connect(new Slot1<Stanza>() { + + public void call(Stanza p1) { + onStanzaAcked.emit(p1); + } + }); + stanzaChannel_.onAvailableChanged.connect(new Slot1<Boolean>() { + + 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_; + }*/ + public void connect(ClientOptions o) { + options = o; + connect(jid_.getDomain()); + } + + public void connect(String host) { + disconnectRequested_ = false; + assert (connector_ == null); + /* FIXME: Port Proxies */ + connector_ = Connector.create(host, networkFactories.getDomainNameResolver(), networkFactories.getConnectionFactory(), networkFactories.getTimerFactory()); + connectorConnectFinishedConnection_ = connector_.onConnectFinished.connect(new Slot1<Connection>() { + public void call(Connection p1) { + handleConnectorFinished(p1); + } + }); + connector_.setTimeoutMilliseconds(60 * 1000); + connector_.start(); + } + + void handleConnectorFinished(Connection connection) { + if (connectorConnectFinishedConnection_ != null) { + connectorConnectFinishedConnection_.disconnect(); + } + connector_ = null; + if (connection == null) { + if (!disconnectRequested_) { + onError.emit(new ClientError(ClientError.Type.ConnectionError)); + } + } else { + assert (connection_ == null); + connection_ = connection; + + assert (sessionStream_ == null); + sessionStream_ = new BasicSessionStream(StreamType.ClientStreamType, connection_, payloadParserFactories_, payloadSerializers_, tlsFactories.getTLSContextFactory(), networkFactories.getTimerFactory(), eventLoop_); + if (certificate_ != null && !certificate_.isEmpty()) { + sessionStream_.setTLSCertificate(new PKCS12Certificate(certificate_, password_)); + } + sessionStreamDataReadConnection_ = sessionStream_.onDataRead.connect(new Slot1<String>() { + + public void call(String p1) { + handleDataRead(p1); + } + }); + + sessionStreamDataWrittenConnection_ = sessionStream_.onDataWritten.connect(new Slot1<String>() { + + public void call(String p1) { + handleDataWritten(p1); + } + }); + + session_ = ClientSession.create(jid_, sessionStream_); + session_.setCertificateTrustChecker(certificateTrustChecker); + session_.setUseStreamCompression(options.useStreamCompression); + switch (options.useTLS) { + case UseTLSWhenAvailable: + session_.setUseTLS(ClientSession.UseTLS.UseTLSWhenAvailable); + break; + case NeverUseTLS: + session_.setUseTLS(ClientSession.UseTLS.NeverUseTLS); + break; + } + stanzaChannel_.setSession(session_); + sessionFinishedConnection_ = session_.onFinished.connect(new Slot1<com.isode.stroke.base.Error>() { + + public void call(com.isode.stroke.base.Error p1) { + handleSessionFinished(p1); + } + }); + sessionNeedCredentialsConnection_ = session_.onNeedCredentials.connect(new Slot() { + + public void call() { + handleNeedCredentials(); + } + }); + session_.start(); + } + } + + 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(String certificate) { + certificate_ = certificate; + } + + private void handleSessionFinished(com.isode.stroke.base.Error error) { + sessionFinishedConnection_.disconnect(); + sessionNeedCredentialsConnection_.disconnect(); + session_ = null; + + sessionStreamDataReadConnection_.disconnect(); + sessionStreamDataWrittenConnection_.disconnect(); + sessionStream_ = null; + + connection_.disconnect(); + connection_ = null; + + if (error != null) { + ClientError clientError = 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; + } + } 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; + } + } + assert clientError != null; + onError.emit(clientError); + } + } + + private void handleNeedCredentials() { + assert session_ != null; + session_.sendCredentials(password_); + } + + private void handleDataRead(String data) { + onDataRead.emit(data); + } + + private void handleDataWritten(String data) { + onDataWritten.emit(data); + } + + private void handleStanzaChannelAvailableChanged(boolean available) { + if (available) { + onConnected.emit(); + } + } + + public void sendMessage(Message message) { + stanzaChannel_.sendMessage(message); + } + + public void sendPresence(Presence presence) { + stanzaChannel_.sendPresence(presence); + } + + public IQRouter getIQRouter() { + return iqRouter_; + } + + public StanzaChannel getStanzaChannel() { + return stanzaChannel_; + } + + public boolean isAvailable() { + return stanzaChannel_.isAvailable(); + } + + /** + * Returns the JID of the client. + * After the session was initialized, this returns the bound JID. + */ + public JID getJID() { + if (session_ != null) { + return session_.getLocalJID(); + } else { + return jid_; + } + } + public final Signal1<ClientError> onError = new Signal1<ClientError>(); + public final Signal onConnected = new Signal(); + public final Signal1<String> onDataRead = new Signal1<String>(); + public final Signal1<String> onDataWritten = new Signal1<String>(); + public final Signal1<Message> onMessageReceived = new Signal1<Message>(); + public final Signal1<Presence> onPresenceReceived = new Signal1<Presence>(); + public final Signal1<Stanza> onStanzaAcked = new Signal1<Stanza>(); + private PlatformDomainNameResolver resolver_; + 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 String certificate_; + private boolean disconnectRequested_; + private ClientOptions options; + private CertificateTrustChecker certificateTrustChecker; + private NetworkFactories networkFactories; + private PlatformTLSFactories tlsFactories; +} diff --git a/src/com/isode/stroke/client/IDGenerator.java b/src/com/isode/stroke/client/IDGenerator.java new file mode 100644 index 0000000..1810cdf --- /dev/null +++ b/src/com/isode/stroke/client/IDGenerator.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.client; + +/** + * + */ +public class IDGenerator { + private int next_ = 42; + public String generateID() { + next_++; + return String.valueOf(next_); + + } +} diff --git a/src/com/isode/stroke/client/StanzaChannel.java b/src/com/isode/stroke/client/StanzaChannel.java new file mode 100644 index 0000000..62984b5 --- /dev/null +++ b/src/com/isode/stroke/client/StanzaChannel.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010, 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.elements.Message; +import com.isode.stroke.elements.Presence; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.queries.IQChannel; +import com.isode.stroke.signals.Signal1; + +public abstract class StanzaChannel extends IQChannel { + + + public abstract void sendMessage(Message message); + + public abstract void sendPresence(Presence presence); + + public abstract boolean isAvailable(); + + public abstract boolean getStreamManagementEnabled(); + + public final Signal1<Message> onMessageReceived = new Signal1<Message>(); + public final Signal1<Presence> onPresenceReceived = new Signal1<Presence>(); + public final Signal1<Boolean> onAvailableChanged = new Signal1<Boolean>(); + public final Signal1<Stanza> onStanzaAcked = new Signal1<Stanza>(); + + +} |