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 | |
download | stroke-2da71a8a85486a494343f1662d64fb5ae5a2a44e.zip stroke-2da71a8a85486a494343f1662d64fb5ae5a2a44e.tar.bz2 |
Initial import
238 files changed, 12997 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48b8b62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +MANIFEST.MF +*\~ +build/ +dist/ +nbproject/ +*.class +isode/Makefile +isode/config.log +isode/config.status +isode/configure +test-results/ +*.patch +.DS_Store
\ No newline at end of file diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..820b482 --- /dev/null +++ b/build.xml @@ -0,0 +1,87 @@ +<!-- + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + --> +<project name="Stroke" default="dist" basedir="."> + <description> + XMPP Library porting Swiften to Java. + </description> + <property name="src" location="src"/> + <property name="src.tests" location="test"/> + <property name="test.results" location="test-results"/> + <property name="build" location="build"/> + <property name="dist" location="dist"/> + <property name="jar" value="${dist}/lib/stroke.jar"/> + <property name="main-class" value="com.isode.stroke.examples.gui.StrokeGUI"/> + <property name="compile.debug" value="true"/> + <property name="testsuiteclass" value="com.isode.stroke.unittest.StrokeTestSuite" /> + + <path id="classpath"> + <fileset dir="../third-party/xpp" includes="xpp.jar"/> + </path> + <target name="init"> + <tstamp/> + <mkdir dir="${build}"/> + </target> + + <target name="compile" depends="init" + description="compile the source " > + <javac srcdir="${src}" destdir="${build}" classpathref="classpath" debug="${compile.debug}"/> + </target> + + <target name="dist" depends="compile" + description="generate the distribution" > + <mkdir dir="${dist}/lib"/> + + <jar jarfile="${jar}" basedir="${build}"/> + <manifest file="MANIFEST.MF"> + <attribute name="Main-Class" value="${main-class}"/> + </manifest> + </target> + +<target name="compile-tests" depends="dist" + description="compile the test sources " > + <javac srcdir="${src.tests}" destdir="${src.tests}" debug="${compile.debug}"> + <classpath> + <pathelement location="${jar}"/> + <pathelement location="${JUNIT_JAR}"/> + </classpath> + </javac> + </target> + + <target name="test" depends="compile-tests"> + <delete dir="${test.results}"/> + <mkdir dir="${test.results}"/> + <junit fork="false"> + <formatter type="plain"/> + <classpath> + <pathelement location="${JUNIT_JAR}"/> + <pathelement location="${jar}"/> + <pathelement location="${src.tests}"/> + <path refid="classpath"/> + </classpath> + <batchtest fork="yes" todir="${test.results}"> + <fileset dir="${src.tests}"> + <include name="**/*Test.java"/> + <!--<exclude name="**/AllTests.java"/>--> + </fileset> + </batchtest> + </junit> + </target> + + <target name="clean" + description="clean up" > + <delete dir="${build}"/> + <delete dir="${test.results}"/> + <delete dir="${dist}"/> + </target> + + <target name="run" description="Run the demo" depends="dist"> + <java fork="true" classname="${main-class}"> + <classpath> + <path refid="classpath"/> + <path location="${jar}"/> + </classpath> + </java> + </target> +</project> diff --git a/src/com/isode/stroke/base/ByteArray.java b/src/com/isode/stroke/base/ByteArray.java new file mode 100644 index 0000000..a36e305 --- /dev/null +++ b/src/com/isode/stroke/base/ByteArray.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.base; + +import java.io.UnsupportedEncodingException; + +/** + * + */ +public class ByteArray { + + public ByteArray() { + } + + public ByteArray(String s) { + try { + fromBytes(s.getBytes("UTF-8")); + } catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("JVM has no 'UTF-8' encoding"); + } + } + + public ByteArray(byte[] c) { + fromBytes(c); + } + + public ByteArray(ByteArray b) { + fromBytes(b.getData()); + } + + private void fromBytes(final byte[] b) { + data_ = new byte[b.length]; + System.arraycopy(b, 0, data_, 0, b.length); + } + + /*public ByteArray(char[] c, int n) { + for (int i = 0; i < n; i++) { + append(c[i]); + } + }*/ + + /** + * These are the raw, modifyable data! + * @return + */ + public byte[] getData() { + return data_; + } + + public int getSize() { + return data_.length; + } + + public boolean isEmpty() { + return getSize() == 0; + } + + /*public void resize(size_t size) { + return data_.resize(size); + }*/ + /** Immutable add */ + public static ByteArray plus(ByteArray a, ByteArray b) { + ByteArray x = new ByteArray(a.getData()); + x.append(b); + return x; + } + + /** Immutable add */ + /*public ByteArray plus(ByteArray a, char b) { + ByteArray x = new ByteArray(a.getData()); + x.append(b); + return x; + }*/ + + /** Mutable add */ + public ByteArray append(ByteArray b) { + append(b.getData()); + return this; + } + + /** Mutable add */ + private ByteArray append(byte[] b) { + int newLength = data_.length + b.length; + byte[] newData = new byte[newLength]; + for (int i = 0; i < data_.length; i++) { + newData[i] = data_[i]; + } + for (int i = 0; i < b.length; i++) { + newData[i + data_.length] = b[i]; + } + data_ = newData; + return this; + } + + /** Mutable add */ + public ByteArray append(byte b) { + byte[] bytes = {b}; + append(bytes); + return this; + } + + /** mutable add */ + public ByteArray append(String s) { + byte[] bytes; + try { + bytes = s.getBytes("UTF-8"); + } catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("JVM has no 'UTF-8' encoding"); + } + append(bytes); + return this; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 97 * hash + (this.data_ != null ? this.data_.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object other) { + return other instanceof ByteArray && toString().equals(other.toString()); + } + + /*public char charAt(int i) { + return data_.charAt(i); + }*/ + + /*public const_iterator begin() const { + return data_.begin(); + } + + public const_iterator end() const { + return data_.end(); + }*/ + @Override + public String toString() { + try { + return new String(data_, "UTF-8"); + } catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("JVM has no 'UTF-8' encoding"); + } + } + + public void readFromFile(String file) { + //FIXME: port + } + + public void clear() { + data_ = new byte[]{}; + } + private byte[] data_ = {}; + +} diff --git a/src/com/isode/stroke/base/Error.java b/src/com/isode/stroke/base/Error.java new file mode 100644 index 0000000..60ccbeb --- /dev/null +++ b/src/com/isode/stroke/base/Error.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.base; + +public interface Error { + +} 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>(); + + +} diff --git a/src/com/isode/stroke/compress/ZLibCompressor.java b/src/com/isode/stroke/compress/ZLibCompressor.java new file mode 100644 index 0000000..f5276c8 --- /dev/null +++ b/src/com/isode/stroke/compress/ZLibCompressor.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.compress; + +import com.isode.stroke.base.ByteArray; +import java.util.zip.Deflater; + +/** + * + * @author Kev + */ +public class ZLibCompressor { + + private static final int COMPRESSION_LEVEL = 9; + + public ByteArray process(ByteArray data) throws ZLibException { + Deflater compressor = new Deflater(COMPRESSION_LEVEL); + compressor.setStrategy(Deflater.DEFAULT_STRATEGY); + compressor.setInput(data.getData()); + compressor.finish(); + byte[] output = new byte[100]; + ByteArray result = new ByteArray(); + while (!compressor.finished()) { + int size = compressor.deflate(output); + for (int i = 0; i < size; i++) { + result.append(output[i]); /* TODO: Terribly slow */ + } + } + return result; + } +} diff --git a/src/com/isode/stroke/compress/ZLibDecompressor.java b/src/com/isode/stroke/compress/ZLibDecompressor.java new file mode 100644 index 0000000..2c78a57 --- /dev/null +++ b/src/com/isode/stroke/compress/ZLibDecompressor.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.compress; + +import com.isode.stroke.base.ByteArray; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * + * @author Kev + */ +public class ZLibDecompressor { + Inflater inflater_ = new Inflater(); + public ByteArray process(ByteArray data) throws ZLibException { + try { + inflater_.setInput(data.getData()); + byte[] output = new byte[100]; + ByteArray result = new ByteArray(); + int size = 0; + while ((size = inflater_.inflate(output)) != 0) { + for (int i = 0; i < size; i++) { + result.append(output[i]); /* TODO: Terribly slow */ + } + } + return result; + } + catch (DataFormatException e) { + throw new ZLibException(); + } + } + +} diff --git a/src/com/isode/stroke/compress/ZLibException.java b/src/com/isode/stroke/compress/ZLibException.java new file mode 100644 index 0000000..48a2d31 --- /dev/null +++ b/src/com/isode/stroke/compress/ZLibException.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.compress; + +/** + * + * @author Kev + */ +public class ZLibException extends Exception { + +} diff --git a/src/com/isode/stroke/elements/AuthChallenge.java b/src/com/isode/stroke/elements/AuthChallenge.java new file mode 100644 index 0000000..ac87a3d --- /dev/null +++ b/src/com/isode/stroke/elements/AuthChallenge.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +import com.isode.stroke.base.ByteArray; + +public class AuthChallenge implements Element { + //FIXME: parser/serialiser + public AuthChallenge() { + value_ = new ByteArray(); + } + + public AuthChallenge(ByteArray value) { + value_ = new ByteArray(value); + } + + public ByteArray getValue() { + return value_; + } + + public void setValue(ByteArray value) { + value_ = new ByteArray(value); + } + + private ByteArray value_; +} diff --git a/src/com/isode/stroke/elements/AuthFailure.java b/src/com/isode/stroke/elements/AuthFailure.java new file mode 100644 index 0000000..ecd7639 --- /dev/null +++ b/src/com/isode/stroke/elements/AuthFailure.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.elements; + + +public class AuthFailure implements Element { + //FIXME: parser/serialiser +} diff --git a/src/com/isode/stroke/elements/AuthRequest.java b/src/com/isode/stroke/elements/AuthRequest.java new file mode 100644 index 0000000..21b1356 --- /dev/null +++ b/src/com/isode/stroke/elements/AuthRequest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +import com.isode.stroke.base.ByteArray; + +public class AuthRequest implements Element { + + public AuthRequest() { + this(null); + } + + //FIXME: parser/serialiser + public AuthRequest(String mechanism) { + mechanism_ = mechanism; + } + + public AuthRequest(String mechanism, ByteArray message) { + mechanism_ = mechanism; + message_ = message; + } + + public ByteArray getMessage() { + return message_; + } + + public void setMessage(ByteArray message) { + message_ = message; + } + + public String getMechanism() { + return mechanism_; + } + + public void setMechanism(String mechanism) { + mechanism_ = mechanism; + } + private String mechanism_; + private ByteArray message_ = new ByteArray(); +} diff --git a/src/com/isode/stroke/elements/AuthResponse.java b/src/com/isode/stroke/elements/AuthResponse.java new file mode 100644 index 0000000..357b72b --- /dev/null +++ b/src/com/isode/stroke/elements/AuthResponse.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.elements; + +import com.isode.stroke.base.ByteArray; + +public class AuthResponse implements Element { + //FIXME: parser/serialiser + + public AuthResponse() { + value = null; + } + + public AuthResponse(ByteArray value) { + this.value = value; + } + + public ByteArray getValue() { + return value; + } + + public void setValue(ByteArray value) { + this.value = value; + } + private ByteArray value; +} diff --git a/src/com/isode/stroke/elements/AuthSuccess.java b/src/com/isode/stroke/elements/AuthSuccess.java new file mode 100644 index 0000000..eba6b5a --- /dev/null +++ b/src/com/isode/stroke/elements/AuthSuccess.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +import com.isode.stroke.base.ByteArray; + +public class AuthSuccess implements Element { + //FIXME: parser/serialiser + + public ByteArray getValue() { + return value; + } + + public void setValue(ByteArray value) { + this.value = value; + } + private ByteArray value; +} diff --git a/src/com/isode/stroke/elements/Body.java b/src/com/isode/stroke/elements/Body.java new file mode 100644 index 0000000..3cda307 --- /dev/null +++ b/src/com/isode/stroke/elements/Body.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class Body extends Payload { + String text_ = ""; + + public Body() {} + + public Body(String text) { + setText(text); + } + + public void setText(String text) { + text_ = text; + } + + public String getText() { + return text_; + } +} diff --git a/src/com/isode/stroke/elements/CompressFailure.java b/src/com/isode/stroke/elements/CompressFailure.java new file mode 100644 index 0000000..c3a19bd --- /dev/null +++ b/src/com/isode/stroke/elements/CompressFailure.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class CompressFailure implements Element { + +} diff --git a/src/com/isode/stroke/elements/CompressRequest.java b/src/com/isode/stroke/elements/CompressRequest.java new file mode 100644 index 0000000..8150f1d --- /dev/null +++ b/src/com/isode/stroke/elements/CompressRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class CompressRequest implements Element { + + public CompressRequest() { + this(""); + } + + public CompressRequest(String method) { + method_ = method; + } + + public String getMethod() { + return method_; + } + + public void setMethod(String method) { + method_ = method; + } + private String method_; +} diff --git a/src/com/isode/stroke/elements/Compressed.java b/src/com/isode/stroke/elements/Compressed.java new file mode 100644 index 0000000..1309f5b --- /dev/null +++ b/src/com/isode/stroke/elements/Compressed.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class Compressed implements Element { + +} diff --git a/src/com/isode/stroke/elements/Element.java b/src/com/isode/stroke/elements/Element.java new file mode 100644 index 0000000..1b931e4 --- /dev/null +++ b/src/com/isode/stroke/elements/Element.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public interface Element { + +} diff --git a/src/com/isode/stroke/elements/EnableStreamManagement.java b/src/com/isode/stroke/elements/EnableStreamManagement.java new file mode 100644 index 0000000..e551551 --- /dev/null +++ b/src/com/isode/stroke/elements/EnableStreamManagement.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class EnableStreamManagement implements Element { + +} diff --git a/src/com/isode/stroke/elements/ErrorPayload.java b/src/com/isode/stroke/elements/ErrorPayload.java new file mode 100644 index 0000000..913296d --- /dev/null +++ b/src/com/isode/stroke/elements/ErrorPayload.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +/** + * Error. + */ +public class ErrorPayload extends Payload { + private Condition condition_; + private Type type_; + private String text_; + + public enum Type { Cancel, Continue, Modify, Auth, Wait }; + + public enum Condition { + BadRequest, + Conflict, + FeatureNotImplemented, + Forbidden, + Gone, + InternalServerError, + ItemNotFound, + JIDMalformed, + NotAcceptable, + NotAllowed, + NotAuthorized, + PaymentRequired, + RecipientUnavailable, + Redirect, + RegistrationRequired, + RemoteServerNotFound, + RemoteServerTimeout, + ResourceConstraint, + ServiceUnavailable, + SubscriptionRequired, + UndefinedCondition, + UnexpectedRequest + }; + + public ErrorPayload(Condition condition, Type type, String text) { + condition_ = condition; + type_ = type; + text_ = text; + } + + public ErrorPayload(Condition condition, Type type) { + this(condition, type, ""); + } + + public ErrorPayload(Condition condition) { + this(condition, Type.Cancel); + } + + public ErrorPayload() { + this(Condition.UndefinedCondition); + } + + public Type getType() { + return type_; + } + + public void setType(Type type) { + type_ = type; + } + + public Condition getCondition() { + return condition_; + } + + public void setCondition(Condition condition) { + condition_ = condition; + } + + public void setText(String text) { + text_ = text; + } + + public String getText() { + return text_; + } +} diff --git a/src/com/isode/stroke/elements/IQ.java b/src/com/isode/stroke/elements/IQ.java new file mode 100644 index 0000000..a4fc9dc --- /dev/null +++ b/src/com/isode/stroke/elements/IQ.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +import com.isode.stroke.jid.JID; + +public class IQ extends Stanza { + public enum Type {Get, Set, Result, Error}; + + private Type type_; + + public IQ() { + this(Type.Get); + } + + public IQ(Type type) { + type_ = type; + } + + public Type getType() { + return type_; + } + + public void setType(Type type) { + type_ = type; + } + + public static IQ createRequest(Type type, JID to, String id, Payload payload) { + IQ iq = new IQ(type); + iq.setTo(to); + iq.setID(id); + iq.addPayload(payload); + return iq; + } + + public static IQ createResult(JID to, String id, Payload payload) { + IQ iq = new IQ(Type.Result); + iq.setTo(to); + iq.setID(id); + iq.addPayload(payload); + return iq; + } + + public static IQ createError(JID to, String id, ErrorPayload.Condition condition, ErrorPayload.Type type) { + IQ iq = new IQ(Type.Error); + iq.setTo(to); + iq.setID(id); + iq.addPayload(new ErrorPayload(condition, type)); + return iq; + } + +} diff --git a/src/com/isode/stroke/elements/Last.java b/src/com/isode/stroke/elements/Last.java new file mode 100644 index 0000000..8f12616 --- /dev/null +++ b/src/com/isode/stroke/elements/Last.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2011, Kevin Smith. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class Last extends Payload { + int seconds_; + + public Last() {} + + public Last(final int seconds) { + setSeconds(seconds); + } + + public void setSeconds(final int seconds) { + seconds_ = seconds; + } + + public int getSeconds() { + return seconds_; + } +} diff --git a/src/com/isode/stroke/elements/Message.java b/src/com/isode/stroke/elements/Message.java new file mode 100644 index 0000000..6820e6f --- /dev/null +++ b/src/com/isode/stroke/elements/Message.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class Message extends Stanza { + + Type type_ = Type.Chat; + + public enum Type { + + Normal, Chat, Error, Groupchat, Headline + }; + + public String getSubject() { + Subject subject = getPayload(new Subject()); + if (subject != null) { + return subject.getText(); + } + return ""; + } + + public void setSubject(String subject) { + updatePayload(new Subject(subject)); + } + + public String getBody() { + Body body = getPayload(new Body()); + if (body != null) { + return body.getText(); + } + return ""; + } + + public void setBody(String body) { + updatePayload(new Body(body)); + } + + public boolean isError() { + ErrorPayload error = getPayload(new ErrorPayload()); + return getType().equals(Type.Error) || error != null; + } + + public Type getType() { + return type_; + } + + public void setType(Type type) { + type_ = type; + } +} diff --git a/src/com/isode/stroke/elements/Payload.java b/src/com/isode/stroke/elements/Payload.java new file mode 100644 index 0000000..fbe47f2 --- /dev/null +++ b/src/com/isode/stroke/elements/Payload.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class Payload { + +} diff --git a/src/com/isode/stroke/elements/Presence.java b/src/com/isode/stroke/elements/Presence.java new file mode 100644 index 0000000..f32fb9f --- /dev/null +++ b/src/com/isode/stroke/elements/Presence.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class Presence extends Stanza { + + private Type type_; + + public enum Type { + + Available, Error, Probe, Subscribe, Subscribed, Unavailable, Unsubscribe, Unsubscribed + }; + + public Presence() { + type_ = Type.Available; + } + + public Presence(String status) { + type_ = Type.Available; + setStatus(status); + } + + public Type getType() { + return type_; + } + + public void setType(Type type) { + type_ = type; + } + + public StatusShow.Type getShow() { + StatusShow show = getPayload (new StatusShow()); + if (show != null) { + return show.getType(); + } + return type_ == Type.Available ? StatusShow.Type.Online : StatusShow.Type.None; + } + + public void setShow(StatusShow.Type show) { + updatePayload(new StatusShow(show)); + } + + public String getStatus() { + Status status = getPayload(new Status()); + if (status != null) { + return status.getText(); + } + return ""; + } + + public void setStatus(String status) { + updatePayload(new Status(status)); + } + + public int getPriority() { + Priority priority = getPayload(new Priority()); + return (priority != null ? priority.getPriority() : 0); + } + + public void setPriority(int priority) { + updatePayload(new Priority(priority)); + } +} diff --git a/src/com/isode/stroke/elements/Priority.java b/src/com/isode/stroke/elements/Priority.java new file mode 100644 index 0000000..850c67a --- /dev/null +++ b/src/com/isode/stroke/elements/Priority.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +/** + * Resource priority from presence stanzas. + */ +public class Priority extends Payload { + int priority_ = 0; + + public Priority(){ + + } + + public Priority (int priority) { + priority_ = priority; + } + + public void setPriority(int priority) { + priority_ = priority; + } + + public int getPriority() { + return priority_; + } +} diff --git a/src/com/isode/stroke/elements/ProtocolHeader.java b/src/com/isode/stroke/elements/ProtocolHeader.java new file mode 100644 index 0000000..16d2925 --- /dev/null +++ b/src/com/isode/stroke/elements/ProtocolHeader.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class ProtocolHeader { + + public ProtocolHeader() { + version = "1.0"; + } + + public String getTo() { + return to; + } + + public void setTo(String a) { + to = a; + } + + public String getFrom() { + return from; + } + + public void setFrom(String a) { + from = a; + } + + public String getVersion() { + return version; + } + + public void setVersion(String a) { + version = a; + } + + public String getID() { + return id; + } + + public void setID(String a) { + id = a; + } + private String to = ""; + private String from = ""; + private String id = ""; + private String version = ""; +} diff --git a/src/com/isode/stroke/elements/RawXMLPayload.java b/src/com/isode/stroke/elements/RawXMLPayload.java new file mode 100644 index 0000000..8c261e9 --- /dev/null +++ b/src/com/isode/stroke/elements/RawXMLPayload.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +/** + * Unparsed content. + */ +public class RawXMLPayload extends Payload { + private String rawXML_; + + public void setRawXML(String data) { + rawXML_ = data; + } + + public String getRawXML() { + return rawXML_; + } +} diff --git a/src/com/isode/stroke/elements/ResourceBind.java b/src/com/isode/stroke/elements/ResourceBind.java new file mode 100644 index 0000000..863ef98 --- /dev/null +++ b/src/com/isode/stroke/elements/ResourceBind.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +import com.isode.stroke.jid.JID; + +public class ResourceBind extends Payload { +//FIXME: serializer and parser + public void setJID(JID jid) { + jid_ = jid; + } + + public JID getJID() { + return jid_; + } + + public void setResource(String resource) { + resource_ = resource; + } + + public String getResource() { + return resource_; + } + private JID jid_ = new JID(); + private String resource_ = ""; +} diff --git a/src/com/isode/stroke/elements/RosterItemPayload.java b/src/com/isode/stroke/elements/RosterItemPayload.java new file mode 100644 index 0000000..a80ecc6 --- /dev/null +++ b/src/com/isode/stroke/elements/RosterItemPayload.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +import com.isode.stroke.jid.JID; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Roster Items + */ +public class RosterItemPayload { + + public enum Subscription { + + None, To, From, Both, Remove + }; + + public RosterItemPayload() { + subscription_ = Subscription.None; + ask_ = false; + } + + public RosterItemPayload(JID jid, String name, Subscription subscription) { + jid_ = jid; + name_ = name; + subscription_ = subscription; + ask_ = false; + } + + public void setJID(JID jid) { + jid_ = jid; + } + + public JID getJID() { + return jid_; + } + + public void setName(String name) { + name_ = name; + } + + public String getName() { + return name_; + } + + public void setSubscription(Subscription subscription) { + subscription_ = subscription; + } + + public Subscription getSubscription() { + return subscription_; + } + + public void addGroup(String group) { + groups_.add(group); + } + + public void setGroups(Collection<String> groups) { + groups_ = new ArrayList<String>(); + groups_.addAll(groups); + } + + public Collection<String> getGroups() { + return groups_; + } + + public void setSubscriptionRequested() { + ask_ = true; + } + + public boolean getSubscriptionRequested() { + return ask_; + } + private JID jid_; + private String name_; + private Subscription subscription_; + private ArrayList<String> groups_ = new ArrayList<String>(); + private boolean ask_; +} diff --git a/src/com/isode/stroke/elements/RosterPayload.java b/src/com/isode/stroke/elements/RosterPayload.java new file mode 100644 index 0000000..6208216 --- /dev/null +++ b/src/com/isode/stroke/elements/RosterPayload.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +import com.isode.stroke.jid.JID; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Roster. + */ +public class RosterPayload extends Payload { + + public RosterPayload() { + } + + public RosterItemPayload getItem(JID jid) { + for (RosterItemPayload item : items_) { + if (item.getJID().equals(jid)) { + return item; + } + } + return null; + } + + public void addItem(RosterItemPayload item) { + items_.add(item); + } + + public Collection<RosterItemPayload> getItems() { + return items_; + } + + private final ArrayList<RosterItemPayload> items_ = new ArrayList<RosterItemPayload>(); +} diff --git a/src/com/isode/stroke/elements/SearchPayload.java b/src/com/isode/stroke/elements/SearchPayload.java new file mode 100644 index 0000000..b8e8a0d --- /dev/null +++ b/src/com/isode/stroke/elements/SearchPayload.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +import com.isode.stroke.jid.JID; +import java.util.ArrayList; +import java.util.List; + +public class SearchPayload extends Payload { + + public static class Item { + + public String first; + public String last; + public String nick; + public String email; + public JID jid; + }; + + public SearchPayload() { + } + + //Form::ref getForm() const { return form; } /* Not ported yet */ + //void setForm(Form::ref f) { form = f; } /* Not ported yet */ + /** + * @return Can be null + */ + public String getInstructions() { + return instructions; + } + + /** + * @return Can be null + */ + public String getNick() { + return nick; + } + + /** + * @return Can be null + */ + public String getFirst() { + return first; + } + + /** + * @return Can be null + */ + public String getLast() { + return last; + } + + /** + * @return Can be null + */ + public String getEMail() { + return email; + } + + /** + * @param v Null means no value. + */ + public void setInstructions(String v) { + this.instructions = v; + } + + /** + * @param v Null means no value. + */ + public void setNick(String v) { + this.nick = v; + } + + /** + * @param v Null means no value. + */ + public void setFirst(String v) { + this.first = v; + } + + /** + * @param v Null means no value. + */ + public void setLast(String v) { + this.last = v; + } + + /** + * @param v Null means no value. + */ + public void setEMail(String v) { + this.email = v; + } + + /** + * + * @return non-null + */ + public List<Item> getItems() { + return items; + } + + /** + * + * @param item Non-null. + */ + public void addItem(Item item) { + items.add(item); + } + + //private Form::ref form; /*Not ported yet*/ + private String instructions; + private String nick; + private String first; + private String last; + private String email; + private ArrayList<Item> items = new ArrayList<Item>(); +} diff --git a/src/com/isode/stroke/elements/Stanza.java b/src/com/isode/stroke/elements/Stanza.java new file mode 100644 index 0000000..ac4f06f --- /dev/null +++ b/src/com/isode/stroke/elements/Stanza.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +import com.isode.stroke.jid.JID; +import java.util.Vector; + +/** + * Basic XMPP stanza. + */ +public class Stanza implements Element { + private String id_; + private JID from_; + private JID to_; + private Vector<Payload> payloads_ = new Vector<Payload>(); + + + public <T extends Payload> T getPayload(T type) { + for (Payload payload : payloads_) { + if (payload.getClass().isAssignableFrom(type.getClass())) { + return (T)payload; + } + } + return null; + } + + public <T extends Payload> Vector<T> getPayloads(T type) { + Vector<T> results = new Vector<T>(); + for (Payload payload : payloads_) { + if (payload.getClass().isAssignableFrom(type.getClass())) { + results.add((T)payload); + } + } + return results; + } + + public Vector<Payload> getPayloads() { + return payloads_; + } + + public void addPayload(Payload payload) { + payloads_.add(payload); + } + + public void updatePayload(Payload payload) { + for (int i = 0; i < payloads_.size(); i++) { + if (payloads_.get(i).getClass() == payload.getClass()) { + payloads_.set(i, payload); + return; + } + } + payloads_.add(payload); + } + + public JID getFrom() { + return from_; + } + + public void setFrom(JID from) { + from_ = from; + } + + public JID getTo() { + return to_; + } + + public void setTo(JID to) { + to_ = to; + } + + public String getID() { + return id_; + } + + public void setID(String id) { + id_ = id; + } + +} diff --git a/src/com/isode/stroke/elements/StanzaAck.java b/src/com/isode/stroke/elements/StanzaAck.java new file mode 100644 index 0000000..0589d39 --- /dev/null +++ b/src/com/isode/stroke/elements/StanzaAck.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.elements; + +public class StanzaAck implements Element { + //FIXME: parser/serialiser + public StanzaAck() { + } + + public StanzaAck(long handledStanzasCount) { + valid = true; + this.handledStanzasCount = handledStanzasCount; + } + + public long getHandledStanzasCount() { + return handledStanzasCount; + } + + public void setHandledStanzasCount(long i) { + handledStanzasCount = i; + valid = true; + } + + public boolean isValid() { + return valid; + } + private boolean valid = false; + private long handledStanzasCount = 0; +} diff --git a/src/com/isode/stroke/elements/StanzaAckRequest.java b/src/com/isode/stroke/elements/StanzaAckRequest.java new file mode 100644 index 0000000..b02ac4e --- /dev/null +++ b/src/com/isode/stroke/elements/StanzaAckRequest.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class StanzaAckRequest implements Element { + +} diff --git a/src/com/isode/stroke/elements/StartSession.java b/src/com/isode/stroke/elements/StartSession.java new file mode 100644 index 0000000..b886535 --- /dev/null +++ b/src/com/isode/stroke/elements/StartSession.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class StartSession extends Payload{ + +} diff --git a/src/com/isode/stroke/elements/StartTLSFailure.java b/src/com/isode/stroke/elements/StartTLSFailure.java new file mode 100644 index 0000000..7b18dfe --- /dev/null +++ b/src/com/isode/stroke/elements/StartTLSFailure.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class StartTLSFailure implements Element { + + //FIXME: parser/serialiser + +} diff --git a/src/com/isode/stroke/elements/StartTLSRequest.java b/src/com/isode/stroke/elements/StartTLSRequest.java new file mode 100644 index 0000000..23c3297 --- /dev/null +++ b/src/com/isode/stroke/elements/StartTLSRequest.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ +package com.isode.stroke.elements; + + +public class StartTLSRequest implements Element { +//FIXME: parser/serialiser + +} diff --git a/src/com/isode/stroke/elements/Status.java b/src/com/isode/stroke/elements/Status.java new file mode 100644 index 0000000..f40814d --- /dev/null +++ b/src/com/isode/stroke/elements/Status.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class Status extends Payload { + + String text_ = ""; + + public Status() { + } + + public Status(String text) { + text_ = text; + } + + public void setText(String text) { + text_ = text; + } + + public String getText() { + return text_; + } +} diff --git a/src/com/isode/stroke/elements/StatusShow.java b/src/com/isode/stroke/elements/StatusShow.java new file mode 100644 index 0000000..d7f8575 --- /dev/null +++ b/src/com/isode/stroke/elements/StatusShow.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class StatusShow extends Payload { + + private Type type_; + + public enum Type { + + Online, Away, FFC, XA, DND, None + }; + + public StatusShow() { + type_ = Type.Online; + } + + public StatusShow(Type type) { + type_ = type; + } + + void setType(Type type) { + type_ = type; + } + + Type getType() { + return type_; + } + + static String typeToFriendlyName(Type type) { + switch (type) { + case Online: + return "Available"; + case FFC: + return "Available"; + case Away: + return "Away"; + case XA: + return "Away"; + case DND: + return "Busy"; + case None: + return "Offline"; + } + return "Unknown"; + } +} diff --git a/src/com/isode/stroke/elements/StreamError.java b/src/com/isode/stroke/elements/StreamError.java new file mode 100644 index 0000000..9335ddb --- /dev/null +++ b/src/com/isode/stroke/elements/StreamError.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class StreamError implements Element { + + public enum Type { + + BadFormat, + BadNamespacePrefix, + Conflict, + ConnectionTimeout, + HostGone, + HostUnknown, + ImproperAddressing, + InternalServerError, + InvalidFrom, + InvalidID, + InvalidNamespace, + InvalidXML, + NotAuthorized, + NotWellFormed, + PolicyViolation, + RemoteConnectionFailed, + Reset, + ResourceConstraint, + RestrictedXML, + SeeOtherHost, + SystemShutdown, + UndefinedCondition, + UnsupportedEncoding, + UnsupportedStanzaType, + UnsupportedVersion, + }; + + public StreamError() { + this(Type.UndefinedCondition); + } + + public StreamError(Type type) { + this(type, ""); + } + + public StreamError(Type type, String text) { + if (type == null) { + throw new IllegalStateException(); + } + type_ = type; + text_ = text; + } + + public Type getType() { + return type_; + } + + public void setType(Type type) { + type_ = type; + } + + public void setText(String text) { + text_ = text; + } + + public String getText() { + return text_; + } + private Type type_; + private String text_; +}; diff --git a/src/com/isode/stroke/elements/StreamFeatures.java b/src/com/isode/stroke/elements/StreamFeatures.java new file mode 100644 index 0000000..c784077 --- /dev/null +++ b/src/com/isode/stroke/elements/StreamFeatures.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +//FIXME: parser/serialiser +import java.util.ArrayList; +import java.util.Collection; + +public class StreamFeatures implements Element { + + public StreamFeatures() { + hasStartTLS_ = false; + hasResourceBind_ = false; + hasSession_ = false; + hasStreamManagement_ = false; + } + + public void setHasStartTLS() { + hasStartTLS_ = true; + } + + public boolean hasStartTLS() { + return hasStartTLS_; + } + + public void setHasSession() { + hasSession_ = true; + } + + public boolean hasSession() { + return hasSession_; + } + + public void setHasResourceBind() { + hasResourceBind_ = true; + } + + public boolean hasResourceBind() { + return hasResourceBind_; + } + + public Collection<String> getCompressionMethods() { + return compressionMethods_; + } + + public void addCompressionMethod(String mechanism) { + compressionMethods_.add(mechanism); + } + + public boolean hasCompressionMethod(String mechanism) { + return compressionMethods_.contains(mechanism); + } + + public Collection<String> getAuthenticationMechanisms() { + return authenticationMechanisms_; + } + + public void addAuthenticationMechanism(String mechanism) { + authenticationMechanisms_.add(mechanism); + } + + public boolean hasAuthenticationMechanism(String mechanism) { + return authenticationMechanisms_.contains(mechanism); + } + + public boolean hasAuthenticationMechanisms() { + return !authenticationMechanisms_.isEmpty(); + } + + public boolean hasStreamManagement() { + return hasStreamManagement_; + } + + public void setHasStreamManagement() { + hasStreamManagement_ = true; + } + + public boolean hasRosterVersioning() { + return hasRosterVersioning_; + } + + public void setHasRosterVersioning() { + hasRosterVersioning_ = true; + } + + private boolean hasStartTLS_; + private ArrayList<String> compressionMethods_ = new ArrayList<String>(); + private ArrayList<String> authenticationMechanisms_ = new ArrayList<String>(); + private boolean hasResourceBind_; + private boolean hasSession_; + private boolean hasStreamManagement_; + private boolean hasRosterVersioning_; +} diff --git a/src/com/isode/stroke/elements/StreamManagementEnabled.java b/src/com/isode/stroke/elements/StreamManagementEnabled.java new file mode 100644 index 0000000..db2aa77 --- /dev/null +++ b/src/com/isode/stroke/elements/StreamManagementEnabled.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class StreamManagementEnabled implements Element { + + public void setResumeSupported() { + resumeSupported = true; + } + + public boolean getResumeSupported() { + return resumeSupported; + } + + public void setResumeID(String id) { + resumeID = id; + } + + public String getResumeID() { + return resumeID; + } + private boolean resumeSupported; + private String resumeID; +} diff --git a/src/com/isode/stroke/elements/StreamManagementFailed.java b/src/com/isode/stroke/elements/StreamManagementFailed.java new file mode 100644 index 0000000..7be357b --- /dev/null +++ b/src/com/isode/stroke/elements/StreamManagementFailed.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class StreamManagementFailed implements Element { + +} diff --git a/src/com/isode/stroke/elements/StreamResume.java b/src/com/isode/stroke/elements/StreamResume.java new file mode 100644 index 0000000..40c6326 --- /dev/null +++ b/src/com/isode/stroke/elements/StreamResume.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2011, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class StreamResume implements Element { + + public void setResumeID(String id) { + resumeID = id; + } + + public String getResumeID() { + return resumeID; + } + + public Long getHandledStanzasCount() { + return handledStanzasCount; + } + + public void setHandledStanzasCount(long i) { + handledStanzasCount = i; + } + private String resumeID; + private Long handledStanzasCount; +} diff --git a/src/com/isode/stroke/elements/StreamResumed.java b/src/com/isode/stroke/elements/StreamResumed.java new file mode 100644 index 0000000..23f0e54 --- /dev/null +++ b/src/com/isode/stroke/elements/StreamResumed.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2011, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.elements; + +public class StreamResumed implements Element { + + public void setResumeID(String id) { + resumeID = id; + } + + public String getResumeID() { + return resumeID; + } + + public Long getHandledStanzasCount() { + return handledStanzasCount; + } + + public void setHandledStanzasCount(long i) { + handledStanzasCount = i; + } + private String resumeID; + private Long handledStanzasCount; +} diff --git a/src/com/isode/stroke/elements/StreamType.java b/src/com/isode/stroke/elements/StreamType.java new file mode 100644 index 0000000..4ec7d9f --- /dev/null +++ b/src/com/isode/stroke/elements/StreamType.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.elements; + + +public enum StreamType { + ClientStreamType, + ServerStreamType, + ComponentStreamType +} diff --git a/src/com/isode/stroke/elements/Subject.java b/src/com/isode/stroke/elements/Subject.java new file mode 100644 index 0000000..0881a26 --- /dev/null +++ b/src/com/isode/stroke/elements/Subject.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class Subject extends Payload { + String text_ = ""; + + public Subject() {} + + public Subject(String text) { + setText(text); + } + + public void setText(String text) { + text_ = text; + } + + public String getText() { + return text_; + } +} diff --git a/src/com/isode/stroke/elements/TLSProceed.java b/src/com/isode/stroke/elements/TLSProceed.java new file mode 100644 index 0000000..d330364 --- /dev/null +++ b/src/com/isode/stroke/elements/TLSProceed.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.elements; + + +public class TLSProceed implements Element { +//FIXME: parser/serialiser +} diff --git a/src/com/isode/stroke/elements/UnknownElement.java b/src/com/isode/stroke/elements/UnknownElement.java new file mode 100644 index 0000000..c9d1f4f --- /dev/null +++ b/src/com/isode/stroke/elements/UnknownElement.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.elements; + +public class UnknownElement implements Element { + +} diff --git a/src/com/isode/stroke/elements/Version.java b/src/com/isode/stroke/elements/Version.java new file mode 100644 index 0000000..80c1af3 --- /dev/null +++ b/src/com/isode/stroke/elements/Version.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * All rights reserved. + */ +/* + * Copyright (c) 2010 Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.elements; + + +public class Version extends Payload { + private String name_; + private String version_; + private String os_; + + public Version(final String name, final String version, final String os) { + name_ = name; + version_ = version; + os_ = os; + } + + public Version() { + + } + + public String getName() { + return name_; + } + + public String getVersion() { + return version_; + } + + public String getOS() { + return os_; + } + + public void setName(final String name) { + name_ = name; + } + + public void setVersion(final String version) { + version_ = version; + } + + public void setOS(final String os) { + os_ = os; + } +} diff --git a/src/com/isode/stroke/eventloop/Event.java b/src/com/isode/stroke/eventloop/Event.java new file mode 100644 index 0000000..b3c8c00 --- /dev/null +++ b/src/com/isode/stroke/eventloop/Event.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.eventloop; + +public class Event { + + public interface Callback { + + void run(); + } + + Event(EventOwner owner, Callback callback, int id) { + this.owner = owner; + this.callback = callback; + this.id = id; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Event) { + return id == ((Event) other).id; + } + return false; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + this.id; + return hash; + } + final int id; + final EventOwner owner; + public final Callback callback; +} diff --git a/src/com/isode/stroke/eventloop/EventLoop.java b/src/com/isode/stroke/eventloop/EventLoop.java new file mode 100644 index 0000000..9399d9a --- /dev/null +++ b/src/com/isode/stroke/eventloop/EventLoop.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.eventloop; + +import java.util.ArrayList; +import java.util.Vector; + +public abstract class EventLoop { + + public EventLoop() { + } + + public void postEvent(Event.Callback callback) { + postEvent(callback, null); + } + + public void postEvent(Event.Callback callback, EventOwner owner) { + Event event; + synchronized (eventsMutex_) { + event = new Event(owner, callback, nextEventID_); + nextEventID_++; + events_.add(event); + } + post(event); + } + + public void removeEventsFromOwner(EventOwner owner) { + synchronized (eventsMutex_) { + ArrayList<Event> matches = new ArrayList<Event>(); + for (Event event : events_) { + if (event.owner == owner) { + matches.add(event); + } + } + events_.removeAll(matches); + } + } + + /** + * Reimplement this to call handleEvent(event) from the thread in which + * the event loop is residing. + */ + protected abstract void post(Event event); + + protected void handleEvent(Event event) { + if (handlingEvents_) { + // We're being called recursively. Push in the list of events to + // handle in the parent handleEvent() + eventsToHandle_.add(event); + return; + } + + boolean doCallback = false; + synchronized (eventsMutex_) { + doCallback = events_.contains(event); + if (doCallback) { + events_.remove(event); + } + } + if (doCallback) { + handlingEvents_ = true; + event.callback.run(); + // Process events that were passed to handleEvent during the callback + // (i.e. through recursive calls of handleEvent) + while (!eventsToHandle_.isEmpty()) { + Event nextEvent = eventsToHandle_.firstElement(); + eventsToHandle_.remove(0); + nextEvent.callback.run(); + } + handlingEvents_ = false; + } + } + // struct HasOwner { + // HasOwner(boost::shared_ptr<EventOwner> owner) : owner(owner) {} + // bool operator()(const Event& event) { return event.owner == owner; } + // boost::shared_ptr<EventOwner> owner; + // }; + private final Object eventsMutex_ = new Object(); + private int nextEventID_ = 0; + private Vector<Event> events_ = new Vector<Event>(); + boolean handlingEvents_ = false; + private Vector<Event> eventsToHandle_ = new Vector<Event>(); +} diff --git a/src/com/isode/stroke/eventloop/EventOwner.java b/src/com/isode/stroke/eventloop/EventOwner.java new file mode 100644 index 0000000..305ab1a --- /dev/null +++ b/src/com/isode/stroke/eventloop/EventOwner.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.eventloop; + +public interface EventOwner { + +} diff --git a/src/com/isode/stroke/eventloop/SimpleEventLoop.java b/src/com/isode/stroke/eventloop/SimpleEventLoop.java new file mode 100644 index 0000000..003fc98 --- /dev/null +++ b/src/com/isode/stroke/eventloop/SimpleEventLoop.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.eventloop; + +/** + * Don't use this, it simply runs the callback in the same thread. + * It is useful for unit testing, but will break GUIs. + */ +public class SimpleEventLoop extends EventLoop { + @Override + protected void post(Event event) { + event.callback.run(); + } +} diff --git a/src/com/isode/stroke/examples/gui/StrokeGUI.form b/src/com/isode/stroke/examples/gui/StrokeGUI.form new file mode 100644 index 0000000..502e00b --- /dev/null +++ b/src/com/isode/stroke/examples/gui/StrokeGUI.form @@ -0,0 +1,201 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.3" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JFrameFormInfo"> + <Properties> + <Property name="defaultCloseOperation" type="int" value="3"/> + </Properties> + <SyntheticProperties> + <SyntheticProperty name="formSizePolicy" type="int" value="1"/> + </SyntheticProperties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="2"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,4,60,0,0,3,46"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + <SubComponents> + <Container class="javax.swing.JPanel" name="jPanel1"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Login Stuff"/> + </Border> + </Property> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="jLabel1"> + <Properties> + <Property name="text" type="java.lang.String" value="JID"/> + </Properties> + </Component> + <Component class="javax.swing.JTextField" name="loginJID_"> + <Properties> + <Property name="bounds" type="java.awt.Rectangle" editor="org.netbeans.beaninfo.editors.RectangleEditor"> + <Rectangle value="[0, 0, 150, 0]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[150, 28]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[250, 28]"/> + </Property> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="jLabel2"> + <Properties> + <Property name="text" type="java.lang.String" value="Password"/> + </Properties> + </Component> + <Component class="javax.swing.JTextField" name="loginPassword_"> + <Properties> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 28]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[150, 28]"/> + </Property> + </Properties> + </Component> + <Component class="javax.swing.JButton" name="loginButton_"> + <Properties> + <Property name="text" type="java.lang.String" value="Login"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="loginButton_ActionPerformed"/> + </Events> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="jPanel2"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Send Stuff"/> + </Border> + </Property> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + <SubComponents> + <Component class="javax.swing.JTextField" name="sendTo_"> + <Properties> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[150, 28]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[250, 28]"/> + </Property> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="jLabel3"> + <Properties> + <Property name="text" type="java.lang.String" value="To"/> + </Properties> + </Component> + <Component class="javax.swing.JButton" name="sendButton_"> + <Properties> + <Property name="text" type="java.lang.String" value="Send"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="sendButton_ActionPerformed"/> + </Events> + </Component> + <Container class="javax.swing.JScrollPane" name="jScrollPane1"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTextArea" name="sendText_"> + <Properties> + <Property name="columns" type="int" value="20"/> + <Property name="rows" type="int" value="5"/> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[150, 16]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[150, 150]"/> + </Property> + <Property name="size" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[450, 150]"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="jPanel3"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="Receive Stuff"/> + </Border> + </Property> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="jScrollPane2"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTextArea" name="receiveText_"> + <Properties> + <Property name="columns" type="int" value="20"/> + <Property name="editable" type="boolean" value="false"/> + <Property name="rows" type="int" value="5"/> + </Properties> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="jPanel4"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="XML"/> + </Border> + </Property> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="jScrollPane3"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTextArea" name="xmlText_"> + <Properties> + <Property name="columns" type="int" value="20"/> + <Property name="editable" type="boolean" value="false"/> + <Property name="rows" type="int" value="5"/> + <Property name="size" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[500, 300]"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/src/com/isode/stroke/examples/gui/StrokeGUI.java b/src/com/isode/stroke/examples/gui/StrokeGUI.java new file mode 100644 index 0000000..09f7936 --- /dev/null +++ b/src/com/isode/stroke/examples/gui/StrokeGUI.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ + +/* + * MainFrame.java + * + * Created on Jul 7, 2010, 10:03:01 AM + */ + +package com.isode.stroke.examples.gui; + +import com.isode.stroke.client.ClientError; +import com.isode.stroke.client.ClientOptions; +import com.isode.stroke.client.CoreClient; +import com.isode.stroke.elements.Message; +import com.isode.stroke.eventloop.Event; +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.jid.JID; +import com.isode.stroke.network.JavaNetworkFactories; +import com.isode.stroke.signals.Slot1; +import java.awt.EventQueue; + +public class StrokeGUI extends javax.swing.JFrame { + + private CoreClient client_; + + /** Creates new form MainFrame */ + public StrokeGUI() { + initComponents(); + } + + /** This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + jPanel1 = new javax.swing.JPanel(); + jLabel1 = new javax.swing.JLabel(); + loginJID_ = new javax.swing.JTextField(); + jLabel2 = new javax.swing.JLabel(); + loginPassword_ = new javax.swing.JTextField(); + loginButton_ = new javax.swing.JButton(); + jPanel2 = new javax.swing.JPanel(); + sendTo_ = new javax.swing.JTextField(); + jLabel3 = new javax.swing.JLabel(); + sendButton_ = new javax.swing.JButton(); + jScrollPane1 = new javax.swing.JScrollPane(); + sendText_ = new javax.swing.JTextArea(); + jPanel3 = new javax.swing.JPanel(); + jScrollPane2 = new javax.swing.JScrollPane(); + receiveText_ = new javax.swing.JTextArea(); + jPanel4 = new javax.swing.JPanel(); + jScrollPane3 = new javax.swing.JScrollPane(); + xmlText_ = new javax.swing.JTextArea(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + getContentPane().setLayout(new java.awt.FlowLayout()); + + jPanel1.setBorder(javax.swing.BorderFactory.createTitledBorder("Login Stuff")); + + jLabel1.setText("JID"); + jPanel1.add(jLabel1); + + loginJID_.setBounds(new java.awt.Rectangle(0, 0, 150, 0)); + loginJID_.setMinimumSize(new java.awt.Dimension(150, 28)); + loginJID_.setPreferredSize(new java.awt.Dimension(250, 28)); + jPanel1.add(loginJID_); + + jLabel2.setText("Password"); + jPanel1.add(jLabel2); + + loginPassword_.setMinimumSize(new java.awt.Dimension(100, 28)); + loginPassword_.setPreferredSize(new java.awt.Dimension(150, 28)); + jPanel1.add(loginPassword_); + + loginButton_.setText("Login"); + loginButton_.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + loginButton_ActionPerformed(evt); + } + }); + jPanel1.add(loginButton_); + + getContentPane().add(jPanel1); + + jPanel2.setBorder(javax.swing.BorderFactory.createTitledBorder("Send Stuff")); + + sendTo_.setMinimumSize(new java.awt.Dimension(150, 28)); + sendTo_.setPreferredSize(new java.awt.Dimension(250, 28)); + jPanel2.add(sendTo_); + + jLabel3.setText("To"); + jPanel2.add(jLabel3); + + sendButton_.setText("Send"); + sendButton_.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + sendButton_ActionPerformed(evt); + } + }); + jPanel2.add(sendButton_); + + sendText_.setColumns(20); + sendText_.setRows(5); + sendText_.setMinimumSize(new java.awt.Dimension(150, 16)); + sendText_.setPreferredSize(new java.awt.Dimension(150, 150)); + sendText_.setSize(new java.awt.Dimension(450, 150)); + jScrollPane1.setViewportView(sendText_); + + jPanel2.add(jScrollPane1); + + getContentPane().add(jPanel2); + + jPanel3.setBorder(javax.swing.BorderFactory.createTitledBorder("Receive Stuff")); + + receiveText_.setColumns(20); + receiveText_.setEditable(false); + receiveText_.setRows(5); + jScrollPane2.setViewportView(receiveText_); + + jPanel3.add(jScrollPane2); + + getContentPane().add(jPanel3); + + jPanel4.setBorder(javax.swing.BorderFactory.createTitledBorder("XML")); + + xmlText_.setColumns(20); + xmlText_.setEditable(false); + xmlText_.setRows(5); + xmlText_.setSize(new java.awt.Dimension(500, 300)); + jScrollPane3.setViewportView(xmlText_); + + jPanel4.add(jScrollPane3); + + getContentPane().add(jPanel4); + jPanel4.getAccessibleContext().setAccessibleName("XML"); + + pack(); + }// </editor-fold>//GEN-END:initComponents + + private void loginButton_ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_loginButton_ActionPerformed + System.out.println("Client created from JID " + loginJID_.getText()); + EventLoop eventLoop = new EventLoop() { + @Override + protected void post(final Event event) { + EventQueue.invokeLater(new Runnable() { + public void run() { + event.callback.run(); + } + }); + } + + }; + client_ = new CoreClient(eventLoop, JID.fromString(loginJID_.getText()), loginPassword_.getText(), new JavaNetworkFactories(eventLoop)); + System.out.println("Connecting"); + try { + client_.connect(new ClientOptions()); + } catch (Exception e) { + //Something bad happened + System.out.println("Exception!"); + } + System.out.println("Connected"); + final StrokeGUI thisObject = this; + client_.onMessageReceived.connect(new Slot1<Message>() { + + public void call(Message p1) { + thisObject.handleMessageReceived(p1); + } + }); + client_.onError.connect(new Slot1<ClientError>() { + + public void call(ClientError p1) { + thisObject.handleClientError(p1); + } + }); + client_.onDataRead.connect(new Slot1<String>() { + + public void call(String p1) { + xmlText_.append(">>> " + p1 + "\n"); + } + }); + client_.onDataWritten.connect(new Slot1<String>() { + + public void call(String p1) { + xmlText_.append("<<< " + p1 + "\n"); + } + }); + }//GEN-LAST:event_loginButton_ActionPerformed + + private void sendButton_ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_sendButton_ActionPerformed + Message message = new Message(); + message.setTo(JID.fromString(sendTo_.getText())); + message.setBody(sendText_.getText()); + System.out.println("Message body is " + message.getBody()); + client_.sendMessage(message); + + }//GEN-LAST:event_sendButton_ActionPerformed + + /** + * @param args the command line arguments + */ + public static void main(String args[]) { + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + new StrokeGUI().setVisible(true); + } + }); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel3; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JPanel jPanel3; + private javax.swing.JPanel jPanel4; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JScrollPane jScrollPane2; + private javax.swing.JScrollPane jScrollPane3; + private javax.swing.JButton loginButton_; + private javax.swing.JTextField loginJID_; + private javax.swing.JTextField loginPassword_; + private javax.swing.JTextArea receiveText_; + private javax.swing.JButton sendButton_; + private javax.swing.JTextArea sendText_; + private javax.swing.JTextField sendTo_; + private javax.swing.JTextArea xmlText_; + // End of variables declaration//GEN-END:variables + + private void handleMessageReceived(Message message) { + String from = message.getFrom().toString(); + String body = message.getBody(); + receiveText_.append("<" + from + "> " + body + "\n"); + } + + private void handleClientError(ClientError error) { + receiveText_.append("Error connecting to server\n"); + } + +} diff --git a/src/com/isode/stroke/idn/IDNA.java b/src/com/isode/stroke/idn/IDNA.java new file mode 100644 index 0000000..e180cc5 --- /dev/null +++ b/src/com/isode/stroke/idn/IDNA.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.idn; + +import java.net.IDN; + +public class IDNA { + public static String getEncoded(String s) { + return IDN.toASCII(s); + } +} diff --git a/src/com/isode/stroke/jid/JID.java b/src/com/isode/stroke/jid/JID.java new file mode 100644 index 0000000..70c2ccc --- /dev/null +++ b/src/com/isode/stroke/jid/JID.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.jid; + +/** + * JID helper. + * + * This represents the JID used in XMPP + * (RFC6120 - http://tools.ietf.org/html/rfc6120 particularly section 1.4), + * further defined in XMPP Address Format (http://tools.ietf.org/html/rfc6122 ). + * For a description of format, see the RFC or page 14 of + * XMPP: The Definitive Guide (Saint-Andre et al.) + * + * Particularly - a Bare JID is a JID without a resource part. + * + * Note that invalid JIDs shouldn't have any calls made to them beyond isValid(). + * + */ +public class JID { + public enum CompareType { + WithResource, WithoutResource + }; + + private final String node_; + private final String domain_; + private final String resource_; + + /** + * Create an invalid JID. + */ + public JID() { + this("", "", null); + } + + /** + * Create a JID using the JID(String) constructor. + * @param jid String formatted JID. + */ + public static JID fromString(final String jid) { + return new JID(jid); + } + + /** + * Create a JID from its String representation. + * + * e.g. + * wonderland.lit + * wonderland.lit/rabbithole + * alice@wonderland.lit + * alice@wonderland.lit/TeaParty + * + * @param jid String representation. Invalid JID if null or invalid. + */ + public JID(final String jid) { + //FIXME: This doesn't nameprep! + if (jid == null || jid.startsWith("@")) { + node_ = ""; + domain_ = ""; + resource_ = ""; + return; + } + + String bare; + String resource; + String[] parts = jid.split("/", 2); + if (parts.length > 1) { + bare = parts[0]; + resource = parts[1]; + } else { + resource = null; + bare = jid; + } + String[] nodeAndDomain = bare.split("@", 2); + if (nodeAndDomain.length == 1) { + node_ = ""; + domain_ = nodeAndDomain[0]; + resource_ = resource; + } else { + node_ = nodeAndDomain[0]; + domain_ = nodeAndDomain[1]; + resource_ = resource; + } + + } + + /** + * Create a bare JID from the node and domain parts. + * + * JID("node@domain") == JID("node", "domain") + * Use a different constructor instead of passing nulls. + * + * @param node JID node part. + * @param domain JID domain part. + */ + public JID(final String node, final String domain) { + this(node, domain, null); + } + + /** + * Create a bare JID from the node, domain and resource parts. + * + * JID("node@domain/resource") == JID("node", "domain", "resource") + * Use a different constructor instead of passing nulls. + * + * @param node JID node part. + * @param domain JID domain part. + * @param resource JID resource part. + */ + public JID(final String node, final String domain, final String resource) { + //FIXME: This doesn't nameprep! + node_ = node; + domain_ = domain; + resource_ = resource; + + } + + /** + * @return Is a correctly-formatted JID. + */ + public boolean isValid() { + return (domain_.length()!=0); + } + + /** + * e.g. JID("node@domain").getNode() == "node" + * @return null for nodeless JIDs. + */ + public String getNode() { + return node_; + } + + /** + * e.g. JID("node@domain").getDomain() == "domain" + * @return only null for invalid JIDs. + */ + public String getDomain() { + return domain_; + } + + /** + * e.g. JID("node@domain/resource").getResource() == "resource" + * @return null for bare JIDs. + */ + public String getResource() { + return resource_ != null ? resource_ : ""; + } + + /** + * Is a bare JID, i.e. has no resource part. + */ + public boolean isBare() { + return resource_ == null; + } + + /** + * Get the JID without a resource. + * @return non-null. Invalid if the original is invalid. + */ + public JID toBare() { + return new JID(getNode(), getDomain()); + } + + @Override + public String toString() { + String string = new String(); + if (node_.length()!=0) { + string += node_ + "@"; + } + string += domain_; + if (!isBare()) { + string += "/" + resource_; + } + return string; + } + + @Override + public boolean equals(final Object otherObject) { + if (otherObject == null || getClass() != otherObject.getClass()) { + return false; + } + if (otherObject == this) { + return true; + } + JID other = (JID)otherObject; + String node1 = getNode(); + String node2 = other.getNode(); + String domain1 = getDomain(); + String domain2 = other.getDomain(); + String resource1 = getResource(); + String resource2 = other.getResource(); + boolean nodeMatch = (node1 == null && node2 == null) || (node1 != null && node1.equals(node2)); + boolean domainMatch = (domain1 == null && domain2 == null) || (domain1 != null && domain1.equals(domain2)); + boolean resourceMatch = (resource1 == null && resource2 == null) || (resource1 != null && resource1.equals(resource2)); + return nodeMatch && domainMatch && resourceMatch; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 73 * hash + (this.node_ != null ? this.node_.hashCode() : 0); + hash = 73 * hash + (this.domain_ != null ? this.domain_.hashCode() : 0); + hash = 73 * hash + (this.resource_ != null ? this.resource_.hashCode() : 0); + return hash; + } +} diff --git a/src/com/isode/stroke/network/Connection.java b/src/com/isode/stroke/network/Connection.java new file mode 100644 index 0000000..ff4a056 --- /dev/null +++ b/src/com/isode/stroke/network/Connection.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.network; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.signals.Signal; +import com.isode.stroke.signals.Signal1; + +public abstract class Connection { + + public enum Error { + + ReadError, + WriteError + }; + + public Connection() { + } + + public abstract void listen(); + + public abstract void connect(HostAddressPort address); + + public abstract void disconnect(); + + public abstract void write(ByteArray data); + + public abstract HostAddressPort getLocalAddress(); + public final Signal1<Boolean /*error*/> onConnectFinished = new Signal1<Boolean>(); + public final Signal1<Error> onDisconnected = new Signal1<Error>(); + public final Signal1<ByteArray> onDataRead = new Signal1<ByteArray>(); + public final Signal onDataWritten = new Signal(); +} diff --git a/src/com/isode/stroke/network/ConnectionFactory.java b/src/com/isode/stroke/network/ConnectionFactory.java new file mode 100644 index 0000000..6764d24 --- /dev/null +++ b/src/com/isode/stroke/network/ConnectionFactory.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.network; + +public interface ConnectionFactory { + Connection createConnection(); +} diff --git a/src/com/isode/stroke/network/Connector.java b/src/com/isode/stroke/network/Connector.java new file mode 100644 index 0000000..560bf7b --- /dev/null +++ b/src/com/isode/stroke/network/Connector.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.network; + +import com.isode.stroke.network.DomainNameServiceQuery.Result; +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 java.util.ArrayList; +import java.util.Collection; + +public class Connector { + + public static Connector create(String hostname, DomainNameResolver resolver, ConnectionFactory connectionFactory, TimerFactory timerFactory) { + return new Connector(hostname, resolver, connectionFactory, timerFactory); + } + + public void setTimeoutMilliseconds(int milliseconds) { + timeoutMilliseconds = milliseconds; + } + + public void start() { + assert currentConnection == null; + assert serviceQuery == null; + assert timer == null; + queriedAllServices = false; + serviceQuery = resolver.createServiceQuery("_xmpp-client._tcp." + hostname); + serviceQuery.onResult.connect(new Slot1<Collection<DomainNameServiceQuery.Result>>() { + public void call(Collection<Result> p1) { + handleServiceQueryResult(p1); + } + }); + if (timeoutMilliseconds > 0) { + timer = timerFactory.createTimer(timeoutMilliseconds); + timer.onTick.connect(new Slot() { + public void call() { + handleTimeout(); + } + }); + timer.start(); + } + serviceQuery.run(); + } + + public void stop() { + finish(null); + } + + public final Signal1<Connection> onConnectFinished = new Signal1<Connection>(); + + private Connector(String hostname, DomainNameResolver resolver, ConnectionFactory connectionFactory, TimerFactory timerFactory) { + this.hostname = hostname; + this.resolver = resolver; + this.connectionFactory = connectionFactory; + this.timerFactory = timerFactory; + } + + private void handleServiceQueryResult(Collection<Result> result) { + serviceQueryResults = new ArrayList<Result>(); + serviceQueryResults.addAll(result); + serviceQuery = null; + tryNextServiceOrFallback(); + } + + private void handleAddressQueryResult(Collection<HostAddress> addresses, DomainNameResolveError error) { + //std::cout << "Connector::handleAddressQueryResult(): Start" << std::endl; + addressQuery = null; + if (error != null || addresses.isEmpty()) { + if (!serviceQueryResults.isEmpty()) { + serviceQueryResults.remove(0); + } + tryNextServiceOrFallback(); + } + else { + addressQueryResults = new ArrayList<HostAddress>(); + addressQueryResults.addAll(addresses); + tryNextAddress(); + } + } + + private void queryAddress(String hostname) { + assert addressQuery == null; + addressQuery = resolver.createAddressQuery(hostname); + addressQuery.onResult.connect(new Slot2<Collection<HostAddress>, DomainNameResolveError>() { + public void call(Collection<HostAddress> p1, DomainNameResolveError p2) { + handleAddressQueryResult(p1, p2); + } + }); + addressQuery.run(); + } + + private void tryNextServiceOrFallback() { + if (queriedAllServices) { + //std::cout << "Connector::tryNextServiceOrCallback(): Queried all hosts. Error." << std::endl; + finish(null); + } + else if (serviceQueryResults.isEmpty()) { + //std::cout << "Connector::tryNextHostName(): Falling back on A resolution" << std::endl; + // Fall back on simple address resolving + queriedAllServices = true; + queryAddress(hostname); + } + else { + //std::cout << "Connector::tryNextHostName(): Querying next address" << std::endl; + queryAddress(serviceQueryResults.get(0).hostname); + } + } + + private void tryNextAddress() { + if (addressQueryResults.isEmpty()) { + //std::cout << "Connector::tryNextAddress(): Done trying addresses. Moving on" << std::endl; + // Done trying all addresses. Move on to the next host. + if (!serviceQueryResults.isEmpty()) { + serviceQueryResults.remove(0); + } + tryNextServiceOrFallback(); + } + else { + //std::cout << "Connector::tryNextAddress(): trying next address." << std::endl; + HostAddress address = addressQueryResults.get(0); + addressQueryResults.remove(0); + + int port = 5222; + if (!serviceQueryResults.isEmpty()) { + port = serviceQueryResults.get(0).port; + } + + tryConnect(new HostAddressPort(address, port)); + } + } + + private void tryConnect(HostAddressPort target) { + assert currentConnection == null; + //std::cout << "Connector::tryConnect() " << target.getAddress().toString() << " " << target.getPort() << std::endl; + currentConnection = connectionFactory.createConnection(); + currentConnectionConnectFinishedConnection = currentConnection.onConnectFinished.connect(new Slot1<Boolean>() { + public void call(Boolean p1) { + handleConnectionConnectFinished(p1); + } + }); + + currentConnection.connect(target); + } + + private void handleConnectionConnectFinished(boolean error) { + //std::cout << "Connector::handleConnectionConnectFinished() " << error << std::endl; + currentConnectionConnectFinishedConnection.disconnect(); + if (error) { + currentConnection = null; + if (!addressQueryResults.isEmpty()) { + tryNextAddress(); + } + else { + if (!serviceQueryResults.isEmpty()) { + serviceQueryResults.remove(0); + } + tryNextServiceOrFallback(); + } + } + else { + finish(currentConnection); + } + } + + private void finish(Connection connection) { + if (timer != null) { + timer.stop(); + timer.onTick.disconnectAll(); + timer = null; + } + if (serviceQuery != null) { + serviceQuery.onResult.disconnectAll(); + serviceQuery = null; + } + if (addressQuery != null) { + addressQuery.onResult.disconnectAll(); + addressQuery = null; + } + if (currentConnection != null) { + currentConnectionConnectFinishedConnection.disconnect(); + currentConnection = null; + } + onConnectFinished.emit(connection); + } + + private void handleTimeout() { + finish(null); + } + private String hostname; + private DomainNameResolver resolver; + private ConnectionFactory connectionFactory; + private TimerFactory timerFactory; + private int timeoutMilliseconds = 0; + private Timer timer; + private DomainNameServiceQuery serviceQuery; + private ArrayList<DomainNameServiceQuery.Result> serviceQueryResults; + private DomainNameAddressQuery addressQuery; + private ArrayList<HostAddress> addressQueryResults; + private boolean queriedAllServices = true; + private Connection currentConnection; + private SignalConnection currentConnectionConnectFinishedConnection; +} diff --git a/src/com/isode/stroke/network/DomainNameAddressQuery.java b/src/com/isode/stroke/network/DomainNameAddressQuery.java new file mode 100644 index 0000000..1bc9edf --- /dev/null +++ b/src/com/isode/stroke/network/DomainNameAddressQuery.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.network; + +import com.isode.stroke.signals.Signal2; +import java.util.Collection; + +public abstract class DomainNameAddressQuery { + public abstract void run(); + + public final Signal2<Collection<HostAddress>, DomainNameResolveError> onResult = new Signal2<Collection<HostAddress>, DomainNameResolveError>(); +} diff --git a/src/com/isode/stroke/network/DomainNameResolveError.java b/src/com/isode/stroke/network/DomainNameResolveError.java new file mode 100644 index 0000000..b470c34 --- /dev/null +++ b/src/com/isode/stroke/network/DomainNameResolveError.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.network; + +public class DomainNameResolveError implements com.isode.stroke.base.Error { + +} diff --git a/src/com/isode/stroke/network/DomainNameResolver.java b/src/com/isode/stroke/network/DomainNameResolver.java new file mode 100644 index 0000000..bbd4e79 --- /dev/null +++ b/src/com/isode/stroke/network/DomainNameResolver.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.network; + + +public abstract class DomainNameResolver { + public abstract DomainNameServiceQuery createServiceQuery(String name); + public abstract DomainNameAddressQuery createAddressQuery(String name); + + protected String getNormalized(String domain) { + return domain; + //FIXME: port idna +// char* output; +// if (idna_to_ascii_8z(domain.getUTF8Data(), &output, 0) == IDNA_SUCCESS) { +// String result(output); +// free(output); +// return result; +// } +// else { +// return domain; +// } + } +} diff --git a/src/com/isode/stroke/network/DomainNameServiceQuery.java b/src/com/isode/stroke/network/DomainNameServiceQuery.java new file mode 100644 index 0000000..ddb94a8 --- /dev/null +++ b/src/com/isode/stroke/network/DomainNameServiceQuery.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.network; + +import com.isode.stroke.signals.Signal1; +import java.util.Collection; + +public abstract class DomainNameServiceQuery { + + public class Result { + + public Result() { + hostname = ""; + port = -1; + priority = -1; + weight = -1; + } + + public Result(String hostname, int port, int priority, int weight) { + this.hostname = hostname; + this.port = port; + this.priority = priority; + this.weight = weight; + } + public final String hostname; + public final int port; + public final int priority; + public final int weight; + }; + + public class ResultPriorityComparator { + + public boolean compare(DomainNameServiceQuery.Result a, DomainNameServiceQuery.Result b) { + return a.priority < b.priority; + } + }; + + public abstract void run(); + public final Signal1<Collection<Result>> onResult = new Signal1<Collection<Result>>(); +} diff --git a/src/com/isode/stroke/network/HostAddress.java b/src/com/isode/stroke/network/HostAddress.java new file mode 100644 index 0000000..152dc2b --- /dev/null +++ b/src/com/isode/stroke/network/HostAddress.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.network; + +import java.net.InetAddress; + +public class HostAddress { + + public HostAddress() { + address_ = null; + } + + public HostAddress(InetAddress address) { + address_ = address; + } + /* public HostAddress(const String&); + public HostAddress(const unsigned char* address, int length); + public HostAddress(const boost::asio::ip::address& address);*/ + + @Override + public String toString() { + return address_.getHostAddress(); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 29 * hash + (this.address_ != null ? this.address_.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object other) { + if (other instanceof HostAddress) { + return address_.equals(((HostAddress)other).getInetAddress()); + } + return false; + } + + public boolean isValid() { + return address_ != null; + } + + InetAddress getInetAddress() { + return address_; + } + + private final InetAddress address_; +} diff --git a/src/com/isode/stroke/network/HostAddressPort.java b/src/com/isode/stroke/network/HostAddressPort.java new file mode 100644 index 0000000..01048a3 --- /dev/null +++ b/src/com/isode/stroke/network/HostAddressPort.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.network; + +public class HostAddressPort { + + public HostAddressPort(HostAddress address) { + address_ = address; + port_ = -1; + } + + public HostAddressPort(HostAddress address, int port) { + address_ = address; + port_ = port; + } + + /* + public HostAddressPort(const boost::asio::ip::tcp::endpoint& endpoint) { + address_ = HostAddress(endpoint.address()); + port_ = endpoint.port(); + }*/ //FIXME + public HostAddress getAddress() { + return address_; + } + + public int getPort() { + return port_; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof HostAddressPort)) return false; + HostAddressPort o = (HostAddressPort)other; + return getAddress().equals(o.getAddress()) && port_ == o.getPort(); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 17 * hash + (this.address_ != null ? this.address_.hashCode() : 0); + hash = 17 * hash + this.port_; + return hash; + } + + public boolean isValid() { + return address_.isValid() && port_ > 0; + } + private HostAddress address_; + private int port_; +} diff --git a/src/com/isode/stroke/network/JavaConnection.java b/src/com/isode/stroke/network/JavaConnection.java new file mode 100644 index 0000000..e43707c --- /dev/null +++ b/src/com/isode/stroke/network/JavaConnection.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.network; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.eventloop.Event.Callback; +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.eventloop.EventOwner; +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class JavaConnection extends Connection implements EventOwner { + + private class Worker implements Runnable { + + private final HostAddressPort address_; + private OutputStream write_; + private BufferedReader read_; + private final List<ByteArray> writeBuffer_ = Collections.synchronizedList(new ArrayList<ByteArray>()); + + public Worker(HostAddressPort address) { + address_ = address; + } + + public void run() { + try { + socket_ = new Socket(address_.getAddress().getInetAddress(), address_.getPort()); + write_ = socket_.getOutputStream(); + read_ = new BufferedReader(new InputStreamReader(socket_.getInputStream(), "utf-8")); + } catch (IOException ex) { + handleConnected(true); + return; + } + handleConnected(false); + while (!disconnecting_) { + boolean didWrite = false; + while (!writeBuffer_.isEmpty()) { + didWrite = true; + ByteArray data = writeBuffer_.get(0); + for (byte datum : data.getData()) { + try { + write_.write(datum); + } catch (IOException ex) { + disconnecting_ = true; + handleDisconnected(Error.WriteError); + } + } + writeBuffer_.remove(0); + } + if (didWrite && !disconnecting_) { + try { + write_.flush(); + } catch (IOException ex) { + disconnecting_ = true; + handleDisconnected(Error.WriteError); + } + } + ByteArray data = new ByteArray(); + try { + while (read_.ready()) { + char[] c = new char[1024]; + int i = read_.read(c, 0, c.length); + if (i > 0) { + data.append(new String(c, 0, i)); + } + } + } catch (IOException ex) { + handleDisconnected(Error.ReadError); + return; + } + if (!data.isEmpty()) { + handleDataRead(data); + } + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + /* We've been woken up, probably to force us to do something.*/ + } + } + try { + read_.close(); + write_.close(); + socket_.close(); + } catch (IOException ex) { + /* Do we need to return an error if we're already trying to close? */ + } + } + + private void handleConnected(final boolean error) { + eventLoop_.postEvent(new Callback() { + public void run() { + onConnectFinished.emit(error); + } + }); + } + + private void handleDisconnected(final Error error) { + eventLoop_.postEvent(new Callback() { + public void run() { + onDisconnected.emit(error); + } + }); + } + + private void handleDataRead(final ByteArray data) { + eventLoop_.postEvent(new Callback() { + public void run() { + onDataRead.emit(data); + } + }); + } + + public void write(ByteArray data) { + writeBuffer_.add(data); + } + } + + private JavaConnection(EventLoop eventLoop) { + eventLoop_ = eventLoop; + } + + public static JavaConnection create(EventLoop eventLoop) { + return new JavaConnection(eventLoop); + } + + @Override + public void listen() { + //TODO: needed for server, not for client. + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void connect(HostAddressPort address) { + worker_ = new Worker(address); + Thread workerThread = new Thread(worker_); + workerThread.setDaemon(true); + workerThread.start(); + } + + @Override + public void disconnect() { + disconnecting_ = true; + } + + @Override + public void write(ByteArray data) { + worker_.writeBuffer_.add(data); + } + + @Override + public HostAddressPort getLocalAddress() { + return new HostAddressPort(new HostAddress(socket_.getLocalAddress()), socket_.getLocalPort()); + } + private final EventLoop eventLoop_; + private boolean disconnecting_ = false; + private Socket socket_; + private Worker worker_; +} diff --git a/src/com/isode/stroke/network/JavaConnectionFactory.java b/src/com/isode/stroke/network/JavaConnectionFactory.java new file mode 100644 index 0000000..956e80b --- /dev/null +++ b/src/com/isode/stroke/network/JavaConnectionFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.network; + +import com.isode.stroke.eventloop.EventLoop; + +public class JavaConnectionFactory implements ConnectionFactory { + + public JavaConnectionFactory(EventLoop eventLoop) { + this.eventLoop = eventLoop; + } + + public Connection createConnection() { + return JavaConnection.create(eventLoop); + } + + private final EventLoop eventLoop; +} diff --git a/src/com/isode/stroke/network/JavaNetworkFactories.java b/src/com/isode/stroke/network/JavaNetworkFactories.java new file mode 100644 index 0000000..acd289b --- /dev/null +++ b/src/com/isode/stroke/network/JavaNetworkFactories.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.network; + +import com.isode.stroke.eventloop.EventLoop; + +public class JavaNetworkFactories implements NetworkFactories { + + public JavaNetworkFactories(EventLoop eventLoop) { + eventLoop_ = eventLoop; + timers_ = new JavaTimerFactory(eventLoop_); + connections_ = new JavaConnectionFactory(eventLoop_); + dns_ = new PlatformDomainNameResolver(eventLoop_); + } + + public TimerFactory getTimerFactory() { + return timers_; + } + + public ConnectionFactory getConnectionFactory() { + return connections_; + } + + public DomainNameResolver getDomainNameResolver() { + return dns_; + } + private final EventLoop eventLoop_; + private final JavaTimerFactory timers_; + private final JavaConnectionFactory connections_; + private final PlatformDomainNameResolver dns_; +} diff --git a/src/com/isode/stroke/network/JavaTimer.java b/src/com/isode/stroke/network/JavaTimer.java new file mode 100644 index 0000000..61357d4 --- /dev/null +++ b/src/com/isode/stroke/network/JavaTimer.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.network; + +import com.isode.stroke.eventloop.Event; +import com.isode.stroke.eventloop.EventLoop; + +class JavaTimer extends Timer { + + private class TimerRunnable implements Runnable { + + boolean running_ = true; + private final EventLoop eventLoop_; + private final int milliseconds_; + + public TimerRunnable(EventLoop eventLoop, int milliseconds) { + eventLoop_ = eventLoop; + milliseconds_ = milliseconds; + } + + public void run() { + while (shouldEmit()) { + try { + Thread.sleep(milliseconds_); + } catch (InterruptedException ex) { + /* If we were interrupted, either emit or don't, based on whether stop was called.*/ + } + if (shouldEmit()) { + eventLoop_.postEvent(new Event.Callback() { + public void run() { + onTick.emit(); + } + }); + } + } + } + + + synchronized boolean shouldEmit() { + return running_; + } + + public synchronized void stop() { + running_ = false; + } + } + + public JavaTimer(EventLoop eventLoop, int milliseconds) { + timer_ = new TimerRunnable(eventLoop, milliseconds); + } + + @Override + public void start() { + Thread thread = (new Thread(timer_)); + thread.setDaemon(true); + thread.start(); + } + + @Override + public void stop() { + timer_.stop(); + //FIXME: This needs to clear any remaining events out of the EventLoop queue. + } + private final TimerRunnable timer_; +} diff --git a/src/com/isode/stroke/network/JavaTimerFactory.java b/src/com/isode/stroke/network/JavaTimerFactory.java new file mode 100644 index 0000000..10017a8 --- /dev/null +++ b/src/com/isode/stroke/network/JavaTimerFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.network; + +import com.isode.stroke.eventloop.EventLoop; + +public class JavaTimerFactory implements TimerFactory { + + public JavaTimerFactory(EventLoop eventLoop) { + eventLoop_ = eventLoop; + } + + public Timer createTimer(int milliseconds) { + return new JavaTimer(eventLoop_, milliseconds); + } + + private final EventLoop eventLoop_; + +} diff --git a/src/com/isode/stroke/network/NetworkFactories.java b/src/com/isode/stroke/network/NetworkFactories.java new file mode 100644 index 0000000..b630b85 --- /dev/null +++ b/src/com/isode/stroke/network/NetworkFactories.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.network; + +public interface NetworkFactories { + + TimerFactory getTimerFactory(); + ConnectionFactory getConnectionFactory(); + DomainNameResolver getDomainNameResolver(); + +} diff --git a/src/com/isode/stroke/network/PlatformDomainNameResolver.java b/src/com/isode/stroke/network/PlatformDomainNameResolver.java new file mode 100644 index 0000000..50d0e11 --- /dev/null +++ b/src/com/isode/stroke/network/PlatformDomainNameResolver.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.network; + +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.eventloop.EventOwner; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; + + +public class PlatformDomainNameResolver extends DomainNameResolver { + + private class AddressQuery extends DomainNameAddressQuery implements EventOwner { + AddressQuery(String host, EventLoop eventLoop) { + hostname = host; + this.eventLoop = eventLoop; + //FIXME: port asyncDNS +// thread = null; +// safeToJoin = false; + } + + public void run() { + //FIXME: port asyncDNS + Collection<HostAddress> results = new ArrayList<HostAddress>(); + try { + results.add(new HostAddress(InetAddress.getByName(hostname))); + } catch (UnknownHostException ex) { + + } + onResult.emit(results, results.isEmpty() ? new DomainNameResolveError() : null); + +// safeToJoin = false; +// thread = new boost::thread(boost::bind(&AddressQuery::doRun, shared_from_this())); + } +// FIXME: Port async DNS. +// void doRun() { +// //std::cout << "PlatformDomainNameResolver::doRun()" << std::endl; +// boost::asio::ip::tcp::resolver resolver(ioService); +// boost::asio::ip::tcp::resolver::query query(hostname.getUTF8String(), "5222"); +// try { +// //std::cout << "PlatformDomainNameResolver::doRun(): Resolving" << std::endl; +// boost::asio::ip::tcp::resolver::iterator endpointIterator = resolver.resolve(query); +// //std::cout << "PlatformDomainNameResolver::doRun(): Resolved" << std::endl; +// if (endpointIterator == boost::asio::ip::tcp::resolver::iterator()) { +// //std::cout << "PlatformDomainNameResolver::doRun(): Error 1" << std::endl; +// emitError(); +// } +// else { +// std::vector<HostAddress> results; +// for ( ; endpointIterator != boost::asio::ip::tcp::resolver::iterator(); ++endpointIterator) { +// boost::asio::ip::address address = (*endpointIterator).endpoint().address(); +// results.push_back(address.is_v4() ? HostAddress(&address.to_v4().to_bytes()[0], 4) : HostAddress(&address.to_v6().to_bytes()[0], 16)); +// } +// +// //std::cout << "PlatformDomainNameResolver::doRun(): Success" << std::endl; +// eventLoop->postEvent( +// boost::bind(boost::ref(onResult), results, boost::optional<DomainNameResolveError>()), +// shared_from_this()); +// } +// } +// catch (...) { +// //std::cout << "PlatformDomainNameResolver::doRun(): Error 2" << std::endl; +// emitError(); +// } +// safeToJoin = true; +// } +// +// void emitError() { +// eventLoop->postEvent(boost::bind(boost::ref(onResult), std::vector<HostAddress>(), boost::optional<DomainNameResolveError>(DomainNameResolveError())), shared_from_this()); +// } +// +// boost::asio::io_service ioService; + String hostname; + EventLoop eventLoop; +// boost::thread* thread; +// bool safeToJoin; + } + + public PlatformDomainNameResolver(EventLoop eventLoop) { + this.eventLoop = eventLoop; + } + + @Override + public DomainNameServiceQuery createServiceQuery(String name) { + return new PlatformDomainNameServiceQuery(getNormalized(name), eventLoop); + } + + @Override + public DomainNameAddressQuery createAddressQuery(String name) { + return new AddressQuery(getNormalized(name), eventLoop); + } + + private final EventLoop eventLoop; +} diff --git a/src/com/isode/stroke/network/PlatformDomainNameServiceQuery.java b/src/com/isode/stroke/network/PlatformDomainNameServiceQuery.java new file mode 100644 index 0000000..5c94e4d --- /dev/null +++ b/src/com/isode/stroke/network/PlatformDomainNameServiceQuery.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.network; + +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.eventloop.EventOwner; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Hashtable; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + + +public class PlatformDomainNameServiceQuery extends DomainNameServiceQuery implements EventOwner { + + public PlatformDomainNameServiceQuery(String service, EventLoop eventLoop) { + this.service = service; + this.eventLoop = eventLoop; + } + + @Override + public void run() { + //TODO: Make async + Collection<Result> results = new ArrayList<Result>(); + Hashtable env = new Hashtable(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + env.put("java.naming.provider.url", "dns:"); + try { + DirContext ctx = new InitialDirContext(env); + Attributes attrs = ctx.getAttributes(this.service, new String[]{"SRV"}); + Attribute attribute = attrs.get("SRV"); + for (int i = 0; attribute != null && i < attribute.size(); i++) { + /* SRV results are going to be returned in the space-separated format + * Priority Weight Port Target + * (See RFC2782) + */ + String[] srvParts = ((String) attribute.get(i)).split(" "); + String host = srvParts[3]; + if (host.endsWith(".")) { + host = host.substring(0, host.length() - 1); + } + Result result = new Result(host, Integer.parseInt(srvParts[2]), Integer.parseInt(srvParts[0]), Integer.parseInt(srvParts[1])); + results.add(result); + } + } catch (NamingException ex) { + /* Turns out that you get the exception just for not finding a result, so we want to fall through to A lookups and ignore.*/ + } + + onResult.emit(results); + } + + +// void PlatformDomainNameServiceQuery::doRun() { +// std::vector<DomainNameServiceQuery::Result> records; +// +//#if defined(SWIFTEN_PLATFORM_WINDOWS) +// DNS_RECORD* responses; +// // FIXME: This conversion doesn't work if unicode is deffed above +// if (DnsQuery(service.getUTF8Data(), DNS_TYPE_SRV, DNS_QUERY_STANDARD, NULL, &responses, NULL) != ERROR_SUCCESS) { +// emitError(); +// return; +// } +// +// DNS_RECORD* currentEntry = responses; +// while (currentEntry) { +// if (currentEntry->wType == DNS_TYPE_SRV) { +// DomainNameServiceQuery::Result record; +// record.priority = currentEntry->Data.SRV.wPriority; +// record.weight = currentEntry->Data.SRV.wWeight; +// record.port = currentEntry->Data.SRV.wPort; +// +// // The pNameTarget is actually a PCWSTR, so I would have expected this +// // conversion to not work at all, but it does. +// // Actually, it doesn't. Fix this and remove explicit cast +// // Remove unicode undef above as well +// record.hostname = String((const char*) currentEntry->Data.SRV.pNameTarget); +// records.push_back(record); +// } +// currentEntry = currentEntry->pNext; +// } +// DnsRecordListFree(responses, DnsFreeRecordList); +// +//#else +// // Make sure we reinitialize the domain list every time +// res_init(); +// +// //std::cout << "SRV: Querying " << service << std::endl; +// ByteArray response; +// response.resize(NS_PACKETSZ); +// int responseLength = res_query(const_cast<char*>(service.getUTF8Data()), ns_c_in, ns_t_srv, reinterpret_cast<u_char*>(response.getData()), response.getSize()); +// if (responseLength == -1) { +// emitError(); +// return; +// } +// +// // Parse header +// HEADER* header = reinterpret_cast<HEADER*>(response.getData()); +// unsigned char* messageStart = reinterpret_cast<unsigned char*>(response.getData()); +// unsigned char* messageEnd = messageStart + responseLength; +// unsigned char* currentEntry = messageStart + NS_HFIXEDSZ; +// +// // Skip over the queries +// int queriesCount = ntohs(header->qdcount); +// while (queriesCount > 0) { +// int entryLength = dn_skipname(currentEntry, messageEnd); +// if (entryLength < 0) { +// emitError(); +// return; +// } +// currentEntry += entryLength + NS_QFIXEDSZ; +// queriesCount--; +// } +// +// // Process the SRV answers +// int answersCount = ntohs(header->ancount); +// while (answersCount > 0) { +// DomainNameServiceQuery::Result record; +// +// int entryLength = dn_skipname(currentEntry, messageEnd); +// currentEntry += entryLength; +// currentEntry += NS_RRFIXEDSZ; +// +// // Priority +// if (currentEntry + 2 >= messageEnd) { +// emitError(); +// return; +// } +// record.priority = ns_get16(currentEntry); +// currentEntry += 2; +// +// // Weight +// if (currentEntry + 2 >= messageEnd) { +// emitError(); +// return; +// } +// record.weight = ns_get16(currentEntry); +// currentEntry += 2; +// +// // Port +// if (currentEntry + 2 >= messageEnd) { +// emitError(); +// return; +// } +// record.port = ns_get16(currentEntry); +// currentEntry += 2; +// +// // Hostname +// if (currentEntry >= messageEnd) { +// emitError(); +// return; +// } +// ByteArray entry; +// entry.resize(NS_MAXDNAME); +// entryLength = dn_expand(messageStart, messageEnd, currentEntry, entry.getData(), entry.getSize()); +// if (entryLength < 0) { +// emitError(); +// return; +// } +// record.hostname = String(entry.getData()); +// records.push_back(record); +// currentEntry += entryLength; +// answersCount--; +// } +//#endif +// +// safeToJoin = true; +// std::sort(records.begin(), records.end(), ResultPriorityComparator()); +// //std::cout << "Sending out " << records.size() << " SRV results " << std::endl; +// eventLoop->postEvent(boost::bind(boost::ref(onResult), records)); +//} + + + private final String service; + private final EventLoop eventLoop; +} diff --git a/src/com/isode/stroke/network/Timer.java b/src/com/isode/stroke/network/Timer.java new file mode 100644 index 0000000..5dc3854 --- /dev/null +++ b/src/com/isode/stroke/network/Timer.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.network; + +import com.isode.stroke.signals.Signal; + +public abstract class Timer { + public abstract void start(); + public abstract void stop(); + public final Signal onTick = new Signal(); +} diff --git a/src/com/isode/stroke/network/TimerFactory.java b/src/com/isode/stroke/network/TimerFactory.java new file mode 100644 index 0000000..2732aaf --- /dev/null +++ b/src/com/isode/stroke/network/TimerFactory.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.network; + + +public interface TimerFactory { + Timer createTimer(int milliseconds); +} diff --git a/src/com/isode/stroke/parser/AttributeMap.java b/src/com/isode/stroke/parser/AttributeMap.java new file mode 100644 index 0000000..dc948ce --- /dev/null +++ b/src/com/isode/stroke/parser/AttributeMap.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import java.util.HashMap; + +/** + * XML element attributes. + */ +public class AttributeMap extends HashMap<String, String> { + public String getAttribute(String attribute) { + return this.get(attribute); + } + + public String getAttributeValue(String attribute) { + return this.containsKey(attribute) ? this.get(attribute) : null; + } + + public boolean getBoolAttribute(String attribute) { + return getBoolAttribute(attribute, false); + } + + public boolean getBoolAttribute(String attribute, boolean defaultValue) { + String value = getAttribute(attribute); + return "true".equals(value) || "1".equals(value); + } +} diff --git a/src/com/isode/stroke/parser/AuthChallengeParser.java b/src/com/isode/stroke/parser/AuthChallengeParser.java new file mode 100644 index 0000000..20238dd --- /dev/null +++ b/src/com/isode/stroke/parser/AuthChallengeParser.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.AuthChallenge; +import com.isode.stroke.stringcodecs.Base64; + +class AuthChallengeParser extends GenericElementParser<AuthChallenge> { + + public AuthChallengeParser() { + super(AuthChallenge.class); + } + + @Override + public void handleStartElement(String unused1, String unused2, AttributeMap unused3) { + ++depth; + } + + @Override + public void handleEndElement(String unused1, String unused2) { + --depth; + if (depth == 0) { + getElementGeneric().setValue(Base64.decode(text)); + } + } + + @Override + public void handleCharacterData(String text) { + this.text += text; + } + private int depth = 0; + private String text = ""; +} diff --git a/src/com/isode/stroke/parser/AuthFailureParser.java b/src/com/isode/stroke/parser/AuthFailureParser.java new file mode 100644 index 0000000..c42abed --- /dev/null +++ b/src/com/isode/stroke/parser/AuthFailureParser.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.AuthFailure; + +class AuthFailureParser extends GenericElementParser<AuthFailure> { + + public AuthFailureParser() { + super(AuthFailure.class); + } + +} diff --git a/src/com/isode/stroke/parser/AuthRequestParser.java b/src/com/isode/stroke/parser/AuthRequestParser.java new file mode 100644 index 0000000..7f40c9c --- /dev/null +++ b/src/com/isode/stroke/parser/AuthRequestParser.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.AuthRequest; +import com.isode.stroke.stringcodecs.Base64; + +class AuthRequestParser extends GenericElementParser<AuthRequest> { + + public AuthRequestParser() { + super(AuthRequest.class); + } + + @Override + public void handleStartElement(String a, String b, AttributeMap attribute) { + if (depth_ == 0) { + getElementGeneric().setMechanism(attribute.getAttribute("mechanism")); + } + ++depth_; + } + + @Override + public void handleEndElement(String a, String b) { + --depth_; + if (depth_ == 0) { + getElementGeneric().setMessage(Base64.decode(text_)); + } + } + + @Override + public void handleCharacterData(String a) { + text_ += a; + } + String text_ = ""; + int depth_ = 0; +} diff --git a/src/com/isode/stroke/parser/AuthResponseParser.java b/src/com/isode/stroke/parser/AuthResponseParser.java new file mode 100644 index 0000000..a93249a --- /dev/null +++ b/src/com/isode/stroke/parser/AuthResponseParser.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.AuthResponse; +import com.isode.stroke.stringcodecs.Base64; + +class AuthResponseParser extends GenericElementParser<AuthResponse> { + + public AuthResponseParser() { + super(AuthResponse.class); + } + + @Override + public void handleStartElement(String unused1, String unused2, AttributeMap unused3) { + ++depth; + } + + @Override + public void handleEndElement(String unused1, String unused2) { + --depth; + if (depth == 0) { + getElementGeneric().setValue(Base64.decode(text)); + } + } + + @Override + public void handleCharacterData(String text) { + this.text += text; + } + private int depth = 0; + private String text = ""; +} diff --git a/src/com/isode/stroke/parser/AuthSuccessParser.java b/src/com/isode/stroke/parser/AuthSuccessParser.java new file mode 100644 index 0000000..111d3ce --- /dev/null +++ b/src/com/isode/stroke/parser/AuthSuccessParser.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.AuthSuccess; +import com.isode.stroke.stringcodecs.Base64; + +class AuthSuccessParser extends GenericElementParser<AuthSuccess> { + + public AuthSuccessParser() { + super(AuthSuccess.class); + } + + @Override + public void handleStartElement(String a, String b, AttributeMap attribute) { + ++depth_; + } + + @Override + public void handleEndElement(String a, String b) { + --depth_; + if (depth_ == 0) { + getElementGeneric().setValue(Base64.decode(text_)); + } + } + + @Override + public void handleCharacterData(String a) { + text_ += a; + } + String text_ = ""; + int depth_ = 0; +} diff --git a/src/com/isode/stroke/parser/CompressFailureParser.java b/src/com/isode/stroke/parser/CompressFailureParser.java new file mode 100644 index 0000000..cbfa413 --- /dev/null +++ b/src/com/isode/stroke/parser/CompressFailureParser.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.CompressFailure; + +class CompressFailureParser extends GenericElementParser<CompressFailure> { + + public CompressFailureParser() { + super(CompressFailure.class); + } + +} diff --git a/src/com/isode/stroke/parser/CompressParser.java b/src/com/isode/stroke/parser/CompressParser.java new file mode 100644 index 0000000..133b20b --- /dev/null +++ b/src/com/isode/stroke/parser/CompressParser.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.CompressRequest; + +class CompressParser extends GenericElementParser<CompressRequest> { + + private int currentDepth_ = 0; + private String currentText_; + private boolean inMethod_; + + public CompressParser() { + super(CompressRequest.class); + } + + @Override + public void handleStartElement(String element, String ns, AttributeMap attributes) { + if (currentDepth_ == 1 && element.equals("method")) { + inMethod_ = true; + currentText_ = ""; + } + ++currentDepth_; + } + + @Override + public void handleEndElement(String el, String ns) { + --currentDepth_; + if (currentDepth_ == 1 && inMethod_) { + getElementGeneric().setMethod(currentText_); + inMethod_ = false; + } + } + + @Override + public void handleCharacterData(String data) { + currentText_ += data; + } +} diff --git a/src/com/isode/stroke/parser/CompressedParser.java b/src/com/isode/stroke/parser/CompressedParser.java new file mode 100644 index 0000000..77d17e4 --- /dev/null +++ b/src/com/isode/stroke/parser/CompressedParser.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Compressed; + +class CompressedParser extends GenericElementParser<Compressed> { + + public CompressedParser() { + super(Compressed.class); + } + +} diff --git a/src/com/isode/stroke/parser/ElementParser.java b/src/com/isode/stroke/parser/ElementParser.java new file mode 100644 index 0000000..eebb51a --- /dev/null +++ b/src/com/isode/stroke/parser/ElementParser.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Element; + +/** + * Parse XML Elements. + */ +public interface ElementParser { + + void handleStartElement(String element, String ns, AttributeMap attributes); + + void handleEndElement(String element, String ns); + + void handleCharacterData(String data); + + Element getElement(); +} diff --git a/src/com/isode/stroke/parser/EnableStreamManagementParser.java b/src/com/isode/stroke/parser/EnableStreamManagementParser.java new file mode 100644 index 0000000..fe657dd --- /dev/null +++ b/src/com/isode/stroke/parser/EnableStreamManagementParser.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.EnableStreamManagement; + +class EnableStreamManagementParser extends GenericElementParser<EnableStreamManagement> { + + public EnableStreamManagementParser() { + super(EnableStreamManagement.class); + } + +} diff --git a/src/com/isode/stroke/parser/GenericElementParser.java b/src/com/isode/stroke/parser/GenericElementParser.java new file mode 100644 index 0000000..da961f6 --- /dev/null +++ b/src/com/isode/stroke/parser/GenericElementParser.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Element; + +public class GenericElementParser<T extends Element> implements ElementParser { + + private final T element_; + + public GenericElementParser(Class c) { + try { + element_ = (T) c.newInstance(); + } catch (InstantiationException ex) { + /* Fatal */ + throw new RuntimeException(ex); + } catch (IllegalAccessException ex) { + /* Fatal */ + throw new RuntimeException(ex); + } + } + + + public Element getElement() { + return element_; + } + + protected T getElementGeneric() { + return element_; + } + + public void handleStartElement(String a, String b, AttributeMap c) { + } + + public void handleEndElement(String a, String b) { + } + + public void handleCharacterData(String a) { + } +} diff --git a/src/com/isode/stroke/parser/GenericPayloadParser.java b/src/com/isode/stroke/parser/GenericPayloadParser.java new file mode 100644 index 0000000..7ac8337 --- /dev/null +++ b/src/com/isode/stroke/parser/GenericPayloadParser.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Payload; + +public abstract class GenericPayloadParser <T extends Payload> implements PayloadParser { + private T payload_; + + public GenericPayloadParser(T payload) { + payload_ = payload; + } + + public Payload getPayload() { + return payload_; + } + + protected T getPayloadInternal() { + return payload_; + } +} diff --git a/src/com/isode/stroke/parser/GenericPayloadParserFactory.java b/src/com/isode/stroke/parser/GenericPayloadParserFactory.java new file mode 100644 index 0000000..a6d7e25 --- /dev/null +++ b/src/com/isode/stroke/parser/GenericPayloadParserFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +public class GenericPayloadParserFactory<T extends PayloadParser> implements PayloadParserFactory { + + private final String tag_; + private final String xmlns_; + private final Class payloadClass_; + + public GenericPayloadParserFactory(final String tag, final Class<? extends PayloadParser> payloadClass) { + this(tag, "", payloadClass); + } + + public GenericPayloadParserFactory(final String tag, final String xmlns, final Class<? extends PayloadParser> payloadClass) { + tag_ = tag; + xmlns_ = xmlns; + payloadClass_ = payloadClass; + } + + public boolean canParse(final String element, final String ns, final AttributeMap attributes) { + return element.equals(tag_) && xmlns_.equals(ns); + } + + public final PayloadParser createPayloadParser() { + try { + return (PayloadParser) payloadClass_.newInstance(); + } catch (InstantiationException ex) { + /* Fatal */ + throw new RuntimeException(ex); + } catch (IllegalAccessException ex) { + /* Fatal */ + throw new RuntimeException(ex); + } + } +} diff --git a/src/com/isode/stroke/parser/GenericStanzaParser.java b/src/com/isode/stroke/parser/GenericStanzaParser.java new file mode 100644 index 0000000..0f3c6c1 --- /dev/null +++ b/src/com/isode/stroke/parser/GenericStanzaParser.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.Stanza; + +public class GenericStanzaParser<T extends Stanza> extends StanzaParser { + private final T stanza_; + public GenericStanzaParser(PayloadParserFactoryCollection collection, T blankStanza) { + super(collection); + stanza_ = blankStanza; + } + + public Element getElement() { + return stanza_; + } + + public T getStanzaGeneric() { + return stanza_; + } +} diff --git a/src/com/isode/stroke/parser/IQParser.java b/src/com/isode/stroke/parser/IQParser.java new file mode 100644 index 0000000..3e8325a --- /dev/null +++ b/src/com/isode/stroke/parser/IQParser.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.IQ; + +public class IQParser extends GenericStanzaParser<IQ> { + + public IQParser(PayloadParserFactoryCollection factories) { + super(factories, new IQ()); + } + + @Override + void handleStanzaAttributes(AttributeMap attributes) { + String type = attributes.getAttribute("type"); + if ("set".equals(type)) { + getStanzaGeneric().setType(IQ.Type.Set); + } else if ("get".equals(type)) { + getStanzaGeneric().setType(IQ.Type.Get); + } else if ("result".equals(type)) { + getStanzaGeneric().setType(IQ.Type.Result); + } else if ("error".equals(type)) { + getStanzaGeneric().setType(IQ.Type.Error); + } else { + getStanzaGeneric().setType(IQ.Type.Get); + } + } +} diff --git a/src/com/isode/stroke/parser/MessageParser.java b/src/com/isode/stroke/parser/MessageParser.java new file mode 100644 index 0000000..1294e62 --- /dev/null +++ b/src/com/isode/stroke/parser/MessageParser.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Message; + +public class MessageParser extends GenericStanzaParser<Message> { + + public MessageParser(PayloadParserFactoryCollection factories) { + super(factories, new Message()); + } + + @Override + void handleStanzaAttributes(AttributeMap attributes) { + String type = attributes.getAttribute("type"); + if ("chat".equals(type)) { + getStanzaGeneric().setType(Message.Type.Chat); + } else if ("error".equals(type)) { + getStanzaGeneric().setType(Message.Type.Error); + } else if ("groupchat".equals(type)) { + getStanzaGeneric().setType(Message.Type.Groupchat); + } else if ("headline".equals(type)) { + getStanzaGeneric().setType(Message.Type.Headline); + } else { + getStanzaGeneric().setType(Message.Type.Normal); + } + } +} diff --git a/src/com/isode/stroke/parser/PayloadParser.java b/src/com/isode/stroke/parser/PayloadParser.java new file mode 100644 index 0000000..c0dfb43 --- /dev/null +++ b/src/com/isode/stroke/parser/PayloadParser.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Payload; + +/** + * Parse a payload. + */ +public interface PayloadParser { + + void handleStartElement(String element, String ns, AttributeMap attributes); + + void handleEndElement(String element, String ns); + + void handleCharacterData(String data); + + Payload getPayload(); +} diff --git a/src/com/isode/stroke/parser/PayloadParserFactory.java b/src/com/isode/stroke/parser/PayloadParserFactory.java new file mode 100644 index 0000000..3b07395 --- /dev/null +++ b/src/com/isode/stroke/parser/PayloadParserFactory.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +/** + * Create a parser. + */ +public interface PayloadParserFactory { + boolean canParse(String element, String ns, AttributeMap attributes); + PayloadParser createPayloadParser(); +} diff --git a/src/com/isode/stroke/parser/PayloadParserFactoryCollection.java b/src/com/isode/stroke/parser/PayloadParserFactoryCollection.java new file mode 100644 index 0000000..2d3601b --- /dev/null +++ b/src/com/isode/stroke/parser/PayloadParserFactoryCollection.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import java.util.Vector; + +/** + * Collection of parser factories. + */ +public class PayloadParserFactoryCollection { + + private final Vector<PayloadParserFactory> factories_ = new Vector<PayloadParserFactory>(); + private PayloadParserFactory defaultFactory_ = null; + + public void addFactory(PayloadParserFactory factory) { + synchronized (factories_) { + factories_.add(factory); + } + } + + public void setDefaultFactory(PayloadParserFactory factory) { + defaultFactory_ = factory; + } + + public PayloadParserFactory getPayloadParserFactory(String element, String ns, AttributeMap attributes) { + synchronized(factories_) { + for (PayloadParserFactory factory : factories_) { + if (factory.canParse(element, ns, attributes)) { + return factory; + } + } + } + return defaultFactory_; + } +} diff --git a/src/com/isode/stroke/parser/PlatformXMLParserFactory.java b/src/com/isode/stroke/parser/PlatformXMLParserFactory.java new file mode 100644 index 0000000..85c9ae9 --- /dev/null +++ b/src/com/isode/stroke/parser/PlatformXMLParserFactory.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.eventloop.EventLoop; + +class PlatformXMLParserFactory { + /** + * Unlike Swiften, this may be threaded, and therefore needs an eventloop. + */ + public static XMLParser createXMLParser(XMLParserClient client, EventLoop eventLoop) { + return new PullXMLParser(client, eventLoop); + } +} diff --git a/src/com/isode/stroke/parser/PresenceParser.java b/src/com/isode/stroke/parser/PresenceParser.java new file mode 100644 index 0000000..cca8729 --- /dev/null +++ b/src/com/isode/stroke/parser/PresenceParser.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Presence; + +public class PresenceParser extends GenericStanzaParser<Presence> { + + public PresenceParser(PayloadParserFactoryCollection factories) { + super(factories, new Presence()); + } + + @Override + void handleStanzaAttributes(AttributeMap attributes) { + String type = attributes.getAttribute("type"); + if (type != null) { + if (type.equals("unavailable")) { + getStanzaGeneric().setType(Presence.Type.Unavailable); + } else if (type.equals("probe")) { + getStanzaGeneric().setType(Presence.Type.Probe); + } else if (type.equals("subscribe")) { + getStanzaGeneric().setType(Presence.Type.Subscribe); + } else if (type.equals("subscribed")) { + getStanzaGeneric().setType(Presence.Type.Subscribed); + } else if (type.equals("unsubscribe")) { + getStanzaGeneric().setType(Presence.Type.Unsubscribe); + } else if (type.equals("unsubscribed")) { + getStanzaGeneric().setType(Presence.Type.Unsubscribed); + } else if (type.equals("error")) { + getStanzaGeneric().setType(Presence.Type.Error); + } else { + getStanzaGeneric().setType(Presence.Type.Available); + } + } else { + getStanzaGeneric().setType(Presence.Type.Available); + } + } +} diff --git a/src/com/isode/stroke/parser/PullXMLParser.java b/src/com/isode/stroke/parser/PullXMLParser.java new file mode 100644 index 0000000..8b42afe --- /dev/null +++ b/src/com/isode/stroke/parser/PullXMLParser.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.eventloop.Event.Callback; +import com.isode.stroke.eventloop.EventLoop; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Parser based around the XmlPullParser + */ +class PullXMLParser extends XMLParser { + + private final Logger logger_ = Logger.getLogger(this.getClass().getName()); + private final XmlPullParser parser_ = new MXParser(); + private final PipedInputStream reader_; + private final PipedOutputStream writer_; + private final ArrayBlockingQueue<Event> events_ = new ArrayBlockingQueue<Event>(20); + private final Thread parserThread_; + private boolean error_ = false; + private final EventLoop eventLoop_; + + + private enum EventType {Start, End, Text}; + + /** + * XML Event struct. + */ + private class Event { + private final EventType type; + private final String name; + private final String namespace; + private final AttributeMap attributes; + public Event(EventType type, String name, String namespace, AttributeMap attributes) { + this.type = type; + this.name = name; + this.namespace = namespace; + this.attributes = attributes; + } + + public Event(String name) { + this(EventType.Text, name, null, null); + } + + + public Event(String name, String namespace) { + this(EventType.End, name, namespace, null); + } + + public Event(String name, String namespace, AttributeMap attributes) { + this(EventType.Start, name, namespace, attributes); + } + + } + + /** + * Put an XML event onto the queue ready for the main thread to pick up later. + */ + private void addEvent(Event event) throws InterruptedException { + events_.put(event); + } + + /** + * Deal with whatever was just read out of the parser_. + */ + private void handleEvent(int eventType) throws XmlPullParserException, InterruptedException { + if (eventType == XmlPullParser.START_TAG) { + AttributeMap map = new AttributeMap(); + for (int i = 0; i < parser_.getAttributeCount(); i++) { + map.put(parser_.getAttributeName(i), parser_.getAttributeValue(i)); + } + addEvent(new Event(parser_.getName(), parser_.getNamespace(), map)); + bump(); + } else if (eventType == XmlPullParser.END_TAG) { + addEvent(new Event(parser_.getName(), parser_.getNamespace())); + bump(); + } else if (eventType == XmlPullParser.TEXT) { + StringBuilder text = new StringBuilder(); + int holderForStartAndLength[] = new int[2]; + char ch[] = parser_.getTextCharacters(holderForStartAndLength); + int start = holderForStartAndLength[0]; + int length = holderForStartAndLength[1]; + for (int i = start; i < start + length; i++) { + text.append(ch[i]); + } + addEvent(new Event(text.toString())); + bump(); + } else if (eventType == XmlPullParser.START_DOCUMENT) { + //System.out.println("Starting document"); + } else if (eventType == XmlPullParser.END_DOCUMENT) { + //System.out.println("Ending document"); + + } else { + //System.out.println("Unhandled event"); + } + } + + /** + * Cause the main thread to process any outstanding events. + */ + private void bump() { + eventLoop_.postEvent(new Callback() { + public void run() { + processEvents(); + } + }); + } + + public PullXMLParser(XMLParserClient client, EventLoop eventLoop) { + super(client); + eventLoop_ = eventLoop; + writer_ = new PipedOutputStream(); + try { + reader_ = new PipedInputStream(writer_, 128000); + } catch (IOException ex) { + Logger.getLogger(PullXMLParser.class.getName()).log(Level.SEVERE, null, ex); + throw new IllegalStateException(ex); + } + try { + parser_.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser_.setInput(reader_, "UTF-8"); + } catch (XmlPullParserException ex) { + Logger.getLogger(PullXMLParser.class.getName()).log(Level.SEVERE, null, ex); + throw new IllegalStateException(ex); + } + Runnable parserRunnable = new Runnable() { + public void run() { + int eventType = XmlPullParser.END_DOCUMENT - 1; /* Anything to make the following not true*/ + while (eventType != XmlPullParser.END_DOCUMENT) { + try { + parser_.next(); + eventType = parser_.getEventType(); + handleEvent(eventType); + } catch (XmlPullParserException ex) { + error_ = true; + Logger.getLogger(PullXMLParser.class.getName()).log(Level.SEVERE, null, ex); + break; + } catch (IOException ex) { + error_ = true; + Logger.getLogger(PullXMLParser.class.getName()).log(Level.SEVERE, null, ex); + break; + } catch (InterruptedException ex) { + /* The thread was interrupted while trying to process an event - presumably this is because we're shutting down.*/ + error_ = true; + Logger.getLogger(PullXMLParser.class.getName()).log(Level.SEVERE, null, ex); + break; + } + } + } + }; + parserThread_ = new Thread(parserRunnable); + parserThread_.setDaemon(true); + parserThread_.start(); + } + + /** + * Do not do this! + * This is only to allow the unit tests to join onto it. + * @return + */ + Thread getParserThread() { + return parserThread_; + } + + /** + * Process outstanding events. + * Call in the main thread only. + */ + private void processEvents() { + while (events_.size() > 0) { + processEvent(events_.poll()); + } + } + + /** + * Main thread only. + */ + private void processEvent(Event event) { + String name = event.name; + String namespace = event.namespace; + AttributeMap attributes = event.attributes; + switch (event.type) { + case Start: getClient().handleStartElement(name, namespace, attributes); break; + case End: getClient().handleEndElement(name, namespace); break; + case Text: getClient().handleCharacterData(name); break; + } + } + + /** + * Cause the parser thread to parse these data later. + * Note that the return code is a best guess based on previous parsing, + * and will almost always give a false negative on a stanza before a + * true negative. True negatives will always mean an error in the stream. + */ + @Override + public boolean parse(String data) { + try { + writer_.write(new ByteArray(data).getData()); + writer_.flush(); + } catch (IOException ex) { + error_ = true; + Logger.getLogger(PullXMLParser.class.getName()).log(Level.SEVERE, null, ex); + } + return !error_; + } + + +} diff --git a/src/com/isode/stroke/parser/SerializingParser.java b/src/com/isode/stroke/parser/SerializingParser.java new file mode 100644 index 0000000..1346dc8 --- /dev/null +++ b/src/com/isode/stroke/parser/SerializingParser.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.serializer.xml.XMLElement; +import com.isode.stroke.serializer.xml.XMLTextNode; +import java.util.Vector; + +public class SerializingParser { + + private final Vector<XMLElement> elementStack_ = new Vector<XMLElement>(); + private XMLElement rootElement_; + + public void handleStartElement(String tag, String ns, AttributeMap attributes) { + XMLElement element = new XMLElement(tag, ns); + for (String name : attributes.keySet()) { + element.setAttribute(name, attributes.get(name)); + } + + if (elementStack_.isEmpty()) { + rootElement_ = element; + } else { + elementStack_.lastElement().addNode(element); + } + elementStack_.add(element); + } + + public void handleEndElement(String tag, String ns) { + assert (!elementStack_.isEmpty()); + elementStack_.remove(elementStack_.size() - 1); + } + + public void handleCharacterData(String data) { + if (!elementStack_.isEmpty()) { + elementStack_.lastElement().addNode(new XMLTextNode(data)); + } + } + + public String getResult() { + return (rootElement_ != null ? rootElement_.serialize() : ""); + } +} diff --git a/src/com/isode/stroke/parser/StanzaAckParser.java b/src/com/isode/stroke/parser/StanzaAckParser.java new file mode 100644 index 0000000..40fab80 --- /dev/null +++ b/src/com/isode/stroke/parser/StanzaAckParser.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StanzaAck; + +class StanzaAckParser extends GenericElementParser<StanzaAck> { + + private int depth = 0; + + public StanzaAckParser() { + super(StanzaAck.class); + } + + @Override + public void handleStartElement(String el, String ns, AttributeMap attributes) { + if (depth == 0) { + String handledStanzasString = attributes.getAttribute("h"); + try { + getElementGeneric().setHandledStanzasCount(Long.parseLong(handledStanzasString)); + } catch (NumberFormatException e) { + + } + } + ++depth; + } + + @Override + public void handleEndElement(String el, String ns) { + --depth; + } +} diff --git a/src/com/isode/stroke/parser/StanzaAckRequestParser.java b/src/com/isode/stroke/parser/StanzaAckRequestParser.java new file mode 100644 index 0000000..9a61d7f --- /dev/null +++ b/src/com/isode/stroke/parser/StanzaAckRequestParser.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StanzaAckRequest; + +class StanzaAckRequestParser extends GenericElementParser<StanzaAckRequest> { + + public StanzaAckRequestParser() { + super(StanzaAckRequest.class); + } + +} diff --git a/src/com/isode/stroke/parser/StanzaParser.java b/src/com/isode/stroke/parser/StanzaParser.java new file mode 100644 index 0000000..2aa865e --- /dev/null +++ b/src/com/isode/stroke/parser/StanzaParser.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.Payload; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.jid.JID; + +public abstract class StanzaParser implements ElementParser { + + protected int currentDepth_ = 0; + protected final PayloadParserFactoryCollection factories_; + protected PayloadParser currentPayloadParser_; + + public StanzaParser(PayloadParserFactoryCollection factories) { + factories_ = factories; + } + + void handleStanzaAttributes(AttributeMap map) { + } + + Stanza getStanza() { + return (Stanza) getElement(); + } + + private boolean inPayload() { + return currentDepth_ > 1; + } + + private boolean inStanza() { + return currentDepth_ > 0; + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + if (inStanza()) { + if (!inPayload()) { + assert currentPayloadParser_ == null; + PayloadParserFactory payloadParserFactory = factories_.getPayloadParserFactory(element, ns, attributes); + if (payloadParserFactory != null) { + currentPayloadParser_ = payloadParserFactory.createPayloadParser(); + } else { + currentPayloadParser_ = new UnknownPayloadParser(); + } + } + assert currentPayloadParser_ != null; + currentPayloadParser_.handleStartElement(element, ns, attributes); + } else { + String from = attributes.getAttribute("from"); + if (from != null) { + getStanza().setFrom(JID.fromString(from)); + } + String to = attributes.getAttribute("to"); + if (to != null) { + getStanza().setTo(JID.fromString(to)); + } + String id = attributes.getAttribute("id"); + if (id != null) { + getStanza().setID(id); + } + handleStanzaAttributes(attributes); + } + ++currentDepth_; + } + + public void handleEndElement(String element, String ns) { + assert (inStanza()); + if (inPayload()) { + assert currentPayloadParser_ != null; + currentPayloadParser_.handleEndElement(element, ns); + --currentDepth_; + if (!inPayload()) { + Payload payload = currentPayloadParser_.getPayload(); + if (payload != null) { + getStanza().addPayload(payload); + } + currentPayloadParser_ = null; + } + } else { + --currentDepth_; + } + } + + public void handleCharacterData(String data) { + if (currentPayloadParser_ != null) { + currentPayloadParser_.handleCharacterData(data); + } + } +} diff --git a/src/com/isode/stroke/parser/StartTLSFailureParser.java b/src/com/isode/stroke/parser/StartTLSFailureParser.java new file mode 100644 index 0000000..e1a858c --- /dev/null +++ b/src/com/isode/stroke/parser/StartTLSFailureParser.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StartTLSFailure; + +class StartTLSFailureParser extends GenericElementParser<StartTLSFailure>{ + + public StartTLSFailureParser() { + super(StartTLSFailure.class); + } + +} diff --git a/src/com/isode/stroke/parser/StartTLSParser.java b/src/com/isode/stroke/parser/StartTLSParser.java new file mode 100644 index 0000000..c2f3e5d --- /dev/null +++ b/src/com/isode/stroke/parser/StartTLSParser.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StartTLSRequest; + +class StartTLSParser extends GenericElementParser<StartTLSRequest> { + + public StartTLSParser() { + super(StartTLSRequest.class); + } + +} diff --git a/src/com/isode/stroke/parser/StreamFeaturesParser.java b/src/com/isode/stroke/parser/StreamFeaturesParser.java new file mode 100644 index 0000000..7181659 --- /dev/null +++ b/src/com/isode/stroke/parser/StreamFeaturesParser.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StreamFeatures; + +class StreamFeaturesParser extends GenericElementParser<StreamFeatures> { + + public StreamFeaturesParser() { + super(StreamFeatures.class); + } + + @Override + public void handleStartElement(String element, String ns, AttributeMap unused) { + if (currentDepth_ == 1) { + if (element.equals("starttls") && ns.equals("urn:ietf:params:xml:ns:xmpp-tls")) { + getElementGeneric().setHasStartTLS(); + } else if (element.equals("session") && ns.equals("urn:ietf:params:xml:ns:xmpp-session")) { + getElementGeneric().setHasSession(); + } else if (element.equals("bind") && ns.equals("urn:ietf:params:xml:ns:xmpp-bind")) { + getElementGeneric().setHasResourceBind(); + } else if (element.equals("sm") && ns.equals("urn:xmpp:sm:2")) { + getElementGeneric().setHasStreamManagement(); + } else if (element.equals("mechanisms") && ns.equals("urn:ietf:params:xml:ns:xmpp-sasl")) { + inMechanisms_ = true; + } else if (element.equals("compression") && ns.equals("http://jabber.org/features/compress")) { + inCompression_ = true; + } + } else if (currentDepth_ == 2) { + if (inCompression_ && element.equals("method")) { + inCompressionMethod_ = true; + currentText_ = ""; + } else if (inMechanisms_ && element.equals("mechanism")) { + inMechanism_ = true; + currentText_ = ""; + } + } + ++currentDepth_; + } + + @Override + public void handleEndElement(String unused1, String unused2) { + --currentDepth_; + if (currentDepth_ == 1) { + inCompression_ = false; + inMechanisms_ = false; + } else if (currentDepth_ == 2) { + if (inCompressionMethod_) { + getElementGeneric().addCompressionMethod(currentText_); + inCompressionMethod_ = false; + } else if (inMechanism_) { + getElementGeneric().addAuthenticationMechanism(currentText_); + inMechanism_ = false; + } + } + } + + @Override + public void handleCharacterData(String data) { + currentText_ = currentText_ + data; + } + private int currentDepth_ = 0; + private String currentText_ = ""; + private boolean inMechanisms_ = false; + private boolean inMechanism_ = false; + private boolean inCompression_ = false; + private boolean inCompressionMethod_ = false; +} diff --git a/src/com/isode/stroke/parser/StreamManagementEnabledParser.java b/src/com/isode/stroke/parser/StreamManagementEnabledParser.java new file mode 100644 index 0000000..a2d12b0 --- /dev/null +++ b/src/com/isode/stroke/parser/StreamManagementEnabledParser.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2011 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StreamManagementEnabled; + +class StreamManagementEnabledParser extends GenericElementParser<StreamManagementEnabled> { + + private int level = 0; + private final static int TopLevel = 0; + + public StreamManagementEnabledParser() { + super(StreamManagementEnabled.class); + } + + @Override + public void handleStartElement(String el, String ns, AttributeMap attributes) { + if (level == TopLevel) { + if (attributes.getBoolAttribute("resume", false)) { + getElementGeneric().setResumeSupported(); + } + getElementGeneric().setResumeID(attributes.getAttribute("id")); + } + ++level; + } + + @Override + public void handleEndElement(String el, String ns) { + --level; + } +} diff --git a/src/com/isode/stroke/parser/StreamManagementFailedParser.java b/src/com/isode/stroke/parser/StreamManagementFailedParser.java new file mode 100644 index 0000000..f8c63e7 --- /dev/null +++ b/src/com/isode/stroke/parser/StreamManagementFailedParser.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StreamManagementFailed; + +class StreamManagementFailedParser extends GenericElementParser<StreamManagementFailed> { + + public StreamManagementFailedParser() { + super(StreamManagementFailed.class); + } + +} diff --git a/src/com/isode/stroke/parser/StreamResumeParser.java b/src/com/isode/stroke/parser/StreamResumeParser.java new file mode 100644 index 0000000..924e865 --- /dev/null +++ b/src/com/isode/stroke/parser/StreamResumeParser.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2011 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StreamResume; + +class StreamResumeParser extends GenericElementParser<StreamResume> { + + private int level = 0; + private final static int TopLevel = 0; + + public StreamResumeParser() { + super(StreamResume.class); + } + + @Override + public void handleStartElement(String el, String ns, AttributeMap attributes) { + if (level == TopLevel) { + String handledStanzasCount = attributes.getAttributeValue("h"); + if (handledStanzasCount != null) { + try { + getElementGeneric().setHandledStanzasCount(Long.parseLong(handledStanzasCount)); + } catch (NumberFormatException e) { + } + } + getElementGeneric().setResumeID(attributes.getAttribute("previd")); + } + ++level; + } + + @Override + public void handleEndElement(String el, String ns) { + --level; + } +} diff --git a/src/com/isode/stroke/parser/StreamResumedParser.java b/src/com/isode/stroke/parser/StreamResumedParser.java new file mode 100644 index 0000000..2a874a2 --- /dev/null +++ b/src/com/isode/stroke/parser/StreamResumedParser.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2011 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.StreamResumed; + +class StreamResumedParser extends GenericElementParser<StreamResumed> { + + private int level = 0; + private final static int TopLevel = 0; + + public StreamResumedParser() { + super(StreamResumed.class); + } + + @Override + public void handleStartElement(String el, String ns, AttributeMap attributes) { + if (level == TopLevel) { + String handledStanzasCount = attributes.getAttributeValue("h"); + if (handledStanzasCount != null) { + try { + getElementGeneric().setHandledStanzasCount(Long.parseLong(handledStanzasCount)); + } catch (NumberFormatException e) { + } + } + getElementGeneric().setResumeID(attributes.getAttribute("previd")); + } + ++level; + } + + @Override + public void handleEndElement(String el, String ns) { + --level; + } +} diff --git a/src/com/isode/stroke/parser/TLSProceedParser.java b/src/com/isode/stroke/parser/TLSProceedParser.java new file mode 100644 index 0000000..b6f196d --- /dev/null +++ b/src/com/isode/stroke/parser/TLSProceedParser.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.TLSProceed; + +class TLSProceedParser extends GenericElementParser<TLSProceed>{ + + public TLSProceedParser() { + super(TLSProceed.class); + } + +} diff --git a/src/com/isode/stroke/parser/UnknownElementParser.java b/src/com/isode/stroke/parser/UnknownElementParser.java new file mode 100644 index 0000000..cf70dd6 --- /dev/null +++ b/src/com/isode/stroke/parser/UnknownElementParser.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.UnknownElement; + +public class UnknownElementParser extends GenericElementParser<UnknownElement> { + public UnknownElementParser() { + super(UnknownElement.class); + } +} diff --git a/src/com/isode/stroke/parser/UnknownPayloadParser.java b/src/com/isode/stroke/parser/UnknownPayloadParser.java new file mode 100644 index 0000000..2bdfbae --- /dev/null +++ b/src/com/isode/stroke/parser/UnknownPayloadParser.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Payload; + +public class UnknownPayloadParser implements PayloadParser { + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + + } + + public void handleEndElement(String element, String ns) { + + } + + public void handleCharacterData(String data) { + + } + + public Payload getPayload() { + return null; + } + +} diff --git a/src/com/isode/stroke/parser/XMLParser.java b/src/com/isode/stroke/parser/XMLParser.java new file mode 100644 index 0000000..a564b12 --- /dev/null +++ b/src/com/isode/stroke/parser/XMLParser.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +public abstract class XMLParser { + private final XMLParserClient client_; + + public XMLParser(XMLParserClient client) { + client_ = client; + } + + public abstract boolean parse(String data); + + protected XMLParserClient getClient() { + return client_; + } +} diff --git a/src/com/isode/stroke/parser/XMLParserClient.java b/src/com/isode/stroke/parser/XMLParserClient.java new file mode 100644 index 0000000..5fb7071 --- /dev/null +++ b/src/com/isode/stroke/parser/XMLParserClient.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + + +public interface XMLParserClient { + void handleStartElement(String element, String ns, AttributeMap attributes); + + void handleEndElement(String element, String ns); + + void handleCharacterData(String data); +} diff --git a/src/com/isode/stroke/parser/XMPPParser.java b/src/com/isode/stroke/parser/XMPPParser.java new file mode 100644 index 0000000..c16a250 --- /dev/null +++ b/src/com/isode/stroke/parser/XMPPParser.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.elements.ProtocolHeader; +import com.isode.stroke.eventloop.EventLoop; +import java.util.logging.Logger; + + +public class XMPPParser implements XMLParserClient { + + private final XMLParser xmlParser_ ; + private final XMPPParserClient client_; + private final PayloadParserFactoryCollection payloadParserFactories_; + private int currentDepth_ = 0; + private ElementParser currentElementParser_ = null; + private boolean parseErrorOccurred_ = false; + private Logger logger_ = Logger.getLogger(this.getClass().getName()); + + public XMPPParser(XMPPParserClient parserClient, PayloadParserFactoryCollection payloadParserFactories, EventLoop eventLoop) { + client_ = parserClient; + payloadParserFactories_ = payloadParserFactories; + xmlParser_ = PlatformXMLParserFactory.createXMLParser(this, eventLoop); + } + + public boolean parse(String data) { + parseErrorOccurred_ = false; + boolean xmlParseResult = xmlParser_.parse(data); + if (parseErrorOccurred_ || !xmlParseResult) { + logger_.warning(String.format("When parsing, %b and %b", parseErrorOccurred_, xmlParseResult)); + } + return xmlParseResult && !parseErrorOccurred_; + } + + public void handleStartElement( + String element, + String ns, + AttributeMap attributes) { + if (!inStream()) { + logger_.warning("Not in stream"); + if (element.equals("stream") && ns.equals("http://etherx.jabber.org/streams")) { + ProtocolHeader header = new ProtocolHeader(); + header.setFrom(attributes.getAttribute("from")); + header.setTo(attributes.getAttribute("to")); + header.setID(attributes.getAttribute("id")); + header.setVersion(attributes.getAttribute("version")); + client_.handleStreamStart(header); + } else { + parseErrorOccurred_ = true; + } + } else { + if (!inElement()) { + assert currentElementParser_ == null; + currentElementParser_ = createElementParser(element, ns); + } + currentElementParser_.handleStartElement(element, ns, attributes); + } + ++currentDepth_; + } + + public void handleEndElement(String element, String ns) { + assert (inStream()); + if (inElement()) { + assert currentElementParser_ != null; + currentElementParser_.handleEndElement(element, ns); + --currentDepth_; + if (!inElement()) { + client_.handleElement(currentElementParser_.getElement()); + currentElementParser_ = null; + } + } else { + assert (element.equals("stream")); + --currentDepth_; + client_.handleStreamEnd(); + } + } + + public void handleCharacterData(String data) { + if (currentElementParser_ != null) { + currentElementParser_.handleCharacterData(data); + } + } + + private boolean inStream() { + return currentDepth_ > 0; + } + + private boolean inElement() { + return currentDepth_ > 1; + } + + private ElementParser createElementParser(String element, String xmlns) { + if (element.equals("presence")) { + return new PresenceParser(payloadParserFactories_); + } + else if (element.equals("iq")) { + return new IQParser(payloadParserFactories_); + } + else if (element.equals("message")) { + return new MessageParser(payloadParserFactories_); + } + else if (element.equals("features") && xmlns.equals("http://etherx.jabber.org/streams")) { + return new StreamFeaturesParser(); + } + else if (element.equals("auth")) { + return new AuthRequestParser(); + } + else if (element.equals("success")) { + return new AuthSuccessParser(); + } + else if (element.equals("failure") && xmlns.equals("urn:ietf:params:xml:ns:xmpp-sasl")) { + return new AuthFailureParser(); + } + else if (element.equals("challenge") && xmlns.equals("urn:ietf:params:xml:ns:xmpp-sasl")) { + return new AuthChallengeParser(); + } + else if (element.equals("response") && xmlns.equals("urn:ietf:params:xml:ns:xmpp-sasl")) { + return new AuthResponseParser(); + } + else if (element.equals("starttls")) { + return new StartTLSParser(); + } + else if (element.equals("failure") && xmlns.equals("urn:ietf:params:xml:ns:xmpp-tls")) { + return new StartTLSFailureParser(); + } + else if (element.equals("compress")) { + return new CompressParser(); + } + else if (element.equals("compressed")) { + return new CompressedParser(); + } + else if (element.equals("failure") && xmlns.equals("http://jabber.org/protocol/compress")) { + return new CompressFailureParser(); + } + else if (element.equals("proceed")) { + return new TLSProceedParser(); + } + else if (element.equals("enable") && xmlns.equals("urn:xmpp:sm:2")) { + return new EnableStreamManagementParser(); + } + else if (element.equals("enabled") && xmlns.equals("urn:xmpp:sm:2")) { + return new StreamManagementEnabledParser(); + } + else if (element.equals("failed") && xmlns.equals("urn:xmpp:sm:2")) { + return new StreamManagementFailedParser(); + } + else if (element.equals("resume") && xmlns.equals("urn:xmpp:sm:2")) { + return new StreamResumeParser(); + } + else if (element.equals("resumed") && xmlns.equals("urn:xmpp:sm:2")) { + return new StreamResumedParser(); + } + else if (element.equals("a") && xmlns.equals("urn:xmpp:sm:2")) { + return new StanzaAckParser(); + } + else if (element.equals("r") && xmlns.equals("urn:xmpp:sm:2")) { + return new StanzaAckRequestParser(); + } + return new UnknownElementParser(); + + } +} diff --git a/src/com/isode/stroke/parser/XMPPParserClient.java b/src/com/isode/stroke/parser/XMPPParserClient.java new file mode 100644 index 0000000..c4499cd --- /dev/null +++ b/src/com/isode/stroke/parser/XMPPParserClient.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.ProtocolHeader; + +public interface XMPPParserClient { + void handleStreamStart(ProtocolHeader header); + void handleElement(Element element); + void handleStreamEnd(); +} diff --git a/src/com/isode/stroke/parser/payloadparsers/BodyParser.java b/src/com/isode/stroke/parser/payloadparsers/BodyParser.java new file mode 100644 index 0000000..3672c56 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/BodyParser.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.Body; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +public class BodyParser extends GenericPayloadParser<Body> { + + private int level_ = 0; + private String text_ = ""; + + public BodyParser() { + super(new Body()); + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + ++level_; + } + + public void handleEndElement(String element, String ns) { + --level_; + if (level_ == 0) { + getPayloadInternal().setText(text_); + } + } + + public void handleCharacterData(String data) { + text_ += data; + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/BodyParserFactory.java b/src/com/isode/stroke/parser/payloadparsers/BodyParserFactory.java new file mode 100644 index 0000000..154281b --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/BodyParserFactory.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.parser.GenericPayloadParserFactory; + +public class BodyParserFactory extends GenericPayloadParserFactory<BodyParser> { + + public BodyParserFactory() { + super("body", BodyParser.class); + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/FullPayloadParserFactoryCollection.java b/src/com/isode/stroke/parser/payloadparsers/FullPayloadParserFactoryCollection.java new file mode 100644 index 0000000..dfc1b61 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/FullPayloadParserFactoryCollection.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.parser.GenericPayloadParserFactory; +import com.isode.stroke.parser.PayloadParserFactory; +import com.isode.stroke.parser.PayloadParserFactoryCollection; + +public class FullPayloadParserFactoryCollection extends PayloadParserFactoryCollection { + public FullPayloadParserFactoryCollection() { + /* TODO: Port more */ + //addFactory(new GenericPayloadParserFactory<IBBParser>("", "http://jabber.org/protocol/ibb")); + //addFactory(new GenericPayloadParserFactory<StatusShowParser>("show", StatusShowParser.class)); + //addFactory(new GenericPayloadParserFactory<StatusParser>("status", StatusParser.class)); + //addFactory(new GenericPayloadParserFactory<ReplaceParser>("replace", "http://swift.im/protocol/replace")); + addFactory(new GenericPayloadParserFactory<LastParser>("query", "jabber:iq:last", LastParser.class)); + addFactory(new GenericPayloadParserFactory<BodyParser>("body", BodyParser.class)); + //addFactory(new GenericPayloadParserFactory<SubjectParser>("subject", SubjectParser.class)); + //addFactory(new GenericPayloadParserFactory<PriorityParser>("priority", PriorityParser.class)); + //addFactory(new ErrorParserFactory(this))); + addFactory(new SoftwareVersionParserFactory()); + //addFactory(new StorageParserFactory()); + addFactory(new RosterParserFactory()); + //addFactory(new DiscoInfoParserFactory()); + //addFactory(new DiscoItemsParserFactory()); + //addFactory(new CapsInfoParserFactory()); + addFactory(new ResourceBindParserFactory()); + addFactory(new StartSessionParserFactory()); + //addFactory(new SecurityLabelParserFactory()); + //addFactory(new SecurityLabelsCatalogParserFactory()); + //addFactory(new FormParserFactory()); + //addFactory(new CommandParserFactory()); + //addFactery(new InBandRegistrationPayloadParserFactory()); + addFactory(new SearchPayloadParserFactory()); + //addFactory(new StreamInitiationParserFactory()); + //addFactory(new BytestreamsParserFactory()); + //addFactory(new VCardUpdateParserFactory()); + //addFactory(new VCardParserFactory()); + //addFactory(new PrivateStorageParserFactory(this)); + //addFactory(new ChatStateParserFactory()); + //addFactory(new DelayParserFactory()); + //addFactory(new MUCUserPayloadParserFactory()); + //addFactory(new NicknameParserFactory()); + + + PayloadParserFactory defaultFactory = new RawXMLPayloadParserFactory(); + setDefaultFactory(defaultFactory); + } +} diff --git a/src/com/isode/stroke/parser/payloadparsers/LastParser.java b/src/com/isode/stroke/parser/payloadparsers/LastParser.java new file mode 100644 index 0000000..d2c2a82 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/LastParser.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2011, Kevin Smith. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.Last; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +public class LastParser extends GenericPayloadParser<Last> { + + private int level_ = 0; + + public LastParser() { + super(new Last()); + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + if (level_ == 0) { + int seconds = 0; + + try { + seconds = Integer.parseInt(attributes.getAttribute("seconds")); + } + catch (NumberFormatException ex) { + } + getPayloadInternal().setSeconds(seconds); + } + ++level_; + } + + public void handleEndElement(String element, String ns) { + + } + + public void handleCharacterData(String data) { + + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/RawXMLPayloadParser.java b/src/com/isode/stroke/parser/payloadparsers/RawXMLPayloadParser.java new file mode 100644 index 0000000..2a97fed --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/RawXMLPayloadParser.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.RawXMLPayload; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; +import com.isode.stroke.parser.SerializingParser; + +public class RawXMLPayloadParser extends GenericPayloadParser<RawXMLPayload> { + + private int level_; + private final SerializingParser serializingParser_ = new SerializingParser(); + + public RawXMLPayloadParser() { + super(new RawXMLPayload()); + } + + + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + ++level_; + serializingParser_.handleStartElement(element, ns, attributes); + } + + public void handleEndElement(String element, String ns) { + serializingParser_.handleEndElement(element, ns); + --level_; + if (level_ == 0) { + getPayloadInternal().setRawXML(serializingParser_.getResult()); + } + } + + public void handleCharacterData(String data) { + serializingParser_.handleCharacterData(data); + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/RawXMLPayloadParserFactory.java b/src/com/isode/stroke/parser/payloadparsers/RawXMLPayloadParserFactory.java new file mode 100644 index 0000000..e0914e1 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/RawXMLPayloadParserFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.PayloadParser; +import com.isode.stroke.parser.PayloadParserFactory; + +public class RawXMLPayloadParserFactory implements PayloadParserFactory { + + public boolean canParse(String element, String ns, AttributeMap attributes) { + return true; + } + + public PayloadParser createPayloadParser() { + return new RawXMLPayloadParser(); + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/ResourceBindParser.java b/src/com/isode/stroke/parser/payloadparsers/ResourceBindParser.java new file mode 100644 index 0000000..be409f4 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/ResourceBindParser.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.ResourceBind; +import com.isode.stroke.jid.JID; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +public class ResourceBindParser extends GenericPayloadParser<ResourceBind> { + + public ResourceBindParser() { + super(new ResourceBind()); + level_ = 0; + inJID_ = false; + inResource_ = false; + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + if (level_ == 1) { + text_ = ""; + if (element.equals("resource")) { + inResource_ = true; + } + if (element.equals("jid")) { + inJID_ = true; + } + } + ++level_; + } + + public void handleEndElement(String element, String ns) { + --level_; + if (level_ == 1) { + if (inJID_) { + getPayloadInternal().setJID(JID.fromString(text_)); + } else if (inResource_) { + getPayloadInternal().setResource(text_); + } + } + } + + public void handleCharacterData(String data) { + text_ += data; + } + private int level_; + private boolean inJID_; + private boolean inResource_; + private String text_ = ""; +} diff --git a/src/com/isode/stroke/parser/payloadparsers/ResourceBindParserFactory.java b/src/com/isode/stroke/parser/payloadparsers/ResourceBindParserFactory.java new file mode 100644 index 0000000..a6b644d --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/ResourceBindParserFactory.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.parser.GenericPayloadParserFactory; + +class ResourceBindParserFactory extends GenericPayloadParserFactory<ResourceBindParser> { + + public ResourceBindParserFactory() { + super("bind", "urn:ietf:params:xml:ns:xmpp-bind", ResourceBindParser.class); + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/RosterParser.java b/src/com/isode/stroke/parser/payloadparsers/RosterParser.java new file mode 100644 index 0000000..fa115c2 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/RosterParser.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.Payload; +import com.isode.stroke.elements.RosterItemPayload; +import com.isode.stroke.elements.RosterPayload; +import com.isode.stroke.jid.JID; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +public class RosterParser extends GenericPayloadParser<RosterPayload> { + + public RosterParser() { + super(new RosterPayload()); + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + if (level_ == PayloadLevel) { + if (element.equals("item")) { + inItem_ = true; + currentItem_ = new RosterItemPayload(); + + currentItem_.setJID(JID.fromString(attributes.getAttribute("jid"))); + currentItem_.setName(attributes.getAttribute("name")); + + String subscription = attributes.getAttribute("subscription"); + if ("both".equals(subscription)) { + currentItem_.setSubscription(RosterItemPayload.Subscription.Both); + } else if ("to".equals(subscription)) { + currentItem_.setSubscription(RosterItemPayload.Subscription.To); + } else if ("frome".equals(subscription)) { + currentItem_.setSubscription(RosterItemPayload.Subscription.From); + } else if ("remove".equals(subscription)) { + currentItem_.setSubscription(RosterItemPayload.Subscription.Remove); + } else { + currentItem_.setSubscription(RosterItemPayload.Subscription.None); + } + + if ("subscribe".equals(attributes.getAttribute("ask"))) { + currentItem_.setSubscriptionRequested(); + } + } + } else if (level_ == ItemLevel) { + if (element.equals("group")) { + currentText_ = ""; + } + } + ++level_; + } + + public void handleEndElement(String element, String ns) { + --level_; + if (level_ == PayloadLevel) { + if (inItem_) { + getPayloadInternal().addItem(currentItem_); + inItem_ = false; + } + } else if (level_ == ItemLevel) { + if (element.equals("group")) { + currentItem_.addGroup(currentText_); + } + } + } + + public void handleCharacterData(String data) { + currentText_ += data; + } + private final int TopLevel = 0; + private final int PayloadLevel = 1; + private final int ItemLevel = 2; + private int level_ = TopLevel; + private boolean inItem_ = false; + private RosterItemPayload currentItem_; + private String currentText_; +} diff --git a/src/com/isode/stroke/parser/payloadparsers/RosterParserFactory.java b/src/com/isode/stroke/parser/payloadparsers/RosterParserFactory.java new file mode 100644 index 0000000..ad690e1 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/RosterParserFactory.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.parser.GenericPayloadParserFactory; + +public class RosterParserFactory extends GenericPayloadParserFactory<RosterParser> { + + public RosterParserFactory() { + super("query", "jabber:iq:roster", RosterParser.class); + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/SearchPayloadParser.java b/src/com/isode/stroke/parser/payloadparsers/SearchPayloadParser.java new file mode 100644 index 0000000..0ee1499 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/SearchPayloadParser.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.SearchPayload; +import com.isode.stroke.jid.JID; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +public class SearchPayloadParser extends GenericPayloadParser<SearchPayload> { + + private static final int TopLevel = 0; + private static final int PayloadLevel = 1; + private static final int ItemLevel = 2; + + private int level = 0; + private String currentText = ""; + //private FormParserFactory formParserFactory = new FormParserFactory(); /* Not ported yet*/ + //private FormParser formParser; + SearchPayload.Item currentItem; + + public SearchPayloadParser() { + super(new SearchPayload()); + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + if (level == TopLevel) { + } + else if (level == PayloadLevel) { + //if (element.equals("x") && ns.equals("jabber:x:data")) { + // assert formParser == null; + // formParser = dynamic_cast<FormParser*>(formParserFactory->createPayloadParser()); + //} /* Not ported yet */ + //else + if (element.equals("item")) { + assert currentItem == null; + currentItem = new SearchPayload.Item(); + currentItem.jid = JID.fromString(attributes.getAttribute("jid")); + } + else { + currentText = ""; + } + } + else if (level == ItemLevel && currentItem != null) { + currentText = ""; + } + + //if (formParser) { + // formParser->handleStartElement(element, ns, attributes); + //} /* Not ported yet */ + + ++level; + } + + public void handleEndElement(String element, String ns) { + --level; + + //if (formParser) { + // formParser->handleEndElement(element, ns); + //} /*Not Ported yet*/ + + if (level == TopLevel) { + } + else if (level == PayloadLevel) { + //if (formParser) { + // getPayloadInternal()->setForm(formParser->getPayloadInternal()); + // delete formParser; + // formParser = NULL; + //} + //else /*Not ported yet*/ + if (element.equals("item")) { + assert currentItem != null; + getPayloadInternal().addItem(currentItem); + currentItem = null; + } + else if (element.equals("instructions")) { + getPayloadInternal().setInstructions(currentText); + } + else if (element.equals("nick")) { + getPayloadInternal().setNick(currentText); + } + else if (element.equals("first")) { + getPayloadInternal().setFirst(currentText); + } + else if (element.equals("last")) { + getPayloadInternal().setLast(currentText); + } + else if (element.equals("email")) { + getPayloadInternal().setEMail(currentText); + } + } + else if (level == ItemLevel && currentItem != null) { + if (element.equals("nick")) { + currentItem.nick = currentText; + } + else if (element.equals("first")) { + currentItem.first = currentText; + } + else if (element.equals("last")) { + currentItem.last = currentText; + } + else if (element.equals("email")) { + currentItem.email = currentText; + } + } + } + + public void handleCharacterData(String data) { + //if (formParser) { + // formParser->handleCharacterData(data); + //} + //else { /*Not ported yet*/ + currentText += data; + //} + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/SearchPayloadParserFactory.java b/src/com/isode/stroke/parser/payloadparsers/SearchPayloadParserFactory.java new file mode 100644 index 0000000..4add0a1 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/SearchPayloadParserFactory.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * All rights reserved. + */ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.parser.GenericPayloadParserFactory; +import com.isode.stroke.parser.PayloadParser; + +public class SearchPayloadParserFactory extends GenericPayloadParserFactory<SearchPayloadParser> { + + public SearchPayloadParserFactory() { + super("query", "jabber:iq:search", SearchPayloadParser.class); + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/SoftwareVersionParser.java b/src/com/isode/stroke/parser/payloadparsers/SoftwareVersionParser.java new file mode 100644 index 0000000..93563a3 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/SoftwareVersionParser.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * All rights reserved. + */ +/* + * Copyright (c) 2010 Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.Version; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +public class SoftwareVersionParser extends GenericPayloadParser<Version> { + + public SoftwareVersionParser() { + super(new Version()); + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + level_++; + } + + public void handleEndElement(String element, String ns) { + --level_; + if (level_ == PayloadLevel) { + if (element.equals("name")) { + getPayloadInternal().setName(currentText_); + } else if (element.equals("version")) { + getPayloadInternal().setVersion(currentText_); + } else if (element.equals("os")) { + getPayloadInternal().setOS(currentText_); + } + currentText_ = ""; + } + + } + + public void handleCharacterData(String data) { + currentText_ += data; + } + + private static final int TopLevel = 0; + private static final int PayloadLevel = 1; + private int level_ = TopLevel; + private String currentText_ = ""; + + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/SoftwareVersionParserFactory.java b/src/com/isode/stroke/parser/payloadparsers/SoftwareVersionParserFactory.java new file mode 100644 index 0000000..e2180f5 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/SoftwareVersionParserFactory.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * All rights reserved. + */ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.parser.GenericPayloadParserFactory; + +public class SoftwareVersionParserFactory extends GenericPayloadParserFactory<SoftwareVersionParser> { + + public SoftwareVersionParserFactory() { + super("query", "jabber:iq:version", SoftwareVersionParser.class); + } + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/StartSessionParser.java b/src/com/isode/stroke/parser/payloadparsers/StartSessionParser.java new file mode 100644 index 0000000..ba7804c --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/StartSessionParser.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.StartSession; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +public class StartSessionParser extends GenericPayloadParser<StartSession> { + + public StartSessionParser() { + super(new StartSession()); + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + + } + + public void handleEndElement(String element, String ns) { + + } + + public void handleCharacterData(String data) { + + } + + +} diff --git a/src/com/isode/stroke/parser/payloadparsers/StartSessionParserFactory.java b/src/com/isode/stroke/parser/payloadparsers/StartSessionParserFactory.java new file mode 100644 index 0000000..c2c9777 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/StartSessionParserFactory.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.parser.GenericPayloadParserFactory; + +class StartSessionParserFactory extends GenericPayloadParserFactory<StartSessionParser> { + + public StartSessionParserFactory() { + super("session", "urn:ietf:params:xml:ns:xmpp-session", StartSessionParser.class); + } + +} diff --git a/src/com/isode/stroke/queries/GenericRequest.java b/src/com/isode/stroke/queries/GenericRequest.java new file mode 100644 index 0000000..cfbfb52 --- /dev/null +++ b/src/com/isode/stroke/queries/GenericRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010, 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.queries; + +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.elements.Payload; +import com.isode.stroke.jid.JID; +import com.isode.stroke.signals.Signal2; + +/** + * IQ Request for standard payload. + */ +public class GenericRequest<T extends Payload> extends Request { + + public Signal2<T, ErrorPayload> onResponse = new Signal2<T, ErrorPayload>(); + + public GenericRequest(IQ.Type type, JID receiver, Payload payload, IQRouter router) { + super(type, receiver, payload, router); + } + + @Override + protected void handleResponse(Payload payload, ErrorPayload error) { + T genericPayload = null; + try { + genericPayload = (T)payload; + } catch (Exception ex) { + /* This isn't legal XMPP, so treat as NULL.*/ + } + onResponse.emit(genericPayload, error); + } + + protected T getPayloadGeneric() { + return (T)getPayload(); + } + +} diff --git a/src/com/isode/stroke/queries/GetRosterRequest.java b/src/com/isode/stroke/queries/GetRosterRequest.java new file mode 100644 index 0000000..1c7ea6e --- /dev/null +++ b/src/com/isode/stroke/queries/GetRosterRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.queries; + +import com.isode.stroke.elements.IQ.Type; +import com.isode.stroke.elements.RosterPayload; +import com.isode.stroke.jid.JID; + +public class GetRosterRequest extends GenericRequest<RosterPayload> { + public GetRosterRequest(JID target, IQRouter iqRouter) { + super(Type.Get, target, new RosterPayload(), iqRouter); + } + + public GetRosterRequest(IQRouter iqRouter) { + super(Type.Get, new JID(), new RosterPayload(), iqRouter); + } +} diff --git a/src/com/isode/stroke/queries/GetVersionRequest.java b/src/com/isode/stroke/queries/GetVersionRequest.java new file mode 100644 index 0000000..75db384 --- /dev/null +++ b/src/com/isode/stroke/queries/GetVersionRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * All rights reserved. + */ +/* + * Copyright (c) 2010 Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.queries; + +import com.isode.stroke.elements.IQ.Type; +import com.isode.stroke.elements.Version; +import com.isode.stroke.jid.JID; + +public class GetVersionRequest extends GenericRequest<Version> { + public GetVersionRequest(JID target, IQRouter iqRouter) { + super(Type.Get, target, new Version(), iqRouter); + } + + public GetVersionRequest(IQRouter iqRouter) { + super(Type.Get, new JID(), new Version(), iqRouter); + } +} diff --git a/src/com/isode/stroke/queries/IQChannel.java b/src/com/isode/stroke/queries/IQChannel.java new file mode 100644 index 0000000..fd5fe8e --- /dev/null +++ b/src/com/isode/stroke/queries/IQChannel.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.queries; + +import com.isode.stroke.elements.IQ; +import com.isode.stroke.signals.Signal1; + +public abstract class IQChannel { + + public abstract void sendIQ(IQ iq); + + public abstract String getNewIQID(); + + public abstract boolean isAvailable(); + + public final Signal1<IQ> onIQReceived = new Signal1<IQ>(); +} diff --git a/src/com/isode/stroke/queries/IQHandler.java b/src/com/isode/stroke/queries/IQHandler.java new file mode 100644 index 0000000..2d6f35e --- /dev/null +++ b/src/com/isode/stroke/queries/IQHandler.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.queries; + +import com.isode.stroke.elements.IQ; + +/** + * Thing reacting to IQs. + */ +public interface IQHandler { + boolean handleIQ(IQ iq); +} diff --git a/src/com/isode/stroke/queries/IQReceivedHandler.java b/src/com/isode/stroke/queries/IQReceivedHandler.java new file mode 100644 index 0000000..37db43a --- /dev/null +++ b/src/com/isode/stroke/queries/IQReceivedHandler.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.queries; + +import com.isode.stroke.elements.IQ; + +interface IQReceivedHandler { + void handleIQ(IQ iq); +} diff --git a/src/com/isode/stroke/queries/IQRouter.java b/src/com/isode/stroke/queries/IQRouter.java new file mode 100644 index 0000000..04ddb20 --- /dev/null +++ b/src/com/isode/stroke/queries/IQRouter.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.queries; + +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.signals.Slot1; +import java.util.Vector; + +/** + * Route IQs to handlers + */ +public class IQRouter { + + private final Vector<IQHandler> handlers_ = new Vector<IQHandler>(); + private final IQChannel channel_; + + public IQRouter(IQChannel channel) { + channel_ = channel; + final IQRouter thisObject = this; + channel_.onIQReceived.connect(new Slot1<IQ>() { + + public void call(IQ p1) { + handleIQ(p1); + } + }); + } + + public void addHandler(IQHandler handler) { + synchronized (handlers_) { + handlers_.add(handler); + } + } + + public void removeHandler(IQHandler handler) { + synchronized (handlers_) { + handlers_.remove(handler); + } + } + + public void sendIQ(IQ iq) { + channel_.sendIQ(iq); + } + + public String getNewIQID() { + return channel_.getNewIQID(); + } + + public boolean isAvailable() { + return channel_.isAvailable(); + } + + private void handleIQ(IQ iq) { + boolean handled = false; + synchronized (handlers_) { + for (IQHandler handler : handlers_) { + handled |= handler.handleIQ(iq); + if (handled) { + break; + } + } + } + if (!handled && (iq.getType().equals(IQ.Type.Get) || iq.getType().equals(IQ.Type.Set))) { + sendIQ(IQ.createError(iq.getFrom(), iq.getID(), ErrorPayload.Condition.FeatureNotImplemented, ErrorPayload.Type.Cancel)); + } + } +} diff --git a/src/com/isode/stroke/queries/Request.java b/src/com/isode/stroke/queries/Request.java new file mode 100644 index 0000000..df38b10 --- /dev/null +++ b/src/com/isode/stroke/queries/Request.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.queries; + +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.elements.IQ.Type; +import com.isode.stroke.elements.Payload; +import com.isode.stroke.jid.JID; + +/** + * Base class for IQ requests. + */ +public abstract class Request implements IQHandler { + protected final Type type_; + protected final IQRouter router_; + protected final JID receiver_; + private boolean sent_; + private Payload payload_; + private String id_; + + public Request(IQ.Type type, JID receiver, IQRouter router) { + this(type, receiver, null, router); + } + + public Request(IQ.Type type, JID receiver, Payload payload, IQRouter router) { + type_ = type; + router_ = router; + receiver_ = receiver; + payload_ = payload; + sent_ = false; + } + + public void send() { + assert payload_ != null; + assert !sent_; + sent_ = true; + + IQ iq = new IQ(type_); + iq.setTo(receiver_); + iq.addPayload(payload_); + id_ = router_.getNewIQID(); + iq.setID(id_); + + router_.addHandler(this); + + router_.sendIQ(iq); + } + + protected void setPayload(Payload payload) { + payload_ = payload; + } + + protected Payload getPayload() { + return payload_; + } + + protected abstract void handleResponse(Payload payload, ErrorPayload error); + + public boolean handleIQ(IQ iq) { + boolean handled = false; + if (sent_ && iq.getID().equals(id_)) { + if (iq.getType().equals(IQ.Type.Result)) { + handleResponse(iq.getPayload(payload_), null); + } else { + ErrorPayload errorPayload = iq.getPayload(new ErrorPayload()); + if (errorPayload != null) { + handleResponse(null, errorPayload); + } else { + handleResponse(null, new ErrorPayload(ErrorPayload.Condition.UndefinedCondition)); + } + } + router_.removeHandler(this); + handled = true; + } + return handled; + } + +}
\ No newline at end of file diff --git a/src/com/isode/stroke/sasl/ClientAuthenticator.java b/src/com/isode/stroke/sasl/ClientAuthenticator.java new file mode 100644 index 0000000..e8fcff9 --- /dev/null +++ b/src/com/isode/stroke/sasl/ClientAuthenticator.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.sasl; + +import com.isode.stroke.base.ByteArray; + +public abstract class ClientAuthenticator { + + public ClientAuthenticator(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setCredentials(String authcid, String password) { + setCredentials(authcid, password, ""); + } + + public void setCredentials(String authcid, String password, String authzid) { + this.authcid = authcid; + this.password = password; + this.authzid = authzid; + } + + public abstract ByteArray getResponse(); + + public abstract boolean setChallenge(ByteArray challenge); + + public String getAuthenticationID() { + return authcid; + } + + public String getAuthorizationID() { + return authzid; + } + + public String getPassword() { + return password; + } + private String name; + private String authcid; + private String password; + private String authzid; +} diff --git a/src/com/isode/stroke/sasl/PLAINClientAuthenticator.java b/src/com/isode/stroke/sasl/PLAINClientAuthenticator.java new file mode 100644 index 0000000..889ac54 --- /dev/null +++ b/src/com/isode/stroke/sasl/PLAINClientAuthenticator.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.sasl; + +import com.isode.stroke.base.ByteArray; + +public class PLAINClientAuthenticator extends ClientAuthenticator { + public PLAINClientAuthenticator() { + super("PLAIN"); + } + + public ByteArray getResponse() { + return new ByteArray().append(getAuthorizationID()).append((byte)0).append(getAuthenticationID()).append((byte)0).append(getPassword()); + } + + public boolean setChallenge(ByteArray challenge) { + return true; + } +} + diff --git a/src/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticator.java b/src/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticator.java new file mode 100644 index 0000000..da44d85 --- /dev/null +++ b/src/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticator.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.sasl; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.stringcodecs.Base64; +import com.isode.stroke.stringcodecs.HMACSHA1; +import com.isode.stroke.stringcodecs.PBKDF2; +import com.isode.stroke.stringcodecs.SHA1; +import java.text.Normalizer; +import java.text.Normalizer.Form; +import java.util.HashMap; +import java.util.Map; + +public class SCRAMSHA1ClientAuthenticator extends ClientAuthenticator { + + static String escape(String s) { + String result = ""; + for (int i = 0; i < s.length(); ++i) { + if (s.charAt(i) == ',') { + result += "=2C"; + } else if (s.charAt(i) == '=') { + result += "=3D"; + } else { + result += s.charAt(i); + } + } + return result; + } + + public SCRAMSHA1ClientAuthenticator(String nonce) { + this(nonce, false); + } + public SCRAMSHA1ClientAuthenticator(String nonce, boolean useChannelBinding) { + super(useChannelBinding ? "SCRAM-SHA-1-PLUS" : "SCRAM-SHA-1"); + step = Step.Initial; + clientnonce = nonce; + this.useChannelBinding = useChannelBinding; + } + + public void setTLSChannelBindingData(ByteArray channelBindingData) { + tlsChannelBindingData = channelBindingData; + } + + public ByteArray getResponse() { + if (step.equals(Step.Initial)) { + return ByteArray.plus(getGS2Header(), getInitialBareClientMessage()); + } else if (step.equals(Step.Proof)) { + ByteArray clientKey = HMACSHA1.getResult(saltedPassword, new ByteArray("Client Key")); + ByteArray storedKey = SHA1.getHash(clientKey); + ByteArray clientSignature = HMACSHA1.getResult(storedKey, authMessage); + ByteArray clientProof = clientKey; + byte[] clientProofData = clientProof.getData(); + for (int i = 0; i < clientProofData.length; ++i) { + clientProofData[i] ^= clientSignature.getData()[i]; + } + ByteArray result = getFinalMessageWithoutProof().append(",p=").append(Base64.encode(clientProof)); + return result; + } else { + return null; + } + } + + public boolean setChallenge(ByteArray challenge) { + if (step.equals(Step.Initial)) { + if (challenge == null) { + return false; + } + initialServerMessage = challenge; + + Map<Character, String> keys = parseMap(initialServerMessage.toString()); + + // Extract the salt + ByteArray salt = Base64.decode(keys.get('s')); + + // Extract the server nonce + String clientServerNonce = keys.get('r'); + if (clientServerNonce.length() <= clientnonce.length()) { + return false; + } + String receivedClientNonce = clientServerNonce.substring(0, clientnonce.length()); + if (!receivedClientNonce.equals(clientnonce)) { + return false; + } + serverNonce = new ByteArray(clientServerNonce.substring(clientnonce.length())); + + + // Extract the number of iterations + int iterations = 0; + try { + iterations = Integer.parseInt(keys.get('i')); + } catch (NumberFormatException e) { + return false; + } + if (iterations <= 0) { + return false; + } + + ByteArray channelBindData = new ByteArray(); + if (useChannelBinding && tlsChannelBindingData != null) { + channelBindData = tlsChannelBindingData; + } + + // Compute all the values needed for the server signature + saltedPassword = PBKDF2.encode(new ByteArray(SASLPrep(getPassword())), salt, iterations); + authMessage = getInitialBareClientMessage().append(",").append(initialServerMessage).append(",").append(getFinalMessageWithoutProof()); + ByteArray serverKey = HMACSHA1.getResult(saltedPassword, new ByteArray("Server Key")); + serverSignature = HMACSHA1.getResult(serverKey, authMessage); + + step = Step.Proof; + return true; + } else if (step.equals(step.Proof)) { + ByteArray result = new ByteArray("v=").append(new ByteArray(Base64.encode(serverSignature))); + step = Step.Final; + return challenge != null && challenge.equals(result); + } else { + return true; + } + } + + private String SASLPrep(String source) { + return Normalizer.normalize(source, Form.NFKC); /* FIXME: Implement real SASLPrep */ + } + + private Map<Character, String> parseMap(String s) { + HashMap<Character, String> result = new HashMap<Character, String>(); + if (s.length() > 0) { + char key = '~'; /* initialise so it'll compile */ + String value = ""; + int i = 0; + boolean expectKey = true; + while (i < s.length()) { + if (expectKey) { + key = s.charAt(i); + expectKey = false; + i++; + } else if (s.charAt(i) == ',') { + result.put(key, value); + value = ""; + expectKey = true; + } else { + value += s.charAt(i); + } + i++; + } + result.put(key, value); + } + return result; + } + + private ByteArray getInitialBareClientMessage() { + String authenticationID = SASLPrep(getAuthenticationID()); + return new ByteArray("n=" + escape(authenticationID) + ",r=" + clientnonce); + } + + private ByteArray getGS2Header() { + + ByteArray channelBindingHeader = new ByteArray("n"); + if (tlsChannelBindingData != null) { + if (useChannelBinding) { + channelBindingHeader = new ByteArray("p=tls-unique"); + } + else { + channelBindingHeader = new ByteArray("y"); + } + } + return new ByteArray().append(channelBindingHeader).append(",").append(getAuthorizationID().isEmpty() ? new ByteArray() : new ByteArray("a=" + escape(getAuthorizationID()))).append(","); + } + + private ByteArray getFinalMessageWithoutProof() { + ByteArray channelBindData = new ByteArray(); + if (useChannelBinding && tlsChannelBindingData != null) { + channelBindData = tlsChannelBindingData; + } + return new ByteArray("c=" + Base64.encode(new ByteArray(getGS2Header()).append(channelBindData)) + ",r=" + clientnonce).append(serverNonce); + } + + private enum Step { + + Initial, + Proof, + Final + }; + private Step step; + private String clientnonce = ""; + private ByteArray initialServerMessage = new ByteArray(); + private ByteArray serverNonce = new ByteArray(); + private ByteArray authMessage = new ByteArray(); + private ByteArray saltedPassword = new ByteArray(); + private ByteArray serverSignature = new ByteArray(); + private boolean useChannelBinding; + private ByteArray tlsChannelBindingData; +} diff --git a/src/com/isode/stroke/serializer/AuthChallengeSerializer.java b/src/com/isode/stroke/serializer/AuthChallengeSerializer.java new file mode 100644 index 0000000..eee392e --- /dev/null +++ b/src/com/isode/stroke/serializer/AuthChallengeSerializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.elements.AuthChallenge; +import com.isode.stroke.elements.Element; +import com.isode.stroke.stringcodecs.Base64; + +class AuthChallengeSerializer extends GenericElementSerializer<AuthChallenge> { + + public AuthChallengeSerializer() { + super(AuthChallenge.class); + } + + public String serialize(Element element) { + AuthChallenge authChallenge = (AuthChallenge)element; + String value = ""; + ByteArray message = authChallenge.getValue(); + if (message != null) { + if (message.isEmpty()) { + value = "="; + } + else { + value = Base64.encode(message); + } + } + return "<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">" + value + "</challenge>"; + } + +} diff --git a/src/com/isode/stroke/serializer/AuthFailureSerializer.java b/src/com/isode/stroke/serializer/AuthFailureSerializer.java new file mode 100644 index 0000000..e75a0f4 --- /dev/null +++ b/src/com/isode/stroke/serializer/AuthFailureSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.AuthFailure; +import com.isode.stroke.elements.Element; +import com.isode.stroke.serializer.xml.XMLElement; + +class AuthFailureSerializer extends GenericElementSerializer<AuthFailure>{ + + public AuthFailureSerializer() { + super(AuthFailure.class); + } + + public String serialize(Element element) { + return new XMLElement("failure", "urn:ietf:params:xml:ns:xmpp-sasl").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/AuthRequestSerializer.java b/src/com/isode/stroke/serializer/AuthRequestSerializer.java new file mode 100644 index 0000000..eb3fbd5 --- /dev/null +++ b/src/com/isode/stroke/serializer/AuthRequestSerializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.elements.AuthRequest; +import com.isode.stroke.elements.Element; +import com.isode.stroke.stringcodecs.Base64; + +class AuthRequestSerializer extends GenericElementSerializer<AuthRequest> { + + public AuthRequestSerializer() { + super(AuthRequest.class); + } + + public String serialize(Element element) { + AuthRequest authRequest = (AuthRequest)element; + String value = ""; + ByteArray message = authRequest.getMessage(); + if (message != null) { + if (message.isEmpty()) { + value = "="; + } + else { + value = Base64.encode(message); + } + } + return "<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"" + authRequest.getMechanism() + "\">" + value + "</auth>"; + } + +} diff --git a/src/com/isode/stroke/serializer/AuthResponseSerializer.java b/src/com/isode/stroke/serializer/AuthResponseSerializer.java new file mode 100644 index 0000000..b65f4b1 --- /dev/null +++ b/src/com/isode/stroke/serializer/AuthResponseSerializer.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.serializer; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.elements.AuthResponse; +import com.isode.stroke.elements.Element; +import com.isode.stroke.stringcodecs.Base64; + +class AuthResponseSerializer extends GenericElementSerializer<AuthResponse> { + + public AuthResponseSerializer() { + super(AuthResponse.class); + } + + public String serialize(Element element) { + AuthResponse authResponse = (AuthResponse) element; + String value = ""; + ByteArray message = authResponse.getValue(); + if (message != null) { + if (message.isEmpty()) { + value = "="; + } else { + value = Base64.encode(message); + } + } + return "<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">" + value + "</response>"; + } +} diff --git a/src/com/isode/stroke/serializer/AuthSuccessSerializer.java b/src/com/isode/stroke/serializer/AuthSuccessSerializer.java new file mode 100644 index 0000000..1b8be25 --- /dev/null +++ b/src/com/isode/stroke/serializer/AuthSuccessSerializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.elements.AuthSuccess; +import com.isode.stroke.elements.Element; +import com.isode.stroke.stringcodecs.Base64; + + +class AuthSuccessSerializer extends GenericElementSerializer<AuthSuccess> { + + public AuthSuccessSerializer() { + super(AuthSuccess.class); + } + + public String serialize(Element element) { + AuthSuccess authSuccess = (AuthSuccess)element; + String value = ""; + ByteArray message = authSuccess.getValue(); + if (message != null) { + if (message.isEmpty()) { + value = "="; + } + else { + value = Base64.encode(message); + } + } + return "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">" + value + "</success>"; + } + +} diff --git a/src/com/isode/stroke/serializer/CompressFailureSerializer.java b/src/com/isode/stroke/serializer/CompressFailureSerializer.java new file mode 100644 index 0000000..fe31f27 --- /dev/null +++ b/src/com/isode/stroke/serializer/CompressFailureSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.CompressFailure; +import com.isode.stroke.elements.Element; +import com.isode.stroke.serializer.xml.XMLElement; + +class CompressFailureSerializer extends GenericElementSerializer<CompressFailure> { + + public CompressFailureSerializer() { + super(CompressFailure.class); + } + + public String serialize(Element element) { + return new XMLElement("failure", "http://jabber.org/protocol/compress").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/CompressRequestSerializer.java b/src/com/isode/stroke/serializer/CompressRequestSerializer.java new file mode 100644 index 0000000..512e178 --- /dev/null +++ b/src/com/isode/stroke/serializer/CompressRequestSerializer.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.CompressRequest; +import com.isode.stroke.elements.Element; + +class CompressRequestSerializer implements ElementSerializer { + + public CompressRequestSerializer() { + } + + public String serialize(Element element) { + CompressRequest compressRequest = (CompressRequest) element; + return "<compress xmlns='http://jabber.org/protocol/compress'><method>" + compressRequest.getMethod() + "</method></compress>"; + } + + public boolean canSerialize(Element element) { + return element instanceof CompressRequest; + } +} diff --git a/src/com/isode/stroke/serializer/ElementSerializer.java b/src/com/isode/stroke/serializer/ElementSerializer.java new file mode 100644 index 0000000..8f4ade7 --- /dev/null +++ b/src/com/isode/stroke/serializer/ElementSerializer.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; + +public interface ElementSerializer { + String serialize(Element element); + boolean canSerialize(Element element); +} diff --git a/src/com/isode/stroke/serializer/EnableStreamManagementSerializer.java b/src/com/isode/stroke/serializer/EnableStreamManagementSerializer.java new file mode 100644 index 0000000..8862bb3 --- /dev/null +++ b/src/com/isode/stroke/serializer/EnableStreamManagementSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.EnableStreamManagement; +import com.isode.stroke.serializer.xml.XMLElement; + +class EnableStreamManagementSerializer extends GenericElementSerializer<EnableStreamManagement> { + + public EnableStreamManagementSerializer() { + super(EnableStreamManagement.class); + } + + public String serialize(Element element) { + return new XMLElement("enable", "urn:xmpp:sm:2").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/GenericElementSerializer.java b/src/com/isode/stroke/serializer/GenericElementSerializer.java new file mode 100644 index 0000000..ecd5bf4 --- /dev/null +++ b/src/com/isode/stroke/serializer/GenericElementSerializer.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; + +public abstract class GenericElementSerializer<T> implements ElementSerializer { + + private final Class elementClass_; + + GenericElementSerializer(Class elementClass) { + elementClass_ = elementClass; + } + + public boolean canSerialize(Element element) { + return elementClass_.isAssignableFrom(element.getClass()); + } + + +}
\ No newline at end of file diff --git a/src/com/isode/stroke/serializer/GenericPayloadSerializer.java b/src/com/isode/stroke/serializer/GenericPayloadSerializer.java new file mode 100644 index 0000000..3f5c92e --- /dev/null +++ b/src/com/isode/stroke/serializer/GenericPayloadSerializer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Payload; + +public abstract class GenericPayloadSerializer<T extends Payload> extends PayloadSerializer { + + private final Class class_; + + public GenericPayloadSerializer(Class c) { + class_ = c; + } + + @Override + public boolean canSerialize(Payload payload) { + return class_.isAssignableFrom(payload.getClass()); + } + + @Override + public String serialize(Payload payload) { + return serializePayload((T)payload); + } + + protected abstract String serializePayload(T payload); + +} diff --git a/src/com/isode/stroke/serializer/GenericStanzaSerializer.java b/src/com/isode/stroke/serializer/GenericStanzaSerializer.java new file mode 100644 index 0000000..89ac1d1 --- /dev/null +++ b/src/com/isode/stroke/serializer/GenericStanzaSerializer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.serializer.xml.XMLElement; + +public abstract class GenericStanzaSerializer<T extends Stanza> extends StanzaSerializer { + + private final Class stanzaClass_; + + GenericStanzaSerializer(Class stanzaClass, String tag, PayloadSerializerCollection payloadSerializers) { + super(tag, payloadSerializers); + stanzaClass_ = stanzaClass; + } + + public boolean canSerialize(Element element) { + return stanzaClass_.isAssignableFrom(element.getClass()); + } + + public void setStanzaSpecificAttributes(Element stanza, XMLElement element) { + setStanzaSpecificAttributesGeneric((T)stanza, element); + } + + abstract void setStanzaSpecificAttributesGeneric(T stanza, XMLElement element); +} diff --git a/src/com/isode/stroke/serializer/IQSerializer.java b/src/com/isode/stroke/serializer/IQSerializer.java new file mode 100644 index 0000000..0025aad --- /dev/null +++ b/src/com/isode/stroke/serializer/IQSerializer.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.IQ; +import com.isode.stroke.serializer.xml.XMLElement; + +public class IQSerializer extends GenericStanzaSerializer<IQ> { + + public IQSerializer(PayloadSerializerCollection payloadSerializers) { + super(IQ.class, "iq", payloadSerializers); + } + + @Override + void setStanzaSpecificAttributesGeneric(IQ iq, XMLElement element) { + switch (iq.getType()) { + case Get: + element.setAttribute("type", "get"); + break; + case Set: + element.setAttribute("type", "set"); + break; + case Result: + element.setAttribute("type", "result"); + break; + case Error: + element.setAttribute("type", "error"); + break; + } + } +} diff --git a/src/com/isode/stroke/serializer/MessageSerializer.java b/src/com/isode/stroke/serializer/MessageSerializer.java new file mode 100644 index 0000000..27d5c73 --- /dev/null +++ b/src/com/isode/stroke/serializer/MessageSerializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Message; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.serializer.xml.XMLElement; + +public class MessageSerializer extends GenericStanzaSerializer<Message>{ + + public MessageSerializer(PayloadSerializerCollection payloadSerializers) { + super(Message.class, "message", payloadSerializers); + } + + @Override + void setStanzaSpecificAttributesGeneric(Message message, XMLElement element) { + if (message.getType().equals(Message.Type.Chat)) { + element.setAttribute("type", "chat"); + } + else if (message.getType().equals(Message.Type.Groupchat)) { + element.setAttribute("type", "groupchat"); + } + else if (message.getType().equals(Message.Type.Headline)) { + element.setAttribute("type", "headline"); + } + else if (message.getType().equals(Message.Type.Error)) { + element.setAttribute("type", "error"); + } + } + +} diff --git a/src/com/isode/stroke/serializer/PayloadSerializer.java b/src/com/isode/stroke/serializer/PayloadSerializer.java new file mode 100644 index 0000000..2d15408 --- /dev/null +++ b/src/com/isode/stroke/serializer/PayloadSerializer.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Payload; + +/** + * Serialise a particular payload. + */ +public abstract class PayloadSerializer { + public abstract boolean canSerialize(Payload payload); + public abstract String serialize(Payload payload); +} diff --git a/src/com/isode/stroke/serializer/PayloadSerializerCollection.java b/src/com/isode/stroke/serializer/PayloadSerializerCollection.java new file mode 100644 index 0000000..49ffe41 --- /dev/null +++ b/src/com/isode/stroke/serializer/PayloadSerializerCollection.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Payload; +import java.util.Vector; + +public class PayloadSerializerCollection { + + private final Vector<PayloadSerializer> serializers_ = new Vector<PayloadSerializer>(); + + public void addSerializer(PayloadSerializer serializer) { + synchronized (serializers_) { + serializers_.add(serializer); + } + } + + public PayloadSerializer getPayloadSerializer(Payload payload) { + synchronized (serializers_) { + for (PayloadSerializer serializer : serializers_) { + if (serializer.canSerialize(payload)) { + return serializer; + } + } + } + return null; + } +} diff --git a/src/com/isode/stroke/serializer/PresenceSerializer.java b/src/com/isode/stroke/serializer/PresenceSerializer.java new file mode 100644 index 0000000..a7439f7 --- /dev/null +++ b/src/com/isode/stroke/serializer/PresenceSerializer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Presence; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.serializer.xml.XMLElement; + +public class PresenceSerializer extends GenericStanzaSerializer<Presence> { +public PresenceSerializer(PayloadSerializerCollection payloadSerializers) { + super(Presence.class, "presence", payloadSerializers); +} + + @Override + void setStanzaSpecificAttributesGeneric(Presence presence, XMLElement element) { + switch (presence.getType()) { + case Unavailable: element.setAttribute("type","unavailable"); break; + case Probe: element.setAttribute("type","probe"); break; + case Subscribe: element.setAttribute("type","subscribe"); break; + case Subscribed: element.setAttribute("type","subscribed"); break; + case Unsubscribe: element.setAttribute("type","unsubscribe"); break; + case Unsubscribed: element.setAttribute("type","unsubscribed"); break; + case Error: element.setAttribute("type","error"); break; + case Available: break; + } + } + + +} diff --git a/src/com/isode/stroke/serializer/StanzaAckRequestSerializer.java b/src/com/isode/stroke/serializer/StanzaAckRequestSerializer.java new file mode 100644 index 0000000..2721518 --- /dev/null +++ b/src/com/isode/stroke/serializer/StanzaAckRequestSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.StanzaAckRequest; +import com.isode.stroke.serializer.xml.XMLElement; + +class StanzaAckRequestSerializer extends GenericElementSerializer<StanzaAckRequest> { + + public StanzaAckRequestSerializer() { + super(StanzaAckRequest.class); + } + + public String serialize(Element element) { + return new XMLElement("r", "urn:xmpp:sm:2").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/StanzaAckSerializer.java b/src/com/isode/stroke/serializer/StanzaAckSerializer.java new file mode 100644 index 0000000..e8051a6 --- /dev/null +++ b/src/com/isode/stroke/serializer/StanzaAckSerializer.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.StanzaAck; +import com.isode.stroke.serializer.xml.XMLElement; + +class StanzaAckSerializer extends GenericElementSerializer<StanzaAck> { + + public StanzaAckSerializer() { + super(StanzaAck.class); + } + + public String serialize(Element element) { + StanzaAck stanzaAck = (StanzaAck) element; + assert stanzaAck.isValid(); + XMLElement result = new XMLElement("a", "urn:xmpp:sm:2"); + result.setAttribute("h", Double.toString(stanzaAck.getHandledStanzasCount())); + return result.serialize(); + } +} diff --git a/src/com/isode/stroke/serializer/StanzaSerializer.java b/src/com/isode/stroke/serializer/StanzaSerializer.java new file mode 100644 index 0000000..950ccf7 --- /dev/null +++ b/src/com/isode/stroke/serializer/StanzaSerializer.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.Payload; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.serializer.xml.XMLElement; +import com.isode.stroke.serializer.xml.XMLRawTextNode; +import java.util.logging.Logger; + +public abstract class StanzaSerializer implements ElementSerializer { + + private final String tag_; + private final PayloadSerializerCollection payloadSerializers_; + private final Logger logger_ = Logger.getLogger(this.getClass().getName()); + + public StanzaSerializer(String tag, PayloadSerializerCollection payloadSerializers) { + payloadSerializers_ = payloadSerializers; + tag_ = tag; + } + + public String serialize(Element element) { + assert element != null; + assert payloadSerializers_ != null; + Stanza stanza = (Stanza) element; + XMLElement stanzaElement = new XMLElement(tag_); + if (stanza.getFrom() != null && stanza.getFrom().isValid()) { + stanzaElement.setAttribute("from", stanza.getFrom().toString()); + } + if (stanza.getTo() != null && stanza.getTo().isValid()) { + stanzaElement.setAttribute("to", stanza.getTo().toString()); + } + if (stanza.getID() != null && (stanza.getID().length()!=0)) { + stanzaElement.setAttribute("id", stanza.getID()); + } + setStanzaSpecificAttributes(stanza, stanzaElement); + + StringBuilder serializedPayloads = new StringBuilder(); + for (Payload payload : stanza.getPayloads()) { + PayloadSerializer serializer = payloadSerializers_.getPayloadSerializer(payload); + if (serializer != null) { + serializedPayloads.append(serializer.serialize(payload)); + } else { + /*TODO: port*/ + assert false; + //std::cerr << "Could not find serializer for " << typeid(*(payload.get())).name() << std::endl; + } + } + if (serializedPayloads.toString().length()!=0) { + stanzaElement.addNode(new XMLRawTextNode(serializedPayloads.toString())); + } + return stanzaElement.serialize(); + } + + public abstract void setStanzaSpecificAttributes(Element element, XMLElement xmlElement); +} diff --git a/src/com/isode/stroke/serializer/StartTLSFailureSerializer.java b/src/com/isode/stroke/serializer/StartTLSFailureSerializer.java new file mode 100644 index 0000000..2de5193 --- /dev/null +++ b/src/com/isode/stroke/serializer/StartTLSFailureSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.StartTLSFailure; +import com.isode.stroke.serializer.xml.XMLElement; + +class StartTLSFailureSerializer extends GenericElementSerializer<StartTLSFailure> { + + public StartTLSFailureSerializer() { + super(StartTLSFailure.class); + } + + public String serialize(Element element) { + return new XMLElement("failure", "urn:ietf:params:xml:ns:xmpp-tls").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/StartTLSRequestSerializer.java b/src/com/isode/stroke/serializer/StartTLSRequestSerializer.java new file mode 100644 index 0000000..afa6b7b --- /dev/null +++ b/src/com/isode/stroke/serializer/StartTLSRequestSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.StartTLSRequest; +import com.isode.stroke.serializer.xml.XMLElement; + +class StartTLSRequestSerializer extends GenericElementSerializer<StartTLSRequest> { + + public StartTLSRequestSerializer() { + super(StartTLSRequest.class); + } + + public String serialize(Element element) { + return new XMLElement("starttls", "urn:ietf:params:xml:ns:xmpp-tls").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/StreamManagementEnabledSerializer.java b/src/com/isode/stroke/serializer/StreamManagementEnabledSerializer.java new file mode 100644 index 0000000..84b127b --- /dev/null +++ b/src/com/isode/stroke/serializer/StreamManagementEnabledSerializer.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.StreamManagementEnabled; +import com.isode.stroke.serializer.xml.XMLElement; + +class StreamManagementEnabledSerializer extends GenericElementSerializer<StreamManagementEnabled> { + + public StreamManagementEnabledSerializer() { + super(StreamManagementEnabled.class); + } + + public String serialize(Element el) { + StreamManagementEnabled e = (StreamManagementEnabled) el; + XMLElement element = new XMLElement("enabled", "urn:xmpp:sm:2"); + if (!e.getResumeID().isEmpty()) { + element.setAttribute("id", e.getResumeID()); + } + if (e.getResumeSupported()) { + element.setAttribute("resume", "true"); + } + return element.serialize(); + } +} diff --git a/src/com/isode/stroke/serializer/StreamManagementFailedSerializer.java b/src/com/isode/stroke/serializer/StreamManagementFailedSerializer.java new file mode 100644 index 0000000..7f4fb6b --- /dev/null +++ b/src/com/isode/stroke/serializer/StreamManagementFailedSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.StreamManagementFailed; +import com.isode.stroke.serializer.xml.XMLElement; + +class StreamManagementFailedSerializer extends GenericElementSerializer<StreamManagementFailed> { + + public StreamManagementFailedSerializer() { + super(StreamManagementFailed.class); + } + + public String serialize(Element element) { + return new XMLElement("failed", "urn:xmpp:sm:2").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/StreamResumeSerializer.java b/src/com/isode/stroke/serializer/StreamResumeSerializer.java new file mode 100644 index 0000000..9e40f44 --- /dev/null +++ b/src/com/isode/stroke/serializer/StreamResumeSerializer.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2011, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.StreamResume; +import com.isode.stroke.serializer.xml.XMLElement; + +class StreamResumeSerializer extends GenericElementSerializer<StreamResume> { + + public StreamResumeSerializer() { + super(StreamResume.class); + } + + public String serialize(Element el) { + StreamResume e = (StreamResume)el; + XMLElement element = new XMLElement("resume", "urn:xmpp:sm:2"); + element.setAttribute("previd", e.getResumeID()); + if (e.getHandledStanzasCount() != null) { + element.setAttribute("h", Long.toString(e.getHandledStanzasCount())); + } + return element.serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/StreamResumedSerializer.java b/src/com/isode/stroke/serializer/StreamResumedSerializer.java new file mode 100644 index 0000000..8fe6f53 --- /dev/null +++ b/src/com/isode/stroke/serializer/StreamResumedSerializer.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2011, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.StreamResumed; +import com.isode.stroke.serializer.xml.XMLElement; + +class StreamResumedSerializer extends GenericElementSerializer<StreamResumed> { + + public StreamResumedSerializer() { + super(StreamResumed.class); + } + + public String serialize(Element el) { + StreamResumed e = (StreamResumed)el; + XMLElement element = new XMLElement("resumed", "urn:xmpp:sm:2"); + element.setAttribute("previd", e.getResumeID()); + if (e.getHandledStanzasCount() != null) { + element.setAttribute("h", Long.toString(e.getHandledStanzasCount())); + } + return element.serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/TLSProceedSerializer.java b/src/com/isode/stroke/serializer/TLSProceedSerializer.java new file mode 100644 index 0000000..ca73456 --- /dev/null +++ b/src/com/isode/stroke/serializer/TLSProceedSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.TLSProceed; +import com.isode.stroke.serializer.xml.XMLElement; + +class TLSProceedSerializer extends GenericElementSerializer<TLSProceed>{ + + public TLSProceedSerializer() { + super(TLSProceed.class); + } + + public String serialize(Element element) { + return new XMLElement("proceed", "urn:ietf:params:xml:ns:xmpp-tls").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/XMPPSerializer.java b/src/com/isode/stroke/serializer/XMPPSerializer.java new file mode 100644 index 0000000..b51a4dc --- /dev/null +++ b/src/com/isode/stroke/serializer/XMPPSerializer.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer; + +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.ProtocolHeader; +import com.isode.stroke.elements.StreamType; +import java.util.Vector; + +public class XMPPSerializer { + + private final Vector<ElementSerializer> serializers_ = new Vector<ElementSerializer>(); + private final StreamType type_; + + public XMPPSerializer(PayloadSerializerCollection payloadSerializers, StreamType type) { + type_ = type; + serializers_.add(new PresenceSerializer(payloadSerializers)); + serializers_.add(new IQSerializer(payloadSerializers)); + serializers_.add(new MessageSerializer(payloadSerializers)); + serializers_.add(new CompressRequestSerializer()); + serializers_.add(new CompressFailureSerializer()); + serializers_.add(new AuthRequestSerializer()); + serializers_.add(new AuthFailureSerializer()); + serializers_.add(new AuthSuccessSerializer()); + serializers_.add(new AuthChallengeSerializer()); + serializers_.add(new AuthResponseSerializer()); + serializers_.add(new StartTLSRequestSerializer()); + serializers_.add(new StartTLSFailureSerializer()); + serializers_.add(new TLSProceedSerializer()); + //serializers_.add(new StreamFeaturesSerializer()); //TODO: Port + //serializers_.add(new StreamErrorSerializer()); //FIXME!!!: Port + serializers_.add(new EnableStreamManagementSerializer()); + serializers_.add(new StreamManagementEnabledSerializer()); + serializers_.add(new StreamManagementFailedSerializer()); + serializers_.add(new StreamResumeSerializer()); + serializers_.add(new StreamResumedSerializer()); + serializers_.add(new StanzaAckSerializer()); + serializers_.add(new StanzaAckRequestSerializer()); + //serializers_.add(new ComponentHandshakeSerializer()); + } + + public String serializeHeader(ProtocolHeader header) { + String result = "<?xml version=\"1.0\"?><stream:stream xmlns=\"" + getDefaultNamespace() + "\" xmlns:stream=\"http://etherx.jabber.org/streams\""; + if (header.getFrom().length() != 0) { + result += " from=\"" + header.getFrom() + "\""; + } + if (header.getTo().length() != 0) { + result += " to=\"" + header.getTo() + "\""; + } + if (header.getID().length() != 0) { + result += " id=\"" + header.getID() + "\""; + } + if (header.getVersion().length() != 0) { + result += " version=\"" + header.getVersion() + "\""; + } + result += ">"; + return result; + } + + public String serializeFooter() { + return "</stream:stream>"; + } + + public String getDefaultNamespace() { + switch (type_) { + case ClientStreamType: + return "jabber:client"; + case ServerStreamType: + return "jabber:server"; + case ComponentStreamType: + return "jabber:component:accept"; + } + assert false; + return ""; + } + + public String serializeElement(Element element) { + for (ElementSerializer serializer : serializers_) { + if (serializer.canSerialize(element)) { + return serializer.serialize(element); + } + } + throw new IllegalStateException("Trying to send an unknown element"); + //assert false; /* UNKNOWN ELEMENT */ + //return ""; + } +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/BodySerializer.java b/src/com/isode/stroke/serializer/payloadserializers/BodySerializer.java new file mode 100644 index 0000000..31037db --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/BodySerializer.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.Body; +import com.isode.stroke.serializer.GenericPayloadSerializer; +import com.isode.stroke.serializer.xml.XMLTextNode; + +/** + * Body to String. + */ +public class BodySerializer extends GenericPayloadSerializer<Body> { + + public BodySerializer() { + super(Body.class); + } + + @Override + protected String serializePayload(Body body) { + XMLTextNode textNode = new XMLTextNode(body.getText()); + return "<body>" + textNode.serialize() + "</body>"; + } +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/ErrorSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/ErrorSerializer.java new file mode 100644 index 0000000..9c34d53 --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/ErrorSerializer.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.serializer.GenericPayloadSerializer; +import com.isode.stroke.serializer.xml.XMLTextNode; + +class ErrorSerializer extends GenericPayloadSerializer<ErrorPayload> { + + public ErrorSerializer() { + super(ErrorPayload.class); + } + + @Override + protected String serializePayload(ErrorPayload error) { + String result = "<error type=\""; + switch (error.getType()) { + case Continue: result += "continue"; break; + case Modify: result += "modify"; break; + case Auth: result += "auth"; break; + case Wait: result += "wait"; break; + default: result += "cancel"; break; + } + result += "\">"; + + String conditionElement; + switch (error.getCondition()) { + case BadRequest: conditionElement = "bad-request"; break; + case Conflict: conditionElement = "conflict"; break; + case FeatureNotImplemented: conditionElement = "feature-not-implemented"; break; + case Forbidden: conditionElement = "forbidden"; break; + case Gone: conditionElement = "gone"; break; + case InternalServerError: conditionElement = "internal-server-error"; break; + case ItemNotFound: conditionElement = "item-not-found"; break; + case JIDMalformed: conditionElement = "jid-malformed"; break; + case NotAcceptable: conditionElement = "not-acceptable"; break; + case NotAllowed: conditionElement = "not-allowed"; break; + case NotAuthorized: conditionElement = "not-authorized"; break; + case PaymentRequired: conditionElement = "payment-required"; break; + case RecipientUnavailable: conditionElement = "recipient-unavailable"; break; + case Redirect: conditionElement = "redirect"; break; + case RegistrationRequired: conditionElement = "registration-required"; break; + case RemoteServerNotFound: conditionElement = "remote-server-not-found"; break; + case RemoteServerTimeout: conditionElement = "remote-server-timeout"; break; + case ResourceConstraint: conditionElement = "resource-constraint"; break; + case ServiceUnavailable: conditionElement = "service-unavailable"; break; + case SubscriptionRequired: conditionElement = "subscription-required"; break; + case UnexpectedRequest: conditionElement = "unexpected-request"; break; + default: conditionElement = "undefined-condition"; break; + } + result += "<" + conditionElement + " xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/>"; + + if (error.getText().length() != 0) { + XMLTextNode textNode = new XMLTextNode(error.getText()); + result += "<text xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\">" + textNode.serialize() + "</text>"; + } + + result += "</error>"; + return result; + } + +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/FullPayloadSerializerCollection.java b/src/com/isode/stroke/serializer/payloadserializers/FullPayloadSerializerCollection.java new file mode 100644 index 0000000..52de35b --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/FullPayloadSerializerCollection.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.serializer.PayloadSerializerCollection; + +public class FullPayloadSerializerCollection extends PayloadSerializerCollection { + + public FullPayloadSerializerCollection() { + /*FIXME: Implement what's needed. */ + //addSerializer(new IBBSerializer()); + addSerializer(new BodySerializer()); + //addSerializer(new SubjectSerializer()); + //addSerializer(new ChatStateSerializer()); + //addSerializer(new PrioritySerializer()); + addSerializer(new ErrorSerializer()); + addSerializer(new RosterSerializer()); + //addSerializer(new MUCPayloadSerializer()); + //addSerializer(new MUCUserPayloadSerializer()); + //addSerializer(new MUCOwnerPayloadSerializer(this)); + addSerializer(new SoftwareVersionSerializer()); + //addSerializer(new StatusSerializer()); + //addSerializer(new StatusShowSerializer()); + //addSerializer(new DiscoInfoSerializer()); + //addSerializer(new DiscoItemsSerializer()); + //addSerializer(new CapsInfoSerializer()); + addSerializer(new ResourceBindSerializer()); + addSerializer(new StartSessionSerializer()); + //addSerializer(new SecurityLabelSerializer()); + //addSerializer(new SecurityLabelsCatalogSerializer()); + //addSerializer(new StreamInitiationSerializer()); + //addSerializer(new BytestreamsSerializer()); + //addSerializer(new VCardSerializer()); + //addSerializer(new VCardUpdateSerializer()); + addSerializer(new RawXMLPayloadSerializer()); + //addSerializer(new StorageSerializer()); + //addSerializer(new DelaySerializer()); + //addSerializer(new FormSerializer()); + //addSerializer(new PrivateStorageSerializer(this)); + //addSerializer(new CommandSerializer()); + //addSerializer(new NicknameSerializer()); + addSerializer(new SearchPayloadSerializer()); + addSerializer(new LastSerializer()); + } + +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/LastSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/LastSerializer.java new file mode 100644 index 0000000..e78a724 --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/LastSerializer.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2011 Kevin Smith + * All rights reserved. + */ +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.Last; +import com.isode.stroke.serializer.GenericPayloadSerializer; + +public class LastSerializer extends GenericPayloadSerializer<Last> { + + public LastSerializer() { + super(Last.class); + } + + @Override + protected String serializePayload(Last last) { + return "<query xmlns='jabber:iq:last' seconds='" + Integer.toString(last.getSeconds()) + "'/>"; + } +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/RawXMLPayloadSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/RawXMLPayloadSerializer.java new file mode 100644 index 0000000..4128537 --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/RawXMLPayloadSerializer.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.RawXMLPayload; +import com.isode.stroke.serializer.GenericPayloadSerializer; + +class RawXMLPayloadSerializer extends GenericPayloadSerializer<RawXMLPayload> { + + public RawXMLPayloadSerializer() { + super(RawXMLPayloadSerializer.class); + } + + @Override + protected String serializePayload(RawXMLPayload payload) { + return payload.getRawXML(); + } + +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/ResourceBindSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/ResourceBindSerializer.java new file mode 100644 index 0000000..a33627b --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/ResourceBindSerializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.ResourceBind; +import com.isode.stroke.serializer.GenericPayloadSerializer; +import com.isode.stroke.serializer.xml.XMLElement; +import com.isode.stroke.serializer.xml.XMLTextNode; + +class ResourceBindSerializer extends GenericPayloadSerializer<ResourceBind> { + + public ResourceBindSerializer() { + super(ResourceBind.class); + } + + @Override + protected String serializePayload(ResourceBind resourceBind) { + XMLElement bindElement = new XMLElement("bind", "urn:ietf:params:xml:ns:xmpp-bind"); + if (resourceBind.getJID().isValid()) { + XMLElement jidNode = new XMLElement("jid"); + jidNode.addNode(new XMLTextNode(resourceBind.getJID().toString())); + bindElement.addNode(jidNode); + } + else if (resourceBind.getResource().length() != 0) { + XMLElement resourceNode = new XMLElement("resource"); + resourceNode.addNode(new XMLTextNode(resourceBind.getResource())); + bindElement.addNode(resourceNode); + } + return bindElement.serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/RosterSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/RosterSerializer.java new file mode 100644 index 0000000..08a4b8e --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/RosterSerializer.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.RosterItemPayload; +import com.isode.stroke.elements.RosterPayload; +import com.isode.stroke.serializer.GenericPayloadSerializer; +import com.isode.stroke.serializer.xml.XMLElement; +import com.isode.stroke.serializer.xml.XMLTextNode; + +/** + * Roster to string. + */ +public class RosterSerializer extends GenericPayloadSerializer<RosterPayload> { + + public RosterSerializer() { + super(RosterPayload.class); + } + + @Override + protected String serializePayload(RosterPayload roster) { + XMLElement queryElement = new XMLElement("query", "jabber:iq:roster"); + for (RosterItemPayload item : roster.getItems()) { + XMLElement itemElement = new XMLElement("item"); + itemElement.setAttribute("jid", item.getJID().toString()); + if (item.getName() != null) { + itemElement.setAttribute("name", item.getName()); + } + + if (item.getSubscription() != null) { + switch (item.getSubscription()) { + case To: itemElement.setAttribute("subscription", "to"); break; + case From: itemElement.setAttribute("subscription", "from"); break; + case Both: itemElement.setAttribute("subscription", "both"); break; + case Remove: itemElement.setAttribute("subscription", "remove"); break; + case None: itemElement.setAttribute("subscription", "none"); break; + } + } + + if (item.getSubscriptionRequested()) { + itemElement.setAttribute("ask", "subscribe"); + } + + for (String group : item.getGroups()) { + XMLElement groupElement = new XMLElement("group"); + groupElement.addNode(new XMLTextNode(group)); + itemElement.addNode(groupElement); + } + + queryElement.addNode(itemElement); + } + + return queryElement.serialize(); + + + + } +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/SearchPayloadSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/SearchPayloadSerializer.java new file mode 100644 index 0000000..6928c97 --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/SearchPayloadSerializer.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.SearchPayload; +import com.isode.stroke.serializer.GenericPayloadSerializer; +import com.isode.stroke.serializer.xml.XMLElement; +import com.isode.stroke.serializer.xml.XMLTextNode; + +/** + * SearchPayload to String. + */ +public class SearchPayloadSerializer extends GenericPayloadSerializer<SearchPayload> { + + public SearchPayloadSerializer() { + super(SearchPayload.class); + } + + @Override + protected String serializePayload(SearchPayload searchPayload) { + XMLElement searchElement = new XMLElement("query", "jabber:iq:search"); + + if (searchPayload.getInstructions() != null) { + searchElement.addNode(new XMLElement("instructions", "", searchPayload.getInstructions())); + } + + if (searchPayload.getNick() != null) { + searchElement.addNode(new XMLElement("nick", "", searchPayload.getNick())); + } + + if (searchPayload.getFirst() != null) { + searchElement.addNode(new XMLElement("first", "", searchPayload.getFirst())); + } + + if (searchPayload.getLast() != null) { + searchElement.addNode(new XMLElement("last", "", searchPayload.getLast())); + } + + if (searchPayload.getEMail() != null) { + searchElement.addNode(new XMLElement("email", "", searchPayload.getEMail())); + } + + for (SearchPayload.Item item : searchPayload.getItems()) { + XMLElement itemElement = new XMLElement("item"); + itemElement.setAttribute("jid", item.jid.toString()); + itemElement.addNode(new XMLElement("first", "", item.first)); + itemElement.addNode(new XMLElement("last", "", item.last)); + itemElement.addNode(new XMLElement("nick", "", item.nick)); + itemElement.addNode(new XMLElement("email", "", item.email)); + + searchElement.addNode(itemElement); + } + + //if (Form::ref form = searchPayload->getForm()) { + // searchElement.addNode(boost::shared_ptr<XMLRawTextNode>(new XMLRawTextNode(FormSerializer().serialize(form)))); + //} /* Not ported yet. When the time comes, look at Swiften to check if it's changed. It will have. */ + + return searchElement.serialize(); + } +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/SoftwareVersionSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/SoftwareVersionSerializer.java new file mode 100644 index 0000000..cb2b653 --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/SoftwareVersionSerializer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * All rights reserved. + */ +/* + * Copyright (c) 2010 Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.Version; +import com.isode.stroke.serializer.GenericPayloadSerializer; + +public class SoftwareVersionSerializer extends GenericPayloadSerializer<Version>{ + + public SoftwareVersionSerializer() { + super(Version.class); + } + + @Override + protected String serializePayload(Version version) { + StringBuilder result = new StringBuilder(); + result.append("<query xmlns=\"jabber:iq:version\">"); + if (version.getName() != null && version.getName().length() > 0) { + result.append("<name>").append(version.getName()).append("</name>"); + } + if (version.getVersion() != null && version.getVersion().length() > 0) { + result.append("<version>").append(version.getVersion()).append("</version>"); + } + if (version.getOS() != null && version.getOS().length() > 0) { + result.append("<os>").append(version.getOS()).append("</os>"); + } + result.append("</query>"); + return result.toString(); + + } + +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/StartSessionSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/StartSessionSerializer.java new file mode 100644 index 0000000..8d36b8c --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/StartSessionSerializer.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.StartSession; +import com.isode.stroke.serializer.GenericPayloadSerializer; +import com.isode.stroke.serializer.xml.XMLElement; + +class StartSessionSerializer extends GenericPayloadSerializer<StartSession> { + + public StartSessionSerializer() { + super(StartSession.class); + } + + @Override + protected String serializePayload(StartSession payload) { + return new XMLElement("session", "urn:ietf:params:xml:ns:xmpp-session").serialize(); + } + +} diff --git a/src/com/isode/stroke/serializer/xml/XMLElement.java b/src/com/isode/stroke/serializer/xml/XMLElement.java new file mode 100644 index 0000000..909f277 --- /dev/null +++ b/src/com/isode/stroke/serializer/xml/XMLElement.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer.xml; + +import java.util.HashMap; +import java.util.Vector; + +public class XMLElement implements XMLNode { + + private final String tag_; + private final HashMap<String, String> attributes_ = new HashMap<String, String>(); + private final Vector<XMLNode> childNodes_ = new Vector<XMLNode>(); + + public XMLElement(String tag) { + this(tag, ""); + } + + public XMLElement(String tag, String xmlns) { + tag_ = tag; + if (xmlns.length()!=0) { + setAttribute("xmlns", xmlns); + } + } + + public XMLElement(String tag, String xmlns, String text) { + this(tag, xmlns); + if (text.length() > 0) { + addNode(new XMLTextNode(text)); + } + } + + public String serialize() { + String result = ""; + result += "<" + tag_; + for (String key : attributes_.keySet()) { + result += " " + key + "=\"" + attributes_.get(key) + "\""; + } + + if (childNodes_.size() > 0) { + result += ">"; + for (XMLNode node : childNodes_) { + result += node.serialize(); + } + result += "</" + tag_ + ">"; + } else { + result += "/>"; + } + return result; + } + + public void setAttribute(String attribute, String value) { + String escapedValue = value; + escapedValue.replaceAll("&", "&"); + escapedValue.replaceAll("<", "<"); + escapedValue.replaceAll(">", ">"); + escapedValue.replaceAll("'", "'"); + escapedValue.replaceAll("\"", """); + attributes_.put(attribute, escapedValue); + } + + public void addNode(XMLNode node) { + childNodes_.add(node); + } +} diff --git a/src/com/isode/stroke/serializer/xml/XMLNode.java b/src/com/isode/stroke/serializer/xml/XMLNode.java new file mode 100644 index 0000000..0b64f85 --- /dev/null +++ b/src/com/isode/stroke/serializer/xml/XMLNode.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.serializer.xml; + +public interface XMLNode { + public String serialize(); +} diff --git a/src/com/isode/stroke/serializer/xml/XMLRawTextNode.java b/src/com/isode/stroke/serializer/xml/XMLRawTextNode.java new file mode 100644 index 0000000..ad726bc --- /dev/null +++ b/src/com/isode/stroke/serializer/xml/XMLRawTextNode.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.serializer.xml; + +public class XMLRawTextNode implements XMLNode { + private final String text_; + + public XMLRawTextNode(String text) { + text_ = text; + } + + public String serialize() { + return text_; + } +} diff --git a/src/com/isode/stroke/serializer/xml/XMLTextNode.java b/src/com/isode/stroke/serializer/xml/XMLTextNode.java new file mode 100644 index 0000000..af11c27 --- /dev/null +++ b/src/com/isode/stroke/serializer/xml/XMLTextNode.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.serializer.xml; + +public class XMLTextNode implements XMLNode { + + private final String text_; + + public XMLTextNode(String text) { + text_ = text; + text_.replaceAll("&", "&"); // Should come first + text_.replaceAll("<", "<"); + text_.replaceAll(">", ">"); + } + + public String serialize() { + return text_; + } +} diff --git a/src/com/isode/stroke/session/BasicSessionStream.java b/src/com/isode/stroke/session/BasicSessionStream.java new file mode 100644 index 0000000..c7c98cf --- /dev/null +++ b/src/com/isode/stroke/session/BasicSessionStream.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.session; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.ProtocolHeader; +import com.isode.stroke.elements.StreamType; +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.network.Connection; +import com.isode.stroke.network.TimerFactory; +import com.isode.stroke.parser.PayloadParserFactoryCollection; +import com.isode.stroke.serializer.PayloadSerializerCollection; +import com.isode.stroke.signals.Slot; +import com.isode.stroke.signals.Slot1; +import com.isode.stroke.streamstack.CompressionLayer; +import com.isode.stroke.streamstack.ConnectionLayer; +import com.isode.stroke.streamstack.StreamStack; +import com.isode.stroke.streamstack.TLSLayer; +import com.isode.stroke.streamstack.WhitespacePingLayer; +import com.isode.stroke.tls.TLSContextFactory; +import com.isode.stroke.streamstack.XMPPLayer; +import com.isode.stroke.tls.Certificate; +import com.isode.stroke.tls.CertificateVerificationError; + +public class BasicSessionStream extends SessionStream { + + public BasicSessionStream( + StreamType streamType, + Connection connection, + PayloadParserFactoryCollection payloadParserFactories, + PayloadSerializerCollection payloadSerializers, + TLSContextFactory tlsContextFactory, + TimerFactory timerFactory, + EventLoop eventLoop) { + available = false; + this.connection = connection; + this.payloadParserFactories = payloadParserFactories; + this.payloadSerializers = payloadSerializers; + this.tlsContextFactory = tlsContextFactory; + this.timerFactory = timerFactory; + if (timerFactory == null) { + throw new IllegalStateException(); //FIXME: remove conditional, debugging only. + } + this.streamType = streamType; + this.compressionLayer = null; + this.tlsLayer = null; + this.whitespacePingLayer = null; + + xmppLayer = new XMPPLayer(payloadParserFactories, payloadSerializers, streamType, eventLoop); + xmppLayer.onStreamStart.connect(new Slot1<ProtocolHeader>() { + + public void call(ProtocolHeader p1) { + handleStreamStartReceived(p1); + } + }); + xmppLayer.onElement.connect(new Slot1<Element>() { + + public void call(Element p1) { + handleElementReceived(p1); + } + }); + xmppLayer.onError.connect(new Slot() { + + public void call() { + handleXMPPError(); + } + }); + xmppLayer.onDataRead.connect(new Slot1<ByteArray>() { + + public void call(ByteArray p1) { + handleDataRead(p1); + } + }); + xmppLayer.onWriteData.connect(new Slot1<ByteArray>() { + + public void call(ByteArray p1) { + handleDataWritten(p1); + } + }); + + connection.onDisconnected.connect(new Slot1<Connection.Error>() { + + public void call(Connection.Error p1) { + handleConnectionFinished(p1); + } + }); + connectionLayer = new ConnectionLayer(connection); + + streamStack = new StreamStack(xmppLayer, connectionLayer); + + available = true; + + } + + public void writeHeader(ProtocolHeader header) { + assert available; + xmppLayer.writeHeader(header); + } + + public void writeElement(Element element) { + assert available; + xmppLayer.writeElement(element); + } + + public void writeFooter() { + assert available; + xmppLayer.writeFooter(); + } + + public void writeData(String data) { + assert available; + xmppLayer.writeData(data); + } + + public void close() { + connection.disconnect(); + } + + public boolean isOpen() { + return available; + } + + public boolean supportsTLSEncryption() { + return tlsContextFactory != null && tlsContextFactory.canCreate(); + } + + public void addTLSEncryption() { + assert available; + tlsLayer = new TLSLayer(tlsContextFactory); + if (hasTLSCertificate() && !tlsLayer.setClientCertificate(getTLSCertificate())) { + onClosed.emit(new Error(Error.Type.InvalidTLSCertificateError)); + } else { + streamStack.addLayer(tlsLayer); + tlsLayer.onError.connect(new Slot() { + + public void call() { + handleTLSError(); + } + }); + tlsLayer.onConnected.connect(new Slot() { + + public void call() { + handleTLSConnected(); + } + }); + tlsLayer.connect(); + } + } + + public boolean isTLSEncrypted() { + return tlsLayer != null; + } + + public Certificate getPeerCertificate() { + return tlsLayer.getPeerCertificate(); + } + + public CertificateVerificationError getPeerCertificateVerificationError() { + return tlsLayer.getPeerCertificateVerificationError(); + } + + public ByteArray getTLSFinishMessage() { + return tlsLayer.getContext().getFinishMessage(); + } + + public void addZLibCompression() { + compressionLayer = new CompressionLayer(); + streamStack.addLayer(compressionLayer); + } + + public void setWhitespacePingEnabled(boolean enabled) { + if (enabled) { + if (whitespacePingLayer == null) { + whitespacePingLayer = new WhitespacePingLayer(timerFactory); + streamStack.addLayer(whitespacePingLayer); + } + whitespacePingLayer.setActive(); + } + else if (whitespacePingLayer != null) { + whitespacePingLayer.setInactive(); + } + } + + public void resetXMPPParser() { + xmppLayer.resetParser(); + } + + private void handleStreamStartReceived(ProtocolHeader header) { + onStreamStartReceived.emit(header); + } + + private void handleElementReceived(Element element) { + onElementReceived.emit(element); + } + + private void handleXMPPError() { + available = false; + onClosed.emit(new Error(Error.Type.ParseError)); + } + + private void handleTLSConnected() { + onTLSEncrypted.emit(); + } + + private void handleTLSError() { + available = false; + onClosed.emit(new Error(Error.Type.TLSError)); + } + + private void handleConnectionFinished(Connection.Error error) { + available = false; + if (Connection.Error.ReadError.equals(error)) { + onClosed.emit(new Error(Error.Type.ConnectionReadError)); + } + else if (error != null) { + onClosed.emit(new Error(Error.Type.ConnectionWriteError)); + } + else { + onClosed.emit(null); + } + } + + private void handleDataRead(ByteArray data) { + onDataRead.emit(data.toString()); + } + + private void handleDataWritten(ByteArray data) { + onDataWritten.emit(data.toString()); + } + private boolean available; + private Connection connection; + private PayloadParserFactoryCollection payloadParserFactories; + private PayloadSerializerCollection payloadSerializers; + private TLSContextFactory tlsContextFactory; + private TimerFactory timerFactory; + private StreamType streamType; + private XMPPLayer xmppLayer; + private ConnectionLayer connectionLayer; + private CompressionLayer compressionLayer; + private TLSLayer tlsLayer; + private WhitespacePingLayer whitespacePingLayer; + private StreamStack streamStack; + +} diff --git a/src/com/isode/stroke/session/Session.java b/src/com/isode/stroke/session/Session.java new file mode 100644 index 0000000..79132e4 --- /dev/null +++ b/src/com/isode/stroke/session/Session.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.session; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.ProtocolHeader; +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.parser.PayloadParserFactoryCollection; +import com.isode.stroke.serializer.PayloadSerializerCollection; +import com.isode.stroke.signals.Signal1; +import com.isode.stroke.signals.Slot; +import com.isode.stroke.signals.Slot1; +import com.isode.stroke.streamstack.ConnectionLayer; +import com.isode.stroke.streamstack.StreamStack; +import com.isode.stroke.streamstack.XMPPLayer; + +public abstract class Session { + + public enum SessionError { + + ConnectionReadError, + ConnectionWriteError, + XMLError, + AuthenticationFailedError, + NoSupportedAuthMechanismsError, + UnexpectedElementError, + ResourceBindError, + SessionStartError, + TLSError, + ClientCertificateLoadError, + ClientCertificateError + }; + + public Session( + final Connection connection, + final PayloadParserFactoryCollection payloadParserFactories, + final PayloadSerializerCollection payloadSerializers, + final EventLoop eventLoop) { + this.connection = connection; + this.eventLoop = eventLoop; + this.payloadParserFactories = payloadParserFactories; + this.payloadSerializers = payloadSerializers; + finishing = false; + } + + + public void startSession() { + initializeStreamStack(); + handleSessionStarted(); + } + + public void finishSession() { + finishing = true; + connection.disconnect(); + handleSessionFinished(null); + finishing = false; + onSessionFinished.emit(null); + } + + public void sendElement(Element stanza) { + xmppLayer.writeElement(stanza); + } + + public JID getLocalJID() { + return localJID; + } + + public JID getRemoteJID() { + return remoteJID; + } + public final Signal1<Element> onElementReceived = new Signal1<Element>(); + public final Signal1<SessionError> onSessionFinished = new Signal1<SessionError>(); + public final Signal1<ByteArray> onDataWritten = new Signal1<ByteArray>(); + public final Signal1<ByteArray> onDataRead = new Signal1<ByteArray>(); + + protected void setRemoteJID(JID j) { + remoteJID = j; + } + + protected void setLocalJID(JID j) { + localJID = j; + } + + protected void finishSession(SessionError error) { + finishing = true; + connection.disconnect(); + handleSessionFinished(error); + finishing = false; + onSessionFinished.emit(error); + } + + protected void handleSessionStarted() { + } + + protected void handleSessionFinished(SessionError error) { + } + + protected abstract void handleElement(Element element); + + protected abstract void handleStreamStart(ProtocolHeader header); + + protected void initializeStreamStack() { + xmppLayer = new XMPPLayer(payloadParserFactories, payloadSerializers, StreamType.ClientStreamType, eventLoop); + xmppLayer.onStreamStart.connect(new Slot1<ProtocolHeader>() { + + public void call(ProtocolHeader header) { + handleStreamStart(header); + } + }); + xmppLayer.onElement.connect(new Slot1<Element>() { + + public void call(Element p1) { + handleElement(p1); + } + }); + xmppLayer.onError.connect(new Slot() { + + public void call() { + finishSession(SessionError.XMLError); + } + }); + xmppLayer.onDataRead.connect(onDataRead); + xmppLayer.onWriteData.connect(onDataWritten); + connection.onDisconnected.connect(new Slot1<Connection.Error>() { + + public void call(Connection.Error p1) { + handleDisconnected(p1); + } + }); + connectionLayer = new ConnectionLayer(connection); + streamStack = new StreamStack(xmppLayer, connectionLayer); + } + + public XMPPLayer getXMPPLayer() { + return xmppLayer; + + + } + + public StreamStack getStreamStack() { + return streamStack; + + + } + + /*void setFinished();*/ /* This seems to be unused in Swiften*/ + + private void handleDisconnected(Connection.Error connectionError) { + if (connectionError != null) { + switch (connectionError) { + case ReadError: + finishSession(SessionError.ConnectionReadError); + break; + case WriteError: + finishSession(SessionError.ConnectionWriteError); + break; + } + } else { + finishSession(); + } + } + private JID localJID; + private JID remoteJID; + private Connection connection; + private PayloadParserFactoryCollection payloadParserFactories; + private PayloadSerializerCollection payloadSerializers; + private XMPPLayer xmppLayer; + private ConnectionLayer connectionLayer; + private StreamStack streamStack; + private boolean finishing; + private final EventLoop eventLoop; +} diff --git a/src/com/isode/stroke/session/SessionStream.java b/src/com/isode/stroke/session/SessionStream.java new file mode 100644 index 0000000..c70622b --- /dev/null +++ b/src/com/isode/stroke/session/SessionStream.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.session; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.ProtocolHeader; +import com.isode.stroke.signals.Signal; +import com.isode.stroke.signals.Signal1; +import com.isode.stroke.tls.Certificate; +import com.isode.stroke.tls.CertificateVerificationError; +import com.isode.stroke.tls.PKCS12Certificate; + +public abstract class SessionStream { + + public static class Error implements com.isode.stroke.base.Error { + + public enum Type { + + ParseError, + TLSError, + InvalidTLSCertificateError, + ConnectionReadError, + ConnectionWriteError + }; + + public Error(Type type) { + this.type = type; + } + public final Type type; + }; + + public abstract void close(); + + public abstract boolean isOpen(); + + public abstract void writeHeader(ProtocolHeader header); + + public abstract void writeFooter(); + + public abstract void writeElement(Element element); + + public abstract void writeData(String data); + + public abstract void addZLibCompression(); + + public abstract boolean supportsTLSEncryption(); + + public abstract void addTLSEncryption(); + + public abstract boolean isTLSEncrypted(); + + public abstract void setWhitespacePingEnabled(boolean enabled); + + public abstract void resetXMPPParser(); + + public void setTLSCertificate(PKCS12Certificate cert) { + certificate = cert; + } + + public boolean hasTLSCertificate() { + return certificate != null && !certificate.isNull(); + } + + public abstract Certificate getPeerCertificate(); + + public abstract CertificateVerificationError getPeerCertificateVerificationError(); + + public abstract ByteArray getTLSFinishMessage(); + + public final Signal1<ProtocolHeader> onStreamStartReceived = new Signal1<ProtocolHeader>(); + public final Signal1<Element> onElementReceived = new Signal1<Element>(); + public final Signal1<Error> onClosed = new Signal1<Error>(); + public final Signal onTLSEncrypted = new Signal(); + public final Signal1<String> onDataRead = new Signal1<String>(); + public final Signal1<String> onDataWritten = new Signal1<String>(); + protected PKCS12Certificate getTLSCertificate() { + return certificate; + } + private PKCS12Certificate certificate; +} diff --git a/src/com/isode/stroke/session/SessionTracer.java b/src/com/isode/stroke/session/SessionTracer.java new file mode 100644 index 0000000..93c51a8 --- /dev/null +++ b/src/com/isode/stroke/session/SessionTracer.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.session; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.signals.Slot1; + +public class SessionTracer { + + public SessionTracer(Session session) { + this.session = session; + session.onDataRead.connect(new Slot1<ByteArray>() { + + public void call(ByteArray p1) { + printData('<', p1); + } + }); + + session.onDataWritten.connect(new Slot1<ByteArray>() { + + public void call(ByteArray p1) { + printData('>', p1); + } + }); + } + + private void printData(char direction, ByteArray data) { + System.err.print("" + direction + direction + " " + session.getLocalJID().toString() + " "); + for (int i = 0; i < 72 - session.getLocalJID().toString().length() - session.getRemoteJID().toString().length(); ++i) { + System.err.print(direction); + } + System.err.println(" " + session.getRemoteJID().toString() + " " + direction + direction); + System.err.println(data); + } + private Session session; +}
\ No newline at end of file diff --git a/src/com/isode/stroke/signals/Signal.java b/src/com/isode/stroke/signals/Signal.java new file mode 100644 index 0000000..cfa8665 --- /dev/null +++ b/src/com/isode/stroke/signals/Signal.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.signals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * An approximation of the boost::signals system, although a little more warty. + */ +public class Signal { + + private final Map<SignalConnection, Slot> binds_ = Collections.synchronizedMap(new HashMap<SignalConnection, Slot>()); + + public SignalConnection connect(Slot bind) { + final SignalConnection connection = new SignalConnection(); + binds_.put(connection, bind); + connection.onDestroyed.connectWithoutReturn(new Slot() { + + public void call() { + binds_.remove(connection); + } + }); + return connection; + } + + public SignalConnection connect(final Signal target) { + return connect(new Slot() { + public void call() { + target.emit(); + } + }); + } + + void connectWithoutReturn(Slot bind) { + binds_.put(null, bind); + } + + public void emit() { + ArrayList<Slot> binds = new ArrayList<Slot>(); + binds.addAll(binds_.values()); + for (Slot bind : binds) { + bind.call(); + } + } + + public void disconnectAll() { + binds_.clear(); + } +} diff --git a/src/com/isode/stroke/signals/Signal1.java b/src/com/isode/stroke/signals/Signal1.java new file mode 100644 index 0000000..c799d4c --- /dev/null +++ b/src/com/isode/stroke/signals/Signal1.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.signals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * An approximation of the boost::signals system, although a little more warty. + */ +public class Signal1<T1> { + private final Map<SignalConnection, Slot1<T1> > binds_ = Collections.synchronizedMap(new HashMap<SignalConnection, Slot1<T1> >()); + public SignalConnection connect(Slot1<T1> bind) { + final SignalConnection connection = new SignalConnection(); + binds_.put(connection, bind); + connection.onDestroyed.connect(new Slot() { + public void call() { + binds_.remove(connection); + } + }); + return connection; + } + + public void emit(T1 p1) { + ArrayList<Slot1<T1>> binds = new ArrayList<Slot1<T1>>(); + binds.addAll(binds_.values()); + for (Slot1<T1> bind : binds) { + bind.call(p1); + } + } + + public SignalConnection connect(final Signal1<T1> target) { + return connect(new Slot1<T1>() { + public void call(T1 p1) { + target.emit(p1); + } + }); + } + + public void disconnectAll() { + binds_.clear(); + } +} diff --git a/src/com/isode/stroke/signals/Signal2.java b/src/com/isode/stroke/signals/Signal2.java new file mode 100644 index 0000000..fa3c4b5 --- /dev/null +++ b/src/com/isode/stroke/signals/Signal2.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.signals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * An approximation of the boost::signals system, although a little more warty. + */ +public class Signal2<T1, T2> { + private final Map<SignalConnection, Slot2<T1, T2> > binds_ = Collections.synchronizedMap(new HashMap<SignalConnection, Slot2<T1, T2> >()); + public SignalConnection connect(Slot2<T1, T2> bind) { + final SignalConnection connection = new SignalConnection(); + binds_.put(connection, bind); + connection.onDestroyed.connect(new Slot() { + public void call() { + binds_.remove(connection); + } + }); + return connection; + } + + public void emit(T1 p1, T2 p2) { + ArrayList<Slot2<T1,T2>> binds = new ArrayList<Slot2<T1, T2>>(); + binds.addAll(binds_.values()); + for (Slot2<T1, T2> bind : binds) { + bind.call(p1, p2); + } + } + + public void disconnectAll() { + binds_.clear(); + } +} diff --git a/src/com/isode/stroke/signals/SignalConnection.java b/src/com/isode/stroke/signals/SignalConnection.java new file mode 100644 index 0000000..452c80b --- /dev/null +++ b/src/com/isode/stroke/signals/SignalConnection.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.signals; + + +public class SignalConnection { + public final Signal onDestroyed = new Signal(); + + public void disconnect() { + onDestroyed.emit(); + } +} diff --git a/src/com/isode/stroke/signals/Slot.java b/src/com/isode/stroke/signals/Slot.java new file mode 100644 index 0000000..35b8c70 --- /dev/null +++ b/src/com/isode/stroke/signals/Slot.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.signals; + +/** + * Bind class for connecting to a signal. + */ +public interface Slot { + void call(); +} diff --git a/src/com/isode/stroke/signals/Slot1.java b/src/com/isode/stroke/signals/Slot1.java new file mode 100644 index 0000000..a8f0179 --- /dev/null +++ b/src/com/isode/stroke/signals/Slot1.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.signals; + +/** + * Bind class for connecting to a signal. + */ +public interface Slot1<T1> { + void call(T1 p1); +} diff --git a/src/com/isode/stroke/signals/Slot2.java b/src/com/isode/stroke/signals/Slot2.java new file mode 100644 index 0000000..b3b9330 --- /dev/null +++ b/src/com/isode/stroke/signals/Slot2.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.signals; + +/** + * Bind class for connecting to a signal. + */ +public interface Slot2<T1, T2> { + void call(T1 p1, T2 p2); +} diff --git a/src/com/isode/stroke/streammanagement/StanzaAckRequester.java b/src/com/isode/stroke/streammanagement/StanzaAckRequester.java new file mode 100644 index 0000000..d698e89 --- /dev/null +++ b/src/com/isode/stroke/streammanagement/StanzaAckRequester.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.streammanagement; + +import com.isode.stroke.elements.Message; +import com.isode.stroke.elements.Stanza; +import com.isode.stroke.signals.Signal; +import com.isode.stroke.signals.Signal1; +import java.util.ArrayList; +import java.util.List; + +public class StanzaAckRequester { + + static final long MAX_HANDLED_STANZA_COUNT = Long.parseLong("4294967295"); //boost::numeric_cast<unsigned int>((1ULL<<32) - 1); + + public StanzaAckRequester() { + + } + + public void handleStanzaSent(Stanza stanza) { + unackedStanzas.add(stanza); + if (stanza instanceof Message) { + onRequestAck.emit(); + } + } + + public void handleAckReceived(long handledStanzasCount) { + long i = lastHandledStanzasCount; + while (i != handledStanzasCount) { + if (unackedStanzas.isEmpty()) { + //std::cerr << "Warning: Server acked more stanzas than we sent" << std::endl; + break; + } + Stanza ackedStanza = unackedStanzas.get(0); + unackedStanzas.remove(0); + onStanzaAcked.emit(ackedStanza); + i = (i == MAX_HANDLED_STANZA_COUNT ? 0 : i + 1); + } + lastHandledStanzasCount = handledStanzasCount; + } + + public Signal onRequestAck = new Signal(); + + public Signal1<Stanza> onStanzaAcked = new Signal1<Stanza>(); + + private long lastHandledStanzasCount; + + private List<Stanza> unackedStanzas = new ArrayList<Stanza>(); +} diff --git a/src/com/isode/stroke/streammanagement/StanzaAckResponder.java b/src/com/isode/stroke/streammanagement/StanzaAckResponder.java new file mode 100644 index 0000000..e0de87a --- /dev/null +++ b/src/com/isode/stroke/streammanagement/StanzaAckResponder.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.streammanagement; + +import com.isode.stroke.signals.Signal1; + +public class StanzaAckResponder { + + static final long MAX_HANDLED_STANZA_COUNT = Long.parseLong("4294967295"); //boost::numeric_cast<unsigned int>((1ULL<<32) - 1); + + public StanzaAckResponder() { + } + + public void handleStanzaReceived() { + handledStanzasCount = (handledStanzasCount == MAX_HANDLED_STANZA_COUNT ? 0 : handledStanzasCount + 1); + } + + public void handleAckRequestReceived() { + onAck.emit(handledStanzasCount); + } + public Signal1<Long> onAck = new Signal1<Long>(); + private long handledStanzasCount = 0; +} diff --git a/src/com/isode/stroke/streamstack/CompressionLayer.java b/src/com/isode/stroke/streamstack/CompressionLayer.java new file mode 100644 index 0000000..fae8263 --- /dev/null +++ b/src/com/isode/stroke/streamstack/CompressionLayer.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.streamstack; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.compress.ZLibCompressor; +import com.isode.stroke.compress.ZLibDecompressor; +import com.isode.stroke.compress.ZLibException; +import com.isode.stroke.signals.Signal; + +public class CompressionLayer extends StreamLayer { + + public void writeData(ByteArray data) { + try { + writeDataToChildLayer(compressor_.process(data)); + } + catch (ZLibException e) { + onError.emit(); + } + } + + public void handleDataRead(ByteArray data) { + try { + writeDataToParentLayer(decompressor_.process(data)); + } + catch (ZLibException e) { + onError.emit(); + } + } + + public Signal onError = new Signal(); + + private ZLibCompressor compressor_ = new ZLibCompressor(); + private ZLibDecompressor decompressor_ = new ZLibDecompressor(); + +} diff --git a/src/com/isode/stroke/streamstack/ConnectionLayer.java b/src/com/isode/stroke/streamstack/ConnectionLayer.java new file mode 100644 index 0000000..525da50 --- /dev/null +++ b/src/com/isode/stroke/streamstack/ConnectionLayer.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.streamstack; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.network.Connection; +import com.isode.stroke.signals.Signal1; +import com.isode.stroke.signals.Slot1; + +public class ConnectionLayer implements LowLayer { + + public ConnectionLayer(Connection connection) { + this.connection = connection; + connection.onDataRead.connect(new Slot1<ByteArray>() { + + public void call(ByteArray p1) { + writeDataToParentLayer(p1); + } + }); + } + + public void writeData(ByteArray data) { + connection.write(data); + } + + private Connection connection; + + /* Work around multiple inheritance workaround again */ + StreamLayer fakeStreamLayer_ = new StreamLayer() { + + public void writeData(ByteArray data) { + connection.write(data); + } + + public void handleDataRead(ByteArray data) { + throw new UnsupportedOperationException("Not supported yet."); + } + }; + + public HighLayer getParentLayer() { + return fakeStreamLayer_.getParentLayer(); + } + + public void setParentLayer(HighLayer parentLayer) { + fakeStreamLayer_.setParentLayer(parentLayer); + } + + public void writeDataToParentLayer(ByteArray data) { + fakeStreamLayer_.writeDataToParentLayer(data); + } +} diff --git a/src/com/isode/stroke/streamstack/HighLayer.java b/src/com/isode/stroke/streamstack/HighLayer.java new file mode 100644 index 0000000..9ef37d1 --- /dev/null +++ b/src/com/isode/stroke/streamstack/HighLayer.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.streamstack; + +import com.isode.stroke.base.ByteArray; + +/** + * Because of the lack of multiple inheritance in Java, this has to be done + * slightly differently from Swiften. What happens is that the methods in Swiften + * are provided abstract here, and implemented in the StreamLayer instead. + */ +public interface HighLayer { + + void handleDataRead(ByteArray data); + + + /* Should be protected */ + LowLayer getChildLayer(); + + void setChildLayer(LowLayer childLayer); + + void writeDataToChildLayer(ByteArray data); + +} diff --git a/src/com/isode/stroke/streamstack/LowLayer.java b/src/com/isode/stroke/streamstack/LowLayer.java new file mode 100644 index 0000000..fa31075 --- /dev/null +++ b/src/com/isode/stroke/streamstack/LowLayer.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.streamstack; + +import com.isode.stroke.base.ByteArray; + +/** + * Because of the lack of multiple inheritance in Java, this has to be done + * slightly differently from Swiften. What happens is that the methods in Swiften + * are provided abstract here, and implemented in the StreamLayer instead. + */ +public interface LowLayer { + + void writeData(ByteArray data); + + /* Should be protected */ + + HighLayer getParentLayer(); + + void setParentLayer(HighLayer parentLayer); + + void writeDataToParentLayer(ByteArray data); +} diff --git a/src/com/isode/stroke/streamstack/StreamLayer.java b/src/com/isode/stroke/streamstack/StreamLayer.java new file mode 100644 index 0000000..c6d8f50 --- /dev/null +++ b/src/com/isode/stroke/streamstack/StreamLayer.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.streamstack; + +import com.isode.stroke.base.ByteArray; + +/** + * Because of the lack of multiple inheritance in Java, this implements + * the abstract methods that should have been implemented in + * LowLayer and HighLayer. + */ +public abstract class StreamLayer implements LowLayer, HighLayer { + + public HighLayer getParentLayer() { + return parentLayer; + } + + public void setParentLayer(final HighLayer parentLayer) { + this.parentLayer = parentLayer; + } + + public void writeDataToParentLayer(final ByteArray data) { + assert parentLayer != null; + parentLayer.handleDataRead(data); + } + + public LowLayer getChildLayer() { + return childLayer; + } + + public void setChildLayer(final LowLayer childLayer) { + this.childLayer = childLayer; + } + + public void writeDataToChildLayer(final ByteArray data) { + assert childLayer != null; + childLayer.writeData(data); + } + + private HighLayer parentLayer; + private LowLayer childLayer; +} diff --git a/src/com/isode/stroke/streamstack/StreamStack.java b/src/com/isode/stroke/streamstack/StreamStack.java new file mode 100644 index 0000000..f539624 --- /dev/null +++ b/src/com/isode/stroke/streamstack/StreamStack.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.streamstack; + +import java.util.ArrayList; +import java.util.List; + +public class StreamStack { + + public StreamStack(XMPPLayer xmppLayer, LowLayer physicalLayer) { + xmppLayer_ = xmppLayer; + physicalLayer_ = physicalLayer; + physicalLayer_.setParentLayer(xmppLayer_); + xmppLayer.setChildLayer(physicalLayer_); + } + + public void addLayer(final StreamLayer newLayer) { + final LowLayer lowLayer = (layers_.isEmpty() ? physicalLayer_ : layers_.get(layers_.size() - 1)); + + xmppLayer_.setChildLayer(newLayer); + newLayer.setParentLayer(xmppLayer_); + + lowLayer.setParentLayer(newLayer); + newLayer.setChildLayer(lowLayer); + + layers_.add(newLayer); + } + + public XMPPLayer getXMPPLayer() { + return xmppLayer_; + } + + public Object getLayer(Class layerClass) { + for (StreamLayer layer : layers_) { + if (layerClass.isAssignableFrom(layer.getClass())) { + return layer; + } + } + return null; + } + + private XMPPLayer xmppLayer_; + private LowLayer physicalLayer_; + private List<StreamLayer> layers_ = new ArrayList<StreamLayer>(); +} diff --git a/src/com/isode/stroke/streamstack/TLSLayer.java b/src/com/isode/stroke/streamstack/TLSLayer.java new file mode 100644 index 0000000..ecf908d --- /dev/null +++ b/src/com/isode/stroke/streamstack/TLSLayer.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.streamstack; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.signals.Signal; +import com.isode.stroke.signals.Slot1; +import com.isode.stroke.tls.Certificate; +import com.isode.stroke.tls.CertificateVerificationError; +import com.isode.stroke.tls.PKCS12Certificate; +import com.isode.stroke.tls.TLSContext; +import com.isode.stroke.tls.TLSContextFactory; + +public class TLSLayer extends StreamLayer { + + public TLSLayer(TLSContextFactory factory) { + context = factory.createTLSContext(); + context.onDataForNetwork.connect(new Slot1<ByteArray>() { + + public void call(ByteArray p1) { + writeDataToChildLayer(p1); + } + }); + context.onDataForApplication.connect(new Slot1<ByteArray>() { + + public void call(ByteArray p1) { + writeDataToParentLayer(p1); + } + }); + context.onConnected.connect(onConnected); + context.onError.connect(onError); + } + + public void connect() { + context.connect(); + } + + public void writeData(ByteArray data) { + context.handleDataFromApplication(data); + } + + public void handleDataRead(ByteArray data) { + context.handleDataFromNetwork(data); + } + + public boolean setClientCertificate(PKCS12Certificate certificate) { + return context.setClientCertificate(certificate); + } + + public Certificate getPeerCertificate() { + return context.getPeerCertificate(); + } + + public CertificateVerificationError getPeerCertificateVerificationError() { + return context.getPeerCertificateVerificationError(); + } + + public TLSContext getContext() { + return context; + } + + public final Signal onError = new Signal(); + public final Signal onConnected = new Signal(); + + private final TLSContext context; +} diff --git a/src/com/isode/stroke/streamstack/WhitespacePingLayer.java b/src/com/isode/stroke/streamstack/WhitespacePingLayer.java new file mode 100644 index 0000000..09d3af8 --- /dev/null +++ b/src/com/isode/stroke/streamstack/WhitespacePingLayer.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.streamstack; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.network.Timer; +import com.isode.stroke.network.TimerFactory; +import com.isode.stroke.signals.Slot; + +public class WhitespacePingLayer extends StreamLayer { + + private static final int TIMEOUT_MILLISECONDS = 60000; + + public WhitespacePingLayer(TimerFactory timerFactory) { + isActive = false; + timer = timerFactory.createTimer(TIMEOUT_MILLISECONDS); + timer.onTick.connect(new Slot() { + public void call() { + handleTimerTick(); + } + }); + } + + public void writeData(ByteArray data) { + writeDataToChildLayer(data); + } + + public void handleDataRead(ByteArray data) { + writeDataToParentLayer(data); + } + + private void handleTimerTick() { + timer.stop(); + writeDataToChildLayer(new ByteArray(" ")); + timer.start(); + } + + public void setActive() { + isActive = true; + timer.start(); + } + + public void setInactive() { + timer.stop(); + isActive = false; + } + + public boolean getIsActive() { + return isActive; + } + + private boolean isActive; + private Timer timer; +} diff --git a/src/com/isode/stroke/streamstack/XMPPLayer.java b/src/com/isode/stroke/streamstack/XMPPLayer.java new file mode 100644 index 0000000..86c7b0e --- /dev/null +++ b/src/com/isode/stroke/streamstack/XMPPLayer.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.streamstack; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.elements.Element; +import com.isode.stroke.elements.ProtocolHeader; +import com.isode.stroke.elements.StreamType; +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.parser.PayloadParserFactoryCollection; +import com.isode.stroke.parser.XMPPParser; +import com.isode.stroke.parser.XMPPParserClient; +import com.isode.stroke.serializer.PayloadSerializerCollection; +import com.isode.stroke.serializer.XMPPSerializer; +import com.isode.stroke.signals.Signal; +import com.isode.stroke.signals.Signal1; + +/** + * This uses the inner StreamLayer to work around the HighLayer not having + * implementations because of the lack of multiple inheritance. + * Swiften doesn't require an eventLoop, Stroke does because of + * XML parsing being multi-threaded here. + */ +public class XMPPLayer implements HighLayer, XMPPParserClient { + + public XMPPLayer( + PayloadParserFactoryCollection payloadParserFactories, + PayloadSerializerCollection payloadSerializers, + StreamType streamType, + EventLoop eventLoop) { + payloadParserFactories_ = payloadParserFactories; + payloadSerializers_ = payloadSerializers; + resetParserAfterParse_ = false; + eventLoop_ = eventLoop; + inParser_ = false; + xmppParser_ = new XMPPParser(this, payloadParserFactories_, eventLoop_); + xmppSerializer_ = new XMPPSerializer(payloadSerializers_, streamType); + } + + public void writeHeader(ProtocolHeader header) { + writeDataInternal(new ByteArray(xmppSerializer_.serializeHeader(header))); + } + + public void writeFooter() { + writeDataInternal(new ByteArray(xmppSerializer_.serializeFooter())); + } + + public void writeElement(Element element) { + writeDataInternal(new ByteArray(xmppSerializer_.serializeElement(element))); + } + + public void writeData(String data) { + writeDataInternal(new ByteArray(data)); + } + + public void resetParser() { + if (inParser_) { + resetParserAfterParse_ = true; + } + else { + doResetParser(); + } + } + + /** + * Should be protected, but can't because of interface implementation. + * @param data + */ + public void handleDataRead(ByteArray data) { + handleDataReadInternal(data); + } + + protected void writeDataInternal(ByteArray data) { + onWriteData.emit(data); + writeDataToChildLayer(data); + } + + public final Signal1<ProtocolHeader> onStreamStart = new Signal1<ProtocolHeader>(); + public final Signal1<Element> onElement = new Signal1<Element>(); + public final Signal1<ByteArray> onWriteData = new Signal1<ByteArray>(); + public final Signal1<ByteArray> onDataRead = new Signal1<ByteArray>(); + public final Signal onError = new Signal(); + + public void handleStreamStart(ProtocolHeader header) { + onStreamStart.emit(header); + } + + public void handleElement(Element element) { + onElement.emit(element); + } + + public void handleStreamEnd() { + } + + private void doResetParser() { + xmppParser_ = new XMPPParser(this, payloadParserFactories_, eventLoop_); + resetParserAfterParse_ = false; + } + + private PayloadParserFactoryCollection payloadParserFactories_; + private XMPPParser xmppParser_; + private PayloadSerializerCollection payloadSerializers_; + private XMPPSerializer xmppSerializer_; + private boolean resetParserAfterParse_; + private boolean inParser_; + private EventLoop eventLoop_; + + /* Multiple-inheritance workarounds */ + + private StreamLayer fakeStreamLayer_ = new StreamLayer() { + public void writeData(ByteArray data) { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void handleDataRead(ByteArray data) { + handleDataReadInternal(data); + } + }; + + private void handleDataReadInternal(ByteArray data) { + onDataRead.emit(data); + inParser_ = true; + if(!xmppParser_.parse(data.toString())) { + inParser_ = false; + onError.emit(); + return; + } + inParser_ = false; + if (resetParserAfterParse_) { + doResetParser(); + } + } + + public LowLayer getChildLayer() { + return fakeStreamLayer_.getChildLayer(); + } + + public void setChildLayer(LowLayer childLayer) { + fakeStreamLayer_.setChildLayer(childLayer); + } + + public void writeDataToChildLayer(ByteArray data) { + fakeStreamLayer_.writeDataToChildLayer(data); + } +} diff --git a/src/com/isode/stroke/stringcodecs/Base64.java b/src/com/isode/stroke/stringcodecs/Base64.java new file mode 100644 index 0000000..3b0faa5 --- /dev/null +++ b/src/com/isode/stroke/stringcodecs/Base64.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.stringcodecs; + +import com.isode.stroke.base.ByteArray; + +public class Base64 { + /* FIXME: Check license is ok (it is, it's BSD) */ + public static ByteArray decode(String input) { + return new ByteArray(Base64BSD.decode(input)); + } + + public static String encode(ByteArray input) { + return Base64BSD.encodeToString(input.getData(), false); + } +} diff --git a/src/com/isode/stroke/stringcodecs/Base64BSD.java b/src/com/isode/stroke/stringcodecs/Base64BSD.java new file mode 100644 index 0000000..9a3e0e4 --- /dev/null +++ b/src/com/isode/stroke/stringcodecs/Base64BSD.java @@ -0,0 +1,575 @@ +package com.isode.stroke.stringcodecs;
+
+import java.util.Arrays;
+
+/** A very fast and memory efficient class to encode and decode to and from BASE64 in full accordance
+ * with RFC 2045.<br><br>
+ * On Windows XP sp1 with 1.4.2_04 and later ;), this encoder and decoder is about 10 times faster
+ * on small arrays (10 - 1000 bytes) and 2-3 times as fast on larger arrays (10000 - 1000000 bytes)
+ * compared to <code>sun.misc.Encoder()/Decoder()</code>.<br><br>
+ *
+ * On byte arrays the encoder is about 20% faster than Jakarta Commons Base64 Codec for encode and
+ * about 50% faster for decoding large arrays. This implementation is about twice as fast on very small
+ * arrays (< 30 bytes). If source/destination is a <code>String</code> this
+ * version is about three times as fast due to the fact that the Commons Codec result has to be recoded
+ * to a <code>String</code> from <code>byte[]</code>, which is very expensive.<br><br>
+ *
+ * This encode/decode algorithm doesn't create any temporary arrays as many other codecs do, it only
+ * allocates the resulting array. This produces less garbage and it is possible to handle arrays twice
+ * as large as algorithms that create a temporary array. (E.g. Jakarta Commons Codec). It is unknown
+ * whether Sun's <code>sun.misc.Encoder()/Decoder()</code> produce temporary arrays but since performance
+ * is quite low it probably does.<br><br>
+ *
+ * The encoder produces the same output as the Sun one except that the Sun's encoder appends
+ * a trailing line separator if the last character isn't a pad. Unclear why but it only adds to the
+ * length and is probably a side effect. Both are in conformance with RFC 2045 though.<br>
+ * Commons codec seem to always att a trailing line separator.<br><br>
+ *
+ * <b>Note!</b>
+ * The encode/decode method pairs (types) come in three versions with the <b>exact</b> same algorithm and
+ * thus a lot of code redundancy. This is to not create any temporary arrays for transcoding to/from different
+ * format types. The methods not used can simply be commented out.<br><br>
+ *
+ * There is also a "fast" version of all decode methods that works the same way as the normal ones, but
+ * har a few demands on the decoded input. Normally though, these fast verions should be used if the source if
+ * the input is known and it hasn't bee tampered with.<br><br>
+ *
+ * If you find the code useful or you find a bug, please send me a note at base64 @ miginfocom . com.
+ *
+ * Licence (BSD):
+ * ==============
+ *
+ * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or other
+ * materials provided with the distribution.
+ * Neither the name of the MiG InfoCom AB nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+ * OF SUCH DAMAGE.
+ *
+ * @version 2.2
+ * @author Mikael Grev
+ * Date: 2004-aug-02
+ * Time: 11:31:11
+ */
+
+public class Base64BSD
+{
+ private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
+ private static final int[] IA = new int[256];
+ static {
+ Arrays.fill(IA, -1);
+ for (int i = 0, iS = CA.length; i < iS; i++)
+ IA[CA[i]] = i;
+ IA['='] = 0;
+ }
+
+ // ****************************************************************************************
+ // * char[] version
+ // ****************************************************************************************
+
+ /** Encodes a raw byte array into a BASE64 <code>char[]</code> representation i accordance with RFC 2045.
+ * @param sArr The bytes to convert. If <code>null</code> or length 0 an empty array will be returned.
+ * @param lineSep Optional "\r\n" after 76 characters, unless end of file.<br>
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a
+ * little faster.
+ * @return A BASE64 encoded array. Never <code>null</code>.
+ */
+ public final static char[] encodeToChar(byte[] sArr, boolean lineSep)
+ {
+ // Check special case
+ int sLen = sArr != null ? sArr.length : 0;
+ if (sLen == 0)
+ return new char[0];
+
+ int eLen = (sLen / 3) * 3; // Length of even 24-bits.
+ int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count
+ int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array
+ char[] dArr = new char[dLen];
+
+ // Encode even 24-bits
+ for (int s = 0, d = 0, cc = 0; s < eLen;) {
+ // Copy next three bytes into lower 24 bits of int, paying attension to sign.
+ int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff);
+
+ // Encode the int into four chars
+ dArr[d++] = CA[(i >>> 18) & 0x3f];
+ dArr[d++] = CA[(i >>> 12) & 0x3f];
+ dArr[d++] = CA[(i >>> 6) & 0x3f];
+ dArr[d++] = CA[i & 0x3f];
+
+ // Add optional line separator
+ if (lineSep && ++cc == 19 && d < dLen - 2) {
+ dArr[d++] = '\r';
+ dArr[d++] = '\n';
+ cc = 0;
+ }
+ }
+
+ // Pad and encode last bits if source isn't even 24 bits.
+ int left = sLen - eLen; // 0 - 2.
+ if (left > 0) {
+ // Prepare the int
+ int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0);
+
+ // Set last four chars
+ dArr[dLen - 4] = CA[i >> 12];
+ dArr[dLen - 3] = CA[(i >>> 6) & 0x3f];
+ dArr[dLen - 2] = left == 2 ? CA[i & 0x3f] : '=';
+ dArr[dLen - 1] = '=';
+ }
+ return dArr;
+ }
+
+ /** Decodes a BASE64 encoded char array. All illegal characters will be ignored and can handle both arrays with
+ * and without line separators.
+ * @param sArr The source array. <code>null</code> or length 0 will return an empty array.
+ * @return The decoded array of bytes. May be of length 0. Will be <code>null</code> if the legal characters
+ * (including '=') isn't divideable by 4. (I.e. definitely corrupted).
+ */
+ public final static byte[] decode(char[] sArr)
+ {
+ // Check special case
+ int sLen = sArr != null ? sArr.length : 0;
+ if (sLen == 0)
+ return new byte[0];
+
+ // Count illegal characters (including '\r', '\n') to know what size the returned array will be,
+ // so we don't have to reallocate & copy it later.
+ int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...)
+ for (int i = 0; i < sLen; i++) // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out.
+ if (IA[sArr[i]] < 0)
+ sepCnt++;
+
+ // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045.
+ if ((sLen - sepCnt) % 4 != 0)
+ return null;
+
+ int pad = 0;
+ for (int i = sLen; i > 1 && IA[sArr[--i]] <= 0;)
+ if (sArr[i] == '=')
+ pad++;
+
+ int len = ((sLen - sepCnt) * 6 >> 3) - pad;
+
+ byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
+
+ for (int s = 0, d = 0; d < len;) {
+ // Assemble three bytes into an int from four "valid" characters.
+ int i = 0;
+ for (int j = 0; j < 4; j++) { // j only increased if a valid char was found.
+ int c = IA[sArr[s++]];
+ if (c >= 0)
+ i |= c << (18 - j * 6);
+ else
+ j--;
+ }
+ // Add the bytes
+ dArr[d++] = (byte) (i >> 16);
+ if (d < len) {
+ dArr[d++]= (byte) (i >> 8);
+ if (d < len)
+ dArr[d++] = (byte) i;
+ }
+ }
+ return dArr;
+ }
+
+ /** Decodes a BASE64 encoded char array that is known to be resonably well formatted. The method is about twice as
+ * fast as {@link #decode(char[])}. The preconditions are:<br>
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
+ * + Line separator must be "\r\n", as specified in RFC 2045
+ * + The array must not contain illegal characters within the encoded string<br>
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.<br>
+ * @param sArr The source array. Length 0 will return an empty array. <code>null</code> will throw an exception.
+ * @return The decoded array of bytes. May be of length 0.
+ */
+ public final static byte[] decodeFast(char[] sArr)
+ {
+ // Check special case
+ int sLen = sArr.length;
+ if (sLen == 0)
+ return new byte[0];
+
+ int sIx = 0, eIx = sLen - 1; // Start and end index after trimming.
+
+ // Trim illegal chars from start
+ while (sIx < eIx && IA[sArr[sIx]] < 0)
+ sIx++;
+
+ // Trim illegal chars from end
+ while (eIx > 0 && IA[sArr[eIx]] < 0)
+ eIx--;
+
+ // get the padding count (=) (0, 1 or 2)
+ int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end.
+ int cCnt = eIx - sIx + 1; // Content count including possible separators
+ int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0;
+
+ int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes
+ byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
+
+ // Decode all but the last 0 - 2 bytes.
+ int d = 0;
+ for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
+ // Assemble three bytes into an int from four "valid" characters.
+ int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]];
+
+ // Add the bytes
+ dArr[d++] = (byte) (i >> 16);
+ dArr[d++] = (byte) (i >> 8);
+ dArr[d++] = (byte) i;
+
+ // If line separator, jump over it.
+ if (sepCnt > 0 && ++cc == 19) {
+ sIx += 2;
+ cc = 0;
+ }
+ }
+
+ if (d < len) {
+ // Decode last 1-3 bytes (incl '=') into 1-3 bytes
+ int i = 0;
+ for (int j = 0; sIx <= eIx - pad; j++)
+ i |= IA[sArr[sIx++]] << (18 - j * 6);
+
+ for (int r = 16; d < len; r -= 8)
+ dArr[d++] = (byte) (i >> r);
+ }
+
+ return dArr;
+ }
+
+ // ****************************************************************************************
+ // * byte[] version
+ // ****************************************************************************************
+
+ /** Encodes a raw byte array into a BASE64 <code>byte[]</code> representation i accordance with RFC 2045.
+ * @param sArr The bytes to convert. If <code>null</code> or length 0 an empty array will be returned.
+ * @param lineSep Optional "\r\n" after 76 characters, unless end of file.<br>
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a
+ * little faster.
+ * @return A BASE64 encoded array. Never <code>null</code>.
+ */
+ public final static byte[] encodeToByte(byte[] sArr, boolean lineSep)
+ {
+ // Check special case
+ int sLen = sArr != null ? sArr.length : 0;
+ if (sLen == 0)
+ return new byte[0];
+
+ int eLen = (sLen / 3) * 3; // Length of even 24-bits.
+ int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count
+ int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array
+ byte[] dArr = new byte[dLen];
+
+ // Encode even 24-bits
+ for (int s = 0, d = 0, cc = 0; s < eLen;) {
+ // Copy next three bytes into lower 24 bits of int, paying attension to sign.
+ int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff);
+
+ // Encode the int into four chars
+ dArr[d++] = (byte) CA[(i >>> 18) & 0x3f];
+ dArr[d++] = (byte) CA[(i >>> 12) & 0x3f];
+ dArr[d++] = (byte) CA[(i >>> 6) & 0x3f];
+ dArr[d++] = (byte) CA[i & 0x3f];
+
+ // Add optional line separator
+ if (lineSep && ++cc == 19 && d < dLen - 2) {
+ dArr[d++] = '\r';
+ dArr[d++] = '\n';
+ cc = 0;
+ }
+ }
+
+ // Pad and encode last bits if source isn't an even 24 bits.
+ int left = sLen - eLen; // 0 - 2.
+ if (left > 0) {
+ // Prepare the int
+ int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0);
+
+ // Set last four chars
+ dArr[dLen - 4] = (byte) CA[i >> 12];
+ dArr[dLen - 3] = (byte) CA[(i >>> 6) & 0x3f];
+ dArr[dLen - 2] = left == 2 ? (byte) CA[i & 0x3f] : (byte) '=';
+ dArr[dLen - 1] = '=';
+ }
+ return dArr;
+ }
+
+ /** Decodes a BASE64 encoded byte array. All illegal characters will be ignored and can handle both arrays with
+ * and without line separators.
+ * @param sArr The source array. Length 0 will return an empty array. <code>null</code> will throw an exception.
+ * @return The decoded array of bytes. May be of length 0. Will be <code>null</code> if the legal characters
+ * (including '=') isn't divideable by 4. (I.e. definitely corrupted).
+ */
+ public final static byte[] decode(byte[] sArr)
+ {
+ // Check special case
+ int sLen = sArr.length;
+
+ // Count illegal characters (including '\r', '\n') to know what size the returned array will be,
+ // so we don't have to reallocate & copy it later.
+ int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...)
+ for (int i = 0; i < sLen; i++) // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out.
+ if (IA[sArr[i] & 0xff] < 0)
+ sepCnt++;
+
+ // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045.
+ if ((sLen - sepCnt) % 4 != 0)
+ return null;
+
+ int pad = 0;
+ for (int i = sLen; i > 1 && IA[sArr[--i] & 0xff] <= 0;)
+ if (sArr[i] == '=')
+ pad++;
+
+ int len = ((sLen - sepCnt) * 6 >> 3) - pad;
+
+ byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
+
+ for (int s = 0, d = 0; d < len;) {
+ // Assemble three bytes into an int from four "valid" characters.
+ int i = 0;
+ for (int j = 0; j < 4; j++) { // j only increased if a valid char was found.
+ int c = IA[sArr[s++] & 0xff];
+ if (c >= 0)
+ i |= c << (18 - j * 6);
+ else
+ j--;
+ }
+
+ // Add the bytes
+ dArr[d++] = (byte) (i >> 16);
+ if (d < len) {
+ dArr[d++]= (byte) (i >> 8);
+ if (d < len)
+ dArr[d++] = (byte) i;
+ }
+ }
+
+ return dArr;
+ }
+
+
+ /** Decodes a BASE64 encoded byte array that is known to be resonably well formatted. The method is about twice as
+ * fast as {@link #decode(byte[])}. The preconditions are:<br>
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
+ * + Line separator must be "\r\n", as specified in RFC 2045
+ * + The array must not contain illegal characters within the encoded string<br>
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.<br>
+ * @param sArr The source array. Length 0 will return an empty array. <code>null</code> will throw an exception.
+ * @return The decoded array of bytes. May be of length 0.
+ */
+ public final static byte[] decodeFast(byte[] sArr)
+ {
+ // Check special case
+ int sLen = sArr.length;
+ if (sLen == 0)
+ return new byte[0];
+
+ int sIx = 0, eIx = sLen - 1; // Start and end index after trimming.
+
+ // Trim illegal chars from start
+ while (sIx < eIx && IA[sArr[sIx] & 0xff] < 0)
+ sIx++;
+
+ // Trim illegal chars from end
+ while (eIx > 0 && IA[sArr[eIx] & 0xff] < 0)
+ eIx--;
+
+ // get the padding count (=) (0, 1 or 2)
+ int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end.
+ int cCnt = eIx - sIx + 1; // Content count including possible separators
+ int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0;
+
+ int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes
+ byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
+
+ // Decode all but the last 0 - 2 bytes.
+ int d = 0;
+ for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
+ // Assemble three bytes into an int from four "valid" characters.
+ int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]];
+
+ // Add the bytes
+ dArr[d++] = (byte) (i >> 16);
+ dArr[d++] = (byte) (i >> 8);
+ dArr[d++] = (byte) i;
+
+ // If line separator, jump over it.
+ if (sepCnt > 0 && ++cc == 19) {
+ sIx += 2;
+ cc = 0;
+ }
+ }
+
+ if (d < len) {
+ // Decode last 1-3 bytes (incl '=') into 1-3 bytes
+ int i = 0;
+ for (int j = 0; sIx <= eIx - pad; j++)
+ i |= IA[sArr[sIx++]] << (18 - j * 6);
+
+ for (int r = 16; d < len; r -= 8)
+ dArr[d++] = (byte) (i >> r);
+ }
+
+ return dArr;
+ }
+
+ // ****************************************************************************************
+ // * String version
+ // ****************************************************************************************
+
+ /** Encodes a raw byte array into a BASE64 <code>String</code> representation i accordance with RFC 2045.
+ * @param sArr The bytes to convert. If <code>null</code> or length 0 an empty array will be returned.
+ * @param lineSep Optional "\r\n" after 76 characters, unless end of file.<br>
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a
+ * little faster.
+ * @return A BASE64 encoded array. Never <code>null</code>.
+ */
+ public final static String encodeToString(byte[] sArr, boolean lineSep)
+ {
+ // Reuse char[] since we can't create a String incrementally anyway and StringBuffer/Builder would be slower.
+ return new String(encodeToChar(sArr, lineSep));
+ }
+
+ /** Decodes a BASE64 encoded <code>String</code>. All illegal characters will be ignored and can handle both strings with
+ * and without line separators.<br>
+ * <b>Note!</b> It can be up to about 2x the speed to call <code>decode(str.toCharArray())</code> instead. That
+ * will create a temporary array though. This version will use <code>str.charAt(i)</code> to iterate the string.
+ * @param str The source string. <code>null</code> or length 0 will return an empty array.
+ * @return The decoded array of bytes. May be of length 0. Will be <code>null</code> if the legal characters
+ * (including '=') isn't divideable by 4. (I.e. definitely corrupted).
+ */
+ public final static byte[] decode(String str)
+ {
+ // Check special case
+ int sLen = str != null ? str.length() : 0;
+ if (sLen == 0)
+ return new byte[0];
+
+ // Count illegal characters (including '\r', '\n') to know what size the returned array will be,
+ // so we don't have to reallocate & copy it later.
+ int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...)
+ for (int i = 0; i < sLen; i++) // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out.
+ if (IA[str.charAt(i)] < 0)
+ sepCnt++;
+
+ // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045.
+ if ((sLen - sepCnt) % 4 != 0)
+ return null;
+
+ // Count '=' at end
+ int pad = 0;
+ for (int i = sLen; i > 1 && IA[str.charAt(--i)] <= 0;)
+ if (str.charAt(i) == '=')
+ pad++;
+
+ int len = ((sLen - sepCnt) * 6 >> 3) - pad;
+
+ byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
+
+ for (int s = 0, d = 0; d < len;) {
+ // Assemble three bytes into an int from four "valid" characters.
+ int i = 0;
+ for (int j = 0; j < 4; j++) { // j only increased if a valid char was found.
+ int c = IA[str.charAt(s++)];
+ if (c >= 0)
+ i |= c << (18 - j * 6);
+ else
+ j--;
+ }
+ // Add the bytes
+ dArr[d++] = (byte) (i >> 16);
+ if (d < len) {
+ dArr[d++]= (byte) (i >> 8);
+ if (d < len)
+ dArr[d++] = (byte) i;
+ }
+ }
+ return dArr;
+ }
+
+ /** Decodes a BASE64 encoded string that is known to be resonably well formatted. The method is about twice as
+ * fast as {@link #decode(String)}. The preconditions are:<br>
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
+ * + Line separator must be "\r\n", as specified in RFC 2045
+ * + The array must not contain illegal characters within the encoded string<br>
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.<br>
+ * @param s The source string. Length 0 will return an empty array. <code>null</code> will throw an exception.
+ * @return The decoded array of bytes. May be of length 0.
+ */
+ public final static byte[] decodeFast(String s)
+ {
+ // Check special case
+ int sLen = s.length();
+ if (sLen == 0)
+ return new byte[0];
+
+ int sIx = 0, eIx = sLen - 1; // Start and end index after trimming.
+
+ // Trim illegal chars from start
+ while (sIx < eIx && IA[s.charAt(sIx) & 0xff] < 0)
+ sIx++;
+
+ // Trim illegal chars from end
+ while (eIx > 0 && IA[s.charAt(eIx) & 0xff] < 0)
+ eIx--;
+
+ // get the padding count (=) (0, 1 or 2)
+ int pad = s.charAt(eIx) == '=' ? (s.charAt(eIx - 1) == '=' ? 2 : 1) : 0; // Count '=' at end.
+ int cCnt = eIx - sIx + 1; // Content count including possible separators
+ int sepCnt = sLen > 76 ? (s.charAt(76) == '\r' ? cCnt / 78 : 0) << 1 : 0;
+
+ int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes
+ byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
+
+ // Decode all but the last 0 - 2 bytes.
+ int d = 0;
+ for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) {
+ // Assemble three bytes into an int from four "valid" characters.
+ int i = IA[s.charAt(sIx++)] << 18 | IA[s.charAt(sIx++)] << 12 | IA[s.charAt(sIx++)] << 6 | IA[s.charAt(sIx++)];
+
+ // Add the bytes
+ dArr[d++] = (byte) (i >> 16);
+ dArr[d++] = (byte) (i >> 8);
+ dArr[d++] = (byte) i;
+
+ // If line separator, jump over it.
+ if (sepCnt > 0 && ++cc == 19) {
+ sIx += 2;
+ cc = 0;
+ }
+ }
+
+ if (d < len) {
+ // Decode last 1-3 bytes (incl '=') into 1-3 bytes
+ int i = 0;
+ for (int j = 0; sIx <= eIx - pad; j++)
+ i |= IA[s.charAt(sIx++)] << (18 - j * 6);
+
+ for (int r = 16; d < len; r -= 8)
+ dArr[d++] = (byte) (i >> r);
+ }
+
+ return dArr;
+ }
+}
\ No newline at end of file diff --git a/src/com/isode/stroke/stringcodecs/HMACSHA1.java b/src/com/isode/stroke/stringcodecs/HMACSHA1.java new file mode 100644 index 0000000..1d8c9b7 --- /dev/null +++ b/src/com/isode/stroke/stringcodecs/HMACSHA1.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.stringcodecs; + +import com.isode.stroke.base.ByteArray; + +public class HMACSHA1 { + + private static final int B = 64; + + public static ByteArray getResult(ByteArray key, ByteArray data) { + assert key.getSize() <= B; + + /* And an assert that does something */ + if (key.getSize() > B) { + throw new IllegalStateException("Invalid key size."); + } + + // Create the padded key + ByteArray paddedKey = new ByteArray(key); + for (int i = key.getSize(); i < B; ++i) { + paddedKey.append((byte) 0x0); + } + + // Create the first value + ByteArray x = new ByteArray(paddedKey); + byte[] xInner = x.getData(); + for (int i = 0; i < xInner.length; ++i) { + xInner[i] ^= 0x36; + } + x.append(data); + + // Create the second value + ByteArray y = new ByteArray(paddedKey); + byte[] yInner = y.getData(); + for (int i = 0; i < yInner.length; ++i) { + yInner[i] ^= 0x5c; + } + y.append(SHA1.getHash(x)); + + return SHA1.getHash(y); + } +} diff --git a/src/com/isode/stroke/stringcodecs/Hexify.java b/src/com/isode/stroke/stringcodecs/Hexify.java new file mode 100644 index 0000000..7f51fb9 --- /dev/null +++ b/src/com/isode/stroke/stringcodecs/Hexify.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.stringcodecs; + +import com.isode.stroke.base.ByteArray; + +public class Hexify { + + public static String hexify(byte datum) { + return String.format("%x", new Byte(datum)); + } + + public static String hexify(ByteArray data) { + StringBuilder result = new StringBuilder(); + for (byte b : data.getData()) { + result.append(hexify(b)); + } + return result.toString(); + } +} diff --git a/src/com/isode/stroke/stringcodecs/PBKDF2.java b/src/com/isode/stroke/stringcodecs/PBKDF2.java new file mode 100644 index 0000000..547cbec --- /dev/null +++ b/src/com/isode/stroke/stringcodecs/PBKDF2.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.stringcodecs; + +import com.isode.stroke.base.ByteArray; + +public class PBKDF2 { + + public static ByteArray encode(ByteArray password, ByteArray salt, int iterations) { + ByteArray u = HMACSHA1.getResult(password, ByteArray.plus(salt, new ByteArray("\0\0\0\1"))); + ByteArray result = new ByteArray(u); + byte[] resultData = result.getData(); + int i = 1; + while (i < iterations) { + u = HMACSHA1.getResult(password, u); + for (int j = 0; j < u.getSize(); ++j) { + resultData[j] ^= u.getData()[j]; + } + ++i; + } + return result; + } +} diff --git a/src/com/isode/stroke/stringcodecs/SHA1.java b/src/com/isode/stroke/stringcodecs/SHA1.java new file mode 100644 index 0000000..e3d0e79 --- /dev/null +++ b/src/com/isode/stroke/stringcodecs/SHA1.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.stringcodecs; + +import com.isode.stroke.base.ByteArray; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class SHA1 { + + public static ByteArray getHash(ByteArray data) { + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("JRE doesn't have an SHA hash function", ex); + } + md.update(data.getData()); + return new ByteArray(md.digest()); + } +} diff --git a/src/com/isode/stroke/tls/Certificate.java b/src/com/isode/stroke/tls/Certificate.java new file mode 100644 index 0000000..ac7daed --- /dev/null +++ b/src/com/isode/stroke/tls/Certificate.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.tls; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.stringcodecs.Hexify; +import com.isode.stroke.stringcodecs.SHA1; +import java.util.List; + +public abstract class Certificate { + + /** + * Returns the textual representation of the full Subject + * name. + */ + public abstract String getSubjectName(); + + public abstract List<String> getCommonNames(); + + public abstract List<String> getSRVNames(); + + public abstract List<String> getDNSNames(); + + public abstract List<String> getXMPPAddresses(); + + public abstract ByteArray toDER(); + + public String getSHA1Fingerprint() { + ByteArray hash = SHA1.getHash(toDER()); + StringBuilder s = new StringBuilder(); + for (int i = 0; i < hash.getSize(); ++i) { + if (i > 0) { + s.append(":"); + } + s.append(Hexify.hexify(hash.getData()[i])); + } + return s.toString(); + } + protected String ID_ON_XMPPADDR_OID = "1.3.6.1.5.5.7.8.5"; + protected String ID_ON_DNSSRV_OID = "1.3.6.1.5.5.7.8.7"; +} diff --git a/src/com/isode/stroke/tls/CertificateFactory.java b/src/com/isode/stroke/tls/CertificateFactory.java new file mode 100644 index 0000000..a4c34db --- /dev/null +++ b/src/com/isode/stroke/tls/CertificateFactory.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.tls; + +import com.isode.stroke.base.ByteArray; + +public interface CertificateFactory { + Certificate createCertificateFromDER(ByteArray der); +} diff --git a/src/com/isode/stroke/tls/CertificateTrustChecker.java b/src/com/isode/stroke/tls/CertificateTrustChecker.java new file mode 100644 index 0000000..08d4506 --- /dev/null +++ b/src/com/isode/stroke/tls/CertificateTrustChecker.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.tls; + +/** + * A class to implement a check for certificate trust. + */ +public interface CertificateTrustChecker { + + /** + * This method is called to find out whether a certificate is + * trusted. This usually happens when a certificate's validation + * fails, to check whether to proceed with the connection or not. + */ + boolean isCertificateTrusted(Certificate certificate); +} diff --git a/src/com/isode/stroke/tls/CertificateVerificationError.java b/src/com/isode/stroke/tls/CertificateVerificationError.java new file mode 100644 index 0000000..a8309ca --- /dev/null +++ b/src/com/isode/stroke/tls/CertificateVerificationError.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.tls; + +import com.isode.stroke.base.Error; + +public class CertificateVerificationError implements Error { + + public enum Type { + + UnknownError, + Expired, + NotYetValid, + SelfSigned, + Rejected, + Untrusted, + InvalidPurpose, + PathLengthExceeded, + InvalidSignature, + InvalidCA, + InvalidServerIdentity, + }; + + public CertificateVerificationError(Type type) { + if (type == null) { + throw new IllegalStateException(); + } + this.type = type; + } + public final Type type; +}; + diff --git a/src/com/isode/stroke/tls/PKCS12Certificate.java b/src/com/isode/stroke/tls/PKCS12Certificate.java new file mode 100644 index 0000000..af66545 --- /dev/null +++ b/src/com/isode/stroke/tls/PKCS12Certificate.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.tls; + +import com.isode.stroke.base.ByteArray; + +public class PKCS12Certificate { + + public PKCS12Certificate() { + } + + public PKCS12Certificate(String filename, String password) { + password_ = password; + data_.readFromFile(filename); + } + + public boolean isNull() { + return data_.isEmpty(); + } + + public ByteArray getData() { + return data_; + } + + public void setData(ByteArray data) { + data_ = data; + } + + public String getPassword() { + return password_; + } + private ByteArray data_; + private String password_; +} diff --git a/src/com/isode/stroke/tls/PlatformTLSFactories.java b/src/com/isode/stroke/tls/PlatformTLSFactories.java new file mode 100644 index 0000000..0959d07 --- /dev/null +++ b/src/com/isode/stroke/tls/PlatformTLSFactories.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2011 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.tls; + +public class PlatformTLSFactories { + public TLSContextFactory getTLSContextFactory() { + /*FIXME: Implement*/ + return null; + } + + public CertificateFactory getCertificateFactory() { + /*FIXME: Implement*/ + return null; + } +} diff --git a/src/com/isode/stroke/tls/ServerIdentityVerifier.java b/src/com/isode/stroke/tls/ServerIdentityVerifier.java new file mode 100644 index 0000000..903b296 --- /dev/null +++ b/src/com/isode/stroke/tls/ServerIdentityVerifier.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.tls; + +import com.isode.stroke.idn.IDNA; +import com.isode.stroke.jid.JID; +import java.util.List; + +public class ServerIdentityVerifier { + + public ServerIdentityVerifier(JID jid) { + domain = jid.getDomain(); + encodedDomain = IDNA.getEncoded(domain); + } + + public boolean certificateVerifies(Certificate certificate) { + boolean hasSAN = false; + + // DNS names + List<String> dnsNames = certificate.getDNSNames(); + for (String dnsName : dnsNames) { + if (matchesDomain(dnsName)) { + return true; + } + } + hasSAN |= !dnsNames.isEmpty(); + + // SRV names + List<String> srvNames = certificate.getSRVNames(); + for (String srvName : srvNames) { + // Only match SRV names that begin with the service; this isn't required per + // spec, but we're being purist about this. + if (srvName.startsWith("_xmpp-client.") && matchesDomain(srvName.substring("_xmpp-client.".length()))) { + return true; + } + } + hasSAN |= !srvNames.isEmpty(); + + // XmppAddr + List<String> xmppAddresses = certificate.getXMPPAddresses(); + for (String xmppAddress : xmppAddresses) { + if (matchesAddress(xmppAddress)) { + return true; + } + } + hasSAN |= !xmppAddresses.isEmpty(); + + // CommonNames. Only check this if there was no SAN (according to spec). + if (!hasSAN) { + List<String> commonNames = certificate.getCommonNames(); + for (String commonName : commonNames) { + if (matchesDomain(commonName)) { + return true; + } + } + } + + return false; + } + + boolean matchesDomain(String s) { + if (s.startsWith("*.")) { + String matchString = s.substring(2); + String matchDomain = encodedDomain; + int dotIndex = matchDomain.indexOf('.'); + if (dotIndex >= 0) { + matchDomain = matchDomain.substring(dotIndex + 1); + } + return matchString.equals(matchDomain); + } + else { + return s.equals(encodedDomain); + } + } + + boolean matchesAddress(String s) { + return s.equals(domain); + } + private String domain; + private String encodedDomain; +} diff --git a/src/com/isode/stroke/tls/TLSContext.java b/src/com/isode/stroke/tls/TLSContext.java new file mode 100644 index 0000000..16f2ae3 --- /dev/null +++ b/src/com/isode/stroke/tls/TLSContext.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.tls; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.signals.Signal; +import com.isode.stroke.signals.Signal1; + +public abstract class TLSContext { + //See SSLEngine for real implementation when the time comes + public abstract void connect(); + + public abstract boolean setClientCertificate(PKCS12Certificate cert); + + public abstract void handleDataFromNetwork(ByteArray data); + public abstract void handleDataFromApplication(ByteArray data); + + public abstract Certificate getPeerCertificate(); + public abstract CertificateVerificationError getPeerCertificateVerificationError(); + + public abstract ByteArray getFinishMessage(); + + public Signal1<ByteArray> onDataForNetwork = new Signal1<ByteArray>(); + public Signal1<ByteArray> onDataForApplication = new Signal1<ByteArray>(); + public Signal onError = new Signal(); + public Signal onConnected = new Signal(); +} diff --git a/src/com/isode/stroke/tls/TLSContextFactory.java b/src/com/isode/stroke/tls/TLSContextFactory.java new file mode 100644 index 0000000..1b84a42 --- /dev/null +++ b/src/com/isode/stroke/tls/TLSContextFactory.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ + +package com.isode.stroke.tls; + +public interface TLSContextFactory { + boolean canCreate(); + TLSContext createTLSContext(); +} diff --git a/test/com/isode/stroke/base/ByteArrayTest.java b/test/com/isode/stroke/base/ByteArrayTest.java new file mode 100644 index 0000000..183ae76 --- /dev/null +++ b/test/com/isode/stroke/base/ByteArrayTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.base; + +import java.io.UnsupportedEncodingException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ByteArrayTest { + @Test + public void testASCIIString_fromString() { + String testling = "Wheeblahpling"; + assertEquals(testling, new ByteArray(testling).toString()); + } + + @Test + public void testASCIIString_fromBytesSimple() throws UnsupportedEncodingException { + String testling = "WheeBlahBlahPling"; + byte[] bytes = testling.getBytes("UTF-8"); + assertEquals(testling, new ByteArray(bytes).toString()); + } + + @Test + public void testASCIIString_fromBytes() throws UnsupportedEncodingException { + String target = "ABCZ"; + byte[] bytes = {65, 66, 67, 90}; + assertEquals(target, new ByteArray(bytes).toString()); + } + + @Test + public void testExtendedString_fromString() { + String testling = "Wheeblahpling\u0041\u00DF\u00F7"; + assertEquals(testling, new ByteArray(testling).toString()); + } + + @Test + public void testExtendedString_fromBytes() { + String target = "ABCZ\u0041\u00DF\u00F7"; + byte[] bytes = byteify(new int[]{65, 66, 67, 90, 0x41, 0xc3, 0x9f, 0xc3, 0xb7}); + assertEquals(target, new ByteArray(bytes).toString()); + } + + @Test + public void testExtendedString_fromBytesSegmented() { + String target = "ABCZ\u0041\u00DF\u00F7"; + byte[] bytes = byteify(new int[]{65, 66, 67, 90, 0x41, 0xc3, 0x9f}); + ByteArray testling = new ByteArray(bytes); + testling.append((byte)0xc3); + testling.append((byte)0xb7); + assertEquals(target, testling.toString()); + } + + @Test + public void testExtendedBytes_fromString() { + String string = "ABCZ\u0041\u00DF\u00F7"; + byte[] target = byteify(new int[]{65, 66, 67, 90, 0x41, 0xc3, 0x9f, 0xc3, 0xb7}); + assertArrayEquals(target, new ByteArray(string).getData()); + } + + private byte[] byteify(int[] ints) { + byte[] bytes = new byte[ints.length]; + for (int i = 0; i < ints.length; i++) { + int j = ints[i]; + bytes[i] = (byte)j; + } + return bytes; + } +} diff --git a/test/com/isode/stroke/compress/ZLibCompressorTest.java b/test/com/isode/stroke/compress/ZLibCompressorTest.java new file mode 100644 index 0000000..3f9593b --- /dev/null +++ b/test/com/isode/stroke/compress/ZLibCompressorTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.compress; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.stringcodecs.Hexify; +import javax.xml.bind.annotation.adapters.HexBinaryAdapter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author Kev + */ +public class ZLibCompressorTest { + + @Test + public void testProcess() throws Exception { + ZLibCompressor testling = new ZLibCompressor(); + ByteArray result = testling.process(new ByteArray("foo")); + + assertEquals("78da4acbcf07000000ffff", Hexify.hexify(result)); + } + + @Test + public void testProcess_Twice() throws ZLibException { + ZLibCompressor testling = new ZLibCompressor(); + testling.process(new ByteArray("foo")); + ByteArray result = testling.process(new ByteArray("bar")); + + assertEquals("4a4a2c02000000ffff", Hexify.hexify(result)); + } + + public static ByteArray unhex(String string) { + HexBinaryAdapter adaptor = new HexBinaryAdapter(); + return new ByteArray(adaptor.unmarshal(string)); + } +} diff --git a/test/com/isode/stroke/compress/ZLibDecompressorTest.java b/test/com/isode/stroke/compress/ZLibDecompressorTest.java new file mode 100644 index 0000000..838a324 --- /dev/null +++ b/test/com/isode/stroke/compress/ZLibDecompressorTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2010 Remko Tron¨on + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +package com.isode.stroke.compress; + +import com.isode.stroke.base.ByteArray; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author Kev + */ +public class ZLibDecompressorTest { + + @Test + public void testProcess() throws ZLibException { + ZLibDecompressor testling = new ZLibDecompressor(); + ByteArray result = testling.process(ZLibCompressorTest.unhex("78da4acbcf07000000ffff")); + + assertEquals(new ByteArray("foo"), result); + } + + @Test + public void testProcess_Twice() throws ZLibException { + ZLibDecompressor testling = new ZLibDecompressor(); + testling.process(ZLibCompressorTest.unhex("78da4acbcf07000000ffff")); + ByteArray result = testling.process(ZLibCompressorTest.unhex("4a4a2c02000000ffff")); + + assertEquals(new ByteArray("bar"), result); + } + + @Test(expected = ZLibException.class) + public void testProcess_Invalid() throws ZLibException { + ZLibDecompressor testling = new ZLibDecompressor(); + testling.process(new ByteArray("invalid")); + } + + @Test + public void testProcess_Huge() throws ZLibException { + ByteArray data = new ByteArray(); + for (int i = 0; i < 2048; ++i) { + data.append((byte) i); + } + ByteArray original = new ByteArray(data); + ByteArray compressed = new ZLibCompressor().process(original); + ByteArray decompressed = new ZLibDecompressor().process(compressed); + + assertEquals(original, decompressed); + } + + @Test + public void testProcess_ChunkSize() throws ZLibException { + ByteArray data = new ByteArray(); + for (int i = 0; i < 1024; ++i) { + data.append((byte) i); + } + ByteArray original = new ByteArray(data); + ByteArray compressed = new ZLibCompressor().process(original); + ByteArray decompressed = new ZLibDecompressor().process(compressed); + + assertEquals(original, decompressed); + } +} diff --git a/test/com/isode/stroke/parser/XMLParserTest.java b/test/com/isode/stroke/parser/XMLParserTest.java new file mode 100644 index 0000000..f874eea --- /dev/null +++ b/test/com/isode/stroke/parser/XMLParserTest.java @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2010-2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron¨on. + * All rights reserved. + */ +package com.isode.stroke.parser; + +import com.isode.stroke.eventloop.SimpleEventLoop; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class XMLParserTest { + + @Before + public void setUp() { + client_ = new Client(); + } + + private XMLParser parser() { + return PlatformXMLParserFactory.createXMLParser(client_, new SimpleEventLoop()); + } + + private void join(XMLParser parser) { + try { + ((PullXMLParser) parser).getParserThread().join(300); + } catch (InterruptedException ex) { + Logger.getLogger(XMLParserTest.class.getName()).log(Level.SEVERE, null, ex); + } + } + + @Test + public void testParse_characters() { + XMLParser testling = parser(); + + String data = "ABCZ\u0041\u00DF\u00F7\u0410\u0498"; + assertTrue(testling.parse("<body>" + data + "</body>")); + join(testling); + assertEquals(3, client_.events.size()); + + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("body", client_.events.get(0).data); + + assertEquals(Client.Type.CharacterData, client_.events.get(1).type); + assertEquals(data, client_.events.get(1).data); + + assertEquals(Client.Type.EndElement, client_.events.get(2).type); + assertEquals("body", client_.events.get(2).data); + + } + + @Test + public void testParse_NestedElements() { + XMLParser testling = parser(); + + assertTrue(testling.parse( + "<iq type=\"get\">" + + "<query xmlns='jabber:iq:version'/>" + + "</iq>")); + + join(testling); + + assertEquals(4, client_.events.size()); + + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("iq", client_.events.get(0).data); + assertEquals(1, client_.events.get(0).attributes.size()); + assertEquals("get", client_.events.get(0).attributes.get("type")); + assertEquals("", client_.events.get(0).ns); + + assertEquals(Client.Type.StartElement, client_.events.get(1).type); + assertEquals("query", client_.events.get(1).data); + assertEquals(0, client_.events.get(1).attributes.size()); + assertEquals("jabber:iq:version", client_.events.get(1).ns); + + assertEquals(Client.Type.EndElement, client_.events.get(2).type); + assertEquals("query", client_.events.get(2).data); + assertEquals("jabber:iq:version", client_.events.get(2).ns); + + assertEquals(Client.Type.EndElement, client_.events.get(3).type); + assertEquals("iq", client_.events.get(3).data); + assertEquals("", client_.events.get(3).ns); + } + + @Test + public void testParse_ElementInNamespacedElement() { + XMLParser testling = parser(); + + assertTrue(testling.parse( + "<query xmlns='jabber:iq:version'>" + + "<name>Swift</name>" + + "</query>")); + join(testling); + assertEquals(5, client_.events.size()); + + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("query", client_.events.get(0).data); + assertEquals(0, client_.events.get(0).attributes.size()); + assertEquals("jabber:iq:version", client_.events.get(0).ns); + + assertEquals(Client.Type.StartElement, client_.events.get(1).type); + assertEquals("name", client_.events.get(1).data); + assertEquals("jabber:iq:version", client_.events.get(1).ns); + + assertEquals(Client.Type.CharacterData, client_.events.get(2).type); + assertEquals("Swift", client_.events.get(2).data); + + assertEquals(Client.Type.EndElement, client_.events.get(3).type); + assertEquals("name", client_.events.get(3).data); + assertEquals("jabber:iq:version", client_.events.get(3).ns); + + assertEquals(Client.Type.EndElement, client_.events.get(4).type); + assertEquals("query", client_.events.get(4).data); + assertEquals("jabber:iq:version", client_.events.get(4).ns); + } + + @Test + public void testParse_CharacterData() { + XMLParser testling = parser(); + + assertTrue(testling.parse("<html>bla<i>bli</i>blo</html>")); + join(testling); + assertEquals(7, client_.events.size()); + + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("html", client_.events.get(0).data); + + assertEquals(Client.Type.CharacterData, client_.events.get(1).type); + assertEquals("bla", client_.events.get(1).data); + + assertEquals(Client.Type.StartElement, client_.events.get(2).type); + assertEquals("i", client_.events.get(2).data); + + assertEquals(Client.Type.CharacterData, client_.events.get(3).type); + assertEquals("bli", client_.events.get(3).data); + + assertEquals(Client.Type.EndElement, client_.events.get(4).type); + assertEquals("i", client_.events.get(4).data); + + assertEquals(Client.Type.CharacterData, client_.events.get(5).type); + assertEquals("blo", client_.events.get(5).data); + + assertEquals(Client.Type.EndElement, client_.events.get(6).type); + assertEquals("html", client_.events.get(6).data); + } + + @Test + public void testParse_NamespacePrefix() { + XMLParser testling = parser(); + + assertTrue(testling.parse("<p:x xmlns:p='bla'><p:y/></p:x>")); + join(testling); + assertEquals(4, client_.events.size()); + + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("x", client_.events.get(0).data); + assertEquals("bla", client_.events.get(0).ns); + + assertEquals(Client.Type.StartElement, client_.events.get(1).type); + assertEquals("y", client_.events.get(1).data); + assertEquals("bla", client_.events.get(1).ns); + + assertEquals(Client.Type.EndElement, client_.events.get(2).type); + assertEquals("y", client_.events.get(2).data); + assertEquals("bla", client_.events.get(2).ns); + + assertEquals(Client.Type.EndElement, client_.events.get(3).type); + assertEquals("x", client_.events.get(3).data); + assertEquals("bla", client_.events.get(3).ns); + } + + @Test + public void testParse_UnhandledXML() { + XMLParser testling = parser(); + + assertTrue(testling.parse("<iq><!-- Testing --></iq>")); + join(testling); + assertEquals(2, client_.events.size()); + + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("iq", client_.events.get(0).data); + + assertEquals(Client.Type.EndElement, client_.events.get(1).type); + assertEquals("iq", client_.events.get(1).data); + } + + //@Test /*TODO: uncomment if we ever get a sane incremental parser */ + public void testParse_InvalidXML() { + XMLParser testling = parser(); + + assertTrue(!testling.parse("<iq><bla></iq>")); + } + + //@Test /*TODO: uncomment if we ever get a sane incremental parser */ + public void testParse_InErrorState() { + XMLParser testling = parser(); + + assertTrue(!testling.parse("<iq><bla></iq>")); + assertTrue(!testling.parse("<iq/>")); + } + + @Test + public void testParse_Incremental() { + XMLParser testling = parser(); + + assertTrue(testling.parse("<iq")); + assertTrue(testling.parse("></iq>")); + join(testling); + assertEquals(2, client_.events.size()); + + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("iq", client_.events.get(0).data); + + assertEquals(Client.Type.EndElement, client_.events.get(1).type); + assertEquals("iq", client_.events.get(1).data); + } + + @Test + public void testParse_IncrementalWithCloses() { + XMLParser testling = parser(); + + assertTrue(testling.parse("<iq")); + assertTrue(testling.parse("><></iq>")); + join(testling); + assertEquals(3, client_.events.size()); + + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("iq", client_.events.get(0).data); + + assertEquals(Client.Type.CharacterData, client_.events.get(1).type); + assertEquals("<>", client_.events.get(1).data); + + assertEquals(Client.Type.EndElement, client_.events.get(2).type); + assertEquals("iq", client_.events.get(2).data); + } + + @Test + public void testParse_WhitespaceInAttribute() { + XMLParser testling = parser(); + + assertTrue(testling.parse( + "<query xmlns='http://www.xmpp.org/extensions/xep-0084.html#ns-data '>")); + assertTrue(testling.parse( + "<presence/>")); + join(testling); + assertEquals(3, client_.events.size()); + assertEquals(Client.Type.StartElement, client_.events.get(0).type); + assertEquals("query", client_.events.get(0).data); + assertEquals(Client.Type.StartElement, client_.events.get(1).type); + assertEquals("presence", client_.events.get(1).data); + assertEquals(Client.Type.EndElement, client_.events.get(2).type); + assertEquals("presence", client_.events.get(2).data); + } + + private static class Client implements XMLParserClient { + + public enum Type { + + StartElement, EndElement, CharacterData + }; + + private class Event { + + Event( + Type type, + String data, + String ns, + AttributeMap attributes) { + this.type = type; + this.data = data; + this.ns = ns; + this.attributes = attributes; + } + + Event(Type type, String data, String ns) { + this.type = type; + this.data = data; + this.ns = ns; + } + + Event(Type type, String data) { + this(type, data, ""); + } + Type type; + String data; + String ns; + AttributeMap attributes; + }; + + Client() { + } + + public void handleStartElement(String element, String ns, AttributeMap attributes) { + events.add(new Event(Type.StartElement, element, ns, attributes)); + } + + public void handleEndElement(String element, String ns) { + events.add(new Event(Type.EndElement, element, ns)); + } + + public void handleCharacterData(String data) { + events.add(new Event(Type.CharacterData, data)); + } + List<Event> events = new ArrayList<Event>(); + }; + private Client client_; +} diff --git a/test/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticatorTest.java b/test/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticatorTest.java new file mode 100644 index 0000000..44a179e --- /dev/null +++ b/test/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticatorTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2011, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ +package com.isode.stroke.sasl; + +import com.isode.stroke.base.ByteArray; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author Kev + */ +public class SCRAMSHA1ClientAuthenticatorTest { + + @Test + public void testGetInitialResponse() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefghABCDEFGH"); + testling.setCredentials("user", "pass", ""); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("n,,n=user,r=abcdefghABCDEFGH"), response); + } + + @Test + public void testGetInitialResponse_UsernameHasSpecialChars() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefghABCDEFGH"); + testling.setCredentials(",us=,er=", "pass", ""); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("n,,n==2Cus=3D=2Cer=3D,r=abcdefghABCDEFGH"), response); + } + + @Test + public void testGetInitialResponse_WithAuthorizationID() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefghABCDEFGH"); + testling.setCredentials("user", "pass", "auth"); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("n,a=auth,n=user,r=abcdefghABCDEFGH"), response); + } + + @Test + public void testGetInitialResponse_WithAuthorizationIDWithSpecialChars() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefghABCDEFGH"); + testling.setCredentials("user", "pass", "a=u,th"); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("n,a=a=3Du=2Cth,n=user,r=abcdefghABCDEFGH"), response); + } + + @Test + public void testGetInitialResponse_WithoutChannelBindingWithTLSChannelBindingData() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefghABCDEFGH", false); + testling.setTLSChannelBindingData(new ByteArray("xyza")); + testling.setCredentials("user", "pass", ""); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("y,,n=user,r=abcdefghABCDEFGH"), response); + } + + @Test + public void testGetInitialResponse_WithChannelBindingWithTLSChannelBindingData() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefghABCDEFGH", true); + testling.setTLSChannelBindingData(new ByteArray("xyza")); + testling.setCredentials("user", "pass", ""); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("p=tls-unique,,n=user,r=abcdefghABCDEFGH"), response); + } + + @Test + public void testGetFinalResponse() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + assertTrue(testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=4096"))); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("c=biws,r=abcdefghABCDEFGH,p=CZbjGDpIteIJwQNBgO0P8pKkMGY="), response); + } + + @Test + public void testGetFinalResponse_WithoutChannelBindingWithTLSChannelBindingData() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh", false); + testling.setCredentials("user", "pass", ""); + testling.setTLSChannelBindingData(new ByteArray("xyza")); + testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=4096")); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("c=eSws,r=abcdefghABCDEFGH,p=JNpsiFEcxZvNZ1+FFBBqrYvYxMk="), response); + } + + @Test + public void testGetFinalResponse_WithChannelBindingWithTLSChannelBindingData() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh", true); + testling.setCredentials("user", "pass", ""); + testling.setTLSChannelBindingData(new ByteArray("xyza")); + testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=4096")); + + ByteArray response = testling.getResponse(); + + assertEquals(new ByteArray("c=cD10bHMtdW5pcXVlLCx4eXph,r=abcdefghABCDEFGH,p=i6Rghite81P1ype8XxaVAa5l7v0="), response); + } + + @Test + public void testSetFinalChallenge() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=4096")); + + boolean result = testling.setChallenge(new ByteArray("v=Dd+Q20knZs9jeeK0pi1Mx1Se+yo=")); + + assertTrue(result); + } + + @Test + public void testSetChallenge() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + + boolean result = testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=4096")); + + assertTrue(result); + } + + @Test + public void testSetChallenge_InvalidClientNonce() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + + boolean result = testling.setChallenge(new ByteArray("r=abcdefgiABCDEFGH,s=MTIzNDU2NzgK,i=4096")); + + assertTrue(!result); + } + + @Test + public void testSetChallenge_OnlyClientNonce() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + + boolean result = testling.setChallenge(new ByteArray("r=abcdefgh,s=MTIzNDU2NzgK,i=4096")); + + assertTrue(!result); + } + + @Test + public void testSetChallenge_InvalidIterations() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + + boolean result = testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=bla")); + + assertTrue(!result); + } + + @Test + public void testSetChallenge_MissingIterations() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + + boolean result = testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK")); + + assertTrue(!result); + } + + @Test + public void testSetChallenge_ZeroIterations() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + + boolean result = testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=0")); + + assertTrue(!result); + } + + @Test + public void testSetChallenge_NegativeIterations() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + + boolean result = testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=-1")); + + assertTrue(!result); + } + + @Test + public void testSetFinalChallenge_InvalidChallenge() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=4096")); + boolean result = testling.setChallenge(new ByteArray("v=e26kI69ICb6zosapLLxrER/631A=")); + + assertTrue(!result); + } + + @Test + public void testGetResponseAfterFinalChallenge() { + SCRAMSHA1ClientAuthenticator testling = new SCRAMSHA1ClientAuthenticator("abcdefgh"); + testling.setCredentials("user", "pass", ""); + testling.setChallenge(new ByteArray("r=abcdefghABCDEFGH,s=MTIzNDU2NzgK,i=4096")); + testling.setChallenge(new ByteArray("v=Dd+Q20knZs9jeeK0pi1Mx1Se+yo=")); + + assertTrue(testling.getResponse() == null); + } +} diff --git a/test/com/isode/stroke/stringcodecs/HMACSHA1Test.java b/test/com/isode/stroke/stringcodecs/HMACSHA1Test.java new file mode 100644 index 0000000..dedcd7c --- /dev/null +++ b/test/com/isode/stroke/stringcodecs/HMACSHA1Test.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ + +package com.isode.stroke.stringcodecs; + +import com.isode.stroke.base.ByteArray; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +public class HMACSHA1Test { + + private ByteArray cast(int[] source) { + byte[] result = new byte[source.length]; + for (int i = 0; i < source.length; i++) { + result[i] = (byte)source[i]; + } + return new ByteArray(result); + } + + @Test + public void testGetResult() { + ByteArray result = HMACSHA1.getResult(new ByteArray("foo"), new ByteArray("foobar")); + assertEquals(cast(new int[]{0xa4, 0xee, 0xba, 0x8e, 0x63, 0x3d, 0x77, 0x88, 0x69, 0xf5, 0x68, 0xd0, 0x5a, 0x1b, 0x3d, 0xc7, 0x2b, 0xfd, 0x4, 0xdd}), result); + } +} diff --git a/test/com/isode/stroke/stringcodecs/SHA1Test.java b/test/com/isode/stroke/stringcodecs/SHA1Test.java new file mode 100644 index 0000000..c8c8a9b --- /dev/null +++ b/test/com/isode/stroke/stringcodecs/SHA1Test.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010, Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010, Remko Tron?on. + * All rights reserved. + */ +package com.isode.stroke.stringcodecs; + +import com.isode.stroke.base.ByteArray; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +public class SHA1Test { + + public SHA1Test() { + } + + @Test + public void testGetHash() { + ByteArray result = new ByteArray(SHA1.getHash(new ByteArray("client/pc//Exodus 0.9.1<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<"))); + ByteArray target = new ByteArray(new byte[]{(byte) 0x42, (byte) 0x06, (byte) 0xb2, (byte) 0x3c, (byte) 0xa6, (byte) 0xb0, (byte) 0xa6, (byte) 0x43, (byte) 0xd2, (byte) 0x0d, (byte) 0x89, (byte) 0xb0, (byte) 0x4f, (byte) 0xf5, (byte) 0x8c, (byte) 0xf7, (byte) 0x8b, (byte) 0x80, (byte) 0x96, (byte) 0xed}); + assertEquals(target, result); + } + + @Test + public void testGetHash_Twice() { + ByteArray input = new ByteArray("client/pc//Exodus 0.9.1<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<"); + SHA1.getHash(input); + ByteArray result = SHA1.getHash(input); + + assertEquals(new ByteArray(new byte[]{(byte) 0x42, (byte) 0x06, (byte) 0xb2, (byte) 0x3c, (byte) 0xa6, (byte) 0xb0, (byte) 0xa6, (byte) 0x43, (byte) 0xd2, (byte) 0x0d, (byte) 0x89, (byte) 0xb0, (byte) 0x4f, (byte) 0xf5, (byte) 0x8c, (byte) 0xf7, (byte) 0x8b, (byte) 0x80, (byte) 0x96, (byte) 0xed}), result); + } + + @Test + public void testGetHash_NoData() { + ByteArray result = SHA1.getHash(new ByteArray()); + + assertEquals(new ByteArray(new byte[]{(byte) 0xda, (byte) 0x39, (byte) 0xa3, (byte) 0xee, (byte) 0x5e, (byte) 0x6b, (byte) 0x4b, (byte) 0x0d, (byte) 0x32, (byte) 0x55, (byte) 0xbf, (byte) 0xef, (byte) 0x95, (byte) 0x60, (byte) 0x18, (byte) 0x90, (byte) 0xaf, (byte) 0xd8, (byte) 0x07, (byte) 0x09}), result); + } +} |