summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKevin Smith <git@kismith.co.uk>2011-07-01 09:19:49 (GMT)
committerKevin Smith <git@kismith.co.uk>2011-07-01 09:19:49 (GMT)
commit2da71a8a85486a494343f1662d64fb5ae5a2a44e (patch)
tree23992f9f2a00bac23b345e5c2cc9c1194efc25be /src/com/isode/stroke/client
downloadstroke-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.java57
-rw-r--r--src/com/isode/stroke/client/ClientOptions.java45
-rw-r--r--src/com/isode/stroke/client/ClientSession.java608
-rw-r--r--src/com/isode/stroke/client/ClientSessionStanzaChannel.java125
-rw-r--r--src/com/isode/stroke/client/CoreClient.java385
-rw-r--r--src/com/isode/stroke/client/IDGenerator.java22
-rw-r--r--src/com/isode/stroke/client/StanzaChannel.java35
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>();
+
+
+}